NoriKV Go Client API Guide¶
Complete reference for the NoriKV Go Client SDK.
Table of Contents¶
- Installation
- Quick Start
- Client Configuration
- Core Operations
- Advanced Features
- Vector Operations
- Error Handling
- Best Practices
Installation¶
Quick Start¶
package main
import (
"context"
"fmt"
"log"
norikv "github.com/norikv/norikv-go"
)
func main() {
ctx := context.Background()
// Configure client
config := norikv.ClientConfig{
Nodes: []string{"localhost:9001", "localhost:9002"},
TotalShards: 1024,
Timeout: 5 * time.Second,
}
// Create client
client, err := norikv.NewClient(ctx, &config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Put a value
key := []byte("user:alice")
value := []byte(`{"name":"Alice","age":30}`)
version, err := client.Put(ctx, key, value, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Wrote version: %v\n", version)
// Get the value
result, err := client.Get(ctx, key, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Value: %s\n", result.Value)
// Delete
err = client.Delete(ctx, key, nil)
if err != nil {
log.Fatal(err)
}
}
Client Configuration¶
Basic Configuration¶
config := &norikv.ClientConfig{
Nodes: []string{"node1:9001", "node2:9001"},
TotalShards: 1024,
Timeout: 5 * time.Second,
}
Configuration Options¶
| Field | Type | Default | Description |
|---|---|---|---|
Nodes |
[]string |
Required | List of node addresses (host:port) |
TotalShards |
int |
Required | Total number of shards in cluster |
Timeout |
time.Duration |
5s | Request timeout |
Retry |
*RetryConfig |
See below | Retry policy configuration |
Retry Configuration¶
retryConfig := &norikv.RetryConfig{
MaxAttempts: 10,
InitialDelay: 100 * time.Millisecond,
MaxDelay: 5 * time.Second,
Jitter: 100 * time.Millisecond,
}
config := &norikv.ClientConfig{
Nodes: []string{"localhost:9001"},
TotalShards: 1024,
Retry: retryConfig,
}
Retry Behavior:
- Retries transient errors: Unavailable, Aborted, DeadlineExceeded, ResourceExhausted
- Does NOT retry: InvalidArgument, NotFound, FailedPrecondition, PermissionDenied
- Uses exponential backoff with jitter
Default Configuration¶
Core Operations¶
PUT - Write Data¶
Basic PUT¶
key := []byte("user:123")
value := []byte(`{"name":"Alice"}`)
version, err := client.Put(ctx, key, value, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Written at version: %v\n", version)
PUT with Options¶
ttl := uint64(60000) // 60 seconds
options := &norikv.PutOptions{
TTLMs: &ttl,
IdempotencyKey: "order-12345",
IfMatchVersion: expectedVersion, // CAS
}
version, err := client.Put(ctx, key, value, options)
PutOptions Fields:
| Field | Type | Description |
|---|---|---|
TTLMs |
*uint64 |
Time-to-live in milliseconds |
IdempotencyKey |
string |
Key for idempotent operations |
IfMatchVersion |
*Version |
Expected version for CAS |
IfNotExists |
bool |
Only write if key doesn't exist |
GET - Read Data¶
Basic GET¶
key := []byte("user:123")
result, err := client.Get(ctx, key, nil)
if err != nil {
log.Fatal(err)
}
value := result.Value
version := result.Version
GET with Consistency Level¶
options := &norikv.GetOptions{
Consistency: norikv.ConsistencyLinearizable,
}
result, err := client.Get(ctx, key, options)
Consistency Levels:
| Level | Description | Use Case |
|---|---|---|
ConsistencyLease |
Default, lease-based read | Most operations (fast, usually consistent) |
ConsistencyLinearizable |
Strictest, always up-to-date | Critical reads requiring absolute consistency |
ConsistencyStaleOK |
May return stale data | Read-heavy workloads, caching |
DELETE - Remove Data¶
Basic DELETE¶
DELETE with Options¶
options := &norikv.DeleteOptions{
IdempotencyKey: "delete-order-12345",
IfMatchVersion: expectedVersion,
}
err := client.Delete(ctx, key, options)
Advanced Features¶
Compare-And-Swap (CAS)¶
Optimistic concurrency control using version matching:
// Read current value
result, err := client.Get(ctx, key, nil)
if err != nil {
log.Fatal(err)
}
// Modify value
value, _ := strconv.Atoi(string(result.Value))
newValue := []byte(strconv.Itoa(value + 1))
// Update with CAS
options := &norikv.PutOptions{
IfMatchVersion: result.Version,
}
_, err = client.Put(ctx, key, newValue, options)
if errors.Is(err, norikv.ErrVersionMismatch) {
fmt.Println("CAS failed - version changed")
} else if err != nil {
log.Fatal(err)
}
Idempotent Operations¶
Safe retries using idempotency keys:
idempotencyKey := "payment-" + uuid.New().String()
options := &norikv.PutOptions{
IdempotencyKey: idempotencyKey,
}
// First attempt
v1, err := client.Put(ctx, key, value, options)
// Retry with same key (safe - returns same version)
v2, err := client.Put(ctx, key, value, options)
// v1 and v2 are equal
Time-To-Live (TTL)¶
Automatic expiration:
ttl := uint64(60000) // 60 seconds
options := &norikv.PutOptions{
TTLMs: &ttl,
}
client.Put(ctx, key, value, options)
// Key automatically deleted after TTL
time.Sleep(61 * time.Second)
_, err := client.Get(ctx, key, nil)
if errors.Is(err, norikv.ErrKeyNotFound) {
fmt.Println("Key expired")
}
Cluster Topology¶
Monitor cluster changes:
// Get current cluster view
view := client.GetClusterView()
if view != nil {
fmt.Printf("Cluster epoch: %d\n", view.Epoch)
fmt.Printf("Nodes: %d\n", len(view.Nodes))
}
// Subscribe to topology changes
unsubscribe := client.OnTopologyChange(func(event *norikv.TopologyChangeEvent) {
fmt.Printf("Topology changed!\n")
fmt.Printf("Previous epoch: %d\n", event.PreviousEpoch)
fmt.Printf("Current epoch: %d\n", event.CurrentEpoch)
fmt.Printf("Added nodes: %v\n", event.AddedNodes)
fmt.Printf("Removed nodes: %v\n", event.RemovedNodes)
})
// Later: unsubscribe
defer unsubscribe()
Client Statistics¶
Monitor client performance:
stats := client.Stats()
fmt.Printf("Active connections: %d\n", stats.Pool.ActiveConnections)
fmt.Printf("Total nodes: %d\n", stats.Router.TotalNodes)
fmt.Printf("Cached leaders: %d\n", stats.Topology.CachedLeaders)
Vector Operations¶
NoriKV supports vector similarity search for building AI/ML applications, recommendation systems, and semantic search.
Creating a Vector Index¶
Before inserting vectors, create an index with your configuration:
created, err := client.VectorCreateIndex(
ctx,
"embeddings", // namespace
1536, // dimensions
norikv.DistanceCosine, // distance function
norikv.VectorIndexHNSW, // index type
nil, // options
)
if err != nil {
log.Fatal(err)
}
if created {
fmt.Println("Index created")
} else {
fmt.Println("Index already exists")
}
With Options¶
options := &norikv.CreateVectorIndexOptions{
IdempotencyKey: "create-embeddings-index",
}
created, err := client.VectorCreateIndex(
ctx,
"embeddings",
1536,
norikv.DistanceCosine,
norikv.VectorIndexHNSW,
options,
)
Distance Functions¶
| Constant | Description | Use Case |
|---|---|---|
DistanceEuclidean |
L2 distance | General purpose |
DistanceCosine |
Cosine similarity (1 - cos) | Text embeddings, normalized vectors |
DistanceInnerProduct |
Negative inner product | Maximum inner product search |
Index Types¶
| Constant | Description | Trade-off |
|---|---|---|
VectorIndexBruteForce |
Exact linear scan | Exact results, O(n) complexity |
VectorIndexHNSW |
Hierarchical Navigable Small World | Approximate, O(log n) complexity |
Inserting Vectors¶
embedding := getEmbedding("Hello world")
version, err := client.VectorInsert(
ctx,
"embeddings", // namespace
"doc-123", // unique ID
embedding, // []float32
nil, // options
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted at version: %v\n", version)
With Options¶
options := &norikv.VectorInsertOptions{
IdempotencyKey: "insert-doc-123",
}
version, err := client.VectorInsert(ctx, "embeddings", "doc-123", embedding, options)
Searching for Similar Vectors¶
query := getEmbedding("Find similar documents")
result, err := client.VectorSearch(
ctx,
"embeddings", // namespace
query, // query vector
10, // k nearest neighbors
nil, // options
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Search took %dus\n", result.SearchTimeUs)
for _, match := range result.Matches {
fmt.Printf("ID: %s, Distance: %.4f\n", match.ID, match.Distance)
}
With Options¶
options := &norikv.VectorSearchOptions{
IncludeVectors: true, // include vector data in results
}
result, err := client.VectorSearch(ctx, "embeddings", query, 10, options)
if err != nil {
log.Fatal(err)
}
for _, match := range result.Matches {
fmt.Printf("ID: %s, Distance: %.4f, Vector dims: %d\n",
match.ID, match.Distance, len(match.Vector))
}
Getting a Vector by ID¶
vector, err := client.VectorGet(ctx, "embeddings", "doc-123")
if err != nil {
if errors.Is(err, norikv.ErrKeyNotFound) {
fmt.Println("Vector not found")
} else {
log.Fatal(err)
}
}
if vector != nil {
fmt.Printf("Vector has %d dimensions\n", len(vector))
}
Deleting Vectors¶
deleted, err := client.VectorDelete(ctx, "embeddings", "doc-123", nil)
if err != nil {
log.Fatal(err)
}
if deleted {
fmt.Println("Vector deleted")
} else {
fmt.Println("Vector not found")
}
With Options¶
options := &norikv.VectorDeleteOptions{
IdempotencyKey: "delete-doc-123",
}
deleted, err := client.VectorDelete(ctx, "embeddings", "doc-123", options)
Dropping a Vector Index¶
dropped, err := client.VectorDropIndex(ctx, "embeddings", nil)
if err != nil {
log.Fatal(err)
}
if dropped {
fmt.Println("Index dropped")
} else {
fmt.Println("Index did not exist")
}
Complete Vector Example¶
package main
import (
"context"
"fmt"
"log"
norikv "github.com/norikv/norikv-go"
)
func main() {
ctx := context.Background()
config := norikv.DefaultClientConfig([]string{"localhost:9001"})
client, err := norikv.NewClient(ctx, config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create index
_, err = client.VectorCreateIndex(
ctx,
"products",
768,
norikv.DistanceCosine,
norikv.VectorIndexHNSW,
nil,
)
if err != nil {
log.Fatal(err)
}
// Insert product embeddings
productEmbedding := getProductEmbedding("Red running shoes")
_, err = client.VectorInsert(ctx, "products", "prod-001", productEmbedding, nil)
if err != nil {
log.Fatal(err)
}
// Search for similar products
queryEmbedding := getProductEmbedding("Athletic footwear")
results, err := client.VectorSearch(ctx, "products", queryEmbedding, 5, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Similar products:")
for _, match := range results.Matches {
fmt.Printf(" %s (distance: %.4f)\n", match.ID, match.Distance)
}
// Cleanup
client.VectorDelete(ctx, "products", "prod-001", nil)
client.VectorDropIndex(ctx, "products", nil)
}
func getProductEmbedding(text string) []float32 {
// Call your embedding model here
return make([]float32, 768)
}
Error Handling¶
Error Types¶
var (
ErrKeyNotFound error // Key does not exist
ErrVersionMismatch error // CAS version conflict
ErrAlreadyExists error // IfNotExists conflict
ErrConnection error // Network or cluster issues
)
Handling Specific Errors¶
result, err := client.Get(ctx, key, nil)
if err != nil {
switch {
case errors.Is(err, norikv.ErrKeyNotFound):
fmt.Println("Key not found")
case errors.Is(err, norikv.ErrConnection):
fmt.Println("Connection error:", err)
default:
fmt.Println("Error:", err)
}
}
Retry Pattern¶
maxAttempts := 3
for attempt := 1; attempt <= maxAttempts; attempt++ {
_, err := client.Put(ctx, key, value, nil)
if err == nil {
break // Success
}
if !errors.Is(err, norikv.ErrConnection) {
return err // Non-retryable
}
if attempt == maxAttempts {
return err // Give up
}
// Exponential backoff
time.Sleep(time.Duration(1<<attempt) * 100 * time.Millisecond)
}
Graceful Degradation¶
func getWithFallback(client *norikv.Client, ctx context.Context, key, defaultValue []byte) []byte {
result, err := client.Get(ctx, key, nil)
if err != nil {
log.Printf("Failed to get key, using default: %v", err)
return defaultValue
}
return result.Value
}
Best Practices¶
1. Use Context for Timeouts¶
Always pass context with timeout:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := client.Get(ctx, key, nil)
2. Reuse Client Instances¶
Clients manage connection pools and should be reused:
// Good: Single client instance
var client *norikv.Client
func init() {
config := norikv.DefaultClientConfig([]string{"localhost:9001"})
client, _ = norikv.NewClient(context.Background(), config)
}
// Bad: Creating client per request
func handleRequest() {
client, _ := norikv.NewClient(context.Background(), config)
defer client.Close() // Closes connections!
}
3. Use defer for Cleanup¶
4. Use Idempotency Keys¶
For operations that must not be duplicated:
idempotencyKey := "order-" + orderID
options := &norikv.PutOptions{
IdempotencyKey: idempotencyKey,
}
client.Put(ctx, key, value, options)
5. Choose Appropriate Consistency¶
- Use
ConsistencyLease(default) for most operations - Use
ConsistencyLinearizablefor critical reads - Use
ConsistencyStaleOKfor caching/read-heavy workloads
6. Handle Version Conflicts¶
Implement retry logic for CAS operations:
maxRetries := 10
for i := 0; i < maxRetries; i++ {
result, err := client.Get(ctx, key, nil)
if err != nil {
return err
}
// ... compute new value ...
options := &norikv.PutOptions{
IfMatchVersion: result.Version,
}
_, err = client.Put(ctx, key, newValue, options)
if err == nil {
break // Success
}
if !errors.Is(err, norikv.ErrVersionMismatch) {
return err
}
if i == maxRetries-1 {
return err
}
time.Sleep(10 * time.Millisecond) // Small backoff
}
7. Use Goroutines Safely¶
Client is goroutine-safe:
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
key := []byte(fmt.Sprintf("key-%d", i))
client.Put(ctx, key, value, nil)
}(i)
}
wg.Wait()
8. Monitor Client Health¶
// Periodically check stats
stats := client.Stats()
if stats.Pool.ActiveConnections == 0 {
log.Error("No active connections!")
}
Performance Tips¶
1. Concurrent Access¶
Client is optimized for concurrent use:
numWorkers := runtime.NumCPU()
work := make(chan []byte, 100)
for i := 0; i < numWorkers; i++ {
go func() {
for key := range work {
client.Put(ctx, key, value, nil)
}
}()
}
2. Zero-Allocation Routing¶
The client uses optimized routing with zero heap allocations in the hot path.
3. Connection Pooling¶
The client maintains a connection pool internally - no external pooling needed.
4. Single-Flight Pattern¶
Concurrent requests for the same shard's leader are deduplicated automatically.
5. Use Appropriate Value Sizes¶
- Optimal: 100 bytes - 10 KB
- Maximum: Limited by memory and network
Complete Example¶
package main
import (
"context"
"fmt"
"log"
"time"
norikv "github.com/norikv/norikv-go"
)
func main() {
ctx := context.Background()
// Configure with retry policy
retryConfig := &norikv.RetryConfig{
MaxAttempts: 5,
InitialDelay: 100 * time.Millisecond,
MaxDelay: 2 * time.Second,
}
config := &norikv.ClientConfig{
Nodes: []string{"localhost:9001", "localhost:9002"},
TotalShards: 1024,
Timeout: 5 * time.Second,
Retry: retryConfig,
}
client, err := norikv.NewClient(ctx, config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Write with TTL and idempotency
key := []byte("session:abc123")
value := []byte(`{"user_id":42}`)
ttl := uint64(3600000) // 1 hour
putOpts := &norikv.PutOptions{
TTLMs: &ttl,
IdempotencyKey: "session-create-abc123",
}
version, err := client.Put(ctx, key, value, putOpts)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Written: %v\n", version)
// Read with linearizable consistency
getOpts := &norikv.GetOptions{
Consistency: norikv.ConsistencyLinearizable,
}
result, err := client.Get(ctx, key, getOpts)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read: %s\n", result.Value)
// Update with CAS
newValue := []byte(`{"user_id":42,"active":true}`)
casOpts := &norikv.PutOptions{
IfMatchVersion: result.Version,
}
_, err = client.Put(ctx, key, newValue, casOpts)
if err != nil {
if errors.Is(err, norikv.ErrVersionMismatch) {
fmt.Println("CAS failed - retry needed")
} else {
log.Fatal(err)
}
}
// Monitor topology
client.OnTopologyChange(func(event *norikv.TopologyChangeEvent) {
fmt.Printf("Cluster changed: epoch %d\n", event.CurrentEpoch)
})
// Get statistics
stats := client.Stats()
fmt.Printf("Stats: %+v\n", stats)
}
Next Steps¶
- Architecture Guide - Understanding client internals
- Troubleshooting Guide - Solving common issues
- Advanced Patterns - Complex use cases
- Examples - Working code samples