@@ -35,8 +35,18 @@ pub struct JsonPathFilter {
3535pub enum JsonPredicate {
3636 Text ( String ) ,
3737
38- IntRange { lower : Option < i64 > , upper : Option < i64 > } ,
39- FloatRange { lower : Option < f64 > , upper : Option < f64 > } ,
38+ IntRange {
39+ lower : Option < i64 > ,
40+ upper : Option < i64 > ,
41+ } ,
42+ FloatRange {
43+ lower : Option < f64 > ,
44+ upper : Option < f64 > ,
45+ } ,
46+ DateRange {
47+ lower : Option < tantivy:: DateTime > ,
48+ upper : Option < tantivy:: DateTime > ,
49+ } ,
4050 Boolean ( bool ) ,
4151}
4252
@@ -63,9 +73,13 @@ fn build_leaf_query(filter: &JsonPathFilter, json_field: Field) -> Box<dyn Query
6373
6474 match & filter. predicate {
6575 JsonPredicate :: Text ( val) => {
66- let mut term = Term :: from_field_json_path ( json_field, & path, false ) ;
76+ // Use the fast field to do exact match
77+ let mut term = Term :: from_field_json_path ( json_field, & path, true ) ;
6778 term. append_type_and_str ( val) ;
68- Box :: new ( TermQuery :: new ( term, IndexRecordOption :: Basic ) )
79+ Box :: new ( FastFieldRangeQuery :: new (
80+ Bound :: Included ( term. clone ( ) ) ,
81+ Bound :: Included ( term) ,
82+ ) )
6983 }
7084
7185 JsonPredicate :: IntRange { lower, upper } => {
@@ -101,6 +115,20 @@ fn build_leaf_query(filter: &JsonPathFilter, json_field: Field) -> Box<dyn Query
101115 term. append_type_and_fast_value ( * val) ;
102116 Box :: new ( TermQuery :: new ( term, IndexRecordOption :: Basic ) )
103117 }
118+
119+ JsonPredicate :: DateRange { lower, upper } => {
120+ let build_bound = |opt : & Option < tantivy:: DateTime > | -> Bound < Term > {
121+ match opt {
122+ None => Bound :: Unbounded ,
123+ Some ( v) => {
124+ let mut term = Term :: from_field_json_path ( json_field, & path, false ) ;
125+ term. append_type_and_fast_value ( * v) ;
126+ Bound :: Included ( term)
127+ }
128+ }
129+ } ;
130+ Box :: new ( FastFieldRangeQuery :: new ( build_bound ( lower) , build_bound ( upper) ) )
131+ }
104132 }
105133}
106134pub ( crate ) fn build_tantivy_query ( expr : & JsonFilterExpression , json_field : Field ) -> Box < dyn Query > {
@@ -247,11 +275,18 @@ mod tests {
247275 #[ test]
248276 fn test_exact_match ( ) {
249277 let ( svc, apple, _banana, _cherry) = build_test_index ( ) ;
278+ // Full stored value matches.
250279 let results = search (
251280 & svc,
252- path ( "t/product" , "name" , JsonPredicate :: Text ( "apple" . to_string ( ) ) ) ,
281+ path ( "t/product" , "name" , JsonPredicate :: Text ( "red apple" . to_string ( ) ) ) ,
253282 ) ;
254283 assert ! ( results. contains( & apple) ) ;
284+ // Partial token no longer matches (fast-field exact match, not token lookup).
285+ let results = search (
286+ & svc,
287+ path ( "t/product" , "name" , JsonPredicate :: Text ( "apple" . to_string ( ) ) ) ,
288+ ) ;
289+ assert ! ( !results. contains( & apple) ) ;
255290 }
256291
257292 #[ test]
@@ -382,4 +417,148 @@ mod tests {
382417 assert ! ( results. contains( & banana) ) ;
383418 assert ! ( !results. contains( & cherry) ) ;
384419 }
420+
421+ fn build_date_index ( ) -> ( JsonReaderService , Uuid , Uuid , Uuid ) {
422+ let schema = JsonSchema :: new ( ) ;
423+ let index = Index :: create_in_ram ( schema. schema . clone ( ) ) ;
424+ let mut writer: IndexWriter = index. writer ( 15_000_000 ) . expect ( "writer failed" ) ;
425+
426+ let old_id = Uuid :: parse_str ( "00000000000000000000000000000011" ) . unwrap ( ) ;
427+ let mid_id = Uuid :: parse_str ( "00000000000000000000000000000012" ) . unwrap ( ) ;
428+ let new_id = Uuid :: parse_str ( "00000000000000000000000000000013" ) . unwrap ( ) ;
429+
430+ let dt = |secs : i64 | OwnedValue :: Date ( tantivy:: DateTime :: from_timestamp_secs ( secs) ) ;
431+
432+ // old: 2020-01-01 00:00:00 UTC (1577836800)
433+ add_doc (
434+ & mut writer,
435+ & schema,
436+ old_id,
437+ "t/event" ,
438+ vec ! [ ( "ts" . to_string( ) , dt( 1577836800 ) ) ] ,
439+ ) ;
440+ // mid: 2022-06-15 00:00:00 UTC (1655251200)
441+ add_doc (
442+ & mut writer,
443+ & schema,
444+ mid_id,
445+ "t/event" ,
446+ vec ! [ ( "ts" . to_string( ) , dt( 1655251200 ) ) ] ,
447+ ) ;
448+ // new: 2024-01-01 00:00:00 UTC (1704067200)
449+ add_doc (
450+ & mut writer,
451+ & schema,
452+ new_id,
453+ "t/event" ,
454+ vec ! [ ( "ts" . to_string( ) , dt( 1704067200 ) ) ] ,
455+ ) ;
456+
457+ writer. commit ( ) . expect ( "commit failed" ) ;
458+ let reader = index
459+ . reader_builder ( )
460+ . reload_policy ( tantivy:: ReloadPolicy :: Manual )
461+ . try_into ( )
462+ . expect ( "reader failed" ) ;
463+ ( JsonReaderService { index, schema, reader } , old_id, mid_id, new_id)
464+ }
465+
466+ #[ test]
467+ fn test_exact_match_text_field ( ) {
468+ // Verifies that JsonPredicate::Text does a true exact match against the
469+ // fast-field (columnar) value — no tokenization, case-sensitive.
470+ let schema = JsonSchema :: new ( ) ;
471+ let index = Index :: create_in_ram ( schema. schema . clone ( ) ) ;
472+ let mut writer: IndexWriter = index. writer ( 15_000_000 ) . expect ( "writer failed" ) ;
473+
474+ let id = Uuid :: parse_str ( "00000000000000000000000000000099" ) . unwrap ( ) ;
475+ add_doc (
476+ & mut writer,
477+ & schema,
478+ id,
479+ "k/product" ,
480+ vec ! [ ( "color" . to_string( ) , OwnedValue :: Str ( "Red Apple" . to_string( ) ) ) ] ,
481+ ) ;
482+ writer. commit ( ) . expect ( "commit failed" ) ;
483+ let reader = index
484+ . reader_builder ( )
485+ . reload_policy ( tantivy:: ReloadPolicy :: Manual )
486+ . try_into ( )
487+ . expect ( "reader failed" ) ;
488+ let svc = JsonReaderService { index, schema, reader } ;
489+
490+ // Exact full value matches.
491+ let results = search (
492+ & svc,
493+ path ( "k/product" , "color" , JsonPredicate :: Text ( "Red Apple" . to_string ( ) ) ) ,
494+ ) ;
495+ assert ! ( results. contains( & id) , "exact full value should match" ) ;
496+
497+ // Partial token does NOT match.
498+ let results = search ( & svc, path ( "k/product" , "color" , JsonPredicate :: Text ( "red" . to_string ( ) ) ) ) ;
499+ assert ! ( !results. contains( & id) , "partial/lowercased token should not match" ) ;
500+
501+ // Wrong case does NOT match (fast-field match is case-sensitive).
502+ let results = search (
503+ & svc,
504+ path ( "k/product" , "color" , JsonPredicate :: Text ( "red apple" . to_string ( ) ) ) ,
505+ ) ;
506+ assert ! ( !results. contains( & id) , "wrong case should not match" ) ;
507+ }
508+
509+ #[ test]
510+ fn test_date_range_bounded ( ) {
511+ let ( svc, _old, mid, _new) = build_date_index ( ) ;
512+ // [2021-01-01 .. 2023-01-01]
513+ let results = search (
514+ & svc,
515+ path (
516+ "t/event" ,
517+ "ts" ,
518+ JsonPredicate :: DateRange {
519+ lower : Some ( tantivy:: DateTime :: from_timestamp_secs ( 1609459200 ) ) , // 2021
520+ upper : Some ( tantivy:: DateTime :: from_timestamp_secs ( 1672531200 ) ) , // 2023
521+ } ,
522+ ) ,
523+ ) ;
524+ assert_eq ! ( results, HashSet :: from( [ mid] ) ) ;
525+ }
526+
527+ #[ test]
528+ fn test_date_range_unbounded_upper ( ) {
529+ let ( svc, _old, mid, new) = build_date_index ( ) ;
530+ // [2022-01-01 .. ]
531+ let results = search (
532+ & svc,
533+ path (
534+ "t/event" ,
535+ "ts" ,
536+ JsonPredicate :: DateRange {
537+ lower : Some ( tantivy:: DateTime :: from_timestamp_secs ( 1640995200 ) ) , // 2022
538+ upper : None ,
539+ } ,
540+ ) ,
541+ ) ;
542+ assert ! ( results. contains( & mid) ) ;
543+ assert ! ( results. contains( & new) ) ;
544+ assert ! ( !results. contains( & _old) ) ;
545+ }
546+
547+ #[ test]
548+ fn test_date_range_unbounded_lower ( ) {
549+ let ( svc, old, _mid, _new) = build_date_index ( ) ;
550+ // [ .. 2021-01-01]
551+ let results = search (
552+ & svc,
553+ path (
554+ "t/event" ,
555+ "ts" ,
556+ JsonPredicate :: DateRange {
557+ lower : None ,
558+ upper : Some ( tantivy:: DateTime :: from_timestamp_secs ( 1609459200 ) ) , // 2021
559+ } ,
560+ ) ,
561+ ) ;
562+ assert_eq ! ( results, HashSet :: from( [ old] ) ) ;
563+ }
385564}
0 commit comments