Skip to content

[Backport 3.0] [Star Tree] Support of Boolean Queries in Aggregations (#17941) #18097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added
- Add multi-threaded writer support in pull-based ingestion ([#17912](https://github.com/opensearch-project/OpenSearch/pull/17912))
- Unset discovery nodes for every transport node actions request ([#17682](https://github.com/opensearch-project/OpenSearch/pull/17682))
- [Star Tree] Support of Boolean Queries in Aggregations ([#17941](https://github.com/opensearch-project/OpenSearch/pull/17941))
- Enabled default throttling for all tasks submitted to cluster manager ([#17711](https://github.com/opensearch-project/OpenSearch/pull/17711))

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
*/
boolean matchDimValue(long ordinal, StarTreeValues starTreeValues);

default String getDimensionName() {
return null;

Check warning on line 78 in server/src/main/java/org/opensearch/search/startree/filter/DimensionFilter.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L78 was not covered by tests
}

/**
* Represents how to match a value when comparing during StarTreeTraversal
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.search.startree.filter;

import org.opensearch.search.startree.filter.provider.DimensionFilterMapper;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Utility class for merging different types of {@link DimensionFilter}
* Handles intersection operations between {@link ExactMatchDimFilter} and {@link RangeMatchDimFilter}
*/
public class DimensionFilterMergerUtils {

Check warning on line 22 in server/src/main/java/org/opensearch/search/startree/filter/DimensionFilterMergerUtils.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/search/startree/filter/DimensionFilterMergerUtils.java#L22

Added line #L22 was not covered by tests

/**
* Gets intersection of two DimensionFilters
* Returns null if intersection results in no possible matches.
*/
public static DimensionFilter intersect(DimensionFilter filter1, DimensionFilter filter2, DimensionFilterMapper mapper) {
if (filter1 == null || filter2 == null) {
return null;
}

if (filter1.getDimensionName() == null || filter2.getDimensionName() == null) {
throw new IllegalArgumentException("Cannot intersect filters with null dimension name");
}

// Verify filters are for same dimension
if (!filter1.getDimensionName().equals(filter2.getDimensionName())) {
throw new IllegalArgumentException(
"Cannot intersect filters for different dimensions: " + filter1.getDimensionName() + " and " + filter2.getDimensionName()
);
}

// Handle Range + Range combination
if (filter1 instanceof RangeMatchDimFilter && filter2 instanceof RangeMatchDimFilter) {
return intersectRangeFilters((RangeMatchDimFilter) filter1, (RangeMatchDimFilter) filter2, mapper);
}

// Handle ExactMatch + ExactMatch combination
if (filter1 instanceof ExactMatchDimFilter && filter2 instanceof ExactMatchDimFilter) {
return intersectExactMatchFilters((ExactMatchDimFilter) filter1, (ExactMatchDimFilter) filter2);
}

// Handle Range + ExactMatch combination
if (filter1 instanceof RangeMatchDimFilter && filter2 instanceof ExactMatchDimFilter) {
return intersectRangeWithExactMatch((RangeMatchDimFilter) filter1, (ExactMatchDimFilter) filter2, mapper);
}

// Handle ExactMatch + Range combination
if (filter1 instanceof ExactMatchDimFilter && filter2 instanceof RangeMatchDimFilter) {
return intersectRangeWithExactMatch((RangeMatchDimFilter) filter2, (ExactMatchDimFilter) filter1, mapper);
}

// throw exception for unsupported exception
throw new IllegalArgumentException(
"Unsupported filter combination: " + filter1.getClass().getSimpleName() + " and " + filter2.getClass().getSimpleName()
);
}

/**
* Intersects two range filters
* Returns null if ranges don't overlap
*/
private static DimensionFilter intersectRangeFilters(
RangeMatchDimFilter range1,
RangeMatchDimFilter range2,
DimensionFilterMapper mapper
) {
Object low1 = range1.getLow();
Object high1 = range1.getHigh();
Object low2 = range2.getLow();
Object high2 = range2.getHigh();

// Find the more restrictive bounds
Object newLow;
boolean includeLow;
if (low1 == null) {
newLow = low2;
includeLow = range2.isIncludeLow();
} else if (low2 == null) {
newLow = low1;
includeLow = range1.isIncludeLow();
} else {
int comparison = mapper.compareValues(low1, low2);
if (comparison > 0) {
newLow = low1;
includeLow = range1.isIncludeLow();
} else if (comparison < 0) {
newLow = low2;
includeLow = range2.isIncludeLow();
} else {
newLow = low1;
includeLow = range1.isIncludeLow() && range2.isIncludeLow();
}
}

Object newHigh;
boolean includeHigh;
if (high1 == null) {
newHigh = high2;
includeHigh = range2.isIncludeHigh();
} else if (high2 == null) {
newHigh = high1;
includeHigh = range1.isIncludeHigh();
} else {
int comparison = mapper.compareValues(high1, high2);
if (comparison < 0) {
newHigh = high1;
includeHigh = range1.isIncludeHigh();
} else if (comparison > 0) {
newHigh = high2;
includeHigh = range2.isIncludeHigh();
} else {
newHigh = high1;
includeHigh = range1.isIncludeHigh() && range2.isIncludeHigh();
}
}

// Check if range is valid
if (newLow != null && newHigh != null) {
if (!mapper.isValidRange(newLow, newHigh, includeLow, includeHigh)) {
return null; // No overlap
}
}

return new RangeMatchDimFilter(range1.getDimensionName(), newLow, newHigh, includeLow, includeHigh);
}

/**
* Intersects two exact match filters
* Returns null if no common values
*/
private static DimensionFilter intersectExactMatchFilters(ExactMatchDimFilter exact1, ExactMatchDimFilter exact2) {
List<Object> values1 = exact1.getRawValues();
Set<Object> values2Set = new HashSet<>(exact2.getRawValues());

List<Object> intersection = new ArrayList<>();
for (Object value : values1) {
if (values2Set.contains(value)) {
intersection.add(value);
}
}

if (intersection.isEmpty()) {
return null;
}

return new ExactMatchDimFilter(exact1.getDimensionName(), intersection);
}

/**
* Intersects a range filter with an exact match filter.
* Returns null if no values from exact match are within range.
*/
private static DimensionFilter intersectRangeWithExactMatch(
RangeMatchDimFilter range,
ExactMatchDimFilter exact,
DimensionFilterMapper mapper
) {
List<Object> validValues = new ArrayList<>();

for (Object value : exact.getRawValues()) {
if (isValueInRange(value, range, mapper)) {
validValues.add(value);
}
}

if (validValues.isEmpty()) {
return null;
}

return new ExactMatchDimFilter(exact.getDimensionName(), validValues);
}

/**
* Checks if a value falls within a range.
*/
private static boolean isValueInRange(Object value, RangeMatchDimFilter range, DimensionFilterMapper mapper) {
return mapper.isValueInRange(value, range.getLow(), range.getHigh(), range.isIncludeLow(), range.isIncludeHigh());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,13 @@ public void matchStarTreeNodes(StarTreeNode parentNode, StarTreeValues starTreeV
public boolean matchDimValue(long ordinal, StarTreeValues starTreeValues) {
return convertedOrdinals.contains(ordinal);
}

public List<Object> getRawValues() {
return rawValues;
}

@Override
public String getDimensionName() {
return dimensionName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/
@ExperimentalApi
public class MatchNoneFilter implements DimensionFilter {

@Override
public void initialiseForSegment(StarTreeValues starTreeValues, SearchContext searchContext) {
// Nothing to do as we won't match anything.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
public class RangeMatchDimFilter implements DimensionFilter {

private final String dimensionName;

private final Object low;
private final Object high;
private final boolean includeLow;
Expand All @@ -35,6 +34,7 @@ public class RangeMatchDimFilter implements DimensionFilter {
private Long lowOrdinal;
private Long highOrdinal;

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

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

@Override
public String getDimensionName() {
return dimensionName;
}

public Object getLow() {
return low;
}

public Object getHigh() {
return high;
}

public boolean isIncludeLow() {
return includeLow;
}

public boolean isIncludeHigh() {
return includeHigh;
}

}
Loading
Loading