ASP.NET Core で検索アプリを作成する
このチュートリアルでは、localhost で実行され、検索サービスの hotels-sample-index に接続する基本的な ASP.NET Core (Model-View-Controller) アプリを作成します。 このチュートリアルで学習する内容は次のとおりです。
- 基本的な検索ページを作成する
- 結果のフィルター処理
- 結果の並べ替え
このチュートリアルでは、Search API を介して呼び出されるサーバー側の操作に焦点を当てます。 クライアント側スクリプトで並べ替えとフィルター処理を行うのが一般的ですが、サーバー上でこれらの操作を呼び出す方法を知ることで、検索エクスペリエンスを設計するときにさらに多くのオプションが得られます。
このチュートリアルのサンプル コードは、GitHub の azure-search-dotnet-samples リポジトリにあります。
前提条件
- Visual Studio
- Azure.Search.Documents NuGet パッケージ
- Azure AI Search、あらゆる層、ただし、公衆ネットワーク アクセスが必要です。
- ホテル サンプル インデックス
データのインポート ウィザードの手順を実行し、検索サービスに hotels-sample-index を作成します。 または、HomeController.cs
ファイルでインデックス名を変更します。
プロジェクトを作成する
Visual Studio を開始し、 [新しいプロジェクトの作成] を選択します。
[ASP.NET Core Web アプリ (Model-View-Controller)] を選んでから、[次へ] を選択します。
プロジェクト名を指定してから、[次へ] を選択します。
次のページで、[.NET 6.0]、[.NET 7.0]、.NET 8.0 を選択します。
最上位レベルのステートメントを使用しない がオフになっていることを確認します。
[作成] を選択します
NuGet パッケージを追加する
[ツール] で、[NuGet パッケージ マネージャー]>[ソリューションの NuGet パッケージの管理] の順に選択します。
Azure.Search.Documents
を参照し、最新の安定したバージョンをインストールします。Microsoft.Spatial
パッケージを参照してインストールします。 サンプル インデックスには GeographyPoint データ型が含まれています。 このパッケージをインストールすると、実行時エラーを回避できます。 または、パッケージをインストールしない場合は、Hotels クラスから "Location" フィールドを削除します。 このチュートリアルでは、このフィールドは使用されません。
サービス情報を追加する
接続の場合、アプリによって、クエリ API キーが完全修飾検索 URL に提示されます。 どちらも appsettings.json
ファイルで指定されます。
検索サービスとクエリ API キーを指定するように appsettings.json
を変更します。
{
"SearchServiceUri": "<YOUR-SEARCH-SERVICE-URL>",
"SearchServiceQueryApiKey": "<YOUR-SEARCH-SERVICE-QUERY-API-KEY>"
}
Azure portal からサービス URL と API キーを取得できます。 このコードはインデックスを作成するのではなく、インデックスに対してクエリを実行するため、管理キーの代わりにクエリ キーを使用できます。
必ず hotels-sample-index がある検索サービスを指定してください。
モデルを追加する
この手順では、hotels-sample-index のスキーマを表すモデルを作成します。
ソリューション エクスプローラーで、[モデル] を右選択し、次のコードに対して "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; } } }
"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; } } }
"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; } } }
"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
を変更します。
ソリューション エクスプローラーの [モデル] で、
HomeController
を開きます。既定値を次の内容に置き換えます。
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(); } } }
ビューを変更する
ソリューション エクスプローラーの [ビュー]>[ホーム] で、
index.cshtml
を開きます。既定値を次の内容に置き換えます。
@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>
サンプルを実行する
F5 キーを押して、プロジェクトをコンパイルし、実行します。 アプリはローカル ホストで実行され、既定のブラウザーで開きます。
[検索] を選択すると、すべての結果が返されます。
このコードでは、シンプルな構文と
searchMode=Any
をサポートする既定の検索構成を使用します。 キーワードを入力したり、ブール演算子で拡張したり、プレフィックス検索 (pool*
) を実行したりできます。
次のいくつかのセクションで、HomeController
の RunQueryAsync メソッド を変更して、フィルターと並べ替えを追加します。
結果のフィルター処理
インデックス フィールド属性では、検索可能、フィルター可能、並べ替え可能、ファセット可能、および取得可能なフィールドを決定します。 hotels-sample-index のフィルター可能なフィールドには、Category、Address/City、Address/StateProvince が含まれます。 この例では、Category に $Filter 式を追加します。
フィルターは常に最初に実行され、その後にクエリが指定されていると仮定して実行されます。
HomeController
を開き、RunQueryAsync メソッドを見つけます。 Filter を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); }
アプリケーションを実行します。
[検索] を選択して、空のクエリを実行します。 フィルターでは、元の 50 ではなく 18 個のドキュメントが返されます。
フィルター式の詳細については、「Azure AI Search のフィルター」および「Azure AI Search での OData $filter 構文」を参照してください。
結果の並べ替え
hotels-sample-index には、並べ替え可能なフィールドに Rating と LastRenovated が含まれます。 この例では、Rating フィールドに $OrderBy 式を追加します。
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); }
アプリケーションを実行します。 結果は Rating で降順に並べ替えられます。
並べ替えの詳細については、「Azure AI Search での OData $orderby 構文」を参照してください。
次のステップ
このチュートリアルでは、検索サービスに接続し、サーバー側のフィルター処理と並べ替えのために Search API を呼び出す ASP.NET Core (MVC) プロジェクトを作成しました。
ユーザー アクションに応答するクライアント側のコードを調べる場合は、ソリューションに React テンプレートを追加することを検討してください。