use std::collections::HashMap;
use lunaris_core::LunarisError;
use lunaris_core::storage::keyword::min_max_normalize;
use lunaris_core::storage::types::Filter;
use crate::operators::QueryContext;
use crate::operators::Retriever;
use crate::operators::combinators::AndRetriever;
use crate::operators::graph::Graph;
use crate::operators::keyword::Keyword;
use crate::operators::vector::Vector;
use crate::types::{RawHit, SourceOp};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FusedKind {
VectorKeywordSameIndex,
Other,
}
#[derive(Clone, Debug)]
pub struct FusedBranchHint {
pub kind: FusedKind,
pub index: String,
pub k: usize,
}
pub fn inspect_branches(inner: &dyn Retriever) -> Option<FusedBranchHint> {
let and = inner.as_any().downcast_ref::<AndRetriever>()?;
let left_is_graph = and.left.as_any().downcast_ref::<Graph>().is_some();
let right_is_graph = and.right.as_any().downcast_ref::<Graph>().is_some();
if left_is_graph || right_is_graph {
return Some(FusedBranchHint { kind: FusedKind::Other, index: String::new(), k: 0 });
}
let left_v = and.left.as_any().downcast_ref::<Vector>();
let right_v = and.right.as_any().downcast_ref::<Vector>();
let left_k = and.left.as_any().downcast_ref::<Keyword>();
let right_k = and.right.as_any().downcast_ref::<Keyword>();
let vector_keyword = match (left_v, right_k, left_k, right_v) {
(Some(v), Some(k), _, _) => Some((v, k)),
(_, _, Some(k), Some(v)) => Some((v, k)),
_ => None,
};
match vector_keyword {
Some((v, k)) if v.index == k.index => Some(FusedBranchHint {
kind: FusedKind::VectorKeywordSameIndex,
index: v.index.clone(),
k: v.k.max(k.k),
}),
Some((v, k)) => Some(FusedBranchHint {
kind: FusedKind::Other,
index: v.index.clone(),
k: v.k.max(k.k),
}),
None => Some(FusedBranchHint { kind: FusedKind::Other, index: String::new(), k: 0 }),
}
}
pub async fn fuse_via_moon_native(
ctx: &QueryContext,
hint: &FusedBranchHint,
k: usize,
branch_weights: &HashMap<SourceOp, f32>,
) -> Result<Vec<RawHit>, LunarisError> {
let moon = ctx.moon_storage.as_ref().ok_or_else(|| {
LunarisError::Storage(lunaris_core::StorageError::Backend(
"fuse_via_moon_native called without ctx.moon_storage; check builder wiring"
.to_string(),
))
})?;
let q_emb = ctx.embed_once().await?;
let typed = moon.client().typed();
let mut text = typed.text();
let weights: [f64; 3] = if branch_weights.is_empty() {
[0.5_f64, 0.5_f64, 0.0_f64]
} else {
let w_bm25 = branch_weights.get(&SourceOp::Keyword).copied().unwrap_or(0.5_f32) as f64;
let w_dense = branch_weights.get(&SourceOp::Vector).copied().unwrap_or(0.5_f32) as f64;
[w_bm25, w_dense, 0.0_f64]
};
let moon_filter = filter_to_moon_hybrid_filter(&ctx.query.filter);
let per_scope_index = format!("lunaris_{}_{}_idx", ctx.scope.as_str(), hint.index);
let hits: Vec<moon::TextSearchHit> = crate::missing_index::no_rows_if_index_absent(
text.hybrid_search(
&per_scope_index,
&ctx.query.text,
&q_emb,
"vec",
None,
k,
weights,
moon_filter.as_ref(),
)
.await
.map_err(|e| lunaris_core::StorageError::Backend(format!("moon hybrid_search: {e}"))),
)
.map_err(LunarisError::Storage)?;
let raw_scores: Vec<f32> = hits
.iter()
.map(|h| {
h.fields
.get("__rrf_score")
.and_then(|s| s.parse::<f32>().ok())
.unwrap_or(h.score as f32)
})
.collect();
let normalized = min_max_normalize(&raw_scores);
Ok(hits
.into_iter()
.zip(normalized)
.filter_map(|(h, score)| {
let raw_key = h.key.into_bytes();
let id = decode_moon_vector_key(&raw_key, &per_scope_index)?;
let metadata = parse_moon_metadata(&h.fields);
Some(RawHit {
id,
score,
rerank_applied: false,
degraded: false,
metadata,
source_op: SourceOp::Fused,
})
})
.collect())
}
fn parse_moon_metadata(fields: &HashMap<String, String>) -> serde_json::Value {
fields
.get("__metadata")
.or_else(|| fields.get("meta"))
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
.unwrap_or(serde_json::Value::Null)
}
fn decode_moon_vector_key(key: &[u8], index: &str) -> Option<Vec<u8>> {
let prefix_len = index.len() + 1;
if key.len() < prefix_len || !key.starts_with(index.as_bytes()) || key[index.len()] != b':' {
return None;
}
hex::decode(&key[prefix_len..]).ok().filter(|b| b.len() == 16)
}
fn filter_to_moon_hybrid_filter(filter: &Option<Filter>) -> Option<moon::text::HybridFilter> {
filter.as_ref().and_then(filter_node_to_hybrid)
}
fn filter_node_to_hybrid(f: &Filter) -> Option<moon::text::HybridFilter> {
use moon::text::HybridFilter as Hf;
match f {
Filter::Eq { field, value } => {
Some(Hf::Tag { field: field.clone(), value: json_bare(value) })
}
Filter::StartsWith { field, prefix } => {
Some(Hf::Tag { field: field.clone(), value: format!("{prefix}*") })
}
Filter::And(xs) => {
let children: Vec<Hf> = xs.iter().filter_map(filter_node_to_hybrid).collect();
(!children.is_empty()).then_some(Hf::And(children))
}
Filter::Or(xs) => {
let children: Vec<Hf> = xs.iter().filter_map(filter_node_to_hybrid).collect();
(!children.is_empty()).then_some(Hf::Or(children))
}
Filter::ValidTimeRange { after, before } => Some(Hf::Numeric {
field: "valid_time".to_string(),
min: after.map_or(0.0_f64, |h| h.wall_ms as f64),
max: before.map_or(u64::MAX as f64, |h| h.wall_ms.saturating_sub(1) as f64),
}),
_ => {
tracing::warn!(
"unknown Filter variant in retrieve fusion path — emitting no native filter constraint"
);
None
}
}
}
fn json_bare(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => format!("{v}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::operators::combinators::AndRetriever;
use lunaris_core::storage::types::Filter;
use moon::text::HybridFilter as Hf;
#[test]
fn hybrid_filter_none_when_no_filter() {
assert!(filter_to_moon_hybrid_filter(&None).is_none());
}
#[test]
fn hybrid_filter_eq_renders_exact_tag() {
let f = Some(Filter::Eq { field: "source".into(), value: serde_json::json!("notes.md") });
match filter_to_moon_hybrid_filter(&f) {
Some(Hf::Tag { field, value }) => {
assert_eq!(field, "source");
assert_eq!(value, "notes.md");
}
other => panic!("expected Tag, got {other:?}"),
}
}
#[test]
fn hybrid_filter_startswith_renders_prefix_tag() {
let f = Some(Filter::StartsWith { field: "source".into(), prefix: "helios".into() });
match filter_to_moon_hybrid_filter(&f) {
Some(Hf::Tag { field, value }) => {
assert_eq!(field, "source");
assert_eq!(value, "helios*"); }
other => panic!("expected prefix Tag, got {other:?}"),
}
}
#[test]
fn hybrid_filter_and_recurses_into_children() {
let f = Some(Filter::And(vec![
Filter::Eq { field: "source".into(), value: serde_json::json!("x") },
Filter::Eq { field: "kind".into(), value: serde_json::json!("y") },
]));
match filter_to_moon_hybrid_filter(&f) {
Some(Hf::And(children)) => {
assert_eq!(children.len(), 2);
assert!(
matches!(&children[0], Hf::Tag { field, value } if field == "source" && value == "x")
);
assert!(
matches!(&children[1], Hf::Tag { field, value } if field == "kind" && value == "y")
);
}
other => panic!("expected And, got {other:?}"),
}
}
#[test]
fn hybrid_filter_or_recurses_into_children() {
let f = Some(Filter::Or(vec![
Filter::Eq { field: "source".into(), value: serde_json::json!("a") },
Filter::Eq { field: "source".into(), value: serde_json::json!("b") },
]));
match filter_to_moon_hybrid_filter(&f) {
Some(Hf::Or(children)) => assert_eq!(children.len(), 2),
other => panic!("expected Or, got {other:?}"),
}
}
#[test]
fn hybrid_filter_valid_time_range_renders_finite_numeric() {
use lunaris_core::hlc::Hlc;
let after = Hlc { wall_ms: 1_700_000_000_000, counter: 0, node_id: 0 };
let before = Hlc { wall_ms: 1_760_000_000_000, counter: 0, node_id: 0 };
let f = Some(Filter::ValidTimeRange { after: Some(after), before: Some(before) });
match filter_to_moon_hybrid_filter(&f) {
Some(Hf::Numeric { field, min, max }) => {
assert_eq!(field, "valid_time");
assert_eq!(min, 1_700_000_000_000_f64);
assert_eq!(max, 1_759_999_999_999_f64);
}
other => panic!("expected Numeric, got {other:?}"),
}
}
#[test]
fn hybrid_filter_valid_time_open_sides_use_finite_sentinels() {
let f = Some(Filter::ValidTimeRange { after: None, before: None });
match filter_to_moon_hybrid_filter(&f) {
Some(Hf::Numeric { min, max, .. }) => {
assert!(min.is_finite() && max.is_finite());
assert_eq!(min, 0.0_f64);
assert_eq!(max, u64::MAX as f64);
}
other => panic!("expected Numeric, got {other:?}"),
}
}
#[test]
fn parse_moon_metadata_reads_current_meta_field() {
let fields = HashMap::from([("meta".to_string(), r#"{"source":"notes.md"}"#.to_string())]);
assert_eq!(parse_moon_metadata(&fields), serde_json::json!({"source":"notes.md"}));
}
#[test]
fn parse_moon_metadata_reads_legacy_metadata_field() {
let fields =
HashMap::from([("__metadata".to_string(), r#"{"source":"legacy.md"}"#.to_string())]);
assert_eq!(parse_moon_metadata(&fields), serde_json::json!({"source":"legacy.md"}));
}
#[test]
fn parse_moon_metadata_prefers_legacy_metadata_field_when_both_exist() {
let fields = HashMap::from([
("__metadata".to_string(), r#"{"source":"legacy.md"}"#.to_string()),
("meta".to_string(), r#"{"source":"current.md"}"#.to_string()),
]);
assert_eq!(parse_moon_metadata(&fields), serde_json::json!({"source":"legacy.md"}));
}
#[test]
fn parse_moon_metadata_returns_null_for_missing_or_invalid_json() {
let missing = HashMap::new();
let invalid = HashMap::from([("meta".to_string(), "not-json".to_string())]);
assert_eq!(parse_moon_metadata(&missing), serde_json::Value::Null);
assert_eq!(parse_moon_metadata(&invalid), serde_json::Value::Null);
}
#[test]
fn inspect_recognizes_vector_plus_keyword_same_index() {
let v = Vector::new("chunks", 30);
let k = Keyword::bm25("chunks", 30);
let and = AndRetriever::new(Box::new(v), Box::new(k));
let hint = inspect_branches(&and).expect("must detect");
assert_eq!(hint.kind, FusedKind::VectorKeywordSameIndex);
assert_eq!(hint.index, "chunks");
}
#[test]
fn inspect_recognizes_keyword_plus_vector_same_index() {
let k = Keyword::bm25("chunks", 12);
let v = Vector::new("chunks", 30);
let and = AndRetriever::new(Box::new(k), Box::new(v));
let hint = inspect_branches(&and).expect("must detect");
assert_eq!(hint.kind, FusedKind::VectorKeywordSameIndex);
assert_eq!(hint.index, "chunks");
assert_eq!(hint.k, 30);
}
#[test]
fn inspect_recognizes_different_index_as_other() {
let v = Vector::new("chunks", 30);
let k = Keyword::bm25("entities", 30);
let and = AndRetriever::new(Box::new(v), Box::new(k));
let hint = inspect_branches(&and).expect("AND visible");
assert_eq!(hint.kind, FusedKind::Other);
}
#[test]
fn inspect_recognizes_reversed_different_index_as_other() {
let k = Keyword::bm25("entities", 30);
let v = Vector::new("chunks", 30);
let and = AndRetriever::new(Box::new(k), Box::new(v));
let hint = inspect_branches(&and).expect("AND visible");
assert_eq!(hint.kind, FusedKind::Other);
assert_eq!(hint.index, "chunks");
}
#[test]
fn inspect_returns_none_for_non_and() {
let v = Vector::new("chunks", 30);
let hint = inspect_branches(&v);
assert!(hint.is_none());
}
#[test]
fn inspect_forces_other_when_left_branch_is_graph() {
let g = Graph::anchored(Vec::<(lunaris_extract::EntityId, f32)>::new(), 2);
let v = Vector::new("chunks", 30);
let and = AndRetriever::new(Box::new(g), Box::new(v));
let hint = inspect_branches(&and).expect("AND visible");
assert_eq!(hint.kind, FusedKind::Other, "Graph branch must force client-side fusion path",);
}
#[test]
fn inspect_forces_other_when_right_branch_is_graph() {
let v = Vector::new("chunks", 30);
let g = Graph::anchored(Vec::<(lunaris_extract::EntityId, f32)>::new(), 2);
let and = AndRetriever::new(Box::new(v), Box::new(g));
let hint = inspect_branches(&and).expect("AND visible");
assert_eq!(
hint.kind,
FusedKind::Other,
"Graph branch must force client-side fusion path regardless of position",
);
}
}