使用 Blazor 和 Elasticsearch 构建搜索应用程序

作者:来自 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) 节点。在此处了解更多信息。

首先创建推理端点:

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"
      }
   }
}

我们将创建一个能够运行语义和全文查询的索引。semantic_text 字段类型将负责数据分块和嵌入。请注意,我们将 longDescription 索引为 semantic_text,如果你想将字段同时索引为 semantic_text 和 text,则可以使用 copy_to。

构建应用程序

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.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 文件。你应该有以下导入:

@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 查询命中中返回的数据,这些数据将作为结果打印在我们的搜索页面中。

我们首先创建文件 /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 文件中设置 Elastic 响应并添加以下内容:

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; }
    }
}

配置基本 UI

接下来,添加 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);
  }
}

该组件从 author、categories 和 status 字段的 terms 聚合中读取内容,然后生成一个过滤器列表以发送回 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# 构建 Web 应用程序。Elasticsearch 是一个强大的搜索引擎,允许你构建搜索应用程序。结合两者,你可以轻松构建强大的搜索应用程序,利用 ESRE 的强大功能在短时间内创建语义搜索体验。

准备好自己尝试一下了吗?开始免费试用。

Elasticsearch 集成了 LangChain、Cohere 等工具。加入我们的高级语义搜索网络研讨会,构建你的下一个 GenAI 应用程序!

原文:Blazor and Elasticsearch: How to build a search app — Search Labs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/889013.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

解决新版Android studio不能连接手机的问题

我要说的是一个特例&#xff0c;装了22年的版本AS可以正常连接手机&#xff0c;装了23年以后新版本&#xff0c;AS不能正常连接手机了&#xff0c;但是在CMD控制台可以正常的执行adb命令&#xff0c;并且CMD和AS都是指向D:\android_sdk\platform-tools\adb.exe 一、 为什么会出…

ChatGPT完成论文润色的提示词分享

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 在论文写作的最后阶段&#xff0c;润色是确保文章质量的重要步骤。无论是语法检查、句式优化&#xff0c;还是提升语言的连贯性和一致性&#xff0c;润色都能显著提高论文的专业性与可读…

openstack-swift.18421165

对象存储 swift 对象存储 是一种用于存储和管理大量数据的系统。类似于一个超大云盘。可以存储各种文件。&#xff08;照片&#xff0c;视频&#xff0c;文档等等&#xff09;。与传统的文件存储不同&#xff0c;对下个存储不关心文件的目录结构和层级关系&#xff0c;而是将每…

【unity游戏开发】彻底理解AnimatorStateInfo,获取真实动画长度

前言 前置知识&#xff1a;设置参数后&#xff0c;下一个循环才会切换对应动画&#xff0c;所以在下一个循环获取真实的动画长度 AnimatorStateInfo是结构体&#xff01;值类型&#xff0c;要不断重复获取才是最新的 主要是自动设置trigger切换的动画自动切回上一个动画&#x…

【读书笔记·VLSI电路设计方法解密】问题7:什么是基于标准单元的专用集成电路 (ASIC) 设计方法论

标准单元方法论是一种基于预组装库单元的芯片设计方法。该库中包含的标准单元和宏单元(例如存储器、I/O、特殊功能单元、锁相环(PLLs)等)已经在预定的工艺节点中设计、布局并经过验证。这些单元经过完全表征,并在逻辑、时序、物理和电气模型方面进行了定义,并正确地打包在…

Day2 IDEA

使用IDEA开发第一个程序 代码结构&#xff1a;Project - Module - Package - Class 作用&#xff1a;便于管理代码 例如&#xff1a; 创建一个空工程 创建module模块 创建package&#xff0c;一般以公司域名倒写技术名称 例如&#xff1a;com.test.hello 创建类 class He…

Axios 网络请求

文章目录 Axios 网络请求1.Axios 使用1.Axios 简介2.Axios 安装安装命令 3.Axios 引入方式全局引入局部引入 2.整合 vue1.在组件中使用 axios 发送请求发送结果这里就出现了跨域问题 3.跨域后端解决办法全局配置类 加入注解 CrossOrigin请求结果 全局配置 baseUrl Axios 网络请…

Nodejs-Nestjs框架 RBAC(基于角色的访问控制模型) 微服务 仿小米商城实战视频教程-2024年-试看学习记录

