Skip to content

Commit 09b3b96

Browse files
Fix IndexOutOfBoundsException when running include/exclude with non-existent prefix term (#19637)
Signed-off-by: Harsha Vamsi Kalluri <harshavamsi096@gmail.com> Signed-off-by: Ankit Jain <jainankitk@apache.org> Co-authored-by: Ankit Jain <jainankitk@apache.org>
1 parent 80141d5 commit 09b3b96

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4242
- [Java Agent] Allow JRT protocol URLs in protection domain extraction ([#19683](https://github.com/opensearch-project/OpenSearch/pull/19683))
4343
- Fix potential concurrent modification exception when updating allocation filters ([#19701])(https://github.com/opensearch-project/OpenSearch/pull/19701))
4444
- Fix file-based ingestion consumer to handle start point beyond max line number([#19757])(https://github.com/opensearch-project/OpenSearch/pull/19757))
45+
- Fix IndexOutOfBoundsException when running include/exclude on non-existent prefix in terms aggregations ([#19637](https://github.com/opensearch-project/OpenSearch/pull/19637))
4546
- Fixed assertion unsafe use of ClusterService.state() in ResourceUsageCollectorService ([#19775])(https://github.com/opensearch-project/OpenSearch/pull/19775))
4647
- Add S3Repository.LEGACY_MD5_CHECKSUM_CALCULATION to list of repository-s3 settings ([#19788](https://github.com/opensearch-project/OpenSearch/pull/19788))
4748

server/src/main/java/org/opensearch/search/aggregations/bucket/terms/IncludeExclude.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,10 @@ private static void process(SortedSetDocValues globalOrdinals, long length, Sort
440440
if (startOrd < 0) {
441441
// The prefix is not an exact match in the ordinals (can skip equal length below)
442442
startOrd = -1 - startOrd;
443+
// Check bounds before calling lookupOrd to avoid IndexOutOfBoundsException
444+
if (startOrd >= length) {
445+
continue;
446+
}
443447
// Make sure that the term at startOrd starts with prefix
444448
BytesRef startTerm = globalOrdinals.lookupOrd(startOrd);
445449
if (startTerm.length <= prefix.length
@@ -453,8 +457,8 @@ private static void process(SortedSetDocValues globalOrdinals, long length, Sort
453457
)) {
454458
continue;
455459
}
456-
}
457-
if (startOrd >= length) {
460+
} else if (startOrd >= length) {
461+
// Exact match found, but out of bounds
458462
continue;
459463
}
460464
BytesRef next = nextBytesRef(prefix);

server/src/test/java/org/opensearch/search/aggregations/bucket/terms/IncludeExcludeTests.java

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,4 +545,153 @@ public void testOnlyIncludeExcludePrefix() throws IOException {
545545
assertEquals(!expectedFilter[i], longBitSet.get(i));
546546
}
547547
}
548+
549+
/**
550+
* Test case for prefix filter when the prefix doesn't exist and would be inserted beyond all existing terms.
551+
* This validates the fix for the IndexOutOfBoundsException bug.
552+
*/
553+
public void testPrefixFilterWithNonExistentPrefixBeyondRange() throws IOException {
554+
// Create a regex pattern that will trigger prefix optimization
555+
// The prefix "zzz" doesn't exist in our doc values and would be inserted after all existing terms
556+
IncludeExclude includeExclude = new IncludeExclude("zzz.*", null);
557+
558+
OrdinalsFilter ordinalsFilter = includeExclude.convertToOrdinalsFilter(DocValueFormat.RAW);
559+
560+
// Create doc values with terms that all come before "zzz" alphabetically
561+
BytesRef[] bytesRefs = toBytesRefArray("aaa", "bbb", "ccc");
562+
563+
SortedSetDocValues sortedSetDocValues = new AbstractSortedSetDocValues() {
564+
@Override
565+
public boolean advanceExact(int target) {
566+
return false;
567+
}
568+
569+
@Override
570+
public long nextOrd() {
571+
return 0;
572+
}
573+
574+
@Override
575+
public int docValueCount() {
576+
return 1;
577+
}
578+
579+
@Override
580+
public BytesRef lookupOrd(long ord) {
581+
if (ord < 0 || ord >= bytesRefs.length) {
582+
throw new IndexOutOfBoundsException("ord=" + ord + " is out of bounds [0," + bytesRefs.length + ")");
583+
}
584+
int ordIndex = Math.toIntExact(ord);
585+
return bytesRefs[ordIndex];
586+
}
587+
588+
@Override
589+
public long getValueCount() {
590+
return bytesRefs.length;
591+
}
592+
};
593+
594+
// This should not throw IndexOutOfBoundsException after the fix
595+
LongBitSet acceptedOrds = ordinalsFilter.acceptedGlobalOrdinals(sortedSetDocValues);
596+
597+
// Since "zzz" doesn't exist in the doc values, no ordinals should be accepted
598+
assertEquals(bytesRefs.length, acceptedOrds.length());
599+
for (int i = 0; i < bytesRefs.length; i++) {
600+
assertFalse("Ordinal " + i + " should not be accepted", acceptedOrds.get(i));
601+
}
602+
}
603+
604+
/**
605+
* Test case for prefix filter with exclude pattern when the prefix doesn't exist.
606+
*/
607+
public void testPrefixFilterWithNonExistentExcludePrefixBeyondRange() throws IOException {
608+
// Test with an exclude pattern where the prefix doesn't exist
609+
IncludeExclude includeExclude = new IncludeExclude(null, "zzz.*");
610+
611+
OrdinalsFilter ordinalsFilter = includeExclude.convertToOrdinalsFilter(DocValueFormat.RAW);
612+
613+
BytesRef[] bytesRefs = toBytesRefArray("aaa", "bbb", "ccc");
614+
615+
SortedSetDocValues sortedSetDocValues = new AbstractSortedSetDocValues() {
616+
@Override
617+
public boolean advanceExact(int target) {
618+
return false;
619+
}
620+
621+
@Override
622+
public long nextOrd() {
623+
return 0;
624+
}
625+
626+
@Override
627+
public int docValueCount() {
628+
return 1;
629+
}
630+
631+
@Override
632+
public BytesRef lookupOrd(long ord) {
633+
if (ord < 0 || ord >= bytesRefs.length) {
634+
throw new IndexOutOfBoundsException("ord=" + ord + " is out of bounds [0," + bytesRefs.length + ")");
635+
}
636+
int ordIndex = Math.toIntExact(ord);
637+
return bytesRefs[ordIndex];
638+
}
639+
640+
@Override
641+
public long getValueCount() {
642+
return bytesRefs.length;
643+
}
644+
};
645+
646+
// This should not throw IndexOutOfBoundsException after the fix
647+
LongBitSet acceptedOrds = ordinalsFilter.acceptedGlobalOrdinals(sortedSetDocValues);
648+
649+
// Since "zzz" doesn't exist and we're excluding it, all ordinals should be accepted
650+
assertEquals(bytesRefs.length, acceptedOrds.length());
651+
for (int i = 0; i < bytesRefs.length; i++) {
652+
assertTrue("Ordinal " + i + " should be accepted", acceptedOrds.get(i));
653+
}
654+
}
655+
656+
/**
657+
* Test case for prefix filter when the prefix exists before all terms.
658+
*/
659+
public void testPrefixFilterWithNonExistentPrefixBeforeRange() throws IOException {
660+
// Test with a prefix that would be inserted before all existing terms
661+
IncludeExclude includeExclude = new IncludeExclude("aaa.*", null);
662+
663+
OrdinalsFilter ordinalsFilter = includeExclude.convertToOrdinalsFilter(DocValueFormat.RAW);
664+
665+
// Create doc values where "aaa" would be at the beginning but doesn't exist
666+
BytesRef[] bytesRefs = toBytesRefArray("bbb", "ccc", "ddd");
667+
668+
LongBitSet acceptedOrds = ordinalsFilter.acceptedGlobalOrdinals(toDocValues(bytesRefs));
669+
670+
// No ordinals should be accepted since "aaa" doesn't exist
671+
assertEquals(bytesRefs.length, acceptedOrds.length());
672+
for (int i = 0; i < bytesRefs.length; i++) {
673+
assertFalse("Ordinal " + i + " should not be accepted", acceptedOrds.get(i));
674+
}
675+
}
676+
677+
/**
678+
* Test case for prefix filter when the prefix matches some terms.
679+
*/
680+
public void testPrefixFilterWithMatchingPrefix() throws IOException {
681+
// Test with a prefix that matches some terms
682+
IncludeExclude includeExclude = new IncludeExclude("aa.*", null);
683+
684+
OrdinalsFilter ordinalsFilter = includeExclude.convertToOrdinalsFilter(DocValueFormat.RAW);
685+
686+
BytesRef[] bytesRefs = toBytesRefArray("aaa", "aab", "bbb", "ccc");
687+
688+
LongBitSet acceptedOrds = ordinalsFilter.acceptedGlobalOrdinals(toDocValues(bytesRefs));
689+
690+
// Only the first two ordinals should be accepted (matching "aa" prefix)
691+
assertEquals(bytesRefs.length, acceptedOrds.length());
692+
assertTrue("Ordinal 0 (aaa) should be accepted", acceptedOrds.get(0));
693+
assertTrue("Ordinal 1 (aab) should be accepted", acceptedOrds.get(1));
694+
assertFalse("Ordinal 2 (bbb) should not be accepted", acceptedOrds.get(2));
695+
assertFalse("Ordinal 3 (ccc) should not be accepted", acceptedOrds.get(3));
696+
}
548697
}

0 commit comments

Comments
 (0)