다음을 통해 공유

在 ASP.NET Core 中创建搜索应用

在本教程中,你将创建一个基本的 ASP.NET Core (Model-View-Controller) 应用,该应用在 localhost 中运行,并连接到搜索服务上的 hotels-sample-index。 学习如何:

  • 创建基本搜索页
  • 筛选结果
  • 对结果进行排序

本教程重点介绍通过 搜索 API 调用的服务器端作。 尽管在客户端脚本中进行排序和筛选很常见,但在设计搜索体验时,了解如何在服务器上调用这些操作可为你提供更多选项。

可以在 GitHub 上的 azure-search-dotnet-samples 存储库中找到本教程的示例代码。

先决条件

逐步完成导入数据向导,以在搜索服务上创建 hotels-sample-index。 或者更改 HomeController.cs 文件中的索引名称。

创建项目

  1. 启动 Visual Studio 并选择“创建新项目”。

  2. 选择“ASP.NET Core Web 应用(模型-视图-控制器)”,然后选择“下一步”。

  3. 输入项目名称,然后选择“ 下一步”。

  4. 在下一页上,选择 .NET 9.0

  5. 接受默认设置。

  6. 选择“创建”。

添加 NuGet 包

  1. “工具” 菜单上,为解决方案选择 “NuGet 包管理器>管理 NuGet 包”。

  2. 找到 Azure.Search.Documents 并安装最新稳定版本。

  3. 找到并安装 Microsoft.Spatial 包。 示例索引包括 GeographyPoint 数据类型。 安装此包可避免运行时错误。 或者,如果不想安装包,请从 Hotels 类中删除“位置”字段。 本教程中未使用该字段。

添加服务信息

对于连接,应用会向完全限定的搜索 URL 提供查询 API 密钥。 两者都在 appsettings.json 文件中指定。

修改 appsettings.json 以指定搜索服务和查询 API 密钥

{
    "SearchServiceUri": "<YOUR-SEARCH-SERVICE-URL>",
    "SearchServiceQueryApiKey": "<YOUR-SEARCH-SERVICE-QUERY-API-KEY>"
}

可以从 Azure 门户获取服务 URL 和 API 密钥。 由于此代码可查询而不是创建索引,因此可以使用查询密钥而不是管理密钥。

请确保指定一个具有hotels-sample-index的搜索服务。

添加模型

