Criando um app de buscas com o Blazor e o Elasticsearch
Aprenda como criar um aplicativo de busca usando Blazor e Elasticsearch, e como usar o cliente Elasticsearch .NET para busca híbrida.
Neste artigo, você aprenderá como aproveitar suas habilidades em C# para criar um aplicativo de busca usando Blazor e Elasticsearch. Vamos usar o cliente Elasticsearch .NET para executar consultas de pesquisa de texto completo, semântica e híbrida .
NOTA: Se você estiver familiarizado com a versão antiga do cliente C# do Elasticsearch, o NEST, leia esta postagem do blog sobre a descontinuação do cliente NEST e seus novos recursos. O NEST era a geração anterior do cliente .NET, que foi substituído pelo Elastic.Clients.Elasticsearch package

O que é Blazor?

Imagem: ASP.NETCoreBlazorhostingmodels
Blazor é um framework web de código aberto baseado em HTML, CSS e C#, criado pela Microsoft para permitir que desenvolvedores criem aplicações web que rodam tanto no cliente quanto no servidor. O Blazor também permite criar componentes reutilizáveis para desenvolver aplicações mais rapidamente; ele possibilita que os desenvolvedores criem a visualização HTML e as ações em C# no mesmo arquivo, o que ajuda a manter um código legível e limpo. Além disso, com o Blazor Hybrid, você pode criar aplicativos móveis nativos acessando os recursos nativos da plataforma por meio de código .NET.
Algumas das características que fazem do Blazor uma ótima estrutura para se trabalhar:
Opções de renderização do lado do servidor e do lado do cliente
Componentes de interface do usuário reutilizáveis
Atualizações em tempo real com SignalR
Gerenciamento de estado integrado
Sistema de roteamento integrado
Tipagem forte e verificações em tempo de compilação
Por que Blazor?
O Blazor oferece diversas vantagens em relação a outros frameworks e bibliotecas: permite que os desenvolvedores usem C# tanto para o código do cliente quanto para o código do servidor, fornecendo tipagem forte e verificação em tempo de compilação, o que aumenta a confiabilidade. Ele se integra perfeitamente ao ecossistema .NET, permitindo a reutilização de bibliotecas e ferramentas .NET, e oferece suporte robusto para depuração.
O que é ESRE?
O Elasticsearch Relevance Engine ™ (ESRE) é um conjunto de ferramentas para criar aplicações de busca utilizando aprendizado de máquina e inteligência artificial sobre o poderoso mecanismo de busca Elasticsearch.

Para saber mais sobre ESRE, você pode ler nossa postagem esclarecedora no blog, que está disponível aqui.
Configurando o ELSER
Para aproveitar os recursos do ESRE da Elastic, usaremos o ELSER como nosso provedor de modelos.
Para usar os modelos ELSER do Elasticsearch, você precisa de uma licença Platinum ou Enterprise e de um nó dedicado para Machine Learning (ML) com no mínimo 4 GB de espaço. Saiba mais aqui.
Comece criando o ponto de extremidade de inferência:
PUT _inference/sparse_embedding/my-elser-model
{
"service": "elser",
"service_settings": {
"num_allocations": 1,
"num_threads": 1
}
}Se esta for a sua primeira vez usando o ELSER, você poderá encontrar um erro 502 Bad Gateway enquanto o modelo é carregado em segundo plano. Você pode verificar o status do modelo em Machine Learning > Trained Models no Kibana. Após a implantação, você poderá prosseguir para a próxima etapa.

