use anyhow::{Context, Result};
use qdrant_client::qdrant::{
Condition, CreateCollectionBuilder, CreateFieldIndexCollectionBuilder, DeletePointsBuilder,
Distance, FieldType, Filter, PointId, PointStruct, PointsIdsList, Query, QueryPointsBuilder,
UpsertPointsBuilder, Value, VectorInput, VectorParamsBuilder, VectorsConfigBuilder,
};
use std::collections::HashMap;
use uuid::Uuid;
use crate::retrieval::qdrant::QdrantWrap;
const ARTIFACT_NS: Uuid = Uuid::from_bytes([
0x63, 0x73, 0x2d, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2d, 0x76, 0x35, 0x00, 0x01,
]);
pub fn artifact_point_id(artifact_id: &str) -> PointId {
PointId::from(Uuid::new_v5(&ARTIFACT_NS, artifact_id.as_bytes()).to_string())
}
impl QdrantWrap {
pub async fn ensure_artifacts_collection(&self, name: &str, dim: u64) -> Result<()> {
if self.collection_exists(name).await? {
return Ok(());
}
let mut vectors = VectorsConfigBuilder::default();
vectors.add_named_vector_params("dense", VectorParamsBuilder::new(dim, Distance::Cosine));
self.client
.create_collection(CreateCollectionBuilder::new(name).vectors_config(vectors))
.await
.context("create_collection(artifacts)")?;
self.client
.create_field_index(CreateFieldIndexCollectionBuilder::new(
name,
"project_id",
FieldType::Keyword,
))
.await
.context("create_field_index(project_id)")?;
Ok(())
}
pub async fn artifact_upsert(
&self,
collection: &str,
project_id: &str,
id: &str,
dense: Vec<f32>,
) -> Result<()> {
let mut payload: HashMap<String, Value> = HashMap::new();
payload.insert("project_id".into(), Value::from(project_id.to_string()));
payload.insert("artifact_id".into(), Value::from(id.to_string()));
let mut named: HashMap<String, qdrant_client::qdrant::Vector> = HashMap::new();
named.insert("dense".to_string(), dense.into());
let point = PointStruct::new(artifact_point_id(id), named, payload);
self.client
.upsert_points(UpsertPointsBuilder::new(collection, vec![point]).wait(true))
.await
.context("upsert_points(artifact)")?;
Ok(())
}
pub async fn artifact_delete(&self, collection: &str, id: &str) -> Result<()> {
self.client
.delete_points(
DeletePointsBuilder::new(collection)
.points(PointsIdsList {
ids: vec![artifact_point_id(id)],
})
.wait(true),
)
.await
.context("delete_points(artifact)")?;
Ok(())
}
pub async fn artifact_knn_ids(
&self,
collection: &str,
project_id: Option<&str>,
dense: Vec<f32>,
top_n: usize,
) -> Result<Vec<String>> {
let mut req = QueryPointsBuilder::new(collection)
.query(Query::new_nearest(VectorInput::new_dense(dense)))
.using("dense")
.limit(top_n as u64)
.with_payload(true);
if let Some(pid) = project_id {
req = req.filter(Filter::must(vec![Condition::matches(
"project_id",
pid.to_string(),
)]));
}
let resp = self
.client
.query(req.build())
.await
.context("artifact_knn_ids")?;
Ok(resp
.result
.into_iter()
.filter_map(|pt| {
pt.payload
.get("artifact_id")
.and_then(|v| v.as_str().map(|s| s.as_str().to_owned()))
})
.collect())
}
}