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.
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
bashgo 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.
gopackage 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.
gotype 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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
MultiPut(ctx, entries) | Write multiple keys in one round trip |
MultiGet(ctx, keys) | Read multiple keys in one round trip |
goentries := []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
| Method | Description |
|---|---|
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)
| Method | Description |
|---|---|
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):
| Method | Description |
|---|---|
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.
| Method | Description |
|---|---|
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
goimport "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
goclient, _ := 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.
goval, 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:
goconst 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 }
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:
| Type | Constructor | What it is |
|---|---|---|
BinaryConn | DialBinary(addr string, timeout time.Duration) | One connection, binary protocol. KV, MPut/MGet, scans, Txn, indexes, Query, and the full search API. |
TCPConn | DialTCP(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(). |
Pipeline | NewPipeline(bc *BinaryConn) | Queues Put/Get/Delete on a BinaryConn and sends them with Exec(). |
Client | NewClient(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.
Search (in-repo client)
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.
gopackage 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 command | Notes |
|---|---|---|
VCreateWithOptions(ns string, dim int, opts VectorNamespaceOptions) error | VCREATE 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) error | VCREATE ns dim [QUANT quant] | Quantization only. |
VSetNS(ns, id string, vec []float32) error | VSET 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) error | VSET key f1 … | Namespace default. |
VDel(ns, id string) error | VDEL id NS ns | Pass 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) error | TSET id NS ns TEXT … / TDEL id NS ns | Text 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 text | TextSearchOptions{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 text | HybridSearchOptions{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. |
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.