refactor(search): 只展示 keyword 续接词,去掉 titles 段

产品决定:搜索建议只展示 ts_stat 高频词续接(如'美'→美国/美军/美国政府),
不要真实文章 id 提示(用户认为这种'文章#566871'是噪音,没连续性)。

改动:
- SearchSuggestionsResponse 去 title,只剩 query + keywords
- SearchService 只查 search_keywords,fallback 路径也只针对 keywords
- Feed.vue: 删掉 suggestTitles 状态 + SuggestTitleOption 类型联合,
  renderSuggestion 简化成 '词' 标签 + 词文本 + 右侧 weight 数字
- 0011 迁移: 删 search_title_suggestions 表 + 3 索引 + trigger + 函数
  (trigger 在每篇文章 INSERT/UPDATE 都会跑,删了能省掉无用性能损耗)
- 删除: app/models/search_title_suggestion.py + backfill_search_suggestions.py
  替换成: app/scripts/refresh_search_keywords.py(只跑一次词频刷新)
This commit is contained in:
mavis
2026-06-15 19:37:40 +08:00
parent db4fd8699b
commit 85c05c19a7
10 changed files with 277 additions and 366 deletions

View File

@@ -54,6 +54,8 @@
- 👤 **双角色鉴权**:JWT(access 60min + refresh 14d) + API Token(sha256,可撤销,给 Android / ingest 预留)
- 📌 **收藏 + 关键词订阅**:用户级书签,服务端定时按关键词命中推送(预留 Telegram 通道)
- 📊 **管理看板**:源健康度 / 翻译配额 / LLM 状态,全部可视化
- 🔍 **智能搜索建议** *(新)*:`GET /api/v1/search/suggestions?q=prefix` 实时返回高频词续接词(输入"美国"→ ["美国", "美国政府", "美国签证"]);zhparser 中文分词 + GIN 数组索引,前端 debounce 250ms 自动补全
后端用 zhparser 中文分词 + PG 全文搜索 + 候选词固化表,前端 debounce 250ms 自动补全
- 🔄 **热加载**:源/提示词改了不用重启,worker 每天 00:30 重建 job
- 🚀 **一键部署**:SSH 推公钥 + 一键 `git pull` 流程
- 🔒 **安全默认**:bcrypt 密码、API Token 加密、SQL 注入免疫(SQLAlchemy 2.0 参数化)
@@ -597,6 +599,63 @@ docker compose exec api alembic upgrade head
每个任务独立 try/except,失败标 `*_status='failed'`,**不影响**其他任务。
`enrichment_loop``*_status``pending/failed/n/a` 的文章,自动重试 failed。
### 智能搜索建议(autocomplete)
搜索框输入前缀(如"美"),下拉弹出**高频词续接词**:
- 输入"美" → ["美国", "美军", "美国政府", "美方", "美国队", ...]
- 输入"美国" → ["美国", "美国政府", "美国签证", "美国军事", ...]
- 输入"美国政" → ["美国政府"]
来源:`search_keywords` 表按 `prefix_keys @> ARRAY['前缀']` + `weight DESC` 查(ts_stat 从 articles.title_zh + body_zh_text + commentary 聚合的词频)。
**后端架构**:
| 组件 | 作用 | 更新时机 |
|------|------|---------|
| `search_keywords` | 存 ts_stat 词频(全文 + 评论) + prefix_keys 数组 | worker 每日 03:00 全量重建 + 启动时 10s 后跑一次 |
| `articles.title_zh_tsv` | `GENERATED``to_tsvector('chinese_zh', title_zh)` + GIN 索引 | 写入自动维护(commit 11 TODO 顺手填了) |
| `chinese_zh` text search config | zhparser 中文分词 + 简单词映射 | 0010 迁移一次建好 |
| `_fallback_keywords` 实时 ts_stat | search_keywords 表空时,fallback 到实时 ts_stat(慢但能用) | 冷启动友好 |
**中文分词**(`zhparser`):
PG `simple` parser 对中文按整段当一个 token,`ts_stat` 词频聚合不出有意义的结果(整句算 1 个词)。
`zhparser`(scws 字典)解决:Dockerfile 全源码编译(Alpine/Debian/PGDG 都没现成包),建 `chinese_zh` config。
**⚠️ 关键踩坑**: `ts_stat(query, 'a')` 第二参是 weights mask(只统计 A 权重位置),zhparser 不标 A 权重 → 静默 0 行。**用 `ts_stat(query text)` 单参**(等价 mask='abcd',聚合所有权重)。
**性能**:
- `prefix_keys text[]` + GIN 索引,`@> ARRAY['美']` 亚毫秒
- 1545 篇文章 → `search_keywords` 33639 词,`ts_stat` 全量 88s,凌晨一次用户无感
- 搜索建议 API 接口 P99 < 50ms
**冷启动**:
`search_keywords` 表为空时(刚建库 / worker 没刷新过),`_fallback_keywords` 实时跑 `ts_stat` 兜底。
无需手动回灌(不像之前用 articles trigger 维护的 `search_title_suggestions`)。
**API 契约**:
```http
GET /api/v1/search/suggestions?q=&limit=10
Authorization: Bearer <token>
200 {
"query": "",
"keywords": [
{"word": "", "weight": 4865, "source": "ts_stat"},
{"word": "", "weight": 203, "source": "ts_stat"},
{"word": "", "weight": 98, "source": "ts_stat"},
...
]
}
```
- `q` 1-20 字符
- `keywords``weight` 排(高→低),最多 limit 条
- 选词 → 自动填入 q + 触发搜索;回车仍然走原搜索路径
### 历史文章批量 enrich
新功能**只对**翻译完成后入库的文章生效。历史已翻译文章,手动 reset: