Inicio / Artículos / Búsqueda híbrida para un homelab: Qdrant RRF, BM25 y chunking a medida

Búsqueda híbrida para un homelab: Qdrant RRF, BM25 y chunking a medida

Un asistente autoalojado para el homelab que trocea notas, hojas de cálculo, diagramas de Draw.io y logs de Loki, y luego recupera con vectores densos y dispersos, RRF y reordenación con cross-encoder.

1386 palabras

Rebuscar entre cincuenta archivos markdown, o seguir docker logs en tres ventanas de terminal, solo para encontrar una dirección IP es una forma agotadora de gestionar un homelab. Las notas están bien. Las respuestas inmediatas de una búsqueda vectorial híbrida están mejor. Este artículo construye un asistente totalmente autoalojado que indexa notas de Nextcloud, diagramas de red de Draw.io y flujos de logs en vivo de Grafana Loki, y se centra en la mecánica: cómo se trocean los datos, cómo se comportan matemáticamente los embeddings y cómo Qdrant recupera y fusiona los resultados.

La paradoja de la documentación

El laboratorio en cuestión tiene un hipervisor Proxmox de varios nodos, un firewall OPNsense que enruta varias VLAN y decenas de contenedores Docker como Jellyfin, Radarr, Prowlarr, Grafana y Home Assistant. Con el tiempo, la documentación acabó repartida en cuatro sitios:

  1. Notas de red y páginas de guía en Nextcloud Collectives, guardadas como archivos .md.
  2. Inventarios de hardware y asignaciones de IP en hojas de Excel .xlsx.
  3. Diagramas de topología dibujados en Draw.io y guardados como archivos .drawio.
  4. Logs de contenedores en vivo recogidos por Grafana Loki.

Cualquier incidencia o cambio en un bridge suponía una rutina manual: abrir Nextcloud, buscar la nota correcta, abrir un diagrama para comprobar las conexiones de las interfaces, recorrer una hoja de cálculo y luego ejecutar docker logs para revisar errores de contenedores. Frustrante y lento.

Los chatbots públicos no sirven aquí. No saben nada de subredes privadas como 10.27.30.2, bridges de Proxmox como vmbr1 o asignaciones de NIC como igc0, y cuando se les pregunta por infraestructura privada se inventan disparates con total seguridad. Así que el conocimiento tiene que quedarse en local y recuperarse en el momento de la pregunta.

Trocear cada formato a su manera

Las configuraciones RAG estándar cortan el texto cada pocos cientos de caracteres o palabras. Eso destroza los datos de un homelab. Una frase como «Proxmox Host 1 se conecta al puerto 2» puede quedar partida entre dos fragmentos, y la relación entre servidor y puerto se pierde. Por eso cada formato tiene su propia estrategia.

Markdown: cortar por encabezados

Los documentos se cortan por sus límites estructurales, es decir, #, ## y ###. Nextcloud Collectives guarda las páginas en Markdown por debajo, así que los encabezados ya están ahí. Cortar por ellos conserva la ruta de la sección, de modo que un fragmento no es texto suelto, sino que lleva toda su jerarquía:

Document: Home Server Setup | Section: Network > Firewalls > OPNsense
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OPNsense VM 101 is configured with passthrough interface igc0…

Hojas de cálculo: filas como frases

Con openpyxl se extrae cada fila de hardware y se reescribe como una afirmación explícita de clave y valor. Las hojas en bruto pierden los encabezados de columna cuando un troceador ingenuo las convierte en bloques de texto, mientras que una fila reescrita se sostiene por sí sola:

Row 4: Device: Proxmox Host 1 | Port: Port 2 | IP: 10.27.10.3 | Connection: Airtel Router

every single row becomes an independently searchable record in vector storage.

Cada fila pasa a ser un registro que se puede buscar de forma independiente en el almacén vectorial.

Draw.io: el XML oculto

Para leer diagramas de red no hace falta un OCR caro ni visión artificial, porque los archivos .drawio no son más que cadenas XML comprimidas. El zlib de Python se encarga del primer paso: decodificar la carga en base64 y descomprimirla con zlib.decompress(data, -15). Del árbol XML resultante de elementos <mxCell> salen nodos, dispositivos y aristas de conexión, que se escriben así:

## Nodes
- Node: Proxmox Host 1 (10.27.10.3)
- Node: OPNsense Firewall VM (101)
## Connections
- Connection: Airtel WAN Router -> Port 2 (igc0) -> OPNsense VM (101)

Las conexiones salen exactas, sin errores de reconocimiento y sin carga en la GPU.

Logs de Loki: dos niveles

Volcar las líneas en bruto de los logs de Docker desde Loki al almacén vectorial lo inundaría con millones de mensajes INFO inútiles. Un esquema adaptativo de dos niveles lo evita:

  • El nivel 1 cubre incidencias. Las ráfagas de errores críticos como ETIMEDOUT, respuestas HTTP 500, fallos y caídas de contenedores se trocean con marcas de tiempo exactas, etiquetas de contenedor como jellyfin o radarr y trazas de pila.
  • El nivel 2 cubre la actividad rutinaria. La salida normal de heartbeat se condensa en resúmenes de 15 minutos cada uno.
