VeltrixDB Docs
v1.16 GitHub FAQ Quickstart

Go Client

There are two Go clients. veltrixdb-client-go (from the separate Veltrixdb-client repository) is a thread-safe, connection-pooled driver for single-key ops, batches, namespaces, hash fields, atomics, scans and transactions. The client package inside the server repository (github.com/VeltrixDB/veltrixdb/client) is the only client with the v1.11 search API — vector namespaces, filtered vector search, BM25 text search and hybrid search.

Which one do you need? For search (VCREATE, VSET … NS, VSEARCH with options, VDEL, TSET/TDEL/TSEARCH, HSEARCH) jump to Search (in-repo client). The sections from Install to Concurrent usage describe veltrixdb-client-go; that SDK lives outside the server repository and was not updated with the search release.

Install

bash
go get github.com/VeltrixDB/veltrixdb-client-go

Requires Go 1.19+. No external dependencies — the client is built entirely on the standard library (net, crypto/tls, encoding/binary).

Quick start

This example is drawn directly from the SDK's bundled go/example/main.go: connect, ping, put/get/delete, run a batch, and fan out concurrent writes across the pool.

go
package main import ( "context" "errors" "fmt" "log" "time" vdb "github.com/VeltrixDB/veltrixdb-client-go" ) func main() { // ── Connect ────────────────────────────────────────────────────── cfg := vdb.DefaultConfig() cfg.PoolSize = 16 // 16 persistent connections client, err := vdb.New("127.0.0.1:9000", cfg) if err != nil { log.Fatalf("connect: %v", err) } defer client.Close() ctx := context.Background() // ── Ping ───────────────────────────────────────────────────────── if err := client.Ping(ctx); err != nil { log.Fatalf("ping: %v", err) } // ── Put / Get / Delete ─────────────────────────────────────────── if err := client.Put(ctx, "hello", []byte("world")); err != nil { log.Fatalf("put: %v", err) } val, err := client.Get(ctx, "hello") if err != nil { log.Fatalf("get: %v", err) } fmt.Printf("GET hello = %s\n", val) if err := client.Delete(ctx, "hello"); err != nil { log.Fatalf("delete: %v", err) } // ErrNotFound is a known sentinel, not a hard error. _, err = client.Get(ctx, "hello") if errors.Is(err, vdb.ErrNotFound) { fmt.Println("GET hello → not found (expected after delete)") } }

Config

Config is passed to New; DefaultConfig() returns production-sane defaults that you can override selectively.

go
type Config struct { PoolSize int // connections to maintain (default 8) DialTimeout time.Duration // TCP/TLS handshake timeout (default 5s) IOTimeout time.Duration // per-operation deadline (default 10s); 0 = no deadline TLS *tls.Config // nil = plain TCP }

The client pre-dials all PoolSize connections inside New, so construction fails fast if the server is unreachable. Close() drains and closes every pooled connection; after Close, all methods return ErrClosed.

API surface

Every method takes a context.Context as its first argument, so cancellation and per-call deadlines compose naturally with the pool's own IOTimeout.

Core key-value

MethodDescription
New(addr, cfg)Create client and pre-dial all pool connections
Put(ctx, key, value)Write a key
Get(ctx, key)Read a key. Returns nil, ErrNotFound if missing
Delete(ctx, key)Delete a key (no error if already absent)
Ping(ctx)Check connection health
Info(ctx)Get server stats string
Auth(ctx, user, pass)Authenticate (only needed if the server has auth enabled)
Close()Close all pool connections

Batch operations

MethodDescription
MultiPut(ctx, entries)Write multiple keys in one round trip
MultiGet(ctx, keys)Read multiple keys in one round trip
go
entries := []vdb.Entry{ {Key: "user:1", Value: []byte(`{"name":"Alice","age":30}`)}, {Key: "user:2", Value: []byte(`{"name":"Bob","age":25}`)}, // With a 60-second TTL: {Key: "session:abc", Value: []byte("token-xyz"), TTLSeconds: 60}, } mputResults, err := client.MultiPut(ctx, entries) if err != nil { log.Fatalf("multiput: %v", err) } for i, r := range mputResults { if r.Err != nil { log.Printf("entry %d failed: %v", i, r.Err) } } getResults, err := client.MultiGet(ctx, []string{"user:1", "user:2", "user:99"}) for _, r := range getResults { if r.Found { fmt.Printf("GET %s = %s\n", r.Key, r.Value) } else { fmt.Printf("GET %s → not found\n", r.Key) } }

Results are returned in the same order as the input. Only individual entries fail on partial errors — the batch itself only errors on connection or protocol-level failure.

Namespaces

MethodDescription
PutNS(ctx, ns, key, value, ttl)Write into a namespace
GetNS(ctx, ns, key)Read from a namespace
DeleteNS(ctx, ns, key)Delete from a namespace
ScanNamespace(ctx, ns, prefix, limit)Scan keys in a namespace
ListNamespaces(ctx)List all namespaces and key counts
DropNamespace(ctx, ns)Delete all keys in a namespace

Hash fields (per-field TTL)

MethodDescription
HSet(ctx, key, field, value, ttl)Set a field with its own TTL
HGet(ctx, key, field)Get a field value
HDel(ctx, key, field)Delete one field
HGetAll(ctx, key)Get all live fields and values
HKeys(ctx, key)Get all live field names
HLen(ctx, key)Count live fields
HExpire(ctx, key, field, ttl)Update TTL of an existing field
HTTL(ctx, key, field)Remaining TTL (-1=forever, -2=not found)

Atomic operations

New in v1.1.0 — typed atomic ops (previously Rust/C++ only):

MethodDescription
CAS(ctx, key, expected, newValue) (bool, error)Compare-and-swap. (true, nil) = swapped, (false, nil) = current value didn't match expected, (false, ErrNotFound) = key doesn't exist
Incr(ctx, key, delta) (int64, error)Atomically add delta (int64) and return the new value; a missing key starts from 0. Errors if the existing value is not an integer
Decr(ctx, key, delta) (int64, error)Atomically subtract delta and return the new value; a missing key starts from 0
SetNX(ctx, key, value) (bool, error)Set only if absent. (true, nil) = created, (false, nil) = key already existed
go
// Counter — missing key starts from 0 n, err := client.Incr(ctx, "counter", 5) // n = 5 n, err = client.Decr(ctx, "counter", 2) // n = 3 // Compare-and-swap swapped, err := client.CAS(ctx, "hello", []byte("world"), []byte("earth")) if err != nil { if errors.Is(err, vdb.ErrNotFound) { // key doesn't exist } } else if !swapped { // current value didn't match "world" } // Distributed lock-style set-if-absent created, err := client.SetNX(ctx, "lock", []byte("owner-A")) // created == false → someone else holds the key

Range scans, transactions, indexes, and vectors

Beyond the wire-protocol operations documented in the client README, veltrixdb-client-go exposes a further set of methods (RangeScan, ScanCursor, Txn, KeyVersion, IdxCreate/IdxDrop/IdxQuery, VSet/VSearch, Query). Its VSet/VSearch use only the default vector namespace with no options; namespaces, filters, beam width, deletes, text and hybrid search are in the in-repo client.

MethodDescription
RangeScan(ctx, start, end, limit, reverse)Ordered key-range scan; requires the server's ordered index
ScanCursor(ctx, cursor, limit)Cursor-paginated full-keyspace scan; pass "" to start
Txn(ctx, ops)Atomic optimistic transaction (SET/SETIF/DEL ops); returns ErrTxnConflict on a failed version guard
KeyVersion(ctx, key)Optimistic version token for a key (0 = absent) — feeds TxnOp.ExpectedVersion
IdxCreate(ctx, name, field) / IdxDrop(ctx, name)Manage a persistent secondary index on a JSON field
IdxQuery(ctx, name, value, limit)Look up primary keys by indexed field value
VSet(ctx, key, vec) / VSearch(ctx, k, query)Store/search vectors by cosine similarity in the default namespace
Query(ctx, ns, field, op, value, limit)Field-predicate query over a namespace (= != > < >= <= contains)

Txn semantics are read-committed optimistic CAS — atomic within the batch, but not serializable. Re-read versions with KeyVersion and retry on ErrTxnConflict.

TLS

go
import "crypto/tls" // Server verification only tlsCfg := &tls.Config{RootCAs: loadCACert("ca.crt")} client, _ := veltrix.New("veltrix.example.com:9443", veltrix.Config{TLS: tlsCfg}) // mTLS (client certificate + server certificate) tlsCfg := &tls.Config{ Certificates: []tls.Certificate{loadCert("client.crt", "client.key")}, RootCAs: loadCACert("ca.crt"), }

Auth

go
client, _ := veltrix.New("127.0.0.1:9000", veltrix.Config{PoolSize: 4}) client.Auth(ctx, "alice", "secret") client.Put(ctx, "key", []byte("value"))

Error handling

The client defines three sentinel errors you should check with errors.Is: ErrNotFound, ErrClosed, and ErrTxnConflict.

go
val, err := client.Get(ctx, "missing-key") if errors.Is(err, veltrix.ErrNotFound) { // key doesn't exist } if errors.Is(err, context.DeadlineExceeded) { // operation timed out }

Internally, when an operation fails with anything other than ErrNotFound or ErrTxnConflict, the pool assumes the connection is broken: it discards it and dials a fresh replacement before returning the error to the caller.

Concurrent usage

The Client is safe for concurrent use from multiple goroutines — each call borrows a connection from the pool independently:

go
const workers = 50 done := make(chan struct{}, workers) for i := 0; i < workers; i++ { go func(id int) { key := fmt.Sprintf("concurrent:%d", id) val := []byte(fmt.Sprintf("value-%d", id)) if err := client.Put(ctx, key, val); err != nil { log.Printf("worker %d put: %v", id, err) } done <- struct{}{} }(i) } for i := 0; i < workers; i++ { <-done }
Match pool size to concurrency If you run 16 concurrent goroutines against the client, set PoolSize: 16. A larger pool doesn't help throughput and just wastes idle connections.

In-repo client package

The server repository ships its own Go client, github.com/VeltrixDB/veltrixdb/client (module github.com/VeltrixDB/veltrixdb, Go 1.19+), used by the server's tests, cmd/loadtest, cmd/admin and bench/compare. It has four types:

TypeConstructorWhat it is
BinaryConnDialBinary(addr string, timeout time.Duration)One connection, binary protocol. KV, MPut/MGet, scans, Txn, indexes, Query, and the full search API.
TCPConnDialTCP(addr string, timeout time.Duration)One connection, text protocol. The same search API (line-based: no newlines in text, no spaces in filter values), plus Info() and Topology().
PipelineNewPipeline(bc *BinaryConn)Queues Put/Get/Delete on a BinaryConn and sends them with Exec().
ClientNewClient(seeds []string, cfg *ClientConfig)Cluster-aware: builds the server's consistent-hash ring from the TOPOLOGY command and follows MOVED redirects to the Raft leader. Only Put, Get, Delete, Topology and Close — no search methods.

Neither connection type is safe for concurrent use (one per goroutine), and both dial plain TCP — there is no TLS option in this package. With -auth-config, call Auth(user, pass) first.

New in v1.11 Search methods exist on both BinaryConn and TCPConn with identical signatures. Results are []client.VectorResult (ID string, Score float64): cosine similarity for vector search, BM25 for text, the fused reciprocal-rank score for hybrid. Filters apply to the KV record whose key equals the vector / document id.

go
package main import ( "fmt" "log" "time" "github.com/VeltrixDB/veltrixdb/client" ) func main() { bc, err := client.DialBinary("127.0.0.1:9000", 5*time.Second) if err != nil { log.Fatal(err) } defer bc.Close() // Namespace "docs": 4 dimensions, int8 codes in RAM, float32 re-rank from disk. if err := bc.VCreateWithOptions("docs", 4, client.VectorNamespaceOptions{Quantization: "int8"}); err != nil { log.Fatal(err) } // The record that filters read: same key as the vector / document id. if err := bc.Put("doc-42", []byte(`{"lang":"en","tier":"gold"}`), -1); err != nil { log.Fatal(err) } if err := bc.VSetNS("docs", "doc-42", []float32{0.12, -0.03, 0.88, 0.41}); err != nil { log.Fatal(err) } if err := bc.TSet("docs", "doc-42", "how to rotate encryption keys"); err != nil { log.Fatal(err) } // Optional: an index on the field makes "=" filters exact and cheap. if err := bc.IdxCreate("by_lang", "lang"); err != nil { log.Fatal(err) } q := []float32{0.1, 0.0, 0.9, 0.4} // Top 10 by cosine, beam width 128, only records with lang = en. hits, err := bc.VSearchWithOptions(10, q, client.VectorSearchOptions{ NS: "docs", Ef: 128, FilterField: "lang", FilterOp: "=", FilterValue: "en", }) if err != nil { log.Fatal(err) // e.g. "search indexes are still rebuilding after restart ..." } for _, h := range hits { fmt.Printf("vec %s %.4f\n", h.ID, h.Score) } // BM25 full-text search. text, err := bc.TSearch(10, "rotate keys", client.TextSearchOptions{NS: "docs"}) if err != nil { log.Fatal(err) } fmt.Println("text hits:", len(text)) // Hybrid: both halves fused by reciprocal rank; alpha = vector weight. alpha := 0.7 hybrid, err := bc.HSearch(10, q, "rotate keys", client.HybridSearchOptions{NS: "docs", Alpha: &alpha}) if err != nil { log.Fatal(err) } fmt.Println("hybrid hits:", len(hybrid)) // Deletes. _ = bc.VDel("docs", "doc-42") _ = bc.TDel("docs", "doc-42") }
Method (on BinaryConn and TCPConn)Wire commandNotes
VCreateWithOptions(ns string, dim int, opts VectorNamespaceOptions) errorVCREATE ns dim [QUANT …] [PQM m] [PQTRAIN n] [GRAPH memory|disk]VectorNamespaceOptions{Quantization, PQSubspaces, PQTrainAt, Graph}: ""/"none", "int8" or "pq"; PQ bytes per vector (0 = dim/8); vectors before PQ training (0 = 10,000); "disk" puts layer-0 graph edges in a memory-mapped file (v1.12). Re-running it on an existing namespace re-encodes it; changing the dimension is refused.
VCreate(ns string, dim int, quant string) errorVCREATE ns dim [QUANT quant]Quantization only.
VSetNS(ns, id string, vec []float32) errorVSET id NS ns f1 …Upsert. The first VSET in a namespace that was never created makes a float32 one with that dimension.
VSet(key string, vec []float32) errorVSET key f1 …Namespace default.
VDel(ns, id string) errorVDEL id NS nsPass the namespace explicitly ("default" for the default one).
VSearchWithOptions(k int, query []float32, opts VectorSearchOptions) ([]VectorResult, error)VSEARCH k [NS ns] [EF n] [FILTER f op v] f1 …VectorSearchOptions{NS, Ef, FilterField, FilterOp, FilterValue}; NS "" = default, Ef 0 = server default. Ops: = != > < >= <= contains. k = 0 returns every match (exact scan).
VSearch(k int, query []float32) ([]VectorResult, error)VSEARCH k f1 …Namespace default, no options.
TSet(ns, id, text string) error / TDel(ns, id string) errorTSET id NS ns TEXT … / TDEL id NS nsText up to 1 MiB. TCPConn.TSet rejects text containing a newline; BinaryConn carries it.
TSearch(k int, query string, opts TextSearchOptions) ([]VectorResult, error)TSEARCH k NS ns [FILTER …] QUERY textTextSearchOptions{NS, FilterField, FilterOp, FilterValue}.
HSearch(k int, vec []float32, query string, opts HybridSearchOptions) ([]VectorResult, error)HSEARCH k NS ns [EF] [ALPHA] [CAND] [FILTER …] [VEC …] QUERY textHybridSearchOptions{NS, Alpha *float64, Ef, Candidates, FilterField, FilterOp, FilterValue}; Alpha nil = 0.5, Candidates 0 = max(50, 4k). k must be > 0; either half may be empty, not both.
IdxCreate(name, field string) error / IdxDrop(name string) error / IdxQuery(name, value string, limit int) ([]string, error)IDXCREATE / IDXDROP / IDXQUERY name value [LIMIT n]Secondary index on a record field; = filters on an indexed field are served from it.
Things the code does that you might not expect After a restart, searches return an error until the node has rebuilt its indexes from the persisted vectors and documents (search_ready=1 in INFO), unless the server runs with -search-allow-partial. On v1.13 and earlier, BinaryConn.Put takes a ttl argument but does not send it (the value is stored without a TTL); Client.Put accepts WithTTL / WithConsistency options but sends neither (the server's -consistency setting decides); and TCPConn.Get (and so Client.Get) returns nil, nil for any ERR reply, not only a missing key. Fixed in v1.14 BinaryConn.Put and Pipeline.Put with ttl > 0 send a one-entry MPUT (the binary PUT frame has no TTL field), Client.Put(…, WithTTL(n)) sends PUTEX, Get returns nil, nil only for a missing or expired key and surfaces any other server error, and Client.Get follows MOVED like Put. WithConsistency is deprecated: the protocol has no per-request consistency field.

In a cluster, send searches to any node: with -search-fanout (default on) the server runs them on every node and merges the results; a peer that does not answer within -search-timeout-ms fails the request unless -search-allow-partial. Filters, memory layouts, tuning and measured recall are on Vector & Hybrid Search; the flags are in Server Configuration.

Cluster-aware client

client.NewClient from the in-repo package discovers the cluster topology over the data port (TOPOLOGY), builds a consistent-hash ring identical to the server's (64 virtual nodes per node), routes each key to its owner, follows up to 3 MOVED redirects to the current Raft leader, and refreshes the topology every 30 s by default. It speaks the text protocol through TCPConn and has only Put(ctx, key, value, opts...), Get(ctx, key, opts...), Delete(ctx, key), Topology() and Close(). Reads go to the key's owner node, so they can be stale in raft mode unless the server runs with -linearizable-reads. For search against a cluster, use a BinaryConn to any node. See Deployment Modes.