Skip to content

@lastshotlabs/slingshot-search

npm install @lastshotlabs/slingshot-search

Create an Algolia search provider.

Communicates with the Algolia REST API over HTTPS using native fetch with configurable retry and exponential backoff. Supports full index management, document operations, search, suggest, multi-search, and health checks.

Remarks: Index naming — Algolia uses “index” directly. The indexName maps to the Algolia index name without transformation. The plugin-level indexPrefix is applied by the search manager before reaching this provider.

Remarks: Dual HTTP clients — two HTTP clients are created: one using adminApiKey (for index management and document writes) and one using apiKey (search-only key for read operations). This allows safe separation of write permissions from read permissions. When adminApiKey is not set, the search-only key is used for all operations.

Remarks: Attribute configcreateOrUpdateIndex() maps SearchIndexSettings to Algolia’s settings structure: - searchableFieldssearchableAttributes - filterableFields (not facetable) → filterOnly(field) inside attributesForFaceting (Algolia merges filterable and facetable) - facetableFields → plain entries in attributesForFaceting - sortableFields → added to attributesForFaceting as plain entries - excludedFieldsunretrievableAttributes

Remarks: Pagination — Algolia pages are 0-indexed internally. The provider transparently translates between the 1-indexed page in SearchQuery and Algolia’s 0-indexed page parameter, and back to 1-indexed in the response.

Remarks: Async task IDs — Algolia mutation operations return task IDs. The waitForTask method is a best-effort no-op because Algolia’s task status API requires the index name alongside the task ID, which is not available at the waitForTask call site. Mutations are typically propagated within a few hundred milliseconds.

Remarks: Filter syntaxSearchFilter ASTs are translated to Algolia filter strings via searchFilterToAlgoliaFilter(): - =field:"value" or field:number - IN(field:"a" OR field:"b") - BETWEENfield:min TO max - $geoRadiusaroundLatLng:lat,lng,aroundRadius:r - $geoBoundingBoxinsideBoundingBox:lat1,lng1,lat2,lng2 - STARTS_WITH: not natively supported; falls back to equality with a console.warn.

Remarks: API key requirements — read-only operations (search, suggest, multiSearch) use the search-only API key. Index management and document write operations require the admin API key. Without adminApiKey, all calls use apiKey — ensure that key has the required ACL permissions.

function createAlgoliaProvider(config: AlgoliaProviderConfig): SearchProvider

Source: packages/slingshot-search/src/providers/algolia.ts

Create an Elasticsearch (or OpenSearch-compatible) search provider.

Communicates with an Elasticsearch cluster over HTTP using native fetch with configurable retry and exponential backoff. Supports full index management, document operations, search, suggest, multi-search, and health checks.

Remarks: Index naming — Elasticsearch uses the term “index” directly. The indexName maps to the Elasticsearch index name without transformation. The plugin-level indexPrefix is applied by the search manager.

Remarks: Mapping strategycreateOrUpdateIndex() sends a PUT /<index> with full mapping. If the index already exists (HTTP 400), it falls back to PUT /<index>/_mapping to update mappings. Changing field types in an existing mapping is not permitted by Elasticsearch without a reindex.

Remarks: Field types — searchable fields are mapped as text with a keyword sub-field for exact matching and sorting. Filterable-only fields are mapped as keyword. This is a reasonable default; production deployments may need to override the mapping for numeric, date, or geo fields.

Remarks: Query DSLSearchFilter ASTs are translated to Elasticsearch query DSL via searchFilterToElasticsearchQuery(): - $andbool.filter - $orbool.should with minimum_should_match: 1 - $notbool.must_not - $geoRadiusgeo_distance query on _geo - $geoBoundingBoxgeo_bounding_box on _geo - CONTAINSmatch (full-text, not substring) - STARTS_WITHprefix query - IS_EMPTYbool.should[term:'', must_not[exists]]

Remarks: Authentication — supports HTTP basic auth ({ username, password }), Bearer token ({ bearer }), and ApiKey header. Pass via config.auth or config.apiKey.

Remarks: Bulk operationsindexDocuments() and deleteDocuments() use the /_bulk NDJSON endpoint. Multi-search uses /_msearch.

