Weave is an open-source Go library for retrieval-augmented generation (RAG). You hand it documents, and it chunks the text, generates embeddings, writes the vectors to a vector store such as pgvector, then retrieves the closest chunks for a query and assembles them into prompt context with a token budget and citation markers. Collections, documents and chunks are tenant-scoped and live in PostgreSQL, SQLite, MongoDB or memory, and the whole engine runs standalone or as a Forge extension with an HTTP API.
Documentation · Quick start · Features · Components · Storage · Forge · FAQ
You need Go 1.26 or later and an OpenAI API key for the embeddings.
go get github.com/xraph/weave@latestSave this as main.go:
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/xraph/weave/chunker"
"github.com/xraph/weave/collection"
"github.com/xraph/weave/embedder"
"github.com/xraph/weave/engine"
"github.com/xraph/weave/store/memory"
vsmemory "github.com/xraph/weave/vectorstore/memory"
)
func main() {
ctx := context.Background()
eng, err := engine.New(
engine.WithStore(memory.New()),
engine.WithVectorStore(vsmemory.New()),
engine.WithEmbedder(embedder.NewOpenAIEmbedder(os.Getenv("OPENAI_API_KEY"))),
engine.WithChunker(chunker.NewRecursiveChunker()),
)
if err != nil {
log.Fatal(err)
}
defer eng.Stop(ctx)
col := &collection.Collection{Name: "handbook"}
if err := eng.CreateCollection(ctx, col); err != nil {
log.Fatal(err)
}
res, err := eng.Ingest(ctx, &engine.IngestInput{
CollectionID: col.ID,
Title: "Leave policy",
Content: "Full-time staff get 25 days of paid leave a year. " +
"Unused days carry over until the end of March. " +
"Requests longer than two weeks need approval from your manager.",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ingested %s into %d chunks\n", res.DocumentID, res.ChunkCount)
hits, err := eng.Retrieve(ctx, "How many vacation days do I get?",
engine.WithCollection(col.ID),
engine.WithTopK(3),
)
if err != nil {
log.Fatal(err)
}
prompt, err := eng.Assemble(ctx, hits, engine.AssembleParams{MaxTokens: 1024})
if err != nil {
log.Fatal(err)
}
fmt.Println(prompt.Context)
}Run it:
go mod tidy
OPENAI_API_KEY=sk-... go run .You'll see the document's TypeID (doc_...) and its chunk count, then the context block Weave built from the best hits, with a [1]-style marker in front of each chunk. That block is what you paste into your LLM prompt. prompt.Included tells you which hit each marker points at, so you can show citations next to the answer.
Everything here lives in memory and disappears when the program exits. Swap in a Postgres store and pgvector when you want to keep it.
A RAG feature looks small until you build it: you need a chunking strategy, an embedding call, somewhere to keep vectors, a ranking step, a way to fit the results into a context window, and tenant isolation across all of it. Weave puts those steps behind one engine.Engine, and each step is a Go interface you can replace with your own.
| You need | Weave gives you |
|---|---|
| Text out of files | Loaders for plain text, Markdown, HTML, CSV, JSON, URLs and whole directories |
| Chunks that fit an embedding model | Recursive, fixed-size, sliding-window, sentence-based semantic and code-aware chunkers, with size and overlap per collection |
| Embeddings | An OpenAI embedder (any OpenAI-compatible endpoint through WithOpenAIBaseURL), or your own embedder.Embedder |
| Vector search | In-memory, PostgreSQL with pgvector, or Fabriq's vector plane |
| Better ranking than plain similarity | MMR for diversity, hybrid retrieval with Reciprocal Rank Fusion, and a reranker wrapper |
| Prompt context that fits | Token-budgeted assembly with [n] citation markers |
| One index per customer | Tenant and app IDs on the context, carried into every store and vector query |
| Hooks into your own systems | 14 lifecycle hooks, a Chronicle audit bridge and a metrics extension |
| An API without writing handlers | Forge routes for collections, documents, retrieval and search, with OpenAPI metadata |
engine.New takes one option per step. You have to give it a store, a vector store, an embedder and a chunker before you can ingest anything. If you forget one, Ingest tells you with weave.ErrNoStore, ErrNoEmbedder and friends.
| Step | Option | Built in |
|---|---|---|
| Metadata store | engine.WithStore |
store/memory, store/postgres, store/sqlite, store/mongo |
| Vector store | engine.WithVectorStore |
vectorstore/memory, vectorstore/pgvector, vectorstore/fabriq |
| Embedder | engine.WithEmbedder |
embedder.NewOpenAIEmbedder(apiKey, opts...) |
| Chunker | engine.WithChunker |
NewRecursiveChunker, NewFixedChunker, NewSlidingChunker, NewSemanticChunker, NewCodeChunker |
| Loader | engine.WithLoader |
NewTextLoader, NewMarkdownLoader, NewHTMLLoader, NewCSVLoader, NewJSONLoader(fields...), NewURLLoader(delegate), NewDirectoryLoader(loaders...) |
| Retriever | engine.WithRetriever |
NewSimilarityRetriever, NewMMRRetriever, NewHybridRetriever, NewRerankerRetriever |
| Hooks | engine.WithExtension |
audit_hook, observability |
| Defaults | engine.WithConfig |
weave.DefaultConfig() |
A loader only runs when the IngestInput names a SourceType the loader supports, such as text/markdown or text/html. Without one, Weave chunks Content as it is.
embedder.NewLocalEmbedder exists, but it's a placeholder that returns ErrNotImplemented. Until a local model lands, use the OpenAI embedder against any compatible server, or implement the two-method embedder.Embedder interface yourself.
With no retriever option, Retrieve embeds the query and searches the vector store directly. Every hit is then read back from the metadata store by chunk ID, and a hit whose row has gone missing stays in the results with Orphaned set.
When plain similarity isn't enough, compose retrievers:
st := postgres.New(db)
vs := pgvector.New(db)
emb := embedder.NewOpenAIEmbedder(apiKey)
eng, err := engine.New(
engine.WithStore(st),
engine.WithVectorStore(vs),
engine.WithEmbedder(emb),
engine.WithChunker(chunker.NewRecursiveChunker()),
engine.WithRetriever(retriever.NewHybridRetriever(
retriever.NewSimilarityRetriever(vs, emb, st),
retriever.NewMMRRetriever(vs, emb, 0.5),
)),
)Retrieve takes WithCollection, WithTopK, WithMinScore, WithTenantID and WithTenantFilter. For a query across several collections, use eng.HybridSearch(ctx, query, &engine.HybridSearchParams{...}). eng.RetrieveCompare puts your configured ranking next to the raw vector ranking for the same query, so you can see what a reranker or MMR actually moved.
eng.Assemble(ctx, hits, engine.AssembleParams{MaxTokens: n}) builds the context string in rank order until the budget runs out (4096 tokens when you pass zero). It returns Context, TotalTokens, Included (which hits made it in, in marker order) and FirstExcluded. If you want your own template or token counter, use the assembler package directly with assembler.WithTemplate, WithTokenCounter and WithMaxTokens.
You configure two stores. The metadata store holds collections, documents and chunks, and the vector store holds the embeddings, which is why you can mix them: SQLite for metadata with vectors in memory on a laptop, or Postgres for both in production.
| Metadata store | Package | Notes |
|---|---|---|
| PostgreSQL | store/postgres, postgres.New(db *grove.DB) |
Grove migrations, run by Migrate |
| SQLite | store/sqlite, sqlite.New(db *grove.DB) |
Single node, pair it with the in-memory vector store |
| MongoDB | store/mongo, mongo.New(db *grove.DB) |
|
| Memory | store/memory, memory.New() |
Tests and prototypes |
| Vector store | Package | Notes |
|---|---|---|
| pgvector | vectorstore/pgvector, pgvector.New(db *grove.DB, opts...) |
Table weave_vectors by default, change it with WithTableName |
| Fabriq | vectorstore/fabriq (package fabriqvec), fabriqvec.New(vq, opts...) |
Embeddings live in Fabriq next to the rest of your data |
| Memory | vectorstore/memory, memory.New() |
Tests and prototypes |
Call Migrate(ctx) on the metadata store before first use (the Forge extension does it for you on start). The pgvector store has no migration of its own, so create the vector extension and the weave_vectors table in your own schema migrations before the first ingest.
All stores are built on Grove, so you open one *grove.DB and share it between the metadata store and pgvector.
Put the tenant and app on the context, and Weave scopes everything under it:
ctx = weave.WithTenant(ctx, "acme")
ctx = weave.WithApp(ctx, "support-bot")A collection created under that context belongs to acme, and Retrieve filters the vector search by the same tenant. If you need to query one tenant explicitly from a context that has none, pass engine.WithTenantFilter("acme"). Unlike WithTenantID, an empty string there means rows with no tenant, not everyone.
The extension package registers Weave as a Forge extension. On Register it reads config from extensions.weave (or the older weave key), takes a store from a *grove.DB in the container when there is one, builds the engine, mounts the HTTP API under /weave, and provides *engine.Engine to the container. Start runs the store migration.
import (
"github.com/xraph/forge"
"github.com/xraph/weave/chunker"
"github.com/xraph/weave/embedder"
"github.com/xraph/weave/engine"
weaveext "github.com/xraph/weave/extension"
"github.com/xraph/weave/store/memory"
vsmemory "github.com/xraph/weave/vectorstore/memory"
)
app := forge.NewApp(forge.AppConfig{
Name: "search-api",
HTTPAddress: ":8080",
Extensions: []forge.Extension{
weaveext.New(
weaveext.WithStore(memory.New()), // leave out when Grove is in the container
weaveext.WithVectorStore(vsmemory.New()),
weaveext.WithEngineOption(engine.WithEmbedder(embedder.NewOpenAIEmbedder(os.Getenv("OPENAI_API_KEY")))),
weaveext.WithEngineOption(engine.WithChunker(chunker.NewRecursiveChunker())),
),
},
})The extension has no embedder or chunker option of its own, so you pass those through WithEngineOption. Other extensions resolve the engine with forge.Inject[*engine.Engine](app.Container()).
extensions:
weave:
base_path: /weave
default_chunk_size: 512
default_chunk_overlap: 50
default_embedding_model: text-embedding-3-small
default_chunk_strategy: recursive
default_top_k: 10
ingest_concurrency: 4
shutdown_timeout: 30s
grove_database: "" # a named grove.DB; empty uses the default one
disable_routes: false
disable_migrate: falseThose values are the defaults. default_chunk_overlap has to be smaller than default_chunk_size, or Register fails. In code, the same settings go through WithConfig, WithBasePath, WithGroveDatabase, WithDisableRoutes, WithDisableMigrate and WithRequireConfig(true), and WithExtension adds a lifecycle hook.
With the default base path, the routes are:
| Method and path | Does |
|---|---|
POST /weave/v1/collections, GET /weave/v1/collections |
Create or list collections |
GET /weave/v1/collections/:collectionId, DELETE ... |
Read or delete one, with its documents and chunks |
GET /weave/v1/collections/:collectionId/stats |
Document and chunk counts |
POST /weave/v1/collections/:collectionId/reindex |
Re-embed every chunk |
POST /weave/v1/collections/:collectionId/documents, .../documents/batch |
Ingest one document or a batch |
GET /weave/v1/collections/:collectionId/documents |
List documents |
GET /weave/v1/documents/:documentId, DELETE ... |
Read or delete a document |
POST /weave/v1/collections/:collectionId/retrieve |
Retrieve from one collection |
POST /weave/v1/search |
Search across collections |
The routes carry OpenAPI metadata, so they show up in your Forge app's spec. The extension also contributes a weave page to the Forge dashboard.
You write a hook as any value that implements one or more of these methods, and you add it with engine.WithExtension:
| Area | Hooks |
|---|---|
| Collections | OnCollectionCreated, OnCollectionDeleted |
| Ingestion | OnIngestStarted, OnIngestChunked, OnIngestEmbedded, OnIngestCompleted, OnIngestFailed |
| Retrieval | OnRetrievalStarted, OnRetrievalCompleted, OnRetrievalFailed |
| Documents and reindex | OnDocumentDeleted, OnReindexStarted, OnReindexCompleted |
| Shutdown | OnShutdown |
audithook.New(recorder) (import path github.com/xraph/weave/audit_hook) turns these into audit events for Chronicle or any Recorder, and observability.NewMetricsExtension() counts ingests, retrievals and failures.
| Topic | Read |
|---|---|
| First pipeline | Getting started, architecture, end-to-end example |
| Ingestion | Document ingestion, loaders, chunkers, embedders |
| Retrieval and context | Semantic retrieval, context assembler |
| Storage | PostgreSQL, SQLite, MongoDB, Fabriq vectors |
| Tenants and config | Multi-tenancy, configuration |
| Forge and HTTP | Forge extension, HTTP API |
| Extending it | Extension system, custom components |
The Go API is on pkg.go.dev.
Create an engine.Engine with a store, a vector store, an embedder and a chunker, then call Ingest for each document, Retrieve for each question and Assemble to turn the hits into prompt context. The quick start is the whole loop in about sixty lines.
Yes. vectorstore/pgvector stores embeddings in a Postgres table through Grove and searches by vector distance. Create the vector extension and the weave_vectors table yourself, since Weave doesn't migrate it.
Yes. engine, the stores and every component are plain Go. Only extension and api import Forge.
The built-in embedder calls OpenAI's embeddings API, text-embedding-3-small by default, and WithOpenAIModel, WithOpenAIDimensions and WithOpenAIBaseURL let you point it at another model or any OpenAI-compatible server. Anything else works through your own embedder.Embedder.
Set the tenant on the context with weave.WithTenant. Collections, documents and chunks are stored under it, and retrieval filters the vector search to it.
Issues and focused pull requests are welcome. Open an issue first for anything large.
git clone https://github.com/xraph/weave.git
cd weave
GOWORK=off go build ./...
make test # go test -v ./...
make lint # golangci-lint v2
make fmt # gofmt and goimportsPostgres and MongoDB store tests skip unless you set WEAVE_TEST_POSTGRES_DSN or WEAVE_TEST_MONGO_DSN, and they refuse the default ports, so point them at throwaway containers. Skip make build, make run and make dev, which point at a cmd/weave that doesn't exist. AGENTS.md has the CI jobs, test setup and release steps. Commits follow Conventional Commits with a scope, as in fix(engine): ....
See the Weave repository for license terms.
Weave is part of the forgery stack built on Forge, by Rex Raphael and contributors.