File size: 5,800 Bytes
4dfafb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""M5 sub-experiment B — early directional read from a PARTIAL golds-first index.

The full UniXcoder index runs golds-first (index_corpus.py GOLDS_FIRST=1), so the
first ~22k embedded docs are every eval query's gold target; docs after that are
train-split distractors whose pool grows as indexing proceeds. This script does a
fair, exact brute-force cosine A/B over the SAME candidate pool for both models,
reading vectors straight from the .cache/*.npy files — no Qdrant, no HNSW recall
loss, no CPU contention with the running indexer.

Pool at run time = the first P = unixcoder embed-progress docs (golds-first order).
UniXcoder doc vectors come from its cache[0:P]; MiniLM doc vectors are pulled from
the full MiniLM cache for the SAME doc set (mapped by doc_id). Queries evaluated =
those whose gold is within the pool. Metrics: MRR@10 / nDCG@10 / Recall@100.

Usage:
    .venv/bin/python scripts/sample_eval_partial.py            # eval at current P
    .venv/bin/python scripts/sample_eval_partial.py --min-docs 44000   # require P first
"""
from __future__ import annotations

import argparse
import os
import sys

import numpy as np

sys.path.insert(0, "src")
from codesearch.data import load_codesearch  # noqa: E402
from codesearch.eval.metrics import (  # noqa: E402
    mean_ndcg,
    mean_recall,
    mean_reciprocal_rank,
)
from codesearch.embedding import load_encoder  # noqa: E402

CACHE = ".cache"
UNIX_MODEL = "microsoft/unixcoder-base"
MINILM_MODEL = "all-MiniLM-L6-v2"
UNIX_NPY = f"{CACHE}/corpus_vectors_microsoft_unixcoder-base.npy"
UNIX_PROG = f"{CACHE}/corpus_vectors_microsoft_unixcoder-base.progress"
MINILM_NPY = f"{CACHE}/corpus_vectors_all-MiniLM-L6-v2.npy"
UNIX_DIM, MINILM_DIM = 768, 384


def _read_progress(path: str) -> int:
    with open(path) as f:
        return int(f.read().strip() or "0")


def _golds_first_corpus(corpus, queries):
    """Reproduce index_corpus.py's GOLDS_FIRST reorder exactly."""
    gold_ids = {q["relevant_id"] for q in queries}
    golds = [d for d in corpus if d["id"] in gold_ids]
    rest = [d for d in corpus if d["id"] not in gold_ids]
    return golds + rest


def _topk_ids(query_vecs, doc_vecs, doc_ids, k=100, chunk=512):
    """Brute-force cosine (vectors are pre-normalized): top-k doc_ids per query."""
    out = []
    for i in range(0, len(query_vecs), chunk):
        sims = query_vecs[i : i + chunk] @ doc_vecs.T           # (b, P)
        kk = min(k, sims.shape[1])
        part = np.argpartition(-sims, kk - 1, axis=1)[:, :kk]
        for r in range(sims.shape[0]):
            order = part[r][np.argsort(-sims[r, part[r]])]
            out.append([doc_ids[j] for j in order])
    return out


def _eval_model(name, npy_path, dim, doc_rows, query_texts, relevant_ids, encoder):
    """doc_rows: np.ndarray of cache-row indices for the pool docs (in pool order)."""
    mm = np.memmap(npy_path, dtype=np.float32, mode="r").reshape(-1, dim)
    doc_vecs = np.ascontiguousarray(mm[doc_rows])                # (P, dim)
    print(f"[{name}] encoding {len(query_texts):,} queries...", flush=True)
    qv = encoder.encode(query_texts, normalize_embeddings=True, batch_size=128)
    qv = np.asarray(qv, dtype=np.float32)
    retrieved = _topk_ids(qv, doc_vecs, POOL_IDS, k=100)
    qr = list(zip(retrieved, relevant_ids))
    return (
        mean_reciprocal_rank(qr, k=10),
        mean_ndcg(qr, k=10),
        mean_recall(qr, k=100),
    )


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--min-docs", type=int, default=0,
                    help="Require at least this many embedded docs before evaluating.")
    args = ap.parse_args()

    P = _read_progress(UNIX_PROG)
    print(f"UniXcoder embed-progress P = {P:,} docs")
    if P < args.min_docs:
        print(f"Not enough yet (need {args.min_docs:,}); exiting.")
        return 0

    corpus, queries = load_codesearch(n=-1)
    orig_row = {d["id"]: i for i, d in enumerate(corpus)}        # doc_id -> MiniLM cache row
    gf = _golds_first_corpus(corpus, queries)                    # golds-first (== unixcoder cache order)
    pool = gf[:P]
    global POOL_IDS
    POOL_IDS = [d["id"] for d in pool]
    pool_id_set = set(POOL_IDS)

    # rows into each cache for the pool docs, in pool order
    unix_rows = np.arange(P)                                     # unixcoder cache is already golds-first
    minilm_rows = np.array([orig_row[i] for i in POOL_IDS])      # map to original-order MiniLM cache

    subset = [q for q in queries if q["relevant_id"] in pool_id_set]
    texts = [q["query"] for q in subset]
    rel = [q["relevant_id"] for q in subset]
    print(f"Pool: {P:,} docs  |  eval queries (gold in pool): {len(subset):,}")
    if not subset:
        print("No queries have their gold in the pool yet — wait for more golds.")
        return 0

    unix_enc = load_encoder(UNIX_MODEL)
    minilm_enc = load_encoder(MINILM_MODEL)

    u_mrr, u_ndcg, u_rec = _eval_model("unixcoder", UNIX_NPY, UNIX_DIM, unix_rows, texts, rel, unix_enc)
    m_mrr, m_ndcg, m_rec = _eval_model("minilm", MINILM_NPY, MINILM_DIM, minilm_rows, texts, rel, minilm_enc)

    print("\n" + "=" * 62)
    print(f"PARTIAL-INDEX A/B  (pool={P:,} docs, n={len(subset):,} queries, brute-force cosine)")
    print("NOTE: reduced pool inflates absolute scores; the MODEL DELTA is the signal.")
    print("=" * 62)
    print(f"{'Model':<16}{'MRR@10':>10}{'nDCG@10':>10}{'Recall@100':>12}")
    print("-" * 62)
    print(f"{'MiniLM (base)':<16}{m_mrr:>10.4f}{m_ndcg:>10.4f}{m_rec:>12.4f}")
    print(f"{'UniXcoder':<16}{u_mrr:>10.4f}{u_ndcg:>10.4f}{u_rec:>12.4f}")
    print(f"{'Δ (Unix-Mini)':<16}{u_mrr-m_mrr:>+10.4f}{u_ndcg-m_ndcg:>+10.4f}{u_rec-m_rec:>+12.4f}")
    print("=" * 62)
    return 0


if __name__ == "__main__":
    sys.exit(main())