十Elasticsearch 8.x 分布式搜索引擎 -2

Posted Three-Sides

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了十Elasticsearch 8.x 分布式搜索引擎 -2相关的知识,希望对你有一定的参考价值。

SpringCloud

一、SpringCloud 微服务架构

二、SpringCloud-Eureka 注册中心

三、SpringCloud-Nacos注册中心

四、SpringCloud-Ribbon负载均衡

五、SpringCloud-Feign、OpenFeign 通信

六、SpringCloud-Gateway 网关

七、SpringCloud-Nacos配置管理

八、SpringCloud-RabbitMQ + Spring AMQP 消息队列

九、Elasticsearch 8.x 分布式搜索引擎 -1

上一章介绍了对索引库的存储数据,本章来介绍对索引库的搜索

一、DSL查询文档

1、查询所有

语法:

GET /索引库名/_search

  "query": 
    "查询类型": 
      "查询条件": "条件值"
    
  

查询所有:

  • 查询类型为:match_all
  • 查询条件:没有

2、全文搜索查询

参与搜索的字段也必须是可分词的text类型的字段

2.1、查询类型:match(单字段查询)

语法
GET /索引库名/_search

  "query": 
    "match": 
      "字段名称": "搜索内容"
    
  

2.2、查询类型:multi_match(多字段查询,任意一个字段符合条件就算符合查询条件)

语法
GET /索引库名/_search

  "query": 
    "multi_match": 
      "query": "搜索内容",
      "fields": ["字段名称1", " 字段名称2"]
    
  

3、精准查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。

3.1、查询类型:term (根据词条精确值查询)

语法
GET /索引库名/_search

  "query": 
    "term": 
      "字段名称": 
        "value": "搜索内容"
      
    
  

3.2、查询类型:range(根据值的范围查询)

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

语法:
GET /索引库名/_search

  "query": 
    "range": 
      "字段名称": 
        "gte": 查询值1, // gte代表大于等于,gt则代表大于
        "lte":  查询值2// lte代表小于等于,lt则代表小于
      
    
  

4、地理坐标查询

地理坐标查询,其实就是根据经纬度查询

4.1、查询类型:geo_bounding_box(矩形范围查询)

查询坐标落在某个矩形范围的所有文档:

需要指定矩形的左上右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。

语法
GET /索引库名/_search

  "query": 
    "geo_bounding_box": 
      "字段名称": 
        "top_left":  // 左上点
          "lat": 31.1, // 纬度
          "lon": 121.5 // 经度
        ,
        "bottom_right":  // 右下点
          "lat": 30.9, // 纬度
          "lon": 121.7  // 经度
        
      
    
  

4.2、查询类型:geo_distance(附近查询)

查询到指定中心点小于某个距离值的所有文档

语法
GET /索引库名/_search

  "query": 
    "geo_distance": 
      "distance": "15km", // 半径
      "字段名称"": "31.21,121.5" // 圆心
    
  

5、复合查询

复合查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:

  • fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
  • bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索

5.1、相关性算分

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

在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下:

在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:

TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:

5.2、算分函数查询

语法

function score 查询中包含四部分内容:
  • 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
  • 过滤条件:filter部分,符合该条件的文档才会重新算分
  • 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
    • weight:函数结果是常量
    • field_value_factor:以文档中的某个字段值作为函数结果
    • random_score:以随机数作为函数结果
    • script_score:自定义算分函数算法
  • 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
    • multiply:相乘
    • replace:用function score替换query score
    • 其它,例如:sum、avg、max、min
function score的运行流程如下:
  • 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
  • 2)根据过滤条件,过滤文档
  • 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
  • 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
其中的关键点是:
  • 过滤条件:决定哪些文档的算分被修改
  • 算分函数:决定函数算分的算法
  • 运算模式:决定最终算分结果
示例

需求:给“如家”这个品牌的酒店排名靠前一些

翻译一下这个需求,转换为之前说的四个要点:

  • 原始条件:不确定,可以任意变化
  • 过滤条件:brand = “如家”
  • 算分函数:可以简单粗暴,直接给固定的算分结果,weight
  • 运算模式:比如求和

因此最终的DSL语句如下:

GET /hotel/_search

  "query": 
    "function_score": 
      "query":   .... , // 原始查询,可以是任意条件
      "functions": [ // 算分函数
        
          "filter":  // 满足的条件,品牌必须是如家
            "term": 
              "brand": "如家"
            
          ,
          "weight": 2 // 算分权重为2
        
      ],
      "boost_mode": "sum" // 加权模式,求和
    
  

测试,在未添加算分函数时,如家得分如下:

添加了算分函数后,如家得分就提升了:

6、布尔查询

布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分
需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:
  • 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
  • 其它过滤条件,采用filter查询。不参与算分

语法

GET /hotel/_search

  "query": 
    "bool": 
      "must": [
        "term": "city": "上海" 
      ],
      "should": [
        "term": "brand": "皇冠假日" ,
        "term": "brand": "华美达" 
      ],
      "must_not": [
         "range":  "price":  "lte": 500  
      ],
      "filter": [
         "range": "score":  "gte": 45  
      ]
    
  

示例

需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

分析:

  • 名称搜索,属于全文检索查询,应该参与算分。放到must中
  • 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
  • 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中

二、搜索结果处理

1、排序

elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

1.1、普通字段排序

keyword、数值、日期类型排序的语法基本一致。

语法
GET /索引库名/_search

  "query": 
    "match_all": 
  ,
  "sort": [
    
      "字段名称": "desc"  // 排序字段、排序方式ASC、DESC
    
  ]

示例

需求描述:酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序

1.2、地理坐标排序

