Cómo implementar un sistema de búsqueda avanzada con Elasticsearch y filtros facetados en tu aplicación web a medida
Cómo implementar un sistema de búsqueda avanzada con Elasticsearch y filtros facetados en tu aplicación web a medida
Llega un momento en el que tu aplicación web maneja miles (o millones) de registros — productos, expedientes, fichas técnicas, documentos — y la búsqueda nativa de la base de datos se rinde. Un LIKE '%foo%' contra PostgreSQL aguanta un tiempo, pero acaba devorando CPU y dando una experiencia mediocre. Aquí es donde Elasticsearch, combinado con filtros facetados, se convierte en la pieza que tus usuarios reconocen sin saberlo de Amazon, Booking o cualquier marketplace serio.
Te cuento cómo lo monto yo: arquitectura, decisiones que ahorran disgustos y los pasos concretos para meter Elasticsearch con filtros facetados en una aplicación web a medida.
Por qué Elasticsearch y no otro motor
Elasticsearch es un motor distribuido encima de Apache Lucene. Comparado con Solr, Meilisearch o Typesense, gana en ecosistema, escalado horizontal y, sobre todo, en agregaciones potentes — que es lo que hace posible los filtros facetados sin malabares.
Razones técnicas por las que lo elijo en proyectos a medida:
- Full-text con relevancia tuneable: ponderas campos (que el título pese más que la descripción), aplicas boosting por fecha o popularidad y configuras analyzers por idioma.
- Agregaciones nativas: las facetas salen en la misma petición. Una sola query devuelve resultados y contadores.
- Escala horizontal: ¿crece el volumen? Añades nodos al clúster sin rediseñar la app.
- Ecosistema alrededor: Kibana para visualizar, Logstash y Beats para ingesta, y una API REST decente.
Ahora, si tienes 5.000 registros y un buscador sencillo, no te metas en este charco. Meilisearch o PostgreSQL con pg_trgm te sobran. Elasticsearch justifica su coste operativo cuando necesitas facetas dinámicas, autocompletado tolerante a errores y búsquedas complejas sobre varios campos.
Arquitectura: Elasticsearch como capa, no como sustituto
El error clásico es pensar que Elasticsearch reemplaza a tu base de datos. No lo hace. La arquitectura que funciona deja la base de datos relacional (PostgreSQL, MySQL) como fuente de verdad y usa Elasticsearch solo como índice de búsqueda.
Flujo de datos básico
- Escritura: la app escribe en la BD relacional (INSERT, UPDATE, DELETE).
- Sincronización: un proceso detecta cambios y actualiza el índice de Elasticsearch.
- Lectura de búsqueda: las queries van contra Elasticsearch, que devuelve IDs.
- Hidratación: la app recupera los datos completos de la BD usando esos IDs.
Estrategias de sincronización
Aquí es donde se la juega la arquitectura. Tres enfoques:
- Síncrona: indexas dentro de la misma transacción. Simple, pero acoplas el rendimiento de escritura al estado del clúster. Si Elasticsearch va lento, tus INSERTs se arrastran. No lo recomiendo en producción.
- Cola asíncrona: cada cambio publica un evento en RabbitMQ, Redis Streams o SQS. Un worker consume e indexa. Desacopla escritura de indexado y tolera caídas temporales del clúster.
- Change Data Capture (CDC): Debezium y similares leen el WAL/binlog de la BD y replican a Elasticsearch. No tocas el código de la app, pero ganas complejidad operativa.
Para la mayoría de aplicaciones a medida, la cola asíncrona da el mejor equilibrio. La latencia entre escritura y disponibilidad en búsqueda baja del segundo, y eso es aceptable salvo en casos muy concretos.
Diseño del índice y mapping de campos
El mapping decide qué puedes buscar y cómo se presentan las facetas. Cada campo necesita un tipo adecuado, y cambiarlo después implica reindexar — así que piénsalo antes.
Tipos de campo útiles para facetas
- keyword: valores exactos para filtrar (categorías, marcas, estados). No se tokenizan.
- text: contenido full-text. Se tokeniza y analiza según el idioma.
- integer / float: rangos numéricos (precio, peso, score).
- date: filtros temporales.
- nested: objetos compuestos donde la relación entre campos internos importa.
Un mapping típico para un catálogo de productos:
{
"mappings": {
"properties": {
"nombre": { "type": "text", "analyzer": "spanish" },
"descripcion": { "type": "text", "analyzer": "spanish" },
"categoria": { "type": "keyword" },
"marca": { "type": "keyword" },
"precio": { "type": "float" },
"fecha_publicacion": { "type": "date" },
"atributos": {
"type": "nested",
"properties": {
"nombre": { "type": "keyword" },
"valor": { "type": "keyword" }
}
}
}
}
}
Analyzer para español
El analyzer spanish que viene de serie aplica stemming (junta "implementación" e "implementar" en la misma raíz) y quita stopwords. Pero para una búsqueda decente conviene montar un analyzer personalizado con:
- Filtro de sinónimos: que "móvil" encuentre también "teléfono" o "smartphone".
- ASCII folding: que "búsqueda" matchee con "busqueda" sin tilde. Tus usuarios no escriben tildes, asúmelo.
- N-gramas para autocompletado: sugerencias mientras el usuario teclea.
Filtros facetados con agregaciones
Los filtros facetados son esos paneles laterales con el recuento por valor. En Elasticsearch se construyen con aggregations.
Agregaciones de términos para facetas categóricas
Para una faceta de "Categoría" con contadores:
{
"query": { "match": { "nombre": "gestión documental" } },
"aggs": {
"categorias": {
"terms": { "field": "categoria", "size": 20 }
},
"marcas": {
"terms": { "field": "marca", "size": 50 }
}
}
}
Una petición, resultados y contadores. No necesitas N queries extra.
Agregaciones de rango para facetas numéricas
Para precio o cualquier numérico:
{
"aggs": {
"rangos_precio": {
"range": {
"field": "precio",
"ranges": [
{ "to": 50 },
{ "from": 50, "to": 200 },
{ "from": 200, "to": 500 },
{ "from": 500 }
]
}
}
}
}
El gotcha del filtrado cruzado (post_filter)
Cuando el usuario marca una categoría, los contadores de otras categorías tienen que seguir mostrándose (para que pueda cambiar), pero los contadores de marca y precio deben reflejar solo lo que coincide con esa categoría. Esto se resuelve combinando post_filter con agregaciones filtradas.
La mecánica: aplicas los filtros seleccionados dentro de cada agregación excepto el filtro propio de esa faceta. Faceta de categoría enseña todas las opciones; faceta de marca enseña solo las que casan con la categoría elegida.
Con muchas facetas esto se vuelve un lío considerable. Abstráelo en un servicio que construya las queries programáticamente. Lo agradecerás al cuarto cambio.
Autocompletado y sugerencias en tiempo real
Un buscador serio sugiere mientras el usuario escribe. Elasticsearch te da varias opciones:
- Completion Suggester: estructura en memoria (FST) para sugerencias ultrarrápidas. Ideal para nombres de productos o términos frecuentes.
- Search-as-you-type: tipo de campo que genera n-gramas automáticamente. Va bien para texto libre.
- Prefix queries sobre keyword: lo más simple, pero no perdona erratas.
Lo que monto normalmente: Completion Suggester para los términos top, más una búsqueda fuzzy por debajo para tolerar fallos de tecleo. El endpoint de autocompletado tiene que responder en menos de 100 milisegundos o la experiencia se nota torpe.
Ejemplo de campo search-as-you-type
{
"nombre_sugerencia": {
"type": "search_as_you_type",
"max_shingle_size": 3
}
}
La query es un multi_match sobre los subcampos generados (nombre_sugerencia, nombre_sugerencia._2gram, nombre_sugerencia._3gram).
Rendimiento y tuning del clúster
Un buscador que tarda más de 300 ms deja de ser útil. La optimización va por capas.
Dimensionamiento de shards
Cada índice se parte en shards. La regla práctica: entre 10 y 50 GB por shard. Si tienes 100.000 productos ocupando 2 GB, un solo shard primario sobra. No, no necesitas un cluster de 15 nodos para 50.000 documentos — sobredimensionar shards solo añade overhead.
Caché de queries
Elasticsearch cachea filtros frecuentes solo. Para sacarle partido, separa la parte de filtrado (cacheable) de la de scoring (no cacheable) usando bool con cláusulas filter en vez de must. Pequeño cambio, impacto medible.
Métricas que hay que vigilar
- Latencia P95: percentil 95 del tiempo de respuesta. Objetivo: por debajo de 200 ms.
- Tasa de indexación: documentos por segundo. Detecta cuellos de botella en la sincronización.
- Heap JVM: si pasa del 75% sostenido, necesitas más memoria o más nodos. Punto.
- Merge throttling: si los merges de segmentos Lucene están limitando la escritura.
Kibana trae dashboards predefinidos para todo esto con Stack Monitoring. Úsalos, no reinventes la rueda.
Seguridad y control de acceso
Si distintos usuarios ven distintos subconjuntos de datos, la búsqueda tiene que respetar permisos. Dos enfoques:
- Filtrado en aplicación: el backend añade un filtro de permisos a cada query (por ejemplo
"filter": {"term": {"tenant_id": "cliente-123"}}). Simple y efectivo para multitenancy. - Document Level Security (DLS): Elasticsearch con licencia Platinum o superior permite roles que limitan qué documentos ve cada usuario. Útil cuando varias apps tocan el mismo clúster.
En desarrollos a medida casi siempre voy con filtrado en aplicación: no dependes de licencias comerciales y la lógica de permisos se queda centralizada en el backend, que es donde tiene que estar.
Cuándo un buscador facetado cambia las reglas
Meter Elasticsearch con filtros facetados transforma la experiencia en aplicaciones con volúmenes importantes de datos estructurados. Catálogos de producto, portales inmobiliarios, bases de conocimiento, gestión documental — todos estos casos ganan con una búsqueda que no solo encuentra, sino que deja explorar y refinar.
El esfuerzo no es trivial: diseñas la sincronización, defines mappings, construyes las agregaciones con filtrado cruzado y tuneas el clúster. Pero el retorno en usabilidad y reducción de tiempo de búsqueda lo paga con creces.
Si tu equipo está dándole vueltas a integrar búsqueda avanzada en una aplicación existente o arrancando un desarrollo nuevo que va a manejar mucho dato, hablemos del diseño técnico que encaja con tu caso.