Overview
Orama’s plugin system allows you to extend and customize search behavior by hooking into lifecycle events. This guide will teach you how to write your own plugins from scratch.Plugin Structure
A plugin is an object that implements theOramaPluginSync interface:
import { OramaPluginSync } from '@orama/orama'
const myPlugin: OramaPluginSync = {
name: 'my-custom-plugin',
// Optional hooks
afterCreate: (orama) => { /* ... */ },
beforeInsert: (orama, id, doc) => { /* ... */ },
afterInsert: (orama, id, doc) => { /* ... */ },
beforeSearch: (orama, params, language) => { /* ... */ },
afterSearch: (orama, params, language, results) => { /* ... */ }
// ... more hooks
}
Available Hooks
Based on/home/daytona/workspace/source/packages/orama/src/types.ts:1372-1406:
Document Lifecycle
Insert Hooks
Insert Hooks
beforeInsert?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
afterInsert?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
beforeInsertMultiple?: <T extends AnyOrama>(
orama: T,
docs: AnyDocument[]
) => SyncOrAsyncValue
afterInsertMultiple?: <T extends AnyOrama>(
orama: T,
docs: AnyDocument[]
) => SyncOrAsyncValue
Update Hooks
Update Hooks
beforeUpdate?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
afterUpdate?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
beforeUpdateMultiple?: <T extends AnyOrama>(
orama: T,
docs: AnyDocument[]
) => SyncOrAsyncValue
afterUpdateMultiple?: <T extends AnyOrama>(
orama: T,
docs: AnyDocument[]
) => SyncOrAsyncValue
Remove Hooks
Remove Hooks
beforeRemove?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
afterRemove?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
beforeRemoveMultiple?: <T extends AnyOrama>(
orama: T,
ids: string[]
) => SyncOrAsyncValue
afterRemoveMultiple?: <T extends AnyOrama>(
orama: T,
ids: string[]
) => SyncOrAsyncValue
Upsert Hooks
Upsert Hooks
beforeUpsert?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
afterUpsert?: <T extends AnyOrama>(
orama: T,
id: string,
doc: AnyDocument
) => SyncOrAsyncValue
beforeUpsertMultiple?: <T extends AnyOrama>(
orama: T,
docs: AnyDocument[]
) => SyncOrAsyncValue
afterUpsertMultiple?: <T extends AnyOrama>(
orama: T,
docs: AnyDocument[]
) => SyncOrAsyncValue
Search Hooks
beforeSearch?: <T extends AnyOrama>(
orama: T,
params: SearchParams<T>,
language: string | undefined
) => SyncOrAsyncValue
afterSearch?: <T extends AnyOrama>(
orama: T,
params: SearchParams<T>,
language: string | undefined,
results: Results<TypedDocument<T>>
) => SyncOrAsyncValue
Instance Hooks
afterCreate?: <T extends AnyOrama>(orama: T) => SyncOrAsyncValue
beforeLoad?: <T extends AnyOrama>(orama: T) => SyncOrAsyncValue
afterLoad?: <T extends AnyOrama>(orama: T) => SyncOrAsyncValue
Basic Plugin Example
Logging Plugin
Log all operations for debugging:import { create, insert, search } from '@orama/orama'
const loggingPlugin = {
name: 'logging-plugin',
afterInsert: (orama, id, doc) => {
console.log(`[INSERT] Document ${id}:`, doc)
},
beforeSearch: (orama, params) => {
console.log('[SEARCH] Query:', params)
},
afterSearch: (orama, params, language, results) => {
console.log(`[SEARCH] Found ${results.count} results in ${results.elapsed.formatted}`)
}
}
const db = await create({
schema: {
title: 'string',
description: 'string'
},
plugins: [loggingPlugin]
})
await insert(db, { title: 'Test', description: 'A test document' })
// [INSERT] Document 1: { title: 'Test', description: 'A test document' }
await search(db, { term: 'test' })
// [SEARCH] Query: { term: 'test' }
// [SEARCH] Found 1 results in 2ms
Advanced Plugin Examples
Validation Plugin
Validate documents before insertion:function createValidationPlugin(schema) {
return {
name: 'validation-plugin',
beforeInsert: (orama, id, doc) => {
// Validate required fields
for (const [field, type] of Object.entries(schema)) {
if (!(field in doc)) {
throw new Error(`Missing required field: ${field}`)
}
const actualType = typeof doc[field]
if (type === 'string' && actualType !== 'string') {
throw new Error(`Field ${field} must be a string, got ${actualType}`)
}
}
}
}
}
// Usage
const db = await create({
schema: {
title: 'string',
price: 'number'
},
plugins: [
createValidationPlugin({
title: 'string',
price: 'number'
})
]
})
// This will throw an error
try {
await insert(db, { title: 'Product' }) // Missing 'price'
} catch (error) {
console.error(error.message) // "Missing required field: price"
}
Timestamp Plugin
Automatically add timestamps to documents:const timestampPlugin = {
name: 'timestamp-plugin',
beforeInsert: (orama, id, doc) => {
doc.createdAt = new Date().toISOString()
doc.updatedAt = new Date().toISOString()
},
beforeUpdate: (orama, id, doc) => {
doc.updatedAt = new Date().toISOString()
}
}
const db = await create({
schema: {
title: 'string',
createdAt: 'string',
updatedAt: 'string'
},
plugins: [timestampPlugin]
})
await insert(db, { title: 'Article 1' })
// Document now has createdAt and updatedAt fields
Cache Plugin
Cache search results for better performance:function createCachePlugin(ttl = 60000) {
const cache = new Map()
return {
name: 'cache-plugin',
beforeSearch: (orama, params) => {
const key = JSON.stringify(params)
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('Cache hit!')
// Return cached results (need to modify search flow)
params._cached = cached.results
}
},
afterSearch: (orama, params, language, results) => {
if (!params._cached) {
const key = JSON.stringify(params)
cache.set(key, {
results,
timestamp: Date.now()
})
}
},
// Clean up expired cache entries periodically
afterCreate: (orama) => {
setInterval(() => {
const now = Date.now()
for (const [key, value] of cache.entries()) {
if (now - value.timestamp >= ttl) {
cache.delete(key)
}
}
}, ttl)
}
}
}
const db = await create({
schema: { title: 'string' },
plugins: [createCachePlugin(30000)] // 30 second cache
})
Analytics Plugin
Track search metrics:function createAnalyticsPlugin() {
const analytics = {
totalSearches: 0,
totalResults: 0,
searchTerms: new Map(),
avgResponseTime: 0
}
return {
name: 'analytics-plugin',
extra: { analytics },
afterSearch: (orama, params, language, results) => {
analytics.totalSearches++
analytics.totalResults += results.count
// Track search terms
const term = params.term || ''
analytics.searchTerms.set(
term,
(analytics.searchTerms.get(term) || 0) + 1
)
// Update average response time
const responseTime = results.elapsed.raw / 1_000_000 // ns to ms
analytics.avgResponseTime =
(analytics.avgResponseTime * (analytics.totalSearches - 1) + responseTime) /
analytics.totalSearches
},
// Add method to get stats
getStats: () => ({
...analytics,
topSearchTerms: Array.from(analytics.searchTerms.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
})
}
}
const analyticsPlugin = createAnalyticsPlugin()
const db = await create({
schema: { title: 'string' },
plugins: [analyticsPlugin]
})
// After some searches
await search(db, { term: 'test' })
await search(db, { term: 'example' })
await search(db, { term: 'test' })
// Get analytics
console.log(analyticsPlugin.getStats())
// {
// totalSearches: 3,
// totalResults: 15,
// avgResponseTime: 2.5,
// topSearchTerms: [['test', 2], ['example', 1]]
// }
Asynchronous Plugins
Plugins can be async and perform async operations:function createEmbeddingsPlugin(apiKey) {
return {
name: 'embeddings-plugin',
beforeInsert: async (orama, id, doc) => {
// Generate embeddings asynchronously
const response = await fetch('https://api.example.com/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: doc.text })
})
const { embeddings } = await response.json()
doc.embeddings = embeddings
},
beforeSearch: async (orama, params) => {
if (params.mode === 'vector' && params.term) {
const response = await fetch('https://api.example.com/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: params.term })
})
const { embeddings } = await response.json()
params.vector = {
property: 'embeddings',
value: embeddings
}
}
}
}
}
const db = await create({
schema: {
text: 'string',
embeddings: 'vector[384]'
},
plugins: [createEmbeddingsPlugin('your-api-key')]
})
Plugin Factory Pattern
Create configurable plugins using factory functions:function createTransformPlugin(config) {
const {
fields = [],
transform = (value) => value,
onInsert = true,
onUpdate = true
} = config
const applyTransform = (doc) => {
for (const field of fields) {
if (field in doc) {
doc[field] = transform(doc[field])
}
}
}
return {
name: 'transform-plugin',
beforeInsert: onInsert ? (orama, id, doc) => {
applyTransform(doc)
} : undefined,
beforeUpdate: onUpdate ? (orama, id, doc) => {
applyTransform(doc)
} : undefined
}
}
// Usage: Lowercase all titles
const lowercasePlugin = createTransformPlugin({
fields: ['title', 'category'],
transform: (value) => value.toLowerCase()
})
// Usage: Trim whitespace
const trimPlugin = createTransformPlugin({
fields: ['description'],
transform: (value) => value.trim()
})
const db = await create({
schema: {
title: 'string',
category: 'string',
description: 'string'
},
plugins: [lowercasePlugin, trimPlugin]
})
Plugin with Extra Data
Store plugin-specific data using theextra property:
function createCounterPlugin() {
const stats = {
inserts: 0,
updates: 0,
removes: 0,
searches: 0
}
return {
name: 'counter-plugin',
extra: { stats },
afterInsert: () => { stats.inserts++ },
afterUpdate: () => { stats.updates++ },
afterRemove: () => { stats.removes++ },
afterSearch: () => { stats.searches++ }
}
}
const counterPlugin = createCounterPlugin()
const db = await create({
schema: { title: 'string' },
plugins: [counterPlugin]
})
// Access plugin data
const plugin = db.plugins.find(p => p.name === 'counter-plugin')
console.log(plugin.extra.stats)
// { inserts: 5, updates: 2, removes: 1, searches: 10 }
TypeScript Support
Create fully typed plugins:import { OramaPluginSync, AnyOrama, AnyDocument } from '@orama/orama'
interface ValidationConfig {
required?: string[]
custom?: (doc: AnyDocument) => void
}
interface ValidationPluginExtra {
config: ValidationConfig
errors: Array<{ id: string; error: string }>
}
function createValidationPlugin(
config: ValidationConfig
): OramaPluginSync<ValidationPluginExtra> {
const errors: Array<{ id: string; error: string }> = []
return {
name: 'validation-plugin',
extra: { config, errors },
beforeInsert: <T extends AnyOrama>(orama: T, id: string, doc: AnyDocument) => {
// Check required fields
for (const field of config.required || []) {
if (!(field in doc)) {
const error = `Missing required field: ${field}`
errors.push({ id, error })
throw new Error(error)
}
}
// Custom validation
if (config.custom) {
try {
config.custom(doc)
} catch (error) {
errors.push({ id, error: (error as Error).message })
throw error
}
}
}
}
}
// Usage with full type safety
const plugin = createValidationPlugin({
required: ['title', 'price'],
custom: (doc) => {
if (typeof doc.price === 'number' && doc.price < 0) {
throw new Error('Price cannot be negative')
}
}
})
Testing Plugins
import { test } from 'node:test'
import assert from 'node:assert'
import { create, insert, search } from '@orama/orama'
test('logging plugin logs operations', async () => {
const logs = []
const loggingPlugin = {
name: 'test-logging',
afterInsert: (orama, id, doc) => {
logs.push({ type: 'insert', id, doc })
},
afterSearch: (orama, params, lang, results) => {
logs.push({ type: 'search', count: results.count })
}
}
const db = await create({
schema: { title: 'string' },
plugins: [loggingPlugin]
})
await insert(db, { title: 'Test' })
assert.equal(logs.length, 1)
assert.equal(logs[0].type, 'insert')
await search(db, { term: 'test' })
assert.equal(logs.length, 2)
assert.equal(logs[1].type, 'search')
})
Best Practices
1. Keep Plugins Focused
Each plugin should have a single, clear purpose:// ✅ Good - focused on one task
const timestampPlugin = { /* adds timestamps */ }
const validationPlugin = { /* validates data */ }
// ❌ Bad - does too many things
const megaPlugin = { /* timestamps, validation, logging, analytics */ }
2. Use Factory Functions
Make plugins configurable:// ✅ Good - configurable
function createPlugin(options) {
return { name: 'plugin', /* ... */ }
}
// ❌ Bad - hardcoded behavior
const plugin = { name: 'plugin', /* hardcoded */ }
3. Handle Errors Gracefully
const safePlugin = {
name: 'safe-plugin',
afterInsert: (orama, id, doc) => {
try {
// Plugin logic
} catch (error) {
console.error('Plugin error:', error)
// Don't throw - let insert succeed
}
}
}
4. Document Your Plugin
/**
* Creates a plugin that normalizes text fields
*
* @param {Object} options - Plugin options
* @param {string[]} options.fields - Fields to normalize
* @param {boolean} options.lowercase - Convert to lowercase
* @param {boolean} options.trim - Trim whitespace
* @returns {OramaPluginSync} The plugin instance
*
* @example
* const plugin = createNormalizePlugin({
* fields: ['title', 'description'],
* lowercase: true,
* trim: true
* })
*/
function createNormalizePlugin(options) {
// ...
}
5. Performance Considerations
Minimize work in hot paths:// ✅ Good - minimal overhead
const fastPlugin = {
name: 'fast',
afterInsert: (orama, id, doc) => {
doc.indexed = true // Quick operation
}
}
// ❌ Bad - heavy operation in hot path
const slowPlugin = {
name: 'slow',
afterInsert: async (orama, id, doc) => {
await fetch('...') // Network call on every insert!
}
}
Publishing Your Plugin
If you create a useful plugin, consider publishing it:Package Structure
my-orama-plugin/
├── src/
│ └── index.ts
├── dist/
│ ├── index.js
│ └── index.d.ts
├── package.json
├── README.md
└── tsconfig.json
package.json
{
"name": "@yourname/orama-plugin-example",
"version": "1.0.0",
"description": "Example plugin for Orama",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": ["orama", "plugin", "search"],
"peerDependencies": {
"@orama/orama": "^2.0.0"
}
}
Next Steps
Plugin Overview
Learn more about the plugin system
Official Plugins
Explore official Orama plugins
API Reference
Explore the Orama API reference
Examples
View plugin examples on GitHub