Spaces:
Sleeping
Sleeping
File size: 24,064 Bytes
77348b4 | 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 | """
Vector Database Management for Legal RAG Pipeline
================================================
This module provides comprehensive vector database functionality for the Legal
RAG Pipeline, including document loading, text extraction, OCR processing,
and FAISS-based vector storage with GPU acceleration support.
Features:
- Multi-format document loading (PDF, web pages, text files)
- OCR fallback for scanned documents and images
- FAISS vector store with GPU acceleration
- Chunking and embedding of documents
- Web document scraping and processing
- Local file caching and management
"""
from langchain_community.document_loaders import WebBaseLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import faiss
try:
from faiss import StandardGpuResources, index_cpu_to_gpu
except ImportError:
StandardGpuResources = None
index_cpu_to_gpu = None
from langchain_community.vectorstores import FAISS
from langchain_community.docstore.in_memory import InMemoryDocstore
from langchain.text_splitter import RecursiveCharacterTextSplitter
from src.generation.model_provider import get_embedding_model
from langchain_core.documents import Document
import os
import pytesseract
from pdf2image import convert_from_path
import webbrowser
from PIL import Image
import pytesseract
import requests
import time
from src.generation.model_provider import get_embedding_model
import numpy as np
from uuid import uuid4
from pathlib import Path
from tqdm import tqdm
import logging
# Set up logger
logger = logging.getLogger(__name__)
# Ensure local directory exists for storing downloaded PDFs
local_file_dir = "app_data/local_pdfs"
os.makedirs(local_file_dir, exist_ok=True)
def extract_text_with_ocr(file_path):
"""
Extract text from PDF or image files using OCR technology.
This function uses Tesseract OCR to extract text from scanned documents
and images when standard text extraction methods fail.
Args:
file_path (str): Path to the PDF or image file
Returns:
str: Extracted text content
"""
ext = os.path.splitext(file_path)[1].lower()
text = ""
try:
if ext == ".pdf":
# Convert PDF pages to images and apply OCR
images = convert_from_path(file_path)
for img in images:
text += pytesseract.image_to_string(img)
elif ext in [".png", ".jpg", ".jpeg", ".bmp", ".tiff"]:
# Apply OCR directly to image files
img = Image.open(file_path)
text = pytesseract.image_to_string(img)
except Exception as e:
logger.info(f"OCR extraction failed for {file_path}: {e}")
return text
def load_source_with_fallback(source):
"""
Load document text with automatic fallback to OCR if needed.
This function attempts to load documents using standard methods
(PyPDFLoader for PDFs, web scraping for URLs) and falls back to
OCR processing if standard extraction fails or produces poor results.
Supports multiple document formats:
- PDF files (local and web URLs)
- Image files (PNG, JPG, JPEG, BMP, TIFF)
- Text files (UTF-8 encoded)
Args:
source (str): Path to the document file or URL
Returns:
Document: LangChain Document object containing extracted text and metadata
"""
doc = None
local_file_path = os.path.join(local_file_dir, source.split("/")[-1])
ext = os.path.splitext(source)[1].lower()
previous_cache_from_web = f"{local_file_path}.txt"
if os.path.exists(previous_cache_from_web):
source = previous_cache_from_web
ext = ".txt"
if ext == ".pdf":
# Handle web PDF downloads
if source.startswith("https"):
logger.info(f"\tLoading PDF from URL: {source}")
# Download the PDF if it doesn't exist locally
if not os.path.exists(local_file_path):
headers = {
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0",
}
response = requests.get(source, headers=headers, stream=True)
if response.status_code == 200:
os.makedirs(local_file_dir, exist_ok=True)
with open(local_file_path, "wb") as pdf_file:
for chunk in response.iter_content(chunk_size=1024):
pdf_file.write(chunk)
logger.info(f"PDF downloaded successfully to {local_file_path}")
ocr_text = extract_text_with_ocr(local_file_path)
if ocr_text.strip():
doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
elif response.status_code == 404:
source = source.replace(".pdf", ".html")
web_doc = WebBaseLoader(source, continue_on_failure=True).load()[0]
web_doc.page_content = web_doc.page_content.strip()
with open(previous_cache_from_web, "w", encoding="utf-8") as f:
f.write(web_doc.page_content)
return web_doc
elif response.status_code == 429:
time.sleep(10) # Wait for the browser to close
return load_source_with_fallback(source)
else:
logger.info(f"PDF already exists locally at {local_file_path}")
ocr_text = extract_text_with_ocr(local_file_path)
if ocr_text.strip():
doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
else:
logger.info(f"\tUsing PyPDFLoader for local PDF")
loader = PyPDFLoader(source)
doc = loader.load()[0]
# If no text extracted, fallback to OCR
if not doc or not doc.page_content.strip():
logger.info(f"\tNo text extracted from PDF, applying OCR")
ocr_text = extract_text_with_ocr(source)
if ocr_text.strip():
doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
elif ext in [".png", ".jpg", ".jpeg", ".bmp", ".tiff"]:
ocr_text = extract_text_with_ocr(source)
if ocr_text.strip():
doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
else:
# Fallback: treat as text file
logger.info(f"\tLoading text file: {source}")
with open(source, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
doc = Document(page_content=content, metadata={'source': source})
return doc
# Index documents from URLs and create a retriever for RAG
class VectorStoreManager:
"""
Vector Store Manager for Legal Document Processing.
This class manages FAISS vector stores with GPU acceleration support,
handling document indexing, embedding generation, and similarity search
for legal document retrieval.
Features:
- GPU-accelerated FAISS indexing with configurable memory allocation
- Multiple document format support (PDF, web, text)
- Automatic embedding dimension detection
- IVF (Inverted File) indexing for large-scale similarity search
- Persistent storage and loading of vector indexes
- Memory optimization for resource-constrained environments
Attributes:
file_path (str): Path to vector store file for persistence
use_gpu (bool): Whether to use GPU acceleration
nlist (int): Number of clusters for IVF index
gpu_memory_mb (int): GPU memory allocation in MB
gpu_res: FAISS GPU resources object
embeddings: Embedding model instance
dim (int): Embedding vector dimensionality
vector_store: FAISS vector store instance
"""
def __init__(self, file_path=None, nlist=100, gpu_memory_mb=128):
"""
Initialize VectorStoreManager with configurable GPU and memory settings.
Sets up embedding model, detects vector dimensions, and initializes
GPU resources.
Args:
file_path (str, optional): Path to existing vector store file
nlist (int): Number of clusters for IVF index
gpu_memory_mb (int): GPU memory allocation in MB
"""
self.file_path = file_path
self.nlist = nlist
self.gpu_memory_mb = gpu_memory_mb
self.gpu_res = None
self.batch_size = 128
# Configure embedding model with optimized settings
self.embeddings_config = {"wait_time": 60}
self.embeddings = get_embedding_model("mistral-embed", **self.embeddings_config)
# Detect embedding dimensionality
test_embedding = self.embeddings.embed_query("Test query to determine embedding dimensionality.")
self.dim = len(test_embedding)
logger.info(f"Embedding dimension detected: {self.dim}")
# Initialize GPU resources when available. Hugging Face Spaces and
# most local installs use faiss-cpu, which does not expose GPU helpers.
if StandardGpuResources is not None:
try:
self.gpu_res = StandardGpuResources()
self.gpu_res.setTempMemory(self.gpu_memory_mb * 1024 * 1024)
logger.info(f"GPU resources initialized for FAISS with {self.gpu_memory_mb} MB memory allocation.")
except Exception as e:
logger.info(f"FAISS GPU initialization failed; using CPU index: {e}")
self.gpu_res = None
else:
logger.info("FAISS GPU helpers are unavailable; using CPU index.")
if self.file_path: # Load existing vector store from disk
self.vector_store = FAISS.load_local(
self.file_path, self.embeddings, allow_dangerous_deserialization=True
)
self._move_to_gpu()
else: # Create a new vector store
# Start with 'Flat' and upgrade to IVF when dataset size exceeds threshold
index = self._create_optimized_index()
self.vector_store = FAISS(
embedding_function=self.embeddings,
index=index,
docstore=InMemoryDocstore(),
index_to_docstore_id={},
)
self.text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=1500,
chunk_overlap=150, # 10% overlap
separators=["\n\n", "\n", ".", "!", "?", "。", "!", "?", " "]
)
def _train_index(self, embedding_vectors: list[np.ndarray]):
"""
Train the FAISS index if it requires training (IVF indices).
Also handles switching from Flat to IVF index when enough vectors are available.
Args:
embedding_vectors: List of embedding vectors for training
"""
index = self.vector_store.index
num_vectors = len(embedding_vectors)
# Check if we have enough vectors to qualify for IVF
ivf_index_required = num_vectors + index.ntotal >= self.nlist * 2
# Upgrade
if ("Flat" in type(index).__name__ and
ivf_index_required): # Check if we have enough vectors to qualify for IVF
logger.info(f"Switching to IVF index with {num_vectors + index.ntotal} vectors (>= {self.nlist * 2})")
self._upgrade_to_ivf_index()
index = self.vector_store.index
# Check if we need to train the index
if hasattr(index, "is_trained") and not index.is_trained:
# Check if we have enough vectors to train IVF index
if ivf_index_required:
logger.info(f"Not enough vectors to train IVF index with {self.nlist} clusters. "
f"Need at least {self.nlist} vectors. Deferring training.")
return
vectors_np = np.array(embedding_vectors, dtype='float32')
logger.info(f"Training index with {len(vectors_np)} vectors...")
# Training happens on the current device (GPU or CPU)
index.train(vectors_np)
logger.info("Index training completed.")
def _move_to_gpu(self):
"""
Move the loaded vector store index to GPU.
Handles different index types that might be loaded from disk.
"""
if self.gpu_res:
current_index = self.vector_store.index
# Check if index is already on GPU
if "Gpu" in type(current_index).__name__:
logger.info("Index is already on GPU.")
return
try:
# Move index to GPU
gpu_index = index_cpu_to_gpu(self.gpu_res, 0, current_index)
self.vector_store.index = gpu_index
logger.info(f"Vector store index moved to GPU after load: {type(gpu_index).__name__}")
except Exception as e:
logger.info(f"Failed to move index to GPU (will continue with CPU): {e}")
def _upgrade_to_ivf_index(self):
"""
Upgrade from a Flat index to an IVF index when we have enough vectors.
"""
current_index = self.vector_store.index
# Extract all vectors from current index
if current_index.ntotal > 0:
logger.info(f"Extracting {current_index.ntotal} vectors from current index...")
vectors = np.zeros((current_index.ntotal, self.dim), dtype='float32')
for i in range(current_index.ntotal):
vectors[i] = current_index.reconstruct(i)
else:
vectors = np.empty((0, self.dim), dtype='float32')
# Create new IVF index
quantizer = faiss.IndexFlatL2(self.dim)
new_index = faiss.IndexIVFFlat(quantizer, self.dim, self.nlist)
# Move to GPU if needed
if self.gpu_res:
try:
new_index = index_cpu_to_gpu(self.gpu_res, 0, new_index)
logger.info("New IVF index moved to GPU.")
except Exception as e:
logger.info(f"Failed to move new IVF index to GPU: {e}")
logger.info("Continuing with CPU-based IVF index.")
# Don't disable GPU entirely, just use CPU for this index
# Train the new index if we have enough vectors
if len(vectors) >= self.nlist:
new_index.train(vectors)
logger.info(f"New IVF index trained with {len(vectors)} vectors.")
# Add existing vectors to new index
if len(vectors) > 0:
new_index.add(vectors)
logger.info(f"Added {len(vectors)} existing vectors to new IVF index.")
# Replace the index in vector store
self.vector_store.index = new_index
logger.info("Successfully upgraded to IVF index.")
def _index_document(self, content, doc_metadata={'type': "unknown"}):
"""
Index a single document by splitting it into chunks and adding to vector store.
Args:
content: Document content to index
doc_metadata: Metadata for the document
Returns:
List of chunk UUIDs
"""
doc_chunks = self.text_splitter.split_text(content)
chunk_uuids = [str(uuid4()) for _ in range(len(doc_chunks))]
# Clean up chunks to remove any empty strings
doc_chunks = [chunk for chunk in doc_chunks if chunk.strip()]
# Generate embeddings for all chunks
vectors = [self.embeddings.embed_query(chunk) for chunk in doc_chunks]
try:
# Train the index if necessary (only for IVF indices)
self._train_index(vectors)
# Add texts with appropriate batch size for GPU performance
self.vector_store.add_texts(
texts=doc_chunks,
metadatas=[doc_metadata] * len(doc_chunks),
ids=chunk_uuids,
batch_size=self.batch_size
)
logger.info(f"Successfully indexed {len(doc_chunks)} chunks from {doc_metadata.get('type', 'unknown')} document.")
except Exception as e:
logger.info(f"Error adding document {doc_metadata.get('type', 'unknown')} to vector store: {e}")
return chunk_uuids
def save_vector_store(self, index_path=None):
if not index_path:
if not self.file_path:
timestamp = int(time.time())
index_path = os.path.join("data/vector_db", f"vector_store_{timestamp}")
os.makedirs(index_path, exist_ok=True)
else:
index_path = self.file_path
else:
os.makedirs(index_path, exist_ok=True)
# Move index to CPU before saving if on GPU
index_on_gpu = False
current_index = self.vector_store.index
# Check if index is on GPU by examining its type name
if "Gpu" in type(current_index).__name__:
try:
import faiss
cpu_index = faiss.index_gpu_to_cpu(current_index)
self.vector_store.index = cpu_index
index_on_gpu = True
logger.info("Moved index to CPU for saving.")
except Exception as e:
logger.info(f"Failed to move index to CPU for saving: {e}")
self.vector_store.save_local(index_path)
self.file_path = index_path
logger.info(f"Saved vector store to: {index_path}")
# Move back to GPU if needed
if index_on_gpu and self.gpu_res:
try:
from faiss import index_cpu_to_gpu
self.vector_store.index = index_cpu_to_gpu(self.gpu_res, 0, self.vector_store.index)
logger.info("Restored index to GPU after saving.")
except Exception as e:
logger.info(f"Failed to restore index to GPU after saving: {e}")
return index_path
def index_file_documents(self, paths, source_type):
meta_datas = []
for path in tqdm(paths, desc="Indexing file documents"):
doc = load_source_with_fallback(path)
if not doc:
logger.info(f"Failed to load document from {path}. Skipping.")
continue
doc.metadata["type"] = source_type
doc.metadata["chunk_ids"] = self._index_document(doc.page_content, doc.metadata)
meta_datas.append(doc.metadata)
return meta_datas
def index_web_documents(self, urls, source_type):
web_documents = WebBaseLoader(urls, continue_on_failure=True).load()
# Process each URL
meta_datas = []
for doc in web_documents:
# Doc metadata set by WebBaseLoader:
# "source": url
# "title": soup.find("title").get_text()
# "description": soup.find("meta", attrs={"name": "description"}).get("content", "No description found.")
# "language": soup.find("html").get("lang", "No language found.")
doc.metadata["source_type"] = source_type
doc.metadata["chunk_ids"] = self._index_document(doc.page_content, doc.metadata)
meta_datas.append(doc.metadata)
return meta_datas
def add_texts(self, texts, metadatas=None, ids=None):
"""
Add texts to the vector store with proper training if needed.
Optimized for GPU performance when available.
Args:
texts: List of text strings to add
metadatas: List of metadata dictionaries (optional)
ids: List of document IDs (optional)
Returns:
List of document IDs
"""
if not ids:
ids = [str(uuid4()) for _ in range(len(texts))]
# Generate embeddings for training if needed
logger.info(f"Generating embeddings for {len(texts)} texts...")
vectors = [self.embeddings.embed_query(text) for text in texts]
# Train the index if necessary
self._train_index(vectors)
# Add the texts to the vector store
result = self.vector_store.add_texts(
texts=texts, metadatas=metadatas, ids=ids, batch_size=self.batch_size
)
logger.info(f"Successfully added {len(texts)} texts to vector store.")
return result
def query_vector_store(self, query, k=5, meta_filter=None):
if not self.vector_store:
logger.info("No vector store loaded.")
return []
return self.vector_store.similarity_search_with_score(query, k=k, filter=meta_filter)
def _create_optimized_index(self):
"""
Create an optimized FAISS index based on expected dataset size and GPU availability.
Starts with a Flat index and upgrades to IVF when enough vectors are available.
Returns:
FAISS index optimized for the current configuration
"""
# Always start with a Flat index for small datasets
# We'll upgrade to IVF when we have enough vectors (>= nlist * 2)
index = faiss.IndexFlatL2(self.dim)
if self.gpu_res:
try:
# Move to GPU immediately for better performance
index = index_cpu_to_gpu(self.gpu_res, 0, index)
logger.info(f"Created Flat index on GPU (will upgrade to IVF when >= {self.nlist * 2} vectors)")
return index
except Exception as e:
logger.info(f"Failed to create GPU index: {e}")
logger.info("This might be due to GPU memory limitations. Falling back to CPU.")
self.gpu_res = None
# CPU index
logger.info(f"Using CPU-based IndexFlatL2 (will upgrade to IVF when >= {self.nlist * 2} vectors)")
return index
def get_index_info(self):
"""
Get information about the current FAISS index configuration.
Returns:
Dictionary with index information
"""
if not self.vector_store or not self.vector_store.index:
return {"status": "No index available"}
index = self.vector_store.index
info = {
"index_type": type(index).__name__,
"dimension": self.dim,
"total_vectors": index.ntotal,
"is_trained": getattr(index, "is_trained", True),
"on_gpu": "Gpu" in type(index).__name__
}
if hasattr(index, "nlist"):
info["nlist"] = index.nlist
return info
def print_index_status(self):
"""
Print current index status and configuration.
"""
info = self.get_index_info()
logger.info("=== FAISS Index Status ===")
for key, value in info.items():
logger.info(f"{key}: {value}")
logger.info("=" * 26)
def get_case_vector_store_dict(self):
"""
Get a dictionary representation of the vector store for case management.
"""
return self.vector_store.docstore._dict
def delete_documents(self, uuids_to_delete):
"""
Delete documents from the vector store by their UUIDs.
Args:
uuids_to_delete (list): List of UUIDs to delete from the vector store
"""
if not uuids_to_delete:
logger.info("No UUIDs provided for deletion.")
return
logger.info(f"Deleting {len(uuids_to_delete)} documents from vector store...")
self.vector_store.delete(uuids_to_delete)
logger.info("Deletion completed.")
|