use log;
use self::qdrant_client::Qdrant;
use self::qdrant_client::{
qdrant::{
CreateCollectionBuilder, Distance, PointId, PointStruct, SearchPoints, UpsertPointsBuilder,
VectorParams, Vectors, WithPayloadSelector, WithVectorsSelector,
},
Payload,
};
pub use qdrant_client; use anyhow::{anyhow, Result};
use uuid::Uuid;
use serde::Deserialize;
use serde::Serialize;
use async_trait::async_trait;
use self::qdrant_client::qdrant::value::Kind as QdrantValueKind;
use crate::domain::reference::SearchResult;
use crate::domain::vector_repository::VectorRepository;
mod payload {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DocumentPayload {
pub file_path: String,
pub source: Option<String>, pub content_chunk: String, pub metadata: Option<serde_json::Value>, }
}
pub use self::payload::DocumentPayload;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DocumentToUpsert {
pub file_path: String,
pub vector: Vec<f32>,
pub source: Option<String>, pub content_chunk: String, pub metadata: Option<serde_json::Value>, }
pub struct VectorDb {
client: Box<Qdrant>,
collection_name: String,
vector_size: u64,
}
impl VectorDb {
pub fn new(client: Box<Qdrant>, collection_name: String, vector_size: u64) -> Result<Self> {
if collection_name.is_empty() {
return Err(anyhow!("Collection name cannot be empty"));
}
if vector_size == 0 {
return Err(anyhow!("Vector size must be greater than zero"));
}
Ok(Self {
client,
collection_name,
vector_size,
})
}
pub async fn initialize_collection(&self) -> Result<()> {
log::info!(
"Checking if collection '{}' exists...",
self.collection_name
);
match self.client.collection_info(&self.collection_name).await {
Ok(_) => {
log::info!("Collection '{}' already exists.", self.collection_name);
Ok(())
}
Err(e) => {
log::warn!("Collection '{}' not found or error checking existence: {}. Attempting to create...", self.collection_name, e);
self.create_collection_internal().await
}
}
}
async fn create_collection_internal(&self) -> Result<()> {
log::info!(
"Creating collection '{}' with size {} and distance Cosine...",
self.collection_name,
self.vector_size
);
let vector_params = VectorParams {
size: self.vector_size,
distance: Distance::Cosine.into(),
hnsw_config: None,
quantization_config: None,
on_disk: None,
multivector_config: None,
datatype: None,
};
let create_builder = CreateCollectionBuilder::new(self.collection_name.clone())
.vectors_config(vector_params);
match self.client.create_collection(create_builder).await {
Ok(_) => {
log::info!(
"Successfully created collection '{}'.",
self.collection_name
);
Ok(())
}
Err(e) => {
log::error!(
"Failed to create collection '{}': {}",
self.collection_name,
e
);
Err(anyhow!("Failed to create collection: {}", e))
}
}
}
pub async fn upsert_documents_impl(&self, documents: &[DocumentToUpsert]) -> Result<()> {
if documents.is_empty() {
log::info!("No documents provided for upsert.");
return Ok(());
}
log::info!(
"Preparing to upsert {} documents into collection '{}'...",
documents.len(),
self.collection_name
);
let points: Vec<PointStruct> = documents
.iter()
.filter_map(|doc| { let payload_struct = DocumentPayload {
file_path: doc.file_path.clone(),
source: doc.source.clone(),
content_chunk: doc.content_chunk.clone(),
metadata: doc.metadata.clone(),
};
let payload_value: serde_json::Value = match serde_json::to_value(payload_struct) {
Ok(v) => v,
Err(e) => {
log::error!("Failed to serialize DocumentPayload for file '{}': {}", doc.file_path, e);
return None; }
};
let payload: Payload = match Payload::try_from(payload_value) {
Ok(p) => p,
Err(e) => {
log::error!("Failed to convert serde_json::Value to Qdrant Payload for file '{}': {}", doc.file_path, e);
return None; }
};
let point_id: PointId = PointId::from(Uuid::new_v4().to_string());
Some(PointStruct {
id: Some(point_id),
vectors: Some(Vectors::from(doc.vector.clone())),
payload: payload.into(), })
})
.collect();
if points.is_empty() {
log::warn!("No valid points could be prepared for upserting (input count: {}). Check serialization/conversion errors.", documents.len());
return Ok(());
}
let points_count = points.len();
log::info!(
"Upserting {} valid points into collection '{}'...",
points_count,
self.collection_name
);
let upsert_builder =
UpsertPointsBuilder::new(self.collection_name.clone(), points).wait(true);
match self.client.upsert_points(upsert_builder).await {
Ok(response) => {
log::debug!("Upsert response: {:?}", response);
if let Some(result) = response.result {
log::info!(
"Upsert operation completed with status: {:?}",
result.status()
);
} else {
log::warn!("Upsert response did not contain result details.");
}
log::info!("Successfully requested upsert for {} points.", points_count);
Ok(())
}
Err(e) => {
log::error!(
"Failed to upsert points into collection '{}': {}",
self.collection_name,
e
);
Err(anyhow!("Qdrant upsert failed: {}", e))
}
}
}
pub async fn search_impl(
&self,
query_vector: Vec<f32>,
limit: usize,
score_threshold: Option<f32>,
) -> Result<Vec<SearchResult>> {
if query_vector.len() as u64 != self.vector_size {
return Err(anyhow!(
"Query vector dimension ({}) does not match collection dimension ({})",
query_vector.len(),
self.vector_size
));
}
log::info!(
"Searching in collection '{}' with limit {}...",
self.collection_name,
limit
);
let search_request = SearchPoints {
collection_name: self.collection_name.clone(),
vector: query_vector,
limit: limit as u64,
with_payload: Some(WithPayloadSelector {
selector_options: Some(
qdrant_client::qdrant::with_payload_selector::SelectorOptions::Enable(true),
),
}),
with_vectors: Some(WithVectorsSelector {
selector_options: Some(
qdrant_client::qdrant::with_vectors_selector::SelectorOptions::Enable(false),
),
}),
score_threshold,
..Default::default()
};
log::debug!("Sending search request: {:?}", search_request);
match self.client.search_points(search_request).await {
Ok(response) => {
log::info!(
"Search completed successfully, found {} potential results.",
response.result.len()
);
let search_results: Vec<SearchResult> = response.result.into_iter()
.filter_map(|scored_point| {
let payload_map = scored_point.payload;
if payload_map.is_empty() {
log::warn!("Search result point {:?} has no payload, skipping.", scored_point.id);
return None;
}
let json_value = Self::qdrant_payload_to_serde_value(payload_map)?;
match serde_json::from_value::<DocumentPayload>(json_value) {
Ok(payload_data) => Some(SearchResult {
file_path: payload_data.file_path,
score: scored_point.score,
source: payload_data.source,
content_chunk: payload_data.content_chunk,
metadata: payload_data.metadata,
document_content: None,
}),
Err(e) => {
log::error!("Failed to deserialize DocumentPayload from search result {:?}: {}", scored_point.id, e);
None
}
}
})
.collect();
log::info!(
"Successfully mapped {} results to SearchResult.",
search_results.len()
);
Ok(search_results)
}
Err(e) => {
log::error!(
"Qdrant search failed in collection '{}': {}",
self.collection_name,
e
);
Err(anyhow!("Qdrant search failed: {}", e))
}
}
}
fn qdrant_payload_to_serde_value(
payload_map: std::collections::HashMap<String, qdrant_client::qdrant::Value>,
) -> Option<serde_json::Value> {
let mut json_map = serde_json::Map::new();
for (key, qdrant_value) in payload_map {
let json_value = match qdrant_value.kind {
Some(QdrantValueKind::NullValue(_)) => serde_json::Value::Null,
Some(QdrantValueKind::BoolValue(b)) => serde_json::Value::Bool(b),
Some(QdrantValueKind::DoubleValue(d)) => serde_json::Number::from_f64(d)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
Some(QdrantValueKind::IntegerValue(i)) => serde_json::Value::Number(i.into()),
Some(QdrantValueKind::StringValue(s)) => serde_json::Value::String(s),
Some(QdrantValueKind::ListValue(list)) => {
let json_list: Vec<serde_json::Value> = list
.values
.into_iter()
.filter_map(|v| {
Self::qdrant_payload_to_serde_value(std::collections::HashMap::from([
("inner".to_string(), v),
]))
})
.map(|v| v.get("inner").unwrap_or(&serde_json::Value::Null).clone())
.collect();
serde_json::Value::Array(json_list)
}
Some(QdrantValueKind::StructValue(s)) => {
let inner_map = s.fields;
Self::qdrant_payload_to_serde_value(inner_map)? }
None => serde_json::Value::Null,
};
json_map.insert(key, json_value);
}
Some(serde_json::Value::Object(json_map))
}
}
#[async_trait]
impl VectorRepository for VectorDb {
async fn upsert_documents(&self, documents: &[DocumentToUpsert]) -> Result<()> {
self.upsert_documents_impl(documents).await
}
async fn search(
&self,
query_vector: Vec<f32>,
limit: usize,
score_threshold: Option<f32>,
) -> Result<Vec<SearchResult>> {
self.search_impl(query_vector, limit, score_threshold).await
}
}