# riot **Repository Path**: veni0/riot ## Basic Information - **Project Name**: riot - **Description**: 开源,分布式,简单高效的搜索引擎 - **Primary Language**: Go - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: https://www.oschina.net/p/riotsearch - **GVP Project**: No ## Statistics - **Stars**: 198 - **Forks**: 0 - **Created**: 2017-08-03 - **Last Updated**: 2026-10-17 ## Categories & Tags **Categories**: search-engine **Tags**: None ## README # ![Riot](docs/bluge.png) Riot [![PkgGoDev](https://pkg.go.dev/badge/github.com/vcaesar/riot)](https://pkg.go.dev/github.com/vcaesar/riot) [![Tests](https://github.com/vcaesar/riot/actions/workflows/tests.yml/badge.svg?branch=main&event=push)](https://github.com/vcaesar/riot/actions/workflows/tests.yml?query=event%3Apush+branch%3Amain) [![Lint](https://github.com/vcaesar/riot/actions/workflows/lint.yml/badge.svg?branch=main&event=push)](https://github.com/vcaesar/riot/actions/workflows/lint.yml?query=event%3Apush+branch%3Amain) English | [简体中文](langs/README.zh.md) | [繁體中文](langs/README.zht.md) | [日本語](langs/README.ja.md) | [한국어](langs/README.ko.md) | [Français](langs/README.fr.md) | [Deutsch](langs/README.de.md) | [Español](langs/README.es.md) | [Русский](langs/README.ru.md) | [Português](langs/README.pt.md) A fast, modern text indexing library in Go, forked from [Bluge](https://github.com/blugelabs/bluge). ## 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 - Search result match highlighting - Extensible Aggregations: - Bucketing - Terms - Numeric Range - Date Range - Metrics - Min/Max/Count/Sum - Avg/Weighted Avg - Cardinality Estimation ([HyperLogLog++](https://github.com/axiomhq/hyperloglog)) - Quantile Approximation ([T-Digest](https://github.com/caio/go-tdigest)) ## Installation ```sh go get -u github.com/vcaesar/riot ``` ## Usage Runnable versions of the basic indexing, querying, and CJK programs live in [`test/readme_demo`](test/readme_demo). Struct indexing and vector search demos live in [`examples`](examples); run their commands below from the repository root. ### Indexing Save as `write/main.go` and run with `go run ./write`: ```go 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): ```go 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`](gse) package wraps riot with a [gse](https://github.com/go-ego/gse) tokenizer for CJK text, plus query-string search and highlighting. Save as `cjk/main.go` and run with `go run ./cjk`: ```go 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) [見解では、謙虚なヴォードヴィリアンのベテランは、運命の犠牲者と悪役の両方の変遷として代償を払っています] 3 (1.837) [見解では、謙虚なヴォードヴィリアンのベテランは、運命の犠牲者と悪役の両方の変遷として代償を払っています浮き沈み] query "搜索引擎": 1 hits in 19.792µs 5 (1.531) [Riot 是用 Go 语言编写的全文搜索引擎] query "vaudevillian": 1 hits in 1.958µs 4 (0.654) [In view, humble vaudevillian veteran cast vicariously as both victim and villain vicissitudes of fate.] ``` ### Indexing structs with gse ```go 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: ```text article-1: Getting started [Getting started] ``` ### Vector search: exact KNN and approximate ANN Run [`examples/knn`](examples/knn) with `go run ./examples/knn`: ```go 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: ```text KNN: east (1.000) KNN: northeast (0.707) ANN: east (1.000) ANN: northeast (0.707) ``` ## License Apache License Version 2.0