Go App — PG-Wire End-to-End (Vector + Hybrid Search)
Go App — PG-Wire End-to-End (Vector + Hybrid Search)
Available since: v3.6.0 (compression, SQLite import) — vector wire fully stable v3.13.0
Stack: github.com/jackc/pgx/v5 against the Nano binary’s PostgreSQL wire endpoint
HeliosDB build: default — no feature flag required
UVP
There is no in-process Go embedding API for HeliosDB Nano — the engine is Rust. You don’t need one. The Nano binary speaks the PostgreSQL v3 wire protocol natively, so every battle-tested Go driver (pgx, lib/pq, database/sql, GORM, ent) connects with zero adapters. This walkthrough builds a small Go service that connects, runs CRUD, then uses pgx’s prepared statements to push and query 384-dim vectors — same data path your existing Postgres-backed Go code already uses. No CGo. No FFI. Just pgx.
Prerequisites
mkdir helios-go && cd helios-gogo mod init example.com/helios-gogo get github.com/jackc/pgx/v5@latestStart a Nano server:
heliosdb-nano start \ --memory \ --auth scram-sha-256 \ --password s3cret1. Connect
package main
import ( "context" "fmt" "log"
"github.com/jackc/pgx/v5")
const dsn = "postgresql://postgres:s3cret@127.0.0.1:5432/postgres"
func main() { ctx := context.Background() conn, err := pgx.Connect(ctx, dsn) if err != nil { log.Fatal(err) } defer conn.Close(ctx)
var version string if err := conn.QueryRow(ctx, "SELECT version()").Scan(&version); err != nil { log.Fatal(err) } fmt.Println(version) // PostgreSQL 16.0 (compatible) -- HeliosDB Nano 3.19.1}Nano implements the v3 startup handshake, SCRAM-SHA-256, the simple-Q protocol, and the extended-Q (parse/bind/describe/execute) protocol. pgx negotiates extended-Q automatically.
2. CRUD Schema
func setupSchema(ctx context.Context, conn *pgx.Conn) error { _, err := conn.Exec(ctx, ` CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, category TEXT, price NUMERIC(10, 2), in_stock BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT now() )`) return err}3. Basic CRUD with pgx
type Product struct { ID int Name string Category string Price float64}
func insertProduct(ctx context.Context, conn *pgx.Conn, p Product) (int, error) { var id int err := conn.QueryRow(ctx, `INSERT INTO products (name, category, price) VALUES ($1, $2, $3) RETURNING id`, p.Name, p.Category, p.Price, ).Scan(&id) return id, err}
func listProducts(ctx context.Context, conn *pgx.Conn) ([]Product, error) { rows, err := conn.Query(ctx, `SELECT id, name, category, price FROM products ORDER BY id`) if err != nil { return nil, err } defer rows.Close() var out []Product for rows.Next() { var p Product if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Price); err != nil { return nil, err } out = append(out, p) } return out, rows.Err()}
func updatePrice(ctx context.Context, conn *pgx.Conn, id int, price float64) error { _, err := conn.Exec(ctx, `UPDATE products SET price = $1 WHERE id = $2`, price, id) return err}$1, $2 are PostgreSQL’s positional parameters; pgx binds them in the extended-Q protocol so the values never get string-interpolated into SQL.
4. Transactions
func transferStock(ctx context.Context, conn *pgx.Conn, fromID, toID int, qty int) error { tx, err := conn.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `UPDATE products SET in_stock = false WHERE id = $1`, fromID); err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE products SET in_stock = true WHERE id = $1`, toID); err != nil { return err } return tx.Commit(ctx)}Rollback after Commit is a no-op — idiomatic Go pattern works as expected.
5. Vector Schema
func setupVectorSchema(ctx context.Context, conn *pgx.Conn) error { _, err := conn.Exec(ctx, ` CREATE TABLE IF NOT EXISTS docs ( id SERIAL PRIMARY KEY, title TEXT NOT NULL, body TEXT, embedding VECTOR(384) ); CREATE INDEX IF NOT EXISTS docs_embedding_idx ON docs USING hnsw (embedding vector_cosine_ops); `) return err}6. Insert and Query Vectors via pgx
pgx doesn’t ship a pgx.Vector type out of the box, but the VECTOR(n) type accepts the standard Postgres array literal '[v1,v2,...]' over the wire. We marshal a []float32 to that text encoding and pgx forwards it as-is:
import ( "fmt" "strconv" "strings")
// vecLit converts a float slice into the Postgres VECTOR literal form.func vecLit(v []float32) string { parts := make([]string, len(v)) for i, f := range v { parts[i] = strconv.FormatFloat(float64(f), 'f', 6, 32) } return "[" + strings.Join(parts, ",") + "]"}
func insertDoc(ctx context.Context, conn *pgx.Conn, title, body string, vec []float32) error { _, err := conn.Exec(ctx, `INSERT INTO docs (title, body, embedding) VALUES ($1, $2, $3::vector)`, title, body, vecLit(vec), ) return err}The $3::vector cast tells the planner to coerce the text literal into a VECTOR(384) value.
k-NN search
type SearchHit struct { ID int Title string Distance float64}
func vectorSearch(ctx context.Context, conn *pgx.Conn, query []float32, k int) ([]SearchHit, error) { rows, err := conn.Query(ctx, `SELECT id, title, embedding <=> $1::vector AS distance FROM docs ORDER BY distance LIMIT $2`, vecLit(query), k, ) if err != nil { return nil, err } defer rows.Close() var hits []SearchHit for rows.Next() { var h SearchHit if err := rows.Scan(&h.ID, &h.Title, &h.Distance); err != nil { return nil, err } hits = append(hits, h) } return hits, rows.Err()}Distance operators (covered in VECTOR_SEARCH_TUTORIAL):
| Operator | Distance | Use case |
|---|---|---|
<=> | Cosine | Normalized text/sentence embeddings |
<-> | L2 | Image / unnormalized vectors |
<#> | Negative inner product | Pre-normalized, speed-critical |
7. Hybrid Search (BM25 + Vector)
Same query you’d write in Python or psql; pgx scans the result the same way:
func hybridSearch(ctx context.Context, conn *pgx.Conn, qVec []float32, qText string, alpha float64, k int) ([]SearchHit, error) { rows, err := conn.Query(ctx, ` SELECT id, title, $1::float * (1.0 - (embedding <=> $2::vector)) + (1.0 - $1::float) * ts_rank_cd(to_tsvector(body), plainto_tsquery($3)) AS score FROM docs WHERE to_tsvector(body) @@ plainto_tsquery($3) OR embedding <=> $2::vector < 0.5 ORDER BY score DESC LIMIT $4`, alpha, vecLit(qVec), qText, k, ) if err != nil { return nil, err } defer rows.Close() var hits []SearchHit for rows.Next() { var h SearchHit if err := rows.Scan(&h.ID, &h.Title, &h.Distance); err != nil { return nil, err } hits = append(hits, h) } return hits, rows.Err()}8. Putting It All Together
package main
import ( "context" "fmt" "log" "math/rand"
"github.com/jackc/pgx/v5")
func main() { ctx := context.Background() conn, err := pgx.Connect(ctx, "postgresql://postgres:s3cret@127.0.0.1:5432/postgres") if err != nil { log.Fatal(err) } defer conn.Close(ctx)
if err := setupVectorSchema(ctx, conn); err != nil { log.Fatal(err) }
// Insert a few synthetic vectors for i := 0; i < 5; i++ { v := make([]float32, 384) for j := range v { v[j] = rand.Float32() } if err := insertDoc(ctx, conn, fmt.Sprintf("Doc %d", i+1), fmt.Sprintf("Body for doc %d", i+1), v); err != nil { log.Fatal(err) } }
// Search with a fresh random vector q := make([]float32, 384) for j := range q { q[j] = rand.Float32() } hits, err := vectorSearch(ctx, conn, q, 3) if err != nil { log.Fatal(err) } for _, h := range hits { fmt.Printf("dist=%.4f id=%d %s\n", h.Distance, h.ID, h.Title) }}go run ./cmd# dist=0.4823 id=2 Doc 2# dist=0.4901 id=4 Doc 4# dist=0.5012 id=1 Doc 19. Connection Pooling for Servers
For HTTP services, swap pgx.Connect for pgxpool:
import "github.com/jackc/pgx/v5/pgxpool"
pool, err := pgxpool.New(ctx, dsn)// pool.Acquire(ctx) -> *pgxpool.Conn, do work, defer Release()pgxpool handles SCRAM negotiation lazily; first acquire pays the auth cost, subsequent acquires are free.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
connection refused | Nano not started, or wrong port | Confirm heliosdb-nano start ... is running and listening on 5432 |
unsupported authentication method | Old pgx version doesn’t support SCRAM | Upgrade pgx to v5+; or start Nano with --auth md5 |
cannot convert string to vector | Forgot $N::vector cast | Add the cast — pgx sends a text literal, server needs to coerce |
unknown OID warning in logs | pgx queries pg_type at startup | Harmless; pg_type returns the standard set |
| Slow ANN search | HNSW index missing or wrong opclass | Check EXPLAIN; ensure vector_cosine_ops matches <=> |
Where Next
- Go SDK reference — non-PG-wire Go client wrapping the REST surface.
- VECTOR_SEARCH_TUTORIAL — distance metrics, HNSW tuning.
- PYTHON_VECTOR_APP — equivalent end-to-end app in Python.
- BAAS_REST_API_TUTORIAL — when you’d prefer REST over PG-wire from Go.