use fathomdb_engine::{EngineError, QueryRows};
use fathomdb_query::{
CompileError, CompiledGroupedQuery, CompiledQuery, CompiledRetrievalPlan, CompiledSearchPlan,
CompiledVectorSearch, QueryAst, QueryBuilder, QueryStep, SearchRows, TextQuery, compile_search,
compile_search_plan_from_queries, compile_vector_search,
};
use crate::Engine;
#[must_use]
pub struct NodeQueryBuilder<'e> {
engine: &'e Engine,
inner: QueryBuilder,
}
impl<'e> NodeQueryBuilder<'e> {
pub(crate) fn new(engine: &'e Engine, kind: impl Into<String>) -> Self {
Self {
engine,
inner: QueryBuilder::nodes(kind),
}
}
pub fn search(self, query: impl Into<String>, limit: usize) -> SearchBuilder<'e> {
SearchBuilder::new(
self.engine,
self.inner.ast().root_kind.clone(),
query,
limit,
)
}
pub fn text_search(self, query: impl Into<String>, limit: usize) -> TextSearchBuilder<'e> {
TextSearchBuilder {
engine: self.engine,
inner: self.inner.text_search(query, limit),
attribution_requested: false,
}
}
pub fn vector_search(self, query: impl Into<String>, limit: usize) -> VectorSearchBuilder<'e> {
VectorSearchBuilder::new(
self.engine,
self.inner.ast().root_kind.clone(),
query,
limit,
)
}
pub fn traverse(
mut self,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
) -> Self {
self.inner = self.inner.traverse(direction, label, max_depth);
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.inner = self.inner.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.inner = self.inner.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.inner = self.inner.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.inner = self.inner.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.inner = self.inner.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lte(path, value);
self
}
pub fn expand(
mut self,
slot: impl Into<String>,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
) -> Self {
self.inner = self.inner.expand(slot, direction, label, max_depth);
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.inner = self.inner.limit(limit);
self
}
#[must_use]
pub fn as_builder(&self) -> &QueryBuilder {
&self.inner
}
#[must_use]
pub fn into_builder(self) -> QueryBuilder {
self.inner
}
#[must_use]
pub fn into_ast(self) -> fathomdb_query::QueryAst {
self.inner.into_ast()
}
pub fn compile(&self) -> Result<CompiledQuery, CompileError> {
self.inner.compile()
}
pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
self.inner.compile_grouped()
}
pub fn execute(&self) -> Result<QueryRows, EngineError> {
let compiled = self
.inner
.compile()
.map_err(|e| EngineError::InvalidConfig(format!("query compilation failed: {e}")))?;
self.engine.coordinator().execute_compiled_read(&compiled)
}
}
#[must_use]
pub struct TextSearchBuilder<'e> {
engine: &'e Engine,
inner: QueryBuilder,
attribution_requested: bool,
}
impl TextSearchBuilder<'_> {
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.inner = self.inner.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.inner = self.inner.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.inner = self.inner.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.inner = self.inner.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.inner = self.inner.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lte(path, value);
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.inner = self.inner.limit(limit);
self
}
pub fn traverse(
mut self,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
) -> Self {
self.inner = self.inner.traverse(direction, label, max_depth);
self
}
pub fn expand(
mut self,
slot: impl Into<String>,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
) -> Self {
self.inner = self.inner.expand(slot, direction, label, max_depth);
self
}
#[must_use]
pub fn as_builder(&self) -> &QueryBuilder {
&self.inner
}
pub fn compile(&self) -> Result<CompiledQuery, CompileError> {
self.inner.compile()
}
pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
self.inner.compile_grouped()
}
#[must_use]
pub fn into_ast(self) -> fathomdb_query::QueryAst {
self.inner.into_ast()
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let mut compiled = compile_search(self.inner.ast())
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
compiled.attribution_requested = self.attribution_requested;
self.engine.coordinator().execute_compiled_search(&compiled)
}
}
#[must_use]
pub struct FallbackSearchBuilder<'e> {
engine: &'e Engine,
strict: TextQuery,
relaxed: Option<TextQuery>,
limit: usize,
attribution_requested: bool,
filter_builder: QueryBuilder,
}
impl<'e> FallbackSearchBuilder<'e> {
pub(crate) fn new(
engine: &'e Engine,
strict: impl Into<String>,
relaxed: Option<&str>,
limit: usize,
) -> Self {
let strict = TextQuery::parse(&strict.into());
let relaxed = relaxed.map(TextQuery::parse);
let filter_builder = QueryBuilder::nodes(String::new()).text_search("", 0);
Self {
engine,
strict,
relaxed,
limit,
attribution_requested: false,
filter_builder,
}
}
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
self
}
pub fn compile_plan(&self) -> Result<CompiledSearchPlan, CompileError> {
let mut ast = self.filter_builder.clone().into_ast();
ast.root_kind = String::new();
compile_search_plan_from_queries(
&ast,
self.strict.clone(),
self.relaxed.clone(),
self.limit,
self.attribution_requested,
)
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let plan = self
.compile_plan()
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
self.engine
.coordinator()
.execute_compiled_search_plan(&plan)
}
}
#[must_use]
pub struct VectorSearchBuilder<'e> {
engine: &'e Engine,
root_kind: String,
query: String,
limit: usize,
attribution_requested: bool,
filter_builder: QueryBuilder,
}
impl<'e> VectorSearchBuilder<'e> {
pub(crate) fn new(
engine: &'e Engine,
root_kind: impl Into<String>,
query: impl Into<String>,
limit: usize,
) -> Self {
let root_kind = root_kind.into();
let filter_builder = QueryBuilder::nodes(root_kind.clone()).vector_search("", 0);
Self {
engine,
root_kind,
query: query.into(),
limit,
attribution_requested: false,
filter_builder,
}
}
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
self
}
pub fn compile_plan(&self) -> Result<CompiledVectorSearch, CompileError> {
let mut ast = self.filter_builder.clone().into_ast();
ast.root_kind.clone_from(&self.root_kind);
let mut compiled = compile_vector_search(&ast)?;
compiled.query_text.clone_from(&self.query);
compiled.limit = self.limit;
compiled.attribution_requested = self.attribution_requested;
Ok(compiled)
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let plan = self
.compile_plan()
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
self.engine
.coordinator()
.execute_compiled_vector_search(&plan)
}
}
#[must_use]
pub struct SearchBuilder<'e> {
engine: &'e Engine,
root_kind: String,
query: String,
limit: usize,
attribution_requested: bool,
filter_builder: QueryBuilder,
}
impl<'e> SearchBuilder<'e> {
pub(crate) fn new(
engine: &'e Engine,
root_kind: impl Into<String>,
query: impl Into<String>,
limit: usize,
) -> Self {
let root_kind = root_kind.into();
let filter_builder = QueryBuilder::nodes(root_kind.clone()).text_search("", 0);
Self {
engine,
root_kind,
query: query.into(),
limit,
attribution_requested: false,
filter_builder,
}
}
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
self
}
pub fn compile_plan(&self) -> Result<CompiledRetrievalPlan, CompileError> {
let mut ast: QueryAst = self.filter_builder.clone().into_ast();
ast.root_kind.clone_from(&self.root_kind);
let mut replaced = false;
for step in &mut ast.steps {
if let QueryStep::TextSearch {
query: TextQuery::Empty,
limit: 0,
} = step
{
*step = QueryStep::Search {
query: self.query.clone(),
limit: self.limit,
};
replaced = true;
break;
}
}
debug_assert!(
replaced,
"SearchBuilder filter accumulator must contain the seed TextSearch step"
);
let mut plan = fathomdb_query::compile_retrieval_plan(&ast)?;
plan.text.strict.attribution_requested = self.attribution_requested;
if let Some(relaxed) = plan.text.relaxed.as_mut() {
relaxed.attribution_requested = self.attribution_requested;
}
Ok(plan)
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let plan = self
.compile_plan()
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
self.engine
.coordinator()
.execute_retrieval_plan(&plan, &self.query)
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::FallbackSearchBuilder;
use crate::{Engine, EngineOptions};
use fathomdb_query::Predicate;
use tempfile::NamedTempFile;
#[test]
fn fallback_builder_filter_kind_eq_fuses_without_explicit_text_search_step() {
let db = NamedTempFile::new().expect("temporary db");
let engine =
Engine::open(EngineOptions::new(db.path())).expect("engine opens for unit test");
let builder = FallbackSearchBuilder::new(&engine, "budget", Some("budget OR nothing"), 10)
.filter_kind_eq("Goal");
let plan = builder.compile_plan().expect("compile plan");
assert!(
plan.strict
.fusable_filters
.iter()
.any(|p| matches!(p, Predicate::KindEq(k) if k == "Goal")),
"KindEq(\"Goal\") must land in strict.fusable_filters (got {:?})",
plan.strict.fusable_filters
);
assert!(
plan.strict.residual_filters.is_empty(),
"strict.residual_filters should be empty for a single kind filter (got {:?})",
plan.strict.residual_filters
);
let relaxed = plan
.relaxed
.as_ref()
.expect("relaxed branch present when caller supplied a relaxed query");
assert!(
relaxed
.fusable_filters
.iter()
.any(|p| matches!(p, Predicate::KindEq(k) if k == "Goal")),
"KindEq(\"Goal\") must also land in relaxed.fusable_filters (got {:?})",
relaxed.fusable_filters
);
}
}