# ADR-001: Stratum-Embeddings - Biblioteca Unificada de Embeddings ## Estado **Propuesto** ## Contexto ### Estado Actual: Implementaciones Fragmentadas El ecosistema tiene 3 implementaciones independientes de embeddings: | Proyecto | Ubicación | Providers | Caching | | ------------ | ------------------------------------- | ----------------------------- | ------- | | Kogral | `kogral-core/src/embeddings/` | fastembed, rig-core (parcial) | No | | Provisioning | `provisioning-rag/src/embeddings.rs` | OpenAI directo | No | | Vapora | `vapora-llm-router/src/embeddings.rs` | OpenAI, HuggingFace, Ollama | No | ### Problemas Identificados #### 1. Código Duplicado Cada proyecto reimplementa: - HTTP client para OpenAI embeddings - Parsing de respuestas JSON - Manejo de errores - Token estimation **Impacto**: ~400 líneas duplicadas, inconsistencias en manejo de errores. #### 2. Sin Caching Embeddings se regeneran cada vez: ```text "What is Rust?" → OpenAI → 1536 dims → $0.00002 "What is Rust?" → OpenAI → 1536 dims → $0.00002 (mismo resultado) "What is Rust?" → OpenAI → 1536 dims → $0.00002 (mismo resultado) ``` **Impacto**: Costos innecesarios, latencia adicional, rate limits más frecuentes. #### 3. No Hay Fallback Si OpenAI falla, todo falla. No hay fallback a alternativas locales (fastembed, Ollama). **Impacto**: Disponibilidad reducida, dependencia total de un provider. #### 4. Dimension Mismatch Silencioso Diferentes providers producen diferentes dimensiones: | Provider | Modelo | Dimensiones | | --------- | ---------------------- | ----------- | | fastembed | bge-small-en | 384 | | fastembed | bge-large-en | 1024 | | OpenAI | text-embedding-3-small | 1536 | | OpenAI | text-embedding-3-large | 3072 | | Ollama | nomic-embed-text | 768 | **Impacto**: Índices vectoriales corruptos si se cambia de provider. #### 5. Sin Métricas No hay visibilidad de uso, hit rate de cache, latencia por provider, ni costos acumulados. ## Decisión Crear `stratum-embeddings` como crate unificado que: 1. **Unifique** las implementaciones de Kogral, Provisioning, y Vapora 2. **Añada caching** para evitar re-computar embeddings idénticos 3. **Implemente fallback** entre providers (cloud → local) 4. **Documente claramente** las dimensiones y limitaciones por provider 5. **Exponga métricas** para observabilidad 6. **Provea VectorStore trait** con backends LanceDB y SurrealDB según necesidad del proyecto ### Decisión de Backend de Storage Cada proyecto elige su backend de vector storage según su prioridad: | Proyecto | Backend | Prioridad | Justificación | | ------------ | --------- | ----------------- | -------------------------------------------------------- | | Kogral | SurrealDB | Riqueza del grafo | Knowledge Graph necesita queries unificados graph+vector | | Provisioning | LanceDB | Escala vectorial | RAG con millones de chunks documentales | | Vapora | LanceDB | Escala vectorial | Traces de ejecución, pattern matching a escala | #### Por qué SurrealDB para Kogral Kogral es un Knowledge Graph donde las relaciones son el valor principal. Con arquitectura híbrida (LanceDB vectores + SurrealDB graph), un query típico requeriría: 1. LanceDB: búsqueda vectorial → candidate_ids 2. SurrealDB: filtro de grafo sobre candidates → results 3. App layer: merge, re-rank, deduplicación **Trade-off aceptado**: SurrealDB tiene peor rendimiento vectorial puro que LanceDB, pero la escala de Kogral está limitada por curación humana del conocimiento (10K-100K conceptos típicamente). #### Por qué LanceDB para Provisioning y Vapora | Aspecto | SurrealDB | LanceDB | | --------------- | ---------- | -------------------- | | Storage format | Row-based | Columnar (Lance) | | Vector index | HNSW (RAM) | IVF-PQ (disk-native) | | Escala práctica | Millones | Billones | | Compresión | ~1x | ~32x (PQ) | | Zero-copy read | No | Sí | ### Arquitectura ```text ┌─────────────────────────────────────────────────────────────────┐ │ stratum-embeddings │ ├─────────────────────────────────────────────────────────────────┤ │ EmbeddingProvider trait │ │ ├─ embed(text) → Vec │ │ ├─ embed_batch(texts) → Vec> │ │ ├─ dimensions() → usize │ │ └─ is_local() → bool │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ FastEmbed │ │ OpenAI │ │ Ollama │ │ │ │ (local) │ │ (cloud) │ │ (local) │ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ └────────────┬────────────┘ │ │ ▼ │ │ EmbeddingCache (memory/disk) │ │ │ │ │ ▼ │ │ EmbeddingService │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ VectorStore trait │ │ │ │ ├─ upsert(id, embedding, metadata) │ │ │ │ ├─ search(embedding, limit, filter) → Vec │ │ │ │ └─ delete(id) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ SurrealDbStore │ │ LanceDbStore │ │ │ │ (Kogral) │ │ (Prov/Vapora) │ │ │ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Justificación ### Por Qué Caching es Crítico Para un sistema RAG típico (10,000 chunks): - **Sin cache**: Re-indexaciones y queries repetidas multiplican costos - **Con cache**: Primera indexación paga, resto son cache hits **Ahorro estimado**: 60-80% en costos de embeddings. ### Por Qué Fallback es Importante | Escenario | Sin Fallback | Con Fallback | | ----------------- | ------------ | ------------------- | | OpenAI rate limit | ERROR | → fastembed (local) | | OpenAI downtime | ERROR | → Ollama (local) | | Sin internet | ERROR | → fastembed (local) | ### Por Qué Providers Locales Primero Para desarrollo: fastembed carga modelo local (~100MB), no requiere API keys, sin costos, funciona offline. Para producción: OpenAI para calidad, fastembed como fallback. ## Consecuencias ### Positivas 1. Single source of truth para todo el ecosistema 2. 60-80% menos llamadas a APIs de embeddings (caching) 3. Alta disponibilidad con providers locales (fallback) 4. Métricas de uso y costos 5. Feature-gated: solo compila lo necesario 6. Storage flexibility: VectorStore trait permite elegir backend por proyecto ### Negativas 1. **Dimension lock-in**: Cambiar provider requiere re-indexar 2. **Cache invalidation**: Contenido actualizado puede servir embeddings stale 3. **Model download**: fastembed descarga ~100MB en primer uso 4. **Storage lock-in por proyecto**: Kogral atado a SurrealDB, otros a LanceDB ### Mitigaciones | Negativo | Mitigación | | ----------------- | ------------------------------------------------------ | | Dimension lock-in | Documentar claramente, warn en cambio de provider | | Cache stale | TTL configurable, opción de bypass | | Model download | Mostrar progreso, cache en ~/.cache/fastembed | | Storage lock-in | Decisión consciente basada en prioridades del proyecto | ## Métricas de Éxito | Métrica | Actual | Objetivo | | --------------------------- | ------ | -------- | | Implementaciones duplicadas | 3 | 1 | | Cache hit rate | 0% | >60% | | Fallback availability | 0% | 100% | | Cost per 10K embeddings | ~$0.20 | ~$0.05 | ## Guía de Selección de Provider ### Desarrollo ```rust // Local, gratis, offline let service = EmbeddingService::builder() .with_provider(FastEmbedProvider::small()?) // 384 dims .with_memory_cache() .build()?; ``` ### Producción (Calidad) ```rust // OpenAI con fallback local let service = EmbeddingService::builder() .with_provider(OpenAiEmbeddingProvider::large()?) // 3072 dims .with_provider(FastEmbedProvider::large()?) // Fallback .with_memory_cache() .build()?; ``` ### Producción (Costo-Optimizado) ```rust // OpenAI small con fallback let service = EmbeddingService::builder() .with_provider(OpenAiEmbeddingProvider::small()?) // 1536 dims .with_provider(OllamaEmbeddingProvider::nomic()) // Fallback .with_memory_cache() .build()?; ``` ## Matriz de Compatibilidad de Dimensiones | Si usas... | Puedes cambiar a... | NO puedes cambiar a... | | ---------------------- | --------------------------- | ---------------------- | | fastembed small (384) | fastembed small, all-minilm | Cualquier otro | | fastembed large (1024) | fastembed large | Cualquier otro | | OpenAI small (1536) | OpenAI small, ada-002 | Cualquier otro | | OpenAI large (3072) | OpenAI large | Cualquier otro | **Regla**: Solo puedes cambiar entre modelos con las MISMAS dimensiones. ## Prioridad de Implementación | Orden | Feature | Razón | | ----- | ----------------------- | ---------------------------- | | 1 | EmbeddingProvider trait | Base para todo | | 2 | FastEmbed provider | Funciona sin API keys | | 3 | Memory cache | Mayor impacto en costos | | 4 | VectorStore trait | Abstracción de storage | | 5 | SurrealDbStore | Kogral necesita graph+vector | | 6 | LanceDbStore | Provisioning/Vapora escala | | 7 | OpenAI provider | Producción | | 8 | Ollama provider | Fallback local | | 9 | Batch processing | Eficiencia | | 10 | Metrics | Observabilidad | ## Referencias **Implementaciones Existentes**: - Kogral: `kogral-core/src/embeddings/` - Vapora: `vapora-llm-router/src/embeddings.rs` - Provisioning: `provisioning/platform/crates/rag/src/embeddings.rs` **Ubicación Objetivo**: `stratumiops/crates/stratum-embeddings/`