| """ |
| CDMS RAG Search Module |
| Searches Qdrant vector database for CDMS label information with page citations |
| """ |
|
|
| import sys |
| from pathlib import Path |
| from typing import Dict, List, Optional |
|
|
| |
| project_root = Path(__file__).parent.parent.parent |
| sys.path.insert(0, str(project_root)) |
|
|
| from src.rag.vector_store import QdrantVectorStore, get_shared_vector_store |
| from src.rag.embeddings import OpenAIEmbeddingService |
| from src.config.credentials import CredentialsManager |
| from qdrant_client.models import Filter, FieldCondition, MatchValue |
|
|
|
|
| class CDMSRAGSearch: |
| """ |
| RAG search for CDMS pesticide labels |
| |
| Searches Qdrant vector database for relevant label information |
| with accurate page number tracking |
| |
| Usage: |
| searcher = CDMSRAGSearch() |
| results = searcher.search("What's the application rate for Roundup?", product_name="Roundup") |
| """ |
| |
| def __init__(self, vector_store=None): |
| """Initialize RAG search components. |
| |
| vector_store: optional shared QdrantVectorStore. The embedded on-disk |
| Qdrant permits one client per path per process, so online mode passes a |
| single store shared with the DocumentLoader instead of opening a second. |
| """ |
| |
| |
| if vector_store is not None: |
| self.vector_store = vector_store |
| else: |
| try: |
| self.vector_store = get_shared_vector_store() |
| except Exception as e: |
| print(f"⚠️ Warning: Could not initialize Qdrant: {e}") |
| self.vector_store = None |
| |
| |
| try: |
| creds = CredentialsManager() |
| openai_key = creds.get_api_key("openai") |
| self.embedding_service = OpenAIEmbeddingService(api_key=openai_key) |
| except Exception as e: |
| print(f"⚠️ Warning: Could not initialize OpenAI embeddings: {e}") |
| self.embedding_service = None |
| |
| def search( |
| self, |
| query: str, |
| product_name: Optional[str] = None, |
| limit: int = 5, |
| score_threshold: float = 0.4 |
| ) -> List[Dict]: |
| """ |
| Search CDMS documents in Qdrant |
| |
| Args: |
| query: User's question or search query |
| product_name: Optional product name to filter results (e.g., "Roundup") |
| limit: Maximum number of results to return |
| score_threshold: Minimum similarity score (0-1) |
| |
| Returns: |
| List of dicts with: |
| { |
| "content": str (chunk text), |
| "page_number": int (exact page number), |
| "source_file": str (PDF filename), |
| "score": float (similarity score 0-1), |
| "document_id": str, |
| "chunk_index": int |
| } |
| """ |
| if not self.vector_store or not self.embedding_service: |
| return [] |
|
|
| |
| |
| |
| |
| if product_name: |
| try: |
| from src.cdms.product_catalog import get_catalog |
| if not get_catalog().is_available(product_name): |
| print(f"ℹ️ '{product_name}' is not in the indexed catalog - abstaining.") |
| return [] |
| except Exception as e: |
| print(f"⚠️ Catalog availability check skipped: {e}") |
|
|
| try: |
| |
| query_embedding = self.embedding_service.generate_embedding(query) |
| |
| if not query_embedding: |
| print("⚠️ Warning: Failed to generate query embedding") |
| return [] |
| |
| |
| |
| search_limit = limit * 3 |
|
|
| if product_name: |
| |
| |
| |
| |
| |
| from src.cdms.product_catalog import normalize_filename |
| canonical = normalize_filename(product_name) |
| |
| |
| |
| |
| |
| |
| product_threshold = min(score_threshold, 0.2) |
| product_filter = Filter( |
| must=[FieldCondition(key="product", match=MatchValue(value=canonical))] |
| ) |
| results = self.vector_store.search_documents( |
| query_embedding=query_embedding, |
| limit=search_limit, |
| score_threshold=product_threshold, |
| query_filter=product_filter, |
| ) |
|
|
| if results: |
| results = results[:limit] |
| else: |
| |
| |
| |
| |
| global_results = self.vector_store.search_documents( |
| query_embedding=query_embedding, |
| limit=search_limit, |
| score_threshold=product_threshold, |
| ) |
| product_lower = product_name.lower() |
| results = [ |
| r for r in global_results |
| if product_lower in r.get("source_file", "").lower() |
| or product_lower in r.get("metadata", {}).get("document_name", "").lower() |
| ][:limit] |
| else: |
| |
| |
| results = self.vector_store.search_documents( |
| query_embedding=query_embedding, |
| limit=search_limit, |
| score_threshold=score_threshold, |
| ) |
| from src.cdms.product_catalog import diversify_by_product |
| results = diversify_by_product(results, limit=limit, max_per_product=2) |
| |
| |
| formatted_results = [] |
| for result in results: |
| |
| metadata = result.get("metadata", {}) |
| |
| |
| pdf_url = result.get("pdf_url", "") |
| |
| |
| if not pdf_url: |
| pdf_url = metadata.get("pdf_url", "") |
| |
| |
| if not pdf_url and isinstance(metadata, dict): |
| pdf_url = metadata.get("pdf_url", "") |
| |
| |
| url_hash = result.get("url_hash", "") or metadata.get("url_hash", "") |
| |
| |
| page_number = result.get("page_number", 0) |
| |
| |
| if page_number <= 0: |
| |
| page_number = metadata.get("page_number", 0) |
| |
| |
| if page_number <= 0: |
| |
| chunk_index = metadata.get("chunk_index", 0) |
| if chunk_index > 0: |
| |
| page_number = (chunk_index // 3) + 1 |
| else: |
| |
| page_number = 1 |
| print(f"⚠️ Warning: Invalid or missing page_number for chunk, using estimated value: {page_number}") |
| |
| formatted_results.append({ |
| "content": result.get("content", ""), |
| "page_number": page_number, |
| "source_file": result.get("source_file", "Unknown"), |
| "score": result.get("score", 0.0), |
| "document_id": result.get("document_id", ""), |
| "chunk_index": metadata.get("chunk_index", 0), |
| "document_name": metadata.get("document_name", ""), |
| "pdf_url": pdf_url, |
| "url_hash": url_hash |
| }) |
| |
| return formatted_results |
| |
| except Exception as e: |
| print(f"⚠️ Warning: RAG search failed: {e}") |
| import traceback |
| traceback.print_exc() |
| return [] |
| |
| def search_by_product( |
| self, |
| product_name: str, |
| query: Optional[str] = None, |
| limit: int = 5 |
| ) -> List[Dict]: |
| """ |
| Search for specific product information |
| |
| Args: |
| product_name: Product name (e.g., "Roundup") |
| query: Optional specific question (if None, returns general product info) |
| limit: Maximum results |
| |
| Returns: |
| List of relevant chunks with page numbers |
| """ |
| if query: |
| return self.search(query=query, product_name=product_name, limit=limit) |
| else: |
| |
| return self.search( |
| query=f"{product_name} pesticide label information", |
| product_name=product_name, |
| limit=limit |
| ) |
| |
| def get_collection_stats(self) -> Dict: |
| """ |
| Get statistics about the CDMS documents collection |
| |
| Returns: |
| Dict with collection information |
| """ |
| if not self.vector_store: |
| return {"error": "Vector store not initialized"} |
| |
| try: |
| info = self.vector_store.get_collection_info() |
| return info |
| except Exception as e: |
| return {"error": str(e)} |
|
|
|
|
| |
| if __name__ == "__main__": |
| print("=" * 80) |
| print("Testing CDMS RAG Search") |
| print("=" * 80) |
| |
| try: |
| searcher = CDMSRAGSearch() |
| |
| |
| print("\n📊 Collection Stats:") |
| stats = searcher.get_collection_stats() |
| if "error" not in stats: |
| if "cdms_documents" in stats: |
| doc_info = stats["cdms_documents"] |
| print(f" Documents in Qdrant: {doc_info.get('points_count', 0)}") |
| print(f" Vectors: {doc_info.get('vectors_count', 0)}") |
| else: |
| print(" ⚠️ No documents indexed yet") |
| else: |
| print(f" ⚠️ {stats['error']}") |
| |
| |
| if stats.get("cdms_documents", {}).get("points_count", 0) > 0: |
| print("\n🔍 Testing search: 'application rate'") |
| results = searcher.search("application rate", limit=3) |
| |
| if results: |
| print(f" ✅ Found {len(results)} result(s)") |
| for i, result in enumerate(results, 1): |
| print(f"\n {i}. Score: {result['score']:.3f}") |
| print(f" Page: {result['page_number']}") |
| print(f" File: {result['source_file']}") |
| print(f" Content: {result['content'][:100]}...") |
| else: |
| print(" ⚠️ No results found") |
| else: |
| print("\n💡 To test search:") |
| print(" 1. Download PDFs (Phase 1)") |
| print(" 2. Process and index PDFs in Qdrant") |
| print(" 3. Run this test again") |
| |
| print("\n" + "=" * 80) |
| |
| except Exception as e: |
| print(f"❌ Error: {e}") |
| import traceback |
| traceback.print_exc() |
|
|
|
|