05-ElasticSearch高级搜索

2、ElasticSearch高级搜索

  • Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型如下所示
    • ①、查询所有
      • 查询出所有数据,一般测试用;例如
        • match_all
        • 如下图所示
    • ②、全文检索(full text)查询
      • 利用分词器对用户输入内容分词,然后去倒排索引库中匹配,例如
        • match_query
        • multi_match_query
    • ③、精确查询
      • 根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型的字段,例如
        • ids
        • range
        • term
    • ④、地理(geo)查询
      • 根据经纬度查询,例如
        • geo_distance
        • geo_bounding_box
    • ⑤、复合(compound)查询
      • 复合查询可以将上述各种查询条件组合起来,合并查询条件,例如
      • bool
      • function_score

2.1、全文检索查询

2.1.1、使用场景

  • 全文检索查询的基本流程如下所示
    • ①、对用户搜索的内容做分词,得到词条
    • ②、根据词条去倒排索引库中匹配,得到文档id
    • ③、根据文档id找到文档,把所有匹配结果以并集或交集返回给用户
  • 比较常用的场景包括
    • 商城的输入框搜索
    • 百度搜索框搜索
  • 因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段

2.1.2、DSL语句格式

  • 常见的全文检索查询包括

    • match查询:单字段查询
    • multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
  • match查询语法如下所示

    • GET /indexName/_search
      {
      "query": {
      "match":{
      "FIELD": "TEXT"
      }
      }
      }
  • match_all查询语法如下

    • GET /indexName/_search
      {
      "query": {
      "multi_match": {
      "query": "TEXT",
      "fileds": ["FILED1", "FILED2"]
      }
      }
      }

2.1.3、match查询DSL语句示例&&RestAPI示例

①、DSL语句

  • 比如要搜索name字段中存在 如家酒店,DSL语句如下所示

    • GET hotel/_search
      {
      "query": {
      "match": {
      "name": "如家酒店"
      }
      },
      "size": 2 # size的意思是只显示n条数据
      }
  • 搜索结果如下所示

  • 结果分析

    • 因为name字段是类型是text,搜索的时候会对这个字段进行分词

    • 如搜索如家酒店,那么就会分词称为如家酒店相当于会搜索三次,并取这三次搜索的并集(ES默认的是并集),所以搜索的命中率才会如此之高

      • 通俗的来说
        • 并集就相当于搜索到name like %如家%算一条数据,搜索到酒店也算一条数据
        • 那么交集就跟它相反,必须是name like %如家酒店%才能算是一条数据
    • 那么如何取交集呢?,如下所示

      • DSL

        • # 取交集,并集是or
          GET hotel/_search
          {
          "query": {
          "match": {
          "name": {
          "query": "如家酒店",
          "operator": "and"
          }
          }
          }
          }
      • 运行结果

②、RestAPI

math_all
  • 代码如下所示

    • package com.coolman.hotel.test;
      import com.coolman.hotel.pojo.HotelDoc;
      import com.fasterxml.jackson.core.JsonProcessingException;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import org.apache.lucene.search.TotalHits;
      import org.elasticsearch.action.search.SearchRequest;
      import org.elasticsearch.action.search.SearchResponse;
      import org.elasticsearch.client.RequestOptions;
      import org.elasticsearch.client.RestHighLevelClient;
      import org.elasticsearch.index.query.MatchAllQueryBuilder;
      import org.elasticsearch.index.query.QueryBuilder;
      import org.elasticsearch.index.query.QueryBuilders;
      import org.elasticsearch.search.SearchHit;
      import org.elasticsearch.search.SearchHits;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import java.io.IOException;
      @SpringBootTest
      public class FullTextSearchDemo {
      // 注入 RestHighLevelClient对象
      @Autowired
      private RestHighLevelClient restHighLevelClient;
      // jackson
      private final ObjectMapper objectMapper = new ObjectMapper();
      /**
      * 查询所有测试
      */
      @Test
      public void testMatchAll() throws IOException {
      // 1. 创建一个查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel"); // 指定索引
      // 2. 添加查询的类型
      MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
      searchRequest.source().query(matchAllQueryBuilder); // source就相当于{}
      searchRequest.source().size(100); // RestAPI默认返回的是10条数据,可以更改size的属性,即可自定义返回的数据量
      // 3. 发出查询的请求,得到响应结果
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理响应的结果
      handlerResponse(response);
      }
      /**
      * 用来处理响应数据(相当于解析返回的JSON数据)
      * @param response
      */
      private void handlerResponse(SearchResponse response) throws JsonProcessingException {
      // 1. 得到命中的数量(即总记录数量)
      SearchHits hits = response.getHits();
      long totalCount = hits.getTotalHits().value;// 总记录数
      System.out.println("总记录数量为:" + totalCount);
      // 2. 获取本次查询出来的列表数据
      SearchHit[] hitsArray = hits.getHits();
      for (SearchHit hit : hitsArray) {
      // 得到json字符串
      String json = hit.getSourceAsString();
      // 将json字符串转换为实体类对象
      HotelDoc hotelDoc = objectMapper.readValue(json, HotelDoc.class);
      System.out.println(hotelDoc);
      }
      }

      }

      ​~~~

