generated from Kentico/repo-template
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from Kentico/TK/lucene_facets
lucene facets
- Loading branch information
Showing
21 changed files
with
829 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
src/Kentico.Xperience.Lucene.Sample/Search/CafeSearchController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
using CMS.Core; | ||
using Kentico.Xperience.Lucene; | ||
using Kentico.Xperience.Lucene.Services; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace DancingGoat.Search; | ||
|
||
public class CafeSearchController: Controller | ||
{ | ||
|
||
public record SearchRequest(string Query = "", int PageSize = 20, int Page = 1); | ||
public record RebuildSearchIndexRequest(string IndexName, string Secret); | ||
|
||
// replace with real secret loaded from config | ||
private const string REBUILD_SECRET = "1234567890aaabbbccc"; | ||
private readonly CafeSearchService searchService; | ||
private readonly ILuceneClient luceneClient; | ||
private readonly IEventLogService eventLogService; | ||
|
||
public CafeSearchController(CafeSearchService searchService, ILuceneClient luceneClient, IEventLogService eventLogService) | ||
{ | ||
this.searchService = searchService; | ||
this.luceneClient = luceneClient; | ||
this.eventLogService = eventLogService; | ||
} | ||
|
||
[HttpGet] | ||
public IActionResult Index(string query, int pageSize = 10, int page = 1, string? facet = null) | ||
{ | ||
var results = searchService.Search(query, pageSize, page, facet); | ||
|
||
return View(results); | ||
} | ||
|
||
|
||
/// <summary> | ||
/// Rebuild of index could be initialized by HTTP POST request to url [webroot]/search/rebuild with body | ||
/// <code> | ||
/// { | ||
/// "indexName": "...", | ||
/// "secret": "..." | ||
/// } | ||
/// </code> | ||
/// </summary> | ||
/// <param name="request"></param> | ||
/// <returns></returns> | ||
[HttpPost] | ||
public async Task<IActionResult> Rebuild([FromBody] RebuildSearchIndexRequest request) | ||
{ | ||
try | ||
{ | ||
if (request.Secret != REBUILD_SECRET) | ||
{ | ||
return Unauthorized("Invalid Secret"); | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(request.IndexName)) | ||
{ | ||
return NotFound($"IndexName is required"); | ||
} | ||
|
||
var index = IndexStore.Instance.GetIndex(CafeSearchModel.IndexName); | ||
if (index == null) | ||
{ | ||
return NotFound($"Index not found: {request.IndexName}"); | ||
} | ||
|
||
await luceneClient.Rebuild(index.IndexName, null); | ||
return Ok("Index rebuild started"); | ||
} | ||
catch (Exception ex) | ||
{ | ||
eventLogService.LogException(nameof(SearchController), nameof(Rebuild), ex, 0, $"IndexName: {request.IndexName}"); | ||
return Problem("Index rebuild failed"); | ||
} | ||
} | ||
} | ||
|
49 changes: 49 additions & 0 deletions
49
src/Kentico.Xperience.Lucene.Sample/Search/CafeSearchModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using CMS.DocumentEngine; | ||
using CMS.DocumentEngine.Types.DancingGoatCore; | ||
using Kentico.Xperience.Lucene.Attributes; | ||
using Kentico.Xperience.Lucene.Models; | ||
using Kentico.Xperience.Lucene.Services.Implementations; | ||
using Lucene.Net.Facet; | ||
|
||
namespace DancingGoat.Search; | ||
|
||
[IncludedPath("/%", ContentTypes = new string[] { Cafe.CLASS_NAME })] | ||
public class CafeSearchModel: LuceneSearchModel | ||
{ | ||
public const string IndexName = "CafeIndex"; | ||
|
||
[TextField(true)] | ||
[Source(new string[] { nameof(TreeNode.DocumentName) })] | ||
public string Title { get; set; } | ||
|
||
[TextField(true)] | ||
public string CafeCountry { get; set; } | ||
|
||
[TextField(true)] | ||
public string CafeCity { get; set; } | ||
|
||
[TextField(true)] | ||
public string CafeZipCode { get; set; } | ||
|
||
public override IEnumerable<FacetField> OnTaxonomyFieldCreation() | ||
{ | ||
string[] countries = CafeCountry?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(country => country.Trim()).ToArray() ?? Array.Empty<string>(); | ||
yield return countries switch | ||
{ | ||
{ Length: >= 2 } => new FacetField("Country", countries[0], countries[1]), | ||
{ Length: 1 } => new FacetField("Country", countries[0], "no state"), | ||
_ => new FacetField("Country", "no country", "no state") | ||
}; | ||
} | ||
} | ||
|
||
|
||
public class CafeLuceneIndexingStrategy : DefaultLuceneIndexingStrategy | ||
{ | ||
public override FacetsConfig FacetsConfigFactory() | ||
{ | ||
var facetConfig = new FacetsConfig(); | ||
facetConfig.SetHierarchical("Country", true); | ||
return facetConfig; | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
src/Kentico.Xperience.Lucene.Sample/Search/CafeSearchService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
using Kentico.Xperience.Lucene; | ||
using Kentico.Xperience.Lucene.Models; | ||
using Kentico.Xperience.Lucene.Services; | ||
using Lucene.Net.Documents; | ||
using Lucene.Net.Facet; | ||
using Lucene.Net.Search; | ||
using Lucene.Net.Util; | ||
|
||
namespace DancingGoat.Search; | ||
|
||
public class CafeSearchService | ||
{ | ||
private const int PHRASE_SLOP = 3; | ||
private const int MAX_RESULTS = 1000; | ||
|
||
private readonly ILuceneIndexService luceneIndexService; | ||
|
||
public CafeSearchService(ILuceneIndexService luceneIndexService) => this.luceneIndexService = luceneIndexService; | ||
|
||
public LuceneSearchResultModel<CafeSearchModel> Search(string searchText, int pageSize = 20, int page = 1, string facet = null) | ||
{ | ||
var index = IndexStore.Instance.GetIndex(CafeSearchModel.IndexName) ?? throw new Exception($"Index {CafeSearchModel.IndexName} was not found!!!"); | ||
pageSize = Math.Max(1, pageSize); | ||
page = Math.Max(1, page); | ||
int offset = pageSize * (page - 1); | ||
int limit = pageSize; | ||
|
||
var queryBuilder = new QueryBuilder(index.Analyzer); | ||
|
||
var query = string.IsNullOrWhiteSpace(searchText) | ||
? new MatchAllDocsQuery() | ||
: GetTermQuery(queryBuilder, searchText); | ||
|
||
DrillDownQuery drillDownQuery = null; | ||
if (facet != null) | ||
{ | ||
var indexingStrategy = new CafeLuceneIndexingStrategy(); | ||
var config = indexingStrategy.FacetsConfigFactory(); | ||
drillDownQuery = new DrillDownQuery(indexingStrategy.FacetsConfigFactory(), query); | ||
|
||
string[] f = facet.Split(';', StringSplitOptions.RemoveEmptyEntries); | ||
if (f.Length >= 2) | ||
{ | ||
var countryDim = config?.GetDimConfig("Country"); | ||
var boolQuery = new BooleanQuery(); | ||
boolQuery.Add(new TermQuery(DrillDownQuery.Term(countryDim.IndexFieldName, "Country", f.Skip(1).ToArray())), Occur.MUST); | ||
boolQuery.Add(query, Occur.MUST); | ||
drillDownQuery.Add("Country", boolQuery); | ||
} | ||
} | ||
|
||
var result = luceneIndexService.UseSearcherWithFacets( | ||
index, | ||
query, 20, | ||
(searcher, facets) => | ||
{ | ||
var topDocs = searcher.Search(drillDownQuery ?? query, MAX_RESULTS, | ||
new Sort(new SortField( | ||
nameof(DancingGoatSearchModel.PublishedDateTicks), | ||
FieldCache.NUMERIC_UTILS_INT64_PARSER, | ||
true))); | ||
return new LuceneSearchResultModel<CafeSearchModel> | ||
{ | ||
Query = searchText ?? "", | ||
Page = page, | ||
PageSize = pageSize, | ||
TotalPages = topDocs.TotalHits <= 0 ? 0 : ((topDocs.TotalHits - 1) / pageSize) + 1, | ||
TotalHits = topDocs.TotalHits, | ||
Hits = topDocs.ScoreDocs | ||
.Skip(offset) | ||
.Take(limit) | ||
.Select(d => MapToResultItem(searcher.Doc(d.Doc))) | ||
.ToList(), | ||
Facet = facet, | ||
Facets = facets?.GetTopChildren(10, "Country", facet?.Split(';').Skip(1).ToArray() ?? Array.Empty<string>())?.LabelValues | ||
}; | ||
} | ||
); | ||
|
||
return result; | ||
} | ||
|
||
private static Query GetTermQuery(QueryBuilder queryBuilder, string searchText) | ||
{ | ||
var titlePhrase = queryBuilder.CreatePhraseQuery(nameof(CafeSearchModel.Title), searchText, PHRASE_SLOP); | ||
titlePhrase.Boost = 5; | ||
|
||
return new BooleanQuery | ||
{ | ||
{ titlePhrase, Occur.SHOULD }, | ||
}; | ||
} | ||
|
||
private CafeSearchModel MapToResultItem(Document doc) => new() | ||
{ | ||
Title = doc.Get(nameof(CafeSearchModel.Title)), | ||
Url = doc.Get(nameof(CafeSearchModel.Url)), | ||
CafeCity = doc.Get(nameof(CafeSearchModel.CafeCity)), | ||
CafeCountry = doc.Get(nameof(CafeSearchModel.CafeCountry)), | ||
CafeZipCode = doc.Get(nameof(CafeSearchModel.CafeZipCode)), | ||
}; | ||
} |
Oops, something went wrong.