2026-09-19 — Retriever: cómo gestionar/escalar y hacerlo multifaceta

Investigación (búsqueda web) para el silo-engine (~/nef/nef-silo) y news-silo. Objetivo: pasar de “captura por cron” a un retriever gestionable, escalable y de múltiples facetas.

Fuentes consultadas

1. Gestión y escala (lo que falta en nef-silo)

Hoy: capturar recorre las 122 fuentes en serie cada 30 min, sin estado por fuente. Eso no escala ni se puede operar. El patrón estándar:

  1. Scheduler por fuente, no un barrido uniforme. Guardar por fuente last_fetch, etag, last_modified, fallos_consecutivos, n_items.
    • Conditional GET: si el feed responde 304 Not Modified, se salta parseo y downstream.
    • Backoff exponencial para fuentes muertas; “priority lane” para fuentes que el usuario añade.
  2. Dedup en dos etapas, en dos momentos (no se sustituyen):
    • Antes de fetch: dedup de URL (set exacto + bloom para memoria).
    • Después de parsear: fingerprint de contenido (SimHash/MinHash) para colapsar la misma noticia sindicada en URLs distintas. Hoy solo deduplico por URL → se cuelan copias.
  3. Aislamiento por fuente: un feed roto no debe parar el pipeline (ya lo hace, con errores por fuente) y hay que alertar si una fuente deja de producir (hoy solo se listan errores).
  4. Métricas operativas (en estado.json): ingest lag (publicación→captura), éxito de fetch por fuente, tasa de error de parseo, items por tema. Sin esto no hay gestión.
  5. Almacenamiento por dataset: SQLite (corpus/metadata, ya) + índice de vectores (ANN) para similitud + KV/contadores para el feed. No forzar todo a una sola DB.

2. Multifaceta (lo que pide “más completo”)

Faceta = dimensión de filtrado/agrupación. Hoy solo hay tema. Añadir a items: lang, region, tipo (tuit/nota/telegram), fuente_tipo (ya), y tiempo. Con eso el home y la búsqueda pueden cortar por cualquiera.

Búsqueda híbrida (el estándar 2026 para RAG/retrieval):

  • Rama léxica: BM25 (FTS5 de SQLite sirve).
  • Rama densa: embeddings (Qwen3-Embedding-0.6B local, o BGE-M3 multilingüe).
  • Fusión: RRF con k=60 (sin normalizar scores; robusto por defecto).
  • Rerank: cross-encoder (bge-reranker-v2-m3) sobre top-50 → top-5.
  • Filtros de faceta idénticos en ambas ramas (tema, idioma, rango de fechas); pre-filtro cuando la faceta es muy selectiva.

Clustering incremental (en vez de re-cluster completo por corrida): fingerprint + LSH para candidatos + “assign-or-open” (si el candidato más cercano supera umbral, se une al silo; si no, abre uno nuevo). Costo ~constante por artículo. Un job de union-find repara fragmentación en background.

3. Fix inmediato del clustering (ya aplicado)

silo.py reventaba con TypeError: only 0-dimensional arrays can be converted to Python scalars: bug de scikit-learn en traverse_upwards cuando cluster_selection_epsilon > 0 y hay distancias empatadas (NumPy ≥ 2.4). Workaround aplicado en news-silo/silo.py: epsilon por defecto 0.0 + copy=True. Alternativas si se necesita epsilon > 0: pin numpy<2.4 o el paquete hdbscan. Corrida verificada: 600 textos → 85 silos, 346 outliers (calidad mixta: varios silos se agrupan por fuente, no por evento; coincide con la nota previa de que KMeans separa mejor en corpus AI).

4. Roadmap concreto para nef-silo

  1. Tabla fuentes (last_fetch/etag/backoff) + conditional GET + métricas.
  2. Facetas en items (lang, region, tipo) + FTS5.
  3. Fingerprint de contenido (MinHash) además del dedup por URL.
  4. buscar: híbrida BM25+FTS5 + embeddings + RRF(k=60) + rerank.
  5. Clustering incremental (LSH + assign-or-open) al estilo SyFeed; inherit-first para no pagar embeddings de lo ya agrupado.

Nota de arquitectura: el caso SyFeed (un operador, self-hosted) usa Redis para scheduling, worker async, BGE-M3 + Qwen3-30B en vLLM, Meilisearch + Qdrant. Es el norte razonable, pero se llega por pasos: primero scheduler+dedup+métricas, luego híbrida+facetas, luego clustering incremental.