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

在本教程中,创建在 localhost 中运行并连接到搜索服务上的 hotels-sample-index 的基本 ASP.NET Core(模型-视图-控制器)应用。 本教程介绍如何执行下列操作:

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

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

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

先决条件

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

创建项目

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

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

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

  4. 在下一页上,选择“.NET 6.0”、“.NET 7.0”或“.NET 8.0”。

  5. 验证是否未选中“不使用顶级语句”。

  6. 选择“创建”。

添加 NuGet 包

  1. 在工具页上,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。

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

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

添加服务信息

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

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

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

可以从门户获取服务 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 编译并运行项目。 应用会在本地主机上运行,并在默认浏览器中打开。

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

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

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

筛选结果

索引字段属性确定哪些字段可搜索、可筛选、可排序、可查找和可检索。 在 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. 打开 HomeController 并将 RunQueryAsync 方法替换为以下版本:

     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 模板: