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 设计文档

文档导航

  • README.md(本文):完整方案设计(架构、索引、构建、查询、验证)
  • RUNBOOK.md:日常运行手册(如何构建、如何回归、如何发布)
  • TROUBLESHOOTING.md:故障排查手册(空结果、tenant 丢失、ES 401、版本未生效等)

本文档定义 search_suggestions 独立索引方案,用于支持多语言自动补全(suggestion)与结果直达。

1. 背景与目标

当前搜索系统已具备多语言商品索引(title.{lang}qanchors.{lang})与主搜索能力。为了实现输入中实时下拉 suggestion,需要新增一套面向“词”的能力。

核心目标:

  • 在不耦合主搜索链路的前提下,提供低延迟 suggestion(实时输入)。
  • 支持多语言,按请求语言路由到对应 suggestion 语种。
  • 支持“结果直达”:每条 suggestion 可附带候选商品列表(通过二次查询 search_products 完成)。
  • 支持后续词级排序演进(行为信号、运营控制、去噪治理)。

非目标(当前阶段):

  • 不做个性化推荐(用户级 personalization)。
  • 不引入复杂在线学习排序服务。

2. 总体架构

采用双索引架构(支持多环境 namespace 前缀):

  • 商品索引:{ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}
  • 建议词索引:{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}

在线查询主路径:

  1. 仅查询 {ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id} 得到 suggestion 列表。
  2. 对每条 suggestion 进行“结果直达”的二次查询(msearch)到 {ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}
    • 使用 suggestion 文本对 title.{lang} / qanchors.{lang} 执行 term / match_phrase_prefix 组合查询。
  3. 回填每条 suggestion 的商品卡片列表(例如每条 3~5 个)。

3. API 设计

建议保留并增强现有接口:GET /search/suggestions

3.1 请求参数

  • q (string, required): 用户输入前缀
  • size (int, optional, default=10, max=20): 返回 suggestion 数量
  • language (string, required): 请求语言(如 zh, en, ar, ru
  • with_results (bool, optional, default=true): 是否附带每条 suggestion 的直达商品
  • result_size (int, optional, default=3, max=10): 每条 suggestion 附带商品条数
  • debug (bool, optional, default=false): 是否返回调试信息

Header:

  • X-Tenant-ID (required)

3.2 响应结构

{
  "query": "iph",
  "language": "en",
  "suggestions": [
    {
      "text": "iphone 15",
      "lang": "en",
      "score": 12.37,
      "sources": ["query_log", "qanchor"],
      "products": [
        {
          "spu_id": "12345",
          "title": "iPhone 15 Pro Max",
          "price": 999.0,
          "image_url": "https://..."
        }
      ]
    }
  ],
  "took_ms": 14,
  "debug_info": {}
}

4. 索引设计:search_suggestions_tenant_{tenant_id}

文档粒度:tenant_id + lang + text_norm 唯一一条文档。

4.1 字段定义(建议)

  • tenant_id (keyword)
  • lang (keyword)
  • text (keyword):展示文本
  • text_norm (keyword):归一化文本(去重键)
  • sources (keyword[]):来源集合,取值:title / qanchor / query_log
  • title_doc_count (integer):来自 title 的命中文档数
  • qanchor_doc_count (integer):来自 qanchor 的命中文档数
  • query_count_7d (integer):7 天搜索词计数
  • query_count_30d (integer):30 天搜索词计数
  • rank_score (float):离线计算总分
  • status (byte):1=online, 0=offline
  • updated_at (date)

用于召回:

  • completion (object):
    • completion.{lang}: completion 类型(按语言设置 analyzer)
  • sat (object):
    • sat.{lang}: search_as_you_type(增强多词前缀效果)

可选字段(用于加速直达):

  • top_spu_ids (keyword[]):预计算商品候选 id

4.2 Mapping 样例(简化)

{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "tenant_id": { "type": "keyword" },
      "lang": { "type": "keyword" },
      "text": { "type": "keyword" },
      "text_norm": { "type": "keyword" },
      "sources": { "type": "keyword" },
      "title_doc_count": { "type": "integer" },
      "qanchor_doc_count": { "type": "integer" },
      "query_count_7d": { "type": "integer" },
      "query_count_30d": { "type": "integer" },
      "rank_score": { "type": "float" },
      "status": { "type": "byte" },
      "updated_at": { "type": "date" },
      "completion": {
        "properties": {
          "zh": { "type": "completion", "analyzer": "index_ansj", "search_analyzer": "query_ansj" },
          "en": { "type": "completion", "analyzer": "english" },
          "ar": { "type": "completion", "analyzer": "arabic" },
          "ru": { "type": "completion", "analyzer": "russian" }
        }
      },
      "sat": {
        "properties": {
          "zh": { "type": "search_as_you_type", "analyzer": "index_ansj" },
          "en": { "type": "search_as_you_type", "analyzer": "english" },
          "ar": { "type": "search_as_you_type", "analyzer": "arabic" },
          "ru": { "type": "search_as_you_type", "analyzer": "russian" }
        }
      },
      "top_spu_ids": { "type": "keyword" }
    }
  }
}

