当前位置: 首页 > news >正文

基于 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

基于 NGram 分词&#xff0c;优化 Es 搜索逻辑&#xff0c;并深入理解了 matchPhraseQuery 与 termQuery 前言问题描述排查索引库分词&#xff08;发现问题&#xff09;如何去解决这个问题&#xff1f;IK 分词器NGram 分词器使用替换 NGram 分词器后进行测试matchPhraseQuery 查…...

PivotNet:Vectorized Pivot Learning for End-to-end HD Map Construction

参考代码&#xff1a;BeMapNet。PS&#xff1a;代码暂未放出&#xff0c;关注该仓库动态 动机和主要贡献 在MapTR系列的算法中将单个车道线建模为固定数量的有序点集&#xff08;对应下图Evenly-based&#xff09;&#xff0c;这样的方式对于普通道路场景具备一定适应性。但是…...

阿里云安全恶意程序检测

阿里云安全恶意程序检测 赛题理解赛题介绍赛题说明数据说明评测指标 赛题分析数据特征解题思路 数据探索数据特征类型数据分布箱型图 变量取值分布缺失值异常值分析训练集的tid特征标签分布测试集数据探索同上 数据集联合分析file_id分析API分析 特征工程与基线模型构造特征与特…...

Xcode中如何操作Git

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是全栈工…...

浅述边缘计算场景下的云边端协同融合架构的应用场景示例

云计算正在向一种更加全局化的分布式节点组合形态进阶&#xff0c;而边缘计算是云计算能力向边缘侧分布式拓展的新触角。随着城市建设进程加快&#xff0c;海量设备产生的数据&#xff0c;若上传到云端进行处理&#xff0c;会对云端造成巨大压力。如果利用边缘计算来让云端的能…...

C++中禁止在栈中实例化的类

C中禁止在栈中实例化的类 栈空间通常有限。如果您要编写一个数据库类&#xff0c;其内部结构包含数 TB 数据&#xff0c;可能应该禁止在栈上实例化它&#xff0c;而只允许在自由存储区中创建其实例。为此&#xff0c;关键在于将析构函数声明为私有的&#xff1a; class Monst…...

MsgPack和Protobuf

MsgPack可以在C下序列化类&#xff0c;Protobuf只能在C#下序列化类 Cocos Creator安装msgpack-lite 项目文件夹执行 rpm -i msgpack-lite...

自定义类型联合体

目录 联合体联合体类型的声明联合体的特点相同成员的结构体和联合体对比联合体大小的计算联合体的应用联合的一个练习 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接 &#x1f412;&#x1f412;&#x1f412; 个人主页 &#x1f978;&#x1f978;&#x1f…...

【Shell 系列教程】Shell printf 命令( 六)

文章目录 往期回顾Shell printf 命令printf 的转义序列 往期回顾 【Shell 系列教程】shell介绍&#xff08;一&#xff09;【Shell 系列教程】shell变量&#xff08;二&#xff09;【Shell 系列教程】shell数组&#xff08;三&#xff09;【Shell 系列教程】shell基本运算符&a…...

2022年电工杯数学建模B题5G网络环境下应急物资配送问题求解全过程论文及程序

2022年电工杯数学建模 B题 5G网络环境下应急物资配送问题 原题再现&#xff1a; 一些重特大突发事件往往会造成道路阻断、损坏、封闭等意想不到的情况&#xff0c;对人们的日常生活会造成一定的影响。为了保证人们的正常生活&#xff0c;将应急物资及时准确地配送到位尤为重要…...

git reflog 恢复git reset --hard 回退的内容

首先使用 git reflog 查看处理的历史&#xff0c;历史是由新到旧排列的&#xff0c;找到回退前的commit的id&#xff0c;找的过程可以只关注HEAD的部分&#xff0c;HEAD括号中的值越大越旧&#xff0c;越小越新。 找到后执行以下命令 git reset --hard 你的commit_id 然后…...

kali Linux中更换为阿里镜像源

准备&#xff1a; kali Linux 阿里源链接 deb kali安装包下载_开源镜像站-阿里云 kali-rolling main non-free contrib deb-src kali安装包下载_开源镜像站-阿里云 kali-rolling main non-free contrib 配置&#xff1a; 打开kali 终端输入&#xff1a;sudo nano /etc/apt…...

【每日一题】移除链表元素(C语言)