Dados de indexação
Você pode baixar o conjunto de dados aqui e, em seguida, importar os dados usando o Kibana. Para isso, acesse a página inicial e clique em "Carregar dados". Em seguida, carregue o arquivo e clique em Import. Por fim, acesse a aba Advanced e cole os seguintes mapeamentos:
{
"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 criar um índice capaz de executar consultas semânticas e de texto completo. O tipo de campo semantic_text cuidará do agrupamento e da incorporação dos dados. Observe que estamos indexando longDescription como semantic_text, você pode usar copy_to se quiser indexar um campo como semantic_text e text`.
Construindo o aplicativo com Blazor e Elasticsearch
Chave de API
A primeira coisa que precisamos fazer é criar uma chave de API para autenticar nossas solicitações ao Elasticsearch. A chave da API deve ser somente leitura e permitir apenas consultas ao índice books-blazor .
POST /_security/api_key
{
"name": "books-blazor-key",
"role_descriptors": {
"books-blazor-reader": {
"indices": [
{
"names": ["books-blazor"],
"privileges": ["read"]
}
]
}
}
}Você verá algo parecido com isto:
{
"id": "XXXXXXXXXXXXXXXXXXXXXXXX",
"name": "books-blazor-key",
"api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}Salve o valor do campo de resposta encoded , pois ele será necessário posteriormente. Se você estiver usando o Elastic Cloud, também precisará do seu ID da nuvem. (Você pode encontrá-lo aqui).
Criando o projeto Blazor
Comece instalando o Blazor e criando um projeto de exemplo seguindo as instruções oficiais.

Após a criação do projeto, a estrutura de pastas e arquivos deve ser semelhante a esta:
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/O aplicativo modelo inclui o Bootstrap v5.1.0. para estilização.
Finalize a configuração do projeto instalando o cliente Elasticsearch .NET :
dotnet add package Elastic.Clients.ElasticsearchApós concluir esta etapa, sua página deverá ter esta aparência:

Estrutura de pastas
Agora, vamos organizar nossas pastas da seguinte forma:
BlazorApp/
|-- Components/
| |-- Pages/
| | |-- Search.razor
| | `-- Search.razor.css
| `-- Elasticsearch/
| |-- SearchBar.razor
| |-- Results.razor
| `-- Facet.razor
|-- Models/
| |-- Book.cs
| `-- Response.cs
`-- Services/
`-- ElasticsearchService.csExplicação dos arquivos:
Components/Pages/Search.razor: página principal contendo a barra de pesquisa, os resultados e os filtros.
Components/Pages/Search.razor.css: estilos de página.
Components/Elasticsearch/SearchBar.razor: componente da barra de pesquisa.
Components/Elasticsearch/Results.razor: componente de resultados.
Components/Elasticsearch/Facet.razor: componente de filtros.
Components/Svg/GlassIcon.razor: ícone de pesquisa.
Components/_Imports.razor: este arquivo importará todos os componentes.
Models/Book.cs: este arquivo armazenará o esquema do campo do livro.
Models/Response.cs: este arquivo armazenará o esquema de resposta, incluindo os resultados da pesquisa, as facetas e o total de resultados.
Services/ElasticsearchService.cs: Serviço Elasticsearch. Ele será responsável pela conexão e pelas consultas ao Elasticsearch.
Configuração inicial
Vamos começar com uma limpeza.
Apague os arquivos:
Components/Pages/Counter.razor
Componentes/Páginas/Weather.razor
Componentes/Páginas/Home.razor
Components/Layout/NavMenu.razor
Components/Layout/NavMenu.razor.css
Verifique o arquivo /Components/_Imports.razor . Você deve ter as seguintes importações:
@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.ComponentsIntegrando o Elastic ao projeto
Agora, vamos importar os componentes do 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 remover a barra lateral padrão para termos mais espaço para nossa aplicação, removendo-a do arquivo /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>
Agora vamos inserir as credenciais do Elasticsearch para os segredos do usuário:
dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"Usando esta abordagem, o .Net 8 armazena dados sensíveis em um local separado, fora da pasta do projeto e os torna acessíveis usando a interface IConfiguration . Essas variáveis estarão disponíveis para qualquer projeto .NET que utilize os mesmos segredos de usuário.
Em seguida, vamos modificar o arquivo Program.cs para ler os segredos e montar o cliente Elasticsearch:
Primeiro, importe as bibliotecas necessárias:
using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;BlazorApp.Services: contém o serviço Elasticsearch.
Elastic.Clients.Elasticsearch: importa a biblioteca cliente Elasticsearch para .NET 8.
Elastic.Transport: importa a biblioteca de transporte do Elasticsearch, que nos permite usar a classe ApiKey para autenticar nossas solicitações.
Em segundo lugar, insira o seguinte código antes da linha 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 lerá as credenciais do Elasticsearch a partir dos segredos do usuário e criará uma instância de cliente Elasticsearch.
Após a inicialização do cliente Elasticsearch, adicione a seguinte linha para registrar o serviço Elasticsearch:
builder.Services.AddScoped<ElasticsearchService>();O próximo passo será construir a lógica de busca no arquivo /Services/ElasticsearchService.cs :
Primeiro, importe as bibliotecas e os modelos necessários:
using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;Em segundo lugar, adicione a classe ElasticsearchService, o construtor e as variáveis:
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;
}
}
}Configurando a pesquisa
Agora, vamos construir nossa lógica de busca:
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;
}BuildFiltersirá construir os filtros para a consulta de pesquisa usando as facetas selecionadas pelo usuário.BuildHybridQueryirá construir uma consulta de pesquisa híbrida que combina pesquisa de texto completo e pesquisa semântica.
Em seguida, adicione o método de pesquisa:
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;
}SearchBooksAsyncA função realizará a busca usando a consulta híbrida e retornará os resultados, incluindo as agregações para a construção das facetas.FormatFacets: irá formatar a resposta de agregações em um dicionário.ConvertFacetDictionary: irá converter o dicionário de facetas em um formato mais legível.
O próximo passo é criar os modelos que representarão os dados retornados no hits da consulta Elasticsearch que serão impressos como resultados em nossa página de pesquisa.
Começamos criando o arquivo /Models/Book.cs e adicionando o seguinte:
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; }
}
}Em seguida, configure a resposta do Elastic no arquivo /Models/Response.cs e adicione o seguinte:
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; }
}
}Configurando uma interface de usuário básica
Em seguida, adicione o componente SearchBar. No arquivo /Components/Elasticsearch/SearchBar.razor , adicione o seguinte:
@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 contém uma barra de pesquisa e um botão para realizar a pesquisa.
O Blazor oferece grande flexibilidade ao permitir a geração dinâmica de HTML usando código C# dentro do mesmo arquivo.
Em seguida, no arquivo /Components/Elasticsearch/Results.razor construiremos o componente de resultados que exibirá os resultados da pesquisa:
@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 "";
}
}Por fim, precisaremos criar facetas para filtrar os resultados da pesquisa.
Nota: Os facets são filtros que permitem aos usuários refinar os resultados da pesquisa com base em atributos ou categorias específicos, como tipo de produto, faixa de preço ou marca. Esses filtros geralmente são apresentados como opções clicáveis, frequentemente na forma de caixas de seleção, para ajudar os usuários a refinar sua pesquisa e encontrar resultados relevantes com mais facilidade. No contexto do Elasticsearch, os facets são criados usando agregações.
Configuramos as facetas inserindo o seguinte código no arquivo /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 lê de uma agregação terms nos campos author, categories e status e, em seguida, produz uma lista de filtros para enviar de volta ao Elasticsearch.
Agora, vamos juntar tudo.
No arquivo /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);
}
}Nossa página está funcionando!