[Tier-1 Incident Event]
Timestamp: 2026–09–14 18:30:12 UTC | Service: jellyfin | Container: jellyfin
Log Event: ERR_FR_REDIRECTION_FAILURE attempting to connect to 10.27.30.2:8096

Dos espacios de embeddings

¿Por qué fiarse de un único modelo de embeddings cuando se pueden fusionar dos espacios matemáticos?

Vectores densos con all-MiniLM-L6-v2

Es un transformer MiniLM de 6 capas que produce vectores de 384 dimensiones. La relevancia se mide con la similitud del coseno entre el vector de la consulta A y el vector del fragmento B.

Los vectores densos captan el significado. El modelo entiende que un firewall es vecino conceptual de OPNsense y que el almacenamiento tiene que ver con un pool de ZFS. Su punto débil son los números, que se suavizan. 10.27.30.2 y 10.27.30.3 acaban en coordenadas casi idénticas, lo que provoca errores de precisión con las direcciones IP.

Vectores dispersos con FastEmbed BM25

La parte dispersa usa la frecuencia del término ponderada por la frecuencia inversa de documento, y puntúa coincidencias de palabras clave en una colección de documentos D para los términos de consulta q.

Aquí la fuerza es la exactitud. Tokens técnicos como direcciones IP (10.27.30.2), interfaces (igc0, vmbr1), puertos (8096) y códigos de error (ETIMEDOUT) coinciden a la perfección. El precio es que un sinónimo en lugar de la palabra literal no encuentra nada.

Almacenamiento, fusión y reordenación

Ambas representaciones van a una base de datos Qdrant en localhost:6333, dentro de una colección llamada homelab. Una consulta lanza entonces la recuperación híbrida.

Reciprocal Rank Fusion

Las puntuaciones densas del coseno (por ejemplo, 0,85) y las dispersas de BM25 (por ejemplo, 14,2) están en escalas completamente distintas, así que sumarlas sin más no tiene sentido. En su lugar, Qdrant combina las listas ordenadas de candidatos con Reciprocal Rank Fusion.

RRF premia el acuerdo. Un documento que queda primero en la búsqueda dispersa gracias a una coincidencia exacta con 10.27.30.2, y segundo en la densa gracias a una fuerte similitud conceptual, ve cómo su puntuación fusionada salta a lo más alto.

Reordenación con cross-encoder

Los bi-encoders como MiniLM codifican por separado consulta y documento al indexar. Para descartar falsos positivos, los diez mejores candidatos de RRF pasan por un cross-encoder, ms-marco-MiniLM-L-6-v2, que procesa juntos la consulta y el documento mediante la self-attention del transformer.

Los fragmentos con una puntuación inferior a MIN_RERANK_SCORE = -2.0 se descartan. Si el ruido eliminara todos los fragmentos, el pipeline recurre a una alternativa elegante: una cascada especulativa conserva al candidato mejor puntuado.

Un logit bruto del cross-encoder (z) se convierte en una probabilidad de relevancia intuitiva mediante la función sigmoide.

Distintos valores del logit se traducen en probabilidades como estas:

Con z = -2.0, un fragmento tiene alrededor de un 11,9 % de probabilidad de ser relevante. Todo lo que esté por encima aporta contexto útil o al menos un solapamiento temático débil que puede ayudar al modelo de lenguaje. Lo que esté por debajo cae rápido hacia cero, con z = -4.0 en apenas un 1,8 %, y suele reflejar contaminación de otro dominio, como ráfagas de logs de Docker en bruto mezcladas en una pregunta sobre subredes.

Query: "What is the OPNsense management IP?"

[Top Retrieved Chunks]
 Chunk 1: "OPNsense Firewall VM 101 IP: 10.27.10.1..."    ──► Logit: +4.8 (Keep)
 Chunk 2: "Proxmox host bridge vmbr0 LAN 10.27.10.3..."  ──► Logit: +0.4 (Keep)
 Chunk 3: "Grafana Loki log error: connection refused"  ──► Logit: -4.2 (Filtered out!)

Sin el umbral, el tercer fragmento llegaría al contexto del modelo, y el error del log podría confundirlo hasta dar una respuesta equivocada. El umbral de -2.0 funciona como un portero automático a la entrada de la ventana de contexto.

Verificación

Un script de regresión automatizado, tests/run_regression.py, evalúa el sistema con un conjunto de prueba de 17 preguntas sobre el homelab:

Ran 3 tests in 1.307s
OK
[Qdrant Connected] Vector DB ready at http://localhost:6333 (Collection: 'homelab').
[Regression Assertion] Hit@1 Accuracy Rate: 100.0% | MRR: 1.0000
```
* **Hit@1 Accuracy Rate**: **100.0%**
* **MRR (Mean Reciprocal Rank)**: **1.0000**
* **Time-to-First-Token (TTFT)**: **640.8 ms**

Todo junto

El troceado multimodal (encabezados de Markdown, filas de Excel, grafos XML de Draw.io descomprimidos y logs de Loki en dos niveles), los embeddings densos de MiniLM y dispersos de BM25, la fusión de rangos recíprocos de Qdrant y la reordenación con cross-encoder forman juntos un asistente privado y autoalojado. Puede responder preguntas sobre cada cable, dirección IP y error de contenedor Docker del rack.