Skip to content

Commit b06278b

Browse files
authored
🔀 Merge pull request #6 from nmdra/redis-cache. Closes #5
Add redis Cache support.
2 parents c45f724 + e27afbc commit b06278b

8 files changed

Lines changed: 132 additions & 9 deletions

File tree

cmd/main.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ type config struct {
2323
port int
2424
apiKey string
2525
db struct {
26-
dsn string
26+
dsn string
27+
redis string
2728
}
2829
}
2930

@@ -32,10 +33,12 @@ func loadConfig() config {
3233

3334
defaultAPIKey := os.Getenv("GEMINI_API_KEY")
3435
defaultDSN := os.Getenv("DATABASE_URL")
36+
defaultRedisUrl := os.Getenv("REDIS_URL")
3537

3638
flag.IntVar(&cfg.port, "port", 8080, "API server port")
3739
flag.StringVar(&cfg.apiKey, "apikey", defaultAPIKey, "Gemini API Key")
3840
flag.StringVar(&cfg.db.dsn, "db-dsn", defaultDSN, "PostgreSQL DSN")
41+
flag.StringVar(&cfg.db.redis, "redis", defaultRedisUrl, "Redis instance URL")
3942
flag.Parse()
4043

4144
return cfg
@@ -64,13 +67,26 @@ func main() {
6467
defer dbpool.Close()
6568
logger.Info("Connected to PostgreSQL")
6669

70+
var embedder embed.Embedder
71+
6772
// Embedding client
68-
embedder, err := embed.NewGeminiEmbedder(ctx, logger, cfg.apiKey)
73+
baseEmbedder, err := embed.NewGeminiEmbedder(ctx, logger, cfg.apiKey)
6974
if err != nil {
7075
logger.Error("Failed to initialize embedder", "error", err)
7176
os.Exit(1)
7277
}
7378

79+
if cfg.db.redis != "" {
80+
redisClient := db.NewRedisClient(cfg.db.redis, logger)
81+
embedder = &embed.CachedEmbedder{
82+
Base: baseEmbedder,
83+
Redis: redisClient,
84+
Logger: logger,
85+
}
86+
} else {
87+
embedder = baseEmbedder
88+
}
89+
7490
// Services and handlers
7591
repo := repository.New(dbpool)
7692
bookService := &service.BookService{
@@ -87,7 +103,7 @@ func main() {
87103
e.Use(middleware.Logger())
88104
e.Use(middleware.Recover())
89105
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
90-
Timeout: 5 * time.Second,
106+
Timeout: 5 * time.Second,
91107
ErrorMessage: "Request timed out.",
92108
OnTimeoutRouteErrorHandler: func(err error, c echo.Context) {
93109
logger.Warn("Timeout on route", "path", c.Path(), "error", err)

docker-compose.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,15 @@ services:
1111
volumes:
1212
- pgdata:/var/lib/postgresql/data
1313

14+
redis:
15+
image: redis:alpine
16+
container_name: redis-cache
17+
ports:
18+
- "6379:6379"
19+
command: redis-server --save 20 1 --loglevel warning
20+
volumes:
21+
- redis_data:/data
22+
1423
volumes:
15-
pgdata:
24+
pgdata:
25+
redis_data:

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ module semantic-search
33
go 1.24.5
44

55
require (
6+
github.com/cespare/xxhash/v2 v2.3.0
67
github.com/jackc/pgx/v5 v5.7.5
78
github.com/joho/godotenv v1.5.1
89
github.com/labstack/echo/v4 v4.13.4
910
github.com/lmittmann/tint v1.1.2
1011
github.com/pgvector/pgvector-go v0.3.0
12+
github.com/redis/go-redis/v9 v9.11.0
1113
github.com/stretchr/testify v1.10.0
1214
google.golang.org/genai v1.15.0
1315
)
@@ -17,6 +19,7 @@ require (
1719
cloud.google.com/go/auth v0.9.3 // indirect
1820
cloud.google.com/go/compute/metadata v0.5.0 // indirect
1921
github.com/davecgh/go-spew v1.1.1 // indirect
22+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
2023
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
2124
github.com/google/go-cmp v0.7.0 // indirect
2225
github.com/google/s2a-go v0.1.8 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykW
88
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
99
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
1010
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
11+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
12+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
13+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
14+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
1115
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
16+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
17+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
1218
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
1319
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
1420
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1521
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1622
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1723
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
25+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
1826
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
1927
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
2028
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -91,6 +99,8 @@ github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9R
9199
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
92100
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
93101
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
102+
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
103+
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
94104
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
95105
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
96106
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

internal/db/db.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,18 @@ func NewPool(ctx context.Context, dsn string, logger *slog.Logger) (*pgxpool.Poo
1818
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
1919
defer cancel()
2020

21-
logger.Info("Parsing database configuration")
2221
config, err := pgxpool.ParseConfig(dsn)
2322
if err != nil {
2423
logger.Error("Failed to parse DSN", "error", err)
2524
return nil, fmt.Errorf("failed to parse DSN: %w", err)
2625
}
2726

28-
logger.Info("Creating database connection pool")
2927
pool, err := pgxpool.NewWithConfig(ctx, config)
3028
if err != nil {
3129
logger.Error("Failed to create DB pool", "error", err)
3230
return nil, fmt.Errorf("failed to create pool: %w", err)
3331
}
3432

35-
logger.Info("Pinging database to verify connection")
3633
if err := pool.Ping(ctx); err != nil {
3734
pool.Close()
3835
return nil, fmt.Errorf("failed to ping database: %w", err)

internal/db/redis.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package db
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"time"
7+
8+
"github.com/redis/go-redis/v9"
9+
)
10+
11+
// NewRedisClient creates a Redis client and logs the connection status.
12+
func NewRedisClient(addr string, logger *slog.Logger) *redis.Client {
13+
client := redis.NewClient(&redis.Options{
14+
Addr: addr,
15+
ReadTimeout: 1 * time.Second,
16+
WriteTimeout: 1 * time.Second,
17+
PoolSize: 10,
18+
})
19+
20+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
21+
defer cancel()
22+
23+
if err := client.Ping(ctx).Err(); err != nil {
24+
logger.Error("Failed to connect to Redis", "addr", addr, "error", err)
25+
panic("Failed to connect to Redis: " + err.Error())
26+
}
27+
28+
logger.Info("Connected to Redis", "addr", addr)
29+
return client
30+
}

internal/embed/cached_embedder.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package embed
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log/slog"
8+
"strings"
9+
"time"
10+
11+
"github.com/cespare/xxhash/v2"
12+
"github.com/redis/go-redis/v9"
13+
)
14+
15+
type CachedEmbedder struct {
16+
Base Embedder
17+
Redis *redis.Client
18+
Logger *slog.Logger
19+
}
20+
21+
func (c *CachedEmbedder) Embed(ctx context.Context, input string) ([]float32, error) {
22+
normalized := strings.TrimSpace(strings.ToLower(input))
23+
hash := xxhash.Sum64String(normalized)
24+
cacheKey := fmt.Sprintf("embed:%x", hash)
25+
26+
cached, err := c.Redis.Get(ctx, cacheKey).Bytes()
27+
if err == nil {
28+
var vec []float32
29+
if err := json.Unmarshal(cached, &vec); err == nil {
30+
c.Logger.Info("Embedding cache hit", "query", input)
31+
return vec, nil
32+
}
33+
c.Logger.Warn("Failed to unmarshal cached embedding", "error", err)
34+
} else if err != redis.Nil {
35+
// Only log real Redis errors (not cache misses)
36+
c.Logger.Warn("Redis GET failed", "key", cacheKey, "error", err)
37+
}
38+
39+
// Cache Miss
40+
vec, err := c.Base.Embed(ctx, input)
41+
if err != nil {
42+
return nil, fmt.Errorf("embedding failed: %w", err)
43+
}
44+
45+
data, err := json.Marshal(vec)
46+
if err != nil {
47+
c.Logger.Warn("Failed to marshal embedding for cache", "error", err)
48+
} else {
49+
err := c.Redis.Set(ctx, cacheKey, data, 24*time.Hour).Err()
50+
if err != nil {
51+
c.Logger.Warn("Failed to store embedding in Redis", "key", cacheKey, "error", err)
52+
} else {
53+
c.Logger.Debug("Cached embedding", "query", input)
54+
}
55+
}
56+
57+
return vec, nil
58+
}

internal/embed/embed.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ type GeminiEmbedder struct {
1919
logger *slog.Logger
2020
}
2121

22-
func NewGeminiEmbedder(ctx context.Context, logger *slog.Logger, apikey string) (*GeminiEmbedder, error) {
22+
func NewGeminiEmbedder(ctx context.Context, logger *slog.Logger, apiKey string) (*GeminiEmbedder, error) {
2323

24-
apiKey := apikey
2524
if apiKey == "" {
2625
return nil, errors.New("GEMINI_API_KEY is not set")
2726
}

0 commit comments

Comments
 (0)