Spaces:
Sleeping
Sleeping
| """Offline (v2): embed the MSigDB corpus by concept name and upsert to Pinecone. | |
| This is the one-time build for the optional semantic-search / annotation-matching module. It needs | |
| `PINECONE_API_KEY` and downloads the MSigDB metadata JSON once. Staged-subset default | |
| (Hallmark + Reactome + GO:BP, ~9.4K sets); widen `--collections` to scale toward the full corpus. | |
| PINECONE_API_KEY=... python scripts/build_pinecone_index.py # staged subset | |
| PINECONE_API_KEY=... python scripts/build_pinecone_index.py --limit 500 # quick smoke | |
| PINECONE_API_KEY=... python scripts/build_pinecone_index.py --collections H C2:CP:REACTOME C5:GO:BP C5:GO:MF | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import time | |
| import urllib.request | |
| from pathlib import Path | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| import config # noqa: E402 | |
| from mcp_server.corpus import DEFAULT_COLLECTIONS, concept_text # noqa: E402 | |
| MSIGDB_URL = "https://data.broadinstitute.org/gsea-msigdb/msigdb/release/2024.1.Hs/msigdb.v2024.1.Hs.json" | |
| CACHE_JSON = Path(os.environ.get("SEMBLANCE_MSIGDB_JSON", "/tmp/msigdb.json")) | |
| def load_corpus(collections: set[str], json_path: Path, limit: int | None) -> list[dict]: | |
| if not json_path.exists(): | |
| print(f"downloading MSigDB JSON → {json_path} …") | |
| urllib.request.urlretrieve(MSIGDB_URL, json_path) | |
| data = json.loads(json_path.read_text()) | |
| records = [] | |
| for name, meta in data.items(): | |
| coll = meta.get("collection") | |
| if coll not in collections: | |
| continue | |
| records.append({"name": name, "collection": coll, "concept": concept_text(name)}) | |
| if limit and len(records) >= limit: | |
| break | |
| return records | |
| def ensure_index(pc, name: str, dim: int): | |
| from pinecone import ServerlessSpec | |
| existing = {i.name for i in pc.list_indexes()} | |
| if name not in existing: | |
| print(f"creating serverless index '{name}' ({config.PINECONE_CLOUD}/{config.PINECONE_REGION}) …") | |
| pc.create_index(name=name, dimension=dim, metric="cosine", | |
| spec=ServerlessSpec(cloud=config.PINECONE_CLOUD, region=config.PINECONE_REGION)) | |
| for _ in range(60): | |
| if pc.describe_index(name).status.get("ready"): | |
| break | |
| time.sleep(1) | |
| return pc.Index(name) | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--collections", nargs="+", default=list(DEFAULT_COLLECTIONS)) | |
| ap.add_argument("--limit", type=int, default=None, help="cap total sets (for a quick smoke)") | |
| ap.add_argument("--embed-batch", type=int, default=256) | |
| ap.add_argument("--upsert-batch", type=int, default=200) | |
| ap.add_argument("--json-path", type=Path, default=CACHE_JSON) | |
| ap.add_argument("--index", default=config.PINECONE_INDEX) | |
| ap.add_argument("--recreate", action="store_true", help="delete and rebuild the index first") | |
| args = ap.parse_args() | |
| api_key = os.environ.get("PINECONE_API_KEY") | |
| if not api_key: | |
| sys.exit("PINECONE_API_KEY is not set.") | |
| records = load_corpus(set(args.collections), args.json_path, args.limit) | |
| print(f"{len(records)} sets from {args.collections}") | |
| if not records: | |
| sys.exit("no records matched the requested collections") | |
| from pinecone import Pinecone | |
| from sentence_transformers import SentenceTransformer | |
| pc = Pinecone(api_key=api_key) | |
| if args.recreate and args.index in {i.name for i in pc.list_indexes()}: | |
| print(f"deleting existing index '{args.index}' …") | |
| pc.delete_index(args.index) | |
| index = ensure_index(pc, args.index, config.EMBED_DIM) | |
| print(f"loading BioLORD ({config.EMBEDDER_DEFAULT}) …") | |
| model = SentenceTransformer(config.EMBEDDER_DEFAULT, device=config.EMBED_DEVICE) | |
| concepts = [r["concept"] for r in records] | |
| upserted = 0 | |
| for start in range(0, len(records), args.embed_batch): | |
| chunk = records[start : start + args.embed_batch] | |
| vecs = model.encode([r["concept"] for r in chunk], normalize_embeddings=True, | |
| batch_size=64, show_progress_bar=False) | |
| items = [ | |
| {"id": r["name"], "values": np.asarray(v, dtype=float).tolist(), | |
| "metadata": {"name": r["name"], "collection": r["collection"], "concept": r["concept"]}} | |
| for r, v in zip(chunk, vecs) | |
| ] | |
| for u in range(0, len(items), args.upsert_batch): | |
| index.upsert(vectors=items[u : u + args.upsert_batch]) | |
| upserted += len(items) | |
| print(f" upserted {upserted}/{len(records)}") | |
| stats = index.describe_index_stats() | |
| print(f"\ndone. index '{args.index}' vector count: {stats.get('total_vector_count')}") | |
| # quick self-check query | |
| q = model.encode(["interferon signaling"], normalize_embeddings=True)[0] | |
| res = index.query(vector=np.asarray(q, dtype=float).tolist(), top_k=5, include_metadata=True) | |
| print("sample query 'interferon signaling' →") | |
| for m in res.get("matches", []): | |
| md = m.get("metadata", {}) | |
| print(f" {m['score']:.3f} {md.get('name')} [{md.get('collection')}]") | |
| if __name__ == "__main__": | |
| main() | |