diff --git a/compose.prod.yml b/compose.prod.yml index c0698a3..2c95bfa 100644 --- a/compose.prod.yml +++ b/compose.prod.yml @@ -9,58 +9,18 @@ services: - HOST=0.0.0.0 - PORT=3000 - ALLOW_REGISTRATION=false - - NATS_URL=nats://k_nats:4222 - - QDRANT_URL=http://qdrant:6334 - - QDRANT_COLLECTION=notes - # SPA is bundled in the image at /app/frontend/dist (set in Dockerfile) - # Override here only if you move the dist elsewhere: - # - SPA_DIR=/app/frontend/dist volumes: - ./data:/app/data networks: - traefik - - shared-services - - internal labels: - "traefik.enable=true" - "traefik.docker.network=traefik" - # Both the app domain and the API subdomain point to the same service. - # The backend serves /api/v1/* and falls back to the SPA for everything else. - "traefik.http.routers.knotes.rule=Host(`knotes.gabrielkaszewski.dev`) || Host(`api.knotes.gabrielkaszewski.dev`)" - "traefik.http.routers.knotes.entrypoints=websecure" - "traefik.http.routers.knotes.tls.certresolver=letsencrypt" - "traefik.http.services.knotes.loadbalancer.server.port=3000" - worker: - image: registry.gabrielkaszewski.dev/k-notes:latest - command: ["./worker"] - environment: - - DATABASE_URL=sqlite:///app/data/notes.db - - NATS_URL=nats://k_nats:4222 - - QDRANT_URL=http://qdrant:6334 - - ENABLE_EMBEDDINGS=true - depends_on: - - backend - - qdrant - volumes: - - ./data:/app/data - networks: - - internal - - shared-services - - qdrant: - image: qdrant/qdrant:latest - container_name: k_notes_qdrant - volumes: - - ./data/qdrant_storage:/qdrant/storage:z - restart: unless-stopped - networks: - - internal - networks: traefik: external: true - shared-services: - external: true - internal: - driver: bridge diff --git a/crates/wiring/Cargo.toml b/crates/wiring/Cargo.toml index 6e91f6d..e2930cb 100644 --- a/crates/wiring/Cargo.toml +++ b/crates/wiring/Cargo.toml @@ -3,6 +3,10 @@ name = "wiring" version = "0.1.0" edition = "2024" +[features] +default = [] +smart = ["dep:fastembed-adapter", "dep:qdrant-adapter"] + [dependencies] # domain + application domain = { workspace = true } @@ -13,8 +17,8 @@ sqlite = { workspace = true } auth = { workspace = true } event-publisher-memory = { workspace = true } nats = { workspace = true } -fastembed-adapter = { workspace = true } -qdrant-adapter = { workspace = true } +fastembed-adapter = { workspace = true, optional = true } +qdrant-adapter = { workspace = true, optional = true } # utilities tokio = { workspace = true } diff --git a/crates/wiring/src/lib.rs b/crates/wiring/src/lib.rs index 9208723..c76ff30 100644 --- a/crates/wiring/src/lib.rs +++ b/crates/wiring/src/lib.rs @@ -12,7 +12,9 @@ use domain::{ type OptEmbedding = Option>; type OptVectorStore = Option>; use event_publisher_memory::MemoryEventBus; +#[cfg(feature = "smart")] use fastembed_adapter::{FastEmbedConfig, FastEmbedGenerator}; +#[cfg(feature = "smart")] use qdrant_adapter::{QdrantConfig, QdrantVectorStore}; use sqlite::{ db::{connect, run_migrations}, @@ -61,34 +63,38 @@ pub async fn build_context(cfg: &WiringConfig) -> anyhow::Result { (bus.publisher(), bus.consumer()) }; - // ── Smart features ──────────────────────────────────────────────────────── - // EmbeddingGenerator: only load the fastembed model in the worker. - // The backend only needs VectorStore (for querying related notes). - // Loading the model in both processes wastes ~150 MB per process. - let embedding: OptEmbedding = if cfg.enable_embeddings && cfg.qdrant_url.is_some() { - tracing::info!("loading fastembed embedding model"); - let embedder = FastEmbedGenerator::new(FastEmbedConfig::default()) - .map_err(|e| anyhow::anyhow!("fastembed init failed: {e}"))?; - Some(Arc::new(embedder) as Arc) - } else { - None + // ── Smart features (behind "smart" feature flag) ──────────────────────── + #[cfg(feature = "smart")] + let (embedding, vector_store): (OptEmbedding, OptVectorStore) = { + let emb: OptEmbedding = if cfg.enable_embeddings && cfg.qdrant_url.is_some() { + tracing::info!("loading fastembed embedding model"); + let embedder = FastEmbedGenerator::new(FastEmbedConfig::default()) + .map_err(|e| anyhow::anyhow!("fastembed init failed: {e}"))?; + Some(Arc::new(embedder) as Arc) + } else { + None + }; + + let vs: OptVectorStore = if let Some(ref url) = cfg.qdrant_url { + tracing::info!("connecting to qdrant at {url}"); + let qdrant = QdrantVectorStore::new(QdrantConfig { + url: url.clone(), + collection: cfg.qdrant_collection.clone(), + vector_size: cfg.qdrant_vector_size, + }) + .map_err(|e| anyhow::anyhow!("qdrant client init failed: {e}"))?; + qdrant.init(cfg.qdrant_vector_size).await?; + tracing::info!(collection = %cfg.qdrant_collection, "qdrant collection ready"); + Some(Arc::new(qdrant) as Arc) + } else { + None + }; + + (emb, vs) }; - let vector_store: OptVectorStore = if let Some(ref url) = cfg.qdrant_url { - tracing::info!("connecting to qdrant at {url}"); - let qdrant = QdrantVectorStore::new(QdrantConfig { - url: url.clone(), - collection: cfg.qdrant_collection.clone(), - vector_size: cfg.qdrant_vector_size, - }) - .map_err(|e| anyhow::anyhow!("qdrant client init failed: {e}"))?; - qdrant.init(cfg.qdrant_vector_size).await?; - tracing::info!(collection = %cfg.qdrant_collection, "qdrant collection ready"); - Some(Arc::new(qdrant) as Arc) - } else { - tracing::info!("no QDRANT_URL — smart features disabled"); - None - }; + #[cfg(not(feature = "smart"))] + let (embedding, vector_store): (OptEmbedding, OptVectorStore) = (None, None); Ok(AppContext { repos, diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index c26d4d9..9a439b0 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [dependencies] domain = { workspace = true } application = { workspace = true } -wiring = { workspace = true } +wiring = { workspace = true, features = ["smart"] } async-trait = { workspace = true } dotenvy = "0.15" tokio = { workspace = true } diff --git a/k-notes-frontend/src/assets/react.svg b/k-notes-frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/k-notes-frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file