作者:来自 Elastic Gustavo Llermaly
了解如何使用 Blazor 和 Elasticsearch 构建搜索应用程序,以及如何使用 Elasticsearch .NET 客户端进行混合搜索。
在本文中,你将学习如何利用 C# 技能使用 Blazor 和 Elasticsearch 构建搜索应用程序。我们将使用 Elasticsearch .NET 客户端运行全文、语义和混合搜索查询。
注意如果你熟悉旧版本的 Elasticsearch C# 客户端 NEST,请阅读这篇关于 NEST 客户端弃用和新功能的博客文章。NEST 是上一代 .NET 客户端,已被 Elasticsearch .NET 客户端取代。
什么是 Blazor?
Image: ASP.NETCoreBlazorhostingmodels
Blazor 是一个基于 HTML、CSS 和 C# 的开源 Web 框架,由 Microsoft 创建,允许开发人员构建在客户端或服务器上运行的 Web 应用程序。Blazor 还允许你创建可重用的组件,以更快地构建应用程序;它使开发人员能够在同一文件中用 C# 构建 HTML 视图和操作,这有助于维护可读且干净的代码。此外,使用 Blazor Hybrid,你可以构建通过 .NET 代码访问本机平台功能的本机移动应用程序。
一些功能使 Blazor 成为一个很棒的框架:
- 服务器端和客户端渲染选项
- 可重用的 UI 组件
- 使用 SignalR 进行实时更新
- 内置状态管理
- 内置路由系统
- 强类型和编译时检查
为什么选择 Blazor?
与其他框架和库相比,Blazor 具有多项优势:它允许开发人员使用 C# 编写客户端和服务器代码,提供强类型和编译时检查,从而提高可靠性。它与 .NET 生态系统无缝集成,支持重用 .NET 库和工具,并提供强大的调试支持。
什么是 ESRE?
Elasticsearch Relevance Engine™ (ESRE) 是一套工具,用于在强大的 Elasticsearch 搜索引擎上使用机器学习和人工智能构建搜索应用程序。
要了解有关 ESRE 的更多信息,你可以阅读我们位于此处的富有洞察力的博客文章
配置 ELSER
为了利用 Elastic 的 ESRE 功能,我们将使用 ELSER 作为我们的模型提供者。
请注意,要使用 Elasticsearch 的 ELSER 模型,你必须拥有白金版或企业版许可证,并且至少拥有 4GB 大小的专用机器学习 (ML) 节点。在此处了解更多信息。
首先创建推理端点:
1. PUT _inference/sparse_embedding/my-elser-model
2. {
3. "service": "elser",
4. "service_settings": {
5. "num_allocations": 1,
6. "num_threads": 1
7. }
8. }
如果这是你第一次使用 ELSER,你可能会在模型在后台加载时遇到 502 Bad Gateway 错误。你可以在 Kibana 中的 Machine Learning > Trained Models 中检查模型的状态。部署后,你可以继续下一步。
索引数据
你可以在此处下载数据集,然后使用 Kibana 导入数据。为此,请转到主页并单击 “Upload data”。然后,上传文件并单击 Import。最后,转到 “Advanced” 选项卡并粘贴以下映射:
1. {
2. "properties":{
3. "authors":{
4. "type":"keyword"
5. },
6. "categories":{
7. "type":"keyword"
8. },
9. "longDescription":{
10. "type":"semantic_text",
11. "inference_id":"my-elser-model",
12. "model_settings":{
13. "task_type":"sparse_embedding"
14. }
15. },
16. "pageCount":{
17. "type":"integer"
18. },
19. "publishedDate":{
20. "type":"date"
21. },
22. "shortDescription":{
23. "type":"text"
24. },
25. "status":{
26. "type":"keyword"
27. },
28. "thumbnailUrl":{
29. "type":"keyword"
30. },
31. "title":{
32. "type":"text"
33. }
34. }
35. }
我们将创建一个能够运行语义和全文查询的索引。semantic_text 字段类型将负责数据分块和嵌入。请注意,我们将 longDescription 索引为 semantic_text,如果你想将字段同时索引为 semantic_text 和 text,则可以使用 copy_to。
构建应用程序
API 密钥
我们需要做的第一件事是创建一个 API 密钥来验证我们对 Elasticsearch 的请求。API 密钥应该是只读的,并且只允许查询 books-blazor 索引。
1. POST /_security/api_key
2. {
3. "name": "books-blazor-key",
4. "role_descriptors": {
5. "books-blazor-reader": {
6. "indices": [
7. {
8. "names": ["books-blazor"],
9. "privileges": ["read"]
10. }
11. ]
12. }
13. }
14. }
你会看到类似这样的内容:
1. {
2. "id": "XXXXXXXXXXXXXXXXXXXXXXXX",
3. "name": "books-blazor-key",
4. "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
5. "encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
6. }
保存 encoded 响应字段的值,因为稍后会用到它。如果你在 Elastic Cloud 上运行,你还需要你的 Cloud ID。(你可以在此处找到它)。
创建 Blazor 项目
首先安装 Blazor 并按照官方说明创建示例项目。
创建项目后,文件夹结构和文件应如下所示:
1. BlazorApp/
2. |-- BlazorApp.csproj
3. |-- BlazorApp.sln
4. |-- Program.cs
5. |-- appsettings.Development.json
6. |-- appsettings.json
7. |-- Properties/
8. | `-- launchSettings.json
9. |-- Components/
10. | |-- App.razor
11. | |-- Routes.razor
12. | |-- _Imports.razor
13. | |-- Layout/
14. | | |-- MainLayout.razor
15. | | |-- MainLayout.razor.css
16. | | |-- NavMenu.razor
17. | | `-- NavMenu.razor.css
18. | `-- Pages/
19. | |-- Counter.razor
20. | |-- Error.razor
21. | |-- Home.razor
22. | `-- Weather.razor
23. |-- wwwroot/
24. |-- bin/
25. `-- obj/
模板应用程序包含用于样式设置的 Bootstrap v5.1.0。
通过安装 Elasticsearch .NET 客户端完成项目设置
dotnet add package Elastic.Clients.Elasticsearch
完成此步骤后,你的页面应如下所示:
文件夹结构
现在,我们将按如下方式组织文件夹:
1. BlazorApp/
2. |-- Components/
3. | |-- Pages/
4. | | |-- Search.razor
5. | | `-- Search.razor.css
6. | `-- Elasticsearch/
7. | |-- SearchBar.razor
8. | |-- Results.razor
9. | `-- Facet.razor
10. |-- Models/
11. | |-- Book.cs
12. | `-- Response.cs
13. `-- Services/
14. `-- ElasticsearchService.cs
文件说明:
- Components/Pages/Search.razor:包含搜索栏、结果和过滤器的主页。
- Components/Pages/Search.razor.cs:页面样式。
- 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 文件。你应该有以下导入:
1. @using System.Net.Http
2. @using System.Net.Http.Json
3. @using Microsoft.AspNetCore.Components.Forms
4. @using Microsoft.AspNetCore.Components.Routing
5. @using Microsoft.AspNetCore.Components.Web
6. @using static Microsoft.AspNetCore.Components.Web.RenderMode
7. @using Microsoft.AspNetCore.Components.Web.Virtualization
8. @using Microsoft.JSInterop
9. @using BlazorApp
10. @using BlazorApp.Components
将 Elastic 集成到项目中
现在,让我们导入 Elasticsearch 组件:
1. @using System.Net.Http
2. @using System.Net.Http.Json
3. @using Microsoft.AspNetCore.Components.Forms
4. @using Microsoft.AspNetCore.Components.Routing
5. @using Microsoft.AspNetCore.Components.Web
6. @using static Microsoft.AspNetCore.Components.Web.RenderMode
7. @using Microsoft.AspNetCore.Components.Web.Virtualization
8. @using Microsoft.JSInterop
9. @using BlazorApp
10. @using BlazorApp.Components
11. @using BlazorApp.Components.Elasticsearch @* <--- Add this line *@
我们将从 /Components/Layout/MainLayout.razor 文件中删除默认侧边栏,以便为我们的应用程序提供更多空间:
1. @inherits LayoutComponentBase
3. <div class="page">
4. <main>
5. <article class="content">
6. @Body
7. </article>
8. </main>
9. </div>
11. <div id="blazor-error-ui">
12. An unhandled error has occurred.
13. <a href="" class="reload">Reload</a>
14. <a class="dismiss">🗙</a>
15. </div>
现在让我们输入用户机密的 Elasticsearch 凭证:
1. dotnet user-secrets init
2. dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
3. dotnet user-secrets set ElasticsearchApiKey "your API Key"
使用这种方法,.Net 8 将敏感数据存储在项目文件夹之外的单独位置,并使用 IConfiguration 接口使其可访问。这些变量将可供使用相同用户机密的任何 .Net 项目使用。
然后,让我们修改 Program.cs 文件以读取机密并安装 Elasticsearch 客户端:
首先,导入必要的库:
1. using BlazorApp.Services;
2. using Elastic.Clients.Elasticsearch;
3. using Elastic.Transport;
- BlazorApp.Services:包含 Elasticsearch 服务。
- Elastic.Clients.Elasticsearch:导入 Elasticsearch 客户端 .Net 8 库。
- Elastic.Transport:导入 Elasticsearch 传输库,它允许我们使用 ApiKey 类来验证我们的请求。
其次,在 var app = builder.Build() 行之前插入以下代码:
1. // Initialize the Elasticsearch client.
2. builder.Services.AddScoped(sp =>
3. {
4. // Getting access to the configuration service to read the Elasticsearch credentials.
5. var configuration = sp.GetRequiredService<IConfiguration>();
6. var cloudId = configuration["ElasticsearchCloudId"];
7. var apiKey = configuration["ElasticsearchApiKey"];
9. if (string.IsNullOrEmpty(cloudId) || string.IsNullOrEmpty(apiKey))
10. {
11. throw new InvalidOperationException(
12. "Elasticsearch credentials are missing in configuration."
13. );
14. }
16. var settings = new ElasticsearchClientSettings(cloudId, new ApiKey(apiKey)).EnableDebugMode();
17. return new ElasticsearchClient(settings);
18. });
此代码将从用户机密中读取 Elasticsearch 凭据并创建 Elasticsearch 客户端实例。
ElasticSearch 客户端初始化后,添加以下行以注册 Elasticsearch 服务:
builder.Services.AddScoped<ElasticsearchService>();
下一步是在 /Services/ElasticsearchService.cs 文件中构建搜索逻辑:
首先,导入必要的库和模型:
1. using BlazorApp.Models;
2. using Elastic.Clients.Elasticsearch;
3. using Elastic.Clients.Elasticsearch.QueryDsl;
其次,添加ElasticsearchService类、构造函数和变量:
1. namespace BlazorApp.Services
2. {
3. public class ElasticsearchService
4. {
5. private readonly ElasticsearchClient _client;
7. // The logger is used to log information, warnings and errors about the Elasticsearch service and requests.
8. private readonly ILogger<ElasticsearchService> _logger;
10. public ElasticsearchService(
11. ElasticsearchClient client,
12. ILogger<ElasticsearchService> logger
13. )
14. {
15. _client = client ?? throw new ArgumentNullException(nameof(client));
16. _logger = logger;
17. }
18. }
19. }
配置搜索
现在,让我们构建搜索逻辑:
1. private static Action<RetrieverDescriptor<BookDoc>> BuildHybridQuery(
2. string searchTerm,
3. Dictionary<string, List<string>> selectedFacets
4. )
5. {
6. var filters = BuildFilters(selectedFacets);
8. return retrievers =>
9. retrievers.Rrf(rrf =>
10. rrf.RankWindowSize(50)
11. .RankConstant(20)
12. .Retrievers(
13. retrievers =>
14. retrievers.Standard(std =>
15. std.Query(q =>
16. q.Bool(b =>
17. b.Must(m =>
18. m.MultiMatch(mm =>
19. mm.Query(searchTerm)
20. .Fields(
21. new[]
22. {
23. "title",
24. "shortDescription",
25. }
26. )
27. )
28. )
29. .Filter(filters.ToArray())
30. )
31. )
32. ),
33. retrievers =>
34. retrievers.Standard(std =>
35. std.Query(q =>
36. q.Bool(b =>
37. b.Must(m =>
38. m.Semantic(sem =>
39. sem.Field("longDescription")
40. .Query(searchTerm)
41. )
42. )
43. .Filter(filters.ToArray())
44. )
45. )
46. )
47. )
48. );
49. }
51. public static List<Action<QueryDescriptor<BookDoc>>> BuildFilters(
52. Dictionary<string, List<string>> selectedFacets
53. )
54. {
55. var filters = new List<Action<QueryDescriptor<BookDoc>>>();
57. if (selectedFacets != null)
58. {
59. foreach (var facet in selectedFacets)
60. {
61. foreach (var value in facet.Value)
62. {
63. var field = facet.Key.ToLower();
64. if (!string.IsNullOrEmpty(field))
65. {
66. filters.Add(m => m.Term(t => t.Field(new Field(field)).Value(value)));
67. }
68. }
69. }
70. }
72. return filters;
73. }
- BuildFilters 将使用用户选择的方面构建搜索查询的过滤器。
- BuildHybridQuery 将构建结合全文和语义搜索的混合搜索查询。
接下来,添加搜索方法:
1. public async Task<ElasticResponse> SearchBooksAsync(
2. string searchTerm,
3. Dictionary<string, List<string>> selectedFacets
4. )
5. {
6. try
7. {
8. _logger.LogInformation($"Performing search for: {searchTerm}");
10. // Retrieve the hybrid query with filters applied.
11. var retrieverQuery = BuildHybridQuery(searchTerm, selectedFacets);
13. var response = await _client.SearchAsync<BookDoc>(s =>
14. s.Index("elastic-blazor-books")
15. .Retriever(retrieverQuery)
16. .Aggregations(aggs =>
17. aggs.Add("Authors", agg => agg.Terms(t => t.Field(p => p.Authors)))
18. .Add(
19. "Categories",
20. agg => agg.Terms(t => t.Field(p => p.Categories))
21. )
22. .Add("Status", agg => agg.Terms(t => t.Field(p => p.Status)))
23. )
24. );
26. if (response.IsValidResponse)
27. {
28. _logger.LogInformation($"Found {response.Documents.Count} documents");
30. var hits = response.Total;
31. var facets =
32. response.Aggregations != null
33. ? FormatFacets(response.Aggregations)
34. : new Dictionary<string, Dictionary<string, long>>();
36. var elasticResponse = new ElasticResponse
37. {
38. TotalHits = hits,
39. Documents = response.Documents.ToList(),
40. Facets = facets,
41. };
43. return elasticResponse;
44. }
45. else
46. {
47. _logger.LogWarning($"Invalid response: {response.DebugInformation}");
48. return new ElasticResponse();
49. }
50. }
51. catch (Exception ex)
52. {
53. _logger.LogError(ex, "Error performing search");
54. return new ElasticResponse();
55. }
56. }
58. public static Dictionary<string, Dictionary<string, long>> FormatFacets(
59. Elastic.Clients.Elasticsearch.Aggregations.AggregateDictionary aggregations
60. )
61. {
62. var facets = new Dictionary<string, Dictionary<string, long>>();
64. foreach (var aggregation in aggregations)
65. {
66. if (
67. aggregation.Value
68. is Elastic.Clients.Elasticsearch.Aggregations.StringTermsAggregate termsAggregate
69. )
70. {
71. var facetName = aggregation.Key;
72. var facetDictionary = ConvertFacetDictionary(
73. termsAggregate.Buckets.ToDictionary(b => b.Key, b => b.DocCount)
74. );
75. facets[facetName] = facetDictionary;
76. }
77. }
79. return facets;
80. }
82. private static Dictionary<string, long> ConvertFacetDictionary(
83. Dictionary<Elastic.Clients.Elasticsearch.FieldValue, long> original
84. )
85. {
86. var result = new Dictionary<string, long>();
87. foreach (var kvp in original)
88. {
89. result[kvp.Key.ToString()] = kvp.Value;
90. }
91. return result;
92. }
- SearchBooksAsync:将使用混合查询执行搜索,并返回包含用于构建方面的聚合的结果。
- FormatFacets:将聚合响应格式化为字典。
- ConvertFacetDictionary:将把方面字典转换为更易读的格式。
下一步是创建模型,这些模型将表示 Elasticsearch 查询命中中返回的数据,这些数据将作为结果打印在我们的搜索页面中。
我们首先创建文件 /Models/Book.cs 并添加以下内容:
1. namespace BlazorApp.Models
2. {
3. public class BookDoc
4. {
5. public string? Title { get; set; }
6. public int? PageCount { get; set; }
7. public string? PublishedDate { get; set; }
8. public string? ThumbnailUrl { get; set; }
9. public string? ShortDescription { get; set; }
10. public LongDescription? LongDescription { get; set; }
11. public string? Status { get; set; }
12. public List<string>? Authors { get; set; }
13. public List<string>? Categories { get; set; }
14. }
16. public class LongDescription
17. {
18. public string? Text { get; set; }
19. }
20. }
然后,在 /Models/Response.cs 文件中设置 Elastic 响应并添加以下内容:
1. namespace BlazorApp.Models
2. {
3. public class ElasticResponse
4. {
5. public ElasticResponse()
6. {
7. Documents = new List<BookDoc>();
8. Facets = new Dictionary<string, Dictionary<string, long>>();
9. }
11. public long TotalHits { get; set; }
12. public List<BookDoc> Documents { get; set; }
13. public Dictionary<string, Dictionary<string, long>> Facets { get; set; }
14. }
15. }
配置基本 UI
接下来,添加 SearchBar 组件。在文件 /Components/Elasticsearch/SearchBar.razor 中添加以下内容:
1. @using System.Threading.Tasks
3. <form @onsubmit="SubmitSearch">
4. <div class="input-group mb-3">
5. <input type="text" @bind-value="searchTerm" class="form-control" placeholder="Enter search term..." />
6. <button type="submit" class="btn btn-primary input-btn">
7. <span class="input-group-svg">
8. Search
9. </span>
10. </button>
11. </div>
12. </form>
14. @code {
15. [Parameter]
16. public EventCallback<string> OnSearch { get; set; }
18. private string searchTerm = "";
20. private async Task SubmitSearch()
21. {
22. await OnSearch.InvokeAsync(searchTerm);
23. }
24. }
该组件包含一个搜索栏和一个用于执行搜索的按钮。
Blazor 允许在同一文件中使用 C# 代码动态生成 HTML,从而提供了极大的灵活性。
之后,在文件 /Components/Elasticsearch/Results.razor 中,我们将构建显示搜索结果的结果组件:
1. @using BlazorApp.Models
3. @if (SearchResults != null && SearchResults.Any())
4. {
5. <div class="row">
6. @foreach (var result in SearchResults)
7. {
8. <div class="col-12 mb-3">
9. <div class="card">
10. <div class="row g-0">
11. <div class="col-md-3 image-container">
12. @if (!string.IsNullOrEmpty(result?.ThumbnailUrl))
13. {
14. <img src="@result?.ThumbnailUrl" class="img-fluid rounded-start" alt="Thumbnail">
15. }
16. else
17. {
18. <div class="placeholder">
19. @result?.Title
20. </div>
21. }
22. </div>
24. <div class="col-md-9"> <!-- Adjusted to use the remaining 75% -->
25. <div class="card-body">
26. <h4 class="card-title">
27. @result?.Title
28. </h4>
30. <div class="details-container">
31. <div class="">
33. @if (result?.Authors?.Any() == true)
34. {
35. <p class="card-text p-first">
36. Authors: <small class="text-muted">@string.Join(", ", result.Authors)</small>
37. </p>
38. }
40. @if (result?.Categories?.Any() == true)
41. {
42. <p class="card-text p-second">
43. Categories: <small class="text-muted">@string.Join(", ", result.Categories)</small>
44. </p>
45. }
46. </div>
47. <div class="numPages-status">
48. @if (result?.PageCount != null)
49. {
50. <p class="card-text p-first">
51. Pages: <small class="text-muted">@result.PageCount</small>
52. </p>
53. }
55. @if (result?.Status != null)
56. {
57. <p class="card-text p-second">
58. Status: <small class="text-muted">@result.Status</small>
59. </p>
60. }
61. </div>
62. </div>
64. <div class="long-text-container">
65. <p class="card-text"><small class="text-muted">@result?.LongDescription?.Text</small></p>
66. </div>
67. @if (!string.IsNullOrEmpty(result?.PublishedDate))
68. {
69. <div class="date-container">
70. <p class="card-text">
71. Published Date: <small class="text-muted small-date">@FormatDate(result.PublishedDate)</small>
72. </p>
73. </div>
74. }
75. </div>
76. </div>
77. </div>
78. </div>
79. </div>
80. }
81. </div>
82. }
83. else if (SearchResults != null)
84. {
85. <p>No results found.</p>
86. }
88. @code {
89. [Parameter]
90. public List<BookDoc> SearchResults { get; set; } = new List<BookDoc>();
92. private string FormatDate(string? date)
93. {
94. if (DateTime.TryParse(date, out DateTime parsedDate))
95. {
96. return parsedDate.ToString("MMMM dd, yyyy");
97. }
98. return "";
99. }
100. }
最后,我们需要创建方面来过滤搜索结果。
注意:方面是允许用户根据特定属性或类别(例如产品类型、价格范围或品牌)缩小搜索结果范围的过滤器。这些过滤器通常以可点击的选项形式呈现,通常以复选框的形式呈现,以帮助用户优化搜索并更轻松地找到相关结果。在 Elasticsearch 上下文中,方面是使用聚合创建的。
我们通过将以下代码放入文件 /Components/Elasticsearch/Facet.razor 中来设置方面:
1. @if (Facets != null)
2. {
3. <div class="facets-container">
4. @foreach (var facet in Facets)
5. {
6. <h3>@facet.Key</h3>
7. @foreach (var option in facet.Value)
8. {
9. <div>
10. <input type="checkbox" checked="@IsFacetSelected(facet.Key, option.Key)"
11. @onclick="() => ToggleFacet(facet.Key, option.Key)" />
12. @option.Key (@option.Value)
13. </div>
14. }
15. }
16. </div>
17. }
20. @code {
21. [Parameter]
22. public Dictionary<string, Dictionary<string, long>>? Facets { get; set; }
24. [Parameter]
25. public EventCallback<Dictionary<string, List<string>>> OnFacetChanged { get; set; }
27. private Dictionary<string, List<string>> selectedFacets = new();
29. private void ToggleFacet(string facetName, string facetValue)
30. {
31. if (!selectedFacets.TryGetValue(facetName, out var facetValues))
32. {
33. facetValues = selectedFacets[facetName] = new List<string>();
34. }
36. if (!facetValues.Remove(facetValue))
37. {
38. facetValues.Add(facetValue);
39. }
41. OnFacetChanged.InvokeAsync(selectedFacets);
42. }
44. private bool IsFacetSelected(string facetName, string facetValue)
45. {
46. return selectedFacets.ContainsKey(facetName) && selectedFacets[facetName].Contains(facetValue);
47. }
48. }
该组件从 author、categories 和 status 字段的 terms 聚合中读取内容,然后生成一个过滤器列表以发送回 Elasticsearch。
现在,让我们将所有内容放在一起。
在 /Components/Pages/Search.razor 文件中:
1. @page "/"
2. @rendermode InteractiveServer
3. @using BlazorApp.Models
4. @using BlazorApp.Services
5. @inject ElasticsearchService ElasticsearchService
6. @inject ILogger<Search> Logger
8. <PageTitle>Search</PageTitle>
10. <div class="top-row px-4 ">
12. <div class="searchbar-container">
13. <h4>Semantic Search with Elasticsearch and Blazor</h4>
15. <SearchBar OnSearch="PerformSearch" />
16. </div>
18. <a href="https://www.elastic.co/search-labs/esre-with-blazor" target="_blank">About</a>
19. </div>
21. <div class="px-4">
23. <div class="search-details-container">
24. <p role="status">Current search term: @currentSearchTerm</p>
25. <p role="status">Total results: @totalResults</p>
26. </div>
28. <div class="results-facet-container">
29. <div class="facets-container">
30. <Facet Facets="facets" OnFacetChanged="OnFacetChanged" />
31. </div>
32. <div class="results-container">
33. <Results SearchResults="searchResults" />
34. </div>
35. </div>
36. </div>
38. @code {
39. private string currentSearchTerm = "";
40. private long totalResults = 0;
41. private List<BookDoc> searchResults = new List<BookDoc>();
42. private Dictionary<string, Dictionary<string, long>> facets = new Dictionary<string, Dictionary<string, long>>();
43. private Dictionary<string, List<string>> selectedFacets = new Dictionary<string, List<string>>();
45. protected override async Task OnInitializedAsync()
46. {
47. await PerformSearch();
48. }
50. private async Task PerformSearch(string searchTerm = "")
51. {
52. try
53. {
54. currentSearchTerm = searchTerm;
56. var response = await ElasticsearchService.SearchBooksAsync(currentSearchTerm, selectedFacets);
57. if (response != null)
58. {
59. searchResults = response.Documents;
60. facets = response.Facets;
61. totalResults = response.TotalHits;
62. }
63. else
64. {
65. Logger.LogWarning("Search response is null.");
66. }
68. StateHasChanged();
69. }
70. catch (Exception ex)
71. {
72. Logger.LogError(ex, "Error performing search.");
73. }
74. }
76. private async Task OnFacetChanged(Dictionary<string, List<string>> newSelectedFacets)
77. {
78. selectedFacets = newSelectedFacets;
79. await PerformSearch(currentSearchTerm);
80. }
81. }
我们的页面正在运行!
如你所见,该页面功能齐全,但缺少样式。让我们添加一些 CSS,使其看起来更有条理、响应更快。
让我们开始替换布局样式。在 Components/Layout/MainLayout.razor.css 文件中:
1. .page {
2. position: relative;
3. display: flex;
4. flex-direction: column;
5. }
7. main {
8. flex: 1;
9. }
11. #blazor-error-ui {
12. background: lightyellow;
13. bottom: 0;
14. box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
15. display: none;
16. left: 0;
17. padding: 0.6rem 1.25rem 0.7rem 1.25rem;
18. position: fixed;
19. width: 100%;
20. z-index: 1000;
21. }
23. #blazor-error-ui .dismiss {
24. cursor: pointer;
25. position: absolute;
26. right: 0.75rem;
27. top: 0.5rem;
28. }
在 Components/Pages/Search.razor.css 文件中添加搜索页面的样式:
1. .input-group .input-group-svg {
2. background: transparent;
3. border: transparent;
4. pointer-events: none;
5. }
7. .results-facet-container {
8. display: flex;
9. margin-top: 1rem;
10. overflow-x: auto;
11. }
13. .search-details-container {
14. display: flex;
15. justify-content: space-between;
16. margin-top: 1rem;
17. }
19. .searchbar-container {
20. padding-top: 2rem;
21. display: flex;
22. flex-direction: column;
23. flex-grow: 1;
24. height: 100%;
25. max-width: 100%;
26. }
28. .searchbar-container h4 {
29. margin: 0;
30. }
32. .top-row {
33. margin-top: -1.1rem;
34. position: relative;
35. background-color: hsl(216, 29%, 67%);
36. border-bottom: 1px solid #d6d5d5;
37. display: flex;
38. align-items: center;
39. height: 100%;
40. padding: 0 1rem;
41. }
43. .top-row a {
44. margin-left: auto;
45. margin-top: -4rem;
46. color: #000000;
47. text-decoration: none;
48. }
50. .top-row a:hover {
51. text-decoration: underline;
52. }
54. @media (max-width: 640.98px) {
55. .top-row {
56. justify-content: space-between;
57. }
59. .top-row ::deep a,
60. .top-row ::deep .btn-link {
61. margin-left: 0;
62. }
63. }
65. @media (min-width: 641px) {
66. .top-row.auth ::deep a:first-child {
67. flex: 1;
68. text-align: right;
69. width: 0;
70. }
72. .top-row,
73. article {
74. padding-left: 2rem !important;
75. padding-right: 1.5rem !important;
76. }
77. }
我们的页面开始看起来更好了:
让我们做最后的润色:
创建以下文件:
- Components/Elasticsearch/Facet.razor.css
- Components/Elasticsearch/Results.razor.css
并添加 Facet.razor.css 的样式:
1. .facets-container {
2. font-size: 15px;
3. margin-right: 4rem;
4. overflow-x: auto;
5. white-space: nowrap;
6. max-width: 300px;
7. }
9. .results-facet-container {
10. display: flex;
11. margin-top: 1rem;
12. overflow-x: auto;
13. }
15. .results-facet-container > * {
16. flex-shrink: 0;
17. }
对于 Results.razor.css:
1. .image-container {
2. display: flex;
3. justify-content: center;
4. align-items: center;
5. height: 100%;
6. padding: 1rem;
7. box-sizing: border-box;
8. }
10. .image-container img {
11. max-width: 100%;
12. height: auto;
13. border-radius: 0.5rem;
14. }
16. .placeholder {
17. display: flex;
18. justify-content: center;
19. align-items: center;
20. height: 100%;
21. width: 100%;
22. background-color: #f0f0f0;
23. border: 1px solid #ccc;
24. font-size: 0.9rem;
25. color: #888;
26. text-align: center;
27. padding: 1rem;
28. border-radius: 0.5rem;
29. }
31. .card-body {
32. padding: 1rem;
33. }
35. .details-container {
36. display: flex;
37. justify-content: space-between;
38. padding: 1.5rem 0;
39. }
41. .date-container {
42. margin-top: 1rem;
43. display: flex;
44. justify-content: flex-end;
45. }
47. .date-container .small-date {
48. font-weight: bold;
49. }
最终结果:
要运行该应用程序,你可以使用以下命令:
dotnet watch
你做到了!现在,你可以使用搜索栏在 Elasticsearch 索引中搜索书籍,并按作者、类别和状态筛选结果。
执行全文和语义搜索
默认情况下,我们的应用程序将使用全文和语义搜索执行混合搜索。你可以通过创建两个单独的方法(一个用于全文,另一个用于语义搜索)来更改搜索逻辑,然后选择一种方法根据用户的输入构建查询。
将以下方法添加到 /Services/ElasticsearchService.cs 文件中的 ElasticsearchService 类:
1. private static Action<QueryDescriptor<BookDoc>> BuildSemanticQuery(
2. string searchTerm,
3. Dictionary<string, List<string>> selectedFacets
4. )
5. {
6. var filters = BuildFilters(selectedFacets);
8. return query =>
9. query.Bool(b =>
10. b.Must(m => m.Semantic(sem => sem.Field("longDescription").Query(searchTerm)))
11. .Filter(filters.ToArray())
12. );
13. }
15. private static Action<QueryDescriptor<BookDoc>> BuildMultiMatchQuery(
16. string searchTerm,
17. Dictionary<string, List<string>> selectedFacets
18. )
19. {
20. var filters = BuildFilters(selectedFacets);
22. if (string.IsNullOrEmpty(searchTerm))
23. {
24. return query => query.Bool(b => b.Filter(filters.ToArray()));
25. }
27. return query =>
28. query.Bool(b =>
29. b.Should(m =>
30. m.MultiMatch(mm =>
31. mm.Query(searchTerm).Fields(new[] { "title", "shortDescription" })
32. )
33. )
34. .Filter(filters.ToArray())
35. );
36. }
这两种方法的工作方式与 BuildHybridQuery 方法类似,但它们仅执行全文或语义搜索。
你可以修改 SearchBooksAsync 方法以使用所选的搜索方法:
1. public async Task<ElasticResponse> SearchBooksAsync(
2. string searchTerm,
3. Dictionary<string, List<string>> selectedFacets
4. )
5. {
6. try
7. {
8. _logger.LogInformation($"Performing search for: {searchTerm}");
10. // Modify the query builder to use the selected search method.
11. var multiMatchQuery = BuildMultiMatchQuery(searchTerm, selectedFacets); // For full text search
12. var semanticQuery = BuildSemanticQuery(searchTerm, selectedFacets); // For semantic search
14. // In this case we will not use retrievers, but you can add them if you want to use them.
15. var response = await _client.SearchAsync<BookDoc>(s =>
16. s.Index("elastic-blazor-books")
17. .Query(multiMatchQuery) // Change this line to use different search methods, for example: .Query(semanticQuery) for semantic search
18. .Aggregations(aggs =>
19. aggs.Add("Authors", agg => agg.Terms(t => t.Field(p => p.Authors)))
20. .Add(
21. "Categories",
22. agg => agg.Terms(t => t.Field(p => p.Categories))
23. )
24. .Add("Status", agg => agg.Terms(t => t.Field(p => p.Status)))
25. )
26. );
28. if (response.IsValidResponse)
29. {
30. _logger.LogInformation($"Found {response.Documents.Count} documents");
32. var hits = response.Total;
33. var facets =
34. response.Aggregations != null
35. ? FormatFacets(response.Aggregations)
36. : new Dictionary<string, Dictionary<string, long>>();
38. var elasticResponse = new ElasticResponse
39. {
40. TotalHits = hits,
41. Documents = response.Documents.ToList(),
42. Facets = facets,
43. };
45. return elasticResponse;
46. }
47. else
48. {
49. _logger.LogWarning($"Invalid response: {response.DebugInformation}");
50. return new ElasticResponse();
51. }
52. }
53. catch (Exception ex)
54. {
55. _logger.LogError(ex, "Error performing search");
56. return new ElasticResponse();
57. }
58. }
你可以在此处找到完整的应用程序。
结论
Blazor 是一个有效的框架,允许你使用 C# 构建 Web 应用程序。Elasticsearch 是一个强大的搜索引擎,允许你构建搜索应用程序。结合两者,你可以轻松构建强大的搜索应用程序,利用 ESRE 的强大功能在短时间内创建语义搜索体验。
准备好自己尝试一下了吗?开始免费试用。
Elasticsearch 集成了 LangChain、Cohere 等工具。加入我们的高级语义搜索网络研讨会,构建你的下一个 GenAI 应用程序!
原文:Blazor and Elasticsearch: How to build a search app — Search Labs