Création d'une application de recherche avec Blazor et Elasticsearch
Apprenez à construire une application de recherche en utilisant Blazor et Elasticsearch, et à utiliser le client Elasticsearch .NET pour la recherche hybride.
Dans cet article, vous apprendrez à tirer parti de vos compétences en C# pour créer une application de recherche à l'aide de Blazor et d'Elasticsearch. Nous allons utiliser le client Elasticsearch .NET pour exécuter des requêtes de recherche plein texte, sémantique et hybride.
NOTE Si vous êtes familier avec l'ancienne version du client Elasticsearch C# NEST, lisez cet article de blog sur la dépréciation du client NEST et les nouvelles fonctionnalités. NEST était la génération précédente du client .NET, qui a été remplacée par le client Elastic.Clients.Elasticsearch package

Qu'est-ce que Blazor ?

Image : ASP.NETCoreBlazorhostingmodels
Blazor est un framework web open source basé sur HTML, CSS et C# créé par Microsoft pour permettre aux développeurs de créer des applications web qui s'exécutent sur le client ou le serveur. Blazor vous permet également de créer des composants réutilisables pour construire des applications plus rapidement ; il permet aux développeurs de construire la vue HTML et les actions en C# dans le même fichier, ce qui aide à maintenir un code lisible et propre. De plus, avec Blazor Hybrid, vous pouvez créer des applications mobiles natives accédant aux capacités de la plateforme native via le code .NET.
Quelques-unes des caractéristiques qui font de Blazor un framework idéal pour travailler :
Options de rendu côté serveur et côté client
Composants réutilisables de l'interface utilisateur
Mises à jour en temps réel avec SignalR
Gestion intégrée des états
Système de routage intégré
Vérifications solides du typage et de la compilation
Pourquoi Blazor ?
Blazor offre plusieurs avantages par rapport à d'autres cadres et bibliothèques : il permet aux développeurs d'utiliser le langage C# pour le code client et le code serveur, en fournissant un typage fort et une vérification au moment de la compilation, ce qui améliore la fiabilité. Il s'intègre parfaitement à l'écosystème .NET, permettant la réutilisation de bibliothèques et d'outils .NET, et offre un support de débogage robuste.
Qu'est-ce que l'ESRE ?
Elasticsearch Relevance Engine™ (ESRE ) est un ensemble d'outils permettant de créer des applications de recherche utilisant l'apprentissage automatique et l'intelligence artificielle au-dessus du puissant moteur de recherche Elasticsearch.

Pour en savoir plus sur l'ESRE, vous pouvez lire notre article de blog ici.
Configuration d'ELSER
Pour tirer parti des capacités ESRE d'Elastic, nous allons utiliser ELSER comme fournisseur de modèle.
Remarque : pour utiliser les modèles ELSER d'Elasticsearch, vous devez disposer d'une licence Platinum ou Enterprise, et d'un nœud de Machine Lerning (ML) dédié d'une taille minimale de 4 Go. Pour en savoir plus, cliquez ici.
Commencez par créer le point d'arrivée de l'inférence :
PUT _inference/sparse_embedding/my-elser-model
{
"service": "elser",
"service_settings": {
"num_allocations": 1,
"num_threads": 1
}
}Si vous utilisez ELSER pour la première fois, il se peut que vous rencontriez une erreur 502 Bad Gateway lorsque le modèle se charge en arrière-plan. Vous pouvez vérifier l'état du modèle sur Machine Learning > Trained Models dans Kibana. Une fois qu'il est déployé, vous pouvez passer à l'étape suivante.

