import sys import os import json # Add project root to python path sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.dataset import load_documents from app.hybrid_search import BM25Index, HybridSearcher from app.vector_store import build_index from sentence_transformers import SentenceTransformer from experiments.benchmark_data import benchmark_queries try: from app.reranker import get_reranker except ImportError: get_reranker = None def run_error_analysis(): print("Loading ArXiv ML Papers Dataset...") docs, labels, label_names = load_documents() # We load a decent chunk to have meaningful retrieval docs = docs[:2000] labels = labels[:2000] print("\nLoading Embedding Model...") model = SentenceTransformer('all-MiniLM-L6-v2') print("Encoding Documents (Dense Index)...") embeddings = model.encode(docs, show_progress_bar=True, convert_to_numpy=True, normalize_embeddings=True) faiss_index = build_index(embeddings, labels) print("\nBuilding BM25 Index (Sparse Index)...") bm25 = BM25Index() bm25.fit(docs) hybrid = HybridSearcher(bm25) reranker = None if get_reranker is not None: try: reranker = get_reranker() # Force load _ = reranker.model print("\nCross-Encoder Reranker loaded successfully.") except Exception as e: print(f"\nFailed to load reranker: {e}") else: print("\nReranker module not available.") print(f"\nRunning Error Analysis on {len(benchmark_queries)} natural language queries...") error_report = [] cat_to_id = {name: i for i, name in enumerate(label_names)} for item in benchmark_queries: q_text = item["query"] relevant_docs = item.get("relevant_docs", []) # Filter out out-of-bound IDs relevant_docs = [doc_id for doc_id in relevant_docs if doc_id < len(docs)] if not relevant_docs: continue # 1. Retrieve using Hybrid (baseline) q_emb = model.encode([q_text], convert_to_numpy=True, normalize_embeddings=True)[0] hybrid_indices, hybrid_scores, _ = hybrid.search(q_text, q_emb, faiss_index, docs, top_k=10) # Check top-3 hits for hybrid hybrid_top3 = [int(idx) for idx in hybrid_indices[:3]] hybrid_hit = any(idx in relevant_docs for idx in hybrid_top3) # 2. Rerank top 30 reranked_hit = False reranked_top3 = [] if reranker is not None: cand_indices, _, _ = hybrid.search(q_text, q_emb, faiss_index, docs, top_k=30) cand_docs = [docs[idx] for idx in cand_indices] r_indices, r_scores = reranker.rerank(q_text, cand_docs, cand_indices, top_k=10) reranked_top3 = [int(idx) for idx in r_indices[:3]] reranked_hit = any(idx in relevant_docs for idx in reranked_top3) # If both hit, no error to analyze. If one hits and other misses, or both miss, we analyze. if hybrid_hit and (reranker is None or reranked_hit): continue # Analyze failure failure_type = "UNKNOWN" # What categories DID we retrieve? retrieved_cats_hybrid = [label_names[labels[idx]] for idx in hybrid_top3] retrieved_cats_reranked = [label_names[labels[idx]] for idx in reranked_top3] if reranked_top3 else [] if not hybrid_hit and reranked_hit: failure_type = "HYBRID_MISS_RERANKER_FIXED" reason = "Hybrid search failed to push relevant docs to top 3, but reranker corrected it." elif hybrid_hit and not reranked_hit: failure_type = "RERANKER_DEGRADATION" reason = "Reranker pushed irrelevant documents above the relevant ones found by hybrid." elif not hybrid_hit and not reranked_hit: if set(retrieved_cats_hybrid) == set([expected_cat_name]): failure_type = "DATASET_LABEL_ISSUE" reason = "Retrieved documents look perfectly relevant but ground truth labels don't match." else: # Check if the query is ambiguous between two categories if "cs.LG" in expected_cat_name and any("cs." in c for c in retrieved_cats_hybrid): failure_type = "BOUNDARY_CASE" reason = f"Query is ambiguous between {expected_cat_name} and {set(retrieved_cats_hybrid)}." else: failure_type = "RETRIEVAL_FAILURE" reason = "Neither hybrid nor reranker found relevant documents in top 3." error_report.append({ "query": q_text, "expected_category": expected_cat_name, "hybrid_retrieved_categories": retrieved_cats_hybrid, "reranked_retrieved_categories": retrieved_cats_reranked, "failure_type": failure_type, "reason": reason }) print(f"\nFound {len(error_report)} queries with retrieval issues out of {len(benchmark_queries)}.") os.makedirs("experiments/results", exist_ok=True) with open("experiments/results/error_analysis.json", "w") as f: json.dump(error_report, f, indent=2) print("\n--- Error Analysis Summary ---") for err in error_report: print(f"\nQuery: {err['query']}") print(f"Expected: {err['expected_category']}") print(f"Hybrid Top-3 Cats: {err['hybrid_retrieved_categories']}") if err['reranked_retrieved_categories']: print(f"Reranked Top-3 Cats: {err['reranked_retrieved_categories']}") print(f"Failure Type: {err['failure_type']}") print(f"Reason: {err['reason']}") print("-" * 40) print("\nError analysis report saved to experiments/results/error_analysis.json") if __name__ == "__main__": run_error_analysis()