# retrieve_clippings.py # Verbatim chunk retrieval from clippings index (ChromaDB). # Vector search + cross-encoder re-ranking, no LLM. # # Returns the top re-ranked chunks with their full text, file metadata, and # scores. Includes page numbers for PDF sources when available. # # E.M.F. February 2026 # Environment vars must be set before importing huggingface/transformers # libraries, because huggingface_hub.constants evaluates HF_HUB_OFFLINE # at import time. import os os.environ["TOKENIZERS_PARALLELISM"] = "false" os.environ["SENTENCE_TRANSFORMERS_HOME"] = "./models" os.environ["HF_HUB_OFFLINE"] = "1" import chromadb from llama_index.core import VectorStoreIndex, Settings from llama_index.vector_stores.chroma import ChromaVectorStore from llama_index.embeddings.huggingface import HuggingFaceEmbedding from llama_index.core.postprocessor import SentenceTransformerRerank import sys import textwrap # # Globals # PERSIST_DIR = "./clippings_search/store_clippings" COLLECTION_NAME = "clippings" # Embedding model (must match build_clippings.py) EMBED_MODEL = HuggingFaceEmbedding( cache_folder="./models", model_name="BAAI/bge-large-en-v1.5", local_files_only=True, ) # Cross-encoder model for re-ranking (cached in ./models/) RERANK_MODEL = "cross-encoder/ms-marco-MiniLM-L-12-v2" RERANK_TOP_N = 15 RETRIEVE_TOP_K = 30 # Output formatting WRAP_WIDTH = 80 def main(): # No LLM needed -- set embed model only Settings.embed_model = EMBED_MODEL # Load ChromaDB collection client = chromadb.PersistentClient(path=PERSIST_DIR) collection = client.get_collection(COLLECTION_NAME) # Build index from existing vector store vector_store = ChromaVectorStore(chroma_collection=collection) index = VectorStoreIndex.from_vector_store(vector_store) # Build retriever (vector search only, no query engine / LLM) retriever = index.as_retriever(similarity_top_k=RETRIEVE_TOP_K) # Cross-encoder re-ranker reranker = SentenceTransformerRerank( model=RERANK_MODEL, top_n=RERANK_TOP_N, ) # Query if len(sys.argv) < 2: print("Usage: python retrieve_clippings.py QUERY_TEXT") sys.exit(1) q = " ".join(sys.argv[1:]) # Retrieve and re-rank nodes = retriever.retrieve(q) reranked = reranker.postprocess_nodes(nodes, query_str=q) # Build result list with metadata results = [] for i, node in enumerate(reranked, 1): meta = getattr(node, "metadata", None) or node.node.metadata score = getattr(node, "score", None) file_name = meta.get("file_name", "unknown") page_label = meta.get("page_label", "") results.append((i, node, file_name, page_label, score)) # --- Summary: source files and rankings --- print(f"\nQuery: {q}") print(f"Retrieved {len(nodes)} chunks, re-ranked to top {len(reranked)}") print(f"({collection.count()} total vectors in collection)\n") # Unique source files in rank order seen = set() unique_sources = [] for i, node, file_name, page_label, score in results: if file_name not in seen: seen.add(file_name) unique_sources.append(file_name) print(f"Source files ({len(unique_sources)} unique):") for j, fname in enumerate(unique_sources, 1): print(f" {j}. {fname}") print(f"\nRankings:") for i, node, file_name, page_label, score in results: line = f" [{i:2d}] {score:+7.3f} {file_name}" if page_label: line += f" (p. {page_label})" print(line) # --- Full chunk text --- print(f"\n{'=' * WRAP_WIDTH}") print("CHUNKS") print("=" * WRAP_WIDTH) for i, node, file_name, page_label, score in results: header = f"=== [{i}] {file_name}" if page_label: header += f" (p. {page_label})" header += f" (score: {score:.3f})" print("\n" + "=" * WRAP_WIDTH) print(header) print("=" * WRAP_WIDTH) text = node.get_content() for line in text.splitlines(): if line.strip(): print(textwrap.fill(line, width=WRAP_WIDTH)) else: print() print() if __name__ == "__main__": main()