diff --git a/docs/articles/filtering.md b/docs/articles/filtering.md new file mode 100644 index 00000000..51ffc566 --- /dev/null +++ b/docs/articles/filtering.md @@ -0,0 +1,215 @@ +--- +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(); +``` + +### 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/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/src/Examine.Core/FieldDefinitionTypes.cs b/src/Examine.Core/FieldDefinitionTypes.cs index 5fe13cb3..a6f00b27 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 /// 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/IFilter.cs b/src/Examine.Core/Search/IFilter.cs new file mode 100644 index 00000000..44f5f0fa --- /dev/null +++ b/src/Examine.Core/Search/IFilter.cs @@ -0,0 +1,102 @@ +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); + } +} 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..c157faf2 --- /dev/null +++ b/src/Examine.Core/Search/INestedFilter.cs @@ -0,0 +1,51 @@ +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); + } +} 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..e9093176 100644 --- a/src/Examine.Core/Search/IQuery.cs +++ b/src/Examine.Core/Search/IQuery.cs @@ -134,5 +134,12 @@ public interface IQuery /// /// IBooleanOperation RangeQuery(string[] fields, T? min, T? max, bool minInclusive = true, bool maxInclusive = true) where T : struct; + + /// + /// Apply Filters + /// + /// + /// + IQuery WithFilter(Action filter); } } 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/Search/LuceneBooleanOperation.cs b/src/Examine.Lucene/Search/LuceneBooleanOperation.cs index 18a7a044..1ec3ce83 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 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..9a7d336f --- /dev/null +++ b/src/Examine.Lucene/Search/LuceneFilter.cs @@ -0,0 +1,76 @@ +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 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); + } +} diff --git a/src/Examine.Lucene/Search/LuceneFilteringBooleanOperation.cs b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperation.cs new file mode 100644 index 00000000..b1fc94a6 --- /dev/null +++ b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperation.cs @@ -0,0 +1,83 @@ +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 + } +} diff --git a/src/Examine.Lucene/Search/LuceneFilteringBooleanOperationBase.cs b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperationBase.cs new file mode 100644 index 00000000..156abfeb --- /dev/null +++ b/src/Examine.Lucene/Search/LuceneFilteringBooleanOperationBase.cs @@ -0,0 +1,166 @@ +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 + } +} diff --git a/src/Examine.Lucene/Search/LuceneQuery.cs b/src/Examine.Lucene/Search/LuceneQuery.cs index 42757bf7..29c745c5 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,9 @@ 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); + } } diff --git a/src/Examine.Lucene/Search/LuceneSearchExecutor.cs b/src/Examine.Lucene/Search/LuceneSearchExecutor.cs index 053a8e89..659337b7 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) { 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..1cd01d88 --- /dev/null +++ b/src/Examine.Lucene/Search/LuceneSearchFiltering.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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 + + } +} diff --git a/src/Examine.Lucene/Search/LuceneSearchFilteringOperationBase.cs b/src/Examine.Lucene/Search/LuceneSearchFilteringOperationBase.cs new file mode 100644 index 00000000..1a56a78d --- /dev/null +++ b/src/Examine.Lucene/Search/LuceneSearchFilteringOperationBase.cs @@ -0,0 +1,137 @@ +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); + + /// + 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); + + /// + 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); + } +} diff --git a/src/Examine.Lucene/Search/LuceneSearchQuery.cs b/src/Examine.Lucene/Search/LuceneSearchQuery.cs index b1a787a3..3e036e10 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 { @@ -258,7 +261,14 @@ 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(); @@ -443,5 +453,15 @@ 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; + } } } diff --git a/src/Examine.Lucene/Search/LuceneSearchQueryBase.cs b/src/Examine.Lucene/Search/LuceneSearchQueryBase.cs index e8960684..b43eeb1d 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,22 @@ 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 +56,7 @@ protected LuceneSearchQueryBase(CustomMultiFieldQueryParser queryParser, Category = category; SearchOptions = searchOptions; Queries.Push(new BooleanQuery()); + Filters.Push(new BooleanFilter()); BooleanOperation = occurance; _queryParser = queryParser; } @@ -70,6 +80,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 +142,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 +668,12 @@ private BooleanQuery GetMultiFieldQuery( /// A that represents this instance. /// public override string ToString() => $"{{ Category: {Category}, LuceneQuery: {Query} }}"; + + /// + /// 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..230eda18 100644 --- a/src/Examine.Test/Examine.Lucene/Search/FluentApiTests.cs +++ b/src/Examine.Test/Examine.Lucene/Search/FluentApiTests.cs @@ -5,13 +5,16 @@ 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 @@ -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); + } + } } }