Use Cases (Actualizado: 1/6/2026)

Implementar búsqueda con Claude Code: Postgres, Meilisearch y Algolia

Guía práctica para crear búsqueda con Claude Code: requisitos, índice, sync, filtros, debounce UI, pruebas y despliegue.

Implementar búsqueda con Claude Code: Postgres, Meilisearch y Algolia

La búsqueda es una experiencia de descubrimiento

Una funcionalidad de búsqueda toma la consulta del usuario, encuentra candidatos, los filtra, los ordena y muestra por qué coinciden mediante resaltado. Un campo que ejecutaLIKE '%term%' puede servir para una tabla interna, pero no para un blog, una base de conocimiento o un catálogo que busca aumentar PV, leads o ventas.

La lección práctica de Masa es clara: empezar por la UI genera retrabajo. Antes hay que definir campos indexados, datos públicos y privados, separación por idioma, ranking, sincronización y revisión de logs. Claude Code puede escribir el código rápido, pero necesita un briefing concreto.

Para ampliar, revisa búsqueda Algolia con Claude Code, desarrollo de API con Claude Code y optimización de rendimiento con Claude Code.

Casos de uso que cambian la arquitectura

Caso de usoEjemplosLo importanteMejor opción
Búsqueda de contenidoblog, FAQ, documentaciónpeso del título, resumen, tags, idiomaPostgres full-text o Meilisearch
Catálogoproductos, cursos, plantillasfacetas, orden, sinónimos, analíticaMeilisearch o Algolia
Panel adminclientes, facturas, logspermisos, filtros exactos, auditoríaPostgres primero
Contenido multilingüepáginas en es/en/jalocale, keywords locales, URL correctasMeilisearch o Algolia

Si los datos ya están en Postgres y el volumen es moderado, empieza con búsqueda full-text. Si necesitas tolerancia a errores, facetas y relevancia lista para usar, Meilisearch es una buena evolución. Si la búsqueda impacta ingresos o conversiones, Algolia suele justificar su coste.

Prompt de requisitos para Claude Code

Implementa búsqueda de producción en una app Next.js existente.

Objetivo:
- Buscar artículos publicados y mejorar la navegación interna.
- Soportar query, locale, category y tags.
- Buscar en title, summary, tags y body, con mayor peso en title.
- Devolver campos suficientes para tarjetas de resultado y resaltado.

Restricciones:
- No devolver drafts, registros privados, emails, notas internas ni contenido restringido.
- No exponer claves admin o write en el navegador.
- Usar debounce de 300 ms y AbortController en la UI.
- Registrar búsquedas sin resultado, búsquedas lentas y clics.

Entregables:
- Nota comparando Postgres full-text, Meilisearch y Algolia.
- Esquema de índice.
- Trabajo de sincronización.
- Ruta /api/search.
- UI React.
- Pruebas y checklist de rollout.

Pide a Claude Code que lea primero el esquema, el frontmatter MDX, las reglas de autenticación y la estructura de URL. En búsqueda, el peor error no es un ranking flojo, sino exponer algo que no debía ser buscable.

Elegir Postgres, Meilisearch o Algolia

La documentación oficial de PostgreSQL sobre Full Text Search cubretsvector, tsquery y ranking. Meilisearch tiene una entrada rápida en quick start y una guía de filtering, sorting and faceting. Algolia destaca por sus widgets de InstantSearch.js y React InstantSearch.

CREATE TABLE IF NOT EXISTS articles (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  slug text NOT NULL UNIQUE,
  locale text NOT NULL,
  status text NOT NULL CHECK (status IN ('draft', 'published', 'private')),
  title text NOT NULL,
  summary text NOT NULL,
  body text NOT NULL,
  category text NOT NULL,
  tags text[] NOT NULL DEFAULT '{}',
  popularity integer NOT NULL DEFAULT 0,
  updated_at timestamptz NOT NULL DEFAULT now(),
  search_vector tsvector GENERATED ALWAYS AS (
    setweight(to_tsvector('simple', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('simple', coalesce(summary, '')), 'B') ||
    setweight(to_tsvector('simple', coalesce(array_to_string(tags, ' '), '')), 'B') ||
    setweight(to_tsvector('simple', coalesce(body, '')), 'C')
  ) STORED
);

CREATE INDEX IF NOT EXISTS articles_search_vector_idx
  ON articles USING GIN (search_vector);

CREATE INDEX IF NOT EXISTS articles_locale_status_idx
  ON articles (locale, status, updated_at DESC);

El punto clave es ponderar. Un término en el título debe pesar más que una mención aislada al final del cuerpo. Esta estructura también evita recalcular el vector de búsqueda en cada request.

Job de sincronización con Meilisearch

La base de datos o el CMS siguen siendo la fuente de verdad. El índice de búsqueda solo recibe campos públicos.

// scripts/sync-meilisearch.ts
import "dotenv/config";
import { MeiliSearch } from "meilisearch";

type ArticleRecord = {
  id: string;
  title: string;
  summary: string;
  body: string;
  locale: string;
  status: "published";
  category: string;
  tags: string[];
  url: string;
  popularity: number;
  updatedAtTimestamp: number;
};

const client = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST ?? "http://127.0.0.1:7700",
  apiKey: process.env.MEILISEARCH_ADMIN_KEY
});