Indexation des données
Vous pouvez télécharger le jeu de données ici et importer les données à l'aide de Kibana. Pour ce faire, allez sur la page d'accueil et cliquez sur "Upload data". Ensuite, téléchargez le fichier et cliquez sur Import. Enfin, allez dans l'onglet Advanced et collez les mappings suivants :
{
"properties":{
"authors":{
"type":"keyword"
},
"categories":{
"type":"keyword"
},
"longDescription":{
"type":"semantic_text",
"inference_id":"my-elser-model",
"model_settings":{
"task_type":"sparse_embedding"
}
},
"pageCount":{
"type":"integer"
},
"publishedDate":{
"type":"date"
},
"shortDescription":{
"type":"text"
},
"status":{
"type":"keyword"
},
"thumbnailUrl":{
"type":"keyword"
},
"title":{
"type":"text"
}
}
}
Nous allons créer un index capable d'exécuter des requêtes sémantiques et en texte intégral. Le type de champ semantic_text se chargera du découpage et de l'intégration des données. Notez que nous indexons longDescription comme semantic_text, vous pouvez utiliser copy_to si vous voulez indexer un champ comme semantic_text et text`.
Construire l'application avec Blazor & Elasticsearch
Clé API
La première chose à faire est de créer une clé API pour authentifier nos requêtes à Elasticsearch. La clé API doit être en lecture seule et n'être autorisée qu'à interroger l'index books-blazor.
POST /_security/api_key
{
"name": "books-blazor-key",
"role_descriptors": {
"books-blazor-reader": {
"indices": [
{
"names": ["books-blazor"],
"privileges": ["read"]
}
]
}
}
}Vous verrez quelque chose comme ceci :
{
"id": "XXXXXXXXXXXXXXXXXXXXXXXX",
"name": "books-blazor-key",
"api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}Enregistrez la valeur du champ de réponse encoded car vous en aurez besoin ultérieurement. Si vous travaillez sur Elastic Cloud, vous aurez également besoin de votre Cloud ID. (Vous pouvez le trouver ici).
Création du projet Blazor
Commencez par installer Blazor et créez un projet d'exemple en suivant les instructions officielles.

Une fois le projet créé, la structure des dossiers et des fichiers doit ressembler à ceci :
BlazorApp/
|-- BlazorApp.csproj
|-- BlazorApp.sln
|-- Program.cs
|-- appsettings.Development.json
|-- appsettings.json
|-- Properties/
| `-- launchSettings.json
|-- Components/
| |-- App.razor
| |-- Routes.razor
| |-- _Imports.razor
| |-- Layout/
| | |-- MainLayout.razor
| | |-- MainLayout.razor.css
| | |-- NavMenu.razor
| | `-- NavMenu.razor.css
| `-- Pages/
| |-- Counter.razor
| |-- Error.razor
| |-- Home.razor
| `-- Weather.razor
|-- wwwroot/
|-- bin/
`-- obj/Le modèle d'application comprend Bootstrap v5.1.0 pour le stylisme.
Terminez la configuration du projet en installant le client Elasticsearch .NET :
dotnet add package Elastic.Clients.ElasticsearchUne fois cette étape terminée, votre page devrait ressembler à ceci :

