diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs index 7ba10019c7..daa39dfcf5 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs @@ -5,17 +5,17 @@ using Umbraco.Cms.Infrastructure.Examine; namespace Umbraco.Extensions; -public static class UmbracoExamineExtensions +public static partial class UmbracoExamineExtensions { /// /// Matches a culture iso name suffix /// /// /// myFieldName_en-us will match the "en-us" + /// myBlockListField.items[0].myFieldName_en-us will match the "en-us" /// - internal static readonly Regex _cultureIsoCodeFieldNameMatchExpression = new( - "^(?[_\\w]+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", - RegexOptions.Compiled | RegexOptions.ExplicitCapture); + [GeneratedRegex(@"^(?.+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", RegexOptions.ExplicitCapture)] + internal static partial Regex CultureIsoCodeFieldNameMatchExpression(); // TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression @@ -32,7 +32,7 @@ public static class UmbracoExamineExtensions var results = new List(); foreach (var field in allFields) { - Match match = _cultureIsoCodeFieldNameMatchExpression.Match(field); + Match match = CultureIsoCodeFieldNameMatchExpression().Match(field); if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) { results.Add(field); @@ -54,7 +54,7 @@ public static class UmbracoExamineExtensions foreach (var field in allFields) { - Match match = _cultureIsoCodeFieldNameMatchExpression.Match(field); + Match match = CultureIsoCodeFieldNameMatchExpression().Match(field); if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) { yield return field; // matches this culture field diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs index 9f9e214316..f2c6236a2e 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs @@ -70,7 +70,7 @@ public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection return false; } - Match match = UmbracoExamineExtensions._cultureIsoCodeFieldNameMatchExpression.Match(fieldName); + Match match = UmbracoExamineExtensions.CultureIsoCodeFieldNameMatchExpression().Match(fieldName); if (match.Success) { var nonCultureFieldName = match.Groups["FieldName"].Value; diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs index 6e82239dac..e232876001 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs @@ -131,4 +131,21 @@ public interface IPublishedContentQuery /// The search results. /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); + + /// + /// Executes the query and converts the results to . + /// + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// + /// The search results. + /// + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords, string? culture) + => Search(query, skip, take, out totalRecords); } diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs index d075e8b9d2..758115b9ca 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs @@ -293,21 +293,17 @@ public class PublishedContentQuery : IPublishedContentQuery var fields = umbIndex.GetCultureAndInvariantFields(culture) .ToArray(); // Get all index fields suffixed with the culture name supplied - ordering = query.ManagedQuery(term, fields); + + // Filter out unpublished content for the specified culture if the content varies by culture + // The published__{culture} field is not populated when the content is not published in that culture + ordering = query + .ManagedQuery(term, fields) + .Not().Group(q => q + .Field(UmbracoExamineFieldNames.VariesByCultureFieldName, "y") + .Not().Field($"{UmbracoExamineFieldNames.PublishedFieldName}_{culture.ToLowerInvariant()}", "y")); } - // Filter selected fields because results are loaded from the published snapshot based on these - IOrdering? queryExecutor = ordering.SelectFields(_returnedQueryFields); - - - ISearchResults? results = skip == 0 && take == 0 - ? queryExecutor.Execute() - : queryExecutor.Execute(QueryOptions.SkipTake(skip, take)); - - totalRecords = results.TotalItemCount; - - return new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), - _variationContextAccessor, culture); + return Search(ordering, skip, take, out totalRecords, culture); } /// @@ -316,6 +312,10 @@ public class PublishedContentQuery : IPublishedContentQuery /// public IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords) + => Search(query, skip, take, out totalRecords, null); + + /// + public IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords, string? culture) { if (skip < 0) { @@ -331,8 +331,8 @@ public class PublishedContentQuery : IPublishedContentQuery if (query is IOrdering ordering) { - // Filter selected fields because results are loaded from the published snapshot based on these - query = ordering.SelectFields(_returnedQueryFields); + // Filter selected fields because results are loaded from the published snapshot based on these + query = ordering.SelectFields(_returnedQueryFields); } ISearchResults? results = skip == 0 && take == 0 @@ -341,7 +341,9 @@ public class PublishedContentQuery : IPublishedContentQuery totalRecords = results.TotalItemCount; - return results.ToPublishedSearchResults(_publishedSnapshot); + return culture.IsNullOrWhiteSpace() + ? results.ToPublishedSearchResults(_publishedSnapshot) + : new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); } /// diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs index 84e4be9d60..08e4c20343 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs @@ -3,6 +3,7 @@ using Examine.Lucene; using Examine.Lucene.Directories; using Examine.Lucene.Providers; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Models.PublishedContent; @@ -41,37 +42,27 @@ public class PublishedContentQueryTests : ExamineBaseTest public IEnumerable GetFields() => _fieldNames; } - private TestIndex CreateTestIndex(Directory luceneDirectory, string[] fieldNames) + private TestIndex CreateTestIndex(Directory luceneDirectory, (string Name, string Culture)[] fields) { - var index = new TestIndex(LoggerFactory, "TestIndex", luceneDirectory, fieldNames); + var index = new TestIndex(LoggerFactory, "TestIndex", luceneDirectory, fields.Select(f => f.Name).ToArray()); using (index.WithThreadingMode(IndexThreadingMode.Synchronous)) { - //populate with some test data - index.IndexItem(new ValueSet( - "1", - "content", - new Dictionary - { - [fieldNames[0]] = "Hello world, there are products here", - [UmbracoExamineFieldNames.VariesByCultureFieldName] = "n" - })); - index.IndexItem(new ValueSet( - "2", - "content", - new Dictionary - { - [fieldNames[1]] = "Hello world, there are products here", - [UmbracoExamineFieldNames.VariesByCultureFieldName] = "y" - })); - index.IndexItem(new ValueSet( - "3", - "content", - new Dictionary - { - [fieldNames[2]] = "Hello world, there are products here", - [UmbracoExamineFieldNames.VariesByCultureFieldName] = "y" - })); + // populate with some test data + for (var i = 0; i < fields.Length; i++) + { + var (name, culture) = fields[i]; + + index.IndexItem(new ValueSet( + $"{i + 1}", + "content", + new Dictionary + { + [name] = "Hello world, there are products here", + [UmbracoExamineFieldNames.VariesByCultureFieldName] = culture.IsNullOrEmpty() ? "n" : "y", + [culture.IsNullOrEmpty() ? UmbracoExamineFieldNames.PublishedFieldName : $"{UmbracoExamineFieldNames.PublishedFieldName}_{culture}"] = "y" + })); + } } return index; @@ -102,8 +93,8 @@ public class PublishedContentQueryTests : ExamineBaseTest { using (var luceneDir = new RandomIdRAMDirectory()) { - var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; - using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + var fields = new[] { (Name: "title", Culture: null), (Name: "title_en-us", Culture: "en-us"), (Name: "title_fr-fr", Culture: "fr-fr") }; + using (var indexer = CreateTestIndex(luceneDir, fields)) { var pcq = CreatePublishedContentQuery(indexer);