Skip to main content

velesdb_memory/
rerank.rs

1//! Optional second-stage re-scoring of a [`MemoryService::recall_fused`]
2//! candidate pool, the layer that lifts a ranking miss (a relevant fact deep
3//! in the pool, below the fusion cutoff) into the final `k` — the lever
4//! validated on the LoCoMo ceiling diagnostic (multi-hop recall@8 = 50%,
5//! recall@64 = 89%: the gold fact is IN the pool, just outranked).
6//!
7//! Mirroring the [`crate::embedder`]/[`crate::extract`] pattern, the
8//! plug-point is dependency-free (bring your own cross-encoder or LLM by
9//! implementing [`Reranker`]) and never wired in by default: a reranker
10//! measurably helps ranking-bound corpora but can hurt on out-of-distribution
11//! conversational queries (the LoCoMo panel's own finding) — opt-in only,
12//! never a silent default.
13//!
14//! [`MemoryService::recall_fused`]: crate::service::MemoryService::recall_fused
15
16use crate::model::Recollection;
17
18/// Failure produced by a [`Reranker`] backend (e.g. a network-backed model
19/// that cannot be reached, or output that cannot be mapped back to
20/// candidates).
21#[derive(Debug, thiserror::Error)]
22#[non_exhaustive] // error enum, grows by nature; matching externally requires a wildcard arm
23pub enum RerankError {
24    /// The reranking backend (network, subprocess, …) returned an error.
25    #[error("rerank backend error: {0}")]
26    Backend(String),
27}
28
29/// Re-scores or reorders a fused candidate pool against `query`.
30///
31/// Implement this to plug in a cross-encoder, an LLM judge, or any other
32/// second-stage ranker, and pass it to
33/// [`MemoryService::recall_fused_reranked`](crate::service::MemoryService::recall_fused_reranked).
34/// A well-behaved implementation returns the same candidates (by id), just
35/// reordered or re-scored — it should not invent or drop ids.
36pub trait Reranker {
37    /// Re-score or reorder `candidates` for relevance to `query`.
38    ///
39    /// # Errors
40    /// Returns [`RerankError`] if the backend fails.
41    fn rerank(
42        &self,
43        query: &str,
44        candidates: Vec<Recollection>,
45    ) -> Result<Vec<Recollection>, RerankError>;
46}
47
48/// Forward [`Reranker`] through an [`Arc`](std::sync::Arc), so a shared `Arc<dyn Reranker>`
49/// (e.g. one held by the MCP server) satisfies the `R: Reranker` bound on
50/// [`crate::MemoryService::recall_fused_reranked`].
51impl<T: Reranker + ?Sized> Reranker for std::sync::Arc<T> {
52    fn rerank(
53        &self,
54        query: &str,
55        candidates: Vec<Recollection>,
56    ) -> Result<Vec<Recollection>, RerankError> {
57        (**self).rerank(query, candidates)
58    }
59}
60
61/// A boxed, object-safe reranker. Lets a non-generic caller (e.g. the Node
62/// binding, wrapping a JS callback) hold `Box<dyn Reranker + Send + Sync>`
63/// without threading a generic parameter through `MemoryService`.
64pub type DynReranker = Box<dyn Reranker + Send + Sync>;
65
66impl Reranker for DynReranker {
67    fn rerank(
68        &self,
69        query: &str,
70        candidates: Vec<Recollection>,
71    ) -> Result<Vec<Recollection>, RerankError> {
72        (**self).rerank(query, candidates)
73    }
74}