说明:实际支持语种需与 search_products 已支持语种保持一致。

5. 全量建索引逻辑(核心)

全量程序职责:扫描商品 title/qanchors 与搜索日志 query,聚合后写入 search_suggestions

输入:

  • {ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id} 文档
  • MySQL 表:shoplazza_search_log

输出:

  • {ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id} 全量文档

5.1 流程

  1. 创建/重建 {ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}
  2. 遍历 {ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}scrollsearch_after):
    • 提取每个商品的 title.{lang}qanchors.{lang}
    • 归一化文本(NFKC、trim、lower、空白折叠)。
    • 产出候选词并累加:
      • title_doc_count += 1
      • qanchor_doc_count += 1
      • sources 加来源。
  3. 读取日志:
    • SQL 拉取 tenant_id 下时间窗数据(如 30 天)。
    • 对每条 query 解析语言归属(优先 shoplazza_search_log.language,其次 request_params.language,见第 6 节)。
    • 累加 query_count_7d / query_count_30dsourcesquery_log
  4. 清洗与过滤:
    • 去空、去纯符号、长度阈值过滤。
    • 可选黑名单过滤(运营配置)。
  5. 计算 rank_score(见第 7 节)。
  6. 组装文档:
    • completion.{lang} + sat.{lang}
    • _id = md5(tenant_id|lang|text_norm)
  7. 批量写入(bulk upsert)。

5.2 伪代码

for tenant_id in tenants:
    agg = {}  # key: (lang, text_norm)

    for doc in scan_es_products(tenant_id):
        for lang in index_languages(tenant_id):
            add_from_title(agg, doc.title.get(lang), lang, doc.spu_id)
            add_from_qanchor(agg, doc.qanchors.get(lang), lang, doc.spu_id)

    for row in fetch_search_logs(tenant_id, days=30):
        lang, conf = resolve_query_lang(
            query=row.query,
            log_language=row.language,
            request_params_json=row.request_params,
            tenant_id=tenant_id
        )
        if not lang:
            continue
        add_from_query_log(agg, row.query, lang, row.create_time)

    docs = []
    for (lang, text_norm), item in agg.items():
        if not pass_filters(item):
            continue
        item.rank_score = compute_rank_score(item)
        docs.append(to_suggestion_doc(tenant_id, lang, item))

    bulk_upsert(index=f"{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}", docs=docs)

6. 日志语言解析策略(已新增 language 字段)

现状:shoplazza_search_log 已新增 language 字段,且 request_params(JSON)中也包含 language
因此全量程序不再以“纯离线识别”为主,而是采用“日志显式语言优先”的三级策略。

6.1 语言解析优先级

  1. 一级:shoplazza_search_log.language(最高优先级)
    • 若值存在且合法,直接作为 query 归属语言。
  2. 二级:request_params.language(JSON 兜底)
    • 当表字段为空/非法时,解析 request_params JSON 中的 language
  3. 三级:离线识别(最后兜底)
    • 仅在前两者都缺失时启用:
      • 脚本直判(CJK/Arabic/Cyrillic)
      • 轻量语言识别器(拉丁语)

6.2 一致性校验(推荐)

shoplazza_search_log.languagerequest_params.language 同时存在但不一致时:

  • 默认采用 shoplazza_search_log.language
  • 记录 lang_conflict=true 用于审计
  • 输出监控指标(冲突率)

