Skip to main content

Search

Krios ships a SearchProvider interface with two implementations: Postgres full-text (default) and Meilisearch (optional, faster, with typo tolerance and faceting).

Provider abstraction

interface SearchProvider {
readonly kind: "postgres" | "meilisearch";
readonly supportsFacets: boolean;
indexEntry(entry: IndexableEntry): Promise<void>;
removeEntry(entryId: string): Promise<void>;
search(query: string, options: SearchOptions): Promise<SearchResult>;
reindexAll(projectId: string): Promise<{ indexed: number }>;
}

The provider is resolved lazily on first use:

  • SEARCH_PROVIDER=meilisearch + MEILI_HOST + MEILI_API_KEY → Meilisearch
  • Otherwise → Postgres

When Meilisearch init fails, Krios logs once and falls back to Postgres.

Postgres provider

Built on tsvector + plainto_tsquery. The query computes a per-entry vector at request time from content_field_values.searchText, joined against content_locale_states for status filtering. Always available, no external dependencies. Doesn't surface highlight snippets or facet aggregation.

isSearchable: true on a field is what puts its value into searchText; the search vector aggregates only searchText (plus the content-type apiName and slug). Title-like fields (title, name, headline, label) are not force-included in the vector — that list is used only to enrich the displayed result title, not to make a field searchable.

Meilisearch provider

Talks to a Meilisearch server via the meilisearch Node package (optional peer dep). One index per project: krios_{projectId}. One document per (entry, locale) keyed {entryId}:{locale} so faceted queries can filter on locale without document explosion.

Index settings (auto-applied):

  • FilterablecontentType, siteId, locale, status, workflowState, environmentId
  • SortablecreatedAt, updatedAt, publishedAt, title
  • Searchabletitle, slug, searchText, fields.*

Lifecycle hooks (no-op on Postgres, real ops on Meili):

  • entry create / update → triggerEntryIndex
  • entry publish / unpublish → triggerEntryIndex (so status flips)
  • entry delete → triggerEntryRemoval

All hooks soft-fail; search-provider outages don't break CRUD.

Endpoint

GET /api/v1/projects/{slug}/search?q=hello&type=articlePage&locale=en-US&page=1&limit=25

Response:

{
"data": [
{
"entryId": "ckl_…",
"title": "Hello World",
"contentTypeApiName": "articlePage",
"siteId": "ckl_site_main",
"slug": "hello-world",
"locale": "en-US",
"status": "published",
"snippet": "<mark>Hello</mark> World",
"rank": 0.87,
"updatedAt": "2026-05-04T12:34:56Z"
}
],
"meta": {
"total": 42,
"page": 1,
"limit": 25,
"pages": 2,
"processingTimeMs": 3,
"provider": "meilisearch",
"facets": {
"contentType": [{ "value": "articlePage", "count": 12 }, ...]
}
}
}

facets is populated when the active provider supports it (Meilisearch only).

Reindex

POST /api/v1/projects/{slug}/search/reindex

Or via CLI:

krios search reindex --project demo

No-op when SEARCH_PROVIDER=postgres. Use after a content-type schema change, after switching to Meilisearch, or any time the index has drifted from the database.

Admin UI

/admin/projects/{slug}/search ships a debounced as-you-type search page with a facet sidebar (when the provider supports facets), result cards with content-type / locale / status badges, and snippet highlights.