feat: 知识库朴素语义搜索(输入→检索→结果列表)

This commit is contained in:
simonkoson
2026-05-27 19:25:41 +08:00
parent 0c7d2d7400
commit 783e212bb1
4 changed files with 193 additions and 8 deletions
@@ -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() {
<span style={{ fontSize: 12, color: '#aaa' }}>支持批量上传系统自动解析 yaml frontmatter 并生成语义向量</span>
</div>
{/* 搜索栏(搜索模式/浏览模式互斥,清空恢复树形浏览不丢状态) */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input
placeholder="输入一段文字,语义搜索知识库…"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onPressEnter={handleSearch}
style={{ width: 340, borderRadius: 10 }}
prefix={<SearchOutlined style={{ color: '#aaa' }} />}
suffix={
searchQuery ? (
<ClearOutlined
style={{ color: '#aaa', cursor: 'pointer' }}
onClick={handleClearSearch}
/>
) : null
}
/>
<Button
icon={<SearchOutlined />}
onClick={handleSearch}
loading={searchLoading}
style={{ borderRadius: 10 }}
>
搜索
</Button>
{searchQuery && (
<span style={{ fontSize: 12, color: '#888' }}>
{searchResults.length > 0
? `找到 ${searchResults.length} 条相关结果`
: '无相关结果'}
</span>
)}
</div>
{/* 搜索结果列表(搜索模式显示,浏览模式隐藏) */}
{searchQuery && searchResults.length > 0 && (
<div style={{ marginBottom: 12 }}>
<div style={{ fontSize: 13, color: '#666', marginBottom: 8, fontWeight: 500 }}>
搜索结果
</div>
{searchResults.map(item => (
<Card
key={item.id}
size="small"
style={{ borderRadius: 10, marginBottom: 8 }}
bodyStyle={{ padding: '12px 16px' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 14, color: '#3b4a3b', marginBottom: 4 }}>
{item.title}
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 }}>
<Tag color="default" style={{ borderRadius: 6, fontSize: 12 }}>
{SOURCE_TYPE_LABEL[item.source_type] || item.source_type}
</Tag>
{item.author && (
<span style={{ fontSize: 12, color: '#888' }}>作者{item.author}</span>
)}
{item.source_detail && (
<span style={{ fontSize: 12, color: '#888' }}>出处{item.source_detail}</span>
)}
</div>
<div
style={{
fontSize: 12,
color: '#555',
background: '#f5f5f5',
borderRadius: 6,
padding: '6px 10px',
lineHeight: 1.6,
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
}}
>
{item.snippet}
</div>
</div>
<div style={{
marginLeft: 16,
minWidth: 56,
textAlign: 'center',
background: item.similarity >= 0.7 ? '#e6f4ff' : '#f5f5f5',
borderRadius: 8,
padding: '4px 8px',
}}>
<div style={{ fontSize: 18, fontWeight: 700, color: item.similarity >= 0.7 ? '#1677ff' : '#666' }}>
{Math.max(0, Math.round(item.similarity * 100))}%
</div>
<div style={{ fontSize: 11, color: '#888' }}>相关度</div>
</div>
</div>
</Card>
))}
</div>
)}
{/* 主体:左侧树 + 右侧列表(flex 左右并列,宽度稳定) */}
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
{/* 左侧树 */}
+13
View File
@@ -52,6 +52,19 @@ const knowledgeService = {
const resp = await http.get('/knowledge/grouped')
return resp.data
},
/**
* 语义检索:输入一段文字,返回最相关的知识库条目
* @param {string} queryText - 查询文字
* @param {number} topK - 返回条数,默认 5
*/
async searchItems(queryText, topK = 5) {
const resp = await http.post('/knowledge/search', {
query: queryText,
top_k: topK,
})
return resp.data
},
}
export default knowledgeService