fdb-client
Public Go smart client for FrogoDB.
Scope
./pkg/client: smart client (routing, pooling, policies, batch/pipeline).
./pkg/queries: reusable query helpers (timeseries, previous, window, lsh).
./pkg/protocol, ./pkg/ripemd160: client-side wire/hash support.
Install
go get github.com/FrogoAI/fdb-client
Connect
Use one or more seed nodes. The client connects to the first available seed,
discovers the cluster topology in the background, and routes keys directly to
the node that owns the partition.
package main
import (
"context"
"log"
"time"
"github.com/FrogoAI/fdb-client/pkg/client"
)
func main() {
c, err := client.New("node1:3000", "node2:3000", "node3:3000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
ctx := context.Background()
pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
if err := c.Ping(pingCtx); err != nil {
log.Fatalf("FrogoDB connection is not live: %v", err)
}
}
For explicit connection policy:
c, err := client.NewWithConfig(client.Config{
Seeds: []string{"node1:3000", "node2:3000"},
PoolSizePerNode: 64,
ConnectionTimeout: 5 * time.Second,
IdleTimeout: 55 * time.Second,
TendInterval: 10 * time.Millisecond,
MaxErrorRate: 100,
ErrorRateWindow: time.Second,
})
Records
Put writes bins, Get reads records, and Delete removes records. Write
options make bin semantics, existence checks, and TTL behavior explicit.
err := c.Put(ctx, "myns", "users", "user-123", map[string]any{
"name": "Alice",
"age": int64(30),
"score": 95.5,
}, client.WithMergeBins(), client.WithPreserveTTL())
if err != nil {
log.Fatal(err)
}
rec, err := c.Get(ctx, "myns", "users", "user-123")
if err != nil {
log.Fatal(err)
}
log.Printf("name=%s age=%d", rec.Bins["name"], rec.Bins["age"])
existed, err := c.Delete(ctx, "myns", "users", "user-123")
if err != nil {
log.Fatal(err)
}
log.Printf("deleted=%t", existed)
Common write options:
client.WithMergeBins(): update supplied bins and preserve untouched bins.
client.WithReplaceBins(): rebuild the record from only supplied bins.
client.WithPreserveTTL(): keep the current TTL.
client.WithTTL(seconds): set a new TTL in seconds.
client.WithClearTTL(): clear expiration.
client.WithCreateOnly(): fail if the key already exists.
client.WithReplace(): require that the key already exists.
client.WithGeneration(n): optimistic locking.
Query Helpers
The query packages sit on top of the client and accept the shared
queries.Client interface. *client.Client already satisfies that interface,
so the same client value can be passed directly.
import (
"time"
"github.com/FrogoAI/fdb-client/pkg/queries"
"github.com/FrogoAI/fdb-client/pkg/queries/lsh"
"github.com/FrogoAI/fdb-client/pkg/queries/previous"
"github.com/FrogoAI/fdb-client/pkg/queries/timeseries"
"github.com/FrogoAI/fdb-client/pkg/queries/window"
)
src := queries.NewMapSource(map[string]any{
"event_id": "evt-789",
"standard.user_id": "user-42",
"standard.email": "alice@example.com",
"standard.merchant_id": "shop-456",
"amount": 42.5,
"frequency": int64(10),
"recency": 3.14,
"event_time": time.Now(),
})
Timeseries
Use timeseries queries for time-bucketed aggregations such as counts, averages,
standard deviation, min/max, distinct counts, and percentiles.
tsReq := timeseries.Request{
Name: "transaction_stats",
Namespace: "scoring",
GroupBy: []string{"standard.user_id"},
Range: 24 * time.Hour,
Fields: queries.FieldCount | queries.FieldAvg | queries.FieldSTD | queries.FieldMin | queries.FieldMax,
Value: "amount",
TTL: 48 * time.Hour,
IncludeCurrent: true,
}
tsResult, err := timeseries.Execute(ctx, c, tsReq, src)
if err != nil {
log.Fatal(err)
}
log.Printf("count=%d avg=%.2f std=%.2f", tsResult.Count, tsResult.Average(), tsResult.STD())
Previous
Use previous queries to retrieve a value from the previous event for the same
entity. Exclude can require the previous event to differ on another field.
prevReq := previous.Request{
Name: "prev_amount",
Namespace: "scoring",
Ref: "standard.user_id",
Retrieve: "amount",
Exclude: "standard.merchant_id",
EventID: src.String("event_id"),
TTL: 24 * time.Hour,
IncludeCurrent: false,
}
prevResult, err := previous.Execute(ctx, c, prevReq, src)
if err != nil {
log.Fatal(err)
}
if prevResult.Found {
log.Printf("previous amount=%v", prevResult.Value)
}
Window
Use window queries for exact sliding-window aggregation over the last N events.
winReq := window.Request{
Name: "last_10_amounts",
Namespace: "scoring",
Ref: "standard.user_id",
Value: "amount",
WindowSize: 10,
Fields: queries.FieldCount | queries.FieldAvg | queries.FieldSTD | queries.FieldPercentile,
PercentileP: 0.90,
EventID: src.String("event_id"),
TTL: 24 * time.Hour,
IncludeCurrent: true,
}
winResult, err := window.Execute(ctx, c, winReq, src)
if err != nil {
log.Fatal(err)
}
log.Printf("count=%d avg=%.2f p90=%.2f", winResult.Count, winResult.Avg, winResult.Percentile)
LSH
Use LSH queries for near-duplicate string buckets and vector-based behavioural
clustering. The server computes the LSH bucket and the client stores entries
with the requested TTL.
dedupResult, err := lsh.Dedup(ctx, c, lsh.DedupRequest{
Namespace: "scoring",
Reference: "standard.email",
TTL: 24 * time.Hour,
}, src)
if err != nil {
log.Fatal(err)
}
log.Printf("email dedup bucket=%s", dedupResult.BucketID)
vectorResult, err := lsh.Vector(ctx, c, lsh.VectorRequest{
Namespace: "scoring",
Attributes: []string{"amount", "frequency", "recency"},
TTL: 24 * time.Hour,
}, src)
if err != nil {
log.Fatal(err)
}
log.Printf("behavioral cluster=%s", vectorResult.BehavioralID)
For direct client-level LSH calls without the query helper source mapping:
bucketID, err := c.LSHDedup(ctx, "scoring", "email", "alice@example.com", client.WithTTL(86400))
if err != nil {
log.Fatal(err)
}
log.Printf("email bucket=%s", bucketID)
behavioralID, err := c.LSHVector(ctx, "scoring", "behavior", []float64{42.5, 10, 3.14}, client.WithTTL(86400))
if err != nil {
log.Fatal(err)
}
log.Printf("behavioral id=%s", behavioralID)
Bloom Filter
Bloom filters are exposed through atomic Operate calls. They are useful for
fast membership checks where false positives are acceptable.
_, err := c.Operate(ctx, "myns", "filters", "seen-users", []client.Operation{
client.BloomInitOp("bloom", 10000, 0.01),
})
if err != nil {
log.Fatal(err)
}
_, err = c.Operate(ctx, "myns", "filters", "seen-users", []client.Operation{
client.BloomAddOp("bloom", []byte("user-42")),
})
if err != nil {
log.Fatal(err)
}
rec, err := c.Operate(ctx, "myns", "filters", "seen-users", []client.Operation{
client.BloomTestOp("bloom", []byte("user-42")),
})
if err != nil {
log.Fatal(err)
}
log.Printf("probably seen=%v", rec.Bins["bloom"])
Other bloom helpers include client.BloomRemoveOp and client.BloomResetOp.
HyperLogLog
HyperLogLog estimates cardinality, for example unique users or unique devices.
_, err := c.Operate(ctx, "myns", "stats", "page-visitors", []client.Operation{
client.HLLInitOp("visitors", 14, 6),
})
if err != nil {
log.Fatal(err)
}
_, err = c.Operate(ctx, "myns", "stats", "page-visitors", []client.Operation{
client.HLLAddOp("visitors", []byte("user-1"), []byte("user-2"), []byte("user-3")),
})
if err != nil {
log.Fatal(err)
}
rec, err := c.Operate(ctx, "myns", "stats", "page-visitors", []client.Operation{
client.HLLCountOp("visitors"),
})
if err != nil {
log.Fatal(err)
}
log.Printf("unique visitors=%v", rec.Bins["visitors"])
Use client.HLLUnionOp, client.HLLUnionCountOp, and
client.HLLIntersectCountOp to combine estimates across records.
TDigest
TDigest estimates quantiles and distribution statistics, for example p95 or p99
latency.
_, err := c.Operate(ctx, "myns", "stats", "latency", []client.Operation{
client.TDigestAddOp("tdigest", 42.0, 1.0),
client.TDigestAddOp("tdigest", 125.0, 1.0),
client.TDigestAddOp("tdigest", 300.0, 1.0),
})
if err != nil {
log.Fatal(err)
}
rec, err := c.Operate(ctx, "myns", "stats", "latency", []client.Operation{
client.TDigestQuantileOp("tdigest", 0.99),
})
if err != nil {
log.Fatal(err)
}
log.Printf("p99 latency=%v", rec.Bins["tdigest"])
Other TDigest helpers include client.TDigestCountOp, client.TDigestMinOp,
client.TDigestMaxOp, client.TDigestCDFOp, and client.TDigestMergeOp.
More Documentation
./docs/client.md: full client API, CRUD, scans, batch operations, policies, and topology.
./docs/queries.md: full query helper API and examples.
./docs/publish-checklist.md: repository publication and release checklist.
Contributing and Security
./CONTRIBUTING.md: development workflow and contribution guidelines.
./SECURITY.md: private vulnerability reporting policy.
License
MIT. See ./LICENSE.
Contracts
- This repository must not import private server modules.
- FrogoDB server can depend on this client module.
Build
go test ./...
make lint
make test-race