- 向量数据库
- 数据库
- 人工智能
- 后端
【免费下载链接】lancedb
Developer-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.
导读
本文围绕 LanceDB Node.js SDK 的RRFReranker类,深入讲解如何用 Reciprocal Rank Fusion(RRF)算法融合向量检索(vector search)与全文检索(FTS)的结果,解决混合检索中两类分数不可比、无法直接相加排序的问题。读完本文你将掌握RRFReranker.create()的 k 参数调优、rerankHybrid()的调用契约与返回结构,并能通过Query.rerank()在 LanceDB 中一键完成混合检索重排序。
RRFReranker 是什么
RRFReranker是 LanceDB Node.js SDK 在 rerankers 命名空间 下提供的内置重排序器,其官方定义为:
Reranks the results using the Reciprocal Rank Fusion (RRF) algorithm.
即“使用 RRF 算法对检索结果进行重排序”。它面向的典型场景是混合检索(hybrid search):向量检索擅长语义匹配,全文检索擅长关键词精确匹配,两者各有优劣。将两类结果融合时,最大的障碍是二者的相关性分数(如余弦距离与 BM25 分数)量纲不同、分布不同,不能直接相加。RRF 通过“排名”而非“分数”来融合,天然规避了这个问题,无需任何分数归一化或权重标定。
在架构上,RRFReranker是 TypeScript 侧的一层薄封装:rrf.ts 内部持有一个由原生模块(napi)创建的NativeRRFReranker实例,真正执行融合算法的核心逻辑位于 Rust 侧 rrf.rs。TypeScript 只负责把RecordBatch序列化为 Arrow IPC 缓冲、调用原生方法、再把结果反序列化回RecordBatch。
// 源码位置:nodejs/lancedb/rerankers/rrf.ts import { RecordBatch } from "apache-arrow"; import { fromBufferToRecordBatch, fromRecordBatchToBuffer } from "../arrow"; import { RrfReranker as NativeRRFReranker } from "../native"; export class RRFReranker { private inner: NativeRRFReranker; /** @ignore */ constructor(inner: NativeRRFReranker) { this.inner = inner; } public static async create(k: number = 60) { return new RRFReranker( await NativeRRFReranker.tryNew(new Float32Array([k])), ); } async rerankHybrid( query: string, vecResults: RecordBatch, ftsResults: RecordBatch, ): Promise<RecordBatch> { const buffer = await this.inner.rerankHybrid( query, await fromRecordBatchToBuffer(vecResults), await fromRecordBatchToBuffer(ftsResults), ); const recordBatch = await fromBufferToRecordBatch(buffer); return recordBatch as RecordBatch; } }创建 RRFReranker:create(k)
RRFReranker的构造函数被标注为@hideconstructor,即用户不应直接new,而必须通过静态工厂方法创建:
static create(k: number = 60): Promise<RRFReranker>其中参数k是 RRF 公式中的平滑常数,默认值为60。从 rrf.ts 可以看到,k会被放入一个Float32Array传给原生层的NativeRRFReranker.tryNew,最终落到 Rust 的RRFReranker::new(k)(rrf.rs),并以f32类型存储在结构体内部。
k 值的含义与选择
RRF 的核心公式为:
RRF(d) = Σ 1 / (k + rank_i(d))其中rank_i(d)是文档d在第i路检索结果中的名次(从 1 开始),k为平滑常数。Rust 侧源码(rrf.rs)对此有明确注释:
The parameter k is a constant used in the RRF formula (default is 60). Experiments indicate that k = 60 was near-optimal, but that the choice is not critical.
即实验表明k = 60接近最优,但该取值并不关键。直观理解:
k越大,排名差异带来的分数差距越小,各路结果“越平等”;k越小,排名靠前的结果获得的主导权重越大;- 由于同一文档可能在向量与 FTS 两路中同时出现,其 RRF 得分会累加,从而在最终排序中获得提升——这正是融合的价值所在。
Rust 侧还实现了Defaulttrait,默认值同样为60.0(rrf.rs),说明 60 是贯穿 SDK 各语言的一致默认值。
使用示例
import { rerankers } from "@lancedb/lancedb"; // 使用默认 k=60 const rrf = await rerankers.RRFReranker.create(); // 自定义 k 值(更强调排名靠前的结果) const rrfTuned = await rerankers.RRFReranker.create(30);rerankHybrid:融合的核心方法
rerankHybrid是RRFReranker唯一的方法,也是 Reranker 接口 定义的契约:
rerankHybrid( query: string, vecResults: RecordBatch<any>, ftsResults: RecordBatch<any>, ): Promise<RecordBatch<any>>三个入参分别是:
| 参数 | 类型 | 说明 |
|---|---|---|
query | string | 原始查询串,透传给重排序器(RRF 本身不依赖查询内容,但接口保留该参数以保证自定义重排序器的灵活性) |
vecResults | RecordBatch | 向量检索返回的结果批次 |
ftsResults | RecordBatch | 全文检索返回的结果批次 |
返回值为一个新的RecordBatch,同时包含融合后的行数据与一个额外列_relevance_score(Float32),并按该分数降序排列。
底层调用链与数据交换
从源码可以还原完整的调用链:
- TypeScript 侧 rrf.ts 将两个
RecordBatch通过fromRecordBatchToBuffer序列化为 Arrow IPC 缓冲; - 原生层 rerankers.rs 的
rerank_hybrid将缓冲反序列化回RecordBatch,调用 Rust 核心实现后,再把结果批量打包为 IPC 缓冲返回; - TypeScript 侧用
fromBufferToRecordBatch还原为RecordBatch。
Rust 核心实现(rrf.rs)的处理流程为:
- 分别取出
vecResults与ftsResults中的_rowid列(常量ROW_ID),若缺失则返回InvalidInput错误; - 用
BTreeMap累加每个 row id 的 RRF 得分:对第i名(从 0 起)的结果累加1.0 / (i as f32 + k); - 通过 trait 的默认方法
merge_results(rerankers.rs)将两路结果按_rowid去重合并(concat_batches后以BTreeSet过滤重复 id); - 为合并结果追加
_relevance_score列,并用sort_to_indices按该分数降序排列。
注意 Rust 核心在实现rerank_hybrid时接收_query但未使用(参数命名为_query),印证了 RRF 是纯排名驱动、与查询文本无关的算法。
结果排序验证
Rust 侧测试 rrf.rs 给出了一个完整可验证的例子(k = 1.0):
向量结果(按 row id 排序):foo(1)、bar(4)、baz(2)、bean(5)、dog(3)FTS 结果:bar(4)、bean(5)、dog(3)
计算出的 RRF 得分(名次从 1 开始,1/(rank+k)):
foo = 1/1 = 1.0bar = 1/2 + 1/1 = 1.5baz = 1/3 = 0.333bean = 1/4 + 1/2 = 0.75dog = 1/5 + 1/3 = 0.533
最终按分数降序输出为bar(1.5) → foo(1.0) → bean(0.75) → dog(0.533) → baz(0.333),测试断言同时验证了输出 schema 为[name, _rowid, _relevance_score]三列。可以看到,同时命中两路检索的bar、bean、dog因分数累加而整体排在了只命中一路的foo、baz之前,这正是 RRF 融合的核心价值。
在混合查询中使用 RRFReranker
RRFReranker的典型使用方式是通过VectorQuery.rerank()挂载到混合查询上。该方法定义在 query.ts:
rerank(reranker: Reranker): VectorQuery { this.doVectorCall((inner) => inner.rerank(async (args) => { const vecResults = await fromBufferToRecordBatch(args.vecResults); const ftsResults = await fromBufferToRecordBatch(args.ftsResults); const result = await reranker.rerankHybrid( args.query, vecResults as RecordBatch, ftsResults as RecordBatch, ); const buffer = fromRecordBatchToBuffer(result); return buffer; }), ); return this; }它接受任意实现了Reranker接口的对象——既可以是内置的RRFReranker,也可以是自定义重排序器(接口只要求实现rerankHybrid方法,TypeScript 结构类型天然支持鸭子类型)。rerank会返回VectorQuery本身,因此可以继续链式调用select()、limit()等查询方法。
完整示例(与 rerankers.test.ts 中的集成测试同构):
import { connect, Index } from "@lancedb/lancedb"; import { RRFReranker } from "@lancedb/lancedb/rerankers"; const db = await connect("./data"); const table = await db.openTable("documents"); // 先为文本列建立 FTS 索引(向量索引已就绪) await table.createIndex("text", { config: Index.fts(), replace: true }); // 混合检索 + RRF 重排序 const rrf = await RRFReranker.create(); // k 默认 60 const rows = await table .query() .nearestTo([0.1, 0.2, 0.3]) // 向量条件 .fullTextSearch("lancedb") // 全文条件 .rerank(rrf) // 挂载 RRF 重排序器 .select(["text"]) .limit(10) .toArray(); for (const row of rows) { console.log(row.text, row._relevance_score); }要点说明:
- 使用
.rerank()的混合查询要求向量索引与 FTS 索引都已就绪; - 返回结果中
_relevance_score即 RRF 融合得分,可用于展示或二次过滤; rerank()接受自定义重排序器,rerankers.test.ts 展示了自定义实现(返回静态数据)的用法,说明该接口具备完全的可扩展性;- 该命名空间通过 index.ts 的
export * as rerankers from "./rerankers"挂载到包顶层,因此也可以使用lancedb.rerankers.RRFReranker.create()的形式访问。
自定义重排序器与 Reranker 接口
RRF 只是融合策略之一。LanceDB 将“重排序”抽象为 Reranker 接口,允许你将任意融合逻辑注入混合查询。TS 侧接口定义为(index.ts):
export interface Reranker { rerankHybrid( query: string, vecResults: RecordBatch, ftsResults: RecordBatch, ): Promise<RecordBatch>; }Rust 侧的 trait(rerankers.rs)注释明确了设计意图:rerank 函数接收向量与 FTS 两路结果,“你可以选择使用其中任意结果来生成最终结果,以获得最大灵活性”。
对于自定义实现,需要遵守两个隐式契约(源码均有校验):
- 输出必须包含
_relevance_score列:Rust 侧check_reranker_result(rerankers.rs)会校验结果 schema 中必须存在名为_relevance_score的列,否则抛出Schema错误; - 输出通常按相关性分数降序排列:调用方按该列语义消费结果。
该设计意味着:如果需要按业务逻辑自定义融合(例如只保留 FTS 命中、或对某一路结果加权),实现一个自定义Reranker即可,而无需改动 LanceDB 内核。
总结
| 要点 | 结论 |
|---|---|
| 算法 | Reciprocal Rank Fusion,基于排名而非分数融合两路检索结果 |
| 默认参数 | k = 60,实验表明接近最优且取值不关键 |
| 创建方式 | await RRFReranker.create(k?),构造器被隐藏 |
| 核心方法 | rerankHybrid(query, vecResults, ftsResults) → RecordBatch |
| 输出结构 | 原行数据 +_rowid+_relevance_score(Float32),按分数降序 |
| 使用入口 | table.query().nearestTo(v).fullTextSearch(t).rerank(rrf) |
| 可扩展性 | 实现Reranker接口即可注入自定义融合策略,输出须含_relevance_score列 |
RRFReranker是 LanceDB 混合检索能力开箱即用的答案:无需归一化向量分数与 BM25 分数,无需调权重,一行代码即可获得稳定、可解释的融合排序。对于多数应用,使用默认k = 60即可;当某一路检索的排名置信度明显更高时,可以尝试调低k放大排名差异,通过业务指标验证后再固化配置。
- 向量数据库
- 数据库
- 人工智能
- 后端
【免费下载链接】lancedb
Developer-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.
相关推荐
LLM Zoomcamp 文档重排实战:用 Reciprocal Rank Fusion(RRF)提升 RAG 检索质量
LLM Zoomcamp 文档重排实战:用 Reciprocal Rank Fusion(RRF)提升 RAG 检索质量 本篇技术指南围绕 LLM Zoomca
示例工程教程人工智能大模型LLM Zoomcamp 混合搜索(Hybrid Search)实战指南:用 Reciprocal Rank Fusion 融合关键词与向量检索
LLM Zoomcamp 混合搜索(Hybrid Search)实战指南:用 Reciprocal Rank Fusion 融合关键词与向量检索 本篇是 LLM
示例工程教程人工智能大模型LanceDB Node.js 重排序接口 `Reranker` 实战:自定义混合检索排序与 RRF 实现深度解析
LanceDB Node.js 重排序接口 Reranker 实战:自定义混合检索排序与 RRF 实现深度解析 LanceDB 的 TypeScript/Jav
向量数据库数据库人工智能后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考