feat(knowledge): 新增知识库检索设置功能

- 添加检索设置相关 API 和功能组件
- 实现嵌入式模型列表获取和选择
- 添加重新排序模型配置功能
- 实现权重设置和混合检索功能
- 优化检索设置界面布局和交互
This commit is contained in:
du.meimei
2025-05-07 17:09:04 +08:00
parent 45c84265ea
commit afd13f3b35
8 changed files with 303 additions and 90 deletions

View File

@@ -1,11 +1,11 @@
module.exports = { module.exports = {
presets: ['@vue/app'], presets: ['@vue/app'],
plugins: [ plugins: [
[ // [
'transform-remove-console', // 'transform-remove-console',
{ // {
exclude: ['warn', 'error'] // 可选:保留 warn 和 error // exclude: ['warn', 'error'] // 可选:保留 warn 和 error
} // }
] // ]
] ]
} }

View File

@@ -13,7 +13,7 @@ handle.set('layout', false)
* @param e {MessageEvent} * @param e {MessageEvent}
*/ */
self.onmessage = function initHandler(e) { self.onmessage = function initHandler(e) {
const {ports = [], data} = e const { ports = [], data } = e
// console.log(`Worker收到消息:`, data) // console.log(`Worker收到消息:`, data)
// 只处理初始化消息 // 只处理初始化消息
if (data !== 'init-worker' || ports.length === 0) { if (data !== 'init-worker' || ports.length === 0) {
@@ -36,7 +36,7 @@ self.onmessage = function initHandler(e) {
function messageHandler(event) { function messageHandler(event) {
// console.log('has been received event',event) // console.log('has been received event',event)
if (handle.has(event.data.type)){ if (handle.has(event.data.type)) {
self.port.postMessage(handle.get(event.data.type)) self.port.postMessage(handle.get(event.data.type))
} }
} }

View File

@@ -10,3 +10,22 @@ export function getRerankModels() {
method: 'get' method: 'get'
}) })
} }
/**
* @description 知识库检索模式配置
*/
export function retrievalSetting(data) {
return request({
url: getUrl('/datasets/detailConfigEx/retrievalSetting'),
method: 'post',
data
})
}
/**
* @description 查询系统支持的embedding模型列表
*/
export function getEmbeddingModels() {
return request({
url: getUrl('/third/models/text-embedding'),
method: 'get'
})
}

View File

@@ -1,7 +1,7 @@
let envInfo = process.env let envInfo = process.env
let [admin, jifen, zixi, hz] = [ let [admin, jifen, zixi, hz] = [
envInfo.VUE_APP_ADMIN, envInfo.VUE_APP_ADMIN,
'http://192.168.2.62:7196/', 'http://192.168.8.58:7196/',
'http://192.168.8.165:7196/', 'http://192.168.8.165:7196/',
'http://10.147.17.161:7196/' 'http://10.147.17.161:7196/'
] ]

View File

