Skip to content

Commit

Permalink
Add logic to filter NEST queries using Date/Numeric ranges
Browse files Browse the repository at this point in the history
  • Loading branch information
billbogaiv committed Sep 18, 2018
1 parent ddab57e commit fea37f5
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 11 deletions.
100 changes: 97 additions & 3 deletions src/Filter.Nest/FilterLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,104 @@ internal static List<Func<QueryContainerDescriptor<T>, QueryContainer>> Generate
var queries = new List<Func<QueryContainerDescriptor<T>, QueryContainer>>();
var aliasAttribute = validValueProperty.GetCustomAttribute<MappingAliasAttribute>();

if (typeof(Range.Generic.IRange<>).IsAssignableFrom(filterProperty.PropertyType))
if (filterPropertyValue.GetType().GetInterfaces().Any(
x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(Range.Generic.IRange<>)))
{


Range.Generic.Range<DateTimeOffset> dateRange;
Range.Generic.Range<decimal> numericRange;

if ((dateRange = Range.Range.AsDateRange(filterPropertyValue)) != null)
{
queries.Add(x =>
x.DateRange(y =>
{
if (dateRange.MinValue.HasValue)
{
var minValue = dateRange.MinValue.Value.Date;

if (aliasAttribute != null)
{
y = dateRange.IsMinInclusive
? y.Field(aliasAttribute.Alias).GreaterThanOrEquals(minValue)
: y.Field(aliasAttribute.Alias).GreaterThan(minValue);
}
else
{
y = dateRange.IsMinInclusive
? y.Field(validValueProperty).GreaterThanOrEquals(minValue)
: y.Field(validValueProperty).GreaterThan(minValue);
}
}

if (dateRange.MaxValue.HasValue)
{
var maxValue = dateRange.MaxValue.Value.Date;

if (aliasAttribute != null)
{
y = dateRange.IsMaxInclusive
? y.Field(aliasAttribute.Alias).LessThanOrEquals(maxValue)
: y.Field(aliasAttribute.Alias).LessThan(maxValue);
}
else
{
y = dateRange.IsMaxInclusive
? y.Field(validValueProperty).LessThanOrEquals(maxValue)
: y.Field(validValueProperty).LessThan(maxValue);
}
}

return y;
}));
}
else if ((numericRange = Range.Range.AsNumericRange(filterPropertyValue)) != null)
{
queries.Add(x =>
x.Range(y =>
{
if (numericRange.MinValue.HasValue)
{
var minValue = Convert.ToDouble(numericRange.MinValue.Value);

if (aliasAttribute != null)
{
y = numericRange.IsMinInclusive
? y.Field(aliasAttribute.Alias).GreaterThanOrEquals(minValue)
: y.Field(aliasAttribute.Alias).GreaterThan(minValue);
}
else
{
y = numericRange.IsMinInclusive
? y.Field(validValueProperty).GreaterThanOrEquals(minValue)
: y.Field(validValueProperty).GreaterThan(minValue);
}
}

if (numericRange.MaxValue.HasValue)
{
var maxValue = Convert.ToDouble(numericRange.MaxValue.Value);

if (aliasAttribute != null)
{
y = numericRange.IsMaxInclusive
? y.Field(aliasAttribute.Alias).LessThanOrEquals(maxValue)
: y.Field(aliasAttribute.Alias).LessThan(maxValue);
}
else
{
y = numericRange.IsMaxInclusive
? y.Field(validValueProperty).LessThanOrEquals(maxValue)
: y.Field(validValueProperty).LessThan(maxValue);
}
}

return y;
}));
}
else
{
throw new InvalidOperationException($"NEST filtering does not work on `Range<T>` where `T` is `{filterProperty.PropertyType.Name}`.");
}
}
else
if (typeof(IEnumerable).IsAssignableFrom(filterProperty.PropertyType)
Expand Down
97 changes: 93 additions & 4 deletions tests/Filter.Nest.Tests/BoolQueryDescriptorExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Nest;
using RimDev.Filter.Nest;
using RimDev.Filter.Range;
using System;
using System.Diagnostics;
using System.Linq;
using Xunit;

Expand Down Expand Up @@ -60,7 +63,7 @@ public void Can_query_using_single_value()
q => q.Bool(x => x.Filter(new { Name = new[] { "camaro" } }))));

Assert.NotNull(results);
Assert.Equal(1, results.Hits.Count());
Assert.Single(results.Hits);
Assert.Equal("Camaro", results.Hits.First().Source.Name);
}
}
Expand All @@ -86,7 +89,7 @@ public void Multiple_filter_properties_queried_as_collection_of_and_operators()
q => q.Bool(x => x.Filter(new { Name = new[] { "camaro", "monte carlo" }, Year = 2016 }))));

Assert.NotNull(noResults);
Assert.Equal(0, noResults.Hits.Count());
Assert.Empty(noResults.Hits);

var twoResults = elasticClient
.Search<Car>(x => x.Index("vehicles")
Expand Down Expand Up @@ -168,7 +171,7 @@ public void Nullable_boolean_true_returns_expected_results()
q => q.Bool(x => x.Filter(new { IsElectric = true }))));

Assert.NotNull(results);
Assert.Equal(1, results.Hits.Count());
Assert.Single(results.Hits);
Assert.Equal("Volt", results.Hits.First().Source.Name);
}
}
Expand All @@ -192,10 +195,96 @@ public void Nullable_boolean_false_returns_expected_results()
q => q.Bool(x => x.Filter(new { IsElectric = false }))));