Structure des dossiers
Nous allons maintenant organiser nos dossiers comme suit :
BlazorApp/
|-- Components/
| |-- Pages/
| | |-- Search.razor
| | `-- Search.razor.css
| `-- Elasticsearch/
| |-- SearchBar.razor
| |-- Results.razor
| `-- Facet.razor
|-- Models/
| |-- Book.cs
| `-- Response.cs
`-- Services/
`-- ElasticsearchService.csLes dossiers sont expliqués :
Components/Pages/Search.razor : page principale contenant la barre de recherche, les résultats et les filtres.
Composants/Pages/Search.razor.css : les styles de page.
Components/Elasticsearch/SearchBar.razor : composant de barre de recherche.
Components/Elasticsearch/Results.razor : composant de résultats.
Components/Elasticsearch/Facet.razor : composant de filtres.
Components/Svg/GlassIcon.razor : icône de recherche.
Components/_Imports.razor : ce fichier importera tous les composants.
Models/Book.cs : ce fichier contient le schéma du champ livre.
Models/Response.cs : ce fichier stocke le schéma de la réponse, y compris les résultats de la recherche, les facettes et le nombre total de résultats.
Services/ElasticsearchService.cs : Service Elasticsearch. Il gère la connexion et les requêtes à Elasticsearch.
Configuration initiale
Commençons par un peu de nettoyage.
Supprimer les fichiers :
Composants/Pages/Compteur.razor
Composants/Pages/Météo.razor
Composants/Pages/Home.razor
Components/Layout/NavMenu.razor
Composants/Layout/NavMenu.razor.css
Vérifiez le fichier /Components/_Imports.razor. Vous devriez avoir les importations suivantes :
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp
@using BlazorApp.ComponentsIntégrer Elastic dans le projet
Maintenant, importons les composants d'Elasticsearch :
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp
@using BlazorApp.Components
@using BlazorApp.Components.Elasticsearch @* <--- Add this line *@Nous allons supprimer la barre latérale par défaut afin de disposer de plus d'espace pour notre application en la supprimant du fichier /Components/Layout/MainLayout.razor:
@inherits LayoutComponentBase
<div class="page">
<main>
<article class="content">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
Entrons maintenant les informations d'identification Elasticsearch pour les secrets d'utilisateur:
dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"Grâce à cette approche, .Net 8 stocke les données sensibles dans un emplacement distinct, en dehors du dossier du projet, et les rend accessibles à l'aide de l'interface IConfiguration. Ces variables seront disponibles pour tout projet .Net qui utilise les mêmes secrets d'utilisateur.
Ensuite, modifions le fichier Program.cs pour lire les secrets et monter le client Elasticsearch :
Tout d'abord, il faut importer les bibliothèques nécessaires :
using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;BlazorApp.Services : contient le service Elasticsearch.
Elastic.Clients.Elasticsearch : importe la bibliothèque .Net 8 du client Elasticsearch.
Elastic.Transport : importe la bibliothèque de transport Elasticsearch, ce qui nous permet d'utiliser la classe ApiKey pour authentifier nos requêtes.
Deuxièmement, insérez le code suivant avant la ligne var app = builder.Build():
// Initialize the Elasticsearch client.
builder.Services.AddScoped(sp =>
{
// Getting access to the configuration service to read the Elasticsearch credentials.
var configuration = sp.GetRequiredService<IConfiguration>();
var cloudId = configuration["ElasticsearchCloudId"];
var apiKey = configuration["ElasticsearchApiKey"];
if (string.IsNullOrEmpty(cloudId) || string.IsNullOrEmpty(apiKey))
{
throw new InvalidOperationException(
"Elasticsearch credentials are missing in configuration."
);
}
var settings = new ElasticsearchClientSettings(cloudId, new ApiKey(apiKey)).EnableDebugMode();
return new ElasticsearchClient(settings);
});Ce code va lire les informations d'identification Elasticsearch à partir des secrets d'utilisateur et créer une instance de client Elasticsearch.
Après l'initialisation du client ElasticSearch, ajoutez la ligne suivante pour enregistrer le service Elasticsearch :
builder.Services.AddScoped<ElasticsearchService>();L'étape suivante consistera à construire la logique de recherche dans le fichier /Services/ElasticsearchService.cs:
Tout d'abord, il faut importer les bibliothèques et les modèles nécessaires :
using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;Ensuite, ajoutez la classe ElasticsearchService, le constructeur et les variables :
namespace BlazorApp.Services
{
public class ElasticsearchService
{
private readonly ElasticsearchClient _client;
// The logger is used to log information, warnings and errors about the Elasticsearch service and requests.
private readonly ILogger<ElasticsearchService> _logger;
public ElasticsearchService(
ElasticsearchClient client,
ILogger<ElasticsearchService> logger
)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_logger = logger;
}
}
}Configuration de la recherche
Construisons maintenant notre logique de recherche :
private static Action<RetrieverDescriptor<BookDoc>> BuildHybridQuery(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
var filters = BuildFilters(selectedFacets);
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 static List<Action<QueryDescriptor<BookDoc>>> BuildFilters(
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 filters;
}BuildFiltersconstruira les filtres pour la requête de recherche en utilisant les facettes sélectionnées par l'utilisateur.BuildHybridQueryconstruira une requête de recherche hybride qui combine la recherche plein texte et la recherche sémantique.
Ensuite, ajoutez la méthode de recherche :
public async Task<ElasticResponse> SearchBooksAsync(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
try
{
_logger.LogInformation($"Performing search for: {searchTerm}");
// Retrieve the hybrid query with filters applied.
var retrieverQuery = BuildHybridQuery(searchTerm, selectedFacets);
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)))
)
);
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();
}
}
public static 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 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;
}SearchBooksAsync: effectuera la recherche en utilisant la requête hybride et renverra les résultats en incluant les agrégations pour construire les facettes.FormatFacets: formatera la réponse des agrégations dans un dictionnaire.ConvertFacetDictionary: convertit le dictionnaire de facettes dans un format plus lisible.
L'étape suivante consiste à créer les modèles qui représenteront les données renvoyées dans le site hits de la requête Elasticsearch et qui seront imprimées en tant que résultats dans notre page de recherche.
Nous commençons par créer le fichier /Models/Book.cs et y ajouter ce qui suit :
namespace BlazorApp.Models
{
public class BookDoc
{
public string? Title { get; set; }
public int? PageCount { get; set; }
public string? PublishedDate { get; set; }
public string? ThumbnailUrl { get; set; }
public string? ShortDescription { get; set; }
public LongDescription? LongDescription { get; set; }
public string? Status { get; set; }
public List<string>? Authors { get; set; }
public List<string>? Categories { get; set; }
}
public class LongDescription
{
public string? Text { get; set; }
}
}Ensuite, il faut configurer la réponse Elastic dans le fichier /Models/Response.cs et ajouter ce qui suit :
namespace BlazorApp.Models
{
public class ElasticResponse
{
public ElasticResponse()
{
Documents = new List<BookDoc>();
Facets = new Dictionary<string, Dictionary<string, long>>();
}
public long TotalHits { get; set; }
public List<BookDoc> Documents { get; set; }
public Dictionary<string, Dictionary<string, long>> Facets { get; set; }
}
}Configuration d'une interface utilisateur de base
Ensuite, ajoutez le composant SearchBar. Dans le fichier /Components/Elasticsearch/SearchBar.razor, ajoutez ce qui suit :
@using System.Threading.Tasks
<form @onsubmit="SubmitSearch">
<div class="input-group mb-3">
<input type="text" @bind-value="searchTerm" class="form-control" placeholder="Enter search term..." />
<button type="submit" class="btn btn-primary input-btn">
<span class="input-group-svg">
Search
</span>
</button>
</div>
</form>
@code {
[Parameter]
public EventCallback<string> OnSearch { get; set; }
private string searchTerm = "";
private async Task SubmitSearch()
{
await OnSearch.InvokeAsync(searchTerm);
}
}Ce composant contient une barre de recherche et un bouton pour effectuer la recherche.
Blazor offre une grande flexibilité en permettant de générer du HTML dynamiquement en utilisant du code C# dans le même fichier.
Ensuite, dans le fichier /Components/Elasticsearch/Results.razor, nous construirons le composant de résultats qui affichera les résultats de la recherche :
@using BlazorApp.Models
@if (SearchResults != null && SearchResults.Any())
{
<div class="row">
@foreach (var result in SearchResults)
{
<div class="col-12 mb-3">
<div class="card">
<div class="row g-0">
<div class="col-md-3 image-container">
@if (!string.IsNullOrEmpty(result?.ThumbnailUrl))
{
<img src="@result?.ThumbnailUrl" class="img-fluid rounded-start" alt="Thumbnail">
}
else
{
<div class="placeholder">
@result?.Title
</div>
}
</div>
<div class="col-md-9"> <!-- Adjusted to use the remaining 75% -->
<div class="card-body">
<h4 class="card-title">
@result?.Title
</h4>
<div class="details-container">
<div class="">
@if (result?.Authors?.Any() == true)
{
<p class="card-text p-first">
Authors: <small class="text-muted">@string.Join(", ", result.Authors)</small>
</p>
}
@if (result?.Categories?.Any() == true)
{
<p class="card-text p-second">
Categories: <small class="text-muted">@string.Join(", ", result.Categories)</small>
</p>
}
</div>
<div class="numPages-status">
@if (result?.PageCount != null)
{
<p class="card-text p-first">
Pages: <small class="text-muted">@result.PageCount</small>
</p>
}
@if (result?.Status != null)
{
<p class="card-text p-second">
Status: <small class="text-muted">@result.Status</small>
</p>
}
</div>
</div>
<div class="long-text-container">
<p class="card-text"><small class="text-muted">@result?.LongDescription?.Text</small></p>
</div>
@if (!string.IsNullOrEmpty(result?.PublishedDate))
{
<div class="date-container">
<p class="card-text">
Published Date: <small class="text-muted small-date">@FormatDate(result.PublishedDate)</small>
</p>
</div>
}
</div>
</div>
</div>
</div>
</div>
}
</div>
}
else if (SearchResults != null)
{
<p>No results found.</p>
}
@code {
[Parameter]
public List<BookDoc> SearchResults { get; set; } = new List<BookDoc>();
private string FormatDate(string? date)
{
if (DateTime.TryParse(date, out DateTime parsedDate))
{
return parsedDate.ToString("MMMM dd, yyyy");
}
return "";
}
}Enfin, nous devons créer des facettes pour filtrer les résultats de la recherche.
Remarque : les facettes sont des filtres qui permettent aux utilisateurs d'affiner les résultats de leur recherche en fonction d'attributs ou de catégories spécifiques, tels que le type de produit, la fourchette de prix ou la marque. Ces filtres sont généralement présentés sous forme d'options cliquables, souvent sous forme de cases à cocher, afin d'aider les utilisateurs à affiner leur recherche et à trouver plus facilement des résultats pertinents. Dans le contexte d'Elasticsearch, les facettes sont créées à l'aide d' agrégations.
Nous configurons les facettes en plaçant le code suivant dans le fichier /Components/Elasticsearch/Facet.razor:
@if (Facets != null)
{
<div class="facets-container">
@foreach (var facet in Facets)
{
<h3>@facet.Key</h3>
@foreach (var option in facet.Value)
{
<div>
<input type="checkbox" checked="@IsFacetSelected(facet.Key, option.Key)"
@onclick="() => ToggleFacet(facet.Key, option.Key)" />
@option.Key (@option.Value)
</div>
}
}
</div>
}
@code {
[Parameter]
public Dictionary<string, Dictionary<string, long>>? Facets { get; set; }
[Parameter]
public EventCallback<Dictionary<string, List<string>>> OnFacetChanged { get; set; }
private Dictionary<string, List<string>> selectedFacets = new();
private void ToggleFacet(string facetName, string facetValue)
{
if (!selectedFacets.TryGetValue(facetName, out var facetValues))
{
facetValues = selectedFacets[facetName] = new List<string>();
}
if (!facetValues.Remove(facetValue))
{
facetValues.Add(facetValue);
}
OnFacetChanged.InvokeAsync(selectedFacets);
}
private bool IsFacetSelected(string facetName, string facetValue)
{
return selectedFacets.ContainsKey(facetName) && selectedFacets[facetName].Contains(facetValue);
}
}Ce composant lit une agrégation terms sur les champs author, categories et status, puis produit une liste de filtres à renvoyer à Elasticsearch.
Mettons tout cela bout à bout.
Dans le fichier /Components/Pages/Search.razor:
@page "/"
@rendermode InteractiveServer
@using BlazorApp.Models
@using BlazorApp.Services
@inject ElasticsearchService ElasticsearchService
@inject ILogger<Search> Logger
<PageTitle>Search</PageTitle>
<div class="top-row px-4 ">
<div class="searchbar-container">
<h4>Semantic Search with Elasticsearch and Blazor</h4>
<SearchBar OnSearch="PerformSearch" />
</div>
<a href="https://www.elastic.co/search-labs/esre-with-blazor" target="_blank">About</a>
</div>
<div class="px-4">
<div class="search-details-container">
<p role="status">Current search term: @currentSearchTerm</p>
<p role="status">Total results: @totalResults</p>
</div>
<div class="results-facet-container">
<div class="facets-container">
<Facet Facets="facets" OnFacetChanged="OnFacetChanged" />
</div>
<div class="results-container">
<Results SearchResults="searchResults" />
</div>
</div>
</div>
@code {
private string currentSearchTerm = "";
private long totalResults = 0;
private List<BookDoc> searchResults = new List<BookDoc>();
private Dictionary<string, Dictionary<string, long>> facets = new Dictionary<string, Dictionary<string, long>>();
private Dictionary<string, List<string>> selectedFacets = new Dictionary<string, List<string>>();
protected override async Task OnInitializedAsync()
{
await PerformSearch();
}
private async Task PerformSearch(string searchTerm = "")
{
try
{
currentSearchTerm = searchTerm;
var response = await ElasticsearchService.SearchBooksAsync(currentSearchTerm, selectedFacets);
if (response != null)
{
searchResults = response.Documents;
facets = response.Facets;
totalResults = response.TotalHits;
}
else
{
Logger.LogWarning("Search response is null.");
}
StateHasChanged();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error performing search.");
}
}
private async Task OnFacetChanged(Dictionary<string, List<string>> newSelectedFacets)
{
selectedFacets = newSelectedFacets;
await PerformSearch(currentSearchTerm);
}
}Notre page fonctionne !

