1use fathomdb_engine::{EngineError, GroupedQueryRows, QueryRows};
9use fathomdb_query::{
10 BuilderValidationError, CompileError, CompiledGroupedQuery, CompiledQuery,
11 CompiledRawVectorSearch, CompiledRetrievalPlan, CompiledSearchPlan, CompiledSemanticSearch,
12 CompiledVectorSearch, QueryAst, QueryBuilder, QueryStep, SearchRows, TextQuery,
13 compile_grouped_query, compile_search, compile_search_plan_from_queries, compile_vector_search,
14};
15
16use crate::Engine;
17
18fn validate_fusable_property_path(
32 engine: &Engine,
33 kind: &str,
34 path: &str,
35 method: &str,
36) -> Result<(), BuilderValidationError> {
37 if kind.is_empty() {
38 return Err(BuilderValidationError::KindRequiredForFusion {
39 method: method.to_owned(),
40 });
41 }
42 let schema = engine.describe_fts_property_schema(kind).map_err(|_| {
43 BuilderValidationError::MissingPropertyFtsSchema {
44 kind: kind.to_owned(),
45 }
46 })?;
47 let schema = schema.ok_or_else(|| BuilderValidationError::MissingPropertyFtsSchema {
48 kind: kind.to_owned(),
49 })?;
50 if !schema.property_paths.iter().any(|p| p == path) {
51 return Err(BuilderValidationError::PathNotIndexed {
52 kind: kind.to_owned(),
53 path: path.to_owned(),
54 });
55 }
56 Ok(())
57}
58
59fn filter_builder_kind(builder: &QueryBuilder) -> Option<&str> {
65 for step in &builder.ast().steps {
66 if let QueryStep::Filter(fathomdb_query::Predicate::KindEq(kind)) = step {
67 return Some(kind.as_str());
68 }
69 }
70 None
71}
72
73#[must_use]
80pub struct NodeQueryBuilder<'e> {
81 engine: &'e Engine,
82 inner: QueryBuilder,
83}
84
85impl<'e> NodeQueryBuilder<'e> {
86 pub(crate) fn new(engine: &'e Engine, kind: impl Into<String>) -> Self {
87 Self {
88 engine,
89 inner: QueryBuilder::nodes(kind),
90 }
91 }
92
93 pub fn search(self, query: impl Into<String>, limit: usize) -> SearchBuilder<'e> {
109 SearchBuilder::new(
110 self.engine,
111 self.inner.ast().root_kind.clone(),
112 query,
113 limit,
114 )
115 }
116
117 pub fn text_search(self, query: impl Into<String>, limit: usize) -> TextSearchBuilder<'e> {
121 TextSearchBuilder {
122 engine: self.engine,
123 inner: self.inner.text_search(query, limit),
124 attribution_requested: false,
125 }
126 }
127
128 pub fn vector_search(self, query: impl Into<String>, limit: usize) -> VectorSearchBuilder<'e> {
140 VectorSearchBuilder::new(
141 self.engine,
142 self.inner.ast().root_kind.clone(),
143 query,
144 limit,
145 )
146 }
147
148 pub fn semantic_search(
153 self,
154 text: impl Into<String>,
155 limit: usize,
156 ) -> SemanticSearchBuilder<'e> {
157 SemanticSearchBuilder {
158 engine: self.engine,
159 root_kind: self.inner.ast().root_kind.clone(),
160 text: text.into(),
161 limit,
162 }
163 }
164
165 pub fn raw_vector_search(self, vec: Vec<f32>, limit: usize) -> RawVectorSearchBuilder<'e> {
170 RawVectorSearchBuilder {
171 engine: self.engine,
172 root_kind: self.inner.ast().root_kind.clone(),
173 vec,
174 limit,
175 }
176 }
177
178 pub fn traverse(
180 mut self,
181 direction: fathomdb_query::TraverseDirection,
182 label: impl Into<String>,
183 max_depth: usize,
184 ) -> Self {
185 self.inner = self.inner.traverse(direction, label, max_depth);
186 self
187 }
188
189 pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
191 self.inner = self.inner.filter_logical_id_eq(logical_id);
192 self
193 }
194
195 pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
197 self.inner = self.inner.filter_kind_eq(kind);
198 self
199 }
200
201 pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
203 self.inner = self.inner.filter_source_ref_eq(source_ref);
204 self
205 }
206
207 pub fn filter_content_ref_not_null(mut self) -> Self {
209 self.inner = self.inner.filter_content_ref_not_null();
210 self
211 }
212
213 pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
215 self.inner = self.inner.filter_content_ref_eq(content_ref);
216 self
217 }
218
219 pub fn filter_json_text_eq(
221 mut self,
222 path: impl Into<String>,
223 value: impl Into<String>,
224 ) -> Self {
225 self.inner = self.inner.filter_json_text_eq(path, value);
226 self
227 }
228
229 pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
231 self.inner = self.inner.filter_json_bool_eq(path, value);
232 self
233 }
234
235 pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
237 self.inner = self.inner.filter_json_integer_gt(path, value);
238 self
239 }
240
241 pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
243 self.inner = self.inner.filter_json_integer_gte(path, value);
244 self
245 }
246
247 pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
249 self.inner = self.inner.filter_json_integer_lt(path, value);
250 self
251 }
252
253 pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
255 self.inner = self.inner.filter_json_integer_lte(path, value);
256 self
257 }
258
259 pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
261 self.inner = self.inner.filter_json_timestamp_gt(path, value);
262 self
263 }
264
265 pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
267 self.inner = self.inner.filter_json_timestamp_gte(path, value);
268 self
269 }
270
271 pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
273 self.inner = self.inner.filter_json_timestamp_lt(path, value);
274 self
275 }
276
277 pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
279 self.inner = self.inner.filter_json_timestamp_lte(path, value);
280 self
281 }
282
283 pub fn filter_json_fused_text_eq(
292 mut self,
293 path: impl Into<String>,
294 value: impl Into<String>,
295 ) -> Result<Self, BuilderValidationError> {
296 let path = path.into();
297 let kind = self.inner.ast().root_kind.clone();
298 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_eq")?;
299 self.inner = self.inner.filter_json_fused_text_eq_unchecked(path, value);
300 Ok(self)
301 }
302
303 pub fn filter_json_fused_text_in(
312 mut self,
313 path: impl Into<String>,
314 values: Vec<String>,
315 ) -> Result<Self, BuilderValidationError> {
316 let path = path.into();
317 let kind = self.inner.ast().root_kind.clone();
318 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_in")?;
319 self.inner = self.inner.filter_json_fused_text_in_unchecked(path, values);
320 Ok(self)
321 }
322
323 pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
326 self.inner = self.inner.filter_json_text_in(path, values);
327 self
328 }
329
330 pub fn filter_json_fused_timestamp_gt(
336 mut self,
337 path: impl Into<String>,
338 value: i64,
339 ) -> Result<Self, BuilderValidationError> {
340 let path = path.into();
341 let kind = self.inner.ast().root_kind.clone();
342 validate_fusable_property_path(
343 self.engine,
344 &kind,
345 &path,
346 "filter_json_fused_timestamp_gt",
347 )?;
348 self.inner = self
349 .inner
350 .filter_json_fused_timestamp_gt_unchecked(path, value);
351 Ok(self)
352 }
353
354 pub fn filter_json_fused_timestamp_gte(
360 mut self,
361 path: impl Into<String>,
362 value: i64,
363 ) -> Result<Self, BuilderValidationError> {
364 let path = path.into();
365 let kind = self.inner.ast().root_kind.clone();
366 validate_fusable_property_path(
367 self.engine,
368 &kind,
369 &path,
370 "filter_json_fused_timestamp_gte",
371 )?;
372 self.inner = self
373 .inner
374 .filter_json_fused_timestamp_gte_unchecked(path, value);
375 Ok(self)
376 }
377
378 pub fn filter_json_fused_timestamp_lt(
384 mut self,
385 path: impl Into<String>,
386 value: i64,
387 ) -> Result<Self, BuilderValidationError> {
388 let path = path.into();
389 let kind = self.inner.ast().root_kind.clone();
390 validate_fusable_property_path(
391 self.engine,
392 &kind,
393 &path,
394 "filter_json_fused_timestamp_lt",
395 )?;
396 self.inner = self
397 .inner
398 .filter_json_fused_timestamp_lt_unchecked(path, value);
399 Ok(self)
400 }
401
402 pub fn filter_json_fused_timestamp_lte(
408 mut self,
409 path: impl Into<String>,
410 value: i64,
411 ) -> Result<Self, BuilderValidationError> {
412 let path = path.into();
413 let kind = self.inner.ast().root_kind.clone();
414 validate_fusable_property_path(
415 self.engine,
416 &kind,
417 &path,
418 "filter_json_fused_timestamp_lte",
419 )?;
420 self.inner = self
421 .inner
422 .filter_json_fused_timestamp_lte_unchecked(path, value);
423 Ok(self)
424 }
425
426 pub fn filter_json_fused_bool_eq(
433 mut self,
434 path: impl Into<String>,
435 value: bool,
436 ) -> Result<Self, BuilderValidationError> {
437 let path = path.into();
438 let kind = self.inner.ast().root_kind.clone();
439 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_bool_eq")?;
440 self.inner = self.inner.filter_json_fused_bool_eq_unchecked(path, value);
441 Ok(self)
442 }
443
444 pub fn expand(
450 mut self,
451 slot: impl Into<String>,
452 direction: fathomdb_query::TraverseDirection,
453 label: impl Into<String>,
454 max_depth: usize,
455 filter: Option<fathomdb_query::Predicate>,
456 edge_filter: Option<fathomdb_query::Predicate>,
457 ) -> Self {
458 self.inner = self
459 .inner
460 .expand(slot, direction, label, max_depth, filter, edge_filter);
461 self
462 }
463
464 pub fn limit(mut self, limit: usize) -> Self {
466 self.inner = self.inner.limit(limit);
467 self
468 }
469
470 #[must_use]
472 pub fn as_builder(&self) -> &QueryBuilder {
473 &self.inner
474 }
475
476 #[must_use]
478 pub fn into_builder(self) -> QueryBuilder {
479 self.inner
480 }
481
482 #[must_use]
484 pub fn into_ast(self) -> fathomdb_query::QueryAst {
485 self.inner.into_ast()
486 }
487
488 pub fn compile(&self) -> Result<CompiledQuery, CompileError> {
494 self.inner.compile()
495 }
496
497 pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
503 self.inner.compile_grouped()
504 }
505
506 pub fn execute(&self) -> Result<QueryRows, EngineError> {
511 let compiled = self
512 .inner
513 .compile()
514 .map_err(|e| EngineError::InvalidConfig(format!("query compilation failed: {e}")))?;
515 self.engine.coordinator().execute_compiled_read(&compiled)
516 }
517
518 pub fn execute_grouped(self) -> Result<GroupedQueryRows, EngineError> {
523 let compiled = self.inner.compile_grouped().map_err(|e| {
524 EngineError::InvalidConfig(format!("grouped query compilation failed: {e}"))
525 })?;
526 self.engine
527 .coordinator()
528 .execute_compiled_grouped_read(&compiled)
529 }
530}
531
532#[must_use]
540pub struct TextSearchBuilder<'e> {
541 engine: &'e Engine,
542 inner: QueryBuilder,
543 attribution_requested: bool,
544}
545
546impl TextSearchBuilder<'_> {
547 pub fn with_match_attribution(mut self) -> Self {
556 self.attribution_requested = true;
557 self
558 }
559
560 pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
562 self.inner = self.inner.filter_logical_id_eq(logical_id);
563 self
564 }
565
566 pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
568 self.inner = self.inner.filter_kind_eq(kind);
569 self
570 }
571
572 pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
574 self.inner = self.inner.filter_source_ref_eq(source_ref);
575 self
576 }
577
578 pub fn filter_content_ref_not_null(mut self) -> Self {
580 self.inner = self.inner.filter_content_ref_not_null();
581 self
582 }
583
584 pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
586 self.inner = self.inner.filter_content_ref_eq(content_ref);
587 self
588 }
589
590 pub fn filter_json_text_eq(
592 mut self,
593 path: impl Into<String>,
594 value: impl Into<String>,
595 ) -> Self {
596 self.inner = self.inner.filter_json_text_eq(path, value);
597 self
598 }
599
600 pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
602 self.inner = self.inner.filter_json_bool_eq(path, value);
603 self
604 }
605
606 pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
608 self.inner = self.inner.filter_json_integer_gt(path, value);
609 self
610 }
611
612 pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
614 self.inner = self.inner.filter_json_integer_gte(path, value);
615 self
616 }
617
618 pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
620 self.inner = self.inner.filter_json_integer_lt(path, value);
621 self
622 }
623
624 pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
626 self.inner = self.inner.filter_json_integer_lte(path, value);
627 self
628 }
629
630 pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
632 self.inner = self.inner.filter_json_timestamp_gt(path, value);
633 self
634 }
635
636 pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
638 self.inner = self.inner.filter_json_timestamp_gte(path, value);
639 self
640 }
641
642 pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
644 self.inner = self.inner.filter_json_timestamp_lt(path, value);
645 self
646 }
647
648 pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
650 self.inner = self.inner.filter_json_timestamp_lte(path, value);
651 self
652 }
653
654 pub fn filter_json_fused_text_eq(
663 mut self,
664 path: impl Into<String>,
665 value: impl Into<String>,
666 ) -> Result<Self, BuilderValidationError> {
667 let path = path.into();
668 let kind = self.inner.ast().root_kind.clone();
669 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_eq")?;
670 self.inner = self.inner.filter_json_fused_text_eq_unchecked(path, value);
671 Ok(self)
672 }
673
674 pub fn filter_json_fused_text_in(
680 mut self,
681 path: impl Into<String>,
682 values: Vec<String>,
683 ) -> Result<Self, BuilderValidationError> {
684 let path = path.into();
685 let kind = self.inner.ast().root_kind.clone();
686 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_in")?;
687 self.inner = self.inner.filter_json_fused_text_in_unchecked(path, values);
688 Ok(self)
689 }
690
691 pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
694 self.inner = self.inner.filter_json_text_in(path, values);
695 self
696 }
697
698 pub fn filter_json_fused_timestamp_gt(
704 mut self,
705 path: impl Into<String>,
706 value: i64,
707 ) -> Result<Self, BuilderValidationError> {
708 let path = path.into();
709 let kind = self.inner.ast().root_kind.clone();
710 validate_fusable_property_path(
711 self.engine,
712 &kind,
713 &path,
714 "filter_json_fused_timestamp_gt",
715 )?;
716 self.inner = self
717 .inner
718 .filter_json_fused_timestamp_gt_unchecked(path, value);
719 Ok(self)
720 }
721
722 pub fn filter_json_fused_timestamp_gte(
728 mut self,
729 path: impl Into<String>,
730 value: i64,
731 ) -> Result<Self, BuilderValidationError> {
732 let path = path.into();
733 let kind = self.inner.ast().root_kind.clone();
734 validate_fusable_property_path(
735 self.engine,
736 &kind,
737 &path,
738 "filter_json_fused_timestamp_gte",
739 )?;
740 self.inner = self
741 .inner
742 .filter_json_fused_timestamp_gte_unchecked(path, value);
743 Ok(self)
744 }
745
746 pub fn filter_json_fused_timestamp_lt(
752 mut self,
753 path: impl Into<String>,
754 value: i64,
755 ) -> Result<Self, BuilderValidationError> {
756 let path = path.into();
757 let kind = self.inner.ast().root_kind.clone();
758 validate_fusable_property_path(
759 self.engine,
760 &kind,
761 &path,
762 "filter_json_fused_timestamp_lt",
763 )?;
764 self.inner = self
765 .inner
766 .filter_json_fused_timestamp_lt_unchecked(path, value);
767 Ok(self)
768 }
769
770 pub fn filter_json_fused_timestamp_lte(
776 mut self,
777 path: impl Into<String>,
778 value: i64,
779 ) -> Result<Self, BuilderValidationError> {
780 let path = path.into();
781 let kind = self.inner.ast().root_kind.clone();
782 validate_fusable_property_path(
783 self.engine,
784 &kind,
785 &path,
786 "filter_json_fused_timestamp_lte",
787 )?;
788 self.inner = self
789 .inner
790 .filter_json_fused_timestamp_lte_unchecked(path, value);
791 Ok(self)
792 }
793
794 pub fn filter_json_fused_bool_eq(
801 mut self,
802 path: impl Into<String>,
803 value: bool,
804 ) -> Result<Self, BuilderValidationError> {
805 let path = path.into();
806 let kind = self.inner.ast().root_kind.clone();
807 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_bool_eq")?;
808 self.inner = self.inner.filter_json_fused_bool_eq_unchecked(path, value);
809 Ok(self)
810 }
811
812 pub fn limit(mut self, limit: usize) -> Self {
819 self.inner = self.inner.limit(limit);
820 self
821 }
822
823 pub fn traverse(
827 mut self,
828 direction: fathomdb_query::TraverseDirection,
829 label: impl Into<String>,
830 max_depth: usize,
831 ) -> Self {
832 self.inner = self.inner.traverse(direction, label, max_depth);
833 self
834 }
835
836 pub fn expand(
844 mut self,
845 slot: impl Into<String>,
846 direction: fathomdb_query::TraverseDirection,
847 label: impl Into<String>,
848 max_depth: usize,
849 filter: Option<fathomdb_query::Predicate>,
850 edge_filter: Option<fathomdb_query::Predicate>,
851 ) -> Self {
852 self.inner = self
853 .inner
854 .expand(slot, direction, label, max_depth, filter, edge_filter);
855 self
856 }
857
858 #[must_use]
860 pub fn as_builder(&self) -> &QueryBuilder {
861 &self.inner
862 }
863
864 pub fn compile(&self) -> Result<CompiledQuery, CompileError> {
871 self.inner.compile()
872 }
873
874 pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
879 self.inner.compile_grouped()
880 }
881
882 #[must_use]
884 pub fn into_ast(self) -> fathomdb_query::QueryAst {
885 self.inner.into_ast()
886 }
887
888 pub fn execute(&self) -> Result<SearchRows, EngineError> {
893 let mut compiled = compile_search(self.inner.ast())
894 .map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
895 compiled.attribution_requested = self.attribution_requested;
896 self.engine.coordinator().execute_compiled_search(&compiled)
897 }
898}
899
900#[must_use]
919pub struct FallbackSearchBuilder<'e> {
920 engine: &'e Engine,
921 strict: TextQuery,
922 relaxed: Option<TextQuery>,
923 limit: usize,
924 attribution_requested: bool,
925 filter_builder: QueryBuilder,
928}
929
930impl<'e> FallbackSearchBuilder<'e> {
931 pub(crate) fn new(
932 engine: &'e Engine,
933 strict: impl Into<String>,
934 relaxed: Option<&str>,
935 limit: usize,
936 ) -> Self {
937 let strict = TextQuery::parse(&strict.into());
938 let relaxed = relaxed.map(TextQuery::parse);
939 let filter_builder = QueryBuilder::nodes(String::new()).text_search("", 0);
953 Self {
954 engine,
955 strict,
956 relaxed,
957 limit,
958 attribution_requested: false,
959 filter_builder,
960 }
961 }
962
963 pub fn with_match_attribution(mut self) -> Self {
966 self.attribution_requested = true;
967 self
968 }
969
970 pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
972 self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
973 self
974 }
975
976 pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
986 self.filter_builder = self.filter_builder.filter_kind_eq(kind);
987 self
988 }
989
990 pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
992 self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
993 self
994 }
995
996 pub fn filter_content_ref_not_null(mut self) -> Self {
998 self.filter_builder = self.filter_builder.filter_content_ref_not_null();
999 self
1000 }
1001
1002 pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
1004 self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
1005 self
1006 }
1007
1008 pub fn filter_json_text_eq(
1010 mut self,
1011 path: impl Into<String>,
1012 value: impl Into<String>,
1013 ) -> Self {
1014 self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
1015 self
1016 }
1017
1018 pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
1020 self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
1021 self
1022 }
1023
1024 pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
1026 self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
1027 self
1028 }
1029
1030 pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
1032 self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
1033 self
1034 }
1035
1036 pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
1038 self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
1039 self
1040 }
1041
1042 pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
1044 self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
1045 self
1046 }
1047
1048 pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
1050 self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
1051 self
1052 }
1053
1054 pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
1056 self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
1057 self
1058 }
1059
1060 pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
1062 self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
1063 self
1064 }
1065
1066 pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
1068 self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
1069 self
1070 }
1071
1072 pub fn filter_json_fused_text_eq(
1081 mut self,
1082 path: impl Into<String>,
1083 value: impl Into<String>,
1084 ) -> Result<Self, BuilderValidationError> {
1085 let path = path.into();
1086 let kind = filter_builder_kind(&self.filter_builder)
1087 .ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
1088 method: "filter_json_fused_text_eq".to_owned(),
1089 })?
1090 .to_owned();
1091 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_eq")?;
1092 self.filter_builder = self
1093 .filter_builder
1094 .filter_json_fused_text_eq_unchecked(path, value);
1095 Ok(self)
1096 }
1097
1098 pub fn filter_json_fused_text_in(
1104 mut self,
1105 path: impl Into<String>,
1106 values: Vec<String>,
1107 ) -> Result<Self, BuilderValidationError> {
1108 let path = path.into();
1109 let kind = filter_builder_kind(&self.filter_builder)
1110 .ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
1111 method: "filter_json_fused_text_in".to_owned(),
1112 })?
1113 .to_owned();
1114 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_in")?;
1115 self.filter_builder = self
1116 .filter_builder
1117 .filter_json_fused_text_in_unchecked(path, values);
1118 Ok(self)
1119 }
1120
1121 pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
1124 self.filter_builder = self.filter_builder.filter_json_text_in(path, values);
1125 self
1126 }
1127
1128 pub fn filter_json_fused_timestamp_gt(
1134 mut self,
1135 path: impl Into<String>,
1136 value: i64,
1137 ) -> Result<Self, BuilderValidationError> {
1138 let path = path.into();
1139 let kind = filter_builder_kind(&self.filter_builder)
1140 .ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
1141 method: "filter_json_fused_timestamp_gt".to_owned(),
1142 })?
1143 .to_owned();
1144 validate_fusable_property_path(
1145 self.engine,
1146 &kind,
1147 &path,
1148 "filter_json_fused_timestamp_gt",
1149 )?;
1150 self.filter_builder = self
1151 .filter_builder
1152 .filter_json_fused_timestamp_gt_unchecked(path, value);
1153 Ok(self)
1154 }
1155
1156 pub fn filter_json_fused_timestamp_gte(
1162 mut self,
1163 path: impl Into<String>,
1164 value: i64,
1165 ) -> Result<Self, BuilderValidationError> {
1166 let path = path.into();
1167 let kind = filter_builder_kind(&self.filter_builder)
1168 .ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
1169 method: "filter_json_fused_timestamp_gte".to_owned(),
1170 })?
1171 .to_owned();
1172 validate_fusable_property_path(
1173 self.engine,
1174 &kind,
1175 &path,
1176 "filter_json_fused_timestamp_gte",
1177 )?;
1178 self.filter_builder = self
1179 .filter_builder
1180 .filter_json_fused_timestamp_gte_unchecked(path, value);
1181 Ok(self)
1182 }
1183
1184 pub fn filter_json_fused_timestamp_lt(
1190 mut self,
1191 path: impl Into<String>,
1192 value: i64,
1193 ) -> Result<Self, BuilderValidationError> {
1194 let path = path.into();
1195 let kind = filter_builder_kind(&self.filter_builder)
1196 .ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
1197 method: "filter_json_fused_timestamp_lt".to_owned(),
1198 })?
1199 .to_owned();
1200 validate_fusable_property_path(
1201 self.engine,
1202 &kind,
1203 &path,
1204 "filter_json_fused_timestamp_lt",
1205 )?;
1206 self.filter_builder = self
1207 .filter_builder
1208 .filter_json_fused_timestamp_lt_unchecked(path, value);
1209 Ok(self)
1210 }
1211
1212 pub fn filter_json_fused_timestamp_lte(
1218 mut self,
1219 path: impl Into<String>,
1220 value: i64,
1221 ) -> Result<Self, BuilderValidationError> {
1222 let path = path.into();
1223 let kind = filter_builder_kind(&self.filter_builder)
1224 .ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
1225 method: "filter_json_fused_timestamp_lte".to_owned(),
1226 })?
1227 .to_owned();
1228 validate_fusable_property_path(
1229 self.engine,
1230 &kind,
1231 &path,
1232 "filter_json_fused_timestamp_lte",
1233 )?;
1234 self.filter_builder = self
1235 .filter_builder
1236 .filter_json_fused_timestamp_lte_unchecked(path, value);
1237 Ok(self)
1238 }
1239
1240 pub fn filter_json_fused_bool_eq(
1247 mut self,
1248 path: impl Into<String>,
1249 value: bool,
1250 ) -> Result<Self, BuilderValidationError> {
1251 let path = path.into();
1252 let kind = filter_builder_kind(&self.filter_builder)
1253 .ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
1254 method: "filter_json_fused_bool_eq".to_owned(),
1255 })?
1256 .to_owned();
1257 validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_bool_eq")?;
1258 self.filter_builder = self
1259 .filter_builder
1260 .filter_json_fused_bool_eq_unchecked(path, value);
1261 Ok(self)
1262 }
1263
1264 pub fn compile_plan(&self) -> Result<CompiledSearchPlan, CompileError> {
1270 let mut ast = self.filter_builder.clone().into_ast();
1281 ast.root_kind = String::new();
1282 compile_search_plan_from_queries(
1283 &ast,
1284 self.strict.clone(),
1285 self.relaxed.clone(),
1286 self.limit,
1287 self.attribution_requested,
1288 )
1289 }
1290
1291 pub fn execute(&self) -> Result<SearchRows, EngineError> {
1296 let plan = self
1297 .compile_plan()
1298 .map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
1299 self.engine
1300 .coordinator()
1301 .execute_compiled_search_plan(&plan)
1302 }
1303}
1304
1305#[must_use]
1319pub struct VectorSearchBuilder<'e> {
1320 engine: &'e Engine,
1321 root_kind: String,
1322 query: String,
1323 limit: usize,
1324 attribution_requested: bool,
1325 filter_builder: QueryBuilder,
1328}
1329
1330impl<'e> VectorSearchBuilder<'e> {
1331 pub(crate) fn new(
1332 engine: &'e Engine,
1333 root_kind: impl Into<String>,
1334 query: impl Into<String>,
1335 limit: usize,
1336 ) -> Self {
1337 let root_kind = root_kind.into();
1338 #[allow(deprecated)]
1349 let filter_builder = QueryBuilder::nodes(root_kind.clone()).vector_search("", 0);
1350 Self {
1351 engine,
1352 root_kind,
1353 query: query.into(),
1354 limit,
1355 attribution_requested: false,
1356 filter_builder,
1357 }
1358 }
1359
1360 pub fn with_match_attribution(mut self) -> Self {
1370 self.attribution_requested = true;
1371 self
1372 }
1373
1374 pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
1376 self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
1377 self
1378 }
1379
1380 pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
1382 self.filter_builder = self.filter_builder.filter_kind_eq(kind);
1383 self
1384 }
1385
1386 pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
1388 self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
1389 self
1390 }
1391
1392 pub fn filter_content_ref_not_null(mut self) -> Self {
1394 self.filter_builder = self.filter_builder.filter_content_ref_not_null();
1395 self
1396 }
1397
1398 pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
1400 self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
1401 self
1402 }
1403
1404 pub fn filter_json_text_eq(
1406 mut self,
1407 path: impl Into<String>,
1408 value: impl Into<String>,
1409 ) -> Self {
1410 self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
1411 self
1412 }
1413
1414 pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
1416 self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
1417 self
1418 }
1419
1420 pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
1422 self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
1423 self
1424 }
1425
1426 pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
1428 self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
1429 self
1430 }
1431
1432 pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
1434 self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
1435 self
1436 }
1437
1438 pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
1440 self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
1441 self
1442 }
1443
1444 pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
1446 self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
1447 self
1448 }
1449
1450 pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
1452 self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
1453 self
1454 }
1455
1456 pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
1458 self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
1459 self
1460 }
1461
1462 pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
1464 self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
1465 self
1466 }
1467
1468 pub fn filter_json_fused_text_eq(
1476 mut self,
1477 path: impl Into<String>,
1478 value: impl Into<String>,
1479 ) -> Result<Self, BuilderValidationError> {
1480 let path = path.into();
1481 validate_fusable_property_path(
1482 self.engine,
1483 &self.root_kind,
1484 &path,
1485 "filter_json_fused_text_eq",
1486 )?;
1487 self.filter_builder = self
1488 .filter_builder
1489 .filter_json_fused_text_eq_unchecked(path, value);
1490 Ok(self)
1491 }
1492
1493 pub fn filter_json_fused_text_in(
1499 mut self,
1500 path: impl Into<String>,
1501 values: Vec<String>,
1502 ) -> Result<Self, BuilderValidationError> {
1503 let path = path.into();
1504 validate_fusable_property_path(
1505 self.engine,
1506 &self.root_kind,
1507 &path,
1508 "filter_json_fused_text_in",
1509 )?;
1510 self.filter_builder = self
1511 .filter_builder
1512 .filter_json_fused_text_in_unchecked(path, values);
1513 Ok(self)
1514 }
1515
1516 pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
1519 self.filter_builder = self.filter_builder.filter_json_text_in(path, values);
1520 self
1521 }
1522
1523 pub fn filter_json_fused_timestamp_gt(
1529 mut self,
1530 path: impl Into<String>,
1531 value: i64,
1532 ) -> Result<Self, BuilderValidationError> {
1533 let path = path.into();
1534 validate_fusable_property_path(
1535 self.engine,
1536 &self.root_kind,
1537 &path,
1538 "filter_json_fused_timestamp_gt",
1539 )?;
1540 self.filter_builder = self
1541 .filter_builder
1542 .filter_json_fused_timestamp_gt_unchecked(path, value);
1543 Ok(self)
1544 }
1545
1546 pub fn filter_json_fused_timestamp_gte(
1552 mut self,
1553 path: impl Into<String>,
1554 value: i64,
1555 ) -> Result<Self, BuilderValidationError> {
1556 let path = path.into();
1557 validate_fusable_property_path(
1558 self.engine,
1559 &self.root_kind,
1560 &path,
1561 "filter_json_fused_timestamp_gte",
1562 )?;
1563 self.filter_builder = self
1564 .filter_builder
1565 .filter_json_fused_timestamp_gte_unchecked(path, value);
1566 Ok(self)
1567 }
1568
1569 pub fn filter_json_fused_timestamp_lt(
1575 mut self,
1576 path: impl Into<String>,
1577 value: i64,
1578 ) -> Result<Self, BuilderValidationError> {
1579 let path = path.into();
1580 validate_fusable_property_path(
1581 self.engine,
1582 &self.root_kind,
1583 &path,
1584 "filter_json_fused_timestamp_lt",
1585 )?;
1586 self.filter_builder = self
1587 .filter_builder
1588 .filter_json_fused_timestamp_lt_unchecked(path, value);
1589 Ok(self)
1590 }
1591
1592 pub fn filter_json_fused_timestamp_lte(
1598 mut self,
1599 path: impl Into<String>,
1600 value: i64,
1601 ) -> Result<Self, BuilderValidationError> {
1602 let path = path.into();
1603 validate_fusable_property_path(
1604 self.engine,
1605 &self.root_kind,
1606 &path,
1607 "filter_json_fused_timestamp_lte",
1608 )?;
1609 self.filter_builder = self
1610 .filter_builder
1611 .filter_json_fused_timestamp_lte_unchecked(path, value);
1612 Ok(self)
1613 }
1614
1615 pub fn filter_json_fused_bool_eq(
1622 mut self,
1623 path: impl Into<String>,
1624 value: bool,
1625 ) -> Result<Self, BuilderValidationError> {
1626 let path = path.into();
1627 validate_fusable_property_path(
1628 self.engine,
1629 &self.root_kind,
1630 &path,
1631 "filter_json_fused_bool_eq",
1632 )?;
1633 self.filter_builder = self
1634 .filter_builder
1635 .filter_json_fused_bool_eq_unchecked(path, value);
1636 Ok(self)
1637 }
1638
1639 pub fn compile_plan(&self) -> Result<CompiledVectorSearch, CompileError> {
1645 let mut ast = self.filter_builder.clone().into_ast();
1646 ast.root_kind.clone_from(&self.root_kind);
1647 let mut compiled = compile_vector_search(&ast)?;
1648 compiled.query_text.clone_from(&self.query);
1652 compiled.limit = self.limit;
1653 compiled.attribution_requested = self.attribution_requested;
1654 Ok(compiled)
1655 }
1656
1657 pub fn execute(&self) -> Result<SearchRows, EngineError> {
1664 let plan = self
1665 .compile_plan()
1666 .map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
1667 self.engine
1668 .coordinator()
1669 .execute_compiled_vector_search(&plan)
1670 }
1671}
1672
1673#[must_use]
1694pub struct SearchBuilder<'e> {
1695 engine: &'e Engine,
1696 root_kind: String,
1697 query: String,
1698 limit: usize,
1699 attribution_requested: bool,
1700 filter_builder: QueryBuilder,
1711}
1712
1713impl<'e> SearchBuilder<'e> {
1714 pub(crate) fn new(
1715 engine: &'e Engine,
1716 root_kind: impl Into<String>,
1717 query: impl Into<String>,
1718 limit: usize,
1719 ) -> Self {
1720 let root_kind = root_kind.into();
1721 let filter_builder = QueryBuilder::nodes(root_kind.clone()).text_search("", 0);
1722 Self {
1723 engine,
1724 root_kind,
1725 query: query.into(),
1726 limit,
1727 attribution_requested: false,
1728 filter_builder,
1729 }
1730 }
1731
1732 pub fn with_match_attribution(mut self) -> Self {
1737 self.attribution_requested = true;
1738 self
1739 }
1740
1741 pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
1743 self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
1744 self
1745 }
1746
1747 pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
1749 self.filter_builder = self.filter_builder.filter_kind_eq(kind);
1750 self
1751 }
1752
1753 pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
1755 self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
1756 self
1757 }
1758
1759 pub fn filter_content_ref_not_null(mut self) -> Self {
1761 self.filter_builder = self.filter_builder.filter_content_ref_not_null();
1762 self
1763 }
1764
1765 pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
1767 self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
1768 self
1769 }
1770
1771 pub fn filter_json_text_eq(
1773 mut self,
1774 path: impl Into<String>,
1775 value: impl Into<String>,
1776 ) -> Self {
1777 self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
1778 self
1779 }
1780
1781 pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
1783 self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
1784 self
1785 }
1786
1787 pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
1789 self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
1790 self
1791 }
1792
1793 pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
1795 self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
1796 self
1797 }
1798
1799 pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
1801 self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
1802 self
1803 }
1804
1805 pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
1807 self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
1808 self
1809 }
1810
1811 pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
1813 self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
1814 self
1815 }
1816
1817 pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
1819 self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
1820 self
1821 }
1822
1823 pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
1825 self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
1826 self
1827 }
1828
1829 pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
1831 self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
1832 self
1833 }
1834
1835 pub fn filter_json_fused_text_eq(
1843 mut self,
1844 path: impl Into<String>,
1845 value: impl Into<String>,
1846 ) -> Result<Self, BuilderValidationError> {
1847 let path = path.into();
1848 validate_fusable_property_path(
1849 self.engine,
1850 &self.root_kind,
1851 &path,
1852 "filter_json_fused_text_eq",
1853 )?;
1854 self.filter_builder = self
1855 .filter_builder
1856 .filter_json_fused_text_eq_unchecked(path, value);
1857 Ok(self)
1858 }
1859
1860 pub fn filter_json_fused_text_in(
1866 mut self,
1867 path: impl Into<String>,
1868 values: Vec<String>,
1869 ) -> Result<Self, BuilderValidationError> {
1870 let path = path.into();
1871 validate_fusable_property_path(
1872 self.engine,
1873 &self.root_kind,
1874 &path,
1875 "filter_json_fused_text_in",
1876 )?;
1877 self.filter_builder = self
1878 .filter_builder
1879 .filter_json_fused_text_in_unchecked(path, values);
1880 Ok(self)
1881 }
1882
1883 pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
1886 self.filter_builder = self.filter_builder.filter_json_text_in(path, values);
1887 self
1888 }
1889
1890 pub fn filter_json_fused_timestamp_gt(
1896 mut self,
1897 path: impl Into<String>,
1898 value: i64,
1899 ) -> Result<Self, BuilderValidationError> {
1900 let path = path.into();
1901 validate_fusable_property_path(
1902 self.engine,
1903 &self.root_kind,
1904 &path,
1905 "filter_json_fused_timestamp_gt",
1906 )?;
1907 self.filter_builder = self
1908 .filter_builder
1909 .filter_json_fused_timestamp_gt_unchecked(path, value);
1910 Ok(self)
1911 }
1912
1913 pub fn filter_json_fused_timestamp_gte(
1919 mut self,
1920 path: impl Into<String>,
1921 value: i64,
1922 ) -> Result<Self, BuilderValidationError> {
1923 let path = path.into();
1924 validate_fusable_property_path(
1925 self.engine,
1926 &self.root_kind,
1927 &path,
1928 "filter_json_fused_timestamp_gte",
1929 )?;
1930 self.filter_builder = self
1931 .filter_builder
1932 .filter_json_fused_timestamp_gte_unchecked(path, value);
1933 Ok(self)
1934 }
1935
1936 pub fn filter_json_fused_timestamp_lt(
1942 mut self,
1943 path: impl Into<String>,
1944 value: i64,
1945 ) -> Result<Self, BuilderValidationError> {
1946 let path = path.into();
1947 validate_fusable_property_path(
1948 self.engine,
1949 &self.root_kind,
1950 &path,
1951 "filter_json_fused_timestamp_lt",
1952 )?;
1953 self.filter_builder = self
1954 .filter_builder
1955 .filter_json_fused_timestamp_lt_unchecked(path, value);
1956 Ok(self)
1957 }
1958
1959 pub fn filter_json_fused_timestamp_lte(
1965 mut self,
1966 path: impl Into<String>,
1967 value: i64,
1968 ) -> Result<Self, BuilderValidationError> {
1969 let path = path.into();
1970 validate_fusable_property_path(
1971 self.engine,
1972 &self.root_kind,
1973 &path,
1974 "filter_json_fused_timestamp_lte",
1975 )?;
1976 self.filter_builder = self
1977 .filter_builder
1978 .filter_json_fused_timestamp_lte_unchecked(path, value);
1979 Ok(self)
1980 }
1981
1982 pub fn filter_json_fused_bool_eq(
1989 mut self,
1990 path: impl Into<String>,
1991 value: bool,
1992 ) -> Result<Self, BuilderValidationError> {
1993 let path = path.into();
1994 validate_fusable_property_path(
1995 self.engine,
1996 &self.root_kind,
1997 &path,
1998 "filter_json_fused_bool_eq",
1999 )?;
2000 self.filter_builder = self
2001 .filter_builder
2002 .filter_json_fused_bool_eq_unchecked(path, value);
2003 Ok(self)
2004 }
2005
2006 pub fn compile_plan(&self) -> Result<CompiledRetrievalPlan, CompileError> {
2013 let mut ast: QueryAst = self.filter_builder.clone().into_ast();
2020 ast.root_kind.clone_from(&self.root_kind);
2021 let mut replaced = false;
2022 for step in &mut ast.steps {
2023 if let QueryStep::TextSearch {
2024 query: TextQuery::Empty,
2025 limit: 0,
2026 } = step
2027 {
2028 *step = QueryStep::Search {
2029 query: self.query.clone(),
2030 limit: self.limit,
2031 };
2032 replaced = true;
2033 break;
2034 }
2035 }
2036 debug_assert!(
2037 replaced,
2038 "SearchBuilder filter accumulator must contain the seed TextSearch step"
2039 );
2040 let mut plan = fathomdb_query::compile_retrieval_plan(&ast)?;
2041 plan.text.strict.attribution_requested = self.attribution_requested;
2042 if let Some(relaxed) = plan.text.relaxed.as_mut() {
2043 relaxed.attribution_requested = self.attribution_requested;
2044 }
2045 Ok(plan)
2046 }
2047
2048 pub fn execute(&self) -> Result<SearchRows, EngineError> {
2053 let plan = self
2054 .compile_plan()
2055 .map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
2056 self.engine
2057 .coordinator()
2058 .execute_retrieval_plan(&plan, &self.query)
2059 }
2060
2061 pub fn expand(
2064 mut self,
2065 slot: impl Into<String>,
2066 direction: fathomdb_query::TraverseDirection,
2067 label: impl Into<String>,
2068 max_depth: usize,
2069 filter: Option<fathomdb_query::Predicate>,
2070 edge_filter: Option<fathomdb_query::Predicate>,
2071 ) -> Self {
2072 self.filter_builder =
2073 self.filter_builder
2074 .expand(slot, direction, label, max_depth, filter, edge_filter);
2075 self
2076 }
2077
2078 pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
2086 let mut ast: QueryAst = self.filter_builder.clone().into_ast();
2087 ast.root_kind.clone_from(&self.root_kind);
2088 let mut replaced = false;
2089 for step in &mut ast.steps {
2090 if let QueryStep::TextSearch {
2091 query: TextQuery::Empty,
2092 limit: 0,
2093 } = step
2094 {
2095 *step = QueryStep::TextSearch {
2096 query: TextQuery::parse(&self.query),
2097 limit: self.limit,
2098 };
2099 replaced = true;
2100 break;
2101 }
2102 }
2103 debug_assert!(
2104 replaced,
2105 "SearchBuilder filter accumulator must contain the seed TextSearch step"
2106 );
2107 compile_grouped_query(&ast)
2108 }
2109
2110 pub fn execute_grouped(self) -> Result<GroupedQueryRows, EngineError> {
2115 let compiled = self.compile_grouped().map_err(|e| {
2116 EngineError::InvalidConfig(format!("grouped query compilation failed: {e}"))
2117 })?;
2118 self.engine
2119 .coordinator()
2120 .execute_compiled_grouped_read(&compiled)
2121 }
2122}
2123
2124#[must_use]
2133pub struct SemanticSearchBuilder<'e> {
2134 engine: &'e Engine,
2135 root_kind: String,
2136 text: String,
2137 limit: usize,
2138}
2139
2140impl SemanticSearchBuilder<'_> {
2141 pub fn compile_plan(&self) -> Result<CompiledSemanticSearch, CompileError> {
2147 Ok(CompiledSemanticSearch {
2148 root_kind: self.root_kind.clone(),
2149 text: self.text.clone(),
2150 limit: self.limit,
2151 })
2152 }
2153
2154 pub fn execute(&self) -> Result<SearchRows, EngineError> {
2165 let plan = self
2166 .compile_plan()
2167 .map_err(|e| EngineError::InvalidConfig(format!("semantic_search compile: {e}")))?;
2168 self.engine
2169 .coordinator()
2170 .execute_compiled_semantic_search(&plan)
2171 }
2172}
2173
2174#[must_use]
2180pub struct RawVectorSearchBuilder<'e> {
2181 engine: &'e Engine,
2182 root_kind: String,
2183 vec: Vec<f32>,
2184 limit: usize,
2185}
2186
2187impl RawVectorSearchBuilder<'_> {
2188 pub fn compile_plan(&self) -> Result<CompiledRawVectorSearch, CompileError> {
2194 Ok(CompiledRawVectorSearch {
2195 root_kind: self.root_kind.clone(),
2196 vec: self.vec.clone(),
2197 limit: self.limit,
2198 })
2199 }
2200
2201 pub fn execute(&self) -> Result<SearchRows, EngineError> {
2211 let plan = self
2212 .compile_plan()
2213 .map_err(|e| EngineError::InvalidConfig(format!("raw_vector_search compile: {e}")))?;
2214 self.engine
2215 .coordinator()
2216 .execute_compiled_raw_vector_search(&plan)
2217 }
2218}
2219
2220#[cfg(test)]
2221#[allow(clippy::expect_used, clippy::panic)]
2222mod tests {
2223 use super::{FallbackSearchBuilder, VectorSearchBuilder};
2224 use crate::{BuilderValidationError, Engine, EngineOptions};
2225 use fathomdb_query::Predicate;
2226 use tempfile::NamedTempFile;
2227
2228 fn open_engine_with_schema(register: bool) -> (NamedTempFile, Engine) {
2229 let db = NamedTempFile::new().expect("temporary db");
2230 let engine = Engine::open(EngineOptions::new(db.path())).expect("engine opens");
2231 if register {
2232 engine
2233 .register_fts_property_schema(
2234 "Note",
2235 &["$.title".to_owned(), "$.body".to_owned()],
2236 None,
2237 )
2238 .expect("register fts property schema");
2239 }
2240 (db, engine)
2241 }
2242
2243 #[test]
2244 fn node_query_fused_text_eq_requires_registered_schema() {
2245 let (_db, engine) = open_engine_with_schema(false);
2246 let result = engine
2247 .query("Note")
2248 .filter_json_fused_text_eq("$.title", "hello");
2249 let Err(err) = result else {
2250 panic!("must reject fused filter without schema");
2251 };
2252 assert!(
2253 matches!(err, BuilderValidationError::MissingPropertyFtsSchema { ref kind } if kind == "Note"),
2254 "expected MissingPropertyFtsSchema, got {err:?}"
2255 );
2256 }
2257
2258 #[test]
2259 fn node_query_fused_text_eq_rejects_path_not_in_schema() {
2260 let (_db, engine) = open_engine_with_schema(true);
2261 let result = engine
2262 .query("Note")
2263 .filter_json_fused_text_eq("$.not_covered", "hello");
2264 let Err(err) = result else {
2265 panic!("path not in schema must be rejected");
2266 };
2267 assert!(
2268 matches!(err, BuilderValidationError::PathNotIndexed { ref kind, ref path } if kind == "Note" && path == "$.not_covered"),
2269 "expected PathNotIndexed, got {err:?}"
2270 );
2271 }
2272
2273 #[test]
2274 fn node_query_fused_text_eq_succeeds_with_registered_schema() {
2275 let (_db, engine) = open_engine_with_schema(true);
2276 let builder = engine
2277 .query("Note")
2278 .filter_json_fused_text_eq("$.title", "hello")
2279 .expect("fused filter with registered schema must succeed");
2280 let compiled = builder.compile().expect("compile");
2281 assert!(
2284 compiled.sql.contains("json_extract(src.properties, ?"),
2285 "fused filter must emit against src.properties, got {}",
2286 compiled.sql
2287 );
2288 }
2289
2290 #[test]
2291 fn text_search_fused_timestamp_gt_validates_and_compiles() {
2292 let (_db, engine) = open_engine_with_schema(true);
2293 engine
2295 .register_fts_property_schema("Note2", &["$.written_at".to_owned()], None)
2296 .expect("register Note2 schema");
2297 let builder = engine
2298 .query("Note2")
2299 .text_search("budget", 5)
2300 .filter_json_fused_timestamp_gt("$.written_at", 1_700_000_000)
2301 .expect("fused timestamp gt must succeed with schema");
2302 let _ = builder.compile().expect("compile succeeds");
2303 }
2304
2305 #[test]
2306 fn vector_search_fused_text_eq_validates() {
2307 let (_db, engine) = open_engine_with_schema(true);
2308 let result = VectorSearchBuilder::new(&engine, "NoSchema", "q", 5)
2309 .filter_json_fused_text_eq("$.title", "hello");
2310 let Err(err) = result else {
2311 panic!("missing schema must error");
2312 };
2313 assert!(
2314 matches!(err, BuilderValidationError::MissingPropertyFtsSchema { .. }),
2315 "expected MissingPropertyFtsSchema, got {err:?}"
2316 );
2317 let ok = VectorSearchBuilder::new(&engine, "Note", "q", 5)
2318 .filter_json_fused_text_eq("$.title", "hello");
2319 assert!(ok.is_ok(), "registered kind must succeed");
2320 }
2321
2322 #[test]
2323 fn fallback_search_fused_text_eq_requires_kind_binding() {
2324 let (_db, engine) = open_engine_with_schema(true);
2325 let result = FallbackSearchBuilder::new(&engine, "budget", None, 10)
2326 .filter_json_fused_text_eq("$.title", "hello");
2327 let Err(err) = result else {
2328 panic!("no kind binding must error");
2329 };
2330 assert!(
2331 matches!(err, BuilderValidationError::KindRequiredForFusion { .. }),
2332 "expected KindRequiredForFusion, got {err:?}"
2333 );
2334 let ok = FallbackSearchBuilder::new(&engine, "budget", None, 10)
2335 .filter_kind_eq("Note")
2336 .filter_json_fused_text_eq("$.title", "hello");
2337 assert!(ok.is_ok(), "kind-bound fallback fused filter must succeed");
2338 }
2339
2340 #[test]
2341 fn unified_search_fused_text_eq_validates() {
2342 let (_db, engine) = open_engine_with_schema(true);
2343 let ok = engine
2344 .query("Note")
2345 .search("hello", 5)
2346 .filter_json_fused_text_eq("$.title", "hello");
2347 assert!(
2348 ok.is_ok(),
2349 "unified search builder must accept fused filter"
2350 );
2351 let result = engine
2352 .query("Unknown")
2353 .search("hello", 5)
2354 .filter_json_fused_text_eq("$.title", "hello");
2355 let Err(err) = result else {
2356 panic!("missing schema must error");
2357 };
2358 assert!(matches!(
2359 err,
2360 BuilderValidationError::MissingPropertyFtsSchema { .. }
2361 ));
2362 }
2363
2364 #[test]
2365 fn existing_filter_json_text_eq_still_compiles_unchanged_regression() {
2366 let (_db, engine) = open_engine_with_schema(false);
2371 let compiled = engine
2372 .query("Note")
2373 .text_search("budget", 5)
2374 .filter_json_text_eq("$.status", "active")
2375 .compile()
2376 .expect("compile");
2377 assert!(
2378 compiled
2379 .sql
2380 .contains("\n AND json_extract(n.properties, ?"),
2381 "filter_json_text_eq must emit into outer WHERE, got {}",
2382 compiled.sql
2383 );
2384 }
2385
2386 #[test]
2398 fn fallback_builder_filter_kind_eq_fuses_without_explicit_text_search_step() {
2399 let db = NamedTempFile::new().expect("temporary db");
2400 let engine =
2401 Engine::open(EngineOptions::new(db.path())).expect("engine opens for unit test");
2402
2403 let builder = FallbackSearchBuilder::new(&engine, "budget", Some("budget OR nothing"), 10)
2404 .filter_kind_eq("Goal");
2405 let plan = builder.compile_plan().expect("compile plan");
2406
2407 assert!(
2408 plan.strict
2409 .fusable_filters
2410 .iter()
2411 .any(|p| matches!(p, Predicate::KindEq(k) if k == "Goal")),
2412 "KindEq(\"Goal\") must land in strict.fusable_filters (got {:?})",
2413 plan.strict.fusable_filters
2414 );
2415 assert!(
2416 plan.strict.residual_filters.is_empty(),
2417 "strict.residual_filters should be empty for a single kind filter (got {:?})",
2418 plan.strict.residual_filters
2419 );
2420
2421 let relaxed = plan
2422 .relaxed
2423 .as_ref()
2424 .expect("relaxed branch present when caller supplied a relaxed query");
2425 assert!(
2426 relaxed
2427 .fusable_filters
2428 .iter()
2429 .any(|p| matches!(p, Predicate::KindEq(k) if k == "Goal")),
2430 "KindEq(\"Goal\") must also land in relaxed.fusable_filters (got {:?})",
2431 relaxed.fusable_filters
2432 );
2433 }
2434}