use anyhow::Result;
use arrow_array::{RecordBatch, StringArray};
use async_trait::async_trait;
use futures_util::TryStreamExt;
use itertools::Itertools;
use lancedb::query::{ExecutableQuery, QueryBase};
use swiftide_core::{
Retrieve,
document::Document,
indexing::Metadata,
querying::{
Query,
search_strategies::{CustomStrategy, SimilaritySingleEmbedding},
states,
},
};
use super::{FieldConfig, LanceDB};
#[async_trait]
impl Retrieve<SimilaritySingleEmbedding<String>> for LanceDB {
#[tracing::instrument]
async fn retrieve(
&self,
search_strategy: &SimilaritySingleEmbedding<String>,
query: Query<states::Pending>,
) -> Result<Query<states::Retrieved>> {
let Some(embedding) = &query.embedding else {
anyhow::bail!("No embedding for query")
};
let table = self
.get_connection()
.await?
.open_table(&self.table_name)
.execute()
.await?;
let vector_fields = self
.fields
.iter()
.filter(|field| matches!(field, FieldConfig::Vector(_)))
.collect_vec();
if vector_fields.is_empty() || vector_fields.len() > 1 {
anyhow::bail!("Zero or multiple vector fields configured in schema")
}
let column_name = vector_fields.first().map(|v| v.field_name()).unwrap();
let mut query_builder = table
.query()
.nearest_to(embedding.as_slice())?
.column(&column_name)
.limit(usize::try_from(search_strategy.top_k())?);
if let Some(filter) = &search_strategy.filter() {
query_builder = query_builder.only_if(filter);
}
let batches = query_builder
.execute()
.await?
.try_collect::<Vec<_>>()
.await?;
let documents = Self::retrieve_from_record_batches(batches.as_slice());
Ok(query.retrieved_documents(documents))
}
}
#[async_trait]
impl Retrieve<SimilaritySingleEmbedding> for LanceDB {
async fn retrieve(
&self,
search_strategy: &SimilaritySingleEmbedding,
query: Query<states::Pending>,
) -> Result<Query<states::Retrieved>> {
Retrieve::<SimilaritySingleEmbedding<String>>::retrieve(
self,
&search_strategy.into_concrete_filter::<String>(),
query,
)
.await
}
}
#[async_trait]
impl<Q: ExecutableQuery + Send + Sync + 'static> Retrieve<CustomStrategy<Q>> for LanceDB {
async fn retrieve(
&self,
search_strategy: &CustomStrategy<Q>,
query: Query<states::Pending>,
) -> Result<Query<states::Retrieved>> {
let query_builder = search_strategy.build_query(&query).await?;
let batches = query_builder
.execute()
.await?
.try_collect::<Vec<_>>()
.await?;
let documents = Self::retrieve_from_record_batches(batches.as_slice());
Ok(query.retrieved_documents(documents))
}
}
impl LanceDB {
fn retrieve_from_record_batches(batches: &[RecordBatch]) -> Vec<Document> {
let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
let mut documents = Vec::with_capacity(total_rows);
let process_batch = |batch: &RecordBatch, documents: &mut Vec<Document>| {
for row_idx in 0..batch.num_rows() {
let schema = batch.schema();
let (content, metadata): (String, Option<Metadata>) = {
let mut metadata = Metadata::default();
let mut content = String::new();
for (col_idx, field) in schema.as_ref().fields().iter().enumerate() {
if let Some(array) =
batch.column(col_idx).as_any().downcast_ref::<StringArray>()
{
let value = array.value(row_idx).to_string();
if field.name() == "chunk" {
content = value;
} else {
metadata.insert(field.name().clone(), value);
}
} else {
}
}
(
content,
if metadata.is_empty() {
None
} else {
Some(metadata)
},
)
};
documents.push(Document::new(content, metadata));
}
};
for batch in batches {
process_batch(batch, &mut documents);
}
documents
}
}
#[cfg(test)]
mod test {
use swiftide_core::{
Persist as _,
indexing::{self, EmbeddedField},
};
use temp_dir::TempDir;
use super::*;
async fn setup() -> (TempDir, LanceDB) {
let tempdir = TempDir::new().unwrap();
let lancedb = LanceDB::builder()
.uri(tempdir.child("lancedb").to_str().unwrap())
.vector_size(384)
.with_metadata("filter")
.with_vector(EmbeddedField::Combined)
.table_name("swiftide_test")
.build()
.unwrap();
lancedb.setup().await.unwrap();
(tempdir, lancedb)
}
#[tokio::test]
async fn test_retrieve_multiple_docs_and_filter() {
let (_guard, lancedb) = setup().await;
let nodes = vec![
indexing::TextNode::new("test_query1").with_metadata(("filter", "true")),
indexing::TextNode::new("test_query2").with_metadata(("filter", "true")),
indexing::TextNode::new("test_query3").with_metadata(("filter", "false")),
]
.into_iter()
.map(|node| {
node.with_vectors([(EmbeddedField::Combined, vec![1.0; 384])]);
node.to_owned()
})
.collect();
lancedb
.batch_store(nodes)
.await
.try_collect::<Vec<_>>()
.await
.unwrap();
let mut query = Query::<states::Pending>::new("test_query");
query.embedding = Some(vec![1.0; 384]);
let search_strategy =
SimilaritySingleEmbedding::from_filter("filter = \"true\"".to_string());
let result = lancedb
.retrieve(&search_strategy, query.clone())
.await
.unwrap();
assert_eq!(result.documents().len(), 2);
let search_strategy =
SimilaritySingleEmbedding::from_filter("filter = \"banana\"".to_string());
let result = lancedb
.retrieve(&search_strategy, query.clone())
.await
.unwrap();
assert_eq!(result.documents().len(), 0);
let search_strategy = SimilaritySingleEmbedding::<()>::default();
let result = lancedb
.retrieve(&search_strategy, query.clone())
.await
.unwrap();
assert_eq!(result.documents().len(), 3);
}
}