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}
在线查询主路径:
- 仅查询
{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}得到 suggestion 列表。 - 对每条 suggestion 进行“结果直达”的二次查询(
msearch)到{ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}:- 使用 suggestion 文本对
title.{lang}/qanchors.{lang}执行term/match_phrase_prefix组合查询。
- 使用 suggestion 文本对
- 回填每条 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_logtitle_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=offlineupdated_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 流程
- 创建/重建
{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}。 - 遍历
{ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}(scroll或search_after):- 提取每个商品的
title.{lang}、qanchors.{lang}。 - 归一化文本(NFKC、trim、lower、空白折叠)。
- 产出候选词并累加:
title_doc_count += 1qanchor_doc_count += 1sources加来源。
- 提取每个商品的
- 读取日志:
- SQL 拉取
tenant_id下时间窗数据(如 30 天)。 - 对每条
query解析语言归属(优先shoplazza_search_log.language,其次request_params.language,见第 6 节)。 - 累加
query_count_7d/query_count_30d,sources加query_log。
- SQL 拉取
- 清洗与过滤:
- 去空、去纯符号、长度阈值过滤。
- 可选黑名单过滤(运营配置)。
- 计算
rank_score(见第 7 节)。 - 组装文档:
- 写
completion.{lang}+sat.{lang}。 _id = md5(tenant_id|lang|text_norm)。
- 写
- 批量写入(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 语言解析优先级
- 一级:
shoplazza_search_log.language(最高优先级)- 若值存在且合法,直接作为 query 归属语言。
- 二级:
request_params.language(JSON 兜底)- 当表字段为空/非法时,解析
request_paramsJSON 中的language。
- 当表字段为空/非法时,解析
- 三级:离线识别(最后兜底)
- 仅在前两者都缺失时启用:
- 脚本直判(CJK/Arabic/Cyrillic)
- 轻量语言识别器(拉丁语)
- 仅在前两者都缺失时启用:
6.2 一致性校验(推荐)
当 shoplazza_search_log.language 与 request_params.language 同时存在但不一致时:
- 默认采用
shoplazza_search_log.language - 记录
lang_conflict=true用于审计 - 输出监控指标(冲突率)
6.3 置信度与约束
对于一级/二级来源:
lang_confidence=1.0lang_source=log_field或lang_source=request_params
对于三级离线识别:
confidence >= 0.8:写入 top10.5 <= confidence < 0.8:写入 top1(必要时兼容 top2 降权)< 0.5:写入租户primary_language(降权)
统一约束:
- 最终写入语言必须属于租户
index_languages
建议额外存储:
lang_confidence(float)lang_source(log_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/sourcesproducts[]为直达商品(数量由result_size控制)。
如需进一步排查,可对比:
- 某个 suggestion 的
text与shoplazza_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的案例,采样检查日志中language与request_params.language是否存在冲突。
14. 自动化测试建议
已提供基础单元测试(见 tests/test_suggestions.py):
- 语言解析逻辑:
test_resolve_query_language_prefers_log_fieldtest_resolve_query_language_uses_request_params_when_log_missingtest_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 索引跑通主能力,再逐步增强排序与在线性能。