Skip to content

Commit 45fa4d2

Browse files
refactor: comprehensive type-safe architecture redesign (#50)
* refactor: comprehensive type-safe architecture redesign - Slim Backend[K,V] interface: 9 methods (Set, Get, Delete, Contains, Keys, GetEmbedding, Flush, Len, Close) — no async, no god-struct - Set(ctx, key, embedding, value) instead of Set(ctx, key, Entry[V]) so backends never need to know about Entry internals - VectorSearcher[K,V] optional interface for server-side search (Redis) checked via simple type assertion in Lookup/TopMatches - EmbeddingProvider with context.Context: EmbedText(ctx, text), Close() error - BatchEmbeddingProvider optional interface for batch support - Typed Redis config: remote.WithPrefix(), WithDimensions(), etc. replacing BackendConfig god-struct with map[string]any - errors package with sentinel errors (ErrClosed, ErrNilBackend, etc.) and typed EmbeddingError/BackendError wrappers - Sync-only Cache[K,V] — removed AsyncCache, all async types/methods - Closed-state guard with atomic.Bool on all cache operations - Updated all backends (LRU, LFU, FIFO, Redis), options, providers, tests BREAKING CHANGE: Backend interface signature changed, async methods removed, EmbeddingProvider now requires context.Context, Close() returns error. Co-Authored-By: Botir Khaltaev <btrghstk@gmail.com> * simplify: remove VectorSearcher, EmbeddingStore, async; clean up Redis - Remove VectorSearcher interface and VectorSearchResult type from types - Remove EmbeddingStore interface — Keys/GetEmbedding now on Backend (9 methods) - Remove async.go and cache_async_test.go entirely (sync-only) - Remove Redis vector index creation, WithIndexName, WithDimensions (no longer needed without VectorSearch) - Remove dead Timestamp field from Redis document - Simplify Lookup/TopMatches: always scan via Keys/GetEmbedding - Fix outdated doc comments on backends Co-Authored-By: Botir Khaltaev <btrghstk@gmail.com> * fix: remove unnecessary EmbeddingModel type conversions (unconvert lint) Co-Authored-By: Botir Khaltaev <btrghstk@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Botir Khaltaev <btrghstk@gmail.com>
1 parent af49337 commit 45fa4d2

15 files changed

Lines changed: 839 additions & 2491 deletions

File tree

backends/backends.go

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,28 @@
1+
// Package backends re-exports the concrete backend constructors for convenience.
12
package backends
23

34
import (
4-
"errors"
5-
65
"github.com/botirk38/semanticcache/backends/inmemory"
76
"github.com/botirk38/semanticcache/backends/remote"
87
"github.com/botirk38/semanticcache/types"
98
)
109

11-
var ErrUnsupportedBackend = errors.New("unsupported backend type")
12-
13-
// BackendFactory creates cache backends based on type and configuration
14-
type BackendFactory[K comparable, V any] struct{}
15-
16-
// NewBackend creates a new cache backend of the specified type
17-
func (f *BackendFactory[K, V]) NewBackend(backendType types.BackendType, config types.BackendConfig) (types.CacheBackend[K, V], error) {
18-
switch backendType {
19-
case types.BackendLRU:
20-
return NewLRUBackend[K, V](config)
21-
case types.BackendFIFO:
22-
return NewFIFOBackend[K, V](config)
23-
case types.BackendLFU:
24-
return NewLFUBackend[K, V](config)
25-
case types.BackendRedis:
26-
return NewRedisBackend[K, V](config)
27-
default:
28-
return nil, ErrUnsupportedBackend
29-
}
30-
}
31-
32-
// NewLRUBackend creates a new LRU backend
33-
func NewLRUBackend[K comparable, V any](config types.BackendConfig) (types.CacheBackend[K, V], error) {
34-
return inmemory.NewLRUBackend[K, V](config)
10+
// NewLRUBackend creates a new LRU in-memory backend.
11+
func NewLRUBackend[K comparable, V any](capacity int) (types.Backend[K, V], error) {
12+
return inmemory.NewLRUBackend[K, V](capacity)
3513
}
3614

37-
// NewFIFOBackend creates a new FIFO backend
38-
func NewFIFOBackend[K comparable, V any](config types.BackendConfig) (types.CacheBackend[K, V], error) {
39-
return inmemory.NewFIFOBackend[K, V](config)
15+
// NewFIFOBackend creates a new FIFO in-memory backend.
16+
func NewFIFOBackend[K comparable, V any](capacity int) (types.Backend[K, V], error) {
17+
return inmemory.NewFIFOBackend[K, V](capacity)
4018
}
4119

42-
// NewLFUBackend creates a new LFU backend
43-
func NewLFUBackend[K comparable, V any](config types.BackendConfig) (types.CacheBackend[K, V], error) {
44-
return inmemory.NewLFUBackend[K, V](config)
20+
// NewLFUBackend creates a new LFU in-memory backend.
21+
func NewLFUBackend[K comparable, V any](capacity int) (types.Backend[K, V], error) {
22+
return inmemory.NewLFUBackend[K, V](capacity)
4523
}
4624

47-
// NewRedisBackend creates a new Redis backend
48-
func NewRedisBackend[K comparable, V any](config types.BackendConfig) (types.CacheBackend[K, V], error) {
49-
return remote.NewRedisBackend[K, V](config)
25+
// NewRedisBackend creates a new Redis backend.
26+
func NewRedisBackend[K comparable, V any](addr string, opts ...remote.RedisOption) (types.Backend[K, V], error) {
27+
return remote.NewRedisBackend[K, V](addr, opts...)
5028
}

backends/inmemory/fifo.go

Lines changed: 44 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -7,192 +7,120 @@ import (
77
"github.com/botirk38/semanticcache/types"
88
)
99

10-
// FIFOBackend implements CacheBackend using FIFO (First In, First Out) eviction policy
10+
// FIFOBackend implements Backend using FIFO eviction.
1111
type FIFOBackend[K comparable, V any] struct {
12-
mu *sync.RWMutex
12+
mu sync.RWMutex
1313
entries map[K]types.Entry[V]
14-
index map[K][]float64
1514
queue []K
1615
capacity int
1716
}
1817

19-
// NewFIFOBackend creates a new FIFO backend
20-
func NewFIFOBackend[K comparable, V any](config types.BackendConfig) (*FIFOBackend[K, V], error) {
18+
// NewFIFOBackend creates a new FIFO backend with the given capacity.
19+
func NewFIFOBackend[K comparable, V any](capacity int) (*FIFOBackend[K, V], error) {
2120
return &FIFOBackend[K, V]{
22-
mu: &sync.RWMutex{},
2321
entries: make(map[K]types.Entry[V]),
24-
index: make(map[K][]float64),
25-
queue: make([]K, 0, config.Capacity),
26-
capacity: config.Capacity,
22+
queue: make([]K, 0, capacity),
23+
capacity: capacity,
2724
}, nil
2825
}
2926

30-
// Set stores an entry in the FIFO cache
31-
func (b *FIFOBackend[K, V]) Set(ctx context.Context, key K, entry types.Entry[V]) error {
27+
// Set stores a value with its embedding.
28+
func (b *FIFOBackend[K, V]) Set(_ context.Context, key K, embedding []float64, value V) error {
3229
b.mu.Lock()
3330
defer b.mu.Unlock()
3431

35-
// If key already exists, update it
36-
if _, exists := b.entries[key]; exists {
32+
entry := types.Entry[V]{Embedding: embedding, Value: value}
33+
34+
if _, ok := b.entries[key]; ok {
3735
b.entries[key] = entry
38-
b.index[key] = entry.Embedding
3936
return nil
4037
}
4138

42-
// If at capacity, evict the oldest entry (FIFO)
4339
if len(b.entries) >= b.capacity && b.capacity > 0 {
44-
oldestKey := b.queue[0]
40+
oldest := b.queue[0]
4541
b.queue = b.queue[1:]
46-
delete(b.entries, oldestKey)
47-
delete(b.index, oldestKey)
42+
delete(b.entries, oldest)
4843
}
4944

50-
// Add new entry
5145
b.entries[key] = entry
52-
b.index[key] = entry.Embedding
5346
b.queue = append(b.queue, key)
5447
return nil
5548
}
5649

57-
// Get retrieves an entry from the FIFO cache
58-
func (b *FIFOBackend[K, V]) Get(ctx context.Context, key K) (types.Entry[V], bool, error) {
50+
// Get retrieves the value for a key.
51+
func (b *FIFOBackend[K, V]) Get(_ context.Context, key K) (V, bool, error) {
5952
b.mu.RLock()
6053
defer b.mu.RUnlock()
61-
62-
if entry, ok := b.entries[key]; ok {
63-
return entry, true, nil
54+
if e, ok := b.entries[key]; ok {
55+
return e.Value, true, nil
6456
}
65-
return types.Entry[V]{}, false, nil
57+
var zero V
58+
return zero, false, nil
6659
}
6760

68-
// Delete removes an entry from the FIFO cache
69-
func (b *FIFOBackend[K, V]) Delete(ctx context.Context, key K) error {
61+
// Delete removes an entry by key.
62+
func (b *FIFOBackend[K, V]) Delete(_ context.Context, key K) error {
7063
b.mu.Lock()
7164
defer b.mu.Unlock()
7265

73-
if _, exists := b.entries[key]; !exists {
66+
if _, ok := b.entries[key]; !ok {
7467
return nil
7568
}
76-
7769
delete(b.entries, key)
78-
delete(b.index, key)
7970

80-
// Remove from queue
81-
for i, qKey := range b.queue {
82-
if qKey == key {
71+
for i, k := range b.queue {
72+
if k == key {
8373
b.queue = append(b.queue[:i], b.queue[i+1:]...)
8474
break
8575
}
8676
}
8777
return nil
8878
}
8979

90-
// Contains checks if a key exists in the FIFO cache
91-
func (b *FIFOBackend[K, V]) Contains(ctx context.Context, key K) (bool, error) {
80+
// Contains checks whether a key exists.
81+
func (b *FIFOBackend[K, V]) Contains(_ context.Context, key K) (bool, error) {
9282
b.mu.RLock()
9383
defer b.mu.RUnlock()
94-
95-
_, exists := b.entries[key]
96-
return exists, nil
84+
_, ok := b.entries[key]
85+
return ok, nil
9786
}
9887

99-
// Flush clears all entries from the FIFO cache
100-
func (b *FIFOBackend[K, V]) Flush(ctx context.Context) error {
88+
// Flush removes all entries.
89+
func (b *FIFOBackend[K, V]) Flush(_ context.Context) error {
10190
b.mu.Lock()
10291
defer b.mu.Unlock()
103-
10492
b.entries = make(map[K]types.Entry[V])
105-
b.index = make(map[K][]float64)
10693
b.queue = make([]K, 0, b.capacity)
10794
return nil
10895
}
10996

110-
// Len returns the number of entries in the FIFO cache
111-
func (b *FIFOBackend[K, V]) Len(ctx context.Context) (int, error) {
97+
// Len returns the number of stored entries.
98+
func (b *FIFOBackend[K, V]) Len(_ context.Context) (int, error) {
11299
b.mu.RLock()
113100
defer b.mu.RUnlock()
114-
115101
return len(b.entries), nil
116102
}
117103

118-
// Keys returns all keys in the FIFO cache
119-
func (b *FIFOBackend[K, V]) Keys(ctx context.Context) ([]K, error) {
104+
// Close is a no-op for in-memory backends.
105+
func (b *FIFOBackend[K, V]) Close() error { return nil }
106+
107+
// Keys returns all keys in the cache.
108+
func (b *FIFOBackend[K, V]) Keys(_ context.Context) ([]K, error) {
120109
b.mu.RLock()
121110
defer b.mu.RUnlock()
122-
123-
keys := make([]K, 0, len(b.index))
124-
for key := range b.index {
125-
keys = append(keys, key)
111+
keys := make([]K, 0, len(b.entries))
112+
for k := range b.entries {
113+
keys = append(keys, k)
126114
}
127115
return keys, nil
128116
}
129117

130-
// GetEmbedding retrieves just the embedding for a key
131-
func (b *FIFOBackend[K, V]) GetEmbedding(ctx context.Context, key K) ([]float64, bool, error) {
118+
// GetEmbedding retrieves the embedding for a key.
119+
func (b *FIFOBackend[K, V]) GetEmbedding(_ context.Context, key K) ([]float64, bool, error) {
132120
b.mu.RLock()
133121
defer b.mu.RUnlock()
134-
135-
if embedding, ok := b.index[key]; ok {
136-
return embedding, true, nil
122+
if e, ok := b.entries[key]; ok {
123+
return e.Embedding, true, nil
137124
}
138125
return nil, false, nil
139126
}
140-
141-
// Close closes the FIFO backend (no-op for in-memory)
142-
func (b *FIFOBackend[K, V]) Close() error {
143-
return nil
144-
}
145-
146-
// SetAsync stores an entry asynchronously
147-
func (b *FIFOBackend[K, V]) SetAsync(ctx context.Context, key K, entry types.Entry[V]) <-chan error {
148-
errCh := make(chan error, 1)
149-
go func() {
150-
defer close(errCh)
151-
errCh <- b.Set(ctx, key, entry)
152-
}()
153-
return errCh
154-
}
155-
156-
// GetAsync retrieves an entry asynchronously
157-
func (b *FIFOBackend[K, V]) GetAsync(ctx context.Context, key K) <-chan types.AsyncGetResult[V] {
158-
resultCh := make(chan types.AsyncGetResult[V], 1)
159-
go func() {
160-
defer close(resultCh)
161-
entry, found, err := b.Get(ctx, key)
162-
resultCh <- types.AsyncGetResult[V]{
163-
Entry: entry,
164-
Found: found,
165-
Error: err,
166-
}
167-
}()
168-
return resultCh
169-
}
170-
171-
// DeleteAsync removes an entry asynchronously
172-
func (b *FIFOBackend[K, V]) DeleteAsync(ctx context.Context, key K) <-chan error {
173-
errCh := make(chan error, 1)
174-
go func() {
175-
defer close(errCh)
176-
errCh <- b.Delete(ctx, key)
177-
}()
178-
return errCh
179-
}
180-
181-
// GetBatchAsync retrieves multiple entries asynchronously
182-
func (b *FIFOBackend[K, V]) GetBatchAsync(ctx context.Context, keys []K) <-chan types.AsyncBatchResult[K, V] {
183-
resultCh := make(chan types.AsyncBatchResult[K, V], 1)
184-
go func() {
185-
defer close(resultCh)
186-
entries := make(map[K]types.Entry[V])
187-
for _, key := range keys {
188-
if entry, found, err := b.Get(ctx, key); err == nil && found {
189-
entries[key] = entry
190-
}
191-
}
192-
resultCh <- types.AsyncBatchResult[K, V]{
193-
Entries: entries,
194-
Error: nil,
195-
}
196-
}()
197-
return resultCh
198-
}

0 commit comments

Comments
 (0)