Download backend/scripts/ingest.py from Woodiee/MindRAG: direct link, hf CLI and curl.
- Browser
- Download file 4.6 kB
-
https://huggingface.co/spaces/Woodiee/MindRAG/resolve/main/backend/scripts/ingest.py
- Command line
-
hf download hf://spaces/Woodiee/MindRAG/backend/scripts/ingest.py
-
curl -L -o ingest.py https://huggingface.co/spaces/Woodiee/MindRAG/resolve/main/backend/scripts/ingest.py
4.6 kB
| """PDF ingestion pipeline: chunk, embed with E5, upsert to Pinecone.""" | |
| import argparse | |
| import os | |
| import uuid | |
| import nltk | |
| import torch | |
| from pinecone import Pinecone, ServerlessSpec | |
| from PyPDF2 import PdfReader | |
| from sentence_transformers import SentenceTransformer | |
| from tqdm import tqdm | |
| nltk.download("punkt", quiet=True) | |
| nltk.download("punkt_tab", quiet=True) | |
| def load_settings(): | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| root = Path(__file__).resolve().parents[2] | |
| load_dotenv(root / ".env") | |
| return { | |
| "api_key": os.getenv("PINECONE_API_KEY", ""), | |
| "index_name": os.getenv("PINECONE_INDEX_NAME", "e5"), | |
| "embedding_model": os.getenv("EMBEDDING_MODEL", "intfloat/multilingual-e5-large-instruct"), | |
| } | |
| def extract_text_from_pdf(pdf_path: str) -> str: | |
| text = "" | |
| with open(pdf_path, "rb") as file: | |
| reader = PdfReader(file) | |
| for page in reader.pages: | |
| page_text = page.extract_text() or "" | |
| text += page_text + "\n" | |
| return text.strip() | |
| def chunk_fixed_length(text: str, chunk_size: int = 1000, chunk_overlap: int = 100) -> list[str]: | |
| if len(text) <= chunk_size: | |
| return [text] | |
| chunks: list[str] = [] | |
| start = 0 | |
| while start < len(text): | |
| end = start + chunk_size | |
| if end >= len(text): | |
| chunks.append(text[start:]) | |
| break | |
| while end > start + chunk_size - 100 and end < len(text) and text[end] != " ": | |
| end -= 1 | |
| if end == start + chunk_size - 100: | |
| end = start + chunk_size | |
| chunks.append(text[start:end]) | |
| start = end - chunk_overlap | |
| return chunks | |
| def setup_pinecone_index(api_key: str, index_name: str, dimension: int): | |
| pc = Pinecone(api_key=api_key) | |
| if not pc.has_index(name=index_name): | |
| pc.create_index( | |
| name=index_name, | |
| dimension=dimension, | |
| metric="dotproduct", | |
| spec=ServerlessSpec(cloud="aws", region="us-east-1"), | |
| ) | |
| print(f"Created Pinecone index: {index_name}") | |
| else: | |
| print(f"Using existing Pinecone index: {index_name}") | |
| return pc.Index(name=index_name) | |
| def ingest_pdfs(pdf_dir: str, index, model: SentenceTransformer, chunk_size: int = 1000) -> None: | |
| pdf_files = [f for f in os.listdir(pdf_dir) if f.lower().endswith(".pdf")] | |
| if not pdf_files: | |
| print(f"No PDF files found in {pdf_dir}") | |
| return | |
| batch: list[dict] = [] | |
| batch_size = 100 | |
| for pdf_file in pdf_files: | |
| pdf_path = os.path.join(pdf_dir, pdf_file) | |
| print(f"Processing {pdf_file}...") | |
| text = extract_text_from_pdf(pdf_path) | |
| if not text: | |
| print(f"Skipping {pdf_file} - no text extracted") | |
| continue | |
| chunks = chunk_fixed_length(text, chunk_size=chunk_size) | |
| print(f"Split {pdf_file} into {len(chunks)} chunks") | |
| for i, chunk in enumerate(tqdm(chunks, desc=f"Embedding {pdf_file}")): | |
| embedding = model.encode(f"passage: {chunk}").tolist() | |
| batch.append( | |
| { | |
| "id": f"{pdf_file.replace('.pdf', '')}_{i}_{uuid.uuid4()}", | |
| "values": embedding, | |
| "metadata": { | |
| "filename": pdf_file, | |
| "chunk_index": i, | |
| "text": chunk[:1000], | |
| "full_text": chunk, | |
| }, | |
| } | |
| ) | |
| if len(batch) >= batch_size: | |
| index.upsert(vectors=batch) | |
| batch = [] | |
| if batch: | |
| index.upsert(vectors=batch) | |
| stats = index.describe_index_stats() | |
| print(f"Total vectors in index: {stats.get('total_vector_count', 0)}") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Ingest PDFs into Pinecone for RAG") | |
| parser.add_argument("--pdf_dir", type=str, required=True, help="Directory with PDF files") | |
| parser.add_argument("--chunk_size", type=int, default=1000) | |
| args = parser.parse_args() | |
| cfg = load_settings() | |
| if not cfg["api_key"]: | |
| raise SystemExit("PINECONE_API_KEY is not set") | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| print(f"Using device: {device}") | |
| model = SentenceTransformer(cfg["embedding_model"], device=device) | |
| dimension = model.get_sentence_embedding_dimension() | |
| index = setup_pinecone_index(cfg["api_key"], cfg["index_name"], dimension) | |
| ingest_pdfs(args.pdf_dir, index, model, chunk_size=args.chunk_size) | |
| if __name__ == "__main__": | |
| main() | |