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):
- Filterable —
contentType,siteId,locale,status,workflowState,environmentId - Sortable —
createdAt,updatedAt,publishedAt,title - Searchable —
title,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.