Skip to main content

Semantic Cache

The semantic cache system provides multi-layer caching for LLM responses, combining exact-match Redis caching with pgvector-based semantic search.

Architecture​

Request
│
├─→ [Redis Exact Cache] ─── hit → Return cached response
│ key: cache:{org_id}:{model_tier}:{sha256(prompt)}
│
├─→ [pgvector Exact Cache] ─── hit → Return + warm Redis
│ unique index on (org_id, model_tier, prompt_hash, model_version)
│ (disabled by default — see Read Path below)
│
├─→ [Embedding Service] → vector(384)
│ bge-small-en via text-embeddings-inference sidecar
│
├─→ [pgvector Semantic Search] ─── hit → Return + warm Redis
│ HNSW index with cosine similarity threshold (≥0.95)
│
└─→ [Upstream LLM API]
↓ async write to both Redis and pgvector

Components​

ComponentPackageDescription
Redis exact cachepkg/cache/redis.goSHA-256 keyed Redis store, 24h TTL
pgvector storagepkg/cache/store.goCRUD operations, vector I/O helpers
Semantic searchpkg/cache/semantic.goANN lookup via HNSW cosine distance
Write pipelinepkg/cache/writer.goAsync worker pool for cache writes
SSE replaypkg/cache/replay.goRecordingWriter + 3 replay modes
Key normalizationpkg/cache/normalize.goCanonical JSON for consistent hashing
Embedding servicepkg/embeddings/client.goHTTP client for embedding API
Unified servicepkg/cache/service.goOrchestrates all layers with privacy controls
Privacy policiespkg/cache/service.goPer-org enabled/disabled/read-only modes
TTL cleanerpkg/cache/cleaner.goPeriodic expired entry purge
Prometheus metricspkg/cache/metrics.go8 cache-specific metric vectors
Analytics + A/Bpkg/cache/analytics.goHit rate tracking, traffic splitting, warming

Multilingual traffic (Japanese and others)​

bge-small-en-v1.5 is English-trained: on Japanese (and other non-English) traffic both semantic-cache precision and classifier accuracy degrade. For multilingual deployments swap the embedder for a multilingual model via config — no code change:

YABANIN_EMBEDDER_MODEL=intfloat/multilingual-e5-small
YABANIN_EMBEDDER_QUERY_PREFIX="query: "

Notes:

  • multilingual-e5-small keeps the same 384 dimensions as bge-small-en, so the existing pgvector schema (vector(384)) needs no migration (verify the dimension on first boot — a mismatch fails the insert loudly rather than corrupting the cache).
  • e5-family models are trained with input prefixes. The configured YABANIN_EMBEDDER_QUERY_PREFIX is prepended to every embedded text — lookups and writes alike — which keeps cosine comparisons symmetric. Changing the prefix invalidates existing semantic entries; flush the cache (POST /api/cache/flush) after switching.
  • The task classifier is a separate model with its own (English) training data; routed tiering on Japanese traffic falls back to the rule-tier V0. Retraining the classifier on Japanese prompts is tracked separately.

A compose profile with the multilingual embedder sidecar lives in deploy/docker-compose.yml (embedder-multilingual).

Privacy Controls​

Each organization has a cache policy:

  • enabled — full read/write cache access
  • disabled — no caching at all (privacy-first)
  • read-only — can look up cached responses but never write new ones

Policies are cached in-memory with 60s TTL to avoid DB overhead on every request.

SSE Streaming​

For streaming responses, the cache captures individual SSE frames with timing:

rw := cache.NewRecordingWriter(w)
// ... upstream response is written through rw ...
chunks := rw.Chunks() // []SSEChunk{Data, DelayMS}

Replay modes:

  • faithful — preserve original timing (default)
  • burst — send all chunks immediately
  • paced — equal 10ms intervals

Docker Compose​

The bge embedding sidecar is included in deploy/docker-compose.yml:

bge-embedder:
image: ghcr.io/huggingface/text-embeddings-inference:latest
command: --model-id BAAI/bge-small-en-v1.5 --dtype float32
ports: ["6006:80"]

Prometheus Metrics​

MetricTypeLabelsDescription
cache_hits_totalCountertype, org_idCache hits by lookup type
cache_writes_totalCounterstatusWrite attempts (written/dropped)
cache_purged_totalCounterbackendExpired entries removed
cache_sizeGaugebackendCurrent entry count
cache_lookup_duration_secondsHistogramtypeLookup latency
cache_write_queue_sizeGauge—Pending write jobs
cache_write_queue_drops_totalCounter—Dropped writes (queue full)
cache_embedding_errors_totalCounter—Embedding failures

Migration​

Run migration 0009 to create the cache tables:

-- migrations/0009_pgvector.up.sql
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE cache_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL, model_tier TEXT NOT NULL,
prompt_hash TEXT NOT NULL, embedding vector(384) NOT NULL,
response JSONB NOT NULL, sse_chunks JSONB,
tokens_in INT DEFAULT 0, tokens_out INT DEFAULT 0,
hit_count INT DEFAULT 0, model_version TEXT DEFAULT '1',
created_at TIMESTAMPTZ DEFAULT now(),
expires_at TIMESTAMPTZ DEFAULT (now() + interval '24 hours')
);

Tests​

  • Unit tests: go test ./pkg/cache/...
  • Benchmarks: go test -bench=. ./pkg/cache/...
  • Integration (requires Docker): go test -race ./services/gateway/integration/...