之前知识只能"存",不能"找"。这一版把链路补上:切片 → 向量 → 打分 → 带来源返回。
- chunk 表 + 迁移 0006:正文按标题归段切、代码块整块保留、超长按句切、
相邻片重叠 80 字;RLS 与 GRANT 与其它租户表一致
- 向量提供方做成协议:内置 hash(字符二元组,零依赖可复现,作为基线)
与 OpenAI 兼容端点两种,换实现不动业务代码
- 检索是混合打分:向量 0.6 + pg_trgm 0.4。专有编号、单号这类必须原样命中的词
靠词法兜,换个说法表达的同一件事靠向量兜
- 三条纪律:只搜 published(过滤写在 SQL 里)、低于阈值宁可不返回、
每条命中都带来源标题与 item_id
- 发布时自动建索引,改完正文用 POST /knowledge/{id}/reindex 重建
- Web 知识页加检索框与命中卡片;新增 e2e/retrieval.mjs 6 项
为什么不是 pgvector:本机 Docker 起不来、fnsvr-01 没有该扩展且明确不动。
先用 real[] + SQL 余弦把链路跑通,将来换列类型与索引即可,接口不变。
后端 79 passed,e2e smoke/knowledge/richtext/retrieval 全绿。
This commit is contained in:
@@ -33,6 +33,7 @@ from ..models_task import Activity, Item
|
||||
from ..schemas import clamp_limit, decode_cursor, encode_cursor, ok
|
||||
from ..serializers import item_out, tags_of
|
||||
from ..services.knowledge import create_draft_from_source, find_doc_by_source
|
||||
from ..services.knowledge_index import reindex_document, search_knowledge
|
||||
from .attachments import count_for_items, list_for_item
|
||||
|
||||
router = APIRouter(prefix="/workspaces/{ws_id}", tags=["知识库"])
|
||||
@@ -203,6 +204,20 @@ async def list_knowledge(
|
||||
return ok({"items": items, "next_cursor": next_cursor, "has_more": has_more})
|
||||
|
||||
|
||||
# 注意路由顺序:`/knowledge/search` 必须排在 `/knowledge/{item_id}` 之前,
|
||||
# 否则 "search" 会被当成 uuid 解析,返回 422。
|
||||
@router.get("/knowledge/search", summary="检索已发布知识")
|
||||
async def search(
|
||||
ctx: MemberDep,
|
||||
session: SessionDep,
|
||||
q: Annotated[str, Query(min_length=1, max_length=200, description="检索词")],
|
||||
limit: Annotated[int, Query(ge=1, le=30)] = 10,
|
||||
mode: Annotated[str, Query(pattern="^(hybrid|vector|keyword)$")] = "hybrid",
|
||||
) -> dict:
|
||||
hits = await search_knowledge(session, ctx.ws_id, q, limit=limit, mode=mode)
|
||||
return ok({"query": q, "mode": mode, "items": hits, "has_more": False})
|
||||
|
||||
|
||||
@router.get("/knowledge/{item_id}", summary="知识详情")
|
||||
async def get_knowledge(item_id: uuid.UUID, ctx: MemberDep, session: SessionDep) -> dict:
|
||||
item, doc = await _load_doc(session, ctx.ws_id, item_id)
|
||||
@@ -396,9 +411,28 @@ async def change_status(
|
||||
payload={"to": to_status},
|
||||
)
|
||||
await session.flush()
|
||||
if body.action == "publish":
|
||||
# 发布后立刻建索引:检索只认 published,不在这里建就要等定时任务,
|
||||
# 而"刚发布却搜不到"是最容易被当成 bug 的体验。
|
||||
await reindex_document(session, ws_id, item.id)
|
||||
return ok(await _knowledge_out(session, ws_id, item, doc))
|
||||
|
||||
|
||||
@router.post("/knowledge/{item_id}/reindex", summary="重建这份知识的检索切片")
|
||||
async def reindex(item_id: uuid.UUID, ctx: MemberDep, session: SessionDep) -> dict:
|
||||
# 改完正文不重建,检索命中的还是旧句子——留一个能手动触发的入口
|
||||
_item, doc = await _load_doc(session, ctx.ws_id, item_id)
|
||||
count = await reindex_document(session, ctx.ws_id, item_id)
|
||||
await session.flush()
|
||||
return ok(
|
||||
{
|
||||
"item_id": item_id,
|
||||
"chunk_count": count,
|
||||
"searchable": doc.status_hint == "published",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ── 从任务/工单沉淀 ─────────────────────────────────────────────────────
|
||||
@router.post("/items/{item_id}/knowledge", summary="把这件事沉淀成知识草稿")
|
||||
async def promote_to_knowledge(
|
||||
|
||||
Reference in New Issue
Block a user