Algolia Search skill for Codex

Relevance settings in git, zero-downtime reindexes. Codex designs Algolia records for search rather than mirroring database rows, keeps searchableAttributes, customRanking and faceting in a reviewed settings script, and rebuilds indexes atomically so users never hit an empty index. Tenant isolation uses server-generated secured API keys, and React InstantSearch pages ship with URL routing and Insights events wired up.

  • Integrations
  • $algolia-search
  • 203 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- algolia-search

Installs to ~/.agents/skills/algolia-search/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $algolia-search, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Algolia search end to end - record design, indexing with the v5 algoliasearch client, index settings (searchableAttributes, customRanking, faceting, replicas), atomic reindexing, secured API keys for tenants, and React InstantSearch UIs with facets, routing and Insights events. Use when adding Algolia to an app, syncing a database to an index, fixing irrelevant results or missing facets, building an InstantSearch page, or scoping search per user or tenant.

Use it when

  • Adding hosted search to a site or app with Algolia
  • Syncing database records into an Algolia index or rebuilding it
  • Results feel irrelevant, facets are missing or sort orders are wrong
  • Search must be restricted per user or tenant

Use something else when

How to check the result

getSettings matches the settings file, a fixed list of real queries returns the expected first hit, and a secured key for one tenant finds nothing from another.

The mistake it prevents

Indexing without a stable objectID, so every resync silently duplicates the catalog.

~/.agents/skills/algolia-search/SKILL.md

--- name: algolia-search description: Builds Algolia search end to end - record design, indexing with the v5 algoliasearch client, index settings (searchableAttributes, customRanking,… license: MIT ---

Done well means: records are small and shaped for search (not copies of DB rows), relevance is set explicitly in index settings that live in version control, reindexing never shows users an empty index, and no browser ever receives a key that can write.

Before touching code

  1. Read AGENTS.md for the package manager, env var conventions and where background jobs live.
  2. Find existing usage: rg -n "algoliasearch|instantsearch|ALGOLIA" --glob '!node_modules'.
  3. Check the client major version in the lockfile. v5 (algoliasearch@5) uses a single client with indexName in every call; v4 used client.initIndex(name). Do not mix the two styles.
  4. Confirm which keys exist: Application ID, a Search-only key (public), an Admin key or a scoped write key (server only). Network calls to Algolia need sandbox network approval.

Key handling

KeyWhere it may liveUse
Search-only API keyBrowser, mobile appsQueries only
Secured API key (derived)Browser, per userSearch restricted by filters / expiry
Write key with ACL addObject, deleteObject, settingsServer, CIIndexing jobs
Admin keySecrets manager onlyKey management, never in app runtime

Create a narrow write key for the indexer instead of shipping the Admin key to workers.

Record design

  • One record per searchable thing. Include only attributes you search, filter, rank or display.
  • Stable objectID equal to the DB primary key, so updates overwrite instead of duplicating.
  • Precompute ranking signals as numbers (popularity, sales_30d, created_at_ts as Unix seconds).
  • Keep records small; plans enforce a per-record size limit (10 KB on entry plans). Split long text (docs, articles) into paragraph records sharing a group_id, then set attributeForDistinct: "group_id" and distinct: true so one hit per page appears.
  • Store tenant or permission data as filterable attributes (tenant_id, visible_to).
{
  "objectID": "sku_18342",
  "name": "Trail running shoe",
  "brand": "Norda",
  "categories": ["Shoes", "Shoes > Running"],
  "price": 189,
  "in_stock": true,
  "popularity": 8421,
  "tenant_id": "store_7",
  "url": "/p/sku_18342",
  "image": "https://cdn.example.com/sku_18342.jpg"
}

Index settings as code

Keep settings in a file and apply them from a script, so relevance changes are reviewed in PRs.

// scripts/algolia-settings.ts
import { algoliasearch } from 'algoliasearch';

const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_KEY!);

const { taskID } = await client.setSettings({
  indexName: 'products',
  indexSettings: {
    searchableAttributes: ['name', 'brand', 'unordered(categories)', 'unordered(description)'],
    attributesForFaceting: ['searchable(brand)', 'categories', 'in_stock', 'filterOnly(tenant_id)'],
    customRanking: ['desc(in_stock)', 'desc(popularity)'],
    attributesToSnippet: ['description:20'],
    replicas: ['products_price_asc'],
  },
  forwardToReplicas: true,
});
await client.waitForTask({ indexName: 'products', taskID });

Settings decisions:

NeedSetting
Title matches beat description matchesOrder in searchableAttributes; most important first
Word position inside a long field should not matterunordered(attr)
Business signal breaks tiescustomRanking with numeric attributes only
Sort by priceReplica with ranking starting asc(price); use a virtual replica if your plan supports it
Filter but never show as facetfilterOnly(attr)
Search inside a long facet listsearchable(attr)
Typos hurt SKUs or codesdisableTypoToleranceOnAttributes: ['sku']

