Samouczek: dodawanie nawigacji aspektowej przy użyciu zestawu SDK platformy .NET
Aspekty umożliwiają samodzielną nawigację, udostępniając zestaw linków do filtrowania wyników. W tym samouczku struktura nawigacji aspektowej jest umieszczana po lewej stronie z etykietami i tekstem z możliwością kliknięcia w celu przycinania wyników.
Ten samouczek zawiera informacje na temat wykonywania następujących czynności:
- Ustawianie właściwości modelu jako IsFacetable
- Dodawanie nawigacji aspektowej do aplikacji
Omówienie
Aspekty są oparte na polach w indeksie wyszukiwania. Żądanie zapytania zawierające facet=[string] dostarcza pole do aspektu według. Często uwzględnia wiele aspektów, takich jak &facet=category&facet=amenities
, każdy oddzielony znakiem ampersand (&). Implementacja struktury nawigacji aspektowej wymaga określenia zarówno aspektów, jak i filtrów. Filtr jest używany na zdarzeniu kliknięcia, aby zawęzić wyniki. Na przykład kliknięcie pozycji "budżet" filtruje wyniki na podstawie tych kryteriów.
Ten samouczek rozszerza projekt stronicowania utworzony w samouczku Dodawanie stronicowania do wyników wyszukiwania .
Ukończona wersja kodu w tym samouczku można znaleźć w następującym projekcie:
Wymagania wstępne
- Rozwiązanie 2a-add-paging (GitHub). Ten projekt może być twoją własną wersją utworzoną z poprzedniego samouczka lub kopią z usługi GitHub.
Ustawianie właściwości modelu jako IsFacetable
Aby właściwość modelu znajdowała się w wyszukiwaniu aspektów, musi zostać oznaczona tagiem IsFacetable.
Sprawdź klasę Hotel . Kategoria i tagi, na przykład, są oznaczone jako IsFacetable, ale HotelName i Description nie są.
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 Room[] Rooms { get; set; } }
W ramach tego samouczka nie zmienimy żadnych tagów, więc zamknij plik hotel.cs niezmierzony.
Uwaga
Wyszukiwanie aspektów zgłosi błąd, jeśli pole żądane w wyszukiwaniu nie zostanie odpowiednio oznaczone.
Dodawanie nawigacji aspektowej do aplikacji
W tym przykładzie umożliwimy użytkownikowi wybranie jednej kategorii hotelu lub jednego udogodnienia z list linków wyświetlanych po lewej stronie wyników. Użytkownik rozpoczyna się od wprowadzenia tekstu wyszukiwania, a następnie stopniowo zawęża wyniki wyszukiwania, wybierając kategorię lub udogodnienia.
Jest to zadanie kontrolera, aby przekazać listy aspektów do widoku. Aby zachować wybór użytkownika w miarę postępu wyszukiwania, używamy magazynu tymczasowego jako mechanizmu zachowania stanu.
Dodawanie ciągów filtru do modelu SearchData
Otwórz plik SearchData.cs i dodaj właściwości ciągu do klasy SearchData , aby przechowywać ciągi filtru faceta.
public string categoryFilter { get; set; } public string amenityFilter { get; set; }
Dodawanie metody akcji Facet
Kontroler domu wymaga jednej nowej akcji, aspektu i aktualizacji istniejących akcji indeksu i strony oraz metody RunQueryAsync .
Zastąp metodę akcji Index(SearchData model).
public async Task<ActionResult> Index(SearchData model) { try { // Ensure the search string is valid. if (model.searchText == null) { model.searchText = ""; } // Make the search call for the first page. await RunQueryAsync(model, 0, 0, "", "").ConfigureAwait(false); } catch { return View("Error", new ErrorViewModel { RequestId = "1" }); } return View(model); }
Zastąp metodę akcji PageAsync(SearchData model).
public async Task<ActionResult> PageAsync(SearchData model) { try { int page; // Calculate the page that should be displayed. switch (model.paging) { case "prev": page = (int)TempData["page"] - 1; break; case "next": page = (int)TempData["page"] + 1; break; default: page = int.Parse(model.paging); break; } // Recover the leftMostPage. int leftMostPage = (int)TempData["leftMostPage"]; // Recover the filters. string catFilter = TempData["categoryFilter"].ToString(); string ameFilter = TempData["amenityFilter"].ToString(); // Recover the search text. model.searchText = TempData["searchfor"].ToString(); // Search for the new page. await RunQueryAsync(model, page, leftMostPage, catFilter, ameFilter); } catch { return View("Error", new ErrorViewModel { RequestId = "2" }); } return View("Index", model); }
Dodaj metodę akcji FacetAsync(SearchData model), która ma zostać aktywowana, gdy użytkownik kliknie link aspektu. Model będzie zawierać filtr wyszukiwania kategorii lub udogodnień. Dodaj go po akcji PageAsync .
public async Task<ActionResult> FacetAsync(SearchData model) { try { // Filters set by the model override those stored in temporary data. string catFilter; string ameFilter; if (model.categoryFilter != null) { catFilter = model.categoryFilter; } else { catFilter = TempData["categoryFilter"].ToString(); } if (model.amenityFilter != null) { ameFilter = model.amenityFilter; } else { ameFilter = TempData["amenityFilter"].ToString(); } // Recover the search text. model.searchText = TempData["searchfor"].ToString(); // Initiate a new search. await RunQueryAsync(model, 0, 0, catFilter, ameFilter).ConfigureAwait(false); } catch { return View("Error", new ErrorViewModel { RequestId = "2" }); } return View("Index", model); }
Konfigurowanie filtru wyszukiwania
Gdy użytkownik wybierze określony aspekt, na przykład kliknie kategorię Resort and Spa , a następnie tylko hotele określone jako ta kategoria powinny zostać zwrócone w wynikach. Aby zawęzić wyszukiwanie w ten sposób, musimy skonfigurować filtr.
Zastąp metodę RunQueryAsync następującym kodem. Przede wszystkim przyjmuje ciąg filtru kategorii i ciąg filtru udogodnień oraz ustawia parametr Filter parametru SearchOptions.
private async Task<ActionResult> RunQueryAsync(SearchData model, int page, int leftMostPage, string catFilter, string ameFilter) { InitSearch(); string facetFilter = ""; if (catFilter.Length > 0 && ameFilter.Length > 0) { // Both facets apply. facetFilter = $"{catFilter} and {ameFilter}"; } else { // One, or zero, facets apply. facetFilter = $"{catFilter}{ameFilter}"; } var options = new SearchOptions { Filter = facetFilter, SearchMode = SearchMode.All, // Skip past results that have already been returned. Skip = page * GlobalVariables.ResultsPerPage, // Take only the next page worth of results. Size = GlobalVariables.ResultsPerPage, // Include the total number of results. IncludeTotalCount = true, }; // Return information on the text, and number, of facets in the data. options.Facets.Add("Category,count:20"); options.Facets.Add("Tags,count:20"); // Enter Hotel property names into this list, so only these values will be returned. options.Select.Add("HotelName"); options.Select.Add("Description"); options.Select.Add("Category"); options.Select.Add("Tags"); // 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); // This variable communicates the total number of pages to the view. model.pageCount = ((int)model.resultList.TotalCount + GlobalVariables.ResultsPerPage - 1) / GlobalVariables.ResultsPerPage; // This variable communicates the page number being displayed to the view. model.currentPage = page; // Calculate the range of page numbers to display. if (page == 0) { leftMostPage = 0; } else if (page <= leftMostPage) { // Trigger a switch to a lower page range. leftMostPage = Math.Max(page - GlobalVariables.PageRangeDelta, 0); } else if (page >= leftMostPage + GlobalVariables.MaxPageRange - 1) { // Trigger a switch to a higher page range. leftMostPage = Math.Min(page - GlobalVariables.PageRangeDelta, model.pageCount - GlobalVariables.MaxPageRange); } model.leftMostPage = leftMostPage; // Calculate the number of page numbers to display. model.pageRange = Math.Min(model.pageCount - leftMostPage, GlobalVariables.MaxPageRange); // Ensure Temp data is stored for the next call. TempData["page"] = page; TempData["leftMostPage"] = model.leftMostPage; TempData["searchfor"] = model.searchText; TempData["categoryFilter"] = catFilter; TempData["amenityFilter"] = ameFilter; // Return the new view. return View("Index", model); }
Zwróć uwagę, że właściwości Kategoria i Tagi są dodawane do listy Wybierz elementy do zwrócenia. Ten dodatek nie jest wymaganiem, aby nawigacja aspektowa działała, ale te informacje służą do sprawdzania, czy filtry działają prawidłowo.
Dodawanie list linków aspektów do widoku
Widok będzie wymagał pewnych znaczących zmian.
Zacznij od otwarcia pliku hotels.css (w folderze wwwroot/css) i dodaj następujące klasy.
.facetlist { list-style: none; } .facetchecks { width: 250px; display: normal; color: #666; margin: 10px; padding: 5px; } .facetheader { font-size: 10pt; font-weight: bold; color: darkgreen; }
W przypadku widoku należy zorganizować dane wyjściowe w tabeli, aby starannie wyrównać listy aspektów po lewej stronie i wyniki po prawej stronie. Otwórz plik index.cshtml. Zastąp całą zawartość tagów treści> HTML <następującym kodem.
<body> @using (Html.BeginForm("Index", "Home", FormMethod.Post)) { <table> <tr> <td></td> <td> <h1 class="sampleTitle"> <img src="~/images/azure-logo.png" width="80" /> Hotels Search - Facet Navigation </h1> </td> </tr> <tr> <td></td> <td> <!-- Display the search text box, with the search icon to the right of it.--> <div class="searchBoxForm"> @Html.TextBoxFor(m => m.searchText, new { @class = "searchBox" }) <input value="" class="searchBoxSubmit" type="submit"> </div> </td> </tr> <tr> <td valign="top"> <div id="facetplace" class="facetchecks"> @if (Model != null && Model.resultList != null) { List<string> categories = Model.resultList.Facets["Category"].Select(x => x.Value.ToString()).ToList(); if (categories.Count > 0) { <h5 class="facetheader">Category:</h5> <ul class="facetlist"> @for (var c = 0; c < categories.Count; c++) { var facetLink = $"{categories[c]} ({Model.resultList.Facets["Category"][c].Count})"; <li> @Html.ActionLink(facetLink, "FacetAsync", "Home", new { categoryFilter = $"Category eq '{categories[c]}'" }, null) </li> } </ul> } List<string> tags = Model.resultList.Facets["Tags"].Select(x => x.Value.ToString()).ToList(); if (tags.Count > 0) { <h5 class="facetheader">Amenities:</h5> <ul class="facetlist"> @for (var c = 0; c < tags.Count; c++) { var facetLink = $"{tags[c]} ({Model.resultList.Facets["Tags"][c].Count})"; <li> @Html.ActionLink(facetLink, "FacetAsync", "Home", new { amenityFilter = $"Tags/any(t: t eq '{tags[c]}')" }, null) </li> } </ul> } } </div> </td> <td valign="top"> <div id="resultsplace"> @if (Model != null && Model.resultList != null) { // Show the result count. <p class="sampleText"> @Model.resultList.TotalCount Results </p> var results = Model.resultList.GetResults().ToList(); @for (var i = 0; i < results.Count; i++) { string amenities = string.Join(", ", results[i].Document.Tags); string fullDescription = results[i].Document.Description; fullDescription += $"\nCategory: {results[i].Document.Category}"; fullDescription += $"\nAmenities: {amenities}"; // Display the hotel name and description. @Html.TextAreaFor(m => results[i].Document.HotelName, new { @class = "box1" }) @Html.TextArea($"desc{i}", fullDescription, new { @class = "box2" }) } } </div> </td> </tr> <tr> <td></td> <td valign="top"> @if (Model != null && Model.pageCount > 1) { // If there is more than one page of results, show the paging buttons. <table> <tr> <td class="tdPage"> @if (Model.currentPage > 0) { <p class="pageButton"> @Html.ActionLink("|<", "PageAsync", "Home", new { paging = "0" }, null) </p> } else { <p class="pageButtonDisabled">|<</p> } </td> <td class="tdPage"> @if (Model.currentPage > 0) { <p class="pageButton"> @Html.ActionLink("<", "PageAsync", "Home", new { paging = "prev" }, null) </p> } else { <p class="pageButtonDisabled"><</p> } </td> @for (var pn = Model.leftMostPage; pn < Model.leftMostPage + Model.pageRange; pn++) { <td class="tdPage"> @if (Model.currentPage == pn) { // Convert displayed page numbers to 1-based and not 0-based. <p class="pageSelected">@(pn + 1)</p> } else { <p class="pageButton"> @Html.ActionLink((pn + 1).ToString(), "PageAsync", "Home", new { paging = @pn }, null) </p> } </td> } <td class="tdPage"> @if (Model.currentPage < Model.pageCount - 1) { <p class="pageButton"> @Html.ActionLink(">", "PageAsync", "Home", new { paging = "next" }, null) </p> } else { <p class="pageButtonDisabled">></p> } </td> <td class="tdPage"> @if (Model.currentPage < Model.pageCount - 1) { <p class="pageButton"> @Html.ActionLink(">|", "PageAsync", "Home", new { paging = Model.pageCount - 1 }, null) </p> } else { <p class="pageButtonDisabled">>|</p> } </td> </tr> </table> } </td> </tr> </table> } </body>
Zwróć uwagę na użycie wywołania Html.ActionLink . To wywołanie komunikuje prawidłowe ciągi filtru do kontrolera, gdy użytkownik kliknie link aspektu.
Uruchamianie i testowanie aplikacji
Zaletą nawigacji aspektowej dla użytkownika jest możliwość zawężenia wyszukiwania jednym kliknięciem, które możemy pokazać w poniższej sekwencji.
Uruchom aplikację, wpisz ciąg "airport" jako tekst wyszukiwania. Sprawdź, czy lista aspektów jest starannie wyświetlana po lewej stronie. Te aspekty mają zastosowanie do hoteli, które mają "lotnisko" w danych tekstowych, z liczbą częstotliwości ich występowania.
Kliknij kategorię Ośrodek i Spa . Sprawdź, czy wszystkie wyniki znajdują się w tej kategorii.
Kliknij kontynentalny zestaw śniadaniowy . Sprawdź, czy wszystkie wyniki są nadal dostępne w kategorii "Resort and Spa", z wybranymi udogodnieniami.
Spróbuj wybrać dowolną inną kategorię, a następnie jedną z udogodnień i wyświetlić wyniki zawężenia. Następnie wypróbuj drugą stronę, jeden udogodnienia, a następnie jedną kategorię. Wyślij puste wyszukiwanie, aby zresetować stronę.
Uwaga
Po wybraniu jednego zaznaczenia na liście aspektów (takiej jak kategoria) zastąpi ona wszystkie poprzednie zaznaczenie na liście kategorii.
Wnioski
Weź pod uwagę następujące wnioski z tego projektu:
- Należy oznaczyć każde pole aspektowe właściwością IsFacetable w celu włączenia do nawigacji aspektowej.
- Aspekty są łączone z filtrami w celu zmniejszenia wyników.
- Aspekty są skumulowane, a każdy wybór jest oparty na poprzednim do dalszych wąskich wyników.
Następne kroki
W następnym samouczku przyjrzymy się kolejności wyników. Do tego momentu wyniki są uporządkowane po prostu w kolejności, w której znajdują się w bazie danych.