stratumiops/docs/es/architecture/adrs/001-stratum-embeddings.md
Jesús Pérez 0ae853c2fa
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
chore: create stratum-embeddings and stratum-llm crates, docs
2026-01-24 02:03:12 +00:00

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:

  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

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

  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

// 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/