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.
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 uso | Ejemplos | Lo importante | Mejor opción |
|---|---|---|---|
| Búsqueda de contenido | blog, FAQ, documentación | peso del título, resumen, tags, idioma | Postgres full-text o Meilisearch |
| Catálogo | productos, cursos, plantillas | facetas, orden, sinónimos, analítica | Meilisearch o Algolia |
| Panel admin | clientes, facturas, logs | permisos, filtros exactos, auditoría | Postgres primero |
| Contenido multilingüe | páginas en es/en/ja | locale, keywords locales, URL correctas | Meilisearch 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.
Esquema Postgres para full-text search
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.
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.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.