Lightweight local embedding store for Go β pure Go, no external services, blazing fast nearest-vector search.
π‘ goembedx is a tiny vector database for embeddings β perfect for local LLM agents, RAG systems, and semantic search inside Go applications.
- π₯ Pure Go (no CGO, no external libraries)
- β‘ Fast cosine similarity search
- π¦ In-memory, SQLite, and BadgerDB backends behind one
store.Storeinterface - π§© Searcher interface + composable Filter DSL (
Eq/In/Exists/And/Or/Not) - 𧬠Precomputed vector norms for optimized search
- π Import/export vector functionality
- π§ͺ Blocked dot products with auto-tuned block size
- β‘ DotBatch serial/parallel with parallel threshold heuristic
- π§ RAG pipeline β
Retriever+FormatContext+BuildPromptwith token-budget management - π§©
Embedderinterface + deterministicDummyembedder for offline testing - π₯οΈ CLI tools (
goembedx add/search) for vector management - πΎ Works offline β great for agents on the edge
- π§ͺ Fully tested, clean API, blazing performance
- π Future: goembedx serve β REST API mode
β οΈ Future: SIMD backends (AVX2 / NEON) and Faiss comparison- π§ Future: Optional ANN index (HNSW lite)
Every vector store implements the store.Store interface
(SaveVector/GetVector/GetAllVectors/Add/Get/Search/Close), so
swapping backends is a one-line change. The simplest one is in-memory:
import (
"fmt"
"github.com/ldaidone/goembedx/pkg/store"
)
func main() {
// In-memory store constrained to 384-dim vectors (MiniLM, etc.).
s := store.NewMemory(384)
// Add vectors with optional metadata.
if err := s.Add("doc1", []float32{0.1, 0.2, 0.3}, map[string]any{"title": "hello world"}); err != nil {
panic(err)
}
if err := s.Add("doc2", []float32{0.4, 0.5, 0.6}, map[string]any{"title": "go embedding"}); err != nil {
panic(err)
}
// Cosine similarity search: top-k results sorted by descending score.
results, err := s.Search([]float32{0.15, 0.25, 0.35}, 3)
if err != nil {
panic(err)
}
for _, r := range results {
fmt.Printf("%s %.4f %v\n", r.ID, r.Score, r.Meta)
}
}Use a BadgerDB- or SQLite-backed store when vectors must survive a restart.
Both expose the same store.Store interface β a typical semantic-indexing
workflow reads an existing vector with Get (returning the vector, its
precomputed norm, and metadata), skips re-embedding unchanged payloads, and
writes fresh vectors with Add:
import "github.com/ldaidone/goembedx/pkg/store"
// BadgerDB-backed store (single process, fastest local access).
badgerStore, err := store.NewBadger("./data/badger")
if err != nil {
panic(err)
}
defer badgerStore.Close()
// SQLite-backed store (safe for multiple processes sharing the DB file).
sqliteStore, err := store.NewSQLite("./data/sqlite")
if err != nil {
panic(err)
}
defer sqliteStore.Close()
// Vectors travel with optional metadata, e.g. a payload hash so incremental
// rebuilds only re-embed content that changed since the last run.
const id = "doc1"
const payloadHash = "abc123"
vec, _, meta, err := sqliteStore.Get(id)
if err == nil && meta != nil && meta["payload_hash"] == payloadHash && len(vec) > 0 {
// Vector already stored for this exact payload β nothing to do.
return
}
// Embed the (possibly new) payload and store it with its hash.
if err := sqliteStore.Add(id, []float32{0.1, 0.2, 0.3}, map[string]any{"payload_hash": payloadHash}); err != nil {
panic(err)
}The higher-level embedx engine needs only the basic vector operations
(embedx.VectorStore), which every store.Store satisfies:
import "github.com/ldaidone/goembedx/pkg/embedx"
engine := embedx.New(sqliteStore) // *store.SQLite implements embedx.VectorStore
engine.Add("doc3", []float32{0.1, 0.2, 0.3})
results, err := engine.Search([]float32{0.1, 0.2, 0.3}, 3)Because store.Store is a public interface, you can also implement your own
backend in your own package and plug it in β no internal packages required.
The Searcher interface (embedx.Searcher) exposes SearchContext(ctx, query, opts...).
Use the Filter DSL to restrict results by metadata before ranking:
import "github.com/ldaidone/goembedx/pkg/embedx"
// Search with metadata filters.
results, err := store.SearchContext(ctx, queryVec,
embedx.WithK(5),
embedx.WithFilter(embedx.And(
embedx.Eq("lang", "en"),
embedx.Exists("title"),
)),
)
// Compose filters: Eq, In, Exists, And, Or, Not.
langFilter := embedx.In("lang", "en", "de")
docTypeFilter := embedx.Eq("type", "article")
combined := embedx.And(langFilter, docTypeFilter)The rag package ties everything together for Retrieval-Augmented Generation:
import "github.com/ldaidone/goembedx/pkg/rag"
// Create a retriever backed by any Embedder + Searcher.
ret := rag.NewRetriever(myEmbedder, myStore,
rag.WithTopK(5), // default top-k
rag.WithTextKey("text"), // metadata key holding the chunk text
)
// Retrieve relevant chunks for a query.
chunks, err := ret.Retrieve(ctx, "What is Go?")
// Format chunks into a citation-aware context block.
ctxText := rag.FormatContext(chunks, 2048) // token budget
// Build a full RAG prompt for an LLM.
prompt := rag.BuildPrompt("You are a helpful assistant.", "What is Go?", ctxText)
fmt.Println(prompt)# Add a vector with ID
goembedx add doc1 0.1 0.2 0.3 0.4
# Search for similar vectors
goembedx search 0.15 0.25 0.35 0.45go get github.com/ldaidone/goembedxCheck our complete roadmap and future plans in ROADMAP.md.
go test ./...or you can use Makefile commands
make testTo know all available commands run
make helpApache 2.0 License - see the LICENSE file for details.
If this saves you time or helps your AI project, consider starring β and consider buying me a coffee! βοΈ β it keeps the ideas flowing!
- RAG pipeline:
Retriever,FormatContext,BuildPrompt, token-budget management. Embedderinterface + deterministicDummyembedder (FNV-hash, unit-norm).Searcherinterface with composable Filter DSL (Eq/In/Exists/And/Or/Not).- Blocked dot products with auto-tuned block size; DotBatch serial/parallel threshold.
models/get-small.gtemodelbinary asset.scripts/ascii-banner: portable 3D-shadow ASCII banner with rainbow/solid/per-letter color,NO_COLORsupport, andmake install-bannertarget.