Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ coverage.out
*.direnv
result
CLAUDE.md
go.work
go.work.sum
1 change: 1 addition & 0 deletions doc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [In-Memory](./stores/memory.md)
- [MongoDB](./stores/mongo.md)
- [Valkey](./stores/valkey.md)
- [BoltDB](./stores/bolt.md)

# Meta

Expand Down
99 changes: 99 additions & 0 deletions doc/src/stores/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# BoltDB Store

The BoltDB store provides persistent storage for embeddings using [BoltDB](https://github.com/etcd-io/bbolt), a fast key-value store written in Go.

## Features

- Persistent, file-based storage of embeddings
- Fast read and write operations
- Thread-safe access
- Configurable bucket name
- Context-aware operations with cancellation support

## Installation

```bash
go get github.com/conneroisu/semanticrouter-go/stores/bolt
```

## Usage

```go
import (
"github.com/conneroisu/semanticrouter-go"
"github.com/conneroisu/semanticrouter-go/encoders/ollama"
"github.com/conneroisu/semanticrouter-go/stores/bolt"
"github.com/ollama/ollama/api"
)

func main() {
// Create a new BoltDB store
store, err := bolt.NewStore("embeddings.db")
if err != nil {
log.Fatalf("Failed to create BoltDB store: %v", err)
}
defer store.Close()

// Create a client for Ollama
client, err := api.ClientFromEnvironment()
if err != nil {
log.Fatalf("Failed to create Ollama client: %v", err)
}

// Create a router with the BoltDB store
router, err := semanticrouter.NewRouter(
[]semanticrouter.Route{
{
Name: "greeting",
Utterances: []semanticrouter.Utterance{
{Utterance: "hello there"},
{Utterance: "good morning"},
},
},
{
Name: "farewell",
Utterances: []semanticrouter.Utterance{
{Utterance: "goodbye"},
{Utterance: "see you later"},
},
},
},
&ollama.Encoder{
Client: client,
Model: "mxbai-embed-large",
},
store,
)
if err != nil {
log.Fatalf("Failed to create router: %v", err)
}

// Match an utterance
match, score, err := router.Match(context.Background(), "hi there")
if err != nil {
log.Fatalf("Failed to match utterance: %v", err)
}

fmt.Printf("Match: %s, Score: %f\n", match.Name, score)
}
```

## Configuration Options

The BoltDB store can be configured with the following options:

### Custom Bucket Name

By default, the BoltDB store uses a bucket named "embeddings". You can specify a custom bucket name:

```go
store, err := bolt.NewStore(
"embeddings.db",
bolt.WithBucketName("custom-bucket"),
)
```

## Considerations

- BoltDB only allows one write transaction at a time, but allows multiple concurrent read transactions.
- For high-concurrency applications, consider using the Memory store with periodic snapshots to BoltDB.
51 changes: 51 additions & 0 deletions examples/boltdb-store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# BoltDB Store Example

This example demonstrates how to use the BoltDB store with the semantic router for persistent storage of embeddings.

## Requirements

- Go 1.24 or higher
- Ollama installed and running locally (for the embedding model)

## Running the Example

1. Ensure Ollama is running and that you have the `mxbai-embed-large` model available:

```bash
ollama pull mxbai-embed-large
```

2. Run the example:

```bash
go run main.go
```

## What the Example Does

1. Creates a BoltDB store for persistent storage of embeddings
2. Initializes a router with two routes: "weather" and "travel"
3. Matches several test utterances against these routes
4. Prints the best matching route and score for each utterance

## Expected Output

```
Utterance: 'what's it like outside today?'
Best route: weather
Score: 0.8732

Utterance: 'how can I get to the train station?'
Best route: travel
Score: 0.9126

Utterance: 'will I need an umbrella later?'
Best route: weather
Score: 0.8245
```

## How It Works

The BoltDB store saves embeddings to a file called `embeddings.db`. On the first run, it will encode all the example utterances and store them in the database. On subsequent runs, it will reuse the stored embeddings instead of re-encoding them.

This persistence makes the semantic router much faster after the initial setup, as it doesn't need to re-encode known utterances.
19 changes: 19 additions & 0 deletions examples/boltdb-store/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module github.com/conneroisu/semanticrouter-go/examples/boltdb-store

go 1.24.0

require (
github.com/conneroisu/semanticrouter-go v0.9.4
github.com/conneroisu/semanticrouter-go/encoders/ollama v0.0.0-20250519171221-384fa0cd4b21
github.com/conneroisu/semanticrouter-go/stores/bolt v0.0.0
github.com/ollama/ollama v0.3.10
)

replace github.com/conneroisu/semanticrouter-go/stores/bolt => ../../stores/bolt

require (
go.etcd.io/bbolt v1.3.9 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
gonum.org/v1/gonum v0.15.0 // indirect
)
28 changes: 28 additions & 0 deletions examples/boltdb-store/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
github.com/conneroisu/semanticrouter-go v0.9.4 h1:HExzyIGn3bF/Cy8oR2tAx9ZteoIGe7R7A/BR9Dn8+s0=
github.com/conneroisu/semanticrouter-go v0.9.4/go.mod h1:x5WzHbjBY8j/Gkvy69Zb4xp7pZZmfy4663SPlR3AGAE=
github.com/conneroisu/semanticrouter-go/encoders/ollama v0.0.0-20250519171221-384fa0cd4b21 h1:Pxj75lGDNdByY5chK7pgyGFcfbrIh2NUbiKsuFk/dzo=
github.com/conneroisu/semanticrouter-go/encoders/ollama v0.0.0-20250519171221-384fa0cd4b21/go.mod h1:BPNtBC7TNsJTKv1ub2HEn49vstKBfscJxeH7HXTYKvQ=
github.com/conneroisu/semanticrouter-go/stores/memory v0.0.0-20240909020055-7c8ab7483baa h1:ofFlV/DbgpLd+5VzimdKSiCKfV30XMXS2Z3PGcVuPGw=
github.com/conneroisu/semanticrouter-go/stores/memory v0.0.0-20240909020055-7c8ab7483baa/go.mod h1:yxb6pxh8SauT8ZEtM+iT5P5BAJVqzlCgyz5LE2TZsF4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/ollama/ollama v0.3.10 h1:fVOEBJjCGcWwrimipKWZwq0dBW39fMrYkJYMA81ghaE=
github.com/ollama/ollama v0.3.10/go.mod h1:YrWoNkFnPOYsnDvsf/Ztb1wxU9/IXrNsQHqcxbY2r94=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
96 changes: 96 additions & 0 deletions examples/boltdb-store/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Package main demonstrates using the BoltDB store with the semantic router.
package main

import (
"context"
"fmt"
"log"
"os"

"github.com/conneroisu/semanticrouter-go"
"github.com/conneroisu/semanticrouter-go/encoders/ollama"
"github.com/conneroisu/semanticrouter-go/stores/bolt"
"github.com/ollama/ollama/api"
)

// WeatherRoutes represents routes related to weather.
var WeatherRoutes = semanticrouter.Route{
Name: "weather",
Utterances: []semanticrouter.Utterance{
{Utterance: "how's the weather today?"},
{Utterance: "will it rain tomorrow?"},
{Utterance: "is it going to be sunny this weekend?"},
{Utterance: "what's the temperature outside?"},
},
}

// TravelRoutes represents routes related to travel.
var TravelRoutes = semanticrouter.Route{
Name: "travel",
Utterances: []semanticrouter.Utterance{
{Utterance: "how do I get to the airport?"},
{Utterance: "what time does the train leave?"},
{Utterance: "I need directions to downtown"},
{Utterance: "is there a bus to the museum?"},
},
}

func main() {
if err := run(); err != nil {
log.Fatalf("Error: %v", err)
}
}

func run() error {
// Create a BoltDB store
dbPath := "embeddings.db"
store, err := bolt.NewStore(dbPath)
if err != nil {
return fmt.Errorf("failed to create BoltDB store: %w", err)
}
defer func() {
store.Close()
// Clean up the database file (in a real app, you would keep this file)
os.Remove(dbPath)
}()

// Create a client for Ollama
client, err := api.ClientFromEnvironment()
if err != nil {
return fmt.Errorf("error creating Ollama client: %w", err)
}

// Create a router with the BoltDB store
router, err := semanticrouter.NewRouter(
[]semanticrouter.Route{WeatherRoutes, TravelRoutes},
&ollama.Encoder{
Client: client,
Model: "mxbai-embed-large", // You may need to adjust the model name
},
store,
)
if err != nil {
return fmt.Errorf("error creating router: %w", err)
}

// Test utterances to match
testUtterances := []string{
"what's it like outside today?",
"how can I get to the train station?",
"will I need an umbrella later?",
}

ctx := context.Background()
for _, utterance := range testUtterances {
bestRoute, score, err := router.Match(ctx, utterance)
if err != nil {
fmt.Printf("Error matching '%s': %v\n", utterance, err)
continue
}

fmt.Printf("Utterance: '%s'\nBest route: %s\nScore: %.4f\n\n",
utterance, bestRoute.Name, score)
}

return nil
}
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 15 additions & 8 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,17 @@

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

flake-utils = {
url = "github:numtide/flake-utils";
inputs.systems.follows = "systems";
};
systems.url = "github:nix-systems/default";

treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};

