Elasticsearch 自定义分词匹配与同义词处理实战详解

一、设计思路

在 Elasticsearch 实际检索场景中,使用原生分词器 + 简单匹配往往会出现匹配精度不足同义词生效层级混乱的问题,比如检索 "数据治理" 时,仅匹配单字导致结果冗余,或同义词多层级扩展引发检索结果偏离原意。

针对以上问题,本文基于 ik_max_word 分词器设计了自定义分词匹配 + 单轮同义词处理的方案,核心设计思路如下,确保检索的精准性与同义词扩展的合理性:

  1. 基于minimum_should_match实现分词 token 的多数匹配,避免少量 token 匹配导致的结果冗余;
  2. 对分词后首 token 与原检索词一致的场景做特殊处理,支持原词精准匹配其余分词 token 多数匹配二选一;
  3. 采用单轮同义匹配原则:若原检索词存在同义词,仅扩展原词的同义词;若原词无同义词,再扩展各分词 token 的同义词,避免同义词多层级扩展;
  4. 同义词扩展仅执行一次,要么基于原词扩展,要么基于分词 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 函数核心功能

  1. 为每个去重后的分词 token 生成基础 Term 查询;
  2. 支持同义词开关,开启时为有同义词的 token 扩展同义 Term 查询;
  3. 首 token 与原词一致且 token 数 > 1 的场景做特殊处理:拆分出原词精准匹配其余 token 多数匹配两个分支;
  4. 为普通场景添加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 关键逻辑解析

  1. token 去重 :入参basicUniqueTokenList为分词后的去重 token 列表,避免重复 token 生成冗余查询;
  2. 同义词扩展 :仅当bSyn=True时才会获取 token 的同义词,并将同义词的 Term 查询加入当前 token 的 inner_should,同义词存储在all_field.text字段,实现跨字段匹配;
  3. 首 token 特殊处理 :以 "数据治理" 为例,分词后为[数据治理, 数据, 治理],首 token 与原词一致,此时查询会拆分为精准匹配 "数据治理"多数匹配 "数据""治理",兼顾精准与模糊;
  4. 字段设计 :原 token 匹配目标字段field.text,同义词匹配全局字段all_field.text,保证同义词的检索范围。

四、自定义分词搜索整体实现(tokenQuery)

基于上述的GetLeafTermDSL函数,实现全局的分词查询 + 单轮同义词处理,这是整个方案的核心入口,实现了 "原词同义词优先" 的单轮扩展原则。

4.1 函数核心功能

  1. 支持同义词总开关,关闭时直接调用GetLeafTermDSL生成基础分词查询;
  2. 开启同义词时,优先获取原词的同义词:若原词有同义词,为原词和每个同义词分别生成分词匹配 DSL;
  3. 原词无同义词时,再调用GetLeafTermDSL并开启bSyn,为分词 token 扩展同义词;
  4. 全程遵循单轮同义原则,无多层级扩展。

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 关键逻辑解析

  1. 异步分词GetBasicToken为异步函数,实际开发中可调用 ES 的_analyzeAPI 实现远程分词,保证分词结果与 ES 端一致;
  2. 原词同义词优先 :开启同义词后,第一步先判断原词是否有同义词,有则直接为原词和同义词分别生成分词查询,不涉及 token 的同义词扩展
  3. token 同义词兜底 :原词无同义词时,才开启GetLeafTermDSLbSyn参数,为每个分词 token 扩展同义词,实现兜底的同义匹配;
  4. 日志埋点:关键节点添加日志,方便问题排查与逻辑追溯。

五、方案使用示例

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 核心优势

  1. 精准性 :基于minimum_should_match实现多数 token 匹配,避免少量 token 匹配的无效结果;首 token 特殊处理兼顾精准匹配与模糊匹配;
  2. 灵活性:支持同义词总开关,可根据业务场景灵活开启 / 关闭;分词与同义词处理解耦,便于单独调整;
  3. 合理性:单轮同义词处理原则避免多层级扩展,保证检索意图不偏离,解决原生同义词过滤器的嵌套扩展问题;
  4. 可扩展性 :代码基于 Python 实现,GetBasicTokentool.GetSynonymList为自定义函数,可快速替换为自研的分词 / 同义词服务。

6.2 适用场景

  1. 企业级检索系统:需要精准匹配且支持同义词扩展的场景,如商品检索、文档检索、日志检索;
  2. 基于 ik_max_word 的细粒度分词场景:需要对分词结果做精细化匹配控制的场景;
  3. 对同义词扩展有 "单轮限制" 的业务:避免同义词多层级扩展导致检索结果偏离原意的场景。

七、注意事项

  1. 字段映射 :需保证目标字段field.textkeyword 类型 (Term 查询基于精确匹配),全局同义词字段all_field.text也需为 keyword 类型,且做了合适的分词与索引;
  2. 同义词服务tool.GetSynonymList为自定义的同义词获取函数,建议基于词库 + 缓存实现,提升性能;同义词词库需定期维护,保证准确性;
  3. 分词一致性 :本地 / 客户端的分词函数GetBasicToken需与 ES 端的分词器(ik_max_word)保持一致,避免分词结果差异导致的匹配问题;
  4. minimum_should_match 配置 :需根据业务场景调整表达式,例如短词(token 数≤3)可配置为100%,长词可配置为5<-1 8<-2,平衡精准性与召回率;
  5. 版本兼容:本文方案基于 Elasticsearch 7.17,低版本(如 6.x)需调整 DSL 的字段语法与查询语法,核心逻辑可复用。
相关推荐
天远云服2 小时前
天远企业司法认证API对接实战:PHP构建B2B供应链合规防火墙
大数据·开发语言·后端·node.js·php
赵谨言2 小时前
基于YOLOv5的植物目标检测研究
大数据·开发语言·经验分享·python
Hello.Reader2 小时前
Flink 应用升级与版本迁移Savepoint、状态兼容、跨版本恢复一次讲透
大数据·chrome·flink
毕设源码-朱学姐3 小时前
【开题答辩全过程】以 基于大数据技术的电商推荐系统的设为例,包含答辩的问题和答案
大数据
远方16093 小时前
115-使用freesql体验Oracle 多版本特性
大数据·数据库·sql·ai·oracle·database
上海蓝色星球4 小时前
造价机器人CER V2.0正式上线!
大数据·人工智能·智慧城市·运维开发
八角Z4 小时前
AI价值跃迁的核心:输出责任转移与新兴工种的精准重塑
大数据·人工智能·科技·机器学习·计算机视觉·服务发现
无忧智库4 小时前
某流域“十五五”国家水网骨干工程智慧水利调度系统项目深度解析:构建数字孪生流域的顶层设计与实施路径(WORD)
大数据
ZKNOW甄知科技4 小时前
深度对标ServiceNow:燕千云如何破解企业全球化运维难题?
大数据·运维·人工智能·科技·ai·自动化·运维开发