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