Skip to content

Adding Search

slingshot-search adds full-text entity search to any Slingshot app. It plugs into the entity model so your entities are indexed automatically as they’re created and updated.

Terminal window
bun add @lastshotlabs/slingshot-search
app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';
import { createSearchPackage } from '@lastshotlabs/slingshot-search';
export default defineApp({
plugins: [
createAuthPlugin({
auth: { roles: ['user'], defaultRole: 'user' },
db: { auth: 'memory', sessions: 'memory', oauthState: 'memory' },
}),
],
packages: [
createSearchPackage({
providers: {
default: { provider: 'db-native' },
},
}),
],
});

Add a search block to any entity to make it indexable. Then declare a search operation to expose a search endpoint:

import { defineEntity, defineOperations, field, op } from '@lastshotlabs/slingshot';
export const Post = defineEntity('Post', {
namespace: 'blog',
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
body: field.string(),
tags: field.string({ optional: true }),
status: field.enum(['draft', 'published'] as const),
},
search: {
fields: {
title: { searchable: true, weight: 3 },
body: { searchable: true },
tags: { searchable: true },
status: { filterable: true },
},
syncMode: 'event-bus', // keep the index in sync via CRUD events
},
routes: {
defaults: { auth: 'none' },
},
});
// The search endpoint lives in operations, not in the entity routes block
export const postOperations = defineOperations(Post, {
search: op.search({
fields: ['title', 'body'],
filter: { status: 'published' }, // only surface published posts
paginate: true,
}),
});

The operation’s filter config limits results to published posts. The search plugin keeps the index in sync as entities are created or updated via syncMode: 'event-bus'.

Under the hood that sync path now depends on registry-backed event definitions. Search consumers should publish through ctx.events.publish(...) or entity route config, not through ad hoc raw bus payloads or legacy client-safe registration.

Terminal window
# Basic search
curl "http://localhost:3000/posts/search?q=slingshot"
# With pagination
curl "http://localhost:3000/posts/search?q=slingshot&page=2&limit=20"
# Filtered (additional filters beyond the entity-level filter)
curl "http://localhost:3000/posts/search?q=slingshot&authorId=abc123"

Response:

{
"results": [{ "id": "...", "title": "Getting started with Slingshot", "score": 0.92 }],
"total": 14,
"page": 1,
"pages": 2
}

Register a global search endpoint that queries across multiple entity types:

createSearchPackage({
adapter: 'memory',
globalSearch: {
enabled: true,
path: '/search',
types: ['post', 'thread', 'reply'],
},
});
Terminal window
curl "http://localhost:3000/search?q=authentication&types=post,thread"

Swap the adapter without changing any entity definitions or query code:

Terminal window
bun add meilisearch
createSearchPackage({
adapter: 'meilisearch',
url: 'http://localhost:7700',
apiKey: process.env.MEILI_API_KEY!,
});

If you add search to an existing app, trigger a one-time re-index:

Terminal window
slingshot reindex --entity post
slingshot reindex --all