Appearance
Typesense Search API ​
Overview ​
server/api/typesense/search.post.ts is the portal's only route to Typesense. The browser never holds a Typesense key: it posts a search description here, the route checks that the caller's token can see the requested workspace, validates every ranking parameter, resolves the field set for each collection against the live schema, runs one Typesense multi-search, and replaces raw S3 keys in the hits with signed URLs before returning.
The route does not add any workspace or instance clause to filter_by. Scoping to the portal instance comes from the client: buildFiltersForCollection in useCommonSearch prefixes every collection filter with (instance_visibility:=<brand id> && instance_visibility:!=0). See Security Considerations.
Endpoint ​
Path: POST /api/typesense/search
File: server/api/typesense/search.post.ts
Called from: useSearchApi().searchAssets(body) (through $api with useApiBase: false), used by useSearch, useSearchBar, useFolderDetails and useCollageDetails.
Request Body ​
typescript
interface SearchRequestBody {
request: {
q?: string // default "*"
collections: string[] // required, subset of ALLOWED_COLLECTIONS
sort_by?: string | { [collection: string]: string }
include_fields?: { [collection: string]: string[] }
group_by?: string
group_limit?: number
}
commonSearchParams: {
query_by: string | { [collection: string]: string }
query_by_weights?: string | { [collection: string]: string | { [field: string]: string | number } }
infix?: string | { [collection: string]: string | { [field: string]: string | number } }
per_page: number
page: number
sort_by?: string // fallback when request.sort_by has nothing for a collection
text_match_type?: 'max_score' | 'max_weight' | 'sum_score'
}
workspace_id_filter: string // workspace the token must be able to access
filterQuery?: { [collection: string]: string } // Typesense filter_by per collection, used as sent
}Only three collections can be searched. Anything else is rejected before Typesense is called:
| Collection | Holds | Portal tab |
|---|---|---|
digital_assets | Assets | Assets |
dam_collections | Collages | Collages |
digital_assets_categories | Folders | Folders |
query_by, query_by_weights and infix are each accepted either as one value for every collection or as a per-collection record. Weights and infix may additionally be a positional comma string ("10,6,4,1") or a map keyed by field name ({ "search_name": 10, "tags": 6 }, the shape buildSearchFieldParams in shared/searchFields.ts sends). The map shape is preferred because it survives a field being dropped by the schema check.
Response ​
typescript
{
[collectionName: string]: {
data: SearchResult // the Typesense result for that collection, in request order
}
}Before returning, signS3PathsInResults walks every hit (including grouped_hits) and swaps raw S3 keys for signed URLs. See Typesense Utils.
SearchResult Structure ​
typescript
{
hits: Array<{
document: object // indexed document; S3 key fields replaced with signed URLs
highlights?: object
text_match?: number
}>
grouped_hits?: Array<{ hits: Array<...> }> // present when group_by was sent
found: number
page: number
request_params: object
}Features ​
Authentication & Authorization ​
Authorizationheader is required; without it the route returns 401Authorization header is required.runtimeConfig.public.apiBaseUrlmust be set, otherwise 500API_BASE_URL environment variable is not configured.- The token is sent to
${apiBaseUrl}user. The returneddata.user.accessibleInstancesmust contain an entry whoseworkspace_idequalsworkspace_id_filterand that has a truthyinstance_id. - Any failure in step 3 (network error, empty list, no match) is logged as
Token verification failedand returned as 401Unauthorized - Invalid token or insufficient permissions.
Because the access check runs before parameter validation, a body with no workspace_id_filter fails with 401, not 400.
Multi-Collection Search ​
All collections in request.collections go to Typesense in a single multiSearch.perform call. Per collection the route builds:
| Parameter | Value |
|---|---|
q | "*" stays a wildcard; anything else is lowercased (String(q).toLowerCase()) |
query_by / query_by_weights / infix | resolved by resolveRequestedSearchFields (see Search Fields) |
filter_by | filterQuery[collection] or "" |
sort_by | see sort resolution below |
prefix | true |
num_typos | SEARCH_NUM_TYPOS = 0 |
typo_tokens_threshold | 1 |
prioritize_exact_match | true |
prioritize_token_position | true |
include_fields | include_fields[collection] when present |
group_by / group_limit | copied when present |
text_match_type | caller value, else SEARCH_TEXT_MATCH_TYPE = max_score |
per_page / page | from commonSearchParams |
Sort resolution per collection, first non-empty wins:
Instance Filtering ​
The route applies no instance or workspace filter of its own. The portal client builds it in useCommonSearch.buildFiltersForCollection:
typescript
const instanceId = useAppDataStore().brand?.id
const visibilityFilter = instanceId
? `(instance_visibility:=${instanceId} && instance_visibility:!=0)`
: ''That clause is joined with the collection's own filters and, on the Assets tab, with the asset-type filter, then sent as filterQuery[collection]. When brand.id is not set the visibility clause is empty.
Query Adjustment ​
The query is not wildcarded or tokenised. "*" passes through; every other value is lowercased and sent as typed. Prefix matching comes from prefix: true, substring matching from the per-field infix modes, and ranking order from the _eval relevance band that the client prepends to sort_by (useSearchApi.withRelevanceTier). With num_typos: 0, misspellings do not match.
Error Handling ​
All errors are re-thrown through createError with the original statusCode (default 500) and message. TypesenseConnectionError is logged as Typesense Connection Error:, others as Search Error:.
| Status | statusMessage | Cause |
|---|---|---|
| 401 | Authorization header is required | no Authorization header |
| 500 | API_BASE_URL environment variable is not configured | missing runtime config |
| 401 | Unauthorized - Invalid token or insufficient permissions | token check failed or workspace not in accessibleInstances |
| 400 | Missing required parameters in the request. | request, workspace_id_filter or commonSearchParams missing |
| 400 | Collection name is required in the request. | collections empty |
| 400 | Collection "<name>" is not searchable. | collection outside the allow-list |
| 400 | Parameter `text_match_type` must be one of: max_score, max_weight, sum_score. | bad ranking mode |
| 400 | Invalid query_by for collection "<name>". ... | not field(,field)* of [a-zA-Z0-9_] |
| 400 | Invalid query_by_weights for collection "<name>". ... | string not 1-3 digit integers, or map weight outside 0-127 |
| 400 | Invalid infix for collection "<name>". ... | mode other than always, fallback, off |
| 400 | Parameter `sort_by` is malformed. ... | bad clause in commonSearchParams.sort_by or a single-collection sort_by |
| 400 | Invalid sort_by format for collection "<name>". ... | bad clause in a per-collection sort_by map |
| 400 | Parameter `sort_by` must be a string or an object ... | multi-collection sort_by that is an array or other type |
| 503 / 500 | from performMultiSearch | connection failure or Typesense API error |
Missing Authorization ​
Checked first, before the body is used. Returns 401 Authorization header is required.
Invalid Token ​
Returns 401 Unauthorized - Invalid token or insufficient permissions whenever ${apiBaseUrl}user fails, returns no accessibleInstances, or has no entry for workspace_id_filter.
Missing Parameters ​
Returns 400 Missing required parameters in the request. when request, workspace_id_filter or commonSearchParams is falsy (only reachable once the token check has passed).
Invalid Collections ​
collections missing or empty gives 400 Collection name is required in the request.. A name outside digital_assets, dam_collections, digital_assets_categories gives 400 Collection "<name>" is not searchable. (the first offending name is reported).
Invalid Sort By ​
Every sort_by string is checked by isValidSortBy:
- An optional leading
_eval(...)band must passisValidEvalTier: values inside backticks, only[A-Za-z0-9_()[\]:=|&, ]outside them, and only fields that appear in the tier definitions (search_name,tags,portal_custom_field_values). - After the band is stripped, every comma-separated clause must match
^[a-zA-Z0-9_]+:(asc|desc)$.
A single-collection request must send sort_by as a string; an object there is rejected as malformed. A multi-collection request may send a string (applied to all) or a per-collection map.
Example accepted value for digital_assets and q = "brand":
text
_eval([ (search_name:=`brand` && tags:=`brand`):6, (search_name:=`brand`):5, (tags:=`brand` && portal_custom_field_values:=`brand`):4, (tags:=`brand`):3, (portal_custom_field_values:=`brand`):2 ]):desc,_text_match:desc,modified_at:descTypesense Errors ​
performMultiSearch throws TypesenseConnectionError (its status is 503) for ECONNREFUSED, ECONNRESET, ETIMEDOUT, connection or timeout messages, and HTTP 502/503/504; other Typesense failures become a plain Error with message Typesense Error: <message>. The route re-throws with err.statusCode || 500. TypesenseConnectionError sets status, not statusCode, so connection failures currently surface to the browser as 500 with the connection message.
Usage ​
typescript
const searchApi = useSearchApi()
const results = await searchApi.searchAssets({
request: {
q: 'brand',
collections: ['digital_assets'],
sort_by: '_text_match:desc,modified_at:desc', // _eval band is prepended by searchAssets
},
filterQuery: {
digital_assets: '(instance_visibility:=42 && instance_visibility:!=0)',
},
commonSearchParams: {
...buildSearchFieldParams(['digital_assets']),
per_page: 50,
page: 1,
text_match_type: SEARCH_TEXT_MATCH_TYPE,
},
workspace_id_filter: 'WORKSPACE_ID',
})
const hits = results.digital_assets.data.hitsbuildSearchFieldParams(['digital_assets']) produces:
json
{
"query_by": { "digital_assets": "search_name,tags,portal_custom_field_values,description" },
"query_by_weights": { "digital_assets": { "search_name": 10, "tags": 6, "portal_custom_field_values": 4, "description": 1 } },
"infix": { "digital_assets": { "search_name": "always", "tags": "fallback", "portal_custom_field_values": "fallback", "description": "off" } }
}Request Flow ​
- Read
Authorization; 401 if absent. - Verify the token against
${apiBaseUrl}userand matchworkspace_id_filterinaccessibleInstances; 401 on any failure. - Check
request,workspace_id_filter,commonSearchParams; 400 if missing. - Check
collectionsagainst the allow-list; 400 on the first bad name. - Validate
text_match_type, then per collectionquery_by,query_by_weights,infix; thencommonSearchParams.sort_byandrequest.sort_by. - For each distinct collection, call
resolveRequestedSearchFields(live schema, cached 10 minutes) to getquery_by, weights and infix narrowed to indexed fields. - Build one
SearchRequestper collection (filter, resolved sort, fixed typo and prefix settings); rebuild the_evalband if it names an unindexed field. - Run
performMultiSearch. - Key results by collection name and sign S3 paths.
Security Considerations ​
- The Typesense key stays on the server (
getTypesenseClient()inutils.ts). - Every request needs a token that can access
workspace_id_filter. - Only
digital_assets,dam_collectionsanddigital_assets_categoriesare searchable;digital_assets_tagsis not reachable from the portal. query_bycan only narrow the configured field set, never widen it; unknown fields are dropped and an empty result falls back to the full set. Weights are capped at 127.sort_byand the_evalband are shape-checked so caller text cannot inject arbitrary filter syntax into the sort.filterQueryis passed to Typesense unchanged and the route adds no instance clause. Instance scoping therefore depends on the client sendinginstance_visibility:=<brand id>.- Raw S3 keys never leave the server: a key that fails to sign is replaced with
''.
Related Documentation ​
- Typesense Utils - client, multi-search, S3 signing
- Search Fields - field specs, schema narrowing, relevance bands
- Search Feature - Search feature documentation
- Typesense Integration - Integration guide