基于 NGram 分词,优化 Es 搜索逻辑,并深入理解了 matchPhraseQuery 与 termQuery

本文主要是介绍基于 NGram 分词,优化 Es 搜索逻辑,并深入理解了 matchPhraseQuery 与 termQuery,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

基于 NGram 分词,优化 Es 搜索逻辑,并深入理解了 matchPhraseQuery 与 termQuery

    • 前言
    • 问题描述
    • 排查索引库分词(发现问题)
    • 如何去解决这个问题?
    • IK 分词器
    • NGram 分词器使用
    • 替换 NGram 分词器后进行测试
    • matchPhraseQuery 查询原理
    • termQuery 查询原理
    • 总结

前言

之前不是写过一个全局搜索的功能吗,用户在使用的时候,搜(进出口)关键字,说搜不到数据,但是 Es 中确实是有一条标题为 (202009 进出口)的数据的,按道理来说,这确实要命中的,于是我开始回想我当时是如何写的这段搜索逻辑的代码!!!!

问题描述

之前所有检索的字段全是用的 matchPhraseQuery 查询,matchPhraseQuery 命中的条件其一就是,搜索字段所有的分词都要被 Es 词库命中,其二就是命中的分词在词库中的顺序要紧挨着的。不然就没法查出数据。接下来举例帮助大家理解。

  if (StringUtils.isNotEmpty(articleRequest.getKeyword())) {for (int i = 0; i < articleRequest.getKeys().length; i++) {boolQuery.should(QueryBuilders.matchPhraseQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));}}

使用 kibana 控制台,编写一条 DLS语句,由于 Es 默认使用的分词器是用的 standard,于是查看一下查(进出口)关键字,是被分词成了(进,出,口)

POST _analyze
{"analyzer": "standard","text": "进出口"
}

在这里插入图片描述
一开始建索引的时候,所有字段都没有指定分词器,都是用的默认的 standard 分词器,因此在使用 matchPhraseQuery 的时候,无论是 title 含有(进出口)还是 body 含有(进出口)关键字的数据都能够被正常检索出来,原因就是词库也是按照(进,出,口)存储的,查的关键字也是被分词成(进,出,口)进行匹配词库查询的,所有分词:位置紧挨着、顺序一致、且完全被包含。
在这里插入图片描述
但是后来遇到一个问题就是,搜字母或者是数字,搜不到数据,例如:搜 20 ,但是明明有标题为 (202009 进出口数据 33)的数据,就搜不出来。到这里你会怎么去排查问题?接下来说下我的整个排查问题的流程。

排查索引库分词(发现问题)

基于默认的 standar 分词器查看一下, title 为 (202009 进出口数据33)是如何被分词存到词库中的

POST _analyze
{"analyzer": "standard","text": "202009 进出口数据33"
}

在这里插入图片描述
看了一下 202009 居然没有被分词,而是被当做了一个整体,当我们搜 20 的时候,是按照 20 的这个分词进行查询的,但是索引库中并没有 20 的分词,即不满足查询分词都要被词库包含的关系,更不满足分词顺序和词库保持一致,更不满足命中词库中的分词是紧挨着的条件,三大条件都不满足,能查到才怪呢?怎么去优化搜索逻辑?在这里插入图片描述

如何去解决这个问题?

接下来肯定就是优化索引库中存储的分词结构了,让 title 为( 202009 进出口数据 33) 的这条数据,存储的分词包含 (20),而不是粗略的包含一个(202009),当然你也可以使用 Es 的 模糊查询 wildcard 或者 fuzzy ,考虑到数据量过大,查询性能不咋地,决定优化索引结构,用空间换时间!!!!为什么是空间换时间?存的分词粒度都变细了,意味着存的索引体积变大,这些数据都要硬件来存储的,可不是空间换时间嘛。接下来用主流的 IK 分词器去分下词看满不满足我们的需求

IK 分词器

编写 DLS 语句,对目标数据分词,看到还是没有(20)的分词出现,直接 Pass

POST _analyze
{"analyzer": "ik_max_word","text": "202009进出口数据 33"
}

在这里插入图片描述
对字母分词一样,粒度不满足我们的需求,直接 Pass
在这里插入图片描述

NGram 分词器使用

接下来说本文的主角 NGram 分词器,分词的粒度可以由我们自己控制。在建索引的时候设置一下 Setting 代码都是固定的就好像你使用 Java Api一样,需要注意的是里面的 min_gram 指定最小分词粒度,max_gram 指定最大分词粒度。自定义分词器名字为:my_ngram_analyzer 接来举例说明,这个自定义分词器是干啥的!!!

private static String defaultIndexSetting = "{\n" +"        \"index.max_ngram_diff\":10,\n" +"        \"analysis\": {\n" +"          \"analyzer\": {\n" +"            \"my_ngram_analyzer\": {\n" +"              \"tokenizer\": \"my_ngram_tokenizer\"\n" +"            }\n" +"          },\n" +"          \"tokenizer\": {\n" +"            \"my_ngram_tokenizer\": {\n" +"              \"type\": \"ngram\",\n" +"              \"min_gram\": 1,\n" +"              \"max_gram\": 10,\n" +"              \"token_chars\": [\n" +"                \"letter\",\n" +"                \"digit\"\n" +"              ]\n" +"            }\n" +"          }\n" +"        }\n" +"      }";

