"KB BGE-rerank-v2-m3 cross-encoder — reranks top-K results from TASK-KB-004 lexical + TASK-KB-005 semantic to final ordering"
§1 — Description (BCP-14 normative)
The KB service MUST ship reranker at services/kb/src/rerank/ using BGE-rerank-v2-m3 cross-encoder over TASK-KB-004/005 candidates, hybrid merge, 5min cache, 3 memory audit kinds.
- MUST validate
rerank_sourceagainst closed enum per DEC-1931.
- MUST call BGE-rerank-v2-m3 at
bge_rerank_client.rs::rerank(query, candidates) → Vec<(chunk, score)>per DEC-1930.
- MUST merge hybrid per DEC-1932 at
hybrid_merger.rs::merge(lexical_results, semantic_results):
- Dedup by chunk_id (semantic) or doc_id (lexical).
- Combine top-20 from each → up to 40 candidates.
- Rerank → return top-10.
- MUST cache per DEC-1933: ``
sql CREATE TABLE kb_rerank_cache ( cache_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, query_hash CHAR(64) NOT NULL, -- SHA256(query) source TEXT NOT NULL CHECK (source IN ('lexical_only','semantic_only','hybrid_lexical_semantic','manual_curation')), results_jsonb JSONB NOT NULL, cached_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL, UNIQUE (tenant_id, query_hash, source) ); CREATE INDEX rerank_cache_expiry_idx ON kb_rerank_cache(expires_at); ALTER TABLE kb_rerank_cache ENABLE ROW LEVEL SECURITY; CREATE POLICY rerank_cache_rls ON kb_rerank_cache USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); GRANT DELETE ON kb_rerank_cache TO cyberos_app;``
- MUST expose endpoint: ``
text POST /v1/kb/search/rerank body: {query, source: hybrid_lexical_semantic|lexical_only|semantic_only}``
- MUST emit 3 memory audit kinds per DEC-1934. PII per TASK-MEMORY-111: query SHA256 in chain; results count ok.
- MUST thread trace_id from query → rerank → cache → audit.
- MUST NOT return >10 results per DEC-1932.
- MUST NOT bypass cache per DEC-1933 (perf invariant — same query+source within 5min must hit cache).
- MUST evict cache rows past
expires_atvia TASK-MCP-007 nightly cron.
§2 — Why this design
Why cross-encoder (DEC-1930)? Cross-encoders score query+candidate jointly; far more accurate than dual-encoder similarity.
Why hybrid (DEC-1932)? Lexical catches exact keywords; semantic catches paraphrase. Union maximises recall.
Why 5min cache (DEC-1933)? Cross-encoder calls are expensive (~200ms); same-query repeats common (user iterates on search).
Why top-10 result (DEC-1932)? UX: users scan first 10; longer lists ignored. Cross-encoder cost justifies cap.
§3 — API contract
POST /v1/kb/search/rerank
Sample request:
{
"query": "how do I emit hóa đơn",
"source": "hybrid_lexical_semantic"
}
Sample response:
{
"results": [
{
"chunk_id": "uuid",
"doc_id": "uuid",
"doc_title": "Quy trình xuất hóa đơn",
"snippet": "...",
"rerank_score": 0.94,
"source": "hybrid_lexical_semantic"
}
],
"from_cache": false,
"rerank_duration_ms": 180
}
§4 — Acceptance criteria
- rerank_source enum cardinality 4. 2. BGE-rerank-v2-m3 cross-encoder. 3. Hybrid: lexical+semantic top-20 each, rerank top-40, return top-10. 4. Lexical-only mode. 5. Semantic-only mode. 6. Manual curation passthrough. 7. 5min cache TTL. 8. Cache hit returns from_cache=true. 9. 3 memory audit kinds emitted. 10. PII scrubbed (query SHA256). 11. RLS denies cross-tenant. 12. Trace_id preserved. 13. Cache expiry cron. 14. UNIQUE(tenant_id, query_hash, source). 15. Append-only via REVOKE except DELETE. 16. Rerank duration < 300ms p95. 17. Dedup by chunk_id + doc_id. 18. Empty candidates returns empty. 19. AI-020 service down → fallback to candidate order + sev-2 audit. 20. Score in 0-1 range.
§5 — Verification
#[tokio::test]
async fn hybrid_rerank_returns_top_10() {
let ctx = TestContext::with_50_indexed_docs().await;
let r = ctx.rerank("test", "hybrid_lexical_semantic").await;
assert_eq!(r.results.len(), 10);
}
#[tokio::test]
async fn cache_hit_returns_from_cache() {
let ctx = TestContext::with_docs().await;
let r1 = ctx.rerank("query", "hybrid_lexical_semantic").await;
assert_eq!(r1.from_cache, false);
let r2 = ctx.rerank("query", "hybrid_lexical_semantic").await;
assert_eq!(r2.from_cache, true);
}
#[tokio::test]
async fn dedup_across_sources() {
let ctx = TestContext::with_doc_matching_both().await;
let r = ctx.rerank("term", "hybrid_lexical_semantic").await;
let doc_ids: HashSet<_> = r.results.iter().map(|x| x.doc_id).collect();
assert_eq!(doc_ids.len(), r.results.len());
}
// 5.4..5.10
§7 — Dependencies
Upstream: TASK-AI-020 (BGE-rerank-v2-m3 service), TASK-KB-005. Downstream: TASK-KB-007 (Ask this page Q&A). Cross-module: TASK-KB-004 (lexical input), TASK-MCP-007 (cache eviction cron), TASK-MEMORY-111 (PII).
§10 — Failure modes
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| BGE-rerank service down | catch | fallback candidate order + sev-2 | retry |
| Cache table corruption | DELETE + re-rerank | inherent | inherent |
| Query > 500 chars | validate | 400 | shorten |
| Empty candidates | inherent | [] | inherent |
| Score outside 0-1 | clamp | inherent | inherent |
| Cross-tenant cache | RLS | 0 rows | inherent |
| Expired cache served | expires_at check | re-rerank | inherent |
| Concurrent rerank same query | UNIQUE | first wins | inherent |
| Manual curation list invalid | validate | reject 400 | fix list |
| Result enrichment fail (chunk → doc) | catch | sev-3 + skip | data fix |
§11 — Implementation notes
- §11.1 BGE-rerank model hosted via TASK-AI-020 inference service.
- §11.2 Hybrid merge: round-robin interleave lexical[0], semantic[0], lexical[1], semantic[1]... then dedup.
- §11.3 Cache key: SHA256(query) + source enum; per-tenant scope.
- §11.4 memory audit body: tenant_id, source, candidate_count, returned_count; query SHA256.
- §11.5 Cache eviction cron: hourly DELETE WHERE expires_at < now().
End of TASK-KB-006 spec.