refactor (v2): better arch

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-06-07 21:19:54 +02:00
parent 0753f3d256
commit 839308ec19
166 changed files with 8553 additions and 884 deletions

View File

@@ -0,0 +1,14 @@
[package]
name = "qdrant-adapter"
version = "0.1.0"
edition = "2024"
[dependencies]
domain = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
qdrant-client = "1"
[dev-dependencies]
tokio = { workspace = true }

View File

@@ -0,0 +1,139 @@
use async_trait::async_trait;
use qdrant_client::{
Qdrant, QdrantError,
qdrant::{
CreateCollectionBuilder, DeletePointsBuilder, Distance, PointId, PointStruct,
PointsIdsList, SearchPointsBuilder, UpsertPointsBuilder, VectorParamsBuilder,
point_id::PointIdOptions,
},
};
use uuid::Uuid;
use domain::{
errors::{DomainError, DomainResult},
note::entity::NoteId,
smart::ports::VectorStore,
};
pub struct QdrantConfig {
pub url: String,
pub collection: String,
/// Dimensionality of the vectors stored in this collection.
/// Must match the output size of the embedding model (e.g. 384 for AllMiniLML6V2).
pub vector_size: u64,
}
impl Default for QdrantConfig {
fn default() -> Self {
Self {
url: "http://localhost:6334".into(),
collection: "notes".into(),
vector_size: 384,
}
}
}
pub struct QdrantVectorStore {
client: Qdrant,
collection: String,
}
impl QdrantVectorStore {
pub fn new(config: QdrantConfig) -> Result<Self, Box<QdrantError>> {
let client = Qdrant::from_url(&config.url).build().map_err(Box::new)?;
Ok(Self {
client,
collection: config.collection,
})
}
/// Ensure the collection exists. Call once during startup before accepting requests.
pub async fn init(&self, vector_size: u64) -> DomainResult<()> {
if self
.client
.collection_exists(&self.collection)
.await
.map_err(qdrant_err)?
{
return Ok(());
}
self.client
.create_collection(
CreateCollectionBuilder::new(&self.collection)
.vectors_config(VectorParamsBuilder::new(vector_size, Distance::Cosine)),
)
.await
.map_err(qdrant_err)?;
tracing::info!(collection = %self.collection, "qdrant collection created");
Ok(())
}
}
#[async_trait]
impl VectorStore for QdrantVectorStore {
async fn upsert(&self, id: &NoteId, vector: &[f32]) -> DomainResult<()> {
let point = PointStruct::new(
uuid_to_point_id(id.as_uuid()),
vector.to_vec(),
qdrant_client::Payload::default(),
);
self.client
.upsert_points(UpsertPointsBuilder::new(&self.collection, vec![point]))
.await
.map_err(qdrant_err)
.map(|_| ())
}
async fn find_similar(&self, vector: &[f32], limit: usize) -> DomainResult<Vec<(NoteId, f32)>> {
let response = self
.client
.search_points(
SearchPointsBuilder::new(&self.collection, vector.to_vec(), limit as u64)
.with_payload(false),
)
.await
.map_err(qdrant_err)?;
response
.result
.into_iter()
.filter_map(|scored| {
let uuid_str = match scored.id?.point_id_options? {
PointIdOptions::Uuid(s) => s,
_ => return None,
};
let uuid = Uuid::parse_str(&uuid_str).ok()?;
Some(Ok((NoteId::from_uuid(uuid), scored.score)))
})
.collect()
}
async fn delete(&self, id: &NoteId) -> DomainResult<()> {
self.client
.delete_points(
DeletePointsBuilder::new(&self.collection).points(PointsIdsList {
ids: vec![uuid_to_point_id(id.as_uuid())],
}),
)
.await
.map_err(qdrant_err)
.map(|_| ())
}
}
fn uuid_to_point_id(uuid: Uuid) -> PointId {
PointId {
point_id_options: Some(PointIdOptions::Uuid(uuid.to_string())),
}
}
fn qdrant_err(e: QdrantError) -> DomainError {
DomainError::Infrastructure(format!("qdrant: {e}"))
}
#[cfg(test)]
#[path = "tests/lib.rs"]
mod tests;

View File

@@ -0,0 +1,45 @@
use domain::{note::entity::NoteId, smart::ports::VectorStore};
use crate::{QdrantConfig, QdrantVectorStore};
const VECTOR_SIZE: u64 = 4; // small for tests
fn test_config() -> QdrantConfig {
QdrantConfig {
url: "http://localhost:6334".into(),
collection: "test-notes".into(),
vector_size: VECTOR_SIZE,
}
}
/// Requires a running Qdrant instance. Run with:
/// cargo test -p qdrant-adapter -- --ignored
#[tokio::test]
#[ignore]
async fn upsert_and_find_similar() {
let store = QdrantVectorStore::new(test_config()).unwrap();
store.init(VECTOR_SIZE).await.unwrap();
let id = NoteId::new();
let vector = vec![1.0f32, 0.0, 0.0, 0.0];
store.upsert(&id, &vector).await.unwrap();
let results = store.find_similar(&vector, 1).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, id);
assert!(results[0].1 > 0.99);
}
#[tokio::test]
#[ignore]
async fn delete_removes_vector() {
let store = QdrantVectorStore::new(test_config()).unwrap();
store.init(VECTOR_SIZE).await.unwrap();
let id = NoteId::new();
store.upsert(&id, &[1.0, 0.0, 0.0, 0.0]).await.unwrap();
store.delete(&id).await.unwrap();
let results = store.find_similar(&[1.0, 0.0, 0.0, 0.0], 10).await.unwrap();
assert!(!results.iter().any(|(rid, _)| rid == &id));
}