分布式搜索引擎Elasticsearch(二)

一、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. 全文检索查询

全文检索查询,会对用户输入内容分词,常用于搜索框搜索:

  1. match查询 :全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,根据一个字段查询,语法:

GET /索引名/_search

{

"query": {

"match": {

"FIELD": "TEXT"

}

}

}

如:条件查询 酒店吗为"7天酒店" 的数据

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "7天酒店"
    }
  }
}
  1. 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. 地理坐标查询

根据经纬度查询。常见的使用场景包括:搜索附近的人,附近的酒店等。

  1. 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

}

}

}

}

}

  1. geo_distance:查询指定中心点及半径所形成的圆的所有文档

// geo_distance 查询

GET /索引名/_search

{

"query": {

"geo_distance": {

"distance": "15km",

"FIELD": "31.21,121.5"

}

}

}

5. 复合查询

复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑,例如:

fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价

相关性算分

当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。相关性算分两种方式:

    1. TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大
    1. BM25:在elasticsearch5.0之后,会随着词频增加而增大,但增长曲线会趋于水平

5.1 function score query

使用 function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。

案例 :给"如家"这个品牌的酒店排名靠前一些

解析:function score需要的三要素

  1. 过滤条件 :哪些文档需要算分加权? 答: 品牌为如家的酒店

  2. 算分函数 :算分函数是什么? 答:weight就可以

  3. 加权方式 :加权模式是什么? 答:求和

    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的数据:

  1. 首先在每个数据分片上都排序并查询前1000条文档。
  2. 然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
  3. 最后从这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. 高亮

高亮就是在搜索结果中把搜索关键字突出显示。

高亮原理解析:

  1. 将搜索关键字用标签标记起来。
  2. 在页面中给标签添加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.m
atchAllQuery*();

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.2
1*, 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);
}
}

相关推荐
小黑屋说YYDS1 小时前
ElasticSearch7.x入门教程之全文搜索(九)
elasticsearch
The博宇4 小时前
Spark常问面试题---项目总结
大数据·分布式·spark
冧轩在努力4 小时前
redis的应用--分布式锁
数据库·redis·分布式
AI航海家(Ethan)4 小时前
分布式爬虫那些事儿
分布式·爬虫
jc581275 小时前
PHP RabbitMQ连接超时问题
分布式·rabbitmq
java1234_小锋5 小时前
Zookeeper的通知机制是什么?
分布式·zookeeper·云原生
喝醉酒的小白6 小时前
Kafka 数据写入问题
分布式·kafka
晚风 -6 小时前
SprinBoot整合KafKa的使用(详解)
spring boot·分布式·kafka
济南小草根11 小时前
RabbitMQ学习-Eight
分布式·学习·rabbitmq
LKID体12 小时前
Kafka 消息有序性问题
分布式·kafka