Appearance
Typesense Utilities ​
Overview ​
Server-only helpers behind POST /api/typesense/search: a lazily created Typesense client, performMultiSearch with connection-error classification, S3 signing of file fields in search hits, and a legacy verifyUserToken helper.
File Information ​
- Path:
server/api/typesense/utils.ts - Used by:
server/api/typesense/search.post.ts(performMultiSearch,signS3PathsInResults,SearchRequest),server/utils/searchSchema.ts(getTypesenseClient) - Dependencies:
typesense,h3,@aws-sdk/client-s3,@aws-sdk/s3-request-presigner
Typesense Client ​
Client Configuration ​
The client is created on first use by getTypesenseClient(), not at import time, so the Nuxt server can start without Typesense configuration (for example under PM2 without an .env). The first search then fails with a clear message instead of the process failing to boot.
getTypesenseConfig() reads useRuntimeConfig():
| Runtime config key | Env name in error messages | Rule |
|---|---|---|
typesenseHost | TYPESENSE_HOST | required |
typesensePort | TYPESENSE_PORT | required, integer 1-65535 |
typesenseProtocol | TYPESENSE_PROTOCOL | required |
typesenseApiKey | TYPESENSE_API_KEY | required |
typesenseConnectionTimeout | - | seconds, default 10, non-numeric falls back to 10 |
Missing keys produce one error listing all of them: Typesense is not configured. Missing: TYPESENSE_HOST, ... Set in .env or NUXT_ env at build/runtime. A bad port produces Typesense invalid TYPESENSE_PORT: "<value>". Must be 1-65535.
Fixed client options: one node, retryIntervalSeconds: 0.1, numRetries: 3, healthcheckIntervalSeconds: 30.
typesenseClient is still exported as a Proxy that forwards every property to getTypesenseClient(); it is marked @deprecated and kept for compatibility. performMultiSearch uses it internally.
Error Handling ​
TypesenseConnectionError ​
Custom error class for Typesense connection issues:
typescript
export class TypesenseConnectionError extends Error {
public originalError: any
public status: number // Always 503 (Service Unavailable)
}When Thrown:
- Connection refused errors (
ECONNREFUSED) - Connection reset errors (
ECONNRESET) - Timeout errors (
ETIMEDOUT) - HTTP 502, 503, 504 status codes
- Typesense service unavailable
Usage:
typescript
try {
await performMultiSearch(searches, params)
} catch (error) {
if (error instanceof TypesenseConnectionError) {
// Handle connection error
console.error('Typesense unavailable:', error.message)
}
}Functions ​
performMultiSearch ​
Sends all per-collection requests in one multiSearch.perform call.
Signature:
typescript
export const performMultiSearch = async (
searchRequests: SearchRequest[],
commonSearchParams: CommonSearchParams
)Parameters:
SearchRequest:
typescript
export interface SearchRequest {
collection: string
q: string
query_by?: string
query_by_weights?: string
filter_by?: string
sort_by?: string
include_fields?: string[]
group_by?: string
group_limit?: number
infix?: string
prefix?: boolean
num_typos?: number
typo_tokens_threshold?: number
prioritize_exact_match?: boolean
prioritize_token_position?: boolean
}CommonSearchParams:
typescript
interface CommonSearchParams {
query_by: string | Record<string, string>
query_by_weights?: string | Record<string, unknown> // carried through, not read here
infix?: string | Record<string, unknown> // carried through, not read here
per_page: number
page: number
sort_by?: string
text_match_type?: string // max_score | max_weight | sum_score
}How each search is assembled:
| Typesense param | Source |
|---|---|
query_by | request.query_by, else commonSearchParams.query_by if it is a string, else 'search_name' |
per_page, page | commonSearchParams |
sort_by | request.sort_by, else commonSearchParams.sort_by, else '' |
text_match_type | commonSearchParams.text_match_type when set |
query_by_weights, filter_by, include_fields, group_by, group_limit, infix | request value, only when truthy |
prefix, num_typos, typo_tokens_threshold, prioritize_exact_match, prioritize_token_position | request value, only when not undefined (so num_typos: 0 is sent) |
Returns:
- The Typesense multi-search response;
results[i]corresponds tosearchRequests[i].
Error Handling:
Usage:
typescript
const response = await performMultiSearch(
[
{
collection: 'digital_assets',
q: 'brand',
query_by: 'search_name,tags,portal_custom_field_values,description',
query_by_weights: '10,6,4,1',
infix: 'always,fallback,fallback,off',
filter_by: '(instance_visibility:=42 && instance_visibility:!=0)',
sort_by: '_text_match:desc,modified_at:desc',
prefix: true,
num_typos: 0,
},
],
{ query_by: 'search_name', per_page: 50, page: 1, text_match_type: 'max_score' }
)signS3PathsInResults ​
Typesense documents store file fields as raw S3 keys. Before the search route returns, this function replaces them with signed GET URLs so the browser can load thumbnails and previews without bucket access.
Signature:
typescript
export const signS3PathsInResults = async (results: SearchResults): Promise<SearchResults>Fields checked on every hit (S3_PATH_FIELDS): thumbnail_file, compress_file, preview_image, preview_file, video_preview, display_file, plus each key inside the JSON string preview_images.
Rules:
- Hits are read from
data.grouped_hits[].hitswhen grouping was used, elsedata.hits; the document ishit.documentor the hit itself. isRawS3Path(value)is true only for strings that do not start withhttp:///https://and match^\d+\/digital_assets\/(for example12345/digital_assets/thumbnail/abc.jpg). Already signed URLs are left alone.- All unique keys across all collections are signed in parallel with
getCachedSignedUrl. - A key that fails to sign is logged and replaced with
'', so a raw key never reaches the client. preview_imagesis parsed, each entry getsurlset to the signed key URL (or its existingurl, ornull), and the array is re-serialised to a JSON string so consumers keep the original field type.- When no keys are found,
resultsis returned untouched.
getCachedSignedUrl ​
typescript
export const getCachedSignedUrl = async (key: string): Promise<string>Signs with getSignedUrl(GetObjectCommand) and expiresIn: 86_400 (24 h). Signed URLs are cached in memory:
| Setting | Value |
|---|---|
| Cache TTL | 23 h (CACHE_TTL), one hour less than the URL lifetime so a cached URL is never expired |
| Max entries | 10,000 (CACHE_MAX_SIZE); when full, the oldest entry is evicted |
| Eviction sweep | every hour; the timer is stored on globalThis.__s3_cache_eviction_timer so HMR does not start duplicates |
| LRU | a cache hit is deleted and re-inserted to move it to the end |
The S3 client is created lazily from runtime config (awsAccessKeyId, awsSecretAccessKey, awsDefaultRegion, awsBucket, awsUseAccelerate === 'true' for the accelerate endpoint). Missing values fail with S3 is not configured. Missing: AWS_ACCESS_KEY_ID, ....
Helpers ​
| Export | Purpose |
|---|---|
S3_PATH_FIELDS | list of document fields that may hold raw S3 keys |
isRawS3Path(value) | type guard described above |
parsePreviewImages(value) | parses the preview_images JSON string; returns the array or null on non-string, parse error or non-array |
verifyUserToken ​
Verifies user authentication token and workspace access. Not called by any route on origin/development; search.post.ts performs its own check against accessibleInstances. This helper instead looks up accessibleWorkspaces by id and returns that workspace's workspace_unique_id.
Signature:
typescript
export const verifyUserToken = async (
token: string,
workspaceId?: string
): Promise<string | number>Parameters:
token: string- Authentication tokenworkspaceId?: string- Optional workspace ID to verify access
Returns:
string- Workspace unique ID if user has accessnumber-0if user doesn't have access or workspace not found
Throws:
createError({ statusCode: 401 })- If token is invalid or unauthorized
Usage:
typescript
try {
const workspaceUniqueId = await verifyUserToken(token, workspaceId)
if (workspaceUniqueId) {
// User has access to workspace
console.log('Workspace ID:', workspaceUniqueId)
} else {
// User doesn't have access
console.log('Access denied')
}
} catch (error) {
// Token invalid or unauthorized
console.error('Token verification failed')
}Error Handling Patterns ​
Connection Errors ​
typescript
try {
await performMultiSearch(searches, params)
} catch (error: any) {
if (error.code === 'ECONNREFUSED' ||
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT') {
throw new TypesenseConnectionError(
'Unable to connect to Typesense server',
error
)
}
}Typesense API Errors ​
typescript
try {
await performMultiSearch(searches, params)
} catch (error: any) {
if (error.name === 'TypesenseError' ||
error.name === 'RequestError' ||
error.name === 'ServerError') {
if (error.message.includes('connection') ||
error.message.includes('timeout')) {
throw new TypesenseConnectionError(
'Failed to establish connection with Typesense',
error
)
}
throw new Error(`Typesense Error: ${error.message}`)
}
}HTTP Response Errors ​
typescript
try {
await performMultiSearch(searches, params)
} catch (error: any) {
if (error.response) {
if ([502, 503, 504].includes(error.response.status)) {
throw new TypesenseConnectionError(
'Typesense service is temporarily unavailable',
error
)
}
throw new Error(
`Typesense Error: ${error.response.data.message || 'Unknown error'}`
)
}
}Best Practices ​
- Error Handling: Always handle
TypesenseConnectionErrorseparately from other errors - Retry Logic: The client includes built-in retry logic (3 retries with 0.1s interval)
- Connection Timeout: Configure appropriate timeout based on network conditions
- Health Checks: Client performs health checks every 30 seconds
- Token Verification: Always verify user tokens before performing searches
- Workspace Access: Check workspace access before allowing search operations
- Lazy Init: Call
getTypesenseClient()in new code;typesenseClientis deprecated
Related Documentation ​
- Typesense Search API - Search endpoint documentation
- Search Fields - Field specs, schema narrowing and relevance bands
- Search Feature - Search feature documentation
- Typesense Integration - Integration guide