「高级文本检索:同义词、补全与纠错」

讲解 ES 高级文本检索:同义词过滤与同义词图、edge_ngram 自动补全、completion suggester、模糊匹配与拼写纠错,以及索引时与查询时处理的取舍。

普通全文检索只能命中词面一致的文档,真实的搜索体验还要求:用户搜「手机」能带出「智能手机」、打错一个字也能找到结果、输入前缀立即出现补全建议。本文讲解同义词、n-gram、completion suggester 与模糊匹配四类能力,并讨论索引时与查询时处理的取舍。

1. 同义词与检索扩展

一句话总结: 同义词把语义等价的词折叠成统一词项,扩大召回而不增加查询复杂度。

1.1 同义词过滤器

{
  "settings": {
    "analysis": {
      "filter": {
        "synonym_filter": {
          "type": "synonym",
          "synonyms": [
            "电脑,计算机,PC => 电脑",
            "手机,智能手机,手机终端"
          ]
        }
      },
      "analyzer": {
        "synonym_analyzer": {
          "type": "custom",
          "tokenizer": "ik_max_word",
          "filter": ["lowercase", "synonym_filter"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "synonym_analyzer",
        "search_analyzer": "synonym_analyzer"
      }
    }
  }
}

synonym filter 有两种写法:箭头形式「=>」只把左侧展开成右侧(收敛),逗号形式则组内互相等价(双向展开)。双向展开在查询时膨胀词项、影响打分,量大的同义词表建议收敛方向。

1.2 索引时与查询时

同义词可以在索引时展开,也可以只在查询时展开。索引时展开会让每个同义词都进倒排索引,段变大、词项变多,后续改同义词表必须重建索引;查询时展开保持索引干净,同义词表可即时生效,代价是每次查询多一步分析。生产上默认查询时展开,除非查询 QPS 极高到无法接受分析开销。

1.3 synonym_graph 处理多词同义词

{
  "settings": {
    "analysis": {
      "filter": {
        "synonym_graph_filter": {
          "type": "synonym_graph",
          "synonyms": ["搜索引擎,检索引擎"]
        }
      }
    }
  }
}

多词同义词(如「搜索引擎」对应「检索引擎」)需要 synonym_graph 而非 synonym:graph 会把同义词展开成多词短语图,配合 match_phrase 才能正确匹配位置关系;普通 synonym 对多词短语生成的词元顺序可能错乱。

2. n-gram 与边缘匹配

一句话总结: n-gram 把词切成连续子串,让部分输入也能命中,尤其适合无空格文本与容错补全。

2.1 edge_ngram 前缀补全

{
  "settings": {
    "analysis": {
      "filter": {
        "autocomplete_filter": {
          "type": "edge_ngram",
          "min_gram": 2,
          "max_gram": 20
        }
      },
      "analyzer": {
        "autocomplete": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": ["lowercase", "autocomplete_filter"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "suggest_name": {
        "type": "text",
        "analyzer": "autocomplete",
        "search_analyzer": "keyword"
      }
    }
  }
}

edge_ngram 从词头切出逐字前缀(如「Elastic」切成 E、El、Ela…),索引时用前缀全量,查询时用原词匹配,实现「输入即补全」。min_gram 设 2 可少存单字噪声,max_gram 决定最长的补全长度。字段必须区分 index 与 search analyzer,否则查询也走 ngram 会逐字匹配造成误召回。

2.2 ngram 子串匹配

{
  "settings": {
    "analysis": {
      "filter": {
        "substring_filter": { "type": "ngram", "min_gram": 2, "max_gram": 4 }
      }
    }
  }
}

普通 ngram 切所有连续子串,能命中词中任意片段,代价是索引体积翻几倍。适合商品编码、邮箱、零件号这类需要子串检索的字段,不适合长正文。索引体积膨胀是 ngram 的固有成本,设计时先估字段长度分布再定 gram 范围。

2.3 前缀查询的替代

{
  "query": {
    "prefix": { "brand": "Nike" }
  }
}

prefix 查询不做分析直接按词项前缀匹配,适合 keyword 字段的轻量前缀过滤。它对索引不做 ngram 预处理,性能随匹配面增大而退化,十万级词项以下可用,大规模补全场景让位给 edge_ngram 与 completion suggester。

3. completion suggester

一句话总结: completion suggester 用内存中的 FST 前缀树做亚毫秒级补全,是搜索框自动补全的标准方案。

3.1 字段与映射

{
  "mappings": {
    "properties": {
      "suggest": {
        "type": "completion",
        "analyzer": "standard",
        "contexts": [
          { "name": "category", "type": "category", "path": "category" }
        ]
      }
    }
  }
}

completion 字段在内存中构建 FST(有限状态转换器)前缀树,查询只走内存结构,速度远快于倒排索引。contexts 给补全加过滤维度(如类目、地区),补全结果只返回匹配上下文的建议。

3.2 写入与补全查询

{
  "index": { "_id": "p1" },
  "suggest": {
    "input": ["Nike Air Max", "Air Max 270"],
    "weight": 80,
    "contexts": { "category": ["shoes"] }
  }
}
{
  "suggest": {
    "product-suggest": {
      "prefix": "nike ai",
      "completion": {
        "field": "suggest",
        "size": 5,
        "contexts": { "category": ["shoes"] }
      }
    }
  }
}

input 是补全源文本,weight 控制排序优先级(越热门的建议权重越高)。prefix 查询只需用户输入前缀,FST 树直接定位候选,补全响应通常在 10ms 内。weight 可由业务热度(点击量、销量)映射而来。

3.3 性能与内存

completion 字段全部驻留堆内存,索引越大内存越高,1GB 规模的补全词典约占数十到数百 MB 堆。写入后段内 FST 才可见,新数据有一定延迟;补全字段更新频繁时考虑独立补全索引或定期重建,避免写放大。FST 不支持子串补全,用户输入词中片段时补全失效,这种情况需要 edge_ngram 方案配合。

4. 模糊匹配与拼写纠错

一句话总结: 模糊匹配按编辑距离容忍错别字,让轻微拼写错误也能召回正确文档。

4.1 fuzzy 查询

{
  "query": {
    "fuzzy": {
      "title": {
        "value": "Elasticseach",
        "fuzziness": "AUTO",
        "prefix_length": 2,
        "max_expansions": 50
      }
    }
  }
}

fuzziness 定义允许的编辑距离:AUTO 按词长自动取值(短词容错低、长词容错高),也可直接写 1 或 2。prefix_length 要求前 N 个字符必须精确,显著缩小候选词项范围。max_expansions 限制展开词项数,防模糊查询扫太多倒排词项拖慢查询。

4.2 编辑距离与中文场景

Levenshtein 编辑距离:
fuzziness=1  允许 1 次插入/删除/替换
fuzziness=2  允许 2 次操作
AUTO         词长 0-2:0, 3-5:1, 6+:2

模糊匹配适合字母语言(英文、拼音)的拼写错误。中文错别字是「字形相近但字不同」,编辑距离模型并不匹配,中文纠错要靠分词后的拼音转换或专门的纠错词典,不能直接依赖 fuzzy。

4.3 fuzzy 与 match 的取舍

{
  "query": {
    "match": {
      "title": {
        "query": "Elasticseach 指南",
        "fuzziness": "AUTO"
      }
    }
  }
}

match 查询直接内嵌 fuzziness,比单独 fuzzy 更常用:既保留 BM25 打分又容忍拼写错误。模糊匹配本质是召回工具,会引入噪声,线上常把 fuzzy 结果降权处理或放在 should 分支作为兜底。

5. phrase suggester 与 did-you-mean

一句话总结: phrase suggester 基于候选短语的编辑距离与打分给出纠错建议,实现「你是不是想找」。

5.1 phrase 补全建议

{
  "suggest": {
    "did-you-mean": {
      "text": "Elasticseach 教程",
      "phrase": {
        "field": "title",
        "size": 3,
        "gram_size": 1,
        "direct_generator": {
          "field": "title",
          "suggest_mode": "missing"
        },
        "confidence": 0.5
      }
    }
  }
}

phrase suggester 逐词生成候选修正并打分,text 是用户原输入,输出替换建议(如 Elasticseach → Elasticsearch)。confidence 是采纳阈值,低于该阈值的结果不返回。前端拿到建议后展示「您是不是要找 Elasticsearch 教程」,用户点击后重发正确查询。

5.2 term suggester 与 completion 的配合

term suggester 给出单词的候选替换词,适合只纠一个词的场景;completion suggester 侧重补全前缀而非纠错。三者用途分明:输入前缀 → completion,轻度错字 → fuzzy 兜底召回,整体纠错建议 → phrase suggester 展示 did-you-mean。

5.3 建议质量

建议质量取决于候选源:词典大、字段文本丰富的建议更准确。shingle filter 把相邻词组合成短语,phrase suggester 配合 shingle 才能生成整短语纠错。冷门词的候选少,建议常落空,用 highlight 或置信度门槛兜底,没把握时不展示建议比展示错误建议更好。

6. 索引时与查询时的取舍

一句话总结: 处理发生在索引时还是查询时,决定索引体积、查询延迟与变更成本。

6.1 取舍对比

能力索引时查询时建议
同义词词项展开、需重建分析期展开、即时生效默认查询时
ngram/edge_ngram必须索引时查询时不生效索引时
completion必须索引时(FST)不可索引时
fuzzy不可查询时展开候选查询时

edge_ngram 与 completion 天然是索引时能力,索引结构即检索结构;同义词与 fuzzy 可以放查询时,代价是查询分析变重。设计时把「结构性强、字典稳定」的部分放索引时,「语义规则变化快」的部分放查询时。

6.2 查询时膨胀控制

查询时展开(同义词、fuzzy、通配)都会让单条查询处理更多词项,QPS 高时被放大成写放大级别的负担。控制手段:同义词图收敛方向、fuzzy 加 prefix_length 与 max_expansions、通配查询限制前缀、必要时对热点查询做结果缓存。

6.3 多字段分工

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "title.completion": {
        "type": "completion",
        "analyzer": "standard"
      },
      "title.keyword": {
        "type": "keyword",
        "ignore_above": 128
      }
    }
  }
}

