docs(adr): propuesta de motor de busqueda y facetas (Fase 5 Etapa 3)
Etapa 1 (auditoria/propuesta, sin codigo) de la Etapa 3 de Fase 5: motor de busqueda y filtros facetados reales, reemplazando el filtrado/conteo 100% client-side que dejo la sub-fase 3 de Etapa 2. Verificado contra documentacion oficial vigente (no memoria de entrenamiento): - Medusa no tiene modulo de busqueda propio integrado con Meilisearch; el Search Module nativo llego en Medusa 2.19, y el plugin comunitario @rokmohar/medusa-plugin-meilisearch se conecta a el solo desde su version ^2.0.0 (requiere Medusa >=2.19). Nuestro backend corre Medusa 2.18.0 -- la version compatible es la linea ^1.4.1 del plugin (Medusa >=2.15 <2.19), documentada como deuda tecnica explicita si se sube Medusa mas adelante. - Impacto de recursos evaluado contra el estado real del host (swap al 100%, mismo tipo de contencion que el incidente de Gitea): Meilisearch recomendado sobre Typesense por menor huella de RAM y por ser el unico con integracion Medusa mantenida para nuestra version -- comparacion mantenida breve porque la diferencia es clara, sin profundizar mas de lo necesario. - Diseno de indexacion (campos buscables/filtrables/ordenables), sincronizacion (mismo patron de subscriber por eventos que revalidate-storefront.ts, sumando product-category.* al set de eventos) y UI de facetas/autocompletado (reutiliza catalog-client.tsx y CatalogProvider.searchProducts ya existentes, sin componentes nuevos). Cross-referencia agregada en ADR 0004 (que ya habia identificado Meilisearch como paso natural) y entrada nueva en mkdocs.yml. Etapa 2 (implementacion) NO arranca hasta aprobacion explicita de esta propuesta.
This commit is contained in:
1 parent
3b2f99c7a0
commit
5ce7030c4d
3 files changed
+261
-1
No files matched your search
@@ -109,7 +109,10 @@ Un motor de búsqueda dedicado (Meilisearch, autohospedado) queda
|
||||
identificado como el siguiente paso natural una vez el catálogo
|
||||
crezca más allá de ~100 productos activos, pero no es parte del
|
||||
alcance inmediato de Etapa 2 — se revisará cuando ese techo esté
|
||||
cerca.
|
||||
cerca. **Actualización 2026-08-25:** terminada la Etapa 2 (ver
|
||||
[ADR 0005](0005-rediseno-storefront.md)), el disparador real resultó
|
||||
ser la complejidad de facetas de la taxonomía, no el volumen de
|
||||
productos — ver [ADR 0006](0006-motor-busqueda-facetas.md).
|
||||
|
||||
**Compatibilidad con Medusa Admin confirmada:** ninguna de las tres
|
||||
opciones evaluadas, incluida la elegida, toca
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
# ADR 0006 — Motor de búsqueda y filtros facetados (Fase 5, Etapa 3)
|
||||
|
||||
**Estado:** propuesta — pendiente de aprobación antes de Etapa 2 (implementación)
|
||||
**Fecha:** 2026-08-25
|
||||
|
||||
## Contexto
|
||||
|
||||
[ADR 0004](0004-evaluacion-stack-multi-categoria.md) ya identificó un
|
||||
motor de búsqueda dedicado (Meilisearch, autohospedado) como "el
|
||||
siguiente paso natural" para ARI Shopping, pero lo dejó fuera del
|
||||
alcance de Etapa 2 explícitamente hasta que el catálogo se acercara al
|
||||
techo real de ~100 productos activos que impone
|
||||
`fetchProducts(filters, limit = 100)` en `medusa.ts`.
|
||||
|
||||
**Ese techo todavía no llegó** — el catálogo real hoy son 26 productos
|
||||
(17 legacy + 9 demo, ver [ADR 0005](0005-rediseno-storefront.md)
|
||||
sección 5), lejos de 100. El disparador de esta propuesta no es el
|
||||
techo de volumen sino la **complejidad de facetas**: la Etapa 2 ya
|
||||
construyó una taxonomía real de 6 categorías raíz / 21 nodos
|
||||
(navegación, mega-menú, breadcrumb — PRs #36-42) y un filtrado con
|
||||
conteo por valor (`categoryCounts`/`brandCounts`/`ageCounts` en
|
||||
`catalog-client.tsx`, PR #40) que hoy se calcula **100% client-side**
|
||||
sobre los productos ya traídos — el mismo patrón de "deuda de
|
||||
alcance" que el hallazgo #4 de ADR 0004 ya señaló, ahora con más
|
||||
dimensiones de filtro que multiplicar. Sigue siendo tratable a 26
|
||||
productos; deja de serlo mucho antes de llegar a 100 si el catálogo
|
||||
crece a la vez en número de productos y en dimensiones filtrables.
|
||||
|
||||
Restricción no negociable, igual que en toda la Fase 5: el flujo
|
||||
**Admin → evento `product.updated` → `revalidate-storefront.ts` →
|
||||
`revalidateTag("medusa-products")`**, que hoy mantiene el storefront
|
||||
sincronizado sin intervención manual, no se toca. Cualquier índice de
|
||||
búsqueda debe sincronizarse por el mismo mecanismo de eventos, nunca
|
||||
por un cron ni un paso manual.
|
||||
|
||||
## 1. Estado real de la integración Medusa v2 + Meilisearch
|
||||
|
||||
Verificado contra la documentación oficial vigente el 2026-08-25, no
|
||||
contra memoria de entrenamiento:
|
||||
|
||||
- **Medusa no tiene un módulo de búsqueda propio integrado con
|
||||
Meilisearch hoy.** La guía oficial
|
||||
(`docs.medusajs.com/resources/integrations/guides/meilisearch`) es
|
||||
un tutorial de módulo custom desde cero (`npm install meilisearch` +
|
||||
workflows/subscribers propios) — no un paquete instalable.
|
||||
- **Medusa sí introdujo un "Search Module" propio en la versión
|
||||
2.19.0.** El paquete comunitario `@rokmohar/medusa-plugin-meilisearch`
|
||||
(el más usado y mantenido para Meilisearch+Medusa) se conecta a ese
|
||||
Search Module desde su versión `^2.0.0`, pero esa versión del plugin
|
||||
**requiere Medusa `^2.19.0`**.
|
||||
- **Nuestro backend corre Medusa `2.18.0`** (`commerce-backend/package.json`)
|
||||
— justo por debajo de ese corte. La tabla de compatibilidad del
|
||||
plugin (`github.com/rokmohar/medusa-plugin-meilisearch`) resuelve
|
||||
esto: la línea `^1.4.1` soporta `Medusa >= 2.15 < 2.19`, servidor
|
||||
Meilisearch `>= 1.5`, Node `>= 22`. El propio repo lo explica: *"The
|
||||
2.19 release removed the search interface the v1 line was written
|
||||
against, so the two lines do not overlap: v1 stops at Medusa 2.18,
|
||||
v2 starts at 2.19."*
|
||||
- Node de `commerce-backend` ya es `22.18.0` (Dockerfile) — cumple el
|
||||
mínimo del plugin sin cambios.
|
||||
|
||||
**Implicación concreta:** para adoptar Meilisearch sin subir de
|
||||
versión Medusa (fuera de alcance de este ADR — sería su propia
|
||||
migración), la pieza a instalar es `@rokmohar/medusa-plugin-meilisearch@^1.4.1`,
|
||||
no la última versión del plugin. Esto es deuda técnica documentada,
|
||||
no un obstáculo: si en el futuro se sube Medusa a `2.19+`, migrar a la
|
||||
línea `^2.0.0` del plugin (y al Search Module nativo) es el camino
|
||||
recto.
|
||||
|
||||
## 2. Impacto de recursos
|
||||
|
||||
Estado real del host ZimaOS al momento de esta auditoría (`free -h`,
|
||||
`docker stats`): 15 GiB RAM totales, **swap al 100% (5.2/5.2 GiB
|
||||
usados)**, solo 351 MiB realmente libres (6.5 GiB "available" vía
|
||||
cache reclamable), 4 vCPU — la misma clase de contención que ya forzó
|
||||
el fix de `cpu_shares` de Gitea. Cualquier servicio nuevo en este host
|
||||
es una decisión de capacidad real, no teórica.
|
||||
|
||||
- **Meilisearch no publica un mínimo de RAM oficial.** Su propio FAQ
|
||||
(`meilisearch.com/docs/resources/help/faq`) solo dice que "más RAM
|
||||
implica búsquedas más rápidas" y recomienda ~10x el tamaño del
|
||||
dataset crudo en disco — sin piso duro. La cifra de "~1 GiB
|
||||
cómodo" que circula en blogs de la comunidad es una estimación, no
|
||||
una garantía del proveedor — se cita aquí como referencia, no como
|
||||
hecho verificado.
|
||||
- Meilisearch usa almacenamiento memory-mapped: el índice puede vivir
|
||||
parcialmente en disco cuando no entra en RAM, degradando latencia en
|
||||
vez de fallar. A la escala actual (26 productos, dataset de KB) el
|
||||
uso real de RAM del índice en sí es insignificante — el costo es el
|
||||
proceso base de Meilisearch, no el dataset.
|
||||
- **Comparación con Typesense** (pedida explícitamente, mantenida
|
||||
breve porque la diferencia relevante para este caso es clara y no
|
||||
requiere más profundidad): Typesense mantiene el índice completo en
|
||||
RAM (~30% más RAM que Meilisearch para el mismo dataset, según
|
||||
benchmarks públicos) y, a diferencia de Meilisearch, **no tiene
|
||||
ningún plugin ni integración con Medusa** — habría que escribir la
|
||||
sincronización completa desde cero igual que con la guía "custom"
|
||||
de Meilisearch, pero sin ninguna base comunitaria mantenida
|
||||
compatible con nuestra versión de Medusa. Con Meilisearch ya
|
||||
teniendo un plugin activo para `Medusa 2.18` y menor huella de RAM,
|
||||
no hay motivo para evaluar Typesense más a fondo en este caso.
|
||||
- **Recomendación:** desplegar Meilisearch en k3d con `requests`/`limits`
|
||||
explícitos y conservadores desde el día uno (ej. `request: 256Mi/0.1
|
||||
CPU`, `limit: 768Mi/0.5 CPU` — a ajustar con datos reales de uso, no
|
||||
supuestos), y tratarlo como el mismo tipo de riesgo operativo que ya
|
||||
se vivió con Gitea: monitorear consumo real post-despliegue antes de
|
||||
darlo por estable. Dado el swap ya saturado, este es el punto de
|
||||
mayor incertidumbre de toda la propuesta — más que la elección de
|
||||
motor de búsqueda en sí.
|
||||
|
||||
## 3. Modelo de indexación
|
||||
|
||||
Basado en la taxonomía real ya viva en Medusa (Etapa 2) y en
|
||||
`Product`/`Category` de `lib/headless/types.ts`:
|
||||
|
||||
| Campo | Buscable (texto, tolerante a typos) | Filtrable / faceta | Ordenable |
|
||||
|---|---|---|---|
|
||||
| `name` | Sí | No | No |
|
||||
| `shortDescription` / `description` | Sí | No | No |
|
||||
| `category` (nombre de la hoja real) | Sí | Sí (con conteo) | No |
|
||||
| `collection` | Sí | Sí (con conteo) | No |
|
||||
| `brand` | Sí | Sí (con conteo) | No |
|
||||
| `ageRange` | No | Sí (con conteo) | No |
|
||||
| `tags` | Sí | No | No |
|
||||
| `price` | No | Sí (rango) | Sí |
|
||||
| `compareAtPrice` | No | No | No (solo se usa para el badge, no como filtro) |
|
||||
| `isNew` / `featured` | No | No | Sí (orden "Novedades"/"Destacados", igual que hoy) |
|
||||
| `available` | No | Sí (booleano) | No |
|
||||
| `slug`, `thumbnail`, `id` | No | No | No (solo se devuelven, no se indexan como facetas) |
|
||||
|
||||
La jerarquía de categorías (raíz/hija) no se indexa como un campo
|
||||
aparte: el índice guarda el nombre de la hoja real tal como hoy lo
|
||||
hace `product.category`, y el breadcrumb/mega-menú siguen resolviendo
|
||||
la jerarquía contra `/store/product-categories` (sin cambios, ya
|
||||
funciona desde la sub-fase 2). El motor de búsqueda resuelve
|
||||
**búsqueda de texto + conteo de facetas**, no reemplaza la fuente de
|
||||
la jerarquía.
|
||||
|
||||
## 4. Diseño de sincronización
|
||||
|
||||
Mismo patrón que `revalidate-storefront.ts`, no uno nuevo: un
|
||||
subscriber adicional en `commerce-backend/src/subscribers/` (ej.
|
||||
`sync-search-index.ts`) que reacciona a eventos y le pide al plugin
|
||||
que reindexe el documento afectado — nunca Medusa escribiendo
|
||||
directamente en Meilisearch fuera de este canal.
|
||||
|
||||
Eventos a escuchar, confirmados en la documentación oficial de
|
||||
eventos de Medusa (`docs.medusajs.com/resources/events-reference`):
|
||||
|
||||
- `product.created` / `product.updated` / `product.deleted`
|
||||
- `product-variant.created` / `product-variant.updated` / `product-variant.deleted`
|
||||
(mismo set que ya usa `revalidate-storefront.ts`, hoy validado en
|
||||
producción)
|
||||
- **Nuevo respecto al subscriber actual:** `product-category.created`
|
||||
/ `product-category.updated` / `product-category.deleted` — un
|
||||
cambio de nombre o jerarquía de categoría debe refrescar las
|
||||
facetas del índice, y hoy `revalidate-storefront.ts` no escucha
|
||||
estos eventos porque el storefront los resuelve en cada request
|
||||
contra `/store/product-categories` directamente, sin necesidad de
|
||||
revalidación. El índice de búsqueda sí necesita enterarse.
|
||||
|
||||
**Punto a verificar en vivo en Etapa 2, no asumido aquí:** existe al
|
||||
menos un reporte público de un evento `product-collection.updated`
|
||||
que no se disparaba de forma confiable en ciertas versiones de Medusa
|
||||
(`github.com/medusajs/medusa/issues/11197`). Antes de dar por cerrado
|
||||
el diseño de sincronización hay que repetir exactamente la validación
|
||||
que ya se hizo para `revalidate-storefront.ts` en la auditoría de
|
||||
Etapa 1 de Fase 5: crear/editar una categoría real vía Admin y
|
||||
confirmar en vivo que el evento llega al subscriber, no confiar en que
|
||||
la documentación coincide con el comportamiento real de `2.18.0`
|
||||
(el mismo tipo de brecha que ya se encontró una vez con la relación
|
||||
`+categories` de `/store/products`).
|
||||
|
||||
## 5. Diseño de UI — filtros facetados y búsqueda
|
||||
|
||||
**Filtros facetados (sidebar de `/catalog`):** mismo layout que ya
|
||||
construyó la sub-fase 3 (`catalog-client.tsx`, PR #40) — sidebar con
|
||||
"Categoría"/"Marca"/"Edad" como pills con conteo entre paréntesis y
|
||||
slider de precio máximo. Lo único que cambia es **de dónde sale el
|
||||
conteo**: hoy `categoryCounts`/`brandCounts`/`ageCounts` se calculan
|
||||
recorriendo el arreglo de productos ya traído en el cliente; con el
|
||||
motor de búsqueda, esos conteos vienen del propio Meilisearch
|
||||
(`facetDistribution`) en la misma respuesta que trae los productos
|
||||
filtrados, sin recorrer nada en el navegador. El breadcrumb no cambia
|
||||
— sigue resolviéndose contra la jerarquía real de categorías, no
|
||||
contra el índice de búsqueda.
|
||||
|
||||
```
|
||||
Inicio > Mochilas y morrales > Marvel breadcrumb real (sin cambios)
|
||||
┌────────────┬──────────────────────────────────────┐
|
||||
│ Categoría │ [buscador con autocompletado] │ ← nuevo
|
||||
│ ☐ Marvel(6)│ [card][card][card] │
|
||||
│ ☐ Frozen(4)│ [card][card][card] │
|
||||
│ Marca │ │
|
||||
│ ☐ Disney(2)│ conteos ahora vienen de Meilisearch, │
|
||||
│ Precio │ no de recorrer el arreglo en cliente │
|
||||
│ [====○===] │ │
|
||||
└────────────┴──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Búsqueda con autocompletado (barra del navbar):** hoy
|
||||
`PredictiveSearch` (`components/search/predictive-search.tsx`) filtra
|
||||
por substring sobre los productos ya cargados en memoria del cliente —
|
||||
funciona a 26 productos, no escala y no tolera errores de tipeo (ADR
|
||||
0004, hallazgo #5). Se reemplaza la fuente de datos del mismo
|
||||
componente visual por `searchProducts(query)` del `CatalogProvider`
|
||||
(interfaz que **ya existe** en `lib/headless/types.ts`, sin necesidad
|
||||
de un tipo nuevo) apuntando a Meilisearch en vez de al `?q=` de
|
||||
Medusa: tolerancia a typos y ranking por relevancia nativos del motor,
|
||||
mismo contrato de datos que ya consume el dropdown de resultados —
|
||||
cero cambio de componente React, solo de qué hay detrás de
|
||||
`CatalogProvider.searchProducts`.
|
||||
|
||||
## Opciones consideradas
|
||||
|
||||
| Opción | A favor | En contra |
|
||||
|---|---|---|
|
||||
| **Meilisearch autohospedado** (`@rokmohar/medusa-plugin-meilisearch@^1.4.1`, sincronizado por eventos) | Plugin comunitario activo y compatible con nuestra versión exacta de Medusa (2.18); menor huella de RAM que la alternativa evaluada; almacenamiento memory-mapped se degrada en vez de fallar bajo presión de memoria; mismo patrón de subscriber que `revalidate-storefront.ts`, cero riesgo para ese flujo | Ningún paquete "oficial" de Medusa (el plugin es comunitario, no del core); atado a la línea `v1` del plugin mientras no se suba Medusa a 2.19+; un servicio más para operar en un host ya con swap saturado |
|
||||
| **Typesense autohospedado** | Autocompletado ~2x más rápido según benchmarks públicos | Cero integración con Medusa (habría que escribir la sincronización completa desde cero, igual esfuerzo que la guía "custom" de Meilisearch pero sin base comunitaria); ~30% más RAM que Meilisearch para el mismo dataset, el peor trade-off posible dado el estado actual del host |
|
||||
| **Módulo custom desde cero** (seguir la guía oficial de Medusa, sin el plugin de rokmohar) | Sin dependencia de un paquete de terceros; control total | Reimplementa lo que el plugin comunitario ya resuelve (declaración de índices, reindexado atómico, tracking de escrituras); más superficie propia para mantener sin ganancia clara sobre usar el plugin ya compatible con 2.18 |
|
||||
| **No adoptar motor de búsqueda todavía** (seguir con filtrado 100% client-side) | Cero riesgo, cero componente nuevo | No resuelve la deuda de facetas ya identificada en ADR 0004 y agravada por la sub-fase 3; el techo de complejidad de filtros llega antes que el de 100 productos si el catálogo sigue creciendo en categorías |
|
||||
|
||||
## Decisión
|
||||
|
||||
**Meilisearch autohospedado**, vía `@rokmohar/medusa-plugin-meilisearch@^1.4.1`
|
||||
(no la última versión del plugin — esa exige Medusa 2.19+, fuera de
|
||||
alcance), sincronizado por el mismo patrón de subscriber por eventos
|
||||
que ya usa `revalidate-storefront.ts`, con `product-category.*` sumado
|
||||
al set de eventos existente. Se despliega en k3d con límites de
|
||||
recursos conservadores desde el primer día y se trata como
|
||||
experimento a monitorear, no como capacidad garantizada, dado el
|
||||
estado actual de memoria del host.
|
||||
|
||||
**No se sube la versión de Medusa como parte de esta decisión** —
|
||||
queda como deuda técnica documentada para cuando se justifique
|
||||
migrar a la línea `v2` del plugin y al Search Module nativo de
|
||||
Medusa.
|
||||
|
||||
## Consecuencias
|
||||
|
||||
- Se gana: conteo de facetas real (no client-side), búsqueda tolerante
|
||||
a errores de tipeo con ranking por relevancia, y una arquitectura
|
||||
que ya no depende del techo de 100 productos de `fetchProducts`.
|
||||
- Se acepta como deuda técnica: quedar atado a la línea `v1` del
|
||||
plugin de Meilisearch mientras Medusa siga en `2.18.x`; migrar a
|
||||
Medusa `2.19+` es un proyecto aparte, no parte de esta Etapa 3.
|
||||
- Riesgo operativo real y no resuelto todavía por este ADR: el host ya
|
||||
corre con el swap saturado — el despliegue de Etapa 2 debe empezar
|
||||
con límites de recursos conservadores y validación en vivo de
|
||||
consumo real antes de confiar en el servicio, siguiendo la misma
|
||||
lección que dejó el incidente de recursos de Gitea.
|
||||
- Revertir esta decisión implica desinstalar el plugin y el
|
||||
subscriber nuevo, y volver el buscador/filtrado a 100% client-side
|
||||
— no hay migración de datos que deshacer, porque Meilisearch nunca
|
||||
es la fuente de verdad, solo una copia de solo lectura indexada
|
||||
desde Medusa.
|
||||
Reference in new issue
Block a user