rag_hybrid.py
· 1.8 KiB · Python
Ham
#!/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]
| 1 | #!/usr/bin/env python3 |
| 2 | """rag_hybrid.py — Hybrid search: FTS5 + cosine similarity""" |
| 3 | import sqlite3, numpy as np, math, os |
| 4 | from numpy.linalg import norm |
| 5 | |
| 6 | DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db") |
| 7 | |
| 8 | def blob_to_vector(blob: bytes) -> np.ndarray: |
| 9 | return np.frombuffer(blob, dtype=np.float32).copy() |
| 10 | |
| 11 | def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: |
| 12 | return float(np.dot(a, b)) |
| 13 | |
| 14 | def get_embedding_ollama(text: str) -> np.ndarray: |
| 15 | import requests |
| 16 | resp = requests.post("http://localhost:11434/api/embeddings", |
| 17 | json={"model": "bge-m3", "input": text}) |
| 18 | return np.array(resp.json()["embeddings"][0], dtype=np.float32) |
| 19 | |
| 20 | def hybrid_search(query: str, alpha: float = 0.4, top_k: int = 10): |
| 21 | """ |
| 22 | alpha: peso para FTS5, (1-alpha) para cosine similarity. |
| 23 | 0.6 = nombres propios, 0.2 = conceptual, 0.4 = equilibrio |
| 24 | """ |
| 25 | conn = sqlite3.connect(DB_PATH) |
| 26 | query_emb = get_embedding_ollama(query) |
| 27 | query_emb = query_emb / (norm(query_emb) + 1e-10) |
| 28 | |
| 29 | cursor = conn.execute(""" |
| 30 | SELECT c.id, c.contenido, c.embedding, |
| 31 | bm25(chunks_fts, 0.0, 0.0, 5.0, 5.0) as fts_score |
| 32 | FROM chunks_fts |
| 33 | JOIN chunks c ON chunks_fts.rowid = c.id |
| 34 | WHERE chunks_fts MATCH ? |
| 35 | ORDER BY fts_score DESC |
| 36 | LIMIT ? |
| 37 | """, (query, top_k * 2)) |
| 38 | |
| 39 | results = [] |
| 40 | for row in cursor: |
| 41 | chunk_id, contenido, emb_blob, fts_score = row |
| 42 | fts_norm = 1.0 / (1.0 + math.exp(-fts_score / 10.0)) |
| 43 | emb_vec = blob_to_vector(emb_blob) |
| 44 | emb_vec = emb_vec / (norm(emb_vec) + 1e-10) |
| 45 | cos_sim = cosine_similarity(query_emb, emb_vec) |
| 46 | score_total = alpha * fts_norm + (1.0 - alpha) * cos_sim |
| 47 | results.append((score_total, contenido[:200], fts_norm, cos_sim)) |
| 48 | |
| 49 | results.sort(key=lambda x: x[0], reverse=True) |
| 50 | conn.close() |
| 51 | return results[:top_k] |
rag_hyde.py
· 2.9 KiB · Python
Ham
#!/usr/bin/env python3
"""rag_hyde.py — HyDE: Hypothetical Document Embeddings"""
import requests, numpy as np, sqlite3, os, math
from numpy.linalg import norm
DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db")
OLLAMA_URL = "http://localhost:11434/api"
def generate_hypothetical_document(query: str, model: str ="llama3.2") -> str:
prompt = f"""Genera un texto breve y factual que responda a la siguiente pregunta.
El texto debe ser informativo, objetivo y escrito en español.
Pregunta: {query}
Texto informativo:"""
resp = requests.post(f"{OLLAMA_URL}/generate",
json={"model": model, "prompt": prompt, "stream": False})
return resp.json()["response"]
def get_embedding(text: str, model: str ="bge-m3") -> np.ndarray:
resp = requests.post(f"{OLLAMA_URL}/embed",
json={"model": model, "input": text})
return np.array(resp.json()["embeddings"][0], dtype=np.float32)
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 hyde_search(query: str, top_k: int = 5):
hypothetic_doc = generate_hypothetical_document(query)
print(f"[HyDE] Documento generado: {hypothetic_doc[:100]}...")
hyde_emb = get_embedding(hypothetic_doc)
hyde_emb = hyde_emb / (norm(hyde_emb) + 1e-10)
conn = sqlite3.connect(DB_PATH)
cursor = conn.execute("SELECT id, contenido, embedding FROM chunks")
results = []
for row in cursor:
contenido, emb_blob = row[1], row[2]
emb_vec = blob_to_vector(emb_blob)
emb_vec = emb_vec / (norm(emb_vec) + 1e-10)
cos_sim = cosine_similarity(hyde_emb, emb_vec)
results.append((cos_sim, contenido[:200]))
results.sort(key=lambda x: x[0], reverse=True)
conn.close()
return results[:top_k]
def hyde_hybrid_search(query: str, alpha: float = 0.3, top_k: int = 5):
"""HyDE + Hybrid search combinado. Alpha bajo porque HyDE ya captura semántica."""
hyde_doc = generate_hypothetical_document(query)
hyde_emb = get_embedding(hyde_doc)
hyde_emb = hyde_emb / (norm(hyde_emb) + 1e-10)
conn = sqlite3.connect(DB_PATH)
cursor = conn.execute("""
SELECT c.contenido, c.embedding,
bm25(chunks_fts, 0, 0, 5, 5) as fts_score
FROM chunks_fts JOIN chunks c ON chunks_fts.rowid = c.id
WHERE chunks_fts MATCH ?
""", (query,))
results = []
for row in cursor:
contenido, emb_blob, fts_score = row
emb_vec = blob_to_vector(emb_blob)
emb_vec = emb_vec / (norm(emb_vec) + 1e-10)
cos_sim = cosine_similarity(hyde_emb, emb_vec)
fts_norm = 1.0 / (1.0 + math.exp(-fts_score / 10.0))
score_total = alpha * fts_norm + (1.0 - alpha) * cos_sim
results.append((score_total, contenido[:200]))
results.sort(reverse=True)
conn.close()
return results[:top_k]
| 1 | #!/usr/bin/env python3 |
| 2 | """rag_hyde.py — HyDE: Hypothetical Document Embeddings""" |
| 3 | import requests, numpy as np, sqlite3, os, math |
| 4 | from numpy.linalg import norm |
| 5 | |
| 6 | DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db") |
| 7 | OLLAMA_URL = "http://localhost:11434/api" |
| 8 | |
| 9 | def generate_hypothetical_document(query: str, model: str ="llama3.2") -> str: |
| 10 | prompt = f"""Genera un texto breve y factual que responda a la siguiente pregunta. |
| 11 | El texto debe ser informativo, objetivo y escrito en español. |
| 12 | |
| 13 | Pregunta: {query} |
| 14 | |
| 15 | Texto informativo:""" |
| 16 | resp = requests.post(f"{OLLAMA_URL}/generate", |
| 17 | json={"model": model, "prompt": prompt, "stream": False}) |
| 18 | return resp.json()["response"] |
| 19 | |
| 20 | def get_embedding(text: str, model: str ="bge-m3") -> np.ndarray: |
| 21 | resp = requests.post(f"{OLLAMA_URL}/embed", |
| 22 | json={"model": model, "input": text}) |
| 23 | return np.array(resp.json()["embeddings"][0], dtype=np.float32) |
| 24 | |
| 25 | def blob_to_vector(blob: bytes) -> np.ndarray: |
| 26 | return np.frombuffer(blob, dtype=np.float32).copy() |
| 27 | |
| 28 | def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: |
| 29 | return float(np.dot(a, b)) |
| 30 | |
| 31 | def hyde_search(query: str, top_k: int = 5): |
| 32 | hypothetic_doc = generate_hypothetical_document(query) |
| 33 | print(f"[HyDE] Documento generado: {hypothetic_doc[:100]}...") |
| 34 | hyde_emb = get_embedding(hypothetic_doc) |
| 35 | hyde_emb = hyde_emb / (norm(hyde_emb) + 1e-10) |
| 36 | |
| 37 | conn = sqlite3.connect(DB_PATH) |
| 38 | cursor = conn.execute("SELECT id, contenido, embedding FROM chunks") |
| 39 | results = [] |
| 40 | for row in cursor: |
| 41 | contenido, emb_blob = row[1], row[2] |
| 42 | emb_vec = blob_to_vector(emb_blob) |
| 43 | emb_vec = emb_vec / (norm(emb_vec) + 1e-10) |
| 44 | cos_sim = cosine_similarity(hyde_emb, emb_vec) |
| 45 | results.append((cos_sim, contenido[:200])) |
| 46 | |
| 47 | results.sort(key=lambda x: x[0], reverse=True) |
| 48 | conn.close() |
| 49 | return results[:top_k] |
| 50 | |
| 51 | def hyde_hybrid_search(query: str, alpha: float = 0.3, top_k: int = 5): |
| 52 | """HyDE + Hybrid search combinado. Alpha bajo porque HyDE ya captura semántica.""" |
| 53 | hyde_doc = generate_hypothetical_document(query) |
| 54 | hyde_emb = get_embedding(hyde_doc) |
| 55 | hyde_emb = hyde_emb / (norm(hyde_emb) + 1e-10) |
| 56 | |
| 57 | conn = sqlite3.connect(DB_PATH) |
| 58 | cursor = conn.execute(""" |
| 59 | SELECT c.contenido, c.embedding, |
| 60 | bm25(chunks_fts, 0, 0, 5, 5) as fts_score |
| 61 | FROM chunks_fts JOIN chunks c ON chunks_fts.rowid = c.id |
| 62 | WHERE chunks_fts MATCH ? |
| 63 | """, (query,)) |
| 64 | |
| 65 | results = [] |
| 66 | for row in cursor: |
| 67 | contenido, emb_blob, fts_score = row |
| 68 | emb_vec = blob_to_vector(emb_blob) |
| 69 | emb_vec = emb_vec / (norm(emb_vec) + 1e-10) |
| 70 | cos_sim = cosine_similarity(hyde_emb, emb_vec) |
| 71 | fts_norm = 1.0 / (1.0 + math.exp(-fts_score / 10.0)) |
| 72 | score_total = alpha * fts_norm + (1.0 - alpha) * cos_sim |
| 73 | results.append((score_total, contenido[:200])) |
| 74 | |
| 75 | results.sort(reverse=True) |
| 76 | conn.close() |
| 77 | return results[:top_k] |
rag_reranker.py
· 1.1 KiB · Python
Ham
#!/usr/bin/env python3
"""rag_reranker.py — Re-ranking con cross-encoder"""
from sentence_transformers import CrossEncoder
class Reranker:
def __init__(self, model_name="BAAI/bge-reranker-v2-m3", use_fp16=True):
self.model = CrossEncoder(model_name, max_length=512, device="cpu")
def rerank(self, query: str, candidates: list[dict], top_k: int = 5) -> list[dict]:
pairs = [(query, c["contenido"]) for c in candidates]
scores = self.model.predict(pairs)
for i, score in enumerate(scores):
candidates[i]["rerank_score"] = float(score)
candidates.sort(key=lambda x: x["rerank_score"], reverse=True)
return candidates[:top_k]
def search_with_rerank(query: str, alpha: float = 0.4,
top_k_hybrid: int = 20, top_k_final: int = 5):
from rag_hybrid import hybrid_search
candidates = hybrid_search(query, alpha=alpha, top_k=top_k_hybrid)
candidates_dict = [{"contenido": c[1], "score": c[0]} for c in candidates]
reranker = Reranker()
return reranker.rerank(query, candidates_dict, top_k=top_k_final)
| 1 | #!/usr/bin/env python3 |
| 2 | """rag_reranker.py — Re-ranking con cross-encoder""" |
| 3 | from sentence_transformers import CrossEncoder |
| 4 | |
| 5 | class Reranker: |
| 6 | def __init__(self, model_name="BAAI/bge-reranker-v2-m3", use_fp16=True): |
| 7 | self.model = CrossEncoder(model_name, max_length=512, device="cpu") |
| 8 | |
| 9 | def rerank(self, query: str, candidates: list[dict], top_k: int = 5) -> list[dict]: |
| 10 | pairs = [(query, c["contenido"]) for c in candidates] |
| 11 | scores = self.model.predict(pairs) |
| 12 | for i, score in enumerate(scores): |
| 13 | candidates[i]["rerank_score"] = float(score) |
| 14 | candidates.sort(key=lambda x: x["rerank_score"], reverse=True) |
| 15 | return candidates[:top_k] |
| 16 | |
| 17 | def search_with_rerank(query: str, alpha: float = 0.4, |
| 18 | top_k_hybrid: int = 20, top_k_final: int = 5): |
| 19 | from rag_hybrid import hybrid_search |
| 20 | candidates = hybrid_search(query, alpha=alpha, top_k=top_k_hybrid) |
| 21 | candidates_dict = [{"contenido": c[1], "score": c[0]} for c in candidates] |
| 22 | reranker = Reranker() |
| 23 | return reranker.rerank(query, candidates_dict, top_k=top_k_final) |