Skip to main content

lunaris_rerank/
lib.rs

1//! lunaris-rerank — trait + NoopReranker for the v0 recall hot path (RETRIEVE-06).
2//!
3//! v0.4 N-03 cutover: this crate slimmed from "default backend host" to
4//! "trait + Noop seam." The concrete cross-encoders moved out:
5//!
6//! - `BgeRerankerV2M3` (candle) is deleted. The replacement is
7//!   `lunaris_llamacpp::LlamaCppReranker` (llama.cpp + bge-reranker-v2-m3
8//!   FP32, sigmoid output ∈ [0, 1]).
9//! - `FastembedReranker` (ORT) and the `fastembed_exec` EP helper are deleted
10//!   with the rest of the fastembed transitive surface.
11//!
12//! What stays:
13//!
14//! - [`Reranker`] trait — async, dyn-compatible. Implemented by
15//!   `NativeReranker` (default), `NativeQuantizedReranker` (Q4 GGUF), and
16//!   downstream BYO impls.
17//! - [`RerankCandidate`] DTO — input/output of `Reranker::rerank`.
18//! - [`NoopReranker`] — passthrough fallback for the RETRIEVE-06 contract.
19//!
20//! ## Cold-start contract
21//!
22//! The umbrella `Lunaris::open(url)` catches `Err` from
23//! `NativeReranker::open` (cache miss) and substitutes [`NoopReranker`],
24//! emitting `tracing::warn!` so the operator sees the degradation. Callers
25//! wire their own reranker via `Lunaris::with_reranker(reranker)`.
26//!
27//! ## Latency budget
28//!
29//! Per blueprint §4.2 the budget is 12 ms p50 / 35 ms p99 on CPU.
30
31#![deny(rust_2018_idioms, unreachable_pub)]
32#![forbid(unsafe_code)]
33
34use async_trait::async_trait;
35use lunaris_core::LunarisError;
36use serde::{Deserialize, Serialize};
37
38pub mod noop;
39
40pub use noop::NoopReranker;
41
42/// One pre-rerank candidate. The operator hydrates the chunk text BEFORE
43/// calling the reranker (see `lunaris_retrieve::hydrate::partial_hydrate_text`)
44/// so the cross-encoder can pair-encode `(query, doc.text)` for scoring.
45///
46/// `score` is the upstream operator's score — the reranker REPLACES this with
47/// its own logit so downstream `.top(n)` ranks by cross-encoder relevance.
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct RerankCandidate {
50    /// Backend-issued id (the same id bytes that came from the upstream RawHit).
51    pub id: Vec<u8>,
52    /// Chunk body — required for cross-encoder pair scoring. Empty string is
53    /// allowed (the model will produce a low score, the cull is downstream).
54    pub text: String,
55    /// Upstream operator's score. The reranker REPLACES this with its own
56    /// logit on return.
57    pub score: f32,
58    /// Free-form metadata carried from the upstream RawHit; the reranker
59    /// passes it through unchanged.
60    #[serde(default)]
61    pub metadata: serde_json::Value,
62}
63
64/// Async cross-encoder reranker.
65///
66/// Implementors MUST return exactly `docs.len()` items — preserving the input
67/// set, just re-ordered + re-scored. The retriever's `top(n)` modifier
68/// truncates downstream so the reranker doesn't need to know the user-facing k.
69///
70/// `applies()` reports whether this impl actually applies a model pass (true)
71/// or is a NO-OP passthrough (false). The DSL operator reads this to set
72/// `Hit { rerank_applied }` so callers can tell whether they got the budgeted
73/// 12 ms cross-encoder pass or the degraded path.
74#[async_trait]
75pub trait Reranker: Send + Sync + 'static {
76    /// Re-score (query, docs) pairs and return them sorted by score desc.
77    async fn rerank(
78        &self,
79        query: &str,
80        docs: Vec<RerankCandidate>,
81    ) -> Result<Vec<RerankCandidate>, LunarisError>;
82
83    /// True when this impl actually invokes a model; false for NO-OP fallbacks.
84    /// The DSL operator reads this to set `Hit { rerank_applied }`.
85    fn applies(&self) -> bool;
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use std::sync::Arc;
92
93    /// Compile-time proof the trait is dyn-compatible (object-safe). If this
94    /// stops compiling we've broken the operator wiring contract.
95    #[test]
96    fn reranker_is_dyn_compat() {
97        fn _check<T: Reranker + ?Sized>() {}
98        _check::<dyn Reranker>();
99        let _: Arc<dyn Reranker> = Arc::new(NoopReranker);
100    }
101}