mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-05 12:59:11 +00:00
fix(node): preserve vector rerank query contracts
This commit is contained in:
@@ -80,12 +80,16 @@ describe("rerankers", function () {
|
||||
});
|
||||
|
||||
it("returns relevance scores when reranking a vector search", async function () {
|
||||
const result = await table
|
||||
const query = table
|
||||
.vectorSearch([0.1, 0.1])
|
||||
.limit(2)
|
||||
.rerank(await RRFReranker.create())
|
||||
.toArray();
|
||||
.rerank(await RRFReranker.create());
|
||||
const schema = await query.outputSchema();
|
||||
const result = await query.toArray();
|
||||
|
||||
expect(schema.fields.map((field) => field.name)).toContain(
|
||||
"_relevance_score",
|
||||
);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]._relevance_score).toBeCloseTo(1 / 60);
|
||||
expect(result[1]._relevance_score).toBeCloseTo(1 / 61);
|
||||
|
||||
+25
-13
@@ -5,9 +5,11 @@ import {
|
||||
Table as ArrowTable,
|
||||
type IntoVector,
|
||||
RecordBatch,
|
||||
createEmptyTable,
|
||||
extractVectorBuffer,
|
||||
fromBufferToRecordBatch,
|
||||
fromRecordBatchToBuffer,
|
||||
fromTableToBuffer,
|
||||
tableFromIPC,
|
||||
} from "./arrow";
|
||||
import { type IvfPqOptions } from "./indices";
|
||||
@@ -744,20 +746,30 @@ export class VectorQuery extends StandardQueryBase<NativeVectorQuery> {
|
||||
}
|
||||
|
||||
rerank(reranker: Reranker): VectorQuery {
|
||||
super.doCall((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,
|
||||
);
|
||||
super.doCall((inner) => {
|
||||
const outputSchema = reranker.outputSchema?.bind(reranker);
|
||||
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;
|
||||
}),
|
||||
);
|
||||
const buffer = fromRecordBatchToBuffer(result);
|
||||
return buffer;
|
||||
},
|
||||
outputSchema
|
||||
? async (args) => {
|
||||
const inputSchema = tableFromIPC(args.inputSchema).schema;
|
||||
const result = await outputSchema(inputSchema);
|
||||
return fromTableToBuffer(createEmptyTable(result));
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
import { RecordBatch } from "apache-arrow";
|
||||
import { RecordBatch, Schema } from "apache-arrow";
|
||||
|
||||
export * from "./rrf";
|
||||
|
||||
@@ -9,6 +9,14 @@ export * from "./rrf";
|
||||
// search results. For vector-only searches, query is empty and ftsResults is an
|
||||
// empty batch with the same schema as vecResults.
|
||||
export interface Reranker {
|
||||
/**
|
||||
* Declare the schema returned when reranking a vector-only query.
|
||||
*
|
||||
* This is required for vector-only reranking so query schema introspection
|
||||
* and execution agree. Hybrid-only rerankers may omit it.
|
||||
*/
|
||||
outputSchema?(inputSchema: Schema): Promise<Schema>;
|
||||
|
||||
rerankHybrid(
|
||||
query: string,
|
||||
vecResults: RecordBatch,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
import { RecordBatch } from "apache-arrow";
|
||||
import { Field, Float32, RecordBatch, Schema } from "apache-arrow";
|
||||
import { fromBufferToRecordBatch, fromRecordBatchToBuffer } from "../arrow";
|
||||
import { RrfReranker as NativeRRFReranker } from "../native";
|
||||
|
||||
@@ -24,6 +24,17 @@ export class RRFReranker {
|
||||
);
|
||||
}
|
||||
|
||||
/** Declare the RRF output schema for vector-only query execution. */
|
||||
async outputSchema(inputSchema: Schema): Promise<Schema> {
|
||||
return new Schema(
|
||||
[
|
||||
...inputSchema.fields,
|
||||
new Field("_relevance_score", new Float32(), false),
|
||||
],
|
||||
inputSchema.metadata,
|
||||
);
|
||||
}
|
||||
|
||||
async rerankHybrid(
|
||||
query: string,
|
||||
vecResults: RecordBatch,
|
||||
|
||||
+3
-2
@@ -6,8 +6,8 @@ use std::sync::Arc;
|
||||
use crate::error::NapiErrorExt;
|
||||
use crate::error::convert_error;
|
||||
use crate::iterator::RecordBatchIterator;
|
||||
use crate::rerankers::RerankHybridCallbackArgs;
|
||||
use crate::rerankers::Reranker;
|
||||
use crate::rerankers::{RerankHybridCallbackArgs, RerankOutputSchemaCallbackArgs};
|
||||
use crate::util::{parse_distance_type, schema_to_buffer};
|
||||
use arrow_array::{
|
||||
Array, Float16Array as ArrowFloat16Array, Float32Array as ArrowFloat32Array,
|
||||
@@ -388,8 +388,9 @@ impl VectorQuery {
|
||||
pub fn rerank(
|
||||
&mut self,
|
||||
rerank_hybrid: Function<RerankHybridCallbackArgs, Promise<Buffer>>,
|
||||
output_schema: Option<Function<RerankOutputSchemaCallbackArgs, Promise<Buffer>>>,
|
||||
) -> napi::Result<()> {
|
||||
let reranker = Reranker::new(rerank_hybrid)?;
|
||||
let reranker = Reranker::new(rerank_hybrid, output_schema)?;
|
||||
self.inner = self.inner.clone().rerank(Arc::new(reranker));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+51
-2
@@ -6,7 +6,7 @@ use async_trait::async_trait;
|
||||
use napi::{bindgen_prelude::*, threadsafe_function::ThreadsafeFunction};
|
||||
use napi_derive::napi;
|
||||
|
||||
use lancedb::ipc::batches_to_ipc_file;
|
||||
use lancedb::ipc::{batches_to_ipc_file, ipc_file_to_schema, schema_to_ipc_file};
|
||||
use lancedb::rerankers::Reranker as LanceDBReranker;
|
||||
use lancedb::{error::Error, ipc::ipc_file_to_batches};
|
||||
|
||||
@@ -21,28 +21,72 @@ type RerankHybridFn = ThreadsafeFunction<
|
||||
true,
|
||||
>;
|
||||
|
||||
type RerankOutputSchemaFn = ThreadsafeFunction<
|
||||
RerankOutputSchemaCallbackArgs,
|
||||
Promise<Buffer>,
|
||||
RerankOutputSchemaCallbackArgs,
|
||||
Status,
|
||||
false,
|
||||
true,
|
||||
>;
|
||||
|
||||
/// Reranker implementation that "wraps" a NodeJS Reranker implementation.
|
||||
/// This contains references to the callbacks that can be used to invoke the
|
||||
/// reranking methods on the NodeJS implementation and handles serializing the
|
||||
/// record batches to Arrow IPC buffers.
|
||||
pub struct Reranker {
|
||||
rerank_hybrid: RerankHybridFn,
|
||||
output_schema: Option<RerankOutputSchemaFn>,
|
||||
}
|
||||
|
||||
impl Reranker {
|
||||
pub fn new(
|
||||
rerank_hybrid: Function<RerankHybridCallbackArgs, Promise<Buffer>>,
|
||||
output_schema: Option<Function<RerankOutputSchemaCallbackArgs, Promise<Buffer>>>,
|
||||
) -> napi::Result<Self> {
|
||||
let rerank_hybrid = rerank_hybrid
|
||||
.build_threadsafe_function()
|
||||
.weak::<true>()
|
||||
.build()?;
|
||||
Ok(Self { rerank_hybrid })
|
||||
let output_schema = output_schema
|
||||
.map(|output_schema| {
|
||||
output_schema
|
||||
.build_threadsafe_function()
|
||||
.weak::<true>()
|
||||
.build()
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(Self {
|
||||
rerank_hybrid,
|
||||
output_schema,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl lancedb::rerankers::Reranker for Reranker {
|
||||
async fn output_schema(
|
||||
&self,
|
||||
input: &arrow_schema::SchemaRef,
|
||||
) -> lancedb::error::Result<arrow_schema::SchemaRef> {
|
||||
let output_schema = self.output_schema.as_ref().ok_or(Error::NotSupported {
|
||||
message: "vector rerankers must declare their output schema".to_string(),
|
||||
})?;
|
||||
let callback_args = RerankOutputSchemaCallbackArgs {
|
||||
input_schema: Buffer::from(schema_to_ipc_file(input.as_ref())?),
|
||||
};
|
||||
let promised_buffer: Promise<Buffer> = output_schema
|
||||
.call_async(callback_args)
|
||||
.await
|
||||
.map_err(|e| Error::Runtime {
|
||||
message: format!("napi error status={}, reason={}", e.status, e.reason),
|
||||
})?;
|
||||
let buffer = promised_buffer.await.map_err(|e| Error::Runtime {
|
||||
message: format!("napi error status={}, reason={}", e.status, e.reason),
|
||||
})?;
|
||||
ipc_file_to_schema(buffer.to_vec())
|
||||
}
|
||||
|
||||
async fn rerank_hybrid(
|
||||
&self,
|
||||
query: &str,
|
||||
@@ -86,6 +130,11 @@ pub struct RerankHybridCallbackArgs {
|
||||
pub fts_results: Buffer,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct RerankOutputSchemaCallbackArgs {
|
||||
pub input_schema: Buffer,
|
||||
}
|
||||
|
||||
fn buffer_to_record_batch(buffer: Buffer) -> Result<RecordBatch> {
|
||||
let mut reader = ipc_file_to_batches(buffer.to_vec()).default_error()?;
|
||||
reader
|
||||
|
||||
Reference in New Issue
Block a user