分布式搜索引擎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);
}
}

相关推荐
Elastic 中国社区官方博客1 小时前
Elasticsearch:使用机器学习生成筛选器和分类标签
大数据·人工智能·elasticsearch·机器学习·搜索引擎·ai·分类
清风19811 小时前
kafka消息可靠性传输语义
数据库·分布式·kafka
小诸葛的博客1 小时前
Kafka、RocketMQ、Pulsar对比
分布式·kafka·rocketmq
数据智能老司机3 小时前
CockroachDB权威指南——SQL调优
数据库·分布式·架构
数据智能老司机3 小时前
CockroachDB权威指南——应用设计与实现
数据库·分布式·架构
数据智能老司机3 小时前
CockroachDB权威指南——CockroachDB 模式设计
数据库·分布式·架构
数据智能老司机1 天前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机1 天前
CockroachDB权威指南——开始使用
数据库·分布式·架构
数据智能老司机1 天前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构
IT成长日记1 天前
【Kafka基础】Kafka工作原理解析
分布式·kafka