Remarks: Synchronous operations — Elasticsearch document writes are near-real-time but the HTTP response is synchronous. waitForTask is a no-op that immediately returns { status: 'succeeded' }.

Remarks: Geo coordinates — Elasticsearch uses { lat, lon } (not lng) in its query DSL. The provider translates _geo.lnglon when building geo queries, while the indexed field shape follows the provider-neutral _geo: { lat, lng } convention set by applyGeoTransform().

function createElasticsearchProvider(config: ElasticsearchProviderConfig): SearchProvider

Source: packages/slingshot-search/src/providers/elasticsearch.ts

Create an event-bus sync manager that keeps search indexes current by consuming entity CRUD events emitted by the framework event bus.

When an entity is configured with syncMode: 'event-bus', the search plugin creates one of these managers and calls subscribeConfigEntities() in its setupPost lifecycle phase. From that point the manager listens for entity:<storageName>.created, .updated, and .deleted events and forwards them to the appropriate search provider.

Batching — index operations are queued and flushed either on a timer (flushIntervalMs, default 5 s) or when the total pending queue reaches a threshold (flushThreshold, default 100 documents). Deletions bypass the batch queue and are flushed immediately to prevent stale results.

Idempotency — the pending queue is keyed by (indexName, documentId). If the same document is created/updated multiple times before a flush, only the most recent version is sent to the provider. This collapses rapid updates into a single index call.

Geo transforms — when the entity’s search config includes a geo field mapping, applyGeoTransform() is called before queuing the document so the provider receives the composite _geo: { lat, lng } field expected by Meilisearch and other providers.

Remarks: Eventual consistency — sync is asynchronous and not transactional. A document written to the primary store will be visible in search only after the next flush cycle completes. Under normal conditions this lag is at most flushIntervalMs milliseconds. If the provider is temporarily unavailable, the flush will log an error and emit a search:sync.failed event, but the pending queue entry is lost — there is no retry mechanism or dead-letter queue.

Remarks: Cross-app isolation — all state is closure-owned. Multiple calls to createEventSyncManager() return completely independent instances with no shared state.

Remarks: Entity deduplication — calling subscribeConfigEntity() for the same storage name more than once is a no-op (guarded by subscribedConfigEntities set).

function createEventSyncManager(config: EventSyncManagerConfig): EventSyncManager

Source: packages/slingshot-search/src/eventSync.ts

Create a durable, file-backed dead-letter store.

Entries are persisted as JSON-lines (one FlushDeadLetterEntry per line) so the file is human-readable and trivially greppable in production.

function createFileDlqStore(config: FileDlqStoreConfig): FileDlqStore

Source: packages/slingshot-search/src/eventSync.ts

Build the default in-memory rate-limit store. Each key tracks count and resetAt; entries past their reset time are reset on the next access.

The store does not actively GC stale keys — entries reset themselves when touched again. For a long-running process with many one-off keys this could grow unbounded; in practice tenant + IP cardinality is bounded for any single deployment, and operators who want hard limits should plug in a Redis-backed store.

function createInMemoryRateLimitStore(): RateLimitStore

Source: packages/slingshot-search/src/routes/rateLimiter.ts

Create a Hono middleware that enforces a per-(tenant, ip) request budget.

Returns 429 with a Retry-After header (seconds) when the bucket is full. Adds X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response, even successful ones, so clients can throttle pre-emptively.

Why a custom impl — the framework already has rate-limit infra in some packages but slingshot-search needs a small, dependency-free, injectable shape that mirrors the organizations plugin’s pattern. Shared abstraction is a non-goal until two packages agree on a single contract.

function createRateLimitMiddleware(options: RateLimitOptions = {}): void

Source: packages/slingshot-search/src/routes/rateLimiter.ts

Construct a search manager-level circuit breaker.

Tracks consecutive failures per configured provider. When the threshold is reached the breaker opens, causing all calls to that provider to fail fast with SearchCircuitOpenError. After cooldownMs a single half-open probe is admitted; success resets the breaker, failure re-opens it with a fresh cooldown.

function createSearchCircuitBreaker(opts: SearchCircuitBreakerOptions,): SearchCircuitBreaker

Source: packages/slingshot-search/src/searchCircuitBreaker.ts

