Búsqueda semántica en Shopify con XEYE

Para añadir búsqueda semántica a una tienda Shopify hacen falta cuatro pasos: exportar los productos a JSON, importarlos y entrenarlos en XEYE, desplegar un pequeño proxy que guarda tu clave API y añadir una sección al tema que lo consulta. El proxy es necesario porque la clave es secreta y el código de un tema es público.

Por Joan Martorell

¿Qué necesito antes de empezar?

  • Una cuenta de XEYE y una clave API, creada en la página Claves API de la consola.
  • Acceso para editar el código del tema de tu tienda (Online Store 2.0).
  • Una cuenta gratuita de Cloudflare para el proxy, y Node.js en tu ordenador.

El código de esta página sigue la documentación oficial de Shopify y de Cloudflare. El proxy está probado contra la API de XEYE; la sección del tema no la hemos probado en una tienda Shopify real, así que pruébala primero en una copia de tu tema.

Paso 1: ¿cómo exporto los productos?

XEYE importa un JSON con una lista de objetos. Cada producto necesita un text, y opcionalmente una description y unos params con lo que quieras recuperar en cada resultado. Este script lee los productos publicados de tu tienda y escribe ese formato:

export-shopify.mjs
// node export-shopify.mjs https://your-shop.com > products.json   (Node 18+)
const shop = process.argv[2]
const out = []

for (let page = 1; ; page++) {
  const response = await fetch(`${shop}/products.json?limit=250&page=${page}`)
  const { products } = await response.json()
  if (!products.length) break
  for (const product of products) {
    out.push({
      text: product.title,
      description: (product.body_html || '')
        .replace(/<[^>]+>/g, ' ')
        .replace(/\s+/g, ' ')
        .trim(),
      params: {
        id: product.id,
        handle: product.handle,
        url: `/products/${product.handle}`,
        sku: product.variants[0]?.sku,
        price: product.variants[0]?.price,
        image: product.images[0]?.src,
      },
    })
  }
}

console.log(JSON.stringify(out, null, 2))

Usa la dirección /products.json de la tienda, que devuelve los productos publicados en páginas de 250. Funciona en la práctica, pero Shopify no la documenta y no responde en tiendas protegidas con contraseña. La alternativa oficial es exportar desde el panel, en Productos y luego Exportar, y convertir el CSV al mismo formato.

Paso 2: ¿cómo los cargo en XEYE?

  1. En la consola, crea una lista llamada products y márcala como pública.
  2. Ábrela y usa «Importar desde archivo» con el products.json del paso anterior.
  3. En la pestaña Entrenamientos, elige un modelo y lanza el entrenamiento.
  4. Cuando termine, prueba unas búsquedas en el playground. Es gratis.

Paso 3: ¿cómo protejo la clave API?

Con un proxy: un pequeño programa en un servidor que recibe la búsqueda del navegador, añade tu clave y llama a XEYE. Un Cloudflare Worker es suficiente. Crea un proyecto con estos dos archivos:

wrangler.jsonc
// wrangler.jsonc
{
  "name": "xeye-proxy",
  "main": "src/index.js",
  "compatibility_date": "2026-10-01",
  "vars": { "ALLOWED_ORIGIN": "https://your-shop.com" }
}
src/index.js
// src/index.js
const json = (data, status, headers) =>
  new Response(JSON.stringify(data), {
    status,
    headers: { ...headers, 'Content-Type': 'application/json' },
  })

export default {
  async fetch(request, env) {
    const cors = {
      'Access-Control-Allow-Origin': env.ALLOWED_ORIGIN,
      'Access-Control-Allow-Methods': 'POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
      'Access-Control-Max-Age': '86400',
      Vary: 'Origin',
    }
    // A JSON POST from the browser is always preceded by a preflight.
    if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: cors })
    if (request.method !== 'POST') return json({ error: 'method_not_allowed' }, 405, cors)
    if (request.headers.get('Origin') !== env.ALLOWED_ORIGIN) {
      return json({ error: 'forbidden' }, 403, cors)
    }

    let body
    try {
      body = await request.json()
    } catch {
      return json({ error: 'bad_json' }, 400, cors)
    }
    const q = typeof body?.q === 'string' ? body.q.trim() : ''
    if (q.length < 2 || q.length > 200) return json({ error: 'bad_query' }, 400, cors)
    const limit = Math.min(Math.max(parseInt(body.limit, 10) || 5, 1), 20)

    const upstream = await fetch('https://search.xeye.es/api/v1/search', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-API-Key': env.XEYE_API_KEY },
      // The list name is fixed here: the browser cannot query other lists.
      body: JSON.stringify({ list_name: 'products', search_term: q, limit }),
    })
    if (!upstream.ok) return json({ error: 'search_unavailable' }, 502, cors)
    const data = await upstream.json()
    return json({ results: data.results }, 200, cors)
  },
}

Cambia ALLOWED_ORIGIN por el dominio de tu tienda. Después guarda la clave como secreto y despliega:

bash
npx wrangler secret put XEYE_API_KEY
npx wrangler deploy

El despliegue te da una dirección terminada en workers.dev. Esa es la que consultará el tema.

Paso 4: ¿cómo añado el buscador al tema?

Con una sección nueva. En el editor de código del tema, crea el archivo sections/xeye-search.liquid con este contenido y pon la dirección de tu Worker en ENDPOINT:

