Skip to content

Commit 069c2d8

Browse files
authored
[Backport 3.0] [Star Tree] Support of Boolean Queries in Aggregations (#17941) (#18097)
--------- Signed-off-by: Rishab Nahata <[email protected]>
1 parent e0272e0 commit 069c2d8

14 files changed

+2163
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
### Added
88
- Add multi-threaded writer support in pull-based ingestion ([#17912](https://github.com/opensearch-project/OpenSearch/pull/17912))
99
- Unset discovery nodes for every transport node actions request ([#17682](https://github.com/opensearch-project/OpenSearch/pull/17682))
10+
- [Star Tree] Support of Boolean Queries in Aggregations ([#17941](https://github.com/opensearch-project/OpenSearch/pull/17941))
1011
- Enabled default throttling for all tasks submitted to cluster manager ([#17711](https://github.com/opensearch-project/OpenSearch/pull/17711))
1112

1213
### Changed

server/src/main/java/org/opensearch/search/startree/filter/DimensionFilter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public boolean matchDimValue(long ordinal, StarTreeValues starTreeValues) {
7474
*/
7575
boolean matchDimValue(long ordinal, StarTreeValues starTreeValues);
7676

77+
default String getDimensionName() {
78+
return null;
79+
}
80+
7781
/**
7882
* Represents how to match a value when comparing during StarTreeTraversal
7983
*/
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.search.startree.filter;
10+
11+
import org.opensearch.search.startree.filter.provider.DimensionFilterMapper;
12+
13+
import java.util.ArrayList;
14+
import java.util.HashSet;
15+
import java.util.List;
16+
import java.util.Set;
17+
18+
/**
19+
* Utility class for merging different types of {@link DimensionFilter}
20+
* Handles intersection operations between {@link ExactMatchDimFilter} and {@link RangeMatchDimFilter}
21+
*/
22+
public class DimensionFilterMergerUtils {
23+
24+
/**
25+
* Gets intersection of two DimensionFilters
26+
* Returns null if intersection results in no possible matches.
27+
*/
28+
public static DimensionFilter intersect(DimensionFilter filter1, DimensionFilter filter2, DimensionFilterMapper mapper) {
29+
if (filter1 == null || filter2 == null) {
30+
return null;
31+
}
32+
33+
if (filter1.getDimensionName() == null || filter2.getDimensionName() == null) {
34+
throw new IllegalArgumentException("Cannot intersect filters with null dimension name");
35+
}
36+
37+
// Verify filters are for same dimension
38+
if (!filter1.getDimensionName().equals(filter2.getDimensionName())) {
39+
throw new IllegalArgumentException(
40+
"Cannot intersect filters for different dimensions: " + filter1.getDimensionName() + " and " + filter2.getDimensionName()
41+
);
42+
}
43+
44+
// Handle Range + Range combination
45+
if (filter1 instanceof RangeMatchDimFilter && filter2 instanceof RangeMatchDimFilter) {
46+
return intersectRangeFilters((RangeMatchDimFilter) filter1, (RangeMatchDimFilter) filter2, mapper);
47+
}
48+
49+
// Handle ExactMatch + ExactMatch combination
50+
if (filter1 instanceof ExactMatchDimFilter && filter2 instanceof ExactMatchDimFilter) {
51+
return intersectExactMatchFilters((ExactMatchDimFilter) filter1, (ExactMatchDimFilter) filter2);
52+
}
53+
54+
// Handle Range + ExactMatch combination
55+
if (filter1 instanceof RangeMatchDimFilter && filter2 instanceof ExactMatchDimFilter) {
56+
return intersectRangeWithExactMatch((RangeMatchDimFilter) filter1, (ExactMatchDimFilter) filter2, mapper);
57+
}
58+
59+
// Handle ExactMatch + Range combination
60+
if (filter1 instanceof ExactMatchDimFilter && filter2 instanceof RangeMatchDimFilter) {
61+
return intersectRangeWithExactMatch((RangeMatchDimFilter) filter2, (ExactMatchDimFilter) filter1, mapper);
62+
}
63+
64+
// throw exception for unsupported exception
65+
throw new IllegalArgumentException(
66+
"Unsupported filter combination: " + filter1.getClass().getSimpleName() + " and " + filter2.getClass().getSimpleName()
67+
);
68+
}
69+
70+
/**
71+
* Intersects two range filters
72+
* Returns null if ranges don't overlap
73+
*/
74+
private static DimensionFilter intersectRangeFilters(
75+
RangeMatchDimFilter range1,
76+
RangeMatchDimFilter range2,
77+
DimensionFilterMapper mapper
78+
) {
79+
Object low1 = range1.getLow();
80+
Object high1 = range1.getHigh();
81+
Object low2 = range2.getLow();
82+
Object high2 = range2.getHigh();
83+
84+
// Find the more restrictive bounds
85+
Object newLow;
86+
boolean includeLow;
87+
if (low1 == null) {
88+
newLow = low2;
89+
includeLow = range2.isIncludeLow();
90+
} else if (low2 == null) {
91+
newLow = low1;
92+
includeLow = range1.isIncludeLow();
93+
} else {
94+
int comparison = mapper.compareValues(low1, low2);
95+
if (comparison > 0) {
96+
newLow = low1;
97+
includeLow = range1.isIncludeLow();
98+
} else if (comparison < 0) {
99+
newLow = low2;
100+
includeLow = range2.isIncludeLow();
101+
} else {
102+
newLow = low1;
103+
includeLow = range1.isIncludeLow() && range2.isIncludeLow();
104+
}
105+
}
106+
107+
Object newHigh;
108+
boolean includeHigh;
109+
if (high1 == null) {
110+
newHigh = high2;
111+
includeHigh = range2.isIncludeHigh();
112+
} else if (high2 == null) {
113+
newHigh = high1;
114+
includeHigh = range1.isIncludeHigh();
115+
} else {
116+
int comparison = mapper.compareValues(high1, high2);
117+
if (comparison < 0) {
118+
newHigh = high1;
119+
includeHigh = range1.isIncludeHigh();
120+
} else if (comparison > 0) {
121+
newHigh = high2;
122+
includeHigh = range2.isIncludeHigh();
123+
} else {
124+
newHigh = high1;
125+
includeHigh = range1.isIncludeHigh() && range2.isIncludeHigh();
126+
}
127+
}
128+
129+
// Check if range is valid
130+
if (newLow != null && newHigh != null) {
131+
if (!mapper.isValidRange(newLow, newHigh, includeLow, includeHigh)) {
132+
return null; // No overlap
133+
}
134+
}
135+
136+
return new RangeMatchDimFilter(range1.getDimensionName(), newLow, newHigh, includeLow, includeHigh);
137+
}
138+
139+
/**
140+
* Intersects two exact match filters
141+
* Returns null if no common values
142+
*/
143+
private static DimensionFilter intersectExactMatchFilters(ExactMatchDimFilter exact1, ExactMatchDimFilter exact2) {
144+
List<Object> values1 = exact1.getRawValues();
145+
Set<Object> values2Set = new HashSet<>(exact2.getRawValues());
146+
147+
List<Object> intersection = new ArrayList<>();
148+
for (Object value : values1) {
149+
if (values2Set.contains(value)) {
150+
intersection.add(value);
151+
}
152+
}
153+
154+
if (intersection.isEmpty()) {
155+
return null;
156+
}
157+
158+
return new ExactMatchDimFilter(exact1.getDimensionName(), intersection);
159+
}
160+
161+
/**
162+
* Intersects a range filter with an exact match filter.
163+
* Returns null if no values from exact match are within range.
164+
*/
165+
private static DimensionFilter intersectRangeWithExactMatch(
166+
RangeMatchDimFilter range,
167+
ExactMatchDimFilter exact,
168+
DimensionFilterMapper mapper
169+
) {
170+
List<Object> validValues = new ArrayList<>();
171+
172+
for (Object value : exact.getRawValues()) {
173+
if (isValueInRange(value, range, mapper)) {
174+
validValues.add(value);
175+
}
176+
}
177+
178+
if (validValues.isEmpty()) {
179+
return null;
180+
}
181+
182+
return new ExactMatchDimFilter(exact.getDimensionName(), validValues);
183+
}
184+
185+
/**
186+
* Checks if a value falls within a range.
187+
*/
188+
private static boolean isValueInRange(Object value, RangeMatchDimFilter range, DimensionFilterMapper mapper) {
189+
return mapper.isValueInRange(value, range.getLow(), range.getHigh(), range.isIncludeLow(), range.isIncludeHigh());
190+
}
191+
}

server/src/main/java/org/opensearch/search/startree/filter/ExactMatchDimFilter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,13 @@ public void matchStarTreeNodes(StarTreeNode parentNode, StarTreeValues starTreeV
8484
public boolean matchDimValue(long ordinal, StarTreeValues starTreeValues) {
8585
return convertedOrdinals.contains(ordinal);
8686
}
87+
88+
public List<Object> getRawValues() {
89+
return rawValues;
90+
}
91+
92+
@Override
93+
public String getDimensionName() {
94+
return dimensionName;
95+
}
8796
}

server/src/main/java/org/opensearch/search/startree/filter/MatchNoneFilter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020
@ExperimentalApi
2121
public class MatchNoneFilter implements DimensionFilter {
22+
2223
@Override
2324
public void initialiseForSegment(StarTreeValues starTreeValues, SearchContext searchContext) {
2425
// Nothing to do as we won't match anything.

server/src/main/java/org/opensearch/search/startree/filter/RangeMatchDimFilter.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
public class RangeMatchDimFilter implements DimensionFilter {
2727

2828
private final String dimensionName;
29-
3029
private final Object low;
3130
private final Object high;
3231
private final boolean includeLow;
@@ -35,6 +34,7 @@ public class RangeMatchDimFilter implements DimensionFilter {
3534
private Long lowOrdinal;
3635
private Long highOrdinal;
3736

37+
// TODO - see if we need to handle this while intersecting
3838
private boolean skipRangeCollection = false;
3939

4040
private DimensionFilterMapper dimensionFilterMapper;
@@ -90,4 +90,25 @@ public boolean matchDimValue(long ordinal, StarTreeValues starTreeValues) {
9090
&& dimensionFilterMapper.comparator().compare(ordinal, highOrdinal) <= 0;
9191
}
9292

93+
@Override
94+
public String getDimensionName() {
95+
return dimensionName;
96+
}
97+
98+
public Object getLow() {
99+
return low;
100+
}
101+
102+
public Object getHigh() {
103+
return high;
104+
}
105+
106+
public boolean isIncludeLow() {
107+
return includeLow;
108+
}
109+
110+
public boolean isIncludeHigh() {
111+
return includeHigh;
112+
}
113+
93114
}

0 commit comments

Comments
 (0)