First batch of smart stuff

This commit is contained in:
2025-12-25 23:53:12 +00:00
parent 4cb398869d
commit 58de25e5bc
34 changed files with 2974 additions and 74 deletions

View File

@@ -204,6 +204,27 @@ impl NoteVersion {
}
}
/// A derived link between two notes, typically generated by semantic similarity.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NoteLink {
pub source_note_id: Uuid,
pub target_note_id: Uuid,
/// Similarity score (0.0 to 1.0)
pub score: f32,
pub created_at: DateTime<Utc>,
}
impl NoteLink {
pub fn new(source_note_id: Uuid, target_note_id: Uuid, score: f32) -> Self {
Self {
source_note_id,
target_note_id,
score,
created_at: Utc::now(),
}
}
}
/// Filter options for querying notes
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NoteFilter {

View File

@@ -47,6 +47,10 @@ pub enum DomainError {
/// A repository/infrastructure error occurred
#[error("Repository error: {0}")]
RepositoryError(String),
/// An infrastructure adapter error occurred
#[error("Infrastructure error: {0}")]
InfrastructureError(String),
}
impl DomainError {

View File

@@ -10,6 +10,7 @@
pub mod entities;
pub mod errors;
pub mod ports;
pub mod repositories;
pub mod services;

36
notes-domain/src/ports.rs Normal file
View File

@@ -0,0 +1,36 @@
use async_trait::async_trait;
use uuid::Uuid;
use crate::entities::NoteLink;
use crate::errors::DomainResult;
/// Defines how to generate vector embeddings from text.
#[async_trait]
pub trait EmbeddingGenerator: Send + Sync {
/// Generate a vector embedding for the given text.
async fn generate_embedding(&self, text: &str) -> DomainResult<Vec<f32>>;
}
/// Defines how to store and retrieve vectors.
#[async_trait]
pub trait VectorStore: Send + Sync {
/// Upsert a vector for a given note ID.
async fn upsert(&self, id: Uuid, vector: &[f32]) -> DomainResult<()>;
/// Find similar items to the given vector.
/// Returns a list of (NoteID, Score) tuples.
async fn find_similar(&self, vector: &[f32], limit: usize) -> DomainResult<Vec<(Uuid, f32)>>;
}
/// Defines how to persist note links.
#[async_trait]
pub trait LinkRepository: Send + Sync {
/// Save a batch of generated links.
async fn save_links(&self, links: &[NoteLink]) -> DomainResult<()>;
/// Delete existing links for a specific source note (e.g., before regenerating).
async fn delete_links_for_source(&self, source_note_id: Uuid) -> DomainResult<()>;
/// Get links for a specific source note.
async fn get_links_for_note(&self, source_note_id: Uuid) -> DomainResult<Vec<NoteLink>>;
}

View File

@@ -356,6 +356,66 @@ impl UserService {
}
}
/// Service for Smart Features (Embeddings, Vector Search, Linking)
pub struct SmartNoteService {
embedding_generator: Arc<dyn crate::ports::EmbeddingGenerator>,
vector_store: Arc<dyn crate::ports::VectorStore>,
link_repo: Arc<dyn crate::ports::LinkRepository>,
}
impl SmartNoteService {
pub fn new(
embedding_generator: Arc<dyn crate::ports::EmbeddingGenerator>,
vector_store: Arc<dyn crate::ports::VectorStore>,
link_repo: Arc<dyn crate::ports::LinkRepository>,
) -> Self {
Self {
embedding_generator,
vector_store,
link_repo,
}
}
/// Process a note to generate embeddings and find similar notes
pub async fn process_note(&self, note: &Note) -> DomainResult<()> {
// 1. Generate embedding
let embedding = self
.embedding_generator
.generate_embedding(&note.content)
.await?;
// 2. Upsert to vector store
self.vector_store.upsert(note.id, &embedding).await?;
// 3. Find similar notes
// TODO: Make limit configurable
let similar = self.vector_store.find_similar(&embedding, 5).await?;
// 4. Create links
let links: Vec<crate::entities::NoteLink> = similar
.into_iter()
.filter(|(id, _)| *id != note.id) // Exclude self
.map(|(target_id, score)| crate::entities::NoteLink::new(note.id, target_id, score))
.collect();
// 5. Save links (replacing old ones)
if !links.is_empty() {
self.link_repo.delete_links_for_source(note.id).await?;
self.link_repo.save_links(&links).await?;
}
Ok(())
}
/// Get related notes for a given note ID
pub async fn get_related_notes(
&self,
note_id: Uuid,
) -> DomainResult<Vec<crate::entities::NoteLink>> {
self.link_repo.get_links_for_note(note_id).await
}
}
#[cfg(test)]
mod tests {
use super::*;