nixConfig = {
extra-substituters = ''https://conneroisu.cachix.org'';
extra-trusted-public-keys = ''conneroisu.cachix.org-1:PgOlJ8/5i/XBz2HhKZIYBSxNiyzalr1B/63T74lRcU0='';
extra-experimental-features = "nix-command flakes";
};

outputs = inputs @ {
self,
flake-utils,
Expand Down Expand Up @@ -97,6 +89,21 @@
exec = ''$EDITOR $REPO_ROOT/go.mod'';
description = "Edit go.mod";
};
initWorkspace = {
exec = ''
${pkgs.go}/bin/go work init
${pkgs.go}/bin/go work use .
${pkgs.go}/bin/go work use ./encoders/closedai
${pkgs.go}/bin/go work use ./encoders/google
${pkgs.go}/bin/go work use ./encoders/ollama
${pkgs.go}/bin/go work use ./encoders/voyageai
${pkgs.go}/bin/go work use ./stores/bolt
${pkgs.go}/bin/go work use ./stores/memory
${pkgs.go}/bin/go work use ./stores/mongo
${pkgs.go}/bin/go work use ./stores/valkey
'';
description = "Initialize Workspace";
};
clean = {
exec = ''${pkgs.git}/bin/git clean -fdx'';
description = "Clean Project";
Expand Down
18 changes: 0 additions & 18 deletions go.work

This file was deleted.

Loading