6.3 置信度与约束

对于一级/二级来源:

  • lang_confidence=1.0
  • lang_source=log_fieldlang_source=request_params

对于三级离线识别:

  • confidence >= 0.8:写入 top1
  • 0.5 <= confidence < 0.8:写入 top1(必要时兼容 top2 降权)
  • < 0.5:写入租户 primary_language(降权)

统一约束:

  • 最终写入语言必须属于租户 index_languages

建议额外存储:

  • lang_confidence(float)
  • lang_sourcelog_field/request_params/script/model/default
  • lang_conflict(bool)

便于后续质量审计与数据回溯。

7. 排序分数设计(离线)

建议采用可解释线性组合:

rank_score =
  w1 * log1p(query_count_30d)
  + w2 * log1p(query_count_7d)
  + w3 * log1p(qanchor_doc_count)
  + w4 * log1p(title_doc_count)
  + w5 * business_bonus

推荐初始权重(可配置):

  • w1=1.8, w2=1.2, w3=1.0, w4=0.6, w5=0.3

说明:

  • 搜索日志信号优先级最高(最接近真实用户意图)。
  • qanchor 高于 title(更偏 query 风格)。
  • business_bonus 可接入销量、库存可售率等轻量业务信号。

8. 在线查询逻辑(suggestion)

主路径只查 search_suggestions

8.1 Suggestion 查询 DSL(示例)

{
  "size": 10,
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "filter": [
            { "term": { "lang": "en" } },
            { "term": { "status": 1 } }
          ],
          "should": [
            {
              "multi_match": {
                "query": "iph",
                "type": "bool_prefix",
                "fields": [
                  "sat.en",
                  "sat.en._2gram",
                  "sat.en._3gram"
                ]
              }
            }
          ],
          "minimum_should_match": 1
        }
      },
      "field_value_factor": {
        "field": "rank_score",
        "factor": 1.0,
        "modifier": "log1p",
        "missing": 0
      },
      "boost_mode": "sum",
      "score_mode": "sum"
    }
  },
  "_source": [
    "text",
    "lang",
    "rank_score",
    "sources",
    "top_spu_ids"
  ]
}

可选:completion 方式(极低延迟)也可作为同接口内另一条召回通道,再与上面结果融合去重。

9. 结果直达(二次查询)

with_results=true 时,对每条 suggestion 的 text 做二次查询到 search_products_tenant_{tenant_id}

推荐使用 msearch,每条 suggestion 一个子查询:

  • term(精确)命中 qanchors.{lang}.keyword(若存在 keyword 子字段)
  • match_phrase_prefix 命中 title.{lang}
  • 可加权:qanchors 命中权重高于 title
  • 每条 suggestion 返回 result_size 条商品

若未来希望进一步降在线复杂度,可改为离线写入 top_spu_ids 并在在线用 mget 回填。

10. 数据治理与运营控制

建议加入以下机制:

  • 黑名单词:人工屏蔽垃圾词、敏感词
  • 白名单词:活动词、品牌词强制保留
  • 最小阈值:低频词不过线(例如 query_count_30d < 2 且无 qanchor/title 支撑)
  • 去重规则:text_norm 维度强去重
  • 更新策略:每日全量 + 每小时增量(后续)

11. 实施里程碑

M1(快速上线):

  • search_suggestions 索引
  • 全量程序:title + qanchors + query_log
  • /search/suggestions 仅查 suggestion,不带直达