同一标题拆成三个角色:text 走全文检索,completion 子字段走补全,keyword 子字段走精确排序。索引体积增加但各角色各司其职,是工程上的常见取舍。

7. 综合搜索体验设计

一句话总结: 把补全、纠错、模糊召回组合成「输入即联想、错了也能找、给用户纠错建议」的完整体验。

7.1 三阶段交互

输入前缀 → completion 补全建议(亚毫秒)
回车检索 → match + fuzzy 兜底召回
零结果  → phrase suggester 给出 did-you-mean

搜索框输入时走 completion 返回联想;点击联想或回车走正式检索,match 为主、fuzzy 兜底;检索零结果时调用 phrase suggester 展示纠错建议。三个阶段互不干扰,各自独立接口,前端按状态切换。

7.2 建议与结果的联动

补全建议与检索结果可能不一致:用户点击补全项后,应把补全项的规范文本作为新查询条件,而不是用用户打到一半的输入。建议项排序用 weight(热度)而非字母序,才能让热门商品浮上来。纠错建议只在置信度足够时展示,避免「错误地纠正正确的输入」。

7.3 性能与可观测

补全接口要控制在毫秒级,建议做独立接口独立限流,防热点词刷爆查询。did-you-mean 只在零结果路径触发,QPS 天然低。建议在 Kibana 观察各阶段转化率:补全点击率、模糊召回率、纠错采纳率,用数据迭代权重与词典。

