Documentation
¶
Overview ¶
Package simdvec is a vector index for embedding search, built on [simd.go](https://github.com/sebishogun/simd). No cgo, and the same code runs on amd64, arm64, riscv64, s390x, ppc64le and loong64.
ix := simdvec.New(768, simdvec.Cosine)
ix.Add("doc-1", embedding)
hits := ix.Search(query, 10)
Why this is fast ¶
The obvious way to search N embeddings is N dot products. That is N calls, and for a 768-dimension vector each one is over before the call overhead is amortised.
The vectors are stored instead as one contiguous N×D matrix, which makes the entire scan a single matrix-vector product: simd.GemvParallelInto computes every score in one call, across every core. Searching a hundred thousand embeddings is one Gemv, not a hundred thousand dots.
That is also why Add copies into the matrix rather than keeping a pointer. The layout is the optimisation.
There is no int8 index, and that was measured ¶
An int8 index was written, tested and deleted. Quantizing to int8 is a quarter of the memory and its recall was fine — 0.954 to 0.982 at k=10 — but it is slower, not faster, and by a lot.
The scan becomes simd.QMatMulInt8Into, and searching one query is an n×dim by dim×1 multiply. One output column is a degenerate shape for a matrix-multiply kernel, whose blocking assumes a wide result. Batching helps and does not rescue it, on 100,000 vectors of 768 dimensions:
int8, one query at a time 311.7 ms/query int8, batches of 8 37.5 ms/query int8, batches of 32 1.25 ms/query int8, batches of 128 1.11 ms/query float32, one query 0.21 ms/query
The best int8 arrangement is five times slower than the float32 scan, because GemvParallelInto is parallel and reads memory in the order the prefetcher wants, and four times the elements per register does not make up for either.
So this package stores float32. If the memory matters more than the latency, quantize before inserting — the index does not need to know.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrDim = errors.New("simdvec: wrong vector dimension")
ErrDim is returned when a vector's length does not match the index.
Functions ¶
This section is empty.
Types ¶
type Index ¶
type Index struct {
// contains filtered or unexported fields
}
Index is a flat (brute-force) index over float32 embeddings.
Every search scans every vector. That is the right structure up to a few hundred thousand embeddings, because one matrix-vector product over a contiguous block is bound by memory bandwidth rather than by arithmetic, and an approximate index only starts to win once the scan no longer fits in cache.
func (*Index) Add ¶
Add indexes a vector under id.
The vector is copied into the index's matrix; the caller's slice is not retained. For Cosine the copy is normalised on the way in, so the query-time comparison is a plain dot product.
type Metric ¶
type Metric int
Metric is how two vectors are compared.
const ( // Cosine compares direction and ignores magnitude. Vectors are normalised // on insert and on query, which turns the comparison into a dot product — // the division is done once per vector rather than once per comparison. Cosine Metric = iota // DotProduct compares without normalising, for models whose magnitude // carries meaning. DotProduct // Euclidean is straight-line distance. Computed from the dot product and // the precomputed norms rather than by subtracting, so it is the same one // matrix-vector product as the others. Euclidean )