M2(增强):

  • 增加二次查询直达商品(msearch
  • 引入语言置信度审计报表
  • 加黑白名单与去噪配置

M3(优化):

  • completion + bool_prefix 双通道融合
  • 增量构建任务(小时级)
  • 排序参数在线配置化

12. 关键风险与规避

  • 日志语言字段质量问题导致错写:通过 log_field > request_params > model 三级策略与冲突审计规避
  • 高频噪声词上浮:黑名单 + 最小阈值 + 分数截断
  • 直达二次查询成本上升:控制 size/result_size,优先 msearch
  • 多语言字段不一致:统一语言枚举与映射生成逻辑,避免手写散落

13. 实验与验证建议

以租户 tenant_id=171 为例,推荐如下验证流程(其它租户 / 环境同理,可通过 ES_INDEX_NAMESPACE 区分 prod / uat / test):

13.1 构建索引

./scripts/build_suggestions.sh 171 --days 30 --recreate

期望 CLI 输出类似(prod 环境,ES_INDEX_NAMESPACE 为空):

{
  "tenant_id": "171",
  "index_name": "search_suggestions_tenant_171",
  "total_candidates": 61,
  "indexed_docs": 61,
  "bulk_result": {
    "success": 61,
    "failed": 0,
    "errors": []
  }
}

含义:

  • total_candidates:聚合到的词候选总数(按 (lang,text_norm) 去重)
  • indexed_docs:实际写入 ES 的文档数(通常与 total_candidates 相同)
  • bulk_result:bulk 写入统计

13.2 检查索引结构

# prod / 本地环境:ES_INDEX_NAMESPACE 为空
curl "http://localhost:9200/search_suggestions_tenant_171/_mapping?pretty"
curl "http://localhost:9200/search_suggestions_tenant_171/_count?pretty"
curl "http://localhost:9200/search_suggestions_tenant_171/_search?size=5&pretty" -d '{
  "query": { "match_all": {} }
}'

# UAT 环境:假设 ES_INDEX_NAMESPACE=uat_
curl "http://localhost:9200/uat_search_suggestions_tenant_171/_mapping?pretty"
curl "http://localhost:9200/uat_search_suggestions_tenant_171/_count?pretty"
curl "http://localhost:9200/uat_search_suggestions_tenant_171/_search?size=5&pretty" -d '{
  "query": { "match_all": {} }
}'

重点确认:

  • 是否存在 lang/text/text_norm/sources/rank_score/completion/sat 等字段。
  • 文档中 lang 是否只落在租户配置的 index_languages 范围内。
  • 常见 query(如你期望的热词)是否有对应文档,query_count_* 是否大致正确。

13.3 通过 API 验证 suggestion 行为

启动后端:

python main.py serve --es-host http://localhost:9200 --port 6002

示例调用(中文):

curl "http://localhost:6002/search/suggestions?q=玩具&size=5&language=zh&with_results=true" \
  -H "X-Tenant-ID: 171"

示例调用(英文):

curl "http://localhost:6002/search/suggestions?q=iph&size=5&language=en&with_results=true" \
  -H "X-Tenant-ID: 171"

预期:

  • resolved_language 与传入 language 一致或回落到租户主语言。
  • 返回若干 suggestions[],每条包含:
    • text/lang/score/rank_score/sources
    • products[] 为直达商品(数量由 result_size 控制)。

如需进一步排查,可对比:

  • 某个 suggestion 的 textshoplazza_search_log.query 的出现频次。
  • 该 suggestion 的 products 是否与主搜索接口 POST /search/ 对同 query 的 topN 结果大体一致。

13.4 语言归属与多语言检查

挑选典型场景:

  • 纯中文 query(如商品中文标题)。
  • 纯英文 query(如品牌/型号)。
  • 混合或无明显语言的 query。

验证点:

  • 文档 lang 与期望语言是否匹配。
  • lang_source 是否按优先级反映来源:
    • log_field > request_params > script/model/default
  • 如存在 lang_conflict=true 的案例,采样检查日志中 languagerequest_params.language 是否存在冲突。

14. 自动化测试建议

已提供基础单元测试(见 tests/test_suggestions.py):

  • 语言解析逻辑:
    • test_resolve_query_language_prefers_log_field
    • test_resolve_query_language_uses_request_params_when_log_missing
    • test_resolve_query_language_fallback_to_primary
  • 在线查询逻辑:
    • test_suggestion_service_basic_flow:使用 FakeESClient 验证 suggestion + 结果直达商品整体流程。

推荐在本地环境中执行:

pytest tests/test_suggestions.py -q

后续可根据业务需要补充:

  • 排序正确性测试(构造不同 query_count_*title/qanchor_doc_count)。
  • 多语言覆盖测试(zh/en/ar/ru 等,结合租户 index_languages)。
  • 简单性能回归(单次查询时延、QPS 与 P95/P99 录制)。

本设计优先保证可落地与可演进:先以独立 suggestion 索引跑通主能力,再逐步增强排序与在线性能。