Orama
"Orama: client-side/edge full-text search engine, TypeScript-native, schema, search with filters, vector search, runs in browser/Node/edge"
Orama is a full-text and vector search engine written in TypeScript that runs everywhere JavaScript runs — browsers, Node.js, Deno, Bun, Cloudflare Workers, and edge functions. Its design principles are:
## Key Points
- **Zero infrastructure** — no servers to manage, no external services to call. The search index lives in the same runtime as your application.
- **TypeScript-native** — the API is fully typed. Schemas are defined in TypeScript and the search results preserve your document types.
- **Instant startup** — indices can be created from data in memory or deserialized from a snapshot in milliseconds.
- **Hybrid search** — full-text search and vector search in the same engine, with a single unified API.
- **Tiny footprint** — the core library is under 10 KB gzipped. It adds negligible overhead to a frontend bundle.
- **Use `insertMultiple` for bulk loading.** It is significantly faster than calling `insert` in a loop because it batches internal index updates.
- **Persist and restore indices** instead of rebuilding them on every page load. Use `save`/`load` with localStorage, IndexedDB, or a CDN-hosted JSON file.
- **Set `threshold` for typo tolerance control.** A value of 1.0 means exact matches only. Lower values (0.6-0.8) allow more typos but may reduce precision.
- **Use `enum` or `enum[]` for categorical data** rather than `string`. Enums are stored more efficiently and support exact filtering.
- **Pre-build the index at build time** for static sites. Run a build script that creates and serializes the index, then load it on the client.
- **Use `boost` to weight important fields.** Title and name fields should typically get a 2-3x boost over body/description fields.
- **Keep vector dimensions reasonable.** Larger vectors consume more memory. Use dimensionality reduction if running in the browser with constrained memory.
## Quick Example
```typescript
import { insertMultiple } from "@orama/orama";
async function bulkIndex(products: Product[]) {
await insertMultiple(productDB, products, 500); // batch size of 500
}
```skilldb get search-services-skills/OramaFull skill: 393 linesOrama
Core Philosophy
Orama is a full-text and vector search engine written in TypeScript that runs everywhere JavaScript runs — browsers, Node.js, Deno, Bun, Cloudflare Workers, and edge functions. Its design principles are:
- Zero infrastructure — no servers to manage, no external services to call. The search index lives in the same runtime as your application.
- TypeScript-native — the API is fully typed. Schemas are defined in TypeScript and the search results preserve your document types.
- Instant startup — indices can be created from data in memory or deserialized from a snapshot in milliseconds.
- Hybrid search — full-text search and vector search in the same engine, with a single unified API.
- Tiny footprint — the core library is under 10 KB gzipped. It adds negligible overhead to a frontend bundle.
Orama is ideal for client-side search (documentation sites, static sites, offline-capable apps), edge search (Cloudflare Workers, Vercel Edge Functions), and embedded search in Electron or React Native apps.
Setup
Install and create a typed index:
// npm install @orama/orama
import { create, insert, search, count } from "@orama/orama";
// Define your schema
const productDB = await create({
schema: {
name: "string",
description: "string",
price: "number",
categories: "enum[]",
rating: "number",
inStock: "boolean",
tags: "string[]",
} as const,
});
// Insert documents
async function indexProducts(products: Product[]) {
for (const product of products) {
await insert(productDB, {
name: product.name,
description: product.description,
price: product.price,
categories: product.categories,
rating: product.rating,
inStock: product.inStock,
tags: product.tags,
});
}
console.log(`Indexed ${await count(productDB)} documents`);
}
Batch insert for better performance:
import { insertMultiple } from "@orama/orama";
async function bulkIndex(products: Product[]) {
await insertMultiple(productDB, products, 500); // batch size of 500
}
Key Techniques
Full-Text Search with Filters
async function searchProducts(query: string, filters?: ProductFilters) {
const results = await search(productDB, {
term: query,
properties: ["name", "description"], // fields to search
boost: { name: 2 }, // name matches rank higher
limit: 20,
offset: 0,
threshold: 0.8, // typo tolerance (0-1)
where: filters ? buildWhereClause(filters) : undefined,
});
return {
hits: results.hits.map((h) => ({
document: h.document,
score: h.score,
})),
count: results.count,
elapsed: results.elapsed,
};
}
function buildWhereClause(filters: ProductFilters) {
const where: Record<string, any> = {};
if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
where.price = {
...(filters.minPrice !== undefined && { gte: filters.minPrice }),
...(filters.maxPrice !== undefined && { lte: filters.maxPrice }),
};
}
if (filters.categories?.length) {
where.categories = { containsAll: filters.categories };
}
if (filters.inStock !== undefined) {
where.inStock = filters.inStock;
}
if (filters.minRating !== undefined) {
where.rating = { gte: filters.minRating };
}
return where;
}
interface ProductFilters {
minPrice?: number;
maxPrice?: number;
categories?: string[];
inStock?: boolean;
minRating?: number;
}
Faceted Search
async function facetedSearch(query: string) {
const results = await search(productDB, {
term: query,
properties: ["name", "description"],
facets: {
categories: {
limit: 10,
order: "DESC",
},
price: {
ranges: [
{ from: 0, to: 25 },
{ from: 25, to: 100 },
{ from: 100, to: 500 },
{ from: 500, to: Infinity },
],
},
rating: {
ranges: [
{ from: 4, to: 5 },
{ from: 3, to: 4 },
{ from: 0, to: 3 },
],
},
inStock: {
true: true,
false: true,
},
},
});
return {
hits: results.hits,
facets: results.facets,
};
}
Vector Search
import { create, insert, search } from "@orama/orama";
// Create a database with vector fields
const vectorDB = await create({
schema: {
title: "string",
content: "string",
embedding: "vector[1536]", // OpenAI ada-002 dimensions
} as const,
});
// Insert documents with embeddings
async function indexWithEmbeddings(docs: DocWithEmbedding[]) {
for (const doc of docs) {
await insert(vectorDB, {
title: doc.title,
content: doc.content,
embedding: doc.embedding, // number[] of length 1536
});
}
}
// Pure vector search
async function vectorSearch(queryEmbedding: number[]) {
return search(vectorDB, {
mode: "vector",
vector: {
value: queryEmbedding,
property: "embedding",
},
limit: 10,
similarity: 0.8, // minimum similarity threshold
});
}
// Hybrid search: full-text + vector combined
async function hybridSearch(query: string, queryEmbedding: number[]) {
return search(vectorDB, {
mode: "hybrid",
term: query,
vector: {
value: queryEmbedding,
property: "embedding",
},
properties: ["title", "content"],
limit: 10,
});
}
Serialization and Persistence
import { save, load } from "@orama/orama";
// Serialize the index to JSON
async function persistIndex() {
const snapshot = await save(productDB);
const json = JSON.stringify(snapshot);
// Store anywhere: localStorage, file, KV store, S3
localStorage.setItem("search-index", json);
// or: await fs.writeFile("index.json", json);
// or: await env.KV.put("search-index", json);
}
// Restore from snapshot
async function restoreIndex() {
const json = localStorage.getItem("search-index");
if (!json) return null;
const snapshot = JSON.parse(json);
const db = await create({
schema: {
name: "string",
description: "string",
price: "number",
categories: "enum[]",
rating: "number",
inStock: "boolean",
tags: "string[]",
} as const,
});
await load(db, snapshot);
return db;
}
React Integration
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { create, insert, search, type Orama } from "@orama/orama";
function useOramaSearch(db: Orama | null) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<any[]>([]);
useEffect(() => {
if (!db) return;
const timer = setTimeout(async () => {
if (!query.trim()) {
setResults([]);
return;
}
const res = await search(db, {
term: query,
properties: ["name", "description"],
limit: 20,
threshold: 0.8,
});
setResults(res.hits.map((h) => h.document));
}, 150); // debounce
return () => clearTimeout(timer);
}, [db, query]);
return { query, setQuery, results };
}
function SearchComponent({ products }: { products: Product[] }) {
const [db, setDb] = useState<Orama | null>(null);
useEffect(() => {
(async () => {
const database = await create({
schema: {
name: "string",
description: "string",
price: "number",
} as const,
});
for (const p of products) {
await insert(database, p);
}
setDb(database);
})();
}, [products]);
const { query, setQuery, results } = useOramaSearch(db);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
<ul>
{results.map((r: any) => (
<li key={r.name}>{r.name} — ${r.price}</li>
))}
</ul>
</div>
);
}
Edge/Worker Deployment
// Cloudflare Worker example
import { create, insertMultiple, search, load, save } from "@orama/orama";
interface Env {
SEARCH_INDEX: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/search") {
const query = url.searchParams.get("q") ?? "";
const cached = await env.SEARCH_INDEX.get("index", "json");
const db = await create({
schema: { title: "string", body: "string", url: "string" } as const,
});
if (cached) {
await load(db, cached as any);
}
const results = await search(db, {
term: query,
properties: ["title", "body"],
limit: 10,
});
return Response.json({
hits: results.hits.map((h) => h.document),
count: results.count,
});
}
return new Response("Not found", { status: 404 });
},
};
Best Practices
- Use
insertMultiplefor bulk loading. It is significantly faster than callinginsertin a loop because it batches internal index updates. - Persist and restore indices instead of rebuilding them on every page load. Use
save/loadwith localStorage, IndexedDB, or a CDN-hosted JSON file. - Set
thresholdfor typo tolerance control. A value of 1.0 means exact matches only. Lower values (0.6-0.8) allow more typos but may reduce precision. - Use
enumorenum[]for categorical data rather thanstring. Enums are stored more efficiently and support exact filtering. - Pre-build the index at build time for static sites. Run a build script that creates and serializes the index, then load it on the client.
- Use
boostto weight important fields. Title and name fields should typically get a 2-3x boost over body/description fields. - Keep vector dimensions reasonable. Larger vectors consume more memory. Use dimensionality reduction if running in the browser with constrained memory.
Anti-Patterns
- Using Orama as a server-side database replacement. It is designed for search, not as a persistent data store. Keep a real database as the source of truth.
- Rebuilding the index on every request in serverless. Serialize the index once and load from cache (KV, R2, Redis) on cold starts.
- Indexing HTML or Markdown markup. Strip tags before indexing. Raw markup hurts relevance and produces ugly highlights.
- Ignoring bundle size in the browser. While Orama itself is small, large datasets serialized as JSON can bloat initial page loads. Lazy-load the index after the page renders.
- Using vector search without understanding dimensionality. Mismatched vector dimensions between indexed documents and queries cause runtime errors. Always validate consistency.
- Skipping debounce on keystroke search. Searching on every keystroke without a debounce floods the engine with queries, especially with large indices.
- Storing sensitive data in client-side indices. The entire index is in memory and inspectable via DevTools. Never index private or access-controlled data on the client.
Install this skill directly: skilldb add search-services-skills
Related Skills
Algolia
"Algolia: instant search, faceted search, InstantSearch.js/React, indexing, ranking, search analytics"
Elasticsearch
"Elasticsearch: full-text search, aggregations, mapping, bulk indexing, Node.js client, relevance tuning"
Fuse Js
Fuse.js is a lightweight, powerful fuzzy-search library for JavaScript that runs entirely client-side. It's ideal for quickly adding flexible, typo-tolerant search capabilities to web applications without server-side infrastructure.
Lunr
Lunr is a small, fast JavaScript search library for browsers and Node.js. It allows you to build a search index directly within your application, providing full-text search capabilities without a backend API or external service. It's ideal for static sites, documentation, or client-side applications requiring offline-capable search.
Manticore Search
"Manticore Search: open-source full-text search, SQL-based queries, real-time indexes, columnar storage, Elasticsearch-compatible API"
Meilisearch
"Meilisearch: self-hosted search engine, typo tolerance, faceting, filtering, sorting, REST API, JavaScript SDK"