02 / 06MiniGoogleJava 21 · no framework
How MiniGoogle raised retrieval quality to 0.75 NDCG@10 with hybrid ranking over BM25 alone
A search engine you run yourself, with sharding, gossip and Raft replication, Java 21
- NDCG@10, hybrid ranking
- 0.75
- Source:
Benchmark.md, 2026-08-15

The problem
A search engine's real mechanics, an inverted index, BM25 scoring, link based authority, consistent hashing, gossip membership, log replication, are usually hidden behind a database or a managed service before an engineer gets to build them by hand. I wanted to build all of it myself: one process by default, several when I choose, without Lucene, Elasticsearch or Kafka doing the interesting part for me. I built MiniGoogle, a search engine you run yourself: crawl pages into it, query them over HTTP or the bundled web UI, and, when you need it, run several nodes that place documents by consistent hashing, track each other by gossip and replicate a key value store by Raft. Java 21, one jar, one Docker image.
The mechanism
A STANDALONE node does everything in one JVM. RestServer, built on com.sun.net.httpserver with no framework underneath, exposes POST /api/v1/crawl and POST /api/v1/search. A crawl request runs through com.minigoogle.crawler, its CrawlWorker and jsoup based parser, appends the parsed page to a JSONL file and rebuilds com.minigoogle.indexer's inverted index. A search request reaches com.minigoogle.query's lexer, parser and executor, retrieves candidate postings, and hands them to com.minigoogle.ranking, which scores by BM25, blends in PageRank and, with semantic search on by default, merges lexical and vector candidates before a learning to rank model reorders the top results.
Running as CLUSTER adds three subsystems beside that same index and API. com.minigoogle.cluster.ConsistentHashRing decides which nodes own a crawled document. com.minigoogle.cluster.GossipProtocol keeps membership current: a silent peer becomes SUSPECT, then DEAD, and leaves the ring. com.minigoogle.cluster.RaftConsensus replicates the cluster's key value store through leader election, AppendEntries and snapshots, and never reads the ring or membership once its own configuration is committed.
| Metric | BM25 only | Hybrid + LTR |
|---|---|---|
| NDCG@10 | 0.6929 | 0.7477 |
| MAP | 0.2750 | 0.7877 |
| Standalone search latency | Before | After |
|---|---|---|
| p50 | 36.26 ms | 4.98 ms to 9.59 ms |
| p99 | 62.70 ms to 63.79 ms | 14.82 ms to 32.66 ms |
Retrieval quality rows: seed 42, 320 document corpus, 16 judged queries. Latency rows: a separate 3200 document corpus, 500 queries after a 100 iteration warmup, on one otherwise idle developer machine. Both from Benchmark.md, measured 2026-08-15.
What it did
On the 320 document seeded corpus, turning on hybrid retrieval and the learning to rank reranker moved NDCG@10 from 0.6929 to 0.7477 and MAP from 0.2750 to 0.7877 against the same 16 judged queries. On the separate 3200 document corpus used for timing, standalone search latency dropped from a p50 of 36.26 ms to a p50 between 4.98 ms and 9.59 ms, and p99 from a range of 62.70 ms to 63.79 ms down to 14.82 ms to 32.66 ms, after a 100 iteration warmup.
The honest caveat, stated in Benchmark.md itself: this is a seeded 320 document corpus with 16 judged queries on one idle developer machine, not a public BEIR leaderboard number, and the latency rows are described in the same document as bimodal and range based rather than single deterministic values. The document's own README also carries a second, larger relevance run: pure BM25 on BEIR scifact, 5,183 documents and 300 judged queries, reaching NDCG@10 of 0.6746, a separate measurement on a different corpus that I am not blending into the headline number above.
What I learned
The gap between the quality numbers and the latency numbers taught me that a search engine has two nearly independent report cards, and improving one tells you nothing about the other. Hybrid retrieval and reranking are what moved NDCG@10 and MAP; neither touches how fast a single query returns. The latency drop came from a separate pass through the ranking stage, isolated benchmarks show its 5,000 candidate, top 20 case going from a p50 of 1205.35 ms to 10.42 ms, work that would have looked like nothing on a relevance benchmark and everything on a latency one.
Running the CLUSTER mode end to end also taught me where the three distributed pieces actually meet. Gossip and the hash ring both reason about which nodes exist and what they own, so it is tempting to let them share state. Keeping GossipProtocol responsible only for liveness and ConsistentHashRing responsible only for placement, with Raft never consulting either once its own log is committed, is what let me stop a node, watch it declared SUSPECT then DEAD, restart it, and see repair reconverge the ring without touching a single line of the replicated key value store.
Sources
- Benchmark.md, 2026-08-15
- README.md, 2026-09-04
- ARCHITECTURE.md, 2026-09-04
- src/main/java/com/minigoogle (crawler, indexer, query, ranking, cluster packages), 2026-09-04