需按使用方式单独导入 bindings 包(builtin、cproto、builtinserver 等),漏导入会无法初始化;cproto 连接服务器时数据库需预先用 reindexer_tool 创建或加 WithCreateDBIfMissing 选项;每个 namespace 最多 256 个索引(255 个用户索引加 1 个内部索引)
reindexer
RestreamApache-2.0活跃维护可嵌入内存文档数据库,自带高层查询构建器,支持向量、全文和混合检索,适合在应用内直接做 RAG 与相似度搜索。
Embeddable, in-memory, document-oriented database with a high-level Query builder interface.
go get github.com/restream/reindexer/v5README 未说明显存或 GPU 要求;核心用 C++ 编写,提供 builtin 静态库绑定,可在本地嵌入式运行
嵌入式内存文档数据库,用 Go 查询构建器做复杂检索,作为 Elasticsearch 的高性能替代
可嵌入内存文档数据库,自带高层查询构建器,支持向量、全文和混合检索,适合在应用内直接做 RAG 与相似度搜索。
核心能力
- 用 Go 链式 Query builder 构造复杂查询
- 支持全文检索与向量索引(ANN/KNN)
- 支持 Join、聚合、子查询与复合主键
- 支持可排序索引、数组字段索引与 TTL 过期
- 支持磁盘存储、复制与分片
容易踩的坑
- v5.18.1最新正式版2026-11-05 · 2026-11-05 07:18 UTC
Vector indexes
- [fix] Enabled
TCP_NODELAYfor auto-embedding HTTP connections to reduce latency for requests with small payloads
- [fix] Enabled
- v5.18.02026-11-02 · 2026-11-02 13:15 UTC
查看更新说明
Core
- [fea] Reworked the
UPDATEquery engine. The new implementation supports updates with multidimensional arrays and arrays of vectors (for example:UPDATE ns SET vecs_arr = [[1.0, 1.1], [0.0, 0.7], [[0.5, 0.1], [0.3, 1.0]]]). It also fixes 24 issues with updates to array elements by position or wildcard, arrays of objects, and uninitialized indexed fields - [fea] Added explicit sorting to MERGE queries.
MERGEnow works with any selection, not just full-text queries - [fea] Added deduplication to
MERGEqueries. May break compatibility: automatic deduplication changes the results in some cases - [fea] Added
merged_query_id()for sort expressions. It lets you explicitly prioritize subquery results inMERGE - [fea] Optimized collision handling for string
hashindexes, reducing unnecessary string comparisons - [fea] Added the
items_deletion,items_modification, andwait_replicationstates to#activitystatsforUPDATE/DELETEqueries, transactions, and RAFT cluster replication - [fix] Fixed a possible hang in
UnsubscribeUpdateswhen closing an updates subscription with pending events - [fix] Fixed event loss and unnecessary queue retention when subscribers join or leave
- [fix] Fixed event delivery after queue overflow. Event-loss notifications no longer repeat indefinitely and are delivered to all subscription streams regardless of their filters
- [fix] Fixed metadata key compatibility for the
serial()precept. Both the old key format and the new one introduced afterv5.11.0are now recognized correctly
Fulltext
- [fea] Reduced full-text index memory usage during builds and background cleanup by limiting excess buffer capacity and releasing unused space
- [fea] Reduced peak memory usage for full-text queries by using more compact temporary structures to combine term matches
- [fea] Added
indexing_struct_detailsto#memstatswith a more detailed breakdown of text index memory usage. See the full list of fields in theTextIndexMemoryStatsschema - [fix] Improved BM25 ranking accuracy when deleted or updated documents have not yet been cleaned up from the full-text index
- [fix] Fixed highlight/snippet for arrays of certain lengths
- [fix] Fixed phrase search in individual fields of composite text indexes and in arrays
Replication
- [fix] Fixed a possible temporary split-brain in RAFT clusters during overlapping leader elections. A node could start a new election after already accepting another leader
- [fix] Fixed error handling for snapshots during initial RAFT leader synchronization. Previously, this could terminate the process in rare cases
Reindexer server
- [fea] Added IPv6 address support when displaying connected clients in
#clientsstats - [fix]
WALqueries are now rejected forCSV,Protobuf, andMsgPackoutput formats - [fix] Fixed transaction lifetime handling in gRPC. Active transactions no longer expire prematurely
- [fix] Fixed a possible
heap-use-after-freewhen stopping the RPC server while a client connection is closing
Reindexer tool
- [fea] SQL errors are now displayed when attempting to autocomplete an invalid query
- [fix
- [fea] Reworked the
- v5.17.02026-09-26 · 2026-09-26 07:24 UTC
查看更新说明
Core
- [fea] Optimized transaction commit. Transactions with CJSON items now commit 10-20% faster
- [fea] The namespace write lock is now released before the storage flush during write operations
- [fea] Reduced PK index memory usage by ~20%
- [fea] Added support for subqueries inside joined queries
- [fix] Fixed
Explaintimings for queries withMERGE. Previously, JOIN preselect time was calculated incorrectly for such queries - [fix] Fixed several rare issues with document migration in storage after a PK index change
- [fix] Fixed
*and/operator precedence inUPDATE SETexpressions - [fix] Fixed
total.data_sizecalculation for indexes in#memstats. Previously, it could become negative in some cases - [fix] Combining
EQUAL_POSITIONwithALLSETconditions is now prohibited (such queries used to work incorrectly) - [fix] SQL conditions with
NULLnow follow the Query API rules:<,<=,>,>=, andLIKEwithNULLreturn an error instead of silently working asIS NULL.EMPTYandNOT NULLare now allowed only afterIS - [fix] Fixed a data race on transaction commit while multithreaded vector inserts were still running
- [fix] Text functions (
snippet(),highlight(), etc.) now return an error when applied to a field without a full-text index - [fix] Fixed validation of
#lsnconditions inside nested queries (MERGE/JOIN/subqueries) - [fix] A failed index update (
UpdateIndex) no longer drops the original index, even if the error occurs while converting data to the new index type - [upd] Removed the deprecated
replication.data_hashfield from#memstats. Usereplication.checksuminstead
Fulltext
- [fea] Reworked incremental builds for text indexes. Accumulated updates no longer trigger a full blocking index rebuild: documents deleted from the index are now cleaned up in the background instead. Cleanup is controlled by the new
deleted_docs_optimization_thresholdoption in the text index config and by theft_cleanup_timeout_msoption in thenamespacessection of#config - [fea] Improved handling of queries typed in the wrong keyboard layout
- [fix] Fixed snippets for array indexes where only some of the array elements match the query
Vector indexes
- [fea] Added support for OpenAI-compatible embedding APIs in auto-embedding (
protocol.type: "openai") - [fea] Query-builder and DSL
UPDATEresults no longer include float vector fields unless they are requested explicitly (SelectAllFields()orvectors()), the same asSELECT. SQLUPDATEandDELETEstill return vector fields. See Float vector fields in selection results - [fix] Improved handling of invalid responses from the embedding service
Replication
- [fea]
DELETEqueries, and the delete record written when anUPDATEchanges the primary key, now store only primary key fields in the WAL and in the replication payload. This cuts WAL size and replication traffic for larg
- v5.16.02026-08-19 · 2026-08-19 09:44 UTC
查看更新说明
Core
- [fea] Added support for nested left/inner joins. Warning: For
builtinbuilds,Query/QueryResultsserialization was migrated to a new format. Updated bindings are required forbuiltinmode to work correctly. Full compatibility is preserved forcproto/ucproto - [fea] Improved heuristics for scan order and sort index selection when using
Distinctunder mixed read/write workloads - [fea] Optimized cost/iteration estimation when planning B-tree scans under mixed read/write workloads
- [fix] Fixed
#memstatscalculation for indexes. Sincev5.15.0,idset_plain_sizecould overflow - [fix] Fixed a race when cancelling background index optimization. Since
v5.15.0, background optimization could finish in an incomplete state and cause exceptions during select queries - [fix] Fixed a crash on
int64overflow for string indexes withcollate_mode: numeric - [fix] Fixed a crash when a subquery and an inner join were placed in the same parentheses
- [fix] Fixed a crash in forced sort for composite indexes
- [fix] Improved number parsing in UPDATE SET expressions. Combinations like
9dddare now treated as field names, not numbers
Fulltext
- [fea] Reworked the in-memory storage and incremental build layer for text indexes. The new implementation significantly improves index performance under mixed read/write workloads
- [fea] Removed
max_rebuild_stepsandmax_step_sizefrom the text index config: the new logic is based on the share of deleted documents in the index (rebuild when 50%+ of documents are deleted) - [fea] Added
total_vdocs,removed_vdocs, andvdocs_compactionsfields to#memstatsfor text indexes - [fea] Improved variant generation for wrong keyboard layout. The new version produces fewer potentially irrelevant variants
- [fea] Improved term splitting (
EnableTermsSplit). Letter/digit boundary splitting now works regardless of term length (e.g.season1produces theseason 1variant) - [fix] Fixed double penalty application to suffix ranks
Vector indexes
- [fea] Added built-in circuit breaker logic for auto-embedding. Warning: The circuit breaker is enabled by default and may affect previously configured embedders
Replication
- [fix] Fixed a race between proxying logic and RAFT leader elections. Previously, this race could lead to errors like
Request was proxied to follower node
Reindexer server
- [fea] Improved SQL suggest generation via
/db/{database}/suggestin some cases - [fea] HTTP endpoint
/db/{database}/suggestnow returns the position in characters, not bytes - [fix] Fixed a connection object leak when the client closed the connection immediately without sending any requests (typical for HAProxy keep-alive probes)
- [fix] Fixed error reporting when trying to use protobuf with multidimensional arrays
Reindexer tool
- [fea] Added a progress bar when creating a database dump. See reindexer tool options (
--no-progress-meter) - [fea] Added CLI commands for index management (
\index add/\index update/\index drop) - [fea] The [`outp
- [fea] Added support for nested left/inner joins. Warning: For
- v5.15.02026-07-09 · 2026-07-09 13:26 UTC
查看更新说明
Core
- [fea] Optimized ID-set intersection logic during queries. The new implementation adapts better to different ID distributions within sets
- [fea] Added heap-based intersection for geo-queries with a large number of points. In certain scenarios, this can provide up to a 10x speedup (see the
RStarRTreePointDWithin/1%benchmark) - [fea] Reduced the impact of background index optimization on write operations by increasing the granularity of optimization cancellation checks. Previously, background index optimization could trigger latency spikes for write operations
- [fea] Made stack traces more informative for MUSL builds
- [fea] Slightly optimized indexes memory layout under mixed write/read workloads
- [fix] Fixed a bug with row invalidation in the joined cache
- [fix] Added explicit error in case of
fulltextindex insideJOIN's ON clause. Previously those queries did not work correctly in the most of the cases
Fulltext
- [fix] Fixed possible crash in long words (256+ bytes) indexing
Vector indexes
- [fea] Added an optional streaming fetch for HNSW indexes. In this case, the query planner will rely on the query's final
limit/offsetinstead of thekvalue
Replication
- [fea] Added stricter checks for coroutine stack sizes
- [fix] Fixed a rare issue where a RAFT cluster
leaderreceived a false error from Ping while another coroutine was handling an exception, causing it to fall back to thecandidaterole
Go connector
- [fea] Optimized encoding/decoding logic for binary structures related to queries and query results
- [fix] Fixed
NextObjand joined-fields oncorrect interactions: previously it could generate panic even on the correct data
Build
- [fea] Added
WITH_UBSANbuild option - [fea] Updated the base Docker image to
alpine:3.23
Face
- [fea] Changed format for the
Keep diacriticsoption (fulltextindexes) - [fix] Fixed
Replicationtable column width
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
Reindexer
Reindexer is an embeddable, in-memory, document-oriented database with a high-level Query builder interface.
Reindexer's goal is to provide fast search with complex queries. We at Restream weren't happy with Elasticsearch and created Reindexer as a more performant alternative.
The core is written in C++ and the application level API is in Go.
This document describes the Go connector and its API. For information about the Reindexer server and HTTP API, refer to the Reindexer documentation.
Table of contents:
- Features
- Usage
- Installation
- Advanced Usage
- Index Types and Their Capabilities
- Nested Structs
- Sort
- Functions
- Counting
- Text pattern search with LIKE condition
- Update queries
- Delete queries
- Truncate queries
- Transactions and batch update
- Merge
- Join
- Subqueries (nested queries)
- Complex Primary Keys and Composite Indexes
- Aggregations
- Search in array fields
- Atomic on update functions
- Expire Data from Namespace by Setting TTL
- Direct JSON operations
- Using object cache
- Events subscription
- Logging, debug, profiling and tracing
- Integration with other programming languages
- Limitations and known issues
- Getting help
- References
Features
Key features:
- Sortable indices
- Aggregation queries
- Indices on array fields
- Complex primary keys
- Composite indices
- Join operations
- Full-text search
- Up to 256 indexes (255 user's index + 1 internal index) for each namespace
- ORM-like query interface
- SQL queries
Performance
Performance has been our top priority from the start, and we think we managed to get it pretty good. Benchmarks show that Reindexer's performance is on par with a typical key-value database. On a single CPU core, we get:
- up to 500K queries/sec for queries
SELECT * FROM items WHERE id='?' - up to 50K queries/sec for queries
SELECT * FROM items WHERE year > 2010 AND name = 'string' AND id IN (....) - up to 20K queries/sec for queries
SELECT * FROM items WHERE year > 2010 AND name = 'string' JOIN subitems ON ...
See benchmarking results and more details in benchmarking repo
Memory Consumption
Reindexer aims to consume as little memory as possible; most queries are processed without any memory allocation at all.
To achieve that, several optimizations are employed, both on the C++ and Go level:
-
Documents and indices are stored in dense binary C++ structs, so they don't impose any load on Go's garbage collector.
-
String duplicates are merged.
-
Memory overhead is about 32 bytes per document + ≈4-16 bytes per each search index.
-
There is an object cache on the Go level for deserialized documents produced after query execution. Future queries use pre-deserialized documents, which cuts repeated deserialization and allocation costs
-
The Query interface uses
sync.Poolfor reusing internal structures and buffers. The combination of these technologies allows Reindexer to handle most queries without any allocations.
Full text search
Reindexer has internal full text search engine. Full text search usage documentation and examples are here
Vector indexes (ANN/KNN)
Reindexer has internal k-nearest neighbors search engine. k-nearest neighbors search usage documentation and examples are here. For selective post-filtered pages on HNSW, see streaming KNN (omit k and radius, use LIMIT/OFFSET on the query).
Hybrid search
Reindexer has internal hybrid full text and k-nearest neighbors search engine. Its usage documentation and examples are here
Disk Storage
Reindexer can store documents to and load documents from disk via LevelDB. Documents are written to the storage backend asynchronously by large batches automatically in background.
When a namespace is created, all its documents are stored into RAM, so the queries on these documents run entirely in in-memory mode.
Replication
Reindexer supports synchronous and asynchronous replication. Check replication documentation here
Sharding
Reindexer has some basic support for sharding. Check sharding documentation here
Usage
Here is complete example of basic Reindexer usage:
package main
// Import package
import (
"fmt"
"math/rand"
"github.com/restream/reindexer/v5"
// choose how the Reindexer binds to the app (in this case "builtin," which means link Reindexer as a static library)
_ "github.com/restream/reindexer/v5/bindings/builtin"
// OR use Reindexer as standalone server and connect to it via TCP or unix domain socket (if available).
// _ "github.com/restream/reindexer/v5/bindings/cproto"
// OR link Reindexer as static library with bundled server.
// _ "github.com/restream/reindexer/v5/bindings/builtinserver"
// "github.com/restream/reindexer/v5/bindings/builtinserver/config"
)
// Define struct with reindex tags. Fields must be exported - private fields cannot be written into Reindexer
type Item struct {
ID int64 `reindex:"id,,pk"` // 'id' is primary key
Name string `reindex:"name"` // add index by 'name' field
Articles []int `reindex:"articles"` // add index by articles 'articles' array
Year int `reindex:"year,tree"` // add sortable index by 'year' field
}
func main() {
// Init a database instance and choose the binding (builtin)
db, err := reindexer.NewReindex("builtin:///tmp/reindex/testdb")
// OR - Init a database instance and choose the binding (connect to server via TCP sockets)
// Database should be created explicitly via reindexer_tool or via WithCreateDBIfMissing option:
// If server security mode is enabled, then username and password are mandatory
// db, err := reindexer.NewReindex("cproto://user:[email protected]:6534/testdb", reindexer.WithCreateDBIfMissing())
// OR - Init a database instance and choose the binding (connect to server via TCP sockets with TLS support
// using cprotos-protocol and a package tls from the standard GO library)
// Database should be created explicitly via reindexer_tool or via WithCreateDBIfMissing option:
// If server security mode is enabled, then username and password are mandatory
// It is assumed that reindexer RPC-server with TLS support is running at this address. 6535 is the default port for it.
// tlsConfig := tls.Config{
// /*required options*/
// }
// db, err := reindexer.NewReindex("cprotos://user:[email protected]:6535/testdb", reindexer.WithCreateDBIfMissing(), reindexer.WithTLSConfig(&tlsConfig))
// OR - Init a database instance and choose the binding (connect to server via unix domain sockets)
// Unix domain sockets are available on the unix systems only (socket file has to be explicitly set on the server's side with '--urpcaddr' option)
// Database should be created explicitly via reindexer_tool or via WithCreateDBIfMissing option:
// If server security mode is enabled, then username and password are mandatory
// db, err := reindexer.NewReindex("ucproto://user:pass@/tmp/reindexer.socket:/testdb", reindexer.WithCreateDBIfMissing())
// OR - Init a database instance and choose the binding (builtin, with bundled server)
// serverConfig := config.DefaultServerConfig ()
// If server security mode is enabled, then username and password are mandatory
// db, err := reindexer.NewReindex("builtinserver://user:pass@testdb",reindexer.WithServerConfig(100*time.Second, serverConfig))
// Check the error after rx instance init
if err != nil {
panic(err)
}
// Create new namespace with name 'items', which will store structs of type 'Item'
db.OpenNamespace("items", reindexer.DefaultNamespaceOptions(), Item{})
// Generate dataset
for i := 0; i < 100000; i++ {
err := db.Upsert("items", &Item{
ID: int64(i),
Name: "Vasya",
Articles: []int{rand.Int() % 100, rand.Int() % 100},
Year: 2000 + rand.Int()%50,
})
if err != nil {
panic(err)
}
}
// Query a single document
elem, found := db.Query("items").
Where("id", reindexer.EQ, 40).
Get()
if found {
item := elem.(*Item)
fmt.Println("Found document:", *item)
}
// Query multiple documents
query := db.Query("items").
Sort("year", false). // Sort results by 'year' field in ascending order
WhereString("name", reindexer.EQ, "Vasya"). // 'name' must be 'Vasya'
WhereInt("year", reindexer.GT, 2020). // 'year' must be greater than 2020
WhereInt("articles", reindexer.SET, 6, 1, 8). // 'articles' must contain one of [6,1,8]
Limit(10). // Return maximum 10 documents
Offset(0). // from 0 position
ReqTotal() // Calculate the total count of matching documents
// Execute the query and return an iterator
iterator := query.Exec()
// Iterator must be closed
defer iterator.Close()
fmt.Println("Found", iterator.TotalCount(), "total documents, first", iterator.Count(), "documents:")
// Iterate over results
for iterator.Next() {
// Get the next document and cast it to a pointer
elem := iterator.Object().(*Item)
fmt.Println(*elem)
}
// Check the error
if err = iterator.Error(); err != nil {
panic(err)
}
}
There are also some basic samples for C++ and Go here
SQL compatible interface
As an alternative to the Query builder, Reindexer provides SQL-compatible query interface. Here is a sample of SQL interface usage:
...
iterator := db.ExecSQL ("SELECT * FROM items WHERE name='Vasya' AND year > 2020 AND articles IN (6,1,8) ORDER BY year LIMIT 10")
...
Please note, that the Query Builder interface is preferred: it has more features and is faster than the SQL interface
String literals should be enclosed in single quotes.
Composite indexes should be enclosed in double quotes.
SELECT * FROM items WHERE "field1+field2" = 'Vasya'
If the field name does not start with alpha, '_' or '#' it must be enclosed in double quotes, examples:
UPDATE items DROP "123"
SELECT * FROM ns WHERE "123" = 'some_value'
SELECT * FROM ns WHERE "123abc" = 123
DELETE FROM ns WHERE "123abc123" = 111
Simple Joins may be done via default SQL syntax:
SELECT * FROM ns INNER JOIN ns2 ON ns2.id = ns.fk_id WHERE a > 0
Joins with condition on the left namespace must use subquery-like syntax:
SELECT * FROM ns WHERE a > 0 AND INNER JOIN (SELECT * FROM ns2 WHERE b > 10 AND c = 1) ON ns2.id = ns.fk_id
Subquery can also be a part of the WHERE-condition:
SELECT * FROM ns WHERE (SELECT * FROM ns2 WHERE id < 10 LIMIT 0) IS NOT NULL
SELECT * FROM ns WHERE id = (SELECT id FROM ns2 WHERE id < 10)
SELECT * FROM ns WHERE (SELECT COUNT(*) FROM ns2 WHERE id < 10) > 18
Installation
Reindexer can run in 3 different modes:
embedded (builtin)Reindexer is embedded into application as static library, and does not require separate server process.embedded with server (builtinserver)Reindexer is embedded into the application as a static library and starts a server. In this mode, other clients can connect to the application via cproto, cprotos, ucproto, http, or https.standaloneReindexer runs as a standalone server; the application connects to Reindexer via network or Unix domain sockets.
Installation for server mode
In this mode Reindexer's Go-binding does not depend on reindexer's static library.
- Install Reindexer Server
- go get -a github.com/restream/reindexer/v5
Official docker image
The simplest way to get Reindexer server is to pull and run the Docker image from Docker Hub.
docker run -p9088:9088 -p6534:6534 -it reindexer/reindexer
Installation for embedded mode
Prerequisites
Reindexer's core is written in C++20 and uses LevelDB as the storage backend, so CMake, a C++20 toolchain, and LevelDB must be installed before installing Reindexer. Also, OpenMP and BLAS/LAPACK libraries may be required for full vector index support.
To build Reindexer, g++ 10+, clang 15+, or mingw64 is required.
In embedded mode, Reindexer's Go binding depends on Reindexer's static libraries (core, server, and resource).
Get Reindexer using go.mod
This way is recommended and will fit for the most scenarios.
Go modules with go.mod do not allow to build C++ libraries in modules' directories. Go-binding will use pkg-config to detect libraries' directories.
Reindexer's libraries must be either installed from sources or from prebuilt package via package manager.
Then get the module:
go get -a github.com/restream/reindexer/v5
Get Reindexer using go.mod and replace
If you need modified Reindexer's sources, you can use replace like that.
- Download and build reindexer:
# Clone reindexer via git. It's also possible to use 'go get -a github.com/restream/reindexer/v5', but its behavior may vary depending on Go's version
git clone https://github.com/restream/reindexer.git $GOPATH/src/reindexer
bash $GOPATH/src/reindexer/dependencies.sh
# Generate builtin binding
cd $GOPATH/src/reindexer
go generate ./bindings/builtin
# Optional (build builtin server binding)
go generate ./bindings/builtinserver
- Add Reindexer's module to your application's go.mod and replace it with the local package:
# Go to your app's directory
cd /your/app/path
go get -a github.com/restream/reindexer/v5
go mod edit -replace github.com/restream/reindexer/v5=$GOPATH/src/reindexer
In this case, Go-binding will generate explicit libraries' and paths' list and will not use pkg-config.
Get Reindexer for apps without go.mod (vendoring)
If you're not using go.mod it's possible to get and build reindexer from sources this way:
export GO111MODULE=off # Disable go1.11 modules
# Go to your app's directory
cd /your/app/path
# Clone reindexer via git. It's also possible to use 'go get -a github.com/restream/reindexer', but its behavior may vary depending on Go's version
git clone https://github.com/restream/reindexer.git vendor/github.com/restream/reindexer/v5
# Generate builtin binding
go generate -x ./vendor/github.com/restream/reindexer/v5/bindings/builtin
# Optional (build builtin server binding)
go generate -x ./vendor/github.com/restream/reindexer/v5/bindings/builtinserver
Get Reindexer using go.mod (vendoring)
Go does not support proper vendoring for CGO code (https://github.com/golang/go/issues/26366), however, it's possible to use vend to copy Reindexer's sources into vendor-directory.
With vend you'll be able to call go generate -mod=vendor for builtin and builtinserver, placed in your vendor-directory.
It's also possible to simply copy Reindexer's sources into your project using git clone.
In these cases all the dependencies from Reindexer's go.mod must be installed manually with proper versions.
Advanced Usage
Index Types and Their Capabilities
Internally, structs are split into two parts:
- indexed fields, marked with
reindexstruct tag - tuple of non-indexed fields
Queries are possible only on the indexed fields, marked with reindex tag. The reindex tag contains the index name, type, and additional options:
reindex:"<name>[[,<type>],<opts>]"
name- index name.type- index type:hash- fast select by EQ and SET match. Used by default. Allows slow and inefficient sorting by field.tree- fast select by RANGE, GT, and LT matches. A bit slower for EQ and SET matches thanhashindex. Allows fast sorting results by field.text- full-text search index. Details of full-text search are described here-- column index. Can't perform fast select because it's implemented with a full-scan technique. Has the smallest memory overhead.ttl- TTL index that works only with int64 fields. These indexes are quite convenient for representation of date fields (stored as UNIX timestamps) that expire after specified amount of seconds.rtree- available only DWITHIN match. Acceptable only for[2]float64(orreindexer.Point) field type. For details see geometry subsection.
opts- additional index options:pk- field is part of a primary key. Struct must have at least 1 field tagged withpkcomposite– create composite index. The field type must be an empty struct:struct{}.joined– field is a recipient for join. The field type must be[]*SubitemType.dense- reduce index size. Forhashandtreeit will save 8-16 bytes per unique key value. For-it will save 4-16 bytes per each element. Useful for indexes with high selectivity, but fortreeandhashindexes with low selectivity can seriously decrease update performance. Alsodensewill slow down wide fullscan queries on-indexes, due to lack of CPU cache optimization. For thetextindex does not reduce index' size, but switches index' rebuild to the single-thread mode.sparse- Row (document) contains a value of Sparse index only in case if it's set on purpose - there are no empty (or default) records of this type of indexes in the row (document). It allows to save RAM, but it will cost you performance - it works a bit slower than regular indexes.collate_numeric- create string index that provides values order in numeric sequence. The field type must be a string.collate_ascii- create a case-insensitive string index that works with ASCII. The field type must be a string.collate_utf8- create a case-insensitive string index that works with UTF-8. The field type must be a string.collate_custom=<ORDER>- create custom order string index. The field type must be a string.<ORDER>is sequence of letters, which defines sort order.linear,quadratic,greeneorrstar- specify algorithm for construction ofrtreeindex (by defaultrstar). For details see geometry subsection.uuid- store this value as UUID. This is much more effective from the RAM/network consumption standpoint for UUIDs than strings. Onlyhashand-index types are supported for UUIDs. Can be used with any UUID variant, except variant 0
Fields with regular indexes are not nullable. Condition is NULL is supported only by sparse and array indexes.
NULL-values filtration
Conditions IS NULL/IS NOT NULL may have different behavior depending on target index type.
In the tables below you can find results of IS NULL for different values/indexes/jsonpaths (IS NOT NULL will have the opposite value).
Shortcuts:
+- document will be in the results;-- document will not be in the results;n- document is not valid.
Non-nested fields. Index has json_paths: ["f"] or json_paths: ["obj"] depending on context:
| Documents | Non-indexed | Indexed array | Sparse | Sparse array |
|---|---|---|---|---|
{} | + | + | + | + |
{"f": null} | + | + | + | + |
{"f": []} | + | + | + | + |
{"f": [null]} | + | - | + | + |
{"f": [1, null]} | + | - | + | + |
{"f": 0} | - | - | - | - |
{"f": [0]} | - | - | - | - |
{"obj": {}} | - | n | n | n |
{"obj": {"f": null}} | - | n | n | n |
{"obj": {"f": null, "a": 0}} | - | n | n | n |
Non-nested fields. Index has json_paths: ["f1", "f2"] or json_paths: ["obj1", "obj2"] depending on context:
| Documents | Array with multiple jsonpaths |
|---|---|
{}, {} | + |
{"f1": 1}, {} | - |
{"f1": null}, {"f2": null} | + |
{"f1": 1}, {"f2": null} | - |
{"f1": []}, {"f2": []} | + |
{"f1": [1]}, {"f2": []} | - |
{"f1": [null]}, {"f2": [null]} | - |
{"f1": [1, null]}, {"f2": [1, null]} | - |
{"f1": 0}, {"f2": 0} | - |
{"f1": [0]}, {"f2": [0]} | - |
{"obj1": {}}, {"obj2": {}} | n |
{"obj1": {"f": null}}, {"obj2": {"f": null}} | n |
{"obj1": {"f": null, "a": 0}}, {"obj2": {"f": null, "a": 0}} | n |
Nested fields. Index has json_paths: ["obj.f"]:
| Documents | Indexed array | Sparse | Sparse array |
|---|---|---|---|
{"obj": {}} | + | + | + |
{"obj": {"f": null}} | + | + | + |
{"obj": {"f": null, "a": 0}} | + | + | + |
{"obj": {"f": [null]}} | - | n | + |
{"obj": {"f": [1, null]}} | - | n | + |
Nested fields. Index has json_paths: ["obj1.f", "obj2.f"]:
| Documents | Array with multiple jsonpaths |
|---|---|
{"obj1": {}}, {"obj2": {}} | + |
{"obj1": {"f": 1}}, {"obj2": {}} | - |
{"obj1": {"f": null}}, {"obj2": {"f": null}} | + |
{"obj1": {"f": 1}}, {"obj2": {"f": null}} | - |
{"obj1": {"f": null, "a": 0}}, {"obj2": {"f": null, "a": 0}} | + |
{"obj1": {"f": 1, "a": 0}}, {"obj2": {"f": null, "a": 0}} | - |
{"obj1": {"f": [null]}}, {"obj2": {"f": [null]}} | - |
{"obj1": {"f": [1, null]}}, {"obj2": {"f": [1, null]}} | - |
Nested Structs
By default, Reindexer scans all nested structs and adds their fields to the namespace (as well as indexes specified).
During index tags scanning, private (non-exported) fields, fields tagged with reindex:"-", and fields tagged with json:"-" will be skipped.
type Actor struct {
Name string `reindex:"actor_name"`
Age int `reindex:"age"`
}
type BaseItem struct {
ID int64 `reindex:"id,hash,pk"`
UUIDValue string `reindex:"uuid_value,hash,uuid"`
}
type ComplexItem struct {
BaseItem // Index fields of BaseItem will be added to reindex
Actor []Actor // Index fields ("name" and "age") of Actor will be added to reindex as array-indexes
Name string `reindex:"name"` // Hash-index for "name"
Year int `reindex:"year,tree"` // Tree-index for "year"
Value int `reindex:"value,-"` // Store(column)-index for "value"
Metainfo int `json:"-"` // Field "MetaInfo" will not be stored in reindexer
Parent *Item `reindex:"-"` // Index fields of "Parent" will NOT be added to reindex and all of the "Parent" exported content will be stored as non-indexed data
ParentHidden *Item `json:"-"` // Indexes and fields of "ParentHidden" will NOT be added to reindexer
privateParent *Item // Indexes and fields of "ParentHidden" will NOT be added to reindexer (same as with `json:"-"`)
AnotherActor Actor `reindex:"actor"` // Index fields of "AnotherActor" will be added to reindexer with prefix "actor." (in this example two indexes will be created: "actor.actor_name" and "actor.age")
}
Sort
Reindexer can sort documents by fields (including nested and fields of the joined namespaces) or by expressions in ascending or descending order.
To sort by non-indexed fields, all the values must be convertible to each other, i.e. either have the same types or be one of the numeric types (bool, int, int64, or float).
Sort expressions can contain:
- field and index names (including nested fields and fields of the joined namespaces) of
bool,int,int64,float, orstringtypes. All the values must be convertible to numbers ignoring leading and trailing spaces; - numbers;
- functions
rank(),abs(),ST_Distance()andmerged_query_id(); - parenthesis;
- arithmetic operations:
+,-(unary and binary),*and/.
If a field name is followed by +, it must be separated by a space to distinguish a composite index name.
Fields of the joined namespaces must be written like this: joined_namespace.field.
Abs() means absolute value of an argument.
Rank() means fulltext or KNN rank of match and is applicable only in fulltext and KNN queries.
ST_Distance() means distance between geometry points (see geometry subsection). The points could be columns in current or joined namespaces or fixed point in format ST_GeomFromText('point(1 -3)')
hash() or hash(seed) returns a hash (uint32) from the record id. If seed is not specified, it is generated randomly. This function is useful to achieve randomized sort order.
merged_query_id() returns the index of the current query in a MERGE. The main query and every joined query are 0. Each query added with MERGE is 1, 2, 3, ... in that order. A query without MERGE returns 0.
In SQL query sort expression must be quoted.
type Person struct {
Name string `reindex:"name"`
Age int `reindex:"age"`
}
type City struct {
Id int `reindex:"id"`
NumberOfPopulation int `reindex:"population"`
Center reindexer.Point `reindex:"center,rtree,linear"`
}
type Actor struct {
ID int `reindex:"id"`
PersonData Person `reindex:"person"`
Price int `reindex:"price"`
Description string `reindex:"description,text"`
BirthPlace int `reindex:"birth_place_id"`
Location reindexer.Point `reindex:"location,rtree,greene"`
}
....
query := db.Query("actors").Sort("id", true) // Sort by field
....
query = db.Query("actors").Sort("person.age", true) // Sort by nested field
....
// Sort by joined field
// Works for inner join only, when each item from left namespace has exactly one joined item from right namespace
query = db.Query("actors").
InnerJoin(db.Query("cities")).On("birth_place_id", reindexer.EQ, "id").
Sort("cities.population", true)
....
// Sort by expression:
query = db.Query("actors").Sort("person.age / -10 + price / 1000 * (id - 5)", true)
....
query = db.Query("actors").Where("description", reindexer.EQ, "ququ").
Sort("rank() + id / 100", true) // Sort with rank
....
// Sort by geometry distance
query = db.Query("actors").
Join(db.Query("cities")).On("birth_place_id", reindexer.EQ, "id").
SortStPointDistance(cities.center, reindexer.Point{1.0, -3.0}, true).
SortStFieldDistance("location", "cities.center", true)
....
// Random sorting with stable seed:
query = db.Query("actors").Sort("hash(123123)", true)
....
// In SQL query:
iterator := db.ExecSQL ("SELECT * FROM actors ORDER BY person.name ASC")
....
iterator := db.ExecSQL ("SELECT * FROM actors WHERE description = 'ququ' ORDER BY 'rank() + id / 100' DESC")
....
iterator := db.ExecSQL ("SELECT * FROM actors ORDER BY 'ST_Distance(location, ST_GeomFromText(\'point(1 -3)\'))' ASC")
....
iterator := db.ExecSQL ("SELECT * FROM actors ORDER BY 'ST_Distance(location, cities.center)' ASC")
....
iterator := db.ExecSQL ("SELECT * FROM actors ORDER BY 'hash(123123)'")
It is also possible to set a custom sort order like this
type SortModeCustomItem struct {
ID int `reindex:"id,,pk"`
InsItem string `reindex:"item_custom,hash,collate_custom=a-zA-Z0-9"`
}
or like this
type SortModeCustomItem struct {
ID int `reindex:"id,,pk"`
InsItem string `reindex:"item_custom,hash,collate_custom=АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭ-ЯAaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0-9ЁёЙйэ-я"`
}
The very first character in this list has the highest priority, priority of the last character is the smallest one. It means that sorting algorithm will put items that start with the first character before others. If some characters are skipped their priorities would have their usual values (according to characters in the list).
Forced sort
It's possible to force sorting order by pulling some specific keys on the top of the selection results (works with ASC sort only):
query := db.Query("actors").Sort("id", false, 10, 15, 17) // Documents with IDs 10, 15, 17 will be on the top (if corresponding documents exist)
Sorting, tie-break and deduplication of merged queries are described in Merge.
Counting
Reindexer supports two versions of counting aggregation:
count()- this aggregation counts all the documents that match the query's filters;count_cached()- this allows Reindexer to use cached count results. Generally this method is faster (especially for queries with lowlimit()values); however, it may return outdated results if the cache was not invalidated after some documents' changes.
Go example for count():
query := db.Query("items").
WhereString("name", reindexer.EQ, "Vasya").
Limit(10).
ReqTotal()
it := query.MustExec()
it.TotalCount() // Get total count of items, matching condition "WhereString("name", reindexer.EQ, "Vasya")"
Go example for count_cached():
query := db.Query("items").
WhereString("name", reindexer.EQ, "Vasya").
Limit(10).
CachedTotal()
it := query.MustExec()
it.TotalCount() // Get total count of items, matching condition "WhereString("name", reindexer.EQ, "Vasya"), using queries cache"
If you need to get total count field in JSON representation via Go-API, you have to set name for this field (otherwise it won't be serialized to JSON):
query := db.Query("items").
WhereString("name", reindexer.EQ, "Vasya").
Limit(10).
ReqTotal("my_total_count_name")
json, err := query.ExecToJson().FetchAll()
Text pattern search with LIKE condition
For simple searching text pattern in string fields condition LIKE can be used. It looks for strings matching the pattern. In the pattern _ means any char and % means any sequence of chars.
Go example:
query := db.Query("items").
Where("field", reindexer.LIKE, "pattern")
SQL example:
SELECT * FROM items WHERE fields LIKE 'pattern'
'me_t' corresponds to 'meet', 'meat', 'melt' and so on '%tion' corresponds to 'tion', 'condition', 'creation' and so on
CAUTION: condition LIKE uses scan method. It can be used for debug purposes or within queries with another good selective conditions.
Generally for full text search with reasonable speed we recommend to use fulltext index.
Functions
Reindexer provides built-in functions that can be used within WHERE clauses to enable advanced filtering capabilities beyond simple field comparisons.
flat_array_len(field_name)
The flat_array_len function returns the length or cardinality of a specified field, making it particularly useful for filtering based on array sizes or field presence. The flat_array_len function can be used in both SELECT and UPDATE queries.
Behavior by Field Type:
- Array Fields: returns the number of elements in the array
- Scalar Fields (integers, strings, etc.): always returns 1
- Object Fields: always returns 1
- Nested Array Elements: returns the count of occurrences when the field is nested within arrays
Examples:
-- Find documents with exactly 3 tags
SELECT * FROM items WHERE flat_array_len(tags) = 3
-- Find documents with more than 5 comments
SELECT * FROM articles WHERE flat_array_len(comments) > 5
-- Find documents with at least one phone number
SELECT * FROM users WHERE flat_array_len(phone_numbers) >= 1
-- Filter objects nested in arrays (returns count of occurrences)
SELECT * FROM orders WHERE flat_array_len(items.product_id) = 2
-- Update field value with flat_array_len function
UPDATE items SET size = flat_array_len(comments) where id = 100;
// Find social media posts with between 10 and 50 comments
// and at least 3 attached media files
query := DB.Query("posts").
WhereFlatArrayLen("comment", reindexer.GE, 10).
WhereFlatArrayLen("comment", reindexer.LE, 50).
WhereFlatArrayLen("media", reindexer.GE, 3)
// Execute query
it := query.Exec()
// Update field 'size' with flat_array_len function
res, err := DB.Query("items").Where("id", reindexer.EQ, 1).SetExpression("size", "flat_array_len(comments)").Update().FetchAll()
Notes:
flat_array_lenfunction operates efficiently on indexed fields- Returns 0 if the specified field does not exist in a document
- Supports the following comparison operators: (=, >, >=, <, <=, Range, Set)
- Can be used in both SELECT and UPDATE queries
now(unit)
The now() function returns the current system timestamp, making it particularly useful for time-based filtering and data synchronization. This function can be used in both SELECT and UPDATE queries.
Arguments:
sec- returns timestamp in seconds (default if no argument is provided)msec- returns timestamp in millisecondsusec- returns timestamp in microsecondsnsec- returns timestamp in nanoseconds
Examples:
-- Find documents with exact current timestamp
SELECT * FROM logs WHERE timestamp = now(sec)
-- Filter documents with timestamps in the future (milliseconds)
SELECT * FROM schedules WHERE start_time > now(msec)
-- Update a document with current timestamp in nanoseconds
UPDATE items SET last_modified = now(nsec) WHERE id = 100
-- Set both creation time and modification time
UPDATE users SET created_at = now(sec), updated_at = now(sec) WHERE status = 'new'
// Find events that occurred in the past
query := DB.Query("events").
WhereExpressions(reindexer.Field{Name: "timestamp"}, reindexer.LE, reindexer.Now{TimeUnit: reindexer.Sec})
// Update document with current microsecond timestamp
res, err := DB.Query("items").
Where("id", reindexer.EQ, 42).
SetExpression("updated_at", "now(usec)").
Update().
FetchAll()
// Insert document with serial ID and current timestamp
item := Item{Name: "test"}
DB.Insert("items", &item, "id=serial()", "created_at=now(msec)")
Notes:
- The returned timestamp represents seconds (or subunits) since the Unix epoch (January 1, 1970)
- Time resolution depends on the specified unit - use nsec for maximum precision
- All instances of Now() within a single query share the same value, which is computed at the start of the query execution
- Useful for implementing TTL (Time-To-Live) functionality and audit logging
Update queries
UPDATE queries are used to modify existing items of a namespace. There are several kinds of update queries: updating existing fields, adding new fields and dropping existing non-indexed fields.
General UPDATE SQL-Syntax:
UPDATE nsName
SET field1 = value1, field2 = value2, ..
WHERE condition;
It is also possible to use arithmetic expressions with +, -, /, * and brackets
UPDATE NS SET field1 = field2+field3-(field4+5)/2
including functions like now(), sec() and serial(). To use expressions from Golang code SetExpression() method needs to be called instead of Set().
To make an array-field empty
UPDATE NS SET arrayfield = [] WHERE id = 100
and set it to null
UPDATE NS SET field = NULL WHERE id > 100
In case of non-indexed fields, setting its value to a value of a different type will replace it completely; in case of indexed fields, it is only possible to convert it from adjacent type (integral types and bool), numeric strings (like "123456") to integral types and back. Setting indexed field to null resets it to a default value.
It is possible to add new fields to existing items
UPDATE NS SET newField = 'Brand new!' WHERE id > 100
and even add a new field by a complex nested path like this
UPDATE NS SET nested.nested2.nested3.nested4.newField = 'new nested field!' WHERE id > 100
will create the following nested objects: nested, nested2, nested3, nested4 and newField as a member of object nested4.
Example of using Update queries in golang code:
db.Query("items").Where("id", reindexer.EQ, 40).Set("field1", value1).Set("field2", value2).Update()
- there can be multiple
.Setexpressions - one for a field. Also, it is possible to combine severalSet...expressions of different types in one query, like this:Set().SetExpression().SetObject()...
Updating a missing indexed field
UPDATE can set an indexed field that was absent from a document. The field is added at the index's JSON path, appears in the updated document, and can be found through the index. When the namespace uses persistent storage, the new value is preserved after a restart.
For example, suppose index idx has json_paths: ["field"] and a document contains {"id": 0, "data": "12345"}:
UPDATE ns SET field = 10 WHERE id = 0
SELECT * FROM ns WHERE idx = 10
The updated document is {"id": 0, "data": "12345", "field": 10}, and the SELECT query returns it. If the index name differs from its JSON path, the SET clause can use either name or JSON path.
For an index with multiple JSON paths, specify the path to update instead of the index name. For example, if idx has json_paths: ["field1", "field2"], use SET field1 = 10 or SET field2 = 10. Updating idx by name is ambiguous and returns an error.
Update queries with inner joins and subqueries
Inner join and subquery may also be used as WHERE conditions. Inner joins don't actually join items, but are applied as filtering conditions.
SQL syntax:
UPDATE ns SET val = 100 WHERE INNER JOIN (SELECT * FROM join_ns WHERE id > 100) ON ns.id = join_ns.id
UPDATE ns SET val = 100 WHERE field = (SELECT id FROM subq_ns WHERE val = 200)
Go syntax:
db.Query(items).
OpenBracket()
q.InnerJoin(DB.Query(joinItems).Where("id", reindexer.GT, 100)).
On("ID", reindexer.EQ, "ID")
q.CloseBracket().
Set("val", 100).Update()
DB.Query(items).
Where("field", reindexer.EQ,
DB.Query(subq_ns).Select("id").Where("val", reindexer.EQ, 200)
).
Set("val", 100).Update()
Update field with object
Reindexer enables to update and add object fields. Object can be set by either a struct, a map or a byte array (that is a JSON version of object representation).
type ClientData struct {
Name string `reindex:"name" json:"name"`
Age int `reindex:"age" json:"age"`
Address int `reindex:"year" json:"year"`
Occupation string `reindex:"occupation" json:"occupation"`
TaxYear int `reindex:tax_year json:"tax_year"`
TaxConsultant string `reindex:tax_consultant json:"tax_consultant"`
}
type Client struct {
ID int `reindex:"id" json:"id"`
Data ClientData `reindex:"client_data" json:"client_data"`
...
}
clientData := updateClientData(clientId)
db.Query("clients").Where("id", reindexer.EQ, 100).SetObject("client_data", clientData).Update()
In this case, Map in golang can only work with string as a key. map[string]interface{} is a perfect choice.
Updating an object field via SQL statement:
UPDATE clients SET client_data = {"Name":"John Doe","Age":40,"Address":"Fifth Avenue, Manhattan","Occupation":"Bank Manager","TaxYear":1999,"TaxConsultant":"Jane Smith"} WHERE id = 100;
Remove field via update-query
UPDATE DROP removes fields or array elements from the items matched by the query. It changes item data, but does not remove index definitions or alter the namespace schema.
Complete fields can be dropped if they are non-indexed or belong to sparse indexes. Dropping a non-sparse indexed field is not supported.
SQL syntax:
UPDATE nsName
DROP field1, field2, ..
WHERE condition;
Nested JSON paths and array indexes are supported:
-- Remove a field
UPDATE items DROP obsolete WHERE id = 100;
-- Remove a nested field
UPDATE items DROP metadata.legacy WHERE id = 100;
-- Remove the element at the zero-based index 2
UPDATE items DROP tags[2] WHERE id = 100;
-- Remove all existing array elements, leaving an empty array
UPDATE items DROP tags[*] WHERE id = 100;
Paths are resolved independently for each matched item and for each array branch. A path may legitimately exist only in some branches of a heterogeneous array. Branches where the path is missing are skipped without an error, while the operation is applied to every node that was found. An array index outside the array bounds is also treated as a missing target and produces no change.
For example, the following query removes the first tags element from the first and third branches and skips the second branch:
{
"items": [
{"tags": [1, 2]},
{"name": "item"},
{"tags": [3, 4]}
]
}
UPDATE nsName DROP items[*].tags[0];
DSL syntax:
{
"namespace": "items",
"type": "update",
"drop_fields": [
"obsolete",
"metadata.legacy",
"tags[2]"
]
}
Go API:
db.Query("items").Where("id", reindexer.EQ, 40).Drop("field1").Drop("field2").Update()
Update array elements by indexes
Reindexer update mechanism enables to modify array fields: to modify a certain item of an existing array or even to replace an entire field.
To update an item subscription operator syntax is used:
UPDATE NS SET array[*].prices[0] = 9999 WHERE id = 5
where * means all items.
To update entire array the following is used:
UPDATE NS SET prices = [999, 1999, 2999] WHERE id = 9
any non-indexed field can be easily converted to array using this syntax.
Reindexer also allows to update items of object arrays:
UPDATE NS SET extra.objects[0] = {"Id":0,"Description":"Updated!"} WHERE id = 9
also like this
db.Query("clients").Where("id", reindexer.EQ, 100).SetObject("extra.objects[0]", updatedValue).Update()
Reindexer supports heterogeneous arrays:
UPDATE NS SET info = ["hi", "bro", 111, 2.71] WHERE id = 9
q := DB.Query(ns).Where("id", reindexer.EQ, 1).Set("array", []interface{}{"whatsup", 777, "bro"})
res, err := q.Update().FetchAll()
Index array-fields support values that can be converted to an index type only. When saved, such values may change precision due to conversion.
UPDATE NS SET prices_idx = [11, '2', 3]
An array on the right-hand side replaces the entire value selected on the left-hand side; it does not append elements. This also works for indexed array fields and nested arrays. For example, given idx = [[1, 2], [3, 4]], each of the following statements produces the shown value of idx (run each statement independently):
| UPDATE statement | Resulting idx |
|---|---|
UPDATE NS SET idx = [[5, 6], [7, 8]] WHERE id = 9 | [[5, 6], [7, 8]] |
UPDATE NS SET idx[0] = [5, 6] WHERE id = 9 | [[5, 6], [3, 4]] |
UPDATE NS SET idx[*] = [5, 6] WHERE id = 9 | [[5, 6], [5, 6]] |
UPDATE NS SET idx[0][0] = [5, 6] WHERE id = 9 | [[[5, 6], 2], [3, 4]] |
UPDATE NS SET idx[0][*] = [5, 6] WHERE id = 9 | [[[5, 6], [5, 6]], [3, 4]] |
UPDATE NS SET idx[*][0] = [5, 6] WHERE id = 9 | [[[5, 6], 2], [[5, 6], 4]] |
UPDATE NS SET idx[*][*] = [5, 6] WHERE id = 9 | [[[5, 6], [5, 6]], [[5, 6], [5, 6]]] |
[*] selects every existing element at that level and assigns the right-hand value to each one. It does not create new elements. The selected element can itself be replaced with an array, even if it previously held a scalar. Indexed array fields still require every resulting scalar value to be convertible to the index type.
The same syntax applies to arrays of objects and objects containing arrays. Given nested = [{"values": [1]}, {"values": [2]}]:
UPDATE NS SET nested[*].values = [3, 4] WHERE id = 9
This produces nested = [{"values": [3, 4]}, {"values": [3, 4]}]. To replace an entire object in the array, use a path such as nested[0] and an object value:
UPDATE NS SET nested[0] = {"values": [5, 6]} WHERE id = 9
For a point field, an array of two coordinates replaces the whole point, for example UPDATE NS SET location = [10.5, 20.5] WHERE id = 9. A float vector field likewise accepts a complete vector with the dimension configured for its index, such as UPDATE NS SET vec_idx = [0.1, 0.2, 0.3] WHERE id = 9. If the vector index is configured as an array, use an array of vectors to replace the whole field, or select one vector to replace:
UPDATE NS SET vec_idx = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]] WHERE id = 9
UPDATE NS SET vec_idx[0] = [0.7, 0.8, 0.9] WHERE id = 9
Vector coordinates cannot be updated through nested array indexes such as vec_idx[0][0]; replace the vector instead.
Concatenate arrays
To add items to an existing array the following syntax is supported:
UPDATE NS SET integer_array = integer_array || [5,6,7,8]
and
UPDATE NS SET integer_array = [1,2,3,4,5] || integer_array
The first one adds elements to the end of integer_array, the second one adds 5 items to the front of it. To make this code work in Golang SetExpression() should be used instead of Set().
Remove array elements by values
To remove items by value into an existing array the following syntax is supported:
UPDATE NS SET integer_array = array_remove(integer_array, [5,6,7,8])
and
UPDATE NS SET integer_array = array_remove_once(integer_array, [5,6,7,8,6])
The first one removes all occurrences of the listed values in integer_array, the second one deletes only the first occurrence found. To make this code work in Golang SetExpression() should be used instead of Set().
If you need to remove one value, you can use square brackets [5] or simple value 5.
UPDATE NS SET integer_array = array_remove(integer_array, [5])
update ns set integer_array = array_remove(integer_array, 5)
Remove command can be combined with array concatenate:
UPDATE NS SET integer_array = array_remove_once(integer_array, [5,6,7,8]) || [1,2,3]
also like this
db.Query("main_ns").SetExpression("integer_array", "array_remove(integer_array, [5,6,7,8]) || [1,2,3]").Update()
It is possible to remove the values of the second field from the values of the first field. And also add new values etc. Note: The first parameter in commands is expected to be an array/field-array, the second parameter can be an array/scalar/field-array/field-scalar. For values compatibility/convertibility required
UPDATE NS SET integer_array = [3] || array_remove(integer_array, integer_array2) || integer_array3 || array_remove_once(integer_array, [8,1]) || [2,4]
UPDATE NS SET integer_array = array_remove(integer_array, integer_array2) || array_remove(integer_array, integer_array3) || array_remove_once(integer_array, [33,777])
db.Query("main_ns").SetExpression("integer_array", "[3] || array_remove(integer_array, integer_array2) || integer_array3 || array_remove(integer_array, [8,1]) || [2,4]").Update()
Delete queries
Reindexer also supports delete queries. Those queries may contain conditions, inner joins, and subqueries. Inner joins don't actually join items, but are applied as filtering conditions.
SQL syntax for delete queries:
DELETE FROM ns WHERE field = 111
DELETE FROM ns WHERE id < 5 OR INNER JOIN ns_join ON ns.id = ns_join.id
DELETE FROM ns WHERE id < 5 OR INNER JOIN (SELECT * FROM ns_join WHERE age > 18) ON ns.id = ns_join.id
DELETE FROM ns WHERE id = (SELECT id FROM sub_ns WHERE age < 13)
Go syntax for delete queries:
// Delete query with INNER JOIN
DB.Query(ns).Where("id", reindexer.EQ, 1).
OpenBracket().
InnerJoin(
DB.Query(join_ns).Where("DEVICE", reindexer.EQ, "android"),
"some random name").
On("PRICE_ID", reindexer.SET, "ID")
CloseBracket().
Delete()
// Delete query with subquery
DB.Query(ns).
Where("ID", reindexer.EQ,
DB.Query(sub_ns).Select("id").Where("age", reindexer.LT, 13)
).
Delete()
Truncate queries
Truncate is more effective alternative to delete-query for cases, when you need to remove all the items from namespace without filtering:
TRUNCATE ns
Go syntax:
DB.TruncateNamespace(ns)
Unlike queries like delete * from ns, truncate also deallocates dynamic memory associated with indexing structures and does not bloat WAL with serialized primary keys of deleted items.
Transactions and batch update
Reindexer supports transactions. Transactions perform atomic namespace updates. There are synchronous and async transactions available. To start transaction method db.BeginTx() is used. This method creates transaction object, which provides usual Update/Upsert/Insert/Delete interface for application.
For RPC clients there is transactions count limitation - each connection can't have more than 1024 open transactions at the same time.
Synchronous mode
// Create new transaction object
tx, err := db.BeginTx("items");
if err != nil {
panic(err)
}
// Fill transaction object
tx.Upsert(&Item{ID: 100})
tx.Upsert(&Item{ID: 101})
tx.Query().WhereInt("id", reindexer.EQ, 102).Set("Name", "Petya").Update()
// Apply transaction
if err := tx.Commit(); err != nil {
panic(err)
}
Async batch mode
For speed up insertion of bulk records async mode can be used.
// Create new transaction object
tx, err := db.BeginTx("items");
if err != nil {
panic(err)
}
// Prepare transaction object async.
tx.UpsertAsync(&Item{ID: 100},func(err error) {})
tx.UpsertAsync(&Item{ID: 100},func(err error) {})
// Wait for async operations done, and apply transaction.
if err := tx.Commit(); err != nil {
panic(err)
}
The second argument of UpsertAsync is completion function, which will be called after receiving server response. Also, if any error occurred during prepare process, then tx.Commit should
return an error.
So it is enough, to check error returned by tx.Commit - to be sure, that all data has been successfully committed or not.
Transactions commit strategies
Depending on amount of changes in transaction there are 2 possible Commit strategies:
- Locked atomic update. Reindexer locks namespace and applying all changes under common lock. This mode is used with small amounts of changes.
- Copy & atomic replace. In this mode Reindexer makes namespace's snapshot, applying all changes to this snapshot, and atomically replaces namespace without lock
The amount of data for selecting a Commit strategy can be selected in the namespace configuration. Check fields StartCopyPolicyTxSize, CopyPolicyMultiplier and TxSizeToAlwaysCopy in struct DBNamespacesConfig(describer.go)
If transaction size is less than TxSizeToAlwaysCopy, reindexer uses extra heuristic and trying to avoid namespace copying, if there were no selecting queries seen for this namespace.
In some cases this heuristic may increase selects latency, so it may be disabled by setting REINDEXER_NOTXHEURISTIC env variable to any non-empty value.
Implementation notes
- Transaction object is not thread safe and can't be used from different goroutines;
- Transaction object holds Reindexer's resources, therefore application should explicitly call Rollback or Commit, otherwise resources will leak;
- It is safe to call Rollback after Commit;
- It is possible to call Query from transaction by call
tx.Query("ns").Exec() ...; - Only serializable isolation is available, i.e. each transaction takes exclusive lock over the target namespace until all the steps of the transaction committed.
Merge
MERGE appends the rows of one or more queries to the main query. The queries may read different namespaces. Limit and Offset belong to the main query and are applied to the merged result; an inner query cannot set its own limit or offset. Joins inside a merged query are described in Join. Aggregations of the main query apply to every merged subquery; see Aggregations.
SELECT * FROM books WHERE year > 2000
MERGE (SELECT * FROM archived_books WHERE year > 1990)
it := db.Query("books").
Where("year", reindexer.GT, 2000).
Merge(db.Query("archived_books").Where("year", reindexer.GT, 1990)).
Exec()
Without Sort() / ORDER BY, rows of the main query come first, then each merged query in the order it was written. Within one query the smaller row id comes first.
Sorting
Sort() / ORDER BY belongs to the main query. ORDER BY inside MERGE (...) is rejected, except for a hybrid reranker (Hybrid search). The sort is applied in every merged namespace and then to the concatenated rows. The same field name is resolved separately in each namespace. A field missing from one of the namespaces is checked with that query's strict mode.
When the sort keys are equal, the tie-break follows the direction of the first entry. ASC puts the smaller row id and the earlier merged query first. DESC puts the larger row id and the later merged query first.
A joined field (joined_namespace.field) is resolved in each merged query's