| """Build the compact local corpus from the downloaded HF parquet shards. |
| |
| The full kiyer/pathfinder_arxiv_data dataset is ~20 GB (float64 embeddings + |
| citation graphs). This machine has 16 GB RAM and limited disk, so we keep only |
| what retrieval needs: |
| - text columns -> backend/data/corpus/data-NNNNN.parquet (~1.3 GB total) |
| - embeddings -> a float16-quantized faiss index (~3.8 GB) |
| |
| Each source shard is DELETED from the HF cache after it is consumed (pass |
| --keep-sources to disable) so peak disk usage decreases as the build runs. |
| Shards are re-downloadable from the Hub if ever needed again. |
| |
| Run from backend/: uv run python scripts/build_corpus.py |
| """ |
|
|
| import argparse |
| import glob |
| import os |
| import sys |
| from pathlib import Path |
|
|
| import faiss |
| import numpy as np |
| import pyarrow.parquet as pq |
|
|
| TEXT_COLUMNS = ["title", "abstract", "authors", "date", "keywords", "bibcode", "arxiv_id"] |
| EMBED_DIM = 1536 |
|
|
| DATA_DIR = Path(__file__).resolve().parent.parent / "data" |
| HUB_GLOB = os.path.expanduser( |
| "~/.cache/huggingface/hub/datasets--kiyer--pathfinder_arxiv_data/snapshots/*/data/*.parquet" |
| ) |
|
|
|
|
| def main() -> int: |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--keep-sources", action="store_true", |
| help="do not delete consumed HF cache shards") |
| args = ap.parse_args() |
|
|
| shards = sorted(glob.glob(HUB_GLOB)) |
| if not shards: |
| print(f"no source shards found at {HUB_GLOB}", file=sys.stderr) |
| return 1 |
|
|
| corpus_dir = DATA_DIR / "corpus" |
| corpus_dir.mkdir(parents=True, exist_ok=True) |
| faiss_path = DATA_DIR / "astroparse_fp16.faiss" |
|
|
| index = faiss.IndexScalarQuantizer( |
| EMBED_DIM, faiss.ScalarQuantizer.QT_fp16, faiss.METRIC_L2 |
| ) |
|
|
| total_rows = 0 |
| for i, shard in enumerate(shards): |
| table = pq.read_table(shard, columns=TEXT_COLUMNS + ["embed"]) |
| flat = table.column("embed").combine_chunks().flatten().to_numpy(zero_copy_only=False) |
| embeds = np.ascontiguousarray(flat.reshape(-1, EMBED_DIM), dtype=np.float32) |
| del flat |
| if not index.is_trained: |
| index.train(embeds) |
| index.add(embeds) |
| del embeds |
|
|
| pq.write_table(table.select(TEXT_COLUMNS), corpus_dir / f"data-{i:05d}.parquet") |
| total_rows += table.num_rows |
| del table |
|
|
| |
| if not args.keep_sources: |
| real = os.path.realpath(shard) |
| os.remove(shard) |
| if real != shard and os.path.exists(real): |
| os.remove(real) |
| print(f"[{i + 1}/{len(shards)}] {Path(shard).name}: {total_rows} rows total, " |
| f"index size {index.ntotal}", flush=True) |
|
|
| faiss.write_index(index, str(faiss_path)) |
| print(f"DONE: {total_rows} rows, faiss index at {faiss_path} " |
| f"({faiss_path.stat().st_size / 1e9:.2f} GB)", flush=True) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|