Skip to content

Commit 8db3c8a

Browse files
Frank Guoclaude
andcommitted
Add nomic embedding daemon for fast repeated queries
Background daemon holds the nomic model in memory over a Unix socket, so subsequent queries within 5 minutes skip the ~1s model load. Cache and runtime files live under project-local .rekal/nomic/ instead of global ~/.cache/. Model is pre-decompressed during rekal init. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ad7955e commit 8db3c8a

12 files changed

Lines changed: 516 additions & 26 deletions

File tree

cmd/rekal/cli/checkpoint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ func updateIndexIncremental(gitRoot string, sessionIDs []string, checkpointID st
357357
return err
358358
}
359359

360-
if err := buildNomicEmbeddings(indexDB, sessionContent, w); err != nil {
360+
if err := buildNomicEmbeddings(indexDB, sessionContent, w, gitRoot); err != nil {
361361
fmt.Fprintf(w, "rekal: warning: nomic embeddings skipped: %v\n", err)
362362
}
363363

cmd/rekal/cli/index_cmd.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func runIndex(cmd *cobra.Command, gitRoot string) error {
116116
}
117117

118118
// Nomic pass (non-fatal).
119-
if err := buildNomicEmbeddings(indexDB, sessionContent, w); err != nil {
119+
if err := buildNomicEmbeddings(indexDB, sessionContent, w, gitRoot); err != nil {
120120
fmt.Fprintf(w, "warning: nomic embeddings skipped: %v\n", err)
121121
}
122122
}
@@ -141,19 +141,19 @@ func runIndex(cmd *cobra.Command, gitRoot string) error {
141141

142142
// buildNomicEmbeddings generates nomic-embed-text embeddings for all sessions
143143
// and stores them in the index DB. Non-fatal: returns error on any failure.
144-
func buildNomicEmbeddings(indexDB *sql.DB, sessionContent map[string]string, w io.Writer) error {
144+
func buildNomicEmbeddings(indexDB *sql.DB, sessionContent map[string]string, w io.Writer, gitRoot string) error {
145145
if !nomic.Supported() {
146146
return nil
147147
}
148148

149149
fmt.Fprintln(w, "building nomic deep semantic embeddings...")
150-
embedder, err := nomic.NewEmbedder()
150+
client, err := nomic.NewClient(gitRoot)
151151
if err != nil {
152152
return err
153153
}
154-
defer embedder.Close()
154+
defer client.Close()
155155

156-
vectors, err := embedder.EmbedSessions(sessionContent)
156+
vectors, err := client.EmbedSessions(sessionContent)
157157
if err != nil {
158158
return err
159159
}

cmd/rekal/cli/init.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/rekal-dev/rekal-cli/cmd/rekal/cli/codec"
1212
"github.com/rekal-dev/rekal-cli/cmd/rekal/cli/db"
13+
"github.com/rekal-dev/rekal-cli/cmd/rekal/cli/nomic"
1314
"github.com/rekal-dev/rekal-cli/cmd/rekal/cli/skill"
1415
"github.com/spf13/cobra"
1516
)
@@ -121,6 +122,12 @@ imported into the local data DB automatically.`,
121122
fmt.Fprintf(cmd.ErrOrStderr(), "rekal: warning: initial checkpoint failed: %v\n", err)
122123
}
123124

125+
// Pre-decompress nomic model so first query is fast.
126+
nomicCacheDir := filepath.Join(rekalDir, "nomic")
127+
if err := nomic.WarmCache(nomicCacheDir); err != nil {
128+
fmt.Fprintf(cmd.ErrOrStderr(), "rekal: warning: nomic cache warm failed: %v\n", err)
129+
}
130+
124131
// Detect other AI agents and print integration hints.
125132
printAgentHints(cmd.ErrOrStderr(), gitRoot)
126133

cmd/rekal/cli/nomic/client.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//go:build (darwin && arm64) || (linux && amd64)
2+
3+
package nomic
4+
5+
import "path/filepath"
6+
7+
// Client provides nomic embeddings, transparently using the daemon when
8+
// available or falling back to in-process embedding.
9+
type Client struct {
10+
daemon *daemonClient
11+
embedder *Embedder
12+
}
13+
14+
// NewClient creates a Client that tries the daemon first for fast embedding.
15+
// If the daemon is unavailable, it falls back to loading the model in-process.
16+
// gitRoot is the git repository root (used to locate .rekal/nomic/).
17+
func NewClient(gitRoot string) (*Client, error) {
18+
if !Supported() {
19+
return nil, ErrNotSupported
20+
}
21+
22+
// Try daemon first.
23+
dc, err := ensureDaemon(gitRoot)
24+
if err == nil {
25+
return &Client{daemon: dc}, nil
26+
}
27+
28+
// Fall back to in-process.
29+
cacheDir := filepath.Join(gitRoot, ".rekal", "nomic")
30+
embedder, err := NewEmbedder(cacheDir)
31+
if err != nil {
32+
return nil, err
33+
}
34+
return &Client{embedder: embedder}, nil
35+
}
36+
37+
// EmbedQuery embeds text with the "search_query: " prefix.
38+
func (c *Client) EmbedQuery(text string) ([]float64, error) {
39+
if c.daemon != nil {
40+
return c.daemon.EmbedQuery(text)
41+
}
42+
return c.embedder.EmbedQuery(text)
43+
}
44+
45+
// EmbedDocument embeds text with the "search_document: " prefix.
46+
func (c *Client) EmbedDocument(text string) ([]float64, error) {
47+
if c.daemon != nil {
48+
return c.daemon.EmbedDocument(text)
49+
}
50+
return c.embedder.EmbedDocument(text)
51+
}
52+
53+
// EmbedSessions embeds multiple sessions in batch.
54+
func (c *Client) EmbedSessions(sessions map[string]string) (map[string][]float64, error) {
55+
if c.daemon != nil {
56+
return c.daemon.EmbedSessions(sessions)
57+
}
58+
return c.embedder.EmbedSessions(sessions)
59+
}
60+
61+
// Close releases resources.
62+
func (c *Client) Close() {
63+
if c.daemon != nil {
64+
c.daemon.Close()
65+
}
66+
if c.embedder != nil {
67+
c.embedder.Close()
68+
}
69+
}

0 commit comments

Comments
 (0)