From fcf477191f83bb56d0af968d268f138f9dc7c9ab Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Thu, 25 May 2023 13:43:45 +0200 Subject: [PATCH] Delivery API: Adding support for "Contains" filters (#14289) * Adding support for contains filters * Making a dedicated name filter and making changes to the sort indexer * Fixed the merge from release/12.0 * Make the "name" filter support OR for comma separated filter values --------- Co-authored-by: kjac --- .../Indexing/Filters/NameFilterIndexer.cs | 15 ++++++++ .../Indexing/Sorts/NameSortIndexer.cs | 2 +- .../Querying/Filters/NameFilter.cs | 18 +++++----- .../Services/ApiContentQueryProvider.cs | 34 ++++++++++++++++--- ...ryApiContentIndexFieldDefinitionBuilder.cs | 2 +- 5 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Delivery/Indexing/Filters/NameFilterIndexer.cs diff --git a/src/Umbraco.Cms.Api.Delivery/Indexing/Filters/NameFilterIndexer.cs b/src/Umbraco.Cms.Api.Delivery/Indexing/Filters/NameFilterIndexer.cs new file mode 100644 index 0000000000..eec3c1027d --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Indexing/Filters/NameFilterIndexer.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Delivery.Indexing.Filters; + +public sealed class NameFilterIndexer : IContentIndexHandler +{ + internal const string FieldName = "name"; + + public IEnumerable GetFieldValues(IContent content, string? culture) + => new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.GetCultureName(culture) ?? string.Empty } } }; + + public IEnumerable GetFields() + => new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringAnalyzed, VariesByCulture = true } }; +} diff --git a/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs b/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs index 5f5334cd05..65a1d67ed4 100644 --- a/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs +++ b/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Api.Delivery.Indexing.Sorts; public sealed class NameSortIndexer : IContentIndexHandler { - internal const string FieldName = "name"; + internal const string FieldName = "sortName"; public IEnumerable GetFieldValues(IContent content, string? culture) => new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.GetCultureName(culture) ?? string.Empty } } }; diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs index 0bf3d5e460..e12b20e81c 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs @@ -1,6 +1,6 @@ -using Umbraco.Cms.Api.Delivery.Indexing.Sorts; +using Umbraco.Cms.Api.Delivery.Indexing.Filters; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Querying.Filters; @@ -16,16 +16,16 @@ public sealed class NameFilter : IFilterHandler public FilterOption BuildFilterOption(string filter) { var value = filter.Substring(NameSpecifier.Length); + var negate = value.StartsWith('!'); + var values = value.TrimStart('!').Split(Constants.CharArrays.Comma); return new FilterOption { - FieldName = NameSortIndexer.FieldName, - Values = value.IsNullOrWhiteSpace() == false - ? new[] { value.TrimStart('!') } - : Array.Empty(), - Operator = value.StartsWith('!') - ? FilterOperation.IsNot - : FilterOperation.Is + FieldName = NameFilterIndexer.FieldName, + Values = values, + Operator = negate + ? FilterOperation.DoesNotContain + : FilterOperation.Contains }; } } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs index 99db4534bc..37be4f16ee 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs @@ -1,4 +1,6 @@ using Examine; +using Examine.Lucene.Providers; +using Examine.Lucene.Search; using Examine.Search; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; @@ -77,7 +79,14 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture) { - IQuery query = index.Searcher.CreateQuery(); + // Needed for enabling leading wildcards searches + BaseLuceneSearcher searcher = index.Searcher as BaseLuceneSearcher ?? throw new InvalidOperationException($"Index searcher must be of type {nameof(BaseLuceneSearcher)}."); + + IQuery query = searcher.CreateQuery( + IndexTypes.Content, + BooleanOperation.And, + searcher.LuceneAnalyzer, + new LuceneSearchOptions { AllowLeadingWildcard = true }); IBooleanOperation selectorOperation = selectorOption.Values.Length == 1 ? query.Field(selectorOption.FieldName, selectorOption.Values.First()) @@ -103,6 +112,23 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider } } + void HandleContains(IQuery query, string fieldName, string[] values) + { + if (values.Length == 1) + { + // The trailing wildcard is added automatically + query.Field(fieldName, (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{values[0]}")); + } + else + { + // The trailing wildcard is added automatically + IExamineValue[] examineValues = values + .Select(value => (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{value}")) + .ToArray(); + query.GroupedOr(new[] { fieldName }, examineValues); + } + } + foreach (FilterOption filterOption in filterOptions) { var values = filterOption.Values.Any() @@ -112,18 +138,16 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider switch (filterOption.Operator) { case FilterOperation.Is: - // TODO: test this for explicit word matching HandleExact(queryOperation.And(), filterOption.FieldName, values); break; case FilterOperation.IsNot: - // TODO: test this for explicit word matching HandleExact(queryOperation.Not(), filterOption.FieldName, values); break; - // TODO: Fix case FilterOperation.Contains: + HandleContains(queryOperation.And(), filterOption.FieldName, values); break; - // TODO: Fix case FilterOperation.DoesNotContain: + HandleContains(queryOperation.Not(), filterOption.FieldName, values); break; default: continue; diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs index bfd11defde..a0bbecbd74 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs @@ -68,7 +68,7 @@ internal sealed class DeliveryApiContentIndexFieldDefinitionBuilder : IDeliveryA FieldType.Number => FieldDefinitionTypes.Integer, FieldType.StringRaw => FieldDefinitionTypes.Raw, FieldType.StringAnalyzed => FieldDefinitionTypes.FullText, - FieldType.StringSortable => FieldDefinitionTypes.FullTextSortable, + FieldType.StringSortable => FieldDefinitionTypes.InvariantCultureIgnoreCase, _ => throw new ArgumentOutOfRangeException(nameof(field.FieldType)) };