Crear una aplicación de búsqueda con Blazor y Elasticsearch
Aprende a construir una aplicación de búsqueda usando Blazor y Elasticsearch, y cómo emplear el cliente Elasticsearch .NET para búsqueda híbrida.
En este artículo, aprenderás cómo aprovechar tus habilidades en C# para crear una aplicación de búsqueda usando Blazor y Elasticsearch. Vamos a usar el cliente Elasticsearch .NET para ejecutar consultas de texto completo, semánticas y búsquedas híbridas .
NOTA Si conoces la versión anterior del cliente Nest de Elasticsearch en C#, lee esta entrada del blog sobre la deprecación del cliente NEST y las nuevas funciones. NEST fue la generación anterior del cliente .NET que fue reemplazado por el Elastic.Clients.Elasticsearch package

¿Qué es Blazor?

Imagen: ASP. NETCoreBlazorhostingmodels
Blazor es un framework sitio web de código abierto basado en HTML, CSS y C# creado por Microsoft para permitir a los desarrolladores crear aplicaciones sitio web que se ejecuten en el cliente o en el servidor. Blazor también permite crear componentes reutilizables para construir aplicaciones más rápido; permite a los desarrolladores construir la vista HTML y las acciones en C# dentro del mismo archivo, lo que ayuda a mantener un código legible y limpio. Además, con Blazor Hybrid puedes crear aplicaciones móviles nativas accediendo a las capacidades nativas de la plataforma mediante código .NET.
Algunas de las características que hacen de Blazor un excelente marco para trabajar:
Opciones de renderizado tanto en el lado servidor como en el cliente
Componentes reutilizables de la interfaz
Actualizaciones en tiempo real con SignalR
Gestión del estado integrada
Sistema de enrutamiento integrado
Comprobaciones fuertes de tipado y tiempo de compilación
¿Por qué Blazor?
Blazor ofrece varios beneficios sobre otros frameworks y librerías: permite a los desarrolladores usar C# tanto para el código cliente como para el servidor, proporcionando una fuerte verificación de tipado y compilación que mejora la fiabilidad. Se integra perfectamente con el ecosistema .NET, permitiendo la reutilización de librerías y herramientas .NET, y ofrece un soporte robusto para depuración.
¿Qué es ESRE?
El Elasticsearch Relevance Engine™ (ESRE) es un conjunto de herramientas para construir aplicaciones de búsqueda empleando aprendizaje automático e inteligencia artificial sobre el poderoso motor de búsqueda Elasticsearch.

Para saber más sobre ESRE, puedes leer nuestra interesante entrada de blog aquí
Configuración de ELSER
Para aprovechar las capacidades ESRE de Elastic, vamos a emplear ELSER como nuestro proveedor de modelos.
Nota: para usar los modelos ELSER de Elasticsearch, debes tener una licencia Platinum o Enterprise, y disponer de un nodo mínimo dedicado de Machine Lerning (ML) de 4GB de tamaño. Lee más sobre esto aquí.
Empieza creando el punto final de inferencia:
PUT _inference/sparse_embedding/my-elser-model
{
"service": "elser",
"service_settings": {
"num_allocations": 1,
"num_threads": 1
}
}Si es la primera vez que usas ELSER, puede que te encuentres con un error 502 de Mal Gateway mientras el modelo se carga en segundo plano. Puedes consultar el estado del modelo en Machine Learning > Trained Models en Kibana. Una vez desplegado, puedes pasar al siguiente paso.

