Spaces:
Running
Running
| """ | |
| CodeSearch β Gradio UI | |
| Side-by-side comparison of four retrieval modes over CodeSearchNet Python: | |
| BM25 Β· Dense (MiniLM) Β· Hybrid (RRF) Β· Hybrid + cross-encoder rerank | |
| Boot is NON-INDEXING by design: | |
| - Dense vectors live in Qdrant, built offline via scripts/index_corpus.py. | |
| - BM25 loads a prebuilt slim index (.cache/bm25-slim) in full-corpus mode. | |
| - The app refuses to embed the corpus at boot (see the guard below). | |
| Modes: | |
| SMOKE_TEST_SIZE=-1 β full corpus; BM25 from the slim index. (HF Space secret) | |
| SMOKE_TEST_SIZE=100 β local pipeline check; BM25 built in-memory. Dense/hybrid | |
| modes return full-Qdrant ids not in the small corpus, so | |
| only BM25 is meaningful in smoke mode. | |
| """ | |
| import os | |
| import random | |
| import sys | |
| import time | |
| import traceback | |
| sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")) | |
| import gradio as gr | |
| from codesearch.config import SMOKE_TEST_SIZE, TOP_K | |
| from codesearch.data import load_codesearch | |
| from codesearch.retrievers.bm25 import BM25Retriever | |
| from codesearch.retrievers.bm25_index import BM25Index | |
| from codesearch.retrievers.dense import DenseRetriever | |
| from codesearch.retrievers.hybrid import HybridRetriever | |
| from codesearch.retrievers.reranker import CrossEncoderReranker | |
| _HERE = os.path.dirname(os.path.abspath(__file__)) | |
| BM25_SLIM_DIR = os.path.join(_HERE, ".cache", "bm25-slim") | |
| # Candidate pool for hybrid fusion and reranking in the UI. Small on purpose: | |
| # reranking is inference-bound (~20 pairs/query on CPU), and K=20 keeps | |
| # hybrid+rerank under the 3s latency budget. Eval uses K=100 for Recall; the UI | |
| # trades a little recall for responsiveness. See M3.2 design notes. | |
| UI_POOL = 20 | |
| # --------------------------------------------------------------------------- | |
| # Boot: load corpus + wire retrievers once. No indexing happens here. | |
| # --------------------------------------------------------------------------- | |
| print("=== CodeSearch Boot ===") | |
| corpus, queries = load_codesearch() # queries = eval set (test docstrings w/ known gold) | |
| # Two id-keyed views over the corpus: | |
| # corpus_by_id β for rendering result cards (dense/hybrid hits carry id+score only) | |
| # corpus_lookup β reranker candidate text (code_tokens, already space-joined) | |
| corpus_by_id = {d["id"]: d for d in corpus} | |
| corpus_lookup = {d["id"]: d["code_tokens"] for d in corpus} | |
| # Ground truth for the eval queries: each test docstring maps to the id of the | |
| # function it documents. Lets the UI badge the "correct" hit and its rank β but | |
| # only for these known queries; a free-form query has no gold. | |
| gold_by_query = {q["query"]: q["relevant_id"] for q in queries} | |
| eval_query_texts = list(gold_by_query.keys()) | |
| if SMOKE_TEST_SIZE == -1: | |
| print(f"Loading slim BM25 index from {BM25_SLIM_DIR} ...") | |
| bm25 = BM25Retriever(BM25Index.load_index_only(BM25_SLIM_DIR, corpus)) | |
| else: | |
| print(f"Smoke mode (n={SMOKE_TEST_SIZE}): building in-memory BM25 index...") | |
| bm25 = BM25Retriever(corpus) | |
| dense = DenseRetriever(recreate_collection=False) | |
| if not dense.collection_exists_and_populated(): | |
| raise RuntimeError( | |
| "Qdrant collection is empty. Build it offline with scripts/index_corpus.py β " | |
| "the app will not embed the full corpus at boot." | |
| ) | |
| hybrid = HybridRetriever(bm25, dense, pool_size=UI_POOL) | |
| reranker = CrossEncoderReranker(hybrid, corpus_lookup, pool_size=UI_POOL) | |
| RETRIEVERS = { | |
| "BM25": bm25, | |
| "Dense (MiniLM)": dense, | |
| "Hybrid (RRF)": hybrid, | |
| "Hybrid + Rerank": reranker, | |
| } | |
| MODES = list(RETRIEVERS.keys()) | |
| print(f"Ready. corpus={len(corpus):,} docs Β· modes={MODES}") | |
| # --------------------------------------------------------------------------- | |
| # Search + formatting | |
| # --------------------------------------------------------------------------- | |
| def _fmt_latency(dt: float) -> str: | |
| return f"β± **{dt:.2f} s**" if dt >= 1.0 else f"β± **{dt * 1000:.0f} ms**" | |
| def _format(hits: list[dict], mode: str, gold_id: str | None = None) -> str: | |
| if not hits: | |
| return "_No results._" | |
| cards = [] | |
| for i, h in enumerate(hits, 1): | |
| doc = corpus_by_id.get(h["id"]) | |
| if doc is None: | |
| cards.append( | |
| f"**#{i}** β `{h['id']}` not in local corpus " | |
| f"(expected in smoke mode; full corpus on the Space)." | |
| ) | |
| continue | |
| docstring = (doc.get("docstring") or "").strip()[:200] | |
| code = (doc.get("code") or "").strip()[:400] | |
| url = doc.get("url", "") | |
| score = h.get("score", 0.0) | |
| # Provenance line: shows WHY a doc ranked where it did, per mode. | |
| prov = f"score **{score:.4f}**" | |
| if h.get("bm25_rank") is not None or h.get("dense_rank") is not None: | |
| prov += f" Β· bm25 #{h.get('bm25_rank')} Β· dense #{h.get('dense_rank')}" | |
| if "pre_rerank_score" in h: | |
| prov += f" Β· pre-rerank {h['pre_rerank_score']:.4f}" | |
| gold = " β **GOLD**" if gold_id is not None and h["id"] == gold_id else "" | |
| cards.append( | |
| f"**#{i}**{gold} β {prov}\n\n" | |
| f"{docstring}{'β¦' if docstring else ''}\n\n" | |
| f"```python\n{code}{'β¦' if code else ''}\n```\n" | |
| + (f"[source]({url})\n" if url else "") | |
| + "\n---" | |
| ) | |
| return "\n".join(cards) | |
| def _run(mode: str, query: str, gold_id: str | None) -> tuple[str, str]: | |
| retr = RETRIEVERS[mode] | |
| t0 = time.perf_counter() | |
| try: | |
| hits = retr.retrieve(query, top_k=TOP_K) | |
| except Exception as e: # one mode failing must not take down the search | |
| traceback.print_exc() | |
| return f"β οΈ **{mode} failed:** `{type(e).__name__}: {e}`", "β" | |
| dt = time.perf_counter() - t0 | |
| # Gold verdict header β only for known eval queries (free-form has no gold). | |
| verdict = "" | |
| if gold_id is not None: | |
| rank = next((i for i, h in enumerate(hits, 1) if h["id"] == gold_id), None) | |
| verdict = ( | |
| f"### β Gold answer at rank #{rank}\n\n" | |
| if rank is not None | |
| else f"### β Gold answer not in top-{TOP_K}\n\n" | |
| ) | |
| return verdict + _format(hits, mode, gold_id), _fmt_latency(dt) | |
| def pick_random() -> str: | |
| """Fill the query box with a random eval query (one whose gold we know).""" | |
| return random.choice(eval_query_texts) if eval_query_texts else "" | |
| def search(query: str, left_mode: str, right_mode: str): | |
| """Run both columns and return (left_md, left_latency, right_md, right_latency).""" | |
| if not query.strip(): | |
| msg = "_Enter a query above._" | |
| return msg, "", msg, "" | |
| # Known only for eval queries (exact match); free-form typing β gold_id None. | |
| gold_id = gold_by_query.get(query) or gold_by_query.get(query.strip()) | |
| left_md, left_lat = _run(left_mode, query, gold_id) | |
| right_md, right_lat = _run(right_mode, query, gold_id) | |
| return left_md, left_lat, right_md, right_lat | |
| def update_column(query: str, mode: str) -> tuple[str, str]: | |
| """Re-run a single column β wired to each retriever dropdown's change so | |
| switching the retriever refreshes that column without re-running the other.""" | |
| if not query.strip(): | |
| return "_Enter a query above._", "" | |
| gold_id = gold_by_query.get(query) or gold_by_query.get(query.strip()) | |
| return _run(mode, query, gold_id) | |
| # --------------------------------------------------------------------------- | |
| # UI β selectable two-column compare | |
| # --------------------------------------------------------------------------- | |
| with gr.Blocks(title="CodeSearch") as demo: | |
| gr.Markdown( | |
| f""" | |
| # π CodeSearch | |
| Natural-language code search over **CodeSearchNet Python** ({len(corpus):,} functions). | |
| Pick a retriever per column and compare β watch where lexical (BM25), semantic | |
| (dense), fusion (RRF), and reranking disagree. | |
| Hit **π² Random eval query** to load a real benchmark query with a known answer β | |
| the **β GOLD** badge marks the correct function, and each column reports the rank it landed at. | |
| """ | |
| ) | |
| with gr.Row(): | |
| query_box = gr.Textbox( | |
| placeholder="e.g. parse a JSON file and return a dict", | |
| label="Query", | |
| scale=4, | |
| ) | |
| search_btn = gr.Button("Search", variant="primary", scale=1) | |
| random_btn = gr.Button("π² Random eval query", scale=1) | |
| with gr.Row(): | |
| with gr.Column(): | |
| left_mode = gr.Dropdown(MODES, value="BM25", label="Left retriever") | |
| left_lat = gr.Markdown() | |
| left_out = gr.Markdown() | |
| with gr.Column(): | |
| right_mode = gr.Dropdown( | |
| MODES, value="Hybrid + Rerank", label="Right retriever" | |
| ) | |
| right_lat = gr.Markdown() | |
| right_out = gr.Markdown() | |
| inputs = [query_box, left_mode, right_mode] | |
| outputs = [left_out, left_lat, right_out, right_lat] | |
| search_btn.click(fn=search, inputs=inputs, outputs=outputs) | |
| query_box.submit(fn=search, inputs=inputs, outputs=outputs) | |
| # Random: fill the box with a known-gold eval query, then run the comparison. | |
| random_btn.click(fn=pick_random, outputs=query_box).then( | |
| fn=search, inputs=inputs, outputs=outputs | |
| ) | |
| # Changing a retriever re-runs only its own column (keeps the other as-is). | |
| left_mode.change( | |
| fn=update_column, inputs=[query_box, left_mode], outputs=[left_out, left_lat] | |
| ) | |
| right_mode.change( | |
| fn=update_column, inputs=[query_box, right_mode], outputs=[right_out, right_lat] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(theme=gr.themes.Monochrome()) | |