AIGC标识 商标近似检索的 Elasticsearch Bool 查询优化

1. 背景

商标近似检索同时承担“尽量不漏”和“响应足够快”两个目标。早期实现把精确、包含、倒序、弱相似、拼音等规则一次性展开,并混合使用 regexp、fuzzy、match_phrase 和宽范围过滤。这样的查询表达能力很强,但在商标数据量达到几十万甚至更高时,会产生大量候选文档,后续 scroll 和 Java 筛选成为主要耗时来源。

本次优化的核心思想是:ES 只做可利用倒排索引的粗召回,Java 负责精细规则判断;先用字符重合度和标题长度把候选集缩小。

2. 原查询的主要问题

2.1 复杂查询算子成本高

  • regexp 需要对词项进行模式匹配,难以像 term 一样直接利用简单倒排查找。
  • fuzzy 会扩展编辑距离候选词,扩展规模不可控。
  • match_phrase + slop 需要计算词项位置和顺序,数据量大时明显增加查询成本。
  • 多个规则分支叠加后,即使最终只返回少量结果,ES 仍可能先产生大量中间候选。

2.2 全局长度过滤不够精确

旧逻辑通常是对整个查询统一设置标题长度范围,例如:

titleCharNum > 2 && titleCharNum <= queryLength * 2

它无法表达“命中 2 个字符”和“命中 4 个字符”应有不同长度约束的问题,容易把低相关长标题放入候选集。

2.3 仅依赖去重后的字符会改变匹配门槛

查询词中可能存在重复字符。查询词长度和去重后的字符数不是同一个概念,因此需要明确 minimum-should-match 的计算口径,避免重复字符导致门槛失真。

3. 新的中文粗召回模型

设查询词归一化后包含 N 个不同汉字。对每一个 K 生成一个独立的 Bool 分支:

K = N, N-1, ..., 2

每个分支包含:

  1. 对每个不同汉字生成一个 term(similarTitle, char);
  2. 设置 minimum_should_match = K;
  3. 在分支内部增加标题长度过滤。

长度规则为:

K >= 3: K <= titleCharNum <= 2K
K = 2: titleCharNum = 2

单字符查询作为边界情况使用 minimum_should_match=1,标题长度限制为 1~2。

外层 Bool 只要求命中一个分支:

outer.minimum_should_match = 1

示例:查询词“牧歌哚哆”

去重后有 4 个字符,因此生成 3 个分支:

分支 命中门槛 标题长度
K=4 至少命中 4 个字符 4~8
K=3 至少命中 3 个字符 3~6
K=2 至少命中 2 个字符 2

“牧歌朵多”只命中“牧、歌”两个汉字,不能通过中文字面分支;这类顺序变化或同音变化应交给独立的拼音/音形规则处理,而不是降低中文 term 分支的门槛。

4. 生成的 ES 查询结构

以查询词 ABC 为例,核心结构近似如下:

{
  "bool": {
    "should": [
      {
        "bool": {
          "should": [
            { "term": { "similarTitle": "A" } },
            { "term": { "similarTitle": "B" } },
            { "term": { "similarTitle": "C" } }
          ],
          "minimum_should_match": 3,
          "filter": [
            { "range": { "titleCharNum": { "gte": 3, "lte": 6 } } }
          ]
        }
      },
      {
        "bool": {
          "should": [
            { "term": { "similarTitle": "A" } },
            { "term": { "similarTitle": "B" } },
            { "term": { "similarTitle": "C" } }
          ],
          "minimum_should_match": 2,
          "filter": [
            { "range": { "titleCharNum": { "gte": 2, "lte": 2 } } }
          ]
        }
      }
    ],
    "minimum_should_match": 1
  }
}

实际请求还会叠加申请人、法律状态、商品分类等业务过滤条件。所有长度过滤放在对应分支内部,避免一个全局范围覆盖所有相似度等级。

5. 为什么不用单字 term 一次性召回再全部交给 Java

“命中任意一个字符”虽然写法简单,但会产生大量噪声。例如查询词 ABC 使用 minimum_should_match=1 时,包含单个 A 的长标题也会进入候选集;查询词越长,噪声增长越快,最终把 ES 的工作转移到 scroll 和 Java 去重/筛选,整体耗时反而更高。

新方案把最低重合度放在 ES:

  • 短词保持较高精度;
  • 中长词至少要求多个字符重合;
  • 标题长度与命中数量绑定,减少“短查询匹配超长标题”的情况;
  • Java 只处理已经缩小的候选集。

6. 英文和拼音策略

6.1 英文

英文按空格拆分为单词并转为大写,保留现有 token 召回逻辑。标题长度使用字符数范围,允许查询字符数的 1~3 倍,例如:

APPLE -> titleCharNum 5~15

英文单词数量和字符数量是两个维度,不能直接套用中文单字规则。

6.2 拼音

当前 similarTitlePinyin 是 keyword 字段,例如:

"similarTitlePinyin": {
  "type": "keyword"
}

因此整值 mu ge duo duo 是一个完整 term,不能通过 term(similarTitlePinyin, "mu") 匹配。此次优化暂不修改拼音索引,避免查询逻辑和索引重建同时变更。后续若要支持按音节召回,应新增分词字段或预生成拼音 n-gram,并完成全量重建后再启用对应 Bool 分支。

7. 性能收益与复杂度

对于 N 个不同汉字,中文粗召回最多生成 N-1 个分支,每个分支包含 N 个 term。查询复杂度大致为:

O(N × (N - 1))

这比无边界的 regexp/fuzzy 扩展更可预测。长度过滤使用数值字段 titleCharNum,可以作为 filter 缓存和剪枝条件,不参与相关性评分。

但该方案不是无限扩展的:超长查询词仍会生成较多分支。生产环境建议增加输入长度上限,例如超过 10~12 个字符时只保留高门槛分支,或先提取核心词再召回。

8. 验证与监控建议

上线前建议固定以下样本进行对比:

  • 2 字、3 字、4 字中文查询;
  • 含重复字的查询;
  • 顺序颠倒、增字、减字样本;
  • 中英文混合和纯英文查询;
  • 无结果和高频字查询。

重点记录:

  1. query build 耗时;
  2. 首次 ES search 耗时;
  3. scroll 总耗时;
  4. ES 命中总量和实际返回量;
  5. Java 规则筛选耗时。

如果 scroll 仍然很慢,优先检查候选总量,而不是继续增加更多 ES 规则。近似检索的优化顺序应是:减少候选集 → 缩小 scroll → 再优化 Java 精筛。

9. 总结

本次优化没有追求在 ES 中一次性表达所有“近似”语义,而是采用分层粗召回:

  • term 替代高成本的 regexp、fuzzy 和 match_phrase;
  • minimum_should_match 控制字符重合度;
  • titleCharNum 将标题长度和命中门槛绑定;
  • 外层 Bool 保持规则组合,Java 继续承担最终精筛;
  • 拼音作为独立能力保留,待索引结构支持分词后再增强。

这是一种更适合大规模商标库的工程化折中:在可控查询成本下保留主要召回能力,并让每个性能问题都能通过候选量、分支数和 scroll 耗时定位。

posted @ 2026-10-17 15:13  叶不孤  阅读(5)  评论(0)    收藏  举报