Search is the backbone of any knowledge tool. If you cannot find what you saved, the capture pipeline is pointless. Yet most note-taking apps in 2026 still ship with one of two search approaches — keyword or semantic — and pretend it is sufficient. It is not. This is the story of why Glassy uses both, and how Reciprocal Rank Fusion makes them work together.
The two search paradigms
BM25 (keyword search)
BM25 is the algorithm that powered web search for two decades. It tokenizes documents into words, builds an inverted index, and ranks results by term frequency and inverse document frequency. If you search for "OAuth2," BM25 finds every document containing the exact string "OAuth2."
Strengths: Exact matching. Code identifiers, proper nouns, error messages, API names — BM25 catches them all with high precision. It is fast, well-understood, and implemented in SQLite FTS5, which means it runs on any device with no external dependencies.
Weaknesses: No semantic understanding. "OAuth2" will not match a note about "token-based authentication" unless the exact word appears. BM25 is blind to synonyms, paraphrasing, and conceptual relationships.
Vector search (semantic search)
Vector search converts text into high-dimensional embeddings — arrays of floating-point numbers that capture semantic meaning. Notes about similar topics produce similar vectors, even if they use completely different words. Glassy uses mxbai/MiniLM to generate 384-dimensional vectors locally in your browser via WebGPU. See our post on WebGPU in 2026 for the technical background.
Strengths: Conceptual matching. "Transformer architecture" finds notes about "attention mechanisms" even if the word "transformer" never appears. It handles synonyms, paraphrasing, and cross-language content gracefully.
Weaknesses: Misses exact matches. If you search for a specific error code like "ECONNREFUSED 127.0.0.1:3000," a vector model may return notes about network errors in general but miss the one note that contains the exact string. Vector search also requires more compute — generating embeddings for thousands of notes takes time and memory.
Why neither is enough
Consider a real query: "glassy mcp rate limit." A user searching for this might want:
- A note titled "MCP server configuration" that contains the exact phrase "rate limit" — BM25 catches this
- A note titled "API throttling and quotas" that discusses rate limiting without using those exact words — Vector search catches this
- A note with the code snippet
MCP_FREE_TOOLCALLS_PER_HOUR=120— BM25 catches this - A note about "usage caps for AI agents" that never mentions MCP by name — Vector search catches this
A BM25-only search returns results 1 and 3. A vector-only search returns results 2 and 4. Neither returns all four. The user misses half the relevant content either way.
Reciprocal Rank Fusion
The naive approach to hybrid search is to combine the scores — add BM25 score to vector similarity score and sort by the sum. This does not work. BM25 scores are unbounded positive numbers. Vector cosine similarities are between -1 and 1. Adding them is like adding temperature in Celsius to weight in kilograms — the scales are incomparable.
Reciprocal Rank Fusion (RRF) solves this by ignoring scores entirely. Instead, it works with ranks:
- Run BM25 search. Rank results 1, 2, 3, ... by BM25 score.
- Run vector search. Rank results 1, 2, 3, ... by cosine similarity.
- For each result, compute a fused score:
score = 1 / (k + rank_bm25) + 1 / (k + rank_vector) - Sort by fused score. The constant k (typically 60) dampens the advantage of being ranked first in either list.
The result is a ranking that benefits from both paradigms. A document that ranks high in both BM25 and vector search gets a strong fused score. A document that ranks first in BM25 but 50th in vector search gets a moderate score. This is exactly what we want — documents that are relevant by both keyword and semantic criteria should rank highest.
Glassy's implementation
Glassy runs hybrid search on every query — whether it comes from the web UI, the MCP server, or an AI agent. The pipeline:
- BM25 index — SQLite FTS5 with tokenizer configured for Markdown content. Updated on every note/bookmark write. Runs server-side.
- Vector index — 384-dimensional embeddings from mxbai/MiniLM, generated locally in the browser via WebGPU. Stored in IndexedDB, namespaced by model and dimension. See our post on privacy-first AI for why this matters.
- Fusion — RRF with k=60, applied server-side. The MCP server returns fused results with source URLs and relevance indicators.
Why this matters for AI agents
When Claude, Cursor, Windsurf, OpenCode, Hermes, Openclaw, Pi, or any MCP-compatible AI agent queries your knowledge base via MCP, it sends a natural language query. That query might be "what do I know about authentication?" — a semantic query where vector search shines. Or it might be "find notes mentioning ECONNREFUSED" — a keyword query where BM25 is essential. Hybrid search handles both without the AI agent needing to specify which mode to use.
This is why hybrid search is not a luxury feature. It is the foundation that makes the research capture loop work. Without it, the AI agent misses half the relevant content, and the citations it returns are incomplete.