Never put a timestamp or free text in customRanking; a unique value per record turns the tie-breaker into the only criterion that matters.

Indexing

Incremental updates from the app or a queue worker:

await client.saveObjects({ indexName: 'products', objects: records });          // upsert
await client.partialUpdateObjects({ indexName: 'products', objects: [{ objectID: 'sku_18342', in_stock: false }], createIfNotExists: false });
await client.deleteObjects({ indexName: 'products', objectIDs: ['sku_99'] });

Full rebuilds use replaceAllObjects, which writes to a temporary index and moves it over the live one atomically, so search never sees a half-built index:

await client.replaceAllObjects({ indexName: 'products', objects: allRecords, batchSize: 1000 });

Rules for sync jobs:

  • Send changes from the same transaction boundary as the DB write (outbox table or job queue), not from a request handler that may fail after the commit.
  • Batch at about 1,000 records; each batch counts as operations against your quota.
  • For very large catalogs, stream from the DB with a cursor and call saveObjects per chunk into a fresh index, then use operationIndex with operation: 'move' to swap.
  • replaceAllObjects and move overwrite the live index. Confirm the target index name with the user before running a full rebuild against production.

Python (v4 client) has the same shape: SearchClientSync(app_id, key).save_objects(index_name=..., objects=...).

Multi-tenant and per-user access

Generate a secured key on the server for each session; never filter tenants in the browser only.

const securedKey = client.generateSecuredApiKey({
  parentApiKey: process.env.ALGOLIA_SEARCH_KEY!,
  restrictions: {
    filters: `tenant_id:${session.tenantId}`,
    validUntil: Math.floor(Date.now() / 1000) + 3600,
    restrictIndices: ['products'],
  },
});

The parent must be a search-only key. Return securedKey to the client and initialise InstantSearch with it; refresh before validUntil.

React InstantSearch

import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { InstantSearch, SearchBox, Hits, RefinementList, Pagination, Configure, Highlight } from 'react-instantsearch';
import { history } from 'instantsearch.js/es/lib/routers';

const searchClient = algoliasearch(APP_ID, SEARCH_KEY);

function Hit({ hit }) {
  return <a href={hit.url}><Highlight attribute="name" hit={hit} /></a>;
}

export function ProductSearch() {
  return (
    <InstantSearch searchClient={searchClient} indexName="products" routing={{ router: history() }} insights>
      <Configure hitsPerPage={24} />
      <SearchBox placeholder="Search products" />
      <RefinementList attribute="brand" searchable />
      <Hits hitComponent={Hit} />
      <Pagination />
    </InstantSearch>
  );
}
  • Facet widgets only work for attributes listed in attributesForFaceting.
  • routing puts query and refinements in the URL so results are shareable and back-button safe.
  • insights sends click and view events; send conversions (Added to cart, Purchased) with the queryID so Click and Conversion Analytics, Dynamic Re-Ranking and Recommend have data.
  • Next.js App Router: use react-instantsearch-nextjs (InstantSearchNext) for SSR.
  • Do not render a custom <input> that fires a search per keystroke without debouncing; the built-in SearchBox already handles query hooks.

Verification

  • client.getSettings({ indexName }) output matches the settings file.
  • client.searchSingleIndex({ indexName, searchParams: { query: '<known product>' } }) returns it in the top three; record five real queries and their expected first hit as a smoke test.
  • Facet counts in the UI match a facets: ['brand'] query.
  • Browser network tab shows only the search-only or secured key.
  • A secured key for tenant A returns zero hits for a record owned by tenant B.
  • Rebuild job run twice leaves the record count unchanged (nbHits with empty query).

If relevance is off, open the ranking info (getRankingInfo: true) for the expected and the actual first hit and compare criterion by criterion before changing settings.

Gotchas

  • saveObjects without objectID lets Algolia generate IDs; every resync then duplicates records.
  • Changing settings on a primary without forwardToReplicas leaves sort replicas stale.
  • filters syntax needs quotes for values with spaces: brand:"New Balance".
  • Numeric filters need numbers in the record; "189" as a string will not match price < 200.
  • Deleting a record in the DB but not in Algolia leaves dead links; handle deletes in the same sync.
  • Empty-query browsing returns records in customRanking order; check that it looks sensible.

Output

Report: index names touched, settings diff, records indexed or removed, keys created (by ACL, never the value), and the smoke-test queries with their first hits.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Algolia Search"
  short_description: "Relevance settings in git, zero-downtime reindexes"
  default_prompt: "$algolia-search Add Algolia search to our product catalog and keep it in sync with Postgres"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem