中文/日文检索需使用 gse 子包并指定词典(如 "embed, zh"),直接用 riot 默认分析器效果不同;同进程内写入后搜索要用 writer.Reader() 而非 riot.OpenReader;向量索引默认 VectorStorageDisk 走内存映射,改 VectorStorageMemory 会占用更多堆内存。
go get -u github.com/vcaesar/riotREADME 未说明显存或 GPU 要求;基准测试在 Apple M1 单核(GOMAXPROCS=1)上完成,属于纯 CPU 场景。
Go 写的轻量全文与向量搜索引擎库,可当 Elasticsearch 的轻量替代
用 Go 写的轻量级分布式文本与向量搜索引擎,主打简单和速度,适合给应用快速加上全文检索或向量检索能力。
核心能力
- 支持文本、数值、日期、IP、地理点和向量多种字段类型
- 内置 BM25 打分,相似度接口可插拔替换
- HNSW 近似向量检索带 int8 量化,图随段持久化并内存映射
- 支持 Term、Phrase、Fuzzy、正则、范围、地理和 KNN/ANN 查询
- 提供词项、数值/日期范围分桶与 HyperLogLog++、T-Digest 聚合
容易踩的坑
- v1.72.1最新正式版2026-11-05 · 2026-11-05 21:23 UTC
What's Changed
- update: update go mod by @vcaesar in https://github.com/vcaesar/riot/pull/179
Full Changelog: https://github.com/vcaesar/riot/compare/v1.72.0...v1.72.1
- v1.72.02026-11-05 · 2026-11-05 20:11 UTC
查看更新说明
What's Changed
- chore: optimze index snapshot and HNSW vector index build and storage… by @vcaesar in https://github.com/vcaesar/riot/pull/178
Full Changelog: https://github.com/vcaesar/riot/compare/v1.71.0...v1.72.0
- v1.71.02026-11-05 · 2026-11-05 17:28 UTC
查看更新说明
What's Changed
- chore: add more fast engine code review code and optimizations by @vcaesar in https://github.com/vcaesar/riot/pull/177
Full Changelog: https://github.com/vcaesar/riot/compare/v1.70.0...v1.71.0
- v1.70.02026-11-04 · 2026-11-04 21:17 UTC
查看更新说明
What's Changed
- chore: review quick fixes in index_test.go and check warnings by @vcaesar in https://github.com/vcaesar/riot/pull/173
- update: update go mod fix bugs by @vcaesar in https://github.com/vcaesar/riot/pull/174
- feat: update gomod, add riot and bleve benchmarks to README.md and la… by @vcaesar in https://github.com/vcaesar/riot/pull/175
- chore: add index v10 code, and review code and lint fixes in CI changes by @vcaesar in https://github.com/vcaesar/riot/pull/176
Full Changelog: https://github.com/vcaesar/riot/compare/v1.62.0...v1.70.0
- v1.62.02026-11-01 · 2026-11-01 19:42 UTC
查看更新说明
What's Changed
- feat: add Riot vs Bluge benchmark to README.md and langs/ by @vcaesar in https://github.com/vcaesar/riot/pull/171
- chore: review optimize.go and search_term_prefix.go changes for bugs by @vcaesar in https://github.com/vcaesar/riot/pull/172
Full Changelog: https://github.com/vcaesar/riot/compare/v1.61.3...v1.62.0
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
Riot
English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Русский | Português
A fastest, modern text indexing library in Go.
For a distributed search engine and a lightweight alternative to full Elasticsearch built on riot, see zedsearch.
Features
- Supported field types:
- Text, Numeric, Date, Boolean, IP, Geo Point, Vector
- Supported query types:
- Term, Phrase, Match, Match Phrase, Prefix, Regexp, Wildcard, Fuzzy
- Conjunction, Disjunction, Boolean
- Numeric Range, Date Range, Term Range, IP Range
- Geo Bounding Box, Geo Distance, Geo Polygon, ANN, KNN
- BM25 Similarity/Scoring with pluggable interfaces
- HNSW approximate vector search with int8 quantization; graphs persist with segments and are memory-mapped from disk
- Search result match highlighting
- Extensible Aggregations:
- Bucketing
- Terms
- Numeric Range
- Date Range
- Metrics
- Min/Max/Count/Sum
- Avg/Weighted Avg
- Cardinality Estimation (HyperLogLog++)
- Quantile Approximation (T-Digest)
- Bucketing
Installation
go get -u github.com/vcaesar/riot
Usage
Runnable versions of the basic indexing, querying, and CJK programs live in
test/readme_demo. Struct indexing and vector search demos live in
examples; run their commands below from the repository root.
Indexing
Save as write/main.go and run with go run ./write:
package main
import (
"log"
riot "github.com/vcaesar/riot"
)
func main() {
config := riot.DefaultConfig("./riot_index")
writer, err := riot.OpenWriter(config)
if err != nil {
log.Fatalf("error opening writer: %v", err)
}
defer writer.Close()
doc := riot.NewDocument("example").
AddField(riot.NewTextField("name", "riot"))
err = writer.Update(doc.ID(), doc)
if err != nil {
log.Fatalf("error updating document: %v", err)
}
log.Printf("indexed: %s", doc.ID())
}
Querying
Save as read/main.go and run with go run ./read against the index written above.
If you index and search in the same process, use writer.Reader() instead of riot.OpenReader
(or riot.InMemoryOnlyConfig() for an in-memory index):
package main
import (
"context"
"fmt"
"log"
riot "github.com/vcaesar/riot"
)
func main() {
config := riot.DefaultConfig("./riot_index")
reader, err := riot.OpenReader(config)
if err != nil {
log.Fatalf("error opening reader: %v", err)
}
defer reader.Close()
query := riot.NewMatchQuery("riot").SetField("name")
request := riot.NewTopNSearch(10, query).
WithStandardAggregations()
documentMatchIterator, err := reader.Search(context.Background(), request)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
match, err := documentMatchIterator.Next()
for err == nil && match != nil {
err = match.VisitStoredFields(func(field string, value []byte) bool {
if field == "_id" {
fmt.Printf("match: %s\n", string(value))
}
return true
})
if err != nil {
log.Fatalf("error loading stored fields: %v", err)
}
match, err = documentMatchIterator.Next()
}
if err != nil {
log.Fatalf("error iterator document matches: %v", err)
}
}
Output:
match: example
Chinese / Japanese with gse
The gse package wraps riot with a gse tokenizer for
CJK text, plus query-string search and highlighting. Save as cjk/main.go and run with
go run ./cjk:
package main
import (
"fmt"
"log"
"os"
"github.com/vcaesar/riot/gse"
)
func main() {
opt := gse.Option{
Index: "test.riot",
Dicts: "embed, ja", // or "embed, zh"
Opt: "search-hmm",
}
// Option{Lang: "en"} skips gse and uses a riot analysis/lang analyzer
// instead ("en", "cjk", "de", ...; see gse.Langs()).
defer os.RemoveAll(opt.Index)
index, err := gse.New(opt)
if err != nil {
log.Fatalf("error opening gse index: %v", err)
}
defer index.Close()
text := `見解では、謙虚なヴォードヴィリアンのベテランは、運命の犠牲者と悪役の両方の変遷として代償を払っています`
docs := map[string]string{
"1": text,
"3": text + "浮き沈み",
"4": `In view, a humble vaudevillian veteran cast vicariously as both victim and villain vicissitudes of fate.`,
"2": `It's difficult to understand the sum of a person's life.`,
"5": `Riot 是用 Go 语言编写的全文搜索引擎`,
}
for id, doc := range docs {
if err = index.Index(id, doc); err != nil {
log.Fatalf("error indexing %s: %v", id, err)
}
}
for _, query := range []string{"運命の犠牲者", "搜索引擎", "vaudevillian"} {
req := gse.QueryString(query, true)
res, err := index.Search(req)
if err != nil {
log.Fatalf("error searching %q: %v", query, err)
}
fmt.Printf("query %q: %d hits in %v\n", query, res.Total, res.Took)
for _, hit := range res.Hits {
fmt.Printf(" %s (%.3f) %v\n", hit.ID, hit.Score, hit.Fragments["text"])
}
}
}
Example output (timings may vary):
query "運命の犠牲者": 2 hits in 14.5µs
1 (1.899) [見解では、謙虚なヴォードヴィリアンのベテランは、<mark>運命</mark><mark>の</mark><mark>犠</mark><mark>牲</mark><mark>者</mark>と悪役<mark>の</mark>両方<mark>の</mark>変遷として代償を払っています]
3 (1.837) [見解では、謙虚なヴォードヴィリアンのベテランは、<mark>運命</mark><mark>の</mark><mark>犠</mark><mark>牲</mark><mark>者</mark>と悪役<mark>の</mark>両方<mark>の</mark>変遷として代償を払っています浮き沈み]
query "搜索引擎": 1 hits in 19.792µs
5 (1.531) [Riot 是用 Go 语言编写的全文<mark>搜索</mark><mark>引擎</mark>]
query "vaudevillian": 1 hits in 1.958µs
4 (0.654) [In view, humble <mark>vaudevillian</mark> veteran cast vicariously as both victim and villain vicissitudes of fate.]
Indexing structs with gse
package main
import (
"errors"
"fmt"
"log"
"github.com/vcaesar/riot/gse"
)
type Article struct {
Title string `json:"title"`
Text string `json:"text"`
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() (err error) {
index, err := gse.New(gse.Option{Lang: "en"}) // Empty Index uses memory only.
if err != nil {
return err
}
defer func() { err = errors.Join(err, index.Close()) }()
if err := index.Index("article-1", Article{
Title: "Getting started",
Text: "Riot is a search engine written in Go.",
}); err != nil {
return err
}
request := gse.QueryString("started", true)
request.Field = "title"
result, err := index.Search(request)
if err != nil {
return err
}
for _, hit := range result.Hits {
fmt.Printf("%s: %s %v\n", hit.ID, hit.Fields["title"], hit.Fragments["title"])
}
return nil
}
Output:
article-1: Getting started [Getting <mark>started</mark>]
Vector search: exact KNN and approximate ANN
Run examples/knn with go run ./examples/knn:
package main
import (
"context"
"errors"
"fmt"
"log"
riot "github.com/vcaesar/riot"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() (err error) {
writer, err := riot.OpenWriter(riot.InMemoryOnlyConfig())
if err != nil {
return err
}
defer func() { err = errors.Join(err, writer.Close()) }()
for _, item := range []struct {
id string
vector []float32
}{
{"east", []float32{1, 0}},
{"northeast", []float32{1, 1}},
{"north", []float32{0, 1}},
} {
field, err := riot.NewVectorField("embedding", item.vector)
if err != nil {
return err
}
doc := riot.NewDocument(item.id).AddField(field)
if err := writer.Update(doc.ID(), doc); err != nil {
return err
}
}
reader, err := writer.Reader()
if err != nil {
return err
}
defer func() { err = errors.Join(err, reader.Close()) }()
for _, mode := range []string{"KNN", "ANN"} {
query := riot.NewKNNQuery("embedding", []float32{1, 0}, 2).
SetMetric(riot.Cosine)
if mode == "ANN" {
query.SetANN(riot.ANNParams{EfSearch: 100})
}
matches, err := reader.Search(context.Background(), riot.NewTopNSearch(2, query))
if err != nil {
return err
}
for {
match, err := matches.Next()
if err != nil {
return err
}
if match == nil {
break
}
var id string
if err := match.VisitStoredFields(func(name string, value []byte) bool {
if name == "_id" {
id = string(value)
}
return true
}); err != nil {
return err
}
fmt.Printf("%s: %s (%.3f)\n", mode, id, match.Score)
}
}
return nil
}
Output:
KNN: east (1.000)
KNN: northeast (0.707)
ANN: east (1.000)
ANN: northeast (0.707)
Persisted vector indexes
HNSW graphs are saved with on-disk segments, so a reopened index searches them without rebuilding. WithVectorStorage chooses where their vectors live: riot.VectorStorageDisk (the default) memory-maps each graph with its vectors, so they sit in the OS page cache instead of the Go heap; riot.VectorStorageMemory stores links only and loads vectors onto the heap when a graph opens (smaller files, more memory).
config := riot.DefaultConfig("./riot_vectors").WithVectorStorage(riot.VectorStorageDisk)
examples/ann_disk indexes vectors on disk and searches them after reopening: go run ./examples/ann_disk.
Benchmarks
Riot vs Bleve and Bluge (the engine it forked)
All three engines run the same workload: 100,000 synthetic documents (body text with positions, stored; numeric id; category keyword). Bleve v2.6.2 (scorch, BM25) and Bluge v0.2.2 run against current riot (Go 1.27.3) in its default stream indexing mode, one process at a time on one core (GOMAXPROCS=1), median of 5 rotated rounds (Apple M1, load average 3.1 to 5.4). In every run all three returned the same match counts and match sets as a per-document oracle. The riot column was re-measured on 2026-11-05 with the same harness (load 2.5 to 2.7); the other engines' figures are from the earlier run.
| Workload | Bleve v2.6.2 | Bluge v0.2.2 | Riot | Riot vs Bleve | Riot vs Bluge |
|---|---|---|---|---|---|
| Initial indexing, 100,000 docs | 6,546 ms | 5,029 ms | 500 ms | 13.1× faster | 10.1× faster |
| 1-doc commit + visible reader | 0.151 ms | 0.313 ms | 0.151 ms§ | ≈ | 2.1× faster |
| Index size on disk | 76.71 MB | 61.06 MB | 8.22 MB | 9.3× smaller | 7.4× smaller |
| Peak memory (RSS) | 1,931 MiB | 1,974 MiB | 144 MiB | 13.4× less | 13.7× less |
| Total CPU, whole run | 17.84 s | 19.50 s | 1.05 s | 17.0× less | 18.6× less |
| Rare term | 11.90 µs | 16.98 µs | 2.25 µs | 5.3× faster | 7.5× faster |
| Common term, top 10 + count | 5,848 µs | 10,861 µs | 7.17 µs† | 816× faster | 1,516× faster |
| Boolean AND | 1,788 µs | 1,990 µs | 27.83 µs† | 64× faster | 71× faster |
| Boolean OR | 27.18 µs | 43.61 µs | 4.75 µs | 5.7× faster | 9.2× faster |
| Phrase | 60,645 µs | 61,758 µs | 2,175 µs | 28× faster | 28× faster |
Prefix term12* (constant score in riot) | 3,133 µs | 3,597 µs | 45.38 µs | 69× faster | 79× faster |
Fuzzy serch, distance 1 | 6,798 µs | 12,035 µs | 26.17 µs† | 260× faster | 460× faster |
| Typed numeric range | 166.2 µs | 164.6 µs | 8.79 µs | 19× faster | 19× faster |
| Keyword / facet term | 385.1 µs | 645.7 µs | 2.75 µs | 140× faster | 235× faster |
| Numeric stats‡ | 45,375 µs | 105,356 µs | 433.2 µs | 105× faster | 243× faster |
- Peak memory and CPU cover the whole benchmark process (10K and 100K builds plus every query loop), taken from
wait4rusage. Index size comes from a separate on-disk build (median of 3).
Scores differ from Bluge's because riot fixed Bluge's BM25 idf to Lucene's log(1 + (N − n + 0.5)/(n + 0.5)). Bluge has no vector search; Bleve's needs the vectors build tag and FAISS through cgo. These are single-machine numbers on synthetic data, not a formal benchmark.
Riot (Go) is already on par with Tantivy: it is faster on 7 of 10 query workloads in the same run. An even faster Zig version of riot is also in development. It is still beta and not published yet. In the same benchmark run it was about 2–5× faster than riot (Go) on most queries and 2.8× faster at indexing, and it was at least 1.5× faster than Tantivy on every query workload (2–8× on most).
License
Apache License Version 2.0