语法
GET /indexName/_search

  "query": 
    "match_all": 
  ,
  "sort": [
    
      "_geo_distance" : 
          "FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
          "order" : "asc", // 排序方式
          "unit" : "km" // 排序的距离单位
      
    
  ]

这个查询的含义是:

  • 指定一个坐标,作为目标点
  • 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
  • 根据距离排序

示例:

需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序

提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。

2、分页

elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档

类似于mysql中的limit ?, ?

2.1、普通分页

语法

GET /hotel/_search

  "query": 
    "match_all": 
  ,
  "from": 0, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    "price": "asc"
  ]

2.2、深度分页

查询990~1000的数据,查询逻辑要这么写:

GET /hotel/_search

  "query": 
    "match_all": 
  ,
  "from": 990, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    "price": "asc"
  ]

这里是查询990开始的数据,也就是 第990~第1000条 数据。

不过,elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:

查询TOP1000,如果es是单点模式,这并无太大影响。

但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。

因为节点A的TOP200,在另一个节点可能排到10000名以外了。

因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。

那如果我要查询9900~10000的数据呢?是不是要先查询TOP10000呢?那每个节点都要查询10000条?汇总到内存中?

当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。

针对深度分页,ES提供了两种解决方案,官方文档

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
  • scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。

3、高亮

3.1、高亮原理

高亮显示的实现分为两步:

  • 1)给文档中的所有关键字都添加一个标签,例如<em>标签
  • 2)页面给<em>标签编写CSS样式

3.2、实现高亮

语法

GET /hotel/_search

  "query": 
    "match": 
      "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
    
  ,
  "highlight": 
    "fields":  // 指定要高亮的字段
      "FIELD": 
        "pre_tags": "<em>",  // 用来标记高亮字段的前置标签
        "post_tags": "</em>" // 用来标记高亮字段的后置标签
      
    
  

注意:

  • 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
  • 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
  • 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
    示例:

4、总结

查询的DSL是一个大的JSON对象,包含下列属性:

  • query:查询条件
  • from和size:分页条件
  • sort:排序条件
  • highlight:高亮条件
    示例:

三、ElasticsearchClient查询文档

1、查询所有

1)在EsClient添加queryMatchAll方法实现查询所有

	public <T> List<T>  queryMatchAll(String indexName,Class<T> targetClass) 
        SearchResponse<T> response = null;
        try 
            response=  elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .matchAll(t-> t)
                    ), targetClass
            );
         catch (IOException e) 
            throw new RuntimeException(e);
        
        return handleResponse(response);
 	
	// 封装响应
    private <T> List<T> handleResponse(SearchResponse<T> response) 
        List<T> result = new ArrayList<>();
        if (response == null) 
            return result;
        
        HitsMetadata<T> hitsMetadata = response.hits();
        if (hitsMetadata == null) 
            return result;
        
        List<Hit<T>> hitList = response.hits().hits();
        for (Hit<T> hit : hitList) 
            result.add(hit.source());
        
        return result;
    

2) ESQueryMatchAllTest测试类中,测试查询所有:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryMatchAllTest 

	@Autowired
	private EsClient esClient;

	@Test
	public void queryMatchAllTest() 
		List<HotelDoc> hotelList =esClient.queryMatchAll("hotel",HotelDoc.class);
		for (HotelDoc hotelDoc : hotelList) 
			System.out.println(hotelDoc);
		

	

2、全文搜索查询

2.1 match搜索 单字段搜索

1)在EsClient添加queryMatch方法实现单字段搜索
	public <T> List<T> queryMatch(String indexName, String searchField, String searchContent, Class<T> targetClass) 
        SearchResponse<T> response = null;
        try 
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .match(t -> t
                                    .field(searchField)
                                    .query(searchContent)
                            )
                    ), targetClass
            );
         catch (IOException e) 
            e.printStackTrace();
        
        return handleResponse(response);
    
2) ESQueryMatchTest测试类中,测试单字段搜索:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryMatchTest 

	@Autowired
	private EsClient esClient;

	@Test
	public void queryMatchTest() 

		List<HotelDoc> list = esClient.queryMatch("hotel", "all", "7天连锁酒店", HotelDoc.class);
		for (HotelDoc hotelDoc : list) 
			System.out.println("hotelDoc = " + hotelDoc);
		
	

2.1 mulit_match搜索 多字段搜索

1)在EsClient添加queryMultiMatch方法实现多字段搜索文档
	/**
     * 多字段搜索文档
     *
     * @param indexName     索引名
     * @param searchFields  搜索字段集合
     * @param searchContent 搜索内容
     * @param targetClass   目标类型
     * @param <T>           T
     * @return 目标类集合
     */
    public <T> List<T> queryMultiMatch(String indexName, List<String> searchFields, String searchContent, Class<T> targetClass) 
        SearchResponse<T> response = null;
        try 
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .multiMatch(t -> t
                                    .fields(searchFields)
                                    .query(searchContent)
                            )
                    ), targetClass
            );
         catch (IOException e) 
            e.printStackTrace();
        以上是关于十Elasticsearch 8.x 分布式搜索引擎 -2的主要内容,如果未能解决你的问题,请参考以下文章

elasticsearch之十集群部署及分布式内部机制

Elasticsearch:使用最新的 Nodejs client 8.x 来创建索引并搜索

Elasticsearch:在 Java 客户端应用中管理索引 - Elastic Stack 8.x

Elasticsearch:在 Java 客户端应用中管理索引 - Elastic Stack 8.x

elasticsearch 学习笔记-kibana

干货 | Elasticsearch 8.X 实战视频合集(80 小时+)