sections/xeye-search.liquid
{%- comment -%} sections/xeye-search.liquid {%- endcomment -%}
<input type="search" id="xeye-q" placeholder="Search products" autocomplete="off">
<ul id="xeye-results"></ul>

<script>
  (() => {
    // Your Worker URL, or '/apps/xeye' if you use an app proxy.
    const ENDPOINT = 'https://xeye-proxy.YOUR-ACCOUNT.workers.dev'
    const input = document.getElementById('xeye-q')
    const list = document.getElementById('xeye-results')
    let timer
    let controller

    input.addEventListener('input', () => {
      clearTimeout(timer)
      const q = input.value.trim()
      if (q.length < 2) {
        list.replaceChildren()
        return
      }
      // Wait until the user stops typing: one search, not one per keystroke.
      timer = setTimeout(async () => {
        controller?.abort()
        controller = new AbortController()
        try {
          const response = await fetch(ENDPOINT, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ q, limit: 5 }),
            signal: controller.signal,
          })
          const { results = [] } = await response.json()
          list.replaceChildren(
            ...results.map((result) => {
              const item = document.createElement('li')
              const link = document.createElement('a')
              link.href = result.params?.url || '#'
              link.textContent = result.item
              item.append(link)
              return item
            }),
          )
        } catch (error) {
          if (error.name !== 'AbortError') list.replaceChildren()
        }
      }, 300)
    })
  })()
</script>

{% schema %}
{ "name": "XEYE search", "settings": [], "presets": [{ "name": "XEYE search" }] }
{% endschema %}

La sección aparece en el editor de temas como «XEYE search» y puedes añadirla a cualquier plantilla. Espera 300 milisegundos desde la última tecla antes de buscar, para que una búsqueda sean una o dos llamadas. Los estilos corren de tu cuenta.

¿Es seguro dejar el proxy abierto?

La comprobación de origen del Worker impide que otra web lo use desde un navegador, pero no impide que alguien lo llame con un programa. Como cada búsqueda consume crédito, conviene añadir una regla de limitación de peticiones en Cloudflare y crear una alerta de saldo en XEYE.

La opción más robusta: un proxy de aplicación

Shopify puede reenviar las peticiones de https://tu-tienda/apps/xeye a tu Worker y firmarlas. Así el buscador llama a tu propio dominio y el Worker puede verificar que la petición viene de Shopify. Los pasos son:

  1. Crea una aplicación en el Dev Dashboard de Shopify. Las aplicaciones personalizadas creadas desde el panel de la tienda no admiten proxy.
  2. En una versión de la aplicación, añade el permiso write_app_proxy y configura el proxy con prefijo apps, subruta xeye y la dirección de tu Worker.
  3. Publica la versión e instala la aplicación en tu tienda.
  4. Guarda el secreto de cliente de la aplicación en el Worker con npx wrangler secret put SHOPIFY_APP_SECRET.
  5. Sustituye la comprobación de origen del Worker por la verificación de firma, y cambia ENDPOINT a /apps/xeye en la sección.
src/index.js
// Replace the Origin check in the Worker with this signature check.
async function verifyShopifyProxy(requestUrl, secret) {
  const params = new URL(requestUrl).searchParams
  const signature = params.get('signature') || ''
  if (!/^[0-9a-f]{64}$/.test(signature)) return false

  // key=value pairs (repeated keys joined with ","), sorted and concatenated.
  const keys = [...new Set(params.keys())].filter((key) => key !== 'signature')
  const message = keys
    .map((key) => `${key}=${params.getAll(key).join(',')}`)
    .sort()
    .join('')

  const encoder = new TextEncoder()
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify'],
  )
  const bytes = Uint8Array.from(signature.match(/../g), (hex) => parseInt(hex, 16))
  return crypto.subtle.verify('HMAC', key, bytes, encoder.encode(message))
}

// In fetch():
// if (!(await verifyShopifyProxy(request.url, env.SHOPIFY_APP_SECRET))) {
//   return json({ error: 'forbidden' }, 403, {})
// }

¿Cómo mantengo el catálogo al día?

No hay sincronización automática. Cuando añadas productos, expórtalos, importa solo los nuevos y lanza un entrenamiento. Importar añade elementos y no reemplaza los que ya existen, así que si vuelves a importar el catálogo entero tendrás duplicados. Los cambios en productos existentes se editan en la consola.

Si tu catálogo cambia a diario y necesitas sincronización automática, facetas o una aplicación instalable, te conviene una herramienta pensada para eso. Lo explicamos en XEYE frente a Algolia.

Preguntas frecuentes

¿Sustituye al buscador de Shopify?

No. Este tutorial añade una caja de búsqueda propia. El buscador nativo y la búsqueda predictiva del tema siguen funcionando, y puedes dejar los dos o retirar el nativo del tema.

¿Necesito crear una aplicación de Shopify?

Para la opción rápida, no: basta el Worker y la sección del tema. Solo la necesitas si quieres el proxy de aplicación con firma.

¿Qué guardo en `params`?

Lo que necesites para pintar un resultado sin otra consulta. El script guarda el identificador, el handle, la URL, la referencia, el precio y la imagen del producto.

¿Puedo filtrar por colección o por precio?

XEYE no filtra. Puedes guardar la colección o el precio en params y filtrar los resultados en tu código, o crear una lista por colección.

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