Spaces:
Sleeping
Sleeping
Download app/ingest/materialize.py from Conradf/nullmap-api: direct link, hf CLI and curl.
- Browser
- Download file 5.7 kB
-
https://huggingface.co/spaces/Conradf/nullmap-api/resolve/main/app/ingest/materialize.py
- Command line
-
hf download hf://spaces/Conradf/nullmap-api/app/ingest/materialize.py
-
curl -L -o materialize.py https://huggingface.co/spaces/Conradf/nullmap-api/resolve/main/app/ingest/materialize.py
5.7 kB
| """Bounded normalization/embedding/linking directly into the shared ES index.""" | |
| import asyncio | |
| import json | |
| from pathlib import Path | |
| from elasticsearch.helpers import async_scan | |
| from app.classifier import WEAK_CLASSIFIER_VERSION, weak_classify | |
| from app.config import Settings, settings | |
| from app.ingest.ctgov import flatten_trial | |
| from app.ingest.fetch import atomic_json | |
| from app.ingest.linker import link_studies | |
| from app.ingest.openalex import NORMALIZER_VERSION, normalize_work | |
| from app.ingest.snapshot import fingerprint | |
| async def link_indexed_batch(repository, documents: list[dict]) -> None: | |
| """Link across batches and sources; distinct registered trials stay distinct.""" | |
| pmids = sorted({str(pmid) for doc in documents for pmid in doc.get("pmids", [])}) | |
| nct_ids = sorted({nct for doc in documents for nct in doc.get("nct_ids", [])}) | |
| result_pmids = sorted({str(pmid) for doc in documents for pmid in doc.get("result_pmids", [])}) | |
| clauses = [] | |
| for name, values in (("pmids", result_pmids), ("result_pmids", pmids), ("nct_ids", nct_ids)): | |
| if values: | |
| clauses.append({"terms": {name: values}}) | |
| if not clauses: | |
| return | |
| current = await repository.get_many([doc["id"] for doc in documents], include_vectors=False) | |
| query = { | |
| "query": { | |
| "bool": { | |
| "filter": [{"term": {"record_kind": "study"}}], | |
| "should": clauses, | |
| "minimum_should_match": 1, | |
| "must_not": [{"term": {"is_review": True}}], | |
| } | |
| }, | |
| "_source": {"excludes": ["embedding", "attachments"]}, | |
| } | |
| related = [ | |
| hit["_source"] | |
| async for hit in async_scan( | |
| repository.client, | |
| index=repository.index, | |
| query=query, | |
| size=500, | |
| ) | |
| ] | |
| linked = link_studies([*related, *current]) | |
| merged = [doc for doc in linked if doc.get("source") == "merged"] | |
| await repository.persist_links(merged) | |
| async def materialize( | |
| path: Path, | |
| repository, | |
| *, | |
| embedder=None, | |
| batch_size: int = 100, | |
| config: Settings = settings, | |
| progress=None, | |
| ) -> dict: | |
| """A committed byte offset avoids rereading a million rows on resume. | |
| No vector JSONL intermediate or all-corpus in-memory linker is created. | |
| A crash before checkpoint repeats idempotent writes, never loses a batch. | |
| """ | |
| if batch_size <= 0: | |
| raise ValueError("batch_size must be positive") | |
| source_checkpoint = path.with_suffix(path.suffix + ".checkpoint.json") | |
| source_identity = ( | |
| json.loads(source_checkpoint.read_text())["signature"] | |
| if source_checkpoint.exists() | |
| else [path.stat().st_size, path.stat().st_mtime_ns] | |
| ) | |
| signature = fingerprint( | |
| [ | |
| str(path.resolve()), | |
| source_identity, | |
| repository.index, | |
| config.elastic_url, | |
| NORMALIZER_VERSION, | |
| WEAK_CLASSIFIER_VERSION, | |
| config.embedding_model if embedder is not None else "no_vectors", | |
| ] | |
| ) | |
| checkpoint = path.with_suffix(path.suffix + ".materialize.json") | |
| state = { | |
| "signature": signature, | |
| "offset": 0, | |
| "records": 0, | |
| "metadata_only": 0, | |
| "embedded": 0, | |
| "complete": False, | |
| } | |
| if checkpoint.exists(): | |
| state = json.loads(checkpoint.read_text()) | |
| if state["signature"] != signature: | |
| raise ValueError( | |
| "Source, target index or model changed; use a new materialization checkpoint" | |
| ) | |
| if path.stat().st_size < state["offset"]: | |
| raise ValueError("Materialized source was truncated") | |
| if state["complete"] and path.stat().st_size == state["offset"]: | |
| return state | |
| state["complete"] = False | |
| with path.open("rb") as handle: | |
| handle.seek(state["offset"]) | |
| while True: | |
| batch = [] | |
| while len(batch) < batch_size: | |
| line = handle.readline() | |
| if not line: | |
| break | |
| if not line.strip(): | |
| continue | |
| raw = json.loads(line) | |
| doc = flatten_trial(raw) if "protocolSection" in raw else normalize_work(raw) | |
| if doc["source"] == "openalex": | |
| doc.update(weak_classify(doc["abstract"], is_review=doc["is_review"])) | |
| batch.append(doc) | |
| if not batch: | |
| break | |
| if embedder is not None: | |
| texts = [ | |
| " ".join( | |
| str(doc.get(key) or "") | |
| for key in ("title", "abstract", "intervention", "outcome") | |
| ) | |
| for doc in batch | |
| ] | |
| vectors = await asyncio.to_thread(embedder.encode, texts) | |
| for doc, vector in zip(batch, vectors, strict=True): | |
| doc.update(embedding=vector, embedding_model=config.embedding_model) | |
| await repository.bulk_upsert(batch) | |
| await link_indexed_batch(repository, batch) | |
| state.update( | |
| offset=handle.tell(), | |
| records=state["records"] + len(batch), | |
| metadata_only=state["metadata_only"] | |
| + sum(doc.get("abstract_available") is False for doc in batch), | |
| embedded=state["embedded"] + (len(batch) if embedder is not None else 0), | |
| ) | |
| atomic_json(checkpoint, state) | |
| if progress: | |
| progress(dict(state)) | |
| state["offset"] = handle.tell() | |
| state["complete"] = True | |
| atomic_json(checkpoint, state) | |
| return state | |