Skip to content
Why did we open-source our inference engine? Read the post

Custom Evals

A few hundred labeled queries from your own domain settle model choice faster than any leaderboard. The pattern below is the same one the shipped retrieval-ablation benchmark runs at larger scale: encode, rank, optionally rerank, score.

Three pieces: a corpus, queries, and relevance judgments (qrels).

corpus = [
{"id": "doc1", "text": "Machine learning uses algorithms to learn from data."},
{"id": "doc2", "text": "The weather forecast predicts rain tomorrow."},
]
queries = [
{"id": "q1", "text": "What is machine learning?"},
]
qrels = {
"q1": {"doc1": 2}, # higher grade = more relevant
}

Any graded scale works; the retrieval-ablation dataset uses 1 for relevant and 2 for highly relevant. Pull queries from real logs where you can, and have someone who knows the domain judge relevance. Unjudged documents count as not relevant.

import numpy as np
from sie_sdk import SIEClient
client = SIEClient("http://your-sie-endpoint:8080")
MODEL = "BAAI/bge-m3"
CANDIDATES = 50 # rerank pool size; keep this larger than the final cutoff
doc_vecs = np.stack([r["dense"] for r in client.encode(MODEL, corpus)])
doc_vecs /= np.linalg.norm(doc_vecs, axis=1, keepdims=True)
rankings = {}
for query in queries:
q = client.encode(MODEL, {"text": query["text"]}, is_query=True)
q_vec = q["dense"] / np.linalg.norm(q["dense"])
order = np.argsort(-(doc_vecs @ q_vec))
rankings[query["id"]] = [corpus[i]["id"] for i in order[:CANDIDATES]]

encode accepts a list and returns results in the same order; chunk large corpora into batches. Query-side encoding (is_query=True in Python, isQuery: true in TypeScript) matters for models with asymmetric encoding such as BGE and E5.

Cross-encoder reranking won every condition in the shipped benchmark, and reranker size was the single biggest quality lever (+14.5% NDCG@10 for the large reranker over the base). Pool recall was the bottleneck: the same reranker scored 0.600 NDCG@10 on an 89-candidate pool and 0.621 on a 151-candidate pool (per its RESULTS.md). So retrieve more candidates than you intend to keep, then let the reranker order them:

K = 10 # final cutoff for the metrics below
by_id = {c["id"]: c for c in corpus}
for query in queries:
pool = [by_id[doc_id] for doc_id in rankings[query["id"]]]
result = client.score(
"mixedbread-ai/mxbai-rerank-large-v2",
query={"text": query["text"]},
items=pool,
)
rankings[query["id"]] = [s["item_id"] for s in result["scores"]][:K]

Scores come back sorted by relevance, most relevant first, with each item’s id echoed.

NDCG and recall are a few lines of plain Python. These match the metric functions the retrieval-ablation benchmark ships:

import math
def ndcg_at_k(ranked_ids, rels, k=10):
dcg = sum(rels.get(d, 0) / math.log2(i + 2) for i, d in enumerate(ranked_ids[:k]))
ideal = sorted(rels.values(), reverse=True)[:k]
idcg = sum(r / math.log2(i + 2) for i, r in enumerate(ideal))
return dcg / idcg if idcg > 0 else 0.0
def recall_at_k(ranked_ids, rels, k=10):
relevant = {d for d, r in rels.items() if r > 0}
return len(relevant & set(ranked_ids[:k])) / len(relevant) if relevant else 0.0
ndcg = sum(ndcg_at_k(rankings[qid], qrels[qid]) for qid in qrels) / len(qrels)

To compare models, swap MODEL and rerun. Keep the corpus, queries, and qrels frozen while you iterate; a moving eval set cannot tell you whether the model improved.

For a full multi-strategy comparison template (BM25, dense, hybrid fusion, multi-vector, cross-encoder rerank), start from examples/retrieval-ablation and swap in your own dataset.

Contact us

Tell us about your use case and we'll get back to you shortly.