Skip to content

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 keyEnv name in error messagesRule
typesenseHostTYPESENSE_HOSTrequired
typesensePortTYPESENSE_PORTrequired, integer 1-65535
typesenseProtocolTYPESENSE_PROTOCOLrequired
typesenseApiKeyTYPESENSE_API_KEYrequired
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 paramSource
query_byrequest.query_by, else commonSearchParams.query_by if it is a string, else 'search_name'
per_page, pagecommonSearchParams
sort_byrequest.sort_by, else commonSearchParams.sort_by, else ''
text_match_typecommonSearchParams.text_match_type when set
query_by_weights, filter_by, include_fields, group_by, group_limit, infixrequest value, only when truthy
prefix, num_typos, typo_tokens_threshold, prioritize_exact_match, prioritize_token_positionrequest value, only when not undefined (so num_typos: 0 is sent)

Returns:

  • The Typesense multi-search response; results[i] corresponds to searchRequests[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:

  1. Hits are read from data.grouped_hits[].hits when grouping was used, else data.hits; the document is hit.document or the hit itself.
  2. isRawS3Path(value) is true only for strings that do not start with http:///https:// and match ^\d+\/digital_assets\/ (for example 12345/digital_assets/thumbnail/abc.jpg). Already signed URLs are left alone.
  3. All unique keys across all collections are signed in parallel with getCachedSignedUrl.
  4. A key that fails to sign is logged and replaced with '', so a raw key never reaches the client.
  5. preview_images is parsed, each entry gets url set to the signed key URL (or its existing url, or null), and the array is re-serialised to a JSON string so consumers keep the original field type.
  6. When no keys are found, results is 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:

SettingValue
Cache TTL23 h (CACHE_TTL), one hour less than the URL lifetime so a cached URL is never expired
Max entries10,000 (CACHE_MAX_SIZE); when full, the oldest entry is evicted
Eviction sweepevery hour; the timer is stored on globalThis.__s3_cache_eviction_timer so HMR does not start duplicates
LRUa 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 ​

ExportPurpose
S3_PATH_FIELDSlist 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 token
  • workspaceId?: string - Optional workspace ID to verify access

Returns:

  • string - Workspace unique ID if user has access
  • number - 0 if 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 ​

  1. Error Handling: Always handle TypesenseConnectionError separately from other errors
  2. Retry Logic: The client includes built-in retry logic (3 retries with 0.1s interval)
  3. Connection Timeout: Configure appropriate timeout based on network conditions
  4. Health Checks: Client performs health checks every 30 seconds
  5. Token Verification: Always verify user tokens before performing searches
  6. Workspace Access: Check workspace access before allowing search operations
  7. Lazy Init: Call getTypesenseClient() in new code; typesenseClient is deprecated