由于我只对 title 字段设置了自定义分词器,mapping 如下。

 private static String defaultIndexMapping = "{\n" +"\t\"properties\": {\n" +"\t\t\"author\": {\n" +"\t\t\t\"type\": \"text\",\n" +"\t\t\t\"boost\": \"3\",\n" +"\t\t\t\"fields\": {\n" +"\t\t\t\t\"keyword\": {\n" +"\t\t\t\t\t\"type\": \"keyword\",\n" +"\t\t\t\t\t\"ignore_above\": 256\n" +"\t\t\t\t}\n" +"\t\t\t}\n" +"\t\t},\n" +"\t\t\"body\": {\n" +"\t\t\t\"type\": \"text\",\n" +"\t\t\t\"fields\": {\n" +"\t\t\t\t\"keyword\": {\n" +"\t\t\t\t\t\"type\": \"keyword\",\n" +"\t\t\t\t\t\"ignore_above\": 256\n" +"\t\t\t\t}\n" +"\t\t\t}\n" +"\t\t},\n" +"\t\t\"title\": {\n" +"\t\t\t\"boost\": \"10000\",\n" +"\t\t\t\"type\": \"text\",\n" +"\t\t\t\t\t\t        \"analyzer\": \"my_ngram_analyzer\",\n" +"\t\t\t\"fields\": {\n" +"\t\t\t\t\"keyword\": {\n" +"\t\t\t\t\t\"type\": \"keyword\",\n" +"\t\t\t\t\t\"ignore_above\": 256\n" +"\t\t\t\t}\n" +"\t\t\t}\n" +"\t\t},\n" +"\t\t\"createtime\": {\n" +"\t\t\t\"type\": \"date\",\n" +"\t\t\t\"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"\n" +"\t\t}\n" +"\t}\n" +"}\n";

接下来根据最新的 Setting、Mapping 配置替换之前的旧的索引,然后进行测试

log.info("create index mapping: " + tabIndex.getMapping());CreateIndexRequest indexRequest = new CreateIndexRequest(tabIndex.getIndexName().trim()).settings(tabIndex.getSetting(), XContentType.JSON).mapping("_doc", tabIndex.getMapping(), XContentType.JSON);CreateIndexResponse response = null;try {response = restHighLevelClient.indices().create(indexRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();tabIndexService.delete(new EntityWrapper<TabIndex>().eq("index_name", tabIndex.getIndexName()));return JsonData.buildError("失败" + e.getMessage());}if (response != null) return JsonData.buildSuccess(response.isAcknowledged());else return JsonData.buildError("失败");

替换 NGram 分词器后进行测试

输入关键字:20,发现 title 为 (202009 进出口数据 33) 的这条数据还是查不到???????what fa,再次检查索引库分词,编写 DLS 语句看看,由于创建的新索引的名称是 zza,这里对 zza 索引下面标题包含 (202009 进出口数据 33)的数据进行分词,看看 Es 是如何存的!!!

POST /zza/_analyze
{"field": "title","text": "202009 进出口数据 33"
}

可以看到此时的分词存储了 (2,20,202…)按道理来说查 2 或者 20 或者 202 等等都可以查到这条数据的。难道见鬼啦?于是我决定将代码的生成的 DLS 语句直接 Copy 到 kibana 中跑一下,看到底是代码 Api 的 Bug 还是其他问题。

在这里插入图片描述
于是我就这个 DLS 语句运行了一下,其实不是见鬼了,是我们需要理解一下 termQuery 与 matchPhraseQuery 的查询原理!!!

matchPhraseQuery 查询原理

会将搜索关键字进行分词(这个根据索引用到的分词器一致),然后与词库中的分词进行匹配。例如,现在有一条 title 为(202009 进出口数据 33)的数据,当我们搜 20 的时候,会根据(2,20,0)去匹配词库在这里插入图片描述
但是此时词库是按照(2,20,202…0)这个顺序存的。
再来回顾一下 matchPhraseQuery 命中索引的三大条件

  1. 搜索关键字分词要被词库存的分词完全包含
  2. 在点一的基础上,搜索分词顺序要和词库保持一致
  3. 在前俩点都满足的情况下,词库中匹配到的分词顺序要紧挨着

我们搜关键字 20 时,满足了上述点 1,2。但是不满足点 3,因此使用 matchPhraseQuery 搜不到 title 为(202009 进出口数据 33)的这条数据。那么有什么办法解决吗?答案是有的。就是指定 slop 参数。指定分词紧挨着的最大单位,默认是 1,通过调大这个参数也可以查出来指定数据
在这里插入图片描述
不指定 slop 的情况下查不到数据,但是我现在的需求只要是关键字中包含 20 的数据都要被查到,调 slop 也不是办法,因此 title 字段的搜索不用 matchPhraseQuery,改用 termQuery
在这里插入图片描述

termQuery 查询原理

搜索的关键字不会进行分词去匹配词库,搜 20 就会以 20 去匹配,命中词库中的一个分词即可,例如;现在有一条 title 为(202009 进出口数据 33)的数据,搜关键字 20 即可查出数据,满足现有的业务需求。
在这里插入图片描述

因此最后还改造了一下业务代码逻辑大概是这样,title 字段用 termQuery,其他字段用 matchPhraseQuery。就可以了。

if (StringUtils.isNotEmpty(articleRequest.getKeyword())) {for (int i = 0; i < articleRequest.getKeys().length; i++) {if ("title".equals(articleRequest.getKeys()[i]))boolQuery.should(QueryBuilders.termQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));elseboolQuery.should(QueryBuilders.matchPhraseQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));}boolQuery.minimumShouldMatch(1);
}