Create the slingshot search package.

Provides config-driven indexing and querying for entities registered through the framework entity registry. The package itself owns no entities — it discovers them at boot via the registry — so the definePackage input has empty entities: [] and domains: [] arrays; all route mounting happens imperatively in setupRoutes.

function createSearchPackage(rawConfig: SearchPluginConfig, options?: { logger?: Logger },): SlingshotPackageDefinition

Source: packages/slingshot-search/src/plugin.ts

Create a Typesense search provider.

Communicates with a Typesense instance over HTTP using native fetch with configurable retry and exponential backoff. Supports full index management (collections), document operations, search, suggest, multi-search, and health checks.

Remarks: Collection naming — Typesense uses the term “collection” for what slingshot calls an “index”. The indexName passed to provider methods maps directly to the Typesense collection uid. The configured indexPrefix from the plugin config is applied by the search manager before reaching this provider, so the provider always receives the fully-prefixed name.

Remarks: Schema synccreateOrUpdateIndex() attempts to create the collection; if it already exists (HTTP 409), the collection is deleted and recreated. Typesense does not support in-place schema updates for field type changes. This means a schema change during a rolling deployment will briefly clear all documents — for production use, prefer additive schema changes or schedule reindexes alongside deploys.

Remarks: Filter syntaxSearchFilter ASTs are translated to Typesense filter_by strings via searchFilterToTypesenseFilter(): - Equality: field:=\value`- Range:field:>N, field:[min..max]- IN set:field:[a,b,c]- Geo radius:location:(lat, lng, radiusKm km)- Geo bounding box: approximated as a center + radius (Typesense does not natively support rectangular bounding boxes) -STARTS_WITH: not supported; falls back to equality with a console.warn`.

Remarks: Searchable fields — Typesense requires query_by to name the fields to search. The provider caches the collection’s string-type fields after the first createOrUpdateIndex() call and uses that list for every query. If the cache is empty, the provider falls back to '*'.

Remarks: Synchronous operations — all Typesense document and index mutations complete synchronously. waitForTask is a no-op that immediately returns { status: 'succeeded' }.

Remarks: Batch importindexDocuments() uses Typesense’s JSONL bulk import endpoint (/documents/import?action=upsert) for efficiency. Each document’s primary key is normalised to the string id field expected by Typesense.

function createTypesenseProvider(config: TypesenseProviderConfig): SearchProvider

Source: packages/slingshot-search/src/providers/typesense.ts

Returns true when the error represents a transient condition that should be retried.

Circuit-open errors (SearchCircuitOpenError) are excluded — the breaker handles those separately. Timeout, connection-refused, DNS, and server-error (5xx / 429) responses are treated as retriable.

function isTransientError(err: unknown): boolean

Source: packages/slingshot-search/src/retry.ts

Invoke fn with retries and exponential backoff.

Only transient errors (as defined by isTransientError) trigger a retry. Non-transient errors and circuit-open errors propagate immediately.

async function withRetry<T>(fn: () => Promise<T>, options?: Partial<RetryOptions>,): Promise<T>

Source: packages/slingshot-search/src/retry.ts

Provider-owned package contract for slingshot-search.

Source: packages/slingshot-search/src/public.ts

Named route group identifiers for the search plugin.

Pass values to SearchPluginConfig.disableRoutes to suppress specific route groups at startup.

Source: packages/slingshot-search/src/routes/index.ts

Capability handle for the search plugin runtime.

Cross-package consumers resolve it through ctx.capabilities.require(SearchRuntimeCap) to retrieve typed search clients and ensure config entities are indexed.

Source: packages/slingshot-search/src/public.ts

Structured error thrown when the circuit breaker is open. Callers can pattern-match on code === 'PROVIDER_UNAVAILABLE' to fail fast without waiting for the underlying request retries.

Source: packages/slingshot-search/src/providers/typesense.ts

Thrown when the breaker is open and refuses to invoke the provider.

retryAfterMs is the time remaining until the breaker enters half-open state. Workers can surface this as a backoff hint instead of treating the rejection as a generic transient failure.

Source: packages/slingshot-search/src/searchCircuitBreaker.ts

Thrown when search plugin configuration is invalid.

