Spaces:
Running
Running
File size: 6,077 Bytes
f7b053e 0a799f9 4397111 0a799f9 5688c6d f7b053e 00af3a0 f7b053e 5688c6d f7b053e 0a799f9 f7b053e 0a799f9 5688c6d 0a799f9 5688c6d 0a799f9 f7b053e 00af3a0 f7b053e 00af3a0 f7b053e 00af3a0 f7b053e 00af3a0 f7b053e 00af3a0 f7b053e 00af3a0 f7b053e 00af3a0 f7b053e 0a799f9 f7b053e 00af3a0 0a799f9 f7b053e 0a799f9 f7b053e 00af3a0 f7b053e 00af3a0 0a799f9 f7b053e 619c352 0a799f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | """
ChromaDB client + embedding singleton for ResearchRAG.
Collections per user:
{name}_{hash} β child chunks (small, embedded, searched)
{name}_{hash}_parent β parent chunks (large, LLM context, looked up by ID)
"""
import chromadb
from chromadb.config import Settings as ChromaSettings
from sentence_transformers import SentenceTransformer
from app.config import get_settings
from app.logger import logger
import hashlib
import threading
_client = None
_collections: dict[str, chromadb.Collection] = {}
_embedder = None
_embedder_lock = threading.Lock()
# βββ Embedder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_embedder() -> SentenceTransformer:
global _embedder
# Same race as the reranker: concurrent threadpool workers could each see
# None and each load a ~1.1 GB model. Double-checked locking bounds it to one.
if _embedder is None:
with _embedder_lock:
if _embedder is None:
cfg = get_settings()
logger.info("embedder_loading model=%s", cfg.embedding_model)
_embedder = SentenceTransformer(cfg.embedding_model)
return _embedder
# βββ Collection naming ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _normalize_user_id(user_id: str | None) -> str:
if not user_id:
return "default"
return user_id.strip().lower() or "default"
def _collection_name(user_id: str | None, role: str = "child") -> str:
cfg = get_settings()
normalized = _normalize_user_id(user_id)
if normalized == "default":
base = cfg.chroma_collection
else:
suffix = hashlib.md5(normalized.encode()).hexdigest()[:12]
base = f"{cfg.chroma_collection}_{suffix}"
if role == "parent":
return f"{base}_parent"
return base
# βββ Client init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _get_client() -> chromadb.PersistentClient:
global _client
if _client is None:
cfg = get_settings()
_client = chromadb.PersistentClient(
path = cfg.chroma_path,
settings = ChromaSettings(anonymized_telemetry=False),
)
return _client
def _get_or_create(name: str) -> chromadb.Collection:
client = _get_client()
col = client.get_or_create_collection(
name = name,
metadata = {"hnsw:space": "cosine"},
)
return col
# βββ Public collection accessors βββββββββββββββββββββββββββββββββββββββββββββ
def init_chroma(user_id: str | None = None):
global _collections
normalized = _normalize_user_id(user_id)
child_name = _collection_name(user_id, "child")
parent_name = _collection_name(user_id, "parent")
_collections[normalized] = _get_or_create(child_name)
_collections[f"{normalized}_parent"] = _get_or_create(parent_name)
get_embedder()
print(
f"[ChromaDB] Ready β {child_name} ({_collections[normalized].count()} child chunks) "
f"| {parent_name} ({_collections[f'{normalized}_parent'].count()} parent chunks)"
)
def get_collection(user_id: str | None = None) -> chromadb.Collection:
"""Returns the CHILD collection (for embedding + search)."""
normalized = _normalize_user_id(user_id)
if normalized not in _collections:
init_chroma(user_id)
return _collections[normalized]
def get_parent_collection(user_id: str | None = None) -> chromadb.Collection:
"""Returns the PARENT collection (for LLM context lookup)."""
normalized = _normalize_user_id(user_id)
key = f"{normalized}_parent"
if key not in _collections:
init_chroma(user_id)
return _collections[key]
def get_parents_by_ids(
parent_ids: list[str],
user_id: str | None = None,
) -> list[dict]:
"""
Fetch parent chunks by their IDs from the parent collection.
Returns list of {id, text, metadata} dicts.
"""
if not parent_ids:
return []
col = get_parent_collection(user_id)
unique = list(dict.fromkeys(parent_ids)) # deduplicate, preserve order
try:
results = col.get(ids=unique, include=["documents", "metadatas"])
except Exception:
return []
out = []
for pid, doc, meta in zip(
results.get("ids", []),
results.get("documents", []),
results.get("metadatas", []),
):
out.append({"id": pid, "text": doc, "metadata": meta})
return out
# βββ Utilities ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def embed_documents(texts: list[str]) -> list[list[float]]:
"""Embed passages for indexing. e5 models need the 'passage: ' prefix."""
prefix = get_settings().embedding_passage_prefix
return get_embedder().encode(
[f"{prefix}{t}" for t in texts],
normalize_embeddings=True,
show_progress_bar=False,
).tolist()
def embed_query(text: str) -> list[float]:
"""Embed a search query. e5 models need the 'query: ' prefix."""
prefix = get_settings().embedding_query_prefix
return get_embedder().encode(
f"{prefix}{text}",
normalize_embeddings=True,
).tolist()
# Backward-compat alias β all remaining callers embed documents.
embed_texts = embed_documents
def make_doc_id(source: str, chunk_index: int) -> str:
"""Stable unique ID for a chunk."""
raw = f"{source}::{chunk_index}"
return hashlib.md5(raw.encode()).hexdigest()
|