Hybrid Search

medium15 minhybrid-search.ts

Keyword search misses paraphrases and vector search misses exact terms. You want both, weighted.

What it shows

  • Rank with BM25Index alone
  • Embed notes with GoogleEmbeddingService into InMemoryEmbeddingAdapter
  • Compare keyword, vector and hybrid with HybridSearch

The code

hybrid-search.ts
import { BM25Index, GoogleEmbeddingService, HybridSearch, InMemoryEmbeddingAdapter } from '@cogitator-ai/memory';

const apiKey = process.env.GOOGLE_API_KEY;
if (!apiKey) throw new Error('Set GOOGLE_API_KEY');

const notes = [
  { id: 'note-1', content: 'TypeScript 5.4 introduces the NoInfer utility type for generic functions.' },
  { id: 'note-2', content: 'Bun is a JavaScript runtime built on JavaScriptCore with fast startup times.' },
  { id: 'note-3', content: 'PostgreSQL 16 adds the pg_stat_io view for detailed I/O statistics.' },
  { id: 'note-4', content: 'The CAP theorem: a distributed system guarantees two of consistency, availability, partition tolerance.' },
  { id: 'note-5', content: 'Rust ownership eliminates data races at compile time; the borrow checker prevents use-after-free.' },
];

const bm25 = new BM25Index();
for (const note of notes) bm25.addDocument(note);
console.log('BM25:', bm25.search('type inference', 2).map((hit) => hit.content));

const embeddingService = new GoogleEmbeddingService({ apiKey, model: 'gemini-embedding-001' });
const embeddingAdapter = new InMemoryEmbeddingAdapter();
for (const note of notes) {
  await embeddingAdapter.addEmbedding({
    sourceId: note.id,
    sourceType: 'document',
    content: note.content,
    vector: await embeddingService.embed(note.content),
  });
}

const search = new HybridSearch({
  embeddingAdapter,
  embeddingService,
  defaultWeights: { bm25: 0.4, vector: 0.6 },
});
for (const note of notes) search.indexDocument(note.id, note.content);

const query = 'memory safety without garbage collection';
for (const strategy of ['keyword', 'vector', 'hybrid'] as const) {
  const result = await search.search({ query, strategy, limit: 2, threshold: 0.3 });
  if (!result.success) continue;
  console.log(strategy, result.data.map((hit) => `[${hit.score.toFixed(3)}] ${hit.content.slice(0, 50)}`));
}

Run it

install · bash
pnpm add @cogitator-ai/memory
EnvironmentGOOGLE_API_KEY
run · bash
GOOGLE_API_KEY=your-key npx tsx hybrid-search.ts

Or run the full example from a clone of the repository (keys in .env):

repository · bash
npx tsx examples/memory/03-semantic-search.ts

Learn more