Search (Typesense)
Full-text, typo-tolerant search powered by a Typesense instance dedicated to your project. Mirror Postgres tables or Mongo collections into a search index that stays in lock-step with your data — then query it from your server. Search is a paid-plan add-on.
Auto-sync (CDC)
You don't index documents by hand. ichibase mirrors a Postgres table or a Mongo collection into a Typesense collection automatically, and keeps it current as a change-data-capture (CDC) stream. The sync engine reuses your project's realtime change stream — the same Postgres logical-replication consumer and Mongo change streams that power realtime subscriptions — so one replication slot feeds both WebSocket fan-out and search indexing.
For every write to a synced source: an INSERT or UPDATE upserts the projected document, and a DELETEremoves it by id. Indexing runs even when no client is subscribed and even if the table isn't realtime-listenable, so your index tracks the source of truth without cron jobs or manual reindexing.
Postgres WAL ─┐
├─► realtime consumer ─► (WS fan-out, if subscribed)
Mongo streams ─┘ └─► Typesense sink ─► project TypesenseConfiguring a sync
Sync is set up per source in the dashboard under Add-ons → Search sync:
1. Pick a source — a Postgres table or a Mongo collection — and Load fields.
The platform introspects the columns (and primary key), or samples a
Mongo document, and suggests a Typesense type per field.
2. Select the fields to index, and adjust each one's type / facet / sort.
Only the fields you select are projected into the search document.
3. Choose the document id field (the source primary key, or Mongo _id).
4. Name the target Typesense collection and Save. This creates (or
field-diffs) the collection and starts live sync.
5. Optionally click "Import existing rows" to backfill existing data —
live sync only covers changes from the moment you save.Config is stored per source in auth.typesense_sync (source_type, source_name, target_collection, id_field, fields, enabled). Backfill is owner-triggered (the “Import existing rows” button), not automatic, so you control when a large table is scanned; re-runs are safe because documents are upserted by id.
Filtering which rows sync
A sync can carry an optional filter — a small boolean expression. Only rows that match it are indexed, for both the backfill and live writes. If a row is updated so it no longer matches, it is removed from the search collection; if it starts matching, it is added. The same filter applies to Postgres and Mongo sources (it is evaluated per row/document).
created_at > now() - (5 days) && published = true && title NOT contains ("spam")Grammar:
boolean A && B, A || B, NOT (…)
compare field > 5, field >= 5, field = "x", field != true, field < now()
contains field contains "x" (case-insensitive substring)
field NOT contains "x"
values numbers, "strings", true / false, null
now() now() (current time)
now() - (N unit) unit = ms | seconds | minutes | hours | days | weeks
fields a column name (Postgres) or top-level document key (Mongo)Stored on the sync row as filter (the text you wrote) plus a validated filter_ast JSON tree. A bad expression is rejected when you save. Time comparisons expect a timestamp/date field; contains coerces the value to text. Leave the filter empty to index every row.
The search collection schema
A synced collection is a Typesense collection. You pick which source fields become search fields and their Typesense types when you configure the sync; the resulting schema has the shape Typesense uses:
{
"name": "posts",
"fields": [
{ "name": "id", "type": "string" },
{ "name": "title", "type": "string" },
{ "name": "author", "type": "string", "facet": true },
{ "name": "published", "type": "bool", "facet": true },
{ "name": "created_at", "type": "int64", "sort": true }
],
"default_sorting_field": "created_at"
}Values are coerced to the chosen type on the way in. Common source → Typesense type suggestions:
int2 / int4 → int32
int8 → int64
float4 / float8 / numeric → float
bool → bool
timestamptz / date → int64 (epoch millis)
text / uuid / varchar → string
json / jsonb / Mongo obj → object
arrays → string[] / int64[] / …Querying
@ichibase/typesenseSDK authenticates with your project's Typesense admin key (ICHIBASE_TYPESENSE_API_KEY). That key can read, write, and delete every collection — it must never reach a browser or native app. Use this SDK only from a trusted server or inside an ichibase Edge Function. It is a separate package from @ichibase/client, which is the one you ship to clients.The SDK re-exports the official Typesense client. Inside an Edge Function the URL and admin key are injected as env vars (ICHIBASE_TYPESENSE_URL and ICHIBASE_TYPESENSE_API_KEY), so createTypesense() picks them up with no arguments. The full Typesense surface (search, multi_search, collections, documents, synonyms, overrides, aliases) is available on the returned client.
// Server-side / Edge Function only — uses the project ADMIN key.
// This is @ichibase/typesense, NOT @ichibase/client.
import { createTypesense, tryOp } from 'jsr:@ichibase/typesense';
// Reads ICHIBASE_TYPESENSE_URL + ICHIBASE_TYPESENSE_API_KEY from env.
const ts = createTypesense();
const { data, error } = await tryOp(() =>
ts.collections('posts').documents().search({
q: 'rocket',
query_by: 'title,author',
filter_by: 'published:true',
sort_by: 'created_at:desc',
per_page: 20,
}),
);
if (error) throw new Error(error.detail);
const hits = data?.hits ?? [];Searching from a client app
Because querying needs the admin key, client apps must not talk to Typesense directly. The recommended pattern is to put a thin Edge Function in front of it: the function holds the admin key server-side, runs the search with @ichibase/typesense, and returns only the results (and only the fields) you want to expose. Your browser or native app calls that function with the anon key (ich_pub_…) through @ichibase/client — the admin key (ich_admin_…) and the Typesense key never leave your server.
client app ──(anon key)──► Edge Function ──(admin key)──► Typesense
holds the key,
shapes the responseCaveats
Sync is eventually consistent. Worth knowing before you rely on it:
• Deletes need an id in the change. A Postgres table with no primary
key can't be deindexed on delete (the delete carries only the PK
columns). Mongo deletes always carry _id.
• Eventually consistent, not transactional. If Typesense is briefly
down a change can be missed; the index self-heals on the next change
to that row, or via a re-backfill. Don't treat it as a system of record.
• Schema changes. Re-saving a sync diffs the collection (adds new
fields, drops removed ones; a type change is a drop + re-add).
Existing documents keep values for fields that still exist.