✅派聪明 RAG 知识库检索面试题预测,覆盖 ElasticSearch 的 KNN 和 BM25 – 朝汐の小站
✅派聪明 RAG 知识库检索面试题预测,覆盖 ElasticSearch 的 KNN 和 BM25
本文最后更新于 259 天前,如有错误请邮件至 zhiligyi222na@gmail.com

1.当一个用户在搜索框里输入一句话然后点击搜索,系统大致会经历一个怎样的处理流程?

首先,用户通过前端页面输入搜索内容并提交,前端会将查询语句、用户信息等参数封装成 HTTP 请求发送到后端。后端接收到请求后,会解析出查询关键词和用户身份。

在进入搜索逻辑前,系统首先会调用外部的 Embedding 模型将用户的自然语言查询转化为向量表示。这一步是实现语义相似度搜索的基础。同时,系统还会提取出用户对应的组织标签,用于后续的权限过滤。

随后,系统会构造出一个 Elasticsearch 混合查询。融合了三类能力:首先是基于查询向量的 KNN 语义检索,用于找出语义上最接近的文本块;其次是基于关键词的 BM25 检索,用于匹配关键词相似的文档;最后是权限过滤机制,确保返回的文档必须是公开的、或属于该用户本人,或其组织标签在用户的有效标签列表中。

为了提高结果的相关性和精度,我们还会使用 Elasticsearch 的 rescore 机制,根据 BM25 与向量匹配的得分对初步召回的结果进行重排序,找到最终排名靠前的文档,并打分后返回给前端。

备注:

kNN 又称 k 最近邻算法,会使用临近度来将一个数据点与训练时所使用并已记住的一个数据集进行对比,从而做出预测。其中字母 k 表示在分类或回归问题中所考虑的最近邻的数量,NN 代表 k 所选数字的最近邻。

面试时可以这样回答:kNN 是 Elasticsearch 的一个向量相似度搜索功能。它允许我们搜索‘内容语义’而不仅仅是‘关键词’——比如用问题匹配知识库答案,本质是让搜索引擎具备‘联想’的能力。

想象传统图书馆用关键词查书(BM25),而 kNN 像一位懂内容的图书管家:

  1. 内容转密码(Embedding):管家会把每本书的核心思想(文本 / 图片 / 音频)翻译成一组数字密码(向量),比如《三国演义》可能编码为 [0.8, -0.2, 0.3,...]
  2. 相似即邻近(向量空间):内容相似的书,数字密码在坐标系中的距离越近(比如《水浒传》靠近《三国演义》,远离《量子力学》)。
  3. 按距离推荐(kNN 查询):当你问:“找和《三国演义》风格类似的书”,管家立刻在坐标系中锁定离它最近的 k 本书(k=5 就是找最相似的 5 本)。

在 Elasticsearch 中,kNN 通过两类方式实现:

  • Exact kNN:暴力计算目标向量与所有向量的距离,语法上用 knn 查询 + vector 字段。
  • ANN(Approximate Nearest Neighbor):使用 HNSW 算法(分层导航小世界)建立向量索引,语法上在创建索引时定义 "type": "dense_vector" + "index": true
// 示例:HNSW 索引定义
PUT my_index
{
  "mappings": {
    "properties": {
      "content_vector": {
        "type": "dense_vector",  // 向量类型
        "dims": 768,             // 维度数(需与模型匹配)
        "index": true,           // 启用ANN索引
        "similarity": "cosine"   // 相似度算法(余弦/点积/L2)
      }
    }
  }
}

BM25 是 Elasticsearch 的默认搜索评分算法,它的核心任务是 判断文档和搜索关键词的相关性。可以把它想象成一个公平的裁判——不仅看关键词出现次数,还要看关键词的“含金量”,同时防止长文档作弊。

  • 关键词在 当前文档 出现次数越多,得分越高。
  • 关键词在 所有文档中越稀有(比如“量子计算机” vs “的”),含金量越高,得分越高。
  • 惩罚长文档灌水 —— 比如“区块链”在 10 页的报告中出现 5 次,比在 100 页的教材中出现 5 次更可信。