Como você pode ver, a página é funcional, mas carece de estilo. Vamos adicionar um pouco de CSS para deixar a aparência mais organizada e responsiva.
Vamos começar a substituir os estilos de layout. No arquivo 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;
}Adicione os estilos para a página de pesquisa no arquivo 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;
}
}Nossa página está começando a ficar melhor:

Vamos dar os toques finais:
Crie os seguintes arquivos:
Components/Elasticsearch/Facet.razor.css
Components/Elasticsearch/Results.razor.css
E adicione os 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 executar o aplicativo, você pode usar o seguinte comando:
dotnet watch
Você conseguiu! Agora você pode pesquisar livros em seu índice Elasticsearch usando a barra de pesquisa e filtrar os resultados por autor, categoria e status.
Realização de buscas semânticas e de texto completo
Por padrão, nosso aplicativo realizará uma pesquisa híbrida usando tanto pesquisa de texto completo quanto pesquisa semântica. Você pode alterar a lógica de pesquisa criando dois métodos separados, um para pesquisa de texto completo e outro para pesquisa semântica, e então selecionando um dos métodos para construir a consulta com base na entrada do usuário.
Adicione os seguintes métodos à classe ElasticsearchService no arquivo /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 os métodos funcionam de forma semelhante ao método BuildHybridQuery , mas realizam apenas pesquisa de texto completo ou pesquisa semântica.
Você pode modificar o método SearchBooksAsync para usar o método de pesquisa selecionado:
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();
}
}Você pode encontrar o formulário de inscrição completo aqui.
Conclusão
Blazor é uma estrutura eficaz que permite criar aplicações web usando C#. O Elasticsearch é um mecanismo de busca poderoso que permite criar aplicativos de busca. Combinando ambos, você pode facilmente criar aplicativos de busca robustos, aproveitando o poder do ESRE para criar uma experiência de busca semântica em pouco tempo.
Perguntas frequentes
O que é Blazor?
Blazor é um framework web de código aberto baseado em HTML, CSS e C#, criado pela Microsoft para permitir que desenvolvedores criem aplicações web que rodam tanto no cliente quanto no servidor. Isso também permite criar componentes reutilizáveis para construir aplicativos e muito mais.
Quais são as vantagens de usar o Blazor?
O Blazor oferece diversas vantagens em relação a outros frameworks e bibliotecas: permite que os desenvolvedores usem C# tanto para o código do cliente quanto para o do servidor, integra-se perfeitamente ao ecossistema .NET e oferece um suporte robusto para depuração.




