Name Last Update
..
README.md Loading commit data...
RUNBOOK.md Loading commit data...
TROUBLESHOOTING.md Loading commit data...
__init__.py Loading commit data...
builder.py Loading commit data...
mapping.py Loading commit data...
service.py Loading commit data...

README.md

Suggestion 模块说明

suggestion/ 目录负责搜索框自动补全能力,当前实现只关注 suggestion 本身:离线构建建议词索引,在线根据输入前缀返回建议词列表。

这份 README 以当前代码实现为准,重点说明模块现状、关键设计、索引结构、构建发布方式,以及在线检索和排序细节。

1. 当前能力边界

  • 对外接口:GET /search/suggestions
  • 输入参数:qsizelanguage
  • Header:X-Tenant-ID
  • 返回内容:建议词列表 suggestions[]
  • 不做商品结果拼接,也不走二次商品查询链路

当前模块由三部分组成:

  1. 离线构建:从商品索引和搜索日志构建 suggestion 文档
  2. 索引发布:写入版本化索引,并通过 alias 原子切换
  3. 在线查询:优先走 completion,必要时再走 search_as_you_type 兜底召回

2. 目录与关键代码

  • builder.py:离线构建、增量更新、alias 发布、meta 状态维护
  • mapping.py:suggestion 索引 settings 和 mappings 生成
  • service.py:在线查询服务,负责语言归一化、双路召回、去重和最终排序
  • RUNBOOK.md:构建、发布、验证操作说明
  • TROUBLESHOOTING.md:常见问题排查

命令入口在 main.py 中的 build-suggestions 子命令。

3. 整体架构

在线路径:

Client -> /search/suggestions -> SuggestionService -> Elasticsearch suggestion alias

离线路径:

商品索引 + 搜索日志 -> SuggestionIndexBuilder -> 版本化 suggestion index -> alias publish

设计上有几个核心点:

  • suggestion 独立建索引,不依赖在线商品检索
  • 每个租户单独维护 suggestion alias,避免租户间相互影响
  • 全量构建写新索引,切换 alias 时零停机
  • 增量更新只处理 query log 增量,减少重建成本

4. 索引组织与发布

索引命名在 builder.py 中统一定义:

  • 读别名:{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}_current
  • 版本索引:{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}_v<timestamp>
  • 元信息索引:{ES_INDEX_NAMESPACE}search_suggestions_meta

当前实现是“每租户一个 suggestion alias + 多个版本索引”的模式,而不是环境级共享大索引。

全量构建时的发布流程:

  1. 创建新的版本化索引
  2. 写入本次构建出的 suggestion 文档
  3. 校验新索引可分配、可读
  4. alias 原子切换到新索引
  5. 清理旧版本索引,只保留最近若干份
  6. 更新 search_suggestions_meta

元信息索引里记录:

  • active_alias
  • active_index
  • last_full_build_at
  • last_incremental_build_at
  • last_incremental_watermark

这些信息主要服务于增量更新和排障。

5. Mapping 与索引字段

mapping.py 会根据租户的 index_languages 动态生成字段。

5.1 索引设置

  • number_of_shards = 1
  • number_of_replicas = 0
  • refresh_interval = 30s

中文使用自定义 analyzer:

  • index_ikik_max_word + lowercase + asciifolding
  • query_ikik_smart + lowercase + asciifolding

其他语言优先使用 Elasticsearch 内置 analyzer,例如 englisharabicfrenchgerman 等;未覆盖语言回退到 standard

5.2 核心字段

  • tenant_id:租户隔离
  • lang:建议词所属语言
  • text:原始展示文本
  • text_norm:归一化文本,用于唯一键和去重
  • sources:来源集合,可能包含 titleqanchortagquery_log
  • title_doc_count / qanchor_doc_count / tag_doc_count:该词被多少商品字段支撑
  • query_count_7d / query_count_30d:近 7/30 天搜索热度
  • rank_score:离线预计算排序分
  • lang_confidence / lang_source / lang_conflict:语言识别与冲突信息
  • status:当前是否有效
  • updated_at:最近更新时间

5.3 两类检索字段

  1. completion.<lang>
  2. sat.<lang>

completion.<lang> 用于极速前缀补全,是短 query 下的主召回通道。

sat.<lang> 使用 search_as_you_type,用于多词前缀和 completion 未补足时的兜底召回。

也就是说,当前线上不是只靠一种召回方式,而是 completion 优先、SAT 补全。

6. 候选词从哪里来

builder.py 在全量构建中会聚合两大类数据源。

6.1 商品侧

从租户商品索引中流式读取:

  • title
  • qanchors
  • enriched_tags

处理方式:

  • title.<lang>:经 _prepare_title_for_suggest() 裁剪后作为候选词
  • qanchors.<lang>:按分隔符拆分后作为候选词
  • enriched_tags:支持多语言对象或普通列表,必要时做语言识别

商品扫描不是一次性全量拉入内存,而是通过 search_after 分批读取,这一点在 _iter_products() 已实现。

6.2 搜索日志侧

从 MySQL shoplazza_search_log 中按时间窗口流式读取:

  • query
  • language
  • request_params
  • create_time

读取方式使用 stream_results=True + fetchmany(),避免 fetchall() 带来的内存风险,这也是当前实现相对旧方案的重要改进。

搜索日志主要用于补充:

  • 用户真实搜索词
  • 近 7/30 天热度
  • 语言归属信息

7. 文本清洗与语言策略

7.1 文本归一化

<em>normalize</em>text() 中,当前实现会做:

  • Unicode NFKC 归一化
  • 去首尾空白
  • 转小写
  • 多空白折叠为单空格

这份 text_norm 是 suggestion 文档的稳定键的一部分,文档 _id 形式为:

