Search and attachments
Full-text search across collections, semantic (vector) search, index maintenance, and the attachment lifecycle with image transforms.
Full-text search#
Mark fields as searchable; their values are folded into a per-record tsvector index together with the
record’s display value. Search across every collection the caller can read:
GET /api/_search?q=acme&limit=20
qmust be at least 3 characters;limitis clamped to 1–100 (default 20).- Results carry the collection, record id, display value and rank, ordered by rank.
- Search returns only hits the caller can read. Each hit is checked against its own collection and
record, the same way as
GET /api/<collection>/<id>: collection read access, record-level rules and manual shares all apply. Hits the caller cannot read are left out before the limit is applied, andmeta.totalis the number of hits returned. - The end-user app exposes it as global search (
Cmd/Ctrl+K,/app/search); the MCP user toolset assearch. - Fields with a masking configuration are never indexed.
Rebuild the index after bulk changes with POST /api/admin/search-reindex (Setup → Platform → Search index).
Semantic search#
Give a collection a VECTOR field with an embeddingSource (a text field). On every write the source text is
embedded and stored; a pgvector HNSW cosine index is created automatically.
POST /api/<collection>/semantic-search
{ "query": "late payment from a long-standing customer", "limit": 10 }
Results are records ordered by cosine distance (returned in meta) and filtered by field-level security. MCP:
semantic_search.
Embedding provider. The platform ships an EmbeddingService interface with a dependency-free hashing
implementation as the default so the feature works everywhere; result quality with the default is limited. A
deployment plugs in a real model by providing its own EmbeddingService bean, and pgvector must be installed in
the database. Masking-configured source fields are never embedded; toggling masking purges existing vectors.
Attachments#
Files attach to any record through the attachments system collection. Storage is S3-compatible object storage
(KELTA_S3_* configuration — see Configuration reference).
- Upload —
POST /api/attachments/upload-urlwith the file’s name, type, size and targetcollectionId/recordId. The request is validated (type, size, the tenant’s storage quota), a pending row is created and a presignedPUTURL returned. Upload the bytes straight to that URL, thenPOST /api/attachments/{id}/finalize. - List and download —
GET /api/attachments?filter[collectionId][eq]=…&filter[recordId][eq]=…, or?include=attachmentson the record. Each resource carries a presigneddownloadUrl;GET /api/files/{storageKey}streams server-side withRangesupport. - Delete —
DELETE /api/attachments/{id}removes the object and the row. Deleting the parent record removes its attachments too.
Media fields#
A text field with media enabled (fieldTypeConfig.media.enabled: true) stores an object key instead of a file
record. POST /api/<collection>/upload-url with { fileName, contentType, fileSize, field? } returns a presigned
PUT URL and the storageKey; upload the bytes, then create or update the record with the field set to that key.
GET /api/<collection>/download/<storageKey> answers 302 to a short-lived presigned URL, so an <img src> works
without a bearer token — but only for a key a record of that collection references and the caller can read.
Uploads need create on the collection, downloads read.
Image transforms#
Images can be resized on the fly:
GET /api/images/<storageKey>?w=400&h=300&fit=cover&format=webp&quality=80
fit is cover or contain (default contain); format is webp, jpeg or png.