From 493fd4d8355c9d6aaf53dd5f7eb46d7707fa366d Mon Sep 17 00:00:00 2001 From: rafa Date: Sat, 27 Jun 2026 11:30:34 -0400 Subject: [PATCH] =?UTF-8?q?docs(#8):=20plan=20de=20implementaci=C3=B3n=20d?= =?UTF-8?q?el=20buscador=20Typesense=20(fase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan ejecutable (para Sonnet) de la fase 2: arquitectura, schema, indexador, UI InstantSearch con faceta de autor, seguridad de claves y verificación. La decisión de dónde corre Typesense en producción queda pendiente; todo lo demás se puede construir y validar en local. Co-Authored-By: Claude Opus 4.8 --- docs/plan-buscador-typesense.md | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/plan-buscador-typesense.md diff --git a/docs/plan-buscador-typesense.md b/docs/plan-buscador-typesense.md new file mode 100644 index 0000000..e8e96f1 --- /dev/null +++ b/docs/plan-buscador-typesense.md @@ -0,0 +1,88 @@ +# Plan de implementación — Buscador con Typesense (feadulta #8, fase 2) + +> **Estado:** plan para ejecutar (pensado para que lo ejecute un agente Sonnet). +> **Fase 1 (MVP nativo) ya está en producción** — ver `feadulta-buscador-8` / issue #8: +> barra de búsqueda visible en móvil (`mu-plugins/fea-search.php`) + resultados en +> rejilla (`scripts/set_search_template.php`). Esta fase 2 sustituye el **motor** por +> Typesense manteniendo/мejorando esa UI, y añade **búsqueda por autor** (faceta). +> +> **DECISIÓN PENDIENTE (Rafa) — dónde corre Typesense en producción.** Todo lo demás +> se puede construir y validar en **local** sin esa decisión (Typesense en Docker local). +> Opciones de despliegue público (decidir luego): (a) PC + Cloudflare Tunnel +> `search.feadulta.com` (coste 0, atado al PC encendido, fallback al buscador nativo); +> (b) VPS pequeño dedicado ~4-6€/mes (fiable); (c) Typesense Cloud (gestionado). +> El hosting actual (cPanel compartido, sin Docker/systemd/root) **no sirve** para el motor. + +## Decisiones ya tomadas +- **Integración:** Custom InstantSearch (indexador propio + UI InstantSearch.js), NO plugin. +- **Motor:** Typesense self-host en Docker. +- **Fallback:** si Typesense no responde, mantener el buscador nativo (`/?s=`) ya desplegado. + +## Arquitectura +``` +Navegador del usuario + └── InstantSearch.js (typesense-instantsearch-adapter) ── HTTPS ──► Typesense + (página /buscar en WP, search-only API key) (Docker) + ▲ +WP (wp-nuevo) ── indexador (wp-cli/PHP) admin API key ──────────────────────┘ + └── hook publish_post / cron: reindexa incremental +``` + +## Pasos (ejecutables por Sonnet) + +### A. Typesense local de desarrollo (no depende de la decisión de infra) +1. Añadir servicio `typesense` a `docker-compose.yml` (imagen `typesense/typesense:27.x`), + volumen de datos, `--api-key` admin de desarrollo, puerto 8108 **solo en localhost**. +2. Levantar y verificar salud: `GET http://localhost:8108/health` → `{"ok":true}`. + +### B. Schema de la colección `feadulta_posts` +Campos: `id`(post_id), `title`, `content` (texto plano, sin HTML), `excerpt`, +`author_id`(int32, facet), `author_name`(string, facet), `date`(int64, sort), +`url`, `lang`(string, facet: es/en/fr/it/pt), `categories`(string[], facet), +`thumbnail`(opcional). `default_sorting_field`: `date`. + +### C. Indexador (`scripts/typesense_index.php`, wp-cli `wp eval-file`) +1. Recorre posts `publish` (`post`), excluye los template/feedback CPT. +2. Por cada post construye el documento (content = `wp_strip_all_tags`, author_name = + `display_name` del `post_author`, lang vía Polylang `pll_get_post_language`, + categories por slugs). Sube en **lotes** (import JSONL, action=upsert). +3. Idempotente, reanudable, con `--since` para incremental. ~24.778 posts. +4. Reindexado en caliente: hook `publish_post`/`save_post` → upsert de ese documento; + `before_delete_post` → delete. (Encolar para no penalizar el guardado.) + +### D. UI InstantSearch (sustituye/mejora la página de resultados) +1. Página `/buscar` (o el propio template `search`) con InstantSearch.js + + `typesense-instantsearch-adapter`, apuntando a la URL pública de Typesense y la + **search-only** key (nunca la admin) — ambas configurables (constantes en un + mu-plugin `fea-search-typesense.php`, vacías hasta decidir la infra → fallback nativo). +2. Widgets: searchbox, hits (tarjetas con la estética actual `fea-archive-card`), + **refinement por autor** (faceta `author_name`), por idioma y por categoría, + paginación. Resaltado de coincidencias. +3. Multiidioma: filtrar `lang` = idioma actual por defecto; textos de UI por idioma. +4. Mantener la barra móvil de fase 1 como entrada; en desktop, integrar con el buscador + del menú. + +### E. Seguridad de claves +- **Admin API key**: solo en el servidor (indexador), nunca en el front. +- **Search-only key**: restringida a la colección, embebida en el front (es segura por diseño). +- CORS de Typesense limitado al dominio del sitio. + +### F. Exposición pública (BLOQUEADO por la decisión de infra) +- Opción PC+Tunnel: `cloudflared` → `search.feadulta.com` → `localhost:8108`. +- Opción VPS: Typesense en el VPS + Caddy/HTTPS en `search.feadulta.com`. +- Opción Cloud: usar el endpoint y keys que da Typesense Cloud. +- En los tres casos, la UI solo necesita la **URL pública + search-only key**. + +## Verificación +- Local: `/health` ok; `typesense_index.php` indexa N docs; la página `/buscar` devuelve + resultados relevantes y la faceta de autor filtra (probar «González Amaro», «Pagola»). +- Comparar relevancia vs buscador nativo en consultas reales del feedback + («reflexiones», búsqueda por autor). +- Prod: tras exponer el motor, repetir desde un navegador (Cloudflare bloquea headless). + +## Riesgos / notas +- **Disponibilidad** si corre en el PC: caídas → debe degradar al buscador nativo, no a error. +- **RAM**: Typesense mantiene el índice en memoria; 24.778 posts de texto entran de sobra + en <1 GB, pero dimensionar el host en consecuencia. +- **Reindexado masivo inicial** puede tardar; hacerlo por lotes y fuera de hora punta. +- No exponer nunca la admin key; CORS restringido.