diff --git a/crates/vestige-core/src/embeddings/local.rs b/crates/vestige-core/src/embeddings/local.rs index a727a45..3dfc363 100644 --- a/crates/vestige-core/src/embeddings/local.rs +++ b/crates/vestige-core/src/embeddings/local.rs @@ -33,18 +33,26 @@ pub const BATCH_SIZE: usize = 32; /// Result type for model initialization static EMBEDDING_MODEL_RESULT: OnceLock, String>> = OnceLock::new(); -/// Get the default cache directory for fastembed models -/// Uses FASTEMBED_CACHE_PATH env var, or falls back to platform cache directory -fn get_cache_dir() -> std::path::PathBuf { +/// Get the default cache directory for fastembed models. +/// +/// Resolution order: +/// 1. `FASTEMBED_CACHE_PATH` env var (explicit override) +/// 2. Platform cache dir via `directories::ProjectDirs` +/// - Linux: `$XDG_CACHE_HOME/vestige/fastembed` (typically `~/.cache/vestige/fastembed`) +/// - macOS: `~/Library/Caches/vestige/fastembed` +/// - Windows: `%LOCALAPPDATA%\vestige\cache\fastembed` +/// 3. `~/.cache/vestige/fastembed` (home-dir fallback) +/// 4. `.fastembed_cache` relative to CWD (absolute last resort, should never trigger) +pub(crate) fn get_cache_dir() -> std::path::PathBuf { if let Ok(path) = std::env::var("FASTEMBED_CACHE_PATH") { return std::path::PathBuf::from(path); } - // Use platform-appropriate cache directory via directories crate - // macOS: ~/Library/Caches/com.vestige.core/fastembed - // Linux: ~/.cache/vestige/fastembed - // Windows: %LOCALAPPDATA%\vestige\cache\fastembed - if let Some(proj_dirs) = directories::ProjectDirs::from("com", "vestige", "core") { + // qualifier="" produces a clean app-name-only path on Linux/Windows; + // on macOS the qualifier is used for the bundle ID so we keep it empty + // to get ~/Library/Caches/vestige/fastembed rather than + // ~/Library/Caches/com.vestige.vestige/fastembed. + if let Some(proj_dirs) = directories::ProjectDirs::from("", "vestige", "vestige") { return proj_dirs.cache_dir().join("fastembed"); } @@ -53,7 +61,7 @@ fn get_cache_dir() -> std::path::PathBuf { return base_dirs.home_dir().join(".cache/vestige/fastembed"); } - // Last resort fallback (shouldn't happen) + // Last resort fallback (shouldn't happen in practice) std::path::PathBuf::from(".fastembed_cache") } @@ -210,9 +218,7 @@ impl Default for EmbeddingService { impl EmbeddingService { /// Create a new embedding service pub fn new() -> Self { - Self { - _unused: (), - } + Self { _unused: () } } /// Check if the model is ready @@ -240,9 +246,13 @@ impl EmbeddingService { /// Get the model name pub fn model_name(&self) -> &'static str { #[cfg(feature = "nomic-v2")] - { "nomic-ai/nomic-embed-text-v2-moe" } + { + "nomic-ai/nomic-embed-text-v2-moe" + } #[cfg(not(feature = "nomic-v2"))] - { "nomic-ai/nomic-embed-text-v1.5" } + { + "nomic-ai/nomic-embed-text-v1.5" + } } /// Get the embedding dimensions diff --git a/crates/vestige-core/src/embeddings/mod.rs b/crates/vestige-core/src/embeddings/mod.rs index fcbb9d1..29eb300 100644 --- a/crates/vestige-core/src/embeddings/mod.rs +++ b/crates/vestige-core/src/embeddings/mod.rs @@ -13,6 +13,7 @@ mod code; mod hybrid; mod local; +pub(crate) use local::get_cache_dir; pub use local::{ cosine_similarity, dot_product, euclidean_distance, matryoshka_truncate, Embedding, EmbeddingError, EmbeddingService, BATCH_SIZE, EMBEDDING_DIMENSIONS, MAX_TEXT_LENGTH, diff --git a/crates/vestige-core/src/search/reranker.rs b/crates/vestige-core/src/search/reranker.rs index 5f1058a..1277cf7 100644 --- a/crates/vestige-core/src/search/reranker.rs +++ b/crates/vestige-core/src/search/reranker.rs @@ -10,6 +10,8 @@ //! Falls back to BM25-like term overlap scoring when the cross-encoder //! model is unavailable. +#[cfg(feature = "embeddings")] +use crate::embeddings::get_cache_dir; #[cfg(feature = "embeddings")] use fastembed::{RerankInitOptions, RerankerModel, TextRerank}; @@ -127,6 +129,7 @@ impl Reranker { } let options = RerankInitOptions::new(RerankerModel::JINARerankerV1TurboEn) + .with_cache_dir(get_cache_dir()) .with_show_download_progress(true); match TextRerank::try_new(options) { @@ -163,7 +166,9 @@ impl Reranker { top_k: Option, ) -> Result>, RerankerError> { if query.is_empty() { - return Err(RerankerError::InvalidInput("Query cannot be empty".to_string())); + return Err(RerankerError::InvalidInput( + "Query cannot be empty".to_string(), + )); } if candidates.is_empty() { @@ -190,7 +195,9 @@ impl Reranker { .collect(); results.sort_by(|a, b| { - b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal) + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) }); if let Some(min_score) = self.config.min_score { @@ -217,7 +224,11 @@ impl Reranker { }) .collect(); - results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); + results.sort_by(|a, b| { + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) + }); if let Some(min_score) = self.config.min_score { results.retain(|r| r.score >= min_score); diff --git a/crates/vestige-mcp/src/main.rs b/crates/vestige-mcp/src/main.rs index b594b0e..09a607f 100644 --- a/crates/vestige-mcp/src/main.rs +++ b/crates/vestige-mcp/src/main.rs @@ -180,7 +180,7 @@ async fn main() { if let Err(e) = s.init_embeddings() { error!("Failed to initialize embedding service: {}", e); error!("Smart ingest will fall back to regular ingest without deduplication"); - error!("Hint: Check FASTEMBED_CACHE_PATH or ensure ~/.fastembed_cache exists"); + error!("Hint: Check FASTEMBED_CACHE_PATH or ensure ~/.cache/vestige/fastembed is writable"); } else { info!("Embedding service initialized successfully"); }