使用 Blazor 和 Elasticsearch 构建搜索应用
了解如何使用 Blazor 和 Elasticsearch 构建搜索应用程序,以及如何使用 Elasticsearch .NET 客户端进行混合搜索。
在本文中,您将学习如何利用 C# 技能使用 Blazor 和 Elasticsearch 构建搜索应用程序。我们将使用Elasticsearch .NET客户端运行全文、语义和混合搜索查询。
注意如果您熟悉旧版本的 Elasticsearch C# 客户端NEST,请阅读这篇关于 NEST 客户端弃用和新功能的博文。NEST 是上一代的 .NET 客户端,后来被 Elastic.Clients.Elasticsearch package

什么是 Blazor?

Blazor是微软开发的基于 HTML、CSS 和 C# 的开放源代码网络框架,允许开发人员构建可在客户端或服务器上运行的网络应用程序。Blazor 还允许您制作可重复使用的组件,以更快地构建应用程序;它使开发人员能够在同一个文件中构建 HTML 视图和 C# 操作,这有助于保持代码的可读性和简洁性。此外,有了Blazor Hybrid,您还可以通过.NET代码构建本地移动应用程序,访问本地平台功能。
Blazor 的部分功能使其成为一个非常适合工作的框架:
服务器端和客户端渲染选项
可重复使用的用户界面组件
利用 SignalR 实时更新
内置状态管理
内置路由系统
强大的类型和编译时检查
为什么选择 Blazor?
与其他框架和库相比,Blazor 具有以下优势:它允许开发人员在客户端和服务器代码中使用 C#,提供强大的类型和编译时检查功能,从而提高了可靠性。它与 .NET 生态系统无缝集成,实现了 .NET 库和工具的重用,并提供强大的调试支持。
什么是 ESRE?
Elasticsearch Relevance Engine™ (ESRE)是一套在强大的 Elasticsearch 搜索引擎基础上使用机器学习和人工智能构建搜索应用程序的工具。

要了解有关 ESRE 的更多信息,请点击此处阅读我们的博文。
配置 ELSER
为了充分利用 Elastic 的ESRE功能,我们将使用ELSER作为模型提供者。
请注意,要使用 Elasticsearch 的 ELSER 模型,您必须拥有白金级或企业级许可证,并至少拥有一个 4GB 大小的专用机器学习 (ML) 节点。 点击此处 了解更多信息 。
首先创建推理端点:
PUT _inference/sparse_embedding/my-elser-model
{
"service": "elser",
"service_settings": {
"num_allocations": 1,
"num_threads": 1
}
}如果您是第一次使用 ELSER,在后台加载模型时可能会遇到 502 Bad Gateway 错误。您可以在 Kibana 的Machine Learning > Trained Models 中查看模型的状态。部署完成后,就可以进行下一步。