Source: packages/slingshot-search/src/errors/searchErrors.ts

Thrown for unsupported filter operators.

Source: packages/slingshot-search/src/errors/searchErrors.ts

Thrown for operations on non-existent search indexes.

Source: packages/slingshot-search/src/errors/searchErrors.ts

Thrown for pagination errors (e.g. offset limits exceeded).

Source: packages/slingshot-search/src/errors/searchErrors.ts

Thrown for provider-level failures.

Source: packages/slingshot-search/src/errors/searchErrors.ts

Thrown for duplicate or unknown transform handlers.

Source: packages/slingshot-search/src/errors/searchErrors.ts

Thrown for validation failures on search inputs.

Source: packages/slingshot-search/src/errors/searchErrors.ts

Configuration for the Algolia provider.

Source: packages/slingshot-search/src/types/provider.ts

Snapshot of the provider-level circuit breaker.

Source: packages/slingshot-search/src/providers/typesense.ts

Configuration for the DB-native provider.

Uses the app’s existing database (LIKE/ILIKE/full-text queries). No external service required. Recommended for development and low-traffic apps.

Source: packages/slingshot-search/src/types/provider.ts

Pluggable adapter for dead-letter persistence. The default implementation is in-memory and bounded; durable backends (Redis, Postgres) implement the same shape so DLQ entries survive process restarts.

All methods may throw — the event-sync manager treats DLQ-store failures as non-fatal (DLQ promotion is not aborted) and logs the failure via Logger.

Source: packages/slingshot-search/src/eventSync.ts

Configuration for the Elasticsearch/OpenSearch provider.

Source: packages/slingshot-search/src/types/provider.ts

Health snapshot exposed via EventSyncManager.getEventSyncHealth().

Source: packages/slingshot-search/src/eventSync.ts

Public interface for an event-bus sync manager instance.

Source: packages/slingshot-search/src/eventSync.ts

Configuration for createEventSyncManager().

Source: packages/slingshot-search/src/eventSync.ts

Per-facet display and sorting options for SearchQuery.facetOptions.

Controls how many values are returned per facet and how they are ordered.

Source: packages/slingshot-search/src/types/query.ts

Numeric statistics for a facetable numeric field.

Source: packages/slingshot-search/src/types/response.ts

A single index entry within a FederatedSearchQuery.

Inherits the shared query string from the parent FederatedSearchQuery but can override it, add index-specific filters, and control the relevance weight used in a 'weighted' merge.

Source: packages/slingshot-search/src/types/query.ts

A single hit from a federated search operation.

Extends SearchHit with the source index name and score details needed for weighted merge strategies.

Source: packages/slingshot-search/src/types/response.ts

Multi-index (federated) search query passed to SearchProvider-level federated search endpoints.

Queries multiple indexes simultaneously and merges results according to the configured strategy. Useful for searching across entity types (e.g. threads and users) in one request.

Source: packages/slingshot-search/src/types/query.ts

Response from a federated (multi-index) search operation.

Combines hits from multiple indexes according to the configured merge strategy. Per-index stats are available in indexes.

Source: packages/slingshot-search/src/types/response.ts

Durable DLQ store that persists entries to a JSON-lines file.

Entries are appended to the file immediately on put(). On construction (first put or getAll), any existing entries are reloaded from disk.

Supports replayDlq() which iterates each stored entry through a caller-provided handler and removes successfully handled entries from the file.

Source: packages/slingshot-search/src/eventSync.ts

Configuration for createFileDlqStore.

Source: packages/slingshot-search/src/eventSync.ts

A single dead-lettered op kept in memory after exhausting maxFlushAttempts.

Source: packages/slingshot-search/src/eventSync.ts

Configuration for in-result term highlighting.

When present on a SearchQuery, the provider wraps matched query terms in preTag/postTag HTML tags in the highlights map on each SearchHit.

Source: packages/slingshot-search/src/types/query.ts

Language and dictionary configuration for tokenization.

Source: packages/slingshot-search/src/types/provider.ts

Configuration for the Meilisearch provider.

Source: packages/slingshot-search/src/types/provider.ts

Configuration for createRateLimitMiddleware().

Source: packages/slingshot-search/src/routes/rateLimiter.ts

