Search Integration skill for Claude Code

Search that ranks right and never drifts from the DB. Has Claude treat the search engine as a rebuildable derived store: records shaped for queries, settings declared per engine, atomic reindex via replaceAllObjects, index swaps or aliases, and after-commit sync that propagates deletes. Relevance changes are judged against a golden query set, and private data is protected with server-generated scoped keys rather than client filters.

  • namesearch-integration
  • size230 lines · ≈2,450 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Integrates hosted or self-hosted search engines - Algolia, Meilisearch, and Typesense - into apps, covering record design, index settings, relevance tuning with a golden query set, facets and filters, secured per-user keys, zero-downtime reindexing, and keeping the index in sync with the database. Use when adding site or product search, choosing between Algolia, Meilisearch and Typesense, fixing bad or missing results, building faceted filters, or when the search index drifts out of sync.

Try asking

  • Add product search with brand and price facets using Meilisearch
  • Our Algolia results are bad for SKU searches - tune the relevance
  • Deleted products keep showing up in search; fix the index sync

Use it when

  • Adding site, docs, or product search to an app
  • Choosing between Algolia, Meilisearch, Typesense, or Postgres full-text
  • Results are irrelevant, zero-result rate is high, or facets misbehave
  • The index has drifted from the database or reindexing causes downtime

Skip it when

  • You need embedding similarity search as the core feature - use vector-search instead
  • Search is slow because of database query plans - use sql-query-tuning instead

How to check the result

Run the golden query set before and after changes, then create, update and delete a DB record and confirm each reaches the index within the stated lag.

The common mistake it prevents

Writing to the index inside the database transaction leaves ghost records after rollbacks, and forgetting soft-deletes lets archived items keep appearing.

~/.claude/skills/search-integration/SKILL.md

--- name: search-integration description: Integrates hosted or self-hosted search engines - Algolia, Meilisearch, and Typesense - into apps, covering record design, index settings,… license: MIT ---

Search is done when a fixed set of real queries returns the right top results, the zero-result rate is measured, the index matches the database within a known lag (deletes included), and no browser ever holds a key that can write or read other tenants’ records.

Ground rules

  • Search engines are derived stores. The database is the source of truth; the index must be rebuildable from scratch at any time with one command.
  • Design records for the query, not the table. Flatten joins, precompute display fields and ranking signals, and keep each record small (Algolia has a per-record size limit; split long documents into paragraphs/chunks with a shared distinct key).
  • Browsers get search-only keys, scoped per tenant/user where data is private. Admin/master keys live only on the server, read from env.
  • Tune relevance against a written list of queries with expected results. Never tune by vibes on one query.
  • Every engine applies writes asynchronously. Wait for the task before asserting in tests.

Engine choice

SituationDefault
Want zero ops, strong merchandising/analytics, budget allows per-request pricingAlgolia
Self-host or cheap managed, simple typo-tolerant search, quick setupMeilisearch
Self-host with strict schema, geo, vector/hybrid, high write throughputTypesense
Already on Postgres, < ~1M rows, simple keyword searchPostgres full-text (tsvector + GIN) before adding a service
Semantic similarity is the core featureEngine hybrid search or a vector DB - see vector-search

Workflow

- [ ] 1. Collect 20-50 real queries and expected top results (golden set)
- [ ] 2. Design the record shape and ranking signals
- [ ] 3. Configure index settings (searchable, filterable, sortable, ranking)
- [ ] 4. Build full reindex with atomic swap
- [ ] 5. Build incremental sync incl. deletes
- [ ] 6. Wire the UI with scoped keys
- [ ] 7. Evaluate against the golden set; tune; repeat

1. Golden query set

Pull queries from site search logs, support tickets, or analytics. Include: exact product names, typos, partial words, synonyms (“tee” vs “t-shirt”), SKUs, multi-word intent, and queries that should return nothing. Store as JSON in the repo:

[{ "q": "nike air max 90", "expect_top3": ["prod_812"] },
 { "q": "tshirt", "expect_any_top10": ["prod_11", "prod_45"] },
 { "q": "zzqx", "expect_empty": true }]

2. Record design

{
  "objectID": "prod_812",
  "name": "Air Max 90",
  "brand": "Nike",
  "category_lvl0": "Shoes",
  "category_lvl1": "Shoes > Running",
  "description_short": "first 300 chars...",
  "price": 129.0,
  "in_stock": true,
  "popularity": 8432,
  "tenant_id": "t_42",
  "updated_at": 1760832000
}
  • Use a stable primary key from the DB (objectID in Algolia, id in Meilisearch and Typesense).
  • Add numeric business signals (sales, views, rating) for custom ranking; numbers, not strings.
  • Hierarchical categories as lvl0/lvl1 fields enable hierarchical facets.
  • Do not index fields nobody searches or filters on; they hurt relevance and cost.

3. Settings per engine

Algolia (JS client v5):

import { algoliasearch } from "algoliasearch";
const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY!);

await client.setSettings({
  indexName: "products",
  indexSettings: {
    searchableAttributes: ["name", "brand", "unordered(description_short)"],
    attributesForFaceting: ["searchable(brand)", "category_lvl0", "filterOnly(tenant_id)", "in_stock"],
    customRanking: ["desc(in_stock)", "desc(popularity)"],
    attributeForDistinct: "product_id",
  },
});

Meilisearch (REST; client SDKs map one-to-one):