索引数据
您可以在这里下载数据集,然后使用 Kibana 导入数据。为此,请访问主页并点击"Upload data" 。然后,上传文件并点击Import 。最后,进入Advanced 标签,粘贴以下映射:
{
"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"
}
}
}
我们将创建一个能够运行语义和全文查询的索引。语义文本字段类型将负责数据分块和嵌入。请注意,我们是将 索引longDescription 为semantic_text , 如果要将一个字段 索引 为semantic_text 和 text` , 可以使用 copy_to 。
使用 Blazor& Elasticsearch 构建应用程序
API 密钥
我们需要做的第一件事是创建一个 API 密钥,以验证对 Elasticsearch 的请求。API 密钥应为只读,只允许查询books-blazor 索引。
POST /_security/api_key
{
"name": "books-blazor-key",
"role_descriptors": {
"books-blazor-reader": {
"indices": [
{
"names": ["books-blazor"],
"privileges": ["read"]
}
]
}
}
}你会看到类似这样的内容:
{
"id": "XXXXXXXXXXXXXXXXXXXXXXXX",
"name": "books-blazor-key",
"api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}保存encoded 响应字段的值,以备不时之需。如果您在Elastic Cloud 上运行,还需要您的 Cloud ID。(您可以在此处找到)。
创建 Blazor 项目
首先安装 Blazor,并按照官方说明创建一个示例项目。

创建项目后,文件夹结构和文件应如下所示:
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/模板应用程序包括Bootstrap v5.1.0用于造型。
安装Elasticsearch .NET客户端,完成项目设置:
dotnet add package Elastic.Clients.Elasticsearch完成这一步后,您的页面应该是这样的:

文件夹结构
现在,我们将按如下方式整理文件夹:
BlazorApp/
|-- Components/
| |-- Pages/
| | |-- Search.razor
| | `-- Search.razor.css
| `-- Elasticsearch/
| |-- SearchBar.razor
| |-- Results.razor
| `-- Facet.razor
|-- Models/
| |-- Book.cs
| `-- Response.cs
`-- Services/
`-- ElasticsearchService.cs文件解释:
Components/Pages/Search.razor:包含搜索栏、搜索结果和过滤器的主页面。
Components/Pages/Search.razor.css:页面样式。
Components/Elasticsearch/SearchBar.razor:搜索栏组件。
Components/Elasticsearch/Results.razor:结果组件。
Components/Elasticsearch/Facet.razor: 过滤器组件。
Components/Svg/GlassIcon.razor: 搜索图标。
Components/_Imports.razor:这将导入所有组件。
Models/Book.cs:这将存储图书字段模式。
Models/Response.cs:这将存储响应模式,包括搜索结果、面和总点击数。
Services/ElasticsearchService.cs:Elasticsearch 服务。它将处理与 Elasticsearch 的连接和查询。
初始配置
我们先来清理一下。
删除文件:
Components/Pages/Counter.razor
Components/Pages/Weather.razor
Components/Pages/Home.razor
Components/Layout/NavMenu.razor
Components/Layout/NavMenu.razor.css
检查/Components/_Imports.razor 文件。您应该有以下进口:
@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将 Elastic 集成到项目中
现在,让我们导入 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 *@我们将从/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>
现在让我们输入用户机密的 Elasticsearch 凭据:
dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"使用这种方法,.Net 8 可将敏感数据存储在项目文件夹之外的单独位置,并可通过IConfiguration 界面进行访问。任何使用相同用户秘密的 .Net 项目都可以使用这些变量。
然后,让我们修改Program.cs 文件以读取机密并挂载 Elasticsearch 客户端:
首先,导入必要的库:
using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;BlazorApp.Services:包含 Elasticsearch 服务。
Elastic.Clients.Elasticsearch:导入 Elasticsearch 客户端 .Net 8 库。
Elastic.Transport:导入 Elasticsearch 传输库,它允许我们使用 ApiKey 类来验证请求。
其次,在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);
});这段代码将从用户机密中读取 Elasticsearch 凭据,并创建一个 Elasticsearch 客户端实例。
在初始化 ElasticSearch 客户端后,添加以下一行以注册 Elasticsearch 服务:
builder.Services.AddScoped<ElasticsearchService>();下一步将在/Services/ElasticsearchService.cs 文件中建立搜索逻辑:
首先,导入必要的库和模型:
using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;其次,添加类ElasticsearchService 、构造函数和变量:
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;
}
}
}配置搜索
现在,让我们构建搜索逻辑:
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;
}BuildFilters将使用用户选择的面为搜索查询建立过滤器。BuildHybridQuery将建立一个结合全文和语义搜索的混合搜索查询。
接下来,添加搜索方法:
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该功能将使用混合查询执行搜索,并返回结果,其中包括用于构建切面的聚合。FormatFacets:将聚合响应格式化为字典。ConvertFacetDictionary:将面字典转换为更易读的格式。
下一步是创建模型,这些模型将代表 Elasticsearch 查询hits 中返回的数据,这些数据将作为结果打印在搜索页面中。
我们首先创建文件/Models/Book.cs 并添加以下内容:
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; }
}
}然后,在/Models/Response.cs 文件中设置弹性响应,并添加以下内容:
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; }
}
}配置基本用户界面
接下来,添加 SearchBar 组件。在文件/Components/Elasticsearch/SearchBar.razor 中添加以下内容:
@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);
}
}该组件包含一个搜索栏和一个执行搜索的按钮。
Blazor 允许在同一文件中使用 C# 代码动态生成 HTML,具有极大的灵活性。
之后,我们将在/Components/Elasticsearch/Results.razor 文件中构建显示搜索结果的结果组件:
@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 "";
}
}最后,我们需要创建面来过滤搜索结果。
注:筛选器允许用户根据特定属性或类别(如产品类型、价格范围或品牌)缩小搜索结果的范围。这些筛选器通常以复选框的形式显示为可点击的选项,帮助用户缩小搜索范围,更轻松地找到相关结果。在 Elasticsearch 的上下文中,面是通过 聚合创建的。
我们在文件/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);
}
}该组件从terms 中读取author 、categories 和status 字段的聚合,然后生成一个过滤器列表,发送回 Elasticsearch。
现在,让我们把所有东西放在一起。
在/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);
}
}我们的页面正在运行!

如您所见,该页面功能齐全,但缺乏风格。让我们添加一些 CSS,使其看起来更有条理,反应更灵敏。
让我们开始更换布局样式。在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;
}在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;
}
}我们的页面开始变得更好看了:

让我们来做最后的润色:
创建以下文件
Components/Elasticsearch/Facet.razor.css
Components/Elasticsearch/Results.razor.css
并为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;
}供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;
}最终结果

要运行应用程序,可以使用以下命令:
dotnet watch
你做到了现在,您可以使用搜索栏在 Elasticsearch 索引中搜索图书,并按作者、类别和状态过滤结果。
进行全文和语义搜索
默认情况下,我们的应用程序将使用 全文 和 语义搜索 执行 混合 搜索 。您可以通过创建两个独立的方法来改变搜索逻辑,一个用于全文搜索,另一个用于语义搜索,然后根据用户的输入选择一种方法来创建查询。
在/Services/ElasticsearchService.cs 文件中的ElasticsearchService 类中添加以下方法:
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())
);
}这两种方法的工作原理与BuildHybridQuery 方法类似,但它们只进行全文或语义搜索。
您可以修改SearchBooksAsync 方法,使用选定的搜索方法:
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();
}
}您可在此处找到完整的申请表
结论
Blazor 是一个有效的框架,可让您使用 C# 构建网络应用程序。Elasticsearch 是一个功能强大的搜索引擎,可让您构建搜索应用程序。将两者结合起来,您就可以轻松构建强大的搜索应用程序,利用 ESRE 的强大功能在短时间内创建语义搜索体验。
常见问题
什么是 Blazor?
Blazor 是微软公司开发的基于 HTML、CSS 和 C# 的开放源代码网络框架,允许开发人员构建可在客户端或服务器上运行的网络应用程序。它还允许制作可重复使用的组件来构建应用程序等。
使用Blazor有哪些优势?
与其他框架和库相比,Blazor 具有以下优势:允许开发人员使用 C# 编写客户端和服务器代码,与 .NET 生态系统无缝集成,并提供强大的调试支持。




