@@ -827,6 +827,97 @@ public void testSortedNumericGcdNegativeValuesRangeIntoBitSet() throws Exception
827827 doTestSortedNumericGcdRangeIntoBitSet (true , 3 , 7 , -1_000_000_000L );
828828 }
829829
830+ /**
831+ * Tests rangeIntoBitSet on GCD-encoded sorted numeric with variable cardinality. Exercises the
832+ * per-doc fallback loop (rawValues != null, denseFixedCardinality == -1) rather than the
833+ * fixed-cardinality scalar fast path.
834+ */
835+ public void testSortedNumericGcdVariableCardinalityRangeIntoBitSet () throws Exception {
836+ int numDocs = 4096 * 2 ;
837+ long gcd = 100 ;
838+ long offset = 500_000L ;
839+ try (Directory dir = newDirectory ()) {
840+ IndexWriterConfig iwc = new IndexWriterConfig ().setCodec (new Lucene104Codec ());
841+ try (IndexWriter w = new IndexWriter (dir , iwc )) {
842+ for (int docID = 0 ; docID < numDocs ; docID ++) {
843+ Document doc = new Document ();
844+ long base = ((docID * 13L ) % 100 ) * gcd + offset ;
845+ int cardinality = (docID % 3 ) + 1 ;
846+ for (int i = 0 ; i < cardinality ; i ++) {
847+ doc .add (SortedNumericDocValuesField .indexedField ("sn" , base + i * gcd ));
848+ }
849+ w .addDocument (doc );
850+ }
851+ w .forceMerge (1 );
852+ }
853+
854+ try (DirectoryReader reader = DirectoryReader .open (dir )) {
855+ LeafReaderContext ctx = reader .leaves ().get (0 );
856+ long queryMin = 20 * gcd + offset ;
857+ long queryMax = 40 * gcd + offset ;
858+
859+ FixedBitSet expected = new FixedBitSet (numDocs );
860+ var expectedValues = ctx .reader ().getSortedNumericDocValues ("sn" );
861+ for (int docID = 0 ; docID < numDocs ; docID ++) {
862+ if (expectedValues .advanceExact (docID )) {
863+ for (int i = 0 , count = expectedValues .docValueCount (); i < count ; i ++) {
864+ long value = expectedValues .nextValue ();
865+ if (value >= queryMin ) {
866+ if (value <= queryMax ) {
867+ expected .set (docID );
868+ }
869+ break ;
870+ }
871+ }
872+ }
873+ }
874+
875+ FixedBitSet actual = new FixedBitSet (numDocs );
876+ ctx .reader ()
877+ .getSortedNumericDocValues ("sn" )
878+ .rangeIntoBitSet (0 , numDocs , queryMin , queryMax , actual , 0 );
879+ assertEquals ("GCD sorted numeric variable cardinality" , expected , actual );
880+ }
881+ }
882+ }
883+
884+ /**
885+ * Tests that rangeIntoBitSet correctly handles query bounds that fall between GCD multiples,
886+ * exercising the {@code transformGcdBounds} null early-exit (no raw value can match).
887+ */
888+ public void testSortedNumericGcdBoundsBetweenMultiplesRangeIntoBitSet () throws Exception {
889+ int numDocs = 4096 * 2 ;
890+ long gcd = 1000 ;
891+ long offset = 500_000L ;
892+ try (Directory dir = newDirectory ()) {
893+ IndexWriterConfig iwc = new IndexWriterConfig ().setCodec (new Lucene104Codec ());
894+ try (IndexWriter w = new IndexWriter (dir , iwc )) {
895+ for (int docID = 0 ; docID < numDocs ; docID ++) {
896+ Document doc = new Document ();
897+ long base = ((docID * 13L ) % 100 ) * gcd + offset ;
898+ for (int i = 0 ; i < 3 ; i ++) {
899+ doc .add (SortedNumericDocValuesField .indexedField ("sn" , base + i * gcd ));
900+ }
901+ w .addDocument (doc );
902+ }
903+ w .forceMerge (1 );
904+ }
905+
906+ try (DirectoryReader reader = DirectoryReader .open (dir )) {
907+ LeafReaderContext ctx = reader .leaves ().get (0 );
908+ long queryMin = 20 * gcd + offset + 1 ;
909+ long queryMax = 21 * gcd + offset - 1 ;
910+
911+ FixedBitSet actual = new FixedBitSet (numDocs );
912+ ctx .reader ()
913+ .getSortedNumericDocValues ("sn" )
914+ .rangeIntoBitSet (0 , numDocs , queryMin , queryMax , actual , 0 );
915+ assertEquals (
916+ "GCD bounds between multiples should match zero docs" , 0 , actual .cardinality ());
917+ }
918+ }
919+ }
920+
830921 private void doTestSortedNumericGcdRangeIntoBitSet (
831922 boolean dense , int cardinality , long gcd , long offset ) throws Exception {
832923 int numDocs = 4096 * 2 ;
@@ -891,6 +982,7 @@ public void testSortedNumericGcdRangeIntoBitSetRandomized() throws Exception {
891982 Random rng = random ();
892983 for (int iter = 0 ; iter < 10 ; iter ++) {
893984 boolean dense = rng .nextBoolean ();
985+ boolean variableCardinality = rng .nextBoolean ();
894986 int cardinality = rng .nextInt (1 , 6 );
895987 long gcd = rng .nextLong (2 , 1000 );
896988 long offset = rng .nextLong (0 , 1_000_000_000L );
@@ -903,7 +995,8 @@ public void testSortedNumericGcdRangeIntoBitSetRandomized() throws Exception {
903995 Document doc = new Document ();
904996 if (dense || docID % 3 != 0 ) {
905997 long base = rng .nextLong (0 , 100 ) * gcd + offset ;
906- for (int i = 0 ; i < cardinality ; i ++) {
998+ int docCardinality = variableCardinality ? rng .nextInt (1 , 6 ) : cardinality ;
999+ for (int i = 0 ; i < docCardinality ; i ++) {
9071000 doc .add (SortedNumericDocValuesField .indexedField ("sn" , base + i * gcd ));
9081001 }
9091002 }
@@ -916,6 +1009,10 @@ public void testSortedNumericGcdRangeIntoBitSetRandomized() throws Exception {
9161009 LeafReaderContext ctx = reader .leaves ().get (0 );
9171010 long queryMin = rng .nextLong (0 , 50 ) * gcd + offset ;
9181011 long queryMax = rng .nextLong (50 , 100 ) * gcd + offset ;
1012+ if (rng .nextBoolean ()) {
1013+ queryMin += rng .nextLong (1 , gcd );
1014+ queryMax -= rng .nextLong (1 , gcd );
1015+ }
9191016
9201017 FixedBitSet expected = new FixedBitSet (numDocs );
9211018 var expectedValues = ctx .reader ().getSortedNumericDocValues ("sn" );
0 commit comments