Status (2026-05-02): Implemented with named-store
registry. See ARCHITECTURE.md for the current design. The planning
decisions below were adopted; the main evolution beyond this document is
multi-store support (RagStoreEntry registry,
/rag new NAME, /rag switch NAME,
/rag drop NAME) so different knowledge domains (golang,
writing, research, etc.) can coexist as separate SQLite files while only
the active one is held open in memory.
π§ Harvey RAG Integration Plan (Hybrid Embedding Model Approach)
π― Goal
Add Retrieval-Augmented Generation (RAG) support to Harvey while:
- Leveraging the existing knowledge base (KB) as the source of truth
- Maintaining loose coupling with Ollama
- Avoiding embedding mismatch issues
- Keeping infrastructure simple (SQLite, no vector DB initially)
π§© Core Concept
β Separation of concerns
Knowledge Base (raw data)
β
Embedding Model (e.g. nomic-embed-text)
β
RAG Index (SQLite per embedding model)
β
Generation Model (granite4, llama3, etc.)
β Key Design Decisions
1. Use embedding modelβscoped RAG databases
Instead of per-generation-model:
β granite4.db
β llama3.db
Use:
β
rag_nomic_v1.db
β
rag_mxbai_v1.db
2. Map generation model β embedding model
Example:
type ModelConfig struct {
GenerationModel string
EmbeddingModel string
RagDBPath string
}
var ModelRegistry = map[string]ModelConfig{
"granite4": {
GenerationModel: "granite4",
EmbeddingModel: "nomic-embed-text",
RagDBPath: "rag_nomic_v1.db",
},
"llama3": {
GenerationModel: "llama3",
EmbeddingModel: "nomic-embed-text",
RagDBPath: "rag_nomic_v1.db",
},
}3. Explicit ingestion step
harvey ingest --embedding-model nomic-embed-text
- Generates embeddings
- Stores them in SQLite
- Can be run offline / batch
4. Enforce embedding consistency
Strict runtime check:
if embedder.Name() != r.embeddingModel {
return errors.New("embedding model mismatch")
}Prevents:
- Silent retrieval failures
- Mixed embedding spaces
βοΈ Go Module Design
(package harvey)
π File Structure
harvey/
rag_support.go
rag_support_test.go
π¦ rag_support.go
(Design Overview)
β Responsibilities
- Manage SQLite-based RAG index
- Store embeddings as BLOBs
- Provide ingest + query APIs
- Compute cosine similarity in Go
β Interfaces
type Embedder interface {
Embed(text string) ([]float64, error)
Name() string
}Allows:
- Ollama embedder
- Mock embedder (for tests)
- Future providers
β Core Types
type RagStore struct {
db *sql.DB
embeddingModel string
}
type Chunk struct {
ID int64
Content string
}β SQLite Schema
CREATE TABLE IF NOT EXISTS chunks (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
embedding BLOB NOT NULL
);Future extensions:
source_id TEXT,
chunk_index INTEGER,
tags TEXTβ Initialization
func NewRagStore(dbPath, embeddingModel string) (*RagStore, error)Uses:
import _ "github.com/glebarez/go-sqlite"Driver:
sql.Open("sqlite", dbPath)β Ingest Flow
func (r *RagStore) Ingest(texts []string, embedder Embedder) errorSteps:
- Validate embedding model
- Generate embeddings
- Serialize vectors
- Store in SQLite (transaction)
β Query Flow
func (r *RagStore) Query(query string, embedder Embedder, topK int) ([]Chunk, error)Steps:
- Embed query
- Load all stored embeddings
- Compute cosine similarity
- Sort results
- Return top-K chunks
β Cosine Similarity
func cosineSimilarity(a, b []float64) float64- Pure Go
- Works for small-medium datasets
β Serialization
Binary format:
[int32 length][float64...]
Functions:
serialize([]float64) []byte
deserialize([]byte) []float64π§ͺ rag_support_test.go
β Uses mock embedder
type mockEmbedder struct {
name string
}Ensures:
- Deterministic embeddings
- No dependency on Ollama
β Test Coverage
1. Ingest + query works
- Inserts multiple documents
- Queries for relevant content
- Verifies correct retrieval
2. Embedding mismatch protection
Ensures:
- Cannot ingest with wrong model
- Cannot query with wrong model
π Runtime Flow in Harvey
β Query execution
User selects model (granite4)
β
Lookup ModelConfig
β
Get embedding model (nomic-embed-text)
β
Load corresponding RAG DB
β
Embed query
β
Retrieve top-K chunks
β
Inject into prompt
β
Call Ollama (granite4)
β οΈ Known Trade-offs
β Pros
- Simple (no vector DB)
- Deterministic
- Fully local
- Easy to debug
- Loose coupling to Ollama
β οΈ Cons
- Linear scan (O(n)) query time
- No ANN indexing
- Duplicate embeddings if multiple models used
- Requires re-ingestion if embedding model changes
π Future Enhancements
π§ Performance
- Replace sort with heap (top-K)
- Add approximate search (later)
π§ Retrieval quality
- Add chunking (critical)
- Add metadata filtering
- Add reranking layer
βοΈ Storage
- Add vector dimension column
- Add embedding versioning
- Migrate to pgvector or Qdrant if needed
β Open Questions
These will influence your next steps:
- How large is your knowledge base?
- <10k chunks β current design is perfect
100k β may need indexing soon
- Will users switch models frequently?
- If yes β shared embedding index is important
- Do you already have document chunking?
- If not, this is the next critical feature
- Should ingestion be automatic or manual?
- CLI-driven vs background processing
- Do you want offline-only operation?
- Affects embedding strategy + caching
β TL;DR
- Use embedding-modelβscoped SQLite RAG indexes
- Map generation models β embedding model
- Implement ingest + query pipeline
- Enforce embedding consistency strictly
- Start simple; scale later if needed
If you want, I can next:
- Add a real Ollama embedder implementation
- Design a chunking pipeline for your KB
- Or integrate this directly into Harveyβs request lifecycle end-to-end