use anyhow::{Result, bail};
use std::{
collections::{HashMap, HashSet},
string::ToString,
};
use qdrant_client::{
Payload,
qdrant::{self, Value},
};
use swiftide_core::{Embedding, SparseEmbedding, indexing::EmbeddedField};
use super::NodeWithVectors;
impl TryInto<qdrant::PointStruct> for NodeWithVectors<'_> {
type Error = anyhow::Error;
fn try_into(self) -> Result<qdrant::PointStruct> {
let node = self.node;
let id = node.id();
let mut payload: Payload = node
.metadata
.iter()
.map(|(k, v)| (k.clone(), Value::from(v.clone())))
.collect::<HashMap<String, Value>>()
.into();
payload.insert("path", node.path.to_string_lossy().to_string());
payload.insert("content", node.chunk.clone());
payload.insert(
"last_updated_at",
Value::from(chrono::Utc::now().to_rfc3339()),
);
let Some(vectors) = node.vectors.clone() else {
bail!("Node without vectors")
};
let vectors =
try_create_vectors(&self.vector_fields, vectors, node.sparse_vectors.clone())?;
Ok(qdrant::PointStruct::new(id.to_string(), vectors, payload))
}
}
fn try_create_vectors(
vector_fields: &HashSet<&EmbeddedField>,
vectors: HashMap<EmbeddedField, Embedding>,
sparse_vectors: Option<HashMap<EmbeddedField, SparseEmbedding>>,
) -> Result<qdrant::Vectors> {
if vectors.is_empty() {
bail!("Node with empty vectors")
} else if vectors.len() == 1 && sparse_vectors.is_none() {
let Some(vector) = vectors.into_values().next() else {
bail!("Node has no vector entry")
};
return Ok(vector.into());
}
let mut qdrant_vectors = qdrant::NamedVectors::default();
for (field, vector) in vectors {
if !vector_fields.contains(&field) {
continue;
}
qdrant_vectors = qdrant_vectors.add_vector(field.to_string(), vector);
}
if let Some(sparse_vectors) = sparse_vectors {
for (field, sparse_vector) in sparse_vectors {
if !vector_fields.contains(&field) {
continue;
}
qdrant_vectors = qdrant_vectors.add_vector(
format!("{field}_sparse"),
qdrant::Vector::new_sparse(
sparse_vector.indices.into_iter().collect::<Vec<_>>(),
sparse_vector.values,
),
);
}
}
Ok(qdrant_vectors.into())
}
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use qdrant_client::qdrant::PointStruct;
use swiftide_core::indexing::{EmbeddedField, TextNode};
use test_case::test_case;
use crate::qdrant::indexing_node::NodeWithVectors;
use pretty_assertions::assert_eq;
static EXPECTED_UUID: &str = "d42d252d-671d-37ef-a157-8e85d0710610";
#[test_case(
TextNode::builder()
.path("/path")
.chunk("data")
.vectors([(EmbeddedField::Chunk, vec![1.0])])
.metadata([("m1", "mv1")])
.embed_mode(swiftide_core::indexing::EmbedMode::SingleWithMetadata)
.build().unwrap()
,
HashSet::from([EmbeddedField::Combined]),
PointStruct::new(EXPECTED_UUID, vec![1.0], HashMap::from([
("content", "data".into()),
("path", "/path".into()),
("m1", "mv1".into())])
);
"Node with single vector creates struct with unnamed vector"
)]
#[test_case(
TextNode::builder()
.path("/path")
.chunk("data")
.vectors([
(EmbeddedField::Chunk, vec![1.0]),
(EmbeddedField::Metadata("m1".into()), vec![2.0])
])
.metadata([("m1", "mv1")])
.embed_mode(swiftide_core::indexing::EmbedMode::PerField)
.build().unwrap(),
HashSet::from([EmbeddedField::Chunk, EmbeddedField::Metadata("m1".into())]),
PointStruct::new(EXPECTED_UUID, HashMap::from([
("Chunk".to_string(), vec![1.0]),
("Metadata: m1".to_string(), vec![2.0])
]),
HashMap::from([
("content", "data".into()),
("path", "/path".into()),
("m1", "mv1".into())])
);
"Node with multiple vectors creates struct with named vectors"
)]
#[test_case(
TextNode::builder()
.path("/path")
.chunk("data")
.vectors([
(EmbeddedField::Chunk, vec![1.0]),
(EmbeddedField::Combined, vec![1.0]),
(EmbeddedField::Metadata("m1".into()), vec![1.0]),
(EmbeddedField::Metadata("m2".into()), vec![2.0])
])
.metadata([("m1", "mv1"), ("m2", "mv2")])
.embed_mode(swiftide_core::indexing::EmbedMode::Both)
.build().unwrap(),
HashSet::from([EmbeddedField::Combined]),
PointStruct::new(EXPECTED_UUID,
HashMap::from([
("Combined".to_string(), vec![1.0]),
]),
HashMap::from([
("content", "data".into()),
("path", "/path".into()),
("m1", "mv1".into()),
("m2", "mv2".into())])
);
"Storing only `Combined` vector. Skipping other vectors."
)]
#[allow(clippy::needless_pass_by_value)]
fn try_into_point_struct_test(
node: TextNode,
vector_fields: HashSet<EmbeddedField>,
mut expected_point: PointStruct,
) {
let node = NodeWithVectors::new(&node, vector_fields.iter().collect());
let point: PointStruct = node.try_into().expect("Can create PointStruct");
let last_updated_at_key = "last_updated_at";
let last_updated_at = point
.payload
.get(last_updated_at_key)
.expect("Has autogenerated `last_updated_at` field.");
expected_point
.payload
.insert(last_updated_at_key.into(), last_updated_at.clone());
assert_eq!(point.id, expected_point.id);
assert_eq!(point.payload, expected_point.payload);
assert_eq!(point.vectors, expected_point.vectors);
}
}