综述:BM25 是 Elasticsearch 默认的相关性打分算法,充当了一个很聪明的裁判——用三把尺子来量文档:词频(TF)、关键词含金量(IDF)、以及文档长度惩罚机制。比如搜索‘苹果手机’时,它会优先选聚焦主题的短文,而非泛泛而谈的长文,同时抑制堆砌关键词的行为。

相比旧算法,BM25 通过参数 k1 和 b 平衡了词频饱和度和长文档干扰,让结果更贴合用户意图。简而言之:它让搜索引擎从‘数数’进化到了‘理解内容价值’。

2.这和我们平时用的百度搜索一样吗?还是有什么特别之处?

有相似之处,但又不太一样。我们这套搜索逻辑,可以理解为在传统的关键词搜索的基础上引入了语义理解的能力,它既保留了像百度那样的关键词匹配机制,也融合了向量检索这种更智能的语义搜索能力。

当用户发起查询时,系统首先会通过 embedding 模型把这句话转成高维向量。这个向量可以理解为这句话的“语义特征”,后续我们会用这个特征去做 KNN 检索,找到语义上相似的文本块。

当然,光有语义相似度还不够,我们还引入了 BM25 的关键词匹配。因为有些时候用户会输入一个专有名词,或者组织内部的一些术语,这种场景下,关键词匹配反而比语义更准。所以我们是先用向量去做初筛,再通过 BM25 的 rescore 对初筛结果再排序,这样能把那些“语义匹配 + 关键词命中”的高质量结果排到前面。

除此之外,我们还做了权限控制,比如说用户查询的文档必须是在用户所在的组织标签下,或者必须是公开的资料,或者必须是本人上传的文档等等,这些信息我们会通过 filter 的方式一并传给 Elasticsearch,避免返回用户无权限查看的内容。

整体来看,传统搜索偏向的是“你说了什么,我就找什么”,而 RAG 希望做到的是“你没说,但你想表达的,我也能理解并找到”。

从架构角度讲,这种混合检索也是标准的 RAG 方案。

3.为什么要用‘混合检索’?只用关键词或者只用语义,各自有什么局限性吗?

关键词检索,比如传统的 BM25,它的优势在于直接、高效,适合处理那些比较明确、规范的查询词,比如产品编号、错误码、ID 这类内容。但它的最大短板就在于,缺乏语义理解能力。打个比方,如果用户输入的是“如何成为一名优秀的程序员”,而知识库中某篇文章的标题是“提升代码质量和工程能力的实用技巧”,虽然两者语义高度相关,但由于没有关键词上的明显重合,传统的关键词匹配就会完全错过这条结果。

相反,语义检索恰好解决了这个问题。我们通过豆包的 embedding 模型,把用户的查询和文档内容都向量化,再通过 KNN 进行相似度检索。这种方式就不再局限于字面匹配,而是看内容“表达的意思”。哪怕用户问的内容在文档里并没有出现,只要意思接近,系统也能精准地召回相关文档。但是语义检索也有短板,比如对一些专有名词、产品型号、精确代码这类内容不够敏感。它会倾向于返回“意思差不多”的内容,而不是“完全匹配”的结果。

正是基于这些考虑,派聪明采用了“混合检索”的策略。

4.`topK`参数是用来做什么的?系统是按照什么标准来排序的?

topK 用来控制最后返回给前端的结果数量,默认是 10 条。但真正有意思的是,这个 topK 并不是简单地“查10条就结束”,而是经过了两个阶段的混合排序流程。

第一阶段是向量召回,主要看语义相似度。举个例子,如果用户问的是“如何提升工作效率”,那即便知识库里没有完全一样的问题,但只要有文章讨论的是“时间管理技巧”或“效率工具推荐”,因为它们在语义空间上很接近,也能被召回。这一步我们一般会放宽范围,比如召回 topK 的 30 倍,这样能最大化地覆盖语义相关内容。

