embeddings

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 26, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

embeddings

In-process text embeddings, compiled into the baifo binary. No cgo, no external model runtime, no API key, no network. The model (nomic-embed-text-v1.5, a ~137M-parameter BERT-style encoder) ships inside the binary via go:embed, so baifo stays a single self-contained executable.

Usage (library)

eng, err := embeddings.New()           // loads the embedded model once
vec, err := eng.Embed("some text")     // []float32, length 768
unit, err := eng.EmbedNormalized(text) // L2-normalized, for cosine via dot
sim := embeddings.Cosine(a, b)         // cosine similarity

New() is lazy and cached: the weights are parsed once per process and the returned *Engine is safe for concurrent use (the forward pass allocates its own scratch per call; weights are read-only).

How it works

The forward pass is a faithful pure-Go reimplementation of the NomicBertModel architecture:

  • Tokenizer: BERT WordPiece (uncased), vocab in assets/vocab.txt.
  • Encoder: 12 post-norm transformer layers, hidden 768, 12 heads.
    • Rotary position embeddings (NeoX split convention, theta = 1000).
    • No learned position embeddings, no attention/MLP biases.
    • SwiGLU MLP: fc2( silu(fc12 x) ⊙ fc11 x ).
  • Pooling: mean over all token positions (unnormalized).

Per-token matmuls are parallelized across GOMAXPROCS with stdlib goroutines only.

Quantization

Weights are stored int8 (per-output-row symmetric scale) in assets/nomic-embed-text.weights. This keeps the embedded blob at ~132MB (vs ~547MB for raw f32) while preserving cosine similarity to within ~0.998 of the full-precision model - indistinguishable for retrieval.

Weight blob format (NMB1)
magic   "NMB1"                           (4 bytes)
header  int32 x5: hidden, nLayers, nHeads, vocab, inter
        float32 x2: rope_theta, ln_eps
embeddings:
        qmat  word_embeddings   [vocab, hidden]
        f32   token_type[0]     [hidden]
        f32   emb_ln.weight, emb_ln.bias  [hidden] each
per layer (x nLayers):
        qmat  attn.Wqkv         [3*hidden, hidden]
        qmat  attn.out_proj     [hidden, hidden]
        qmat  mlp.fc11          [inter, hidden]
        qmat  mlp.fc12          [inter, hidden]
        qmat  mlp.fc2           [hidden, inter]
        f32   norm1.weight, norm1.bias, norm2.weight, norm2.bias [hidden]

A qmat is rows*cols int8 values followed by rows float32 scales; element (r,c) = int8[r*cols+c] * scale[r].

Regenerating the weights

The blob is produced offline from the upstream safetensors checkpoint:

# 1. fetch model.safetensors + vocab.txt from
#    huggingface.co/nomic-ai/nomic-embed-text-v1.5
# 2. quantize + pack:
python3 tools/convert.py model.safetensors assets/nomic-embed-text.weights

Tests

embeddings_test.go checks the int8 Go engine against full-precision f32 reference vectors (testdata_ref.json, computed offline and themselves validated against the ollama nomic-embed-text oracle's semantic structure). The bar is cosine ≥ 0.99; in practice it lands at ~0.998. It also asserts the property that actually matters for retrieval: related sentences embed closer than unrelated ones.

Documentation

Overview

Package embeddings provides an in-process text embedding engine backed by the nomic-embed-text-v1.5 model whose weights are compiled into the baifo binary. It has no cgo and no heavyweight dependencies: the model is a small (~137M parameter) BERT-style encoder run in pure Go.

The public surface is intentionally tiny: New() loads the embedded model once, and Embed/EmbedBatch turn text into 768-dimensional float32 vectors suitable for cosine-similarity search.

Index

Constants

View Source
const Dimension = 768

Dimension is the size of every embedding vector this engine produces.

View Source
const MaxTokens = 2048

MaxTokens caps the sequence length fed to the model. nomic-embed-text supports up to 8192 via RoPE; we default to a sane 2048 to bound the O(n^2) attention cost, which is plenty for memory facts and snippets.

Variables

This section is empty.

Functions

func Cosine

func Cosine(a, b []float32) float32

Cosine returns the cosine similarity between two equal-length vectors.

func Normalize

func Normalize(v []float32)

Normalize scales v to unit L2 length in place (no-op for a zero vector).

Types

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine turns text into embedding vectors using the compiled-in nomic-embed-text model. It is safe for concurrent use: forward() allocates its own scratch buffers per call and the weights are read-only after construction.

func New

func New() (*Engine, error)

New builds (or returns the cached) Engine from the embedded weights. The model is loaded lazily and only once per process.

func (*Engine) Embed

func (e *Engine) Embed(text string) ([]float32, error)

Embed returns the embedding of a single piece of text.

func (*Engine) EmbedBatch

func (e *Engine) EmbedBatch(texts []string) ([][]float32, error)

EmbedBatch embeds several texts. Inputs are processed sequentially; the returned slice is index-aligned with texts.

func (*Engine) EmbedNormalized

func (e *Engine) EmbedNormalized(text string) ([]float32, error)

EmbedNormalized returns the L2-normalized embedding, convenient for cosine similarity via a plain dot product.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL