|
@@ -13,65 +13,105 @@
|
|
|
|
|
|
|
|
<!-- Search Form Card -->
|
|
<!-- Search Form Card -->
|
|
|
<el-card class="search-card" shadow="never">
|
|
<el-card class="search-card" shadow="never">
|
|
|
- <div class="search-form-row">
|
|
|
|
|
- <div class="form-item kb-select">
|
|
|
|
|
- <div class="label">知识库</div>
|
|
|
|
|
- <el-select
|
|
|
|
|
- v-model="searchForm.kb_id"
|
|
|
|
|
- placeholder="选择知识库"
|
|
|
|
|
- style="width: 100%"
|
|
|
|
|
- clearable
|
|
|
|
|
- @change="handleKbChange"
|
|
|
|
|
- >
|
|
|
|
|
- <el-option
|
|
|
|
|
- v-for="kb in kbList"
|
|
|
|
|
- :key="kb.id"
|
|
|
|
|
- :label="kb.name"
|
|
|
|
|
- :value="kb.collection_name"
|
|
|
|
|
- />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="form-item meta-select">
|
|
|
|
|
- <div class="label">元数据字典</div>
|
|
|
|
|
- <el-select
|
|
|
|
|
- v-model="searchForm.metadata_field"
|
|
|
|
|
- placeholder="选择元数据"
|
|
|
|
|
- style="width: 100%"
|
|
|
|
|
- clearable
|
|
|
|
|
- :disabled="!searchForm.kb_id"
|
|
|
|
|
- >
|
|
|
|
|
- <!-- 这里的元数据字段应该是根据知识库动态获取的,暂时写死或留空 -->
|
|
|
|
|
- <el-option label="类型" value="type" />
|
|
|
|
|
- <el-option label="作者" value="author" />
|
|
|
|
|
- <el-option label="来源" value="source" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="form-item meta-value">
|
|
|
|
|
- <div class="label">元素字典值</div>
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="searchForm.metadata_value"
|
|
|
|
|
- placeholder="输入字典值"
|
|
|
|
|
- :disabled="!searchForm.metadata_field"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="search-form-container">
|
|
|
|
|
+ <!-- 顶部行:知识库 + 检索模式 + 关键字 -->
|
|
|
|
|
+ <div class="search-form-row main-row">
|
|
|
|
|
+ <div class="form-item kb-select">
|
|
|
|
|
+ <div class="label">知识库</div>
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="searchForm.kb_id"
|
|
|
|
|
+ placeholder="选择知识库"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ @change="handleKbChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="kb in kbList"
|
|
|
|
|
+ :key="kb.id"
|
|
|
|
|
+ :label="kb.name"
|
|
|
|
|
+ :value="kb.collection_name"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-item search-mode">
|
|
|
|
|
+ <div class="label">检索模式</div>
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="searchForm.mode"
|
|
|
|
|
+ placeholder="请选择"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ @change="handleModeChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option label="简单模式" value="simple" />
|
|
|
|
|
+ <el-option label="高级模式" value="advanced" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div class="form-item keyword-input">
|
|
|
|
|
- <div class="label">检索关键字</div>
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="searchForm.query"
|
|
|
|
|
- placeholder="输入检索关键字"
|
|
|
|
|
- clearable
|
|
|
|
|
- @keyup.enter="handleSearch"
|
|
|
|
|
- :disabled="!searchForm.kb_id"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div class="form-item keyword-input">
|
|
|
|
|
+ <div class="label">检索关键字</div>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="searchForm.query"
|
|
|
|
|
+ placeholder="请输入检索内容..."
|
|
|
|
|
+ clearable
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ :disabled="!searchForm.kb_id"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #append>
|
|
|
|
|
+ <el-button :icon="Search" @click="handleSearch" :disabled="!searchForm.kb_id">
|
|
|
|
|
+ 检索
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="search-btn">
|
|
|
|
|
- <el-button type="primary" :icon="Search" @click="handleSearch" :disabled="!searchForm.kb_id">
|
|
|
|
|
- 检索
|
|
|
|
|
- </el-button>
|
|
|
|
|
|
|
+ <!-- 高级过滤区域 (仅在高级模式显示) -->
|
|
|
|
|
+ <div v-if="searchForm.mode === 'advanced'" class="advanced-filter-area">
|
|
|
|
|
+ <div class="filter-header">
|
|
|
|
|
+ <span class="filter-title">元数据过滤条件</span>
|
|
|
|
|
+ <el-button type="primary" link :icon="Plus" size="small" @click="addFilter">添加条件</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="filter-list">
|
|
|
|
|
+ <div v-for="(filter, index) in searchForm.filters" :key="index" class="filter-item">
|
|
|
|
|
+ <el-row :gutter="10" align="middle">
|
|
|
|
|
+ <el-col :span="10">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="filter.field"
|
|
|
|
|
+ placeholder="选择字段"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ :disabled="!searchForm.kb_id"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="field in metadataFields"
|
|
|
|
|
+ :key="field.id"
|
|
|
|
|
+ :label="field.field_zh_name + ' (' + field.field_en_name + ')'"
|
|
|
|
|
+ :value="field.field_en_name"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="1" style="text-align: center; color: #909399;">=</el-col>
|
|
|
|
|
+ <el-col :span="11">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="filter.value"
|
|
|
|
|
+ placeholder="输入值"
|
|
|
|
|
+ :disabled="!filter.field"
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="2" style="text-align: center;">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="searchForm.filters.length > 1"
|
|
|
|
|
+ type="danger"
|
|
|
|
|
+ link
|
|
|
|
|
+ :icon="Delete"
|
|
|
|
|
+ @click="removeFilter(index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-card>
|
|
</el-card>
|
|
@@ -153,13 +193,14 @@
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
|
-import { Search } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
+import { Search, Plus, Delete } from '@element-plus/icons-vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
-import { getKnowledgeBases, type KnowledgeBase } from '@/api/knowledge-base'
|
|
|
|
|
|
|
+import { getKnowledgeBases, getKnowledgeBaseMetadata, type KnowledgeBase } from '@/api/knowledge-base'
|
|
|
import { searchKnowledgeBase, type KBSearchResultItem } from '@/api/search-engine'
|
|
import { searchKnowledgeBase, type KBSearchResultItem } from '@/api/search-engine'
|
|
|
|
|
|
|
|
// Data
|
|
// Data
|
|
|
const kbList = ref<KnowledgeBase[]>([])
|
|
const kbList = ref<KnowledgeBase[]>([])
|
|
|
|
|
+const metadataFields = ref<any[]>([]) // Store available metadata fields for selected KB
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
const hasSearched = ref(false)
|
|
const hasSearched = ref(false)
|
|
|
const tableData = ref<KBSearchResultItem[]>([])
|
|
const tableData = ref<KBSearchResultItem[]>([])
|
|
@@ -169,8 +210,8 @@ const pageSize = ref(10)
|
|
|
|
|
|
|
|
const searchForm = reactive({
|
|
const searchForm = reactive({
|
|
|
kb_id: '',
|
|
kb_id: '',
|
|
|
- metadata_field: '',
|
|
|
|
|
- metadata_value: '',
|
|
|
|
|
|
|
+ mode: 'simple',
|
|
|
|
|
+ filters: [{ field: '', value: '' }] as { field: string, value: string }[],
|
|
|
query: ''
|
|
query: ''
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -178,6 +219,21 @@ const detailVisible = ref(false)
|
|
|
const currentDetail = ref<KBSearchResultItem | null>(null)
|
|
const currentDetail = ref<KBSearchResultItem | null>(null)
|
|
|
|
|
|
|
|
// Methods
|
|
// Methods
|
|
|
|
|
+const handleModeChange = () => {
|
|
|
|
|
+ // Reset advanced fields when switching to simple
|
|
|
|
|
+ if (searchForm.mode === 'simple') {
|
|
|
|
|
+ searchForm.filters = [{ field: '', value: '' }]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const addFilter = () => {
|
|
|
|
|
+ searchForm.filters.push({ field: '', value: '' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const removeFilter = (index: number) => {
|
|
|
|
|
+ searchForm.filters.splice(index, 1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const loadKBs = async () => {
|
|
const loadKBs = async () => {
|
|
|
try {
|
|
try {
|
|
|
const res = await getKnowledgeBases({ page: 1, page_size: 100 }) // Load all KBs (simplified)
|
|
const res = await getKnowledgeBases({ page: 1, page_size: 100 }) // Load all KBs (simplified)
|
|
@@ -187,11 +243,28 @@ const loadKBs = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const handleKbChange = () => {
|
|
|
|
|
|
|
+const handleKbChange = async () => {
|
|
|
// Reset search when KB changes
|
|
// Reset search when KB changes
|
|
|
hasSearched.value = false
|
|
hasSearched.value = false
|
|
|
tableData.value = []
|
|
tableData.value = []
|
|
|
total.value = 0
|
|
total.value = 0
|
|
|
|
|
+
|
|
|
|
|
+ // Reset metadata selection
|
|
|
|
|
+ searchForm.filters = [{ field: '', value: '' }]
|
|
|
|
|
+ metadataFields.value = []
|
|
|
|
|
+
|
|
|
|
|
+ if (searchForm.kb_id) {
|
|
|
|
|
+ // Find selected KB object to get ID (kb_id in form is collection_name)
|
|
|
|
|
+ const selectedKb = kbList.value.find(k => k.collection_name === searchForm.kb_id)
|
|
|
|
|
+ if (selectedKb) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getKnowledgeBaseMetadata(selectedKb.id)
|
|
|
|
|
+ metadataFields.value = res.data
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("Failed to load metadata fields", error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleSearch = async () => {
|
|
const handleSearch = async () => {
|
|
@@ -208,21 +281,36 @@ const handleSearch = async () => {
|
|
|
hasSearched.value = true
|
|
hasSearched.value = true
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 处理多重过滤
|
|
|
|
|
+ // 后端目前可能只支持单一 metadata_field/value,或者我们需要修改后端接口支持 filters 数组
|
|
|
|
|
+ // 这里我们先转换成后端能理解的格式,或者假设后端已更新
|
|
|
|
|
+ // 假设后端接口 searchKnowledgeBase 支持 filters 参数: { field: string, value: string }[]
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤掉空的条件
|
|
|
|
|
+ const validFilters = searchForm.mode === 'advanced'
|
|
|
|
|
+ ? searchForm.filters.filter(f => f.field && f.value)
|
|
|
|
|
+ : []
|
|
|
|
|
+
|
|
|
const res = await searchKnowledgeBase({
|
|
const res = await searchKnowledgeBase({
|
|
|
kb_id: searchForm.kb_id,
|
|
kb_id: searchForm.kb_id,
|
|
|
- query: searchForm.query || '', // Allow empty query for "list all" or similar if supported
|
|
|
|
|
- metadata_field: searchForm.metadata_field || undefined,
|
|
|
|
|
- metadata_value: searchForm.metadata_value || undefined,
|
|
|
|
|
- top_k: pageSize.value, // Simplified pagination logic for vector search
|
|
|
|
|
- // Note: Real vector search pagination is complex. Here we just fetch top K.
|
|
|
|
|
- // If we want real paging, we need to ask backend for offset, but Milvus usually just does top_k.
|
|
|
|
|
- // For UI consistency, we just use pageSize as top_k here for demo.
|
|
|
|
|
|
|
+ query: searchForm.query || '',
|
|
|
|
|
+ // 传递 filters 数组 (需要后端支持,或者我们在前端做兼容处理)
|
|
|
|
|
+ // 为了兼容旧接口,如果只有一个 filter,传旧参数;如果有多个,传新参数 filters
|
|
|
|
|
+ metadata_field: validFilters.length === 1 ? validFilters[0].field : undefined,
|
|
|
|
|
+ metadata_value: validFilters.length === 1 ? validFilters[0].value : undefined,
|
|
|
|
|
+ filters: validFilters.length > 1 ? validFilters : undefined,
|
|
|
|
|
+
|
|
|
|
|
+ top_k: pageSize.value,
|
|
|
|
|
+ page: currentPage.value,
|
|
|
|
|
+ page_size: pageSize.value,
|
|
|
|
|
+ metric_type: 'hybrid',
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
tableData.value = res.data.results
|
|
tableData.value = res.data.results
|
|
|
total.value = res.data.total
|
|
total.value = res.data.total
|
|
|
- } catch (error) {
|
|
|
|
|
|
|
+ } catch (error: any) {
|
|
|
console.error(error)
|
|
console.error(error)
|
|
|
|
|
+ ElMessage.error(error.message || '检索失败,请检查配置或稍后重试')
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.value = false
|
|
loading.value = false
|
|
|
}
|
|
}
|
|
@@ -292,18 +380,72 @@ onMounted(() => {
|
|
|
align-items: flex-end;
|
|
align-items: flex-end;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.form-item {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
|
|
+.search-form-row.main-row {
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kb-select {
|
|
|
|
|
+ flex: 0 0 250px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-mode {
|
|
|
|
|
+ flex: 0 0 150px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.keyword-input {
|
|
|
|
|
+ flex: 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.form-item .label {
|
|
.form-item .label {
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
color: #606266;
|
|
color: #606266;
|
|
|
margin-bottom: 8px;
|
|
margin-bottom: 8px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.advanced-filter-area {
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ padding-top: 20px;
|
|
|
|
|
+ border-top: 1px dashed #dcdfe6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-title {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-item {
|
|
|
|
|
+ width: calc(33.33% - 8px); /* 一行三个 */
|
|
|
|
|
+ background-color: #f5f7fa;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media screen and (max-width: 1200px) {
|
|
|
|
|
+ .filter-item {
|
|
|
|
|
+ width: calc(50% - 6px); /* 一行两个 */
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.search-btn {
|
|
|
|
|
- margin-bottom: 1px;
|
|
|
|
|
|
|
+@media screen and (max-width: 768px) {
|
|
|
|
|
+ .filter-item {
|
|
|
|
|
+ width: 100%; /* 一行一个 */
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.result-card {
|
|
.result-card {
|