supporting-blog-content/esre-with-blazor/elastic-blazor/Services/ElasticsearchService.cs (224 lines of code) (raw):
using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
namespace BlazorApp.Services
{
public class ElasticsearchService
{
private readonly ElasticsearchClient _client;
private readonly ILogger<ElasticsearchService> _logger;
public ElasticsearchService(
ElasticsearchClient client,
ILogger<ElasticsearchService> logger
)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_logger = logger;
}
public Dictionary<string, Dictionary<string, long>> FormatFacets(
Elastic.Clients.Elasticsearch.Aggregations.AggregateDictionary aggregations
)
{
var facets = new Dictionary<string, Dictionary<string, long>>();
foreach (var aggregation in aggregations)
{
if (
aggregation.Value
is Elastic.Clients.Elasticsearch.Aggregations.StringTermsAggregate termsAggregate
)
{
var facetName = aggregation.Key;
var facetDictionary = ConvertFacetDictionary(
termsAggregate.Buckets.ToDictionary(b => b.Key, b => b.DocCount)
);
facets[facetName] = facetDictionary;
}
}
return facets;
}
private static Action<QueryDescriptor<BookDoc>> BuildSemanticQuery(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
var filters = new List<Action<QueryDescriptor<BookDoc>>>();
if (selectedFacets != null)
{
foreach (var facet in selectedFacets)
{
foreach (var value in facet.Value)
{
var field = facet.Key.ToLower();
if (!string.IsNullOrEmpty(field))
{
filters.Add(m => m.Term(t => t.Field(new Field(field)).Value(value)));
}
}
}
}
return query =>
query.Bool(b =>
b.Must(m => m.Semantic(sem => sem.Field("longDescription").Query(searchTerm)))
.Filter(filters.ToArray())
);
}
private static Action<QueryDescriptor<BookDoc>> BuildMultiMatchQuery(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
var filters = new List<Action<QueryDescriptor<BookDoc>>>();
if (selectedFacets != null)
{
foreach (var facet in selectedFacets)
{
foreach (var value in facet.Value)
{
var field = facet.Key.ToLower();
if (!string.IsNullOrEmpty(field))
{
filters.Add(m => m.Term(t => t.Field(new Field(field)).Value(value)));
}
}
}
}
if (string.IsNullOrEmpty(searchTerm))
{
return query => query.Bool(b => b.Filter(filters.ToArray()));
}
return query =>
query.Bool(b =>
b.Should(m =>
m.MultiMatch(mm =>
mm.Query(searchTerm).Fields(new[] { "title", "shortDescription" })
)
)
.Filter(filters.ToArray())
);
}
private static Action<RetrieverDescriptor<BookDoc>> BuildHybridQuery(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
var filters = new List<Action<QueryDescriptor<BookDoc>>>();
if (selectedFacets != null)
{
foreach (var facet in selectedFacets)
{
foreach (var value in facet.Value)
{
var field = facet.Key.ToLower();
if (!string.IsNullOrEmpty(field))
{
filters.Add(m => m.Term(t => t.Field(new Field(field)).Value(value)));
}
}
}
}
return retrievers =>
retrievers.Rrf(rrf =>
rrf.RankWindowSize(50)
.RankConstant(20)
.Retrievers(
retrievers =>
retrievers.Standard(std =>
std.Query(q =>
q.Bool(b =>
b.Must(m =>
m.MultiMatch(mm =>
mm.Query(searchTerm)
.Fields(
new[]
{
"title",
"shortDescription",
}
)
)
)
.Filter(filters.ToArray())
)
)
),
retrievers =>
retrievers.Standard(std =>
std.Query(q =>
q.Bool(b =>
b.Must(m =>
m.Semantic(sem =>
sem.Field("longDescription")
.Query(searchTerm)
)
)
.Filter(filters.ToArray())
)
)
)
)
);
}
public async Task<ElasticResponse> SearchBooksAsync(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
try
{
_logger.LogInformation($"Performing search for: {searchTerm}");
var retrieverQuery = BuildHybridQuery(searchTerm, selectedFacets);
var multiMatchQuery = BuildMultiMatchQuery(searchTerm, selectedFacets);
var semanticQuery = BuildSemanticQuery(searchTerm, selectedFacets);
/**
* * Hybrid Search
*/
var response = await _client.SearchAsync<BookDoc>(s =>
s.Index("elastic-blazor-books")
.Retriever(retrieverQuery)
.Aggregations(aggs =>
aggs.Add("Authors", agg => agg.Terms(t => t.Field(p => p.Authors)))
.Add(
"Categories",
agg => agg.Terms(t => t.Field(p => p.Categories))
)
.Add("Status", agg => agg.Terms(t => t.Field(p => p.Status)))
)
);
/**
* * MultiMatch Search
*/
// var response = await _client.SearchAsync<BookDoc>(s =>
// s.Index("elastic-blazor-books")
// .Query(multiMatchQuery)
// .Aggregations(aggs =>
// aggs.Add("Authors", agg => agg.Terms(t => t.Field(p => p.Authors)))
// .Add(
// "Categories",
// agg => agg.Terms(t => t.Field(p => p.Categories))
// )
// .Add("Status", agg => agg.Terms(t => t.Field(p => p.Status)))
// )
// );
/**
* * Semantic Search
*/
// var response = await _client.SearchAsync<BookDoc>(s =>
// s.Index("elastic-blazor-books")
// .Query(semanticQuery)
// .Aggregations(aggs =>
// aggs.Add("Authors", agg => agg.Terms(t => t.Field(p => p.Authors)))
// .Add(
// "Categories",
// agg => agg.Terms(t => t.Field(p => p.Categories))
// )
// .Add("Status", agg => agg.Terms(t => t.Field(p => p.Status)))
// )
// );
if (response.IsValidResponse)
{
_logger.LogInformation($"Found {response.Documents.Count} documents");
var hits = response.Total;
var facets =
response.Aggregations != null
? FormatFacets(response.Aggregations)
: new Dictionary<string, Dictionary<string, long>>();
var elasticResponse = new ElasticResponse
{
TotalHits = hits,
Documents = response.Documents.ToList(),
Facets = facets,
};
return elasticResponse;
}
else
{
_logger.LogWarning($"Invalid response: {response.DebugInformation}");
return new ElasticResponse();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error performing search");
return new ElasticResponse();
}
}
private Dictionary<string, long> ConvertFacetDictionary(
Dictionary<Elastic.Clients.Elasticsearch.FieldValue, long> original
)
{
var result = new Dictionary<string, long>();
foreach (var kvp in original)
{
result[kvp.Key.ToString()] = kvp.Value;
}
return result;
}
}
}