codesearch / README.md
CamQuestByte
M5-B result: UniXcoder dense +4.5pp MRR over MiniLM (fair code model)
8e4942c
|
Raw
History Blame Contribute Delete
7.04 kB

A newer version of the Gradio SDK is available: 6.22.0

Upgrade
metadata
title: Codesearch
emoji: πŸ”
colorFrom: purple
colorTo: purple
sdk: gradio
sdk_version: 6.13.0
app_file: app.py
pinned: false
license: mit
short_description: BM25 vs dense retrieval on CodeSearchNet

CodeSearch: Semantic Retrieval from Scratch

Learning project: BM25 vs dense vs hybrid retrieval on CodeSearchNet, with rigorous eval.

πŸ” Live demo: https://camquest-codesearch.hf.space β€” pick a retriever per column and compare BM25 / dense / hybrid / reranked results side-by-side, with per-query latency.

Retrieval Results

Reference numbers (Husain et al., 2019)

Eval setup: 1+999 random distractors per query (not full corpus). Only MRR is reported. BM25 indexes docstrings β€” inflated relative to a proper code-search baseline.

Retriever MRR Notes
BM25 (Elasticsearch) ~0.68 docstring index

Our progress

Eval setup: full 434k-doc corpus, 22k queries, BM25 indexes func_code_tokens (docstrings stripped).

Retriever Model MRR@10 nDCG@10 Recall@100 Milestone
BM25 β€” 0.2747 0.3020 0.5208 M1 βœ…
Dense MiniLM-L6-v2 0.3891 (+0.1144) 0.4309 (+0.1289) 0.7520 (+0.2312) M2 βœ…
Hybrid (RRF) MiniLM + BM25 0.3977 (+0.1230) 0.4475 (+0.1455) 0.7750 (+0.2542) M3.1 βœ…
Hybrid + Rerank + ms-marco-MiniLM-L-6-v2 0.4011 (+0.1264) 0.4486 (+0.1466) 0.7750 (+0.2542) M3.2 βœ…
Dense UniXcoder-base (fair code model) 0.4343 (+0.1596) 0.4748 (+0.1728) 0.7769 (+0.2561) M5 βœ…

Deltas are vs BM25 baseline. Recall@100 jumps 23.1pp going from sparse to dense β€” the model bridges the natural-language-query β†’ code vocabulary gap that BM25 cannot. This is the motivating data point for M3: there is 23pp of headroom available to a reranker on top of dense, but only ~8pp on top of BM25 alone.

M3.1 β€” Hybrid RRF lifts Recall@100 by +2.3pp on top of dense (0.7520 β†’ 0.7750), with a much smaller MRR@10 lift (+0.86pp). That's the expected RRF signature: fusion promotes "rank-15 in both lists" candidates into the fused top-50 (fattening the pool for a downstream reranker), but it rarely lifts anything to rank-1 by itself. The Recall headroom from set-union over the same K is +5pp above RRF β€” flagged as the lever to pull if M3.2 underperforms.

M3.2 β€” the MS MARCO cross-encoder produces only a marginal lift over hybrid RRF. Full 22k eval: hybrid MRR@10=0.3977 β†’ hybrid+rerank 0.4011 (+0.34pp); nDCG@10 0.4475 β†’ 0.4486 (+0.11pp); Recall@100 identical (0.7750) by construction β€” the reranker only reorders the pool it's given. The full eval confirms a real but tiny positive effect and resolves an earlier n=2000 sampling artifact (where the sampled rerank MRR, 0.3938, sat slightly below full-hybrid, SEβ‰ˆ0.01). Cost: 24h CPU per full run (2.2M CE forward passes at ~26 pair/s on the eval laptop) β€” poor ROI for +0.3pp. The most likely explanation is the domain gap: MS MARCO CE was trained on NLβ†’NL passage ranking and reads code_tokens (space-joined AST tokens) as an alien input. **The honest takeaway: off-the-shelf NL cross-encoders give only a marginal (+0.3pp) improvement to hybrid RRF on NLβ†’code retrieval β€” real, but not worth the compute.** M5 explores whether code-aware CEs (CodeBERT / UniXcoder-based) or richer candidate text (AST-stripped whole_func_string) can produce a real lift.

