diff --git a/src/core/search/ast_expr.cc b/src/core/search/ast_expr.cc index 1de1f636a211..fe5e3a9bb590 100644 --- a/src/core/search/ast_expr.cc +++ b/src/core/search/ast_expr.cc @@ -7,6 +7,7 @@ #include #include +#include #include #include "base/logging.h" @@ -18,7 +19,8 @@ namespace dfly::search { AstTermNode::AstTermNode(string term) : term{term} { } -AstRangeNode::AstRangeNode(double lo, double hi) : lo{lo}, hi{hi} { +AstRangeNode::AstRangeNode(double lo, bool lo_excl, double hi, bool hi_excl) + : lo{lo_excl ? nextafter(lo, hi) : lo}, hi{hi_excl ? nextafter(hi, lo) : hi} { } AstNegateNode::AstNegateNode(AstNode&& node) : node{make_unique(move(node))} { diff --git a/src/core/search/ast_expr.h b/src/core/search/ast_expr.h index 0da4193377db..2d6e8bdf40e7 100644 --- a/src/core/search/ast_expr.h +++ b/src/core/search/ast_expr.h @@ -31,7 +31,7 @@ struct AstTermNode { // Matches numeric range struct AstRangeNode { - AstRangeNode(double lo, double hi); + AstRangeNode(double lo, bool lo_excl, double hi, bool hi_excl); double lo, hi; }; diff --git a/src/core/search/parser.y b/src/core/search/parser.y index 6acc3040b4a1..9bce8009a5f0 100644 --- a/src/core/search/parser.y +++ b/src/core/search/parser.y @@ -75,7 +75,8 @@ using namespace std; %token DOUBLE "double" %token UINT32 "uint32" %nterm generic_number -%nterm final_query filter search_expr search_unary_expr search_or_expr search_and_expr +%nterm opt_lparen +%nterm final_query filter search_expr search_unary_expr search_or_expr search_and_expr numeric_filter_expr %nterm field_cond field_cond_expr field_unary_expr field_or_expr field_and_expr tag_list %nterm knn_query @@ -128,9 +129,20 @@ field_cond: | UINT32 { $$ = AstTermNode(to_string($1)); } | NOT_OP field_cond { $$ = AstNegateNode(move($2)); } | LPAREN field_cond_expr RPAREN { $$ = move($2); } - | LBRACKET generic_number generic_number RBRACKET { $$ = AstRangeNode(move($2), move($3)); } + | LBRACKET numeric_filter_expr RBRACKET { $$ = move($2); } | LCURLBR tag_list RCURLBR { $$ = move($2); } +numeric_filter_expr: +opt_lparen generic_number opt_lparen generic_number { $$ = AstRangeNode($2, $1, $4, $3); } + +generic_number: + DOUBLE { $$ = $1; } + | UINT32 { $$ = $1; } + +opt_lparen: + /* empty */ { $$ = false; } + | LPAREN { $$ = true; } + field_cond_expr: field_unary_expr { $$ = move($1); } | field_and_expr { $$ = move($1); } @@ -156,9 +168,6 @@ tag_list: | tag_list OR_OP TERM { $$ = AstTagsNode(move($1), move($3)); } | tag_list OR_OP DOUBLE { $$ = AstTagsNode(move($1), to_string($3)); } -generic_number: - DOUBLE { $$ = $1; } - | UINT32 { $$ = $1; } %% diff --git a/src/core/search/search_test.cc b/src/core/search/search_test.cc index a8c58beb3099..c0b384ae208f 100644 --- a/src/core/search/search_test.cc +++ b/src/core/search/search_test.cc @@ -283,14 +283,27 @@ TEST_F(SearchTest, MatchRange) { TEST_F(SearchTest, MatchDoubleRange) { PrepareSchema({{"f1", SchemaField::NUMERIC}}); - PrepareQuery("@f1: [100.03 199.97]"); - ExpectAll(Map{{"f1", "130"}}, Map{{"f1", "170"}}, Map{{"f1", "100.03"}}, Map{{"f1", "199.97"}}); + { + PrepareQuery("@f1: [100.03 199.97]"); - ExpectNone(Map{{"f1", "0"}}, Map{{"f1", "200"}}, Map{{"f1", "100.02999"}}, - Map{{"f1", "199.9700001"}}); + ExpectAll(Map{{"f1", "130"}}, Map{{"f1", "170"}}, Map{{"f1", "100.03"}}, Map{{"f1", "199.97"}}); - EXPECT_TRUE(Check()) << GetError(); + ExpectNone(Map{{"f1", "0"}}, Map{{"f1", "200"}}, Map{{"f1", "100.02999"}}, + Map{{"f1", "199.9700001"}}); + + EXPECT_TRUE(Check()) << GetError(); + } + + { + PrepareQuery("@f1: [(100 (199.9]"); + + ExpectAll(Map{{"f1", "150"}}, Map{{"f1", "100.00001"}}, Map{{"f1", "199.8999999"}}); + + ExpectNone(Map{{"f1", "50"}}, Map{{"f1", "100"}}, Map{{"f1", "199.9"}}, Map{{"f1", "200"}}); + + EXPECT_TRUE(Check()) << GetError(); + } } TEST_F(SearchTest, MatchStar) { diff --git a/tests/dragonfly/search_test.py b/tests/dragonfly/search_test.py index 115aff0b6328..1ab6ed9483b6 100644 --- a/tests/dragonfly/search_test.py +++ b/tests/dragonfly/search_test.py @@ -461,10 +461,11 @@ def make_car(producer, description, speed): CARS = [ make_car("BMW", "Very fast and elegant", 200), make_car("Audi", "Fast & stylish", 170), - make_car("Mercedes", "High class for high prices", 150), + make_car("Mercedes", "High class but expensive!", 150), make_car("Honda", "Good allrounder with flashy looks", 120), make_car("Peugeot", "Good allrounder for the whole family", 100), make_car("Mini", "Fashinable cooper for the big city", 80), + make_car("John Deere", "It's not a car, it's a tractor in fact!", 50), ] for car in CARS: @@ -475,7 +476,7 @@ def make_car(producer, description, speed): # Get all cars assert extract_producers(TestCar.find().all()) == extract_producers(CARS) - # Get all cars which Audi or Honda + # Get all cars of a specific producer assert extract_producers( TestCar.find((TestCar.producer == "Peugeot") | (TestCar.producer == "Mini")) ) == ["Mini", "Peugeot"] @@ -485,16 +486,29 @@ def make_car(producer, description, speed): [c for c in CARS if c.speed >= 150] ) + # Get only slow cars + assert extract_producers(TestCar.find(TestCar.speed < 100).all()) == extract_producers( + [c for c in CARS if c.speed < 100] + ) + # Get all cars which are fast based on description assert extract_producers(TestCar.find(TestCar.description % "fast")) == ["Audi", "BMW"] + # Get all cars which are not marked as extensive by descriptions + assert extract_producers( + TestCar.find(~(TestCar.description % "expensive")).all() + ) == extract_producers([c for c in CARS if c.producer != "Mercedes"]) + # Get a fast allrounder assert extract_producers( TestCar.find((TestCar.speed >= 110) & (TestCar.description % "allrounder")) ) == ["Honda"] # What's the slowest car - assert extract_producers([TestCar.find().sort_by("speed").first()]) == ["Mini"] + assert extract_producers([TestCar.find().sort_by("speed").first()]) == ["John Deere"] + + # What's the fastest car + assert extract_producers([TestCar.find().sort_by("-speed").first()]) == ["BMW"] for index in client.execute_command("FT._LIST"): client.ft(index.decode()).dropindex()