总结

matchPhraseQuery 命中条件

  1. 搜索关键字分词要被词库存的分词完全包含
  2. 在点一的基础上,搜索分词顺序要和词库保持一致
  3. 在前俩点都满足的情况下,词库中匹配到的分词顺序要紧挨着
    matchPhraseQuery 在查询前会对关键字进行分词,用到的分词器和索引中该字段指定的分词器一致,例如本文的 title 用到了 NGram 分词器,那么使用如下代码,检索 title 字段时,用到的分词器也是用的 Ngram
QueryBuilders.termQuery("title", articleRequest.getKeyword())

在这里插入图片描述

这篇关于基于 NGram 分词,优化 Es 搜索逻辑,并深入理解了 matchPhraseQuery 与 termQuery的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/346888

相关文章

深入解析 Java Future 类及代码示例

《深入解析JavaFuture类及代码示例》JavaFuture是java.util.concurrent包中用于表示异步计算结果的核心接口,下面给大家介绍JavaFuture类及实例代码,感兴... 目录一、Future 类概述二、核心工作机制代码示例执行流程2. 状态机模型3. 核心方法解析行为总结:三

SpringBoot中HTTP连接池的配置与优化

《SpringBoot中HTTP连接池的配置与优化》这篇文章主要为大家详细介绍了SpringBoot中HTTP连接池的配置与优化的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、HTTP连接池的核心价值二、Spring Boot集成方案方案1:Apache HttpCl

PyTorch高级特性与性能优化方式

《PyTorch高级特性与性能优化方式》:本文主要介绍PyTorch高级特性与性能优化方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、自动化机制1.自动微分机制2.动态计算图二、性能优化1.内存管理2.GPU加速3.多GPU训练三、分布式训练1.分布式数据

MySQL中like模糊查询的优化方案

《MySQL中like模糊查询的优化方案》在MySQL中,like模糊查询是一种常用的查询方式,但在某些情况下可能会导致性能问题,本文将介绍八种优化MySQL中like模糊查询的方法,需要的朋友可以参... 目录1. 避免以通配符开头的查询2. 使用全文索引(Full-text Index)3. 使用前缀索

C#实现高性能Excel百万数据导出优化实战指南

《C#实现高性能Excel百万数据导出优化实战指南》在日常工作中,Excel数据导出是一个常见的需求,然而,当数据量较大时,性能和内存问题往往会成为限制导出效率的瓶颈,下面我们看看C#如何结合EPPl... 目录一、技术方案核心对比二、各方案选型建议三、性能对比数据四、核心代码实现1. MiniExcel

spring IOC的理解之原理和实现过程

《springIOC的理解之原理和实现过程》:本文主要介绍springIOC的理解之原理和实现过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、IoC 核心概念二、核心原理1. 容器架构2. 核心组件3. 工作流程三、关键实现机制1. Bean生命周期2.

MySQL数据库约束深入详解

《MySQL数据库约束深入详解》:本文主要介绍MySQL数据库约束,在MySQL数据库中,约束是用来限制进入表中的数据类型的一种技术,通过使用约束,可以确保数据的准确性、完整性和可靠性,需要的朋友... 目录一、数据库约束的概念二、约束类型三、NOT NULL 非空约束四、DEFAULT 默认值约束五、UN

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

MySQL索引的优化之LIKE模糊查询功能实现

《MySQL索引的优化之LIKE模糊查询功能实现》:本文主要介绍MySQL索引的优化之LIKE模糊查询功能实现,本文通过示例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录一、前缀匹配优化二、后缀匹配优化三、中间匹配优化四、覆盖索引优化五、减少查询范围六、避免通配符开头七、使用外部搜索引擎八、分

深入理解Apache Kafka(分布式流处理平台)

《深入理解ApacheKafka(分布式流处理平台)》ApacheKafka作为现代分布式系统中的核心中间件,为构建高吞吐量、低延迟的数据管道提供了强大支持,本文将深入探讨Kafka的核心概念、架构... 目录引言一、Apache Kafka概述1.1 什么是Kafka?1.2 Kafka的核心概念二、Ka