281 lines
12 KiB
Markdown
281 lines
12 KiB
Markdown
|
|
# 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<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
|
||
|
|
|
||
|
|
```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/`
|