# ApproSearch — guía de la API de configuración para agentes

Esta guía explica a un agente de IA (o a un desarrollador) cómo revisar y modificar la configuración del buscador ApproSearch de una tienda: pesos de relevancia, sinónimos, reglas, filtros y widget. Complementa el OpenAPI de `https://api.approsearch.com/api/v1/openapi.json`, que describe los cuerpos exactos de cada petición.

Las mismas operaciones están disponibles como herramientas MCP en `POST https://api.approsearch.com/mcp` con la misma API key. Las herramientas llevan esta guía como recurso `approsearch://docs/agent`.

## 1. Autenticación

- Usa una API key de configuración creada en el panel (**API Keys** → preset **Agente IA**). Tiene scope `config:write` (lectura y escritura) o `config:read` (solo lectura).
- Envíala en la cabecera `X-API-Key: <key>` o como `Authorization: Bearer <key>`. La query string no se admite.
- La key suele estar ligada a **una tienda**. En ese caso, cualquier `storeId` distinto devuelve `403`.
- La key del widget o del módulo (la que aparece en el HTML de la tienda) **no** sirve para esta API.

Primer paso, siempre:

```
GET https://api.approsearch.com/api/v1/me
```

Devuelve la cuenta, la tienda (o tiendas) y los scopes de la key. Toma de ahí el `storeId` para el resto de llamadas; no lo pidas al usuario si ya está.

## 2. Flujo de trabajo obligatorio

1. Lee la configuración actual (`GET /stores/{id}/relevance`, `GET .../synonyms`, `GET .../rules`).
2. Reproduce el problema en el simulador: `POST /stores/{id}/relevance/test` con la consulta (`q`) y **sin** pesos, para ver el ranking y el desglose actuales.
3. Prueba la propuesta en el simulador con los `boosts` / `searchSettings` provisionales. El simulador **no guarda nada**.
4. Compara ambos resultados y comprueba que la consulta problemática mejora sin hundir otras consultas típicas de la tienda (prueba al menos dos o tres más).
5. Aplica el cambio (`PUT /stores/{id}/relevance`, `POST .../synonyms`, `POST .../rules`).
6. Vuelve a ejecutar el simulador para confirmar el resultado final. Explica al usuario qué cambiaste y por qué.

Nunca apliques un cambio de relevancia sin haberlo probado antes en el simulador.

## 3. Relevancia: pesos por campo (`boosts`)

`GET /stores/{id}/relevance` devuelve los valores **efectivos** (defaults fusionados con lo guardado). Un peso mayor hace que las coincidencias en ese campo pesen más en el ranking. `0` apaga el campo.

Valores por defecto y significado:

| Campo | Defecto | Qué es |
|---|---|---|
| `name` | 6 | Nombre del producto. La señal principal. |
| `name.autocomplete` | 3 | Coincidencia por prefijo (sugerencias, palabras a medias, tokens tipo `43x55`). |
| `name.partial` | 0 | Coincidencia junto/separado («microconverter» ↔ «micro converter»). Opt-in: requiere reindexar; sin reindex no casa nada. |
| `reference` | 10 | Referencia (SKU) del producto. |
| `ean13`, `upc`, `mpn` | 10 | Códigos de barras y de fabricante. |
| `combinationReferences`, `combinationEans`, `combinationMpns` | 12 | Los mismos códigos en las combinaciones (variantes). Van por encima del producto para que la variante gane. |
| `categories.name` | 3 | Nombre de las categorías del producto. |
| `tags` | 4 | Etiquetas. |
| `attributes.value` | 2 | Valores de atributos (talla, color). |
| `features.value` | 2 | Valores de características (material, medida). |
| `short_description` | 1.5 | Descripción corta. |
| `description` | 1 | Descripción larga. Subirla trae mucho ruido. |
| `manufacturer` | 3 | Marca o fabricante. |
| `supplier` | 3 | Proveedor. |
| `ai_description_manual` | 6 | Descripción enriquecida por IA y revisada a mano. |
| `ai_description` | 5 | Descripción generada por IA. |

Rangos: el tope **recomendado** de un peso es 20. La API admite hasta 200 (10 veces el tope) para catálogos con necesidades concretas, pero por encima de 20 un campo empieza a dominar el ranking y por encima de 100 aplasta al resto. Sube de tramo solo si el simulador demuestra que no hay otra forma.

Aviso importante sobre `PUT /stores/{id}/relevance`:

- `boosts` **reemplaza el mapa completo**. Envía todos los campos que quieras conservar; los que falten vuelven a su valor por defecto. Lee primero con `GET` y modifica sobre esa copia.
- `searchSettings` se **fusiona** clave a clave: puedes enviar solo la que cambia.
- La herramienta MCP `update_relevance` fusiona también los `boosts`, para poder cambiar un solo campo.

## 4. Relevancia: ajustes del motor (`searchSettings`)

