RFC-001: Architecture Decisions
Status: Proposed Author: Manuel Couto Pintos Date: March 2026 Reviewers: (Open for comments)
1. Summary
Section titled “1. Summary”Este RFC documenta las decisiones arquitecturales fundamentales de ChronosVector (CVX), sus alternativas consideradas, y la justificación para cada elección. Cada decisión se registra como un ADR (Architecture Decision Record) independiente, facilitando la trazabilidad y la revisión futura.
2. Motivation
Section titled “2. Motivation”CVX combina tres dominios (vector search, temporal modeling, drift analytics) en un solo sistema. Muchas de las decisiones arquitecturales no tienen precedente directo porque ningún sistema existente integra estos tres dominios. Este RFC sirve como registro permanente del por qué detrás de cada elección, no solo del qué.
3. Architecture Decision Records
Section titled “3. Architecture Decision Records”ADR-001: Rust as Implementation Language
Section titled “ADR-001: Rust as Implementation Language”Context: CVX necesita control de memoria a nivel de byte para SIMD, zero-copy data paths, y concurrencia segura para acceso compartido al índice.
Decision: Implementar en Rust (edition 2021, MSRV 1.75+).
Alternatives Considered:
- C++: Máximo control, pero sin safety guarantees. El modelo de ownership de Rust previene data races en el índice concurrente sin runtime cost.
- Go: Excelente concurrencia, pero el garbage collector introduce latencia impredecible en el hot path de búsqueda. Qdrant (Rust) demuestra ventaja sobre Milvus (Go/C++) en latencia p99.
- Zig: Prometedor, pero ecosistema de crates inmaduro para las dependencias que necesitamos (RocksDB bindings, Arrow, protobuf).
Consequences:
- (+) SIMD explícito sin overhead de runtime.
- (+) Concurrencia fearless para índice shared-nothing por nodo.
- (+) Ecosystem:
rocksdb,arrow-rs,tonic,axum,burndisponibles y maduros. - (-) Curva de aprendizaje para contribuidores.
- (-) Compile times significativos en workspace multi-crate.
ADR-002: Composite Spatiotemporal Distance
Section titled “ADR-002: Composite Spatiotemporal Distance”Context: El enfoque convencional (filtrar por tiempo, luego buscar por distancia semántica) descarta la relación continua entre proximidad temporal y semántica.
Decision: Usar distancia compuesta como función nativa del índice, con configurable por query.
Alternatives Considered:
- Post-filtering: Buscar kNN semántico, luego filtrar por rango temporal. Simple, pero ignora vectores semánticamente lejanos pero temporalmente relevantes.
- Pre-filtering con Roaring Bitmaps: Calcular primero el set de IDs válidos temporalmente, luego buscar kNN solo dentro de ese set. Eficiente, pero no permite gradación temporal (un vector de ayer y uno de hace un año tienen el mismo peso).
- Separate indices per time bucket: Construir un HNSW por bucket temporal. Alto consumo de memoria, O(T) índices.
Consequences:
- (+) Queries expresivas: el usuario controla el trade-off temporal/semántico via α.
- (+) Un solo índice para todas las queries temporales.
- (-) La función de distancia compuesta es más costosa (~2x) que distancia pura.
- (-) α óptimo depende del use case; requiere experimentación por dominio.
Open Question: ¿La combinación lineal es suficiente? Podría necesitarse un modelo aprendido que combine ambas distancias de forma no-lineal. Deferred to Phase 5.
ADR-003: HNSW + Timestamp Graph (Hybrid Indexing)
Section titled “ADR-003: HNSW + Timestamp Graph (Hybrid Indexing)”Context: HNSW es el estado del arte para ANN search pero asume conjuntos estáticos. Wang et al. (ICDE 2025) proponen Timestamp Graphs que explotan la localidad temporal.
Decision: Implementar ST-HNSW como un HNSW estándar aumentado con:
- Roaring Bitmaps para filtrado temporal rápido (pre-filter).
- Timestamp Graph overlay para queries de snapshot exacto (TANNS).
- Time-decay edges para degradación gradual de conexiones antiguas.
Alternatives Considered:
- HNSW puro + metadata filtering: Lo que hace Qdrant. Funciona, pero no es nativo; cada query requiere cruzar filtro con grafo.
- DiskANN temporal partitioned: Un índice Vamana por partición temporal. Bueno para cold data, pero excesivo para hot data en RAM.
- Timestamp Graph solo (sin HNSW): El Timestamp Graph de Wang et al. resuelve TANNS pero no soporta nuestras queries de distancia compuesta.
Consequences:
- (+) Soporta snapshot kNN, range kNN, y distancia compuesta en un solo índice.
- (+) Roaring Bitmaps son O(1) para membership test, eficientes en memoria.
- (-) Complejidad de implementación: tres estructuras superpuestas.
- (-) El Timestamp Graph requiere manejo cuidadoso de expiración de nodos (backup neighbors).
ADR-004: Delta-Embedding Storage with Keyframes
Section titled “ADR-004: Delta-Embedding Storage with Keyframes”Context: Las entidades generan embeddings nuevos frecuentemente, pero entre dos timestamps consecutivos, la mayoría de las dimensiones cambian mínimamente.
Decision: Almacenar solo deltas entre timestamps, con keyframes (vector completo) cada K updates. Los deltas se almacenan como sparse vectors (indices + values).
Alternatives Considered:
- Full vector every time: Simple, pero desperdicia ~90% de storage para las dimensiones que no cambiaron.
- Full vector + compression (zstd): Mejor que raw, pero sigue almacenando datos redundantes. No permite reconstrucción parcial.
- Snapshot + WAL log: Similar a databases. Pero los “logs” no tienen estructura sparse aprovechable.
Consequences:
- (+) Reducción de storage estimada: 5-10x para series densas (updates frecuentes con cambios pequeños).
- (+) Los deltas sparse se comprimen aún más con codificación run-length.
- (-) Lectura de un punto arbitrario requiere reconstrucción: leer keyframe + acumular deltas (max K reads).
- (-) Necesita keyframe interval tuning: K muy grande → reconstrucción lenta; K muy pequeño → poco ahorro.
Mitigation: K=10 como default. Benchmark para encontrar el sweet spot por workload.
ADR-005: Tiered Storage (Hot/Warm/Cold)
Section titled “ADR-005: Tiered Storage (Hot/Warm/Cold)”Context: Los datos temporales tienen un patrón de acceso claro: los recientes se consultan frecuentemente, los históricos raramente.
Decision: Tres tiers con tecnologías diferentes:
- Hot: RocksDB (LSM-tree) con datos en RAM/SSD local. Vectores FP32 completos.
- Warm: Parquet files (arrow-rs). Datos de la última semana/mes. Formato columnar para analytics.
- Cold: Object Store (S3/MinIO) con Product Quantization. Datos históricos comprimidos.
Alternatives Considered:
- Single store (RocksDB for everything): Simple, pero RocksDB no es eficiente para analytics columnar ni para almacenamiento masivo barato.
- Two tiers (Hot + Cold): Viable, pero pierde la capacidad de analytics eficiente sobre datos medianamente recientes.
- SQLite + extension: Demasiado limitado para vectores de alta dimensión a escala.
- Lance format (LanceDB): Prometedor como evolución de Parquet para vectores, pero demasiado inmaduro para depender de él como único warm store. Podría reemplazar Parquet en el futuro.
Consequences:
- (+) Cada tier está optimizado para su patrón de acceso.
- (+) Cost-efficient: cold storage en object store es ~10x más barato que SSD.
- (-) Tres subsistemas de storage a mantener.
- (-) Compaction/migration entre tiers añade complejidad operativa.
ADR-006: RocksDB as Hot Store Engine
Section titled “ADR-006: RocksDB as Hot Store Engine”Context: El hot store necesita writes de baja latencia, range scans por prefix (entity_id + timestamp), y column families para separar vectores/deltas/metadata.
Decision: Usar RocksDB vía rust-rocksdb bindings.
Alternatives Considered:
- LMDB (via
lmdb-rkv): Excelente latencia de lectura, pero writes single-writer limitan throughput de ingesta. - Sled (pure Rust): No production-ready para nuestro volumen. Benchmarks muestran 2-5x más lento que RocksDB.
- Custom LSM-tree: Control total, pero años de engineering. RocksDB es battle-tested.
- FoundationDB: Excelente para distributed, pero overhead innecesario para single-node hot store.
Consequences:
- (+) Column families permiten separar datos con diferentes compaction strategies.
- (+) Bloom filters por prefix reducen lecturas innecesarias.
- (+) Tunable write buffer, block cache, compaction.
- (-) FFI boundary (C++ → Rust) — no podemos usar unsafe-free bindings.
- (-) Write amplification inherente a LSM-trees.
ADR-007: rkyv for Internal Serialization
Section titled “ADR-007: rkyv for Internal Serialization”Context: Los vectores y metadata se serializan/deserializan millones de veces por segundo en el hot path.
Decision: Usar rkyv (zero-copy deserialization) para datos internos. serde + serde_json/prost solo en la API boundary.
Alternatives Considered:
- serde + bincode: Rápido pero no zero-copy. Cada deserialización copia los datos.
- FlatBuffers: Zero-copy, pero la API es ergonómicamente pobre en Rust y requiere schema compilation.
- Cap’n Proto: Similar a FlatBuffers. Bueno, pero
rkyvestá más integrado en el ecosystem Rust. - Raw bytes (manual layout): Máximo rendimiento, pero propenso a errores y no portable entre architectures.
Consequences:
- (+) Deserialización en O(0) — el buffer se usa directamente como struct.
- (+)
rkyvsoporta#[derive(Archive)]nativo en Rust. - (-) Los datos rkyv no son human-readable; debugging requiere herramientas específicas.
- (-) Schema evolution más limitado que protobuf (no hay field numbers).
Mitigation: Versionado explícito en los primeros bytes de cada valor almacenado. Si la versión no coincide, se deserializa con el deserializer legacy y se re-serializa.
ADR-008: Neural ODE for Trajectory Prediction
Section titled “ADR-008: Neural ODE for Trajectory Prediction”Context: Queremos predecir posiciones futuras de vectores basándose en su historial. Las alternativas son interpolación lineal, modelos autorregresivos, o Neural ODEs.
Decision: Implementar un solver Dormand-Prince (RK45) con como MLP ligero entrenado por entity o por cluster de entities. Usar burn como backend de tensores.
Alternatives Considered:
- Linear extrapolation: . Trivial de implementar pero poor para trayectorias no lineales.
- ARIMA / exponential smoothing: Clásicos para time series, pero operan dimension-by-dimension, ignorando la geometría del espacio vectorial.
- Transformer-based: Potente pero costoso en inferencia. Overkill para un servicio de baja latencia.
- Neural CDE (Controlled Differential Equations): Más expresivo que Neural ODE para entradas irregulares, pero más complejo de implementar. Deferred to Phase 6.
Consequences:
- (+) Neural ODEs modelan flujos continuos — natural para embeddings que evolucionan suavemente.
- (+) El solver adaptativo ajusta step size automáticamente (preciso donde la trayectoria curva, rápido donde es recta).
- (+) Memoria constante vía adjoint method para training.
- (-) Training requiere trayectorias históricas suficientes (cold start problem).
- (-) Predicción degrada rápidamente para horizontes largos (fundamentally chaotic systems).
ADR-009: PELT + BOCPD Dual Change Point Detection
Section titled “ADR-009: PELT + BOCPD Dual Change Point Detection”Context: Necesitamos detectar tanto cambios bruscos históricos (análisis offline) como cambios en tiempo real (monitoreo online).
Decision: Implementar ambos algoritmos:
- PELT (offline): Para análisis batch de trayectorias completas. Resultado exacto, O(N).
- BOCPD (online): Para monitoreo streaming durante ingesta. Per-entity state, O(1) amortizado.
Alternatives Considered:
- Solo PELT: Excelente offline, pero no puede operar en streaming.
- Solo BOCPD: Puede operar online, pero para análisis histórico es menos preciso que PELT.
- CUSUM (Cumulative Sum): Clásico y simple, pero solo detecta cambios en media, no en distribución completa.
- Deep learning CPD: Redes neuronales para CPD (e.g., Transformers). Potente pero latencia inaceptable para online monitoring per-entity.
Consequences:
- (+) Cobertura completa: offline preciso + online rápido.
- (+) BOCPD per-entity permite miles de streams simultáneos con poco overhead.
- (-) Dos implementaciones a mantener.
- (-) BOCPD requiere tuning del hazard function y prior por dominio.
ADR-010: Workspace Multi-Crate Architecture
Section titled “ADR-010: Workspace Multi-Crate Architecture”Context: El sistema tiene 6 subsistemas con dependencias claras. Necesitamos compilation units independientes para builds incrementales rápidos.
Decision: Cargo workspace con 8 crates:
cvx-core → cvx-index, cvx-storage, cvx-ingest, cvx-analyticscvx-query → cvx-index, cvx-storage, cvx-analyticscvx-api → cvx-query, cvx-ingestcvx-server → cvx-apiAlternatives Considered:
- Monolithic crate: Compile times inmanejables a medida que crece. No permite feature-gate subsistemas.
- Many small crates (one per module): Over-engineering. Overhead de boilerplate y versioning excesivo para un solo desarrollador.
- Dynamic linking (dylib): Permitiría hot-reload, pero añade complejidad de deployment y pierde LTO optimizations.
Consequences:
- (+) Cada crate compila independientemente; cambios en
cvx-analyticsno recompilancvx-index. - (+) Feature flags por crate permiten builds mínimos (e.g., server sin analytics para testing).
- (+) Tests por crate son rápidos y focalizados.
- (-) Dependency management entre crates requiere disciplina.
- (-) Circular dependencies entre crates son compile errors, no warnings — requiere diseño upfront.
ADR-011: gRPC for Streaming, REST for Request-Response
Section titled “ADR-011: gRPC for Streaming, REST for Request-Response”Context: La ingesta necesita bidirectional streaming de alto throughput. Las queries son request-response con posible streaming de resultados.
Decision: Dual API:
tonic(gRPC) para ingesta streaming y WatchDrift subscriptions.axum(REST) para queries, admin operations, y health checks.
Alternatives Considered:
- gRPC only: Eficiente pero pobre developer experience para queries ad-hoc (no hay curl/Postman equivalente simple).
- REST only: Familiar pero no soporta bidirectional streaming nativamente. WebSockets como workaround añade complejidad.
- GraphQL: Expresivo para queries complejas, pero overhead de parsing innecesario para nuestro modelo de query tipado.
Consequences:
- (+) Mejor herramienta para cada job: gRPC para throughput, REST para usabilidad.
- (+) Protobuf como schema compartido entre ambos (gRPC nativo, REST vía serde mapping).
- (-) Dos servers escuchando en puertos diferentes.
- (-) Duplicación parcial de request/response types entre proto y REST models.
4. Decisions Deferred
Section titled “4. Decisions Deferred”| Decision | Deferred To | Reason |
|---|---|---|
| Hyperbolic metric as default | Phase 5 | Need empirical evidence on real workloads |
| Neural CDE vs Neural ODE | Phase 6 | ODE is simpler; CDE only if ODE proves insufficient |
| Learned distance combination (replacing linear α) | Phase 5 | Linear is baseline; optimize later |
| Sharding strategy (hash vs range) | Phase 5 (distributed) | Single-node first |
| Lance format replacing Parquet | Continuous evaluation | Depends on Lance crate maturity |
5. How to Propose Changes
Section titled “5. How to Propose Changes”- Copy the ADR template below.
- Add it to this RFC with the next sequential number.
- Set status to
Proposed. - After review and implementation, change status to
Acceptedand date.
### ADR-NNN: [Title]
**Context:** [What is the problem?]**Decision:** [What did we decide?]**Alternatives Considered:** [What else was evaluated?]**Consequences:** [What are the trade-offs?]