Tutorial: Adicionar navegação facetada com o SDK .NET
As facetas permitem a navegação auto-direcionada ao fornecer um conjunto de ligações para filtrar resultados. Neste tutorial, é colocada uma estrutura de navegação facetada no lado esquerdo da página, com etiquetas e texto clicável para cortar os resultados.
Neste tutorial, ficará a saber como:
- Definir propriedades do modelo como IsFacetable
- Adicionar navegação de facetas à sua aplicação
Descrição Geral
As facetas baseiam-se em campos no índice de pesquisa. Um pedido de consulta que inclui facet=[string] fornece o campo pelo qual facetar. É comum incluir várias facetas, como &facet=category&facet=amenities
, cada uma separada por um caráter e comercial (&). A implementação de uma estrutura de navegação facetada requer que especifique as facetas e os filtros. O filtro é utilizado num evento de clique para restringir os resultados. Por exemplo, clicar em "orçamento" filtra os resultados com base nesses critérios.
Este tutorial expande o projeto de paginação criado no tutorial Adicionar paginação aos resultados da pesquisa .
Pode encontrar uma versão concluída do código neste tutorial no projeto seguinte:
Pré-requisitos
- Solução 2a-add-paging (GitHub ). Este projeto pode ser a sua própria versão criada a partir do tutorial anterior ou de uma cópia do GitHub.
Definir propriedades do modelo como IsFacetable
Para que uma propriedade de modelo esteja localizada numa pesquisa de facetas, tem de ser etiquetada com IsFacetable.
Examine a classe Hotel . Categoria e Etiquetas, por exemplo, são etiquetadas como IsFacetable, mas HotelName e Description não.
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; } }
Não vamos alterar nenhuma etiqueta como parte deste tutorial, por isso feche o ficheiro hotel.cs sem alterações.
Nota
Uma pesquisa de facetas emitirá um erro se um campo pedido na pesquisa não estiver etiquetado adequadamente.
Adicionar navegação de facetas à sua aplicação
Neste exemplo, vamos permitir que o utilizador selecione uma categoria de hotel, ou uma amenidade, a partir de listas de ligações apresentadas à esquerda dos resultados. O utilizador começa por introduzir algum texto de pesquisa e, em seguida, reduzir progressivamente os resultados da pesquisa ao selecionar uma categoria ou amenidade.
É tarefa do controlador transmitir as listas de facetas para a vista. Para manter as seleções de utilizador à medida que a pesquisa progride, utilizamos o armazenamento temporário como mecanismo de preservação do estado.
Adicionar cadeias de filtro ao modelo SearchData
Abra o ficheiro SearchData.cs e adicione propriedades de cadeia à classe SearchData para manter as cadeias de filtro de faceta.
public string categoryFilter { get; set; } public string amenityFilter { get; set; }
Adicionar o método de ação Faceta
O controlador principal precisa de uma nova ação, Faceta e atualizações para as ações de Índice e Página existentes e para o método RunQueryAsync .
Substitua o método de ação Índice (modelo SearchData ).
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); }
Substitua o método de ação 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); }
Adicione um método de ação FacetAsync(Modelo SearchData) para ser ativado quando o utilizador clicar numa ligação de faceta. O modelo conterá um filtro de pesquisa de categoria ou de amenidade. Adicione-o após a ação 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); }
Configurar o filtro de pesquisa
Quando um utilizador seleciona uma determinada faceta, por exemplo, clica na categoria Resort e Spa , apenas os hotéis especificados como esta categoria devem ser devolvidos nos resultados. Para restringir uma pesquisa desta forma, temos de configurar um filtro.
Substitua o método RunQueryAsync pelo seguinte código. Principalmente, é necessária uma cadeia de filtro de categoria e uma cadeia de filtro de amenidade e define o parâmetro Filtro das Opções de Pesquisa.
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); }
Repare que as propriedades Categoria e Etiquetas são adicionadas à lista de Selecionar itens a devolver. Esta adição não é um requisito para que a navegação por facetas funcione, mas utilizamos estas informações para verificar se os filtros estão a funcionar corretamente.
Adicionar listas de ligações de facetas à vista
A vista vai exigir algumas alterações significativas.
Comece por abrir o ficheiro hotels.css (na pasta wwwroot/css) e adicione as seguintes classes.
.facetlist { list-style: none; } .facetchecks { width: 250px; display: normal; color: #666; margin: 10px; padding: 5px; } .facetheader { font-size: 10pt; font-weight: bold; color: darkgreen; }
Para a vista, organize a saída numa tabela, para alinhar cuidadosamente as listas de facetas à esquerda e os resultados à direita. Abra o ficheiro index.cshtml. Substitua todo o conteúdo das etiquetas de corpo> HTML <pelo seguinte código.
<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>
Repare na utilização da chamada Html.ActionLink . Esta chamada comunica cadeias de filtro válidas ao controlador quando o utilizador clica numa ligação de faceta.
Executar e testar a aplicação
A vantagem da navegação por facetas para o utilizador é que podem restringir as pesquisas com um único clique, que podemos mostrar na sequência seguinte.
Execute a aplicação, escreva "aeroporto" como o texto de pesquisa. Verifique se a lista de facetas aparece bem à esquerda. Estas facetas são todas aplicáveis aos hotéis que têm "aeroporto" nos seus dados de texto, com uma contagem da frequência com que ocorrem.
Clique na categoria Resort e Spa . Verifique se todos os resultados estão nesta categoria.
Clique na amenidade do pequeno-almoço continental . Verifique se todos os resultados ainda estão na categoria "Resort e Spa", com a amenidade selecionada.
Experimente selecionar qualquer outra categoria e, em seguida, uma amenidade e ver os resultados de redução. Em seguida, tente o contrário, uma amenidade e, em seguida, uma categoria. Enviar uma pesquisa vazia para repor a página.
Nota
Quando uma seleção é efetuada numa lista de facetas (por exemplo, categoria), irá substituir qualquer seleção anterior na lista de categorias.
Conclusões
Considere as seguintes conclusões deste projeto:
- É imperativo marcar cada campo de tabela facial com a propriedade IsFacetable para inclusão na navegação de facetas.
- As facetas são combinadas com filtros para reduzir os resultados.
- As facetas são cumulativas, com cada seleção a acumular-se na anterior para reduzir ainda mais os resultados.
Passos seguintes
No próximo tutorial, vamos analisar os resultados da encomenda. Até agora, os resultados são ordenados simplesmente pela ordem em que estão localizados na base de dados.