Assert.NotNull(results);
Assert.Equal(1, results.Hits.Count());
Assert.Single(results.Hits);
Assert.Equal("Camaro", results.Hits.First().Source.Name);
}
}

[Theory]
[InlineData("[2000-01-01,]", 3)]
[InlineData("[2010-01-01,]", 2)]
[InlineData("[2020-01-01,]", 1)]
[InlineData("[2030-01-01,]", 0)]
[InlineData("[,1999-01-01]", 0)]
[InlineData("[,2000-01-01]", 1)]
[InlineData("[,2010-01-01]", 2)]
[InlineData("[,2020-01-01]", 3)]
[InlineData("(2000-01-01,]", 2)]
[InlineData("(2010-01-01,]", 1)]
[InlineData("(2020-01-01,]", 0)]
[InlineData("[,2000-01-01)", 0)]
[InlineData("[,2010-01-01)", 1)]
[InlineData("[,2020-01-01)", 2)]
[InlineData("[,2030-01-01)", 3)]
public void Properly_filters_date_ranges(string startProductionRunRange, int expectedResults)
{
using (var elasticsearch = new ElasticsearchInside.Elasticsearch())
{
var elasticClient = new ElasticClient(new ConnectionSettings(elasticsearch.Url).EnableDebugMode(x =>
{
Debug.WriteLine(x.DebugInformation);
}));

var camaro = new Car { Name = "Camaro", StartProductionRun = DateTimeOffset.Parse("2020-01-01T00:00:00Z") };
var corvette = new Car { Name = "Corvette", StartProductionRun = DateTimeOffset.Parse("2010-01-01T00:00:00Z") };
var monteCarlo = new Car { Name = "Monte Carlo", StartProductionRun = DateTimeOffset.Parse("2000-01-01T00:00:00Z") };

elasticClient.Index(camaro, x => x.Index("vehicles"));
elasticClient.Index(corvette, x => x.Index("vehicles"));
elasticClient.Index(monteCarlo, x => x.Index("vehicles"));

elasticClient.Refresh("vehicles");

var noResults = elasticClient.Search<Car>(s => s.Index("vehicles").Query(
q => q.Bool(x => x.Filter(new { StartProductionRun = Range.FromString<DateTime>(startProductionRunRange) }))));

Assert.NotNull(noResults);
Assert.Equal(expectedResults, noResults.Hits.Count());
}
}

[Theory]
[InlineData("[2000,]", 3)]
[InlineData("[2010,]", 2)]
[InlineData("[2020,]", 1)]
[InlineData("[2030,]", 0)]
[InlineData("[,1999]", 0)]
[InlineData("[,2000]", 1)]
[InlineData("[,2010]", 2)]
[InlineData("[,2020]", 3)]
[InlineData("(2000,]", 2)]
[InlineData("(2010,]", 1)]
[InlineData("(2020,]", 0)]
[InlineData("[,2000)", 0)]
[InlineData("[,2010)", 1)]
[InlineData("[,2020)", 2)]
[InlineData("[,2030)", 3)]
public void Properly_filters_numeric_ranges(string yearRange, int expectedResults)
{
using (var elasticsearch = new ElasticsearchInside.Elasticsearch())
{
var elasticClient = new ElasticClient(new ConnectionSettings(elasticsearch.Url).EnableDebugMode(x =>
{
Debug.WriteLine(x.DebugInformation);
}));

var camaro = new Car { Name = "Camaro", Year = 2020 };
var corvette = new Car { Name = "Corvette", Year = 2010 };
var monteCarlo = new Car { Name = "Monte Carlo", Year = 2000 };

elasticClient.Index(camaro, x => x.Index("vehicles"));
elasticClient.Index(corvette, x => x.Index("vehicles"));
elasticClient.Index(monteCarlo, x => x.Index("vehicles"));

elasticClient.Refresh("vehicles");

var noResults = elasticClient.Search<Car>(s => s.Index("vehicles").Query(
q => q.Bool(x => x.Filter(new { Year = Range.FromString<int>(yearRange) }))));

Assert.NotNull(noResults);
Assert.Equal(expectedResults, noResults.Hits.Count());
}
}
}
}
}
7 changes: 5 additions & 2 deletions tests/Filter.Nest.Tests/Car.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
namespace Filter.Nest.Tests
using System;

namespace Filter.Nest.Tests
{
public class Car
{
public bool IsElectric { get; set; }
public string Name { get; set; }
public DateTimeOffset StartProductionRun { get; set; }
public int Year { get; set; }
public bool IsElectric { get; set; }
}
}
4 changes: 4 additions & 0 deletions tests/Filter.Nest.Tests/Filter.Nest.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
<Project>{39ac08c4-a82d-45f7-bbf2-f239028e6d85}</Project>
<Name>Filter.Nest</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Range\Range.csproj">
<Project>{0e804077-79b6-4e0f-a4bb-ad5f6ec194b3}</Project>
<Name>Range</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
Expand Down
4 changes: 2 additions & 2 deletions tests/Range.Tests/Generic/Range`1Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ public void Should_convert_object_that_is_datetimeoffset_range_to_datetimeoffset
{
var dateTimeRange = new Range<DateTimeOffset>
{
MinValue = new DateTime(2000, 1, 1),
MaxValue = new DateTime(2020, 1, 1)
MinValue = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero),
MaxValue = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero)
} as object;

var result = Range.AsDateRange(dateTimeRange);
Expand Down

0 comments on commit fea37f5

Please sign in to comment.