Satyam311's picture
Clean deploy: numpy vector search, binaries excluded
d4ec2fa
Raw
History Blame Contribute Delete
5.06 kB
"""
Lightweight in-memory vector search — replaces chromadb (Ticket 7's original choice).
Why the change: chromadb pulls in a huge dependency tree (kubernetes client,
opentelemetry, onnxruntime, grpcio, aiohttp...) meant for running a full client-server
vector database. At our actual scale (2,736 vectors x 384 dims = ~4MB), that's wildly
disproportionate -- it was the direct cause of Render's free-tier 512MB build running
out of memory. Brute-force cosine similarity via numpy does the identical job in
~2 milliseconds (measured), with zero extra dependencies.
Loads once at startup from the same embeddings.npy / report_ids.json Ticket 7 already
produced -- no data or embeddings were regenerated, only how they're queried changed.
"""
import json
import logging
import sqlite3
import numpy as np
from api.config import BASE_DIR, DATABASE_URL
logger = logging.getLogger(__name__)
EMBEDDINGS_PATH = BASE_DIR / "data" / "processed" / "embeddings.npy"
REPORT_IDS_PATH = BASE_DIR / "data" / "processed" / "report_ids.json"
class VectorStore:
def __init__(self):
self.embeddings = None # (N, 384) float32, L2-normalized
self.report_ids = None # list[str], same order as embeddings rows
self.metadata = {} # report_id -> {narrative_text, ata_chapter_label, severity_label, ...}
self.loaded = False
self.load_error = None
def load(self):
try:
self.embeddings = np.load(EMBEDDINGS_PATH).astype(np.float32)
with open(REPORT_IDS_PATH) as f:
self.report_ids = [str(x) for x in json.load(f)]
db_path = DATABASE_URL.replace("sqlite:///", "")
conn = sqlite3.connect(db_path)
rows = conn.execute("""
SELECT r.asrs_report_id, r.narrative_text, g.ata_chapter_label, g.severity_label
FROM reports r LEFT JOIN gold_labels g ON r.asrs_report_id = g.asrs_report_id
""").fetchall()
conn.close()
self.metadata = {
rid: {"narrative_text": text, "ata_chapter_label": ata, "severity_label": sev}
for rid, text, ata, sev in rows
}
self.loaded = True
logger.info(f"Vector store loaded: {len(self.report_ids)} vectors")
except Exception as e:
self.loaded = False
self.load_error = str(e)
logger.error(f"Vector store load failed: {e}")
def query(self, query_embedding, system=None, severity=None, limit=20):
if not self.loaded:
raise RuntimeError(self.load_error or "Vector store not loaded")
q = np.asarray(query_embedding, dtype=np.float32)
q = q / (np.linalg.norm(q) + 1e-9)
scores = self.embeddings @ q # cosine similarity, since both sides are L2-normalized
# Apply metadata filters BEFORE ranking, same semantics as the old Chroma `where` clause
candidate_idx = np.arange(len(self.report_ids))
if system or severity:
keep = []
for i in candidate_idx:
meta = self.metadata.get(self.report_ids[i], {})
if system and meta.get("ata_chapter_label") != system:
continue
if severity and meta.get("severity_label") != severity:
continue
keep.append(i)
candidate_idx = np.array(keep, dtype=int)
if len(candidate_idx) == 0:
return []
candidate_scores = scores[candidate_idx]
top_n = min(limit, len(candidate_idx))
top_order = np.argsort(-candidate_scores)[:top_n]
top_idx = candidate_idx[top_order]
results = []
for i in top_idx:
rid = self.report_ids[i]
meta = self.metadata.get(rid, {})
results.append({
"report_id": rid,
"excerpt": (meta.get("narrative_text") or "")[:300],
"ata_chapter": meta.get("ata_chapter_label"),
"severity": meta.get("severity_label"),
"score": round(float(scores[i]), 4),
})
return results
def filter_only(self, system=None, severity=None, limit=20):
"""No query vector at all -- just metadata filtering, no ranking."""
if not self.loaded:
raise RuntimeError(self.load_error or "Vector store not loaded")
results = []
for rid, meta in self.metadata.items():
if system and meta.get("ata_chapter_label") != system:
continue
if severity and meta.get("severity_label") != severity:
continue
results.append({
"report_id": rid,
"excerpt": (meta.get("narrative_text") or "")[:300],
"ata_chapter": meta.get("ata_chapter_label"),
"severity": meta.get("severity_label"),
"score": None,
})
if len(results) >= limit:
break
return results
vector_store = VectorStore()