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

news2024/11/26 20:47:56

目录

一、代码解析

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,  // 知识库 id
    text,  // 搜索测试框输入的检索文本
    limit = 1500, // 引用的 token 上限
    similarity,  // 最低相关度,默认是0
    searchMode,  // 检索模式,例如:
    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 balance
  await 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 token
  if (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 distance
  limit: number; // max Token limit
  datasetIds: 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 */
  // 默认搜索模式是 embeddinng
  searchMode = DatasetSearchModeMap[searchMode] ? searchMode : DatasetSearchModeEnum.embedding;
  // 是否使用重排模型
  usingReRank = usingReRank && global.reRankModels.length > 0;

  // Compatible with topk limit
  if (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. embeddingRecall
  const 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. fullTextRecall
  const 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 })[];

    // resort
    searchResults.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. reRankSearchResult
  const 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 data
      const 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. filterResultsByMaxTokens
  const 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 recall
    const embeddingRecallResList: SearchDataResponseItemType[][] = [];
    const fullTextRecallResList: SearchDataResponseItemType[][] = [];
    let totalTokens = 0;

    await Promise.all(
      queries.map(async (query) => { // 遍历多个 query
        const [{ tokens, embeddingRecallResults }, { fullTextRecallResults }] = await Promise.all([
          embeddingRecall({
            query,
            limit: embeddingLimit
          }),
          fullTextRecall({
            query,
            limit: fullTextLimit
          })
        ]);
        totalTokens += tokens;

        embeddingRecallResList.push(embeddingRecallResults);
        fullTextRecallResList.push(fullTextRecallResults);
      })
    );

    // rrf concat
    const 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 limit
  const { embeddingLimit, fullTextLimit } = countRecallLimit();

  // recall   
  const { embeddingRecallResults, fullTextRecallResults, tokens } = await multiQueryRecall({
    embeddingLimit,
    fullTextLimit
  });

  // ReRank results
  const 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 data
    set = 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 concat
  const rrfConcatResults = datasetSearchResultConcat([
    { k: 60, list: embeddingRecallResults },
    { k: 60, list: fullTextRecallResults },
    { k: 58, list: reRankResults }
  ]);

  // remove same q and a data
  set = 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 filter
  const 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.coloradmin.cn/o/1938465.html

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

相关文章

【HarmonyOS】HarmonyOS NEXT学习日记:五、交互与状态管理

【HarmonyOS】HarmonyOS NEXT学习日记&#xff1a;五、交互与状态管理 在之前我们已经学习了页面布局相关的知识&#xff0c;绘制静态页面已经问题不大。那么今天来学习一下如何让页面动起来、并且结合所学完成一个代码实例。 交互 如果是为移动端开发应用&#xff0c;那么交…

暑假第一周学习内容-ZARA仿写

仿写ZARA总结 文章目录 仿写ZARA总结前言无限轮播图分栏控制器与UIScrollViewUIScorllView的协议部分UISegmentedControl的协议部分 自定义cell 前言 本文主要是用来总结仿写ZARA中遇到的一些问题&#xff0c;以及ZARA中学习到的一些新知识。 无限轮播图 这里我们先给出无限…

Spring Boot 学习(10)——固基(Idea 配置 git 访问 gitee)

几转眼就过了两个月&#xff0c;其实也没有闲着&#xff0c;学也学了&#xff0c;只是繁杂事多&#xff0c;学的不如以前多&#xff0c;也没有做过笔记了。 以前做开发因条件受限&#xff0c;没有什么 git &#xff0c;也没有 gitee。现在出来混要跟上形势才行&#xff0c;学习…

C语言程序设计8

程序设计8 问题8_1代码8_1结果8_1 问题8_2代码8_2结果8_2 问题8_3代码8_3结果8_3 问题8_1 函数 f u n fun fun 的功能是&#xff1a;求 s s ss ss 所指字符串数组中长度最短的字符串所在行下标&#xff0c;作为函数值返回&#xff0c;并把其串长放在形参 n n n 所指的变量中…

2024论文精读:利用大语言模型(GPT)增强上下文学习去做关系抽取任务

文章目录 1. 前置知识2. 文章通过什么来引出他要解决的问题3. 作者通过什么提出RE任务存在上面所提出的那几个问题3.1 问题一&#xff1a;ICL检索到的**示范**中实体个关系的相关性很低。3.2 问题二&#xff1a;示范中缺乏解释输入-标签映射导致ICL效果不佳。 4. 作者为了解决上…

【Android】常用基础布局

布局是一种可用于放置很多控件的容器&#xff0c;它可以按照一定的规律调整内部控件的位置&#xff0c;从而编写出精美的界面&#xff0c;布局内不单单可以放控件&#xff0c;也可以嵌套布局&#xff0c;这样可以完成一些复杂的界面&#xff0c;下面就来认识一些常用的布局吧。…

基于Semaphore与CountDownLatch分析AQS共享模式实现

共享模式与独占模式区别在于&#xff1a;共享模式下允许多条线程同时获取锁资源&#xff0c;而在之前分析的独占模式中&#xff0c;在同一时刻只允许一条线程持有锁资源。 一、快速认识Semaphore信号量及实战 Semaphore信号量是java.util.concurrent(JUC)包下的一个并发工具类…