在此步骤中,将创建表示 hotels-sample-index 架构的模型。

  1. 在解决方案资源管理器中,右键单击 “模型 ”,并为以下代码添加名为“Hotel”的新类:

     using Azure.Search.Documents.Indexes.Models;
     using Azure.Search.Documents.Indexes;
     using Microsoft.Spatial;
     using System.Text.Json.Serialization;
    
     namespace HotelDemoApp.Models
     {
         public partial class Hotel
         {
             [SimpleField(IsFilterable = true, IsKey = true)]
             public string HotelId { get; set; }
    
             [SearchableField(IsSortable = true)]
             public string HotelName { get; set; }
    
             [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnLucene)]
             public string Description { get; set; }
    
             [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.FrLucene)]
             [JsonPropertyName("Description_fr")]
             public string DescriptionFr { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string Category { get; set; }
    
             [SearchableField(IsFilterable = true, IsFacetable = true)]
             public string[] Tags { get; set; }
    
             [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public bool? ParkingIncluded { get; set; }
    
             [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public DateTimeOffset? LastRenovationDate { get; set; }
    
             [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public double? Rating { get; set; }
    
             public Address Address { get; set; }
    
             [SimpleField(IsFilterable = true, IsSortable = true)]
             public GeographyPoint Location { get; set; }
    
             public Rooms[] Rooms { get; set; }
         }
     }
    
  2. 添加名为“Address”的类,并将其替换为以下代码:

     using Azure.Search.Documents.Indexes;
    
     namespace HotelDemoApp.Models
     {
         public partial class Address
         {
             [SearchableField]
             public string StreetAddress { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string City { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string StateProvince { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string PostalCode { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string Country { get; set; }
         }
     }
    
  3. 添加名为“Rooms”的类,并将其替换为以下代码:

     using Azure.Search.Documents.Indexes.Models;
     using Azure.Search.Documents.Indexes;
     using System.Text.Json.Serialization;
    
     namespace HotelDemoApp.Models
     {
         public partial class Rooms
         {
             [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)]
             public string Description { get; set; }
    
             [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.FrMicrosoft)]
             [JsonPropertyName("Description_fr")]
             public string DescriptionFr { get; set; }
    
             [SearchableField(IsFilterable = true, IsFacetable = true)]
             public string Type { get; set; }
    
             [SimpleField(IsFilterable = true, IsFacetable = true)]
             public double? BaseRate { get; set; }
    
             [SearchableField(IsFilterable = true, IsFacetable = true)]
             public string BedOptions { get; set; }
    
             [SimpleField(IsFilterable = true, IsFacetable = true)]
             public int SleepsCount { get; set; }
    
             [SimpleField(IsFilterable = true, IsFacetable = true)]
             public bool? SmokingAllowed { get; set; }
    
             [SearchableField(IsFilterable = true, IsFacetable = true)]
             public string[] Tags { get; set; }
         }
     }
    
  4. 添加名为“SearchData”的类,并将其替换为以下代码:

     using Azure.Search.Documents.Models;
    
     namespace HotelDemoApp.Models
     {
         public class SearchData
         {
             // The text to search for.
             public string searchText { get; set; }
    
             // The list of results.
             public SearchResults<Hotel> resultList;
         }
     }
    

修改控制器

在本教程中,请修改默认值 HomeController 以包含在搜索服务上执行的方法。

  1. “模型”下的“解决方案资源管理器”中,打开 HomeController

  2. 将默认内容替换为以下代码:

    using Azure;
     using Azure.Search.Documents;
     using Azure.Search.Documents.Indexes;
     using HotelDemoApp.Models;
     using Microsoft.AspNetCore.Mvc;
     using System.Diagnostics;
    
     namespace HotelDemoApp.Controllers
     {
         public class HomeController : Controller
         {
             public IActionResult Index()
             {
                 return View();
             }
    
             [HttpPost]
             public async Task<ActionResult> Index(SearchData model)
             {
                 try
                 {
                     // Check for a search string
                     if (model.searchText == null)
                     {
                         model.searchText = "";
                     }
    
                     // Send the query to Search.
                     await RunQueryAsync(model);
                 }
    
                 catch
                 {
                     return View("Error", new ErrorViewModel { RequestId = "1" });
                 }
                 return View(model);
             }
    
             [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
             public IActionResult Error()
             {
                 return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
             }
    
             private static SearchClient _searchClient;
             private static SearchIndexClient _indexClient;
             private static IConfigurationBuilder _builder;
             private static IConfigurationRoot _configuration;
    
             private void InitSearch()
             {
                 // Create a configuration using appsettings.json
                 _builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
                 _configuration = _builder.Build();
    
                 // Read the values from appsettings.json
                 string searchServiceUri = _configuration["SearchServiceUri"];
                 string queryApiKey = _configuration["SearchServiceQueryApiKey"];
    
                 // Create a service and index client.
                 _indexClient = new SearchIndexClient(new Uri(searchServiceUri), new AzureKeyCredential(queryApiKey));
                 _searchClient = _indexClient.GetSearchClient("hotels-sample-index");
             }
    
             private async Task<ActionResult> RunQueryAsync(SearchData model)
             {
                 InitSearch();
    
                 var options = new SearchOptions()
                 {
                     IncludeTotalCount = true
                 };
    
                 // Enter Hotel property names to specify which fields are returned.
                 // If Select is empty, all "retrievable" fields are returned.
                 options.Select.Add("HotelName");
                 options.Select.Add("Category");
                 options.Select.Add("Rating");
                 options.Select.Add("Tags");
                 options.Select.Add("Address/City");
                 options.Select.Add("Address/StateProvince");
                 options.Select.Add("Description");
    
                 // For efficiency, the search call should be asynchronous, so use SearchAsync rather than Search.
                 model.resultList = await _searchClient.SearchAsync<Hotel>(model.searchText, options).ConfigureAwait(false);
    
                 // Display the results.
                 return View("Index", model);
             }
             public IActionResult Privacy()
             {
                 return View();
             }
         }
     }
    

修改视图

  1. 在解决方案资源管理器的 “视图>主页”下,打开 index.cshtml

  2. 将默认内容替换为以下代码:

    @model HotelDemoApp.Models.SearchData;
    
    @{
        ViewData["Title"] = "Index";
    }
    
    <div>
        <h2>Search for Hotels</h2>
    
        <p>Use this demo app to test server-side sorting and filtering. Modify the RunQueryAsync method to change the operation. The app uses the default search configuration (simple search syntax, with searchMode=Any).</p>
    
        <form asp-controller="Home" asp-action="Index">
            <p>
                <input type="text" name="searchText" />
                <input type="submit" value="Search" />
            </p>
        </form>
    </div>
    
    <div>
        @using (Html.BeginForm("Index", "Home", FormMethod.Post))
        {
            @if (Model != null)
            {
                // Show the result count.
                <p>@Model.resultList.TotalCount Results</p>
    
                // Get search results.
                var results = Model.resultList.GetResults().ToList();
    
                {
                    <table class="table">
                        <thead>
                            <tr>
                                <th>Name</th>
                                <th>Category</th>
                                <th>Rating</th>
                                <th>Tags</th>
                                <th>City</th>
                                <th>State</th>
                                <th>Description</th>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach (var d in results)
                            {
                                <tr>
                                    <td>@d.Document.HotelName</td>
                                    <td>@d.Document.Category</td>
                                    <td>@d.Document.Rating</td>
                                    <td>@d.Document.Tags[0]</td>
                                    <td>@d.Document.Address.City</td>
                                    <td>@d.Document.Address.StateProvince</td>
                                    <td>@d.Document.Description</td>
                                </tr>
                            }
                        </tbody>
                      </table>
                }
            }
        }
    </div>
    

运行示例

  1. F5 编译并运行项目。 应用在 localhost 上运行,并在默认浏览器中打开。

  2. 选择“搜索”以返回所有结果。

  3. 此代码使用默认搜索配置,支持简单语法searchMode=Any。 可以输入关键字、使用布尔运算符进行增强,或运行前缀搜索 (pool*)。

在接下来的几个部分中,修改 中的 HomeController 方法以添加筛选器和排序。

筛选结果

索引字段属性确定哪些字段可搜索、可筛选、可排序、可查找和可检索。 在 hotels-sample-index 中,可筛选字段包括“Category”、“Address/City”和“Address/StateProvince”。 本示例在“Category”上添加了 $Filter 表达式。

假设指定了一个筛选器,则筛选器始终先执行,然后执行查询。

  1. 打开 HomeController 并找到 RunQueryAsync 方法。 将筛选器添加到 var options = new SearchOptions()

     private async Task<ActionResult> RunQueryAsync(SearchData model)
     {
         InitSearch();
    
         var options = new SearchOptions()
         {
             IncludeTotalCount = true,
             Filter = "search.in(Category,'Budget,Suite')"
         };
    
         options.Select.Add("HotelName");
         options.Select.Add("Category");
         options.Select.Add("Rating");
         options.Select.Add("Tags");
         options.Select.Add("Address/City");
         options.Select.Add("Address/StateProvince");
         options.Select.Add("Description");
    
         model.resultList = await _searchClient.SearchAsync<Hotel>(model.searchText, options).ConfigureAwait(false);
    
         return View("Index", model);
     }
    
  2. 运行应用程序。

  3. 单击“搜索”以运行空查询。 此筛选器返回 18 个文档,而不是最初的 50 个文档。

有关筛选器表达式的详细信息,请参阅 Azure AI 搜索中的筛选器Azure AI 搜索中的 OData $filter 语法

对结果进行排序

在 hotels-sample-index 中,可排序字段包括“Rating”和“LastRenovated”。 本示例将 $OrderBy 表达式添加到“Rating”字段。

  1. 打开 HomeControllerRunQueryAsync 方法并将其替换为以下版本:

     private async Task<ActionResult> RunQueryAsync(SearchData model)
     {
         InitSearch();
    
         var options = new SearchOptions()
         {
             IncludeTotalCount = true,
         };
    
         options.OrderBy.Add("Rating desc");
    
         options.Select.Add("HotelName");
         options.Select.Add("Category");
         options.Select.Add("Rating");
         options.Select.Add("Tags");
         options.Select.Add("Address/City");
         options.Select.Add("Address/StateProvince");
         options.Select.Add("Description");
    
         model.resultList = await _searchClient.SearchAsync<Hotel>(model.searchText, options).ConfigureAwait(false);
    
         return View("Index", model);
     }
    
  2. 运行应用程序。 结果按“Rating”的降序排序。

有关排序的详细信息,请参阅 Azure AI 搜索中的 OData $orderby 语法

后续步骤

在本教程中,你创建了一个连接到搜索服务的 ASP.NET Core (MVC) 项目,并调用搜索 API 进行服务器端筛选和排序。

若要添加响应用户作的客户端代码,请在解决方案中使用 React 模板: C# 教程:使用 .NET 向网站添加搜索