use crate::{Predicate, TextQuery};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchBranch {
Strict,
Relaxed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchHitSource {
Chunk,
Property,
Vector,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchMatchMode {
Strict,
Relaxed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RetrievalModality {
Text,
Vector,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HitAttribution {
pub matched_paths: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SearchHit {
pub node: NodeRowLite,
pub score: f64,
pub modality: RetrievalModality,
pub source: SearchHitSource,
pub match_mode: Option<SearchMatchMode>,
pub snippet: Option<String>,
pub written_at: i64,
pub projection_row_id: Option<String>,
pub vector_distance: Option<f64>,
pub attribution: Option<HitAttribution>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeRowLite {
pub row_id: String,
pub logical_id: String,
pub kind: String,
pub properties: String,
pub content_ref: Option<String>,
pub last_accessed_at: Option<i64>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SearchRows {
pub hits: Vec<SearchHit>,
pub strict_hit_count: usize,
pub relaxed_hit_count: usize,
pub vector_hit_count: usize,
pub fallback_used: bool,
pub was_degraded: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompiledSearch {
pub root_kind: String,
pub text_query: TextQuery,
pub limit: usize,
pub fusable_filters: Vec<Predicate>,
pub residual_filters: Vec<Predicate>,
pub attribution_requested: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompiledVectorSearch {
pub root_kind: String,
pub query_text: String,
pub limit: usize,
pub fusable_filters: Vec<Predicate>,
pub residual_filters: Vec<Predicate>,
pub attribution_requested: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompiledSearchPlan {
pub strict: CompiledSearch,
pub relaxed: Option<CompiledSearch>,
pub was_degraded_at_plan_time: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompiledRetrievalPlan {
pub text: CompiledSearchPlan,
pub vector: Option<CompiledVectorSearch>,
pub was_degraded_at_plan_time: bool,
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn search_hit_source_has_vector_variant_reserved() {
let source = SearchHitSource::Chunk;
match source {
SearchHitSource::Chunk | SearchHitSource::Property | SearchHitSource::Vector => {}
}
}
#[test]
fn search_match_mode_has_strict_and_relaxed() {
let mode = SearchMatchMode::Strict;
match mode {
SearchMatchMode::Strict | SearchMatchMode::Relaxed => {}
}
}
#[test]
fn compile_search_rejects_ast_without_text_search_step() {
use crate::{CompileError, QueryBuilder, compile_search};
let ast = QueryBuilder::nodes("Goal")
.filter_kind_eq("Goal")
.into_ast();
let result = compile_search(&ast);
assert!(
matches!(result, Err(CompileError::MissingTextSearchStep)),
"expected MissingTextSearchStep, got {result:?}"
);
}
#[test]
fn compile_search_accepts_text_search_step_with_filters() {
use crate::{QueryBuilder, compile_search};
let ast = QueryBuilder::nodes("Goal")
.text_search("quarterly docs", 7)
.filter_kind_eq("Goal")
.into_ast();
let compiled = compile_search(&ast).expect("compiles");
assert_eq!(compiled.root_kind, "Goal");
assert_eq!(compiled.limit, 7);
assert_eq!(compiled.fusable_filters.len(), 1);
assert!(compiled.residual_filters.is_empty());
}
#[test]
fn compile_vector_search_rejects_ast_without_vector_search_step() {
use crate::{CompileError, QueryBuilder, compile_vector_search};
let ast = QueryBuilder::nodes("Goal")
.filter_kind_eq("Goal")
.into_ast();
let result = compile_vector_search(&ast);
assert!(
matches!(result, Err(CompileError::MissingVectorSearchStep)),
"expected MissingVectorSearchStep, got {result:?}"
);
}
#[test]
fn compile_vector_search_accepts_vector_search_step_with_filters() {
use crate::{Predicate, QueryBuilder, compile_vector_search};
let ast = QueryBuilder::nodes("Goal")
.vector_search("[0.1, 0.2, 0.3, 0.4]", 7)
.filter_kind_eq("Goal")
.filter_json_text_eq("$.status", "active")
.into_ast();
let compiled = compile_vector_search(&ast).expect("compiles");
assert_eq!(compiled.root_kind, "Goal");
assert_eq!(compiled.query_text, "[0.1, 0.2, 0.3, 0.4]");
assert_eq!(compiled.limit, 7);
assert_eq!(compiled.fusable_filters.len(), 1);
assert!(matches!(
compiled.fusable_filters[0],
Predicate::KindEq(ref k) if k == "Goal"
));
assert_eq!(compiled.residual_filters.len(), 1);
assert!(!compiled.attribution_requested);
}
}