Embedded Vector Database for Go and Node.js -- milvus-lite Wrappers
Background
I've been building some AI Agent tooling recently and needed a local vector database for semantic search. Milvus is a solid choice, and its lightweight variant milvus-lite is perfect -- no etcd, no MinIO, just a single file database.
The catch? milvus-lite only ships with Python bindings. If you're writing your agent in Go or Node.js, you're out of luck. The official stance is pretty clear: the Node.js SDK maintainer closed the feature request saying "C++ core is not portable to TypeScript"; the Go version has been sitting in an issue discussion with no progress.
But if you look at how milvus-lite actually works, this reasoning doesn't hold up.
What milvus-lite really is
Under the hood, milvus-lite is a standalone C++ gRPC server binary. The Python layer does exactly two things:
- Starts this binary via
subprocess.Popen - Communicates through the standard Milvus gRPC protocol
In other words, Python is just a process launcher. The real work is done by the C++ binary, which exposes the exact same gRPC interface as Milvus Standalone.
Any language that can spawn a subprocess and speak gRPC can use milvus-lite. No CGo, no FFI, no porting C++ to anything.
So I built two wrapper libraries:
- Go: github.com/lyyyuna/milvus-lite-go
- Node.js/TypeScript: @lyyyuna/milvus-lite (npm)
Installation
Go
go get github.com/lyyyuna/milvus-lite-go/v2
The binary is embedded in platform-specific sub-modules via go:embed. The Go module proxy only downloads the sub-module matching your platform (~25-55MB), not all of them.
Node.js
npm install @lyyyuna/milvus-lite @zilliz/milvus2-sdk-node
Same distribution pattern as esbuild and swc: the main package is pure JS, binaries are split into platform-specific npm packages as optionalDependencies. npm automatically installs only the one for your OS/arch.
Both versions require zero runtime downloads. Install and go.
Usage
Go
package main
import (
"context"
"log"
milvuslite "github.com/lyyyuna/milvus-lite-go/v2"
"github.com/milvus-io/milvus-sdk-go/v2/client"
"github.com/milvus-io/milvus-sdk-go/v2/entity"
)
func main() {
// Start milvus-lite, data persisted to local file
server, err := milvuslite.Start("./milvus.db")
if err != nil {
log.Fatal(err)
}
defer server.Stop()
// Connect with the official SDK -- no changes needed
c, _ := client.NewClient(context.Background(), client.Config{
Address: server.Addr(),
})
defer c.Close()
// Standard Milvus operations from here
schema := entity.NewSchema().WithName("demo").
WithField(entity.NewField().WithName("id").WithDataType(entity.FieldTypeInt64).WithIsPrimaryKey(true).WithIsAutoID(true)).
WithField(entity.NewField().WithName("vector").WithDataType(entity.FieldTypeFloatVector).WithDim(128))
c.CreateCollection(context.Background(), schema, entity.DefaultShardNumber)
}
Node.js
import { start } from "@lyyyuna/milvus-lite";
import { MilvusClient, DataType } from "@zilliz/milvus2-sdk-node";
const server = await start("./milvus.db");
const client = new MilvusClient({ address: server.addr });
await client.createCollection({
collection_name: "demo",
fields: [
{ name: "id", data_type: DataType.Int64, is_primary_key: true, autoID: true },
{ name: "vector", data_type: DataType.FloatVector, dim: 128 },
],
});
await server.stop();
The API is minimal for both: Start/start to launch, grab the address, connect with the official Milvus SDK. Data operations are identical to connecting to a remote Milvus cluster -- switching to production is just changing the address.
Supported Platforms
| OS | Arch | Go | npm |
|---|---|---|---|
| macOS | arm64 (Apple Silicon) | ✅ | ✅ |
| macOS | amd64 (Intel) | ✅ | ✅ |
| Linux | amd64 | ✅ | ✅ |
| Linux | arm64 | ✅ | ✅ |
How It Works
The core idea is simple: don't touch the C++ core, just handle process management and distribution.
Where the binary comes from
The milvus-lite Python package on PyPI is a wheel file (basically a zip). Inside it, alongside the Python code, there's a pre-compiled milvus binary and a bunch of shared libraries (libknowhere, libglog, libtbb, etc.).
I wrote a script that downloads wheels for each platform from PyPI, extracts the milvus_lite/lib/ directory, and drops it into the corresponding platform package.
Distribution
Go uses a multi-module monorepo approach. One repo, multiple go.mod files -- one per platform:
platform/
├── darwin-arm64/go.mod # independent module, go:embed the binary
├── darwin-amd64/go.mod
├── linux-amd64/go.mod
└── linux-arm64/go.mod
The Go module proxy excludes sub-directories with their own go.mod when packaging a module. So go get downloads the main module (a few KB) plus only your platform's sub-module (~25-55MB). At runtime, build tags select the right embedded data, which gets extracted to a temp directory.
npm uses optionalDependencies, the same approach as esbuild. The main package declares four platform packages as optional deps. npm skips the ones that don't match the current platform.
Startup sequence
- Extract (Go) or locate (npm) the binary and shared libs
- Set
LD_LIBRARY_PATH(Linux) orDYLD_LIBRARY_PATH(macOS) - Spawn the milvus process via
exec.Command(Go) orchild_process.spawn(Node.js) - Wait for the gRPC port to become ready
- Return the address for the user to connect with the official SDK
A known quirk
milvus-lite's ShowCollections gRPC response populates collection_names but not collection_ids. The Python and Node.js SDKs iterate over collection_names, so they work fine. The Go SDK iterates over collection_ids, so client.ListCollections() returns an empty list.
The Go wrapper provides a workaround:
names, _ := milvuslite.ListCollections(ctx, server.Addr())
Wrapping up
The code is not large -- a few hundred lines for each language. The real effort went into understanding milvus-lite's architecture, figuring out platform distribution mechanics for Go modules and npm, and navigating Go's semver rules for v2+ modules.
If you're building AI Agents in Go or Node.js and need a local embedded vector database for semantic search, RAG, or knowledge bases, give it a try.