Skip to content

RFC-001: Architecture Decisions

Status: Proposed Author: Manuel Couto Pintos Date: March 2026 Reviewers: (Open for comments)


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.


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é.


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, burn disponibles 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 dST=αdsem+(1α)dtimedecayd_{ST} = \alpha \cdot d_{sem} + (1-\alpha) \cdot d_{time} \cdot decay como función nativa del índice, con α\alpha 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:

  1. Roaring Bitmaps para filtrado temporal rápido (pre-filter).
  2. Timestamp Graph overlay para queries de snapshot exacto (TANNS).
  3. 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 Δv=v(ti)v(ti1)\Delta v = v(t_i) - v(t_{i-1}) 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.


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.

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.

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 rkyv está 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.
  • (+) rkyv soporta #[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 fθf_\theta como MLP ligero entrenado por entity o por cluster de entities. Usar burn como backend de tensores.

Alternatives Considered:

  • Linear extrapolation: v(t+Δt)=v(t)+Δtdvdtv(t+\Delta t) = v(t) + \Delta t \cdot \frac{dv}{dt}. 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-analytics
cvx-query → cvx-index, cvx-storage, cvx-analytics
cvx-api → cvx-query, cvx-ingest
cvx-server → cvx-api

Alternatives 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-analytics no recompilan cvx-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.

DecisionDeferred ToReason
Hyperbolic metric as defaultPhase 5Need empirical evidence on real workloads
Neural CDE vs Neural ODEPhase 6ODE is simpler; CDE only if ODE proves insufficient
Learned distance combination (replacing linear α)Phase 5Linear is baseline; optimize later
Sharding strategy (hash vs range)Phase 5 (distributed)Single-node first
Lance format replacing ParquetContinuous evaluationDepends on Lance crate maturity

  1. Copy the ADR template below.
  2. Add it to this RFC with the next sequential number.
  3. Set status to Proposed.
  4. After review and implementation, change status to Accepted and 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?]