2-40 基于Matlab编写的3维FDTD(时域有限差分算法)计算了球的RCS经典散射问题

基于Matlab编写的3维FDTD(时域有限差分算法)计算了球的RCS经典散射问题&#xff0c;采用PEC作边界&#xff0c;高斯波束激励。程序已调通&#xff0c;可直接运行。 2-40 3维FDTD 时域有限差分算法 - 小红书 (xiaohongshu.com)

机器学习——降维算法PCA和SVD(sklearn)

目录 一、基础认识 1. 介绍 2. 认识 “ 维度 ” &#xff08;1&#xff09;数组和Series &#xff08;2&#xff09;DataFrame 表 &#xff08;3&#xff09;图像 3. 降维思想 4. 降维步骤 二、降维算法&#xff08;PCA&#xff09; 1. PCA实现 &#xff08;1&#…

免费视频批量横版转竖版

简介 视频处理器 v1.3 是一款由是貔貅呀开发的视频编辑和处理工具&#xff0c;提供高效便捷的视频批量横转竖&#xff0c;主要功能&#xff1a; 导入与删除文件&#xff1a;轻松导入多个视频文件&#xff0c;删除不必要的文件。暂停与继续处理&#xff1a;随时暂停和继续处理。…

7-20FPGA调试日志

1. 在代码里面定义的ILA的变量名称与波形抓取界面的不一致 问题描述 ::: 2. 直接从其他的播放声音的平台放音乐没问题&#xff0c;但是从AU里面生成的2kHz的正弦波放不出声音 演示视频链接 好像和ILA的例化信号有关&#xff0c;例化ILA信号的驱动时钟信号频率没有内部的其他…

Redis-应用

目录 应用 缓存雪崩、击穿、穿透和解决办法? 布隆过滤器是怎么工作的? 缓存的数据一致性怎么保证 Redis和Mysql消息一致性 业务一致性要求高怎么办? 数据库与缓存的一致性问题 数据库和缓存的一致性如何保证 如何保证本地缓存和分布式缓存的一致&#xff1f; 如果在…

电脑永久性不小心删除了东西还可以恢复吗 电脑提示永久性删除文件怎么找回 怎么恢复电脑永久删除的数据

永久删除电脑数据的操作&#xff0c;对于很多常用电脑设备的用户来说&#xff0c;可以说时有发生&#xff01;但是&#xff0c;因为这些情况大都发生在不经意间&#xff0c;所以每每让广大用户感觉到十分苦恼。永久删除也有后悔药&#xff0c;轻松找回电脑中误删的文件。恢复文…

Github 2024-07-20 Rust开源项目日报 Top10

根据Github Trendings的统计,今日(2024-07-20统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Rust项目10TypeScript项目1Rust: 构建可靠高效软件的开源项目 创建周期:5064 天开发语言:Rust协议类型:OtherStar数量:92978 个Fork数量:1…

Win10环境将Docker部署到非系统盘

Win10环境将Docker部署到非系统盘 目录 Win10环境将Docker部署到非系统盘 一、Docker官网客户端Docker Hub下载 二、windows环境的安装 三、正确的迁移步骤 3.1、确保你的系统分区至少3G的剩余空间&#xff1b; 3.2、默认方式安装Docker hub&#xff1b; 3.3、打开Dock…

linux操作系统之线程

1.线程概念 线程是一个轻量级进程,每一个线程都属于一个进程 进程是操作系统资源分配的最小单位,而线程是CPU任务调度的最小单位 线程是一个任务执行的过程,包括创建,调度,消亡 创建:线程空间位于进程空间,进程中的线程,栈区独立,并共享进程中的数据区,文本区,堆区 调度:宏观…

微积分-微分应用2(平均值定理)

要得出平均值定理&#xff0c;我们首先需要以下结果。 罗尔定理 设函数 f f f 满足以下三个假设&#xff1a; f f f 在闭区间 [ a , b ] [a, b] [a,b] 上连续。 f f f 在开区间 ( a , b ) (a, b) (a,b) 上可导。 f ( a ) f ( b ) f(a) f(b) f(a)f(b) 则在开区间 ( a , b …

【手撕数据结构】拿捏双向链表

目录 链表介绍初始化链表销毁链表查找节点打印链表增加节点尾插头插在指定位置之后插入节点 删除节点尾删头删删除指定位置节点 链表判空 链表介绍 前面说到&#xff0c;链表的结构一共有八种&#xff1a;带头单向循环链表、带头单向非循环链表、带头双向循环链表、带头双向非…

绿色算力|暴雨服务器用芯片筑起“十四五”转型新篇章

面对全球气候变化、技术革新以及能源转型的新形势&#xff0c;发展低碳、高效的绿色算力不仅是顺应时代的要求&#xff0c;更是我国建设数字基础设施和展现节能减碳大国担当的重要命题&#xff0c;在此背景下也要求在提升算力规模和性能的同时&#xff0c;积极探索推动算力基础…

计算机网络参考模型与5G协议

目录 OSI七层参考模型OSI模型vsTCP/IP模型TCP/IP协议族的组成 OSI七层参考模型 分层功能应用层网络服务与最终用户的一个接口表示层数据的表示,安全,压缩会话层建立,管理,终止会话传输层定义传输数据的协议端口号,以及流控和差错校验网络层进行逻辑地址寻址,实现不同网路之间的…