use std::collections::HashMap;
pub mod fusion;
pub mod pushdown;
pub mod searcher;
pub use pushdown::FilterPushdown;
pub use searcher::{
FulltextSearchSource, HybridSearcher, StructuredSearchSource, VectorSearchSource,
};
#[derive(Debug, Clone, Copy)]
pub enum FusionStrategy {
Rrf { k: u32 },
Weighted {
vector_w: f32,
fulltext_w: f32,
structured_w: f32,
},
Cascade,
}
impl Default for FusionStrategy {
fn default() -> Self {
Self::Rrf { k: 60 }
}
}
#[derive(Debug, Clone)]
pub struct VectorQuery {
pub collection: String,
pub query_vector: Vec<f32>,
pub metric: VectorMetric,
pub filter: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VectorMetric {
Cosine,
Euclidean,
DotProduct,
}
#[derive(Debug, Clone)]
pub struct FulltextQuery {
pub index: String,
pub query_text: String,
pub fields: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct StructuredQuery {
pub table: String,
pub where_clauses: Vec<String>,
pub order_by: Option<String>,
}
#[derive(Debug, Clone)]
pub struct HybridQuery {
pub vector: Option<VectorQuery>,
pub fulltext: Option<FulltextQuery>,
pub structured: Option<StructuredQuery>,
pub strategy: FusionStrategy,
pub top_k: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchResultSource {
Vector,
Fulltext,
Structured,
Hybrid,
}
#[derive(Debug, Clone)]
pub struct HybridSearchResult {
pub id: String,
pub score: f32,
pub source: SearchResultSource,
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default)]
pub struct DegradationStatus {
pub vector_degraded: bool,
pub fulltext_degraded: bool,
pub structured_degraded: bool,
}
impl DegradationStatus {
pub fn all_degraded(&self) -> bool {
self.vector_degraded && self.fulltext_degraded && self.structured_degraded
}
pub fn any_degraded(&self) -> bool {
self.vector_degraded || self.fulltext_degraded || self.structured_degraded
}
}
#[derive(Debug, Clone)]
pub struct HybridSearchResponse {
pub results: Vec<HybridSearchResult>,
pub degradation: DegradationStatus,
pub elapsed_ms: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum HybridError {
#[error("source {source_name} timeout")]
SourceTimeout { source_name: String },
#[error("all sources failed")]
AllSourcesFailed,
#[error("vector error: {0}")]
VectorError(String),
#[error("fulltext error: {0}")]
FulltextError(String),
#[error("structured error: {0}")]
StructuredError(String),
}
#[derive(Debug, Clone)]
pub struct SourceResult {
pub id: String,
pub score: f32,
pub source: SearchResultSource,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fusion_strategy_default() {
let strategy = FusionStrategy::default();
assert!(matches!(strategy, FusionStrategy::Rrf { k: 60 }));
}
#[test]
fn test_hybrid_query_construction() {
let query = HybridQuery {
vector: Some(VectorQuery {
collection: "docs".to_string(),
query_vector: vec![1.0, 0.0, 0.0],
metric: VectorMetric::Cosine,
filter: None,
}),
fulltext: Some(FulltextQuery {
index: "docs_idx".to_string(),
query_text: "hello world".to_string(),
fields: vec!["title".to_string(), "content".to_string()],
}),
structured: None,
strategy: FusionStrategy::Rrf { k: 60 },
top_k: 10,
};
assert!(query.vector.is_some());
assert!(query.fulltext.is_some());
assert!(query.structured.is_none());
assert_eq!(query.top_k, 10);
}
#[test]
fn test_degradation_status_default() {
let status = DegradationStatus::default();
assert!(!status.vector_degraded);
assert!(!status.fulltext_degraded);
assert!(!status.structured_degraded);
assert!(!status.all_degraded());
assert!(!status.any_degraded());
}
#[test]
fn test_degradation_status_partial() {
let status = DegradationStatus {
vector_degraded: false,
fulltext_degraded: true,
structured_degraded: false,
};
assert!(status.any_degraded());
assert!(!status.all_degraded());
}
#[test]
fn test_degradation_status_all() {
let status = DegradationStatus {
vector_degraded: true,
fulltext_degraded: true,
structured_degraded: true,
};
assert!(status.all_degraded());
assert!(status.any_degraded());
}
#[test]
fn test_hybrid_error_display() {
let err = HybridError::SourceTimeout {
source_name: "elasticsearch".to_string(),
};
assert!(err.to_string().contains("timeout"));
let err = HybridError::AllSourcesFailed;
assert!(err.to_string().contains("all sources failed"));
let err = HybridError::VectorError("connection refused".to_string());
assert!(err.to_string().contains("vector error"));
}
}