Arquitectura de Búsqueda E-Commerce: MeiliSearch, OpenSearch e Historias Reales de Migración
Cómo diseñar la búsqueda de productos para comercio. MeiliSearch vs OpenSearch vs Elasticsearch, diseño de índices, búsqueda facetada, estrategias multilingües, búsqueda híbrida y sincronización en tiempo real desde PIM y sistemas de comercio.
Por Qué la Búsqueda de Productos No Es Búsqueda de Texto
La búsqueda de productos parece simple. Un usuario escribe "zapatillas azules running talla 42" y espera resultados relevantes. Pero la implementación es fundamentalmente diferente a la búsqueda de documentos o la búsqueda web. Los productos tienen atributos estructurados (talla, color, precio, marca), categorías jerárquicas, disponibilidad que cambia en tiempo real, nombres y descripciones localizados, y facetas por las que los usuarios esperan poder filtrar.
Un motor de búsqueda de documentos encuentra documentos que coinciden con una consulta. Un motor de búsqueda de productos debe encontrar productos, clasificarlos por relevancia comercial (no solo relevancia de texto), presentar facetas filtrables, manejar errores tipográficos y sinónimos, soportar múltiples idiomas y actualizarse en tiempo real cuando cambia el inventario.
Hemos construido sistemas de búsqueda de productos tanto con MeiliSearch como con OpenSearch, migrando desde Elasticsearch 7.4 en un caso y construyendo desde cero en otro. Este artículo cubre las decisiones de arquitectura, no los detalles de configuración. Para patrones de búsqueda vectorial específicamente, consulta nuestra guía de arquitectura de búsqueda vectorial. Para el contexto más amplio de comercio, consulta nuestra guía de plataformas ecommerce.
MeiliSearch vs OpenSearch vs Elasticsearch
| Criterio | MeiliSearch | OpenSearch | Elasticsearch |
|---|---|---|---|
| Lenguaje | Rust | Java | Java |
| Tolerancia a errores | Incorporada, excelente | Plugin/personalizado | Plugin/personalizado |
| Búsqueda facetada | Incorporada, rápida | Incorporada (agregaciones) | Incorporada (agregaciones) |
| Búsqueda vectorial | Experimental | Incorporada (k-NN) | Incorporada (dense_vector) |
| Multilingüe | Bueno (tokenizadores por idioma) | Excelente (analizadores por campo) | Excelente (analizadores por campo) |
| Indexación en tiempo real | Casi instantánea (< 50ms) | Casi tiempo real (1s de refresco) | Casi tiempo real (1s de refresco) |
| Complejidad | Baja (binario único, API REST) | Alta (clúster, shards, réplicas) | Alta (clúster, shards, réplicas) |
| Uso de memoria | Bajo (Rust, eficiente) | Alto (JVM heap) | Alto (JVM heap) |
| Costo operativo | Bajo (funciona en instancias pequeñas) | Medio a alto | Medio a alto |
| Ordenamiento | Reglas de ranking incorporadas | Ordenamiento flexible | Ordenamiento flexible |
| Licencia | MIT | Apache 2.0 | SSPL (no es open source) |
| Ideal para | Catálogos pequeños a medianos (< 500K productos) | Catálogos grandes, consultas complejas, búsqueda híbrida | Igual que OpenSearch (si ya tienes inversión previa) |
Cuándo Elegir MeiliSearch
- Catálogo con menos de 500K productos
- La tolerancia a errores tipográficos es crítica (búsqueda orientada al consumidor)
- El equipo tiene experiencia limitada en infraestructura de búsqueda
- La rapidez de configuración importa más que las funciones avanzadas de consulta
- El presupuesto es ajustado (funciona en una sola instancia pequeña)
Cuándo Elegir OpenSearch
- Catálogo con más de 100K productos con facetas complejas
- Necesitas búsqueda híbrida (texto + vector / k-NN)
- Múltiples grupos de consumidores procesan el mismo índice
- Ya estás en AWS (OpenSearch Serverless es gestionado)
- Necesitas agregaciones avanzadas y analítica sobre datos de búsqueda
Cuándo Elegir Elasticsearch
- Ya estás ejecutando Elasticsearch y no hay razón para migrar
- Necesitas funciones específicas de Elastic (inferencia ML, seguridad)
- Se requiere un contrato de soporte empresarial
Para la mayoría de proyectos de comercio nuevos, recomendamos MeiliSearch por simplicidad u OpenSearch por potencia. La licencia SSPL de Elasticsearch lo hace menos atractivo para nuevos despliegues.
Diseño de Índices
El error más común: indexar tu esquema de base de datos directamente. Las tablas de productos están normalizadas. Los índices de búsqueda deben estar desnormalizados.
// Base de datos: normalizada (relacional)
// tabla products: id, name, category_id, brand_id
// tabla categories: id, name, parent_id
// tabla variants: id, product_id, sku, price, size, color
// tabla translations: id, product_id, locale, name, description
// Índice de búsqueda: desnormalizado (documento plano)
interface ProductSearchDocument {
id: string;
name: string; // locale actual
description: string; // locale actual
slug: string;
sku: string[]; // todos los SKUs de variantes
brand: string; // desnormalizado de la tabla de marcas
categories: string[]; // jerarquía completa: ["Zapatos", "Running", "Trail"]
categoryIds: string[]; // para filtrado de facetas
price: number; // precio más bajo de variantes (para ordenamiento)
priceRange: { min: number; max: number };
sizes: string[]; // todas las tallas disponibles
colors: string[]; // todos los colores disponibles
inStock: boolean; // alguna variante en stock
imageUrl: string; // imagen principal
rating: number; // calificación promedio de reseñas
reviewCount: number;
tags: string[]; // etiquetas buscables
createdAt: number; // para ordenamiento "más recientes primero"
popularity: number; // conteo de ventas o vistas
}
Reglas para la desnormalización:
- Aplana todas las relaciones en el documento (nombre de marca, no ID de marca)
- Incluye la jerarquía completa de categorías como un array (permite drill-down en facetas)
- Incluye todos los atributos de variantes (tallas, colores) como arrays en el producto
- Usa el precio más bajo para ordenar, rango de precios para mostrar
- Incluye campos calculados (rating, reviewCount, popularity) para el ranking
- Un documento por producto por locale (no un documento con todos los locales)
Un Índice por Locale
Para comercio multilingüe, crea un índice por locale:
products_en
products_de
products_fr
products_ar
Cada índice usa analizadores, tokenizadores y stop words específicos del idioma. Una búsqueda en alemán de "Laufschuhe" usa stemming alemán. Una búsqueda en árabe usa análisis morfológico árabe. Mezclar locales en un solo índice te obliga a hacer compromisos en el análisis que degradan la calidad para todos los idiomas.
// MeiliSearch: un índice por locale
await meili.createIndex('products_de', { primaryKey: 'id' });
await meili.index('products_de').updateSettings({
searchableAttributes: ['name', 'description', 'brand', 'tags', 'categories'],
filterableAttributes: ['categories', 'brand', 'sizes', 'colors', 'price', 'inStock'],
sortableAttributes: ['price', 'createdAt', 'popularity', 'rating'],
});
Arquitectura de Búsqueda Facetada
Las facetas son los filtros en el lado izquierdo de toda página de búsqueda de comercio. Parecen simples pero requieren un diseño cuidadoso.
Tipos de Facetas
| Tipo | Ejemplo | Implementación |
|---|---|---|
| Faceta de término | Marca: Nike (42), Adidas (38) | Agregación de términos en campo brand |
| Faceta de rango | Precio: 0-50 (15), 50-100 (28), 100+ (12) | Agregación de rango en campo price |
| Faceta jerárquica | Categoría: Zapatos > Running > Trail | Agregación de términos multinivel en jerarquía de categorías |
| Faceta booleana | En Stock: Sí (89), No (11) | Agregación de términos en campo inStock |
| Faceta de color | Muestras de color con conteos | Agregación de términos en campo array colors |
| Faceta de talla | Talla: 40 (5), 41 (8), 42 (12) | Agregación de términos en campo array sizes |
Interacción de Facetas
Cuando un usuario selecciona una faceta, las demás facetas deben actualizarse para reflejar los resultados filtrados. Esto se llama "refinamiento de facetas" y es la parte más compleja de la UI de búsqueda.
// MeiliSearch: conteo de facetas con filtros activos
const results = await meili.index('products_de').search('laufschuhe', {
filter: ['brand = "Nike"', 'inStock = true'],
facets: ['categories', 'brand', 'sizes', 'colors', 'price'],
});
// results.facetDistribution:
// {
// categories: { "Running": 42, "Trail": 18, "Road": 24 },
// brand: { "Nike": 42 }, // solo Nike (porque está filtrado)
// sizes: { "40": 5, "41": 8, "42": 12, "43": 10, "44": 7 },
// colors: { "Black": 20, "White": 15, "Blue": 7 },
// }
La decisión clave de UX: cuando un filtro de marca está activo, la faceta de marca, debe mostrar solo la marca seleccionada (con su conteo) o todas las marcas (con conteos que reflejen la consulta actual menos el filtro de marca)? El segundo enfoque ("facetado disjuntivo") permite a los usuarios comparar conteos entre marcas. MeiliSearch soporta esto de forma nativa. OpenSearch requiere consultas de agregación separadas por cada faceta disjuntiva.
Sincronización en Tiempo Real desde Sistemas de Origen
Los índices de búsqueda deben mantenerse sincronizados con la fuente de verdad (PIM, base de datos de comercio, ERP). La arquitectura de sincronización depende del sistema de origen.
Sincronización Basada en Eventos (Recomendada)
El sistema de origen emite eventos ante cambios en los datos. Un worker consume los eventos y actualiza el índice de búsqueda.
// Vendure: sincronizar ante eventos de productos
@Injectable()
export class SearchIndexSubscriber {
constructor(
private eventBus: EventBus,
private searchService: SearchIndexService,
) {
this.eventBus.ofType(ProductEvent).subscribe(async event => {
if (event.type === 'updated' || event.type === 'created') {
await this.searchService.indexProduct(event.ctx, event.product.id);
}
if (event.type === 'deleted') {
await this.searchService.removeProduct(event.product.id);
}
});
this.eventBus.ofType(ProductVariantEvent).subscribe(async event => {
// El cambio en la variante afecta al documento de búsqueda del producto padre
await this.searchService.indexProduct(event.ctx, event.productVariant.productId);
});
}
}
Reindexación Completa Programada
Incluso con sincronización basada en eventos, ejecuta una reindexación completa programada como red de seguridad. Los eventos pueden perderse (caída del broker, fallo del worker). Una reindexación completa nocturna captura todo lo que la sincronización basada en eventos haya omitido.
// Job de reindexación completa nocturna
async function fullReindex(locale: string) {
const batchSize = 500;
let offset = 0;
let products = [];
do {
products = await productService.findAll({ take: batchSize, skip: offset });
const documents = products.map(p => buildSearchDocument(p, locale));
await meili.index(`products_${locale}`).addDocuments(documents);
offset += batchSize;
} while (products.length === batchSize);
}
Manejo de Eliminaciones
Las eliminaciones de productos son complicadas. Si eliminas un producto de la base de datos, la sincronización basada en eventos lo elimina del índice. Pero si el evento se pierde, el producto eliminado permanece en los resultados de búsqueda.
Dos soluciones:
- Registrar marcas de tiempo de eliminación y filtrar por "no eliminado" en las consultas
- La reindexación completa reemplaza el índice entero de forma atómica (intercambio de alias)
// Reindexación atómica con intercambio de alias (OpenSearch/Elasticsearch)
async function atomicReindex(locale: string) {
const newIndex = `products_${locale}_${Date.now()}`;
await opensearch.indices.create({ index: newIndex, body: indexSettings });
// Indexar todos los productos en el nuevo índice
await bulkIndex(newIndex, locale);
// Intercambiar alias de forma atómica
await opensearch.indices.updateAliases({
body: {
actions: [
{ remove: { index: `products_${locale}_*`, alias: `products_${locale}` } },
{ add: { index: newIndex, alias: `products_${locale}` } },
],
},
});
// Eliminar índices antiguos
await cleanupOldIndices(`products_${locale}_*`, keepLast: 2);
}
Para ver cómo manejamos pipelines de sincronización de datos a escala, nuestro Vendure Data Hub Plugin implementa todos estos patrones con 7 sinks de búsqueda diferentes.
Ajuste de Relevancia
La relevancia de búsqueda por defecto es incorrecta para comercio. La relevancia de texto (qué tan bien la consulta coincide con el documento) es una señal. La relevancia comercial (qué tan probable es que el usuario compre) es igual de importante.
Señales de Ranking
| Señal | Peso | Origen |
|---|---|---|
| Coincidencia de texto (título) | Alto | Motor de búsqueda |
| Coincidencia de texto (descripción) | Medio | Motor de búsqueda |
| En stock | Crítico (boost o filtro) | Sistema de inventario |
| Popularidad (conteo de ventas) | Medio | Datos de pedidos |
| Calificación de reseñas | Bajo-Medio | Reseñas |
| Novedad (productos nuevos) | Bajo | Fecha de creación del producto |
| Margen (interno) | Opcional | Reglas de negocio |
// MeiliSearch: reglas de ranking personalizadas
await meili.index('products_de').updateSettings({
rankingRules: [
'words', // 1. Calidad de coincidencia de texto
'typo', // 2. Tolerancia a errores tipográficos
'proximity', // 3. Proximidad de palabras
'attribute', // 4. Qué campo coincidió (título > descripción)
'sort', // 5. Ordenamiento solicitado por el usuario
'exactness', // 6. Coincidencia exacta vs parcial
'popularity:desc', // 7. Productos populares se posicionan más arriba
'rating:desc', // 8. Productos mejor calificados se posicionan más arriba
],
});
Impulsar Productos en Stock
Los productos agotados deberían aparecer más abajo en los resultados, no desaparecer por completo. Los usuarios podrían querer ver productos próximos o suscribirse a notificaciones de reposición.
// OpenSearch: impulsar productos en stock
const query = {
bool: {
must: [{ match: { searchText: userQuery } }],
should: [
{ term: { inStock: { value: true, boost: 5.0 } } }, // Impulso fuerte para productos en stock
],
filter: [
{ term: { tenant_id: tenantId } },
],
},
};
Búsqueda Híbrida para Comercio
Combinar búsqueda de texto con búsqueda vectorial mejora los resultados para consultas en lenguaje natural, preservando la capacidad de coincidencia exacta para SKUs y códigos de producto.
// OpenSearch: búsqueda híbrida (texto + vector)
const results = await opensearch.search({
index: 'products_en',
body: {
query: {
bool: {
should: [
// Búsqueda de texto (maneja SKUs, nombres exactos de productos)
{ multi_match: { query: userQuery, fields: ['name^3', 'description', 'sku^5', 'tags'], type: 'best_fields' } },
// Búsqueda vectorial (maneja lenguaje natural, similitud semántica)
{ knn: { embedding: { vector: queryEmbedding, k: 20 } } },
],
},
},
// Facetas
aggs: {
brands: { terms: { field: 'brand.keyword', size: 20 } },
categories: { terms: { field: 'categories.keyword', size: 30 } },
price_ranges: { range: { field: 'price', ranges: [{ to: 50 }, { from: 50, to: 100 }, { from: 100 }] } },
},
},
});
Las consultas de SKU ("ABC-12345") van por la ruta de búsqueda de texto con alta precisión. Las consultas en lenguaje natural ("zapatos cómodos para caminatas largas") van por la ruta de búsqueda vectorial con comprensión semántica. Ambas contribuyen al ranking final.
Para más detalles sobre los mecanismos internos de la búsqueda vectorial, consulta nuestra guía de arquitectura de búsqueda vectorial.
Errores Comunes
-
Indexar datos normalizados. Tus documentos de búsqueda deben estar desnormalizados. Aplana todas las relaciones en el documento. No hagas referencia a IDs que requieran una segunda consulta.
-
Un solo índice para todos los locales. Crea un índice por locale. Los índices con locales mezclados no pueden usar analizadores específicos del idioma, y la calidad de búsqueda se degrada para todos los idiomas.
-
Sin diseño de facetas. Las facetas no son algo secundario. Planifica qué atributos son filtrables, cómo funcionan las categorías jerárquicas y cómo se actualizan los conteos de facetas cuando se aplican filtros.
-
Sincronización solo mediante reindexación programada. La sincronización basada en eventos proporciona actualizaciones casi en tiempo real. La reindexación programada es una red de seguridad, no el mecanismo principal.
-
Sin ajuste de relevancia. La relevancia de texto por defecto es incorrecta para comercio. Impulsa los productos en stock, incorpora popularidad y calificaciones, y pondera las coincidencias en el título por encima de las coincidencias en la descripción.
-
Ignorar productos agotados. No los elimines del índice. Degrádalos en el ranking. Los usuarios podrían querer alertas de reposición o navegar productos próximos.
-
Sin reindexación atómica. Si tu proceso de reindexación falla a la mitad, tienes un índice parcialmente actualizado. Usa el intercambio de alias para un cambio atómico.
-
Tratar la búsqueda como una funcionalidad, no como infraestructura. La búsqueda es un servicio central. Necesita su propio clúster, su propio monitoreo, su propia estrategia de escalado. No la ejecutes en el mismo servidor que tu base de datos.
Conclusiones Clave
-
La búsqueda de productos no es búsqueda de texto. Atributos estructurados, facetas, relevancia comercial, inventario en tiempo real y soporte multilingüe la hacen fundamentalmente diferente.
-
Desnormaliza para la búsqueda, normaliza para el almacenamiento. El documento de búsqueda es una representación plana y autocontenida de todo lo necesario para renderizar un resultado de búsqueda. Sin joins, sin lookups.
-
Un índice por locale. Los analizadores, tokenizadores y stop words específicos del idioma producen resultados drásticamente mejores que un solo índice con idiomas mezclados.
-
Sincronización basada en eventos con reindexación programada como red de seguridad. Actualizaciones en tiempo real para operaciones normales. Reindexación completa nocturna para capturar todo lo que los eventos hayan omitido.
-
El ajuste de relevancia es una decisión de negocio. Calidad de coincidencia de texto, estado de stock, popularidad, calificaciones y margen son todas señales de ranking. La relevancia por defecto es incorrecta para comercio.
-
MeiliSearch para simplicidad, OpenSearch para potencia. MeiliSearch es perfecto para catálogos con menos de 500K productos y excelente tolerancia a errores tipográficos. OpenSearch maneja agregaciones complejas, búsqueda híbrida y despliegues a gran escala.
Construimos infraestructura de búsqueda como parte de nuestra práctica de ingeniería de datos y ecommerce. Si estás construyendo o migrando la búsqueda de productos, habla con nuestro equipo o solicita un presupuesto. Nuestro Vendure Data Hub Plugin incluye sinks de búsqueda para MeiliSearch, OpenSearch, Elasticsearch, Algolia y Typesense.
Temas cubiertos
Guías relacionadas
Guía Empresarial de Sistemas de IA Agéntica
Guia tecnica de sistemas de IA agentica en entornos empresariales. Descubre la arquitectura, capacidades y aplicaciones de agentes IA autonomos.
Leer guíaComercio Agéntico: Cómo Dejar que los Agentes IA Compren de Forma Segura
Cómo diseñar comercio iniciado por agentes IA con gobernanza. Motores de políticas, puertas de aprobación HITL, recibos HMAC, idempotencia, aislamiento de tenants y el Agentic Checkout Protocol completo.
Leer guíaLos 9 Puntos Donde Tu Sistema de IA Filtra Datos (y Cómo Sellar Cada Uno)
Un mapa sistemático de cada lugar donde se filtran datos en sistemas de IA. Prompts, embeddings, logs, llamadas a herramientas, memoria de agentes, mensajes de error, caché, datos de fine-tuning y handoffs entre agentes.
Leer guía¿Listo para construir sistemas de IA listos para producción?
Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.
Iniciar una conversación