Datos de indexación
Puedes descargar el conjunto de datos aquí y luego importar los datos usando Kibana. Para ello, ve a la página principal y haz clic en "Subir datos". Luego, sube el archivo y haz clic Import. Por último, ve a la pestaña Advanced y pega los siguientes mapeos:
{
"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"
}
}
}
Vamos a crear un índice capaz de ejecutar consultas semánticas y de texto completo. El tipo de campo semantic_text se encargará del fragmento de datos y la incrustación. Ten en cuenta que estamos indexando longDescription como semantic_text, puedes usar copy_to si quieres indexar un campo tanto como semantic_text como texto'.
Construcción de la app con Blazor & Elasticsearch
Clave API
Lo primero que tenemos que hacer es crear una clave API para autenticar nuestras solicitudes a Elasticsearch. La clave API debe ser de solo lectura y solo permitir consultar el índice de books-blazor .
POST /_security/api_key
{
"name": "books-blazor-key",
"role_descriptors": {
"books-blazor-reader": {
"indices": [
{
"names": ["books-blazor"],
"privileges": ["read"]
}
]
}
}
}Verás algo así:
{
"id": "XXXXXXXXXXXXXXXXXXXXXXXX",
"name": "books-blazor-key",
"api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}Almacena el valor del campo de respuesta encoded cuando sea necesario más adelante. Si usas Elastic Cloud, también necesitarás tu ID de la nube. (Puedes encontrarlo aquí).
Creación del proyecto Blazor
Empieza instalando Blazor y creando un proyecto de muestra siguiendo las instrucciones oficiales.

Una vez que tengas el proyecto creado, la estructura de carpetas y los archivos deberían ver así:
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/La aplicación plantilla incluye Bootstrap v5.1.0 para peinar.
Termina la configuración del proyecto instalando el cliente Elasticsearch .NET :
dotnet add package Elastic.Clients.ElasticsearchUna vez que completes este paso, tu página debería ver así:

Estructura de carpetas
Ahora, vamos a organizar nuestras carpetas de la siguiente manera:
BlazorApp/
|-- Components/
| |-- Pages/
| | |-- Search.razor
| | `-- Search.razor.css
| `-- Elasticsearch/
| |-- SearchBar.razor
| |-- Results.razor
| `-- Facet.razor
|-- Models/
| |-- Book.cs
| `-- Response.cs
`-- Services/
`-- ElasticsearchService.csArchivos explicados:
Components/Pages/Search.razor: página principal que contiene la barra de búsqueda, los resultados y los filtros.
Componentes/Páginas/Search.razor.css: estilos de página.
Components/Elasticsearch/SearchBar.razor: componente de la barra de búsqueda.
Components/Elasticsearch/Results.razor: componente de resultados.
Components/Elasticsearch/Facet.razor: componente de filtros.
Components/Svg/GlassIcon.razor: icono de búsqueda.
Components/_Imports.razor: esto importará todos los componentes.
Modelos/Book.cs: esto almacenará el esquema del campo libro.
Modelos/Response.cs: esto almacenará el esquema de respuesta, incluyendo los resultados de búsqueda, facetas y resultados totales.
Servicios/ElasticsearchService.cs: Servicio Elasticsearch. Se encargará de la conexión y las consultas a Elasticsearch.
Configuración inicial
Empecemos con una limpieza.
Elimina los archivos:
Componentes/Páginas/Counter.razor
Componentes/Páginas/Weather.razor
Componentes/Páginas/Home.razor
Componentes/Layout/NavMenu.razor
Componentes/Diseño/NavMenu.razor.css
Revisa el archivo /Components/_Imports.razor . Deberías tener las siguientes importaciones:
@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.ComponentsIntegración de Elastic en el proyecto
Ahora, importemos los componentes de 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 *@Vamos a eliminar la barra lateral predeterminada para tener más espacio para nuestra aplicación eliminándola del archivo /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>
Ahora introduzcamos las credenciales de Elasticsearch para los secretos de usuario:
dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"Con este enfoque, .Net 8 almacena datos sensibles en una ubicación separada, fuera de la carpeta del proyecto, y los hace accesibles mediante la interfaz IConfiguration . Estas variables estarán disponibles para cualquier proyecto .Net que emplee los mismos secretos de usuario.
Luego, modifiquemos el archivo Program.cs para leer los secretos y montar el cliente Elasticsearch:
Primero, importa las bibliotecas necesarias:
using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;BlazorApp.Services: contiene el servicio Elasticsearch.
Elastic.Clients.Elasticsearch: importa la biblioteca cliente .Net 8 de Elasticsearch.
Elastic.Transport: importa la biblioteca de transporte Elasticsearch, que nos permite usar la clase ApiKey para autenticar nuestras solicitudes.
Segundo, inserta el siguiente código antes de la línea 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);
});Este código leerá las credenciales de Elasticsearch de los secretos de usuario y creará una instancia cliente de Elasticsearch.
Tras la inicialización del cliente ElasticSearch, agrega la siguiente línea para registrar el servicio Elasticsearch:
builder.Services.AddScoped<ElasticsearchService>();El siguiente paso será construir la lógica de búsqueda en el archivo /Services/ElasticsearchService.cs :
Primero, importa las bibliotecas y modelos necesarios:
using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;Segundo, agregar la clase ElasticsearchService, constructor y 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;
}
}
}Configuración de búsqueda
Ahora, vamos a construir nuestra lógica de búsqueda:
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;
}BuildFiltersconstruirá los filtros para la consulta de búsqueda usando las facetas seleccionadas por el usuario.BuildHybridQueryconstruirá una consulta de búsqueda híbrida que combine texto completo y búsqueda semántica.
A continuación, agrega el método de búsqueda:
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: realizará la búsqueda usando la consulta híbrida y devolverá los resultados incluidas agregaciones para construir las facetas.FormatFacets: formateará la respuesta de agregación en un diccionario.ConvertFacetDictionary: convertirá el diccionario de facetas en un formato más legible.
El siguiente paso es crear los modelos que representarán los datos devueltos en el hits de la consulta de Elasticsearch que se imprimirán como resultados en nuestra página de búsqueda.
Comenzamos creando el /Models/Book.cs de archivo y agregando lo siguiente:
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; }
}
}Luego, configurar la respuesta Elastic en el archivo /Models/Response.cs y agregar lo siguiente:
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; }
}
}Configuración de una interfaz básica
A continuación, agrega el componente Barra de búsqueda. En el archivo /Components/Elasticsearch/SearchBar.razor y agrega lo siguiente:
@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);
}
}Este componente contiene una barra de búsqueda y un botón para realizar la búsqueda.
Blazor ofrece una gran flexibilidad al permitir generar HTML dinámicamente usando código C# dentro del mismo archivo.
Después, en el archivo /Components/Elasticsearch/Results.razor construiremos el componente de resultados que mostrará los resultados de búsqueda:
@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 "";
}
}Finalmente, necesitaremos crear facetas para filtrar los resultados de búsqueda.
Nota: Los facetos son filtros que permiten a los usuarios reducir los resultados de búsqueda en función de atributos o categorías específicas, como el tipo de producto, el rango de precio o la marca. Estos filtros suelen presentar como opciones clicables, a menudo en forma de casillas para marcar, para ayudar a los usuarios a refinar su búsqueda y encontrar resultados relevantes con mayor facilidad. En el contexto de Elasticsearch, las facetas se crean mediante agregaciones.
Configuramos facetas poniendo el siguiente código en el archivo /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);
}
}Este componente lee de una agregación terms en los campos author, categoriesy status , y luego produce una lista de filtros para enviar de vuelta a Elasticsearch.
Ahora, vamos a juntar todo.
En /Components/Pages/Search.razor expediente:
@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);
}
}¡Nuestra página funciona!

Como puedes ver, la página es funcional pero carece de estilos. Vamos a agregar algo de CSS para que parezca más organizado y receptivo.
Empecemos a reemplazar los estilos de distribución. En el archivo 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;
}Agrega los estilos de la página de búsqueda en el archivo 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;
}
}Nuestra página empieza a ver mejor:

Vamos a darle los toques finales:
Crea los siguientes archivos:
Componentes/Elasticsearch/Facet.razor.css
Components/Elasticsearch/Results.razor.css
Y agrega los estilos para 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;
}Para 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;
}Resultado final:

Para ejecutar la aplicación puedes usar el siguiente comando:
dotnet watch
¡Lo conseguiste! Ahora puedes buscar libros en tu índice de Elasticsearch usando la barra de búsqueda y filtrar los resultados por autor, categoría y estado.
Realización de búsqueda en texto completo y semántica
Por defecto, nuestra app realiza una búsqueda híbrida usando tanto texto completo como búsqueda semántica. Puedes cambiar la lógica de búsqueda creando dos métodos separados, uno para texto completo y otro para búsqueda semántica, y luego seleccionar uno para construir la consulta basar en la entrada del usuario.
Agrega los siguientes métodos a la clase ElasticsearchService en el archivo /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())
);
}Ambos métodos funcionan de forma similar al método BuildHybridQuery , pero solo realizan búsqueda en texto completo o semántica.
Puedes modificar el método SearchBooksAsync para usar el método de búsqueda seleccionado:
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();
}
}Puedes encontrar la solicitud completa aquí
Conclusión
Blazor es un framework eficaz que te permite crear aplicaciones sitio web usando C#. Elasticsearch es un poderoso motor de búsqueda que te permite crear aplicaciones de búsqueda. Combinando ambos, puedes construir fácilmente aplicaciones de búsqueda robustas, aprovechando el poder de ESRE para crear una experiencia de búsqueda semántica en poco tiempo.
Preguntas frecuentes
¿Qué es Blazor?
Blazor es un framework sitio web de código abierto basado en HTML, CSS y C# creado por Microsoft para permitir a los desarrolladores crear aplicaciones sitio web que se ejecuten en el cliente o en el servidor. También permite crear componentes reutilizables para construir aplicaciones y más.
¿Cuáles son los beneficios de usar Blazor?
Blazor ofrece varios beneficios sobre otros frameworks y librerías: permite a los desarrolladores usar C# para código de cliente y servidor, se integra perfectamente con el ecosistema .NET y ofrece un soporte robusto para depuración.




