Skip to content

Commit

Permalink
feat(ESCatalog): Add support for pushing down null predicates to elastic
Browse files Browse the repository at this point in the history
  • Loading branch information
kuseman committed Sep 12, 2024
1 parent b50d99b commit 3addd21
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import se.kuseman.payloadbuilder.api.expression.IFunctionCallExpression;
import se.kuseman.payloadbuilder.api.expression.IInExpression;
import se.kuseman.payloadbuilder.api.expression.ILikeExpression;
import se.kuseman.payloadbuilder.api.expression.INullPredicateExpression;

/** Helper class for mocking {@link IAnalyzePair}'s */
public class IPredicateMock
Expand Down Expand Up @@ -81,6 +82,19 @@ public static IPredicate notIn(String column, List<Object> values)
return inPair(column, true, values);
}

public static IPredicate _null(String column, boolean not)
{
IPredicate pair = mock(IPredicate.class);
when(pair.getQualifiedColumn()).thenReturn(QualifiedName.of(column));
when(pair.getType()).thenReturn(IPredicate.Type.NULL);

INullPredicateExpression expression = mock(INullPredicateExpression.class);

when(pair.getNullPredicateExpression()).thenReturn(expression);
when(expression.isNot()).thenReturn(not);
return pair;
}

/** Mock an IN pair */
public static IPredicate inPair(String column, boolean not, List<Object> values)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import se.kuseman.payloadbuilder.api.expression.IFunctionCallExpression;
import se.kuseman.payloadbuilder.api.expression.IInExpression;
import se.kuseman.payloadbuilder.api.expression.ILikeExpression;
import se.kuseman.payloadbuilder.api.expression.INullPredicateExpression;

/** Predicate for a property */
@SuppressWarnings("deprecation")
Expand Down Expand Up @@ -160,6 +161,48 @@ else if (predicate.getType() == IPredicate.Type.LIKE)
.append("}}}");
appendNested(sb);
}
else if (predicate.getType() == Type.NULL)
{
INullPredicateExpression nullExpression = predicate.getNullPredicateExpression();

//@formatter:off
// Elasticsearch ignore null values and these cannot be searched/filtered which means
// that we can use exists for null predicate. If a field exists in a document that field cannot be null
// which in turn means that if a field does not exists on a document it must be null
// IS NULL
//
// "bool": {
// "must_not": [
// {
// "exists": {
// "field": <field>
// }
// }
// ]
// }
//
// IS NOT NULL
// "bool": {
// "filter": [
// {
// "exists": {
// "field": <field>
// }
// }
// ]
// }
//@formatter:on

// NOTE!
// IS NOT NULL -> must
// IS NULL -> must_not
StringBuilder sb = nullExpression.isNot() ? filterMust
: filterMustNot;

sb.append("{\"exists\":{\"field\":\"")
.append(property)
.append("\"}}");
}
else if (fullTextPredicate)
{
appendFullTextOperator(describe, context, predicate.getFunctionCallExpression(), filterMust);
Expand Down Expand Up @@ -294,6 +337,7 @@ static boolean isSupported(IPredicate predicate, String catalogAlias)
{
IPredicate.Type type = predicate.getType();
return type == Type.COMPARISION
|| type == Type.NULL
|| (type == Type.FUNCTION_CALL
&& isFullTextSearchPredicate(predicate.getFunctionCallExpression(), catalogAlias))
|| type == Type.IN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ public void test_search_body_singleType()
ESQueryUtils.getSearchBody(false, new GenericStrategy(), emptyList(), pairs.stream()
.map(p -> new PropertyPredicate("", p, true))
.collect(toList()), null, null, false, context));

pairs = asList(IPredicateMock._null("field1", false), IPredicateMock._null("field2", true));
assertEquals("{\"sort\":[\"_doc\"],\"query\":{\"bool\":{\"filter\":[{\"exists\":{\"field\":\"field2\"}}],\"must_not\":[{\"exists\":{\"field\":\"field1\"}}]}}}",
ESQueryUtils.getSearchBody(false, new GenericStrategy(), emptyList(), pairs.stream()
.map(p -> new PropertyPredicate(p.getQualifiedColumn()
.toString(), p, false))
.collect(toList()), null, null, false, context));
}

@Test
Expand Down Expand Up @@ -318,6 +325,13 @@ public void test_search_body()
ESQueryUtils.getSearchBody(false, new Elastic1XStrategy(), emptyList(), pairs.stream()
.map(p -> new PropertyPredicate("", p, true))
.collect(toList()), null, null, false, context));

pairs = asList(IPredicateMock._null("field1", false), IPredicateMock._null("field2", true));
assertEquals("{\"sort\":[\"_doc\"],\"filter\":{\"bool\":{\"must\":[{\"exists\":{\"field\":\"field2\"}}],\"must_not\":[{\"exists\":{\"field\":\"field1\"}}]}}}",
ESQueryUtils.getSearchBody(false, new Elastic1XStrategy(), emptyList(), pairs.stream()
.map(p -> new PropertyPredicate(p.getQualifiedColumn()
.toString(), p, false))
.collect(toList()), null, null, false, context));
}

private List<IPredicate> getMockPairs()
Expand Down

0 comments on commit 3addd21

Please sign in to comment.