一、设计思路
在 Elasticsearch 实际检索场景中,使用原生分词器 + 简单匹配往往会出现匹配精度不足 、同义词生效层级混乱的问题,比如检索 "数据治理" 时,仅匹配单字导致结果冗余,或同义词多层级扩展引发检索结果偏离原意。
针对以上问题,本文基于 ik_max_word 分词器设计了自定义分词匹配 + 单轮同义词处理的方案,核心设计思路如下,确保检索的精准性与同义词扩展的合理性:
- 基于
minimum_should_match实现分词 token 的多数匹配,避免少量 token 匹配导致的结果冗余; - 对分词后首 token 与原检索词一致的场景做特殊处理,支持原词精准匹配 或其余分词 token 多数匹配二选一;
- 采用单轮同义匹配原则:若原检索词存在同义词,仅扩展原词的同义词;若原词无同义词,再扩展各分词 token 的同义词,避免同义词多层级扩展;
- 同义词扩展仅执行一次,要么基于原词扩展,要么基于分词 token 扩展,杜绝同义嵌套。
本方案适用于 Elasticsearch 7.17 版本,分词器默认使用 ik_max_word(细粒度分词),开发语言为 Python。
二、核心概念铺垫
2.1 minimum_should_match
minimum_should_match是 ES bool 查询中 should 子句的核心参数,用于设置最少匹配的子句数量 ,支持灵活的表达式配置(如5<-1 8<-2表示:分词 token 数≤5 时,最少匹配数 - 1;token 数≤8 时,最少匹配数 - 2),能有效控制分词后 token 的匹配粒度,避免少量 token 匹配带来的无效结果。
2.2 单轮同义词处理
区别于原生的同义词过滤器(分词时直接扩展),单轮同义词处理是检索阶段的动态扩展,核心是 "一次扩展即终止":优先判断原词是否有同义词,有则仅基于原词扩展并生成对应分词匹配逻辑;无则再对每个分词 token 做同义词扩展,避免出现 "原词→同义词→同义词的同义词" 的多层级扩展,保证检索意图不偏离。
2.3 ik_max_word 细粒度分词
ik_max_word 会将检索词做最细粒度的拆分 ,例如检索词 "数据治理" 会被拆分为[数据治理, 数据, 治理],细粒度分词为后续的多数 token 匹配提供了基础,能兼顾精准匹配与模糊匹配的需求。
三、叶子节点 Term 匹配实现(GetLeafTermDSL)
叶子节点的 Term 匹配是整个自定义分词查询的基础,核心是为每个分词 token 生成 Term 查询,并结合同义词配置、首 token 特殊规则生成最终的 bool 查询 DSL,同时实现minimum_should_match的灵活配置。
3.1 函数核心功能
- 为每个去重后的分词 token 生成基础 Term 查询;
- 支持同义词开关,开启时为有同义词的 token 扩展同义 Term 查询;
- 对首 token 与原词一致且 token 数 > 1 的场景做特殊处理:拆分出原词精准匹配 和其余 token 多数匹配两个分支;
- 为普通场景添加
minimum_should_match约束,保证多数 token 匹配。
3.2 完整 Python 实现代码
def GetLeafTermDSL(field: str, word: str, basicUniqueTokenList: list, minShouldMatch: str, bSyn: bool = False):
"""
生成叶子节点的Term匹配DSL
:param field: 检索的目标字段名
:param word: 原始检索词
:param basicUniqueTokenList: 检索词经ik_max_word分词后的去重token列表
:param minShouldMatch: 最少匹配数表达式,如"5<-1 8<-2"
:param bSyn: 是否为分词token启用同义词扩展,默认False
:return: 构造好的ES查询DSL
"""
# 存储每个token对应的should子句
shouldList = list()
for token in basicUniqueTokenList:
inner_should = [
{
"term": {
f"{field}.text": {
"value": token
}
}
}
]
# 开启同义词且当前token有同义词时,扩展同义词的term查询
if bSyn:
# tool.GetSynonymList为自定义的同义词获取函数,入参为token和扩展层级,返回同义词列表
token_syn_list = tool.GetSynonymList(token, 1)
if token_syn_list:
for syn_token in token_syn_list:
inner_should.append(
{
"term": {
"all_field.text": {
"value": syn_token
}
}
}
)
# 将当前token的查询加入总should列表
shouldList.append({"bool": {"should": inner_should}})
# 特殊场景:分词token数>1 且 首token与原词完全一致
if len(basicUniqueTokenList) > 1 and word.strip() == basicUniqueTokenList[0].strip():
return {
"bool": {
"should": [
# 原词精准匹配分支
{
"term": {
f"{field}.text": {
"value": word.strip()
}
}
},
# 其余token多数匹配分支,排除首token
{
"bool": {
"should": shouldList[1:],
"minimum_should_match": minShouldMatch
}
}
]
}
}
# 普通场景:所有token多数匹配
else:
return {
"bool": {
"should": shouldList,
"minimum_should_match": minShouldMatch
}
}
3.3 关键逻辑解析
- token 去重 :入参
basicUniqueTokenList为分词后的去重 token 列表,避免重复 token 生成冗余查询; - 同义词扩展 :仅当
bSyn=True时才会获取 token 的同义词,并将同义词的 Term 查询加入当前 token 的 inner_should,同义词存储在all_field.text字段,实现跨字段匹配; - 首 token 特殊处理 :以 "数据治理" 为例,分词后为
[数据治理, 数据, 治理],首 token 与原词一致,此时查询会拆分为精准匹配 "数据治理"和多数匹配 "数据""治理",兼顾精准与模糊; - 字段设计 :原 token 匹配目标字段
field.text,同义词匹配全局字段all_field.text,保证同义词的检索范围。
四、自定义分词搜索整体实现(tokenQuery)
基于上述的GetLeafTermDSL函数,实现全局的分词查询 + 单轮同义词处理,这是整个方案的核心入口,实现了 "原词同义词优先" 的单轮扩展原则。
4.1 函数核心功能
- 支持同义词总开关,关闭时直接调用
GetLeafTermDSL生成基础分词查询; - 开启同义词时,优先获取原词的同义词:若原词有同义词,为原词和每个同义词分别生成分词匹配 DSL;
- 原词无同义词时,再调用
GetLeafTermDSL并开启bSyn,为分词 token 扩展同义词; - 全程遵循单轮同义原则,无多层级扩展。
4.2 完整 Python 实现代码
import asyncio
import logging
# 初始化日志
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
# 自定义分词函数:入参为文本,返回ik_max_word分词后的去重token列表
async def GetBasicToken(text: str, is_uniq: bool = True) -> list:
# 此处为伪代码,需替换为实际的ik_max_word分词逻辑
# 实际开发中可调用ES的分词API或本地ik分词器
from ikanalyzer import IKAnalyzer
ik = IKAnalyzer()
tokens = ik.analyze(text, mode='max_word')
if is_uniq:
tokens = list(set(tokens))
return tokens
async def tokenQuery(field: str, value: str, minShouldMatch: str, bSyn: bool):
"""
自定义分词查询主入口,实现单轮同义词处理
:param field: 检索目标字段
:param value: 原始检索词
:param minShouldMatch: 最少匹配数表达式
:param bSyn: 同义词总开关
:return: 最终的ES检索DSL
"""
# 获取原始检索词的分词token列表
basicUniqueTokenList = await GetBasicToken(value, True)
# 关闭同义词:直接生成基础分词匹配DSL
if not bSyn:
logger.debug("【同义词开关】关闭,仅执行基础分词匹配")
return GetLeafTermDSL(field, value, basicUniqueTokenList, minShouldMatch, False)
# 开启同义词:执行单轮同义词处理逻辑
logger.debug("【同义词开关】开启,执行单轮同义词扩展")
# 获取原词的同义词列表
raw_syn_list = tool.GetSynonymList(value, 1)
# 场景1:原词存在同义词,仅扩展原词的同义词(单轮)
if raw_syn_list:
logger.debug(f"原词[{value}]存在同义词:{raw_syn_list},仅扩展原词同义词")
should_list = list()
# 为原词生成分词匹配DSL
should_list.append(GetLeafTermDSL(
field, value, basicUniqueTokenList, minShouldMatch
))
# 为每个同义词生成分词匹配DSL
for syn_word in raw_syn_list:
syn_token_list = await GetBasicToken(syn_word, True)
should_list.append(GetLeafTermDSL(
field, syn_word, syn_token_list, minShouldMatch
))
return {
"bool": {
"should": should_list
}
}
# 场景2:原词无同义词,为分词token扩展同义词(单轮)
else:
logger.debug(f"原词[{value}]无同义词,为分词token扩展同义词")
return GetLeafTermDSL(field, value, basicUniqueTokenList, minShouldMatch, True)
4.3 关键逻辑解析
- 异步分词 :
GetBasicToken为异步函数,实际开发中可调用 ES 的_analyzeAPI 实现远程分词,保证分词结果与 ES 端一致; - 原词同义词优先 :开启同义词后,第一步先判断原词是否有同义词,有则直接为原词和同义词分别生成分词查询,不涉及 token 的同义词扩展;
- token 同义词兜底 :原词无同义词时,才开启
GetLeafTermDSL的bSyn参数,为每个分词 token 扩展同义词,实现兜底的同义匹配; - 日志埋点:关键节点添加日志,方便问题排查与逻辑追溯。
五、方案使用示例
5.1 基础使用(关闭同义词)
# 调用示例:检索字段为title,检索词为数据治理,最少匹配数5<-1 8<-2,关闭同义词
dsl = asyncio.run(tokenQuery(
field="title",
value="数据治理",
minShouldMatch="5<-1 8<-2",
bSyn=False
))
print(dsl)
生成 DSL 逻辑 :对 "数据治理" 分词为[数据治理, 数据, 治理],因首 token 与原词一致,拆分为原词精准匹配 + 其余 token 多数匹配。
5.2 高级使用(开启同义词)
场景 1:原词有同义词
假设 "数据治理" 的同义词为 "数据管控",调用后会为 "数据治理" 和 "数据管控" 分别生成分词匹配 DSL,无 token 层面的同义词扩展。
场景 2:原词无同义词
假设 "数据治理" 无同义词,而 "数据" 的同义词为 "数仓"、"治理" 的同义词为 "管控",调用后会为 "数据" 扩展 "数仓"、为 "治理" 扩展 "管控",生成对应的 Term 查询。
六、方案优势与适用场景
6.1 核心优势
- 精准性 :基于
minimum_should_match实现多数 token 匹配,避免少量 token 匹配的无效结果;首 token 特殊处理兼顾精准匹配与模糊匹配; - 灵活性:支持同义词总开关,可根据业务场景灵活开启 / 关闭;分词与同义词处理解耦,便于单独调整;
- 合理性:单轮同义词处理原则避免多层级扩展,保证检索意图不偏离,解决原生同义词过滤器的嵌套扩展问题;
- 可扩展性 :代码基于 Python 实现,
GetBasicToken和tool.GetSynonymList为自定义函数,可快速替换为自研的分词 / 同义词服务。
6.2 适用场景
- 企业级检索系统:需要精准匹配且支持同义词扩展的场景,如商品检索、文档检索、日志检索;
- 基于 ik_max_word 的细粒度分词场景:需要对分词结果做精细化匹配控制的场景;
- 对同义词扩展有 "单轮限制" 的业务:避免同义词多层级扩展导致检索结果偏离原意的场景。
七、注意事项
- 字段映射 :需保证目标字段
field.text为keyword 类型 (Term 查询基于精确匹配),全局同义词字段all_field.text也需为 keyword 类型,且做了合适的分词与索引; - 同义词服务 :
tool.GetSynonymList为自定义的同义词获取函数,建议基于词库 + 缓存实现,提升性能;同义词词库需定期维护,保证准确性; - 分词一致性 :本地 / 客户端的分词函数
GetBasicToken需与 ES 端的分词器(ik_max_word)保持一致,避免分词结果差异导致的匹配问题; - minimum_should_match 配置 :需根据业务场景调整表达式,例如短词(token 数≤3)可配置为
100%,长词可配置为5<-1 8<-2,平衡精准性与召回率; - 版本兼容:本文方案基于 Elasticsearch 7.17,低版本(如 6.x)需调整 DSL 的字段语法与查询语法,核心逻辑可复用。