{tenant_id}|{lang}|{text_norm}

这保证了同租户、同语言、同一归一化词面只会保留一份文档。

7.2 噪声过滤

_looks_noise() 中,以下内容会被过滤:

  • 空文本
  • 长度超过 120
  • 全部由符号组成的文本

7.3 语言判定优先级

日志 query 的语言归属由 <em>resolve_query</em>language() 负责,优先级是:

  1. shoplazza_search_log.language
  2. request_params.language
  3. detect_text_language_for_suggestions()
  4. 租户 primary_language

同时会记录:

  • lang_source:语言来自哪里
  • lang_confidence:识别置信度
  • lang_conflict:日志语言与请求语言是否冲突

在线查询侧在 _resolve_language() 也会做一次语言归一化,确保查询只打到租户允许的 index_languages

8. 排序与 rank 细节

当前排序分成两层:离线 rank_score,以及在线最终排序。

8.1 离线 rank_score

<em>compute_rank</em>score() 中,当前公式是:

rank_score =
  1.8 * log1p(query_count_30d)
  + 1.2 * log1p(query_count_7d)
  + 1.0 * log1p(qanchor_doc_count)
  + 0.85 * log1p(tag_doc_count)
  + 0.6 * log1p(title_doc_count)

含义上是:

  • 搜索日志热度权重大于商品静态字段
  • 30 天热度权重大于 7 天热度,但 7 天热度也会强化近期趋势
  • qanchor 比普通标题更像“可搜索表达”,所以权重更高
  • tag 次之
  • title 提供基础覆盖,但权重相对更低

这个分数会被写入:

  • 文档字段 rank_score
  • completion.<lang>.weight

因此它同时影响 completion 通道和 SAT 通道。

8.2 在线召回排序

service.py 中的在线策略如下:

  1. 先查 completion.<lang>
  2. 若 query 长度大于 2 且 completion 结果不足,再查 sat.<lang>
  3. text 归一化结果去重
  4. 最终排序后截断

completion 通道本身依赖 ES completion 的 _score;SAT 通道则用 function_score + field_value_factor(rank_score)

最终排序由 <em>finalize_suggestion</em>list() 负责,排序 key 为:

  1. score * 长度惩罚系数
  2. rank_score

长度惩罚定义在 <em>suggestion_length</em>factor()

length_factor = 1 / sqrt(token_len)

这意味着在分数相近时,较短、较直接的 suggestion 会更容易排在前面,避免长尾长句把前缀补全结果“顶掉”。

9. 在线查询细节

SuggestionService.search() 的行为可以概括为:

  • 如果 alias 不存在,直接返回空数组,不抛 500
  • 短 query 优先走 completion 快速返回
  • 对于更长 query,再补一次 bool_prefix
  • 查询时始终带 routing=tenant_id

这里的 routing 很重要,它保证 suggestion 查询尽量只落在目标租户对应的分片路由上,减少无效 fan-out。

SAT 查询部分还有两个显式过滤条件:

  • lang == resolved_language
  • status == 1

这能保证召回结果只来自当前语言、当前有效文档。

10. 全量构建与增量更新

10.1 全量构建

入口在 main.pybuild-suggestions --mode full

行为是:

  1. 读取租户配置中的 index_languagesprimary_language
  2. 创建新版本索引并等待 ready
  3. 聚合商品数据和搜索日志,构造候选词
  4. 计算 rank_score
  5. bulk 写入新索引
  6. refresh
  7. 发布 alias
  8. 更新 meta 信息

10.2 增量更新

入口在 main.pybuild-suggestions --mode incremental

当前增量只处理 query log,不回扫商品数据。它依赖 meta 中的 watermark:

  • last_incremental_watermark
  • 不存在时回退到 last_full_build_at
  • 再不行就使用 fallback_days

为了避免边界时间漏数,会额外减去 overlap_minutes,形成一个带重叠窗口的增量区间。

增量写入不是整文档重建,而是通过 scripted_upsert 做原地累加:

  • 增加 query_count_30d
  • 增加 query_count_7d
  • 更新 lang_confidence / lang_source / lang_conflict
  • 重新计算 rank_score
  • 更新 completionsat

对应逻辑在 <em>build_incremental</em>update_script()

如果 alias 尚不存在,而 bootstrap_if_missing=True,增量任务会先自动做一次全量构建作为初始化。

11. 当前实现的一些取舍

11.1 优点

  • 在线链路很短,没有 suggestion 后再查商品的放大成本
  • 构建和发布流程清晰,支持零停机切换
  • 商品侧和日志侧都采用流式处理,能控制内存占用
  • completion + SAT 双路召回兼顾低延迟和补全能力
  • 语言、热度、来源信息都保存在索引中,便于后续优化

11.2 现阶段边界

  • 增量更新目前只增量处理 query log,商品标题、qanchor、tag 变更仍依赖全量构建刷新
  • rank_score 目前只使用热度和商品字段覆盖度,没有接入点击、转化等行为质量信号
  • 文本归一化目前以 NFKC + lower + whitespace fold 为主,尚未做更激进的跨语种归并策略
  • number_of_replicas=0 更偏开发或成本优先配置,生产是否需要副本要结合集群策略评估

12. 常用命令

全量构建:

./scripts/build_suggestions.sh <tenant_id> --mode full

增量构建:

./scripts/build_suggestions.sh <tenant_id> --mode incremental

一键重建并验证:

./scripts/rebuild_suggestions.sh <tenant_id>

接口示例:

curl "http://localhost:6002/search/suggestions?q=shi&size=10&language=en" \
  -H "X-Tenant-ID: 162"

更多操作细节见 RUNBOOK.md,故障排查看 TROUBLESHOOTING.md