Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/integration/retrieval_recall_integration_test.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/integration/retrieval_recall_integration_test.go |
| Original SHA-256 | 2fb92db5029000dc36abc01e5d6ded97dfb0e63eeadcb635a2a9432c911dafff |
| Git blob SHA | ca9c119a401d8d4fbaec3f05024bed816a3da649 |
| Reading-copy SHA-256 | 2fb92db5029000dc36abc01e5d6ded97dfb0e63eeadcb635a2a9432c911dafff |
Recovered source transcript
Download complete source text ↓253 lines · 10.9 KB
//go:build integration
// Retrieval recall regression gate. This is the FIRST evaluation of whether the hybrid search RECALLS the
// right people, not just whether the engine bands score. It loads a versioned gold set (testdata
// retrieval_qrels.json), seeds a fresh per run org with those candidates embedded through the REAL inference
// plane (so the vector leg is meaningful, a deterministic hash embedding would make recall arbitrary), then
// for each query embeds the query through the SAME plane, runs the production ai.HybridSearch under RLS, maps
// the returned CandidateIDs back to the seed keys, and computes recall at k, MRR, and nDCG against the qrels
// with the pure internal/platform/eval metrics. It logs every metric per query and the mean as a readable
// regression baseline, and asserts conservative floors a correct hybrid search clears comfortably while a real
// regression (in embeddings, HNSW tuning, the mission gate, or the RRF k constant) would breach.
//
// This is MEASUREMENT ONLY. It does not touch retrieval.go, the RRF math, or the frozen matching engine. It is
// gated on MM_MEGAMIND_URL and skips cleanly when it is unset, exactly like the sibling brain tests, so the
// orchestrator must set the gateway URL to run it on staging.
package integration
import (
"context"
"encoding/json"
"math"
"os"
"path/filepath"
"testing"
"time"
"github.com/pgvector/pgvector-go"
"github.com/missionworkforce/missionmatch/internal/modules/ai"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
"github.com/missionworkforce/missionmatch/internal/platform/eval"
"github.com/missionworkforce/missionmatch/internal/platform/inference"
)
// qrelsCandidate is one gold set candidate with the stable seed key the harness maps a returned id back to.
type qrelsCandidate struct {
Key string `json:"key"`
FullName string `json:"full_name"`
ClearanceLevel string `json:"clearance_level"`
ClearancePoly string `json:"clearance_poly"`
ClearanceStatus string `json:"clearance_status"`
Location string `json:"location"`
Headline string `json:"headline"`
}
// qrelsQuery is one query with the seed keys judged relevant to it.
type qrelsQuery struct {
ID string `json:"id"`
Text string `json:"text"`
Relevant []string `json:"relevant"`
}
type qrelsFixture struct {
Version string `json:"version"`
Candidates []qrelsCandidate `json:"candidates"`
Queries []qrelsQuery `json:"queries"`
}
func loadQrels(t *testing.T) qrelsFixture {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", "retrieval_qrels.json"))
if err != nil {
t.Fatalf("read qrels fixture: %v", err)
}
var fx qrelsFixture
if err := json.Unmarshal(raw, &fx); err != nil {
t.Fatalf("parse qrels fixture: %v", err)
}
if fx.Version == "" || len(fx.Candidates) == 0 || len(fx.Queries) == 0 {
t.Fatalf("qrels fixture is incomplete: version=%q candidates=%d queries=%d", fx.Version, len(fx.Candidates), len(fx.Queries))
}
return fx
}
// retrievalPlane builds an inference client to the live MEGAMIND plane, or skips when MM_MEGAMIND_URL is unset.
// The model and dimension match the production embedder so stored and query vectors share one space.
func retrievalPlane(t *testing.T) *inference.Client {
t.Helper()
url := os.Getenv("MM_MEGAMIND_URL")
if url == "" {
t.Skip("MM_MEGAMIND_URL not set; skipping the retrieval recall harness that needs the real embedding plane")
}
c, err := inference.New(inference.Config{
BaseURL: url, EmbedModel: "qllama/bge-small-en-v1.5", GenerateModel: "llama3.1:8b",
Dimension: 384, Timeout: 60 * time.Second, MaxRetries: 1,
// The gateway requires a bearer on every route but the health check. Omitting it worked only while
// MM_MEGAMIND_URL pointed at the token injecting proxy, so the suite proved the PROXY authenticates.
Token: os.Getenv("MM_MEGAMIND_TOKEN"),
})
if err != nil {
t.Fatalf("inference client: %v", err)
}
return c
}
// normalizeVec L2 normalizes a vector, exactly as embed backfill does before storing, so a seeded vector
// matches the backfill convention and cosine is a clean dot product.
func normalizeVec(v []float32) []float32 {
var s float64
for _, x := range v {
s += float64(x) * float64(x)
}
if s == 0 {
return v
}
inv := float32(1.0 / math.Sqrt(s))
out := make([]float32, len(v))
for i, x := range v {
out[i] = x * inv
}
return out
}
func embedOne(t *testing.T, client *inference.Client, text string) []float32 {
t.Helper()
resp, err := client.Embed(context.Background(), []string{text})
if err != nil {
t.Fatalf("embed %q: %v", text, err)
}
if len(resp.Embeddings) != 1 || len(resp.Embeddings[0]) != 384 {
t.Fatalf("embed %q returned wrong shape", text)
}
return normalizeVec(resp.Embeddings[0])
}
// TestRetrievalRecallGate is the regression gate. It seeds the gold set candidates into a fresh org with real
// plane embeddings, runs HybridSearch per query, and asserts the mean ranking metrics clear conservative
// floors. The gold set is constructed so each query's relevant candidates are both lexically present (the full
// text leg finds them) and semantically near the query (the vector leg finds them), so a correct hybrid search
// clears the floors comfortably and this is a gate, not a flaky test.
func TestRetrievalRecallGate(t *testing.T) {
client := retrievalPlane(t)
fx := loadQrels(t)
org := seedOrg(t)
ctx := ctxOrg(org)
// Seed every gold set candidate with its real plane embedding, mapping each seeded id to its stable key so
// a returned CandidateID can be resolved back to the key the qrels speak in. Seeding is RLS scoped to this
// fresh org (the candidates rows carry organization_id = org; the search runs in the same org context).
idToKey := make(map[string]string, len(fx.Candidates))
for _, c := range fx.Candidates {
// Embed the SAME text the candidate is findable by (name plus headline), the field the search_tsv and
// the resume vector both draw from, so the vector leg recalls the candidate the qrels expects.
vec := embedOne(t, client, c.FullName+" "+c.Headline)
pv := pgvector.NewVector(vec)
hn, loc := c.Headline, c.Location
created, err := dbgen.New(migrator).CreateCandidate(context.Background(), dbgen.CreateCandidateParams{
OrganizationID: org,
FullName: c.FullName,
Headline: &hn,
ClearanceLevel: c.ClearanceLevel,
ClearancePoly: c.ClearancePoly,
ClearanceStatus: c.ClearanceStatus,
Location: &loc,
Available: true,
ResumeEmbedding: &pv,
})
if err != nil {
t.Fatalf("seed gold candidate %q: %v", c.Key, err)
}
idToKey[created.ID.String()] = c.Key
}
const (
k5 = 5
k10 = 10
// limit pulls a generous depth so recall at 10 is measured over the real ranking, not truncated by a
// tight limit. The gold set has twelve candidates, so twenty surfaces them all.
searchLimit = 20
)
var sumRecall5, sumRecall10, sumMRR, sumNDCG10 float64
nQueries := 0
for _, q := range fx.Queries {
relevant := make(map[string]bool, len(q.Relevant))
for _, key := range q.Relevant {
relevant[key] = true
}
// Run the PRODUCTION hybrid search with an empty mission (the default unscoped path, byte for byte the
// shipped behavior), RLS scoped to this org. The query embeds through the SAME plane as the seed
// vectors, so the vector leg compares like with like.
hits, err := ai.HybridSearch(ctx, appDB, client, q.Text, "", searchLimit)
if err != nil {
t.Fatalf("query %q hybrid search: %v", q.ID, err)
}
// Map the returned CandidateIDs back to seed keys, preserving rank order. A returned id that is not a
// gold candidate (there is none here, the org holds only the gold set) maps to an empty key, which is
// never relevant, so it neither helps nor hurts the metrics.
ranked := make([]string, 0, len(hits))
for _, h := range hits {
ranked = append(ranked, idToKey[h.CandidateID])
}
r5 := eval.RecallAtK(ranked, relevant, k5)
r10 := eval.RecallAtK(ranked, relevant, k10)
mrr := eval.MRR(ranked, relevant)
ndcg10 := eval.NDCGAtK(ranked, relevant, k10)
t.Logf("query %-26s recall@5=%.3f recall@10=%.3f mrr=%.3f ndcg@10=%.3f (relevant=%d, hits=%d)",
q.ID, r5, r10, mrr, ndcg10, len(q.Relevant), len(hits))
sumRecall5 += r5
sumRecall10 += r10
sumMRR += mrr
sumNDCG10 += ndcg10
nQueries++
}
meanRecall5 := sumRecall5 / float64(nQueries)
meanRecall10 := sumRecall10 / float64(nQueries)
meanMRR := sumMRR / float64(nQueries)
meanNDCG10 := sumNDCG10 / float64(nQueries)
t.Logf("MEAN over %d queries (gold set v%s): recall@5=%.3f recall@10=%.3f mrr=%.3f ndcg@10=%.3f",
nQueries, fx.Version, meanRecall5, meanRecall10, meanMRR, meanNDCG10)
// Floors. Each gold query's relevant candidates are lexically present AND semantically near the query, and
// the org holds only twelve candidates, so a correct hybrid search ranks the relevant ones at or near the
// top and recall at ten is essentially complete. The floors sit well below the expected healthy values so
// a passing run is unambiguous, while a real regression (embeddings drift, HNSW ef_search cut, a broken
// mission gate, or an RRF k constant change that scrambles the fusion) drops a metric below its floor.
//
// meanRecall@10 >= 0.8: a healthy search recalls nearly every relevant id within the top ten of a twelve
// candidate pool, so 0.8 is a conservative floor that a correct search clears comfortably and a recall
// regression breaks.
if meanRecall10 < 0.8 {
t.Fatalf("REGRESSION: mean recall@10 = %.3f is below the 0.8 floor; the hybrid search is no longer recalling the relevant candidates", meanRecall10)
}
// meanMRR >= 0.5: a healthy search puts a relevant candidate in the top two on average (rank one is 1.0,
// rank two is 0.5), so 0.5 is a conservative floor on first relevant rank that a ranking regression breaks.
if meanMRR < 0.5 {
t.Fatalf("REGRESSION: mean MRR = %.3f is below the 0.5 floor; the first relevant candidate is ranking too deep", meanMRR)
}
// meanNDCG@10 >= 0.6: nDCG rewards ranking the relevant ids high, not merely present. 0.6 is a conservative
// floor that a correct ordering clears while a fusion or ranking regression that buries relevant ids breaks.
if meanNDCG10 < 0.6 {
t.Fatalf("REGRESSION: mean nDCG@10 = %.3f is below the 0.6 floor; relevant candidates are ranking too low in the fused order", meanNDCG10)
}
// recall@5 is logged as a baseline and sanity checked but not floored as tightly, since a five deep cutoff
// over a twelve candidate pool is the harder bar; it must still recall a clear majority on average.
if meanRecall5 < 0.6 {
t.Fatalf("REGRESSION: mean recall@5 = %.3f is below the 0.6 floor; the top five no longer surfaces the relevant candidates", meanRecall5)
}
// Guard the fixture itself against silent identity collisions: every seeded id resolved to a distinct key.
if len(idToKey) != len(fx.Candidates) {
t.Fatalf("seed key mapping is inconsistent: %d ids for %d candidates", len(idToKey), len(fx.Candidates))
}
}
Mathematical records from this source
010203
func TestRetrievalRecallGate(t *testing.T)
This relation defines meanRecall@10 >= 0.8: a healthy search recalls nearly every relevant id within the top ten in the source section “func TestRetrievalRecallGate(t…
func TestRetrievalRecallGate(t *testing.T)
This relation defines meanMRR >= 0.5: a healthy search puts a relevant candidate in the top two on average (rank in the source section “func TestRetrievalRecallGate(t…
func TestRetrievalRecallGate(t *testing.T)
This relation defines meanNDCG@10 >= 0.6: nDCG rewards ranking the relevant ids high, not merely present. 0.6 is in the source section “func TestRetrievalRecallGate(t…