---
title: "Go App — PG-Wire End-to-End (Vector + Hybrid Search)"
---

# Go App — PG-Wire End-to-End (Vector + Hybrid Search)

**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, and none is needed: the Nano binary speaks the PostgreSQL v3 wire protocol natively, so the standard Go PostgreSQL drivers (`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 or FFI is involved; the service uses pgx only.

---

## Prerequisites

```bash
mkdir helios-go && cd helios-go
go mod init example.com/helios-go
go get github.com/jackc/pgx/v5@latest
```

Start a Nano server:

```bash
heliosdb-nano start \
    --memory \
    --auth scram-sha-256 \
    --password s3cret
```

---

## 1. Connect

```go
// main.go
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 (HeliosDB Nano <release>)
}
```

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

```go
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

```go
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

```go
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

```go
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:

```go
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

```go
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](https://www.heliosdb.com/docs/nano/guides/tutorials/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:

```go
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

```go
// cmd/main.go
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)
    }
}
```

```bash
go run ./cmd
# dist=0.4823  id=2  Doc 2
# dist=0.4901  id=4  Doc 4
# dist=0.5012  id=1  Doc 1
```

---

## 9. Connection Pooling for Servers

For HTTP services, swap `pgx.Connect` for `pgxpool`:

```go
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](https://www.heliosdb.com/docs/nano/api/go-sdk/) — non-PG-wire Go client wrapping the REST surface.
- [VECTOR_SEARCH_TUTORIAL](https://www.heliosdb.com/docs/nano/guides/tutorials/vector_search_tutorial/) — distance metrics, HNSW tuning.
- [PYTHON_VECTOR_APP](https://www.heliosdb.com/docs/nano/guides/tutorials/sdk/python_vector_app/) — equivalent end-to-end app in Python.
- [BAAS_REST_API_TUTORIAL](https://www.heliosdb.com/docs/nano/guides/tutorials/baas_rest_api_tutorial/) — when you'd prefer REST over PG-wire from Go.