接下来会进入第二阶段:关键词重排。我们会对上一步的结果再跑一次 BM25 的关键词匹配,这时候就把那些既有语义相关,又在字面上出现了关键词的文档往前面排。

最终,系统会根据这个综合得分,也就是向量得分加关键词得分的加权结果,排个序,然后返回 topK 条给前端。

5.你用什么工具来实现这个‘混合检索’的?

我用的是 Elasticsearch。

选择 Elasticsearch 的主要原因在于它的开箱即用。它不仅是一个非常成熟的用于关键词匹配的全文搜索引擎,也支持用于语义相似度匹配的向量检索。

6.详细说明混合检索的实现原理…各自的权重是如何确定的?

我们并不是简单地把关键词检索和语义检索这两种方式“各自执行一次,然后把结果合并一下”。我们用的是 Elasticsearch 官方推荐的**“召回 + 重排序(Recall + Rescore)”两阶段策略**。

第一阶段,我们会通过向量检索先从知识库中“捞”出一个比较大的候选集,比如是 topK 的 30 倍。这个阶段的目标很明确,就是“求全”。不管搜索的内容标不标准、意思精不精准,只要语义上跟用户的问题相关,我们就都拿出来,不放过任何一个潜在的好结果。

第二阶段,我们用调用 Elasticsearch 的 rescore 机制。也就是说,我们不再对所有文档做关键词匹配,而只是对刚刚召回的那一批候选集,再做一次 BM25 的关键词打分。这个阶段的目标是“求准”了—— 那些跟用户关键词完全匹配的结果,或者说那些“说到点子上”的结果,会被调高排名。

这种“先召回、后重排”的方式,既利用了向量检索的广度,又利用了关键词检索的精度,并且性能也很高。

权重调整这块也非常灵活,向量查询我们暂定为 0.2,重排序查询我们暂定为向量查询的 5 倍,这样做可以保留一部分向量分数,同时,可以防止那些虽然语义高度相关但关键词匹配稍差的优质结果被排到后面。从大量的检索结果来看,目前这个权重是比较符合预期结果的。

.queryWeight(0.2d)            // KNN 分数权重
.rescoreQueryWeight(1.0d)     // BM25 分数权重

7.如何评估你这套混合检索的结果质量?

首先,我们需要构建一个评测集,每条样本会包含一个用户的查询,一组我们人工判定高度相关的文档,以及一些不相关的文档作为干扰因子。

接着,我们通过这些维度来评测检索的结果质量:

  • 看 topK 里面真正相关的比例;
  • 看我们有没有漏掉关键的文档;
  • 用户是否能在很前面就看到满意答案;

最后,可以做一个 A/B 测试。比如说放出一个新的实验版本,让 5% 的用户使用新版搜索策略,其他 95% 继续用旧的,然后对比两组用户的行为数据:

  • 点击率:新算法返回的结果是不是更吸引用户去点击;
  • 首个点击时长:用户是不是能更快地找到满意的结果;
  • 用户停留时长和满意度反馈:判断用户是不是觉得“这个结果靠谱”。

8.你的ES索引Mapping里都定义了哪些关键字段?

我们的索引名为 knowledge_base,其核心字段大致可以分为三类:内容字段、向量字段、权限字段。

首先是内容字段,我们定义为 textContent,它的类型是 text,底层配的是 ik 中文分词器。这个字段是用来支持关键词检索的,像 BM25、match 查询都是作用在这个字段上。通过中文分词器,我们能把一句话拆得比较细,这样用户用自然语言输关键词的时候,匹配的召回率和准确率会比较好。

然后是向量字段,名叫 vector,我们定义为 dense_vector 类型,维度是 2048,主要是因为我们用的向量模型是 豆包的 2048 维 embedding 模型。这个字段是用来做语义检索的,我们会把每个文档片段做 embedding,然后存到这个字段里。当用户发起查询时,我们也会把查询的内容转成向量,再用 KNN 算法去比相似度。

