File size: 3,995 Bytes
eb22b1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
"""
build_index.py β€” Extract text from SQL PDFs, chunk it, embed it, and save a FAISS index.
Run this once locally before deploying to Hugging Face.
"""

import json
import os
import fitz  # PyMuPDF
import numpy as np
from sentence_transformers import SentenceTransformer

# ── Configuration ──────────────────────────────────────────────────────────────
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
INDEX_DIR = os.path.join(os.path.dirname(__file__), "faiss_index")
CHUNK_SIZE = 500       # approximate tokens (β‰ˆwords for English)
CHUNK_OVERLAP = 50     # overlap between consecutive chunks
EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"


# ── PDF Extraction ─────────────────────────────────────────────────────────────
def extract_text_from_pdf(pdf_path: str) -> str:
    """Extract all text from a PDF using PyMuPDF."""
    doc = fitz.open(pdf_path)
    text = ""
    for page in doc:
        text += page.get_text()
    doc.close()
    return text


# ── Chunking ───────────────────────────────────────────────────────────────────
def chunk_text(text: str, source: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP):
    """Split text into overlapping word-level chunks with metadata."""
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = start + chunk_size
        chunk_words = words[start:end]
        chunk_text_str = " ".join(chunk_words)
        # Skip very short chunks (< 30 words)
        if len(chunk_words) >= 30:
            chunks.append({
                "text": chunk_text_str,
                "source": source,
                "chunk_id": len(chunks),
            })
        start += chunk_size - overlap
    return chunks


# ── Main ───────────────────────────────────────────────────────────────────────
def main():
    os.makedirs(INDEX_DIR, exist_ok=True)

    # 1. Extract & chunk all PDFs
    all_chunks = []
    pdf_files = [f for f in os.listdir(DATA_DIR) if f.lower().endswith(".pdf")]
    print(f"Found {len(pdf_files)} PDFs in {DATA_DIR}")

    for pdf_file in sorted(pdf_files):
        pdf_path = os.path.join(DATA_DIR, pdf_file)
        print(f"  Processing: {pdf_file} ...", end=" ", flush=True)
        text = extract_text_from_pdf(pdf_path)
        chunks = chunk_text(text, source=pdf_file)
        all_chunks.extend(chunks)
        print(f"{len(chunks)} chunks")

    print(f"\nTotal chunks: {len(all_chunks)}")

    # 2. Generate embeddings
    print(f"\nLoading embedding model: {EMBED_MODEL}")
    model = SentenceTransformer(EMBED_MODEL)

    texts = [c["text"] for c in all_chunks]
    print("Generating embeddings ...")
    embeddings = model.encode(texts, show_progress_bar=True, batch_size=64)
    embeddings = np.array(embeddings).astype("float32")
    print(f"Embeddings shape: {embeddings.shape}")

    # 3. Build FAISS index
    import faiss

    dimension = embeddings.shape[1]
    index = faiss.IndexFlatL2(dimension)
    index.add(embeddings)
    print(f"FAISS index size: {index.ntotal} vectors, dimension {dimension}")

    # 4. Save
    faiss.write_index(index, os.path.join(INDEX_DIR, "index.faiss"))
    with open(os.path.join(INDEX_DIR, "chunks.json"), "w", encoding="utf-8") as f:
        json.dump(all_chunks, f, ensure_ascii=False, indent=2)

    print(f"\n[OK] Index saved to {INDEX_DIR}/index.faiss")
    print(f"[OK] Chunks saved to {INDEX_DIR}/chunks.json")


if __name__ == "__main__":
    main()