match
  • 代码如下所示

    • /**
      * 单字段查询
      */
      @Test
      public void testMatch() throws IOException {
      // 1. 创建查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加查询的类型
      MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", "如家酒店");
      searchRequest.source().query(matchQueryBuilder);
      // 3. 发出查询请求,得到响应数据
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理响应的结果
      handlerResponse(response);
      }

自行运行查看结果即可

2.1.4、multi_match查询DSL语句示例&&RestAPI示例

DSL语句

  • 比如搜索namebrand字段中出现如家酒店的数据

    • DSL语句如下所示

      • GET hotel/_search
        {
        "query": {
        "multi_match": {
        "query": "如家酒店",
        "fields": ["name", "brand"]
        }
        }
        }
    • 运行结果如下所示

    • 不过多字段查询的使用很少,因为多字段查询会使得查询效率变慢

    • 一般都会在创建映射的时候,使用copy_to将指定字段的值拷贝到另一个字段,如自定义的all字段

    • 这样子就可以使用单字段查询,提高查询效率

RestAPI

跟单字段查询差不多,只不过使用QueryBuilders创建的对象略有不同罢了

  • 代码如下所示

    • /**
      * 多字段查询
      */
      @Test
      public void testMultiMatch() throws IOException {
      // 1. 创建查询请求球体对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加要查询的字段
      // MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("如家酒店", "name", "brand", "bussiness");
      // searchRequest.source().query(multiMatchQueryBuilder);
      // 因为在创建映射的时候使用了copy_to,索引上面的多字段查询等价于下面的单字段查询
      MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("all", "如家酒店");
      searchRequest.source().query(matchQueryBuilder);
      // 3. 执行查询操作,得到响应对象
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理响应对象
      handlerResponse(response);
      }

2.2、精准查询

2.2.1、使用场景

  • 精确查询一般是查找keyword、数值、日期、boolean等类型的字段,所以不会对搜索条件分词,常见的有如下
    • term
      • 根据词条精确值查询,相当于equals=
    • range
      • 根据值的范围查询,相当于>=<=betweenand

2.2.2、DSL语句格式

①、term查询

  • 因为精确查询的字段搜索的是不分词的字段,因此查询的条件也必须是不分词的词条

  • 查询的时候,用户输入的内容跟自动值完全匹配的时候才认为符合条件

  • 如果用户输入的内容过多,反而搜索不到数据

  • 语法说明

    • # term 精确查询
      GET /indexName/_search
      {
      "query": {
      "term": {
      "FILED": {
      "value": "VALUE"
      }
      }
      }
      }
  • 示例

    • 输入精确词条
    • 输入精确词条

②、range查询

  • 范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤

  • 基本语法

    • # range 精确查询
      # gte表示大于等于;gt表示大于
      # lte表示小于等于;lt表示小于
      GET /indexName/_search
      {
      "query": {
      "range": {
      "FIELD": {
      "gte": 10,
      "lte": 20
      }
      }
      }
      }
  • 示例

    • 查询price大于等于200,小于等于500的酒店

2.2.3、RestAPI

  • term查询

    • 代码如下所示

    • /**
      * term 精确查询
      */
      @Test
      public void testTermQuery() throws IOException {
      // 1. 创建查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加要查询的字段
      TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", "如家");
      searchRequest.source().query(termQueryBuilder);
      // 3. 发出查询的请求,获取响应结果
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理响应的结果
      handlerResponse(response);
      }
  • range查询

    • 代码如下所示

    • /**
      * range 精确查询
      */
      @Test
      public void testRangeQuery() throws IOException {
      // 1. 创建查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加查询的字段
      RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
      rangeQuery.gte(300); // 大于等于300
      rangeQuery.lte(500); // 小于等于500
      searchRequest.source().query(rangeQuery);
      // 3. 执行查询操作,获取响应结果
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理响应结果
      handlerResponse(response);
      }

2.3、地理坐标查询

2.3.1、使用场景

2.3.2、DSL语句格式

①、矩形范围查询

  • 矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档

  • 查询的时候,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点,如下所示

  • 语法如下所示

    • # 地理位置查询(矩形查询)
      GET hotel/_search
      {
      "query": {
      "geo_bounding_box": {
      "location": {
      "top_left": {
      "lat": 31.1,
      "lon": 121.5
      },
      "bottom_right": {
      "lat": 30.9,
      "lon": 121.7
      }
      }
      }
      }
      }
  • 示例

