Skip to content

Commit a3c739a

Browse files
botirkhaltaevclaude
andcommitted
feat: add automatic text chunking with aggregate embeddings
Add automatic text chunking for documents exceeding embedding model token limits: - Create chunker package with configurable chunking strategies - FixedOverlapChunker: splits text with configurable size and overlap - Uses tiktoken for accurate token counting (cl100k_base encoding) - Default: 512 token chunks with 50 token overlap, 8191 max tokens - Implement batch embedding support - Add BatchEmbeddingProvider interface for efficient multi-text embedding - Implement EmbedBatch in OpenAI provider (up to 2048 texts per request) - Automatic fallback to individual embeddings if batch not supported - Aggregate embedding approach - Chunk text when exceeding token limits - Embed all chunks using batch API for performance - Average chunk embeddings into single aggregate embedding - Store as single entry (no derived keys needed) - Convert entire codebase from float32 to float64 - Use OpenAI's native float64 format (eliminates conversions) - Update all similarity functions and backends - Better precision for similarity calculations - Configuration options - Chunking enabled by default with sensible defaults - WithChunking() to customize chunk size/overlap/strategy - WithoutChunking() to disable if not needed - Zero-config works out of the box - Add comprehensive tests - Chunker package tests (validation, chunking logic, token counting) - Cache integration tests (aggregate embeddings, enable/disable) - All existing tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2085fb6 commit a3c739a

27 files changed

Lines changed: 1250 additions & 110 deletions