@@ -1,51 +1,31 @@
<template> <template>
<div> <div>
<el-drawer <el-drawer
title="索设置" title="索设置"
:visible.sync="visible" :visible.sync="visible"
size="50%" size="40%"
@close="handleClose" @close="handleClose"
> >
<el-form ref="form" :model="form" label-width="80%" label-position="top"> <el-form ref="form" :model="form" label-width="80%" label-position="top">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="10">
<el-form-item label="Embedding模型"> <el-form-item label="Embedding模型">
<el-select v-model="form.embeddingModel" placeholder="请选择"> <el-select
v-model="form.embeddingModel"
@change="getModel"
placeholder="请选择"
>
<el-option <el-option
v-for="item in modelList" v-for="item in modelList"
:key="item.value" :key="item.model"
:label="item.label" :label="item.label"
:value="item.value" :value="item.model"
></el-option> ></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<!-- <el-form-item label="检索设置"> <el-collapse v-model="searchMethod" accordion>
<div class="flex justify-content-b">
<div
v-for="(item, index) in settingList"
:key="item.name"
class="setting_item flex"
:class="{ activeItem: index === activeIndex }"
@click="handleClick(index)"
>
<img
src="@/assets/images/konwledge/knowledge-5.png"
alt=""
style="height: 40px"
/>
<div class="ml10">
<p class="fwb label fs15">
{{ item.name }}
<span v-if="index === 2" class="point fs12 ml10">推荐</span>
</p>
<p class="desc">{{ item.desc }}</p>
</div>
</div>
</div>
</el-form-item>-->
<el-collapse v-model="activeName" accordion>
<el-collapse-item <el-collapse-item
v-for="(item, index) in settingList" v-for="(item, index) in settingList"
:key="item.name" :key="item.name"
@@ -69,16 +49,28 @@
</div> </div>
</template> </template>
<div> <div>
<search-form :activeIndex="activeName"></search-form> <search-form
ref="searchForm"
:searchMethod="searchMethod"
:embeddingItem="embeddingItem"
@handleClose="handleClose"
></search-form>
</div> </div>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</el-form> </el-form>
<div class="demo-drawer__footer mt10 text-right">
<el-button @click="handleCancel" size="medium"> </el-button>
<el-button type="primary" @click="handleSubmit" size="medium"
>保存</el-button
>
</div>
</el-drawer> </el-drawer>
</div> </div>
</template> </template>
<script> <script>
import SearchForm from '@/views/knowledge/detail/components/SearchSetting/SearchForm.vue' import SearchForm from '@/views/knowledge/detail/components/SearchSetting/SearchForm.vue'
import { getEmbeddingModels } from '@/api/knowledge/hit-test'
export default { export default {
name: 'SearchSetting', name: 'SearchSetting',
@@ -86,7 +78,9 @@ export default {
props: {}, props: {},
data() { data() {
return { return {
activeName: 0, // 0-向量检索 1-全文检索 2-混合检索
searchMethod: 0,
embeddingItem: {},
visible: false, visible: false,
form: { form: {
embeddingModel: '', embeddingModel: '',
@@ -116,15 +110,38 @@ export default {
} }
} }
}, },
created() {
this.getModelList()
},
methods: { methods: {
// 获取Embedding模型列表
getModelList() {
getEmbeddingModels().then(res => {
if (res) {
const { content } = res.content
this.modelList = content.flatMap(item => item.models)
}
})
},
init() { init() {
this.visible = true this.visible = true
}, },
handleClose() { handleClose() {
this.$emit('close') this.visible = false
}, },
handleClick(index) { handleSubmit() {
this.activeIndex = index console.log(this.$refs.searchForm)
this.$refs.searchForm[this.searchMethod].submitData()
// this.$refs.searchForm.submitData()
},
handleCancel() {
this.visible = false
},
getModel() {
console.log(this.form.embeddingModel)
this.embeddingItem = this.modelList.find(
item => item.model === this.form.embeddingModel
)
} }
} }
} }

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="pl10 searchForm_cont"> <div class="pl10 searchForm_cont">
<el-form :model="form" label-width="80%" label-position="top"> <el-form :model="form" label-width="80%" label-position="top" class="pt20">
<el-form-item label=""> <el-form-item label="" v-if="searchMethod !== 2">
<el-switch v-model="form.modelOpen"></el-switch> <el-switch v-model="form.rerankingEnable"></el-switch>
<span>Rerank 模型</span> <span>Rerank 模型</span>
<el-tooltip <el-tooltip
class="item" class="item"
@@ -13,26 +13,80 @@
<i class="el-icon-info ml5" style="color: #909399;"></i> <i class="el-icon-info ml5" style="color: #909399;"></i>
</el-tooltip> </el-tooltip>
</el-form-item> </el-form-item>
<el-row v-if="activeIndex === 2"> <!-- 混合检索 -->
<el-slider v-model="value" :marks="marks"> </el-slider <div v-if="searchMethod === 2" class="flex mb20">
></el-row> <div
<el-row v-else> class="fixItem flex"
<el-col :span="8"> v-for="(item, index) in fixSearchList"
:key="index"
@click="chooseFixSearch(item)"
>
<img
src="@/assets/images/konwledge/knowledge-2.png"
alt=""
style="height: 30px"
/>
<div class="ml10">
<div class="flex justify-content-b">
<p>{{ item.name }}</p>
<i
class="el-icon-check"
v-if="item.isCheck"
style="color: #4f47f5"
></i>
</div>
<p>{{ item.desc }}</p>
</div>
</div>
</div>
<el-row
v-if="
(form.rerankingEnable && searchMethod !== 2) ||
(searchMethod === 2 && fixSearchList[1].isCheck === true)
"
>
<el-col :span="10">
<el-form-item label=""> <el-form-item label="">
<el-select v-model="form.embeddingModel" placeholder="请选择"> <el-select
v-model="form.rerankingModelName"
@change="getRankModel"
placeholder="请选择"
>
<el-option <el-option
v-for="item in modelList" v-for="item in rankModelList"
:key="item.value" :key="item.model"
:label="item.label" :label="item.label"
:value="item.value" :value="item.model"
></el-option> ></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row> <el-row v-if="searchMethod === 2 && fixSearchList[0].isCheck === true">
<RSlider
v-model="form.weights"
:hit-test="true"
:show-tooltip="false"
@input="getWeight"
style="width: 98%"
/>
</el-row>
<el-row :gutter="20">
<!-- Top K -->
<el-col :span="10" class="flex"> <el-col :span="10" class="flex">
<el-form-item label="Top K"> <el-form-item label="">
<div class="flex align-items-c">
<p class="ml5 fwb">Top K</p>
<el-tooltip
class="item"
effect="dark"
content="用于筛选与用户问题相似度最高的文档片段。系统同时会根据选用模型上下文窗口大小动态调整分段数量。"
placement="top"
>
<i class="el-icon-info ml5" style="color: #909399;"></i>
</el-tooltip>
</div>
<el-input-number <el-input-number
v-model="form.topK" v-model="form.topK"
:min="1" :min="1"
@@ -42,53 +96,169 @@
size="mini" size="mini"
></el-input-number> ></el-input-number>
</el-form-item> </el-form-item>
<div style="width: 300px;"> <div class="ml20 mt40" style="width: 80%;">
<el-slider v-model="value" :min="1" :max="10" :step="1"></el-slider> <el-slider
v-model="form.topK"
:min="1"
:max="10"
:step="1"
></el-slider>
</div> </div>
</el-col> </el-col>
</el-row> <!-- Score阈值-->
<el-row> <el-col :span="10" class="flex">
<el-col :span="10"> <el-form-item label="">
<el-form-item label="Score阈值"> <template slot="label">
<el-slider <div class="flex align-items-c">
v-model="value" <el-switch v-model="form.scoreThresholdEnabled"></el-switch>
show-input <p class="ml5 fwb">Score阈值</p>
show-input-controls <el-tooltip
class="item"
effect="dark"
content="用于设置文本片段筛选的相似度阈值"
placement="top"
>
<i class="el-icon-info ml5" style="color: #909399;"></i>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="form.scoreThreshold"
:min="0" :min="0"
:max="1" :max="1"
:step="0.01" :step="0.01"
:precision="2" label="Top K"
></el-slider> controls-position="right"
size="mini"
:disabled="!form.scoreThresholdEnabled"
></el-input-number>
</el-form-item> </el-form-item>
<div class="ml20 mt40" style="width: 80%;">
<el-slider
v-model="form.scoreThreshold"
:disabled="!form.scoreThresholdEnabled"
:min="0"
:max="1"
:step="0.01"
></el-slider>
</div>
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
</div> </div>
</template> </template>
<script> <script>
import { getRerankModels, retrievalSetting } from '@/api/knowledge/hit-test'
export default { export default {
name: 'SearchForm', name: 'SearchForm',
props: { props: {
activeIndex: { searchMethod: {
type: Number type: Number
},
embeddingItem: {
type: Object
} }
}, },
data() { data() {
return { return {
form: { form: {
modelOpen: true, // 是否启用rerank模型
embeddingModel: 'sentence-transformers/all-MiniLM-L6-v2', rerankingEnable: true,
topK: 3, // Rerank模型
scoreThreshold: 0.5 rerankingModelName: '',
rerankingProviderName: '',
topK: 0,
// 是否启用score阈值
scoreThresholdEnabled: false,
// score阈值
scoreThreshold: 0,
// 混合模式- reranking_model:使用rerank模型; weighted_score使用权重
rerankingMode: '',
// 关键词
keywordWeight: 0,
// 语义
vectorWeight: 0
}, },
modelList: [ rankModelList: [
{ {
value: 'sentence-transformers/all-MiniLM-L6-v2', value: 'sentence-transformers/all-MiniLM-L6-v2',
label: 'sentence-transformers/all-MiniLM-L6-v2' label: 'sentence-transformers/all-MiniLM-L6-v2'
}, },
{} {}
],
fixSearchList: [
{
name: '权重设置',
desc:
'通过调整分配的权重,重新排序策略确定是优先进行语义匹配还是关键字匹配',
isCheck: true,
value: 'weighted_score'
},
{
name: 'Rerank 模型',
value: 'reranking_model',
desc:
'重排序模型将根据候选文档列表与用户问题语义匹配度进行重新排序,从而改进语义排序的结果'
}
] ]
} }
},
created() {
this.getRerankModelList()
},
methods: {
getRerankModelList() {
getRerankModels().then(res => {
if (res) {
const { content } = res.content
this.rankModelList = content.flatMap(item => item.models)
}
})
},
getRankModel() {
let rankItem = this.rankModelList.find(
item => item.model === this.form.rerankingModelName
)
this.form.rerankingProviderName = rankItem.provider
},
chooseFixSearch(val) {
this.fixSearchList.forEach(item => {
item.isCheck = false
})
val.isCheck = true
this.form.rerankingMode = val.value
},
getWeight(val) {
console.log(val)
},
submitData() {
let params = {
datasetId: this.$route.query.datasetId,
searchMethod: this.searchMethod,
embeddingModel: this.embeddingItem.model,
embeddingModelProvider: this.embeddingItem.provider,
...this.form
}
params.rerankingEnable = this.handleBoolean(this.form.rerankingEnable)
params.scoreThresholdEnabled = this.handleBoolean(
this.form.scoreThresholdEnabled
)
retrievalSetting(params).then(res => {
if (res) {
this.$message.success('保存成功')
this.$emit('handleClose')
}
})
},
handleBoolean(val) {
let value = JSON.parse(JSON.stringify(val))
return value ? 1 : 0
}
} }
} }
</script> </script>
@@ -100,4 +270,10 @@ export default {
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
} }
.fixItem {
border: 1px solid #ebeef5;
margin-right: 10px;
padding: 10px;
border-radius: 8px;
}
</style> </style>

View File

@@ -15,14 +15,15 @@
<li <li
v-for="(item, index) in list" v-for="(item, index) in list"
:key="index" :key="index"
class="block flex justify-content-b" class="block flex justify-content-b "
style="align-items: baseline"
> >
<div class="flex"> <div class="flex" style="align-items: baseline">
<i class="el-icon-reading"></i> <i class="el-icon-reading"></i>
<p class="ml5 mr5">{{ item.metadataKey }}</p> <p class="ml5 mr10">{{ item.metadataKey }}</p>
<p>{{ item.dataType }}</p> <p class="fs12">{{ item.dataType }}</p>
</div> </div>
<div class="flex align-items-c"> <div class="flex align-items-c" >
<div class="amount">{{ item.amount ? item.amount :0}}个值</div> <div class="amount">{{ item.amount ? item.amount :0}}个值</div>
<div class="action-buttons"> <div class="action-buttons">
<el-button <el-button

View File

@@ -65,14 +65,14 @@
@click="jumpEditKnowledge" @click="jumpEditKnowledge"
>修改知识库 >修改知识库
</el-button> </el-button>
<!-- <el-button--> <el-button
<!-- type="primary"--> type="primary"
<!-- size="medium"--> size="medium"
<!-- icon="el-icon-search"--> icon="el-icon-search"
<!-- class="line-button"--> class="line-button"
<!-- @click="searchSetting"--> @click="searchSetting"
<!-- >检索设置--> >检索设置
<!-- </el-button>--> </el-button>
<el-button <el-button
type="primary" type="primary"
icon="el-icon-s-promotion" icon="el-icon-s-promotion"