Riot: A 28-Second Indexing Engine That Taught Me About Go's Memory Trade-offs
Hook
What if I told you a search engine could index half a gigabyte of documents in under 30 seconds, but the authors themselves warn you not to use it in production?
Context
In 2017, the Go ecosystem had a problem: if you wanted full-text search in your Go application, you either embedded SQLite's FTS extension (compromising on Go's single-binary promise), called out to Elasticsearch (adding operational complexity), or built something yourself. Bleve existed but was still maturing. Chinese text search was particularly painful—most Western search engines treated CJK characters as opaque tokens, missing the nuances of word segmentation that make or break relevance.
Riot emerged as an experiment in aggressive simplicity: a pure-Go search engine optimized for raw speed, with first-class Chinese language support via the gse segmentation library. The authors made a bet that many applications needed search that was "good enough" rather than perfect, and that developers would trade memory for millisecond-level latency. They were partially right—the project garnered 6,000+ stars—but the memory consumption became so problematic that the team publicly declared they'd rewrite everything for V2. That rewrite never happened, leaving Riot as a fascinating case study in architectural trade-offs.
Technical Insight
Riot's architecture is refreshingly direct: it maintains an in-memory inverted index wrapped in a coroutine-safe Engine struct. When you index a document, Riot tokenizes it, calculates term positions, and updates the index—all without locks at the user level because the Engine handles synchronization internally. Here's the basic indexing pattern:
import (
"github.com/go-ego/riot"
"github.com/go-ego/riot/types"
)
func main() {
var engine riot.Engine
engine.Init(types.EngineOpts{
Using: 4, // 4 segmenter threads
NumShards: 8, // 8 index shards for parallelism
IndexerOpts: &types.IndexerOpts{
IndexType: types.DocIdsIndex,
},
})
defer engine.Close()
// Index with explicit document ID
engine.Index("doc123", types.DocData{
Content: "Go语言搜索引擎 distributed search",
Fields: map[string]interface{}{
"title": "Riot Tutorial",
"date": "2024-01-15",
},
})
// Critical: flush to make searchable
engine.Flush()
// Search with BM25 ranking
results := engine.Search(types.SearchReq{
Text: "Go搜索",
RankOpts: &types.RankOpts{
OutputOffset: 0,
MaxOutputs: 10,
},
})
}
The devil is in the details of those configuration options. NumShards controls how the index is partitioned—more shards mean better parallelism during search but higher memory overhead. Each shard maintains its own inverted index mapping terms to document IDs and positions. The Using parameter spawns dedicated goroutines for text segmentation, which is critical for Chinese text where "搜索引擎" needs to be split into "搜索" (search) and "引擎" (engine) rather than indexed as individual characters.
Riot's BM25 implementation is where things get interesting. Unlike simple TF-IDF, BM25 includes document length normalization with tunable parameters k1 (term saturation) and b (length normalization strength). Riot exposes these through ScoringCriteria, letting you weight different fields differently:
results := engine.Search(types.SearchReq{
Text: "distributed search",
RankOpts: &types.RankOpts{
ScoringCriteria: &types.ScoringCriteria{
A: 1.0, // title field weight
B: 0.5, // content field weight
},
},
})
The memory problem stems from Riot's position-aware indexing. For phrase queries like "Go search engine", Riot doesn't just check if all three words exist—it verifies they appear consecutively by storing every term's position in every document. A 1-million document corpus might have 50 million term positions stored as integer arrays. With Go's runtime overhead (slice headers, alignment, GC metadata), this explodes to gigabytes. The authors chose this approach for query flexibility—proximity searches and phrase matching—but paid dearly in RAM.
Persistence is an afterthought bolted on via a simple interface. Riot can dump its index to disk using gob encoding or LevelDB, but the API is awkward:
engine.Init(types.EngineOpts{
UseStore: true,
StoreFolder: "./riot_data",
StoreEngine: "ldb", // leveldb backend
})
Even with persistence enabled, the entire index still loads into memory on startup. There's no true on-disk index structure like Lucene's segment files—it's just periodic serialization of in-memory data structures. This makes Riot unsuitable for datasets that don't fit in RAM, which is the majority of serious search applications.
Gotcha
The Flush() requirement is Riot's most dangerous footgun. Indexed documents don't become searchable until you explicitly call Flush(), which iterates through pending operations and updates the queryable index. If you're building a real-time application where users expect to search immediately after uploading content, you'll need to flush after every index operation—but that tanks performance because flushing locks the index. The alternative is periodic background flushing, which means accepting stale search results.
More concerning is the project's abandonment. The last significant commit was in 2018, and critical issues remain open with no maintainer response. The v2 rewrite promised in the README never materialized, leaving users with a codebase the authors themselves acknowledge is fundamentally flawed. Dependencies are outdated—gse has moved forward, but Riot pins an old version. This isn't just technical debt; it's archaeological debt. You're adopting code that's frozen in time, which means no security patches, no bug fixes, and no community support when things break.
Verdict
Use if: You're building a prototype or internal tool with <500K documents where Chinese text search is critical, you can tolerate 4-8GB of RAM for the search index, and you understand you're adopting unmaintained software. Riot's segmentation quality and raw speed make it attractive for demos or MVPs where you need impressive benchmarks quickly. It's also valuable for learning—the codebase is small enough to read in an afternoon, and studying its mistakes teaches you what not to do in production systems. Skip if: You need production reliability, your dataset exceeds 1M documents, you can't provision abundant memory, or you expect the software to receive updates. For real applications, invest in Bleve (actively maintained, production-ready, similar API) or bite the bullet and run Elasticsearch. Riot is a cautionary tale about premature optimization: the authors optimized for speed and got it, but created a system too resource-hungry to survive. Learn from it, don't deploy it.