#!/usr/bin/env python3 """rag_hybrid.py — Hybrid search: FTS5 + cosine similarity""" import sqlite3, numpy as np, math, os from numpy.linalg import norm DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db") def blob_to_vector(blob: bytes) -> np.ndarray: return np.frombuffer(blob, dtype=np.float32).copy() def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: return float(np.dot(a, b)) def get_embedding_ollama(text: str) -> np.ndarray: import requests resp = requests.post("http://localhost:11434/api/embeddings", json={"model": "bge-m3", "input": text}) return np.array(resp.json()["embeddings"][0], dtype=np.float32) def hybrid_search(query: str, alpha: float = 0.4, top_k: int = 10): """ alpha: peso para FTS5, (1-alpha) para cosine similarity. 0.6 = nombres propios, 0.2 = conceptual, 0.4 = equilibrio """ conn = sqlite3.connect(DB_PATH) query_emb = get_embedding_ollama(query) query_emb = query_emb / (norm(query_emb) + 1e-10) cursor = conn.execute(""" SELECT c.id, c.contenido, c.embedding, bm25(chunks_fts, 0.0, 0.0, 5.0, 5.0) as fts_score FROM chunks_fts JOIN chunks c ON chunks_fts.rowid = c.id WHERE chunks_fts MATCH ? ORDER BY fts_score DESC LIMIT ? """, (query, top_k * 2)) results = [] for row in cursor: chunk_id, contenido, emb_blob, fts_score = row fts_norm = 1.0 / (1.0 + math.exp(-fts_score / 10.0)) emb_vec = blob_to_vector(emb_blob) emb_vec = emb_vec / (norm(emb_vec) + 1e-10) cos_sim = cosine_similarity(query_emb, emb_vec) score_total = alpha * fts_norm + (1.0 - alpha) * cos_sim results.append((score_total, contenido[:200], fts_norm, cos_sim)) results.sort(key=lambda x: x[0], reverse=True) conn.close() return results[:top_k]