riot

vcaesarApache-2.0活跃维护

用 Go 写的轻量级分布式文本与向量搜索引擎,主打简单和速度,适合给应用快速加上全文检索或向量检索能力。

Go Open Source, Distributed, Simple and fastest efficient text/vector Search Engine

v1.72.1发布于 2026-11-05 21:23 UTC6,064star470fork同步于 2026-11-10 11:34 UTC

怎么上手

由 README 提炼,执行前请确认
go get -u github.com/vcaesar/riot
硬件要求按 README 说明

README 未说明显存或 GPU 要求;基准测试在 Apple M1 单核(GOMAXPROCS=1)上完成,属于纯 CPU 场景。

支持 Apple Silicon
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

Go 写的轻量全文与向量搜索引擎库,可当 Elasticsearch 的轻量替代

用 Go 写的轻量级分布式文本与向量搜索引擎,主打简单和速度,适合给应用快速加上全文检索或向量检索能力。

核心能力

  • 支持文本、数值、日期、IP、地理点和向量多种字段类型
  • 内置 BM25 打分,相似度接口可插拔替换
  • HNSW 近似向量检索带 int8 量化,图随段持久化并内存映射
  • 支持 Term、Phrase、Fuzzy、正则、范围、地理和 KNN/ANN 查询
  • 提供词项、数值/日期范围分桶与 HyperLogLog++、T-Digest 聚合

容易踩的坑

中文/日文检索需使用 gse 子包并指定词典(如 "embed, zh"),直接用 riot 默认分析器效果不同;同进程内写入后搜索要用 writer.Reader() 而非 riot.OpenReader;向量索引默认 VectorStorageDisk 走内存映射,改 VectorStorageMemory 会占用更多堆内存。

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v1.72.1
  1. v1.72.1最新正式版2026-11-05 · 2026-11-05 21:23 UTC

    What's Changed

    Full Changelog: https://github.com/vcaesar/riot/compare/v1.72.0...v1.72.1

  2. v1.72.02026-11-05 · 2026-11-05 20:11 UTC
    查看更新说明

    What's Changed

    Full Changelog: https://github.com/vcaesar/riot/compare/v1.71.0...v1.72.0

  3. v1.71.02026-11-05 · 2026-11-05 17:28 UTC
    查看更新说明

    What's Changed

    Full Changelog: https://github.com/vcaesar/riot/compare/v1.70.0...v1.71.0

  4. v1.70.02026-11-04 · 2026-11-04 21:17 UTC
    查看更新说明

    What's Changed

    Full Changelog: https://github.com/vcaesar/riot/compare/v1.62.0...v1.70.0

  5. v1.62.02026-11-01 · 2026-11-01 19:42 UTC
    查看更新说明

    What's Changed

    Full Changelog: https://github.com/vcaesar/riot/compare/v1.61.3...v1.62.0

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    Riot

    PkgGoDev Tests Lint GitHub release

    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)

    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.

    WorkloadBleve v2.6.2Bluge v0.2.2RiotRiot vs BleveRiot vs Bluge
    Initial indexing, 100,000 docs6,546 ms5,029 ms500 ms13.1× faster10.1× faster
    1-doc commit + visible reader0.151 ms0.313 ms0.151 ms§≈2.1× faster
    Index size on disk76.71 MB61.06 MB8.22 MB9.3× smaller7.4× smaller
    Peak memory (RSS)1,931 MiB1,974 MiB144 MiB13.4× less13.7× less
    Total CPU, whole run17.84 s19.50 s1.05 s17.0× less18.6× less
    Rare term11.90 µs16.98 µs2.25 µs5.3× faster7.5× faster
    Common term, top 10 + count5,848 µs10,861 µs7.17 µs†816× faster1,516× faster
    Boolean AND1,788 µs1,990 µs27.83 µs†64× faster71× faster
    Boolean OR27.18 µs43.61 µs4.75 µs5.7× faster9.2× faster
    Phrase60,645 µs61,758 µs2,175 µs28× faster28× faster
    Prefix term12* (constant score in riot)3,133 µs3,597 µs45.38 µs69× faster79× faster
    Fuzzy serch, distance 16,798 µs12,035 µs26.17 µs†260× faster460× faster
    Typed numeric range166.2 µs164.6 µs8.79 µs19× faster19× faster
    Keyword / facet term385.1 µs645.7 µs2.75 µs140× faster235× faster
    Numeric stats‡45,375 µs105,356 µs433.2 µs105× faster243× faster
    • Peak memory and CPU cover the whole benchmark process (10K and 100K builds plus every query loop), taken from wait4 rusage. 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