Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Las facetas permiten la navegación autodirigida proporcionando un conjunto de vínculos para filtrar los resultados. En este tutorial, se coloca una estructura de navegación por facetas en el lado izquierdo de la página, con etiquetas y texto en el que se puede hacer clic para recortar los resultados.
En este tutorial, aprenderá a:
- Establecer las propiedades del modelo como IsFacetable
- Agregar navegación por facetas a la aplicación
Información general
Las facetas se basan en campos del índice de búsqueda. Una solicitud de consulta que incluye facet=[string] proporciona el campo por el que se va a facetar. Es habitual incluir varias facetas, como
En este tutorial se amplía el proyecto de paginación creado en el tutorial Agregar paginación a los resultados de búsqueda .
En el proyecto siguiente se puede encontrar una versión finalizada del código de este tutorial:
Prerrequisitos
- Solución 2a-add-paging (GitHub). Este proyecto puede ser su propia versión compilada a partir del tutorial anterior o una copia de GitHub.
Establecer las propiedades del modelo como IsFacetable
Para que una propiedad de modelo se encuentre en una búsqueda de facetas, debe etiquetarse con IsFacetable.
Examine la clase Hotel . La categoría y las etiquetas, por ejemplo, se etiquetan como IsFacetable, pero HotelName y Description no.
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; } }No cambiaremos ninguna etiqueta como parte de este tutorial, por lo que cerraremos el archivo hotel.cs sin modificar.
Nota:
Una búsqueda de facetas producirá un error si un campo solicitado en la búsqueda no se etiqueta correctamente.
Agregar navegación por facetas a la aplicación
En este ejemplo, vamos a permitir que el usuario seleccione una categoría de hotel, o un tipo de servicios, en listas de vínculos que se muestran a la izquierda de los resultados. El usuario comienza escribiendo texto de búsqueda y, a continuación, limita progresivamente los resultados de la búsqueda seleccionando una categoría o servicios.
Es el trabajo del controlador pasar las listas de facetas a la vista. Para mantener las selecciones de usuario a medida que avanza la búsqueda, usamos el almacenamiento temporal como mecanismo para conservar el estado.
Adición de cadenas de filtro al modelo SearchData
Abra el archivo SearchData.cs y agregue propiedades de cadena a la clase SearchData para contener las cadenas de filtro de faceta.
public string categoryFilter { get; set; } public string amenityFilter { get; set; }
Adición del método de acción Facet
El controlador principal necesita una nueva acción, Faceta y actualizaciones de sus acciones de índice y página existentes, y al método RunQueryAsync .
Reemplace el método de acción 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); }Reemplace el método de acción 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); }Agregue un método de acción FacetAsync(SearchData model) para activarse cuando el usuario haga clic en un vínculo de faceta. El modelo contendrá un filtro de búsqueda de categorías o servicios. Agréguelo después de la acción 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); }
Configuración del filtro de búsqueda
Cuando un usuario selecciona una determinada faceta, por ejemplo, hace clic en la categoría Resort and Spa y, a continuación, solo los hoteles especificados como esta categoría deben devolverse en los resultados. Para restringir una búsqueda de esta manera, es necesario configurar un filtro.
Reemplace el método RunQueryAsync por el código siguiente. Principalmente, toma una cadena de filtro de categoría y una cadena de filtro de servicios y establece el parámetro Filter de 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); }Tenga en cuenta que las propiedades Categoría y Etiquetas se agregan a la lista de Seleccionar elementos que se van a devolver. Esta adición no es un requisito para que la navegación por facetas funcione, pero usamos esta información para comprobar que los filtros funcionan correctamente.
Agregar listas de vínculos de facetas a la vista
La vista va a requerir algunos cambios significativos.
Comience abriendo el archivo hotels.css (en la carpeta wwwroot/css) y agregue las siguientes clases.
.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 la vista, organice la salida en una tabla para alinear de manera ordenada las listas de facetas a la izquierda y los resultados a la derecha. Abra el archivo index.cshtml. Reemplace todo el contenido de las etiquetas <body> HTML por el código siguiente.
<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>Observe el uso de la llamada Html.ActionLink . Esta llamada comunica cadenas de filtro válidas al controlador cuando el usuario hace clic en un vínculo de faceta.
Ejecución y prueba de la aplicación
La ventaja de la navegación por facetas al usuario es que puede restringir las búsquedas con un solo clic, que podemos mostrar en la siguiente secuencia.
Ejecute la aplicación, escriba "airport" como texto de búsqueda. Compruebe que la lista de facetas aparece perfectamente a la izquierda. Estas facetas son todas las que se aplican a los hoteles que tienen "aeropuerto" en sus datos de texto, con un recuento de la frecuencia con la que se producen.
Haga clic en la categoría Resort and Spa . Compruebe que todos los resultados están en esta categoría.
Haga clic en la amenidad de desayuno continental. Compruebe que todos los resultados siguen estando en la categoría "Resort and Spa", con los servicios seleccionados.
Intente seleccionar cualquier otra categoría, luego una comodidad, y luego ver cómo se filtran los resultados. A continuación, pruebe de otra manera, una amenidad y luego una categoría. Envíe una búsqueda vacía para restablecer la página.
Nota:
Cuando se realiza una selección en una lista de facetas (por ejemplo, categoría), invalidará cualquier selección anterior dentro de la lista de categorías.
Conclusiones
Tenga en cuenta los siguientes aspectos de este proyecto:
- Es imperativo marcar cada campo facetable con la propiedad IsFacetable para su inclusión en la navegación por facetas.
- Las facetas se combinan con filtros para reducir los resultados.
- Las facetas son acumulativas, y cada selección se basa en la anterior para restringir aún más los resultados.
Pasos siguientes
En el siguiente tutorial, veremos los resultados de ordenación. Hasta este punto, los resultados se ordenan simplemente en el orden en que se encuentran en la base de datos.