Fixes #15136: Search includes fields from other cultures (#15148)

* Fixes #15136: Search includes fields from other cultures

Regex was updated to support block list fields
Unpublished nodes on the supplied culture are not filtered out

* Making the code non-breaking

* Fixed failing publish content query integration tests

The tests were not setting the content as publish in the specifed culture
causing the content items to be ignored

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
This commit is contained in:
Vitor Rodrigues
2024-02-07 13:43:37 +01:00
committed by GitHub
parent 2221c4f1c7
commit 839b2ff6a2
5 changed files with 62 additions and 52 deletions

View File

@@ -5,17 +5,17 @@ using Umbraco.Cms.Infrastructure.Examine;
namespace Umbraco.Extensions;
public static class UmbracoExamineExtensions
public static partial class UmbracoExamineExtensions
{
/// <summary>
/// Matches a culture iso name suffix
/// </summary>
/// <remarks>
/// myFieldName_en-us will match the "en-us"
/// myBlockListField.items[0].myFieldName_en-us will match the "en-us"
/// </remarks>
internal static readonly Regex _cultureIsoCodeFieldNameMatchExpression = new(
"^(?<FieldName>[_\\w]+)_(?<CultureName>[a-z]{2,3}(-[a-z0-9]{2,4})?)$",
RegexOptions.Compiled | RegexOptions.ExplicitCapture);
[GeneratedRegex(@"^(?<FieldName>.+)_(?<CultureName>[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<string>();
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

View File

@@ -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;

View File

@@ -131,4 +131,21 @@ public interface IPublishedContentQuery
/// The search results.
/// </returns>
IEnumerable<PublishedSearchResult> Search(IQueryExecutor query, int skip, int take, out long totalRecords);
/// <summary>
/// Executes the query and converts the results to <see cref="PublishedSearchResult" />.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="skip">The amount of results to skip.</param>
/// <param name="take">The amount of results to take/return.</param>
/// <param name="totalRecords">The total amount of records.</param>
/// <param name="culture">The culture (defaults to a culture insensitive search).</param>
/// <returns>
/// The search results.
/// </returns>
/// <remarks>
/// While enumerating results, the ambient culture is changed to be the searched culture.
/// </remarks>
IEnumerable<PublishedSearchResult> Search(IQueryExecutor query, int skip, int take, out long totalRecords, string? culture)
=> Search(query, skip, take, out totalRecords);
}

View File

@@ -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);
}
/// <inheritdoc />
@@ -316,6 +312,10 @@ public class PublishedContentQuery : IPublishedContentQuery
/// <inheritdoc />
public IEnumerable<PublishedSearchResult> Search(IQueryExecutor query, int skip, int take, out long totalRecords)
=> Search(query, skip, take, out totalRecords, null);
/// <inheritdoc />
public IEnumerable<PublishedSearchResult> 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);
}
/// <summary>

View File

@@ -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<string> 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<string, object>
{
[fieldNames[0]] = "Hello world, there are products here",
[UmbracoExamineFieldNames.VariesByCultureFieldName] = "n"
}));
index.IndexItem(new ValueSet(
"2",
"content",
new Dictionary<string, object>
{
[fieldNames[1]] = "Hello world, there are products here",
[UmbracoExamineFieldNames.VariesByCultureFieldName] = "y"
}));
index.IndexItem(new ValueSet(
"3",
"content",
new Dictionary<string, object>
{
[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<string, object>
{
[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);