接着就是权限相关的字段,这是我们在做 B 端多租户场景时非常重视的一块。主要有三个字段:userId、orgTag 和 isPublic。

  • userId 是 keyword 类型,标记这个文档片段属于哪个用户,用于实现私有文档的权限隔离;
  • orgTag 同样是 keyword 类型,表示文档所属的组织标签,用来控制组织内的授权访问;
  • isPublic 是 boolean 类型,标记文档是否对所有人公开。

这样一来,我们在执行混合检索的时候,不但可以对 textContent 做关键词匹配、对 vector 做语义检索,同时还能在查询过滤时加入 userId、orgTag、isPublic 等条件。

备注:

可以通过 curl -k -u "你的 ES 密码" -X GET "[https://localhost:9200/knowledge_base/_mapping"](https://localhost:9200/knowledge_base/_mapping") | jq '.'命令获取索引中的字段信息:

这是我用 warp 做的一个完整字段解释。

9.假设你的检索接口响应很慢…你是如何一步步把它优化的?

当发现搜索接口变慢的时候,我通常会从三个层面去排查:Elasticsearch 本身的查询效率服务端的代码逻辑,还有就是能不能加缓存

Elasticsearch 是影响性能最关键的因素。如果 ES 本身查询慢,上层再怎么做都救不回来。我会先看看索引设计是否合理,比如有没有设置太多字段参与检索?有没有字段类型设置错了?ES 占用的内存是否足够用?

第二层是代码逻辑。我会重点看:ES 的查询语句写得是否优雅,比如能不能用 filter 替代 query 来避免不必要的相关度计算?是不是每次都查了太多字段返回?

最后就是缓存这块。我会针对一些高频的查询,对 embedding 结果做一次缓存,避免用户每次查询都要重新跑向量模型。更重要的是,对于一些热点查询,可以把 ES 返回的结果整体缓存下来,这样后续相同的查询请求可以直接从缓存返回,完全绕过 ES,从而减轻 ES 的负担。

10.我们现在进入检索的核心。假设知识库里有成千上万份文档,分属不同的人和部门。当一个用户(比如‘王二’)发起搜索时,你是如何确保你的检索逻辑,从一开始就只在他有权限的数据范围内进行的?请详细描述一下这个权限过滤的实现机制。

我当时的设计是,查询必须在 ES 这里就进行权限过滤,这样做的好处是,在进行检索的时候,只拿到用户有权限的结果,直接就把没有权限的结果过滤掉。

当用户上传文档的时候,我们就把权限写到 ES 中;当用户进行查询时,我们直接把他的权限写进 ES 的 filter 字句中,这样既不会影响排序相关性计算,ES 也能对 filter 的结果做缓存。

比如说当一个用户(比如“王二”)发起检索请求时,请求会携带他的身份认证信息(也就是 JWT Token)。后端在收到请求后,会解析这个 Token,拿到用户的 userid,再从 Redis 缓存中取出用户的组织标签等信息。

接着,我们会将用户的身份信息,和组织标签带入到 ES 的查询语句中,我们的逻辑是:一个文档,只要满足以下任意一个条件,就被认为用户是有权访问的:

  • 该文档是公开的 ( isPublic: true )。
  • 该文档的创建者是当前用户 ( userId: “王二的 userId” )。
  • 该文档的权限标签(orgTag)与用户的权限标签有交集。

我们在查询时会把这三个条件都加进去,ES 就会自动进行过滤。

// ...
BoolQuery.Builder permissionBoolQuery = new BoolQuery.Builder();

// 条件1: 公开文档
permissionBoolQuery.should(s -> s.term(t -> t.field("isPublic").value(true)));

// 获取当前用户信息
LoginUser loginUser = UserUtils.getLoginUser();
if (Objects.nonNull(loginUser)) {
    // 条件2: 用户自己的文档
    permissionBoolQuery.should(s -> s.term(t -> t.field("userId").value(loginUser.getUserId())));

    // 条件3: 用户所在组织的文档
    if (CollectionUtils.isNotEmpty(loginUser.getOrgTags())) {
        permissionBoolQuery.should(s -> s.terms(t -> t
                .field("orgTag")
                .terms(new TermsQueryField.Builder().value(
                        loginUser.getOrgTags().stream().map(FieldValue::of).collect(Collectors.toList())
                ).build())
        ));
    }
}

// 将权限过滤条件添加到主查询的filter子句中
mainBoolQuery.filter(f -> f.bool(permissionBoolQuery.build()));

// ... 然后将 mainBoolQuery 发送给 Elasticsearch
SearchResponse<EsKnowledgeBase> response = elasticsearchClient.search(s -> s
        .index(esConfig.getIndexName())
        .query(q -> q.bool(mainBoolQuery.build()))
        // ... 其他查询参数,如knn, rescore, size等
, EsKnowledgeBase.class);

11.为什么先用ES,而不是直接用FAISS?

之所以选择使用 Elasticsearch,而不是直接用 FAISS,我也思考了很多。

首先,我对 Elasticsearch 比较熟悉。像派聪明这种企业级的 RAG 应用,除了向量召回以外,还会涉及关键词匹配、权限过滤等,这些用 ES 都能一站式解决。比如我们用 dense_vector 字段配合 HNSW 做语义检索;关键词检索部分用的是 BM25;而像权限控制,我们可以直接在 ES 查询中用 filter 语法,对 userId、orgTag、isPublic 等字段做前置过滤,不需要拉数据回来再用 Java 代码做后置处理。

另外,从运维和研发成本上看,Elasticsearch 生态非常成熟。

当然,FAISS 本身也是一个非常优秀的向量搜索引擎,它的算法性能和内存效率都非常强。下一个版本我是打算用 FAISS 的。

12.你的ES索引是如何处理高维向量的?有哪些特殊的配置?

首先是向量字段 vector 的定义,它的类型为 dense_vector,明确告诉 Elasticsearch,这个字段是用来存储密集向量的。

这是使用 Elasticsearch kNN 搜索的基础,只有定义为 dense_vector 的字段才能用于后续的 knn 查询。

"vector": {
          "type": "dense_vector",
          "dims": 2048,
          "index": true,
          "similarity": "cosine"
        }

维度和我们使用的豆包 embedding 模型对齐,都是 2048 维。

存储单个向量所需的空间越大(2048 个 float ≈ 2048 × 4 bytes = 8KB),构建索引和进行 kNN 搜索的计算开销通常也越大。

然后,我们会设置 index 等于 true,要求 Elasticsearch 为这个向量字段构建专门的 近似最近邻(ANN)索引。

对于包含海量文档的索引,进行精确的 kNN 搜索是不太可行的(时间复杂度为 O(N))。

构建 ANN 索引是为了实现 亚线性时间 的搜索。

背后用到的是 HNSW 算法,该算法会构建一个多层图。

搜索的时候会从顶层开始(节点少),然后快速定位到目标区域。然后下降到下一层更密集的图中,在更小的候选区域内进行更精细的搜索。重复此过程直到最底层。在每一层,算法都会沿着连接边(代表向量在空间中的邻近关系)向查询向量方向“贪心”地移动。

该算法的优点是在精度和召回率(找到真正的最近邻)与查询速度之间取得了非常好的平衡,并且相对容易调优。

还有一个非常重要的参数 “similarity”: “cosine”,指定在构建 ANN 索引和进行 kNN 搜索时,用于衡量 向量之间相似性(或距离)的度量算法。我们选择的是 余弦相似度,只关注向量的方向,忽略其大小(长度)。

这对许多语义搜索任务特别有利,因为文档长度不影响相似性判断(类似于 BM25 的思想)。

13.在大规模场景下,ES会存在哪些性能瓶颈?

首先是内存瓶颈,HNSW 图结构需要完全加载到 JVM 堆内存中。随着向量数量和维度的增加,内存消耗会急剧上升,成为单个节点容量的主要限制因素。

其次是 CPU 瓶颈,k-NN 查询是计算密集型任务。在高并发查询场景下,大量的距离计算会消耗巨大的 CPU 资源,导致查询延迟增加和吞吐量下降。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