Comme vous pouvez le constater, la page est fonctionnelle mais manque de style. Ajoutons quelques feuilles de style CSS pour le rendre plus organisé et plus réactif.
Commençons par remplacer les styles de présentation. Dans le fichier Components/Layout/MainLayout.razor.css:
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}Ajoutez les styles pour la page de recherche dans le fichier Components/Pages/Search.razor.css:
.input-group .input-group-svg {
background: transparent;
border: transparent;
pointer-events: none;
}
.results-facet-container {
display: flex;
margin-top: 1rem;
overflow-x: auto;
}
.search-details-container {
display: flex;
justify-content: space-between;
margin-top: 1rem;
}
.searchbar-container {
padding-top: 2rem;
display: flex;
flex-direction: column;
flex-grow: 1;
height: 100%;
max-width: 100%;
}
.searchbar-container h4 {
margin: 0;
}
.top-row {
margin-top: -1.1rem;
position: relative;
background-color: hsl(216, 29%, 67%);
border-bottom: 1px solid #d6d5d5;
display: flex;
align-items: center;
height: 100%;
padding: 0 1rem;
}
.top-row a {
margin-left: auto;
margin-top: -4rem;
color: #000000;
text-decoration: none;
}
.top-row a:hover {
text-decoration: underline;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a,
.top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row,
article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}Notre page commence à s'améliorer :

