Implémenter une recherche avec Claude Code : Postgres, Meilisearch et Algolia
Guide pratique Claude Code pour la recherche : besoins, index, synchronisation, filtres, UI avec debounce, tests et rollout.
La recherche est une expérience produit
Une fonctionnalité de recherche prend une requête utilisateur, trouve des contenus candidats, les filtre, les trie et met en évidence les passages pertinents. Un simpleLIKE '%term%' peut suffire dans un outil interne, mais il ne suffit pas pour un blog monétisé, une base de connaissance ou un catalogue de formations. Une bonne recherche augmente les pages vues, remet en circulation les contenus durables et transforme les recherches sans résultat en idées éditoriales.
La leçon pratique de Masa est récurrente : commencer par l’interface crée de la dette. Il faut d’abord décider quels champs sont indexés, comment exclure les brouillons, comment isoler les langues, comment classer les résultats et comment relire les logs. Claude Code peut aller vite, mais il doit recevoir un cahier des charges précis.
À lire aussi : recherche Algolia avec Claude Code, développement API avec Claude Code et optimisation des performances avec Claude Code.
Clarifier les cas d’usage
| Cas d’usage | Exemples | Priorité | Bon choix |
|---|---|---|---|
| Recherche de contenu | blog, FAQ, documentation | poids du titre, résumé, tags, langue | Postgres plein texte ou Meilisearch |
| Recherche catalogue | produits, cours, templates | facettes, tri, synonymes, analytics | Meilisearch ou Algolia |
| Recherche admin | clients, factures, logs | droits, filtres exacts, audit | Postgres d’abord |
| Recherche multilingue | articles localisés | séparation par locale, mots-clés locaux | Meilisearch ou Algolia |
Pour un petit site déjà sur Postgres, la recherche plein texte est un bon départ. Quand les fautes de frappe, les facettes et la pertinence par défaut deviennent importantes, Meilisearch simplifie beaucoup. Quand la recherche est liée au chiffre d’affaires ou aux conversions, Algolia devient plus intéressant.
Prompt de cadrage pour Claude Code
Tu implémentes une recherche de production dans une application Next.js existante.
Objectif:
- Rechercher les articles publiés et améliorer la découverte de contenu.
- Supporter query, locale, category et tags.
- Chercher dans title, summary, tags et body, avec title comme champ le plus fort.
- Retourner les champs nécessaires aux cartes de résultat et au highlighting.
Contraintes:
- Ne jamais retourner brouillons, contenus privés, e-mails, notes internes ou données restreintes.
- Ne pas exposer de clé admin ou write dans le navigateur.
- Utiliser un debounce de 300 ms et AbortController côté UI.
- Logger les recherches sans résultat, les recherches lentes et les clics.
Livrables:
- Note de choix entre Postgres full-text, Meilisearch et Algolia.
- Schéma d'index.
- Job de synchronisation.
- Route /api/search.
- UI React.
- Tests et checklist de rollout.
Demande à Claude Code de lire le schéma de données, le frontmatter MDX, les règles d’authentification et la structure d’URL avant de modifier les fichiers. En recherche, la fuite de données est plus grave qu’un mauvais classement.
Choisir le moteur de recherche
La documentation officielle PostgreSQL sur Full Text Search couvretsvector, tsquery et le ranking. Meilisearch propose un quick start et une page claire sur filtering, sorting and faceting. Algolia est très fort côté UI avec InstantSearch.js et React InstantSearch.
Schéma d’index Postgres
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);
L’idée centrale est la pondération : le titre doit compter plus que le corps. C’est ce qui rapproche le classement de l’intention réelle du lecteur.
Job de synchronisation Meilisearch
// 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: "fr_claude-code-search-functionality",
title: "Implémenter une recherche avec Claude Code",
summary: "Guide pratique sur le choix du moteur, l'index, l'UI, les tests et le rollout.",
body: "Texte public extrait des fichiers MDX ou du CMS.",
locale: "fr",
status: "published",
category: "use-cases",
tags: ["Claude Code", "recherche", "plein texte"],
url: "/fr/blog/claude-code-search-functionality",
popularity: 18,
updatedAtTimestamp: 1780272000
}
],
{ primaryKey: "id" }
);
console.log(`Queued Meilisearch task ${task.taskUid}`);
Ne synchronise pas les champs privés en pensant les cacher ensuite dans l’UI. Si le champ est dans l’index, il peut ressortir par une API, un dashboard ou un log.
UI React avec 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 = "fr" }: { 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>
);
}
Tests, pièges et rollout
Les pièges concrets : indexer des brouillons, exposer une clé admin, envoyer des champs privés à un prestataire, ajouter trop de synonymes, transformer toutes les colonnes en facettes et ne jamais relire les recherches sans résultat.
// 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);
});
});
Avant publication, vérifie l’index public-only, l’état 0 résultat, la latence p95, la limite de longueur des requêtes, l’absence de données personnelles dans les logs et le rendu mobile. Ensuite, relis chaque semaine les requêtes sans résultat et les faibles taux de clic pour améliorer titres, synonymes, liens internes et nouveaux articles.
ClaudeCodeLab accompagne la conception de recherche, la formation Claude Code et les revues d’implémentation. Pour un accompagnement structuré, la page formation et conseil est le bon point d’entrée.
Résumé
L’ordre robuste est : exigences, choix du moteur, schéma d’index, synchronisation, filtres et facettes, UI avec debounce, tests, rollout. En pratique, limiter tôt les champs indexés et les champs retournés réduit fortement les corrections, tandis que les recherches sans résultat donnent les meilleures idées de contenus pour augmenter les pages vues.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.