②、附近查询

  • 附近查询,也叫做距离查询(geo_distance)

    • 查询到指定中心小于某个距离值的所有文档
  • 换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件,如下所示

  • 语法如下所示

    • GET hotel/_search
      {
      "query": {
      "geo_distance": {
      "distance": "15km",
      "location": "31.21,121.5"
      }
      }
      }
  • 示例

2.3.3、RestAPI

①、矩形范围查询

  • 代码如下所示

    • /**
      * 地理坐标矩形查询
      */
      @Test
      public void testGeoBoundingBoxSearch() throws IOException {
      // 1. 创建查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加要查询的字段
      // 指定要查询的字段为 location
      GeoBoundingBoxQueryBuilder geoBoundingBoxQueryBuilder = QueryBuilders.geoBoundingBoxQuery("location");
      // 指定 topLeft的坐标
      geoBoundingBoxQueryBuilder.topLeft().resetLat(31.1);
      geoBoundingBoxQueryBuilder.topLeft().resetLon(121.5);
      // 指定 bottom_right的坐标
      geoBoundingBoxQueryBuilder.bottomRight().resetLat(30.9);
      geoBoundingBoxQueryBuilder.bottomRight().resetLon(121.7);
      searchRequest.source().query(geoBoundingBoxQueryBuilder);
      // 3. 发起请求
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理返回的数据
      handlerResponse(response);
      }

②、附近查询

  • 代码如下所示

    • /**
      * 地理坐标附近查询(圆形)
      */
      @Test
      public void testGeoDistanceSearch() throws IOException {
      // 1. 创建查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加要查询的字段
      // 指定要查询的字段是 location
      GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders.geoDistanceQuery("location");
      // 指定中心点坐标
      geoDistanceQueryBuilder.point(new GeoPoint(31.21, 121.5));
      // 指定要查询的范围距离
      geoDistanceQueryBuilder.distance("15km");
      searchRequest.source().query(geoDistanceQueryBuilder);
      // 3. 发起查询请求
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理返回的数据
      handlerResponse(response);
      }

2.4、复合查询之布尔查询

2.4.1、使用场景

  • 布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询,子查询的组合方式有如下几种
    • ①、must必须匹配每个子查询,类似"与"(and),must的条件参与算分
    • ②、should选择性匹配子查询,类似"或"(or)
    • ③、must_not:必须匹配,不参与算分,类似"非"(not)
    • ④、filter:效果和must一样,都是and。必须匹配,filter的条件不参与算分
  • 常见的应用场景
    • 比如在搜索酒店的时候,除了关键字搜索以外,我们还可能根据品牌、价格、城市等字段过滤
    • 每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须使用bool查询
  • 注意事项
    • 不过需要注意的是,搜索的时候,参与算分的字段越多,查询的性能也越差;因此这种多条件查询的时候,可以按照如下类似方法解决
      • 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
      • 其他过滤条件,采用filter查询,不参与算分

2.4.2、DSL语句格式

  • DSL语句如下所示

    • GET hotel/_search
      {
      "query": {
      "bool": {
      "must": [
      {
      "term": {
      "city": {
      "value": "上海"
      }
      }
      }
      ],
      "should": [
      {
      "term": {
      "brand": {
      "value": "皇冠假日"
      }
      }
      },
      {
      "term": {
      "brand": {
      "value": "华美达"
      }
      }
      }
      ],
      "must_not": [
      {
      "range": {
      "price": {
      "lte": 500
      }
      }
      }
      ],
      "filter": {
      "range": {
      "score": {
      "gte": 45
      }
      }
      }
      }
      }
      }
    • 这个DSL语句的意思通俗来说就是

      • ①、城市必须是上海
      • ②、品牌可以是皇冠假日或者华美达
      • ③、价格必须小于等于500
      • ④、得分必须大于等于45
  • 示例

    • 需求如下所示

      • 搜索名字包含"如家酒店",价格不高于400,在坐标31.21,121.5,周围10km范围的酒店
    • 分析

      • ①、名称搜索,属于全文检索查询,应该参与算分
      • ②、价格不高于400,用range过滤查询,不参与算分(可以放到must_not中,当然也可以放到filter中,使用lte表示小于等于400)
      • ③、周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分,放到filter
    • DSL语句如下所示

      • 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
        }
        }
        }
        }
        }
        }
      • PS:在kibana中编写filter中的坐标信息的时候自动补全有些bug,kibana会报错distacne_unitlocation不能共存,所以应该把这个单位删除,然后在distance字段上添加双引号的同时带上单位

