Elasticsearch 建设在 Apache Lucene 之上,于 2010 年由 Elasticsearch NV(现为 Elastic)首次公布。据 Elastic 网站称,它是一个 分布式开源搜寻和剖析引擎,实用于所有类型的数据,包含文本、数值、天文空间、结构化和非结构化。Elasticsearch 操作通过 REST API 实现。次要性能是:
- 将文档存储在索引中,
- 应用弱小的查问搜寻索引以获取这些文档,以及
- 对数据运行剖析函数。
Spring Data Elasticsearch 提供了一个简略的接口来在 Elasticsearch 上执行这些操作,作为间接应用 REST API 的代替办法 。
在这里,咱们将应用 Spring Data Elasticsearch 来演示 Elasticsearch 的索引和搜寻性能,并在最初构建一个简略的搜寻应用程序,用于在产品库存中搜寻产品。
代码示例
本文附有 GitHub 上的工作代码示例。
Elasticsearch 概念
Elasticsearch 概念
理解 Elasticsearch 概念的最简略办法是用数据库进行类比,如下表所示:
Elasticsearch | -> | 数据库 |
---|---|---|
索引 | -> | 表 |
文档 | -> | 行 |
文档 | -> | 列 |
咱们要搜寻或剖析的任何数据都作为文档存储在索引中。在 Spring Data 中,咱们以 POJO 的模式示意一个文档,并用注解对其进行润饰以定义到 Elasticsearch 文档的映射。
与数据库不同,存储在 Elasticsearch 中的文本首先由各种分析器解决。默认分析器通过罕用单词分隔符(如空格和标点符号)拆分文本,并删除罕用英语单词。
如果咱们存储文本“The sky is blue”,分析器会将其存储为蕴含“术语”“sky”和“blue”的文档。咱们将可能应用“blue sky”、“sky”或“blue”模式的文本搜寻此文档,并将匹配水平作为分数。
除了文本之外,Elasticsearch 还能够存储其余类型的数据,称为 Field Type(字段类型)
,如文档中 mapping-types(映射类型)局部所述。
启动 Elasticsearch 实例
在进一步探讨之前,让咱们启动一个 Elasticsearch 实例,咱们将应用它来运行咱们的示例。有多种运行 Elasticsearch 实例的办法:
- 应用托管服务
- 应用来自 AWS 或 Azure 等云提供商的托管服务
- 通过在虚拟机集群中本人装置 Elasticsearch
- 运行 Docker 镜像
咱们将应用来自 Dockerhub 的 Docker 镜像,这对于咱们的演示应用程序来说曾经足够了。让咱们通过运行 Docker run 命令来启动 Elasticsearch 实例:
docker run -p 9200:9200 \
-e "discovery.type=single-node" \
docker.elastic.co/elasticsearch/elasticsearch:7.10.0
执行此命令将启动一个 Elasticsearch 实例,侦听端口 9200。咱们能够通过点击 URL http://localhost:9200 来验证实例状态,并在浏览器中查看后果输入:
{
"name" : "8c06d897d156",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "Jkx..VyQ",
"version" : {
"number" : "7.10.0",
...
},
"tagline" : "You Know, for Search"
}
如果咱们的 Elasticsearch 实例启动胜利,应该看到下面的输入。
应用 REST API 进行索引和搜寻
Elasticsearch 操作通过 REST API 拜访。有两种办法能够将文档增加到索引中:
- 一次增加一个文档,或者
- 批量增加文档。
增加单个文档的 API 承受一个文档作为参数。
对 Elasticsearch 实例的简略 PUT 申请用于存储文档如下所示:
PUT /messages/_doc/1
{"message": "The Sky is blue today"}
这会将音讯 –“The Sky is blue today”存储为“messages”的索引中的文档。
咱们能够应用发送到搜寻 REST API 的搜寻查问来获取此文档:
GET /messages/search
{
"query":
{"match": {"message": "blue sky"}
}
}
这里咱们发送一个 match
类型的查问来获取匹配字符串“blue sky”的文档。咱们能够通过多种形式指定用于搜寻文档的查问。Elasticsearch 提供了一个基于 JSON 的 查问 DSL(Domain Specific Language – 畛域特定语言)来定义查问。
对于批量增加,咱们须要提供一个蕴含相似以下代码段的条目标 JSON 文档:
POST /_bulk
{"index":{"_index":"productindex"}}{"_class":"..Product","name":"Corgi Toys .. Car",..."manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"..Product","name":"CLASSIC TOY .. BATTERY"...,"manufacturer":"ccf"}
应用 Spring Data 进行 Elasticsearch 操作
咱们有两种应用 Spring Data 拜访 Elasticsearch 的办法,如下所示:
- Repositories:咱们在接口中定义方法,Elasticsearch 查问是在运行时依据办法名称生成的。
- ElasticsearchRestTemplate:咱们应用办法链和原生查问创立查问,以便在绝对简单的场景中更好地管制创立 Elasticsearch 查问。
咱们将在以下各节中更具体地钻研这两种形式。
创立应用程序并增加依赖项
让咱们首先通过蕴含 web、thymeleaf 和 lombok 的依赖项,应用 Spring Initializr 创立咱们的应用程序。增加 thymeleaf
依赖项以便减少用户界面。
在 Maven pom.xml
中增加 spring-data-elasticsearch
依赖项:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>
连贯到 Elasticsearch 实例
Spring Data Elasticsearch 应用 Java High Level REST Client (JHLC)”) 连贯到 Elasticsearch 服务器。JHLC 是 Elasticsearch 的默认客户端。咱们将创立一个 Spring Bean 配置来进行设置:
@Configuration
@EnableElasticsearch
Repositories(basePackages
= "io.pratik.elasticsearch.repositories")@ComponentScan(basePackages = { "io.pratik.elasticsearch"})
public class ElasticsearchClientConfig extends
AbstractElasticsearchConfiguration {
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration =
ClientConfiguration
.builder()
.connectedTo("localhost:9200")
.build();
return RestClients.create(clientConfiguration).rest();}
}
在这里,咱们连贯到咱们之前启动的 Elasticsearch 实例。咱们能够通过增加更多属性(例如启用 ssl、设置超时等)来进一步自定义连贯。
为了调试和诊断,咱们将在 logback-spring.xml
的日志配置中关上传输级别的申请 / 响应日志:
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, name = "name")
private String name;
@Field(type = FieldType.Double, name = "price")
private Double price;
@Field(type = FieldType.Integer, name = "quantity")
private Integer quantity;
@Field(type = FieldType.Keyword, name = "category")
private String category;
@Field(type = FieldType.Text, name = "desc")
private String description;
@Field(type = FieldType.Keyword, name = "manufacturer")
private String manufacturer;
...
}
表白文档
在咱们的示例中,咱们将按名称、品牌、价格或形容搜寻产品。因而,为了将产品作为文档存储在 Elasticsearch 中,咱们将产品示意为 POJO,并加上 Field 注解以配置 Elasticsearch 的映射,如下所示:
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, name = "name")
private String name;
@Field(type = FieldType.Double, name = "price")
private Double price;
@Field(type = FieldType.Integer, name = "quantity")
private Integer quantity;
@Field(type = FieldType.Keyword, name = "category")
private String category;
@Field(type = FieldType.Text, name = "desc")
private String description;
@Field(type = FieldType.Keyword, name = "manufacturer")
private String manufacturer;
...
}
@Document
注解指定索引名称。
@Id
注解使注解字段成为文档的 _id
,作为此索引中的惟一标识符。id
字段有 512 个字符的限度。
@Field
注解配置字段的类型。咱们还能够将名称设置为不同的字段名称。
在 Elasticsearch 中基于这些注解创立了名为 productindex 的索引。
应用 Spring Data Repository 进行索引和搜寻
存储库提供了应用 finder 办法拜访 Spring Data 中数据的最不便的办法。Elasticsearch 查问是依据办法名称创立的。然而,咱们必须小心防止产生低效的查问并给集群带来高负载。
让咱们通过扩大 ElasticsearchRepository 接口来创立一个 Spring Data 存储库接口:
public interface ProductRepository
extends ElasticsearchRepository<Product, String> {}
此处 ProductRepository
类继承了 ElasticsearchRepository
接口中蕴含的 save()
、saveAll()
、find()
和 findAll()
等办法。
索引
咱们当初将通过调用 save()
办法存储一个产品,调用 saveAll()
办法来批量索引,从而在索引中存储一些产品。在此之前,咱们将存储库接口放在一个服务类中:
@Service
public class ProductSearchServiceWithRepo {
private ProductRepository productRepository;
public void createProductIndexBulk(final List<Product> products) {productRepository.saveAll(products);
}
public void createProductIndex(final Product product) {productRepository.save(product);
}
}
当咱们从 JUnit 调用这些办法时,咱们能够在跟踪日志中看到 REST API 调用索引和批量索引。
搜寻
为了满足咱们的搜寻要求,咱们将向存储库接口增加 finder
办法:
public interface ProductRepository
extends ElasticsearchRepository<Product, String> {List<Product> findByName(String name);
List<Product> findByNameContaining(String name);
List<Product> findByManufacturerAndCategory
(String manufacturer, String category);
}
在应用 JUnit 运行 findByName()
办法时,咱们能够看到在发送到服务器之前在跟踪日志中生成的 Elasticsearch 查问:
TRACE Sending request POST /productindex/_search? ..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"apple","fields":["name^1.0"],..}
相似地,通过运行findByManufacturerAndCategory()
办法,咱们能够看到应用两个 query_string
参数对应两个字段——“manufacturer”和“category”生成的查问:
TRACE .. Sending request POST /productindex/_search..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"samsung","fields":["manufacturer^1.0"],..}},{"query_string":{"query":"laptop","fields":["category^1.0"],..}}],..}},"version":true}
有多种办法命名模式能够生成各种 Elasticsearch 查问。
应用 ElasticsearchRestTemplate 进行索引和搜寻
当咱们须要更多地管制咱们设计查问的形式,或者团队曾经把握了 Elasticsearch 语法时,Spring Data 存储库可能就不再适宜。
在这种状况下,咱们应用 ElasticsearchRestTemplate。它是 Elasticsearch 基于 HTTP 的新客户端,取代以前应用节点到节点二进制协定的 TransportClient。
ElasticsearchRestTemplate
实现了接口 ElasticsearchOperations
,该接口负责底层搜寻和集群操的繁冗工作。
索引
该接口具备用于增加单个文档的办法 index()
和用于向索引增加多个文档的 bulkIndex()
办法。此处的代码片段显示了如何应用 bulkIndex()
将多个产品增加到索引“productindex
”:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public List<String> createProductIndexBulk
(final List<Product> products) {List<IndexQuery> queries = products.stream()
.map(product->
new IndexQueryBuilder()
.withId(product.getId().toString())
.withObject(product).build())
.collect(Collectors.toList());;
return elasticsearchOperations
.bulkIndex(queries,IndexCoordinates.of(PRODUCT_INDEX));
}
...
}
要存储的文档蕴含在 IndexQuery
对象中。bulkIndex()
办法将 IndexQuery
对象列表和蕴含在 IndexCoordinates
中的 Index
名称作为输出。当咱们执行此办法时,咱们会取得批量申请的 REST API 跟踪:
Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex","_id":"383..35"}}{"_class":"..Product","id":"383..35","name":"New Apple..phone",..manufacturer":"apple"}
..
{"_class":"..Product","id":"d7a..34",.."manufacturer":"samsung"}
接下来,咱们应用 index()
办法增加单个文档:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public String createProductIndex(Product product) {IndexQuery indexQuery = new IndexQueryBuilder()
.withId(product.getId().toString())
.withObject(product).build();
String documentId = elasticsearchOperations
.index(indexQuery, IndexCoordinates.of(PRODUCT_INDEX));
return documentId;
}
}
跟踪相应地显示了用于增加单个文档的 REST API PUT
申请。
Sending request PUT /productindex/_doc/59d..987..:
Request body: {"_class":"..Product","id":"59d..87",..,"manufacturer":"dell"}
搜寻
ElasticsearchRestTemplate
还具备 search()
办法,用于在索引中搜寻文档。此搜寻操作相似于 Elasticsearch 查问,是通过结构 Query
对象并将其传递给搜寻办法来构建的。
Query
对象具备三种变体 – NativeQueryy
、StringQuery
和 CriteriaQuery
,具体取决于咱们如何结构查问。让咱们构建一些用于搜寻产品的查问。
NativeQuery
NativeQuery
为应用示意 Elasticsearch 结构(如聚合、过滤和排序)的对象构建查问提供了最大的灵活性。这是用于搜寻与特定制造商匹配的产品的 NativeQuery
:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public void findProductsByBrand(final String brandName) {
QueryBuilder queryBuilder =
QueryBuilders
.matchQuery("manufacturer", brandName);
Query searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.build();
SearchHits<Product> productHits =
elasticsearchOperations
.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX));
}
}
在这里,咱们应用 NativeSearchQueryBuilder
构建查问,该查问应用 MatchQueryBuilder
指定蕴含字段“制造商”的匹配查问。
StringQuery
StringQuery 通过容许将原生 Elasticsearch 查问用作 JSON 字符串来提供齐全管制,如下所示:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public void findByProductName(final String productName) {
Query searchQuery = new StringQuery("{\"match\":{\"name\":{\"query\":\""+ productName + "\"}}}\"");
SearchHits<Product> products = elasticsearchOperations.search(
searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX_NAME));
...
}
}
在此代码片段中,咱们指定了一个简略的 match
查问,用于获取具备作为办法参数发送的特定名称的产品。
CriteriaQuery
应用 CriteriaQuery
,咱们能够在不理解 Elasticsearch 任何术语的状况下构建查问。查问是应用带有 Criteria
对象的办法链构建的。每个对象指定一些用于搜寻文档的规范:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public void findByProductPrice(final String productPrice) {Criteria criteria = new Criteria("price")
.greaterThan(10.0)
.lessThan(100.0);
Query searchQuery = new CriteriaQuery(criteria);
SearchHits<Product> products = elasticsearchOperations
.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX_NAME));
}
}
在此代码片段中,咱们应用 CriteriaQuery
造成查问以获取价格大于 10.0
且小于 100.0
的产品。
构建搜寻应用程序
咱们当初将向咱们的应用程序增加一个用户界面,以查看产品搜寻的实际效果。用户界面将有一个搜寻输入框,用于按名称或形容搜寻产品。输入框将具备主动实现性能,以显示基于可用产品的倡议列表,如下所示:
咱们将为用户的搜寻输出创立主动实现倡议。而后依据与用户输出的搜寻文本亲密匹配的名称或形容搜寻产品。咱们将构建两个搜寻服务来实现这个用例:
- 获取主动实现性能的搜寻倡议
- 依据用户的搜寻查询处理搜寻产品的搜寻
服务类 ProductSearchService 将蕴含搜寻和获取倡议的办法。
GitHub 存储库中提供了带有用户界面的成熟应用程序。
建设产品搜寻索引
productindex
与咱们之前用于运行 JUnit 测试的索引雷同。咱们将首先应用 Elasticsearch REST API 删除 productindex
,以便在应用程序启动期间应用从咱们的 50 个时尚系列产品的示例数据集中加载的产品创立新的 productindex
:
curl -X DELETE http://localhost:9200/productindex
如果删除操作胜利,咱们将收到音讯 {"acknowledged": true}
。
当初,让咱们为库存中的产品创立一个索引。咱们将应用蕴含 50 种产品的示例数据集来构建咱们的索引。这些产品在 CSV 文件中被排列为独自的行。
每行都有三个属性 – id、name 和 description。咱们心愿在应用程序启动期间创立索引。请留神,在理论生产环境中,索引创立应该是一个独自的过程。咱们将读取 CSV 的每一行并将其增加到产品索引中:
@SpringBootApplication
@Slf4j
public class ProductsearchappApplication {
...
@PostConstruct
public void buildIndex() {esOps.indexOps(Product.class).refresh();
productRepo.saveAll(prepareDataset());
}
private Collection<Product> prepareDataset() {Resource resource = new ClassPathResource("fashion-products.csv");
...
return productList;
}
}
在这个片段中,咱们通过从数据集中读取行并将这些行传递给存储库的 saveAll()
办法以将产品增加到索引中来进行一些预处理。在运行应用程序时,咱们能够在应用程序启动中看到以下跟踪日志。
...Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"Hornby 2014 Catalogue","description":"Product Desc..talogue","manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"FunkyBuys..","description":"Size Name:Lar..& Smoke","manufacturer":"FunkyBuys"}{"index":{"_index":"productindex"}}.
...
应用多字段和含糊搜寻搜寻产品
上面是咱们在办法 processSearch()
中提交搜寻申请时如何解决搜寻申请:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;
public List<Product> processSearch(final String query) {log.info("Search with query {}", query);
// 1. Create query on multiple fields enabling fuzzy search
QueryBuilder queryBuilder =
QueryBuilders
.multiMatchQuery(query, "name", "description")
.fuzziness(Fuzziness.AUTO);
Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.build();
// 2. Execute search
SearchHits<Product> productHits =
elasticsearchOperations
.search(searchQuery, Product.class,
IndexCoordinates.of(PRODUCT_INDEX));
// 3. Map searchHits to product list
List<Product> productMatches = new ArrayList<Product>();
productHits.forEach(searchHit->{productMatches.add(searchHit.getContent());
});
return productMatches;
}...
}
在这里,咱们对多个字段执行搜寻 – 名称和形容。咱们还附加了 fuzziness()
来搜寻严密匹配的文本以解释拼写错误。
应用通配符搜寻获取倡议
接下来,咱们为搜寻文本框构建主动实现性能。当咱们在搜寻文本字段中输出内容时,咱们将通过应用搜寻框中输出的字符执行通配符搜寻来获取倡议。
咱们在 fetchSuggestions()
办法中构建此函数,如下所示:
@Service
@Slf4j
public class ProductSearchService {
private static final String PRODUCT_INDEX = "productindex";
public List<String> fetchSuggestions(String query) {
QueryBuilder queryBuilder = QueryBuilders
.wildcardQuery("name", query+"*");
Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.withPageable(PageRequest.of(0, 5))
.build();
SearchHits<Product> searchSuggestions =
elasticsearchOperations.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX));
List<String> suggestions = new ArrayList<String>();
searchSuggestions.getSearchHits().forEach(searchHit->{suggestions.add(searchHit.getContent().getName());
});
return suggestions;
}
}
咱们以搜寻输出文本的模式应用通配符查问,并附加 * 以便如果咱们输出“red”,咱们将取得以“red”结尾的倡议。咱们应用 withPageable() 办法将倡议的数量限度为 5。能够在此处看到正在运行的应用程序的搜寻后果的一些屏幕截图:
论断
在本文中,咱们介绍了 Elasticsearch 的次要操作——索引文档、批量索引和搜寻——它们以 REST API 的模式提供。Query DSL 与不同分析器的联合使搜寻变得十分弱小。
Spring Data Elasticsearch 通过应用 Spring Data Repositories 或 ElasticsearchRestTemplate
提供了不便的接口来拜访应用程序中的这些操作。
咱们最终构建了一个应用程序,在其中咱们看到了如何在靠近现实生活的应用程序中应用 Elasticsearch 的批量索引和搜寻性能。
- 本文译自:Using Elasticsearch with Spring Boot – Reflectoring
- 无关 ELK 套件请参考:ELK 教程 – 发现、剖析和可视化你的数据