6e3e6770
tangwang
suggest文档维护
|
1
|
# Suggestion 模块说明
|
ded6f29e
tangwang
补充suggestion模块
|
2
|
|
6e3e6770
tangwang
suggest文档维护
|
3
|
`suggestion/` 目录负责搜索框自动补全能力,当前实现只关注 suggestion 本身:离线构建建议词索引,在线根据输入前缀返回建议词列表。
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
4
|
|
6e3e6770
tangwang
suggest文档维护
|
5
|
这份 README 以当前代码实现为准,重点说明模块现状、关键设计、索引结构、构建发布方式,以及在线检索和排序细节。
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
6
|
|
6e3e6770
tangwang
suggest文档维护
|
7
|
## 1. 当前能力边界
|
ded6f29e
tangwang
补充suggestion模块
|
8
|
|
6e3e6770
tangwang
suggest文档维护
|
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
167
168
169
170
171
172
173
174
175
176
177
178
179
|
- 对外接口:`GET /search/suggestions`
- 输入参数:`q`、`size`、`language`
- Header:`X-Tenant-ID`
- 返回内容:建议词列表 `suggestions[]`
- 不做商品结果拼接,也不走二次商品查询链路
当前模块由三部分组成:
1. 离线构建:从商品索引和搜索日志构建 suggestion 文档
2. 索引发布:写入版本化索引,并通过 alias 原子切换
3. 在线查询:优先走 completion,必要时再走 `search_as_you_type` 兜底召回
## 2. 目录与关键代码
- [builder.py](/data/saas-search/suggestion/builder.py):离线构建、增量更新、alias 发布、meta 状态维护
- [mapping.py](/data/saas-search/suggestion/mapping.py):suggestion 索引 settings 和 mappings 生成
- [service.py](/data/saas-search/suggestion/service.py):在线查询服务,负责语言归一化、双路召回、去重和最终排序
- [RUNBOOK.md](/data/saas-search/suggestion/RUNBOOK.md):构建、发布、验证操作说明
- [TROUBLESHOOTING.md](/data/saas-search/suggestion/TROUBLESHOOTING.md):常见问题排查
命令入口在 [main.py](/data/saas-search/main.py) 中的 `build-suggestions` 子命令。
## 3. 整体架构
在线路径:
`Client -> /search/suggestions -> SuggestionService -> Elasticsearch suggestion alias`
离线路径:
`商品索引 + 搜索日志 -> SuggestionIndexBuilder -> 版本化 suggestion index -> alias publish`
设计上有几个核心点:
- suggestion 独立建索引,不依赖在线商品检索
- 每个租户单独维护 suggestion alias,避免租户间相互影响
- 全量构建写新索引,切换 alias 时零停机
- 增量更新只处理 query log 增量,减少重建成本
## 4. 索引组织与发布
索引命名在 [builder.py](/data/saas-search/suggestion/builder.py) 中统一定义:
- 读别名:`{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}_current`
- 版本索引:`{ES_INDEX_NAMESPACE}search_suggestions_tenant_{tenant_id}_v<timestamp>`
- 元信息索引:`{ES_INDEX_NAMESPACE}search_suggestions_meta`
当前实现是“每租户一个 suggestion alias + 多个版本索引”的模式,而不是环境级共享大索引。
全量构建时的发布流程:
1. 创建新的版本化索引
2. 写入本次构建出的 suggestion 文档
3. 校验新索引可分配、可读
4. alias 原子切换到新索引
5. 清理旧版本索引,只保留最近若干份
6. 更新 `search_suggestions_meta`
元信息索引里记录:
- `active_alias`
- `active_index`
- `last_full_build_at`
- `last_incremental_build_at`
- `last_incremental_watermark`
这些信息主要服务于增量更新和排障。
## 5. Mapping 与索引字段
[mapping.py](/data/saas-search/suggestion/mapping.py) 会根据租户的 `index_languages` 动态生成字段。
### 5.1 索引设置
- `number_of_shards = 1`
- `number_of_replicas = 0`
- `refresh_interval = 30s`
中文使用自定义 analyzer:
- `index_ik`:`ik_max_word + lowercase + asciifolding`
- `query_ik`:`ik_smart + lowercase + asciifolding`
其他语言优先使用 Elasticsearch 内置 analyzer,例如 `english`、`arabic`、`french`、`german` 等;未覆盖语言回退到 `standard`。
### 5.2 核心字段
- `tenant_id`:租户隔离
- `lang`:建议词所属语言
- `text`:原始展示文本
- `text_norm`:归一化文本,用于唯一键和去重
- `sources`:来源集合,可能包含 `title`、`qanchor`、`tag`、`query_log`
- `title_doc_count` / `qanchor_doc_count` / `tag_doc_count`:该词被多少商品字段支撑
- `query_count_7d` / `query_count_30d`:近 7/30 天搜索热度
- `rank_score`:离线预计算排序分
- `lang_confidence` / `lang_source` / `lang_conflict`:语言识别与冲突信息
- `status`:当前是否有效
- `updated_at`:最近更新时间
### 5.3 两类检索字段
1. `completion.<lang>`
2. `sat.<lang>`
`completion.<lang>` 用于极速前缀补全,是短 query 下的主召回通道。
`sat.<lang>` 使用 `search_as_you_type`,用于多词前缀和 completion 未补足时的兜底召回。
也就是说,当前线上不是只靠一种召回方式,而是 completion 优先、SAT 补全。
## 6. 候选词从哪里来
[builder.py](/data/saas-search/suggestion/builder.py) 在全量构建中会聚合两大类数据源。
### 6.1 商品侧
从租户商品索引中流式读取:
- `title`
- `qanchors`
- `enriched_tags`
处理方式:
- `title.<lang>`:经 `_prepare_title_for_suggest()` 裁剪后作为候选词
- `qanchors.<lang>`:按分隔符拆分后作为候选词
- `enriched_tags`:支持多语言对象或普通列表,必要时做语言识别
商品扫描不是一次性全量拉入内存,而是通过 `search_after` 分批读取,这一点在 [_iter_products()](/data/saas-search/suggestion/builder.py#L363) 已实现。
### 6.2 搜索日志侧
从 MySQL `shoplazza_search_log` 中按时间窗口流式读取:
- `query`
- `language`
- `request_params`
- `create_time`
读取方式使用 `stream_results=True + fetchmany()`,避免 `fetchall()` 带来的内存风险,这也是当前实现相对旧方案的重要改进。
搜索日志主要用于补充:
- 用户真实搜索词
- 近 7/30 天热度
- 语言归属信息
## 7. 文本清洗与语言策略
### 7.1 文本归一化
在 [_normalize_text()](/data/saas-search/suggestion/builder.py#L176) 中,当前实现会做:
- Unicode `NFKC` 归一化
- 去首尾空白
- 转小写
- 多空白折叠为单空格
这份 `text_norm` 是 suggestion 文档的稳定键的一部分,文档 `_id` 形式为:
`{tenant_id}|{lang}|{text_norm}`
这保证了同租户、同语言、同一归一化词面只会保留一份文档。
### 7.2 噪声过滤
在 [_looks_noise()](/data/saas-search/suggestion/builder.py#L264) 中,以下内容会被过滤:
- 空文本
- 长度超过 120
- 全部由符号组成的文本
|
ded6f29e
tangwang
补充suggestion模块
|
180
|
|
6e3e6770
tangwang
suggest文档维护
|
181
|
### 7.3 语言判定优先级
|
ded6f29e
tangwang
补充suggestion模块
|
182
|
|
6e3e6770
tangwang
suggest文档维护
|
183
|
日志 query 的语言归属由 [_resolve_query_language()](/data/saas-search/suggestion/builder.py#L299) 负责,优先级是:
|
ded6f29e
tangwang
补充suggestion模块
|
184
|
|
6e3e6770
tangwang
suggest文档维护
|
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
226
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
|
1. `shoplazza_search_log.language`
2. `request_params.language`
3. `detect_text_language_for_suggestions()`
4. 租户 `primary_language`
同时会记录:
- `lang_source`:语言来自哪里
- `lang_confidence`:识别置信度
- `lang_conflict`:日志语言与请求语言是否冲突
在线查询侧在 [_resolve_language()](/data/saas-search/suggestion/service.py#L24) 也会做一次语言归一化,确保查询只打到租户允许的 `index_languages`。
## 8. 排序与 rank 细节
当前排序分成两层:离线 `rank_score`,以及在线最终排序。
### 8.1 离线 `rank_score`
在 [_compute_rank_score()](/data/saas-search/suggestion/builder.py#L338) 中,当前公式是:
```text
rank_score =
1.8 * log1p(query_count_30d)
+ 1.2 * log1p(query_count_7d)
+ 1.0 * log1p(qanchor_doc_count)
+ 0.85 * log1p(tag_doc_count)
+ 0.6 * log1p(title_doc_count)
```
含义上是:
- 搜索日志热度权重大于商品静态字段
- 30 天热度权重大于 7 天热度,但 7 天热度也会强化近期趋势
- `qanchor` 比普通标题更像“可搜索表达”,所以权重更高
- `tag` 次之
- `title` 提供基础覆盖,但权重相对更低
这个分数会被写入:
- 文档字段 `rank_score`
- `completion.<lang>.weight`
因此它同时影响 completion 通道和 SAT 通道。
### 8.2 在线召回排序
[service.py](/data/saas-search/suggestion/service.py) 中的在线策略如下:
1. 先查 `completion.<lang>`
2. 若 query 长度大于 2 且 completion 结果不足,再查 `sat.<lang>`
3. 按 `text` 归一化结果去重
4. 最终排序后截断
completion 通道本身依赖 ES completion 的 `_score`;SAT 通道则用 `function_score + field_value_factor(rank_score)`。
最终排序由 [_finalize_suggestion_list()](/data/saas-search/suggestion/service.py#L155) 负责,排序 key 为:
1. `score * 长度惩罚系数`
2. `rank_score`
长度惩罚定义在 [_suggestion_length_factor()](/data/saas-search/suggestion/service.py#L16):
```text
length_factor = 1 / sqrt(token_len)
```
这意味着在分数相近时,较短、较直接的 suggestion 会更容易排在前面,避免长尾长句把前缀补全结果“顶掉”。
## 9. 在线查询细节
[SuggestionService.search()](/data/saas-search/suggestion/service.py#L110) 的行为可以概括为:
- 如果 alias 不存在,直接返回空数组,不抛 500
- 短 query 优先走 completion 快速返回
- 对于更长 query,再补一次 `bool_prefix`
- 查询时始终带 `routing=tenant_id`
这里的 `routing` 很重要,它保证 suggestion 查询尽量只落在目标租户对应的分片路由上,减少无效 fan-out。
SAT 查询部分还有两个显式过滤条件:
- `lang == resolved_language`
- `status == 1`
这能保证召回结果只来自当前语言、当前有效文档。
## 10. 全量构建与增量更新
### 10.1 全量构建
入口在 [main.py](/data/saas-search/main.py#L104) 的 `build-suggestions --mode full`。
行为是:
1. 读取租户配置中的 `index_languages` 和 `primary_language`
2. 创建新版本索引并等待 ready
3. 聚合商品数据和搜索日志,构造候选词
4. 计算 `rank_score`
5. bulk 写入新索引
6. refresh
7. 发布 alias
8. 更新 meta 信息
### 10.2 增量更新
入口在 [main.py](/data/saas-search/main.py#L104) 的 `build-suggestions --mode incremental`。
当前增量只处理 query log,不回扫商品数据。它依赖 meta 中的 watermark:
- `last_incremental_watermark`
- 不存在时回退到 `last_full_build_at`
- 再不行就使用 `fallback_days`
为了避免边界时间漏数,会额外减去 `overlap_minutes`,形成一个带重叠窗口的增量区间。
增量写入不是整文档重建,而是通过 `scripted_upsert` 做原地累加:
- 增加 `query_count_30d`
- 增加 `query_count_7d`
- 更新 `lang_confidence` / `lang_source` / `lang_conflict`
- 重新计算 `rank_score`
- 更新 `completion` 和 `sat`
对应逻辑在 [_build_incremental_update_script()](/data/saas-search/suggestion/builder.py#L834)。
如果 alias 尚不存在,而 `bootstrap_if_missing=True`,增量任务会先自动做一次全量构建作为初始化。
## 11. 当前实现的一些取舍
### 11.1 优点
- 在线链路很短,没有 suggestion 后再查商品的放大成本
- 构建和发布流程清晰,支持零停机切换
- 商品侧和日志侧都采用流式处理,能控制内存占用
- completion + SAT 双路召回兼顾低延迟和补全能力
- 语言、热度、来源信息都保存在索引中,便于后续优化
### 11.2 现阶段边界
- 增量更新目前只增量处理 query log,商品标题、qanchor、tag 变更仍依赖全量构建刷新
- `rank_score` 目前只使用热度和商品字段覆盖度,没有接入点击、转化等行为质量信号
- 文本归一化目前以 `NFKC + lower + whitespace fold` 为主,尚未做更激进的跨语种归并策略
- `number_of_replicas=0` 更偏开发或成本优先配置,生产是否需要副本要结合集群策略评估
## 12. 常用命令
全量构建:
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
333
334
|
```bash
|
ff9efda0
tangwang
suggest
|
335
|
./scripts/build_suggestions.sh <tenant_id> --mode full
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
336
337
|
```
|
6e3e6770
tangwang
suggest文档维护
|
338
|
增量构建:
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
339
340
|
```bash
|
6e3e6770
tangwang
suggest文档维护
|
341
|
./scripts/build_suggestions.sh <tenant_id> --mode incremental
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
342
343
|
```
|
6e3e6770
tangwang
suggest文档维护
|
344
|
一键重建并验证:
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
345
|
|
6e3e6770
tangwang
suggest文档维护
|
346
347
348
|
```bash
./scripts/rebuild_suggestions.sh <tenant_id>
```
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
349
|
|
6e3e6770
tangwang
suggest文档维护
|
350
|
接口示例:
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
351
352
|
```bash
|
ff9efda0
tangwang
suggest
|
353
354
|
curl "http://localhost:6002/search/suggestions?q=shi&size=10&language=en" \
-H "X-Tenant-ID: 162"
|
316c97c4
tangwang
feat: 完整落地多租户 sug...
|
355
|
```
|
6e3e6770
tangwang
suggest文档维护
|
356
357
|
更多操作细节见 [RUNBOOK.md](/data/saas-search/suggestion/RUNBOOK.md),故障排查看 [TROUBLESHOOTING.md](/data/saas-search/suggestion/TROUBLESHOOTING.md)。
|