backends/inmemory/fifo.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
type FIFOBackend[K comparable, V any] struct {
1212
mu *sync.RWMutex
1313
entries map[K]types.Entry[V]
14-
index map[K][]float32
14+
index map[K][]float64
1515
queue []K
1616
capacity int
1717
}
@@ -21,7 +21,7 @@ func NewFIFOBackend[K comparable, V any](config types.BackendConfig) (*FIFOBacke
2121
return &FIFOBackend[K, V]{
2222
mu: &sync.RWMutex{},
2323
entries: make(map[K]types.Entry[V]),
24-
index: make(map[K][]float32),
24+
index: make(map[K][]float64),
2525
queue: make([]K, 0, config.Capacity),
2626
capacity: config.Capacity,
2727
}, nil
@@ -102,7 +102,7 @@ func (b *FIFOBackend[K, V]) Flush(ctx context.Context) error {
102102
defer b.mu.Unlock()
103103

104104
b.entries = make(map[K]types.Entry[V])
105-
b.index = make(map[K][]float32)
105+
b.index = make(map[K][]float64)
106106
b.queue = make([]K, 0, b.capacity)
107107
return nil
108108
}
@@ -128,7 +128,7 @@ func (b *FIFOBackend[K, V]) Keys(ctx context.Context) ([]K, error) {
128128
}
129129

130130
// GetEmbedding retrieves just the embedding for a key
131-
func (b *FIFOBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float32, bool, error) {
131+
func (b *FIFOBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float64, bool, error) {
132132
b.mu.RLock()
133133
defer b.mu.RUnlock()
134134

backends/inmemory/lfu.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type LFUEntry[V any] struct {
1717
type LFUBackend[K comparable, V any] struct {
1818
mu *sync.RWMutex
1919
entries map[K]*LFUEntry[V]
20-
index map[K][]float32
20+
index map[K][]float64
2121
capacity int
2222
}
2323

@@ -26,7 +26,7 @@ func NewLFUBackend[K comparable, V any](config types.BackendConfig) (*LFUBackend
2626
return &LFUBackend[K, V]{
2727
mu: &sync.RWMutex{},
2828
entries: make(map[K]*LFUEntry[V]),
29-
index: make(map[K][]float32),
29+
index: make(map[K][]float64),
3030
capacity: config.Capacity,
3131
}, nil
3232
}
@@ -111,7 +111,7 @@ func (b *LFUBackend[K, V]) Flush(ctx context.Context) error {
111111
defer b.mu.Unlock()
112112

113113
b.entries = make(map[K]*LFUEntry[V])
114-
b.index = make(map[K][]float32)
114+
b.index = make(map[K][]float64)
115115
return nil
116116
}
117117

@@ -136,7 +136,7 @@ func (b *LFUBackend[K, V]) Keys(ctx context.Context) ([]K, error) {
136136
}
137137

138138
// GetEmbedding retrieves just the embedding for a key (without incrementing frequency)
139-
func (b *LFUBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float32, bool, error) {
139+
func (b *LFUBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float64, bool, error) {
140140
b.mu.RLock()
141141
defer b.mu.RUnlock()
142142

backends/inmemory/lru.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
type LRUBackend[K comparable, V any] struct {
1313
mu *sync.RWMutex
1414
cache *lru.Cache[K, types.Entry[V]]
15-
index map[K][]float32
15+
index map[K][]float64
1616
}
1717

1818
// NewLRUBackend creates a new LRU backend
@@ -25,7 +25,7 @@ func NewLRUBackend[K comparable, V any](config types.BackendConfig) (*LRUBackend
2525
return &LRUBackend[K, V]{
2626
mu: &sync.RWMutex{},
2727
cache: lruCache,
28-
index: make(map[K][]float32),
28+
index: make(map[K][]float64),
2929
}, nil
3030
}
3131

@@ -74,7 +74,7 @@ func (b *LRUBackend[K, V]) Flush(ctx context.Context) error {
7474
defer b.mu.Unlock()
7575

7676
b.cache.Purge()
77-
b.index = make(map[K][]float32)
77+
b.index = make(map[K][]float64)
7878
return nil
7979
}
8080

@@ -93,7 +93,7 @@ func (b *LRUBackend[K, V]) Keys(ctx context.Context) ([]K, error) {
9393

9494
// Clean up stale index entries and collect valid keys
9595
keys := make([]K, 0, b.cache.Len())
96-
validIndex := make(map[K][]float32)
96+
validIndex := make(map[K][]float64)
9797

9898
for key, embedding := range b.index {
9999
if b.cache.Contains(key) {
@@ -108,7 +108,7 @@ func (b *LRUBackend[K, V]) Keys(ctx context.Context) ([]K, error) {
108108
}
109109

110110
// GetEmbedding retrieves just the embedding for a key
111-
func (b *LRUBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float32, bool, error) {
111+
func (b *LRUBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float64, bool, error) {
112112
b.mu.RLock()
113113
embedding, hasEmbedding := b.index[key]
114114
cacheHasKey := b.cache.Contains(key)

backends/remote/redis.go

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -193,23 +193,14 @@ func floatsToBytes(fs []float64) []byte {
193193
return buf
194194
}
195195

196-
// float32ToFloat64 converts float32 slice to float64 slice
197-
func float32ToFloat64(fs []float32) []float64 {
198-
result := make([]float64, len(fs))
199-
for i, f := range fs {
200-
result[i] = float64(f)
201-
}
202-
return result
203-
}
204-
205196
// Set stores an entry in Redis using JSON.SET
206197
func (b *RedisBackend[K, V]) Set(ctx context.Context, key K, entry types.Entry[V]) error {
207198
redisKey := b.keyString(key)
208199

209200
doc := redisDocument[V]{
210201
Key: fmt.Sprintf("%v", key),
211202
Value: entry.Value,
212-
Embedding: float32ToFloat64(entry.Embedding),
203+
Embedding: entry.Embedding,
213204
Timestamp: time.Now().Unix(),
214205
}
215206

@@ -245,14 +236,8 @@ func (b *RedisBackend[K, V]) Get(ctx context.Context, key K) (types.Entry[V], bo
245236

246237
doc := docs[0]
247238

248-
// Convert float64 back to float32
249-
embedding := make([]float32, len(doc.Embedding))
250-
for i, f := range doc.Embedding {
251-
embedding[i] = float32(f)
252-
}
253-
254239
entry := types.Entry[V]{
255-
Embedding: embedding,
240+
Embedding: doc.Embedding,
256241
Value: doc.Value,
257242
}
258243

@@ -371,7 +356,7 @@ func (b *RedisBackend[K, V]) Keys(ctx context.Context) ([]K, error) {
371356
}
372357

373358
// GetEmbedding retrieves just the embedding for a key using JSON.GET
374-
func (b *RedisBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float32, bool, error) {
359+
func (b *RedisBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float64, bool, error) {
375360
redisKey := b.keyString(key)
376361

377362
result, err := b.client.JSONGet(ctx, redisKey, "$.embedding").Result()
@@ -391,20 +376,13 @@ func (b *RedisBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float32
391376
return nil, false, nil
392377
}
393378

394-
// Convert float64 back to float32
395-
embedding := make([]float32, len(embeddings[0]))
396-
for i, f := range embeddings[0] {
397-
embedding[i] = float32(f)
398-
}
399-
400-
return embedding, true, nil
379+
return embeddings[0], true, nil
401380
}
402381

403382
// VectorSearch performs vector similarity search using Redis FT.SEARCH
404-
func (b *RedisBackend[K, V]) VectorSearch(ctx context.Context, queryEmbedding []float32, threshold float32, limit int) ([]K, error) {
383+
func (b *RedisBackend[K, V]) VectorSearch(ctx context.Context, queryEmbedding []float64, threshold float64, limit int) ([]K, error) {
405384
// Convert embedding to bytes for search
406-
embedding64 := float32ToFloat64(queryEmbedding)
407-
embeddingBytes := floatsToBytes(embedding64)
385+
embeddingBytes := floatsToBytes(queryEmbedding)
408386

409387
// Perform vector search
410388
query := fmt.Sprintf("*=>[KNN %d @embedding $vec AS vector_distance]", limit)
@@ -440,7 +418,7 @@ func (b *RedisBackend[K, V]) VectorSearch(ctx context.Context, queryEmbedding []
440418
similarity := 1.0 - distance
441419

442420
// Check if similarity meets threshold
443-
if float32(similarity) >= threshold {
421+
if similarity >= threshold {
444422
keyStr, ok := doc.Fields["key"]
445423
if !ok {
446424
continue
@@ -542,13 +520,9 @@ func (b *RedisBackend[K, V]) GetBatchAsync(ctx context.Context, keys []K) <-chan
542520
}
543521

544522
doc := docs[0]
545-
embedding := make([]float32, len(doc.Embedding))
546-
for i, f := range doc.Embedding {
547-
embedding[i] = float32(f)
548-
}
549523

550524
entries[key] = types.Entry[V]{
551-
Embedding: embedding,
525+
Embedding: doc.Embedding,
552526
Value: doc.Value,
553527
}
554528
}

0 commit comments

Comments
 (0)