feat: 收敛知识库检索调度与评分语义

- 固定 rag.engine 与 Milvus 配置,补齐启动期检索基础设施校验

- 支持调用方配置 retrievalMode,并统一知识库检索入口与结果来源展示

- 修正关键词检索 knowledgeId 过滤、混合检索评分归一化与本地 ES 默认配置
This commit is contained in:
2026-04-05 20:23:05 +08:00
parent 2592a1f09d
commit b5dd427920
41 changed files with 1260 additions and 600 deletions

View File

@@ -49,6 +49,12 @@
"publicChatCopySuccess": "Copied",
"publicChatCopyFail": "Copy failed",
"basicInfo": "Basic Info",
"knowledgeRetrievalMode": "Retrieval Mode",
"retrievalModes": {
"hybrid": "Hybrid Retrieval",
"vector": "Vector Retrieval",
"keyword": "Keyword Retrieval"
},
"modal": {
"createDescription": "Set the assistant appearance, identity and base availability.",
"editDescription": "Update the assistant presentation and base availability.",

View File

@@ -159,7 +159,13 @@
"documentPreview": "DocumentPreview",
"total": "Total",
"segments": "Segments",
"similarityScore": "SimilarityScore",
"similarityScore": "Relevance",
"searchFailed": "Search failed. Please try again later.",
"hitSources": {
"vector": "Vector Hit",
"keyword": "Keyword Hit",
"both": "Dual Hit"
},
"alibabaCloud": "AlibabaCloud",
"tencentCloud": "tencentCloud",
"vectorEmbedModelTips": "After successful vector data, it is not allowed to modify the vector model",

View File

@@ -11,6 +11,7 @@
"button": {
"save": "Save Configuration"
},
"engineHint": "The keyword search engine is controlled by platform-level configuration and is no longer configured per knowledge base.",
"message": {
"saveSuccess": "Configuration saved successfully",
"saveFailed": "Configuration saved failed"

View File

@@ -49,6 +49,12 @@
"publicChatCopySuccess": "复制成功",
"publicChatCopyFail": "复制失败",
"basicInfo": "基础信息",
"knowledgeRetrievalMode": "检索方式",
"retrievalModes": {
"hybrid": "混合检索",
"vector": "向量检索",
"keyword": "关键词检索"
},
"modal": {
"createDescription": "设置助手的外观、标识和基础发布状态。",
"editDescription": "更新助手的展示信息与基础状态。",

View File

@@ -159,7 +159,13 @@
"documentPreview": "文档预览",
"total": "共",
"segments": "个分段",
"similarityScore": "相度",
"similarityScore": "相度",
"searchFailed": "检索失败,请稍后重试",
"hitSources": {
"vector": "语义命中",
"keyword": "关键词命中",
"both": "双路命中"
},
"alibabaCloud": "阿里云",
"tencentCloud": "腾讯云",
"vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型",

View File

@@ -11,6 +11,7 @@
"button": {
"save": "保存配置"
},
"engineHint": "关键词检索引擎由平台全局配置决定,知识库侧不再单独配置。",
"message": {
"saveSuccess": "配置保存成功",
"saveFailed": "配置保存失败"

View File

@@ -18,6 +18,7 @@ import {
import { useDebounceFn } from '@vueuse/core';
import {
ElAlert,
ElAvatar,
ElButton,
ElCol,
ElCollapse,
@@ -26,6 +27,7 @@ import {
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElOption,
ElRow,
ElSelect,
@@ -63,6 +65,13 @@ interface ApiKeyOption {
label: string;
}
type RetrievalMode = 'HYBRID' | 'KEYWORD' | 'VECTOR';
interface BotKnowledgeBindingItem {
knowledgeId: string;
retrievalMode: RetrievalMode;
}
const props = defineProps<{
bot?: BotInfo;
hasSavePermission?: boolean;
@@ -70,6 +79,7 @@ const props = defineProps<{
const botStore = useBotStore();
const route = useRoute();
const botId = ref<string>((route.params.id as string) || '');
const fallbackAvatarUrl = `${import.meta.env.BASE_URL || '/'}favicon.svg`;
const options = ref<AiLlm[]>([]);
const selectedId = ref<string>('');
const llmConfig = ref({
@@ -170,11 +180,11 @@ const iframeCodeHighlighted = computed(() => {
}
const escaped = escapeHtml(iframeCode.value);
return escaped
.replace(
.replaceAll(
/(&lt;\/?)([a-zA-Z][\w-]*)/g,
'$1<span class="hljs-name">$2</span>',
)
.replace(
.replaceAll(
/([:@a-zA-Z_][\w:-]*)=(&quot;.*?&quot;)/g,
'<span class="hljs-attr">$1</span>=<span class="hljs-string">$2</span>',
);
@@ -202,6 +212,32 @@ const mcpToolData = ref<any>([]);
const workflowData = ref<any[]>([]);
const knowledgeData = ref<any[]>([]);
const pluginToolData = ref<any[]>([]);
const retrievalModeOptions = computed(() => [
{ label: $t('bot.retrievalModes.hybrid'), value: 'HYBRID' },
{ label: $t('bot.retrievalModes.vector'), value: 'VECTOR' },
{ label: $t('bot.retrievalModes.keyword'), value: 'KEYWORD' },
]);
const normalizeRetrievalMode = (value?: string): RetrievalMode => {
if (value === 'VECTOR' || value === 'KEYWORD' || value === 'HYBRID') {
return value;
}
return 'HYBRID';
};
const buildKnowledgeBindingsPayload = (
selectedIds: Array<number | string>,
): BotKnowledgeBindingItem[] => {
return [...new Set((selectedIds || []).map(String).filter(Boolean))].map(
(knowledgeId) => {
const existing = knowledgeData.value.find(
(item) => String(item.id) === knowledgeId,
);
return {
knowledgeId,
retrievalMode: normalizeRetrievalMode(existing?.retrievalMode),
};
},
);
};
const getAiBotPluginToolList = async () => {
api
.post('/api/v1/pluginItem/tool/list', { botId: botId.value })
@@ -226,6 +262,7 @@ const getAiBotKnowledgeList = async () => {
knowledgeData.value = res.data.map((item: any) => {
return {
recordId: item.id,
retrievalMode: normalizeRetrievalMode(item.options?.retrievalMode),
...item.knowledge,
};
});
@@ -419,7 +456,7 @@ const copyText = async (value: string) => {
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
document.body.appendChild(textarea);
document.body.append(textarea);
const selection = document.getSelection();
const previousRange =
@@ -430,7 +467,7 @@ const copyText = async (value: string) => {
textarea.setSelectionRange(0, textarea.value.length);
const copied = document.execCommand('copy');
document.body.removeChild(textarea);
textarea.remove();
if (selection) {
selection.removeAllRanges();
if (previousRange) {
@@ -543,7 +580,7 @@ const confirmUpdateAiBotKnowledge = (data: any) => {
api
.post('/api/v1/botKnowledge/updateBotKnowledgeIds', {
botId: botId.value,
knowledgeIds: data,
knowledgeBindings: buildKnowledgeBindingsPayload(data),
})
.then((res) => {
if (res.errorCode === 0) {
@@ -555,6 +592,31 @@ const confirmUpdateAiBotKnowledge = (data: any) => {
});
};
const updateKnowledgeBindings = (bindings: BotKnowledgeBindingItem[]) => {
return api.post('/api/v1/botKnowledge/updateBotKnowledgeIds', {
botId: botId.value,
knowledgeBindings: bindings,
});
};
const handleKnowledgeRetrievalModeChange = async (
item: any,
value: RetrievalMode | string,
) => {
item.retrievalMode = normalizeRetrievalMode(value);
const bindings = knowledgeData.value.map((knowledgeItem: any) => ({
knowledgeId: String(knowledgeItem.id),
retrievalMode: normalizeRetrievalMode(knowledgeItem.retrievalMode),
}));
const res = await updateKnowledgeBindings(bindings);
if (res.errorCode === 0) {
ElMessage.success($t('message.updateOkMessage'));
getAiBotKnowledgeList();
return;
}
ElMessage.error(res.message || $t('message.updateFailMessage'));
};
const confirmUpdateAiBotWorkflow = (data: any) => {
api
.post('/api/v1/botWorkflow/updateBotWorkflowIds', {
@@ -616,7 +678,20 @@ const deleteBotMcpTool = (item: any) => {
});
};
const deleteKnowledge = (item: any) => {
const deleteKnowledge = async (item: any) => {
try {
await ElMessageBox.confirm(
$t('message.deleteAlert'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
api
.post('/api/v1/botKnowledge/remove', {
id: item.recordId,
@@ -840,10 +915,10 @@ const handleBasicInfoChange = async (
<div class="bot-avatar-panel">
<span class="bot-avatar-label">{{ $t('common.avatar') }}</span>
<div
:class="[
'bot-avatar-upload-wrap',
!hasSavePermission || updatingBotIcon ? 'is-disabled' : '',
]"
class="bot-avatar-upload-wrap"
:class="
!hasSavePermission || updatingBotIcon ? 'is-disabled' : ''
"
>
<UploadAvatar
v-if="botInfo"
@@ -1105,7 +1180,53 @@ const handleBasicInfoChange = async (
</div>
</div>
</template>
<CollapseViewItem :data="knowledgeData" @delete="deleteKnowledge" />
<div class="knowledge-binding-list">
<div
v-for="item in knowledgeData"
:key="item.recordId"
class="knowledge-binding-item"
>
<div class="knowledge-binding-main">
<ElAvatar
:src="item.icon || fallbackAvatarUrl"
shape="circle"
/>
<div class="knowledge-binding-content">
<div class="knowledge-binding-title">
{{ item.title }}
</div>
<div class="knowledge-binding-description">
{{ item.description }}
</div>
</div>
</div>
<div class="knowledge-binding-actions" @click.stop>
<ElSelect
:model-value="item.retrievalMode"
class="knowledge-binding-select"
size="small"
@change="
(value) => handleKnowledgeRetrievalModeChange(item, value)
"
>
<ElOption
v-for="option in retrievalModeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
<ElIcon
class="knowledge-binding-delete"
color="var(--el-color-danger)"
size="20px"
@click="deleteKnowledge(item)"
>
<Delete />
</ElIcon>
</div>
</div>
</div>
</ElCollapseItem>
<ElCollapseItem>
<template #title>
@@ -1779,4 +1900,64 @@ const handleBasicInfoChange = async (
height: 50px;
border-radius: 50%;
}
.knowledge-binding-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 10px;
background-color: var(--bot-collapse-itme-back);
}
.knowledge-binding-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px;
border-radius: 8px;
background-color: hsl(var(--background));
}
.knowledge-binding-main {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1;
}
.knowledge-binding-content {
min-width: 0;
}
.knowledge-binding-title {
font-size: 12px;
font-weight: 500;
line-height: 24px;
color: var(--el-text-color-primary);
}
.knowledge-binding-description {
font-size: 12px;
line-height: 20px;
color: var(--el-text-color-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.knowledge-binding-actions {
display: flex;
align-items: center;
gap: 12px;
}
.knowledge-binding-select {
width: 140px;
}
.knowledge-binding-delete {
cursor: pointer;
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import { computed, onMounted, ref, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import {
@@ -80,29 +80,6 @@ watch(
const embeddingLlmList = ref<any>([]);
const rerankerLlmList = ref<any>([]);
const vecotrDatabaseList = ref<any>([
{ value: 'milvus', label: 'Milvus' },
{ value: 'redis', label: 'Redis' },
{ value: 'opensearch', label: 'OpenSearch' },
{ value: 'elasticsearch', label: 'ElasticSearch' },
{ value: 'aliyun', label: $t('documentCollection.alibabaCloud') },
{ value: 'qcloud', label: $t('documentCollection.tencentCloud') },
]);
const milvusVectorStoreConfigPlaceholder =
'uri=http://127.0.0.1:19530\n' +
'databaseName=default\n' +
'token=\n' +
'username=\n' +
'password=\n' +
'autoCreateCollection=true';
const vectorStoreConfigPlaceholder = computed(() => {
return entity.value?.vectorStoreType === 'milvus'
? milvusVectorStoreConfigPlaceholder
: '';
});
const getEmbeddingLlmListData = async () => {
try {
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
@@ -146,15 +123,6 @@ const rules = ref({
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
vectorStoreType: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreCollection: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreConfig: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorEmbedModelId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
@@ -239,50 +207,6 @@ async function save() {
:placeholder="$t('documentCollection.placeholder.description')"
/>
</ElFormItem>
<!-- <ElFormItem
prop="vectorStoreEnable"
:label="$t('documentCollection.vectorStoreEnable')"
>
<ElSwitch v-model="entity.vectorStoreEnable" />
</ElFormItem>-->
<ElFormItem
prop="vectorStoreType"
:label="$t('documentCollection.vectorStoreType')"
>
<ElSelect
v-model="entity.vectorStoreType"
:placeholder="$t('documentCollection.placeholder.vectorStoreType')"
>
<ElOption
v-for="item in vecotrDatabaseList"
:key="item.value"
:label="item.label"
:value="item.value || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="vectorStoreCollection"
:label="$t('documentCollection.vectorStoreCollection')"
>
<ElInput
v-model.trim="entity.vectorStoreCollection"
:placeholder="
$t('documentCollection.placeholder.vectorStoreCollection')
"
/>
</ElFormItem>
<ElFormItem
prop="vectorStoreConfig"
:label="$t('documentCollection.vectorStoreConfig')"
>
<ElInput
v-model.trim="entity.vectorStoreConfig"
:rows="4"
type="textarea"
:placeholder="vectorStoreConfigPlaceholder"
/>
</ElFormItem>
<ElFormItem prop="vectorEmbedModelId">
<template #label>
<span style="display: flex; align-items: center">
@@ -373,12 +297,6 @@ async function save() {
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="searchEngineEnable"
:label="$t('documentCollection.searchEngineEnable')"
>
<ElSwitch v-model="entity.searchEngineEnable" />
</ElFormItem>
<ElFormItem style="margin-top: 20px; text-align: right">
<ElButton
type="primary"

View File

@@ -60,15 +60,6 @@ onMounted(async () => {
const saveForm = ref<FormInstance>();
const dialogVisible = ref(false);
const isAdd = ref(true);
const vecotrDatabaseList = ref<any>([
{ value: 'milvus', label: 'Milvus' },
{ value: 'redis', label: 'Redis' },
{ value: 'opensearch', label: 'OpenSearch' },
{ value: 'elasticsearch', label: 'ElasticSearch' },
{ value: 'aliyun', label: $t('documentCollection.alibabaCloud') },
{ value: 'qcloud', label: $t('documentCollection.tencentCloud') },
]);
const defaultEntity = {
collectionType: 'DOCUMENT',
alias: '',
@@ -125,15 +116,6 @@ const rules = ref({
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
vectorStoreType: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreCollection: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreConfig: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorEmbedModelId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
@@ -307,49 +289,6 @@ defineExpose({
:placeholder="$t('documentCollection.placeholder.description')"
/>
</ElFormItem>
<!-- <ElFormItem
prop="vectorStoreEnable"
:label="$t('documentCollection.vectorStoreEnable')"
>
<ElSwitch v-model="entity.vectorStoreEnable" />
</ElFormItem>-->
<ElFormItem
prop="vectorStoreType"
:label="$t('documentCollection.vectorStoreType')"
>
<ElSelect
v-model="entity.vectorStoreType"
:placeholder="$t('documentCollection.placeholder.vectorStoreType')"
>
<ElOption
v-for="item in vecotrDatabaseList"
:key="item.value"
:label="item.label"
:value="item.value || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="vectorStoreCollection"
:label="$t('documentCollection.vectorStoreCollection')"
>
<ElInput
v-model.trim="entity.vectorStoreCollection"
:placeholder="
$t('documentCollection.placeholder.vectorStoreCollection')
"
/>
</ElFormItem>
<ElFormItem
prop="vectorStoreConfig"
:label="$t('documentCollection.vectorStoreConfig')"
>
<ElInput
v-model.trim="entity.vectorStoreConfig"
:rows="4"
type="textarea"
/>
</ElFormItem>
<ElFormItem prop="vectorEmbedModelId">
<template #label>
<span style="display: flex; align-items: center">
@@ -440,12 +379,6 @@ defineExpose({
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="searchEngineEnable"
:label="$t('documentCollection.searchEngineEnable')"
>
<ElSwitch v-model="entity.searchEngineEnable" />
</ElFormItem>
</ElForm>
</EasyFlowFormModal>
</template>

View File

@@ -3,20 +3,40 @@ import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { ElButton, ElInput, ElMessage } from 'element-plus';
import { ElButton, ElInput, ElMessage, ElOption, ElSelect } from 'element-plus';
import { api } from '#/api/request';
import PreviewSearchKnowledge from '#/views/ai/documentCollection/PreviewSearchKnowledge.vue';
type RetrievalMode = 'HYBRID' | 'KEYWORD' | 'VECTOR';
interface SearchResultItem {
sorting: number;
content: string;
score?: number;
hitSource?: 'BOTH' | 'KEYWORD' | 'VECTOR';
vectorScore?: number;
keywordScore?: number;
}
const props = defineProps({
knowledgeId: {
type: String,
required: true,
},
});
const searchDataList = ref([]);
const searchDataList = ref<SearchResultItem[]>([]);
const keyword = ref('');
const retrievalMode = ref<RetrievalMode>('HYBRID');
const previewSearchKnowledgeRef = ref();
const retrievalModeOptions = [
{ label: $t('bot.retrievalModes.hybrid'), value: 'HYBRID' },
{ label: $t('bot.retrievalModes.vector'), value: 'VECTOR' },
{ label: $t('bot.retrievalModes.keyword'), value: 'KEYWORD' },
];
const handleSearch = () => {
if (!keyword.value) {
ElMessage.error($t('message.pleaseInputContent'));
@@ -24,12 +44,22 @@ const handleSearch = () => {
}
previewSearchKnowledgeRef.value.loadingContent(true);
api
.get(
`/api/v1/documentCollection/search?knowledgeId=${props.knowledgeId}&keyword=${keyword.value}`,
)
.get('/api/v1/documentCollection/search', {
params: {
knowledgeId: props.knowledgeId,
keyword: keyword.value,
retrievalMode: retrievalMode.value,
},
})
.then((res) => {
previewSearchKnowledgeRef.value.loadingContent(false);
searchDataList.value = res.data;
})
.catch(() => {
ElMessage.error($t('documentCollection.searchFailed'));
searchDataList.value = [];
})
.finally(() => {
previewSearchKnowledgeRef.value.loadingContent(false);
});
};
</script>
@@ -40,7 +70,16 @@ const handleSearch = () => {
<ElInput
v-model="keyword"
:placeholder="$t('common.searchPlaceholder')"
@keyup.enter="handleSearch"
/>
<ElSelect v-model="retrievalMode" class="retrieval-select">
<ElOption
v-for="item in retrievalModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
<ElButton type="primary" @click="handleSearch">
{{ $t('button.query') }}
</ElButton>
@@ -48,6 +87,7 @@ const handleSearch = () => {
<div class="search-result">
<PreviewSearchKnowledge
:data="searchDataList"
:retrieval-mode="retrievalMode"
ref="previewSearchKnowledgeRef"
/>
</div>
@@ -68,6 +108,12 @@ const handleSearch = () => {
justify-content: space-between;
gap: 12px;
}
.retrieval-select {
width: 180px;
flex-shrink: 0;
}
.search-result {
padding-top: 20px;
flex: 1;

View File

@@ -10,9 +10,6 @@ import {
ElFormItem,
ElInputNumber,
ElMessage,
ElOption,
ElSelect,
ElSwitch,
ElTooltip,
} from 'element-plus';
@@ -32,7 +29,6 @@ const props = defineProps({
onMounted(() => {
getDocumentCollectionConfig();
});
const searchEngineEnable = ref(false);
const baseOptions = ref<Record<string, any>>({});
const getDocumentCollectionConfig = () => {
api
@@ -46,16 +42,13 @@ const getDocumentCollectionConfig = () => {
: 5;
searchConfig.simThreshold = options.simThreshold
? Number(options.simThreshold)
: 0.5;
searchConfig.searchEngineType = options.searchEngineType || 'lucene';
searchEngineEnable.value = !!data.searchEngineEnable;
: 0.6;
});
};
const searchConfig = reactive({
docRecallMaxNum: 5,
simThreshold: 0.5,
searchEngineType: 'lucene',
simThreshold: 0.6,
});
const submitConfig = () => {
@@ -69,9 +62,7 @@ const submitConfig = () => {
...baseOptions.value,
docRecallMaxNum: searchConfig.docRecallMaxNum,
simThreshold: searchConfig.simThreshold,
searchEngineType: searchConfig.searchEngineType,
},
searchEngineEnable: searchEngineEnable.value,
};
api
@@ -85,26 +76,6 @@ const submitConfig = () => {
console.error('保存配置失败:', error);
});
};
const searchEngineOptions = [
{
label: 'Lucene',
value: 'lucene',
},
{
label: 'ElasticSearch',
value: 'elasticSearch',
},
];
const handleSearchEngineEnableChange = () => {
if (!props.manageable) {
return;
}
api.post('/api/v1/documentCollection/update', {
id: props.documentCollectionId,
searchEngineEnable: searchEngineEnable.value,
});
};
</script>
<template>
@@ -176,71 +147,12 @@ const handleSearchEngineEnableChange = () => {
/>
</div>
</ElFormItem>
<!-- 搜索引擎启用开关 -->
<ElFormItem class="form-item">
<div class="form-item-label">
<span>{{
$t('documentCollectionSearch.searchEngineEnable.label')
}}</span>
<ElTooltip
:content="$t('documentCollectionSearch.searchEngineEnable.tooltip')"
placement="top"
effect="dark"
class="label-tooltip"
>
<InfoFilled class="info-icon" />
</ElTooltip>
</div>
<div class="form-item-content">
<ElSwitch
v-model="searchEngineEnable"
@change="handleSearchEngineEnableChange"
:active-text="$t('documentCollectionSearch.switch.on')"
:inactive-text="$t('documentCollectionSearch.switch.off')"
class="form-control switch-control"
/>
</div>
</ElFormItem>
<!-- 通过 searchEngineEnable 控制显示/隐藏 -->
<ElFormItem
v-if="searchEngineEnable"
prop="searchEngineType"
class="form-item"
>
<div class="form-item-label">
<span>{{
$t('documentCollectionSearch.searchEngineType.label')
}}</span>
<ElTooltip
:content="$t('documentCollectionSearch.searchEngineType.tooltip')"
placement="top"
effect="dark"
class="label-tooltip"
>
<InfoFilled class="info-icon" />
</ElTooltip>
</div>
<div class="form-item-content">
<ElSelect
v-model="searchConfig.searchEngineType"
:placeholder="
$t('documentCollectionSearch.searchEngineType.placeholder')
"
class="form-control"
>
<ElOption
v-for="option in searchEngineOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
</div>
</ElFormItem>
</ElForm>
<div class="config-hint">
{{ $t('documentCollectionSearch.engineHint') }}
</div>
<div class="config-footer">
<ElButton
type="primary"
@@ -352,6 +264,13 @@ const handleSearchEngineEnableChange = () => {
padding: 8px 0;
}
.config-hint {
margin: 12px 0 4px;
font-size: 12px;
line-height: 1.6;
color: var(--el-text-color-secondary);
}
.config-footer {
position: sticky;
bottom: 0;

View File

@@ -1,14 +1,21 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { Document } from '@element-plus/icons-vue';
import { ElButton, ElIcon } from 'element-plus';
// 定义类型接口(与 React 版本一致)
import { ElButton, ElIcon, ElTag } from 'element-plus';
type RetrievalMode = 'HYBRID' | 'KEYWORD' | 'VECTOR';
type HitSource = 'BOTH' | 'KEYWORD' | 'VECTOR';
interface PreviewItem {
sorting: string;
sorting: number | string;
content: string;
score: string;
score?: number | string;
hitSource?: HitSource;
}
const props = defineProps({
hideScore: {
type: Boolean,
@@ -46,8 +53,37 @@ const props = defineProps({
type: Boolean,
default: false,
},
retrievalMode: {
type: String as () => RetrievalMode,
default: 'HYBRID',
},
});
const loadingStatus = ref(false);
const resolveHitSourceLabel = (hitSource?: HitSource) => {
if (hitSource === 'VECTOR') {
return $t('documentCollection.hitSources.vector');
}
if (hitSource === 'KEYWORD') {
return $t('documentCollection.hitSources.keyword');
}
if (hitSource === 'BOTH') {
return $t('documentCollection.hitSources.both');
}
return '';
};
const resolveHitSourceType = (hitSource?: HitSource) => {
if (hitSource === 'VECTOR') {
return 'success';
}
if (hitSource === 'KEYWORD') {
return 'warning';
}
return 'info';
};
defineExpose({
loadingContent: (state: boolean) => {
loadingStatus.value = state;
@@ -89,10 +125,22 @@ defineExpose({
{{ item.sorting ?? index + 1 }}
</div>
<div class="el-list-item-meta">
<div v-if="!hideScore">
<div v-if="!hideScore" class="score-text">
{{ $t('documentCollection.similarityScore') }}: {{ item.score }}
</div>
<div class="content-desc">{{ item.content }}</div>
<div
v-if="retrievalMode === 'HYBRID' && item.hitSource"
class="hit-source-row"
>
<ElTag
size="small"
effect="plain"
:type="resolveHitSourceType(item.hitSource)"
>
{{ resolveHitSourceLabel(item.hitSource) }}
</ElTag>
</div>
</div>
</div>
</div>
@@ -203,10 +251,20 @@ defineExpose({
}
}
.score-text {
color: var(--el-text-color-secondary);
}
.hit-source-row {
display: flex;
justify-content: flex-end;
width: 100%;
}
.el-list-item {
display: flex;
gap: 12px;
align-items: center;
align-items: flex-start;
padding: 18px;
border-radius: 8px;
}

View File

@@ -24,6 +24,11 @@
});
const options = getOptions();
const retrievalModeOptions: SelectItem[] = [
{ value: 'HYBRID', label: '混合检索' },
{ value: 'VECTOR', label: '向量检索' },
{ value: 'KEYWORD', label: '关键词检索' }
];
let knowledgeArray = $state<SelectItem[]>([]);
onMount(async () => {
@@ -78,6 +83,16 @@
}
});
$effect(() => {
if (!data.retrievalMode) {
updateNodeData(currentNodeId, () => {
return {
retrievalMode: 'HYBRID'
};
});
}
});
</script>
@@ -134,6 +149,18 @@
/>
</div>
<div class="setting-title">检索方式</div>
<div class="setting-item">
<Select items={retrievalModeOptions} style="width: 100%" placeholder="请选择检索方式" onSelect={(item)=>{
const newValue = item.value;
updateNodeData(currentNodeId, ()=>{
return {
retrievalMode: newValue
}
})
}} value={data.retrievalMode ? [data.retrievalMode] : ['HYBRID']} />
</div>
<div class="setting-title">获取数据量</div>
<div class="setting-item">
@@ -184,4 +211,3 @@
}
</style>