<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[.NET - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[.NET - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/cn/search-labs/blog/category/dot-net-programming</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/blog/category/dot-net-programming</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/category/dot-net-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 11:56:50 GMT</lastBuildDate>
  <item>
    <title><![CDATA[如何使用用于微软语义内核（Microsoft Semantic Kernel）的Elasticsearch矢量存储连接器进行人工智能代理开发]]></title>
    <description><![CDATA[微软语义内核（Microsoft Semantic Kernel）是一款轻量级开源开发工具包，可让您轻松构建人工智能代理，并将最新的人工智能模型集成到您的 C#、Python 或 Java 代码库中。随着Semantic Kernel Elasticsearch向量存储连接器（Elasticsearch Vector Store Connector）的发布，使用Semantic Kernel构建人工智能代理的开发人员现在可以将Elasticsearch作为可扩展的企业级向量存储插件，同时继续使用Semantic Kernel抽象。]]></description>
    <content:encoded><![CDATA[<p>我们与<a href="https://learn.microsoft.com/en-us/semantic-kernel/overview/"> 微软语义内核</a> （ Microsoft<a href="https://learn.microsoft.com/en-us/semantic-kernel/overview/"> Semantic Kernel ）团队合作，宣布面向 微软语义内核</a> （.NET）用户推出<a href="https://github.com/elastic/semantic-kernel-net/"> Semantic Kernel Elasticsearch矢量存储连接器（Vector Store Connector ）。</a>语义内核（Semantic Kernel）简化了企业级人工智能代理的构建过程，包括利用来自矢量存储库（Vector Store）的更多相关数据驱动响应来增强大型语言模型（LLM）的能力。语义内核（Semantic Kernel）为与Elasticsearch等矢量存储进行交互提供了一个无缝的抽象层，可提供创建、列出和删除记录集合以及上传、检索和删除单条记录等基本功能。</p><p><a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/out-of-the-box-connectors/elasticsearch-connector?pivots=programming-language-csharp">开箱即用的Semantic Kernel Elasticsearch向量存储连接器（Vector Store Connector</a>）支持Semantic Kernel<a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/?pivots=programming-language-csharp#the-vector-store-abstraction">向量存储抽象</a>，这使得开发人员在构建人工智能代理时能够非常容易地将Elasticsearch作为向量存储插件。</p><p>Elasticsearch 在开源社区拥有坚实的基础，最近采用了<a href="https://www.elastic.co/blog/elasticsearch-is-open-source-again">AGPL 许可证</a>。这些工具与开源的微软语义内核（Microsoft Semantic Kernel）相结合，可提供强大的企业级解决方案。您可以通过运行此命令<code>curl -fsSL https://elastic.co/start-local | sh </code> ，在几分钟内启动 Elasticsearch（详情请参考<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/run-elasticsearch-locally.html">start-local</a>），然后在生产人工智能代理的同时，迁移到<a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;utm_source=semantickernel&amp;utm_content=documentation">云托管</a>或<a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.16/install-elasticsearch.html">自托管</a>版本。</p><p>在本篇博客中，我们将探讨在使用Semantic Kernel（语义内核）时，如何使用<a href="https://github.com/elastic/semantic-kernel-net/">Semantic Kernel Elasticsearch向量存储连接器</a>。该连接器的 Python 版本将在未来推出。</p><h2>高级应用场景：利用 Semantic Kernel&amp; Elasticsearch 构建 RAG 应用程序</h2><p>下面我们将举例说明。在高层次上，我们正在构建一个 RAG（检索增强生成）应用程序，它将用户的问题作为输入，并返回一个答案。我们将使用 Azure OpenAI （ 也可使用<a href="https://devblogs.microsoft.com/semantic-kernel/introducing-new-ollama-connector-for-local-models/"> 本地 LLM</a> ）作为 LLM，Elasticsearch 作为向量存储，Semantic Kernel (.net) 作为将所有组件连接在一起的框架。</p><p>如果您不熟悉 RAG 架构，可以通过以下文章快速了解<a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">： https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag。</a></p><p>答案由 LLM 生成，LLM 从 Elasticsearch 向量存储中获取与问题相关的上下文。答复还包括法律硕士用作背景的资料来源。</p><h3>RAG 示例</h3><p>在这个具体例子中，我们创建了一个应用程序，允许用户就内部酒店数据库中存储的酒店提出问题。例如，用户可以根据不同标准搜索特定酒店，或要求提供酒店列表。</p><p>在示例数据库中，我们生成了一个包含 100 个条目的<a href="https://github.com/elastic/semantic-kernel-net/blob/main/Elastic.SemanticKernel.Playground/hotels.csv">酒店列表</a>。为了让您尽可能轻松地试用连接器演示，我们特意设置了较小的样本量。在实际应用中，Elasticsearch 连接器将显示出其优于其他选项（如 "InMemory "向量存储实现）的优势，尤其是在处理超大数据量时。</p><p>完整的演示应用程序可在 Elasticsearch 向量存储连接器存储<a href="https://github.com/elastic/semantic-kernel-net/tree/main/Elastic.SemanticKernel.Playground">库中</a>找到。</p><p>让我们先将所需的 NuGet 软件包和指令添加到项目中：</p>dotnet add package "Elastic.Clients.Elasticsearch" -v 8.16.2
dotnet add package "Elastic.SemanticKernel.Connectors.Elasticsearch" -v 0.1.2
dotnet add package "Microsoft.Extensions.Hosting" -v 9.0.0
dotnet add package "Microsoft.SemanticKernel.Connectors.AzureOpenAI" -v 1.30.0
dotnet add package "Microsoft.SemanticKernel.PromptTemplates.Handlebars" -v 1.30.0using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Data;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;<p>现在，我们可以创建我们的数据模型，并为其提供语义内核（Semantic Kernel）的特定属性，以定义存储模型模式和文本搜索的一些提示：</p>/// &lt;summary&gt;
/// Data model for storing a "hotel" with a name, a description, a  description embedding and an optional reference link.
/// &lt;/summary&gt;
public sealed record Hotel
{
	[VectorStoreRecordKey]
	public required string HotelId { get; set; }

	[TextSearchResultName]
	[VectorStoreRecordData(IsFilterable = true)]
	public required string HotelName { get; set; }

	[TextSearchResultValue]
	[VectorStoreRecordData(IsFullTextSearchable = true)]
	public required string Description { get; set; }

	[VectorStoreRecordVector(Dimensions: 1536, DistanceFunction.CosineSimilarity, IndexKind.Hnsw)]
	public ReadOnlyMemory&lt;float&gt;? DescriptionEmbedding { get; set; }

	[TextSearchResultLink]
	[VectorStoreRecordData]
	public string? ReferenceLink { get; set; }
}<p>存储模型模式属性（`VectorStore*`）与 Elasticsearch 向量存储连接器的实际使用最为相关，即</p><p></p><ul><li><p><code>VectorStoreRecordKey</code> 来标记记录类上的一个属性，作为记录存储在向量存储中的键。</p></li><li><p><code>VectorStoreRecordData</code> 将记录类的一个属性标记为 "数据"。</p></li><li><p><code>VectorStoreRecordVector</code> 将记录类的一个属性标记为矢量。</p></li></ul><p>所有这些属性都接受各种可选参数，可用于进一步定制存储模型。以<code>VectorStoreRecordKey </code> 为例，可以指定不同的距离函数或不同的索引类型。</p><p>文本搜索属性 (<code>TextSearch*</code>) 在本示例的最后一步中非常重要。我们稍后再谈。</p><p>下一步，我们将初始化语义内核引擎，并获取核心服务的引用。在实际应用中，应使用<a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection">依赖注入</a>而不是直接访问服务集合。同样的道理也适用于硬编码的配置和秘密，它们应该使用<a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration">配置提供程序</a>来读取：</p>var builder = Host.CreateApplicationBuilder(args);

// Register AI services.
var kernelBuilder = builder.Services.AddKernel();

kernelBuilder.AddAzureOpenAIChatCompletion("gpt-4o", "https://my-service.openai.azure.com", "my_token");

kernelBuilder.AddAzureOpenAITextEmbeddingGeneration("ada-002", "https://my-service.openai.azure.com", "my_token");

// Register text search service.
kernelBuilder.AddVectorStoreTextSearch&lt;Hotel&gt;();

// Register Elasticsearch vector store.
var elasticsearchClientSettings = new ElasticsearchClientSettings(new Uri("https://my-elasticsearch-instance.cloud"))
    .Authentication(new BasicAuthentication("elastic", "my_password"));

kernelBuilder.AddElasticsearchVectorStoreRecordCollection&lt;string, Hotel&gt;("skhotels", elasticsearchClientSettings);

// Build the host.
using var host = builder.Build();

// For demo purposes, we access the services directly without using a DI context.

var kernel = host.Services.GetService&lt;Kernel&gt;()!;
var embeddings = host.Services.GetService&lt;ITextEmbeddingGenerationService&gt;()!;
var vectorStoreCollection = host.Services.GetService&lt;IVectorStoreRecordCollection&lt;string, Hotel&gt;&gt;()!;

// Register search plugin.
var textSearch = host.Services.GetService&lt;VectorStoreTextSearch&lt;Hotel&gt;&gt;()!;
kernel.Plugins.Add(textSearch.CreateWithGetTextSearchResults("SearchPlugin"));<p>现在可以使用<code>vectorStoreCollection</code> 服务创建数据集，并摄取一些<a href="https://github.com/elastic/semantic-kernel-net/blob/main/Elastic.SemanticKernel.Playground/hotels.csv">演示记录</a>：</p>await vectorStoreCollection.CreateCollectionIfNotExistsAsync();

// CSV format: ID;Hotel Name;Description;Reference Link
var hotels = (await File.ReadAllLinesAsync("hotels.csv"))
    .Select(x =&gt; x.Split(';'));

foreach (var chunk in hotels.Chunk(25))
{
    var descriptionEmbeddings = await embeddings.GenerateEmbeddingsAsync(chunk.Select(x =&gt; x[2]).ToArray());
    
    for (var i = 0; i &lt; chunk.Length; ++i)
    {
        var hotel = chunk[i];
        await vectorStoreCollection.UpsertAsync(new Hotel
        {
            HotelId = hotel[0],
            HotelName = hotel[1],
            Description = hotel[2],
            DescriptionEmbedding = descriptionEmbeddings[i],
            ReferenceLink = hotel[3]
        });
    }
}<p>由此可见，语义内核（Semantic Kernel）是如何将向量存储的使用及其复杂性简化为几个简单的方法调用的。</p><p>在 Elasticsearch 中创建一个新索引，并创建所有必要的属性映射。然后，我们的数据集会完全透明地映射到存储模型中，并最终存储到索引中。下面是映射在 Elasticsearch 中的显示方式。</p>{
  "mappings": {
    "properties": {
      "descriptionEmbedding": {
        "dims": 1536,
        "index": true,
        "index_options": {
          "type": "hnsw"
        },
        "similarity": "cosine",
        "type": "dense_vector"
      },
      "hotelName": {
        "type": "keyword"
      },
      "description": {
        "type": "text"
      }
    }
  }
}<p><code>embeddings.GenerateEmbeddingsAsync()</code> 会透明地调用已配置的 Azure AI 嵌入生成服务。</p><p>在这个演示的最后一个步骤中，我们还可以看到更多的神奇之处。</p><p>当用户就数据提问时，只需调用<code>InvokePromptAsync</code> ，就能执行以下所有操作：</p><p>1.为用户的问题生成嵌入代码</p><p>2.在矢量存储器中搜索相关条目</p><p>3.将查询结果插入提示模板</p><p>4.最终提示形式的实际查询将发送到人工智能聊天完成服务</p>// Invoke the LLM with a template that uses the search plugin to
// 1. get related information to the user query from the vector store
// 2. add the information to the LLM prompt.
var response = await kernel.InvokePromptAsync(
    promptTemplate: """
                    Please use this information to answer the question:
                    {{#with (SearchPlugin-GetTextSearchResults question)}}
                      {{#each this}}
                        Name: {{Name}}
                        Value: {{Value}}
                        Source: {{Link}}
                        -----------------
                      {{/each}}
                    {{/with}}
                    
                    Include the source of relevant information in the response.

                    Question: {{question}}
                    """,
    arguments: new KernelArguments
    {
        { "question", "Please show me all hotels that have a rooftop bar." },
    },
    templateFormat: "handlebars",
    promptTemplateFactory: new HandlebarsPromptTemplateFactory());<p>还记得我们之前在数据模型上定义的<code>TextSearch*</code> 属性吗？有了这些属性，我们就能在提示模板中使用相应的占位符，这些占位符会根据向量存储中的条目信息自动填充。</p><p>对于我们的问题"，请告诉我所有拥有屋顶酒吧的酒店。" ，最终答复如下：</p>Console.WriteLine(response.ToString());

// &gt; The hotel that has a rooftop bar is Skyline Suites. You can find more information about this hotel [here](https://example.com/yz567).<p>正确答案是指 hotels.csv 中的以下条目</p>9;
Skyline Suites;
Offering panoramic city views from every suite, this hotel is perfect for those who love the urban landscape. Enjoy luxurious amenities, a rooftop bar, and close proximity to attractions. Luxurious and contemporary.;
https://example.com/yz567<p>这个例子很好地说明了微软语义内核的使用是如何通过其深思熟虑的抽象功能大大降低复杂性，并实现高度灵活性的。例如，只需修改一行代码，就可以更换向量存储或所使用的人工智能服务，而无需重构代码的任何其他部分。</p><p>同时，该框架还提供了大量高级功能，如 "InvokePrompt "函数或模板或搜索插件系统。</p><p>完整的演示应用程序可在 Elasticsearch 向量存储连接器存储库中找到。</p><h2>Elasticsearch 还能做什么</h2><ul><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Elasticsearch 新语义文本映射：简化语义搜索</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-reranking-with-retrievers">利用检索器在 Elasticsearch 中进行语义重排</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1">高级 RAG 技术第 1 部分：数据处理</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2">高级 RAG 技术第 2 部分：查询和测试</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-rag-with-llama3-opensource-and-elastic">使用 Llama 3 开放源代码和 Elastic 构建 RAG</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/local-rag-agent-elasticsearch-langgraph-llama3">使用 LangGraph、LLaMA3 和 Elasticsearch 向量存储从零开始构建本地代理的教程</a></p></li></ul><h2>Elasticsearch&amp; Semantic Kernel（语义内核）：下一步是什么？</h2><ul><li><p>我们展示了在.NET中构建GenAI应用时，如何将Elasticsearch向量存储轻松插入Semantic Kernel。敬请期待下一步的 Python 集成。</p></li><li><p>由于Semantic Kernel（语义内核）为<a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">混合</a>搜索等高级搜索功能建立了抽象，Elasticsearch连接将使.NET开发人员能够在使用Semantic Kernel（语义内核）的同时轻松实现这些功能。</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-connector-microsoft-semantic-kernel</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-connector-microsoft-semantic-kernel</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[.NET]]></category>
    <category><![CDATA[向量数据库]]></category>
    <dc:creator><![CDATA[Florian Bernd,Srikanth Manvi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d8725035e86f8a8/6a17fe447f6f1564f8c09d74/0564fe794e4c66d0507317822d7aa71826183d20-1311x762.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[使用 Blazor 和 Elasticsearch 构建搜索应用]]></title>
    <description><![CDATA[了解如何使用 Blazor 和 Elasticsearch 构建搜索应用程序，以及如何使用 Elasticsearch .NET 客户端进行混合搜索。]]></description>
    <content:encoded><![CDATA[<p>在本文中，您将学习如何利用 C# 技能使用 Blazor 和 Elasticsearch 构建搜索应用程序。我们将使用<a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html">Elasticsearch .NET</a>客户端运行<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html">全文</a>、<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">语义</a>和<a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">混合</a>搜索查询。</p><p><strong>注意</strong>如果您熟悉旧版本的 Elasticsearch C# 客户端<a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest.html">NEST</a>，请阅读这篇关于 NEST 客户端弃用和新功能的<a href="https://www.elastic.co/search-labs/blog/net-client-evolution">博文</a>。<em>NEST 是上一代的 .NET 客户端，后来被 </em><code>Elastic.Clients.Elasticsearch package</code></p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltca8d1da68641fac0/6a17f65763173044d4585bfb/18f890286ca122cd97286c09ebf740208b802d0b-650x395.png" alt="使用 blazor 图表构建 blazor 应用程序：ESRE" /><ul><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-blazor?">什么是 Blazor？</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-esre?">什么是 ESRE？</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#configuring-elser">配置 ELSER</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#indexing-data">索引数据</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#building-the-app-with-blazor-&amp;-elasticsearch">建筑应用</a></p></li></ul><h2>什么是 Blazor？</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91278ae422883e28/6a17f6592f4a5c3213fa8a95/622741915d016b68bf94f742332d10d736d60052-707x461.png" alt="Blazor 服务器" /><p><a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">Blazor</a>是微软开发的基于 HTML、CSS 和 C# 的开放源代码网络框架，允许开发人员构建可在客户端或服务器上运行的网络应用程序。Blazor 还允许您制作可重复使用的组件，以更快地构建应用程序；它使开发人员能够在同一个文件中构建 HTML 视图和 C# 操作，这有助于保持代码的可读性和简洁性。此外，有了Blazor Hybrid，您还可以通过.NET代码构建本地移动应用程序，访问本地平台功能。</p><p>Blazor 的部分功能使其成为一个非常适合工作的框架：</p><ul><li><p>服务器端和客户端渲染选项</p></li><li><p>可重复使用的用户界面组件</p></li><li><p>利用 SignalR 实时更新</p></li><li><p>内置状态管理</p></li><li><p>内置路由系统</p></li><li><p>强大的类型和编译时检查</p></li></ul><h3>为什么选择 Blazor？</h3><p>与其他框架和库相比，Blazor 具有以下优势：它允许开发人员在客户端和服务器代码中使用 C#，提供强大的类型和编译时检查功能，从而提高了可靠性。它与 .NET 生态系统无缝集成，实现了 .NET 库和工具的重用，并提供强大的调试支持。</p><h2>什么是 ESRE？</h2><p><a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">Elasticsearch Relevance Engine™ (ESRE)</a>是一套在强大的 Elasticsearch 搜索引擎基础上使用机器学习和人工智能<a href="https://www.elastic.co/guide/en/esre/current/learn.html">构建搜索应用程序的工具</a>。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf313ba986de01b93/6a17d851505ac306fcad8975/4c0f2645ed1c27fe3ef61a1a9126adadfd8d5368-721x421.png" alt="盐" /><p>要了解有关 ESRE 的更多信息，请<a href="https://www.elastic.co/search-labs/blog/introducing-elasticsearch-relevance-engine-esre">点击此处</a>阅读我们的博文。</p><h2>配置 ELSER</h2><p>为了充分利用 Elastic 的<a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">ESRE</a>功能，我们将使用<a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a>作为模型提供者。</p><p><em>请注意，要使用 Elasticsearch 的 ELSER 模型，您必须拥有白金级或企业级许可证，并至少拥有一个 4GB 大小的专用机器学习 (ML) 节点。</em><a href="https://www.elastic.co/guide/en/machine-learning/8.15/ml-nlp-elser.html#elser-req"><em> 点击此处 了解更多信息 。</em></a></p><p>首先创建推理端点：</p>PUT _inference/sparse_embedding/my-elser-model
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}<p>如果您是第一次使用 ELSER，在后台加载模型时可能会遇到 502 Bad Gateway 错误。您可以在 Kibana 的<code>Machine Learning &gt; Trained Models</code> 中查看模型的状态。部署完成后，就可以进行下一步。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee295a3516338302/6a17f65b414c640deb94531d/a7ff94b72d337cde892165d744b9f42fba702a87-1440x649.png" alt="检查训练有素的模型" /><h2>索引数据</h2><p>您可以<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor/books.zip">在这里</a>下载数据集，然后使用 Kibana 导入数据。为此，请访问主页并点击"Upload data" 。然后，上传文件并点击<code>Import</code> 。最后，进入<code>Advanced</code> 标签，粘贴以下映射：</p>{
   "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"
      }
   }
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f07300647edd2ca/6a17f65dfaa913172a93ca0c/1aea0b9c51e275f89339f5d463fcaef799fc3943-1235x1083.png" alt="导入数据" /><p>我们将创建一个能够运行语义和全文查询的索引。<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">语义文本字段</a>类型将负责数据分块和嵌入。<em>请注意，我们是将 索引</em><em><code>longDescription</code></em><em> 为</em><em><code>semantic_text</code></em><em> ， 如果要将一个字段 索引 为</em><em><code>semantic_text</code></em><em> 和 text` ， 可以使用</em> copy_to 。</p><h2>使用 Blazor&amp; Elasticsearch 构建应用程序</h2><h3>API 密钥</h3><p>我们需要做的第一件事是创建一个 API 密钥，以验证对 Elasticsearch 的请求。API 密钥应为只读，只允许查询<code>books-blazor</code> 索引。</p>POST /_security/api_key
{
  "name": "books-blazor-key",
  "role_descriptors": {
    "books-blazor-reader": {
      "indices": [
        {
          "names": ["books-blazor"],
          "privileges": ["read"]
        }
      ]
    }
  }
}<p>你会看到类似这样的内容：
</p>{
  "id": "XXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "books-blazor-key",
  "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}<p>保存<code>encoded</code> 响应字段的值，以备不时之需。如果您在<a href="https://www.elastic.co/cloud/">Elastic Cloud</a> 上运行，还需要您的 Cloud ID。(您可以<a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id">在此处</a>找到）。</p><h4>创建 Blazor 项目</h4><p>首先安装 Blazor，并按照<a href="https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/install">官方说明</a>创建一个示例项目。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5c20b896fe1e98e/6a17f65f6317301566585bff/f5720867d960c91fd1b3c4ae9be174e062e6b2fc-975x830.png" alt="Blazor 教程 - 创建 Blazor 项目" /><p>创建项目后，文件夹结构和文件应如下所示：</p>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/ <p>模板应用程序包括<a href="https://blog.getbootstrap.com/2021/08/04/bootstrap-5-1-0/">Bootstrap v5.1.0</a>用于造型。</p><p>安装<a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/8.0/installation.html">Elasticsearch .NET</a>客户端，完成项目设置：</p>dotnet add package Elastic.Clients.Elasticsearch<p>完成这一步后，您的页面应该是这样的：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b90e394ded7ff56/6a17f660faa913d49f93ca10/ef9d2186c2cf49e78a307c4aa69c51632e84578d-940x529.png" alt="Blazor hello world" /><h3>文件夹结构</h3><p>现在，我们将按如下方式整理文件夹：</p>BlazorApp/
|-- Components/
|   |-- Pages/
|   |   |-- Search.razor
|   |   `-- Search.razor.css
|   `-- Elasticsearch/
|       |-- SearchBar.razor
|       |-- Results.razor
|       `-- Facet.razor
|-- Models/
|   |-- Book.cs
|   `-- Response.cs
`-- Services/
    `-- ElasticsearchService.cs<p>文件解释：</p><ul><li><p>Components/Pages/Search.razor：包含搜索栏、搜索结果和过滤器的主页面。</p></li><li><p>Components/Pages/Search.razor.css：页面样式。</p></li><li><p>Components/Elasticsearch/SearchBar.razor：搜索栏组件。</p></li><li><p>Components/Elasticsearch/Results.razor：结果组件。</p></li><li><p>Components/Elasticsearch/Facet.razor: 过滤器组件。</p></li><li><p>Components/Svg/GlassIcon.razor: 搜索图标。</p></li><li><p>Components/_Imports.razor：这将导入所有组件。</p></li><li><p>Models/Book.cs：这将存储图书字段模式。</p></li><li><p>Models/Response.cs：这将存储响应模式，包括搜索结果、面和总点击数。</p></li><li><p>Services/ElasticsearchService.cs：Elasticsearch 服务。它将处理与 Elasticsearch 的连接和查询。</p></li></ul><h4>初始配置</h4><p>我们先来清理一下。</p><p>删除文件：</p><ul><li><p>Components/Pages/Counter.razor</p></li><li><p>Components/Pages/Weather.razor</p></li><li><p>Components/Pages/Home.razor</p></li><li><p>Components/Layout/NavMenu.razor</p></li><li><p>Components/Layout/NavMenu.razor.css</p></li></ul><p>检查<code>/Components/_Imports.razor</code> 文件。您应该有以下进口：</p>@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<h4>将 Elastic 集成到项目中</h4><p>现在，让我们导入 Elasticsearch 组件：</p>@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 @* &lt;--- Add this line *@<p>我们将从<code>/Components/Layout/MainLayout.razor</code> 文件中删除默认侧边栏，以便为应用程序提供更多空间：</p>@inherits LayoutComponentBase

&lt;div class="page"&gt;
    &lt;main&gt;
        &lt;article class="content"&gt;
            @Body
        &lt;/article&gt;
    &lt;/main&gt;
&lt;/div&gt;

&lt;div id="blazor-error-ui"&gt;
    An unhandled error has occurred.
    &lt;a href="" class="reload"&gt;Reload&lt;/a&gt;
    &lt;a class="dismiss"&gt;🗙&lt;/a&gt;
&lt;/div&gt;<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt441012c5aeac85f8/6a17f661ec0f8949f15a67ac/55bfff607f9167a532f83ad074b479364a346c6e-816x472.png" alt="从布局中移除导航条" /><p>现在让我们输入<a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&amp;tabs=linux#secret-manager">用户机密</a>的 Elasticsearch 凭据：</p>dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"<p>使用这种方法，.Net 8 可将敏感数据存储在项目文件夹之外的单独位置，并可通过<code>IConfiguration</code> 界面进行访问。任何使用相同用户秘密的 .Net 项目都可以使用这些变量。</p><p>然后，让我们修改<code>Program.cs</code> 文件以读取机密并挂载 Elasticsearch 客户端：</p><p>首先，导入必要的库：</p>using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;<ul><li><p>BlazorApp.Services：包含 Elasticsearch 服务。</p></li><li><p>Elastic.Clients.Elasticsearch：导入 Elasticsearch 客户端 .Net 8 库。</p></li><li><p>Elastic.Transport：导入 Elasticsearch 传输库，它允许我们使用 ApiKey 类来验证请求。</p></li></ul><p>其次，在<code>var app = builder.Build()</code> 行之前插入以下代码：</p>// Initialize the Elasticsearch client.
builder.Services.AddScoped(sp =&gt;
{
    // Getting access to the configuration service to read the Elasticsearch credentials.
    var configuration = sp.GetRequiredService&lt;IConfiguration&gt;();
    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);
});<p>这段代码将从用户机密中读取 Elasticsearch 凭据，并创建一个 Elasticsearch 客户端实例。</p><p>在初始化 ElasticSearch 客户端后，添加以下一行以注册 Elasticsearch 服务：</p>builder.Services.AddScoped&lt;ElasticsearchService&gt;();<p>下一步将在<code>/Services/ElasticsearchService.cs</code> 文件中建立搜索逻辑：</p><p>首先，导入必要的库和模型：</p>using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;<p>其次，添加类<code>ElasticsearchService</code> 、构造函数和变量：</p>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&lt;ElasticsearchService&gt; _logger;

        public ElasticsearchService(
            ElasticsearchClient client,
            ILogger&lt;ElasticsearchService&gt; logger
        )
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _logger = logger;
        }
    }
}<h4>配置搜索</h4><p>现在，让我们构建搜索逻辑：</p>private static Action&lt;RetrieverDescriptor&lt;BookDoc&gt;&gt; BuildHybridQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    return retrievers =&gt;
        retrievers.Rrf(rrf =&gt;
            rrf.RankWindowSize(50)
                .RankConstant(20)
                .Retrievers(
                    retrievers =&gt;
                        retrievers.Standard(std =&gt;
                            std.Query(q =&gt;
                                q.Bool(b =&gt;
                                    b.Must(m =&gt;
                                            m.MultiMatch(mm =&gt;
                                                mm.Query(searchTerm)
                                                    .Fields(
                                                        new[]
                                                        {
                                                            "title",
                                                            "shortDescription",
                                                        }
                                                    )
                                            )
                                        )
                                        .Filter(filters.ToArray())
                                )
                            )
                        ),
                    retrievers =&gt;
                        retrievers.Standard(std =&gt;
                            std.Query(q =&gt;
                                q.Bool(b =&gt;
                                    b.Must(m =&gt;
                                            m.Semantic(sem =&gt;
                                                sem.Field("longDescription")
                                                    .Query(searchTerm)
                                            )
                                        )
                                        .Filter(filters.ToArray())
                                )
                            )
                        )
                )
        );
}

public static List&lt;Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt;&gt; BuildFilters(
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = new List&lt;Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt;&gt;();

    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 =&gt; m.Term(t =&gt; t.Field(new Field(field)).Value(value)));
                }
            }
        }
    }

    return filters;
}<ul><li><p><code>BuildFilters</code> 将使用用户选择的面为搜索查询建立过滤器。</p></li><li><p><code>BuildHybridQuery</code> 将建立一个结合全文和语义搜索的<a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">混合</a>搜索查询。</p></li></ul><p>接下来，添加搜索方法：</p>public async Task&lt;ElasticResponse&gt; SearchBooksAsync(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; 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&lt;BookDoc&gt;(s =&gt;
            s.Index("elastic-blazor-books")
                .Retriever(retrieverQuery)
                .Aggregations(aggs =&gt;
                    aggs.Add("Authors", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Authors)))
                        .Add(
                            "Categories",
                            agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Categories))
                        )
                        .Add("Status", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; 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&lt;string, Dictionary&lt;string, long&gt;&gt;();

            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&lt;string, Dictionary&lt;string, long&gt;&gt; FormatFacets(
    Elastic.Clients.Elasticsearch.Aggregations.AggregateDictionary aggregations
)
{
    var facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();

    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 =&gt; b.Key, b =&gt; b.DocCount)
            );
            facets[facetName] = facetDictionary;
        }
    }

    return facets;
}

private static Dictionary&lt;string, long&gt; ConvertFacetDictionary(
    Dictionary&lt;Elastic.Clients.Elasticsearch.FieldValue, long&gt; original
)
{
    var result = new Dictionary&lt;string, long&gt;();
    foreach (var kvp in original)
    {
        result[kvp.Key.ToString()] = kvp.Value;
    }
    return result;
}<ul><li><p><code>SearchBooksAsync</code>该功能将使用混合查询执行搜索，并返回结果，其中包括用于构建切面的聚合。</p></li><li><p><code>FormatFacets</code>：将聚合响应格式化为字典。</p></li><li><p><code>ConvertFacetDictionary</code>：将面字典转换为更易读的格式。</p></li></ul><p>下一步是创建模型，这些模型将代表 Elasticsearch 查询<code>hits</code> 中返回的数据，这些数据将作为结果打印在搜索页面中。</p><p>我们首先创建文件<code>/Models/Book.cs</code> 并添加以下内容：</p>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&lt;string&gt;? Authors { get; set; }
        public List&lt;string&gt;? Categories { get; set; }
    }

    public class LongDescription
    {
        public string? Text { get; set; }
    }
}<p>然后，在<code>/Models/Response.cs</code> 文件中设置弹性响应，并添加以下内容：</p>namespace BlazorApp.Models
{
    public class ElasticResponse
    {
        public ElasticResponse()
        {
            Documents = new List&lt;BookDoc&gt;();
            Facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();
        }

        public long TotalHits { get; set; }
        public List&lt;BookDoc&gt; Documents { get; set; }
        public Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; Facets { get; set; }
    }
}<h4>配置基本用户界面</h4><p>接下来，添加 SearchBar 组件。在文件<code>/Components/Elasticsearch/SearchBar.razor</code> 中添加以下内容：</p>@using System.Threading.Tasks

&lt;form @onsubmit="SubmitSearch"&gt;
  &lt;div class="input-group mb-3"&gt;
    &lt;input type="text" @bind-value="searchTerm" class="form-control" placeholder="Enter search term..." /&gt;
    &lt;button type="submit" class="btn btn-primary input-btn"&gt;
      &lt;span class="input-group-svg"&gt;
        Search
      &lt;/span&gt;
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/form&gt;

@code {
  [Parameter]
  public EventCallback&lt;string&gt; OnSearch { get; set; }

  private string searchTerm = "";

  private async Task SubmitSearch()
  {
    await OnSearch.InvokeAsync(searchTerm);
  }
}<p>该组件包含一个搜索栏和一个执行搜索的按钮。</p><p>Blazor 允许在同一文件中使用 C# 代码动态生成 HTML，具有极大的灵活性。</p><p>之后，我们将在<code>/Components/Elasticsearch/Results.razor</code> 文件中构建显示搜索结果的结果组件：</p>@using BlazorApp.Models

@if (SearchResults != null &amp;&amp; SearchResults.Any())
{
  &lt;div class="row"&gt;
  @foreach (var result in SearchResults)
    {
      &lt;div class="col-12 mb-3"&gt;
        &lt;div class="card"&gt;
          &lt;div class="row g-0"&gt;
            &lt;div class="col-md-3 image-container"&gt;
              @if (!string.IsNullOrEmpty(result?.ThumbnailUrl))
              {
                &lt;img src="@result?.ThumbnailUrl" class="img-fluid rounded-start" alt="Thumbnail"&gt;
              }
              else
              {
                &lt;div class="placeholder"&gt;
                  @result?.Title
                &lt;/div&gt;
              }
            &lt;/div&gt;

            &lt;div class="col-md-9"&gt; &lt;!-- Adjusted to use the remaining 75% --&gt;
              &lt;div class="card-body"&gt;
                &lt;h4 class="card-title"&gt;
                  @result?.Title
                &lt;/h4&gt;

                &lt;div class="details-container"&gt;
                  &lt;div class=""&gt;

                    @if (result?.Authors?.Any() == true)
                    {
                      &lt;p class="card-text p-first"&gt;
                        Authors: &lt;small class="text-muted"&gt;@string.Join(", ", result.Authors)&lt;/small&gt;
                      &lt;/p&gt;
                    }

                    @if (result?.Categories?.Any() == true)
                    {
                      &lt;p class="card-text p-second"&gt;
                        Categories: &lt;small class="text-muted"&gt;@string.Join(", ", result.Categories)&lt;/small&gt;
                      &lt;/p&gt;
                    }
                  &lt;/div&gt;
                  &lt;div class="numPages-status"&gt;
                    @if (result?.PageCount != null)
                    {
                      &lt;p class="card-text p-first"&gt;
                        Pages: &lt;small class="text-muted"&gt;@result.PageCount&lt;/small&gt;
                      &lt;/p&gt;
                    }

                    @if (result?.Status != null)
                    {
                      &lt;p class="card-text p-second"&gt;
                        Status: &lt;small class="text-muted"&gt;@result.Status&lt;/small&gt;
                      &lt;/p&gt;
                    }
                  &lt;/div&gt;
                &lt;/div&gt;

                &lt;div class="long-text-container"&gt;
                  &lt;p class="card-text"&gt;&lt;small class="text-muted"&gt;@result?.LongDescription?.Text&lt;/small&gt;&lt;/p&gt;
                &lt;/div&gt;
                @if (!string.IsNullOrEmpty(result?.PublishedDate))
                {
                  &lt;div class="date-container"&gt;
                    &lt;p class="card-text"&gt;
                      Published Date: &lt;small class="text-muted small-date"&gt;@FormatDate(result.PublishedDate)&lt;/small&gt;
                    &lt;/p&gt;
                  &lt;/div&gt;
                }
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    }
  &lt;/div&gt;
}
else if (SearchResults != null)
{
  &lt;p&gt;No results found.&lt;/p&gt;
}

@code {
  [Parameter]
  public List&lt;BookDoc&gt; SearchResults { get; set; } = new List&lt;BookDoc&gt;();

  private string FormatDate(string? date)
  {
    if (DateTime.TryParse(date, out DateTime parsedDate))
    {
      return parsedDate.ToString("MMMM dd, yyyy");
    }
    return "";
  }
}<p>最后，我们需要创建面来过滤搜索结果。</p><p><em>注：筛选器允许用户根据特定属性或类别（如产品类型、价格范围或品牌）缩小搜索结果的范围。这些筛选器通常以复选框的形式显示为可点击的选项，帮助用户缩小搜索范围，更轻松地找到相关结果。在 Elasticsearch 的上下文中，面是通过</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html"><em>聚合</em></a>创建的<em>。</em></p><p>我们在文件<code>/Components/Elasticsearch/Facet.razor</code> 中输入以下代码来设置切面：</p>@if (Facets != null)
{
  &lt;div class="facets-container"&gt;
  @foreach (var facet in Facets)
    {
      &lt;h3&gt;@facet.Key&lt;/h3&gt;
      @foreach (var option in facet.Value)
      {
        &lt;div&gt;
          &lt;input type="checkbox" checked="@IsFacetSelected(facet.Key, option.Key)"
            @onclick="() =&gt; ToggleFacet(facet.Key, option.Key)" /&gt;
          @option.Key (@option.Value)
        &lt;/div&gt;
      }
    }
  &lt;/div&gt;
}


@code {
  [Parameter]
  public Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;? Facets { get; set; }

  [Parameter]
  public EventCallback&lt;Dictionary&lt;string, List&lt;string&gt;&gt;&gt; OnFacetChanged { get; set; }

  private Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets = new();

  private void ToggleFacet(string facetName, string facetValue)
  {
    if (!selectedFacets.TryGetValue(facetName, out var facetValues))
    {
      facetValues = selectedFacets[facetName] = new List&lt;string&gt;();
    }

    if (!facetValues.Remove(facetValue))
    {
      facetValues.Add(facetValue);
    }

    OnFacetChanged.InvokeAsync(selectedFacets);
  }

  private bool IsFacetSelected(string facetName, string facetValue)
  {
    return selectedFacets.ContainsKey(facetName) &amp;&amp; selectedFacets[facetName].Contains(facetValue);
  }
}<p>该组件从<code>terms</code> 中读取<code>author</code> 、<code>categories</code> 和<code>status</code> 字段的聚合，然后生成一个过滤器列表，发送回 Elasticsearch。</p><p>现在，让我们把所有东西放在一起。</p><p>在<code>/Components/Pages/Search.razor</code> 文件中：</p>@page "/"
@rendermode InteractiveServer
@using BlazorApp.Models
@using BlazorApp.Services
@inject ElasticsearchService ElasticsearchService
@inject ILogger&lt;Search&gt; Logger

&lt;PageTitle&gt;Search&lt;/PageTitle&gt;

&lt;div class="top-row px-4 "&gt;

    &lt;div class="searchbar-container"&gt;
        &lt;h4&gt;Semantic Search with Elasticsearch and Blazor&lt;/h4&gt;

        &lt;SearchBar OnSearch="PerformSearch" /&gt;
    &lt;/div&gt;

    &lt;a href="https://www.elastic.co/search-labs/esre-with-blazor" target="_blank"&gt;About&lt;/a&gt;
&lt;/div&gt;

&lt;div class="px-4"&gt;

    &lt;div class="search-details-container"&gt;
        &lt;p role="status"&gt;Current search term: @currentSearchTerm&lt;/p&gt;
        &lt;p role="status"&gt;Total results: @totalResults&lt;/p&gt;
    &lt;/div&gt;

    &lt;div class="results-facet-container"&gt;
        &lt;div class="facets-container"&gt;
            &lt;Facet Facets="facets" OnFacetChanged="OnFacetChanged" /&gt;
        &lt;/div&gt;
        &lt;div class="results-container"&gt;
            &lt;Results SearchResults="searchResults" /&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private string currentSearchTerm = "";
    private long totalResults = 0;
    private List&lt;BookDoc&gt; searchResults = new List&lt;BookDoc&gt;();
    private Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();
    private Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets = new Dictionary&lt;string, List&lt;string&gt;&gt;();

    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&lt;string, List&lt;string&gt;&gt; newSelectedFacets)
    {
        selectedFacets = newSelectedFacets;
        await PerformSearch(currentSearchTerm);
    }
}<p>我们的页面正在运行！</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc750e55fff43de4b/6a17f6636864a4ab08b68931/5f0f25cb029a34df7f577a9f307278226ed07c3f-816x473.png" alt="Blazor 页面示例" /><p>如您所见，该页面功能齐全，但缺乏风格。让我们添加一些 CSS，使其看起来更有条理，反应更灵敏。</p><p>让我们开始更换布局样式。在<code>Components/Layout/MainLayout.razor.css</code> 文件中：</p>.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;
}<p>在<code>Components/Pages/Search.razor.css</code> 文件中添加搜索页面的样式：</p>.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;
  }
}<p>我们的页面开始变得更好看了：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3ecc49f404f18591/6a17f6654b055d959b432384/3dadd7ade2dd9070300a4e0514a4da2ae9cc9fb9-817x473.png" alt="为搜索页面添加样式后的 Blazor 页面" /><p>让我们来做最后的润色：</p><p>创建以下文件</p><ul><li><p>Components/Elasticsearch/Facet.razor.css</p></li><li><p>Components/Elasticsearch/Results.razor.css</p></li></ul><p>并为<code>Facet.razor.css</code> 添加样式：</p>.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 &gt; * {
  flex-shrink: 0;
}<p>供<code>Results.razor.css</code> ：</p>.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;
}<p>最终结果</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt78bbd8e6f9e23988/6a17f6664b055d2ed8432388/3e68ec4a38775fdfbeaa1a990e6a1e11dfb081d8-816x472.png" alt="构建 blazor 应用程序页面的最终结果" /><p>要运行应用程序，可以使用以下命令：</p><p><code>dotnet watch</code></p><p>你做到了现在，您可以使用搜索栏在 Elasticsearch 索引中搜索图书，并按作者、类别和状态过滤结果。</p><h3>进行全文和语义搜索</h3><p>默认情况下，我们的应用程序将使用<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html"> 全文</a> 和 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">语义搜索</a><a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search"> 执行 混合</a> 搜索 。您可以通过创建两个独立的方法来改变搜索逻辑，一个用于全文搜索，另一个用于语义搜索，然后根据用户的输入选择一种方法来创建查询。</p><p>在<code>/Services/ElasticsearchService.cs</code> 文件中的<code>ElasticsearchService</code> 类中添加以下方法：</p>private static Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt; BuildSemanticQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    return query =&gt;
        query.Bool(b =&gt;
            b.Must(m =&gt; m.Semantic(sem =&gt; sem.Field("longDescription").Query(searchTerm)))
                .Filter(filters.ToArray())
        );
}

private static Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt; BuildMultiMatchQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    if (string.IsNullOrEmpty(searchTerm))
    {
        return query =&gt; query.Bool(b =&gt; b.Filter(filters.ToArray()));
    }

    return query =&gt;
        query.Bool(b =&gt;
            b.Should(m =&gt;
                    m.MultiMatch(mm =&gt;
                        mm.Query(searchTerm).Fields(new[] { "title", "shortDescription" })
                    )
                )
                .Filter(filters.ToArray())
        );
}<p>这两种方法的工作原理与<code>BuildHybridQuery</code> 方法类似，但它们只进行全文或语义搜索。</p><p>您可以修改<code>SearchBooksAsync</code> 方法，使用选定的搜索方法：</p>public async Task&lt;ElasticResponse&gt; SearchBooksAsync(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; 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&lt;BookDoc&gt;(s =&gt;
            s.Index("elastic-blazor-books")
                .Query(multiMatchQuery) // Change this line to use different search methods, for example: .Query(semanticQuery) for semantic search
                .Aggregations(aggs =&gt;
                    aggs.Add("Authors", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Authors)))
                        .Add(
                            "Categories",
                            agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Categories))
                        )
                        .Add("Status", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; 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&lt;string, Dictionary&lt;string, long&gt;&gt;();

            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();
    }
}<p>您可<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor">在此处</a>找到完整的申请表</p><h2>结论</h2><p>Blazor 是一个有效的框架，可让您使用 C# 构建网络应用程序。Elasticsearch 是一个功能强大的搜索引擎，可让您构建搜索应用程序。将两者结合起来，您就可以轻松构建强大的搜索应用程序，利用 ESRE 的强大功能在短时间内创建<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">语义搜索体验</a>。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor</guid>
    <category><![CDATA[向量数据库]]></category>
    <category><![CDATA[.NET]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte7424ac5f0b223b4/6a17f668414c641971945323/7ba0d6bec908bfcae966b7f38626fabd682c6f3d-1200x628.png" length="0" type="image/png"/>
    <pubDate>Wed, 09 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch .NET 客户端的演变：从 NEST 到 Elastic.Clients.Elasticsearch]]></title>
    <description><![CDATA[了解 Elasticsearch .NET 客户端的演变以及从 NEST 到 Elastic.Clients.Elasticsearch 的过渡。]]></description>
    <content:encoded><![CDATA[<h2>.NET 客户端和 NEST 简介</h2><p>在.NET世界中，与Elasticsearch的集成长期以来一直是通过<code>NEST</code> 库来实现的，该库是开发人员与Elasticsearch强大的搜索和分析功能进行交互的强大接口。<code>NEST</code>Elasticsearch 的本地 .NET 客户端的需求而诞生的，因其丰富的功能集和无缝集成能力而迅速受到开发人员的青睐。</p><p>近<a href="https://github.com/elastic/elasticsearch-net/commit/724f932ba598915c8c3c35a19827fdfa4f782c1d">14 年来</a>，在<a href="https://github.com/elastic/elasticsearch/commit/ec72ca8b7a115f9b2eea3c76c518062b99a1d015">Elasticsearch 首次提交</a>后仅 8 个月，NEST 就一直忠实地跟踪 Elasticsearch 的发布情况。</p><h2>从 NEST 过渡到 Elastic.Clients.Elasticsearch</h2><p>随着 Elasticsearch 的发展，维护<code>NEST</code> 的复杂代码库变得越来越困难。我们认识到需要一种更可持续的客户端开发方法，并开始了从头开始重新设计 .NET 客户端的旅程。我们花了将近一年的时间才发布了第一个测试版，又花了一年的时间才接近支持所有服务器端点。最困难的决定之一是缩小图书馆的范围，以便优先考虑可维护性。</p><p>鉴于目前 Elasticsearch API 的规模，手动维护 450 多个端点和近 3000 种类型（请求、响应、查询、聚合等）已不再现实。为了确保语言客户端和 Elasticsearch 之间一致、准确和及时的对齐，8.x 客户端和许多相关类型现在都是根据<a href="https://github.com/elastic/elasticsearch-specification">共享规范</a>自动生成代码的。这是在 SDK 和库（如 Azure、AWS 和 Google 云平台的 SDK 和库）中保持客户端和服务器之间一致性的常见解决方案。</p><p>Elasticsearch 规范是 8 年前通过从<code>NEST</code> 导出类型映射创建的，通过客户团队的辛勤工作，我们现在可以使用相同的规范创建新的 .NET 客户端（以及 Java、Go 等其他多种语言的客户端）。</p><p>随着 8.13 版的发布，<code>NEST</code> 正式宣布弃用。随着 Elasticsearch 过渡到<code>Elastic.Clients.Elasticsearch</code> ，<code>NEST</code> 将逐步淘汰，并在今年年底达到报废期。我们强烈建议开发人员尽早开始迁移工作，以确保平稳过渡并减少任何潜在的中断。采用<code>Elastic.Clients.Elasticsearch</code> 不仅能确保与最新服务器功能的兼容性，还能使应用程序免受未来功能过时的影响。</p><h2>Elastic.Clients.Elasticsearch：功能和变更概述</h2><p>切换到 v8 客户端<code>Elastic.Clients.Elasticsearch</code> 可以访问 Elasticsearch 8 的所有新功能，还为库本身带来了大量现代化功能，但也意味着与前代产品相比，便利功能有所减少。一些新的核心功能包括查询语言<code>ES|QL</code> 、现代机器学习（ML）功能以及以兼容 OpenTelemetry 活动的形式改进的诊断功能。从 8.13 版开始，<code>Elastic.Clients.Elasticsearch</code> 支持 Elasticsearch 8 的几乎所有服务器功能。</p><p>例如，一个重要的突破性变化与聚合有关。在<code>NEST</code> 中，Fluent API 的用法如下：</p>s =&gt; s
.Aggregations(aggs =&gt; aggs
    .Children&lt;CommitActivity&gt;("name_of_child_agg", child =&gt; child
        .Aggregations(childAggs =&gt; childAggs
            .Average("average_per_child", avg =&gt; avg.Field(p =&gt; p.ConfidenceFactor))
            .Max("max_per_child", max =&gt; max.Field(p =&gt; p.ConfidenceFactor))
            .Min("min_per_child", min =&gt; min.Field(p =&gt; p.ConfidenceFactor))
        )
    )
)
<p>而 v8 客户端需要使用以下语法：</p>s =&gt; s
.Aggregations(aggs =&gt; aggs
	.Add("name_of_child_agg", agg =&gt; agg
		.Children(_ =&gt; {})
		.Aggregations(childAggs =&gt; childAggs
			.Add("average_per_child", agg =&gt; agg.Avg(avg =&gt; avg.Field(p =&gt; p.ConfidenceFactor)))
			.Add("max_per_child", agg =&gt; agg.Max(max =&gt; max.Field(p =&gt; p.ConfidenceFactor)))
			.Add("min_per_child", agg =&gt; agg.Min(min =&gt; min.Field(p =&gt; p.ConfidenceFactor)))
		)
	)
)
<h2>从 NEST v7 迁移到 .NET 客户端 v8</h2><p>这里有一份全面的迁移指南：<a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/8.18/migration-guide.html">迁移指南：从 NEST v7 到 .NET Client v8</a>。</p><h2>其他资源</h2><ul><li><p><a href="https://github.com/elastic/elasticsearch-net">GitHub 上的 Elastic.Clients.Elasticsearch v8 客户端</a></p></li><li><p><a href="https://www.nuget.org/packages/Elastic.Clients.Elasticsearch">NuGet 上的 Elastic.Clients.Elasticsearch v8 客户端</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/net-client-evolution</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/net-client-evolution</guid>
    <category><![CDATA[.NET]]></category>
    <dc:creator><![CDATA[Florian Bernd]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac0adcdaa85d703f/6a17f5816df731db170a1079/d09f7f5cb468d5e84f7f4636d92b3476e6604e11-1024x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 16 Apr 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>