Skip to content
Closed
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
131 changes: 128 additions & 3 deletions src/Nest.OData/ODataFilterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public static QueryContainer ToQueryContainer(this FilterQueryOption filter, ODa

internal static QueryContainer TranslateExpression(QueryNode node, ODataExpressionContext context = null)
{
if (ShouldOptimizeOrOperation(node))
{
return OptimizeIdenticalFunctionCalls(node as BinaryOperatorNode, context);
}

return node.Kind switch
{
QueryNodeKind.Any => TranslateAnyNode(node as AnyNode, context),
Expand All @@ -64,6 +69,71 @@ internal static QueryContainer TranslateExpression(QueryNode node, ODataExpressi
};
}

private static bool ShouldOptimizeOrOperation(QueryNode node)
{
return node is BinaryOperatorNode binaryNode &&
binaryNode.OperatorKind == BinaryOperatorKind.Or &&
binaryNode.Left is SingleValueFunctionCallNode &&
binaryNode.Right is SingleValueFunctionCallNode;
}

private static QueryContainer OptimizeIdenticalFunctionCalls(BinaryOperatorNode node, ODataExpressionContext context)
{
var leftFunc = node.Left as SingleValueFunctionCallNode;
var rightFunc = node.Right as SingleValueFunctionCallNode;

var functionCalls = new[]
{
(Node: leftFunc, IsNested: IsNestedFunctionCall(leftFunc)),
(Node: rightFunc, IsNested: IsNestedFunctionCall(rightFunc))
};

if (AreFunctionCallsIdentical(functionCalls[0], functionCalls[1]))
{
return TranslateExpression(node.Left, context);
}

return TranslateExpression(node.Right, context);
}

private static bool AreFunctionCallsIdentical((SingleValueFunctionCallNode Node, bool IsNested) first, (SingleValueFunctionCallNode Node, bool IsNested) other)
{
if (HasIdenticalFunctionSignature(first.Node, other.Node) == false)
{
return false;
}

if (HasIdenticalParameters(first.Node, other.Node) == false)
{
return false;
}

if (first.IsNested != other.IsNested)
{
return false;
}

return true;
}

private static bool HasIdenticalFunctionSignature(SingleValueFunctionCallNode first, SingleValueFunctionCallNode other)
{
return string.Equals(first.Name, other.Name, StringComparison.OrdinalIgnoreCase) && first.Parameters.Count() == other.Parameters.Count();
}

private static bool HasIdenticalParameters(SingleValueFunctionCallNode first, SingleValueFunctionCallNode other)
{
if (first.Parameters.First().ToString() != other.Parameters.First().ToString())
{
return false;
}

var firstValue = ExtractValue(first.Parameters.Last())?.ToString();
var otherValue = ExtractValue(other.Parameters.Last())?.ToString();

return string.Equals(firstValue, otherValue);
}

private static QueryContainer TranslateAnyNode(AnyNode node, ODataExpressionContext context = null)
{
var fullyQualifiedFieldName = ODataHelpers.ExtractFullyQualifiedFieldName(node.Source, context);
Expand Down Expand Up @@ -212,6 +282,7 @@ private static QueryContainer TranslateFunctionCallNode(SingleValueFunctionCallN
private static QueryContainer TranslateOrOperations(BinaryOperatorNode node, ODataExpressionContext context = null)
{
var queries = new List<QueryContainer>();
var functionCalls = new List<(SingleValueFunctionCallNode Node, bool IsNested)>();

void Collect(QueryNode queryNode)
{
Expand All @@ -222,13 +293,56 @@ void Collect(QueryNode queryNode)
}
else
{
queries.Add(TranslateExpression(queryNode, context));
if (queryNode is SingleValueFunctionCallNode funcNode)
{
var isNested = IsNestedFunctionCall(funcNode);
functionCalls.Add((funcNode, isNested));
}

var query = TranslateExpression(queryNode, context);
if (query != null)
{
queries.Add(query);
}
}
}

Collect(node);

return new BoolQuery { Should = queries, MinimumShouldMatch = 1 };
if (queries.Any() == false)
{
return null;
}

return OptimizeOrQueries(queries, functionCalls);
}

private static bool IsNestedFunctionCall(SingleValueFunctionCallNode funcNode)
{
return funcNode.Parameters.First() is SingleValuePropertyAccessNode propNode && ODataHelpers.IsNavigationNode(propNode.Source.Kind);
}

private static QueryContainer OptimizeOrQueries(List<QueryContainer> queries, List<(SingleValueFunctionCallNode Node, bool IsNested)> functionCalls)
{
if (CanOptimizeFunctionCalls(functionCalls, queries.Count) == false)
{
return new BoolQuery { Should = queries, MinimumShouldMatch = 1 };
}

var first = functionCalls[0];

return AreAllFunctionCallsIdentical(functionCalls, first) ? queries[0] : new BoolQuery { Should = queries, MinimumShouldMatch = 1 };
}

private static bool CanOptimizeFunctionCalls(List<(SingleValueFunctionCallNode Node, bool IsNested)> functionCalls, int queryCount)
{
return functionCalls.Count > 0 && functionCalls.Count == queryCount;
}

private static bool AreAllFunctionCallsIdentical(List<(SingleValueFunctionCallNode Node, bool IsNested)> functionCalls,
(SingleValueFunctionCallNode Node, bool IsNested) first)
{
return functionCalls.All(f => AreFunctionCallsIdentical(first, f));
}

private static QueryContainer TranslateAndOperations(BinaryOperatorNode node, ODataExpressionContext context = null)
Expand All @@ -250,6 +364,17 @@ void Collect(QueryNode queryNode)

Collect(node);

if (queries.Any() == false)
{
return null;
}

// If we have a single query from an AND operation to maintain the expected structure.
if (queries.Count == 1)
{
return new BoolQuery { Must = queries };
}

return new BoolQuery { Must = queries };
}

Expand Down Expand Up @@ -319,7 +444,7 @@ private static string ExtractStringValue(QueryNode node)
{
if (constantNode.Value is DateTime dateTime)
{
return dateTime.ToString("o");
return dateTime.ToString("o");
}
else if (constantNode.Value is DateTimeOffset dateTimeOffset)
{
Expand Down
132 changes: 132 additions & 0 deletions test/Nest.OData.Tests/FilterFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,137 @@ public void ContainsToLowerFunction()

Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
}

[Fact]
public void MultipleIdenticalFunctionCalls()
{
var queryOptions = "$filter=(contains(Category,'Goods')) or (contains(Category,'Goods')) or (contains(Category,'Goods'))".GetODataQueryOptions<Product>();

var elasticQuery = queryOptions.ToElasticQuery();

Assert.NotNull(elasticQuery);

var queryJson = elasticQuery.ToJson();

var expectedJson = @"
{
""query"": {
""wildcard"": {
""Category"": {
""value"": ""*goods*""
}
}
}
}";

var actualJObject = JObject.Parse(queryJson);
var expectedJObject = JObject.Parse(expectedJson);

Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
}

[Fact]
public void MultipleDifferentFunctionCalls()
{
var queryOptions = "$filter=(contains(Category,'Goods')) or (contains(Category,'Food')) or (contains(Name,'Merchandise'))".GetODataQueryOptions<Product>();

var elasticQuery = queryOptions.ToElasticQuery();

Assert.NotNull(elasticQuery);

var queryJson = elasticQuery.ToJson();

var expectedJson = @"
{
""query"": {
""bool"": {
""should"": [
{
""wildcard"": {
""Category"": {
""value"": ""*goods*""
}
}
},
{
""wildcard"": {
""Category"": {
""value"": ""*food*""
}
}
},
{
""wildcard"": {
""Name"": {
""value"": ""*test*""
}
}
}
],
""minimum_should_match"": 1
}
}
}";

var actualJObject = JObject.Parse(queryJson);
var expectedJObject = JObject.Parse(expectedJson);

Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
}

[Fact]
public void MultipleOperatorsFunctionCall()
{
var queryOptions = "$filter=(contains(Category,'Food') and contains(Category,'Goods')) or contains(Name,'Merchandise')".GetODataQueryOptions<Product>();

var elasticQuery = queryOptions.ToElasticQuery();

Assert.NotNull(elasticQuery);

var queryJson = elasticQuery.ToJson();

var expectedJson = @"
{
""query"": {
""bool"": {
""should"": [
{
""bool"": {
""must"": [
{
""wildcard"": {
""Category"": {
""value"": ""*food*""
}
}
},
{
""wildcard"": {
""Category"": {
""value"": ""*fresh*""
}
}
}
]
}
},
{
""wildcard"": {
""Name"": {
""value"": ""*test*""
}
}
}
],
""minimum_should_match"": 1
}
}
}";

var actualJObject = JObject.Parse(queryJson);
var expectedJObject = JObject.Parse(expectedJson);

Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
}
}
}
Loading