# Suggestion 架构方案 V2(仅 Suggest,去除结果直达) ## 0. 结论 本方案将 Suggest 设计为**独立高性能检索系统**,只返回建议词,不再返回商品卡片,也不做历史兼容。 - 只保留 `/search/suggestions` 的词级自动补全能力 - 完全移除 `with_results/result_size/products[]` 链路 - 多语言优先,支持高并发、低延迟、可持续演进 --- ## 1. 当前实现的关键问题(基于现有代码审视) 1. 在线链路曾包含“suggest -> 二次商品查询”,属于典型 N+1 放大,QPS 上升后延迟和 ES 负载都不稳定。 2. `builder.py` 全量构建使用“大量 in-memory 聚合 + fetchall”,大租户下内存风险高。 3. 查询参数上限过大(原 `size<=200`),不符合自动补全接口性能边界。 4. 文档与实现长期混合(README 仍包含结果直达),导致认知不一致。 5. 多语言归一化仍偏基础(仅 lower/空白折叠),对 Unicode、变音符、跨语系兼容不够。 --- ## 2. 目标与 SLO ### 2.1 业务目标 - 输入时实时返回高相关建议词(query suggestion) - 多语言稳定(至少覆盖租户配置 `index_languages`) - 支持词级排序和运营治理(黑白名单、降噪、降权) ### 2.2 性能目标(建议) - P50 < 10ms,P95 < 25ms,P99 < 50ms(ES 查询耗时,不含网关) - 单集群支持高并发(千级 QPS 可横向扩展) - 数据新鲜度:增量 5-15 分钟可见 --- ## 3. 总体架构 ## 3.1 在线路径(单跳) Client -> API `/search/suggestions` -> ES `search_suggestions_v2` -> 返回 suggestions 原则: - **单次 ES 查询完成主路径**(可选双召回融合,但仍在同一次 API 请求内完成) - 不调用 `search_products`,不返回商品结果 - 通过 `routing=tenant_id` 避免跨分片 fan-out ## 3.2 离线路径(构建) 数据源: - 商品字段:`title.{lang}`、`qanchors.{lang}` - 搜索日志:`shoplazza_search_log`(含 `language/request_params`) - 行为信号(可选增强):点击、加购、下单 产物: - Suggest 文档(`tenant_id + lang + text_norm` 唯一) - completion + prefix 检索字段 - 排序特征(热度、近期度、质量分) 发布方式: - 写入新物理索引(版本化) - 原子切换 alias(零停机) --- ## 4. 索引设计(ES) ## 4.1 索引组织 推荐两级策略: 1. 默认:环境级共享索引(降低海量租户 index 数量) 2. 大租户:可升级为租户独享索引(隔离资源) 统一通过 alias 暴露: - `search_suggestions_v2_current` ## 4.2 Mapping(核心字段) ```json { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "refresh_interval": "30s" }, "mappings": { "properties": { "tenant_id": { "type": "keyword" }, "lang": { "type": "keyword" }, "text": { "type": "keyword" }, "text_norm": { "type": "keyword" }, "status": { "type": "byte" }, "sources": { "type": "keyword" }, "query_count_7d": { "type": "integer" }, "query_count_30d": { "type": "integer" }, "ctr_30d": { "type": "float" }, "order_rate_30d": { "type": "float" }, "rank_score": { "type": "float" }, "suggest": { "type": "completion", "contexts": [ { "name": "tenant", "type": "category" }, { "name": "lang", "type": "category" } ] }, "sat": { "properties": { "zh": { "type": "search_as_you_type", "analyzer": "index_ik" }, "en": { "type": "search_as_you_type", "analyzer": "english" }, "ar": { "type": "search_as_you_type", "analyzer": "arabic" } } }, "updated_at": { "type": "date" } } } } ``` 说明: - `completion` 负责极速前缀命中(主召回) - `search_as_you_type` 负责多词前缀和召回兜底 - `contexts` 强制租户与语言隔离 --- ## 5. 多语言策略 1. 语言归属优先级:`log.language > request_params.language > 脚本识别 > tenant.primary_language` 2. 统一归一化:NFKC、大小写折叠、空白折叠、标点清洗 3. 分词器按语言配置: - 中文:IK/ANSJ(与主索引保持一致) - 拉丁语系:对应内置 analyzer - 未覆盖语种:`standard + ICU folding` 兜底 4. 保证写入语言必须在租户 `index_languages` 内 --- ## 6. 在线检索策略(高性能) ## 6.1 双通道召回(推荐) 1. 通道 A:`completion suggester`(prefix,skip_duplicates) 2. 通道 B:`multi_match(type=bool_prefix)` on `search_as_you_type` 3. 融合去重:按 `text_norm` 去重,按最终分排序截断 ## 6.2 查询约束 - 默认 `size=10`,最大 `size=50` - `track_total_hits=false` - `_source` 仅返回必要字段(`text/lang/rank_score/sources`) - `routing=tenant_id` ## 6.3 打分建议 ```text final_score = es_score + a1*log1p(query_count_30d) + a2*log1p(query_count_7d) + a3*ctr_30d + a4*order_rate_30d + a5*freshness_decay ``` --- ## 7. 构建与发布 ## 7.1 构建模式 - 每日全量:重建全量特征,清理脏词 - 小时级增量:只处理新日志窗口 ## 7.2 工程要求 - 禁止 `fetchall` 全量入内存,改为流式读取(分页/游标) - ES 扫描采用 `search_after` 流式聚合 - 批量写入采用 bulk(分块 + 重试 + 失败重放) ## 7.3 发布策略 1. `search_suggestions_v2_YYYYMMDDHHmm` 写入完成 2. 校验 count/抽样查询/核心词覆盖 3. alias 原子切换到新索引 4. 保留上一个版本用于快速回滚 --- ## 8. API 契约(V2) 请求: - `GET /search/suggestions` - 参数:`q`、`language`、`size` - Header:`X-Tenant-ID` 响应: ```json { "query": "iph", "language": "en", "resolved_language": "en", "suggestions": [ { "text": "iphone 15", "lang": "en", "score": 8.31, "rank_score": 6.72, "sources": ["query_log", "qanchor"] } ], "took_ms": 12 } ``` 删除项(明确不支持): - `with_results` - `result_size` - `products[]` --- ## 9. 观测与治理 核心监控: - QPS、P50/P95/P99、错误率 - 空结果率(按语言、按租户) - suggestion 覆盖率(top query 是否命中) - 语言冲突率(log vs request_params) - 噪声词比例、黑名单命中率 治理机制: - 黑名单:强制下线 - 白名单:强制保留并可加权 - 最小热度阈值:低频垃圾词过滤 - 时间衰减:过期词自动下沉 --- ## 10. 与官方最佳实践对齐(ES) 本方案直接采用以下官方建议: 1. `completion` 适合高性能自动补全,支持 `skip_duplicates` 与上下文过滤。 2. `search_as_you_type + bool_prefix` 是官方推荐的 as-you-type 查询方式。 3. `edge_ngram` 仅用于索引时分词,查询时应用普通 analyzer(`search_analyzer`)。 4. 多语言场景使用 ICU Analysis 插件增强 Unicode 处理。 5. 通过 `routing` 将租户请求路由到单分片,降低 fan-out。 --- ## 11. 分阶段落地 1. Phase 1(本次):去除结果直达,稳定 Suggest 单能力 2. Phase 2:流式增量构建 + alias 原子发布 3. Phase 3:行为信号排序(CTR/CVR)+ 运营治理台 4. Phase 4:大租户独享索引自动升降级 --- ## 12. Phase 2 落地命令(当前仓库) 全量重建(版本化索引 + alias 发布): ```bash python main.py build-suggestions \ --tenant-id 162 \ --mode full \ --days 365 \ --publish-alias \ --keep-versions 2 ``` 增量更新(基于 watermark): ```bash python main.py build-suggestions \ --tenant-id 162 \ --mode incremental \ --overlap-minutes 30 ``` 一键脚本(全量 + 增量 + ES/API 验证): ```bash ./scripts/rebuild_suggestions.sh 162 ```