移除链表元素&#xff0c;链接奉上 目录 思路&#xff1a;代码实现&#xff1a;链表题目小技巧&#xff1a; 思路&#xff1a; 在正常情况&#xff1a; 下我们移除链表元素时&#xff0c;需要该位置的前结点与后节点&#xff0c; 在特别情况时&#xff1a; 例如 我们发现&…...

stm32 ADC

目录 简介 stm32的adc 框图 ①电压输入范围 ②输入通道 ​编辑③ADC通道 ④ADC触发 ⑤ADC中断 ⑥ADC数据 ⑦ADC时钟 ADC的四种转换模式 hal库代码 标准库代码 简介 自然界的信号几乎都是模拟信号&#xff0c;比如光亮、温度、压力、声音&#xff0c;而为了方便存储、…...

linux网络服务综合项目

前期环境配置 #主要写了192.168.146.130的代码&#xff0c;131的配置代码和其一样 [rootserver ~]# nmtui #通过图形化界面修改ens160的ip 192.168.146.130 [rootserver ~]# hostnamectl set-hostname Server-Web #修改130主机名…...

每日一题(LeetCode)----数组--移除元素(三)

每日一题(LeetCode)----数组–移除元素&#xff08;三&#xff09; 1.题目&#xff08;[283. 移动零](https://leetcode.cn/problems/sqrtx/)&#xff09; 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请…...

AI:57-基于机器学习的番茄叶部病害图像识别

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌在这个漫长的过程,中途遇到了不少问题,但是…...

人工智能-深度学习计算:层和块

我们关注的是具有单一输出的线性模型。 在这里&#xff0c;整个模型只有一个输出。 注意&#xff0c;单个神经网络 &#xff08;1&#xff09;接受一些输入&#xff1b; &#xff08;2&#xff09;生成相应的标量输出&#xff1b; &#xff08;3&#xff09;具有一组相关 参数…...

Linux第一个小程序进度条

缓冲区 ​ 在写进度条程序之前我们需要介绍一下缓冲区&#xff0c;缓冲区有两种&#xff0c;输入和输出缓冲区&#xff0c;这里主要介绍输出缓冲区。在我们用C语言写代码时&#xff0c;输出一些信息&#xff0c;实际上是先输出到输出缓冲区里&#xff0c;然后才输出到我们的显…...

JavaEE平台技术——预备知识(Maven、Docker)

JavaEE平台技术——预备知识&#xff08;Maven、Docker&#xff09; 1. Maven2. Docker 在观看这个之前&#xff0c;大家请查阅前序内容。 &#x1f600;JavaEE的渊源 &#x1f600;&#x1f600;JavaEE平台技术——预备知识&#xff08;Web、Sevlet、Tomcat&#xff09; 1. M…...

后进先出(LIFO)详解

LIFO 是 Last In, First Out 的缩写&#xff0c;中文译为后进先出。这是一种数据结构的工作原则&#xff0c;类似于一摞盘子或一叠书本&#xff1a; 最后放进去的元素最先出来 -想象往筒状容器里放盘子&#xff1a; &#xff08;1&#xff09;你放进的最后一个盘子&#xff08…...

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

【JavaEE】-- HTTP

1. HTTP是什么&#xff1f; HTTP&#xff08;全称为"超文本传输协议"&#xff09;是一种应用非常广泛的应用层协议&#xff0c;HTTP是基于TCP协议的一种应用层协议。 应用层协议&#xff1a;是计算机网络协议栈中最高层的协议&#xff0c;它定义了运行在不同主机上…...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

GitHub 趋势日报 (2025年06月08日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...

力扣-35.搜索插入位置

题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...

处理vxe-table 表尾数据是单独一个接口,表格tableData数据更新后,需要点击两下,表尾才是正确的

修改bug思路&#xff1a; 分别把 tabledata 和 表尾相关数据 console.log() 发现 更新数据先后顺序不对 settimeout延迟查询表格接口 ——测试可行 升级↑&#xff1a;async await 等接口返回后再开始下一个接口查询 ________________________________________________________…...

Go 并发编程基础:通道(Channel)的使用

在 Go 中&#xff0c;Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式&#xff0c;用于在多个 Goroutine 之间传递数据&#xff0c;从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...

多模态图像修复系统:基于深度学习的图片修复实现

多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...