Cómo funciona de verdad la búsqueda por facetas en Elasticsearch (y dónde falla)
Una faceta son dos preguntas, no una
Una página de búsqueda por facetas plantea dos preguntas distintas a la vez. La primera es evidente: qué documentos coinciden con la búsqueda en curso y con los filtros ya aplicados. La segunda se plantea una vez por cada valor de filtro disponible: cuántos documentos coincidirían si ese valor también estuviera seleccionado. La casilla «Rojo (12)» es la respuesta a la segunda pregunta, calculada mientras se responde a la primera.
Lo esencial de lo que hace difícil la búsqueda por facetas, en Elasticsearch igual que en cualquier otro sitio, viene de esa segunda pregunta. Obtener la lista de documentos coincidentes no es complicado. Obtener un recuento honesto junto a cada valor de cada filtro, en cada carga de página, sin que el tiempo de respuesta se dispare a medida que aumenta el número de facetas, ese es el verdadero problema de ingeniería.
El ladrillo básico: una agregación terms sobre un conjunto filtrado
Elasticsearch responde a las dos preguntas con una sola consulta. La cláusula de consulta restringe el conjunto de documentos (coincidencia de texto completo más los filtros ya activos) y un conjunto de agregaciones asociadas a esa misma consulta calcula, para cada campo de faceta, cuántos documentos de ese conjunto filtrado caen en cada valor. Una agregación terms sobre un campo color devuelve un bucket por cada valor distinto con su número de documentos; una agregación range o histogram hace lo mismo con los campos numéricos como el precio. Todo ocurre en la misma ejecución de la consulta, sobre el mismo conjunto filtrado, en un solo viaje de ida y vuelta.
Es el punto que resulta fácil pasar por alto si solo ha usado SQL: aquí una agregación no es una segunda consulta sobre el resultado de la primera. Se ejecuta sobre los doc values y los postings del índice durante la misma consulta, y por eso añadir veinte facetas más a una página no significa veinte recorridos completos más de los documentos coincidentes. Significa veinte cálculos de buckets más sobre datos que el motor tenía que tocar de todos modos para responder a la consulta.
Contexto filter, no contexto query
Las facetas, y los filtros que se aplican al hacer clic en ellas, deberían estar casi siempre en contexto filter, no en contexto query. Un filtro term sobre brand: acme no necesita calcular una puntuación de relevancia, necesita una respuesta de sí o no, y Elasticsearch almacena en caché las cláusulas de filtro de forma mucho más agresiva que las cláusulas de consulta con puntuación. Construir una página de búsqueda por facetas solo con cláusulas bool must que puntúan todas es una manera habitual de hacerla más lenta de lo necesario sin ningún beneficio: nadie ordena una página de categoría por «relevancia respecto al filtro Rojo». Los filtros van en filter, la búsqueda de texto completo va en must o en una consulta match específica, y esta separación no es cosmética: cambia lo que se almacena en caché y lo que se puntúa.
La trampa de la multiselección
El detalle que rompe más implementaciones de búsqueda por facetas que ningún otro es este: al calcular el recuento de una faceta, hay que excluir el filtro de esa misma faceta del conjunto de documentos sobre el que se cuenta, manteniendo activos todos los demás filtros. Si no, cada faceta sobre la que ya ha filtrado mostrará un recuento igual al número de resultados que hay en pantalla, porque se está contando la intersección con una condición ya aplicada por completo. Haga clic en «Rojo» y cada otro valor de «Color» mostraría por error la misma cifra que el total, en lugar del número de productos rojos y de ese valor que existen realmente.
La corrección habitual consiste en construir una agregación por campo de faceta, cada una envuelta en su propia agregación filter que vuelve a aplicar todos los filtros activos salvo el de ese campo, todas al mismo nivel bajo la misma consulta principal. En concreto: la consulta base ya lleva la búsqueda de texto completo y todos los filtros que no se están contando, y la agregación de cada faceta añade los demás filtros seleccionados, menos el suyo. Esto supone más agregaciones por consulta, no más consultas, y es la parte que merece la pena probar de forma explícita en lugar de validarla a ojo, porque una página puede parecer perfectamente correcta en una demostración con tres productos y mostrar en silencio recuentos falsos en cuanto se cargan datos reales, con solapamientos reales.
Hay una segunda decisión, ligada a la primera, que conviene tomar de forma deliberada: qué significa un grupo de facetas cuando se seleccionan a la vez varios valores bajo la misma etiqueta. Si una tienda agrupa «Carmesí», «Escarlata» y «Burdeos» bajo un único filtro «Rojo», ¿seleccionar dos de esos subvalores significa «tiene esto Y aquello» o «tiene esto O aquello»? Un recuento que responde a la pregunta Y cuando el clic aplica la pregunta O (o al revés) es la causa más frecuente de un filtro que muestra una cifra y devuelve otra. Ambos deben calcularse con exactamente la misma lógica, lo que normalmente equivale a tratar el grupo desde el principio como una única condición de unión, en lugar de sumar recuentos por valor que pueden solaparse o contar dos veces el mismo producto.
Por qué todo se hunde sin un motor de agregación, con una cifra real
La razón de ser de los buscadores por facetas, en lugar de hacerlo todo en SQL, queda clara en cuanto se intenta obtener recuentos exactos por valor en una base de datos relacional a gran escala. En un catálogo de pruebas de 200.000 productos con el buscador integrado de PrestaShop, limitado a la base de datos, con unos 2.250 grupos de atributos y características, calcular un recuento exacto de «cuántos resultados por valor» con una consulta COUNT(DISTINCT ...) por faceta hacía que una página de categoría con un filtro activo tardara 43 segundos. Desactivar los recuentos exactos de esas facetas (mostrar la lista de valores sin cifra) devolvió la misma página a algo menos de 12 segundos, lo que muestra qué parte del coste inicial correspondía al puro recuento y no a la búsqueda en sí.
Un error parecido, detectado en una revisión de código interna de un módulo de búsqueda por facetas, consistía en calcular el intervalo de una faceta numérica (para un control deslizante de «Precio» o «Peso») cargando todos los valores coincidentes en la memoria de la aplicación y calculando ahí el mínimo y el máximo, en lugar de pedírselo a la base de datos o al buscador. En un catálogo donde eso suponía unos dos millones de filas, ese único paso tardaba 11 segundos y 965 MB de memoria por consulta, lo que en algunas configuraciones bastaba para agotar el límite de memoria de un alojamiento compartido y devolver una página de error en lugar de un control deslizante. Trasladar el mismo cálculo a un GROUP BY de la base de datos, o a la agregación range del buscador, lo convirtió de nuevo en una consulta normal y rápida, porque es justo el tipo de cálculo agregado que estos sistemas están diseñados para hacer en una sola pasada sobre datos indexados, y no en el código de la aplicación a posteriori.
Las dos cifras describen el mismo error de fondo en niveles distintos: preguntar «cuántos, para cada valor posible» una vez por valor en lugar de una sola vez para todos los valores juntos. El framework de agregación de Elasticsearch existe precisamente para responder a esta pregunta en una sola pasada sobre datos ya indexados. Ahí está su verdadero valor, con independencia de la ordenación por relevancia de texto completo, y por eso la navegación por facetas es una de las razones más frecuentes para adoptar Elasticsearch, incluso cuando la búsqueda en sí no necesita funciones de relevancia avanzadas.
Una lista de comprobación breve antes de poner en línea una búsqueda por facetas
- Los filtros van en contexto filter, el texto completo en contexto query. No calcule una puntuación para una casilla de verificación.
- La agregación de recuento de cada faceta debe excluir el filtro activo de esa faceta y mantener todos los demás; de lo contrario, las facetas con multiselección mentirán en cuanto se usen dos filtros a la vez.
- Decida desde el principio, y pruebe de forma explícita, si un grupo de valores bajo una misma etiqueta funciona en Y o en O, y haga que el recuento y el clic compartan exactamente la misma lógica.
- Limite el tamaño de las agregaciones y vigile
doc_count_error_upper_bounden los campos de alta cardinalidad; una agregación terms sin límite que devuelve miles de buckets es un problema de rendimiento por sí mismo, independiente de la cuestión de la multiselección expuesta arriba. - No calcule nunca las cifras de una faceta cargando filas en bruto en la memoria de la aplicación. Si se sorprende escribiendo un bucle sobre los resultados de una consulta para construir un recuento o un intervalo, ese bucle tiene su sitio en la agregación, no en su código.
Cuando Elasticsearch no es la respuesta
Nada de esto significa que todo catálogo necesite Elasticsearch. Una tienda de unos pocos miles de productos con un puñado de facetas rara vez notará la diferencia que aporta un buscador dedicado, y mantener un clúster en marcha tiene su propio coste de explotación: memoria, disco y alguien que sepa gestionar la reindexación cuando cambia el esquema. Las cifras anteriores solo empiezan a importar cuando el número de facetas, el tamaño del catálogo, o ambos, se vuelven lo bastante grandes como para que un recuento ingenuo se mida en segundos y no en milisegundos. Si no es su caso, una consulta de base de datos bien optimizada es una elección perfectamente razonable, y la lista de comprobación anterior también se aplica: la cuestión Y/O y la trampa de la multiselección son el mismo problema, sea cual sea el motor que responde.
En pocas palabras
La búsqueda por facetas no es una consulta, es una consulta más una agregación por filtro, y casi todos los errores reales vienen de saltarse este modelo: puntuar lo que debería ser un filtro, contar una faceta teniendo en cuenta su propia condición ya aplicada, o responder al recuento y al clic con dos definiciones distintas de un grupo. El framework de agregación de Elasticsearch resuelve limpiamente el problema de escala, en una sola pasada sobre datos indexados en lugar de una consulta por valor de faceta, y esa es la razón concreta por la que las cifras de 43 y 11 segundos de arriba aparecieron en un motor limitado a la base de datos y no en uno diseñado para ello. Hacemos este trabajo a diario en cll_essearch, nuestro módulo de búsqueda por facetas para PrestaShop, tanto en el driver nativo como en Elasticsearch, OpenSearch y Meilisearch (consulte la guía de instalación), si quiere ver la misma lógica aplicada a una tienda online en lugar de a un índice en bruto.
Búsqueda Elasticsearch y Navegación por FacetasBúsqueda ultrarrápida y tolerante a errores de tecleo con navegación por facetas moderna en panel modal, con e…99,99 €Ver producto