Persistence contract for the rate limiter — only one method, so any storage backend can implement it. The implementation must be reasonably atomic across concurrent calls; the default in-memory store relies on JavaScript’s single-threaded event loop, which is sufficient for one-process deployments.

Source: packages/slingshot-search/src/routes/rateLimiter.ts

Configuration for the exponential-backoff retry loop.

Source: packages/slingshot-search/src/retry.ts

Admin gate for search index management routes.

Controls access to admin endpoints (rebuild index, health check) and optionally logs audit entries for admin actions. Admin routes are only mounted when this is passed to createSearchPackage().

Remarks: Validated at plugin construction time via validateAdapterShape. The verifyRequest method must be present.

Source: packages/slingshot-search/src/types/config.ts

Runtime circuit breaker guarding search provider calls at the manager level.

Source: packages/slingshot-search/src/searchCircuitBreaker.ts

Snapshot of breaker state — useful for health endpoints and metrics.

Source: packages/slingshot-search/src/searchCircuitBreaker.ts

Tunable options used to construct a search manager-level circuit breaker.

Source: packages/slingshot-search/src/searchCircuitBreaker.ts

Logical AND combining multiple filter branches. All branches must match for a document to be included.

Source: packages/slingshot-search/src/types/query.ts

A single field-level filter condition.

Applies op between field and value. For BETWEEN, value must be a [min, max] tuple. For IN / NOT_IN, value must be an array.

Source: packages/slingshot-search/src/types/query.ts

Geo bounding box filter — matches documents within a rectangular area.

Source: packages/slingshot-search/src/types/query.ts

Geo-radius filter — matches documents within a circular area.

The document must have a geo-coordinate field (configured as filterable in the index settings) named _geo or a provider-specific equivalent.

Source: packages/slingshot-search/src/types/query.ts

Logical NOT inverting a single filter branch. Documents matching the inner filter are excluded.

Source: packages/slingshot-search/src/types/query.ts

Logical OR combining multiple filter branches. At least one branch must match for a document to be included.

Source: packages/slingshot-search/src/types/query.ts

Result of SearchProvider.healthCheck().

Source: packages/slingshot-search/src/types/provider.ts

A single search result hit.

@typeParam T — the document type. Defaults to Record<string, unknown>.

Source: packages/slingshot-search/src/types/response.ts

Index configuration settings passed to SearchProvider.createOrUpdateIndex().

Describes which fields are searchable, filterable, sortable, and facetable, plus optional ranking, typo tolerance, synonyms, and language configuration. Provider implementations map these settings to provider-specific index configs.

Source: packages/slingshot-search/src/types/provider.ts

Represents an asynchronous indexing task (Meilisearch, Algolia).

Returned by mutating operations. Use SearchProvider.waitForTask() to poll until the task completes.

Source: packages/slingshot-search/src/types/provider.ts

Top-level configuration for createSearchPackage().

All fields are readonly — the plugin treats the config as frozen after construction. Always pass to createSearchPackage() rather than storing a reference directly.

Remarks: Provider selection — at least one provider must be configured under a named key. The key 'default' is used when an entity’s search config does not explicitly name a provider. Multiple providers can be configured (e.g. one for fast entity types, another for heavyweight full-text indexes) and entities opt in via their search.provider field.

Remarks: Index prefixindexPrefix is prepended to every index name derived from an entity’s storage name or explicit search.indexName. Use it for environment isolation: staging_, test_, myapp_. The prefix is applied by the search manager before any provider sees the name.

Remarks: Tenant isolation — when both tenantResolver and tenantField are set, every search and suggest request is automatically scoped to the current tenant. The tenant ID is extracted from the Hono request context and injected as a filter condition before the query reaches the provider. Entity-level tenantIsolation: 'index-per-tenant' overrides this with index routing instead of filter injection.

Remarks: Transforms — named transform functions are registered at plugin construction and referenced by name in entity search configs (search: { transform: 'myTransform' }). The identity function is used when no transform is configured. Transforms run on every document before it is sent to the search provider.

Remarks: Admin routes — when adminGate is set, routes for index rebuild (POST /search/admin/indexes/:entity/rebuild) and health check (GET /search/admin/health) are mounted. All admin requests must pass adminGate.verifyRequest() or receive a 403.

