ded6f29e
tangwang
补充suggestion模块
|
1
2
|
# Suggestion 设计文档
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
3
4
5
6
7
8
|
## 文档导航
- `README.md`(本文):完整方案设计(架构、索引、构建、查询、验证)
- `RUNBOOK.md`:日常运行手册(如何构建、如何回归、如何发布)
- `TROUBLESHOOTING.md`:故障排查手册(空结果、tenant 丢失、ES 401、版本未生效等)
|
ded6f29e
tangwang
补充suggestion模块
|
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
本文档定义 `search_suggestions` 独立索引方案,用于支持多语言自动补全(suggestion)与结果直达。
## 1. 背景与目标
当前搜索系统已具备多语言商品索引(`title.{lang}`、`qanchors.{lang}`)与主搜索能力。为了实现输入中实时下拉 suggestion,需要新增一套面向“词”的能力。
核心目标:
- 在不耦合主搜索链路的前提下,提供低延迟 suggestion(实时输入)。
- 支持多语言,按请求语言路由到对应 suggestion 语种。
- 支持“结果直达”:每条 suggestion 可附带候选商品列表(通过二次查询 `search_products` 完成)。
- 支持后续词级排序演进(行为信号、运营控制、去噪治理)。
非目标(当前阶段):
- 不做个性化推荐(用户级 personalization)。
- 不引入复杂在线学习排序服务。
## 2. 总体架构
|
648cb4c2
tangwang
ES docs
|
29
|
采用双索引架构(支持多环境 namespace 前缀):
|
ded6f29e
tangwang
补充suggestion模块
|
30
|
|
648cb4c2
tangwang
ES docs
|
31
32
|
- 商品索引:`{ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}`
- 建议词索引:`{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}`
|
ded6f29e
tangwang
补充suggestion模块
|
33
34
35
|
在线查询主路径:
|
648cb4c2
tangwang
ES docs
|
36
37
|
1. 仅查询 `{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}` 得到 suggestion 列表。
2. 对每条 suggestion 进行“结果直达”的二次查询(`msearch`)到 `{ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}`:
|
ded6f29e
tangwang
补充suggestion模块
|
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
- 使用 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 响应结构
```json
{
"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 样例(简化)
```json
{
"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`。
输入:
|
648cb4c2
tangwang
ES docs
|
167
|
- `{ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}` 文档
|
ded6f29e
tangwang
补充suggestion模块
|
168
169
170
171
|
- MySQL 表:`shoplazza_search_log`
输出:
|
648cb4c2
tangwang
ES docs
|
172
|
- `{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}` 全量文档
|
ded6f29e
tangwang
补充suggestion模块
|
173
174
175
|
### 5.1 流程
|
648cb4c2
tangwang
ES docs
|
176
177
|
1. 创建/重建 `{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}`。
2. 遍历 `{ES_INDEX_NAMESPACE}search_products_tenant_{tenant_id}`(`scroll` 或 `search_after`):
|
ded6f29e
tangwang
补充suggestion模块
|
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
- 提取每个商品的 `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_30d`,`sources` 加 `query_log`。
4. 清洗与过滤:
- 去空、去纯符号、长度阈值过滤。
- 可选黑名单过滤(运营配置)。
5. 计算 `rank_score`(见第 7 节)。
6. 组装文档:
- 写 `completion.{lang}` + `sat.{lang}`。
- `_id = md5(tenant_id|lang|text_norm)`。
7. 批量写入(bulk upsert)。
### 5.2 伪代码
```python
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))
|
648cb4c2
tangwang
ES docs
|
226
|
bulk_upsert(index=f"{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}", docs=docs)
|
ded6f29e
tangwang
补充suggestion模块
|
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
|
```
## 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.language` 与 `request_params.language` 同时存在但不一致时:
- 默认采用 `shoplazza_search_log.language`
- 记录 `lang_conflict=true` 用于审计
- 输出监控指标(冲突率)
### 6.3 置信度与约束
对于一级/二级来源:
- `lang_confidence=1.0`
- `lang_source=log_field` 或 `lang_source=request_params`
对于三级离线识别:
- `confidence >= 0.8`:写入 top1
- `0.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. 排序分数设计(离线)
建议采用可解释线性组合:
```text
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(示例)
```json
{
"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`
- 多语言字段不一致:统一语言枚举与映射生成逻辑,避免手写散落
---
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
408
409
|
## 13. 实验与验证建议
|
648cb4c2
tangwang
ES docs
|
410
|
以租户 `tenant_id=171` 为例,推荐如下验证流程(其它租户 / 环境同理,可通过 ES_INDEX_NAMESPACE 区分 prod / uat / test):
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
411
412
413
414
415
416
417
|
### 13.1 构建索引
```bash
./scripts/build_suggestions.sh 171 --days 30 --recreate
```
|
648cb4c2
tangwang
ES docs
|
418
|
期望 CLI 输出类似(prod 环境,ES_INDEX_NAMESPACE 为空):
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
|
```json
{
"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 检查索引结构
```bash
|
648cb4c2
tangwang
ES docs
|
443
|
# prod / 本地环境:ES_INDEX_NAMESPACE 为空
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
444
445
446
447
448
|
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": {} }
}'
|
648cb4c2
tangwang
ES docs
|
449
450
451
452
453
454
455
|
# 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": {} }
}'
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
|
```
重点确认:
- 是否存在 `lang/text/text_norm/sources/rank_score/completion/sat` 等字段。
- 文档中 `lang` 是否只落在租户配置的 `index_languages` 范围内。
- 常见 query(如你期望的热词)是否有对应文档,`query_count_*` 是否大致正确。
### 13.3 通过 API 验证 suggestion 行为
启动后端:
```bash
python main.py serve --es-host http://localhost:9200 --port 6002
```
示例调用(中文):
```bash
curl "http://localhost:6002/search/suggestions?q=玩具&size=5&language=zh&with_results=true" \
-H "X-Tenant-ID: 171"
```
示例调用(英文):
```bash
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 的 `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_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 + 结果直达商品整体流程。
推荐在本地环境中执行:
```bash
pytest tests/test_suggestions.py -q
```
后续可根据业务需要补充:
- 排序正确性测试(构造不同 `query_count_*`、`title/qanchor_doc_count`)。
- 多语言覆盖测试(zh/en/ar/ru 等,结合租户 `index_languages`)。
- 简单性能回归(单次查询时延、QPS 与 P95/P99 录制)。
|
ded6f29e
tangwang
补充suggestion模块
|
536
|
本设计优先保证可落地与可演进:先以独立 suggestion 索引跑通主能力,再逐步增强排序与在线性能。
|