Spaces:
Sleeping
Sleeping
| """One-time schema creation + data ingestion for Pipeline 3 (TigerGraph GraphRAG). | |
| Run once before starting the server: | |
| python core/pipeline_3/setup.py | |
| Requires TigerGraph community container running on localhost:14240: | |
| docker run -d --init -p 14240:14240 --name tigergraph tigergraph/community:4.2.2 | |
| Safe to re-run after interruption β resumes from last completed checkpoint. | |
| """ | |
| import json | |
| import logging | |
| import os | |
| import requests | |
| from pathlib import Path | |
| from typing import Optional | |
| import numpy as np | |
| import pyTigerGraph as tg | |
| from dotenv import load_dotenv | |
| from sentence_transformers import SentenceTransformer | |
| load_dotenv() | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| logger = logging.getLogger(__name__) | |
| # ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TG_HOST = os.environ.get("TG_HOST", "http://localhost") | |
| TG_PORT = int(os.environ.get("TG_PORT", "14240")) | |
| TG_USER = os.environ.get("TG_USER", "tigergraph") | |
| TG_PASS = os.environ.get("TG_PASS", "tigergraph") | |
| TG_SECRET = os.environ.get("TG_SECRET", "") | |
| GRAPH = "PaperGraph" | |
| EMBED_MODEL = "BAAI/bge-large-en-v1.5" | |
| EMBED_DIM = 1024 | |
| EMBED_BATCH = 64 | |
| UPSERT_BATCH = 256 | |
| _PROJECT_ROOT = Path(__file__).parents[2] | |
| _DATA_DIR = _PROJECT_ROOT / "data" | |
| _PIPELINE_DIR = Path(__file__).parent | |
| _EMBED_CACHE = _PIPELINE_DIR / "embeddings.npy" | |
| _EMBED_PARTIAL = _PIPELINE_DIR / "embed_partials" | |
| _CHECKPOINT = _PIPELINE_DIR / "checkpoint.json" | |
| # ββ REST client (bypasses pyTigerGraph auth issues on TG Cloud) ββββββββββββββββ | |
| class _TGClient: | |
| def __init__(self, token: str): | |
| self._url = f"{TG_HOST}:{TG_PORT}/restpp/graph/{GRAPH}" | |
| self._session = requests.Session() | |
| self._session.headers["Authorization"] = f"Bearer {token}" | |
| def upsertVertices(self, vtype: str, vertices: list) -> None: | |
| body = {"vertices": {vtype: { | |
| v_id: {k: {"value": v} for k, v in attrs.items()} | |
| for v_id, attrs in vertices | |
| }}} | |
| resp = self._session.post(self._url, json=body, timeout=120) | |
| resp.raise_for_status() | |
| def upsertEdges(self, src_type: str, edge_type: str, _tgt_type: str, edges: list) -> None: | |
| src_map: dict = {} | |
| for src_id, tgt_id, attrs in edges: | |
| src_map.setdefault(src_id, {}).setdefault(edge_type, {})[tgt_id] = { | |
| k: {"value": v} for k, v in attrs.items() | |
| } | |
| body = {"edges": {src_type: src_map}} | |
| resp = self._session.post(self._url, json=body, timeout=120) | |
| resp.raise_for_status() | |
| def _get_conn(): | |
| if TG_SECRET: | |
| tg_conn = tg.TigerGraphConnection( | |
| host=TG_HOST, graphname=GRAPH, gsqlSecret=TG_SECRET, | |
| restppPort=str(TG_PORT), gsPort=str(TG_PORT), | |
| ) | |
| token = tg_conn.getToken(TG_SECRET)[0] | |
| return _TGClient(token) | |
| return tg.TigerGraphConnection( | |
| host=TG_HOST, graphname=GRAPH, username=TG_USER, password=TG_PASS, | |
| restppPort=str(TG_PORT), gsPort=str(TG_PORT), | |
| ) | |
| # ββ Checkpoint helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _load_checkpoint() -> dict: | |
| if _CHECKPOINT.exists(): | |
| ckpt = json.loads(_CHECKPOINT.read_text()) | |
| ckpt.setdefault("embed_batches", 0) | |
| return ckpt | |
| return { | |
| "embed_batches": 0, | |
| "papers": 0, | |
| "authors": False, | |
| "topics": False, | |
| "authored_by": False, | |
| "has_topic": False, | |
| "cites": False, | |
| } | |
| def _save_checkpoint(ckpt: dict) -> None: | |
| _CHECKPOINT.write_text(json.dumps(ckpt, indent=2)) | |
| # ββ GSQL helper (raw REST β confirmed working with TG 4.2.2) ββββββββββββββββββ | |
| def _gsql(stmt: str) -> str: | |
| resp = requests.post( | |
| f"{TG_HOST}:{TG_PORT}/gsql/v1/statements", | |
| data=stmt.encode("utf-8"), | |
| headers={"Content-Type": "text/plain"}, | |
| auth=(TG_USER, TG_PASS), | |
| timeout=120, | |
| ) | |
| return resp.text | |
| def _graph_ready() -> bool: | |
| try: | |
| resp = requests.get( | |
| f"{TG_HOST}:{TG_PORT}/restpp/graph/{GRAPH}/vertices/Paper", | |
| params={"limit": 1}, | |
| auth=(TG_USER, TG_PASS), | |
| timeout=10, | |
| ) | |
| return resp.status_code == 200 | |
| except Exception: | |
| return False | |
| # ββ Schema βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _SCHEMA = f""" | |
| CREATE GRAPH {GRAPH}() | |
| USE GRAPH {GRAPH} | |
| CREATE SCHEMA_CHANGE JOB init_schema FOR GRAPH {GRAPH} {{ | |
| ADD VERTEX Paper( | |
| PRIMARY_ID paper_id STRING, | |
| title STRING DEFAULT "", | |
| abstract STRING DEFAULT "", | |
| year INT DEFAULT 0, | |
| cited_by_count INT DEFAULT 0, | |
| doi STRING DEFAULT "", | |
| pdf_url STRING DEFAULT "", | |
| venue STRING DEFAULT "", | |
| corresponding_author STRING DEFAULT "", | |
| authors STRING DEFAULT "", | |
| funders STRING DEFAULT "" | |
| ) WITH primary_id_as_attribute="true"; | |
| ADD VERTEX Author( | |
| PRIMARY_ID author_id STRING, | |
| display_name STRING DEFAULT "" | |
| ) WITH primary_id_as_attribute="true"; | |
| ADD VERTEX Topic( | |
| PRIMARY_ID topic_id STRING, | |
| display_name STRING DEFAULT "", | |
| subfield STRING DEFAULT "", | |
| field STRING DEFAULT "", | |
| domain STRING DEFAULT "" | |
| ) WITH primary_id_as_attribute="true"; | |
| ADD DIRECTED EDGE AUTHORED_BY(FROM Paper, TO Author); | |
| ADD DIRECTED EDGE HAS_TOPIC(FROM Paper, TO Topic, score FLOAT DEFAULT 0.0); | |
| ADD DIRECTED EDGE CITES(FROM Paper, TO Paper); | |
| }} | |
| RUN SCHEMA_CHANGE JOB init_schema | |
| DROP JOB init_schema | |
| CREATE SCHEMA_CHANGE JOB add_embedding FOR GRAPH {GRAPH} {{ | |
| ALTER VERTEX Paper ADD VECTOR ATTRIBUTE embedding(dimension={EMBED_DIM}); | |
| }} | |
| RUN SCHEMA_CHANGE JOB add_embedding | |
| DROP JOB add_embedding | |
| """ | |
| def create_schema() -> None: | |
| if _graph_ready(): | |
| logger.info("Graph already exists β skipping schema creation.") | |
| return | |
| logger.info("Creating graph schemaβ¦") | |
| out = _gsql(_SCHEMA) | |
| logger.info(f"Schema output:\n{out[:800]}") | |
| if "error" in out.lower() or "fail" in out.lower(): | |
| raise RuntimeError(f"Schema creation failed:\n{out}") | |
| # ββ Embedding with per-batch checkpointing βββββββββββββββββββββββββββββββββββββ | |
| def _embed_papers(papers: list[dict], ckpt: dict) -> np.ndarray: | |
| if _EMBED_CACHE.exists(): | |
| logger.info(f"Loading full embedding cache from {_EMBED_CACHE}β¦") | |
| embs = np.load(_EMBED_CACHE) | |
| logger.info(f"Loaded embeddings shape: {embs.shape}") | |
| return embs | |
| _EMBED_PARTIAL.mkdir(exist_ok=True) | |
| texts = [f"Title: {p['title']}\n\nAbstract: {p['abstract']}" for p in papers] | |
| n_batches = (len(texts) + EMBED_BATCH - 1) // EMBED_BATCH | |
| completed = ckpt.get("embed_batches", 0) | |
| logger.info(f"Loading embedding model {EMBED_MODEL}β¦") | |
| model = SentenceTransformer(EMBED_MODEL) | |
| logger.info(f"Embedding: {completed}/{n_batches} batches already done, resumingβ¦") | |
| for i in range(completed, n_batches): | |
| start = i * EMBED_BATCH | |
| batch_embs = model.encode( | |
| texts[start : start + EMBED_BATCH], | |
| normalize_embeddings=True, | |
| ) | |
| np.save(_EMBED_PARTIAL / f"batch_{i:04d}.npy", batch_embs) | |
| ckpt["embed_batches"] = i + 1 | |
| _save_checkpoint(ckpt) | |
| if (i + 1) % 10 == 0 or i + 1 == n_batches: | |
| logger.info(f" Embedded {i + 1}/{n_batches} batches β") | |
| # Merge partials into single cache file | |
| all_embs = [np.load(_EMBED_PARTIAL / f"batch_{i:04d}.npy") for i in range(n_batches)] | |
| embeddings = np.vstack(all_embs) | |
| np.save(_EMBED_CACHE, embeddings) | |
| logger.info(f"Full embeddings saved β {_EMBED_CACHE}") | |
| for f in sorted(_EMBED_PARTIAL.glob("*.npy")): | |
| f.unlink() | |
| _EMBED_PARTIAL.rmdir() | |
| return embeddings | |
| # ββ Data parsing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _reconstruct_abstract(inverted: dict) -> str: | |
| if not inverted: | |
| return "" | |
| max_idx = max((max(v) for v in inverted.values() if v), default=0) | |
| words = [""] * (max_idx + 1) | |
| for word, positions in inverted.items(): | |
| for i in positions: | |
| words[i] = word | |
| return " ".join(words).strip() | |
| def _parse_paper(path: Path) -> Optional[dict]: | |
| try: | |
| d = json.loads(path.read_text()) | |
| paper_id = path.stem | |
| abstract = _reconstruct_abstract(d.get("abstract_inverted_index") or {}) | |
| loc = d.get("primary_location") or {} | |
| source = loc.get("source") or {} | |
| venue = source.get("display_name", "") or "" | |
| authorships = d.get("authorships") or [] | |
| author_names = [ | |
| a["author"]["display_name"] | |
| for a in authorships[:5] | |
| if (a.get("author") or {}).get("display_name") | |
| ] | |
| corresponding_author = next( | |
| ( | |
| a["author"].get("display_name", "") | |
| for a in authorships | |
| if a.get("is_corresponding") and a.get("author") | |
| ), | |
| author_names[0] if author_names else "", | |
| ) | |
| funder_names = list({ | |
| a["funder_display_name"] | |
| for a in (d.get("awards") or []) | |
| if a.get("funder_display_name") | |
| }) | |
| best_oa = d.get("best_oa_location") or {} | |
| topics = [ | |
| { | |
| "topic_id": t["id"].split("/")[-1], | |
| "display_name": t.get("display_name", ""), | |
| "subfield": (t.get("subfield") or {}).get("display_name", ""), | |
| "field": (t.get("field") or {}).get("display_name", ""), | |
| "domain": (t.get("domain") or {}).get("display_name", ""), | |
| "score": float(t.get("score", 0.0)), | |
| } | |
| for t in (d.get("topics") or [])[:3] | |
| if t.get("id") | |
| ] | |
| author_vertices = [ | |
| { | |
| "author_id": a["author"]["id"].split("/")[-1], | |
| "display_name": a["author"].get("display_name", ""), | |
| } | |
| for a in authorships | |
| if (a.get("author") or {}).get("id") | |
| ] | |
| referenced = [r.split("/")[-1] for r in (d.get("referenced_works") or [])] | |
| return { | |
| "paper_id": paper_id, | |
| "title": d.get("title", "") or "", | |
| "abstract": abstract, | |
| "year": d.get("publication_year") or 0, | |
| "cited_by_count": d.get("cited_by_count", 0) or 0, | |
| "doi": d.get("doi", "") or "", | |
| "pdf_url": best_oa.get("pdf_url", "") or "", | |
| "venue": venue, | |
| "corresponding_author": corresponding_author, | |
| "authors": ", ".join(author_names), | |
| "funders": ", ".join(funder_names), | |
| "topics": topics, | |
| "author_vertices": author_vertices, | |
| "referenced": referenced, | |
| } | |
| except Exception as e: | |
| logger.warning(f"Skipping {path.name}: {e}") | |
| return None | |
| # ββ Ingestion ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def ingest(data_dir: Path = _DATA_DIR) -> None: | |
| ckpt = _load_checkpoint() | |
| logger.info(f"Resuming from checkpoint: {ckpt}") | |
| files = sorted(data_dir.glob("*.json")) | |
| corpus_ids = {f.stem for f in files} | |
| logger.info(f"Found {len(files)} paper files.") | |
| papers = [p for f in files if (p := _parse_paper(f))] | |
| logger.info(f"Parsed {len(papers)} papers.") | |
| embeddings = _embed_papers(papers, ckpt) | |
| conn = _get_conn() | |
| total_batches = (len(papers) + UPSERT_BATCH - 1) // UPSERT_BATCH | |
| # Paper vertices βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| completed = ckpt["papers"] | |
| if completed < total_batches: | |
| logger.info(f"Upserting Paper vertices β resuming from batch {completed}/{total_batches}β¦") | |
| for batch_idx in range(completed, total_batches): | |
| start = batch_idx * UPSERT_BATCH | |
| chunk = papers[start : start + UPSERT_BATCH] | |
| embs = embeddings[start : start + UPSERT_BATCH] | |
| conn.upsertVertices("Paper", [ | |
| (p["paper_id"], { | |
| "title": p["title"], | |
| "abstract": p["abstract"], | |
| "year": p["year"], | |
| "cited_by_count": p["cited_by_count"], | |
| "doi": p["doi"], | |
| "pdf_url": p["pdf_url"], | |
| "venue": p["venue"], | |
| "corresponding_author": p["corresponding_author"], | |
| "authors": p["authors"], | |
| "funders": p["funders"], | |
| "embedding": emb.tolist(), | |
| }) | |
| for p, emb in zip(chunk, embs) | |
| ]) | |
| ckpt["papers"] = batch_idx + 1 | |
| _save_checkpoint(ckpt) | |
| logger.info(f" Papers batch {batch_idx + 1}/{total_batches} β") | |
| else: | |
| logger.info("Paper vertices already complete β skipping.") | |
| # Collect Author + Topic data ββββββββββββββββββββββββββββββββββββββββββββββ | |
| all_authors: dict[str, dict] = {} | |
| all_topics: dict[str, dict] = {} | |
| for p in papers: | |
| for av in p["author_vertices"]: | |
| all_authors.setdefault(av["author_id"], av) | |
| for t in p["topics"]: | |
| all_topics.setdefault(t["topic_id"], t) | |
| # Author vertices ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not ckpt["authors"]: | |
| author_list = list(all_authors.values()) | |
| logger.info(f"Upserting {len(author_list)} Author verticesβ¦") | |
| for start in range(0, len(author_list), UPSERT_BATCH): | |
| chunk = author_list[start : start + UPSERT_BATCH] | |
| conn.upsertVertices("Author", [ | |
| (a["author_id"], {"display_name": a["display_name"]}) | |
| for a in chunk | |
| ]) | |
| ckpt["authors"] = True | |
| _save_checkpoint(ckpt) | |
| logger.info("Author vertices β") | |
| else: | |
| logger.info("Author vertices already complete β skipping.") | |
| # Topic vertices βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not ckpt["topics"]: | |
| topic_list = list(all_topics.values()) | |
| logger.info(f"Upserting {len(topic_list)} Topic verticesβ¦") | |
| for start in range(0, len(topic_list), UPSERT_BATCH): | |
| chunk = topic_list[start : start + UPSERT_BATCH] | |
| conn.upsertVertices("Topic", [ | |
| (t["topic_id"], { | |
| "display_name": t["display_name"], | |
| "subfield": t["subfield"], | |
| "field": t["field"], | |
| "domain": t["domain"], | |
| }) | |
| for t in chunk | |
| ]) | |
| ckpt["topics"] = True | |
| _save_checkpoint(ckpt) | |
| logger.info("Topic vertices β") | |
| else: | |
| logger.info("Topic vertices already complete β skipping.") | |
| # AUTHORED_BY edges ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not ckpt["authored_by"]: | |
| authored_buf = [ | |
| (p["paper_id"], av["author_id"], {}) | |
| for p in papers | |
| for av in p["author_vertices"] | |
| if av["author_id"] | |
| ] | |
| logger.info(f"Upserting {len(authored_buf)} AUTHORED_BY edgesβ¦") | |
| for start in range(0, len(authored_buf), UPSERT_BATCH): | |
| conn.upsertEdges("Paper", "AUTHORED_BY", "Author", | |
| authored_buf[start : start + UPSERT_BATCH]) | |
| ckpt["authored_by"] = True | |
| _save_checkpoint(ckpt) | |
| logger.info("AUTHORED_BY edges β") | |
| else: | |
| logger.info("AUTHORED_BY edges already complete β skipping.") | |
| # HAS_TOPIC edges ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not ckpt["has_topic"]: | |
| topic_buf = [ | |
| (p["paper_id"], t["topic_id"], {"score": t["score"]}) | |
| for p in papers | |
| for t in p["topics"] | |
| if t["topic_id"] | |
| ] | |
| logger.info(f"Upserting {len(topic_buf)} HAS_TOPIC edgesβ¦") | |
| for start in range(0, len(topic_buf), UPSERT_BATCH): | |
| conn.upsertEdges("Paper", "HAS_TOPIC", "Topic", | |
| topic_buf[start : start + UPSERT_BATCH]) | |
| ckpt["has_topic"] = True | |
| _save_checkpoint(ckpt) | |
| logger.info("HAS_TOPIC edges β") | |
| else: | |
| logger.info("HAS_TOPIC edges already complete β skipping.") | |
| # CITES edges (in-corpus only) βββββββββββββββββββββββββββββββββββββββββββββ | |
| if not ckpt["cites"]: | |
| cites_buf = [ | |
| (p["paper_id"], ref_id, {}) | |
| for p in papers | |
| for ref_id in p["referenced"] | |
| if ref_id in corpus_ids | |
| ] | |
| logger.info(f"Upserting {len(cites_buf)} CITES edges (in-corpus)β¦") | |
| for start in range(0, len(cites_buf), UPSERT_BATCH): | |
| conn.upsertEdges("Paper", "CITES", "Paper", | |
| cites_buf[start : start + UPSERT_BATCH]) | |
| ckpt["cites"] = True | |
| _save_checkpoint(ckpt) | |
| logger.info("CITES edges β") | |
| else: | |
| logger.info("CITES edges already complete β skipping.") | |
| logger.info("Ingestion complete.") | |
| if __name__ == "__main__": | |
| create_schema() | |
| ingest() | |