一、DSL查询文档
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
1. DSL 查询分类概述
查询所有:查询出所有数据,一般测试用。例如:match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
match_query、multi_match_query
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:ids、range、term
地理(geo)查询:根据经纬度查询。例如:geo_distance、geo_bounding_box
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:bool、function_score
基本语法
GET /索引库名/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
例如:查询所有
// 查询所有
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}
2. 全文检索查询
全文检索查询,会对用户输入内容分词,常用于搜索框搜索:
- match查询 :全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,根据一个字段查询,语法:
GET /索引名/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
如:条件查询 酒店吗为"7天酒店" 的数据
GET /hotel/_search
{
"query": {
"match": {
"all": "7天酒店"
}
}
}
- multi_match:与match查询类似,只不过允许同时查询多个字段,根据多个字段查询,参与查询字段越多,查询性能越差。语法:
GET /索引名/_search
{
"query": {
"multi_match": {
"query": "TEXT",
"fields": ["FIELD1", " FIELD12"]
}
}
}
如:满足"brand", "name", "business" 任意字cha
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "7天酒店",
"fields": ["brand", "name", "business"]
}
}
}
3. 精准查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
- term:根据词条精确值查询
- range:根据值的范围查询
term 查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
// term查询
GET /索引名/_search
{
"query": {
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
}
搜索城市为"上海" 的酒店
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "上海"
}
}
}
}
range 查询:根据数值范围查询,可以是数值、日期的范围
// range查询
GET /索引名/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10,
"lte": 20
}
}
}
}
搜索价格区间大于等于100且小于等于300的酒店
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": "100", #gte大于等于, gt大于
"lte": "300" # lte 小于等于, lt 小于
}
}
}
}
4. 地理坐标查询
根据经纬度查询。常见的使用场景包括:搜索附近的人,附近的酒店等。
- geo_bounding_box:查询左上角和右下角相交形成的矩形范围内的所有文档
// geo_bounding_box查询
GET /索引名/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": {
"lat": 31.1,
"lon": 121.5
},
"bottom_right": {
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
- geo_distance:查询指定中心点及半径所形成的圆的所有文档
// geo_distance 查询
GET /索引名/_search
{
"query": {
"geo_distance": {
"distance": "15km",
"FIELD": "31.21,121.5"
}
}
}
5. 复合查询
复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑,例如:
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价
相关性算分:
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。相关性算分两种方式:
-
- TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大
-
- BM25:在elasticsearch5.0之后,会随着词频增加而增大,但增长曲线会趋于水平
5.1 function score query
使用 function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。
案例 :给"如家"这个品牌的酒店排名靠前一些
解析:function score需要的三要素
-
过滤条件 :哪些文档需要算分加权? 答: 品牌为如家的酒店
-
算分函数 :算分函数是什么? 答:weight就可以
-
加权方式 :加权模式是什么? 答:求和
GET /hotel/_search
{
"query":{
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [ #算分函数
{
"filter": { # 满足的条件,品牌必须是如家
"term": {
"brand": "如家"
}
},
"weight": 2 #权重为2
}
],
"boost_mode": "sum"
}
}
}
5.2 Boolean query
布尔查询是一个或多个查询子句的组合。子查询的组合方式有:
must:必须匹配每个子查询,类似"与"
should:选择性匹配子查询,类似"或"
must_not:必须不匹配,不参与算分,类似"非"
filter:必须匹配,不参与算分
需求:搜索名字包含"如家",价格不高于400,在坐标31.21,121.5周围10km范围内的酒店
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
二、搜索结果处理
1. 排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
GET /索引名/_search
{
"query": {
"match_all": { }
},
"sort": [
{
"FIELD": "desc" // 排序字段和排序方式ASC、DESC
}
]
}
案例 :对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
# sort 排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": "desc"
},
{
"price": "asc"
}
]
}
案例2**:** 实现对酒店数据按照到你的位置坐标的距离升序排序
获取经纬度的方式: 获取鼠标点击经纬度-地图属性-示例中心-JS API 2.0 示例 | 高德地图API
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 31.064661,
"lon": 121.621245
},
"order": "asc",
"unit": "km"
}
}
]
}
结果:
2. 分页
elasticsearch 默认情况下值返回前10条数据,如果需要查询更多数据就需要修改分页参数了。
elasticsearch 中通过from, size参数来控制要返回分页结果。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
}
],
"from": 0, // 分页开始的位置,默认为0
"size": 10 // 每页显示的条数
}
通过from, size 有个缺点,就是把所有数据都会查出来,然后通过截取的方式获取分页数据。对于海量数据,这种分页方式效率低下。也就是说,ES是分布式的,所以会面临深度分页问题。例如按price排序后,获取from = 990,size =10的数据:
- 首先在每个数据分片上都排序并查询前1000条文档。
- 然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
- 最后从这1000条中,选取从990开始的10条文档
如果搜索页数过深,或者结果集(from + size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000
针对深度分页,ES提供了两种解决方案,官方文档:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
结果处理集分页总结
from + size:
•优点:支持随机翻页
•缺点:深度分页问题,默认查询上限(from + size)是10000
•场景:百度、京东、谷歌、淘宝这样的随机翻页搜索after search:
•优点:没有查询上限(单次查询的size不超过10000)
•缺点:只能向后逐页查询,不支持随机翻页
•场景:没有随机翻页需求的搜索,例如手机向下滚动翻页scroll:
•优点:没有查询上限(单次查询的size不超过10000)
•缺点:会有额外内存消耗,并且搜索结果是非实时的
•场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
3. 高亮
高亮就是在搜索结果中把搜索关键字突出显示。
高亮原理解析:
- 将搜索关键字用标签标记起来。
- 在页面中给标签添加css样式
基本语法
GET /索引库名/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
},
"highlight": {
"fields": { // 指定要高亮的字段
"FIELD": {
"pre_tags": "<em>", // 用来标记高亮字段的前置标签
"post_tags": "</em>" // 用来标记高亮字段的后置标签
}
}
}
}
# 高亮显示,默认情况,ES搜索字段必须和高亮字段一致
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"require_field_match": "false" #是否需要字段匹配,默认true,关闭后, ES搜索字段可和高亮字段不一致
}
}
}
}
三、RestClient查询文档
1. 快速入门
通过match_all来演示下基本的API,先看请求DSL的组织:
通过match_all来演示下基本的API,解析结果:
详细代码
public class HotelSearchTest {
private RestHighLevelClient client;
// 客户端初始化
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.30.130:9200") // 服务器IP + ES 端口
));
}
@Test
void testMatchAll() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备 DSL
request.source()
.query(QueryBuilders.matchAllQuery());
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析响应
SearchHits hits = response.getHits();
// 4.1 获取总条数
long total = hits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
//4.2 文档数组
SearchHit[] hits1 = hits.getHits();
// 4.3 遍历
for (SearchHit hit : hits1) {
//获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
System.out.println(response);
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
RestAPI中其中构建DSL是通过HighLevelRestClient中的resource()来实现的,其中包含了查询、排序、分页、高亮等所有功能。
RestAPI中其中构建查询条件的核心部分是由一个名为QueryBuilders的工具类提供的,其中包含了各种查询方法:
2. match 查询
全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件,也就是query的部分。同样是利用QueryBuilders提供的方法:
*//单字段查询
QueryBuilders.matchQuery("all", "如家");
*//多字段查询
QueryBuilders.multiMatchQuery("如家"**, "name", "business");//*查询所有
QueryBuilders.matchAllQuery*();
match 查询
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
}
}
match_all 查询
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
@Test
void testMatch() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备 DSL
request.source()
.query(QueryBuilders.matchQuery("all", "如家"));
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析响应
SearchHits hits = response.getHits();
// 4.1 获取总条数
long total = hits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
//4.2 文档数组
SearchHit[] hits1 = hits.getHits();
// 4.3 遍历
for (SearchHit hit : hits1) {
//获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
System.out.println(response);
}
multi_match 查询
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "如家",
"fields": ["brand", "name"]
}
}
}
@Test
void testMultiMatch() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备 DSL
request.source()
.query(QueryBuilders.multiMatchQuery("如家", "name", "business"));
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析响应
SearchHits hits = response.getHits();
// 4.1 获取总条数
long total = hits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
//4.2 文档数组
SearchHit[] hits1 = hits.getHits();
// 4.3 遍历
for (SearchHit hit : hits1) {
//获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
System.out.println(response);
}
3. 精确查询
精确查询常见的有term查询和range查询,同样利用QueryBuilders实现
*//词条查询
QueryBuilders.termQuery("city", "杭州");
*//范围查询
QueryBuilders.rangeQuery("price"**).gte(100).lte(150);
term 查询
GET /hotel/_search
{
"query": {
"term": {
"city": "杭州"
}
}
}
range 查询
GET /hotel/_search
{
"query": {
"range": {
"price": { "gte": 100, "lte": 150 }
}
}
}
@Test
void testBool() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
//2.1 准备BooleanQuery
BoolQueryBuilder booledQuery = QueryBuilders.boolQuery();
//2.2 添加 term
booledQuery.must(QueryBuilders.termQuery("city", "上海"));
//2.3 添加range
booledQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(booledQuery);
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析响应
SearchHits hits = response.getHits();
// 4.1 获取总条数
long total = hits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
//4.2 文档数组
SearchHit[] hits1 = hits.getHits();
// 4.3 遍历
for (SearchHit hit : hits1) {
//获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
System.out.println(response);
}
4. 复合查询
复合查询------boolean query
//创建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
*//添加must*条件
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
//添加filter**条件
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
GET /hotel/_search{
"query": {
"bool": {
"must": [
{
"term": { "city": "杭州" }
}
],
"filter": [
{
"range": {
"price": { "lte": 250 }
}
}
]
}
}
}
tips: 要构建查询条件,只要记住一个类:QueryBuilders
@Test
void testBool() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
//2.1 准备BooleanQuery
BoolQueryBuilder booledQuery = QueryBuilders.boolQuery();
//2.2 添加 term
booledQuery.must(QueryBuilders.termQuery("city", "上海"));
//2.3 添加range
booledQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(booledQuery);
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析响应
SearchHits hits = response.getHits();
// 4.1 获取总条数
long total = hits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
//4.2 文档数组
SearchHit[] hits1 = hits.getHits();
// 4.3 遍历
for (SearchHit hit : hits1) {
//获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
System.out.println(response);
}
5. 排序、分页
搜索结果的排序和分页是与query同级的参数,对应的API如下:
*//*查询
request.source().query(QueryBuilders.matchAllQuery());*//*分页
request.source().from(0).size(5);// 价格排序
request.source().sort("price", SortOrder.ASC);//距离排序
request.source().sort(SortBuilders
.geoDistanceSort("location", newGeoPoint(*"31.21*, 121.5"**))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
DSL 语句
GET /indexName/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"sort": [
{
"FIELD": "desc"
},
]
}
@Test
void testPageAndSort() throws IOException {
int page = 1, size = 5;
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
//2.1 query
request.source().query(QueryBuilders.matchAllQuery());
//2.2 排序
request.source().sort("price", SortOrder.ASC);
//2.3 分页
request.source().from((page - 1) * size).size(5);
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析响应
SearchHits hits = response.getHits();
// 4.1 获取总条数
long total = hits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
//4.2 文档数组
SearchHit[] hits1 = hits.getHits();
// 4.3 遍历
for (SearchHit hit : hits1) {
//获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
System.out.println(response);
}
6. 高亮
高亮API包括请求DSL构建和结果解析两部分。我们先看请求的DSL构建:
request.source().highlighter(newHighlightBuilder()
.field("name")
*//*是否需要与查询字段匹配.requireFieldMatch(false)
);
DSL 语句
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"require_field_match": "false"
}
}
}
}
高亮结果解析
//获取source
HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(), 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);
}
}