curl -X PATCH "$MEILI_HOST/indexes/products/settings" \
  -H "Authorization: Bearer $MEILI_ADMIN_KEY" -H 'Content-Type: application/json' \
  --data '{
    "searchableAttributes": ["name", "brand", "description_short"],
    "filterableAttributes": ["brand", "category_lvl0", "tenant_id", "in_stock", "price"],
    "sortableAttributes": ["price", "popularity"],
    "rankingRules": ["words","typo","proximity","attribute","sort","exactness","popularity:desc"],
    "synonyms": { "tshirt": ["t-shirt", "tee"], "tee": ["t-shirt"] }
  }'
# returns a taskUid; poll GET /tasks/<uid> until status is "succeeded"

Typesense (schema is explicit; ranking via sort_by and query params):

{
  "name": "products_v2",
  "fields": [
    { "name": "name", "type": "string" },
    { "name": "brand", "type": "string", "facet": true },
    { "name": "category_lvl0", "type": "string", "facet": true },
    { "name": "tenant_id", "type": "string", "facet": true },
    { "name": "price", "type": "float" },
    { "name": "popularity", "type": "int32" }
  ],
  "default_sorting_field": "popularity"
}

Query: q=air max&query_by=name,brand&filter_by=in_stock:true&facet_by=brand&sort_by=_text_match:desc,popularity:desc.

Settings rules:

  • Searchable attribute order is the strongest relevance lever. Put names and titles first, long text last.
  • Filtering or faceting on an attribute requires declaring it. Undeclared filters fail or silently match nothing depending on the engine.
  • Changing settings on a large Meilisearch index triggers reindexing; do it on the new index before the swap, not on the live one.

4. Full reindex with zero downtime

EngineMechanism
Algoliaclient.replaceAllObjects({ indexName, objects }) (temp index + atomic move; settings, synonyms and rules copied)
MeilisearchBuild products_new, then POST /swap-indexes with [{"indexes":["products","products_new"]}], delete old
TypesenseBuild products_v3, then point alias products at it (PUT /aliases/products), drop old collection

Batch writes (Algolia ~1,000 records per batch; Typesense documents/import JSONL with action=upsert; Meilisearch POST /indexes/x/documents with batches of a few MB).

5. Incremental sync

Write volume / consistency needPattern
Low volume, a few seconds lag fineEnqueue a job after DB commit (transactional outbox)
High volume or many writersCDC (Debezium, Postgres logical replication) into a consumer
Nightly freshness is enoughBatch reindex on a schedule
  • Never write to the index inside the DB transaction; a rollback leaves a ghost record. Use an outbox row or an after-commit hook.
  • Propagate deletes and soft-deletes (archived, unpublished, out-of-tenant). Missing deletes are the most common drift.
  • Make index writes idempotent upserts keyed by ID; include updated_at and skip stale events.
  • Run a periodic drift check: compare counts and a sample of IDs/updated_at between DB and index; alert above a threshold.

6. UI and keys

  • Use InstantSearch (react-instantsearch) for Algolia; Meilisearch and Typesense provide InstantSearch adapters (@meilisearch/instant-meilisearch, typesense-instantsearch-adapter), so one UI layer works across engines.
  • Multi-tenant or per-user data: generate keys server-side with the filter baked in - Algolia secured API keys (client.generateSecuredApiKey with restrictions.filters), Meilisearch tenant tokens, Typesense scoped search keys. Never rely on a client-side filter for isolation.
  • Debounce input lightly (or not at all with hosted engines); cancel in-flight requests.
  • Send click and conversion events (Algolia Insights, or your analytics) so relevance can be judged on real behavior.

Relevance tuning loop

  1. Run the golden set and record pass/fail per query.
  2. Fix in this order: record content (missing fields) -> searchable attribute order -> synonyms -> typo tolerance per attribute (disable on SKUs/codes) -> custom ranking signals -> query rules/pinning for specific business cases.
  3. Rerun the whole set after each change; a fix for one query often breaks another.
  4. Track zero-result rate and click-through position from production weekly.

Gotchas

  • Typo tolerance on codes and SKUs produces wrong matches; disable it for those attributes (disableTypoToleranceOnAttributes in Algolia, typoTolerance.disableOnAttributes in Meilisearch, num_typos per field in Typesense).
  • Faceting on high-cardinality fields (user IDs, timestamps) slows queries and bloats responses. Use filterOnly or non-facet filterable fields.
  • Default result caps: Meilisearch limits total reachable hits (pagination.maxTotalHits, default 1000); Algolia caps pagination (paginationLimitedTo). Deep pagination is a UX smell; use filters.
  • Prices and dates must be numeric (timestamps), or range filters and sorting break.
  • Tests that write then search immediately are flaky; wait for the task (Algolia waitForTask, Meilisearch task polling) first.

Verification

  • Golden set passes at or above the agreed bar (for example, 90% of expect_top3), and the result is recorded before and after each tuning change.
  • Create, update, and delete a record in the DB; confirm each appears in the index within the stated lag.
  • Use a tenant-scoped key to search for another tenant’s record ID: zero hits.
  • Rebuild the index from scratch in staging with the reindex command; counts match the DB.

Output contract

**Engine:** Meilisearch 1.x (self-hosted) - chosen for cost and typo tolerance
**Index:** products (alias swap via /swap-indexes), 412k docs, rebuild 6 min
**Sync:** outbox -> worker upserts, deletes on archive; p95 lag 3 s; nightly drift check
**Keys:** tenant tokens with filter tenant_id = X, 1 h expiry
**Relevance:** golden set 46/50 -> 49/50 (fixed synonyms, SKU typo tolerance off)
**Open issues:** "usb c cable" ranks adapters first; needs category boost

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc