Búsqueda semántica en Nuxt con XEYE
Para añadir búsqueda semántica a una aplicación Nuxt necesitas tres archivos: la clave API en la configuración privada, una ruta de servidor que llama a XEYE y una página con la caja de búsqueda. La clave no sale nunca del servidor. Sirve igual para Nuxt 4 y para Nuxt 3.
Por Joan Martorell
¿Qué necesito antes de empezar?
- Una cuenta de XEYE con una lista pública y entrenada. Si aún no la tienes, sigue primeros pasos.
- Una clave API, creada en la página Claves API de la consola. Solo se muestra completa una vez.
- Una aplicación Nuxt con servidor: desarrollo local, Node, o un despliegue en Vercel, Netlify o Cloudflare. No sirve un sitio generado solo como estático, porque no tendría dónde guardar la clave.
En los ejemplos la lista se llama products. Cambia ese nombre por el de la tuya.
Paso 1: ¿dónde guardo la clave API?
En la parte privada de runtimeConfig, que solo existe en el servidor. Decláralo vacío en la configuración y da el valor con la variable de entorno NUXT_XEYE_API_KEY:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Private: only available on the server. Set it with NUXT_XEYE_API_KEY.
xeyeApiKey: '',
},
})
En desarrollo, ponla en el archivo .env. En producción ese archivo no se lee: define la variable de entorno en tu plataforma de despliegue.
No la pongas dentro de runtimeConfig.public ni la uses en un componente. Todo lo público viaja al navegador, y quien tenga la clave puede buscar en tus listas y gastar tu crédito.
Paso 2: ¿cómo llamo a XEYE desde el servidor?
Con una ruta de servidor. Recibe la búsqueda del navegador, la valida, llama a XEYE con la clave y devuelve solo los resultados. El archivo va en server/api/, que está en la raíz del proyecto tanto en Nuxt 4 como en Nuxt 3:
// server/api/search.post.ts
export default defineEventHandler(async (event) => {
const { xeyeApiKey } = useRuntimeConfig(event)
const body = await readBody<{ q?: unknown; limit?: unknown }>(event)
const q = typeof body?.q === 'string' ? body.q.trim() : ''
if (q.length < 2 || q.length > 200) {
throw createError({ status: 400, statusText: 'Invalid query' })
}
const limit = Math.min(Math.max(Number(body?.limit) || 5, 1), 20)
try {
const response = await $fetch<{ results: unknown[] }>(
'https://search.xeye.es/api/v1/search',
{
method: 'POST',
headers: { 'X-API-Key': xeyeApiKey },
// The list name is fixed here: the browser cannot query other lists.
body: { list_name: 'products', search_term: q, limit },
},
)
return { results: response.results }
} catch {
throw createError({ status: 502, statusText: 'Search unavailable' })
}
})
Dos detalles que conviene mantener. El nombre de la lista está fijado en el servidor, así el navegador no puede consultar otras listas. Y la longitud y el límite se validan antes de llamar, porque cada búsqueda que llega a XEYE consume crédito.
Paso 3: ¿cómo pinto el buscador?
Con una página que llama a tu ruta, no a XEYE. Este ejemplo espera 300 milisegundos desde la última tecla antes de buscar y descarta las respuestas que llegan desordenadas:
<!-- app/pages/search.vue (Nuxt 4) · pages/search.vue (Nuxt 3) -->
<script setup lang="ts">
interface Result {
item: string
score: number
params?: { url?: string; sku?: string }
}
const q = ref('')
const results = ref<Result[]>([])
let timer: ReturnType<typeof setTimeout> | undefined
let latest = 0
watch(q, (value) => {
clearTimeout(timer)
const term = value.trim()
if (term.length < 2) {
results.value = []
return
}
// Wait until the user stops typing: one search, not one per keystroke.
timer = setTimeout(async () => {
const id = ++latest
const response = await $fetch<{ results: Result[] }>('/api/search', {
method: 'POST',
body: { q: term, limit: 5 },
})
if (id === latest) results.value = response.results // drop stale answers
}, 300)
})
</script>
<template>
<input v-model="q" type="search" placeholder="Search…" />
<ul>
<li v-for="result in results" :key="result.params?.sku ?? result.item">
<a :href="result.params?.url">{{ result.item }}</a>
</li>
</ul>
</template>
Cada resultado trae en params lo que guardaste con el elemento. Si incluiste la URL, el precio o la imagen, ya tienes lo necesario para pintar una tarjeta sin consultar tu base de datos.
¿Cómo lo pruebo?
- Arranca la aplicación con
NUXT_XEYE_API_KEYdefinida. - Abre
/searchy escribe una consulta con un sinónimo o una errata. - Si no devuelve nada, prueba la misma búsqueda en el playground de la consola: te dirá si el problema está en la lista o en la integración.
¿Qué errores puedo encontrar?
| Respuesta de XEYE | Qué significa | Qué hacer |
|---|---|---|
401 | Falta la clave o no es válida. | Revisa la variable de entorno en el servidor. |
403 LIST_NOT_PUBLIC | La lista es privada. | Márcala como pública en la consola. |
404 LIST_NOT_FOUND | No hay ninguna lista con ese nombre en tu cuenta. | Revisa list_name. |
402 CREDIT_EXHAUSTED | No queda crédito. | Solicita más desde la página Crédito. |
429 RATE_LIMITED | Has superado el límite por minuto. | Respeta la cabecera Retry-After. |
En el ejemplo, cualquier fallo se convierte en un 502 para el navegador. La lista completa de errores está en la guía de integración por API.
Preguntas frecuentes
¿Funciona con Nuxt 3?
- Sí. La ruta de servidor es idéntica. Lo único que cambia es dónde vive la página:
app/pages/en Nuxt 4 ypages/en Nuxt 3. ¿Puedo usar `useFetch` en lugar de `$fetch`?
- Sí. Para una búsqueda que se dispara al escribir,
$fetchdentro de unwatches lo más directo.useFetchencaja mejor cuando la búsqueda viene de la URL y quieres que se renderice en el servidor. ¿Cómo evito gastar una búsqueda por tecla?
- Con la espera de 300 milisegundos del ejemplo. Además puedes exigir un mínimo de caracteres y guardar en memoria las respuestas de las consultas repetidas.
Pruébalo con tus propios datos
Crea una cuenta, sube una lista y haz tu primera búsqueda en unos cinco minutos. Empiezas con 5 € de crédito, sin tarjeta.
Sigue leyendo
- API de búsqueda semántica: qué es y cómo elegir unaUna API de búsqueda semántica devuelve los elementos de tu catálogo que significan lo mismo que la consulta. Qué hace, qué necesitas y cómo elegir una.
- Búsqueda semántica en Shopify con XEYEAñade a tu tienda Shopify un buscador que entiende sinónimos y erratas: exporta los productos, entrénalos en XEYE y conecta el tema con un pequeño proxy.
- Precios de XEYEXEYE cuesta 0,001 € por búsqueda por API y 0,30 € por entrenamiento, más 0,0053 € por descripción con IA. Sin cuotas y con 5 € de crédito inicial.