2424import org .apache .lucene .search .TotalHits .Relation ;
2525import org .apache .lucene .store .Directory ;
2626import org .apache .lucene .tests .index .RandomIndexWriter ;
27+ import org .opensearch .common .time .DateFormatter ;
28+ import org .opensearch .common .time .DateMathParser ;
29+ import org .opensearch .index .mapper .DateFieldMapper ;
30+ import org .opensearch .index .mapper .DateFieldMapper .DateFieldType ;
31+ import org .opensearch .index .mapper .DateFieldMapper .Resolution ;
32+ import org .opensearch .index .query .DateRangeIncludingNowQuery ;
2733import org .opensearch .search .internal .SearchContext ;
2834import org .opensearch .search .sort .SortOrder ;
2935import org .opensearch .test .OpenSearchTestCase ;
@@ -730,6 +736,85 @@ public void testNycTaxiDataDistribution() throws IOException {
730736 }
731737 }
732738
739+ public void testDateRangeIncludingNowQueryApproximation () throws IOException {
740+ try (Directory directory = newDirectory ()) {
741+ try (RandomIndexWriter iw = new RandomIndexWriter (random (), directory , new WhitespaceAnalyzer ())) {
742+ int dims = 1 ;
743+
744+ DateFieldType dateFieldType = new DateFieldType ("@timestamp" );
745+ long minute = 60 * 1000L ;
746+
747+ long currentTime = System .currentTimeMillis ();
748+ long startTimestamp = currentTime - (48 * 60 * minute ); // Start 48 hours ago
749+
750+ // Create 10000 documents with 1 minute intervals
751+ for (int i = 0 ; i < 10000 ; i ++) {
752+ long timestamp = startTimestamp + (i * minute );
753+
754+ iw .addDocument (asList (new LongPoint ("@timestamp" , timestamp ), new NumericDocValuesField ("@timestamp" , timestamp )));
755+ }
756+
757+ iw .flush ();
758+ iw .forceMerge (1 );
759+ try (IndexReader reader = iw .getReader ()) {
760+ IndexSearcher searcher = new IndexSearcher (reader );
761+
762+ testApproximateVsExactDateQuery (searcher , "@timestamp" , "now-1h" , "now" , 50 , dims );
763+
764+ testApproximateVsExactDateQuery (searcher , "@timestamp" , "now-1d" , "now" , 50 , dims );
765+
766+ testApproximateVsExactDateQuery (searcher , "@timestamp" , "now-30m" , "now+30m" , 50 , dims );
767+
768+ }
769+ }
770+ }
771+ }
772+
773+ private void testApproximateVsExactDateQuery (
774+ IndexSearcher searcher ,
775+ String field ,
776+ String lowerBound ,
777+ String upperBound ,
778+ int size ,
779+ int dims
780+ ) throws IOException {
781+ // Parse date expressions to milliseconds
782+ DateFormatter formatter = DateFieldMapper .getDefaultDateTimeFormatter ();
783+ DateMathParser parser = formatter .toDateMathParser ();
784+ long nowInMillis = System .currentTimeMillis ();
785+
786+ long lower = Resolution .MILLISECONDS .convert (parser .parse (lowerBound , () -> nowInMillis ));
787+ long upper = Resolution .MILLISECONDS .convert (parser .parse (upperBound , () -> nowInMillis ));
788+
789+ Query exactQuery = LongPoint .newRangeQuery (field , lower , upper );
790+
791+ // Wrap with DateRangeIncludingNowQuery if using "now"
792+ if (lowerBound .contains ("now" ) || upperBound .contains ("now" )) {
793+ exactQuery = new DateRangeIncludingNowQuery (exactQuery );
794+
795+ assertTrue (
796+ "Query should be wrapped in DateRangeIncludingNowQuery when using 'now'" ,
797+ exactQuery instanceof DateRangeIncludingNowQuery
798+ );
799+
800+ exactQuery = ((DateRangeIncludingNowQuery ) exactQuery ).getQuery ();
801+ }
802+
803+ ApproximatePointRangeQuery approximateQuery = new ApproximatePointRangeQuery (
804+ field ,
805+ pack (new long [] { lower }).bytes ,
806+ pack (new long [] { upper }).bytes ,
807+ 1 ,
808+ ApproximatePointRangeQuery .LONG_FORMAT
809+ );
810+ approximateQuery .setSize (size );
811+
812+ TopDocs exactDocs = searcher .search (exactQuery , size );
813+ TopDocs approxDocs = searcher .search (approximateQuery , size );
814+
815+ verifyRangeQueries (searcher , exactQuery , approxDocs , exactDocs , field , lower , upper , size , dims );
816+ }
817+
733818 private void testApproximateVsExactQuery (IndexSearcher searcher , String field , long lower , long upper , int size , int dims )
734819 throws IOException {
735820 // Test with approximate query
@@ -746,6 +831,21 @@ private void testApproximateVsExactQuery(IndexSearcher searcher, String field, l
746831 Query exactQuery = LongPoint .newRangeQuery (field , lower , upper );
747832 TopDocs approxDocs = searcher .search (approxQuery , size );
748833 TopDocs exactDocs = searcher .search (exactQuery , size );
834+
835+ verifyRangeQueries (searcher , exactQuery , approxDocs , exactDocs , field , lower , upper , size , dims );
836+ }
837+
838+ private void verifyRangeQueries (
839+ IndexSearcher searcher ,
840+ Query exactQuery ,
841+ TopDocs approxDocs ,
842+ TopDocs exactDocs ,
843+ String field ,
844+ long lower ,
845+ long upper ,
846+ int size ,
847+ int dims
848+ ) throws IOException {
749849 // Verify approximate query returns correct number of results
750850 assertTrue ("Approximate query should return at most " + size + " docs" , approxDocs .scoreDocs .length <= size );
751851 // If exact query returns fewer docs than size, approximate should match
0 commit comments