您可能多次提到Elasticsearch 一个非常强大的搜索和分析引擎,能够处理各种数据类型,例如文本、数字、地理空间、结构化和非结构化数据。在最近的一次自主学习过程中,我利用 Spring Boot 3 和 Spring Data Elasticsearch 5 的功能集成了 Elasticsearch 8。受这次经历的启发,我决定写这篇文章来分享我的见解和发现。
在本文中,我们将展示一个基本的 Web 应用程序,通过使用 Spring Data Elasticsearch 展示 Elasticsearch 的索引和搜索功能。在本文末尾,您将找到 GitHub 存储库的链接,其中包含可供参考的功能代码。
让我们开始吧!💪
Elasticsearch 中的数据存储
文件
Elasticsearch 中的数据存储为文档。文档是一个 JSON 对象,表示单个信息单元,例如产品、客户或任何其他实体。
指数
文档在索引中分组在一起。索引就像存储相似文档的逻辑容器或数据库。例如,您可能有一个产品索引,另一个客户索引,等等。
领域
文档包含字段,这些字段表示所表示的实体的特定属性或属性。每个字段都有一个名称和一个保存数据的相应值。
数据存储:Elasticsearch 与 SQL — 对比方法
有关 Elasticsearch 中数据存储的更多详细信息,您可以参考此链接:文档索引
设置 Elasticsearch
首先,让我们设置并启动 Elasticsearch。有多种方法可以完成此任务,您可以在这里参考 Elasticsearch 设置的官方文档:Set up Elasticsearch
为了本文简单起见,我们将使用 Docker 安装 Elasticsearch。按照以下命令使用 Docker 启动单节点集群。
docker run -p 9200:9200 \ -e "discovery.type=single-node" \ -e "xpack.security.enabled=false" \ docker.elastic.co/elasticsearch/elasticsearch:8.8.1
Elasticsearch 8默认启用 SSL/TLS,我使用环境变量“xpack.security.enabled=false”禁用了安全性。如果安全性保持启用状态,则配置 Elasticsearch 客户端将需要设置正确的 SSL 连接。我会把这个任务作为作业分配给你,只是为了让事情变得有趣!😆
点击此 URL http://localhost:9200/结果应如下所示。
{ "name": "992e6b8bf7a5", "cluster_name": "docker-cluster", "cluster_uuid": "RxXotwWrTd2lzQBJmQ5gqA", "version": { "number": "8.8.1", "build_flavor": "default", "build_type": "docker", "build_hash": "f8edfccba429b6477927a7c1ce1bc6729521305e", "build_date": "2023-06-05T21:32:25.188464208Z", "build_snapshot": false, "lucene_version": "9.6.0", "minimum_wire_compatibility_version": "7.17.0", "minimum_index_compatibility_version": "7.0.0" }, "tagline": "You Know, for Search" }
应用演示
我们的应用程序有 3 个 API。
- 按名称搜索项目
- 按类别搜索项目
- 按价格范围搜索商品
使用下面POM文件中提供的依赖项将我们的应用程序创建为 Spring Boot 项目。我将其命名为elasticsearch。
Elasticsearch 客户端配置
Spring Data Elasticsearch 在连接到单个 Elasticsearch 节点或集群的 Elasticsearch 客户端(由 Elasticsearch 客户端库提供)上运行。在本文中,我们将使用命令式(非反应式)客户端建立与 Elasticsearch 的连接。
@Configuration @EnableElasticsearchRepositories(basePackages = "github.io.truongbn.elasticsearch.repository") public class ClientConfig extends ElasticsearchConfiguration { @Override public ClientConfiguration clientConfiguration() { return ClientConfiguration.builder() .connectedTo("localhost:9200").build(); } }
可以配置和连接几种其他类型的客户端。更多详细信息,您可以参考以下链接:Elasticsearch 客户端、客户端配置
对象映射
Spring Data Elasticsearch 对象映射是将 Java 对象映射到存储在 Elasticsearch 中的 JSON 表示并映射回来的过程。
在我们的应用程序中,我们将处理具有名称、价格、品牌和类别等属性的项目。为了将这些项目存储为 Elasticsearch 中的文档,我们将使用 POJO(普通旧 Java 对象)来表示它们,如下所示。
@Data @Document(indexName = "itemindex") public class Item { @Id private int id; @Field(type = FieldType.Text, name = "name") private String name; @Field(type = FieldType.Double, name = "price") private Double price; @Field(type = FieldType.Keyword, name = "brand") private String brand; @Field(type = FieldType.Keyword, name = "category") private String category; }
- @Document :指示此类是存储在名为“ itemindex ”的索引中的候选类。
- @Id:带注释的字段确保文档在索引中是唯一的。
- @Field:定义字段的属性(名称、类型、格式等)
有关对象映射的更多详细信息,可以参考此链接:Elasticsearch 对象映射
数据处理
Spring Data Elasticsearch 提供了两种访问和操作数据的方法:Elasticsearch Repositories和Elasticsearch Operations。在我们的应用程序中,我们将重点利用 Spring Data Repository 方法。然而,出于本文的目的,我们将分别探讨每种方法,以深入了解 Elasticsearch 如何处理它们。
Elasticsearch存储库
通过利用存储库方法,Elasticsearch 查询是根据方法名称构建的。让我们首先通过扩展ElasticsearchRepository.
public interface ItemRepository extends ElasticsearchRepository<Item, Integer> { }
当前的接口存储库继承了各种方法ElasticsearchRepository,包括save()、saveAll()、findAll()等。这些继承的方法可以很容易地在我们的应用程序中使用。
实施 3 个附加方法来满足我们的搜索要求。
public interface ItemRepository extends ElasticsearchRepository<Item, Integer> { List<Item> findByName(String name); List<Item> findByCategory(String category); List<Item> findByPriceBetween(Double low, Double high); }
如前所述,Elasticsearch 查询是根据方法名称生成的。例如,名为“ findByPriceBetween ”的方法将被转换为以下 Elasticsearch JSON 查询。
{ "query": { "bool": { "must": [ { "range": { "price": { "from": ?, "to": ?, "include_lower": true, "include_upper": true } } } ] } } }
有一个 Elasticsearch 方法命名模式列表,您可以在这里找到:查询创建
Elasticsearch 操作
Elasticsearch Operations 提供了一组广泛的操作接口用于与 Elasticsearch 交互,包括 CRUD 操作、索引管理等。
- IndexOperations:定义索引级别的操作,例如创建和删除。
- DocumentOperations:定义特定于实体的操作,用于根据实体的 ID 存储、更新和检索实体。
- SearchOperations:定义实体级操作,包括使用查询搜索多个实体。
- ElasticsearchOperations:组合了 DocumentOperations 和 SearchOperations 接口。
为了进一步了解它们的用法,让我们使用 ElasticsearchOperations 检查以下示例。
@Service @RequiredArgsConstructor public class ItemService { // Injecting ElasticsearchOperations Bean private final ElasticsearchOperations elasticsearchOperations; /** * Persist the individual item entity in the Elasticsearch cluster */ public int saveIndex(Item item) { Item itemEntity = elasticsearchOperations.save(item); return itemEntity.getId(); } /** * Bulk-save the items in the Elasticsearch cluster */ public List<Integer> saveIndexBulk(List<Item> itemList) { List<Integer> itemIds = new ArrayList<>(); elasticsearchOperations.save(itemList).forEach(item -> itemIds.add(item.getId())); return itemIds; } /** * Remove a single item from the Elasticsearch cluster */ public String findByCategory(Item item) { return elasticsearchOperations.delete(item); } }
有关更详细的信息,请参阅此处的官方 Elasticsearch Operations 文档:Elasticsearch Operations
此外,Elasticsearch Operations 还具有查询接口,它提供了构建和执行各种类型的搜索查询、应用过滤器、排序、分页和聚合的强大功能。它允许您使用 Elasticsearch 的查询 DSL(特定于域的语言)或基于 JSON 的查询来构建复杂的搜索逻辑。
标准查询
CriteriaQuery 允许在不了解 Elasticsearch 查询的语法或基础知识的情况下创建数据搜索查询。用户可以通过链接和组合 Criteria 对象来构建查询,其中每个对象指定搜索文档的特定条件。
让我们看一下下面的例子。
@Service @RequiredArgsConstructor public class ItemService { private final ElasticsearchOperations elasticsearchOperations; public SearchHits<Item> search(String name) { // Get all item with given name Criteria criteria = new Criteria("name").is(name); Query searchQuery = new CriteriaQuery(criteria); return elasticsearchOperations.search(searchQuery, Item.class); } }
更详细的信息请参考这里:CriteriaQuery
字符串查询
此功能允许将 Elasticsearch 查询构建为 JSON 字符串。借助StringQuery,您可以直接使用 Elasticsearch 查询 DSL(域特定语言)语法以字符串形式编写 Elasticsearch 查询。当您需要动态构造复杂查询或现有字符串格式的查询时,它提供了灵活性。
下面是如何使用 StringQuery 的示例。
@Service @RequiredArgsConstructor public class ItemService { private final ElasticsearchOperations elasticsearchOperations; public SearchHits<Item> search(String name) { // Get all item with given name Criteria criteria = new Criteria("name").is(name); Query searchQuery = new CriteriaQuery(criteria); return elasticsearchOperations.search(searchQuery, Item.class); } }
更详细的信息请参考这里:CriteriaQuery
字符串查询
此功能允许将 Elasticsearch 查询构建为 JSON 字符串。借助StringQuery,您可以直接使用 Elasticsearch 查询 DSL(域特定语言)语法以字符串形式编写 Elasticsearch 查询。当您需要动态构造复杂查询或现有字符串格式的查询时,它提供了灵活性。
下面是如何使用 StringQuery 的示例。
@Service @RequiredArgsConstructor public class ItemService { private final ElasticsearchOperations elasticsearchOperations; public SearchHits<Item> search(String name) { // Get all item with given name Criteria criteria = new Criteria("name").is(name); Query searchQuery = new CriteriaQuery(criteria); return elasticsearchOperations.search(searchQuery, Item.class); } }
更详细的信息请参考这里:CriteriaQuery
字符串查询
此功能允许将 Elasticsearch 查询构建为 JSON 字符串。借助StringQuery,您可以直接使用 Elasticsearch 查询 DSL(域特定语言)语法以字符串形式编写 Elasticsearch 查询。当您需要动态构造复杂查询或现有字符串格式的查询时,它提供了灵活性。
下面是如何使用 StringQuery 的示例。
@Service @RequiredArgsConstructor public class ItemService { private final ElasticsearchOperations elasticsearchOperations; public SearchHits<Item> search(String name) { // Get all item with given name Query query = new StringQuery( "{ \"match\": { \"name\": { \"query\": \"" + name + " \" } } } "); return elasticsearchOperations.search(query, Item.class); } }
更详细的信息请参考这里:StringQuery
原生查询
此功能允许您对 Elasticsearch 执行本机查询。这为构造查询提供了最大的灵活性。
以下是如何使用 NativeQuery 的示例。
@Service @RequiredArgsConstructor public class ItemService { private final ElasticsearchOperations elasticsearchOperations; public SearchHits<Item> search(String name) { // Get all item with given name Query query = NativeQuery.builder() .withQuery(q -> q.match(m -> m.field("name").query(name))).build(); return elasticsearchOperations.search(query, Item.class); } }
更详细的信息请参考这里:NativeQuery
服务层
public interface ItemService { List<Item> findByName(String itemName); List<Item> findByCategory(String category); List<Item> findByPriceBetween(double low, double high); }
@Service @RequiredArgsConstructor public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; @Override public List<Item> findByName(String itemName) { return itemRepository.findByName(itemName); } @Override public List<Item> findByCategory(String category) { return itemRepository.findByCategory(category); } @Override public List<Item> findByPriceBetween(double low, double high) { return itemRepository.findByPriceBetween(low, high); } }
数据设置
在与应用程序交互之前,我们可能需要填充 Elasticsearch 集群的一些默认数据记录。
@Configuration @RequiredArgsConstructor public class DataSetup { private final ItemRepository itemRepository; private final CSVParser csvParser; @PostConstruct public void setupData() { // items.csv is inside resources folder List<Item> itemList = csvParser.csvParser("items.csv"); itemRepository.saveAll(itemList); } }
@Service public class CSVParser { public List<Item> csvParser(String filePath) { List<Item> itemList = new ArrayList<>(); try { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filePath); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); CSVReader reader = new CSVReader(inputStreamReader); String[] headers = reader.readNext(); String[] row; while ((row = reader.readNext()) != null) { Item item = new Item(); for (int i = 0; i < headers.length; i++) { String header = headers[i]; String value = row[i]; if ("id".equalsIgnoreCase(header)) { item.setId(Integer.parseInt(value)); } if ("name".equalsIgnoreCase(header)) { item.setName(value); } if ("price".equalsIgnoreCase(header)) { item.setPrice(Double.valueOf(value)); } if ("brand".equalsIgnoreCase(header)) { item.setBrand(value); } if ("category".equalsIgnoreCase(header)) { item.setCategory(value); } } itemList.add(item); } } catch (CsvValidationException | IOException e) { throw new RuntimeException(e); } return itemList; } }
是时候玩玩Elasticserach了
现在,一切准备就绪!😎
要启动应用程序,运行ElasticsearchApplication.main()方法,它应该在端口 8080 上成功运行。
- 点击此 URL 即可访问 Swagger:http://localhost:8080/swagger-ui/index.html#/
- 尝试POST: “ /api/v1/items/{name} ”,使用名称“Smart”进行搜索。您应该会收到一个名称包含“Smart”的项目列表。
请自行使用剩下的 2 个 API 进行测试 😀
文章变得很长哈哈😂那是因为详细的解释。
我们刚刚探索了 Elasticsearch 概念,并简要演示了如何使用 Spring Data Elasticsearch 与 Elasticsearch 8 进行交互