FastGPT 知识库搜索测试功能解析

目录

一、代码解析

1.1 searchTest.ts

1.2 controller.ts


本文接上一篇文章FastGPT 知识库搜索测试功能解析 对具体代码进行解析。

一、代码解析

FastGPT 知识库的搜索测试功能主要涉及两个文件,分别是 searchTest.ts 和 controller.ts 文件,下面分别进行介绍。

1.1 searchTest.ts

文件路径是 projects/app/src/pages/api/core/dataset/searchTest.ts,搜索测试功能的主文件,代码如下所示。


async function handler(req: NextApiRequest) {console.log("function handler(req: NextApiRequest)")const {datasetId,  // 知识库 idtext,  // 搜索测试框输入的检索文本limit = 1500, // 引用的 token 上限similarity,  // 最低相关度,默认是0searchMode,  // 检索模式,例如:usingReRank, // 是否对召回文本进行相关性重排,需要结合rerank模型;datasetSearchUsingExtensionQuery = false, // 是否开启问题补全;datasetSearchExtensionModel,  // 问题补全所用的模型;datasetSearchExtensionBg = '' // 问题补全的对话背景描述;} = req.body as SearchTestProps;// 判断知识库 id 以及检索文本是否为空if (!datasetId || !text) {return Promise.reject(CommonErrEnum.missingParams);}// 计时const start = Date.now();// auth dataset role 查询是否有读数据库的权限(ReadPermissionVal 读取权限值)const { dataset, teamId, tmbId, apikey } = await authDataset({req,authToken: true,authApiKey: true,datasetId,per: ReadPermissionVal});// auth balanceawait checkTeamAIPoints(teamId);// 获取补全模型const extensionModel =datasetSearchUsingExtensionQuery && datasetSearchExtensionModel? getLLMModel(datasetSearchExtensionModel): undefined;// 问题通过LLM进行补全const { concatQueries, rewriteQuery, aiExtensionResult } = await datasetSearchQueryExtension({query: text,extensionModel,extensionBg: datasetSearchExtensionBg});console.log("[test]: pre searchDatasetData");// pgvector 中查询相似的向量const { searchRes, tokens, ...result } = await searchDatasetData({teamId,reRankQuery: rewriteQuery,queries: concatQueries,model: dataset.vectorModel,limit: Math.min(limit, 20000),similarity,datasetIds: [datasetId],searchMode,usingReRank: usingReRank && (await checkTeamReRankPermission(teamId))});// push bill 更新 token 费用const { totalPoints } = pushGenerateVectorUsage({teamId,tmbId,tokens,model: dataset.vectorModel,source: apikey ? UsageSourceEnum.api : UsageSourceEnum.fastgpt,...(aiExtensionResult &&extensionModel && {extensionModel: extensionModel.name,extensionTokens: aiExtensionResult.tokens})});// Mongodb 更新 apikey tokenif (apikey) {updateApiKeyUsage({apikey,totalPoints: totalPoints});}return {list: searchRes, // 存储检索结果duration: `${((Date.now() - start) / 1000).toFixed(3)}s`, // 时长queryExtensionModel: aiExtensionResult?.model, //...result};
}export default NextAPI(handler);

函数 handler 主要是打辅助,主力在  searchDatasetData 函数中。

 函数 handler 传入的配置多数都是在知识库搜索配置的参数,如下所示。

1.2 controller.ts

主要处理逻辑在 searchDatasetData 函数中,其调用 getVectorsByText 获取测试文本的向量化,在 pgvector 中查询相似度高的向量,然后,通过 mongodb 查询向量的原文。


type SearchDatasetDataProps = {teamId: string;model: string;similarity?: number; // min distancelimit: number; // max Token limitdatasetIds: string[];searchMode?: `${DatasetSearchModeEnum}`;usingReRank?: boolean;reRankQuery: string;queries: string[];
};export async function searchDatasetData(props: SearchDatasetDataProps) {console.log("function searchDatasetData");let {teamId,reRankQuery,queries,model,similarity = 0,limit: maxTokens,searchMode = DatasetSearchModeEnum.embedding,usingReRank = false,datasetIds = []} = props;/* init params */// 默认搜索模式是 embeddinngsearchMode = DatasetSearchModeMap[searchMode] ? searchMode : DatasetSearchModeEnum.embedding;// 是否使用重排模型usingReRank = usingReRank && global.reRankModels.length > 0;// Compatible with topk limitif (maxTokens < 50) {maxTokens = 1500;}let set = new Set<string>();let usingSimilarityFilter = false;/* function */// 1. countRecallLimit,根据搜索模式修改限制,分别对应三种检索方式:const countRecallLimit = () => {if (searchMode === DatasetSearchModeEnum.embedding) { // 语义检索return {embeddingLimit: 100,fullTextLimit: 0};}if (searchMode === DatasetSearchModeEnum.fullTextRecall) { // 全文检索return {embeddingLimit: 0,fullTextLimit: 100};}return { // 混合检索embeddingLimit: 80,fullTextLimit: 60};};// 2. embeddingRecallconst embeddingRecall = async ({ query, limit }: { query: string; limit: number }) => {const { vectors, tokens } = await getVectorsByText({  // 获取输入文本的向量,vectors 为转换后的向量model: getVectorModel(model), // 从配置文件中获取 model 的配置信息input: query,type: 'query'});const { results } = await recallFromVectorStore({ // 在 pg vector 中查找相似向量teamId,datasetIds,vector: vectors[0],limit});// get q and a  在 Mongodb 中查找向量的文本形式const dataList = (await MongoDatasetData.find({teamId,datasetId: { $in: datasetIds },collectionId: { $in: Array.from(new Set(results.map((item) => item.collectionId))) },'indexes.dataId': { $in: results.map((item) => item.id?.trim()) }},'datasetId collectionId q a chunkIndex indexes').populate('collectionId', 'name fileId rawLink externalFileId externalFileUrl').lean()) as DatasetDataWithCollectionType[];// add score to data(It's already sorted. The first one is the one with the most points)const concatResults = dataList.map((data) => {const dataIdList = data.indexes.map((item) => item.dataId);const maxScoreResult = results.find((item) => {return dataIdList.includes(item.id);});return {...data,score: maxScoreResult?.score || 0};});concatResults.sort((a, b) => b.score - a.score);const formatResult = concatResults.map((data, index) => {if (!data.collectionId) {console.log('Collection is not found', data);}const result: SearchDataResponseItemType = {id: String(data._id),q: data.q,a: data.a,chunkIndex: data.chunkIndex,datasetId: String(data.datasetId),collectionId: String(data.collectionId?._id),...getCollectionSourceData(data.collectionId),score: [{ type: SearchScoreTypeEnum.embedding, value: data.score, index }]};return result;});return {embeddingRecallResults: formatResult,tokens};};// 3. fullTextRecallconst fullTextRecall = async ({query,limit}: {query: string;limit: number;}): Promise<{fullTextRecallResults: SearchDataResponseItemType[];tokenLen: number;}> => {if (limit === 0) {return {fullTextRecallResults: [],tokenLen: 0};}let searchResults = (await Promise.all(datasetIds.map((id) =>MongoDatasetData.find({teamId,datasetId: id,$text: { $search: jiebaSplit({ text: query }) }},{score: { $meta: 'textScore' },_id: 1,datasetId: 1,collectionId: 1,q: 1,a: 1,chunkIndex: 1}).sort({ score: { $meta: 'textScore' } }).limit(limit).lean()))).flat() as (DatasetDataSchemaType & { score: number })[];// resortsearchResults.sort((a, b) => b.score - a.score);searchResults.slice(0, limit);const collections = await MongoDatasetCollection.find({_id: { $in: searchResults.map((item) => item.collectionId) }},'_id name fileId rawLink');return {fullTextRecallResults: searchResults.map((item, index) => {const collection = collections.find((col) => String(col._id) === String(item.collectionId));return {id: String(item._id),datasetId: String(item.datasetId),collectionId: String(item.collectionId),...getCollectionSourceData(collection),q: item.q,a: item.a,chunkIndex: item.chunkIndex,indexes: item.indexes,score: [{ type: SearchScoreTypeEnum.fullText, value: item.score, index }]};}),tokenLen: 0};};// 4. reRankSearchResultconst reRankSearchResult = async ({data,query}: {data: SearchDataResponseItemType[];query: string;}): Promise<SearchDataResponseItemType[]> => {try {const results = await reRankRecall({query,documents: data.map((item) => ({id: item.id,text: `${item.q}\n${item.a}`}))});if (results.length === 0) {usingReRank = false;return [];}// add new score to dataconst mergeResult = results.map((item, index) => {const target = data.find((dataItem) => dataItem.id === item.id);if (!target) return null;const score = item.score || 0;return {...target,score: [{ type: SearchScoreTypeEnum.reRank, value: score, index }]};}).filter(Boolean) as SearchDataResponseItemType[];return mergeResult;} catch (error) {usingReRank = false;return [];}};// 5. filterResultsByMaxTokensconst filterResultsByMaxTokens = async (list: SearchDataResponseItemType[],maxTokens: number) => {const results: SearchDataResponseItemType[] = [];let totalTokens = 0;for await (const item of list) {totalTokens += await countPromptTokens(item.q + item.a);if (totalTokens > maxTokens + 500) {break;}results.push(item);if (totalTokens > maxTokens) {break;}}return results.length === 0 ? list.slice(0, 1) : results;};// 6. multiQueryRecall 首先,将 query 转换为 vector,然后,在 pgvector 中检索相似,最后在 mongodb 查找 vector 对应的文本,处理后返回。const multiQueryRecall = async ({embeddingLimit,fullTextLimit}: {embeddingLimit: number;fullTextLimit: number;}) => {// multi query recallconst embeddingRecallResList: SearchDataResponseItemType[][] = [];const fullTextRecallResList: SearchDataResponseItemType[][] = [];let totalTokens = 0;await Promise.all(queries.map(async (query) => { // 遍历多个 queryconst [{ tokens, embeddingRecallResults }, { fullTextRecallResults }] = await Promise.all([embeddingRecall({query,limit: embeddingLimit}),fullTextRecall({query,limit: fullTextLimit})]);totalTokens += tokens;embeddingRecallResList.push(embeddingRecallResults);fullTextRecallResList.push(fullTextRecallResults);}));// rrf concatconst rrfEmbRecall = datasetSearchResultConcat(embeddingRecallResList.map((list) => ({ k: 60, list }))).slice(0, embeddingLimit);const rrfFTRecall = datasetSearchResultConcat(fullTextRecallResList.map((list) => ({ k: 60, list }))).slice(0, fullTextLimit);return {tokens: totalTokens,embeddingRecallResults: rrfEmbRecall,fullTextRecallResults: rrfFTRecall};};// 上面都是函数的定义/* main step */// count limitconst { embeddingLimit, fullTextLimit } = countRecallLimit();// recall   const { embeddingRecallResults, fullTextRecallResults, tokens } = await multiQueryRecall({embeddingLimit,fullTextLimit});// ReRank resultsconst reRankResults = await (async () => {if (!usingReRank) return [];set = new Set<string>(embeddingRecallResults.map((item) => item.id));const concatRecallResults = embeddingRecallResults.concat(fullTextRecallResults.filter((item) => !set.has(item.id)));// remove same q and a dataset = new Set<string>();const filterSameDataResults = concatRecallResults.filter((item) => {// 删除所有的标点符号与空格等,只对文本进行比较const str = hashStr(`${item.q}${item.a}`.replace(/[^\p{L}\p{N}]/gu, ''));if (set.has(str)) return false;set.add(str);return true;});return reRankSearchResult({query: reRankQuery,data: filterSameDataResults});})();// embedding recall and fullText recall rrf concatconst rrfConcatResults = datasetSearchResultConcat([{ k: 60, list: embeddingRecallResults },{ k: 60, list: fullTextRecallResults },{ k: 58, list: reRankResults }]);// remove same q and a dataset = new Set<string>();const filterSameDataResults = rrfConcatResults.filter((item) => {// 删除所有的标点符号与空格等,只对文本进行比较const str = hashStr(`${item.q}${item.a}`.replace(/[^\p{L}\p{N}]/gu, ''));if (set.has(str)) return false;set.add(str);return true;});// score filterconst scoreFilter = (() => {if (usingReRank) {usingSimilarityFilter = true;return filterSameDataResults.filter((item) => {const reRankScore = item.score.find((item) => item.type === SearchScoreTypeEnum.reRank);if (reRankScore && reRankScore.value < similarity) return false;return true;});}if (searchMode === DatasetSearchModeEnum.embedding) {usingSimilarityFilter = true;return filterSameDataResults.filter((item) => {const embeddingScore = item.score.find((item) => item.type === SearchScoreTypeEnum.embedding);if (embeddingScore && embeddingScore.value < similarity) return false;return true;});}return filterSameDataResults;})();return {searchRes: await filterResultsByMaxTokens(scoreFilter, maxTokens),tokens,searchMode,limit: maxTokens,similarity,usingReRank,usingSimilarityFilter};
}

multiQueryRecall : 首先,将 query 转换为 vector,然后,在 pgvector 中检索相似,最后在 mongodb 查找 vector 对应的文本,处理后返回。主要在 embeddingRecall 函数中实现。

getVectorsByText : 负责将搜索的问题转换为向量表示;

recallFromVectorStore : 在 pg vector 中查找相似向量;

MongoDatasetData.find :将 recallFromVectorStore 查询出的相似向量在 mongodb 中找出原文本。

其他内容后面再详细展开介绍。

参考链接:

[1] FastGPT源码深度剖析:混合检索及语料召回逻辑 - 技术栈

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/48364.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

运行springboot项目报错:java: java.lang.NoSuchFieldError: members_field

项目场景&#xff1a; 在idea中运行从git上拉取的基于springboot框架的项目运行报错 问题描述 运行spingboot项目报错 java: java.lang.NoSuchFieldError: members_field原因分析&#xff1a; 检查你所使用的java版本&#xff08;我这里是在idea上运行的&#xff0c;可以很直…

ArkTS语言---基础知识

ArkTS是一种为构建高性能应用而设计的编程语言。ArkTS在继承TypeScript语法的基础上进行了优化&#xff0c;以提供更高的性能和开发效率。目前流行的编程语言TypeScript是在JavaScript基础上通过添加类型定义扩展而来的&#xff0c;而ArkTS则是TypeScript的进一步扩展。TypeScr…

DETR算法解读——Transformer在目标检测任务的首次应用

论文&#xff1a;End-to-End Object Detection with Transformers 作者&#xff1a;Nicolas Carion, Francisco Massa, Gabriel Synnaeve, Nicolas Usunier, Alexander Kirillov, Sergey Zagoruyko 机构&#xff1a;Facebook AI 链接&#xff1a;https://arxiv.org/abs/2005.12…

git教程, 命令行版

前言 git就是代码版本管理系统&#xff0c;很简单的作用就是每一次commit之后&#xff0c;修改文件都是跟上一次commit的仓库文件做对比&#xff0c;也可以调出历史的文件查看某次commit修改了什么东西 0环境准备&#xff1a; 安装git, 百度一下&#xff0c;然后打开cmd&…

Django 执行原生SQL

在Django中&#xff0c;你可以使用Raw SQL queries来执行原生的SQL查询。这对于需要进行复杂查询或Django的ORM无法满足的查询非常有用。 1&#xff0c;添加模型 Test/app11/models.py from django.db import modelsclass Post(models.Model):title models.CharField(max_le…

视频压缩文件太大了怎么缩小?怎么压缩视频大小?视频压缩方法:10个!(宝藏)

视频压缩文件太大了怎么缩小&#xff1f;让我看看是谁下班之后不是一手刷手机短视频&#xff0c;顺便葛优躺在沙发上的&#xff1f;互联网发展到现在&#xff0c;视频已成为我们生活中不可或缺的一部分。不管是视频录制还是视频缓存&#xff0c;视频文件体积越来越庞大&#xf…

reserve和resize

void test_vector4() {vector<int> v1;//cout << v1.max_size() << endl;//v1.reserve(10);v1.resize(10);for (size_t i 0; i < 10; i){v1[i] i;}for (auto e : v1){cout << e << " ";}cout << endl;} 在上面这段代码中对…

使用shedlock实现分布式互斥执行

前言 前序章节&#xff1a;springboot基础(82):分布式定时任务解决方案shedlock 如果你不清楚shedlock&#xff0c;建议先阅读前序章节&#xff0c;再来查看本文。 如果我们不在spring环境下&#xff0c;如何使用shedlock实现分布式互斥执行&#xff1f; 我们可以使用shedl…

深入理解CSS基础【代码审计实战指南】

文章目录 为什么需要cssCSS语法CSS的组成css注释&#xff1a; 快速入门示例&#xff1a;常用样式字体颜色和边框颜色介绍颜色示例&#xff1a;边框边框的宽度与高度 字体样式背景样式文本居中 字体颜色和边框颜色介绍颜色示例&#xff1a;边框边框的宽度与高度 字体样式背景样式…

硬件开发笔记(二十七):AD21导入DC座子原理图库、封装库,然后单独下载其3D模型融合为3D封装

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/140541464 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

韩顺平0基础学Java——第35天

p689-714 格式化语句 gpt说的&#xff1a; System.out.println 方法不支持像 printf 一样的格式化字符串。要使用格式化字符串&#xff0c;你可以使用 System.out.printf 方法或将格式化后的字符串传递给 System.out.println。下面是两种修正的方法&#xff1a; ### 方法一…

【Leetcode】十六、深度优先搜索 宽度优先搜索 :二叉树的层序遍历

文章目录 1、深度优先搜索算法2、宽度优先搜索算法3、leetcode102&#xff1a;二叉树的层序遍历4、leetcode107&#xff1a;二叉树的层序遍历II5、leetcode938&#xff1a;二叉搜索树的范围和 1、深度优先搜索算法 深度优先搜索&#xff0c;即DFS&#xff0c;从root节点开始&a…

Go语言之参数传递

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 修改参数 假设你定义了一个函数&#xff0c;并在函数里对参数进行…

【已解决】Linux(Centos7)中yum过程域名无法解析问题

问题原因 Linux中yum过程域名无法解析问题&#xff0c;但是ping 域名时联通的&#xff08;即DNS没问题&#xff09;&#xff0c;所以初步判断是镜像源的问题。 解决方法&#xff08;Centos7&#xff09; 1、备份/etc/yum.repos.d/CentOS-Base.repo 2、下载CentOS-Base.repo…

【Linux】Linux的基本使用

一.Linux的背景知识. 1.1什么是Linux Linux是一种开源的类Unix操作系统内核. 和Windows是" 并列 "的关系. 1.2Linux的发行版本. Linux 严格意义来说只是一个 “操作系统内核”.一个完整的操作系统 操作系统内核 配套的应用程序. 由于 Linux 是一个完全开源免费…

FPGA:二选一选择器

1、需求 使用XILINX的XC7A35TFFG484-2开发板&#xff0c;完成二选一选择器的设计。 2、分析 二选一选择器如下所示&#xff1a; 观察可知有三个输入端&#xff0c;一个输出端&#xff0c;其逻辑原理为&#xff1a;当sel为高电平时&#xff0c;outa&#xff0c;当sel为低电平…

十二、数组(2)

1.冒泡排序数组&#xff08;升序&#xff09; 冒泡排序&#xff1a;将一个整型数组排序&#xff08;升序&#xff09; 例&#xff1a; 10 9 8 7 6 5 4 3 2 1 9 10 8 7 6 …

第十四届蓝桥杯省赛C++C组C题【三国游戏】题解(AC)

解题思路 由于三种国家都有获胜的可能&#xff0c;所以我们需要分别枚举 X , Y , Z X,Y,Z X,Y,Z 获胜的情况。 设 X X X 获胜&#xff0c;那么对于第 i i i 个事件的贡献为 a [ i ] − ( b [ i ] c [ i ] ) a[i]-(b[i]c[i]) a[i]−(b[i]c[i])&#xff0c;根据贪心的策略…

【银河麒麟服务器操作系统】java进程oom现象分析及处理建议

了解银河麒麟操作系统更多全新产品&#xff0c;请点击访问麒麟软件产品专区&#xff1a;https://product.kylinos.cn 现象描述 某服务器系统升级内核至4.19.90-25.22.v2101版本后仍会触发oom导致java进程被kill。 现象分析 oom现象分析 系统messages日志分析&#xff0c;故…

数据编织 Data Fabric:解决“数据孤岛”的新思路

一个不争的事实是&#xff0c;企业内部数据孤岛的形成&#xff0c;根因在于业务发展的复杂性与技术迭代的快速性导致。具体而言&#xff0c;随着企业业务快速增长&#xff0c;如新生产线的引入或外部公司的并购&#xff0c;这些活动往往伴随着新系统上线与独立数据体系的融入&a…