Spaces:
Sleeping
Sleeping
| """FAISS indexing utilities (mirrors the Colab indexing notebook).""" | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from pathlib import Path | |
| import faiss | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| from app.paths import docs_root, index_path, metadata_path | |
| CHUNK_SIZE = 120 | |
| CHUNK_OVERLAP = 30 | |
| MIN_CHUNK_WORDS = 30 | |
| def clean_markdown(text: str) -> str: | |
| text = text.replace("\\#", "") | |
| text = text.replace("\\-", "") | |
| text = text.replace("\\", "") | |
| text = re.sub(r"[ \t]+", " ", text) | |
| return text.strip() | |
| def chunk_text_sliding_window( | |
| text: str, | |
| chunk_size: int = CHUNK_SIZE, | |
| overlap: int = CHUNK_OVERLAP, | |
| ) -> list[str]: | |
| words = text.split() | |
| chunks: list[str] = [] | |
| start = 0 | |
| while start < len(words): | |
| end = start + chunk_size | |
| chunk = words[start:end] | |
| chunks.append(" ".join(chunk)) | |
| start += chunk_size - overlap | |
| return chunks | |
| def build_chunks_from_document(source: str, text: str) -> list[dict]: | |
| clean_text = clean_markdown(text) | |
| title = Path(source).stem | |
| chunks: list[dict] = [] | |
| for idx, chunk in enumerate(chunk_text_sliding_window(clean_text)): | |
| if len(chunk.split()) > MIN_CHUNK_WORDS: | |
| chunks.append( | |
| { | |
| "id": f"{source}_{idx}", | |
| "text": chunk, | |
| "source": source, | |
| "title": title, | |
| } | |
| ) | |
| return chunks | |
| def existing_sources(metadata: list[dict]) -> set[str]: | |
| return {entry["source"] for entry in metadata} | |
| def list_indexed_documents(metadata: list[dict], total_vectors: int) -> dict: | |
| """Summarize unique indexed sources and chunk counts from metadata.""" | |
| by_source: dict[str, dict] = {} | |
| for entry in metadata: | |
| source = entry["source"] | |
| if source not in by_source: | |
| category, _, _ = source.partition("/") | |
| by_source[source] = { | |
| "source": source, | |
| "title": entry.get("title", Path(source).stem), | |
| "category": category, | |
| "chunks": 0, | |
| } | |
| by_source[source]["chunks"] += 1 | |
| documents = sorted(by_source.values(), key=lambda item: item["source"]) | |
| return { | |
| "total_documents": len(documents), | |
| "total_chunks": len(metadata), | |
| "total_vectors": total_vectors, | |
| "documents": documents, | |
| } | |
| def save_raw_document(category: str, filename: str, content: str) -> Path: | |
| path = docs_root() / category / filename | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(content, encoding="utf-8") | |
| return path | |
| def append_chunks_to_index( | |
| embedder: SentenceTransformer, | |
| index: faiss.Index, | |
| metadata: list[dict], | |
| new_chunks: list[dict], | |
| *, | |
| persist: bool = True, | |
| ) -> int: | |
| """Embed pre-built chunks and append them to the FAISS index and metadata.""" | |
| texts = [chunk["text"] for chunk in new_chunks] | |
| embeddings = embedder.encode( | |
| texts, | |
| convert_to_numpy=True, | |
| normalize_embeddings=True, | |
| ) | |
| index.add(np.asarray(embeddings, dtype=np.float32)) | |
| metadata.extend(new_chunks) | |
| if persist: | |
| faiss.write_index(index, str(index_path())) | |
| with open(metadata_path(), "w", encoding="utf-8") as f: | |
| json.dump(metadata, f, indent=2) | |
| return len(new_chunks) | |
| def append_document_to_index( | |
| embedder: SentenceTransformer, | |
| index: faiss.Index, | |
| metadata: list[dict], | |
| *, | |
| source: str, | |
| text: str, | |
| persist: bool = True, | |
| ) -> tuple[list[dict], int]: | |
| """ | |
| Chunk and embed a new document, append vectors to the FAISS index and metadata. | |
| Raises ValueError if the source is already indexed or no chunks are produced. | |
| """ | |
| if source in existing_sources(metadata): | |
| raise ValueError(f"Document already indexed: {source}") | |
| new_chunks = build_chunks_from_document(source, text) | |
| if not new_chunks: | |
| raise ValueError( | |
| "No indexable chunks produced (document empty or shorter than minimum chunk size)" | |
| ) | |
| vectors_added = append_chunks_to_index( | |
| embedder, index, metadata, new_chunks, persist=persist | |
| ) | |
| return new_chunks, vectors_added | |