/** * ColBERT/PLAID-style late-interaction reranker (issue #29). * * Phase 1 of the multi-vector evaluation. Wires a NEW optional post-step * on top of existing top-50 retrieval — does NOT change the index, does * NOT change indexing cost, does NOT change the on-disk schema. Just * reorders the top-K results using late-interaction scoring. * * Architecture: * * query → hybrid retrieval (BM25 + dense + PageRank, RRF-fused) * → top-50 candidates * → [optional] late-interaction rerank * → top-10 final * * Gated behind SVERKLO_RERANK env var. Off by default. Once we have * bench numbers proving lift, we'll either ship it as the default * behavior or close issue #29 with a "doesn't pay" writeup. * * Late-interaction scoring (MaxSim): * * score(query, doc) = sum_i max_j (q_i · d_j) * * where q_i are query token vectors and d_j are doc token vectors. * The aggregation is "for each query token, find its best-matching * doc token and sum those maxima." Preserves token-level alignment * that single-vector mean-pooled embeddings destroy. * * Three implementation options, in order of effort: * * 1. **Poor-man's late interaction (quick spike).** Use the current * all-MiniLM-L6-v2 model in token-vector mode (skip mean pooling, * keep all 50-200 token outputs per chunk). Cheaper to integrate; * lower-quality than a real ColBERT model but proves the wiring * and lets us see if ANY late interaction helps. * * 2. **ColBERT v2 ONNX (real Phase 1).** Convert colbert-ir/colbertv2.0 * to ONNX, register CoreML execution provider for ANE acceleration * on M-series Macs. Real benchmark target. * * 3. **Code-tuned ColBERT.** If a code-domain ColBERT model exists * (lightonai/colbert-code or similar), prefer it. Otherwise stick * with general ColBERT v2 and accept some out-of-domain noise. * * The poor-man's path is fastest to ship and gives a fast feasibility * answer. If poor-man's late interaction lifts P1 at all, real ColBERT * will lift it more. */ import type { SearchResult } from "../types/index.js"; export interface RerankerConfig { /** Which late-interaction backend. */ mode: "off" | "poor-man" | "colbert-v2" | "colbert-code"; /** Top-K to keep after rerank. */ topK: number; /** Top-N from initial retrieval to feed into rerank. */ candidatePool: number; } export declare const DEFAULT_RERANKER_CONFIG: RerankerConfig; /** * Read reranker config from env. Off by default; opt in via * `SVERKLO_RERANK=poor-man` (or other supported modes). Designed so * the bench runner can A/B test by setting the env var per-baseline. */ export declare function rerankerConfigFromEnv(): RerankerConfig; export declare function rerank(query: string, candidates: SearchResult[], config?: RerankerConfig): Promise; /** * Late-interaction MaxSim score over array-of-arrays inputs. * * score = sum over query tokens i of (max over doc tokens j of (q_i · d_j)) * * Both inputs are L2-normalized; returns scalar (higher is better). * * This signature is preserved as a thin adapter over the flat-array * fast path (`maxSimScoreFlat`) so existing tests don't have to * change. The flat layout is what makes the inner dot-product loop * fast — V8 inlines `Float32Array[offset+i] * Float32Array[offset+i]` * aggressively while array-of-arrays loses bounds-check elision. */ export declare function maxSimScore(queryTokens: Float32Array[], docTokens: Float32Array[]): number; /** * Flat-array MaxSim, used directly by rerank() and adapted from above * for the array-of-arrays path. Manual 4-wide unrolling on the inner * dot-product is the single biggest perf lever (V8 elides bounds * checks inside straight-line typed-array indexing). * * Both query and doc tokens must use the same `dim`. The two `dim` * parameters exist so a future heterogeneous-model path doesn't have * to change this signature, but we throw on mismatch today. */ export declare function maxSimScoreFlat(queryFlat: Float32Array, queryLen: number, queryDim: number, docFlat: Float32Array, docLen: number, docDim: number): number;