diff --git a/docs/articles/configuration.md b/docs/articles/configuration.md
index ff2d4317..dc0d9554 100644
--- a/docs/articles/configuration.md
+++ b/docs/articles/configuration.md
@@ -138,6 +138,8 @@ These are the default field value types provided with Examine. Each value type c
| FacetTaxonomyDateDay | Just like DateTime but with precision only to the day. Stored in the Taxonomy Facet sidecar index. | ✅ |✅ | ✅ | ❌ | ✅ | - |
| FacetTaxonomyDateHour | Just like DateTime but with precision only to the hour. Stored in the Taxonomy Facet sidecar index. | ✅ |✅ | ✅ | ❌ | ✅ | - |
| FacetTaxonomyDateMinute | Just like DateTime but with precision only to the minute. Stored in the Taxonomy Facet sidecar index. | ✅ |✅ | ✅ | ❌ | ✅ | - |
+| GeoSpatialWKT | GeoSpatial field using WKT GeoSpatial format. Uses the GeoSpatialPrefixTreeStrategy | ✅ | ❌ | ✅ | ✅ | ✅ | - |
+
### Custom field value types
A field value type is defined by [`IIndexFieldValueType`](xref:Examine.Lucene.Indexing.IIndexFieldValueType)
diff --git a/docs/articles/filtering.md b/docs/articles/filtering.md
new file mode 100644
index 00000000..03ffccfb
--- /dev/null
+++ b/docs/articles/filtering.md
@@ -0,0 +1,245 @@
+---
+title: Filtering
+permalink: /filtering
+uid: filtering
+order: 3
+---
+Filtering
+===
+
+_**Tip**: There are many examples of filtering in the [`FluentApiTests` source code](https://github.com/Shazwazza/Examine/blob/release/3.0/src/Examine.Test/Examine.Lucene/Search/FluentApiTests.cs) to use as examples/reference._
+
+## Common
+
+Obtain an instance of [`ISearcher`](xref:Examine.ISearcher) for the index to be searched from [`IExamineManager`](xref:Examine.IExamineManager).
+
+### Terms and Phrases
+
+When filtering on fields like in the example above you might want to search on more than one word/term. In Examine this can be done by simply adding more terms to the term filter.
+
+### Term Filter
+
+```csharp
+var searcher = myIndex.Searcher;
+var results = searcher.CreateQuery()
+ // Look for any addresses that has "Hills" or "Rockyroad" or "Hollywood"
+ .WithFilter(
+ filter =>
+ {
+ filter.TermFilter(new FilterTerm("Address", "Hills Rockyroad Hollywood"));
+ })
+ .All()
+ .Execute();
+```
+
+### Terms Filter
+
+```csharp
+var searcher = myIndex.Searcher;
+var results = searcher.CreateQuery()
+ // Look for any addresses that has "Hills" or "Rockyroad" or "Hollywood"
+ .WithFilter(
+ filter =>
+ {
+ filter.TermsFilter(new[] {new FilterTerm("Address", "Hills"), new FilterTerm("Address", "Rockyroad"), new FilterTerm("Address", "Hollywood") });
+ })
+ .All()
+ .Execute();
+```
+
+### Term Prefix Filter
+
+```csharp
+var searcher = myIndex.Searcher;
+var results = searcher.CreateQuery()
+ // Look for any addresses that starts with "Hills"
+ .WithFilter(
+ filter =>
+ {
+ filter.TermPrefixFilter(new FilterTerm("Address", "Hills"));
+ })
+ .All()
+ .Execute();
+```
+
+## Range Filters
+
+Range Filters allow one to match documents whose field(s) values are between the lower and upper bound specified by the Range Filter
+
+### Int Range
+
+Example:
+
+```csharp
+var searcher = myIndex.Searcher;
+var query = searcher.CreateQuery();
+ query.WithFilter(
+ filter =>
+ {
+ filter.IntRangeFilter("SomeInt", 0, 100, minInclusive: true, maxInclusive: true);
+ }).All();
+var results = query.Execute(QueryOptions.Default);
+```
+
+This will return results where the field `SomeInt` is within the range 0 - 100 (min value and max value included).
+
+### Long Range
+
+Example:
+
+```csharp
+var searcher = myIndex.Searcher;
+var query = searcher.CreateQuery();
+ query.WithFilter(
+ filter =>
+ {
+ filter.LongRangeFilter("SomeLong", 0, 100, minInclusive: true, maxInclusive: true);
+ }).All();
+var results = query.Execute(QueryOptions.Default);
+```
+
+This will return results where the field `SomeLong` is within the range 0 - 100 (min value and max value included).
+
+### Float Range
+
+Example:
+
+```csharp
+var searcher = myIndex.Searcher;
+var query = searcher.CreateQuery();
+ query.WithFilter(
+ filter =>
+ {
+ filter.FloatRangeFilter("SomeFloat", 0f, 100f, minInclusive: true, maxInclusive: true);
+ }).All();
+var results = query.Execute(QueryOptions.Default);
+```
+
+This will return results where the field `SomeFloat` is within the range 0 - 100 (min value and max value included).
+
+### Double Range
+
+Example:
+
+```csharp
+var searcher = myIndex.Searcher;
+var query = searcher.CreateQuery();
+ query.WithFilter(
+ filter =>
+ {
+ filter.FloatRangeFilter("SomeDouble", 0.0, 100.0, minInclusive: true, maxInclusive: true);
+ }).All();
+var results = query.Execute(QueryOptions.Default);
+```
+
+This will return results where the field `SomeDouble` is within the range 0 - 100 (min value and max value included).
+
+## Booleans
+
+### Or
+
+```csharp
+var searcher = myIndex.Searcher;
+var results = searcher.CreateQuery()
+ // Look for any addresses that start with "Hills" or "Valleys"
+ .WithFilter(
+ filter =>
+ {
+ filter.TermPrefixFilter(new FilterTerm("Address", "Hills"))
+ .OrFilter()
+ filter.TermPrefixFilter(new FilterTerm("Address", "Valleys"));
+ })
+ .All()
+ .Execute();
+```
+
+### And
+
+```csharp
+var searcher = myIndex.Searcher;
+var results = searcher.CreateQuery()
+ // Look for any addresses that has "Hills" and keyword "Examine"
+ .WithFilter(
+ filter =>
+ {
+ filter.TermFilter(new FilterTerm("Address", "Hills"))
+ .AndFilter()
+ filter.TermFilter(new FilterTerm("Keyword", "Examine"));
+ })
+ .All()
+ .Execute();
+```
+
+### Not
+
+```csharp
+var searcher = myIndex.Searcher;
+var results = searcher.CreateQuery()
+ // Look for any addresses that has "Hills" and keyword "Examine"
+ .WithFilter(
+ filter =>
+ {
+ filter.TermFilter(new FilterTerm("Address", "Hills"))
+ .NotFilter()
+ filter.TermFilter(new FilterTerm("Keyword", "Examine"));
+ })
+ .All()
+ .Execute();
+```
+
+### And Not
+
+```csharp
+var searcher = myIndex.Searcher;
+var results = searcher.CreateQuery()
+ // Look for any addresses that has "Hills" and not keyword "Examine"
+ .WithFilter(
+ filter =>
+ {
+ filter.TermFilter(new FilterTerm("Address", "Hills"))
+ .AndNotFilter(innerFilter => innerFilter.TermFilter(new FilterTerm("Keyword", "Examine")));
+ })
+ .All()
+ .Execute();
+```
+
+## Spatial
+
+Examine supports Spatial Filtering.
+The Examine.Lucene.Spatial package needs to be installed.
+
+### Spatial Operations
+
+Below are the available Spatial Operations in Examine that are supported by the Examine.Lucene.Spatial package. Available operations may vary by provider.
+
+- ExamineSpatialOperation.Intersects
+- ExamineSpatialOperation.Overlaps
+- ExamineSpatialOperation.IsWithin
+- ExamineSpatialOperation.BoundingBoxIntersects
+- ExamineSpatialOperation.BoundingBoxWithin
+- ExamineSpatialOperation.Contains
+- ExamineSpatialOperation.IsDisjointTo
+- ExamineSpatialOperation.IsEqualTo
+
+### Spatial Filtering
+
+The `.SpatialOperationFilter()` method adds a filter to the query results to remove any results that do not pass the filter.
+The example below demonstrates filtering results where the shape stored in the "spatialWKT" field must intersect the rectangle defined.
+
+```csharp
+var query = searcher.CreateQuery()
+ .WithFilter(
+ filter => filter.SpatialOperationFilter("spatialWKT", ExamineSpatialOperation.Intersects, (shapeFactory) => shapeFactory.CreateRectangle(0.0, 1.0, 0.0, 1.0))
+ );
+```
+
+## Custom lucene filter
+
+```csharp
+var searcher = indexer.Searcher;
+var query = searcher.CreateQuery();
+
+var query = (LuceneSearchQuery)query.NativeQuery("hello:world").And(); // Make query ready for extending
+query.LuceneFilter(new TermFilter(new Term("nodeTypeAlias", "CWS_Home"))); // Add the raw lucene query
+var results = query.Execute();
+```
\ No newline at end of file
diff --git a/docs/articles/indexing.md b/docs/articles/indexing.md
index 7ad7ca84..a2a10a34 100644
--- a/docs/articles/indexing.md
+++ b/docs/articles/indexing.md
@@ -137,6 +137,34 @@ Data is easily deleted from the index by the unique identifier you provided in y
indexer.DeleteFromIndex("SKU987");
```
+### Indexing Spatial Shapes
+
+For the Lucene Provider, the package Examine.Lucene.Spatial must be installed.
+
+As you can see, the values being passed into the ValueSet are type `IExamineSpatialShape` created using the `IExamineSpatialShapeFactory` retrieved from the ValueType of the field. A [field definition](configuration#custom-field-definitions) must be set.
+
+Example for Geo Spatial field storing WKT:
+
+```cs
+{FieldDefinitionTypes.GeoSpatialWKT, name => new WKTSpatialIndexFieldValueType(name, loggerFactory, SpatialIndexFieldValueTypeBase.GeoSpatialPrefixTreeStrategyFactory(),true)},
+```
+Indexing example:
+
+```cs
+var geoSpatialFieldType = myIndex.FieldValueTypeCollection.ValueTypes.First(f
+ => f.FieldName.Equals("spatialWKT", StringComparison.InvariantCultureIgnoreCase)) as ISpatialIndexFieldValueTypeShapesBase;
+
+var fieldShapeFactory = geoSpatialFieldType.ExamineSpatialShapeFactory;
+
+ myIndex.IndexItem(
+ ValueSet.FromObject(1.ToString(), "content",
+ new {
+ nodeName = "my name 1",
+ updateDate = now.AddDays(2).ToString("yyyy-MM-dd"),
+ spatialWKT = fieldShapeFactory.CreatePoint(0.0,0.0) })
+ );
+```
+
## Events
#### [IIndex.IndexOperationComplete](xref:Examine.IIndex#Examine_IIndex_IndexOperationComplete)
diff --git a/docs/articles/sorting.md b/docs/articles/sorting.md
index a2e981d3..9725533e 100644
--- a/docs/articles/sorting.md
+++ b/docs/articles/sorting.md
@@ -42,3 +42,24 @@ var orderedDescendingResults = searcher
.OrderByDescending(new SortableField("name", SortType.String))
.Execute();
```
+
+## Spatial Sorting
+
+Example order by distance from a Point.
+
+```cs
+ // Retrieve the Shape Factory from the field
+ var geoSpatialFieldType = myIndex.FieldValueTypeCollection.ValueTypes.First(f
+ => f.FieldName.Equals("spatialWKT", StringComparison.InvariantCultureIgnoreCase)) as ISpatialIndexFieldValueTypeBase;
+
+var fieldShapeFactory = geoSpatialFieldType.SpatialShapeFactory;
+
+// Define the location to compare against
+var searchLocation = fieldShapeFactory.CreatePoint(0.0, 0.0);
+
+// Order by the distance between the center of the Shape in the "spatialWKT" vs the search location, Ascending.
+var orderedDescendingResults = searcher
+ .CreateQuery("content")
+ .OrderBy(new SortableField("spatialWKT", searchLocation))
+ ).Execute();
+```
diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml
index cbe731e4..ab463b5d 100644
--- a/docs/articles/toc.yml
+++ b/docs/articles/toc.yml
@@ -4,6 +4,8 @@
href: indexing.md
- name: Searching
href: searching.md
+- name: Filtering
+ href: filtering.md
- name: Sorting
href: sorting.md
- name: Paging
diff --git a/docs/docs-v1-v2/searching.md b/docs/docs-v1-v2/searching.md
index 2905d780..79356f23 100644
--- a/docs/docs-v1-v2/searching.md
+++ b/docs/docs-v1-v2/searching.md
@@ -248,4 +248,4 @@ var query = searcher.CreateQuery();
var query = (LuceneSearchQuery)query.NativeQuery("hello:world").And(); // Make query ready for extending
query.LuceneQuery(NumericRangeQuery.NewInt64Range("numTest", 4, 5, true, true)); // Add the raw lucene query
var results = query.Execute();
-```
\ No newline at end of file
+```
diff --git a/docs/docs-v1-v2/sorting.md b/docs/docs-v1-v2/sorting.md
index 4d21aa7d..3e664348 100644
--- a/docs/docs-v1-v2/sorting.md
+++ b/docs/docs-v1-v2/sorting.md
@@ -41,8 +41,17 @@ var orderedDescendingResults = searcher
.Field("writerName", "administrator")
.OrderByDescending(new SortableField("name", SortType.String))
.Execute();
+
+// Mixing ascending and descending
+var orderedDescendingResults = searcher
+ .CreateQuery("content")
+ .Field("writerName", "administrator")
+ .OrderByDescending(new SortableField("name", SortType.String)),
+ .OrderBy(new SortableField("date", SortType.String))
+ .Execute();
```
+
## Limiting results
To limit results we can use the `QueryOptions` class when executing a search query. The `QueryOptions` class provides the ability to skip and take.
diff --git a/src/Examine.Core/FieldDefinitionTypes.cs b/src/Examine.Core/FieldDefinitionTypes.cs
index 5fe13cb3..32478a28 100644
--- a/src/Examine.Core/FieldDefinitionTypes.cs
+++ b/src/Examine.Core/FieldDefinitionTypes.cs
@@ -13,10 +13,16 @@ public static class FieldDefinitionTypes
public const string Integer = "int";
///
- /// Will be indexed as a float
+ /// Will be indexed as a single
///
public const string Float = "float";
+ ///
+ /// Will be indexed as a single
+ ///
+ [Obsolete("To remove in Examine V5. Use Float")]
+ public const string Single = "single";
+
///
/// Will be indexed as a double
///
@@ -203,5 +209,11 @@ public static class FieldDefinitionTypes
public const string FacetTaxonomyFullTextSortable = "facettaxonomyfulltextsortable";
+
+ ///
+ /// GEO Spatial Shape. Index as WKT
+ ///
+ public const string GeoSpatialWKT = "spatial.geo.wkt";
+
}
}
diff --git a/src/Examine.Core/ISpatialIndexFieldValueTypeShapesBase.cs b/src/Examine.Core/ISpatialIndexFieldValueTypeShapesBase.cs
new file mode 100644
index 00000000..2be135dd
--- /dev/null
+++ b/src/Examine.Core/ISpatialIndexFieldValueTypeShapesBase.cs
@@ -0,0 +1,15 @@
+using Examine.Search;
+
+namespace Examine
+{
+ ///
+ /// Spatial Index Field Value Type Shape Factory
+ ///
+ public interface ISpatialIndexFieldValueTypeShapesBase
+ {
+ ///
+ /// Gets the Shape Factory for the fields spatial strategy
+ ///
+ ISpatialShapeFactory SpatialShapeFactory { get; }
+ }
+}
diff --git a/src/Examine.Core/Search/ExamineSpatialDistanceComparison.cs b/src/Examine.Core/Search/ExamineSpatialDistanceComparison.cs
new file mode 100644
index 00000000..efe1dbef
--- /dev/null
+++ b/src/Examine.Core/Search/ExamineSpatialDistanceComparison.cs
@@ -0,0 +1,28 @@
+namespace Examine.Search
+{
+ ///
+ /// Type of Spatial Distance Comparison
+ ///
+ public enum ExamineSpatialDistanceComparison
+ {
+ ///
+ /// Distance Less Than
+ ///
+ LessThan = 0,
+
+ ///
+ /// Distance Less Than or EEqual
+ ///
+ LessThanOrEqual = 1,
+
+ ///
+ /// Distance Greater Than
+ ///
+ GreaterThan = 2,
+
+ ///
+ /// Distance Greater Than or EEqual
+ ///
+ GreaterThanOrEqual = 3,
+ }
+}
diff --git a/src/Examine.Core/Search/ExamineSpatialOperation.cs b/src/Examine.Core/Search/ExamineSpatialOperation.cs
new file mode 100644
index 00000000..e2afe194
--- /dev/null
+++ b/src/Examine.Core/Search/ExamineSpatialOperation.cs
@@ -0,0 +1,17 @@
+namespace Examine.Search
+{
+ ///
+ /// Spatial Operation Type
+ ///
+ public enum ExamineSpatialOperation
+ {
+ Intersects = 0,
+ Overlaps = 1,
+ IsWithin = 2,
+ BoundingBoxIntersects = 3,
+ BoundingBoxWithin = 4,
+ Contains = 5,
+ IsDisjointTo = 6,
+ IsEqualTo = 7
+ }
+}
diff --git a/src/Examine.Core/Search/FilterTerm.cs b/src/Examine.Core/Search/FilterTerm.cs
new file mode 100644
index 00000000..79643c68
--- /dev/null
+++ b/src/Examine.Core/Search/FilterTerm.cs
@@ -0,0 +1,30 @@
+namespace Examine.Search
+{
+ ///
+ /// Term
+ ///
+ public struct FilterTerm
+ {
+ ///
+ /// Name of the Field
+ ///
+ public string FieldName { get; }
+
+ ///
+ /// Value of the Term
+ ///
+ public string FieldValue { get; }
+
+ ///
+ /// Constructor
+ ///
+ /// Name of the Field
+ /// Value of the Term
+ public FilterTerm(string fieldName, string fieldValue)
+ {
+ FieldName = fieldName;
+ FieldValue = fieldValue;
+ }
+
+ }
+}
diff --git a/src/Examine.Core/Search/IBooleanFilterOperation.cs b/src/Examine.Core/Search/IBooleanFilterOperation.cs
new file mode 100644
index 00000000..1f6af2b4
--- /dev/null
+++ b/src/Examine.Core/Search/IBooleanFilterOperation.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Examine.Search
+{
+ public interface IBooleanFilterOperation
+ {
+ ///
+ /// Sets the next operation to be AND
+ ///
+ ///
+ IFilter AndFilter();
+
+ ///
+ /// Adds the nested filter
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation AndFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ ///
+ /// Sets the next operation to be OR
+ ///
+ ///
+ IFilter OrFilter();
+
+ ///
+ /// Adds the nested filter
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation OrFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ ///
+ /// Sets the next operation to be NOT
+ ///
+ ///
+ IFilter NotFilter();
+
+ ///
+ /// Adds the nested filter
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation AndNotFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+ }
+}
diff --git a/src/Examine.Core/Search/IBooleanOperation.cs b/src/Examine.Core/Search/IBooleanOperation.cs
index 8ac3dd66..80966a40 100644
--- a/src/Examine.Core/Search/IBooleanOperation.cs
+++ b/src/Examine.Core/Search/IBooleanOperation.cs
@@ -1,4 +1,4 @@
-
+
using System;
namespace Examine.Search
diff --git a/src/Examine.Core/Search/IFilter.cs b/src/Examine.Core/Search/IFilter.cs
new file mode 100644
index 00000000..94f89636
--- /dev/null
+++ b/src/Examine.Core/Search/IFilter.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+
+namespace Examine.Search
+{
+ public interface IFilter
+ {
+ ///
+ /// Term must match
+ ///
+ ///
+ ///
+ IBooleanFilterOperation TermFilter(FilterTerm term);
+
+ ///
+ /// Terms must match
+ ///
+ ///
+ ///
+ IBooleanFilterOperation TermsFilter(IEnumerable terms);
+
+ ///
+ /// Term must match as prefix
+ ///
+ ///
+ ///
+ IBooleanFilterOperation TermPrefixFilter(FilterTerm term);
+
+ ///
+ /// Document must have value for field
+ ///
+ ///
+ ///
+ IBooleanFilterOperation FieldValueExistsFilter(string field);
+
+ ///
+ /// Document must not have value for field
+ ///
+ ///
+ ///
+ IBooleanFilterOperation FieldValueNotExistsFilter(string field);
+
+ ///
+ /// Must match query
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation QueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ ///
+ /// Matches items as defined by the IIndexFieldValueType used for the fields specified.
+ /// If a type is not defined for a field name, or the type does not implement IIndexRangeValueType for the types of min and max, nothing will be added
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation IntRangeFilter(string field, int? min, int? max, bool minInclusive, bool maxInclusive);
+
+
+ ///
+ /// Matches items as defined by the IIndexFieldValueType used for the fields specified.
+ /// If a type is not defined for a field name, or the type does not implement IIndexRangeValueType for the types of min and max, nothing will be added
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation LongRangeFilter(string field, long? min, long? max, bool minInclusive, bool maxInclusive);
+
+
+ ///
+ /// Matches items as defined by the IIndexFieldValueType used for the fields specified.
+ /// If a type is not defined for a field name, or the type does not implement IIndexRangeValueType for the types of min and max, nothing will be added
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation FloatRangeFilter(string field, float? min, float? max, bool minInclusive, bool maxInclusive);
+
+
+ ///
+ /// Matches items as defined by the IIndexFieldValueType used for the fields specified.
+ /// If a type is not defined for a field name, or the type does not implement IIndexRangeValueType for the types of min and max, nothing will be added
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ IBooleanFilterOperation DoubleRangeFilter(string field, double? min, double? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ /// Executes Spatial operation as a Filter on field and shape
+ ///
+ /// Index field name
+ /// Shape
+ ///
+ IBooleanFilterOperation SpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape);
+ }
+}
diff --git a/src/Examine.Core/Search/INestedBooleanFilterOperation.cs b/src/Examine.Core/Search/INestedBooleanFilterOperation.cs
new file mode 100644
index 00000000..072bfe2e
--- /dev/null
+++ b/src/Examine.Core/Search/INestedBooleanFilterOperation.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Examine.Search
+{
+ public interface INestedBooleanFilterOperation
+ {
+ ///
+ /// Sets the next operation to be AND
+ ///
+ ///
+ INestedFilter And();
+
+ ///
+ /// Adds the nested filter
+ ///
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation And(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ ///
+ /// Sets the next operation to be OR
+ ///
+ ///
+ INestedFilter Or();
+
+ ///
+ /// Adds the nested filter
+ ///
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation Or(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ ///
+ /// Sets the next operation to be NOT
+ ///
+ ///
+ INestedFilter Not();
+
+ ///
+ /// Adds the nested filter
+ ///
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation AndNot(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+ }
+}
diff --git a/src/Examine.Core/Search/INestedFilter.cs b/src/Examine.Core/Search/INestedFilter.cs
new file mode 100644
index 00000000..6eb154ca
--- /dev/null
+++ b/src/Examine.Core/Search/INestedFilter.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+
+namespace Examine.Search
+{
+ public interface INestedFilter
+ {
+ ///
+ /// Term must match
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation NestedTermFilter(FilterTerm term);
+
+ ///
+ /// Terms must match
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation NestedTermsFilter(IEnumerable terms);
+
+ ///
+ /// Term must match as prefix
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation NestedTermPrefix(FilterTerm term);
+
+ ///
+ /// Document must have value for field
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation NestedFieldValueExists(string field);
+
+ ///
+ /// Document must not have value for field
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation NestedFieldValueNotExists(string field);
+
+ ///
+ /// Must match query
+ ///
+ ///
+ ///
+ ///
+ INestedBooleanFilterOperation NestedQueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ ///
+ /// Executes Spatial operation as a Filter on field and shape
+ ///
+ /// Index field name
+ /// Shape
+ ///
+ INestedBooleanFilterOperation NestedSpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape);
+ }
+}
diff --git a/src/Examine.Core/Search/IOrdering.cs b/src/Examine.Core/Search/IOrdering.cs
index 00acae92..806ae77f 100644
--- a/src/Examine.Core/Search/IOrdering.cs
+++ b/src/Examine.Core/Search/IOrdering.cs
@@ -40,6 +40,6 @@ public interface IOrdering : IQueryExecutor
/// Return all fields in the index
///
///
- IOrdering SelectAllFields();
+ IOrdering SelectAllFields();
}
}
diff --git a/src/Examine.Core/Search/IQuery.cs b/src/Examine.Core/Search/IQuery.cs
index 3a172e03..71c7e03b 100644
--- a/src/Examine.Core/Search/IQuery.cs
+++ b/src/Examine.Core/Search/IQuery.cs
@@ -134,5 +134,20 @@ public interface IQuery
///
///
IBooleanOperation RangeQuery(string[] fields, T? min, T? max, bool minInclusive = true, bool maxInclusive = true) where T : struct;
+
+ ///
+ /// Executes Spatial operation on field and shape as a Query
+ ///
+ /// Index field name
+ /// Shape
+ ///
+ IBooleanOperation SpatialOperationQuery(string field, ExamineSpatialOperation spatialOperation, Func shape);
+
+ ///
+ /// Apply Filters
+ ///
+ ///
+ ///
+ IQuery WithFilter(Action filter);
}
}
diff --git a/src/Examine.Core/Search/ISpatialCircle.cs b/src/Examine.Core/Search/ISpatialCircle.cs
new file mode 100644
index 00000000..7c783e80
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialCircle.cs
@@ -0,0 +1,13 @@
+namespace Examine.Search
+{
+ ///
+ /// Spatial Circle Shape
+ ///
+ public interface ISpatialCircle : ISpatialShape
+ {
+ ///
+ /// Circle Radius
+ ///
+ double Radius { get; }
+ }
+}
diff --git a/src/Examine.Core/Search/ISpatialEmptyShape.cs b/src/Examine.Core/Search/ISpatialEmptyShape.cs
new file mode 100644
index 00000000..295f688e
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialEmptyShape.cs
@@ -0,0 +1,10 @@
+namespace Examine.Search
+{
+ ///
+ /// Empty Spatial Shape
+ ///
+ public interface ISpatialEmptyShape : ISpatialShape
+ {
+
+ }
+}
diff --git a/src/Examine.Core/Search/ISpatialLineString.cs b/src/Examine.Core/Search/ISpatialLineString.cs
new file mode 100644
index 00000000..02b626e5
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialLineString.cs
@@ -0,0 +1,9 @@
+namespace Examine.Search
+{
+ ///
+ /// Spatial Line String Shape
+ ///
+ public interface ISpatialLineString : ISpatialShape
+ {
+ }
+}
diff --git a/src/Examine.Core/Search/ISpatialPoint.cs b/src/Examine.Core/Search/ISpatialPoint.cs
new file mode 100644
index 00000000..b9a0ec8d
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialPoint.cs
@@ -0,0 +1,18 @@
+namespace Examine.Search
+{
+ ///
+ /// Spatial Point Shape
+ ///
+ public interface ISpatialPoint : ISpatialShape
+ {
+ ///
+ /// The X coordinate, or Longitude in geospatial contexts.
+ ///
+ double X { get; }
+
+ ///
+ /// The Y coordinate, or Latitude in geospatial contexts.
+ ///
+ double Y { get; }
+ }
+}
diff --git a/src/Examine.Core/Search/ISpatialRectangle.cs b/src/Examine.Core/Search/ISpatialRectangle.cs
new file mode 100644
index 00000000..8fa4aa0b
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialRectangle.cs
@@ -0,0 +1,20 @@
+namespace Examine.Search
+{
+ ///
+ /// Spatial Rectangle Shape
+ ///
+ public interface ISpatialRectangle : ISpatialShape
+ {
+ /// The left edge of the X coordinate.
+ double MinX { get; }
+
+ /// The bottom edge of the Y coordinate.
+ double MinY { get; }
+
+ /// The right edge of the X coordinate.
+ double MaxX { get; }
+
+ /// The top edge of the Y coordinate.
+ double MaxY { get; }
+ }
+}
diff --git a/src/Examine.Core/Search/ISpatialShape.cs b/src/Examine.Core/Search/ISpatialShape.cs
new file mode 100644
index 00000000..b6331ca6
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialShape.cs
@@ -0,0 +1,18 @@
+namespace Examine.Search
+{
+ ///
+ /// Spatial Shape
+ ///
+ public interface ISpatialShape
+ {
+ ///
+ /// Center Point of Shape
+ ///
+ ISpatialPoint Center { get; }
+
+ ///
+ /// Whether the Shape is Empty
+ ///
+ bool IsEmpty { get; }
+ }
+}
diff --git a/src/Examine.Core/Search/ISpatialShapeCollection.cs b/src/Examine.Core/Search/ISpatialShapeCollection.cs
new file mode 100644
index 00000000..390f62f1
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialShapeCollection.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace Examine.Search
+{
+ ///
+ /// A Collection of Shapes
+ ///
+ public interface ISpatialShapeCollection : ISpatialShape
+ {
+ ///
+ /// Shapes in the Shape Collection
+ ///
+ IList Shapes { get; }
+ }
+}
diff --git a/src/Examine.Core/Search/ISpatialShapeFactory.cs b/src/Examine.Core/Search/ISpatialShapeFactory.cs
new file mode 100644
index 00000000..ed6e9d1e
--- /dev/null
+++ b/src/Examine.Core/Search/ISpatialShapeFactory.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Examine.Search
+{
+ ///
+ /// Creates Shapes
+ ///
+ public interface ISpatialShapeFactory
+ {
+ ///
+ /// Create a Point
+ ///
+ ///
+ ///
+ ///
+ ISpatialPoint CreatePoint(double x, double y);
+
+ ///
+ /// Create a Point from a Latitude and longitude
+ ///
+ ///
+ ///
+ ///
+ ISpatialPoint CreateGeoPoint(double latitude, double longitude);
+
+ ///
+ /// Create a Rectangle.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ISpatialRectangle CreateRectangle(double minX, double maxX, double minY, double maxY);
+
+ ///
+ /// Creates a Empty Shape. Used for not exists
+ ///
+ ///
+ ISpatialEmptyShape CreateEmpty();
+
+ ///
+ /// Create a circle
+ ///
+ ///
+ ///
+ ///
+ ///
+ ISpatialCircle CreateCircle(double x, double y, double distance);
+
+ ///
+ /// Create a Circle around a point on a spherical Earth model (Mean Earth Radius in Kilometers)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ISpatialCircle CreateEarthMeanSearchRadiusKMCircle(double x, double y, double radius);
+
+ ///
+ /// Create a Circle around a point on a spherical Earth model (Equatorial Earth Radius in Kilometers)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ISpatialCircle CreateEarthEquatorialSearchRadiusKMCircle(double x, double y, double radius);
+
+ ///
+ /// Create a Line String from a ordered set of Points (Vertices)
+ ///
+ ///
+ ///
+ ISpatialLineString CreateLineString(IList points);
+
+ ///
+ /// Create a Shape Collection from a list of Shapes
+ ///
+ ///
+ ///
+ ISpatialShapeCollection CreateShapeCollection(IList shapes);
+ }
+}
diff --git a/src/Examine.Core/Search/SortDirection.cs b/src/Examine.Core/Search/SortDirection.cs
new file mode 100644
index 00000000..ddb72f8b
--- /dev/null
+++ b/src/Examine.Core/Search/SortDirection.cs
@@ -0,0 +1,18 @@
+namespace Examine.Search
+{
+ ///
+ /// Sort Direction
+ ///
+ public enum SortDirection
+ {
+ ///
+ /// Sort Ascending
+ ///
+ Ascending = 0,
+
+ ///
+ /// Sort Descending
+ ///
+ Descending = 1
+ }
+}
diff --git a/src/Examine.Core/Search/SortType.cs b/src/Examine.Core/Search/SortType.cs
index 33c4ac47..1f6180eb 100644
--- a/src/Examine.Core/Search/SortType.cs
+++ b/src/Examine.Core/Search/SortType.cs
@@ -46,6 +46,11 @@ public enum SortType
/// lower values are at the front.
///
///
- Double
+ Double,
+
+ ///
+ /// Sort using distance
+ ///
+ SpatialDistance
}
}
diff --git a/src/Examine.Core/Search/SortableField.cs b/src/Examine.Core/Search/SortableField.cs
index 2ef14efa..29a7ada5 100644
--- a/src/Examine.Core/Search/SortableField.cs
+++ b/src/Examine.Core/Search/SortableField.cs
@@ -1,4 +1,4 @@
-namespace Examine.Search
+namespace Examine.Search
{
///
/// Represents a field used to sort results
@@ -15,25 +15,44 @@ public struct SortableField
///
public SortType SortType { get; }
+ ///
+ /// The point to calculate distance from
+ ///
+ public ISpatialPoint SpatialPoint { get; }
+
///
/// Constructor
///
- ///
+ /// The field name to sort by
public SortableField(string fieldName)
{
FieldName = fieldName;
SortType = SortType.String;
+ SpatialPoint = null;
}
///
/// Constructor
///
- ///
- ///
+ /// The field name to sort by
+ /// The way in which the results will be sorted by the field specified.
public SortableField(string fieldName, SortType sortType)
{
FieldName = fieldName;
SortType = sortType;
+ SpatialPoint = null;
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The field name to sort by
+ /// The point to calculate distance from
+ public SortableField(string fieldName, ISpatialPoint spatialPoint)
+ {
+ FieldName = fieldName;
+ SortType = SortType.SpatialDistance;
+ SpatialPoint = spatialPoint;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Examine.Lucene.Spatial/Examine.Lucene.Spatial.csproj b/src/Examine.Lucene.Spatial/Examine.Lucene.Spatial.csproj
new file mode 100644
index 00000000..76b9a247
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Examine.Lucene.Spatial.csproj
@@ -0,0 +1,26 @@
+
+
+
+ disable
+ disable
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Examine.Lucene.Spatial/Indexing/SpatialIndexFieldValueTypeBase.cs b/src/Examine.Lucene.Spatial/Indexing/SpatialIndexFieldValueTypeBase.cs
new file mode 100644
index 00000000..8ae9271f
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Indexing/SpatialIndexFieldValueTypeBase.cs
@@ -0,0 +1,74 @@
+using Lucene.Net.Spatial;
+using System;
+using Microsoft.Extensions.Logging;
+using Lucene.Net.Spatial.Queries;
+using Examine.Search;
+using Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Prefix;
+using Spatial4n.Context;
+using Lucene.Net.Search;
+using Examine.Lucene.Indexing;
+
+namespace Examine.Lucene.Spatial.Indexing
+{
+ ///
+ /// Spatial Index Field Value Type
+ ///
+ public abstract class SpatialIndexFieldValueTypeBase : IndexFieldValueTypeBase, ISpatialIndexFieldValueTypeBase
+ {
+ ///
+ /// Spatial Strategy for Field
+ ///
+ public SpatialStrategy SpatialStrategy { get; }
+
+ ///
+ /// Spatial Args Parser for Field
+ ///
+ public SpatialArgsParser SpatialArgsParser { get; }
+
+
+ ///
+ public abstract ISpatialShapeFactory SpatialShapeFactory { get; }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected SpatialIndexFieldValueTypeBase(string fieldName, ILoggerFactory loggerFactory, Func spatialStrategyFactory, bool store = true)
+ : base(fieldName, loggerFactory, store)
+ {
+ SpatialStrategy = spatialStrategyFactory(fieldName);
+ SpatialArgsParser = new SpatialArgsParser();
+ }
+
+ ///
+ public abstract SortField ToSpatialDistanceSortField(SortableField sortableField, SortDirection sortDirection);
+
+ ///
+ /// Creates a RecursivePrefixTreeStrategy for A Geo SpatialContext
+ ///
+ /// Default value of 11 results in sub-meter precision for geohash
+ /// SpatialStrategy Factory
+ public static Func GeoSpatialPrefixTreeStrategyFactory(int maxLevels = 11)
+ {
+ Func geoSpatialPrefixTreeStrategy = (fieldName) =>
+ {
+ var ctx = SpatialContext.Geo;
+
+ SpatialPrefixTree grid = new GeohashPrefixTree(ctx, maxLevels);
+ var strategy = new RecursivePrefixTreeStrategy(grid, fieldName);
+ return strategy;
+ };
+ return geoSpatialPrefixTreeStrategy;
+ }
+
+ ///
+ public abstract Query GetQuery(string field, ExamineSpatialOperation spatialOperation, Func shape);
+
+ ///
+ public abstract Filter GetFilter(string field, ExamineSpatialOperation spatialOperation, Func shape);
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Indexing/WKTSpatialIndexFieldValueType.cs b/src/Examine.Lucene.Spatial/Indexing/WKTSpatialIndexFieldValueType.cs
new file mode 100644
index 00000000..5ab2695e
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Indexing/WKTSpatialIndexFieldValueType.cs
@@ -0,0 +1,149 @@
+using System;
+using Examine.Lucene.Search;
+using Examine.Lucene.Spatial.Search;
+using Examine.Search;
+using Lucene.Net.Documents;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Search;
+using Lucene.Net.Spatial;
+using Lucene.Net.Spatial.Queries;
+using Microsoft.Extensions.Logging;
+using Spatial4n.Distance;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Indexing
+{
+ ///
+ /// WKT Spatial Index Field Value Type
+ ///
+ public class WKTSpatialIndexFieldValueType : SpatialIndexFieldValueTypeBase
+ {
+ private readonly bool _stored;
+ private Spatial4nShapeFactory _shapeFactory;
+
+ ///
+ public override ISpatialShapeFactory SpatialShapeFactory => _shapeFactory;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ /// Given field name, return Spatial Strategy
+ ///
+ public WKTSpatialIndexFieldValueType(string fieldName, ILoggerFactory loggerFactory, Func spatialStrategyFactory, bool stored = true)
+ : base(fieldName, loggerFactory, spatialStrategyFactory, true)
+ {
+ _stored = stored;
+ _shapeFactory = new Spatial4nShapeFactory(SpatialStrategy.SpatialContext);
+ }
+
+ ///
+ protected override void AddSingleValue(Document doc, object value)
+ {
+ if (TryConvert(value, out var examineLuceneShape))
+ {
+ IShape shape = examineLuceneShape.Shape;
+ foreach (Field field in SpatialStrategy.CreateIndexableFields(shape))
+ {
+ doc.Add(field);
+ }
+
+ if (_stored)
+ {
+ doc.Add(new StoredField(ExamineFieldNames.SpecialFieldPrefix + FieldName, SpatialStrategy.SpatialContext.ToString(shape)));
+ }
+ }
+ else if (TryConvert(value, out var str))
+ {
+ IShape shape = SpatialStrategy.SpatialContext.ReadShapeFromWkt(str);
+ foreach (Field field in SpatialStrategy.CreateIndexableFields(shape))
+ {
+ doc.Add(field);
+ }
+
+ if (_stored)
+ {
+ doc.Add(new StoredField(ExamineFieldNames.SpecialFieldPrefix + FieldName, str));
+ }
+ }
+ }
+
+ ///
+ public override Query GetQuery(string query)
+ {
+ var spatialArgs = SpatialArgsParser.Parse(query, SpatialStrategy.SpatialContext);
+ return SpatialStrategy.MakeQuery(spatialArgs);
+ }
+
+ ///
+ public override SortField ToSpatialDistanceSortField(SortableField sortableField, SortDirection sortDirection)
+ {
+ var pt = (sortableField.SpatialPoint as ExamineLucenePoint).Shape as IPoint;
+ if (!SpatialStrategy.SpatialContext.IsGeo)
+ {
+ throw new NotSupportedException("This implementation may not be suitable for non GeoSpatial SpatialContext");
+ }
+ var valueSource = SpatialStrategy.MakeDistanceValueSource(pt, DistanceUtils.DegreesToKilometers);//the distance (in km)
+ return(valueSource.GetSortField(sortDirection == SortDirection.Descending));
+ }
+
+ ///
+ public override Query GetQuery(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ {
+ var shapeVal = shape(SpatialShapeFactory);
+ var luceneSpatialOperation = MapToSpatialOperation(spatialOperation);
+ var spatial4nShape = (shapeVal as ExamineLuceneShape)?.Shape;
+ var spatialArgs = new SpatialArgs(luceneSpatialOperation, spatial4nShape);
+ var query = SpatialStrategy.MakeQuery(spatialArgs);
+ return query;
+ }
+
+ ///
+ public override Filter GetFilter(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ {
+ var shapeVal = shape(SpatialShapeFactory);
+ var luceneSpatialOperation = MapToSpatialOperation(spatialOperation);
+ var spatial4nShape = (shapeVal as ExamineLuceneShape)?.Shape;
+ var spatialArgs = new SpatialArgs(luceneSpatialOperation, spatial4nShape);
+ var filter = SpatialStrategy.MakeFilter(spatialArgs);
+ return filter;
+ }
+
+ private static SpatialOperation MapToSpatialOperation(ExamineSpatialOperation spatialOperation)
+ {
+ SpatialOperation luceneSpatialOperation;
+ switch (spatialOperation)
+ {
+ case ExamineSpatialOperation.Intersects:
+ luceneSpatialOperation = SpatialOperation.Intersects;
+ break;
+ case ExamineSpatialOperation.Overlaps:
+ luceneSpatialOperation = SpatialOperation.Overlaps;
+ break;
+ case ExamineSpatialOperation.IsWithin:
+ luceneSpatialOperation = SpatialOperation.IsWithin;
+ break;
+ case ExamineSpatialOperation.BoundingBoxIntersects:
+ luceneSpatialOperation = SpatialOperation.BBoxIntersects;
+ break;
+ case ExamineSpatialOperation.BoundingBoxWithin:
+ luceneSpatialOperation = SpatialOperation.BBoxWithin;
+ break;
+ case ExamineSpatialOperation.Contains:
+ luceneSpatialOperation = SpatialOperation.Contains;
+ break;
+ case ExamineSpatialOperation.IsDisjointTo:
+ luceneSpatialOperation = SpatialOperation.IsDisjointTo;
+ break;
+ case ExamineSpatialOperation.IsEqualTo:
+ luceneSpatialOperation = SpatialOperation.IsEqualTo;
+ break;
+ default:
+ throw new NotSupportedException(nameof(spatialOperation));
+ }
+
+ return luceneSpatialOperation;
+ }
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/ExamineLuceneCircle.cs b/src/Examine.Lucene.Spatial/Search/ExamineLuceneCircle.cs
new file mode 100644
index 00000000..73078062
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/ExamineLuceneCircle.cs
@@ -0,0 +1,26 @@
+using Examine.Search;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ ///
+ /// Spatial Circle Shape
+ ///
+ public class ExamineLuceneCircle : ExamineLuceneShape, ISpatialCircle
+ {
+ private readonly ICircle _circle;
+
+ ///
+ /// Constructor
+ ///
+ /// Circle
+ public ExamineLuceneCircle(ICircle circle) : base(circle)
+ {
+ _circle = circle;
+ }
+
+ ///
+ public double Radius => _circle.Radius;
+
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/ExamineLuceneEmptyShape.cs b/src/Examine.Lucene.Spatial/Search/ExamineLuceneEmptyShape.cs
new file mode 100644
index 00000000..69dc16fd
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/ExamineLuceneEmptyShape.cs
@@ -0,0 +1,20 @@
+using Examine.Search;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ ///
+ /// Empty Spatial Shape
+ ///
+ public class ExamineLuceneEmptyShape : ExamineLuceneShape, ISpatialEmptyShape
+ {
+ ///
+ /// Constructor
+ ///
+ public ExamineLuceneEmptyShape() : base(null)
+ {
+ }
+
+ ///
+ public override bool IsEmpty => true;
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/ExamineLuceneLineString.cs b/src/Examine.Lucene.Spatial/Search/ExamineLuceneLineString.cs
new file mode 100644
index 00000000..459df507
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/ExamineLuceneLineString.cs
@@ -0,0 +1,22 @@
+using Examine.Search;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ ///
+ /// Spatial Line String Shape
+ ///
+ public class ExamineLuceneLineString : ExamineLuceneShape, ISpatialLineString
+ {
+ private readonly IShape _lineString;
+
+ ///
+ /// Constructor
+ ///
+ /// Line String Shape
+ public ExamineLuceneLineString(IShape lineString) : base(lineString)
+ {
+ _lineString = lineString;
+ }
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/ExamineLucenePoint.cs b/src/Examine.Lucene.Spatial/Search/ExamineLucenePoint.cs
new file mode 100644
index 00000000..c2726019
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/ExamineLucenePoint.cs
@@ -0,0 +1,34 @@
+using Examine.Search;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ ///
+ /// Spatial Point Shape
+ ///
+ public class ExamineLucenePoint : ExamineLuceneShape, ISpatialPoint
+ {
+ private readonly IPoint _point;
+
+ ///
+ /// Constructor
+ ///
+ /// Point Shape
+ public ExamineLucenePoint(IPoint point) : base(point)
+ {
+ _point = point;
+ }
+
+ ///
+ /// The X coordinate, or Longitude in geospatial contexts.
+ ///
+ ///
+ public double X => _point.X;
+
+ ///
+ /// The Y coordinate, or Latitude in geospatial contexts.
+ ///
+ ///
+ public double Y => _point.Y;
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/ExamineLuceneRectangle.cs b/src/Examine.Lucene.Spatial/Search/ExamineLuceneRectangle.cs
new file mode 100644
index 00000000..80a4c8a6
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/ExamineLuceneRectangle.cs
@@ -0,0 +1,34 @@
+using Examine.Search;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ ///
+ /// Spatial Rectangle Shape
+ ///
+ public class ExamineLuceneRectangle : ExamineLuceneShape, ISpatialRectangle
+ {
+ private readonly IRectangle _rectangle;
+
+ ///
+ /// Constructor
+ ///
+ /// Rectangle Shape
+ public ExamineLuceneRectangle(IRectangle rectangle) : base(rectangle)
+ {
+ _rectangle = rectangle;
+ }
+
+ ///
+ public double MinX => _rectangle.MinX;
+
+ ///
+ public double MinY => _rectangle.MinY;
+
+ ///
+ public double MaxX => _rectangle.MaxX;
+
+ ///
+ public double MaxY => _rectangle.MaxY;
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/ExamineLuceneShape.cs b/src/Examine.Lucene.Spatial/Search/ExamineLuceneShape.cs
new file mode 100644
index 00000000..8a6e4f91
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/ExamineLuceneShape.cs
@@ -0,0 +1,31 @@
+using Examine.Search;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ ///
+ /// Lucene.Net Shape
+ ///
+ public class ExamineLuceneShape : ISpatialShape
+ {
+ ///
+ /// Constructor
+ ///
+ ///
+ public ExamineLuceneShape(IShape shape)
+ {
+ Shape = shape;
+ }
+
+ ///
+ public ISpatialPoint Center => new ExamineLucenePoint(Shape.Center);
+
+ ///
+ public virtual bool IsEmpty => Shape.IsEmpty;
+
+ ///
+ /// Shape
+ ///
+ public IShape Shape { get; }
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/ExamineLuceneShapeCollection.cs b/src/Examine.Lucene.Spatial/Search/ExamineLuceneShapeCollection.cs
new file mode 100644
index 00000000..65133141
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/ExamineLuceneShapeCollection.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using Examine.Search;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ ///
+ /// Collection of Shapes
+ ///
+ public class ExamineLuceneShapeCollection : ExamineLuceneShape, ISpatialShapeCollection
+ {
+ ///
+ /// Constructor
+ ///
+ /// Shapes
+ public ExamineLuceneShapeCollection(ShapeCollection shapes) : base(null)
+ {
+ Shapes = shapes;
+ }
+
+ ///
+ public override bool IsEmpty => Shapes.IsEmpty;
+
+ ///
+ public ShapeCollection Shapes { get; }
+
+ ///
+ IList ISpatialShapeCollection.Shapes => Shapes.Shapes.Select(x=> new ExamineLuceneShape(x)).ToList();
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/Search/Spatial4nShapeFactory.cs b/src/Examine.Lucene.Spatial/Search/Spatial4nShapeFactory.cs
new file mode 100644
index 00000000..c4699d84
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/Search/Spatial4nShapeFactory.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Linq;
+using Examine.Search;
+using Spatial4n.Context;
+using Spatial4n.Distance;
+using Spatial4n.Shapes;
+
+namespace Examine.Lucene.Spatial.Search
+{
+ public class Spatial4nShapeFactory : ISpatialShapeFactory
+ {
+ private SpatialContext _spatialContext;
+ ///
+ /// Contructor
+ ///
+ /// Spatial Context
+ public Spatial4nShapeFactory(SpatialContext spatialContext)
+ {
+ _spatialContext = spatialContext;
+ }
+
+ public SpatialContext SpatialContext { get => _spatialContext; }
+
+ ///
+ public ISpatialCircle CreateCircle(double x, double y, double distance)
+ {
+ var spatial4NCircle = _spatialContext.MakeCircle(x, y, distance);
+ return new ExamineLuceneCircle(spatial4NCircle);
+ }
+
+ ///
+ public ISpatialCircle CreateEarthEquatorialSearchRadiusKMCircle(double x, double y, double radius)
+ {
+ var spatial4NCircle = _spatialContext.MakeCircle(x, y, DistanceUtils.Dist2Degrees(radius, DistanceUtils.EarthEquatorialRadiusKilometers));
+ return new ExamineLuceneCircle(spatial4NCircle);
+ }
+
+ ///
+ public ISpatialCircle CreateEarthMeanSearchRadiusKMCircle(double x, double y, double radius)
+ {
+ var spatial4NCircle = _spatialContext.MakeCircle(x, y, DistanceUtils.Dist2Degrees(radius, DistanceUtils.EarthMeanRadiusKilometers));
+ return new ExamineLuceneCircle(spatial4NCircle);
+ }
+
+ ///
+ public ISpatialEmptyShape CreateEmpty()
+ {
+ return new ExamineLuceneEmptyShape();
+ }
+
+ ///
+ public ISpatialPoint CreateGeoPoint(double latitude, double longitude)
+ {
+ //Swapped on purpose
+ double y = latitude;
+ double x = longitude;
+ var spatial4NPoint = _spatialContext.MakePoint(x, y);
+ return new ExamineLucenePoint(spatial4NPoint);
+ }
+
+ ///
+ public ISpatialPoint CreatePoint(double x, double y)
+ {
+ var spatial4NPoint = _spatialContext.MakePoint(x, y);
+ return new ExamineLucenePoint(spatial4NPoint);
+ }
+
+ ///
+ public ISpatialRectangle CreateRectangle(double minX, double maxX, double minY, double maxY)
+ {
+ var spatial4NRect = _spatialContext.MakeRectangle(minX, maxX, minY, maxY);
+ return new ExamineLuceneRectangle(spatial4NRect);
+ }
+
+ ///
+ public ISpatialShapeCollection CreateShapeCollection(IList shapes)
+ {
+ var shapeList = shapes.Select(x => x as ExamineLuceneShape).Select(x => x.Shape).ToList();
+ var shapeCollection = new ShapeCollection(shapeList, SpatialContext);
+ var examineShapeCollection = new ExamineLuceneShapeCollection(shapeCollection);
+ return examineShapeCollection;
+ }
+
+ ///
+ public ISpatialLineString CreateLineString(IList points)
+ {
+ var shapeList = points.Select(x => x as ExamineLucenePoint).Select(x => x.Shape as IPoint).ToList();
+ var spatial4NRect = _spatialContext.MakeLineString(shapeList);
+ return new ExamineLuceneLineString(spatial4NRect);
+ }
+ }
+}
diff --git a/src/Examine.Lucene.Spatial/SpatialValueTypeFactoryCollection.cs b/src/Examine.Lucene.Spatial/SpatialValueTypeFactoryCollection.cs
new file mode 100644
index 00000000..321b511e
--- /dev/null
+++ b/src/Examine.Lucene.Spatial/SpatialValueTypeFactoryCollection.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Examine.Lucene.Indexing;
+using Examine.Lucene.Spatial.Indexing;
+using Lucene.Net.Analysis;
+using Microsoft.Extensions.Logging;
+
+namespace Examine.Lucene.Spatial
+{
+ public class SpatialValueTypeFactoryCollection
+ {
+ ///
+ /// Returns the default index value types that is used in normal construction of an indexer
+ ///
+ ///
+ public static IReadOnlyDictionary GetDefaultValueTypes(ILoggerFactory loggerFactory, Analyzer defaultAnalyzer)
+ => GetDefaults(loggerFactory, defaultAnalyzer).ToDictionary(x => x.Key, x => (IFieldValueTypeFactory)new DelegateFieldValueTypeFactory(x.Value));
+
+ private static IReadOnlyDictionary> GetDefaults(ILoggerFactory loggerFactory, Analyzer defaultAnalyzer = null)
+ {
+ return new Dictionary>(StringComparer.InvariantCultureIgnoreCase) //case insensitive
+ {
+ {FieldDefinitionTypes.GeoSpatialWKT, name => new WKTSpatialIndexFieldValueType(name, loggerFactory, SpatialIndexFieldValueTypeBase.GeoSpatialPrefixTreeStrategyFactory(),true)},
+ };
+ }
+ }
+}
diff --git a/src/Examine.Lucene/Indexing/FloatType.cs b/src/Examine.Lucene/Indexing/FloatType.cs
new file mode 100644
index 00000000..079f8e33
--- /dev/null
+++ b/src/Examine.Lucene/Indexing/FloatType.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using Examine.Lucene.Providers;
+using Examine.Lucene.Search;
+using Examine.Search;
+using Lucene.Net.Documents;
+using Lucene.Net.Facet;
+using Lucene.Net.Facet.SortedSet;
+using Lucene.Net.Search;
+using Microsoft.Extensions.Logging;
+using static Lucene.Net.Queries.Function.ValueSources.MultiFunction;
+
+namespace Examine.Lucene.Indexing
+{
+ ///
+ /// Represents a float/single
+ ///
+ public class FloatType : IndexFieldRangeValueType, IIndexFacetValueType
+ {
+ private readonly bool _isFacetable;
+#pragma warning disable IDE0032 // Use auto property
+ private readonly bool _taxonomyIndex;
+#pragma warning restore IDE0032 // Use auto property
+
+ ///
+ public FloatType(string fieldName, bool isFacetable, bool taxonomyIndex, ILoggerFactory logger, bool store)
+ : base(fieldName, logger, store)
+ {
+ _isFacetable = isFacetable;
+ _taxonomyIndex = taxonomyIndex;
+ }
+
+ ///
+ [Obsolete("To be removed in Examine V5")]
+#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
+ public FloatType(string fieldName, ILoggerFactory logger, bool store = true)
+#pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
+ : base(fieldName, logger, store)
+ {
+ _isFacetable = false;
+ }
+
+ ///
+ /// Can be sorted by the normal field name
+ ///
+ public override string SortableFieldName => FieldName;
+
+ ///
+ public bool IsTaxonomyFaceted => _taxonomyIndex;
+
+ ///
+ public override void AddValue(Document doc, object? value)
+ {
+ // Support setting taxonomy path
+ if (_isFacetable && _taxonomyIndex && value is object[] objArr && objArr != null && objArr.Length == 2)
+ {
+ if (!TryConvert(objArr[0], out float parsedVal))
+ {
+ return;
+ }
+
+ if (!TryConvert(objArr[1], out string[]? parsedPathVal))
+ {
+ return;
+ }
+
+ doc.Add(new SingleField(FieldName, parsedVal, Store ? Field.Store.YES : Field.Store.NO));
+
+ doc.Add(new FacetField(FieldName, parsedPathVal));
+ doc.Add(new SingleDocValuesField(FieldName, parsedVal));
+ return;
+ }
+ base.AddValue(doc, value);
+ }
+
+ ///
+ protected override void AddSingleValue(Document doc, object value)
+ {
+ if (!TryConvert(value, out float parsedVal))
+ {
+ return;
+ }
+
+ doc.Add(new SingleField(FieldName, parsedVal, Store ? Field.Store.YES : Field.Store.NO));
+
+ if (_isFacetable && _taxonomyIndex)
+ {
+ doc.Add(new FacetField(FieldName, parsedVal.ToString()));
+ doc.Add(new SingleDocValuesField(FieldName, parsedVal));
+ }
+ else if (_isFacetable && !_taxonomyIndex)
+ {
+ doc.Add(new SortedSetDocValuesFacetField(FieldName, parsedVal.ToString()));
+ doc.Add(new SingleDocValuesField(FieldName, parsedVal));
+ }
+ }
+
+ ///
+ public override Query? GetQuery(string query) => !TryConvert(query, out float parsedVal) ? null : GetQuery(parsedVal, parsedVal);
+
+ ///
+ public override Query GetQuery(float? lower, float? upper, bool lowerInclusive = true, bool upperInclusive = true)
+ {
+ return NumericRangeQuery.NewSingleRange(FieldName,
+ lower ?? float.MinValue,
+ upper ?? float.MaxValue, lowerInclusive, upperInclusive);
+ }
+
+ ///
+ public virtual IEnumerable> ExtractFacets(IFacetExtractionContext facetExtractionContext, IFacetField field)
+ => field.ExtractFacets(facetExtractionContext);
+ }
+
+}
diff --git a/src/Examine.Lucene/Indexing/ISpatialIndexFieldValueTypeBase.cs b/src/Examine.Lucene/Indexing/ISpatialIndexFieldValueTypeBase.cs
new file mode 100644
index 00000000..b7d7833e
--- /dev/null
+++ b/src/Examine.Lucene/Indexing/ISpatialIndexFieldValueTypeBase.cs
@@ -0,0 +1,38 @@
+using System;
+using Examine.Search;
+using Lucene.Net.Search;
+
+namespace Examine.Lucene.Indexing
+{
+ ///
+ /// Spatial Index Field Value Type
+ ///
+ public interface ISpatialIndexFieldValueTypeBase : ISpatialIndexFieldValueTypeShapesBase
+ {
+ ///
+ /// Converts an Examine Spatial SortableField to a Lucene SortField
+ ///
+ ///
+ ///
+ ///
+ SortField ToSpatialDistanceSortField(SortableField sortableField, SortDirection sortDirection);
+
+ ///
+ /// Gets a spatial query as a Lucene Query
+ ///
+ ///
+ ///
+ ///
+ ///
+ Query GetQuery(string field, ExamineSpatialOperation spatialOperation, Func shape);
+
+ ///
+ /// Gets a spatial filer as a Lucene Filter
+ ///
+ ///
+ ///
+ ///
+ ///
+ Filter GetFilter(string field, ExamineSpatialOperation spatialOperation, Func shape);
+ }
+}
diff --git a/src/Examine.Lucene/Search/LuceneBooleanOperation.cs b/src/Examine.Lucene/Search/LuceneBooleanOperation.cs
index 18a7a044..dc610472 100644
--- a/src/Examine.Lucene/Search/LuceneBooleanOperation.cs
+++ b/src/Examine.Lucene/Search/LuceneBooleanOperation.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using Examine.Lucene.Providers;
using Examine.Search;
+using Lucene.Net.Facet;
using Lucene.Net.Search;
namespace Examine.Lucene.Search
@@ -82,5 +82,7 @@ public override IQueryExecutor WithFacets(Action facets)
facets.Invoke(luceneFacetOperation);
return luceneFacetOperation;
}
+
+
}
}
diff --git a/src/Examine.Lucene/Search/LuceneBooleanOperationBase.cs b/src/Examine.Lucene/Search/LuceneBooleanOperationBase.cs
index e40ebe04..7cd70c17 100644
--- a/src/Examine.Lucene/Search/LuceneBooleanOperationBase.cs
+++ b/src/Examine.Lucene/Search/LuceneBooleanOperationBase.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Examine.Search;
+using Lucene.Net.Queries;
using Lucene.Net.Search;
namespace Examine.Lucene.Search
@@ -118,6 +119,42 @@ protected internal LuceneBooleanOperationBase Op(
return _search.LuceneQuery(_search.Queries.Pop(), outerOp);
}
+ ///
+ /// Used to add a operation
+ ///
+ /// Function that the base query will be passed into to create the outer Filter
+ ///
+ ///
+ ///
+ ///
+ internal LuceneBooleanOperationBase OpBaseFilter(
+ Func baseFilterBuilder,
+ Func inner,
+ BooleanOperation outerOp,
+ BooleanOperation? defaultInnerOp = null)
+ {
+ _search.Queries.Push(new BooleanQuery());
+
+ //change the default inner op if specified
+ var currentOp = _search.BooleanOperation;
+ if (defaultInnerOp != null)
+ {
+ _search.BooleanOperation = defaultInnerOp.Value;
+ }
+
+ //run the inner search
+ inner(_search);
+
+ //reset to original op if specified
+ if (defaultInnerOp != null)
+ {
+ _search.BooleanOperation = currentOp;
+ }
+ var baseBoolQuery = _search.Queries.Pop();
+ var baseFilter = baseFilterBuilder(baseBoolQuery);
+ return _search.LuceneFilter(baseFilter, outerOp);
+ }
+
///
public abstract ISearchResults Execute(QueryOptions? options = null);
@@ -138,5 +175,6 @@ protected internal LuceneBooleanOperationBase Op(
///
public abstract IQueryExecutor WithFacets(Action facets);
+
}
}
diff --git a/src/Examine.Lucene/Search/LuceneFilter.cs b/src/Examine.Lucene/Search/LuceneFilter.cs
new file mode 100644
index 00000000..8c9f4378
--- /dev/null
+++ b/src/Examine.Lucene/Search/LuceneFilter.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using Examine.Search;
+using Lucene.Net.Search;
+
+namespace Examine.Lucene.Search
+{
+ ///
+ /// Lucene Filter
+ ///
+ public class LuceneFilter: IFilter, INestedFilter
+ {
+ private readonly LuceneSearchFilteringOperation _search;
+
+ private readonly Occur _occurrence;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The filter.
+ /// The occurance.
+ public LuceneFilter(LuceneSearchFilteringOperation search, Occur occurrence)
+ {
+ _search = search;
+ _occurrence = occurrence;
+ }
+
+ ///
+ public IBooleanFilterOperation TermFilter(FilterTerm term) => _search.TermFilterInternal(term,_occurrence);
+
+ ///
+ public IBooleanFilterOperation TermsFilter(IEnumerable terms) => _search.TermsFilterInternal(terms, _occurrence);
+
+ ///
+ public IBooleanFilterOperation TermPrefixFilter(FilterTerm term) => _search.TermPrefixFilterInternal(term, _occurrence);
+
+ ///
+ public IBooleanFilterOperation FieldValueExistsFilter(string field) => _search.FieldValueExistsFilterInternal(field, _occurrence);
+
+ ///
+ public IBooleanFilterOperation FieldValueNotExistsFilter(string field) => _search.FieldValueNotExistsFilterInternal(field, _occurrence);
+
+ ///
+ public IBooleanFilterOperation QueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And) => _search.QueryFilterInternal(inner, defaultOp, _occurrence);
+
+ ///
+ public INestedBooleanFilterOperation NestedTermFilter(FilterTerm term) => _search.NestedTermFilterInternal(term, _occurrence);
+
+ ///
+ public INestedBooleanFilterOperation NestedTermsFilter(IEnumerable terms) => _search.NestedTermsFilterInternal(terms, _occurrence);
+
+ ///
+ public INestedBooleanFilterOperation NestedTermPrefix(FilterTerm term) => _search.NestedTermPrefixFilterInternal(term, _occurrence);
+
+ ///
+ public INestedBooleanFilterOperation NestedFieldValueExists(string field) => _search.NestedFieldValueExistsFilterInternal(field, _occurrence);
+
+ ///
+ public INestedBooleanFilterOperation NestedFieldValueNotExists(string field) => _search.NestedFieldValueNotExistsFilterInternal(field, _occurrence);
+
+ ///
+ public INestedBooleanFilterOperation NestedQueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And) => _search.NestedQueryFilterInternal(inner, defaultOp, _occurrence);
+
+ ///
+ public INestedBooleanFilterOperation NestedSpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ => _search.NestedSpatialOperationFilterInternal(field, spatialOperation, shape, _occurrence);
+
+ ///
+ public IBooleanFilterOperation IntRangeFilter(string field, int? min, int? max, bool minInclusive, bool maxInclusive) => _search.IntRangeFilterInternal(field, min, max, minInclusive, maxInclusive, _occurrence);
+
+ ///
+ public IBooleanFilterOperation LongRangeFilter(string field, long? min, long? max, bool minInclusive, bool maxInclusive) => _search.LongRangeFilterInternal(field, min, max, minInclusive, maxInclusive, _occurrence);
+
+ ///
+ public IBooleanFilterOperation FloatRangeFilter(string field, float? min, float? max, bool minInclusive, bool maxInclusive) => _search.FloatRangeFilterInternal(field, min, max, minInclusive, maxInclusive, _occurrence);
+
+ ///
+ public IBooleanFilterOperation DoubleRangeFilter(string field, double? min, double? max, bool minInclusive, bool maxInclusive) => _search.DoubleRangeFilterInternal(field, min, max, minInclusive, maxInclusive, _occurrence);
+
+ public IBooleanFilterOperation SpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ => _search.SpatialOperationFilterInternal(field, spatialOperation, shape, _occurrence);
+ }
+}
diff --git a/src/Examine.Lucene/Search/LuceneFilteringBooleanOperation.cs b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperation.cs
new file mode 100644
index 00000000..96df264d
--- /dev/null
+++ b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperation.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using Examine.Search;
+using Lucene.Net.Search;
+
+namespace Examine.Lucene.Search
+{
+ ///
+ /// Filter Boolean Operation
+ ///
+ public class LuceneFilteringBooleanOperation : LuceneFilteringBooleanOperationBase
+ {
+ private readonly LuceneSearchFilteringOperation _search;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public LuceneFilteringBooleanOperation(LuceneSearchFilteringOperation luceneSearch) : base(luceneSearch)
+ {
+ _search = luceneSearch;
+ }
+
+ #region IBooleanFilterOperation
+
+ ///
+ public override IFilter AndFilter() => new LuceneFilter(this._search, Occur.MUST);
+
+ ///
+ public override IFilter OrFilter() => new LuceneFilter(this._search, Occur.SHOULD);
+
+ ///
+ public override IFilter NotFilter() => new LuceneFilter(this._search, Occur.MUST_NOT);
+ #endregion
+
+ #region IFilter
+
+ ///
+ public override IBooleanFilterOperation TermFilter(FilterTerm term) => _search.TermFilter(term);
+
+ ///
+ public override IBooleanFilterOperation TermsFilter(IEnumerable terms) => _search.TermsFilter(terms);
+
+ ///
+ public override IBooleanFilterOperation TermPrefixFilter(FilterTerm term) => _search.TermPrefixFilter(term);
+
+ ///
+ public override IBooleanFilterOperation FieldValueExistsFilter(string field) => _search.FieldValueExistsFilter(field);
+
+ ///
+ public override IBooleanFilterOperation FieldValueNotExistsFilter(string field) => _search.FieldValueNotExistsFilter(field);
+
+ ///
+ public override IBooleanFilterOperation QueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And) => _search.QueryFilter(inner, defaultOp);
+
+ #endregion
+
+ #region INestedBooleanFilterOperation
+
+ ///
+ public override INestedFilter And() => new LuceneFilter(_search, Occur.MUST);
+
+ ///
+ public override INestedFilter Or() => new LuceneFilter(_search, Occur.SHOULD);
+
+ ///
+ public override INestedFilter Not() => new LuceneFilter(_search, Occur.MUST_NOT);
+
+ ///
+ public override IBooleanFilterOperation IntRangeFilter(string field, int? min, int? max, bool minInclusive, bool maxInclusive) => _search.IntRangeFilter(field, min, max, minInclusive, maxInclusive);
+
+ ///
+ public override IBooleanFilterOperation LongRangeFilter(string field, long? min, long? max, bool minInclusive, bool maxInclusive) => _search.LongRangeFilter(field, min, max, minInclusive, maxInclusive);
+
+ ///
+ public override IBooleanFilterOperation FloatRangeFilter(string field, float? min, float? max, bool minInclusive, bool maxInclusive) => _search.FloatRangeFilter(field, min, max, minInclusive, maxInclusive);
+
+ ///
+ public override IBooleanFilterOperation DoubleRangeFilter(string field, double? min, double? max, bool minInclusive, bool maxInclusive) => _search.DoubleRangeFilter(field, min, max, minInclusive, maxInclusive);
+
+ #endregion
+
+
+ ///
+ public override IBooleanFilterOperation SpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ => _search.SpatialOperationFilter(field, spatialOperation, shape);
+
+ }
+}
diff --git a/src/Examine.Lucene/Search/LuceneFilteringBooleanOperationBase.cs b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperationBase.cs
new file mode 100644
index 00000000..e891ff05
--- /dev/null
+++ b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperationBase.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using Examine.Search;
+using Lucene.Net.Queries;
+using Lucene.Net.Search;
+
+namespace Examine.Lucene.Search
+{
+ ///
+ /// Boolean Lucene Filtering Operation Base
+ ///
+ public abstract class LuceneFilteringBooleanOperationBase : IFilter, IBooleanFilterOperation, INestedBooleanFilterOperation
+ {
+ private readonly LuceneSearchFilteringOperationBase _search;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public LuceneFilteringBooleanOperationBase(LuceneSearchFilteringOperationBase luceneSearch)
+ {
+ _search = luceneSearch;
+ }
+
+ ///
+ /// Used to add a operation
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal LuceneFilteringBooleanOperationBase Op(
+ Func inner,
+ BooleanOperation outerOp,
+ BooleanOperation? defaultInnerOp = null)
+ {
+ _search.Filters.Push(new BooleanFilter());
+
+ //change the default inner op if specified
+ var currentOp = _search.BooleanFilterOperation;
+ if (defaultInnerOp != null)
+ {
+ _search.BooleanFilterOperation = defaultInnerOp.Value;
+ }
+
+ //run the inner search
+ inner(_search);
+
+ //reset to original op if specified
+ if (defaultInnerOp != null)
+ {
+ _search.BooleanFilterOperation = currentOp;
+ }
+
+ return _search.LuceneFilter(_search.Filters.Pop(), outerOp);
+ }
+ ///
+ /// Used to add a operation
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal Filter GetNestedFilterOp(
+ Func inner,
+ BooleanOperation outerOp,
+ BooleanOperation? defaultInnerOp = null)
+ {
+ _search.Filters.Push(new BooleanFilter());
+
+ //change the default inner op if specified
+ var currentOp = _search.BooleanFilterOperation;
+ if (defaultInnerOp != null)
+ {
+ _search.BooleanFilterOperation = defaultInnerOp.Value;
+ }
+
+ //run the inner search
+ inner(_search);
+
+ //reset to original op if specified
+ if (defaultInnerOp != null)
+ {
+ _search.BooleanFilterOperation = currentOp;
+ }
+
+ return _search.Filters.Pop();
+ }
+
+ ///
+ public abstract IBooleanFilterOperation TermFilter(FilterTerm term);
+
+ ///
+ public abstract IBooleanFilterOperation TermsFilter(IEnumerable terms);
+
+ ///
+ public abstract IBooleanFilterOperation TermPrefixFilter(FilterTerm term);
+
+ ///
+ public abstract IBooleanFilterOperation FieldValueExistsFilter(string field);
+
+ ///
+ public abstract IBooleanFilterOperation FieldValueNotExistsFilter(string field);
+
+ ///
+ public abstract IBooleanFilterOperation QueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ #region IBooleanFilterOperation
+
+ ///
+ public abstract IFilter AndFilter();
+
+ ///
+ public IBooleanFilterOperation AndFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And)
+ => Op(inner, BooleanOperation.And, defaultOp);
+
+ ///
+ public abstract IFilter OrFilter();
+
+ ///
+ public IBooleanFilterOperation OrFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And)
+ => Op(inner, BooleanOperation.Or, defaultOp);
+
+ ///
+ public abstract IFilter NotFilter();
+
+ ///
+ public IBooleanFilterOperation AndNotFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And)
+ => Op(inner, BooleanOperation.Not, defaultOp);
+
+ ///
+ public abstract INestedFilter And();
+
+ ///
+ public INestedBooleanFilterOperation And(Func inner, BooleanOperation defaultOp = BooleanOperation.And)
+ => Op(inner, BooleanOperation.And, defaultOp);
+
+ ///
+ public abstract INestedFilter Or();
+
+ ///
+ public INestedBooleanFilterOperation Or(Func inner, BooleanOperation defaultOp = BooleanOperation.And)
+ => Op(inner, BooleanOperation.Or, defaultOp);
+
+ ///
+ public abstract INestedFilter Not();
+
+ ///
+ public INestedBooleanFilterOperation AndNot(Func inner, BooleanOperation defaultOp = BooleanOperation.And)
+ => Op(inner, BooleanOperation.Not, defaultOp);
+
+ ///
+ public abstract IBooleanFilterOperation IntRangeFilter(string field, int? min, int? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ public abstract IBooleanFilterOperation LongRangeFilter(string field, long? min, long? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ public abstract IBooleanFilterOperation FloatRangeFilter(string field, float? min, float? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ public abstract IBooleanFilterOperation DoubleRangeFilter(string field, double? min, double? max, bool minInclusive, bool maxInclusive);
+
+ #endregion
+
+ ///
+ public abstract IBooleanFilterOperation SpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape);
+ }
+}
diff --git a/src/Examine.Lucene/Search/LuceneQuery.cs b/src/Examine.Lucene/Search/LuceneQuery.cs
index 42757bf7..e75c9713 100644
--- a/src/Examine.Lucene/Search/LuceneQuery.cs
+++ b/src/Examine.Lucene/Search/LuceneQuery.cs
@@ -1,9 +1,7 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Examine.Search;
-using Lucene.Net.Facet.Range;
using Lucene.Net.Search;
namespace Examine.Lucene.Search
@@ -137,5 +135,12 @@ INestedBooleanOperation INestedQuery.ManagedQuery(string query, string[]? fields
///
INestedBooleanOperation INestedQuery.RangeQuery(string[] fields, T? min, T? max, bool minInclusive, bool maxInclusive)
=> _search.RangeQueryInternal(fields, min, max, minInclusive: minInclusive, maxInclusive: maxInclusive, _occurrence);
+
+ ///
+ public IQuery WithFilter(Action filter) => _search.WithFilter(filter);
+
+ ///
+ public IBooleanOperation SpatialOperationQuery(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ => _search.SpatialOperationQueryInternal(field, spatialOperation, shape, _occurrence);
}
}
diff --git a/src/Examine.Lucene/Search/LuceneSearchExecutor.cs b/src/Examine.Lucene/Search/LuceneSearchExecutor.cs
index 053a8e89..642ec2fc 100644
--- a/src/Examine.Lucene/Search/LuceneSearchExecutor.cs
+++ b/src/Examine.Lucene/Search/LuceneSearchExecutor.cs
@@ -27,10 +27,11 @@ public class LuceneSearchExecutor
private readonly ISet? _fieldsToLoad;
private readonly IEnumerable? _facetFields;
private readonly FacetsConfig? _facetsConfig;
+ private readonly Filter? _filter;
private int? _maxDoc;
internal LuceneSearchExecutor(QueryOptions? options, Query query, IEnumerable sortField, ISearchContext searchContext,
- ISet? fieldsToLoad, IEnumerable? facetFields, FacetsConfig? facetsConfig)
+ ISet? fieldsToLoad, IEnumerable? facetFields, FacetsConfig? facetsConfig, Filter? filter)
{
_options = options ?? QueryOptions.Default;
_luceneQueryOptions = _options as LuceneQueryOptions;
@@ -40,6 +41,7 @@ internal LuceneSearchExecutor(QueryOptions? options, Query query, IEnumerable 0)
{
sort = new Sort(sortFields);
- sort.Rewrite(searcher.IndexSearcher);
+ sort = sort.Rewrite(searcher.IndexSearcher);
}
if (_luceneQueryOptions != null && _luceneQueryOptions.SearchAfter != null)
{
@@ -165,7 +167,15 @@ public ISearchResults Execute()
}
else
{
- searcher.IndexSearcher.Search(_luceneQuery, MultiCollector.Wrap(topDocsCollector, facetsCollector));
+ if (facetsCollector != null)
+ {
+ searcher.IndexSearcher.Search(_luceneQuery, filter, MultiCollector.Wrap(topDocsCollector, facetsCollector));
+ }
+ else
+ {
+ searcher.IndexSearcher.Search(_luceneQuery, filter, topDocsCollector);
+ }
+
if (sortFields.Length > 0)
{
topDocs = ((TopFieldCollector)topDocsCollector).GetTopDocs(_options.Skip, _options.Take);
diff --git a/src/Examine.Lucene/Search/LuceneSearchFiltering.cs b/src/Examine.Lucene/Search/LuceneSearchFiltering.cs
new file mode 100644
index 00000000..d591bb36
--- /dev/null
+++ b/src/Examine.Lucene/Search/LuceneSearchFiltering.cs
@@ -0,0 +1,434 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Examine.Lucene.Indexing;
+using Examine.Search;
+using Lucene.Net.Index;
+using Lucene.Net.Queries;
+using Lucene.Net.Search;
+
+namespace Examine.Lucene.Search
+{
+ ///
+ /// Lucene Search Filter Operation
+ ///
+ public class LuceneSearchFilteringOperation : LuceneSearchFilteringOperationBase
+ {
+ private readonly LuceneSearchQuery _luceneSearchQuery;
+
+ ///
+ /// Search Query
+ ///
+ public LuceneSearchQuery LuceneSearchQuery => _luceneSearchQuery;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public LuceneSearchFilteringOperation(LuceneSearchQuery luceneSearchQuery)
+ : base(luceneSearchQuery)
+ {
+ _luceneSearchQuery = luceneSearchQuery;
+ }
+
+ ///
+ /// Creates a new
+ ///
+ ///
+ protected override LuceneFilteringBooleanOperationBase CreateBooleanOp() => new LuceneFilteringBooleanOperation(this);
+
+ #region IFilter
+
+ ///
+ public override IBooleanFilterOperation TermFilter(FilterTerm term) => TermFilterInternal(term);
+
+ ///
+ internal IBooleanFilterOperation TermFilterInternal(FilterTerm term, Occur occurance = Occur.MUST)
+ {
+ if (term.FieldName is null)
+ {
+ throw new ArgumentNullException(nameof(term.FieldName));
+ }
+
+ var filterToAdd = new TermFilter(new Term(term.FieldName, term.FieldValue));
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation TermsFilter(IEnumerable terms) => TermsFilterInternal(terms);
+
+ ///
+ internal IBooleanFilterOperation TermsFilterInternal(IEnumerable terms, Occur occurance = Occur.MUST)
+ {
+ if (terms is null)
+ {
+ throw new ArgumentNullException(nameof(terms));
+ }
+
+ if (!terms.Any() || terms.Any(x => string.IsNullOrWhiteSpace(x.FieldName)))
+ {
+ throw new ArgumentOutOfRangeException(nameof(terms));
+ }
+
+ var luceneTerms = terms.Select(x => new Term(x.FieldName, x.FieldValue)).ToArray();
+ var filterToAdd = new TermsFilter(luceneTerms);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation TermPrefixFilter(FilterTerm term) => TermPrefixFilterInternal(term);
+
+ ///
+ internal IBooleanFilterOperation TermPrefixFilterInternal(FilterTerm term, Occur occurance = Occur.MUST)
+ {
+ if (term.FieldName is null)
+ {
+ throw new ArgumentNullException(nameof(term.FieldName));
+ }
+
+ var filterToAdd = new PrefixFilter(new Term(term.FieldName, term.FieldValue));
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation FieldValueExistsFilter(string field) => FieldValueExistsFilterInternal(field);
+
+ ///
+ internal IBooleanFilterOperation FieldValueExistsFilterInternal(string field, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = new FieldValueFilter(field);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation FieldValueNotExistsFilter(string field) => FieldValueNotExistsFilterInternal(field);
+
+ ///
+ internal IBooleanFilterOperation FieldValueNotExistsFilterInternal(string field, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = new FieldValueFilter(field);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation QueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And) => QueryFilterInternal(inner, defaultOp);
+
+ ///
+ internal IBooleanFilterOperation QueryFilterInternal(Func inner, BooleanOperation defaultOp, Occur occurance = Occur.MUST)
+ {
+ if (inner is null)
+ {
+ throw new ArgumentNullException(nameof(inner));
+ }
+
+ Func buildFilter = (baseQuery) =>
+ {
+ var queryWrapperFilter = new QueryWrapperFilter(baseQuery);
+
+ return queryWrapperFilter;
+ };
+
+ var bo = new LuceneBooleanOperation(_luceneSearchQuery);
+
+ var baseOp = bo.OpBaseFilter(buildFilter, inner, occurance.ToBooleanOperation(), defaultOp);
+
+ var op = CreateBooleanOp();
+ return op;
+ }
+
+ ///
+ public override IBooleanFilterOperation DoubleRangeFilter(string field, double? min, double? max, bool minInclusive, bool maxInclusive)
+ {
+ return DoubleRangeFilterInternal(field, min, max, minInclusive, maxInclusive);
+ }
+
+ internal IBooleanFilterOperation DoubleRangeFilterInternal(string field, double? min, double? max, bool minInclusive, bool maxInclusive, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = NumericRangeFilter.NewDoubleRange(field, min, max, minInclusive, maxInclusive);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation FloatRangeFilter(string field, float? min, float? max, bool minInclusive, bool maxInclusive)
+ {
+ return FloatRangeFilterInternal(field, min, max, minInclusive, maxInclusive);
+ }
+
+ internal IBooleanFilterOperation FloatRangeFilterInternal(string field, float? min, float? max, bool minInclusive, bool maxInclusive, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = NumericRangeFilter.NewSingleRange(field, min, max, minInclusive, maxInclusive);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation IntRangeFilter(string field, int? min, int? max, bool minInclusive, bool maxInclusive)
+ {
+ return IntRangeFilterInternal(field, min, max, minInclusive, maxInclusive);
+ }
+
+ internal IBooleanFilterOperation IntRangeFilterInternal(string field, int? min, int? max, bool minInclusive, bool maxInclusive, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = NumericRangeFilter.NewInt32Range(field, min, max, minInclusive, maxInclusive);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ public override IBooleanFilterOperation LongRangeFilter(string field, long? min, long? max, bool minInclusive, bool maxInclusive)
+ {
+ return LongRangeFilterInternal(field, min, max, minInclusive, maxInclusive);
+ }
+
+ internal IBooleanFilterOperation LongRangeFilterInternal(string field, long? min, long? max, bool minInclusive, bool maxInclusive, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = NumericRangeFilter.NewInt64Range(field, min, max, minInclusive, maxInclusive);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+ #endregion
+
+
+ #region INestedFilter
+
+ ///
+ protected override INestedBooleanFilterOperation NestedTermFilter(FilterTerm term) => NestedTermFilterInternal(term);
+
+ ///
+ protected override INestedBooleanFilterOperation NestedTermsFilter(IEnumerable terms) => NestedTermsFilterInternal(terms);
+
+ ///
+ protected override INestedBooleanFilterOperation NestedTermPrefixFilter(FilterTerm term) => NestedTermPrefixFilterInternal(term);
+
+ ///
+ protected override INestedBooleanFilterOperation NestedFieldValueExistsFilter(string field) => NestedFieldValueExistsFilterInternal(field);
+
+ ///
+ protected override INestedBooleanFilterOperation NestedFieldValueNotExistsFilter(string field) => NestedFieldValueNotExistsFilterInternal(field);
+
+ ///
+ protected override INestedBooleanFilterOperation NestedQueryFilter(Func inner, BooleanOperation defaultOp) => NestedQueryFilterInternal(inner, defaultOp);
+
+ ///
+ internal INestedBooleanFilterOperation NestedTermFilterInternal(FilterTerm term, Occur occurance = Occur.MUST)
+ {
+ if (term.FieldName is null)
+ {
+ throw new ArgumentNullException(nameof(term.FieldName));
+ }
+
+ var filterToAdd = new TermFilter(new Term(term.FieldName, term.FieldValue));
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ internal INestedBooleanFilterOperation NestedTermsFilterInternal(IEnumerable terms, Occur occurance = Occur.MUST)
+ {
+ if (terms is null)
+ {
+ throw new ArgumentNullException(nameof(terms));
+ }
+
+ if (!terms.Any() || terms.Any(x => string.IsNullOrWhiteSpace(x.FieldName)))
+ {
+ throw new ArgumentOutOfRangeException(nameof(terms));
+ }
+
+ var luceneTerms = terms.Select(x => new Term(x.FieldName, x.FieldValue)).ToArray();
+ var filterToAdd = new TermsFilter(luceneTerms);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ internal INestedBooleanFilterOperation NestedTermPrefixFilterInternal(FilterTerm term, Occur occurance = Occur.MUST)
+ {
+ if (term.FieldName is null)
+ {
+ throw new ArgumentNullException(nameof(term.FieldName));
+ }
+
+ var filterToAdd = new PrefixFilter(new Term(term.FieldName, term.FieldValue));
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ internal INestedBooleanFilterOperation NestedFieldValueExistsFilterInternal(string field, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = new FieldValueFilter(field);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ internal INestedBooleanFilterOperation NestedFieldValueNotExistsFilterInternal(string field, Occur occurance = Occur.MUST)
+ {
+ if (field is null)
+ {
+ throw new ArgumentNullException(nameof(field));
+ }
+
+ var filterToAdd = new FieldValueFilter(field);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ return CreateBooleanOp();
+ }
+
+ ///
+ internal INestedBooleanFilterOperation NestedQueryFilterInternal(Func inner, BooleanOperation defaultOp, Occur occurance = Occur.MUST)
+ {
+ if (inner is null)
+ {
+ throw new ArgumentNullException(nameof(inner));
+ }
+
+ Func buildFilter = (baseQuery) =>
+ {
+ var queryWrapperFilter = new QueryWrapperFilter(baseQuery);
+
+ return queryWrapperFilter;
+ };
+
+ var bo = new LuceneBooleanOperation(_luceneSearchQuery);
+
+ var baseOp = bo.OpBaseFilter(buildFilter, inner, occurance.ToBooleanOperation(), defaultOp);
+
+ var op = CreateBooleanOp();
+ return op;
+ }
+
+ #endregion
+
+ ///
+ public override IBooleanFilterOperation SpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ => SpatialOperationFilterInternal(field, spatialOperation, shape, Occurrence);
+
+ internal IBooleanFilterOperation SpatialOperationFilterInternal(string field, ExamineSpatialOperation spatialOperation, Func shape, Occur occurance)
+ {
+ var spatialField = _luceneSearchQuery.SearchContext.GetFieldValueType(field) as ISpatialIndexFieldValueTypeBase;
+ var filterToAdd = spatialField.GetFilter(field, spatialOperation, shape);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ var op = CreateBooleanOp();
+ return op;
+ }
+
+ internal INestedBooleanFilterOperation NestedSpatialOperationFilterInternal(string field, ExamineSpatialOperation spatialOperation, Func shape, Occur occurance)
+ {
+ var spatialField = _luceneSearchQuery.SearchContext.GetFieldValueType(field) as ISpatialIndexFieldValueTypeBase;
+ var filterToAdd = spatialField.GetFilter(field, spatialOperation, shape);
+ if (filterToAdd != null)
+ {
+ Filter.Add(filterToAdd, occurance);
+ }
+
+ var op = CreateBooleanOp();
+ return op;
+ }
+
+ ///
+ protected override INestedBooleanFilterOperation NestedSpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ => NestedSpatialOperationFilterInternal(field, spatialOperation, shape, Occurrence);
+ }
+}
diff --git a/src/Examine.Lucene/Search/LuceneSearchFilteringOperationBase.cs b/src/Examine.Lucene/Search/LuceneSearchFilteringOperationBase.cs
new file mode 100644
index 00000000..7651a441
--- /dev/null
+++ b/src/Examine.Lucene/Search/LuceneSearchFilteringOperationBase.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using Examine.Search;
+using Lucene.Net.Queries;
+using Lucene.Net.Search;
+
+namespace Examine.Lucene.Search
+{
+ ///
+ /// Filtering Operation
+ ///
+ public abstract class LuceneSearchFilteringOperationBase : IFilter, INestedFilter
+ {
+ internal Stack Filters => _luceneSearchQueryBase.Filters;
+
+ ///
+ /// The
+ ///
+ internal BooleanFilter Filter => _luceneSearchQueryBase.Filters.Peek();
+
+ private BooleanOperation _boolFilterOp;
+ private readonly LuceneSearchQueryBase _luceneSearchQueryBase;
+
+ ///
+ /// Specifies how clauses are to occur in matching documents
+ ///
+ protected Occur Occurrence { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public LuceneSearchFilteringOperationBase(LuceneSearchQueryBase luceneSearchQueryBase)
+ {
+ _boolFilterOp = BooleanOperation.And;
+ _luceneSearchQueryBase = luceneSearchQueryBase;
+ }
+
+ ///
+ /// The type of boolean operation
+ ///
+ public BooleanOperation BooleanFilterOperation
+ {
+ get => _boolFilterOp;
+ set
+ {
+ _boolFilterOp = value;
+ Occurrence = _boolFilterOp.ToLuceneOccurrence();
+ }
+ }
+
+ ///
+ /// Adds a true Lucene Filter
+ ///
+ ///
+ ///
+ ///
+ public LuceneFilteringBooleanOperationBase LuceneFilter(Filter filter, BooleanOperation? op = null)
+ {
+ Filter.Add(filter, (op ?? BooleanFilterOperation).ToLuceneOccurrence());
+ return CreateBooleanOp();
+ }
+
+
+ ///
+ /// Creates a
+ ///
+ ///
+ protected abstract LuceneFilteringBooleanOperationBase CreateBooleanOp();
+
+ ///
+ public abstract IBooleanFilterOperation TermFilter(FilterTerm term);
+
+ ///
+ public abstract IBooleanFilterOperation TermsFilter(IEnumerable terms);
+
+ ///
+ public abstract IBooleanFilterOperation TermPrefixFilter(FilterTerm term);
+
+ ///
+ public abstract IBooleanFilterOperation FieldValueExistsFilter(string field);
+
+ ///
+ public abstract IBooleanFilterOperation FieldValueNotExistsFilter(string field);
+
+ ///
+ public abstract IBooleanFilterOperation QueryFilter(Func inner, BooleanOperation defaultOp = BooleanOperation.And);
+
+ ///
+ protected abstract INestedBooleanFilterOperation NestedTermFilter(FilterTerm term);
+
+ ///
+ protected abstract INestedBooleanFilterOperation NestedTermsFilter(IEnumerable terms);
+
+ ///
+ protected abstract INestedBooleanFilterOperation NestedTermPrefixFilter(FilterTerm term);
+
+ ///
+ protected abstract INestedBooleanFilterOperation NestedFieldValueExistsFilter(string field);
+
+ ///
+ protected abstract INestedBooleanFilterOperation NestedFieldValueNotExistsFilter(string field);
+
+ ///
+ protected abstract INestedBooleanFilterOperation NestedQueryFilter(Func inner, BooleanOperation defaultOp);
+
+ ///
+ protected abstract INestedBooleanFilterOperation NestedSpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape);
+
+ ///
+ INestedBooleanFilterOperation INestedFilter.NestedTermFilter(FilterTerm term) => NestedTermFilter(term);
+
+ ///
+ INestedBooleanFilterOperation INestedFilter.NestedTermsFilter(IEnumerable terms) => NestedTermsFilter(terms);
+
+ ///
+ INestedBooleanFilterOperation INestedFilter.NestedTermPrefix(FilterTerm term) => NestedTermPrefixFilter(term);
+
+ ///
+ INestedBooleanFilterOperation INestedFilter.NestedFieldValueExists(string field) => NestedFieldValueExistsFilter(field);
+
+ ///
+ INestedBooleanFilterOperation INestedFilter.NestedFieldValueNotExists(string field) => NestedFieldValueNotExistsFilter(field);
+
+ ///
+ INestedBooleanFilterOperation INestedFilter.NestedQueryFilter(Func inner, BooleanOperation defaultOp) => NestedQueryFilter(inner, defaultOp);
+
+ ///
+ INestedBooleanFilterOperation INestedFilter.NestedSpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape) => NestedSpatialOperationFilter(field, spatialOperation, shape);
+
+ ///
+ public abstract IBooleanFilterOperation IntRangeFilter(string field, int? min, int? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ public abstract IBooleanFilterOperation LongRangeFilter(string field, long? min, long? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ public abstract IBooleanFilterOperation FloatRangeFilter(string field, float? min, float? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ public abstract IBooleanFilterOperation DoubleRangeFilter(string field, double? min, double? max, bool minInclusive, bool maxInclusive);
+
+ ///
+ public abstract IBooleanFilterOperation SpatialOperationFilter(string field, ExamineSpatialOperation spatialOperation, Func shape);
+ }
+}
diff --git a/src/Examine.Lucene/Search/LuceneSearchQuery.cs b/src/Examine.Lucene/Search/LuceneSearchQuery.cs
index b1a787a3..bf91e482 100644
--- a/src/Examine.Lucene/Search/LuceneSearchQuery.cs
+++ b/src/Examine.Lucene/Search/LuceneSearchQuery.cs
@@ -6,7 +6,10 @@
using Examine.Search;
using Lucene.Net.Analysis;
using Lucene.Net.Facet;
+using Lucene.Net.Index;
+using Lucene.Net.Queries;
using Lucene.Net.Search;
+using static Lucene.Net.Util.OfflineSorter;
namespace Examine.Lucene.Search
{
@@ -21,6 +24,8 @@ public class LuceneSearchQuery : LuceneSearchQueryBase, IQueryExecutor
private ISet? _fieldsToLoad = null;
private readonly IList _facetFields = new List();
+ public ISearchContext SearchContext => _searchContext;
+
///
[Obsolete("To be removed in Examine V5")]
public LuceneSearchQuery(
@@ -36,7 +41,7 @@ public LuceneSearchQuery(
ISearchContext searchContext,
string? category, Analyzer analyzer, LuceneSearchOptions searchOptions, BooleanOperation occurance, FacetsConfig facetsConfig)
: base(CreateQueryParser(searchContext, analyzer, searchOptions), category, searchOptions, occurance)
- {
+ {
_searchContext = searchContext;
_facetsConfig = facetsConfig;
}
@@ -104,6 +109,7 @@ private static CustomMultiFieldQueryParser CreateQueryParser(ISearchContext sear
///
///
///
+
public virtual IBooleanOperation OrderByDescending(params SortableField[] fields) => OrderByInternal(true, fields);
///
@@ -137,7 +143,7 @@ internal LuceneBooleanOperationBase ManagedQueryInternal(string query, string[]?
//if no fields are specified then use all fields
fields ??= AllFields;
- var types = fields.Select(f => _searchContext.GetFieldValueType(f)).OfType();
+ var types = fields.Select(f => SearchContext.GetFieldValueType(f)).OfType();
//Strangely we need an inner and outer query. If we don't do this then the lucene syntax returned is incorrect
//since it doesn't wrap in parenthesis properly. I'm unsure if this is a lucene issue (assume so) since that is what
@@ -179,7 +185,7 @@ internal LuceneBooleanOperationBase RangeQueryInternal(string[] fields, T? mi
foreach (var f in fields)
{
- var valueType = _searchContext.GetFieldValueType(f);
+ var valueType = SearchContext.GetFieldValueType(f);
if (valueType is IIndexRangeValueType type)
{
@@ -192,7 +198,7 @@ internal LuceneBooleanOperationBase RangeQueryInternal(string[] fields, T? mi
}
}
#if !NETSTANDARD2_0 && !NETSTANDARD2_1
- else if(typeof(T) == typeof(DateOnly) && valueType is IIndexRangeValueType dateOnlyType)
+ else if (typeof(T) == typeof(DateOnly) && valueType is IIndexRangeValueType dateOnlyType)
{
var minValueTime = minInclusive ? TimeOnly.MinValue : TimeOnly.MaxValue;
var minValue = min.HasValue ? (min.Value as DateOnly?)?.ToDateTime(minValueTime) : null;
@@ -258,12 +264,19 @@ private ISearchResults Search(QueryOptions? options)
}
}
- var executor = new LuceneSearchExecutor(options, query, SortFields, _searchContext, _fieldsToLoad, _facetFields, _facetsConfig);
+ // capture local
+ Filter? filter = Filter;
+ if (filter is BooleanFilter boolFilter && boolFilter.Clauses.Count == 0)
+ {
+ filter = null;
+ }
+
+ var executor = new LuceneSearchExecutor(options, query, SortFields, SearchContext, _fieldsToLoad, _facetFields, _facetsConfig, filter);
var pagesResults = executor.Execute();
return pagesResults;
- }
+ }
///
/// Internal operation for adding the ordered results
@@ -282,7 +295,7 @@ private LuceneBooleanOperationBase OrderByInternal(bool descending, params Sorta
{
var fieldName = f.FieldName;
- var defaultSort = SortFieldType.STRING;
+ var defaultSort = SortFieldType.STRING;
switch (f.SortType)
{
@@ -306,20 +319,34 @@ private LuceneBooleanOperationBase OrderByInternal(bool descending, params Sorta
break;
case SortType.Double:
defaultSort = SortFieldType.DOUBLE;
- break;
+ break;
+ case SortType.SpatialDistance:
+ defaultSort = SortFieldType.CUSTOM;
+ break;
default:
throw new ArgumentOutOfRangeException();
}
//get the sortable field name if this field type has one
- var valType = _searchContext.GetFieldValueType(fieldName);
+ var valType = SearchContext.GetFieldValueType(fieldName);
if (valType?.SortableFieldName != null)
{
fieldName = valType.SortableFieldName;
}
-
- SortFields.Add(new SortField(fieldName, defaultSort, descending));
+ if (f.SortType == SortType.SpatialDistance)
+ {
+ var spatialField = valType as ISpatialIndexFieldValueTypeBase;
+ if (spatialField is null)
+ {
+ throw new NotSupportedException("Spatial Distance Sort requires the field to implement ISpatialIndexFieldValueTypeBase");
+ }
+ SortFields.Add(spatialField.ToSpatialDistanceSortField(f, descending ? SortDirection.Descending : SortDirection.Ascending));
+ }
+ else
+ {
+ SortFields.Add(new SortField(fieldName, defaultSort, descending));
+ }
}
return CreateOp();
@@ -396,7 +423,7 @@ internal IFacetOperations FacetInternal(string field, params Int64Range[] longRa
{
longRanges ??= Array.Empty();
- var valueType = _searchContext.GetFieldValueType(field) as IIndexFacetValueType;
+ var valueType = SearchContext.GetFieldValueType(field) as IIndexFacetValueType;
var facet = new FacetLongField(field, longRanges, GetFacetField(field), isTaxonomyIndexed: valueType?.IsTaxonomyFaceted ?? false);
_facetFields.Add(facet);
@@ -406,7 +433,7 @@ internal IFacetOperations FacetInternal(string field, params Int64Range[] longRa
private string GetFacetField(string field)
{
- if(_facetsConfig is null)
+ if (_facetsConfig is null)
{
throw new InvalidOperationException("FacetsConfig not set. User a LuceneSearchQuery constructor with all parameters");
}
@@ -443,5 +470,31 @@ private bool GetFacetFieldIsHierarchical(string field)
}
return false;
}
+
+ ///
+ public override IQuery WithFilter(Action filter)
+ {
+ var lfilter = new LuceneSearchFilteringOperation(this);
+ filter.Invoke(lfilter);
+ var op = CreateOp();
+ var queryOp = op.And();
+ return queryOp;
+ }
+
+ ///
+ public override IBooleanOperation SpatialOperationQuery(string field, ExamineSpatialOperation spatialOperation, Func shape)
+ => SpatialOperationQueryInternal(field, spatialOperation, shape, Occurrence);
+
+ internal IBooleanOperation SpatialOperationQueryInternal(string field, ExamineSpatialOperation spatialOperation, Func shape, Occur occurance)
+ {
+ var spatialField = SearchContext.GetFieldValueType(field) as ISpatialIndexFieldValueTypeBase;
+ var queryToAdd = spatialField.GetQuery(field, spatialOperation, shape);
+ if (queryToAdd != null)
+ {
+ Query.Add(queryToAdd, occurance);
+ }
+
+ return CreateOp();
+ }
}
}
diff --git a/src/Examine.Lucene/Search/LuceneSearchQueryBase.cs b/src/Examine.Lucene/Search/LuceneSearchQueryBase.cs
index e8960684..fe2b4bce 100644
--- a/src/Examine.Lucene/Search/LuceneSearchQueryBase.cs
+++ b/src/Examine.Lucene/Search/LuceneSearchQueryBase.cs
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Examine.Search;
-using Lucene.Net.Facet.Range;
using Lucene.Net.Index;
+using Lucene.Net.Queries;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
@@ -34,12 +33,21 @@ public abstract class LuceneSearchQueryBase : IQuery, INestedQuery
///
public IList SortFields { get; } = new List();
+ internal Stack Filters { get; } = new Stack();
+
+ ///
+ /// The
+ ///
+ public BooleanFilter Filter => Filters.Peek();
+
///
/// Specifies how clauses are to occur in matching documents
///
protected Occur Occurrence { get; set; }
private BooleanOperation _boolOp;
+ private BooleanOperation _boolFilterOp;
+
///
protected LuceneSearchQueryBase(CustomMultiFieldQueryParser queryParser,
string? category, LuceneSearchOptions searchOptions, BooleanOperation occurance)
@@ -47,6 +55,7 @@ protected LuceneSearchQueryBase(CustomMultiFieldQueryParser queryParser,
Category = category;
SearchOptions = searchOptions;
Queries.Push(new BooleanQuery());
+ Filters.Push(new BooleanFilter());
BooleanOperation = occurance;
_queryParser = queryParser;
}
@@ -70,6 +79,19 @@ public BooleanOperation BooleanOperation
}
}
+ ///
+ /// The type of boolean operation
+ ///
+ public BooleanOperation BooleanFilterOperation
+ {
+ get => _boolFilterOp;
+ set
+ {
+ _boolFilterOp = value;
+ Occurrence = _boolFilterOp.ToLuceneOccurrence();
+ }
+ }
+
///
/// The category of the query
///
@@ -119,6 +141,19 @@ public LuceneBooleanOperationBase LuceneQuery(Query query, BooleanOperation? op
return CreateOp();
}
+ ///
+ /// Adds a true Lucene Filter
+ ///
+ ///
+ ///
+ ///
+ public LuceneBooleanOperationBase LuceneFilter(Filter filter, BooleanOperation? op = null)
+ {
+ Filter.Add(filter, (op ?? BooleanOperation).ToLuceneOccurrence());
+ return CreateOp();
+ }
+
+
///
public IBooleanOperation Id(string id) => IdInternal(id, Occurrence);
@@ -632,5 +667,15 @@ private BooleanQuery GetMultiFieldQuery(
/// A that represents this instance.
///
public override string ToString() => $"{{ Category: {Category}, LuceneQuery: {Query} }}";
+
+ ///
+ public abstract IBooleanOperation SpatialOperationQuery(string field, ExamineSpatialOperation spatialOperation, Func shape);
+
+ ///
+ /// Apply a filter
+ ///
+ ///
+ ///
+ public abstract IQuery WithFilter(Action filter);
}
}
diff --git a/src/Examine.Lucene/ValueTypeFactoryCollection.cs b/src/Examine.Lucene/ValueTypeFactoryCollection.cs
index dc8f955b..9952f890 100644
--- a/src/Examine.Lucene/ValueTypeFactoryCollection.cs
+++ b/src/Examine.Lucene/ValueTypeFactoryCollection.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Runtime.InteropServices;
using Examine.Lucene.Analyzers;
using Examine.Lucene.Indexing;
using Lucene.Net.Analysis;
@@ -75,7 +76,8 @@ private static IReadOnlyDictionary> G
{
{"number", name => new Int32Type(name, loggerFactory)},
{FieldDefinitionTypes.Integer, name => new Int32Type(name, loggerFactory)},
- {FieldDefinitionTypes.Float, name => new SingleType(name, loggerFactory)},
+ {FieldDefinitionTypes.Float, name => new FloatType(name, loggerFactory)},
+ {FieldDefinitionTypes.Single, name => new SingleType(name, loggerFactory)},
{FieldDefinitionTypes.Double, name => new DoubleType(name, loggerFactory)},
{FieldDefinitionTypes.Long, name => new Int64Type(name, loggerFactory)},
{"date", name => new DateTimeType(name, loggerFactory, DateResolution.MILLISECOND)},
@@ -91,7 +93,7 @@ private static IReadOnlyDictionary> G
{FieldDefinitionTypes.InvariantCultureIgnoreCase, name => new GenericAnalyzerFieldValueType(name, loggerFactory, new CultureInvariantWhitespaceAnalyzer())},
{FieldDefinitionTypes.EmailAddress, name => new GenericAnalyzerFieldValueType(name, loggerFactory, new EmailAddressAnalyzer())},
{FieldDefinitionTypes.FacetInteger, name => new Int32Type(name, true,false,loggerFactory, true)},
- {FieldDefinitionTypes.FacetFloat, name => new SingleType(name, true, false, loggerFactory, true)},
+ {FieldDefinitionTypes.FacetFloat, name => new FloatType(name, true, false, loggerFactory, true)},
{FieldDefinitionTypes.FacetDouble, name => new DoubleType(name,true, false, loggerFactory, true)},
{FieldDefinitionTypes.FacetLong, name => new Int64Type(name, true, false, loggerFactory, true)},
{FieldDefinitionTypes.FacetDateTime, name => new DateTimeType(name, true, true, false, loggerFactory, DateResolution.MILLISECOND)},
@@ -103,7 +105,7 @@ private static IReadOnlyDictionary> G
{FieldDefinitionTypes.FacetFullText, name => new FullTextType(name, loggerFactory, true, false, false, defaultAnalyzer ?? new CultureInvariantStandardAnalyzer())},
{FieldDefinitionTypes.FacetFullTextSortable, name => new FullTextType(name, loggerFactory, true, false,true, defaultAnalyzer ?? new CultureInvariantStandardAnalyzer())},
{FieldDefinitionTypes.FacetTaxonomyInteger, name => new Int32Type(name,true,true, loggerFactory, true)},
- {FieldDefinitionTypes.FacetTaxonomyFloat, name => new SingleType(name,isFacetable: true, taxonomyIndex: true, loggerFactory, true)},
+ {FieldDefinitionTypes.FacetTaxonomyFloat, name => new FloatType(name,isFacetable: true, taxonomyIndex: true, loggerFactory, true)},
{FieldDefinitionTypes.FacetTaxonomyDouble, name => new DoubleType(name, true, true, loggerFactory, true)},
{FieldDefinitionTypes.FacetTaxonomyLong, name => new Int64Type(name, isFacetable: true, taxonomyIndex: true, loggerFactory, true)},
{FieldDefinitionTypes.FacetTaxonomyDateTime, name => new DateTimeType(name,true, true, taxonomyIndex : true, loggerFactory, DateResolution.MILLISECOND)},
diff --git a/src/Examine.Test/Examine.Lucene/Search/FluentApiTests.cs b/src/Examine.Test/Examine.Lucene/Search/FluentApiTests.cs
index a7e8746d..a29345af 100644
--- a/src/Examine.Test/Examine.Lucene/Search/FluentApiTests.cs
+++ b/src/Examine.Test/Examine.Lucene/Search/FluentApiTests.cs
@@ -5,20 +5,23 @@
using Examine.Lucene.Providers;
using Examine.Lucene.Search;
using Examine.Search;
+using Lucene.Net.Analysis;
using Lucene.Net.Analysis.En;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Facet;
+using Lucene.Net.Queries;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
+using Lucene.Net.Store;
using NUnit.Framework;
-
+using LuceneTerm = Lucene.Net.Index.Term;
namespace Examine.Test.Examine.Lucene.Search
{
[TestFixture]
[Parallelizable(ParallelScope.All)]
- public class FluentApiTests : ExamineBaseTest
+ public partial class FluentApiTests : ExamineBaseTest
{
public enum FacetTestType
{
@@ -4900,11 +4903,12 @@ public void SearchAfter_NonSorted_Results_Returns_Different_Results()
}
#if NET6_0_OR_GREATER
- [TestCase(FacetTestType.TaxonomyFacets)] [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
[TestCase(FacetTestType.NoFacets)]
public void Range_DateOnly(FacetTestType withFacets)
{
- FieldDefinitionCollection fieldDefinitionCollection = null;
+ FieldDefinitionCollection fieldDefinitionCollection = null;
switch (withFacets)
{
case FacetTestType.TaxonomyFacets:
@@ -5190,5 +5194,561 @@ public void GivenTaxonomyIndexSearchAfterTake_Returns_ExpectedTotals_Facet(int f
}
}
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void TermFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.TermFilter(new FilterTerm("nodeTypeAlias", "CWS_Home"));
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void TermPrefixFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.TermPrefixFilter(new FilterTerm("nodeTypeAlias", "CWS_H"));
+ });
+ var boolOp = criteria.All();//.Field("nodeTypeAlias", "CWS_Home".Escape());
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void TermsFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.TermsFilter(new[] {
+ new FilterTerm("nodeTypeAlias", "CWS_Home"),
+ new FilterTerm("nodeName", "my name 2")
+ });
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void TermAndTermPrefixFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.TermFilter(new FilterTerm("nodeTypeAlias", "CWS_Home"))
+ .AndFilter()
+ .TermPrefixFilter(new FilterTerm("nodeTypeAlias", "CWS_H"));
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void TermAndNotTermPrefixFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.TermPrefixFilter(new FilterTerm("nodeTypeAlias", "CWS_"))
+ .AndNotFilter(
+ inner => inner.NestedTermFilter(new FilterTerm("nodeTypeAlias", "CWS_Home")));
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+
+ Assert.IsTrue(results.All(x => x.Values["nodeTypeAlias"].StartsWith("CWS_")));
+ Assert.IsFalse(results.Any(x => x.Values["nodeTypeAlias"] == "CWS_Home"));
+
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.IsTrue(results.All(x => x.Values["nodeTypeAlias"].StartsWith("CWS_")));
+ Assert.IsFalse(results.Any(x => x.Values["nodeTypeAlias"] == "CWS_Home"));
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void QueryFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.QueryFilter(
+ query =>
+ query.Field("nodeTypeAlias", "CWS_Home"));
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void NestedQueryFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.TermFilter(new FilterTerm("nodeTypeAlias", "CWS_Home"))
+ .AndFilter(
+ innerFilter => innerFilter.NestedQueryFilter(
+ query => query.Field("nodeTypeAlias", "CWS_Home"))
+ );
+
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void IntRangeFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.IntRangeFilter("intNumber", 2, 3, true, true);
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void LongRangeFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.LongRangeFilter("longNumber", 2, 3, true, true);
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void FloatRangeFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.FloatRangeFilter("floatNumber", 2.0f, 3.0f, true, true);
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void DoubleRangeFilter(FacetTestType withFacets)
+ {
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = searcher.CreateQuery("content")
+ .WithFilter(
+ filter =>
+ {
+ filter.DoubleRangeFilter("doubleNumber", 2.0, 3.0, true, true);
+ });
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ [TestCase(FacetTestType.TaxonomyFacets)]
+ [TestCase(FacetTestType.SortedSetFacets)]
+ [TestCase(FacetTestType.NoFacets)]
+ public void Custom_Lucene_Filter(FacetTestType withFacets)
+ {
+
+ Action actAssertAction
+ = (fieldDefinitionCollection, indexAnalyzer, indexDirectory, taxonomyDirectory, testIndex, searcher)
+ =>
+ {
+ var criteria = (LuceneSearchQuery)searcher.CreateQuery("content");
+
+ criteria.LuceneFilter(new TermFilter(new LuceneTerm("nodeTypeAlias", "CWS_Home")));
+ var boolOp = criteria.All();
+
+ if (HasFacets(withFacets))
+ {
+ var results = boolOp.WithFacets(facets => facets.FacetString("nodeName")).Execute();
+
+ var facetResults = results.GetFacet("nodeName");
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ Assert.AreEqual(2, facetResults.Count());
+ }
+ else
+ {
+ var results = boolOp.Execute();
+
+ Assert.AreEqual(2, results.TotalItemCount);
+ }
+ };
+ RunFilterTest(withFacets, actAssertAction);
+ }
+
+ private void RunFilterTest(FacetTestType withFacets, Action actAssertAction)
+ {
+ FieldDefinitionCollection fieldDefinitionCollection = null;
+ switch (withFacets)
+ {
+ case FacetTestType.TaxonomyFacets:
+
+ fieldDefinitionCollection = new FieldDefinitionCollection(new FieldDefinition("nodeTypeAlias", "raw"),
+ new FieldDefinition("longNumber", FieldDefinitionTypes.Long),
+ new FieldDefinition("intNumber", FieldDefinitionTypes.Integer),
+ new FieldDefinition("floatNumber", FieldDefinitionTypes.Float),
+ new FieldDefinition("doubleNumber", FieldDefinitionTypes.Double),
+ new FieldDefinition("nodeName", FieldDefinitionTypes.FacetTaxonomyFullText));
+ break;
+ case FacetTestType.SortedSetFacets:
+ fieldDefinitionCollection = new FieldDefinitionCollection(new FieldDefinition("nodeTypeAlias", "raw"),
+ new FieldDefinition("longNumber", FieldDefinitionTypes.Long),
+ new FieldDefinition("intNumber", FieldDefinitionTypes.Integer),
+ new FieldDefinition("floatNumber", FieldDefinitionTypes.Float),
+ new FieldDefinition("doubleNumber", FieldDefinitionTypes.Double),
+ new FieldDefinition("nodeName", FieldDefinitionTypes.FacetFullText));
+ break;
+ default:
+ fieldDefinitionCollection = new FieldDefinitionCollection(new FieldDefinition("nodeTypeAlias", "raw"),
+ new FieldDefinition("intNumber", FieldDefinitionTypes.Integer),
+ new FieldDefinition("floatNumber", FieldDefinitionTypes.Float),
+ new FieldDefinition("doubleNumber", FieldDefinitionTypes.Double),
+ new FieldDefinition("longNumber", FieldDefinitionTypes.Long));
+ break;
+ }
+
+ var analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
+ using (var luceneDir = new RandomIdRAMDirectory())
+ using (var luceneTaxonomyDir = new RandomIdRAMDirectory())
+ using (var indexer = GetTaxonomyTestIndex(
+ luceneDir,
+ luceneTaxonomyDir,
+ analyzer,
+ fieldDefinitionCollection))
+ {
+ indexer.IndexItems(new[] {
+ new ValueSet(1.ToString(), "content",
+ new Dictionary
+ {
+ {"nodeName", "my name 1"},
+ {"nodeTypeAlias", "CWS_Home"},
+ { "longNumber", 1 },
+ { "intNumber", 1 },
+ { "floatNumber", 1.0f },
+ { "doubleNumber", 1.0 }
+ }),
+ new ValueSet(2.ToString(), "content",
+ new Dictionary
+ {
+ {"nodeName", "my name 2"},
+ {"nodeTypeAlias", "CWS_Home"},
+ { "longNumber",2 },
+ { "intNumber", 2 },
+ { "floatNumber", 2.0f },
+ { "doubleNumber", 2.0 }
+ }),
+ new ValueSet(3.ToString(), "content",
+ new Dictionary
+ {
+ {"nodeName", "my name 3"},
+ {"nodeTypeAlias", "CWS_Page"},
+ { "longNumber", 3 },
+ { "intNumber", 3 },
+ { "floatNumber", 3.0f },
+ { "doubleNumber", 3.0 }
+ }),
+ new ValueSet(4.ToString(), "content",
+ new Dictionary
+ {
+ {"nodeName", "my name 4"},
+ {"nodeTypeAlias", "CWS_Page"},
+ { "longNumber", 4 },
+ { "intNumber", 4 },
+ { "floatNumber", 4.0f },
+ { "doubleNumber", 4.0 }
+ }),
+ });
+
+ var searcher = indexer.Searcher;
+ actAssertAction.Invoke(fieldDefinitionCollection, analyzer, luceneDir, luceneTaxonomyDir, indexer, searcher);
+ }
+ }
}
}
diff --git a/src/Examine.Test/Examine.Lucene/Search/SpatialFluentApiTests.cs b/src/Examine.Test/Examine.Lucene/Search/SpatialFluentApiTests.cs
new file mode 100644
index 00000000..80849781
--- /dev/null
+++ b/src/Examine.Test/Examine.Lucene/Search/SpatialFluentApiTests.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Examine.Lucene;
+using Examine.Lucene.Indexing;
+using Examine.Lucene.Spatial;
+using Examine.Search;
+using Lucene.Net.Analysis.Standard;
+using NUnit.Framework;
+
+namespace Examine.Test.Examine.Lucene.Search
+{
+ [TestFixture]
+ public partial class FluentApiTests : ExamineBaseTest
+ {
+ [Test]
+ public void Sort_Result_By_Geo_Spatial_Field_Distance()
+ {
+ var analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
+ var examineDefault = ValueTypeFactoryCollection.GetDefaultValueTypes(Logging, analyzer);
+ var examineSpatialDefault = SpatialValueTypeFactoryCollection.GetDefaultValueTypes(Logging, analyzer);
+ Dictionary valueTypeFactoryDictionary = new Dictionary(examineDefault);
+ foreach (var item in examineSpatialDefault)
+ {
+ valueTypeFactoryDictionary.Add(item.Key, item.Value);
+ }
+
+ using (var luceneDir = new RandomIdRAMDirectory())
+ using (var indexer = GetTestIndex(
+ luceneDir,
+ analyzer,
+ //Ensure it's set to a date, otherwise it's not sortable
+ new FieldDefinitionCollection(
+ new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime),
+ new FieldDefinition("parentID", FieldDefinitionTypes.Integer),
+ new FieldDefinition("spatialWKT", FieldDefinitionTypes.GeoSpatialWKT)
+ ), indexValueTypesFactory: valueTypeFactoryDictionary))
+ {
+ var now = DateTime.Now;
+ var geoSpatialFieldType = indexer.FieldValueTypeCollection.ValueTypes.First(f
+ => f.FieldName.Equals("spatialWKT", StringComparison.InvariantCultureIgnoreCase)) as ISpatialIndexFieldValueTypeBase;
+
+ var fieldShapeFactory = geoSpatialFieldType.SpatialShapeFactory;
+
+ indexer.IndexItems(new[] {
+ ValueSet.FromObject(1.ToString(), "content",
+ new { nodeName = "my name 1", updateDate = now.AddDays(2).ToString("yyyy-MM-dd"), parentID = "1143" , spatialWKT = fieldShapeFactory.CreatePoint(0.0,0.0) }),
+ ValueSet.FromObject(2.ToString(), "content",
+ new { nodeName = "my name 2", updateDate = now.ToString("yyyy-MM-dd"), parentID = 1143, spatialWKT = fieldShapeFactory.CreatePoint(1.0,1.0) }),
+ ValueSet.FromObject(3.ToString(), "content",
+ new { nodeName = "my name 3", updateDate = now.AddDays(1).ToString("yyyy-MM-dd"), parentID = 1143, spatialWKT = fieldShapeFactory.CreatePoint(2.0,2.0) }),
+ ValueSet.FromObject(4.ToString(), "content",
+ new { nodeName = "my name 4", updateDate = now, parentID = "2222", spatialWKT = fieldShapeFactory.CreatePoint(3.0,3.0) }),
+ });
+
+ var searcher = indexer.Searcher;
+ var searchLocation = fieldShapeFactory.CreatePoint(0.0, 0.0);
+ var sc = searcher.CreateQuery("content");
+ var sc1 = sc.Field("parentID", 1143)
+ .OrderBy(new SortableField("spatialWKT", searchLocation));
+
+ var results1 = sc1.Execute().ToArray();
+
+ Assert.AreEqual(3, results1.Length);
+
+ }
+ }
+
+ [Test]
+ public void Filter_Result_By_Geo_Spatial_Field_Distance()
+ {
+ var analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
+ var examineDefault = ValueTypeFactoryCollection.GetDefaultValueTypes(Logging, analyzer);
+ var examineSpatialDefault = SpatialValueTypeFactoryCollection.GetDefaultValueTypes(Logging, analyzer);
+ Dictionary valueTypeFactoryDictionary = new Dictionary(examineDefault);
+ foreach (var item in examineSpatialDefault)
+ {
+ valueTypeFactoryDictionary.Add(item.Key, item.Value);
+ }
+
+ using (var luceneDir = new RandomIdRAMDirectory())
+ using (var indexer = GetTestIndex(
+ luceneDir,
+ analyzer,
+ //Ensure it's set to a date, otherwise it's not sortable
+ new FieldDefinitionCollection(
+ new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime),
+ new FieldDefinition("parentID", FieldDefinitionTypes.Integer),
+ new FieldDefinition("spatialWKT", FieldDefinitionTypes.GeoSpatialWKT)
+ ), indexValueTypesFactory: valueTypeFactoryDictionary))
+ {
+ var now = DateTime.Now;
+ var geoSpatialFieldType = indexer.FieldValueTypeCollection.ValueTypes.First(f
+ => f.FieldName.Equals("spatialWKT", StringComparison.InvariantCultureIgnoreCase)) as ISpatialIndexFieldValueTypeBase;
+
+ var fieldShapeFactory = geoSpatialFieldType.SpatialShapeFactory;
+
+ indexer.IndexItems(new[] {
+ ValueSet.FromObject(1.ToString(), "content",
+ new { nodeName = "my name 1", updateDate = now.AddDays(2).ToString("yyyy-MM-dd"), parentID = "1143" , spatialWKT = fieldShapeFactory.CreatePoint(0.0,0.0) }),
+ ValueSet.FromObject(2.ToString(), "content",
+ new { nodeName = "my name 2", updateDate = now.ToString("yyyy-MM-dd"), parentID = 1143, spatialWKT = fieldShapeFactory.CreatePoint(1.0,1.0) }),
+ ValueSet.FromObject(3.ToString(), "content",
+ new { nodeName = "my name 3", updateDate = now.AddDays(1).ToString("yyyy-MM-dd"), parentID = 1143, spatialWKT = fieldShapeFactory.CreatePoint(2.0,2.0) }),
+ ValueSet.FromObject(4.ToString(), "content",
+ new { nodeName = "my name 4", updateDate = now, parentID = "2222", spatialWKT = fieldShapeFactory.CreatePoint(3.0,3.0) }),
+ });
+
+ var searcher = indexer.Searcher;
+ var searchLocation = fieldShapeFactory.CreatePoint(0.0, 0.0);
+ var sc = searcher.CreateQuery("content");
+ var sc1 = sc.WithFilter(
+ filter => filter.SpatialOperationFilter("spatialWKT", ExamineSpatialOperation.Intersects, (shapeFactory) => shapeFactory.CreateRectangle(0.0, 1.0, 0.0, 1.0)))
+ .Field("parentID", 1143)
+ .OrderBy(new SortableField("spatialWKT", searchLocation));
+
+ var results1 = sc1.Execute().ToArray();
+
+ Assert.AreEqual(2, results1.Length);
+
+ }
+ }
+
+ [Test]
+ public void Query_Result_By_Geo_Spatial_Field_Distance()
+ {
+ var analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
+ var examineDefault = ValueTypeFactoryCollection.GetDefaultValueTypes(Logging, analyzer);
+ var examineSpatialDefault = SpatialValueTypeFactoryCollection.GetDefaultValueTypes(Logging, analyzer);
+ Dictionary valueTypeFactoryDictionary = new Dictionary(examineDefault);
+ foreach (var item in examineSpatialDefault)
+ {
+ valueTypeFactoryDictionary.Add(item.Key, item.Value);
+ }
+
+ using (var luceneDir = new RandomIdRAMDirectory())
+ using (var indexer = GetTestIndex(
+ luceneDir,
+ analyzer,
+ //Ensure it's set to a date, otherwise it's not sortable
+ new FieldDefinitionCollection(
+ new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime),
+ new FieldDefinition("parentID", FieldDefinitionTypes.Integer),
+ new FieldDefinition("spatialWKT", FieldDefinitionTypes.GeoSpatialWKT)
+ ), indexValueTypesFactory: valueTypeFactoryDictionary))
+ {
+ var now = DateTime.Now;
+ var geoSpatialFieldType = indexer.FieldValueTypeCollection.ValueTypes.First(f
+ => f.FieldName.Equals("spatialWKT", StringComparison.InvariantCultureIgnoreCase)) as ISpatialIndexFieldValueTypeShapesBase;
+
+ var fieldShapeFactory = geoSpatialFieldType.SpatialShapeFactory;
+
+ indexer.IndexItems(new[] {
+ ValueSet.FromObject(1.ToString(), "content",
+ new { nodeName = "my name 1", updateDate = now.AddDays(2).ToString("yyyy-MM-dd"), parentID = "1143" , spatialWKT = fieldShapeFactory.CreatePoint(0.0,0.0) }),
+ ValueSet.FromObject(2.ToString(), "content",
+ new { nodeName = "my name 2", updateDate = now.ToString("yyyy-MM-dd"), parentID = 1143, spatialWKT = fieldShapeFactory.CreatePoint(1.0,1.0) }),
+ ValueSet.FromObject(3.ToString(), "content",
+ new { nodeName = "my name 3", updateDate = now.AddDays(1).ToString("yyyy-MM-dd"), parentID = 1143, spatialWKT = fieldShapeFactory.CreatePoint(2.0,2.0) }),
+ ValueSet.FromObject(4.ToString(), "content",
+ new { nodeName = "my name 4", updateDate = now, parentID = "2222", spatialWKT = fieldShapeFactory.CreatePoint(3.0,3.0) }),
+ });
+
+ var searcher = indexer.Searcher;
+ var searchLocation = fieldShapeFactory.CreatePoint(0.0, 0.0);
+ var sc = searcher.CreateQuery("content");
+ var sc1 = sc.Field("parentID", 1143)
+ .And()
+ .SpatialOperationQuery("spatialWKT", ExamineSpatialOperation.Intersects, (shapeFactory) => shapeFactory.CreateRectangle(0.0, 1.0, 0.0, 1.0))
+ .OrderBy(new SortableField("spatialWKT", searchLocation));
+
+ var results1 = sc1.Execute().ToArray();
+
+ Assert.AreEqual(2, results1.Length);
+
+ }
+ }
+ }
+}
diff --git a/src/Examine.Test/Examine.Test.csproj b/src/Examine.Test/Examine.Test.csproj
index 9a5e64bc..10824c98 100644
--- a/src/Examine.Test/Examine.Test.csproj
+++ b/src/Examine.Test/Examine.Test.csproj
@@ -45,6 +45,7 @@
+
diff --git a/src/Examine.Test/ExamineBaseTest.cs b/src/Examine.Test/ExamineBaseTest.cs
index a75b6741..5217fc7e 100644
--- a/src/Examine.Test/ExamineBaseTest.cs
+++ b/src/Examine.Test/ExamineBaseTest.cs
@@ -14,11 +14,15 @@ namespace Examine.Test
{
public abstract class ExamineBaseTest
{
+ private ILoggerFactory _loggerFactory;
+
+ protected ILoggerFactory Logging { get => _loggerFactory; }
+
[SetUp]
public virtual void Setup()
{
- var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
- loggerFactory.CreateLogger(typeof(ExamineBaseTest)).LogDebug("Initializing test");
+ _loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
+ _loggerFactory.CreateLogger(typeof(ExamineBaseTest)).LogDebug("Initializing test");
}
public TestIndex GetTestIndex(Directory d, Analyzer analyzer, FieldDefinitionCollection fieldDefinitions = null, IndexDeletionPolicy indexDeletionPolicy = null, IReadOnlyDictionary indexValueTypesFactory = null, FacetsConfig facetsConfig = null)
diff --git a/src/Examine.sln b/src/Examine.sln
index 4841ded4..d856220a 100644
--- a/src/Examine.sln
+++ b/src/Examine.sln
@@ -27,7 +27,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examine.Core", "Examine.Cor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examine", "Examine.Host\Examine.csproj", "{6988B93D-8FA9-4F4F-AC66-E748777FA226}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examine.Web.Demo", "Examine.Web.Demo\Examine.Web.Demo.csproj", "{99D0B284-AFDA-4A32-A88B-9B182DF8CE2F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examine.Web.Demo", "Examine.Web.Demo\Examine.Web.Demo.csproj", "{99D0B284-AFDA-4A32-A88B-9B182DF8CE2F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examine.Lucene.Spatial", "Examine.Lucene.Spatial\Examine.Lucene.Spatial.csproj", "{C83E7BAB-57D8-4622-A0A6-EF320AB1BCF3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -55,6 +57,10 @@ Global
{99D0B284-AFDA-4A32-A88B-9B182DF8CE2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99D0B284-AFDA-4A32-A88B-9B182DF8CE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99D0B284-AFDA-4A32-A88B-9B182DF8CE2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C83E7BAB-57D8-4622-A0A6-EF320AB1BCF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C83E7BAB-57D8-4622-A0A6-EF320AB1BCF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C83E7BAB-57D8-4622-A0A6-EF320AB1BCF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C83E7BAB-57D8-4622-A0A6-EF320AB1BCF3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE