Files
ai-contract-platform/project-docs/V2-database-keyword-search-sql-optimization-2026-06-24.md
T
yuqei a3bb78e06b
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m35s
release: freeze V2 release candidate
2026-07-12 15:32:24 +08:00

215 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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、备份恢复演练补记录。