Meshyboi's picture
Update core/pipeline_3/setup.py
022af0d verified
Raw
History Blame Contribute Delete
19.1 kB
"""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()