a3bb78e06b
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m35s
215 lines
6.8 KiB
Markdown
215 lines
6.8 KiB
Markdown
# AI 合同平台 V2 数据库优化:关键词检索 SQL 形态收口
|
||
|
||
日期:2026-06-24
|
||
|
||
## 本轮目标
|
||
|
||
延续 DB-5 性能基线,本轮专门处理 AI 中台知识库混合检索中的关键词候选补充慢查询。
|
||
|
||
上一轮已经把向量检索收口到 pgvector HNSW。本轮目标是压缩高频中文词,例如“合同”“风险”,避免 `lower(...) LIKE '%词%'` 在 `knowledge_chunks` 上顺序扫描。
|
||
|
||
## 现状问题
|
||
|
||
原关键词候选 SQL:
|
||
|
||
```sql
|
||
SELECT id, ...
|
||
FROM knowledge_chunks
|
||
WHERE embedding IS NOT NULL
|
||
AND (
|
||
lower(title) LIKE :term
|
||
OR lower(reference) LIKE :term
|
||
OR lower(content) LIKE :term
|
||
)
|
||
ORDER BY created_at DESC
|
||
LIMIT :limit;
|
||
```
|
||
|
||
问题:
|
||
|
||
- 现有 trigram 索引建在原字段 `title/reference/content` 上。
|
||
- 查询使用 `lower(...) LIKE`,无法稳定使用这些原字段索引。
|
||
- 对“合同”“风险”这类高频词,即使改为 raw `LIKE` 或 trigram `%`,也无法稳定改善。
|
||
|
||
远端 smoke 库验证:
|
||
|
||
| 查询形态 | 关键词 | 查询计划 | 耗时 |
|
||
| --- | --- | --- | ---: |
|
||
| `lower(...) LIKE '%合同%'` | 合同 | Seq Scan | 约 948 ms |
|
||
| raw `LIKE '%合同%'` | 合同 | Parallel Seq Scan | 约 1135 ms |
|
||
| trigram `% '合同'` | 合同 | Bitmap Index Scan,但相似度阈值导致 0 行且回表重 | 约 8308 ms |
|
||
| 表达式 trigram 索引 + `lower(...) LIKE` | 合同 | 仍 Seq Scan | 约 475 ms |
|
||
|
||
结论:不能简单把 `LIKE` 替换成 `%`,也不能只补表达式 trigram 索引。
|
||
|
||
## 已完成代码
|
||
|
||
### 1. 新增独立迁移 `20260624_0016`
|
||
|
||
文件:
|
||
|
||
- `yuqei-ai-platform/services/ai-platform-api/migrations/versions/20260624_0016_add_knowledge_chunk_search_vector.py`
|
||
|
||
迁移内容:
|
||
|
||
```sql
|
||
ALTER TABLE knowledge_chunks ADD COLUMN IF NOT EXISTS search_vector tsvector;
|
||
|
||
UPDATE knowledge_chunks
|
||
SET search_vector = to_tsvector(
|
||
'simple',
|
||
coalesce(title, '') || ' ' || coalesce(reference, '') || ' ' || coalesce(content, '')
|
||
)
|
||
WHERE search_vector IS NULL;
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_knowledge_chunks_search_vector_gin
|
||
ON knowledge_chunks USING gin (search_vector);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_knowledge_chunks_embedding_created_desc
|
||
ON knowledge_chunks (created_at DESC, id)
|
||
WHERE embedding IS NOT NULL;
|
||
```
|
||
|
||
说明:
|
||
|
||
- `0015` 在 smoke 库已经执行过,因此本轮没有继续修改已应用迁移,而是新增 `0016` 增量迁移。
|
||
- `search_vector` 用 `simple` 配置,适合当前中文法规片段以词/短语为主的最小可用检索。
|
||
- `created_at DESC` 局部索引用于高频词场景,让数据库可以按最新片段顺序快速停止。
|
||
|
||
### 2. repository 查询改造
|
||
|
||
文件:
|
||
|
||
- `yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/repository.py`
|
||
|
||
改造点:
|
||
|
||
- 混合检索仍先走 pgvector HNSW 获取全局语义候选。
|
||
- 关键词候选补充优先使用:
|
||
|
||
```sql
|
||
search_vector @@ plainto_tsquery('simple', :keyword_query)
|
||
```
|
||
|
||
- 查询继续按 `created_at DESC LIMIT :remaining_limit`,用于快速补足混合候选。
|
||
- 如果目标库没有 `search_vector` 列,或者 SQL 执行失败,自动回退到旧 `LIKE` 逻辑,保证兼容。
|
||
- 新增/重建 chunk 后自动刷新对应行的 `search_vector`。
|
||
|
||
### 3. 中文核心词保持可搜索
|
||
|
||
延续上一轮修复:
|
||
|
||
- “合同”“风险”不再作为 embedding 泛词过滤。
|
||
- 法律问答层仍保留泛问题保护,避免“这个合同需要注意什么?”误召回不相关专门法条。
|
||
|
||
## 本地验证
|
||
|
||
命令:
|
||
|
||
```powershell
|
||
python -m pytest yuqei-ai-platform\services\ai-platform-api\tests -q
|
||
python -m compileall yuqei-ai-platform\services\ai-platform-api\migrations\versions\20260624_0016_add_knowledge_chunk_search_vector.py yuqei-ai-platform\services\ai-platform-api\src\yuqei_ai_platform_api\repository.py
|
||
```
|
||
|
||
结果:
|
||
|
||
- `40 passed`
|
||
- compileall 通过
|
||
|
||
新增测试:
|
||
|
||
- `_to_tsquery_text(["合同", "", " 风险 "]) == "合同 风险"`
|
||
|
||
## V2 smoke 部署验证
|
||
|
||
部署命令:
|
||
|
||
```powershell
|
||
$env:SSHPASS='YOUR_REMOTE_PASSWORD'
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File yuqei-infra\scripts\deploy-v2-smoke-docker.ps1 -UseSshpass -AllowDirtyWorktree -SkipSmoke
|
||
```
|
||
|
||
最终结果:
|
||
|
||
- `V2_SMOKE_DOCKER_DEPLOY_PASS`
|
||
- 产物目录:`project-docs/qa-artifacts/v2-smoke-docker-deploy/2026-06-24T21-35-08`
|
||
|
||
中间情况:
|
||
|
||
- 第一次失败:拉取 `node:22-alpine` 元数据时 Docker 镜像代理 TLS 超时。
|
||
- 第二次失败:Postgres 因上次中断进入 crash recovery/fsync,healthcheck 时间不够。
|
||
- 等待 Postgres 恢复后手动 `docker compose up -d` 成功。
|
||
- 第三次完整部署通过。
|
||
|
||
远端数据库状态:
|
||
|
||
- `ai_platform_alembic_version = 20260624_0016`
|
||
- `knowledge_chunks = 60002`
|
||
- `search_vector` 非空数量:`60002`
|
||
- 索引存在:
|
||
- `ix_knowledge_chunks_embedding_hnsw`
|
||
- `ix_knowledge_chunks_search_vector_gin`
|
||
- `ix_knowledge_chunks_embedding_created_desc`
|
||
|
||
## EXPLAIN ANALYZE 结果
|
||
|
||
验证 SQL:
|
||
|
||
```sql
|
||
EXPLAIN (ANALYZE, BUFFERS)
|
||
SELECT id
|
||
FROM knowledge_chunks
|
||
WHERE embedding IS NOT NULL
|
||
AND search_vector @@ plainto_tsquery('simple', '合同')
|
||
ORDER BY created_at DESC
|
||
LIMIT 100;
|
||
```
|
||
|
||
关键结果:
|
||
|
||
```text
|
||
Index Scan using ix_knowledge_chunks_embedding_created_desc on knowledge_chunks
|
||
Execution Time: 0.396 ms
|
||
```
|
||
|
||
对比:
|
||
|
||
| 查询 | 优化前 | 优化后 |
|
||
| --- | ---: | ---: |
|
||
| 高频词“合同”候选查询 | 约 475-948 ms | 约 0.396 ms |
|
||
|
||
## API 抽样验证
|
||
|
||
接口:
|
||
|
||
- `GET /api/v1/knowledge-chunks/search?strategy=hybrid&limit=3`
|
||
|
||
warm 抽样结果:
|
||
|
||
| 关键词 | 5 次耗时 ms | 中位数 |
|
||
| --- | --- | ---: |
|
||
| 合同 | 70.41, 17.32, 14.51, 14.77, 15.51 | 15.51 ms |
|
||
| 风险 | 15.45, 14.85, 31.43, 14.13, 14.99 | 14.99 ms |
|
||
| 押金 | 16.69, 15.39, 26.76, 59.36, 17.49 | 17.49 ms |
|
||
| 租赁 | 15.55, 30.02, 14.56, 15.61, 28.65 | 15.61 ms |
|
||
|
||
说明:
|
||
|
||
- 首次请求可能包含服务/缓存预热成本。
|
||
- 连续访问热路径稳定在约 15-17 ms。
|
||
- 返回仍包含匹配词,检索语义没有丢失。
|
||
|
||
## 当前边界
|
||
|
||
- 本轮不是完整中文分词搜索引擎改造,而是 AI 中台知识库候选查询的数据库执行层优化。
|
||
- `simple` tsvector 对长中文连续文本的切词能力有限;当前法规片段和测试数据以短词/短语为主,可以满足 MVP 性能闭环。
|
||
- 后续真实法规库导入后,应继续用真实问题集验证“法条依据命中率”,必要时再引入专门中文分词、jieba/SCWS 或向量+BM25 服务。
|
||
|
||
## 下一步建议
|
||
|
||
DB-5 关键词 SQL 形态已经收口。下一步更自然进入:
|
||
|
||
1. RAG explain 增强:显示本次检索是否命中 HNSW、是否使用 search_vector、关键词候选数量、向量候选数量和最终排序原因。
|
||
2. 真实法规库质量基线:用真实法律问题集验证主要依据命中率和错误引用率。
|
||
3. 数据库运维基线:为迁移耗时、索引大小、autovacuum、备份恢复演练补记录。
|