Elasticsearch 的搜索功能_es搜索
Elasticsearch 的搜索功能
建议阅读顺序:
- Elasticsearch 入门
- Elasticsearch 搜索(本文)
- Elasticsearch 搜索高级
- Elasticsearch 高级
1. 介绍
使用 Elasticsearch 最终目的是为了实现搜索功能,现在先将文档添加到索引中,接下来完成搜索的方法。
查询的分类:
- 叶子查询:叶查询子句在特定字段中查找特定值,例如
match
、term
或range
查询。- 精确查询:根据精确词条值查找数据,一般是查找 keyword、数值、日期、boolean 等类型字段。例如:
- ids:根据文档 ID 查找文档
- range:返回包含指定范围内的文档,比如:查询年龄在 10 到 20 岁的学生信息。
- term:根据精确值(例如价格、产品 ID 或用户名)查找文档。
- 全文检索查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query:对一个字段进行全文检索
- multi_match_query:对多个字段进行全文检索
- 精确查询:根据精确词条值查找数据,一般是查找 keyword、数值、日期、boolean 等类型字段。例如:
- 复合查询:以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
1.1 精确查询
1.1.1 term
语法:
GET /{索引库名}/_search{ \"query\": { \"term\": { \"字段名\": { \"value\": \"搜索条件\" } } }}
当输入的搜索条件不是词条,而是短语时,由于不做分词,反而搜索不到:
1.1.2 range
语法:
GET /{索引库名}/_search{ \"query\": { \"range\": { \"字段名\": { \"gte\": {最小值}, \"lte\": {最大值} } } }}
range
是范围查询,对于范围筛选的关键字有:
gte
:大于等于gt
:大于lte
:小于等于lt
:小于
1.2 全文检索
会对搜索条件进行拆分。
1.2.1 match
语法:
GET /{索引库名}/_search{ \"query\": { \"match\": { \"字段名\": \"搜索条件\" } }}
1.2.2 multi_match
同时对多个字段搜索,而且多个字段都要满足,语法:
GET /{索引库名}/_search{ \"query\": { \"multi_match\": { \"query\": \"搜索条件\", \"fields\": [\"字段1\", \"字段2\"] } }}
1.3 排序
语法:
GET /indexName/_search{ \"query\": { \"match_all\": {} }, \"sort\": [ { \"排序字段\": { \"order\": \"排序方式asc和desc\" } } ]}
如果按照商品价格排序:
GET /items/_search{ \"query\": { \"match_all\": {} }, \"sort\": [ { \"price\": { \"order\": \"desc\" } } ]}
1.4 分页查询
elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch 中通过修改 from
、size
参数来控制要返回的分页结果:
from
:从第几个文档开始size
:总共查询几个文档
语法:
GET /items/_search{ \"query\": { \"match_all\": {} }, \"from\": 0, // 分页开始的位置,默认为0 \"size\": 10, // 每页文档数量,默认10 \"sort\": [ { \"price\": { \"order\": \"desc\" } } ]}
2. Java Client 实现搜索
2.1 准备
代码:
@SpringBootTestpublic class SearchTest { @Autowired private IItemService itemService; private RestClient restClient = null; private ElasticsearchTransport transport = null; private ElasticsearchClient esClient = null; { // 使用 RestClient 作为底层传输对象 restClient = RestClient.builder(new HttpHost(\"192.168.101.68\", 9200)).build(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); // 使用 Jackson 作为 JSON 解析器 transport = new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper)); } // 实现后续操作 // TODO @BeforeEach public void searchTest() { // 创建客户端 esClient = new ElasticsearchClient(transport); System.out.println(esClient); } @AfterEach public void close() throws IOException { transport.close(); }}
后续代码放在代码的 TODO 处运行即可!!!
2.2 精准查询
2.2.1 Term 查询
根据 DSL 语句编写 java 代码:
GET /items/_search{ \"query\": { \"term\": { \"category\": { \"value\": \"拉杆箱\" } } }}
代码:
@Testpublic void testTermSearch() throws IOException { SearchResponse<ItemDoc> search = esClient.search( // 搜索索引 s -> s.index(\"items\").query( // 精准匹配 q -> q.term(t -> t.field(\"category\").value(\"牛奶\"))), // 指定返回类型 ItemDoc.class ); handleResponse(search);}
2.2.2 range 查询
GET /items/_search{ \"query\": { \"range\": { \"price\": { \"gte\": 100000, \"lte\": 20 } } }}
代码:
@Testpublic void testRangeSearch() throws IOException { SearchResponse<ItemDoc> search = esClient.search( // 搜索索引 s -> s.index(\"items\").query( // 范围匹配,price >= 100000 && price < 200000 q -> q.range( t -> t.field(\"price\").gte(JsonData.of(100000)).lt(JsonData.of(200000)) ) ), // 指定返回类型 ItemDoc.class );
2.3 全文检索
2.3.1 match 查询
GET /items/_search{ \"query\": { \"match\": { \"name\": \"德国进口纯奶\" } }}
代码:
@Testpublic void testMatchSearch() throws IOException { SearchResponse<ItemDoc> search = esClient.search( // 搜索索引 s -> s.index(\"items\").query( // 模糊匹配 q -> q.match( // 在 name 字段中模糊匹配 \"德国进口纯奶\" t -> t.field(\"name\").query(\"德国进口纯奶\"))), // 返回值类型 ItemDoc.class ); handleResponse(search);}
2.3.2 multi_match 查询
GET /items/_search{ \"query\": { \"multi_match\": { \"query\": \"笔记本\", \"fields\": [\"name\", \"category\"] } }}
代码:
@Testpublic void testMultiMatchSearch() throws IOException { SearchResponse<ItemDoc> search = esClient.search( // 搜索索引 s -> s.index(\"items\").query( // 多字段模糊匹配 q -> q.multiMatch( // 匹配字关键字 t -> t.query(\"笔记本\") // 匹配字段 .fields(\"name\", \"category\"))), // 指定返回类型 ItemDoc.class ); handleResponse(search);}
2.4 排序和分页
GET /items/_search{ \"query\": { \"multi_match\": { \"query\": \"绿色拉杆箱\", \"fields\": [\"name\",\"category\"] } }, \"sort\": [ { \"price\": { \"order\": \"asc\" } } ], \"size\": 20, \"from\": 0}
代码:
@Testpublic void testSortSearch() throws IOException { SearchResponse<ItemDoc> search = esClient.search( // 搜索索引 s -> s.index(\"items\") // 查询条件 .query(q -> q.multiMatch( // 匹配字段 m -> m.query(\"绿色拉杆箱\").fields(\"name\", \"category\") )) // 排序规则 .sort( s1 -> s1.field( // 排序字段 f -> f.field(\"price\") // 排序规则 .order(SortOrder.Desc) ) ) // 分页 .from(0).size(10), // 指定返回类型 ItemDoc.class ); handleResponse(search);}
3. 复合查询
3.1 布尔查询
bool 查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool 查询支持的逻辑运算有:
- must:必须匹配每个子查询,类似 “与”;
- should:选择性匹配子查询,类似 “或”;
- must_not:必须不匹配,不参与算分,类似 “非”;
- filter:必须匹配,不参与算分。
举例:
GET /items/_search{ \"query\": { \"bool\": { \"must\": [ {\"match\": {\"name\": \"手机\"}} ], \"should\": [ {\"term\": {\"brand\": { \"value\": \"vivo\" }}}, {\"term\": {\"brand\": { \"value\": \"小米\" }}} ], \"must_not\": [ {\"range\": {\"price\": {\"gte\": 2500}}} ] } }, \"sort\": [ { \"brand\": { \"order\": \"desc\" } } ]}
说明:
- 必须条件(
must
):- 文档的
name
字段必须包含“手机”。
- 文档的
- 可选条件(
should
):- 文档的
brand
字段应该是 “vivo” 或者 “小米”。只要满足其中一个条件即可。
- 文档的
- 排除条件(
must_not
):- 文档的
price
字段不能大于等于 2500 元。
- 文档的
- 过滤条件(
filter
):- 文档的
price
字段必须小于等于 1000 元。
- 文档的
当 should 与 must、must_not 同时使用时 should 会失效,需要指定 minimum_should_match。
3.2 尽量使用 filter
出于性能考虑,与搜索关键字无关的查询尽量采用 must_not 或 filter 逻辑运算,避免参与相关性算分(如:下拉菜单、多级菜单等)。
比如,要搜索 手机
,但品牌必须是 华为
,价格必须是 900~1599
,那么可以这样写:
GET /items/_search{ \"query\": { \"bool\": { \"must\": [ {\"match\": {\"name\": \"手机\"}} ], \"filter\": [ {\"term\": {\"brand\": { \"value\": \"华为\" }}}, {\"range\": {\"price\": {\"gte\": 90000, \"lte\": 159900}}} ] } }}
3.3 Java Client
@Testvoid testBoolQuery() throws Exception { //构建请求 SearchRequest.Builder builder = new SearchRequest.Builder(); //设置索引 builder.index(\"items\"); //设置查询条件 SearchRequest.Builder searchRequestBuilder = builder.query( // bool 查询,多条件匹配 q -> q.bool( // must 连接 b -> b.must( m -> m.match( // name 检索 mm -> mm.field(\"name\").query(\"手机\") ) ).should( s1 -> s1.term( t -> t.field(\"brand\").value(\"小米\")) ).should( s1 -> s1.term(t -> t.field(\"brand\").value(\"vivo\")) ).minimumShouldMatch(\"1\") ) ) // 排序· .sort(sort -> sort.field(f -> f.field(\"brand\").order(SortOrder.Asc))); SearchRequest build = searchRequestBuilder.build(); //执行请求 SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class); //解析结果 handleResponse(searchResponse);}
4. 高亮显示
4.1 高亮显示原理
什么是高亮显示呢?
我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示。
观察页面源码,你会发现两件事情:
- 高亮词条都被加了
标签
标签都添加了红色样式
因此实现高亮的思路就是:
- 用户输入搜索关键字搜索数据
- 服务端根据搜索关键字到 elasticsearch 搜索,并给搜索结果中的关键字词条添加
html
标签 - 前端提前给约定好的
html
标签添加CSS
样式
4.2 实现高亮
语法:
GET /{索引库名}/_search{ \"query\": { \"match\": { \"搜索字段\": \"搜索关键字\" } }, \"highlight\": { \"fields\": { \"高亮字段名称\": { \"pre_tags\": \"\", \"post_tags\": \"\" } }, \"require_field_match\": \"true\" }}
注意:
- 搜索必须有查询条件,而且是全文检索类型的查询条件,例如
match
; - 参与高亮的字段必须是
text
类型的字段; - 默认情况下参与高亮的字段要与搜索字段一致,除非添加:
required_field_match = false
。
代码:
@Testpublic void testHighLightSearch() throws Exception { SearchResponse<ItemDoc> search = esClient.search( // 搜索索引 s -> s.index(\"items\").query( // 匹配字段 q -> q.match( // 匹配字段 m -> m.field(\"name\").query(\"笔记本\") ) ) // 高亮 .highlight( h -> h.fields(\"name\", f -> f) // 高亮标签,前后缀 .preTags(\"\") .postTags(\"\") // .requireFieldMatch(false) ), ItemDoc.class); long total = search.hits().total().value(); System.out.println(\"total = \" + total); List<Hit<ItemDoc>> hits = search.hits().hits(); hits.forEach(hit -> { ItemDoc source = hit.source(); // 高亮数据 Map<String, List<String>> highlight = hit.highlight(); List<String> highlightName = highlight.get(\"name\"); if(highlightName != null && !highlightName.isEmpty()){ String s = highlightName.get(0); source.setName(s); System.out.println(\"s = \" + s); } });}
5. 数据聚合
5.1 介绍
聚合(aggregations
)可以让我们极其方便的实现对数据的统计、分析、运算。
应用场景:
- 对数据进行统计
- 在搜索界面显示符合条件的品牌、分类、规格等信息
聚合常见的有三类:
- 桶(
Bucket
)聚合:用来对文档做分组
TermAggregation
:按照文档字段值分组,例如按照品牌值分组、按照国家分组Date Histogram
:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(
Metric
)聚合:用以计算一些值,比如:最大值、最小值、平均值等
Avg
:求平均值Max
:求最大值Min
:求最小值Stats
:同时求max
、min
、avg
、sum
等
- 管道(
pipeline
)聚合:其它聚合的结果为基础做进一步运算
5.2 Bucket 聚合
5.2.1 语法
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category 值一样的放在同一组,属于 Bucket
聚合中的 Term
聚合。
基本语法如下:
GET /items/_search{ \"size\": 0, \"aggs\": { \"category_agg\": { \"terms\": { \"field\": \"category\", \"size\": 20, \"order\": { \"_count\": \"desc\" } } } }}
属性说明:
aggregations
:定义聚合
-
category_agg
:聚合名称,自定义,但不能重复-
terms
:聚合的类型,按分类聚合,所以用term
-
field
:参与聚合的字段名称 -
size
:希望返回的聚合结果的最大数量设置
size
为 0,查询 0 条数据即结果中不包含文档,只包含聚合 -
order
:对聚合结果排序
-
-
5.2.2 多级聚合
同时对品牌分组统计,此时需要按分类统计,按品牌统计,这时需要定义多个桶,如下:
GET /items/_search{ \"size\": 0, \"aggs\": { \"category_agg\": { \"terms\": { \"field\": \"category\", \"size\": 20 } }, \"brand_agg\":{ \"terms\": { \"field\": \"brand\", \"size\": 20 } } }}
现在需要统计同一分类下的不同品牌的商品数量,这时就需要对桶内的商品二次聚合,如下:
GET /items/_search{ \"aggs\" : { \"category_agg\" : { \"aggs\" : { \"brand_agg\" : { \"terms\" : { \"field\" : \"brand\", \"size\" : 20 } } }, \"terms\" : { \"field\" : \"category\", \"size\" : 20 } } }, \"size\" : 0}
5.3 带条件聚合
默认情况下,Bucket 聚合是对索引库的所有文档做聚合,例如我们统计商品中所有的品牌,结果如下:
可以看到统计出的品牌非常多。
但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
例如,我想知道价格高于 3000 元的手机品牌有哪些,该怎么统计呢?
语法如下:
增加 \"query\"
标签。
GET /items/_search{ \"query\": { \"bool\": { \"filter\": [ { \"term\": { \"category\": \"手机\" } }, { \"range\": { \"price\": { \"gte\": 300000 } } } ] } }, \"size\": 0, \"aggs\": { \"brand_agg\": { \"terms\": { \"field\": \"brand\", \"size\": 20 } } }}
5.4 Metric 聚合
统计了价格高于 3000 的手机品牌,形成了一个个桶。现在我们需要对桶内的商品做运算,获取每个品牌价格的最小值、最大值、平均值。
语法:
GET /items/_search{ \"query\": { \"bool\": { \"filter\": [ { \"term\": { \"category\": \"手机\" } }, { \"range\": { \"price\": { \"gte\": 300000 } } } ] } }, \"size\": 0, \"aggs\": { \"brand_agg\": { \"terms\": { \"field\": \"brand\", \"size\": 20, \"order\": { \"stats_metric.avg\": \"desc\" } }, \"aggs\": { \"stats_metric\": { \"stats\": { \"field\": \"price\" } } } } }}
属性说明:
stats_meric
:聚合名称,自定义名称
stats
:聚合类型,stats 是metric
聚合的一种field
:聚合字段,这里选择price
,统计价格
另外,我们还可以让聚合按照每个品牌的价格平均值排序:
5.5 Java Client
参考 DSL 语句编写 Java Client 代码
@Testvoid testAggs() throws Exception { //构建请求 SearchRequest.Builder builder = new SearchRequest.Builder(); //设置索引名 builder.index(\"items\"); //设置查询条件 builder.query( q -> q.bool( b -> b.filter( f -> f.term( t -> t.field(\"category\").value(\"手机\") ) ).filter( f -> f.range( r -> r.field(\"price\").gte(JsonData.of(3000)) ) ) ) ); //设置返回数量 builder.size(0); //设置聚合 builder.aggregations( \"brand_agg\", a -> a.terms( t -> t.field(\"brand\").size(10) .order(NamedValue.of(\"stats_metric.avg\", SortOrder.Desc)) ).aggregations( \"stats_metric\", a1 -> a1.stats( s -> s.field(\"price\") ) ) ); SearchRequest build = builder.build(); //执行请求 SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class); //解析出聚合结果 Aggregate brandAgg = searchResponse.aggregations().get(\"brand_agg\"); brandAgg.sterms().buckets().array().forEach(bucket -> { String key = bucket.key().stringValue(); Long docCount = bucket.docCount(); StatsAggregate statsMetric = bucket.aggregations().get(\"stats_metric\").stats(); //平均价格 Double avg = statsMetric.avg(); //最大价格 Double max = statsMetric.max(); //最小价格 Double min = statsMetric.min(); log.info(\"品牌:{},商品数量:{},平均价格:{},最大价格:{},最小价格:{}\", key, docCount, avg, max, min); });}