Mettons-y les dernières touches :
Créez les fichiers suivants :
Composants/Elasticsearch/Facet.razor.css
Composants/Elasticsearch/Results.razor.css
Et ajoutez les styles pour Facet.razor.css:
.facets-container {
font-size: 15px;
margin-right: 4rem;
overflow-x: auto;
white-space: nowrap;
max-width: 300px;
}
.results-facet-container {
display: flex;
margin-top: 1rem;
overflow-x: auto;
}
.results-facet-container > * {
flex-shrink: 0;
}Pour Results.razor.css:
.image-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 1rem;
box-sizing: border-box;
}
.image-container img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
}
.placeholder {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
background-color: #f0f0f0;
border: 1px solid #ccc;
font-size: 0.9rem;
color: #888;
text-align: center;
padding: 1rem;
border-radius: 0.5rem;
}
.card-body {
padding: 1rem;
}
.details-container {
display: flex;
justify-content: space-between;
padding: 1.5rem 0;
}
.date-container {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
}
.date-container .small-date {
font-weight: bold;
}Résultat final :

Pour lancer l'application, vous pouvez utiliser la commande suivante :
dotnet watch
Vous avez réussi ! Vous pouvez désormais rechercher des livres dans votre index Elasticsearch en utilisant la barre de recherche et filtrer les résultats par auteur, catégorie et statut.
Effectuer une recherche plein texte et sémantique
Par défaut, notre application effectuera une recherche hybride utilisant à la fois le texte intégral et la recherche sémantique. Vous pouvez modifier la logique de recherche en créant deux méthodes distinctes, l'une pour le texte intégral et l'autre pour la recherche sémantique, puis en sélectionnant une méthode pour élaborer la requête en fonction des données saisies par l'utilisateur.
Ajoutez les méthodes suivantes à la classe ElasticsearchService dans le fichier /Services/ElasticsearchService.cs:
private static Action<QueryDescriptor<BookDoc>> BuildSemanticQuery(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
var filters = BuildFilters(selectedFacets);
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 = BuildFilters(selectedFacets);
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())
);
}Ces deux méthodes fonctionnent de manière similaire à la méthode BuildHybridQuery, mais elles n'effectuent qu'une recherche plein texte ou sémantique.
Vous pouvez modifier la méthode SearchBooksAsync pour utiliser la méthode de recherche sélectionnée :
public async Task<ElasticResponse> SearchBooksAsync(
string searchTerm,
Dictionary<string, List<string>> selectedFacets
)
{
try
{
_logger.LogInformation($"Performing search for: {searchTerm}");
// Modify the query builder to use the selected search method.
var multiMatchQuery = BuildMultiMatchQuery(searchTerm, selectedFacets); // For full text search
var semanticQuery = BuildSemanticQuery(searchTerm, selectedFacets); // For semantic search
// In this case we will not use retrievers, but you can add them if you want to use them.
var response = await _client.SearchAsync<BookDoc>(s =>
s.Index("elastic-blazor-books")
.Query(multiMatchQuery) // Change this line to use different search methods, for example: .Query(semanticQuery) for semantic search
.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();
}
}Vous pouvez trouver le formulaire complet ici
Conclusion
Blazor est un framework efficace qui vous permet de construire des applications web en utilisant C#. Elasticsearch est un moteur de recherche puissant qui vous permet de créer des applications de recherche. En combinant les deux, vous pouvez facilement créer des applications de recherche robustes, en tirant parti de la puissance d'ESRE pour créer une expérience de recherche sémantique en peu de temps.
Questions fréquentes
Qu'est-ce que Blazor ?
Blazor est un framework web open source basé sur HTML, CSS et C# créé par Microsoft pour permettre aux développeurs de créer des applications web qui s'exécutent sur le client ou le serveur. Il permet également de créer des composants réutilisables pour construire des applications et plus encore.
Quels sont les avantages de l'utilisation de Blazor ?
Blazor offre plusieurs avantages par rapport à d'autres cadres et bibliothèques : il permet aux développeurs d'utiliser le langage C# pour le code client et le code serveur, il s'intègre de manière transparente à l'écosystème .NET et offre un support de débogage robuste.




