A lightweight, zero-dependency vector search engine built entirely in TypeScript. Add semantic search to any Deno or TypeScript project in minutes — no databases, no external services, no infrastructure.
Quick Start · API Reference · Powers (Plugins) · Getting Started Guide
Most vector search solutions require you to stand up Postgres + pgvector, run a Docker container for Qdrant/Milvus, or pay for a hosted service. MySSE is the opposite: import a single module and you have a working semantic search engine running entirely in-process — no network, no disk, no YAML.
| Consideration | MySSE | Typical alternatives |
|---|---|---|
| Setup | import { SemanticEngine } from … |
Docker / cloud provisioning |
| External services | None | PostgreSQL, Redis, vector DB |
| Dependencies | Zero | Dozens to hundreds |
| Language | Pure TypeScript | C++/Rust with JS bindings |
| Runtime | Deno (secure by default) | Node.js / mixed |
| Latency (10k docs) | ~1.5 ms (HNSW) / ~5 ms (brute-force) | Varies by network round-trip |
| Embedding models | Pluggable — swap at runtime | Locked at build time |
If you need a self-contained, hackable search engine that you can embed in a CLI tool, microservice, RAG pipeline, or Deno Deploy function — and you value simplicity over cluster-scale — MySSE is built for you.
- Why MySSE?
- Features
- Quick Start
- API Reference
- Architecture
- How It Works
- Powers (Plugin System)
- Performance Benchmarks
- Tasks
- Use Cases
- Roadmap
- Getting Started Guide
- Contributing
- License
- 100% In-Memory Vector Search — No external database, no disk I/O during
queries. All vectors live in RAM as
Float32Arrayfor cache-friendly access. - HNSW Approximate Nearest Neighbor Index — Pure-TypeScript implementation of the 2016 Malkov & Yashunin paper. Automatic approximate nearest-neighbor search above 2 000 documents, exact brute-force below.
- Adaptive Search Strategy — Brute-force (recall = 100%) when the index is small; HNSW (recall@10 ≥ 92%, 4–6× faster) when it grows — fully automatic, no config needed.
- Pluggable Embedding Models — Ship with a deterministic hash-based embedder for zero-setup dev. Hot-swap to Ollama, Transformers.js, OpenAI, or any custom model at runtime via the EmbeddingSwap Power.
- BM25 + Semantic Hybrid Search — Built-in Reciprocal Rank Fusion (RRF) blends keyword and dense retrieval for stronger results out of the box.
- Powers Plugin System — Lifecycle hooks for caching, hybrid search, metadata filtering, and custom embedding models — without touching core code.
- Pure TypeScript, Zero Dependencies — No native bindings, no WASM, no node_modules. Ships as a single JSR package.
- Deno Secure-by-Default — Runs under Deno's permission system. No accidental file/network access.
- ~1 000 Lines of Code — ~720 LOC core engine + ~310 LOC Powers. Read the whole thing in an afternoon.
deno add jsr:@wxt/my-search-engineOr add it manually to your import map:
import { SemanticEngine, HybridSearch } from "@wxt/my-search-engine";
const engine = SemanticEngine.getInstance();
engine.use(HybridSearch());
await engine.add([
{ id: "1", content: "Deno is a secure runtime for JS and TS" },
{ id: "2", content: "Fresh is a modern web framework for Deno" },
]);
const results = await engine.search("secure typescript runtime");
console.log(results);- Deno 2.0 or later
# Clone the repository
git clone https://github.com/thewizster/MySSE.git
cd MySSE
# Start the development server
deno task devThe server will start at http://localhost:8000 with a built-in search UI.
POST /api/add
Content-Type: application/json
# Single document
{"id": "doc1", "content": "Your document text", "metadata": {"source": "example"}}
# Multiple documents
[
{"id": "doc1", "content": "First document text"},
{"id": "doc2", "content": "Second document text"}
]Example:
curl -X POST http://localhost:8000/api/add \
-H "Content-Type: application/json" \
-d '[
{"id":"1", "content":"Deno is a secure runtime for JavaScript and TypeScript", "metadata":{"source":"docs"}},
{"id":"2", "content":"Fresh is a modern web framework for Deno", "metadata":{"source":"docs"}},
{"id":"3", "content":"Transformers.js runs ML models in the browser", "metadata":{"source":"docs"}}
]'GET /api/search?q=<query>&k=<top_k>Parameters:
q(required): The search queryk(optional): Number of results to return (default: 10, max: 100)
Example:
curl "http://localhost:8000/api/search?q=secure+typescript+runtime&k=5"GET /api/statusReturns the current engine status and document count.
DELETE /api/clearRemoves all documents from the index.
MySSE/
├── lib/
│ ├── hnsw.ts # HNSW approximate nearest-neighbor index (~210 LOC)
│ ├── semantic-engine.ts # Core semantic search engine (~510 LOC)
│ └── powers/ # Extensibility plugins
│ ├── cache.ts # QueryCache — search result caching
│ ├── embedding-swap.ts # EmbeddingSwap — hot-swap embedding models
│ ├── hybrid-search.ts # HybridSearch — BM25 + semantic RRF fusion
│ └── metadata-filter.ts # MetadataFilter — filter results by metadata
├── tests/
│ ├── hnsw_test.ts # HNSW unit tests (19 tests)
│ ├── semantic-engine_test.ts # Engine unit tests (7 tests)
│ ├── semantic-engine-ann_test.ts # ANN integration tests (8 tests)
│ └── powers_test.ts # Powers unit tests (20 tests)
├── main.ts # HTTP server with routing & UI
├── deno.json # Deno configuration
└── README.md
- Document Ingestion: Documents are embedded into 384-dimensional vectors
- Normalization: All vectors are pre-normalized to unit length
- Storage: Embeddings stored as
Float32Arrayfor cache-friendly access - Indexing: Vectors are inserted into an HNSW graph (built incrementally on
add()) - Search: Under 2000 docs → exact brute-force dot product; above → HNSW approximate search with O(log n) query time
The HNSW implementation follows the original 2016 paper:
- Level multiplier:
mL = 1/ln(M)— ~94% of nodes on layer 0 - Two-phase insert: greedy walk (ef = 1) on upper layers, then efConstruction-width search + bidirectional connect on lower layers
- Neighbor shrinkage: connections pruned when exceeding M_max (2·M on layer 0)
- Configurable: M, efConstruction, and efSearch exposed as optional parameters
The engine uses a pluggable embedding interface:
- SimpleEmbeddingModel (default): Hash-based embeddings for zero-dependency operation. Great for development and testing.
- Ollama + nomic-embed-text (recommended): Run nomic-embed-text locally via Ollama for production-quality 768-dim embeddings. Scores jump to the 0.6–0.95 range with much stronger semantic matching.
- TransformersJsEmbedding: Swap in
@huggingface/transformersfor in-process ML embeddings without a separate server.
Swap the model at runtime using the EmbeddingSwap Power — no restart needed:
# Install Ollama (https://ollama.com), then pull the model:
ollama pull nomic-embed-textimport { engine } from "./lib/semantic-engine.ts";
import { EmbeddingSwap } from "./lib/powers/embedding-swap.ts";
engine.use(EmbeddingSwap(async (texts) => {
const res = await fetch("http://localhost:11434/api/embed", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "nomic-embed-text", input: texts }),
});
const { embeddings } = await res.json();
return embeddings.map((e: number[]) => new Float32Array(e));
}));All indexing and searching will now use nomic-embed-text. See the
Getting Started guide for a full walkthrough.
Powers are MySSE's extensibility mechanism. A Power is a plain object with
lifecycle hooks that the engine calls at key points in the search pipeline.
Register a Power with engine.use(), remove it with engine.eject(). When no
Powers are registered there is zero overhead.
The engine runs each hook in registration order at the appropriate point:
| Hook | When it runs | Typical use |
|---|---|---|
beforeAdd |
Before documents are embedded | Transform or enrich documents |
afterAdd |
After documents are stored and indexed | Build auxiliary indexes (e.g. BM25) |
beforeSearch |
Before the core search runs | Rewrite queries, return cached results |
afterSearch |
After results are returned | Re-rank, filter, or fuse results |
embed |
Replaces the default embedding model | Plug in a real ML model at runtime |
onDelete |
After a document is deleted | Clean up auxiliary state |
onClear |
After the entire index is cleared | Reset auxiliary state |
MySSE ships with four ready-to-use Powers in lib/powers/:
Caches search results by query string. Repeated identical queries are served instantly from memory.
import { QueryCache } from "./lib/powers/cache.ts";
engine.use(QueryCache()); // defaults: 100 entries, 60 s TTL
engine.use(QueryCache({ maxSize: 200, ttl: 30_000 })); // custom settingsCombines dense semantic retrieval with sparse BM25 keyword retrieval using Reciprocal Rank Fusion (RRF). This gives you the best of both worlds — semantic understanding and exact keyword matching.
import { HybridSearch } from "./lib/powers/hybrid-search.ts";
engine.use(HybridSearch()); // 50/50 blend (recommended)
engine.use(HybridSearch({ alpha: 0.7 })); // 70% semantic, 30% keywordalpha value |
Behaviour |
|---|---|
1.0 |
Pure semantic (dense only) |
0.5 |
Equal blend (default) |
0.0 |
Pure keyword (BM25 only) |
Filters search results by document metadata using a predicate function.
import { MetadataFilter } from "./lib/powers/metadata-filter.ts";
// Only return published documents
engine.use(MetadataFilter((meta) => meta.published === true));
// Only return documents from a specific source
engine.use(MetadataFilter((meta) => meta.source === "docs"));Hot-swaps the embedding model at runtime without restarting the engine or changing constructor options.
import { EmbeddingSwap } from "./lib/powers/embedding-swap.ts";
engine.use(EmbeddingSwap(async (texts) => {
// Call your ML model, external API, etc.
return texts.map(() => new Float32Array(384).fill(0.1));
}));Powers compose naturally. Register as many as you need — they run in order:
import { QueryCache } from "./lib/powers/cache.ts";
import { HybridSearch } from "./lib/powers/hybrid-search.ts";
import { MetadataFilter } from "./lib/powers/metadata-filter.ts";
engine.use(QueryCache({ ttl: 30_000 }));
engine.use(HybridSearch({ alpha: 0.6 }));
engine.use(MetadataFilter((meta) => meta.published === true));With this setup, each search: checks the cache → runs semantic + BM25 fusion → filters by metadata → caches the result for next time.
// Register a Power
engine.use(QueryCache());
// List registered Powers
console.log(engine.powers); // ["QueryCache"]
// Remove a Power by name
engine.eject("QueryCache"); // returns true if found and removed
// The /api/status endpoint also reports active Powers
curl http://localhost:8000/api/status
// { "status": "healthy", "documents": 42, "powers": ["QueryCache"], ... }A Power is any object that satisfies the Power interface. At minimum it needs
a name; every hook is optional — implement only what you need.
import type { Power } from "./lib/semantic-engine.ts";
const logger: Power = {
name: "Logger",
beforeSearch(ctx) {
console.log(`Searching for: ${ctx.query}`);
return ctx;
},
afterSearch(results, query) {
console.log(`Found ${results.length} results for "${query}"`);
return results;
},
};
engine.use(logger);The engine supports exporting and importing its full state for persistence
(e.g. to Deno KV or a JSON file). The HNSW index is rebuilt automatically on
import, and afterAdd hooks fire so Powers like HybridSearch can rebuild
auxiliary state.
// Export
const snapshot = engine.toJSON();
await Deno.writeTextFile("index.json", JSON.stringify(snapshot));
// Import
const data = JSON.parse(await Deno.readTextFile("index.json"));
await engine.fromJSON(data);Benchmarked on a single thread with 10 000 documents and 384-dimensional vectors:
| Metric | Value |
|---|---|
| Brute-force query | ~5 ms per query |
| HNSW query | ~1.5 ms per query (4–6× faster) |
| HNSW recall@10 | ≥ 92% (typically 95–97%) |
| Index build (10k docs) | ~20 s (one-time, incremental on add) |
| Memory per vector | ~1.5 KB (384 × 4 bytes + graph edges) |
Tip: For real-world latency, pair MySSE with the
QueryCachePower so repeated queries resolve in <0.1 ms.
deno task dev # Start development server with hot reload
deno task start # Start production server
deno task check # Run format, lint, and type checks
deno task test # Run all 54 tests (engine, HNSW, Powers)MySSE is a great fit anywhere you need fast, embedded semantic search without infrastructure overhead:
- RAG (Retrieval-Augmented Generation) — Retrieve relevant context from your knowledge base before prompting an LLM. MySSE works as the retrieval layer in any RAG pipeline.
- Help Center / FAQ Search — Let users ask questions in natural language and surface the right article even when wording doesn't match.
- Chatbot Context Engine — Feed relevant documents to a conversational AI so it can answer grounded in your data.
- CLI & Desktop Tools — Embed search directly in a Deno CLI app or Electron tool — no server required.
- Note & Knowledge Base Search — Import Markdown or text files and find content by meaning, not just keywords.
- Product Search — Let customers describe what they want ("lightweight running shoes") instead of navigating filter trees.
- Deno Deploy Functions — Stand up a serverless semantic search endpoint with zero infrastructure.
- Prototyping & Hackathons — Get semantic search working in minutes, then swap in a production model when you're ready.
- Transformers.js Integration — Enable real in-process ML embeddings with
@huggingface/transformers(or use the EmbeddingSwap Power today) - WebGPU Acceleration — Automatic GPU acceleration when available
- Multi-Modal Search — Swap to CLIP model for image+text embeddings via EmbeddingSwap
- Quantization —
quantized: truefor smaller model footprint - Persistence Adapters — Expand
toJSON/fromJSONwith streaming and Deno KV adapters - Community Powers — Build and share your own Powers — logging, rate-limiting, A/B testing, result enrichment, and more
New to semantic search? The Getting Started guide walks you through everything from installing Deno to adding documents, running queries, swapping embedding models, and using Powers — no ML background needed.
Contributions are welcome! Whether it's bug fixes, new Powers, documentation improvements, or performance optimizations — open an issue or submit a pull request on GitHub.
# Clone and run the test suite
git clone https://github.com/thewizster/MySSE.git
cd MySSE
deno task test # 54 tests across engine, HNSW, and PowersMIT License — see LICENSE for details.

