use std::collections::BTreeMap;
use std::sync::Arc;
use arrow::array::downcast_array;
use arrow::compute::{sort_to_indices, take};
use arrow_array::{Float32Array, RecordBatch, UInt64Array};
use arrow_schema::{DataType, Field, Schema, SortOptions};
use async_trait::async_trait;
use lance::dataset::ROW_ID;
use lancedb::rerankers::Reranker;
const RELEVANCE_SCORE: &str = "_relevance_score";
#[derive(Debug)]
pub struct WeightedRRFReranker {
k: f32,
vector_weight: f32,
keyword_weight: f32,
}
impl WeightedRRFReranker {
pub fn new(k: f32, vector_weight: f32, keyword_weight: f32) -> Self {
Self {
k: k.max(1.0),
vector_weight: vector_weight.max(0.0),
keyword_weight: keyword_weight.max(0.0),
}
}
}
#[async_trait]
impl Reranker for WeightedRRFReranker {
async fn rerank_hybrid(
&self,
_query: &str,
vector_results: RecordBatch,
fts_results: RecordBatch,
) -> lancedb::error::Result<RecordBatch> {
let mut rrf_scores: BTreeMap<u64, f32> = BTreeMap::new();
if vector_results.num_rows() > 0 {
let vec_ids = vector_results.column_by_name(ROW_ID).ok_or_else(|| {
lancedb::error::Error::InvalidInput {
message: format!("missing {} column on vector results", ROW_ID),
}
})?;
let vec_ids: UInt64Array = downcast_array(&vec_ids);
for (i, id) in vec_ids.values().iter().enumerate() {
let score = self.vector_weight / (i as f32 + self.k);
rrf_scores
.entry(*id)
.and_modify(|e| *e += score)
.or_insert(score);
}
}
if fts_results.num_rows() > 0 {
let fts_ids = fts_results.column_by_name(ROW_ID).ok_or_else(|| {
lancedb::error::Error::InvalidInput {
message: format!("missing {} column on fts results", ROW_ID),
}
})?;
let fts_ids: UInt64Array = downcast_array(&fts_ids);
for (i, id) in fts_ids.values().iter().enumerate() {
let score = self.keyword_weight / (i as f32 + self.k);
rrf_scores
.entry(*id)
.and_modify(|e| *e += score)
.or_insert(score);
}
}
let combined = merge_dedup(vector_results, fts_results)?;
let combined_row_ids =
combined
.column_by_name(ROW_ID)
.ok_or_else(|| lancedb::error::Error::InvalidInput {
message: format!("missing {} on combined batch", ROW_ID),
})?;
let combined_row_ids: UInt64Array = downcast_array(combined_row_ids);
let relevance_scores = Float32Array::from_iter_values(
combined_row_ids
.values()
.iter()
.map(|id| *rrf_scores.get(id).unwrap_or(&0.0)),
);
let sort_indices = sort_to_indices(
&relevance_scores,
Some(SortOptions {
descending: true,
..Default::default()
}),
None,
)
.map_err(|e| lancedb::error::Error::InvalidInput {
message: format!("sort failed: {e}"),
})?;
let mut columns = combined.columns().to_vec();
columns.push(Arc::new(relevance_scores));
let columns: Vec<_> = columns
.iter()
.map(|c| {
take(c, &sort_indices, None).map_err(|e| lancedb::error::Error::InvalidInput {
message: format!("take failed: {e}"),
})
})
.collect::<lancedb::error::Result<Vec<_>>>()?;
let mut fields = combined.schema().fields().to_vec();
fields.push(Arc::new(Field::new(
RELEVANCE_SCORE,
DataType::Float32,
false,
)));
let schema = Schema::new(fields);
RecordBatch::try_new(Arc::new(schema), columns).map_err(|e| {
lancedb::error::Error::InvalidInput {
message: format!("rebuild record batch failed: {e}"),
}
})
}
}
fn merge_dedup(
vector_results: RecordBatch,
fts_results: RecordBatch,
) -> lancedb::error::Result<RecordBatch> {
use arrow::array::BooleanArray;
use arrow::compute::{concat_batches, filter_record_batch};
use std::collections::BTreeSet;
let combined = concat_batches(&fts_results.schema(), [vector_results, fts_results].iter())
.map_err(|e| lancedb::error::Error::InvalidInput {
message: format!("concat failed: {e}"),
})?;
let mut mask = BooleanArray::builder(combined.num_rows());
let mut seen: BTreeSet<u64> = BTreeSet::new();
let row_ids =
combined
.column_by_name(ROW_ID)
.ok_or_else(|| lancedb::error::Error::InvalidInput {
message: format!("missing {} on combined batch", ROW_ID),
})?;
let row_ids: UInt64Array = downcast_array(row_ids);
for id in row_ids.values().iter() {
mask.append_value(seen.insert(*id));
}
filter_record_batch(&combined, &mask.finish()).map_err(|e| {
lancedb::error::Error::InvalidInput {
message: format!("filter failed: {e}"),
}
})
}