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 웹앱(Model-View-Controller)을 선택한 후 다음을 선택합니다.
프로젝트 이름을 입력한 후 다음을 선택합니다.
다음 페이지에서 .NET 6.0 또는 .NET 7.0 또는 .NET 8.0을 선택합니다.
최상위 문을 사용하지 않음이 선택 취소되어 있는지 확인합니다.
만들기를 실행합니다.
NuGet 패키지 추가
도구에서 솔루션용 NuGet 패키지 관리자>NuGet 패키지 관리자를 선택합니다.
Azure.Search.Documents
를 검색하고 안정적인 최신 버전을 설치합니다.Microsoft.Spatial
패키지를 찾아 설치합니다. 샘플 인덱스에는 GeographyPoint 데이터 형식이 포함됩니다. 이 패키지를 설치하면 런타임 오류가 발생하지 않습니다. 또는 패키지를 설치하지 않으려면 호텔 클래스에서 '위치' 필드를 제거합니다. 이 필드는 이 자습서에서 사용되지 않습니다.
서비스 정보 추가
연결하기 위해 앱에서 정규화된 검색 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의 스키마를 나타내는 모델을 만듭니다.
솔루션 탐색기에서 모델을 마우스 오른쪽 단추로 선택하고 다음 코드에 '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 메서드를 찾습니다.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 검색의 필터 및 Azure AI 검색의 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 검색의 OData $orderby 구문을 참조하세요.
다음 단계
이 자습서에서는 검색 서비스에 연결되고 서버 쪽 필터링 및 정렬을 위해 Search API를 호출하는 ASP.NET Core(MVC) 프로젝트를 만들었습니다.
사용자 작업에 응답하는 클라이언트 쪽 코드를 탐색하려면 솔루션에 React 템플릿을 추가하는 것이 좋습니다.