| Ajuste | Defecto | Qué hace |
|---|---|---|
| `fuzziness` | `AUTO` | Tolerancia a erratas: `0` ninguna, `1` una letra, `2` dos letras, `AUTO` según la longitud de la palabra. |
| `fuzzyPrefixLength` | 2 | Letras iniciales que deben ser exactas antes de tolerar erratas. `2` estricto, `1` normal, `0` corrige incluso la primera letra (más ruido). |
| `minMatchShort` | `75%` | Porcentaje de palabras de la consulta que deben casar en consultas cortas. Con 3 palabras, 75 % significa 2 de 3. Súbelo a `100%` si el cliente quiere que cuenten todas las palabras (menos resultados). |
| `minMatchLong` | `30%` | Lo mismo para consultas largas. |
| `longQueryThreshold` | 5 | Número de palabras a partir del cual una consulta es «larga». |
| `phraseBoost` | 8 | Premio cuando el nombre (o una etiqueta) contiene la frase exacta buscada. Tope recomendado 30. |
| `exactRefBoost` | 20 | Premio cuando la consulta es exactamente una referencia, EAN, UPC o MPN. Tope recomendado 50. |
| `crossFieldsBoost` | 3 | Peso de la coincidencia repartida entre campos (una palabra en el nombre y otra en la descripción). Tope recomendado 20. |
| `stockBoost` | 1.5 | Factor que suma un producto en stock. Tope recomendado 5. `0` desactiva. |
| `salesBoost` | 0.5 | Factor logarítmico por unidades vendidas (`sales_count`). Solo lo alimenta el conector de PrestaShop. |
| `ratingBoost` | 0.3 | Factor logarítmico por valoración. |
| `discountBoost` | 1.1 | Factor que suma un producto con descuento activo. |
| `hideOutOfStock` | `false` | Excluye de los resultados los productos marcados como agotados. |
| `exactReferenceSearch` | `true` | `true`: la referencia debe casar completa. `false`: también por subcadena («6760» encuentra «MO6760»). |
| `referenceCaseSensitive` | `false` | `true` solo si el catálogo tiene códigos que se diferencian únicamente por mayúsculas. |
| `scoreCutoff` | 0 | Corte por relevancia: descarta los productos cuyo score baje del N % del primero. `0` desactivado. Un valor alto puede ocultar productos legítimos con nombres largos. |
| `referenceMode` | `off` | Modo referencia automático cuando la consulta es un código: `priority` ordena las referencias por tramos (exacta, empieza por, contiene, con errata) y deja el texto detrás; `strict` devuelve solo referencias. |

Cómo puntúa el motor, en una frase: `score = relevancia_de_texto × (1 + factores)`, donde los factores son stock, ventas, valoración y descuento. Un producto agotado sin ventas tiene factor 1 (neutro), no 0.

Casos habituales y qué ajustar:

- «Salen productos que no son lo buscado»: la búsqueda es «lo más parecido», no un filtro. Sube `minMatchShort` a `100%` o activa un `scoreCutoff` moderado (20 a 30). Prueba varias consultas: el corte es agresivo con nombres largos.
- «Una referencia no aparece la primera»: comprueba `exactReferenceSearch` y `referenceMode`; sube `exactRefBoost` antes que los pesos de código.
- «Una palabra rara pesa demasiado»: es el IDF de Elasticsearch. Baja el peso del campo donde aparece o usa un sinónimo hacia el término común.
- «Erratas del cliente no se corrigen»: baja `fuzzyPrefixLength` a 1 (o 0) antes de tocar `fuzziness`.
- «Productos agotados arriba»: sube `stockBoost` o activa `hideOutOfStock`.

## 5. Sinónimos

- Un sinónimo pertenece a **un idioma** (`language`, ISO de 2 letras, por defecto `es`).
- `BIDIRECTIONAL`: todos los términos de `terms` son equivalentes entre sí («zapatillas, tenis, sneakers»).
- `UNIDIRECTIONAL`: `input` es lo que escribe el cliente y `terms` a qué se expande («nevera» → «frigorífico»). Necesita `input`.
- Los sinónimos casan **formas exactas** tras pasar a minúsculas y quitar acentos. No cubren plurales ni flexiones: lista las variantes que quieras («camara, cámara, camaras, cámaras, camera»). Tampoco admiten comodines.
- Se aplican **al guardar**, sin reindexar. El widget puede tardar hasta 3 minutos por la caché de búsqueda; el simulador los ve al instante.
- Antes de crear uno, lista los existentes del idioma para no duplicar. La importación CSV (`POST .../synonyms/import`, una línea por grupo, términos separados por coma) omite duplicados e informa de creados y omitidos.
- Si tras un reindexado los sinónimos dejan de aplicarse, vuelve a guardar uno cualquiera: eso reconstruye el filtro del índice.

## 6. Reglas

