askmydocs / scripts /ingest_corpus.py
Prabu17's picture
Upload folder using huggingface_hub
8adc13f verified
Raw History Blame Contribute Delete
4.1 kB
"""Bulk-ingest the local document corpus into AskMyDocs.
Walks ``data/corpus`` and runs the :class:`IngestionPipeline` for every
supported document (markdown, HTML, PDF, DOCX), which parses -> cleans ->
chunks -> persists to PostgreSQL and indexes into Qdrant + keyword postings.
Usage:
python scripts/ingest_corpus.py [--root data/corpus] [--skip-existing]
"""
from __future__ import annotations
import argparse
import hashlib
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.db.models import Document # noqa: E402
from app.db.session import session_scope # noqa: E402
from app.ingestion.pipeline import IngestionPipeline # noqa: E402
from sqlalchemy import select # noqa: E402
SUPPORTED_EXTENSIONS = {".md", ".html", ".pdf", ".docx"}
def doc_id_from_path(file_path: Path) -> str:
"""Stable SHA-1 doc ID from the canonical posix path (matches pipeline)."""
return hashlib.sha1(file_path.resolve().as_posix().encode("utf-8")).hexdigest()[:32]
def _collect_files(root: Path) -> list[Path]:
"""Return supported corpus files, sorted for deterministic ordering."""
return sorted(
p
for p in root.rglob("*")
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
)
def register_document(file_path: Path, title: str) -> str:
"""Upsert a Document row (required by the ingestion_jobs FK). Returns doc_id."""
doc_id = doc_id_from_path(file_path)
with session_scope() as sess:
existing = sess.execute(
select(Document).where(Document.document_id == doc_id)
).scalar_one_or_none()
if existing is not None:
existing.title = title
return doc_id
fmt = file_path.suffix.lstrip(".").lower()
sess.add(
Document(
document_id=doc_id,
title=title,
module="corpus",
source=str(file_path.resolve()),
format=fmt,
file_size_bytes=file_path.stat().st_size,
)
)
sess.commit()
return doc_id
def main() -> int:
parser = argparse.ArgumentParser(description="Bulk-ingest the document corpus.")
parser.add_argument(
"--root",
default=str(ROOT / "data" / "corpus"),
help="Corpus root directory (default: data/corpus)",
)
parser.add_argument(
"--limit",
type=int,
default=0,
help=(
"Ingest at most N documents (0 = no limit). Files are sorted, so "
"the selection is deterministic. Useful on slow CPU where the full "
"corpus would take many minutes."
),
)
args = parser.parse_args()
root = Path(args.root)
files = _collect_files(root)
if not files:
print(f"No supported documents found under {root}")
return 1
if args.limit and args.limit > 0:
files = files[: args.limit]
print(f"Found {len(files)} documents to ingest under {root}")
pipeline = IngestionPipeline()
ok = 0
failed: list[str] = []
for i, path in enumerate(files, start=1):
try:
register_document(path, title=path.stem)
print(f"[{i}/{len(files)}] Ingesting {path.name} ...", flush=True)
result = pipeline.ingest(file_path=path, title=path.stem)
print(
f" -> ok doc_id={result.document_id} chunks={result.chunk_count}",
flush=True,
)
ok += 1
except Exception as exc: # noqa: BLE001
print(f" -> FAILED: {exc}", flush=True)
failed.append(f"{path}: {exc}")
print(f"\nDone. Ingested {ok}/{len(files)} documents.")
if failed:
print("Failures:")
for f in failed:
print(" -", f)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())