2.4.3、RestAPI

  • 代码如下所示

    • /**
      * 复合查询之布尔查询
      */
      @Test
      public void testBooleanQuery() throws IOException {
      // 1. 创建查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加要查询的字段
      BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
      // must
      MatchQueryBuilder brand = QueryBuilders.matchQuery("name", "如家酒店");
      boolQueryBuilder.must(brand);
      // must_not
      RangeQueryBuilder price = QueryBuilders.rangeQuery("price").gt(400);
      boolQueryBuilder.mustNot(price);
      // filter
      GeoDistanceQueryBuilder location = QueryBuilders.geoDistanceQuery("location").point(new GeoPoint(31.21, 121.5)).distance("10km");
      boolQueryBuilder.filter(location);
      searchRequest.source().query(boolQueryBuilder);
      // 3. 执行查询,得到响应数据
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理响应数据
      handlerResponse(response);
      }

2.5、复合查询之算分函数查询

2.5.1、使用场景

  • 当我们使用match查询的时候,文档结果会根据搜索词条的关联度打分(_score),返回结果时按照分值降序排序
    • 可以自行查询查看验证
  • 在Elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下
    • TF(词条频率):描述某一词在一篇文档中出现的频繁程度。出现越多,分值越高,反之,分值月底
    • IDF(逆文档频率):通过公式可以看到,词条出现的文档数量越多,分值越低,反之越高
  • 在后来的5.1版本升级后,Elasticsearch将算法改进为BM25算法,公式如下
  • TF-IDF算法有一个缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算法有一个上限,曲线更加平滑
  • 根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的
  • 以某度为例,在搜索的结果中,并不是相关度越高,排名越靠前;而是谁掏的钱多,排名就越靠前
  • 要想人为控制相关性算分,就需要利用Elasticsearch中的function score查询

2.5.2、DSL语句格式

  • 可以通过下图来理解算分函数查询的DSL语句基本格式
  • function score 查询中包含四部分内容
    • ①、原始查询条件
      • query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
    • ②、过滤条件
      • filter部分,符合该条件的文档才会重新算分
    • ③、算分函数
      • 符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
        • weight:函数结果是常量
        • field_value_factor:以文档中的某个字段值作为函数结果
        • random_score:以随机数作为函数结果
        • script_score:自定义算分函数算法
    • ④、运算模式
      • 算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括
        • multiply:相乘
        • replace:用function score替换query score
        • 其它,例如:sumavgmaxmin
  • function score 的运行流程如下所示
    • a. 根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
    • b. 根据过滤条件,过滤文档
    • c. 符合过滤条件的文档,基于算分函数的运算,得到函数算分(function score)
    • d. 将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分
  • 因此,其中的关键点是
    • 过滤条件:决定哪些文档的算分被修改
    • 算分函数:决定函数算分的算法
    • 运算模式:决定最终算分结果

需求

  • 让"如家"这个品牌的酒店排名靠前一点

  • 这个需求很简单,可以理解为如下几部分

    • ①、原始条件:不确定,可以任意变化
    • ②、过滤条件:brand = "如家"
    • ③、算分函数:可以简单粗暴,直接使用weight给固定的算分结果
    • ④、运算模式:比如求和
  • 因此DSL语句如下所示

    • GET hotel/_search
      {
      "query": {
      "function_score": {
      "query": {
      "match": {
      "name": "酒店"
      }
      },
      "functions": [
      {
      "filter": {
      "term": {
      "brand": "如家"
      }
      },
      "weight": 10
      }
      ],
      "boost_mode": "sum"
      }
      }
      }
  • 结果如下所示

  • 原始搜索结果如下所示

2.5.3、RestAPI

  • 代码如下所示,可以对照着DSL语句进行编写

    • /**
      * 复合查询之算分函数查询
      */
      @Test
      public void testFunctionScoreQuery() throws IOException {
      // 1. 创建查询请求对象
      SearchRequest searchRequest = new SearchRequest("hotel");
      // 2. 添加查询的请求体
      searchRequest.source().query( // query
      QueryBuilders.functionScoreQuery( // function_score
      QueryBuilders.matchQuery("name", "酒店"), // match
      new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // functions
      new FunctionScoreQueryBuilder.FilterFunctionBuilder( // filter
      QueryBuilders.termQuery("brand", "如家"), // term
      ScoreFunctionBuilders.weightFactorFunction(10) // weight
      )
      }
      ).boostMode(CombineFunction.SUM) // boost_mode
      );
      // 3. 执行查询,获取响应数据
      SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
      // 4. 处理响应数据
      handlerResponse(response);
      }
posted @   OnlyOnYourself-Lzw  阅读(382)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示