Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """One-time build: transform a Common Crawl domain hyperlink graph into a | |
| partitioned Parquet dataset on Hugging Face, queryable remotely for backlinks. | |
| Streams the graph straight from CC's CDN (no 24 GB local copy of the source — | |
| DuckDB reads the gzipped CSVs over httpfs in a single pass). Output (~25-30 GB | |
| of Parquet) is written locally, then pushed to an HF dataset repo. | |
| Run anywhere with ~30 GB free disk + an HF token: | |
| HF_TOKEN=hf_xxx python3 build.py --repo isam/cc-domain-graph | |
| Produces on the repo: | |
| manifest.json | |
| vertices_by_domain.parquet (id, rev_domain) sorted by rev_domain | |
| vertices_by_id.parquet (id, rev_domain) sorted by id | |
| ranks_by_id.parquet (id, harmonic_pos) | |
| edges/part=<n>/data_0.parquet (from_id, to_id) partitioned by to_id // PART_SIZE | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| import duckdb | |
| RELEASE = "cc-main-2025-oct-nov-dec" | |
| BASE = f"https://data.commoncrawl.org/projects/hyperlinkgraph/{RELEASE}/domain" | |
| V_URL = f"{BASE}/{RELEASE}-domain-vertices.txt.gz" | |
| E_URL = f"{BASE}/{RELEASE}-domain-edges.txt.gz" | |
| R_URL = f"{BASE}/{RELEASE}-domain-ranks.txt.gz" | |
| # ~121M domain nodes → ~1M ids per partition → ~122 partition files. | |
| PART_SIZE = 1_000_000 | |
| def log(msg: str): | |
| print(msg, flush=True) | |
| def build(out_dir: Path, memory_limit: str = "12GB") -> dict: | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| (out_dir / "edges").mkdir(exist_ok=True) | |
| tmp = out_dir / "_duck_tmp" | |
| tmp.mkdir(exist_ok=True) | |
| con = duckdb.connect(config={"memory_limit": memory_limit, "temp_directory": str(tmp)}) | |
| con.execute("INSTALL httpfs; LOAD httpfs;") | |
| # ---- vertices: id, rev_domain (3rd col n_hosts ignored) ---- | |
| log("Loading vertices (851 MiB gz) ...") | |
| con.execute(f""" | |
| CREATE TABLE v AS | |
| SELECT c0::UINTEGER AS id, c1 AS rev_domain | |
| FROM read_csv('{V_URL}', delim='\t', header=false, auto_detect=false, | |
| columns={{'c0':'UINTEGER','c1':'VARCHAR','c2':'UBIGINT'}}) | |
| """) | |
| n_nodes = con.execute("SELECT count(*) FROM v").fetchone()[0] | |
| log(f" {n_nodes:,} domain nodes") | |
| con.execute(f"COPY (SELECT * FROM v ORDER BY rev_domain) TO '{out_dir}/vertices_by_domain.parquet' (FORMAT parquet)") | |
| con.execute(f"COPY (SELECT * FROM v ORDER BY id) TO '{out_dir}/vertices_by_id.parquet' (FORMAT parquet)") | |
| log(" wrote vertices_by_domain.parquet, vertices_by_id.parquet") | |
| # ---- ranks: harmonicc_pos keyed by host_rev → join to id ---- | |
| log("Loading ranks ...") | |
| con.execute(f""" | |
| CREATE TABLE r AS | |
| SELECT c0::UBIGINT AS harmonic_pos, c4 AS rev_domain | |
| FROM read_csv('{R_URL}', delim='\t', header=false, skip=1, auto_detect=false, | |
| columns={{'c0':'UBIGINT','c1':'VARCHAR','c2':'UBIGINT', | |
| 'c3':'VARCHAR','c4':'VARCHAR','c5':'UBIGINT'}}) | |
| """) | |
| con.execute(f""" | |
| COPY (SELECT v.id, r.harmonic_pos | |
| FROM r JOIN v USING (rev_domain) ORDER BY v.id) | |
| TO '{out_dir}/ranks_by_id.parquet' (FORMAT parquet) | |
| """) | |
| log(" wrote ranks_by_id.parquet") | |
| # ---- edges: partition by to_id // PART_SIZE (no global sort needed, | |
| # each partition already spans a narrow to_id range) ---- | |
| log("Streaming + partitioning edges (22.9 GiB gz, single pass) — this is the long step ...") | |
| con.execute(f""" | |
| COPY (SELECT c0::UINTEGER AS from_id, c1::UINTEGER AS to_id, | |
| (c1::UBIGINT // {PART_SIZE})::UINTEGER AS part | |
| FROM read_csv('{E_URL}', delim='\t', header=false, auto_detect=false, | |
| columns={{'c0':'UINTEGER','c1':'UINTEGER'}})) | |
| TO '{out_dir}/edges' (FORMAT parquet, PARTITION_BY (part), OVERWRITE_OR_IGNORE) | |
| """) | |
| n_parts = len(list((out_dir / "edges").glob("part=*"))) | |
| log(f" wrote {n_parts} edge partitions") | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| manifest = { | |
| "release": RELEASE, | |
| "source": BASE, | |
| "part_size": PART_SIZE, | |
| "domain_nodes": n_nodes, | |
| "schema": { | |
| "vertices": ["id", "rev_domain"], | |
| "edges": ["from_id", "to_id"], | |
| "ranks": ["id", "harmonic_pos"], | |
| }, | |
| "note": "rev_domain is label-reversed (blackstump.com.au -> au.com.blackstump). " | |
| "Backlinks: read edges/part=<to_id//part_size> WHERE to_id=<id>.", | |
| } | |
| (out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2)) | |
| log(" wrote manifest.json") | |
| return manifest | |
| def push(out_dir: Path, repo: str, token: str): | |
| from huggingface_hub import HfApi | |
| api = HfApi(token=token) | |
| api.create_repo(repo, repo_type="dataset", exist_ok=True) | |
| log(f"Uploading {out_dir} → {repo} (dataset) ...") | |
| # upload_large_folder handles many files + resumes; fall back to upload_folder. | |
| try: | |
| api.upload_large_folder(folder_path=str(out_dir), repo_id=repo, repo_type="dataset") | |
| except AttributeError: | |
| api.upload_folder(folder_path=str(out_dir), repo_id=repo, repo_type="dataset") | |
| log("Done. Query with: python3 backlinks.py <domain> --repo " + repo) | |
| def main(): | |
| ap = argparse.ArgumentParser(description="Build the hosted CC domain backlink graph.") | |
| ap.add_argument("--repo", required=True, help="Target HF dataset repo, e.g. isam/cc-domain-graph") | |
| ap.add_argument("--out", default="./graph_out", help="Local output dir (default: ./graph_out)") | |
| ap.add_argument("--memory-limit", default="12GB", help="DuckDB memory limit (default: 12GB)") | |
| ap.add_argument("--no-push", action="store_true", help="Build locally only, skip HF upload") | |
| args = ap.parse_args() | |
| out = Path(args.out) | |
| build(out, memory_limit=args.memory_limit) | |
| if args.no_push: | |
| log(f"Built in {out} (not pushed).") | |
| return | |
| token = os.environ.get("HF_TOKEN") | |
| if not token: | |
| sys.exit("Set HF_TOKEN env var to push (or use --no-push).") | |
| push(out, args.repo, token) | |
| if __name__ == "__main__": | |
| main() | |