const index = client.index<ArticleRecord>("articles");

await index.updateSettings({
  searchableAttributes: ["title", "summary", "body", "tags"],
  filterableAttributes: ["locale", "status", "category", "tags"],
  sortableAttributes: ["updatedAtTimestamp", "popularity"],
  displayedAttributes: ["id", "title", "summary", "locale", "category", "tags", "url"]
});

const task = await index.addDocuments(
  [
    {
      id: "es_claude-code-search-functionality",
      title: "Implementar búsqueda con Claude Code",
      summary: "Guía práctica sobre backend, índice, UI, pruebas y rollout.",
      body: "Texto público extraído de MDX o del CMS.",
      locale: "es",
      status: "published",
      category: "use-cases",
      tags: ["Claude Code", "búsqueda", "full-text"],
      url: "/es/blog/claude-code-search-functionality",
      popularity: 18,
      updatedAtTimestamp: 1780272000
    }
  ],
  { primaryKey: "id" }
);

console.log(`Queued Meilisearch task ${task.taskUid}`);

No conviertas cada columna en una faceta. Para contenidos, suele bastar concategory, tags ylocale. Los filtros de permisos deben vivir en el servidor o en claves de búsqueda restringidas.

UI React con debounce

// components/ArticleSearchBox.tsx
"use client";

import { useEffect, useMemo, useState } from "react";

type SearchHit = {
  id: string;
  title: string;
  summary: string;
  url: string;
  category: string;
};

function useDebounce<T>(value: T, delayMs: number) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = window.setTimeout(() => setDebounced(value), delayMs);
    return () => window.clearTimeout(timer);
  }, [value, delayMs]);

  return debounced;
}

export function ArticleSearchBox({ locale = "es" }: { locale?: string }) {
  const [query, setQuery] = useState("");
  const [category, setCategory] = useState("");
  const [hits, setHits] = useState<SearchHit[]>([]);
  const [loading, setLoading] = useState(false);
  const debouncedQuery = useDebounce(query, 300);

  const params = useMemo(() => {
    const next = new URLSearchParams({ q: debouncedQuery, locale });
    if (category) next.set("category", category);
    return next;
  }, [category, debouncedQuery, locale]);

  useEffect(() => {
    if (debouncedQuery.trim().length < 2) {
      setHits([]);
      return;
    }

    const controller = new AbortController();
    setLoading(true);

    fetch(`/api/search?${params.toString()}`, { signal: controller.signal })
      .then((response) => {
        if (!response.ok) throw new Error("Search request failed");
        return response.json();
      })
      .then((data: { hits: SearchHit[] }) => setHits(data.hits))
      .catch((error) => {
        if (error.name !== "AbortError") console.error(error);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [debouncedQuery, params]);

  return (
    <section aria-label="Article search">
      <input
        aria-label="Search keywords"
        type="search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search Claude Code articles"
      />
      <select aria-label="Category" value={category} onChange={(event) => setCategory(event.target.value)}>
        <option value="">All</option>
        <option value="use-cases">Use cases</option>
        <option value="advanced">Advanced</option>
      </select>
      {loading && <p>Searching...</p>}
      <ul>
        {hits.map((hit) => (
          <li key={hit.id}>
            <a href={hit.url}>{hit.title}</a>
            <p>{hit.summary}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

Pruebas, rollout y errores concretos

Los fallos más comunes son indexar borradores, exponer claves admin, enviar campos privados a un proveedor externo, añadir demasiados sinónimos, crear demasiadas facetas y no revisar búsquedas sin resultado. Prueba al menos consultas cortas, filtropublished, filtro por categoría y render móvil.

// tests/search-query.test.ts
import { describe, expect, it } from "vitest";

function shouldSearch(query: string) {
  return query.trim().length >= 2 && query.length <= 80;
}

describe("search request rules", () => {
  it("rejects empty and one-character queries", () => {
    expect(shouldSearch("")).toBe(false);
    expect(shouldSearch("a")).toBe(false);
    expect(shouldSearch("api")).toBe(true);
  });
});

Antes de publicar, valida que el índice solo contiene datos públicos, que hay estado de 0 resultados, que la latencia p95 es aceptable, que las queries largas se recortan y que los logs no guardan datos personales. Después, revisa semanalmente 0 resultados y bajo CTR para mejorar títulos, sinónimos, enlaces internos y nuevas piezas de contenido.

ClaudeCodeLab ayuda con diseño de búsqueda, formación en Claude Code y revisión de implementación. Si necesitas acompañamiento, la página de formación y consultoría es el siguiente paso suave.

Resumen

El orden robusto es: requisitos, elección de motor, esquema de índice, sync job, filtros y facetas, UI con debounce, pruebas y rollout. En la práctica, limitar pronto los campos indexados y devueltos reduce correcciones, y revisar búsquedas sin resultado produce buenas ideas para crecer en PV.

#Claude Code #búsqueda #búsqueda full-text #Meilisearch #Algolia
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.