12 KiB
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:
"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:
- Unifique las implementaciones de Kogral, Provisioning, y Vapora
- Añada caching para evitar re-computar embeddings idénticos
- Implemente fallback entre providers (cloud → local)
- Documente claramente las dimensiones y limitaciones por provider
- Exponga métricas para observabilidad
- 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:
- LanceDB: búsqueda vectorial → candidate_ids
- SurrealDB: filtro de grafo sobre candidates → results
- 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
┌─────────────────────────────────────────────────────────────────┐
│ stratum-embeddings │
├─────────────────────────────────────────────────────────────────┤
│ EmbeddingProvider trait │
│ ├─ embed(text) → Vec<f32> │
│ ├─ embed_batch(texts) → Vec<Vec<f32>> │
│ ├─ 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<Match> │ │
│ │ └─ 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
- Single source of truth para todo el ecosistema
- 60-80% menos llamadas a APIs de embeddings (caching)
- Alta disponibilidad con providers locales (fallback)
- Métricas de uso y costos
- Feature-gated: solo compila lo necesario
- Storage flexibility: VectorStore trait permite elegir backend por proyecto
Negativas
- Dimension lock-in: Cambiar provider requiere re-indexar
- Cache invalidation: Contenido actualizado puede servir embeddings stale
- Model download: fastembed descarga ~100MB en primer uso
- 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
// Local, gratis, offline
let service = EmbeddingService::builder()
.with_provider(FastEmbedProvider::small()?) // 384 dims
.with_memory_cache()
.build()?;
Producción (Calidad)
// 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)
// 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/