Skip to content

Eval gate — tune floor and exponents

Ranking and gate thresholds are empirical. The values shipped today are placeholders:

  • FASHION_CONFIDENCE_FLOOR = 0.5 in @samesake/presets (fashion.indexing().gate)
  • relevanceExponent = 1 (default in rankingPolicy / core ranking fusion)

Do not change these without running the harness. Fabricating “calibrated” numbers defeats the purpose of the eval loop.

examples/fashion-search/eval-judge.ts is the runnable CI gate. It:

  1. Loads the frozen golden set evals/golden-queries-fashion-lk.json (50 queries with objective constraints).
  2. Calls matcher.runEval(...) with a graded ESCI LLM judge (makeLlmJudge) — a different model family than the enrich pipeline (runEval rejects a same-family or undeclared judge on enriched collections).
  3. Aggregates Hit@K, nDCG@K, MRR, null-rate, and constraint-violation rate.
  4. Sets pass from thresholds — exits 0 when all metrics meet thresholds, nonzero when any regress.
import { createMatcher, makeLlmJudge } from "@samesake/server";
const matcher = createMatcher({
databaseUrl: process.env.SAMESAKE_DATABASE_URL!,
apiKey: process.env.SAMESAKE_API_KEY!,
embed: geminiEmbed,
generate: geminiGenerate,
});
const res = await matcher.runEval(PROJECT, COLLECTION, {
queries: golden.queries,
judge: makeLlmJudge(openaiGenerate, { model: "gpt-4.1-mini" }), // vs a Gemini pipeline
k: 10,
relevanceFloor: 2, // ESCI: Substitute or better counts as relevant
thresholds: { ndcgAtK: 0.6, nullRate: 0.1, constraintViolationRate: 0 },
});
process.exit(res.pass ? 0 : 1);

runEval writes a JSON artifact under evals/runs/ and caches judge calls under evals/.cache/ (keyed by judge version + query + candidate text).

When GEMINI_API_KEY or OPENAI_API_KEY is unset, the script dry-runs cleanly (exit 0): it prints how many queries would run and does not call the judge. Use this in CI type-check paths; live gating requires both keys (Gemini pipeline + cross-family OpenAI judge).

Terminal window
# Dry-run (no key) — verifies the script loads; exit 0
bun examples/fashion-search/eval-judge.ts
# Live gate (key required) — exit 0 only when pass=true
GEMINI_API_KEY=... bun examples/fashion-search/eval-judge.ts

Procedure — derive G2 floor and G7 exponents

Section titled “Procedure — derive G2 floor and G7 exponents”
  1. Calibrate the judge (once per prompt change). Run calibrateJudge (from @samesake/server) / calibrate.ts against a labeled fixture so judge-vs-human F1 meets your trust bar. Rubric edits invalidate cached grades automatically (the judge version embeds a content hash of the prompt — esci-v1@<hash>); pass a new version tag only for model or methodology changes. See Relevance judge for the judge API, prompt, and schema.

  2. Baseline the current placeholders. With GEMINI_API_KEY set, run the gate and save the artifact:

    Terminal window
    GEMINI_API_KEY=... bun examples/fashion-search/eval-judge.ts
    # note aggregate ndcgAtK, nullRate, constraintViolationRate; artifact path printed
  3. Sweep candidate configs. For each candidate:

    • G2 floor: change FASHION_CONFIDENCE_FLOOR (or override the gate locally), re-enrich if the gate affects quarantine, re-index, re-run eval.
    • G7 exponents: change search.rankingPolicy.relevanceExponent (and optional axis weights), re-run eval (no reindex unless ranking inputs change).

    Compare aggregate.ndcgAtK, aggregate.nullRate, and aggregate.constraintViolationRate across the sweep. Prefer the config that maximizes nDCG@K without regressing null-rate or constraint-violation rate below your thresholds.

  4. Commit the winner. Update the placeholder constant / default in code only after the harness shows improvement on the golden set. Document the artifact path and thresholds in the commit message.

  5. Wire CI. Run eval-judge.ts in CI with GEMINI_API_KEY; fail the build when pass is false. Keep thresholds aligned with product requirements (the example uses ndcgAtK: 0.6, nullRate: 0.1, constraintViolationRate: 0).

KnobLocationRe-index / re-enrich?
Confidence floor (G2)FASHION_CONFIDENCE_FLOOR / fashion.indexing().gateRe-enrich + re-index (gate changes quarantine)
Relevance exponent (G7)search.rankingPolicy.relevanceExponentRe-run eval only
Axis weights (G7)rankingPolicy.weights.*Re-run eval only
Rerank blendsamesake({ rerank: llmRerank(generate) })Re-run eval only

Core ranking uses multiplicative fusion: normalized relevance raised to relevanceExponent, multiplied by availability / business / personalization axes. Hard axes (e.g. availability) apply multiplicatively; soft axes add after the product. A minRelevanceFloor drops hits below a relevance cutoff before fusion.