Una regla se dispara cuando la consulta cumple el `trigger` (`query` + `match`: `exact`, `contains`, `starts_with`, `regex`) y aplica `actions` según su `type`:

| Tipo | Acción | Uso |
|---|---|---|
| `BOOST` | `actions.boost[]` | Sube productos que cumplen una condición sobre un campo. |
| `PIN` | `actions.pin[]` (IDs de producto) | Fija productos en las primeras posiciones. |
| `HIDE` | `actions.hide[]` (IDs) | Oculta productos para esa consulta. |
| `REDIRECT` | `actions.redirect` (URL) | Redirige la búsqueda a una página. |
| `BANNER` | `actions.banner` (`imageUrl`, `link`, `position`) | Muestra un banner en los resultados. |
| `FILTER` | `actions.filter` (campo → valor o valores) | Restringe los resultados a un filtro. |

Modos de una entrada de `boost`:

- `term` (por defecto): `field`, `value` (valor exacto del campo, con mayúsculas y acentos tal cual están indexados) y `weight` (0.1 a 100).
- `value`: proporcional al valor de un campo **numérico** (`field`, `factor`, `modifier` en `none`, `log1p`, `ln1p`, `sqrt`; `missing` para productos sin valor). La API rechaza campos no numéricos.
- `range`: `field`, `min` y/o `max`, `weight`.

Otros campos: `priority` (0 a 1000, mayor gana), `active`, `startDate` y `endDate` (ISO 8601) para campañas. Cada regla pertenece a un idioma. Un peso alto multiplica el score, así que empieza en valores bajos (2 a 5) y comprueba en el simulador con `applyRules: true`.

## 7. Filtros y widget

- Los filtros configurados sustituyen al conjunto automático de facetas: en cuanto hay uno configurado, el widget muestra solo los configurados. Antes de crear el primero, avisa al usuario de ese efecto o usa `POST .../filters/sync` para materializar todos los automáticos y luego ocultar los que sobren.
- **Valores unificados** (`config.valueGroups`): un filtro puede presentar varios valores de su faceta como una sola opción (tonos de un color, duplicados por mayúsculas o grafía). Cada grupo es `{ "label": "Rojo", "values": ["12", "45"] }`; `label` es opcional (sin él, la opción hereda la etiqueta del primer valor del grupo en el idioma del visitante) y `key` la asigna la API. Un valor solo puede estar en un grupo. Para saber qué ids agrupar, consulta `GET .../filters/values?field=<campo>&lang=<idioma>`: devuelve `id`, `label` y `count` de cada valor presente en el índice, y ese `id` es exactamente lo que va en `values`. En `PUT`, `config` se fusiona con la guardada: manda solo `{ "config": { "valueGroups": [...] } }`; `[]` elimina todos los grupos. El precio (rango) no admite grupos.
- En el widget, `features` se fusiona con lo guardado; `colors`, `translations` y el resto se reemplazan. `customJS` y `customCSS` no se pueden modificar por API key.

## 8. Errores y límites

| Código | Significado | Qué hacer |
|---|---|---|
| `400` | Datos inválidos; `message` indica campo y motivo. | Corrige el cuerpo. No reintentes igual. |
| `401` | Key ausente, inválida, desactivada o caducada. | Pide al usuario una key válida. |
| `403` | Scope insuficiente, tienda distinta a la de la key, u operación reservada al panel (`DELETE` de tienda, `customJS`, campos de tienda distintos de `name` y `devUrl`). | Explica al usuario que ese cambio se hace desde el panel. |
| `404` | Recurso inexistente o de otra cuenta. | Vuelve a listar y comprueba el ID. |
| `429` | Límite superado: 600 peticiones por minuto en total y 120 escrituras de configuración por minuto y key. | Espera `retryAfter` segundos. |

Todas las escrituras hechas con API key quedan registradas en la auditoría de la cuenta (quién, qué, cuándo). Trabaja con cambios pequeños y explicables.

## 9. Conectar por MCP

Servidor MCP con transporte Streamable HTTP, sin estado. La autenticación es la misma API key.

Claude Code (usa un nombre de servidor por cuenta, `approsearch-<cliente>`, para poder conectar varias tiendas a la vez sin que las herramientas se confundan):

```
claude mcp add --transport http approsearch-<cliente> https://api.approsearch.com/mcp --header "Authorization: Bearer <API_KEY>"
```

Herramientas: `get_account`, `get_relevance`, `test_relevance`, `update_relevance`, `list_synonyms`, `create_synonym`, `update_synonym`, `delete_synonym`, `import_synonyms_csv`, `list_rules`, `get_rule`, `create_rule`, `update_rule`, `delete_rule`, `list_filters`. Las de escritura solo aparecen si la key tiene `config:write`. Recursos: `approsearch://docs/agent` (esta guía) y `approsearch://docs/openapi`.
