use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use super::types::Filter;
use crate::error::StorageError;
use crate::hlc::Hlc;
#[async_trait]
pub trait KeywordPort: Send + Sync + 'static {
async fn keyword_search(
&self,
scope: &crate::scope::Scope,
index: &str,
query: &str,
k: usize,
filter: Option<&Filter>,
as_of: Option<Hlc>,
) -> Result<Vec<KeywordHit>, StorageError>;
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KeywordHit {
pub id: Vec<u8>,
pub score: f32,
pub raw_score: f32,
#[serde(default)]
pub metadata: serde_json::Value,
}
impl KeywordHit {
pub fn new(id: Vec<u8>, score: f32, raw_score: f32, metadata: serde_json::Value) -> Self {
Self { id, score, raw_score, metadata }
}
}
pub fn min_max_normalize(raw: &[f32]) -> Vec<f32> {
if raw.is_empty() {
return Vec::new();
}
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for &v in raw {
if v < min {
min = v;
}
if v > max {
max = v;
}
}
let span = max - min;
if span <= f32::EPSILON {
return vec![1.0; raw.len()];
}
raw.iter().map(|&v| (v - min) / span).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn min_max_empty() {
assert!(min_max_normalize(&[]).is_empty());
}
#[test]
fn min_max_single_returns_one() {
let n = min_max_normalize(&[3.5]);
assert_eq!(n, vec![1.0]);
}
#[test]
fn min_max_tie_returns_one() {
let n = min_max_normalize(&[2.0, 2.0, 2.0]);
assert_eq!(n, vec![1.0, 1.0, 1.0]);
}
#[test]
fn min_max_normal_span() {
let n = min_max_normalize(&[1.0, 3.0, 5.0]);
assert!((n[0] - 0.0).abs() < 1e-6);
assert!((n[1] - 0.5).abs() < 1e-6);
assert!((n[2] - 1.0).abs() < 1e-6);
}
#[test]
fn keyword_hit_roundtrips_via_serde() {
let h = KeywordHit::new(b"abc".to_vec(), 0.42, 7.1, serde_json::json!({"text": "hello"}));
let s = serde_json::to_string(&h).unwrap();
let back: KeywordHit = serde_json::from_str(&s).unwrap();
assert_eq!(back.id, b"abc".to_vec());
assert!((back.score - 0.42).abs() < 1e-6);
assert!((back.raw_score - 7.1).abs() < 1e-4);
}
}