# DSL 查询文档
# DSL 查询分类
Elasticsearch 提供了基于 JSON 的 DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
- 查询所有:查询出所有数据,一般测试用。例如:match_all
- 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query
- multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找 keyword、数值、日期、boolean 等类型字段。例如:
- 地理(geo)查询:根据经纬度查询。例如:
- geo_distance
- geo_bounding_box
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
| GET /indexName/_search |
| { |
| "query": { |
| "查询类型": { |
| "查询条件": "条件值" |
| } |
| } |
| } |
# 全文检索查询
# 语法
# match 查询
单字段查询
| GET /indexName/_search |
| { |
| "query": { |
| "match": { |
| "FIELD": "TEXT" |
| } |
| } |
| } |
# mulit_match 查询
多字段查询,任意一个字符符合条件就算符合查询条件
| GET /indexName/_search |
| { |
| "query": { |
| "multi_match": { |
| "query": "TEXT", |
| "fields": ["FIELD1", " FIELD12"] |
| } |
| } |
| } |
# 精确查询
# term 查询
# 说明
因为精确查询的字段搜是不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据
# 语法
| |
| GET /indexName/_search |
| { |
| "query": { |
| "term": { |
| "FIELD": { |
| "value": "VALUE" |
| } |
| } |
| } |
| } |
# range 查询
# 说明
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤
# 语法
| |
| GET /indexName/_search |
| { |
| "query": { |
| "range": { |
| "FIELD": { |
| "gte": 10, |
| "lte": 20 |
| } |
| } |
| } |
| } |
# 地理坐标查询
# 矩形范围查询
# 说明
矩形范围查询,也就是 geo_bounding_box 查询,查询坐标落在某个矩形范围的所有文档
查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点
# 语法
| |
| GET /indexName/_search |
| { |
| "query": { |
| "geo_bounding_box": { |
| "FIELD": { |
| "top_left": { |
| "lat": 31.1, |
| "lon": 121.5 |
| }, |
| "bottom_right": { |
| "lat": 30.9, |
| "lon": 121.7 |
| } |
| } |
| } |
| } |
| } |
# 附近查询
# 说明
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档
# 语法
| |
| GET /indexName/_search |
| { |
| "query": { |
| "geo_distance": { |
| "distance": "15km", |
| "FIELD": "31.21,121.5" |
| } |
| } |
| } |
# 复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
# 相关性算分
# 语法
| [ |
| { |
| "_score" : 17.850193, |
| "_source" : { |
| "name" : "虹桥如家酒店真不错", |
| } |
| }, |
| { |
| "_score" : 12.259849, |
| "_source" : { |
| "name" : "外滩如家酒店真不错", |
| } |
| }, |
| { |
| "_score" : 11.91091, |
| "_source" : { |
| "name" : "迪士尼如家酒店真不错", |
| } |
| } |
| ] |
# TF-IDF 算法
# BM25 算法
# 算分函数查询
# 语法
# function score
查询中包含的四部分内容
- 原始查询条件:query 部分,基于这个条件搜索文档,并且基于 BM25 算法给文档打分,原始算分(query score)
- 过滤条件:filter 部分,符合该条件的文档才会重新算分
- 算分函数:符合 filter 条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果
- random_score:以随机数作为函数结果
- script_score:自定义算分函数算法
- 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- multiply:相乘
- replace:用 function score 替换 query score
- 其它,例如:sum、avg、max、min
# function score
的运行流程
- 根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
- 根据过滤条件,过滤文档
- 符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
- 将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分
# 代码
| GET /hotel/_search |
| { |
| "query": { |
| "function_score": { |
| "query": { .... }, |
| "functions": [ |
| { |
| "filter": { |
| "term": { |
| "brand": "如家" |
| } |
| }, |
| "weight": 2 |
| } |
| ], |
| "boost_mode": "sum" |
| } |
| } |
| } |
# 布尔查询
# 说明
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
- must:必须匹配每个子查询,类似 “与”
- should:选择性匹配子查询,类似 “或”
- must_not:必须不匹配,不参与算分,类似 “非”
- filter:必须匹配,不参与算分
# 语法
| GET /hotel/_search |
| { |
| "query": { |
| "bool": { |
| "must": [ |
| {"term": {"city": "上海" }} |
| ], |
| "should": [ |
| {"term": {"brand": "皇冠假日" }}, |
| {"term": {"brand": "华美达" }} |
| ], |
| "must_not": [ |
| { "range": { "price": { "lte": 500 } }} |
| ], |
| "filter": [ |
| { "range": {"score": { "gte": 45 } }} |
| ] |
| } |
| } |
| } |
# 搜索结果处理
# 排序
# 普通字段排序
# 说明
keyword、数值、日期类型排序的语法基本一致
# 语法
| GET /indexName/_search |
| { |
| "query": { |
| "match_all": {} |
| }, |
| "sort": [ |
| { |
| "FIELD": "desc" |
| } |
| ] |
| } |
# 地理坐标排序
# 说明
地理坐标排序略有不同
# 语法
| GET /indexName/_search |
| { |
| "query": { |
| "match_all": {} |
| }, |
| "sort": [ |
| { |
| "_geo_distance" : { |
| "FIELD" : "纬度,经度", |
| "order" : "asc", |
| "unit" : "km" |
| } |
| } |
| ] |
| } |
# 分页
# 介绍
elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch 中通过修改 from、size 参数来控制要返回的分页结果:
- from:从第几个文档开始
- size:总共查询几个文档
# 基本的分页
# 语法
| GET /hotel/_search |
| { |
| "query": { |
| "match_all": {} |
| }, |
| "from": 0, |
| "size": 10, |
| "sort": [ |
| {"price": "asc"} |
| ] |
| } |
# 深度分页
# 语法
| GET /hotel/_search |
| { |
| "query": { |
| "match_all": {} |
| }, |
| "from": 990, |
| "size": 10, |
| "sort": [ |
| {"price": "asc"} |
| ] |
| } |
# 比较
from + size
:
- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是 10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search
:
- 优点:没有查询上限(单次查询的 size 不超过 10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll
:
- 优点:没有查询上限(单次查询的 size 不超过 10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从 ES7.1 开始不推荐,建议用 after search 方案
# 高亮
# 高亮原理
- 给文档中的所有关键字都添加一个标签,例如
<em>
标签
- 页面给
<em>
标签编写 CSS 样式
# 实现高亮
# 语法
| GET /hotel/_search |
| { |
| "query": { |
| "match": { |
| "FIELD": "TEXT" |
| } |
| }, |
| "highlight": { |
| "fields": { |
| "FIELD": { |
| "pre_tags": "<em>", |
| "post_tags": "</em>" |
| } |
| } |
| } |
| } |
# 注意
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
# RestClient 查询文档
# match 查询
# 介绍
全文检索的 match 和 multi_match 查询与 match_all 的 API 基本一致。差别是查询条件,也就是 query 的部分
# 代码
| @Test |
| void testMatch() throws IOException { |
| |
| SearchRequest request = new SearchRequest("hotel"); |
| |
| request.source() |
| .query(QueryBuilders.matchQuery("all", "如家")); |
| |
| SearchResponse response = client.search(request, RequestOptions.DEFAULT); |
| |
| handleResponse(response); |
| } |
# 精确查询
# 介绍
# API
# 布尔查询
# 介绍
布尔查询是用 must、must_not、filter 等方式组合其它查询
# API
# 代码
| @Test |
| void testBool() throws IOException { |
| |
| SearchRequest request = new SearchRequest("hotel"); |
| |
| |
| BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); |
| |
| boolQuery.must(QueryBuilders.termQuery("city", "杭州")); |
| |
| boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250)); |
| request.source().query(boolQuery); |
| |
| SearchResponse response = client.search(request, RequestOptions.DEFAULT); |
| |
| handleResponse(response); |
| } |
# 排序分页
# 介绍
搜索结果的排序和分页是与 query 同级的参数,因此同样是使用 request.source () 来设置
# API
# 代码
| @Test |
| void testPageAndSort() throws IOException { |
| |
| int page = 1, size = 5; |
| |
| SearchRequest request = new SearchRequest("hotel"); |
| |
| |
| request.source().query(QueryBuilders.matchAllQuery()); |
| |
| request.source().sort("price", SortOrder.ASC); |
| |
| request.source().from((page - 1) * size).size(5); |
| |
| SearchResponse response = client.search(request, RequestOptions.DEFAULT); |
| |
| handleResponse(response); |
| } |
# 高亮
# 介绍
- 查询的 DSL:其中除了查询条件,还需要添加高亮条件,同样是与 query 同级。
- 结果解析:结果除了要解析_source 文档数据,还要解析高亮结果
# 高亮请求 API
# 代码
| @Test |
| void testHighlight() throws IOException { |
| |
| SearchRequest request = new SearchRequest("hotel"); |
| |
| |
| request.source().query(QueryBuilders.matchQuery("all", "如家")); |
| |
| request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false)); |
| |
| SearchResponse response = client.search(request, RequestOptions.DEFAULT); |
| |
| handleResponse(response); |
| } |
# 高亮结果解析
# 步骤
- 第一步:从结果中获取 source。hit.getSourceAsString (),这部分是非高亮结果,json 字符串。还需要反序列为 HotelDoc 对象
- 第二步:获取高亮结果。hit.getHighlightFields (),返回值是一个 Map,key 是高亮字段名称,值是 HighlightField 对象,代表高亮值
- 第三步:从 map 中根据高亮字段名称,获取高亮字段值对象 HighlightField
- 第四步:从 HighlightField 中获取 Fragments,并且转为字符串。这部分就是真正的高亮字符串了
- 第五步:用高亮的结果替换 HotelDoc 中的非高亮结果
# 代码
| private void handleResponse(SearchResponse response) { |
| |
| SearchHits searchHits = response.getHits(); |
| |
| long total = searchHits.getTotalHits().value; |
| System.out.println("共搜索到" + total + "条数据"); |
| |
| SearchHit[] hits = searchHits.getHits(); |
| |
| for (SearchHit hit : hits) { |
| |
| String json = hit.getSourceAsString(); |
| |
| HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); |
| |
| Map<String, HighlightField> highlightFields = hit.getHighlightFields(); |
| if (!CollectionUtils.isEmpty(highlightFields)) { |
| |
| HighlightField highlightField = highlightFields.get("name"); |
| if (highlightField != null) { |
| |
| String name = highlightField.getFragments()[0].string(); |
| |
| hotelDoc.setName(name); |
| } |
| } |
| System.out.println("hotelDoc = " + hotelDoc); |
| } |
| } |