use super::{ConceptMetadata, ObjectKey};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchHit {
pub object_key: ObjectKey,
pub byte_start: u64,
pub byte_end: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub heading_path: Vec<String>,
pub score: f32,
pub preview: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub okf: Option<ConceptMetadata>,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_hit() -> SearchHit {
SearchHit {
object_key: ObjectKey::try_new("docs/rfc/7231.md").unwrap(),
byte_start: 1024,
byte_end: 2048,
heading_path: vec!["Section 1".into(), "Subsection 1.2".into()],
score: 0.0163_f32,
preview: "RFC 7231 defines HTTP semantics.".into(),
okf: None,
}
}
#[test]
fn serde_round_trip() {
let hit = make_hit();
let json = serde_json::to_string(&hit).unwrap();
let back: SearchHit = serde_json::from_str(&json).unwrap();
assert_eq!(back.object_key, hit.object_key);
assert_eq!(back.byte_start, hit.byte_start);
assert_eq!(back.byte_end, hit.byte_end);
assert_eq!(back.heading_path, hit.heading_path);
assert!((back.score - hit.score).abs() < 1e-6);
assert_eq!(back.preview, hit.preview);
}
#[test]
fn empty_heading_path_omitted_in_json() {
let hit = SearchHit {
object_key: ObjectKey::try_new("notes/a.md").unwrap(),
byte_start: 0,
byte_end: 100,
heading_path: vec![],
score: 0.5,
preview: "preview text".into(),
okf: None,
};
let json = serde_json::to_string(&hit).unwrap();
assert!(!json.contains("heading_path"));
assert!(!json.contains("okf"));
}
#[test]
fn missing_heading_path_deserializes_as_empty() {
let json = r#"{"object_key":"notes/a.md","byte_start":0,"byte_end":100,"score":0.5,"preview":"hello"}"#;
let hit: SearchHit = serde_json::from_str(json).unwrap();
assert!(hit.heading_path.is_empty());
assert!(hit.okf.is_none());
}
#[test]
fn emoji_in_preview_survives_round_trip() {
let hit = SearchHit {
object_key: ObjectKey::try_new("notes/b.md").unwrap(),
byte_start: 0,
byte_end: 50,
heading_path: vec![],
score: 0.1,
preview: "Hello 🚀 World".into(),
okf: None,
};
let json = serde_json::to_string(&hit).unwrap();
let back: SearchHit = serde_json::from_str(&json).unwrap();
assert_eq!(back.preview, "Hello 🚀 World");
}
#[test]
fn score_preserved() {
let hit = make_hit();
let json = serde_json::to_string(&hit).unwrap();
let back: SearchHit = serde_json::from_str(&json).unwrap();
assert!((back.score - 0.0163_f32).abs() < 1e-6);
}
#[test]
fn send_sync_clone_debug() {
fn assert<T: Send + Sync + Clone + std::fmt::Debug>() {}
assert::<SearchHit>();
}
}