Merge origin/main (Tantivy content search) into delta-sync branch

Both sides added a parameter to create_application_services and a
setup step before it: this branch's storage-usage/quota service (for
the instant-upload path) and main's Tantivy content index (for
SearchService). The resolution keeps both — the signature takes both
arguments and the build runs storage usage as step 3c and the content
index as 3d.

https://claude.ai/code/session_01WdNenpnujNR2sc32XVvwfS
This commit is contained in:
Claude
2026-06-11 18:32:27 +00:00
19 changed files with 2807 additions and 68 deletions
@@ -0,0 +1,49 @@
//! Content Index Port - Application layer abstraction for full-text search
//! over file names and extracted file content.
//!
//! The implementation (an embedded Tantivy BM25 index) lives in the
//! infrastructure layer; `SearchService` only sees this port. The index is a
//! DERIVED artifact fed asynchronously by a background worker — it never sits
//! on a request path, and PostgreSQL remains the source of truth (hits are
//! re-validated and hydrated through SQL before they reach the caller, so a
//! stale index can only ever produce a dropped candidate, never a leak).
use async_trait::async_trait;
use uuid::Uuid;
use crate::common::errors::DomainError;
/// One content-index hit: a candidate file id with its BM25 score and an
/// optional plain-text snippet around the first match.
///
/// `file_id` is a CANDIDATE — callers must hydrate it through the metadata
/// repository (which re-applies user scoping, trash state and the active
/// search filters) before exposing it.
#[derive(Debug, Clone)]
pub struct ContentHitDto {
/// File UUID as string (matches `storage.files.id`).
pub file_id: String,
/// BM25 relevance score (positive, unbounded — normalize per result set).
pub score: f32,
/// Plain-text fragment around the first matched term, when available.
pub snippet: Option<String>,
}
/// Port for querying the full-text content index.
///
/// `#[async_trait]` is used so the trait is dyn-compatible — `SearchService`
/// holds an `Option<Arc<dyn ContentIndexPort>>` (the feature is toggleable).
#[async_trait]
pub trait ContentIndexPort: Send + Sync + 'static {
/// Search indexed file names + content for `query`, scoped to `user_id`.
///
/// Returns up to `limit` hits sorted by BM25 score descending. Matching is
/// tokenized (not substring): exact terms, typo-tolerant fuzzy terms
/// (edit distance 1) and prefix expansion on the last query token.
async fn search_content(
&self,
user_id: Uuid,
query: &str,
limit: usize,
) -> Result<Vec<ContentHitDto>, DomainError>;
}
+1
View File
@@ -7,6 +7,7 @@ pub mod calendar_ports;
pub mod carddav_ports;
pub mod chunked_upload_ports;
pub mod compression_ports;
pub mod content_index_ports;
pub mod dedup_ports;
pub mod email_sender;
pub mod favorites_ports;