README.md
Suggestion 设计文档
本文档定义 search_suggestions 独立索引方案,用于支持多语言自动补全(suggestion)与结果直达。
1. 背景与目标
当前搜索系统已具备多语言商品索引(title.{lang}、qanchors.{lang})与主搜索能力。为了实现输入中实时下拉 suggestion,需要新增一套面向“词”的能力。
核心目标:
- 在不耦合主搜索链路的前提下,提供低延迟 suggestion(实时输入)。
- 支持多语言,按请求语言路由到对应 suggestion 语种。
- 支持“结果直达”:每条 suggestion 可附带候选商品列表(通过二次查询
search_products完成)。 - 支持后续词级排序演进(行为信号、运营控制、去噪治理)。
非目标(当前阶段):
- 不做个性化推荐(用户级 personalization)。
- 不引入复杂在线学习排序服务。
2. 总体架构
采用双索引架构:
- 商品索引:
search_products_tenant_{tenant_id} - 建议词索引:
search_suggestions_tenant_{tenant_id}
在线查询主路径:
- 仅查询
search_suggestions_tenant_{tenant_id}得到 suggestion 列表。 - 对每条 suggestion 进行“结果直达”的二次查询(
msearch)到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。
输入:
search_products_tenant_{tenant_id}文档- MySQL 表:
shoplazza_search_log
输出:
search_suggestions_tenant_{tenant_id}全量文档
5.1 流程
- 创建/重建
search_suggestions_tenant_{tenant_id}。 - 遍历
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"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 - 多语言字段不一致:统一语言枚举与映射生成逻辑,避免手写散落
本设计优先保证可落地与可演进:先以独立 suggestion 索引跑通主能力,再逐步增强排序与在线性能。