use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
pub id: String,
pub source_uri: String,
pub title: String,
pub hash: String,
#[serde(default)]
pub metadata: serde_json::Value,
pub created_at: String,
}
impl Document {
pub fn new(
source_uri: impl Into<String>,
title: impl Into<String>,
hash: impl Into<String>,
) -> Self {
Document {
id: new_id(),
source_uri: source_uri.into(),
title: title.into(),
hash: hash.into(),
metadata: serde_json::Value::Null,
created_at: now_rfc3339(),
}
}
pub fn with_metadata(mut self, meta: serde_json::Value) -> Self {
self.metadata = meta;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chunk {
pub id: String,
pub doc_id: String,
pub ordinal: i64,
pub text: String,
pub token_count: i64,
#[serde(default)]
pub metadata: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<Vec<f32>>,
}
impl Chunk {
pub fn new(
doc_id: impl Into<String>,
ordinal: i64,
text: impl Into<String>,
token_count: i64,
) -> Self {
Chunk {
id: new_id(),
doc_id: doc_id.into(),
ordinal,
text: text.into(),
token_count,
metadata: serde_json::Value::Null,
embedding: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Scored {
pub chunk: Chunk,
pub score: f32,
}
impl Scored {
pub fn new(chunk: Chunk, score: f32) -> Self {
Scored { chunk, score }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RetrievalMode {
Vector,
Bm25,
Hybrid,
MultiQuery,
Hyde,
}
impl RetrievalMode {
pub fn needs_llm(self) -> bool {
matches!(self, RetrievalMode::MultiQuery | RetrievalMode::Hyde)
}
pub const ALL: [RetrievalMode; 5] = [
RetrievalMode::Vector,
RetrievalMode::Bm25,
RetrievalMode::Hybrid,
RetrievalMode::MultiQuery,
RetrievalMode::Hyde,
];
pub const OFFLINE: [RetrievalMode; 3] = [
RetrievalMode::Vector,
RetrievalMode::Bm25,
RetrievalMode::Hybrid,
];
}
impl std::fmt::Display for RetrievalMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
RetrievalMode::Vector => "vector",
RetrievalMode::Bm25 => "bm25",
RetrievalMode::Hybrid => "hybrid",
RetrievalMode::MultiQuery => "multi-query",
RetrievalMode::Hyde => "hyde",
};
f.write_str(s)
}
}
impl FromStr for RetrievalMode {
type Err = crate::RagError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().replace('_', "-").as_str() {
"vector" | "dense" | "semantic" => Ok(RetrievalMode::Vector),
"bm25" | "keyword" | "sparse" => Ok(RetrievalMode::Bm25),
"hybrid" => Ok(RetrievalMode::Hybrid),
"multi-query" | "multiquery" | "fusion" => Ok(RetrievalMode::MultiQuery),
"hyde" => Ok(RetrievalMode::Hyde),
other => Err(crate::RagError::config(format!(
"unknown retrieval mode '{other}'"
))),
}
}
}
pub fn new_id() -> String {
uuid::Uuid::new_v4().to_string()
}
pub fn content_hash(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(bytes);
let digest = h.finalize();
let mut out = String::with_capacity(64);
for b in digest {
out.push_str(&format!("{b:02x}"));
}
out
}
pub fn now_rfc3339() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => format_epoch_utc(d.as_secs()),
Err(_) => "1970-01-01T00:00:00Z".to_string(),
}
}
fn format_epoch_utc(secs: u64) -> String {
let days = (secs / 86_400) as i64;
let rem = secs % 86_400;
let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60);
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}-{m:02}-{d:02}T{hh:02}:{mm:02}:{ss:02}Z")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retrieval_mode_roundtrip() {
for m in RetrievalMode::ALL {
let s = m.to_string();
assert_eq!(RetrievalMode::from_str(&s).unwrap(), m, "roundtrip {s}");
}
assert_eq!(
RetrievalMode::from_str("KEYWORD").unwrap(),
RetrievalMode::Bm25
);
assert!(RetrievalMode::from_str("nope").is_err());
}
#[test]
fn hash_is_stable_and_hex() {
let h = content_hash(b"hello world");
assert_eq!(h.len(), 64);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(h, content_hash(b"hello world"));
assert_ne!(h, content_hash(b"hello worlds"));
}
#[test]
fn epoch_formats_known_dates() {
assert_eq!(format_epoch_utc(0), "1970-01-01T00:00:00Z");
assert_eq!(format_epoch_utc(1_609_459_200), "2021-01-01T00:00:00Z");
}
}