@@ -897,6 +897,97 @@ public void testSortedNumericGcdNegativeValuesRangeIntoBitSet() throws Exception
897897 doTestSortedNumericGcdRangeIntoBitSet (true , 3 , 7 , -1_000_000_000L );
898898 }
899899
900+ /**
901+ * Tests rangeIntoBitSet on GCD-encoded sorted numeric with variable cardinality. Exercises the
902+ * per-doc fallback loop (rawValues != null, denseFixedCardinality == -1) rather than the
903+ * fixed-cardinality scalar fast path.
904+ */
905+ public void testSortedNumericGcdVariableCardinalityRangeIntoBitSet () throws Exception {
906+ int numDocs = 4096 * 2 ;
907+ long gcd = 100 ;
908+ long offset = 500_000L ;
909+ try (Directory dir = newDirectory ()) {
910+ IndexWriterConfig iwc = new IndexWriterConfig ().setCodec (new Lucene104Codec ());
911+ try (IndexWriter w = new IndexWriter (dir , iwc )) {
912+ for (int docID = 0 ; docID < numDocs ; docID ++) {
913+ Document doc = new Document ();
914+ long base = ((docID * 13L ) % 100 ) * gcd + offset ;
915+ int cardinality = (docID % 3 ) + 1 ;
916+ for (int i = 0 ; i < cardinality ; i ++) {
917+ doc .add (SortedNumericDocValuesField .indexedField ("sn" , base + i * gcd ));
918+ }
919+ w .addDocument (doc );
920+ }
921+ w .forceMerge (1 );
922+ }
923+
924+ try (DirectoryReader reader = DirectoryReader .open (dir )) {
925+ LeafReaderContext ctx = reader .leaves ().get (0 );
926+ long queryMin = 20 * gcd + offset ;
927+ long queryMax = 40 * gcd + offset ;
928+
929+ FixedBitSet expected = new FixedBitSet (numDocs );
930+ var expectedValues = ctx .reader ().getSortedNumericDocValues ("sn" );
931+ for (int docID = 0 ; docID < numDocs ; docID ++) {
932+ if (expectedValues .advanceExact (docID )) {
933+ for (int i = 0 , count = expectedValues .docValueCount (); i < count ; i ++) {
934+ long value = expectedValues .nextValue ();
935+ if (value >= queryMin ) {
936+ if (value <= queryMax ) {
937+ expected .set (docID );
938+ }
939+ break ;
940+ }
941+ }
942+ }
943+ }
944+
945+ FixedBitSet actual = new FixedBitSet (numDocs );
946+ ctx .reader ()
947+ .getSortedNumericDocValues ("sn" )
948+ .rangeIntoBitSet (0 , numDocs , queryMin , queryMax , actual , 0 );
949+ assertEquals ("GCD sorted numeric variable cardinality" , expected , actual );
950+ }
951+ }
952+ }
953+
954+ /**
955+ * Tests that rangeIntoBitSet correctly handles query bounds that fall between GCD multiples,
956+ * exercising the {@code transformGcdBounds} null early-exit (no raw value can match).
957+ */
958+ public void testSortedNumericGcdBoundsBetweenMultiplesRangeIntoBitSet () throws Exception {
959+ int numDocs = 4096 * 2 ;
960+ long gcd = 1000 ;
961+ long offset = 500_000L ;
962+ try (Directory dir = newDirectory ()) {
963+ IndexWriterConfig iwc = new IndexWriterConfig ().setCodec (new Lucene104Codec ());
964+ try (IndexWriter w = new IndexWriter (dir , iwc )) {
965+ for (int docID = 0 ; docID < numDocs ; docID ++) {
966+ Document doc = new Document ();
967+ long base = ((docID * 13L ) % 100 ) * gcd + offset ;
968+ for (int i = 0 ; i < 3 ; i ++) {
969+ doc .add (SortedNumericDocValuesField .indexedField ("sn" , base + i * gcd ));
970+ }
971+ w .addDocument (doc );
972+ }
973+ w .forceMerge (1 );
974+ }
975+
976+ try (DirectoryReader reader = DirectoryReader .open (dir )) {
977+ LeafReaderContext ctx = reader .leaves ().get (0 );
978+ long queryMin = 20 * gcd + offset + 1 ;
979+ long queryMax = 21 * gcd + offset - 1 ;
980+
981+ FixedBitSet actual = new FixedBitSet (numDocs );
982+ ctx .reader ()
983+ .getSortedNumericDocValues ("sn" )
984+ .rangeIntoBitSet (0 , numDocs , queryMin , queryMax , actual , 0 );
985+ assertEquals (
986+ "GCD bounds between multiples should match zero docs" , 0 , actual .cardinality ());
987+ }
988+ }
989+ }
990+
900991 private void doTestSortedNumericGcdRangeIntoBitSet (
901992 boolean dense , int cardinality , long gcd , long offset ) throws Exception {
902993 int numDocs = 4096 * 2 ;
@@ -961,6 +1052,7 @@ public void testSortedNumericGcdRangeIntoBitSetRandomized() throws Exception {
9611052 Random rng = random ();
9621053 for (int iter = 0 ; iter < 10 ; iter ++) {
9631054 boolean dense = rng .nextBoolean ();
1055+ boolean variableCardinality = rng .nextBoolean ();
9641056 int cardinality = rng .nextInt (1 , 6 );
9651057 long gcd = rng .nextLong (2 , 1000 );
9661058 long offset = rng .nextLong (0 , 1_000_000_000L );
@@ -973,7 +1065,8 @@ public void testSortedNumericGcdRangeIntoBitSetRandomized() throws Exception {
9731065 Document doc = new Document ();
9741066 if (dense || docID % 3 != 0 ) {
9751067 long base = rng .nextLong (0 , 100 ) * gcd + offset ;
976- for (int i = 0 ; i < cardinality ; i ++) {
1068+ int docCardinality = variableCardinality ? rng .nextInt (1 , 6 ) : cardinality ;
1069+ for (int i = 0 ; i < docCardinality ; i ++) {
9771070 doc .add (SortedNumericDocValuesField .indexedField ("sn" , base + i * gcd ));
9781071 }
9791072 }
@@ -986,6 +1079,10 @@ public void testSortedNumericGcdRangeIntoBitSetRandomized() throws Exception {
9861079 LeafReaderContext ctx = reader .leaves ().get (0 );
9871080 long queryMin = rng .nextLong (0 , 50 ) * gcd + offset ;
9881081 long queryMax = rng .nextLong (50 , 100 ) * gcd + offset ;
1082+ if (rng .nextBoolean ()) {
1083+ queryMin += rng .nextLong (1 , gcd );
1084+ queryMax -= rng .nextLong (1 , gcd );
1085+ }
9891086
9901087 FixedBitSet expected = new FixedBitSet (numDocs );
9911088 var expectedValues = ctx .reader ().getSortedNumericDocValues ("sn" );
0 commit comments