M5 β€” embedding experiments (in progress)

Sub-experiment A β€” does the doc representation matter? No (null result). Re-embedded the corpus with the docstring-stripped function source (data.strip_docstring β€” AST span-excision, validated 0 docstring leakage) instead of CSN code_tokens, holding the model (MiniLM-L6-v2) fixed. Same-sample A/B (n=2,000, seed=42): Dense MRR@10 0.3938 β†’ 0.3938 (Β±0.00), Hybrid 0.3875 β†’ 0.3853 β€” every delta inside SEβ‰ˆ0.01. Interpretation: a general-purpose NL embedder can't exploit code structure, so how the code is serialised barely moves retrieval. The model is the bottleneck, not the representation β€” which is what motivates B.

Sub-experiment B — swap in a fair code bi-encoder: +4.5pp MRR, the biggest lever in the project. Replacing MiniLM with UniXcoder-base (code-pretrained but not CodeSearchNet-retrieval-finetuned — an honest test; CSN-tuned encoders would be rigged in-distribution) lifts dense MRR@10 0.3891 → 0.4343 (+4.5pp), nDCG@10 0.4309 → 0.4748, Recall@100 0.7520 → 0.7769 — full 434k corpus, 22k queries, same code_tokens input, same Qdrant HNSW method. For scale that's ~5× the hybrid-RRF gain (+0.9pp) and ~14× the cross-encoder rerank gain (+0.3pp). This confirms the A→B thesis: the model was the dense bottleneck, not the representation.

Implementation notes: the official unixcoder.py can't run on our pinned transformers 5.x β€” its 2-D (mask_iΒ·mask_j) attention mask crashes on batched input and goes silently causal on single input β€” so codesearch/embedding.py reproduces the official encoder-only recipe (mode-token framing β†’ mean-pool β†’ L2-normalize) in a form 5.x executes, validated element-wise identical (max|Ξ”|=0) to the official model run bidirectionally. Held to the same max_seq_length=256 as the MiniLM baseline (apples-to-apples model swap). 768-dim Γ— 434k > 1GB free tier β†’ the Qdrant collection is on_disk (β‰ˆ0.97s/query vs MiniLM's in-RAM ms; a raised client timeout + query-retry guard handle the slow reads). Next: a code-to-code variant (UniXcoder on AST-docstring-stripped whole_func_string, its native input) and a UniXcoder hybrid row.

Stack

  • Dataset: CodeSearchNet Python split (~400k functions, 4k eval queries)
  • BM25: bm25s (vectorized; ~200x faster than rank_bm25 on full corpus)
  • Embeddings: sentence-transformers (local, no API cost)
  • Vector DB: Qdrant Cloud (free tier, 1GB)
  • UI: Gradio on Hugging Face Spaces (free)

Milestones

Milestone Status
M0 Β· Hello World βœ… done
M1 Β· BM25 Baseline + Eval βœ… done
M2 Β· Dense Retrieval βœ… done
M3 Β· Hybrid + Reranking βœ… done
M4 Β· UI + Deployment βœ… done β€” live
M5 · Model Swap (optional) ⬜ not started

Local Setup

# Install uv if you don't have it
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create env and install deps
uv venv
source .venv/bin/activate
uv sync

# Copy env template and fill in your Qdrant credentials
cp .env.example .env
# edit .env

# Run locally
python app.py

HF Spaces Deployment

  1. Create a new Space (Gradio SDK) at huggingface.co/spaces
  2. Add QDRANT_URL and QDRANT_API_KEY as Space secrets
  3. Push this repo: git push hf main

The requirements.txt (generated by uv export) is what HF Spaces uses to install deps. To regenerate it after adding packages: uv export --no-hashes > requirements.txt