Remarks: Route disabling — individual route groups can be disabled via disableRoutes. Valid values: 'search', 'suggest', 'federated', 'admin'.

Source: packages/slingshot-search/src/types/config.ts

Full-featured search provider. Extends the minimal SearchProviderContract (indexDocument + deleteDocument) with lifecycle, index management, batch operations, search, suggest, and task monitoring.

Each provider implementation (Meilisearch, Typesense, Elasticsearch, Algolia, DB-native) implements this interface. Obtain instances via the factory functions exported from the package (e.g. createTypesenseProvider()).

Remarks: Providers must be registered in SearchPluginConfig.providers by name. The search plugin calls connect() during setupPost and teardown() on graceful shutdown.

Source: packages/slingshot-search/src/types/provider.ts

Base configuration shared by all search provider configs.

Extended by each provider-specific config with required credentials.

Source: packages/slingshot-search/src/types/provider.ts

Full-featured search query passed to SearchProvider.search().

Supports full-text search, structured filters, multi-field sorting, faceted aggregation, highlighting, snippets, pagination (page-based or offset-based), and hybrid semantic/keyword search.

Source: packages/slingshot-search/src/types/query.ts

Custom relevance ranking configuration.

Defines the ordered list of ranking criteria applied during search scoring.

Source: packages/slingshot-search/src/types/provider.ts

The top-level response from a search operation.

@typeParam T — the document type. Defaults to Record<string, unknown>. Consumers can pass a concrete entity type for typed hit.document access.

Source: packages/slingshot-search/src/types/response.ts

Configuration for extracting contextual text snippets around matching terms.

When present on a SearchQuery, the provider returns short passages from each field in the snippets map on each SearchHit.

Source: packages/slingshot-search/src/types/query.ts

Autocomplete/suggestion query passed to SearchProvider.suggest().

Returns a short ordered list of candidate strings matching the prefix q. Typically used for real-time search-as-you-type UIs.

Source: packages/slingshot-search/src/types/query.ts

Response from a suggest/autocomplete operation.

The suggestions array is ordered by relevance score descending. Each entry contains the matched text and, when highlight was requested, an HTML-annotated version of that text.

Source: packages/slingshot-search/src/types/response.ts

A synonym group definition for query expansion.

Source: packages/slingshot-search/src/types/provider.ts

Configuration for the Typesense provider.

Source: packages/slingshot-search/src/types/provider.ts

Typo tolerance configuration for fuzzy matching.

Source: packages/slingshot-search/src/types/provider.ts

Discriminated union of all supported search provider configs.

Used as the value type in SearchPluginConfig.providers.

Source: packages/slingshot-search/src/types/provider.ts

Recursive search filter expression.

Compose conditions with $and, $or, $not, geo-radius, and geo-bounding-box operators. All leaves are SearchFilterCondition nodes.

Source: packages/slingshot-search/src/types/query.ts

Comparison operator for SearchFilterCondition.

  • '=' / '!=' — exact match / negation
  • '>' / '>=' / '<' / '<=' — range comparisons
  • 'IN' / 'NOT_IN' — set membership (value must be an array)
  • 'EXISTS' / 'NOT_EXISTS' — field presence check
  • 'CONTAINS' — substring match
  • 'BETWEEN' — range (value must be [min, max] tuple)
  • 'STARTS_WITH' — prefix match
  • 'IS_EMPTY' / 'IS_NOT_EMPTY' — null/empty check

Source: packages/slingshot-search/src/types/query.ts

Value type accepted in SearchFilterCondition.value.

readonly [number, number] is used with the BETWEEN operator.

Source: packages/slingshot-search/src/types/query.ts

A single ranking criterion.

Built-in rules: 'words', 'typo', 'proximity', 'attribute', 'sort', 'exactness'. Custom rules specify a field and direction.

Source: packages/slingshot-search/src/types/provider.ts

Union type of valid search route group names.

Source: packages/slingshot-search/src/routes/index.ts

Sort criterion for SearchQuery.sort.

Either sort by a named field (ascending/descending) or by geo-distance from a center point (ascending = nearest first).

Source: packages/slingshot-search/src/types/query.ts