MindRAG / backend /scripts /ingest.py
Woodiee's picture
Upload folder using huggingface_hub
087f8f8 verified
Raw History Blame Contribute Delete
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()