diff --git a/backend/app/api/knowledge.py b/backend/app/api/knowledge.py index 910adae..fef9686 100644 --- a/backend/app/api/knowledge.py +++ b/backend/app/api/knowledge.py @@ -1,8 +1,9 @@ """ -知识库 API — 上传 / 列表 / 删除 / 来源筛选 +知识库 API — 上传 / 列表 / 删除 / 来源筛选 / 语义搜索 """ from typing import Optional +from pydantic import BaseModel from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, status from sqlmodel import Session @@ -104,3 +105,28 @@ def get_grouped_knowledge_items( """ svc = KnowledgeService() return svc.get_grouped_items() + + +class SearchRequest(BaseModel): + query: str + top_k: int = 5 + + +@router.post("/search") +def search_knowledge( + body: SearchRequest, + session: Session = Depends(get_session), + current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)), +): + """ + 语义检索:输入一段文字,返回最相关的知识库条目及相似度。 + 查询向量用 type="query"(区分于存入时的 type="db")。 + 三角色均可读。 + """ + svc = KnowledgeService() + results = svc.search_similar(query_text=body.query, top_k=body.top_k) + return { + "results": results, + "query": body.query, + "count": len(results), + } diff --git a/backend/app/services/knowledge_service.py b/backend/app/services/knowledge_service.py index 3e976d1..1ea32f5 100644 --- a/backend/app/services/knowledge_service.py +++ b/backend/app/services/knowledge_service.py @@ -232,7 +232,10 @@ class KnowledgeService: def search_similar(self, query_text: str, top_k: int = 5) -> list[dict]: """ 语义检索:查询句转为向量,用 SQL 余弦距离(<=>)在数据库层检索 - 返回 top_k 条相似笔记,含相似度分数 + 返回 top_k 条相似笔记,含相似度分数 + 原文片段(SQL 端截断前 200 字)。 + + 注意:当前取前 200 字是已知妥协(整篇向量检索无法定位中段命中点), + Phase 4a 做切块检索(chunk)时可优化为取最相关片段。 """ query_vector = self.embedder.embed_single(query_text, embed_type="query") vec_str = "[" + ",".join(str(v) for v in query_vector) + "]" @@ -243,6 +246,9 @@ class KnowledgeService: ki.id, ki.title, ki.source_type, + ki.author, + ki.tags, + SUBSTRING(ki.content_md, 1, 200) AS snippet, 1 - (ke.embedding <=> '{vec_str}'::vector) AS similarity FROM knowledge_embeddings ke JOIN knowledge_items ki ON ke.knowledge_id = ki.id @@ -252,10 +258,20 @@ class KnowledgeService: """ stmt = text(sql) rows = session.execute(stmt).all() - return [ - {"id": r.id, "title": r.title, "source_type": r.source_type, "similarity": round(r.similarity, 4)} - for r in rows - ] + results = [] + for r in rows: + tags = r.tags or {} + source_detail = tags.get("source_detail") if isinstance(tags, dict) else None + results.append({ + "id": r.id, + "title": r.title, + "source_type": r.source_type, + "author": r.author, + "source_detail": source_detail, + "snippet": r.snippet, + "similarity": round(r.similarity, 4), + }) + return results def get_item_count(self) -> int: with Session(engine) as session: diff --git a/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx b/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx index b0d8961..1d8bff4 100644 --- a/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' -import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag } from 'antd' -import { DeleteOutlined, ReloadOutlined, UploadOutlined } from '@ant-design/icons' +import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag, Input } from 'antd' +import { DeleteOutlined, ReloadOutlined, UploadOutlined, SearchOutlined, ClearOutlined } from '@ant-design/icons' import useAuthStore from '../../stores/authStore' import knowledgeService from '../../services/knowledgeService' import KnowledgeTree from '../../components/KnowledgeTree/KnowledgeTree' @@ -34,6 +34,11 @@ export default function KnowledgeBase() { const [sourceDetailFilter, setSourceDetailFilter] = useState('') const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { type, detail } + // 搜索状态 + const [searchQuery, setSearchQuery] = useState('') + const [searchResults, setSearchResults] = useState([]) + const [searchLoading, setSearchLoading] = useState(false) + const fetchItems = async () => { setLoading(true) try { @@ -109,6 +114,30 @@ export default function KnowledgeBase() { } } + // 搜索处理 + const handleSearch = async () => { + if (!searchQuery.trim()) { + setSearchResults([]) + return + } + setSearchLoading(true) + try { + const data = await knowledgeService.searchItems(searchQuery.trim()) + setSearchResults(data.results || []) + } catch { + message.error('搜索失败') + setSearchResults([]) + } finally { + setSearchLoading(false) + } + } + + // 清空搜索 → 恢复树形浏览(不丢树状态) + const handleClearSearch = () => { + setSearchQuery('') + setSearchResults([]) + } + // 树节点选中 → 联动过滤 const handleNodeSelect = (node) => { setSelectedTreeNode(node) @@ -229,6 +258,107 @@ export default function KnowledgeBase() { 支持批量上传,系统自动解析 yaml frontmatter 并生成语义向量 + {/* 搜索栏(搜索模式/浏览模式互斥,清空恢复树形浏览不丢状态) */} +