use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankRequest {
pub query: String,
pub documents: Vec<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub top_n: Option<usize>,
#[serde(default)]
pub instruction: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankedDocument {
pub index: usize,
pub score: f32,
pub document: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankResult {
pub ranked: Vec<RerankedDocument>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_used: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimal_request_deserializes_with_defaults() {
let json = r#"{"query":"who is the president","documents":["doc a","doc b"]}"#;
let req: RerankRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.query, "who is the president");
assert_eq!(req.documents.len(), 2);
assert!(req.model.is_none());
assert!(req.top_n.is_none());
assert!(req.instruction.is_none());
}
#[test]
fn full_request_roundtrip() {
let req = RerankRequest {
query: "q".into(),
documents: vec!["d1".into(), "d2".into()],
model: Some("Qwen3-Reranker-0.6B".into()),
top_n: Some(1),
instruction: Some("retrieve passages relevant to the query".into()),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: RerankRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.query, req.query);
assert_eq!(parsed.documents, req.documents);
assert_eq!(parsed.top_n, Some(1));
assert_eq!(
parsed.instruction.as_deref(),
Some("retrieve passages relevant to the query")
);
}
#[test]
fn result_serializes_without_null_model_used() {
let result = RerankResult {
ranked: vec![RerankedDocument {
index: 0,
score: 0.97,
document: "doc".into(),
}],
model_used: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(
!json.contains("model_used"),
"None should be omitted, not null"
);
assert!(json.contains("\"index\":0"));
assert!(json.contains("\"score\":0.97"));
}
}