use std::collections::HashSet;
use serde::Serialize;
use super::types::{CacheStatus, QueryResponse, SearchResult};
use crate::config::FederatedConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ResultSource {
Local,
Remote,
}
#[derive(Debug, Clone, Serialize)]
pub struct FederatedResult {
#[serde(flatten)]
pub result: SearchResult,
pub source: ResultSource,
}
impl FederatedResult {
pub fn local(result: SearchResult) -> Self {
Self {
result,
source: ResultSource::Local,
}
}
pub fn remote(result: SearchResult) -> Self {
Self {
result,
source: ResultSource::Remote,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MergeMode {
#[default]
LocalOnlyFallback,
LocalPriority,
RemotePriority,
Interleave,
}
impl MergeMode {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"local_only_fallback" | "localonly" | "fallback" => MergeMode::LocalOnlyFallback,
"local_priority" | "local" => MergeMode::LocalPriority,
"remote_priority" | "remote" => MergeMode::RemotePriority,
"interleave" | "blend" | "merge" => MergeMode::Interleave,
_ => MergeMode::LocalOnlyFallback,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FallbackDecision {
LocalSufficient,
EmptyLocal,
BelowMinResults,
LowConfidence,
AlwaysRemote,
}
#[derive(Debug, Clone)]
pub struct FederatedSearchConfig {
pub enabled: bool,
pub min_local_results: usize,
pub min_local_confidence: f32,
pub fallback_on_empty: bool,
pub fallback_on_low_confidence: bool,
pub merge_mode: MergeMode,
pub max_merged_results: usize,
}
impl Default for FederatedSearchConfig {
fn default() -> Self {
Self {
enabled: false,
min_local_results: 3,
min_local_confidence: 0.7,
fallback_on_empty: true,
fallback_on_low_confidence: true,
merge_mode: MergeMode::LocalOnlyFallback,
max_merged_results: 10,
}
}
}
impl From<&FederatedConfig> for FederatedSearchConfig {
fn from(config: &FederatedConfig) -> Self {
Self {
enabled: config.enabled(),
min_local_results: config.min_local_results(),
min_local_confidence: config.min_local_confidence(),
fallback_on_empty: config.fallback_on_empty.unwrap_or(true),
fallback_on_low_confidence: config.fallback_on_low_confidence.unwrap_or(true),
merge_mode: MergeMode::parse(config.merge_mode()),
max_merged_results: config.max_merged_results.unwrap_or(10),
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct FederatedStats {
pub local_count: usize,
pub remote_count: usize,
pub merged_count: usize,
pub duplicates_removed: usize,
pub remote_queried: bool,
pub fallback_reason: Option<String>,
pub local_latency_ms: u64,
pub remote_latency_ms: Option<u64>,
}
pub struct FederatedSearch {
config: FederatedSearchConfig,
}
impl FederatedSearch {
pub fn new(config: FederatedSearchConfig) -> Self {
Self { config }
}
pub fn from_config(config: &FederatedConfig) -> Self {
Self::new(FederatedSearchConfig::from(config))
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn should_query_remote(&self, local_results: &[SearchResult]) -> FallbackDecision {
if matches!(
self.config.merge_mode,
MergeMode::Interleave | MergeMode::RemotePriority
) {
return FallbackDecision::AlwaysRemote;
}
if local_results.is_empty() {
if self.config.fallback_on_empty {
return FallbackDecision::EmptyLocal;
}
return FallbackDecision::LocalSufficient;
}
if local_results.len() < self.config.min_local_results {
return FallbackDecision::BelowMinResults;
}
if self.config.fallback_on_low_confidence {
let max_score = local_results.iter().map(|r| r.score).fold(0.0f32, f32::max);
if max_score < self.config.min_local_confidence {
return FallbackDecision::LowConfidence;
}
}
FallbackDecision::LocalSufficient
}
pub fn merge(
&self,
local_results: Vec<SearchResult>,
remote_results: Vec<SearchResult>,
) -> (Vec<FederatedResult>, FederatedStats) {
let local_count = local_results.len();
let remote_count = remote_results.len();
let (merged, duplicates_removed) = match self.config.merge_mode {
MergeMode::LocalOnlyFallback => {
if local_results.is_empty() {
(
remote_results
.into_iter()
.map(FederatedResult::remote)
.collect(),
0,
)
} else {
(
local_results
.into_iter()
.map(FederatedResult::local)
.collect(),
0,
)
}
}
MergeMode::LocalPriority => self.merge_priority(local_results, remote_results, true),
MergeMode::RemotePriority => self.merge_priority(local_results, remote_results, false),
MergeMode::Interleave => self.merge_interleave(local_results, remote_results),
};
let merged: Vec<_> = merged
.into_iter()
.take(self.config.max_merged_results)
.collect();
let stats = FederatedStats {
local_count,
remote_count,
merged_count: merged.len(),
duplicates_removed,
remote_queried: remote_count > 0,
fallback_reason: None,
local_latency_ms: 0,
remote_latency_ms: None,
};
(merged, stats)
}
fn merge_priority(
&self,
local_results: Vec<SearchResult>,
remote_results: Vec<SearchResult>,
local_first: bool,
) -> (Vec<FederatedResult>, usize) {
let mut seen_ids: HashSet<String> = HashSet::new();
let mut merged = Vec::new();
let (primary, secondary) = if local_first {
(local_results, remote_results)
} else {
(remote_results, local_results)
};
let primary_source = if local_first {
ResultSource::Local
} else {
ResultSource::Remote
};
let secondary_source = if local_first {
ResultSource::Remote
} else {
ResultSource::Local
};
for result in primary {
seen_ids.insert(result.id.clone());
merged.push(FederatedResult {
result,
source: primary_source,
});
}
let mut duplicates: usize = 0;
for result in secondary {
if !seen_ids.contains(&result.id) {
seen_ids.insert(result.id.clone());
merged.push(FederatedResult {
result,
source: secondary_source,
});
} else {
duplicates = duplicates.saturating_add(1);
}
}
(merged, duplicates)
}
fn merge_interleave(
&self,
local_results: Vec<SearchResult>,
remote_results: Vec<SearchResult>,
) -> (Vec<FederatedResult>, usize) {
let mut all: Vec<FederatedResult> = Vec::new();
let mut seen_ids: HashSet<String> = HashSet::new();
let mut duplicates: usize = 0;
for result in local_results {
all.push(FederatedResult::local(result));
}
for result in remote_results {
all.push(FederatedResult::remote(result));
}
all.sort_by(|a, b| {
b.result
.score
.partial_cmp(&a.result.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut deduped = Vec::new();
for item in all {
if !seen_ids.contains(&item.result.id) {
seen_ids.insert(item.result.id.clone());
deduped.push(item);
} else {
duplicates = duplicates.saturating_add(1);
}
}
(deduped, duplicates)
}
pub fn merge_responses(
&self,
local_response: QueryResponse,
remote_response: Option<QueryResponse>,
) -> (QueryResponse, FederatedStats) {
let local_latency = local_response.took_ms;
let remote_latency = remote_response.as_ref().map(|r| r.took_ms);
let remote_results = remote_response.map(|r| r.results).unwrap_or_default();
let (merged, mut stats) = self.merge(local_response.results, remote_results);
stats.local_latency_ms = local_latency;
stats.remote_latency_ms = remote_latency;
let response = QueryResponse {
results: merged.into_iter().map(|f| f.result).collect(),
cache_status: CacheStatus::Miss, took_ms: local_latency.saturating_add(remote_latency.unwrap_or(0)),
generated_at: None,
miss_reason: None,
};
(response, stats)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FederatedResponse {
pub results: Vec<FederatedResult>,
pub cache_status: CacheStatus,
pub took_ms: u64,
pub stats: FederatedStats,
}
#[cfg(test)]
#[path = "tests/federated_tests.rs"]
mod tests;