文章目录 前提-安装环境Nestjs框架介绍Nestjs框架环境搭建创建nestjs项目运行nestjs项目demonestjs新项目结构解释nestjs中的控制器、路由、Get、Post、方法参数装饰器nestjs模板引擎、配置静态资源(了解即可)nestjs中的服务(Model)nestjs中的cookie(了解即可)nestjs中的se…

YOLOv11训练自己数据集_笔记1

一、前言 yolov11-main 官网 分析YOLO11的关键改进点 YOLO11 相比之前版本&#xff0c;带来了五大关键改进&#xff1a; 增强特征提取&#xff1a;通过改进Backbone和Neck架构&#xff0c;新增了C3k2和C2PSA等组件&#xff0c;提升了目标检测的精度。 优化效率和速度&#xf…

深入理解HTTP Cookie

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 HTTP Cookie定义工作原理分类安全性用途 认识 cookie基本格式实验测试 cookie 当我们登录了B站过后&#xff0c;为什么下次访问B站就…

ctfshow-web 萌新题

给她 spring漏洞 pyload: 1.dirsearch扫描&#xff0c;发现git 2. GitHack工具得到.git文件 <?php $passsprintf("and pass%s",addslashes($_GET[pass])); $sqlsprintf("select * from user where name%s $pass",addslashes($_GET[name])); ?>…

HTML5实现古典音乐网站源码模板1

文章目录 1.设计来源1.1 网站首页1.2 古典音乐界面1.3 著名人物界面1.4 古典乐器界面1.5 历史起源界面2.效果和源码2.1 动态效果2.2 源代码源码下载万套模板,程序开发,在线开发,在线沟通作者:xcLeigh 文章地址:https://blog.csdn.net/weixin_43151418/article/details/142…

aws(学习笔记第一课) AWS CLI,创建ec2 server以及drawio进行aws画图

aws(学习笔记第一课) 使用AWS CLI 学习内容&#xff1a; 使用AWS CLI配置密钥对创建ec2 server使用drawio&#xff08;vscode插件&#xff09;进行AWS的画图 1. 使用AWS CLI 注册AWS账号 AWS是通用的云计算平台&#xff0c;可以提供ec2&#xff0c;vpc&#xff0c;SNS以及clo…

无人机之飞控仿真技术篇

一、无人机飞控仿真技术的定义 无人机飞控仿真技术主要是指飞行控制系统仿真&#xff0c;它是以无人机的运动情况为研究对象&#xff0c;面向对象的复杂系统仿真。通过该技术&#xff0c;可以模拟无人机的飞行过程&#xff0c;评估飞行控制系统的性能&#xff0c;优化飞行参数&…

【Linux:线程控制】

目录 线程的创建与等待&#xff1a; ​编辑 代码中tid是什么&#xff1f; 如何看待线程函数传参&#xff1f; ​编辑 ​编辑创建多线程&#xff1a;​编辑 终止多线程&#xff1a; 线程分离&#xff1a; 线程封装&#xff1a; 线程的创建与等待&#xff1a; void *thre…

leetcode125:验证回文串

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后&#xff0c;短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。 字母和数字都属于字母数字字符。 给你一个字符串 s&#xff0c;如果它是 回文串 &#xff0c;返回 true &#xff1b;否则&#…

华为OD机试 - 贪吃蛇 - 队列(Python/JS/C/C++ 2024 E卷 200分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试真题&#xff08;Python/JS/C/C&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加入华为OD刷题交流群&#xff0c;…

计算机网络:数据链路层 —— 数据链路层概述

文章目录 数据链路层主要功能 基本概念链路数据链路帧 数据链路层 在计算机网络中&#xff0c;链路层&#xff08;Data Link Layer&#xff09;是网络协议栈中的一层&#xff0c;负责管理和控制链路的建立、维护和释放&#xff0c;以及处理链路层的数据帧传输和错误控制等功能…

Linux入门3——vim的简单使用

1.vim 1.1 vim的模式 vim有三种主要模式&#xff1a; ①命令模式&#xff1a;使用vim刚打开进入的模式就是命令模式&#xff1b; ②插入模式&#xff1a;只有在插入模式下才可以做文字输入&#xff0c;按[Esc]键可退回命令模式&#xff1b; ③末行模式&#xff1a;文件保存或退…

大数据毕业设计选题推荐-王者荣耀战队数据分析-Python数据可视化-Hive-Hadoop-Spark

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…