Elasticsearch(二)
Elasticsearch搜索引擎的黑马学习笔记
原文:day08-Elasticsearch - 飞书云文档 (feishu.cn)
一、DSL查询
前面我们只根据id查询结果。那么如何根据一些别的字段来查询呢? 这里就引入了DSL(Domain Specific Language)领域特定语言
Elasticsearch的查询可以分为两大类:
通过match_all简单查询:
1 2 3 4 5 6 7 8
| GET /items/_search { "query": { "match_all": { } } }
|
常见查询方式:
-
全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
-
精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:
-
**地理坐标查询:**用于搜索地理位置,搜索方式很多,例如:
geo_bounding_box:按矩形搜索
geo_distance:按点和半径搜索
二、叶子查询
1、全文检索查询
match 匹配一个字段
1 2 3 4 5 6 7 8
| GET /items/_search { "query": { "match": { "name": "华为荣耀" } } }
|
multi_match 匹配多个字段
1 2 3 4 5 6 7 8 9
| GET /items/_search { "query": { "multi_match": { "query": "华为荣耀", "fields": ["name", "brand"] } } }
|
2、精确查询
**Term-level query ** 精确匹配,适合查找keyword、数值、日期、boolean类型的字段
term 查询一个
1 2 3 4 5 6 7 8 9 10
| GET /items/_search { "query": { "term": { "brand": { "value": "格力" } } } }
|
range 查询范围
1 2 3 4 5 6 7 8 9 10 11
| GET /items/_search { "query": { "range": { "price": { "gte": 0, "lte": 100 } } } }
|
三、复合查询
1、算分函数
function_query结构包含四个部分:
“query”:上面学的查询条件,基于BM25算法给文档打分,得到原始算分(query score)
“filter”: 过滤条件,基于该条件的文档才会重新算分。
“functions”:算分函数,基于filter过滤后的文档,根据这个函数进行算分,得到函数算分(function score),可以选四种函数
weight :函数结果是常量
field_value_factor :以文档中某个字段值作为函数结果
random_score :随机数作为函数结果
script_score :自定义算分算法
“boost_mode”: 算分模式, 将算分函数得到的function score与query score 不同方式进行运算。
multiply: 相乘
replace: 用function score代替query score
其他: sum、avg、max、min
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| GET /hotel/_search { "query": { "function_score": { "query": { .... }, "functions": [ { "filter": { "term": { "brand": "Iphone" } }, "weight": 10 } ], "boost_mode": "multipy" } } }
|
2、bool查询
利用逻辑运算组合一个或多个查询子句的组合
must :必须匹配每个子查询,&&
should:选择性匹配每个子查询, ||
must_not: 必须不匹配,不参与算分。 !
filter:必须匹配,不参与算分
实际上就是叶子查询前面套了一些逻辑运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| GET /items/_search { "query": { "bool": { "must": [ {"match": {"name": "手机"}} ], "should": [ {"term": {"brand": { "value": "vivo" }}}, {"term": {"brand": { "value": "小米" }}} ], "must_not": [ {"range": {"price": {"gte": 2500}}} ], "filter": [ {"range": {"price": {"lte": 1000}}} ] } } }
|
必须是name包含手机,品牌可以是vivo或者小米。价格必须小于2500。过滤只剩下价格小于1000的结果。
小细节:
与搜索关键字有关的搜索字段可以采用must或者should。与关键字无关的采用must_not或者filter,可以避免参与相关性计算。
比如,我们要搜索手机,但品牌必须是华为,价格必须是900~1599,那么可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| GET /items/_search { "query": { "bool": { "must": [ {"match": {"name": "手机"}} ], "filter": [ {"term": {"brand": { "value": "华为" }}}, {"range": {"price": {"gte": 90000, "lt": 159900}}} ] } } }
|
3、排序
默认通过相关度算分_score来排序,也支持自定义方式排序。但是分词字段不能用来排序。可以排序的字段:keyword类型、数值类型、地理坐标类型、日期类型等。
1 2 3 4 5 6 7 8 9 10 11 12 13
| GET /items/_search { "query": { "match_all": {} }, "sort": [ { "price": { "order": "desc" } } ] }
|
order可以选择desc或者是asc
4、分页
基础分页
ES默认只返回前十条,想要得到更多的数据只能修改分页参数了。
-
from:从第几个文档开始
-
size:总共查询几个文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| GET /items/_search { "query": { "match_all": {} }, "from": 0, "size": 10, "sort": [ { "price": { "order": "desc" } } ] }
|
深度分页
ES一般会将数据采用分片存储,但是这样会造成问题: 比如我们想要得到价格降序条件下, 每页10条 第99页的的数据。那么我们就要先得到前1000个数据进行排序。
又因为我们分片存储了,又得得到每一片的前1000个,把他们全部汇总再去排序得到排序后的前1000个。
这样如果汇总数据很多,就会对内存和CPU会产生非常大的压力。
针对深度分页,elasticsearch提供了两种解决方案:
5、高亮
![8]()
前端高亮原理:
-
高亮词条都被加了<em>标签
-
<em>标签都添加了红色样式
em可以自定义
前端并不知道什么时候需要高亮。词条的高亮标签肯定是由服务端提供数据的时候已经加上的
实现高亮的原理:
用户通过关键词搜索数据 -> 后端ES查询到数据后,将数据中包含关键词的词条加上高亮标签(要先和前端约定好高亮标签)
格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| GET /{索引库名}/_search { "query": { "match": { "搜索字段": "搜索关键字" } }, "highlight": { "fields": { "高亮字段名称": { "pre_tags": "<em>", "post_tags": "</em>" } } } }
|
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| GET /items/_search { "query": { "match": { "name": "脱脂牛奶" } }, "highlight": { "fields": { "name": { "pre_tags": "<em>", "post_tags": "</em>" } } } }
|
四、RestClient查询
通过Java代码操作ES来查询
准备请求参数完全可以对应上面JSON查询代码来构建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Test void testMatchAll() throws IOException { SearchRequest request = new SearchRequest("items");
request.source() .query(QueryBuilders.matchAllQuery()); SearchResponse response = client.search(request, RequestOptions.DEFAULT);
parseResponseResult(response);
}
private static void parseResponseResult(SearchResponse response){ SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value; System.out.println("total = " + total); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { String json = hit.getSourceAsString(); ItemDoc doc = JSONUtil.toBean(json, ItemDoc.class); Map<String, HighlightField> hfs = hit.getHighlightFields(); if(hfs !=null && !hfs.isEmpty()){ HighlightField hf = hfs.get("name"); String hfName = hf.getFragments()[0].string(); doc.setName(hfName); }
System.out.println("doc = " + doc);
} }
|
request.source()包含DSL所需要的功能
![9]()
QueryBuilders.matchAllQuery()包含很多查询方式:
![10]()
1、叶子查询
match
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Test void testMatchAll() throws IOException { SearchRequest request = new SearchRequest("items");
request.source() .query(QueryBuilders.matchAllQuery()); SearchResponse response = client.search(request, RequestOptions.DEFAULT); parseResponseResult(response);
}
|
multiple_match
1 2 3 4 5 6 7 8 9 10 11
| @Test void testMultiMatch() throws IOException { SearchRequest request = new SearchRequest("items"); request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category")); SearchResponse response = client.search(request, RequestOptions.DEFAULT); parseResponseResult(response); }
|
range
1 2 3 4 5 6 7 8 9 10 11
| @Test void testRange() throws IOException { SearchRequest request = new SearchRequest("items"); request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000)); SearchResponse response = client.search(request, RequestOptions.DEFAULT); parseResponseResult(response); }
|
term
1 2 3 4 5 6 7 8 9 10 11
| @Test void testTerm() throws IOException { SearchRequest request = new SearchRequest("items"); request.source().query(QueryBuilders.termQuery("brand", "华为")); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response); }
|
2、复合查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Test void testBool() throws IOException { SearchRequest request = new SearchRequest("items"); BoolQueryBuilder bool = QueryBuilders.boolQuery(); bool.must(QueryBuilders.matchQuery("name", "脱脂牛奶")); bool.filter(QueryBuilders.termQuery("brand", "德亚")); bool.filter(QueryBuilders.rangeQuery("price").lte(30000)); request.source().query(bool); SearchResponse response = client.search(request, RequestOptions.DEFAULT); parseResponseResult(response); }
|
实际上就是支持嵌套查询,我们在BoolQueryBuilder中又添加了叶子查询的参数
3、排序和分页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Test void testPageAndSort() throws IOException { int pageNo = 1, pageSize = 5;
SearchRequest request = new SearchRequest("items"); request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶")); request.source().sort("price", SortOrder.ASC); request.source().from((pageNo - 1) * pageSize).size(pageSize); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response); }
|
在不同的维度。之前都是添加在query中。现在选择source的不同方法中
4、高亮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Test void testHighlight() throws IOException { SearchRequest request = new SearchRequest("items"); request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶")); request.source().highlighter( SearchSourceBuilder.highlight() .field("name") .preTags("<em>") .postTags("</em>") ); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response); }
|
处理高亮结果完整方法在上面。 由于高亮查询结果并不会自动添加到_source内,会放在highlight中,所以我们需要手动替换。
![11]()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| for (SearchHit hit : hits) { String source = hit.getSourceAsString(); ItemDoc item = JSONUtil.toBean(source, ItemDoc.class); Map<String, HighlightField> hfs = hit.getHighlightFields(); if (CollUtils.isNotEmpty(hfs)) { HighlightField hf = hfs.get("name"); if (hf != null) { String hfName = hf.getFragments()[0].string(); item.setName(hfName); } } System.out.println(item); }
|
只能说人家函数这么设计的。我们只能这么取
五、数据聚合
聚合(aggregations)可以非常方便的实现对数据的统计、分析、运算。例如:
-
什么品牌的手机最受欢迎?
-
这些手机的平均价格、最高价格、最低价格?
-
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
聚合常见的有三类:
-
桶(Bucket)聚合:用来对文档做分组
-
TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
-
Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
-
度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
-
Avg:求平均值
-
Max:求最大值
-
Min:求最小值
-
Stats:同时求max、min、avg、sum等
-
管道(pipeline)聚合:其它聚合的结果为基础做进一步运算
**注意:**参加聚合的字段必须是keyword、日期、数值、布尔类型
1、Bucket聚合
1 2 3 4 5 6 7 8 9 10 11 12
| GET /items/_search { "size": 0, "aggs": { "category_agg": { "terms": { "field": "category", "size": 20 } } } }
|
size: 设置为0条,表示只做文档查询,不需要查看文档。
aggs: 定义聚合。
category_agg: 自定义聚合名称,不可重复。
terms: 聚合类型,按分类聚合,用terms
field:参与聚合字段名称
size: 希望返回的聚合结果最大值。
得到查询结果中分类排名,以及分类的数量。安装分类数量排名
![12]()
2、带条件的聚合
bucket集合与bool查询一起使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| GET /items/_search { "query": { "bool": { "filter": [ { "term": { "category": "手机" } }, { "range": { "price": { "gte": 300000 } } } ] } }, "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 20 } } } }
|
![13]()
3、Metric聚合(度量聚合)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| GET /items/_search { "query": { "bool": { "filter": [ { "term": { "category": "手机" } }, { "range": { "price": { "gte": 300000 } } } ] } }, "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 20 }, "aggs": { "stats_meric": { "stats": { "field": "price" } } } } } }
|
可以看到我们在brand_agg聚合的内部,我们新加了一个aggs参数。这个聚合就是brand_agg的子聚合,会对brand_agg形成的每个桶中的文档分别统计。
-
stats_meric:聚合名称
stats:聚合类型,stats是metric聚合的一种
field:聚合字段,这里选择price,统计价格
![14]()
对分组结果进行metric聚合。
另外,我们还可以让聚合按照每个品牌的价格平均值排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| GET /items/_search { "query": { "bool": { "filter": [ { "term": { "category": "手机" } }, { "range": { "price": { "gte": 300000 } } } ] } }, "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 20, "order": { "stats_meric.avg": "desc" } }, "aggs": { "stats_meric": { "stats": { "field": "price" } } } } } }
|
![15]()
aggs代表聚合,与query同级,此时query的作用是?
聚合必须的三要素:
聚合可配置属性有:
-
size:指定聚合结果数量
-
order:指定聚合结果排序方式
-
field:指定聚合字段
4、RestClient实现聚合
aggs聚合条件与query条件是同一级别,都属于查询JSON参数。因此依然是利用request.source()方法来设置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Test void testAgg() throws IOException {
SearchRequest request = new SearchRequest("items");
request.source().size(0); String brandAggName = "brandAgg"; request.source().aggregation( AggregationBuilders.terms(brandAggName) .field("brand").size(10)); SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Aggregations aggregations = response.getAggregations(); Terms brandTerms = aggregations.get(brandAggName); List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
for (Terms.Bucket bucket : buckets) { System.out.println("bucket = " + bucket.getKeyAsString()); System.out.println("count = " + bucket.getDocCount()); } }
|
![16]()
六、复杂的例子
![17]()
对应上述搜索的查询语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| public PageDTO<ItemDTO> detailSearch(ItemPageQuery itemPageQuery) throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.101.128:9200") ));
SearchRequest request = new SearchRequest("items");
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery(); if (StrUtil.isNotBlank(itemPageQuery.getKey())) { queryBuilder.must(QueryBuilders.matchQuery("name", itemPageQuery.getKey())); } if (StrUtil.isNotBlank(itemPageQuery.getBrand())) { queryBuilder.must(QueryBuilders.termQuery("brand", itemPageQuery.getBrand())); } if (StrUtil.isNotBlank(itemPageQuery.getCategory())) { queryBuilder.must(QueryBuilders.termQuery("category", itemPageQuery.getCategory())); } if (itemPageQuery.getMaxPrice() != null) { queryBuilder.filter(QueryBuilders.rangeQuery("price").lt(itemPageQuery.getMaxPrice())); } if (itemPageQuery.getMinPrice() != null) { queryBuilder.filter(QueryBuilders.rangeQuery("price").gt(itemPageQuery.getMinPrice())); }
FunctionScoreQueryBuilder scoreQueryBuilder = QueryBuilders.functionScoreQuery( queryBuilder, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ new FunctionScoreQueryBuilder.FilterFunctionBuilder( QueryBuilders.termQuery("isAD", "true"), ScoreFunctionBuilders.weightFactorFunction(100) ) } ).boostMode(CombineFunction.MULTIPLY);
request.source().query(scoreQueryBuilder);
request.source().from((itemPageQuery.getPageNo() - 1) * itemPageQuery.getPageSize()).size(itemPageQuery.getPageSize());
if (StrUtil.isNotBlank(itemPageQuery.getSortBy())) request.source().sort(StrUtil.toCamelCase(itemPageQuery.getSortBy()), itemPageQuery.getIsAsc() ? SortOrder.ASC : SortOrder.DESC);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
long total = response.getHits().getTotalHits().value;
List<ItemDTO> itemDTOList = new ArrayList<>();
SearchHits hits = response.getHits(); for (SearchHit hit : hits) { String source = hit.getSourceAsString(); ItemDoc itemDoc = JSONUtil.toBean(source, ItemDoc.class); ItemDTO itemDTO = BeanUtil.copyProperties(itemDoc, ItemDTO.class); itemDTOList.add(itemDTO); }
if (client != null) { client.close(); } PageDTO<ItemDTO> pageDTO = new PageDTO<>(); pageDTO.setTotal(total); pageDTO.setList(itemDTOList); pageDTO.setPages(itemPageQuery.getPageNo().longValue()); return pageDTO; }
|
得到聚合结果,用于搜索结果的分类,品牌的动态展示
不放在一起是因为聚合只需要根据key得到聚合结果就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public String filters(ItemPageQuery query) throws IOException { RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.174.129:9200") ));
SearchRequest request = new SearchRequest("items");
SearchSourceBuilder sourceBuilder = request.source(); sourceBuilder.query(QueryBuilders.matchQuery("name", query.getKey())); sourceBuilder.aggregation(AggregationBuilders.terms("brand_agg").field("brand")); sourceBuilder.aggregation(AggregationBuilders.terms("category_agg").field("category"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Terms brandTerm = response.getAggregations().get("brand_agg"); Terms categoryAgg = response.getAggregations().get("category_agg");
List<String> brandList = new ArrayList<>(); List<String> categoryList = new ArrayList<>();
for (Terms.Bucket bucket : brandTerm.getBuckets()) { String keyAsString = bucket.getKeyAsString(); brandList.add(keyAsString); }
for (Terms.Bucket bucket : categoryAgg.getBuckets()) { String keyAsString = bucket.getKeyAsString(); categoryList.add(keyAsString); }
if (client != null) { client.close(); }
Map<String,List<String>> map = new HashMap<>(); map.put("category",categoryList); map.put("brand",brandList); return JSONUtil.toJsonStr(map); }
|