Las Fases 3 y 4 de la Sesión 166 agregaron capacidades por las que la mayoría de las bases de datos cobran extra. Las consultas de grafos -- camino más corto, PageRank, componentes conectados, detección de ciclos, ordenamiento topológico -- son típicamente el dominio de bases de datos de grafos dedicadas como Neo4j. La búsqueda semántica -- embeddings vectoriales, ranking de palabras clave BM25, recuperación híbrida -- es típicamente el dominio de motores de búsqueda dedicados como Elasticsearch o bases de datos vectoriales como Pinecone.
FlinDB tiene ambas. Integradas en la misma base de datos embebida. Sin servicios adicionales. Sin llamadas de red. Sin configuración. Porque si estás construyendo una aplicación potenciada por IA en FLIN, no deberías necesitar configurar tres bases de datos diferentes para almacenar datos, recorrer relaciones y buscar semánticamente.
Consultas de grafos: tratando las relaciones como aristas
El sistema de referencias de entidades de FlinDB forma naturalmente un grafo. Cada referencia de una entidad a otra es una arista. Cada entidad es un nodo. El motor de consultas de grafos opera sobre este grafo implícito sin requerir un modelo de grafo separado.
Camino más corto: BFS entre entidades
El método find_path() usa búsqueda en anchura para encontrar el camino más corto entre dos entidades a través de sus referencias:
rustpub fn find_path(
&self,
entity_type: &str,
from_id: u64,
to_id: u64,
) -> DatabaseResult<Option<Vec<u64>>> {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
let mut parent = HashMap::new();
queue.push_back(from_id);
visited.insert(from_id);
while let Some(current) = queue.pop_front() {
if current == to_id {
return Ok(Some(reconstruct_path(&parent, from_id, to_id)));
}
// Find all entities that this entity references
if let Ok(entity) = self.find_by_id(entity_type, current) {
for (field, value) in &entity.fields {
if let Value::EntityRef(_, ref_id) = value {
if !visited.contains(ref_id) {
visited.insert(*ref_id);
parent.insert(*ref_id, current);
queue.push_back(*ref_id);
}
}
}
}
}
Ok(None) // No path found
}Caso de uso: redes sociales ("¿cómo está conectado Thales con este otro usuario?"), jerarquías organizacionales ("¿cuál es la cadena de reporte desde este empleado hasta el CEO?"), grafos de dependencias ("¿cuál es el camino de dependencia más corto entre estos dos paquetes?").
Recorrido multi-salto
traverse() recorre el grafo de relaciones hasta una profundidad configurable:
rustlet related = db.traverse("Category", start_id, "parent", 3)?;
// Returns all categories reachable within 3 hops through the "parent" referenceEsto es esencial para datos jerárquicos. Dada una categoría raíz "Electrónica", un recorrido de 3 saltos a través de la referencia parent encuentra "Electrónica" -> "Teléfonos" -> "Smartphones" -> "iPhone 16". Sin recorrido de grafos, necesitarías consultas recursivas o múltiples viajes de ida y vuelta.
PageRank: puntuación de influencia
FlinDB implementa el algoritmo PageRank para computar puntuaciones de influencia a través de grafos de entidades:
PR(u) = (1-d) + d * SUM(PR(v) / out_degree(v))La implementación usa conteo de iteraciones y factor de amortiguación configurables:
rustpub fn page_rank(
&self,
entity_type: &str,
ref_field: &str,
iterations: usize, // default 20
damping: f64, // default 0.85
) -> DatabaseResult<HashMap<u64, f64>>Caso de uso: ranking de contenido por importancia ("¿qué artículos son más referenciados por otros artículos?"), identificación de entidades clave ("¿qué usuarios tienen más conexiones?"), priorización de resultados de búsqueda.
Componentes conectados
connected_components() identifica grupos de entidades que están conectadas a través de referencias:
rustlet components = db.connected_components("User", "follows")?;
// Returns: [[1, 2, 3], [4, 5], [6]]
// Three groups of connected usersCaso de uso: detección de comunidades en redes sociales, identificación de clústeres de datos aislados, encontrar subgrafos desconectados.
Detección de ciclos
find_cycles() detecta referencias circulares en grafos de entidades:
rustlet cycles = db.find_cycles("Task", "depends_on")?;
// Returns: [[1, 3, 5, 1]]
// Task 1 depends on 3, which depends on 5, which depends on 1Caso de uso: gestión de dependencias ("¿estas tareas tienen dependencias circulares?"), validación de datos ("¿es acíclica esta jerarquía de categorías?"), motores de flujo de trabajo ("¿este proceso hará un bucle infinito?").
Ordenamiento topológico
topological_sort() ordena entidades de forma que las dependencias vengan antes que los dependientes:
rustlet ordered = db.topological_sort("Task", "depends_on")?;
// Returns tasks in execution order: [5, 3, 1, 2, 4]Caso de uso: sistemas de compilación, programación de tareas, ordenamiento de migraciones, planificación de prerrequisitos de cursos.
Estadísticas de grafos
graph_stats() proporciona métricas de centralidad para el grafo de entidades:
rustpub struct GraphStats {
pub node_count: usize,
pub edge_count: usize,
pub avg_in_degree: f64,
pub avg_out_degree: f64,
pub max_in_degree: usize,
pub max_out_degree: usize,
pub highest_in_degree_node: Option<u64>,
pub highest_out_degree_node: Option<u64>,
}Estas estadísticas responden preguntas como "¿qué tan conectado está este grafo?", "¿qué entidad tiene más referencias entrantes?" y "¿cuál es el número promedio de relaciones por entidad?".
Búsqueda semántica: consultas potenciadas por IA
La Fase 4 de la Sesión 166 agregó búsqueda semántica -- la capacidad de encontrar entidades basándose en significado en lugar de coincidencias exactas de palabras clave. Esta es la funcionalidad que hace de FlinDB una base de datos nativa de IA.
Ranking de palabras clave BM25
Antes de la búsqueda vectorial, FlinDB implementa BM25 -- el mismo algoritmo de ranking usado por Elasticsearch y Solr. BM25 puntúa documentos por frecuencia de términos, frecuencia inversa de documentos y longitud del documento:
score(D, Q) = SUM( IDF(qi) * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * |D|/avgdl)) )
where:
- IDF = ln((N - n + 0.5) / (n + 0.5) + 1)
- k1 = 1.2 (term frequency saturation)
- b = 0.75 (length normalization)El BM25Index mantiene un índice invertido de términos a IDs de documentos, junto con estadísticas de frecuencia de documentos:
rustpub struct BM25Index {
inverted_index: HashMap<String, Vec<(u64, f64)>>, // term -> [(doc_id, tf)]
doc_lengths: HashMap<u64, usize>,
avg_doc_length: f64,
total_docs: usize,
}La búsqueda BM25 devuelve entidades rankeadas por relevancia de palabras clave:
rustdb.keyword_search("comfortable office chair", "Product", "description", 10)?;Búsqueda de similitud vectorial
Para la comprensión semántica más allá de la coincidencia de palabras clave, FlinDB genera embeddings vectoriales para campos semantic text. La configuración de embeddings es enchufable:
rustpub enum EmbeddingConfig {
Mock, // Hash-based, for testing
OpenAI { api_key: String, model: String },
Local { model_path: String },
}Cuando se guarda una entidad con un campo semantic text, FlinDB genera automáticamente un embedding y lo almacena en el índice vectorial:
rust// Setup
db.enable_semantic_search();
db.add_semantic_field("Product", "description");
// Save triggers automatic embedding
db.save("Product", None, {
"name": "Ergonomic Office Chair",
"description": "Comfortable seating with lumbar support..."
})?;
// Search by meaning
db.semantic_search(
"comfortable seating for work",
"Product",
"description",
10
)?;La consulta "comfortable seating for work" no tiene palabras en común con "Ergonomic Office Chair with lumbar support", pero la búsqueda semántica la encuentra porque el significado es similar. La búsqueda convierte la consulta a un vector, luego encuentra los vecinos más cercanos en el índice vectorial usando similitud coseno.
Búsqueda híbrida: lo mejor de ambos mundos
La búsqueda pura de palabras clave pierde sinónimos y paráfrasis. La búsqueda semántica pura puede perder coincidencias exactas y terminología específica. La búsqueda híbrida combina ambas usando Reciprocal Rank Fusion (RRF):
rustpub fn hybrid_search(
&self,
query: &str,
entity_type: &str,
field: &str,
limit: usize,
) -> DatabaseResult<Vec<HybridSearchResult>>RRF fusiona el ranking de palabras clave y el ranking semántico por posición recíproca de rango:
RRF_score(d) = 1/(k + rank_keyword(d)) + 1/(k + rank_semantic(d))Donde k es una constante (típicamente 60) que evita que cualquier ranking individual domine. Una entidad que ranquea #1 en búsqueda de palabras clave y #5 en búsqueda semántica puntúa más alto que una entidad que ranquea #2 en ambas -- porque el ranking #1 de palabras clave indica una fuerte coincidencia exacta.
flinresults = db.hybrid_search(
"comfortable seating for work",
"Product",
"description",
10
)
for r in results {
print("{r.entity_id}: keyword={r.keyword_score}, semantic={r.semantic_score}")
}Persistencia
Los índices de búsqueda se persisten en disco y se cargan al inicio:
rustdb.save_semantic_indexes(dir)?; // Save BM25 + vector indexes
db.load_semantic_indexes(dir)?; // Load on restart
db.reindex_semantic()?; // Rebuild indexes from dataEsto asegura que las capacidades de búsqueda sobrevivan a reinicios del servidor sin re-generar embeddings para todas las entidades.
Por qué estas funcionalidades pertenecen juntas
Una arquitectura tradicional para una aplicación potenciada por IA con relaciones de grafo requeriría:
- PostgreSQL para datos relacionales (CRUD, transacciones)
- Neo4j para consultas de grafos (camino más corto, PageRank)
- Elasticsearch para búsqueda de palabras clave (BM25, texto completo)
- Pinecone para búsqueda vectorial (similitud semántica)
Cuatro bases de datos. Cuatro cadenas de conexión. Cuatro esquemas. Cuatro objetivos de despliegue. Cuatro conjuntos de credenciales. Cuatro puntos potenciales de fallo.
FlinDB proporciona las cuatro capacidades en una sola base de datos embebida:
flin// CRUD
save user
// Graph
path = db.find_path("User", user1.id, user2.id)
// Keyword search
results = db.keyword_search("office chair", "Product", "description", 10)
// Semantic search
results = db.semantic_search("comfortable seating", "Product", "description", 10)
// Hybrid search
results = db.hybrid_search("comfortable office chair", "Product", "description", 10)Sin servicios adicionales. Sin latencia de red entre servicios. Sin sincronización de datos entre bases de datos. Los datos viven en un solo lugar, y todas las modalidades de consulta operan sobre los mismos datos.
Las cuarenta y cuatro pruebas de búsqueda semántica
La implementación de búsqueda semántica por sí sola requirió cuarenta y cuatro pruebas -- la mayor cantidad de pruebas de cualquier funcionalidad individual en FlinDB:
Pruebas BM25 (15): construcción del índice, cálculo de frecuencia de términos, frecuencia inversa de documentos, normalización de longitud de documentos, consultas multi-término, índice vacío, documento único, términos duplicados, ordenamiento por relevancia.
Pruebas de búsqueda vectorial (8): generación de embeddings, similitud coseno, vecino más cercano, múltiples resultados, índice vacío, discrepancia de dimensiones, filtrado por umbral.
Pruebas de búsqueda híbrida (7): cálculo RRF, rankings mixtos, coincidencias solo por palabras clave, coincidencias solo semánticas, impulso por coincidencia en ambos, resultados vacíos, aplicación de límite.
Pruebas de integración ZeroCore (7): habilitar/deshabilitar búsqueda semántica, auto-indexación al guardar, registro de campos, ciclo completo de persistencia, re-indexación.
Pruebas de consultas de grafos (11): camino más corto, recorrido multi-salto, convergencia de PageRank, componentes conectados, detección de ciclos, ordenamiento topológico, estadísticas de grafos.
Combinadas con las 12 pruebas de transacciones, la Sesión 166 agregó 94 pruebas en total -- llevando el conteo acumulado a 2.270.
Consideraciones de rendimiento
Las consultas de grafos y la búsqueda semántica tienen diferentes perfiles de rendimiento:
Las consultas de grafos están acotadas por el tamaño y conectividad del grafo. El camino más corto BFS es O(V + E) donde V son vértices y E son aristas. PageRank es O(iteraciones * E). Para el caso de uso embebido de FlinDB (miles a decenas de miles de entidades), estas son operaciones de sub-milisegundo.
La búsqueda BM25 es O(k * |V|) donde k es el número de términos de consulta y V es el tamaño del vocabulario. Para colecciones de documentos moderadas, esto es rápido. Para colecciones grandes, el índice invertido lo mantiene eficiente.
La búsqueda vectorial es O(n * d) donde n es el número de documentos y d es la dimensión del embedding. Este es un escaneo por fuerza bruta. Para colecciones grandes (millones de documentos), se necesitaría un índice de vecinos más cercanos aproximado como HNSW. Para la escala objetivo de FlinDB, la fuerza bruta es suficiente y evita la complejidad de mantener una estructura de índice separada.
La búsqueda híbrida combina los costos de BM25 y búsqueda vectorial, más un ordenamiento O(n log n) para la fusión RRF. La sobrecarga de fusión es insignificante comparada con los costos de búsqueda.
Estas características de rendimiento son apropiadas para una base de datos embebida. FlinDB no compite con Elasticsearch para búsqueda de miles de millones de documentos ni con Neo4j para recorrido de grafos de miles de millones de aristas. Proporciona estas capacidades a escala de aplicación -- donde la conveniencia de tener todo en una base de datos supera con creces la ventaja de rendimiento de sistemas especializados.
Esta es la Parte 9 de la serie "Cómo construimos FlinDB", documentando cómo construimos un motor de base de datos embebido completo para el lenguaje de programación FLIN.
Navegación de la serie: - [062] Relationships and Eager/Lazy Loading - [063] Transactions and Continuous Backup - [064] Graph Queries and Semantic Search (estás aquí) - [065] The EAVT Storage Model - [066] Database Encryption and Configuration