Skip to main content

car_inference/tasks/
rerank.rs

1//! Reranking request/result types.
2//!
3//! A reranker takes a query plus a candidate set of documents and
4//! returns them scored by relevance. Qwen3-Reranker is the canonical
5//! model here — it's a small generative LM fine-tuned to emit
6//! `P("yes" | query, document)` as the relevance signal, paired with
7//! the Qwen3-Embedding family for a full retrieval stack.
8//!
9//! The actual scoring logic lives in backend-specific modules; this
10//! module defines the public request/result shape used by
11//! `InferenceEngine::rerank()`.
12
13use serde::{Deserialize, Serialize};
14
15/// A reranking request.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct RerankRequest {
18    /// The search query to score candidates against.
19    pub query: String,
20    /// Candidate documents to score. Order is preserved in the result
21    /// via [`RerankedDocument::index`].
22    pub documents: Vec<String>,
23    /// Optional model override (e.g. "Qwen3-Reranker-0.6B").
24    #[serde(default)]
25    pub model: Option<String>,
26    /// If set, only the top-N highest-scoring candidates are returned.
27    /// Otherwise all candidates are returned, sorted by score desc.
28    #[serde(default)]
29    pub top_n: Option<usize>,
30    /// Optional task instruction (Qwen3-Reranker prompt prefix).
31    /// Defaults to "Given a web search query, retrieve relevant
32    /// passages that answer the query" — the Qwen upstream default.
33    #[serde(default)]
34    pub instruction: Option<String>,
35}
36
37/// One scored candidate returned from a rerank call.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct RerankedDocument {
40    /// Original position of this document in [`RerankRequest::documents`].
41    pub index: usize,
42    /// Relevance score in `[0.0, 1.0]`. Higher = more relevant.
43    pub score: f32,
44    /// The document text. Included so callers can use the result
45    /// directly without cross-referencing against the request.
46    pub document: String,
47}
48
49/// Reranking result — candidates sorted by descending relevance.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct RerankResult {
52    /// Scored candidates, best first. Length is `min(documents.len(), top_n)`
53    /// when `top_n` is set.
54    pub ranked: Vec<RerankedDocument>,
55    /// Name of the model that produced these scores, when known.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub model_used: Option<String>,
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn minimal_request_deserializes_with_defaults() {
66        let json = r#"{"query":"who is the president","documents":["doc a","doc b"]}"#;
67        let req: RerankRequest = serde_json::from_str(json).unwrap();
68        assert_eq!(req.query, "who is the president");
69        assert_eq!(req.documents.len(), 2);
70        assert!(req.model.is_none());
71        assert!(req.top_n.is_none());
72        assert!(req.instruction.is_none());
73    }
74
75    #[test]
76    fn full_request_roundtrip() {
77        let req = RerankRequest {
78            query: "q".into(),
79            documents: vec!["d1".into(), "d2".into()],
80            model: Some("Qwen3-Reranker-0.6B".into()),
81            top_n: Some(1),
82            instruction: Some("retrieve passages relevant to the query".into()),
83        };
84        let json = serde_json::to_string(&req).unwrap();
85        let parsed: RerankRequest = serde_json::from_str(&json).unwrap();
86        assert_eq!(parsed.query, req.query);
87        assert_eq!(parsed.documents, req.documents);
88        assert_eq!(parsed.top_n, Some(1));
89        assert_eq!(
90            parsed.instruction.as_deref(),
91            Some("retrieve passages relevant to the query")
92        );
93    }
94
95    #[test]
96    fn result_serializes_without_null_model_used() {
97        let result = RerankResult {
98            ranked: vec![RerankedDocument {
99                index: 0,
100                score: 0.97,
101                document: "doc".into(),
102            }],
103            model_used: None,
104        };
105        let json = serde_json::to_string(&result).unwrap();
106        assert!(
107            !json.contains("model_used"),
108            "None should be omitted, not null"
109        );
110        assert!(json.contains("\"index\":0"));
111        assert!(json.contains("\"score\":0.97"));
112    }
113}