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);
      
          }