8. 总结

一句话总结: 高级文本检索用同义词扩展召回、edge_ngram 与 completion 做补全、fuzzy 兜底错字、phrase suggester 收口纠错,构成完整的搜索体验。

环节要点
同义词查询时展开、graph 处理多词、表收敛防膨胀
ngramedge_ngram 前缀补全,索引时切、查询时原词
completionFST 内存补全、weight 排序、context 过滤
fuzzy编辑距离兜底召回,AUTO 与 prefix_length 控量
did-you-meanphrase suggester 生成纠错建议,置信度门槛
取舍结构稳定放索引时,语义多变放查询时
体验补全/检索/纠错三阶段联动
红线中文错字不依赖 fuzzy,fuzzy 膨胀控 max_expansions

高级文本检索的价值在把「能搜」升级成「好搜」:用户输入到一半就有联想,输错了也能找到,找不到还有纠错指引。设计顺序是先用 completion 与 edge_ngram 解决补全,再用 fuzzy 兜底召回,最后用 phrase suggester 收口纠错。分词与索引原理可阅读《倒排索引与分词原理》,查询打分参考《Query DSL 与相关性打分》,字段设计参考《数据建模与 Mapping 设计》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 「搜索服务架构:从索引到容错」
  2. 「安全加固与访问控制:从角色到审计」
  3. 「地理空间搜索:从坐标到地图」