Documentation
¶
Overview ¶
Package vector is the pgvector extension: the four vector types, every distance operator and function, the utility functions, aggregates, arithmetic, casts, and the query-time settings. Needs CREATE EXTENSION vector on the server. Wire forms are pgvector's text I/O, so any database/sql driver carries them without a codec.
Example (AggregatesAndArithmetic) ¶
Aggregates and arithmetic: the centroid of a category, and a query vector shifted by another.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
cat = sqlb.Col[int32](items, "category_id")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
show(sel().Exprs(cat, sqlb.As(vector.Avg(emb), "centroid"), sqlb.As(vector.Sum(emb), "total")).From(items).GroupBy(cat).Build())
q := vector.ParamVector(vector.Vector{3, 1, 2})
shift := vector.ParamVector(vector.Vector{0, 0, 1})
show(sel().Exprs(id).From(items).
Order(sqlb.OrderBy(vector.L2(emb, vector.L2Normalize(vector.Add(q, shift))), sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "items"."category_id", avg("items"."embedding") AS "centroid", sum("items"."embedding") AS "total" FROM "items" AS "items" GROUP BY "items"."category_id" args: [] SELECT "items"."id" FROM "items" AS "items" ORDER BY ("items"."embedding" <-> l2_normalize(($1::vector(3) + $2::vector(3)))) ASC LIMIT $3 args: [[3 1 2] [0 0 1] 5]
Example (BinaryQuantizeRerank) ¶
Binary quantization with re-ranking: the bit index finds candidates by Hamming distance, the full vector re-ranks them.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamVector(vector.Vector{3, 1, 2})
candidates := sqlb.Derived(sel().Exprs(id, emb).From(items).
Order(sqlb.OrderBy(vector.Hamming(vector.CastBit(vector.BinaryQuantize(emb), 3), vector.BinaryQuantize(q)), sqlb.SortAsc)).
Limit(20), "c")
c := sqlb.Ref(candidates)
cID, cEmb := sqlb.Col[int64](c, "id"), sqlb.Col[vector.Vector](c, "embedding")
show(sel().Exprs(cID).From(candidates).Order(sqlb.OrderBy(vector.L2(cEmb, q), sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "c"."id" FROM (SELECT "items"."id", "items"."embedding" FROM "items" AS "items" ORDER BY (binary_quantize("items"."embedding")::bit(3) <~> binary_quantize($1::vector(3))) ASC LIMIT $2) AS "c" ORDER BY ("c"."embedding" <-> $3::vector(3)) ASC LIMIT $4 args: [[3 1 2] 20 [3 1 2] 5]
Example (BinaryVectors) ¶
A native bit column: Hamming and Jaccard directly.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
bits = sqlb.Col[vector.BitVector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamBit("101")
show(sel().Exprs(id, vector.HammingDistance(bits, q)).From(items).
Order(sqlb.OrderBy(vector.Jaccard(bits, q), sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "items"."id", hamming_distance("items"."embedding", $1::bit(3)) FROM "items" AS "items" ORDER BY ("items"."embedding" <%> $2::bit(3)) ASC LIMIT $3 args: [101 101 5]
Example (CastFromArray) ¶
A float array column or literal becomes a vector by cast, the README's way in from an existing schema.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
arr := sqlb.Col[[]float32](items, "arr")
show(sel().Exprs(id, vector.Dims(vector.CastVector(arr, 0))).From(items).
Where(sqlb.Less(vector.L2(vector.CastVector(arr, 3), vector.ParamVector(vector.Vector{1, 2, 3})), 1.0)).Build())
}
Output: SELECT "items"."id", vector_dims("items"."arr"::vector) FROM "items" AS "items" WHERE (("items"."arr"::vector(3) <-> $1::vector(3)) < $2) args: [[1 2 3] 1]
Example (Distances) ¶
Cosine similarity as 1 minus cosine distance, inner product negated back to positive, and a distance filter in WHERE.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamVector(vector.Vector{3, 1, 2})
sim := sqlb.Binary[float64](sqlb.Param(1.0), sqlb.OpSub, vector.Cosine(emb, q))
ip := sqlb.Binary[float64](vector.InnerProduct(emb, q), sqlb.OpMul, sqlb.Param(-1.0))
show(sel().Exprs(id, sqlb.As(sim, "cosine_similarity"), sqlb.As(ip, "inner_product")).From(items).
Where(sqlb.Less(vector.L2(emb, q), 5.0)).Build())
}
Output: SELECT "items"."id", ($1 - ("items"."embedding" <=> $2::vector(3))) AS "cosine_similarity", (("items"."embedding" <#> $3::vector(3)) * $4) AS "inner_product" FROM "items" AS "items" WHERE (("items"."embedding" <-> $5::vector(3)) < $6) args: [1 [3 1 2] [3 1 2] -1 [3 1 2] 5]
Example (FilteredWithIterativeScan) ¶
Filtering with an approximate index: a WHERE on another column, and the iterative scan setting bound for the transaction so the index keeps scanning until LIMIT rows pass the filter. Two statements, one transaction.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
cat = sqlb.Col[int32](items, "category_id")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
show(sel().Exprs(vector.SetHNSWIterativeScan(vector.ScanRelaxedOrder), vector.SetEfSearch(100)).Build())
q := vector.ParamVector(vector.Vector{3, 1, 2})
show(sel().Exprs(id).From(items).Where(sqlb.Eq(cat, int32(123))).
Order(sqlb.OrderBy(vector.L2(emb, q), sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT set_config('hnsw.iterative_scan', $1, true), set_config('hnsw.ef_search', $2, true) args: [relaxed_order 100] SELECT "items"."id" FROM "items" AS "items" WHERE ("items"."category_id" = $1) ORDER BY ("items"."embedding" <-> $2::vector(3)) ASC LIMIT $3 args: [123 [3 1 2] 5]
Example (HalfPrecision) ¶
Half-precision: the column is halfvec, or a vector column is cast to halfvec so an expression index on the cast serves the search.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
half = sqlb.Col[vector.HalfVector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamHalf(vector.HalfVector{3, 1, 2})
show(sel().Exprs(id).From(items).
Order(sqlb.OrderBy(vector.L2(vector.CastHalf(emb, 3), q), sqlb.SortAsc)).Limit(5).Build())
show(sel().Exprs(id, vector.L2Norm(half)).From(items).
Order(sqlb.OrderBy(vector.Cosine(half, q), sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "items"."id" FROM "items" AS "items" ORDER BY ("items"."embedding"::halfvec(3) <-> $1::halfvec(3)) ASC LIMIT $2 args: [[3 1 2] 5] SELECT "items"."id", l2_norm("items"."embedding") FROM "items" AS "items" ORDER BY ("items"."embedding" <=> $1::halfvec(3)) ASC LIMIT $2 args: [[3 1 2] 5]
Example (HybridSearchRRF) ¶
Hybrid search with Reciprocal Rank Fusion, the README's example: a semantic CTE ranked by cosine distance, a keyword CTE ranked by ts_rank_cd, a FULL JOIN on id, and 1/(k+rank) summed.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
docs := sqlb.Table{Name: "documents", Alias: "documents"}
dID, content, dEmb := sqlb.Col[int64](docs, "id"), sqlb.Col[string](docs, "content"), sqlb.Col[vector.Vector](docs, "embedding")
q := vector.ParamVector(vector.Vector{3, 1, 2})
dist := vector.Cosine(dEmb, q)
semantic := sel().Exprs(dID, sqlb.As(sqlb.Rank().Over(sqlb.NewWindow().OrderBy(sqlb.OrderBy(dist, sqlb.SortAsc))), "rank")).
From(docs).Order(sqlb.OrderBy(dist, sqlb.SortAsc)).Limit(20)
tsq := pg.PlainToTSQuery("english", "postgres vector search")
tsv := pg.ToTSVector("english", content)
score := pg.TSRankCD(tsv, tsq)
keyword := sel().Exprs(dID, sqlb.As(sqlb.Rank().Over(sqlb.NewWindow().OrderBy(sqlb.OrderBy(score, sqlb.SortDesc))), "rank")).
From(docs).Where(pg.Matches(tsv, tsq)).Order(sqlb.OrderBy(score, sqlb.SortDesc)).Limit(20)
s := sqlb.Table{Name: "semantic_search", Alias: "s"}
k := sqlb.Table{Name: "keyword_search", Alias: "k"}
sID, sRank := sqlb.Col[int64](s, "id"), sqlb.Nullable(sqlb.Col[int64](s, "rank"))
kID, kRank := sqlb.Col[int64](k, "id"), sqlb.Nullable(sqlb.Col[int64](k, "rank"))
// 1/(k+rank) is NULL for a side the FULL JOIN did not match; COALESCE makes it 0.
rrf := func(rank sqlb.Expr[*int64]) sqlb.Expr[float64] {
score := sqlb.Binary[*float64](sqlb.Param(1.0), sqlb.OpDiv, sqlb.Binary[*float64](sqlb.Param(60), sqlb.OpAdd, rank))
return sqlb.AssertNotNull(sqlb.Coalesce(score, sqlb.Nullable(sqlb.Param(0.0))))
}
total := sqlb.Binary[float64](rrf(sRank), sqlb.OpAdd, rrf(kRank))
show(sel().With("semantic_search", semantic).With("keyword_search", keyword).
Exprs(sqlb.As(sqlb.Coalesce(sqlb.Nullable(sID), sqlb.Nullable(kID)), "id"), sqlb.As(total, "score")).
From(s).Join(sqlb.Join{Type: pg.JoinFull, Table: k, On: []sqlb.Predicate{sqlb.EqExpr(sID, kID)}}).
Order(sqlb.OrderBy(sqlb.OutputCol[float64]("score"), sqlb.SortDesc)).Limit(5).Build())
}
Output: WITH "semantic_search" AS (SELECT "documents"."id", rank() OVER (ORDER BY ("documents"."embedding" <=> $1::vector(3)) ASC) AS "rank" FROM "documents" AS "documents" ORDER BY ("documents"."embedding" <=> $2::vector(3)) ASC LIMIT $3), "keyword_search" AS (SELECT "documents"."id", rank() OVER (ORDER BY ts_rank_cd(to_tsvector('english', "documents"."content"), plainto_tsquery('english', $4)) DESC) AS "rank" FROM "documents" AS "documents" WHERE ((to_tsvector('english', "documents"."content") @@ plainto_tsquery('english', $5))) ORDER BY ts_rank_cd(to_tsvector('english', "documents"."content"), plainto_tsquery('english', $6)) DESC LIMIT $7) SELECT COALESCE("s"."id", "k"."id") AS "id", (COALESCE(($8 / ($9 + "s"."rank")), $10) + COALESCE(($11 / ($12 + "k"."rank")), $13)) AS "score" FROM "semantic_search" AS "s" FULL JOIN "keyword_search" AS "k" ON ("s"."id" = "k"."id") ORDER BY "score" DESC LIMIT $14 args: [[3 1 2] [3 1 2] 20 postgres vector search postgres vector search postgres vector search 20 1 60 0 1 60 0 5]
Example (Insert) ¶
Insert vectors: pgvector-go's first example. The Vector value renders in text form and the driver sends it as a string.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
emb = sqlb.Col[vector.Vector](items, "embedding")
)
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
show(sqlb.NewInsert(d).Into(items).
Row(sqlb.Set(emb, vector.Vector{1, 2, 3})).
Row(sqlb.Set(emb, vector.Vector{4, 5, 6})).Build())
}
Output: INSERT INTO "items" AS "items" ("embedding") VALUES ($1), ($2) args: [[1 2 3] [4 5 6]]
Example (Nearest) ¶
Nearest neighbours by L2: ORDER BY distance LIMIT n is the shape an HNSW or IVFFlat index serves. The same expression is the SELECT item.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamVector(vector.Vector{3, 1, 2})
dist := vector.L2(emb, q)
show(sel().Exprs(id, sqlb.As(dist, "distance")).From(items).
Order(sqlb.OrderBy(dist, sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "items"."id", ("items"."embedding" <-> $1::vector(3)) AS "distance" FROM "items" AS "items" ORDER BY ("items"."embedding" <-> $2::vector(3)) ASC LIMIT $3 args: [[3 1 2] [3 1 2] 5]
Example (NearestToRow) ¶
Nearest neighbours to a row: the query vector is a subquery.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
target := sqlb.AssertNotNull(sqlb.Subquery[vector.Vector](sel().Exprs(emb).From(items).Where(sqlb.Eq(id, int64(1)))))
dist := vector.L2(emb, target)
show(sel().Exprs(id).From(items).Where(sqlb.NotEq(id, int64(1))).
Order(sqlb.OrderBy(dist, sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "items"."id" FROM "items" AS "items" WHERE ("items"."id" <> $1) ORDER BY ("items"."embedding" <-> (SELECT "items"."embedding" FROM "items" AS "items" WHERE ("items"."id" = $2))) ASC LIMIT $3 args: [1 1 5]
Example (RelaxedOrderStrictResult) ¶
A relaxed-order scan can return rows slightly out of order. The README's fix: a MATERIALIZED CTE for the scan and an outer ORDER BY.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamVector(vector.Vector{3, 1, 2})
dist := vector.L2(emb, q)
inner := sel().Exprs(id, sqlb.As(dist, "distance")).From(items).
Order(sqlb.OrderBy(dist, sqlb.SortAsc)).Limit(5)
relaxed := sqlb.Table{Name: "relaxed_results", Alias: "relaxed_results"}
rDist := sqlb.Col[float64](relaxed, "distance")
show(sel().WithCTE(sqlb.CTE{Name: "relaxed_results", Body: inner, Prefix: pg.Materialized(true)}).
Exprs(sqlb.Col[int64](relaxed, "id"), rDist).From(relaxed).
Order(sqlb.OrderBy(rDist, sqlb.SortAsc)).Build())
}
Output: WITH "relaxed_results" AS MATERIALIZED (SELECT "items"."id", ("items"."embedding" <-> $1::vector(3)) AS "distance" FROM "items" AS "items" ORDER BY ("items"."embedding" <-> $2::vector(3)) ASC LIMIT $3) SELECT "relaxed_results"."id", "relaxed_results"."distance" FROM "relaxed_results" AS "relaxed_results" ORDER BY "relaxed_results"."distance" ASC args: [[3 1 2] [3 1 2] 5]
Example (Sparse) ¶
Sparse vectors: the same operators over sparsevec, with the text form {index:value,...}/dims bound as the parameter.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
spv = sqlb.Col[vector.SparseVector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamSparse(vector.NewSparse([]float32{0, 1, 0, 2, 0}))
show(sel().Exprs(id).From(items).Order(sqlb.OrderBy(vector.L2(spv, q), sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "items"."id" FROM "items" AS "items" ORDER BY ("items"."embedding" <-> $1::sparsevec(5)) ASC LIMIT $2 args: [{2:1,4:2}/5 5]
Example (SubvectorRerank) ¶
Subvector indexing: an index on the first dimensions of a Matryoshka embedding serves the candidate scan, the full vector re-ranks.
package main
import (
"fmt"
"strings"
"github.com/ubgo/sqlb"
"github.com/ubgo/sqlb/pg/vector"
)
var (
d = sqlb.PostgresDialect{}
items = sqlb.Table{Name: "items", Alias: "items"}
id = sqlb.Col[int64](items, "id")
emb = sqlb.Col[vector.Vector](items, "embedding")
)
func sel() *sqlb.Select[sqlb.PostgresDialect] { return sqlb.NewSelect(d) }
func show(q sqlb.Query, err error) {
if err != nil {
fmt.Println("error:", err)
return
}
sql := strings.ReplaceAll(q.SQL, ") SELECT ", ")\nSELECT ")
sql = strings.ReplaceAll(sql, `), "`, "),\n\"")
fmt.Println(sql)
fmt.Println("args:", q.Args)
}
func main() {
q := vector.ParamVector(vector.Vector{3, 1, 2, 4})
head := vector.CastVector(vector.Subvector(emb, 1, 2), 2)
qHead := vector.CastVector(vector.Subvector(q, 1, 2), 2)
candidates := sqlb.Derived(sel().Exprs(id, emb).From(items).
Order(sqlb.OrderBy(vector.Cosine(head, qHead), sqlb.SortAsc)).Limit(20), "c")
c := sqlb.Ref(candidates)
cID, cEmb := sqlb.Col[int64](c, "id"), sqlb.Col[vector.Vector](c, "embedding")
show(sel().Exprs(cID).From(candidates).Order(sqlb.OrderBy(vector.Cosine(cEmb, q), sqlb.SortAsc)).Limit(5).Build())
}
Output: SELECT "c"."id" FROM (SELECT "items"."id", "items"."embedding" FROM "items" AS "items" ORDER BY (subvector("items"."embedding", $1, $2)::vector(2) <=> subvector($3::vector(4), $4, $5)::vector(2)) ASC LIMIT $6) AS "c" ORDER BY ("c"."embedding" <=> $7::vector(4)) ASC LIMIT $8 args: [1 2 [3 1 2 4] 1 2 20 [3 1 2 4] 5]
Index ¶
- Constants
- Variables
- func Add[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[V]
- func Avg[V Elementwise](v sqlb.Expr[V], opts ...sqlb.AggOption) sqlb.Expr[V]
- func BinaryQuantize[V Elementwise](v sqlb.Expr[V]) sqlb.Expr[BitVector]
- func CastBit[T any](e sqlb.Expr[T], dims int) sqlb.Expr[BitVector]
- func CastHalf[T any](e sqlb.Expr[T], dims int) sqlb.Expr[HalfVector]
- func CastSparse[T any](e sqlb.Expr[T], dims int) sqlb.Expr[SparseVector]
- func CastVector[T any](e sqlb.Expr[T], dims int) sqlb.Expr[Vector]
- func Concat[V Elementwise](a, b sqlb.Expr[V]) sqlb.Expr[V]
- func Cosine[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func CosineDistance[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func Dims(v sqlb.Expr[Vector]) sqlb.Expr[int32]
- func EfConstruction(n int64) pg.IndexParam
- func ForceExact() sqlb.Expr[string]
- func ForceIndex() sqlb.Expr[string]
- func Hamming(a, b sqlb.Expr[BitVector]) sqlb.Expr[float64]
- func HammingDistance(a, b sqlb.Expr[BitVector]) sqlb.Expr[float64]
- func InnerProduct[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func InnerProductOf[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func Jaccard(a, b sqlb.Expr[BitVector]) sqlb.Expr[float64]
- func JaccardDistance(a, b sqlb.Expr[BitVector]) sqlb.Expr[float64]
- func L1[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func L1Distance[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func L2[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func L2Distance[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[float64]
- func L2Norm[V HalfVector | SparseVector](v sqlb.Expr[V]) sqlb.Expr[float64]
- func L2Normalize[V Dense](v sqlb.Expr[V]) sqlb.Expr[V]
- func Lists(n int64) pg.IndexParam
- func M(n int64) pg.IndexParam
- func Mul[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[V]
- func Norm(v sqlb.Expr[Vector]) sqlb.Expr[float64]
- func ParamBit(v BitVector) sqlb.Expr[BitVector]
- func ParamHalf(v HalfVector) sqlb.Expr[HalfVector]
- func ParamSparse(v SparseVector) sqlb.Expr[SparseVector]
- func ParamVector(v Vector) sqlb.Expr[Vector]
- func SetEfSearch(n int) sqlb.Expr[string]
- func SetHNSWIterativeScan(mode IterativeScan) sqlb.Expr[string]
- func SetIVFFlatIterativeScan(mode IterativeScan) sqlb.Expr[string]
- func SetMaxProbes(n int) sqlb.Expr[string]
- func SetMaxScanTuples(n int) sqlb.Expr[string]
- func SetProbes(n int) sqlb.Expr[string]
- func Sub[V Dense](a, b sqlb.Expr[V]) sqlb.Expr[V]
- func Subvector[V Elementwise](v sqlb.Expr[V], start, count int) sqlb.Expr[V]
- func Sum[V Dense](v sqlb.Expr[V], opts ...sqlb.AggOption) sqlb.Expr[V]
- type BitVector
- type Dense
- type Elementwise
- type HalfVector
- type IterativeScan
- type SparseVector
- type Vector
Examples ¶
- Package (AggregatesAndArithmetic)
- Package (BinaryQuantizeRerank)
- Package (BinaryVectors)
- Package (CastFromArray)
- Package (Distances)
- Package (FilteredWithIterativeScan)
- Package (HalfPrecision)
- Package (HybridSearchRRF)
- Package (Insert)
- Package (Nearest)
- Package (NearestToRow)
- Package (RelaxedOrderStrictResult)
- Package (Sparse)
- Package (SubvectorRerank)
Constants ¶
const ( HNSW pg.IndexMethod = "hnsw" IVFFlat pg.IndexMethod = "ivfflat" )
HNSW and IVFFlat are pgvector's index methods. HNSW builds slower and answers better; IVFFlat needs rows in the table before it is built, because it clusters what it finds.
const ( // Settings pgvector reads at query time. SetEfSearch and friends bind // them for the transaction through set_config(..., true). SettingHNSWEfSearch = "hnsw.ef_search" SettingHNSWIterativeScan = "hnsw.iterative_scan" SettingHNSWMaxScanTuples = "hnsw.max_scan_tuples" SettingHNSWScanMemMultiplier = "hnsw.scan_mem_multiplier" SettingIVFFlatProbes = "ivfflat.probes" SettingIVFFlatIterativeScan = "ivfflat.iterative_scan" SettingIVFFlatMaxProbes = "ivfflat.max_probes" SettingEnableSeqscan = "enable_seqscan" SettingEnableIndexscan = "enable_indexscan" )
Construct names, as they appear in ErrUnsupportedDialect.
Variables ¶
var ( CosineOps = pg.NewOpClass[Vector]("vector_cosine_ops") L2Ops = pg.NewOpClass[Vector]("vector_l2_ops") IPOps = pg.NewOpClass[Vector]("vector_ip_ops") L1Ops = pg.NewOpClass[Vector]("vector_l1_ops") HalfCosineOps = pg.NewOpClass[HalfVector]("halfvec_cosine_ops") HalfL2Ops = pg.NewOpClass[HalfVector]("halfvec_l2_ops") HalfIPOps = pg.NewOpClass[HalfVector]("halfvec_ip_ops") )
The operator classes, one per distance, typed to the vector kind they index: an index is used only by a query ordering by the same distance.
var ErrFormat = errors.New("vector: value is not in pgvector text form")
ErrFormat is returned by Scan for a value not in the type's text form.
var Extension = pg.Extension{Name: "vector"}
Extension is the pgvector extension this package's constructs need, for pg.CreateExtension(Extension), so a library installs what it uses without spelling its name.
Functions ¶
func Add ¶
Add renders a + b, element-wise. The two operands must have equal dimensions or the server raises an error.
func Avg ¶
Avg renders avg(v), the element-wise mean: the centroid of a group. There is no avg(sparsevec) in pgvector, so this takes Elementwise only.
func BinaryQuantize ¶
BinaryQuantize renders binary_quantize(v): one bit per element, positive is 1. Cast it with CastBit to compare with a bit(n) column.
func CastHalf ¶
CastHalf renders e::halfvec(dims). An expression index on this cast is how a vector column gets a half-precision index.
func CastSparse ¶
CastSparse renders e::sparsevec(dims).
func CastVector ¶
CastVector renders e::vector(dims). e may be a float array, text, or another vector type.
func Concat ¶
func Concat[V Elementwise](a, b sqlb.Expr[V]) sqlb.Expr[V]
Concat renders a || b: the two vectors end to end, so the result has the sum of their dimensions.
func CosineDistance ¶
CosineDistance renders cosine_distance(a, b), the function form of <=>.
func EfConstruction ¶
func EfConstruction(n int64) pg.IndexParam
EfConstruction is HNSW's candidate list while building, 64 by default.
func ForceExact ¶
ForceExact binds enable_indexscan = off, so the search is exact and slow. It is how a recall measurement gets its ground truth.
func ForceIndex ¶
ForceIndex binds enable_seqscan = off, so the planner takes the vector index. It is for proving an index is used, never for production traffic.
func HammingDistance ¶
HammingDistance renders hamming_distance(a, b), the function form of <~>.
func InnerProduct ¶
InnerProduct renders a <#> b, the NEGATIVE inner product, because Postgres indexes support ascending order only. InnerProductOf is the positive function form.
func InnerProductOf ¶
InnerProductOf renders inner_product(a, b), the positive value.
func JaccardDistance ¶
JaccardDistance renders jaccard_distance(a, b), the function form of <%>.
func L1Distance ¶
L1Distance renders l1_distance(a, b), the function form of <+>.
func L2Distance ¶
L2Distance renders l2_distance(a, b), the same number as the <-> operator. The function form goes where an operator cannot: a function argument, a CASE branch, a check constraint. Only the operator uses an index.
func L2Norm ¶
func L2Norm[V HalfVector | SparseVector](v sqlb.Expr[V]) sqlb.Expr[float64]
L2Norm renders l2_norm(v), the Euclidean length of a halfvec or sparsevec. Postgres names this function differently per type, hence the two helpers.
func L2Normalize ¶
L2Normalize renders l2_normalize(v): the unit-length vector.
func Lists ¶
func Lists(n int64) pg.IndexParam
Lists is IVFFlat's cluster count; rows/1000 is pgvector's starting point.
func M ¶
func M(n int64) pg.IndexParam
M is HNSW's connections per layer, 16 by default: higher answers better and builds slower.
func Norm ¶
Norm renders vector_norm(v), the Euclidean length. It is for vector only; L2Norm is the halfvec and sparsevec spelling of the same thing.
func ParamHalf ¶
func ParamHalf(v HalfVector) sqlb.Expr[HalfVector]
ParamHalf binds v as $n::halfvec(len).
func ParamSparse ¶
func ParamSparse(v SparseVector) sqlb.Expr[SparseVector]
ParamSparse binds v as $n::sparsevec(dims), the dimension count taken from the value, not from the number of non-zero elements.
func ParamVector ¶
ParamVector binds v and casts it: $n::vector(len). The dimension comes from the value, so it cannot disagree with it.
func SetEfSearch ¶
SetEfSearch binds hnsw.ef_search: candidates per HNSW scan, default 40.
func SetHNSWIterativeScan ¶
func SetHNSWIterativeScan(mode IterativeScan) sqlb.Expr[string]
SetHNSWIterativeScan binds hnsw.iterative_scan, so a filtered search keeps scanning until LIMIT rows pass the WHERE clause instead of returning fewer. ScanRelaxedOrder is faster and may return rows slightly out of order; wrap the scan in a MATERIALIZED CTE and sort again to fix that.
func SetIVFFlatIterativeScan ¶
func SetIVFFlatIterativeScan(mode IterativeScan) sqlb.Expr[string]
SetIVFFlatIterativeScan binds ivfflat.iterative_scan. IVFFlat supports ScanRelaxedOrder only, never ScanStrictOrder.
func SetMaxProbes ¶
SetMaxProbes binds ivfflat.max_probes, the IVFFlat ceiling matching SetMaxScanTuples.
func SetMaxScanTuples ¶
SetMaxScanTuples binds hnsw.max_scan_tuples, the ceiling on how far an iterative scan goes before it gives up and returns what it has.
Types ¶
type BitVector ¶
type BitVector string
BitVector is a bit(n) used as a binary vector, text form "0101".
type Dense ¶
type Dense interface {
Vector | HalfVector | SparseVector
}
Dense is a vector type with element-wise distance operators.
type Elementwise ¶
type Elementwise interface{ Vector | HalfVector }
Elementwise is a vector type with arithmetic and concatenation.
type HalfVector ¶
type HalfVector []float32
HalfVector is a halfvec(n): up to 4000 half-precision elements. Go has no float16, so it carries float32 and the server rounds. Text form "[1,2,3]".
func (*HalfVector) Scan ¶
func (v *HalfVector) Scan(src any) error
Scan parses "[1,2,3]" into float32. The server has already rounded to half precision, so the value read back is not the value written.
type IterativeScan ¶
type IterativeScan string
IterativeScan is a value for hnsw.iterative_scan or ivfflat.iterative_scan.
const ( ScanOff IterativeScan = "off" ScanStrictOrder IterativeScan = "strict_order" // hnsw only ScanRelaxedOrder IterativeScan = "relaxed_order" // hnsw and ivfflat )
The iterative-scan modes. Off is the default and can return fewer rows than LIMIT once a filter rejects candidates; the other two keep scanning, and relaxed order trades exact ordering for speed.
type SparseVector ¶
type SparseVector struct {
Dimensions int32
Indices []int32 // 0-based, as pgvector-go keeps them
Values []float32
}
SparseVector is a sparsevec(n): only the non-zero elements, text form "{1:1.5,3:2}/5" with 1-based indices. Indices must be strictly increasing.
func NewSparse ¶
func NewSparse(dense []float32) SparseVector
NewSparse builds a SparseVector from a dense slice, keeping the non-zero elements, the same as pgvector-go's NewSparseVector.
func (SparseVector) String ¶
func (v SparseVector) String() string
String is the text form, so a logged parameter reads as pgvector sees it.