Skip to content

Latest commit

 

History

History
657 lines (524 loc) · 26.6 KB

File metadata and controls

657 lines (524 loc) · 26.6 KB

up-vector — Implementation Plan

A self-hosted, Upstash Vector-compatible HTTP proxy backed by Redis Stack. Drop-in replacement for implemented dense-vector @upstash/vector surfaces, plus dense raw-text RAG endpoints when a server-side embedding provider is configured.

Sibling project to up-redis (same idea, but for vectors).


Architecture

@upstash/vector SDK (your app, Vercel edge, anywhere)
        |
        | HTTP REST (Upstash Vector protocol)
        |
   ┌────▼─────────────────────┐
   │       up-vector           │
   │  (Hono on Bun, ~800 LOC) │
   │                           │
   │  Accepts Upstash Vector   │
   │  REST calls, translates   │
   │  to Redis Stack FT.*      │
   │  commands                 │
   └────┬─────────────────────┘
        |
        | Redis protocol (FT.CREATE, FT.SEARCH, HSET, DEL...)
        |
   ┌────▼─────────────────────┐
   │    Redis Stack            │
   │  (redis-stack-server)     │
   │                           │
   │  RediSearch module does   │
   │  HNSW vector indexing,    │
   │  KNN search, filtering   │
   └──────────────────────────┘

Key design decisions:

  1. Separate from up-redis — Different protocol (resource REST vs command-array forwarding), different concerns. Clean separation. Can run alongside up-redis against the same Redis Stack, or standalone with its own.
  2. Own Redis Stack in compose — Self-contained, portable. One docker compose up and it works. Can also connect to an external Redis Stack via env var.
  3. Bun runtime — Native TypeScript, fastest JS runtime, built-in test runner.
  4. Hono framework — Lightweight, fast, excellent middleware, portable (Bun/Node/Deno/Workers).

Tech Stack

Layer Choice Version Why
Runtime Bun 1.3+ Native TS, fast runtime, built-in test runner
HTTP Hono v4 Lightweight, fast, great middleware, portable
Redis client Bun.redis built-in Native Bun Redis client with send() for raw FT.* commands, zero deps
Validation Zod v4 Request body validation, type inference
Linting/Format Biome v2 Fast, modern, replaces ESLint+Prettier
Testing Bun test built-in Fast, Jest-compatible API
Container Bun Alpine oven/bun:alpine Minimal image size
Vector backend Redis Stack latest (7.4+) RediSearch HNSW, production-grade, FT.* commands

API Compatibility Matrix

Endpoints — Dense-Vector Upstash Vector REST API Subset

The @upstash/vector SDK sends ALL requests as HTTP POST with JSON body. We support both the SDK's POST-only pattern and the documented HTTP methods for curl/raw usage.

Endpoint SDK Method Priority Status Notes
POST /upsert[/{ns}] POST P0 Supported Dense vectors — HSET + lazy FT.CREATE
POST /query[/{ns}] POST P0 Supported Dense KNN via FT.SEARCH
GET/POST /fetch[/{ns}] POST P0 Supported HGETALL per ID, prefix scan
DELETE/POST /delete[/{ns}] POST P0 Supported DEL keys by id, prefix, or filter
POST /update[/{ns}] POST P0 Supported Atomic Lua update, RFC 7396 metadata PATCH
GET/POST /range[/{ns}] POST P0 Supported Offset cursor pagination
GET/POST /random[/{ns}] n/a P1 Supported Reservoir sample over namespace keys
DELETE/POST /reset[/{ns}] POST P0 Supported FT.DROPINDEX + key cleanup
GET/POST /info POST P0 Supported FT.INFO + key count, indexType: DENSE
GET/POST /list-namespaces POST P1 Supported SMEMBERS on namespace registry
DELETE/POST /delete-namespace/{ns} POST P1 Supported Drop index + keys + registry entry
POST /rename-namespace n/a P1 Supported Move keys, rebuild index, update registry
GET / GET P0 Supported Health check
Sparse/hybrid vector payloads POST P2 Deferred Dense-only backend; reject explicitly
POST /upsert-data[/{ns}] POST P1 Supported Dense only; embeds raw text through configured provider and stores it as data
POST /query-data[/{ns}] POST P1 Supported Dense only; embeds query text then reuses dense query execution
POST /resumable-query[/{ns}] POST P2 Unsupported Explicit 501; stateful cursor semantics deferred
POST /resumable-query-data[/{ns}] POST P2 Unsupported Explicit 501; stateful cursor semantics deferred
POST /resumable-query-next POST P2 Unsupported Explicit 501; stateful cursor semantics deferred
POST /resumable-query-end POST P2 Unsupported Explicit 501; stateful cursor semantics deferred

Response Envelope

Every response follows the Upstash convention:

// Success
{ "result": <data> }

// Error
{ "error": "<message>", "status": <http_status_code> }

Authentication

Authorization: Bearer <token> header on every request. Token validated against config (env var or file, same pattern as up-redis).


Redis Stack Translation Mapping

Data Model

Each vector is stored as a Redis Hash:

Key:    v:{namespace}:{id}
Fields:
  vec       → binary blob (Float32Array as Buffer)
  metadata  → JSON string
  data      → raw string (optional, for the `data` field)
  id        → string (redundant with key, but needed for FT.SEARCH result parsing)

Sparse vectors (if implemented later):

  svec_idx  → binary blob (Int32Array of indices)
  svec_val  → binary blob (Float32Array of values)

Index Management

One RediSearch index per namespace, created lazily on first upsert:

FT.CREATE idx:{namespace}
  ON HASH
  PREFIX 1 v:{namespace}:
  SCHEMA
    vec VECTOR HNSW 6
      TYPE FLOAT32
      DIM {dimension}          ← detected from first upsert
      DISTANCE_METRIC {metric} ← from config (COSINE default)
    metadata TAG SEPARATOR ""  ← for existence checks
    id TAG                     ← for exact match lookups

Metadata field indexing is the tricky part — RediSearch requires fields to be declared in the schema. Options:

  1. Dynamic re-indexing: Track metadata fields seen, ALTER index when new fields appear. Complex, fragile.
  2. JSON module: Use RedisJSON + FT.CREATE ON JSON with JSONPath. More flexible for nested metadata. Requires Redis Stack (which we have).
  3. Metadata-as-JSON-string + application-level filtering: Store metadata as JSON string, do KNN search in Redis, filter in application code. Simple but less efficient for large result sets.

Recommended: Option 3 for v1, upgrade to Option 2 for v2.

Rationale: Application-level metadata filtering is simpler to implement correctly and handles the full Upstash filter syntax without being limited by RediSearch's schema model. For RAG workloads with topK of 5-20, filtering a few extra results in-app is negligible. For production at scale, upgrade to RedisJSON indexing.

Command Translation

Upstash Vector Redis Stack Commands
upsert Atomic Lua write: HSET v:{ns}:{id} id <id> vec <blob> _vec <base64> plus HSET/HDEL for optional metadata and data (+ lazy FT.CREATE on first upsert per namespace)
upsert-data Embed raw text with configured provider → same storage path as upsert, with data set to the raw text; marks the namespace as embedding-backed
query FT.SEARCH idx:{ns} "*=>[KNN {topK * overFetchFactor} @vec $BLOB AS score]" PARAMS 2 BLOB <bytes> SORTBY score LIMIT 0 {topK * overFetchFactor} DIALECT 2 → then app-level metadata filter → trim to topK
query-data Embed raw query text with configured provider → same query path as dense query
fetch by IDs HGETALL v:{ns}:{id} per ID (pipelined)
fetch by prefix SCAN 0 MATCH v:{ns}:{prefix}* COUNT 100HGETALL per match
delete by IDs DEL v:{ns}:{id1} v:{ns}:{id2} ...
delete by prefix SCAN + DEL (batched)
delete by filter FT.SEARCH (broad) → app-level filter → DEL matching keys
update HSET v:{ns}:{id} [vec <blob>] [metadata <json>] [data <str>] (partial, only provided fields); data-only updates re-embed when the namespace was populated through /upsert-data
range SCAN {cursor} MATCH v:{ns}:* COUNT {limit}HGETALL per key
reset namespace FT.DROPINDEX idx:{ns} + SCAN + DEL all v:{ns}:* keys; keep namespace registry entries and clear the embedding-backed marker
reset all Drop known idx:* indexes + delete known namespace key prefixes; keep namespace registry entries and clear embedding-backed markers
info FT.INFO idx:{ns} (per namespace) + SCARD _ns_registry
list-namespaces SMEMBERS _ns_registry
delete-namespace Drop index + DEL keys + SREM _ns_registry {ns}

Vector Serialization

Upstash expects number[] (JSON float arrays). Redis Stack expects raw binary blobs.

// Encode: number[] → Buffer (Float32, little-endian)
function encodeVector(vec: number[]): Buffer {
  const buf = Buffer.alloc(vec.length * 4)
  for (let i = 0; i < vec.length; i++) {
    buf.writeFloatLE(vec[i], i * 4)
  }
  return buf
}

// Decode: Buffer → number[]
function decodeVector(buf: Buffer): number[] {
  const vec: number[] = new Array(buf.length / 4)
  for (let i = 0; i < vec.length; i++) {
    vec[i] = buf.readFloatLE(i * 4)
  }
  return vec
}

Score Normalization

Upstash normalizes all scores to 0-1 range. Redis Stack returns raw distances.

Metric Redis Stack returns Upstash returns Conversion
COSINE 1 - cosine_similarity (0 = identical) (1 + cosine_similarity) / 2 (1 = identical) (1 - (1 - redis_score)) / 2 + 0.51 - redis_score / 2
EUCLIDEAN squared L2 distance 1 / (1 + squared_distance) 1 / (1 + redis_score)
DOT_PRODUCT negative dot product (1 + dot_product) / 2 (1 + (-redis_score)) / 2

Metadata Filter Parser

The most complex component. Upstash uses a SQL-like filter language:

population >= 1000000 AND geography.continent = 'Asia'
genre IN ('comedy', 'drama') AND year > 2020
tags CONTAINS 'featured' AND HAS FIELD premium
title GLOB 'The *' OR (rating >= 4.5 AND reviews > 100)

Supported Operators

Operator Example v1 Strategy
=, != status = 'active' App-level: parse JSON metadata, compare
<, <=, >, >= price >= 100 App-level: parse JSON metadata, compare
GLOB, NOT GLOB name GLOB 'Ben*' App-level: convert to regex, test
IN, NOT IN tag IN ('a','b') App-level: set membership test
CONTAINS, NOT CONTAINS tags CONTAINS 'x' App-level: array includes check
HAS FIELD, HAS NOT FIELD HAS FIELD email App-level: key existence in parsed JSON
AND, OR, () grouped conditions App-level: boolean combinator tree
Dot notation geo.country = 'FR' App-level: nested field access
Array indexing items[0] = 'x' App-level: array index access

Parser Architecture

  1. Tokenizer — Splits filter string into tokens: identifiers, operators, strings, numbers, parens
  2. AST builder — Recursive descent parser producing a tree of AND/OR/comparison nodes
  3. Evaluator — Walks the AST against a parsed metadata JSON object, returns boolean

This is a self-contained module (~200-300 lines). Well-suited for thorough unit testing.

Over-fetching Strategy

Since filtering happens in application code (v1), we need to fetch more results than topK to account for filtered-out vectors:

const OVER_FETCH_FACTOR = 3  // Fetch 3x topK from Redis
const MAX_OVER_FETCH = 1000  // Cap at 1000 to prevent memory issues

const fetchCount = Math.min(topK * OVER_FETCH_FACTOR, MAX_OVER_FETCH)
// FT.SEARCH with fetchCount, then filter, then trim to topK

If after filtering we have fewer than topK results, we can re-query with a larger fetch (doubling strategy). This is a pragmatic approach — for most RAG workloads with light filtering, the first pass will suffice.


Server-Side Embedding Provider

/upsert-data and /query-data are intentionally provider-backed rather than pretending to be Upstash-hosted embedding models. Current provider modes:

Provider Status Notes
disabled Default Raw-text endpoints return a clear 400. Dense vector endpoints continue to work.
openai Supported Calls an OpenAI-compatible POST /embeddings endpoint with model, input, and optional dimensions.
fake Test/dev only Deterministic vectors for CI and local tests. Not semantic.

Operational rules:

  • Provider calls are timeout-bounded by UPVECTOR_EMBEDDING_TIMEOUT_MS.
  • Timeout, HTTP 429, and HTTP 5xx responses are retried up to UPVECTOR_EMBEDDING_RETRIES.
  • Returned embedding dimensions are validated against UPVECTOR_EMBEDDING_DIMENSION when set.
  • Existing namespace dimensions are validated before writes/queries proceed.
  • UPVECTOR_DIMENSION and UPVECTOR_EMBEDDING_DIMENSION must match when both are set.

This is not exact parity with Upstash-hosted model provisioning. It is a practical RAG path for self-hosters who already operate an OpenAI-compatible embedding provider.


Project Structure

up-vector/
├── src/
│   ├── index.ts                 # Entry point — starts server
│   ├── server.ts                # Hono app + middleware setup
│   ├── config.ts                # Env var config with Zod validation
│   ├── middleware/
│   │   ├── auth.ts              # Bearer token validation
│   │   ├── error-handler.ts     # Global error → Upstash error envelope
│   │   └── logger.ts            # Request logging
│   ├── redis.ts                 # Bun.redis client, connection management
│   ├── routes/
│   │   ├── health.ts            # GET /
│   │   ├── upsert.ts            # POST /upsert[/{ns}]
│   │   ├── query.ts             # POST /query[/{ns}]
│   │   ├── fetch.ts             # POST /fetch[/{ns}]
│   │   ├── delete.ts            # POST|DELETE /delete[/{ns}]
│   │   ├── update.ts            # POST /update[/{ns}]
│   │   ├── range.ts             # POST /range[/{ns}]
│   │   ├── reset.ts             # POST|DELETE /reset[/{ns}]
│   │   ├── info.ts              # GET|POST /info
│   │   └── namespaces.ts        # list-namespaces, delete-namespace
│   ├── translate/
│   │   ├── vectors.ts           # Float32 encode/decode
│   │   ├── scores.ts            # Distance metric normalization
│   │   ├── keys.ts              # Key naming: v:{ns}:{id}, idx:{ns}
│   │   └── index.ts             # Lazy FT.CREATE management
│   ├── filter/
│   │   ├── tokenizer.ts         # Filter string → tokens
│   │   ├── parser.ts            # Tokens → AST
│   │   ├── evaluator.ts         # AST × metadata → boolean
│   │   └── types.ts             # AST node types
│   └── types.ts                 # Shared types (Vector, QueryResult, etc.)
├── tests/
│   ├── unit/
│   │   ├── filter.test.ts       # Filter parser + evaluator (extensive)
│   │   ├── vectors.test.ts      # Encode/decode roundtrip
│   │   ├── scores.test.ts       # Score normalization
│   │   └── keys.test.ts         # Key generation
│   ├── integration/
│   │   ├── upsert.test.ts       # Against real Redis Stack
│   │   ├── query.test.ts
│   │   ├── fetch.test.ts
│   │   ├── delete.test.ts
│   │   ├── namespaces.test.ts
│   │   └── setup.ts             # Test Redis connection, cleanup
│   └── compatibility/
│       └── README.md            # Instructions for running @upstash/vector test suite
├── docker-compose.yml
├── docker-compose.dev.yml       # Dev overrides (volume mounts, debug)
├── Dockerfile
├── .env.example
├── .gitignore
├── biome.json
├── bunfig.toml
├── package.json
├── tsconfig.json
├── LICENSE
├── README.md
└── PLAN.md                      # This file

Configuration

Environment Variables

Variable Default Required Purpose
UPVECTOR_TOKEN Yes Bearer token for API auth
UPVECTOR_REDIS_URL redis://localhost:6379 No Redis Stack connection URL
UPVECTOR_PORT 8080 No HTTP listen port
UPVECTOR_HOST 0.0.0.0 No HTTP listen host
UPVECTOR_DIMENSION No Fixed vector dimension (auto-detected from first upsert if omitted)
UPVECTOR_METRIC COSINE No Distance metric: COSINE, EUCLIDEAN, DOT_PRODUCT
UPVECTOR_LOG_LEVEL info No debug, info, warn, error
UPVECTOR_MAX_CONNECTIONS 10 No Redis connection pool size

Multi-token mode (future)

Like up-redis's file mode, support a JSON config mapping tokens to separate Redis instances / index configs. Not needed for v1.


Implementation Phases

Phase 1 — Scaffold + Core (this session → next session)

  • Project setup (package.json, tsconfig, Docker, Biome)
  • PLAN.md
  • Hono server with auth middleware and error handling
  • Redis connection with Bun.redis
  • Vector encode/decode utilities
  • Key naming module
  • Health endpoint (GET /)

Phase 2 — CRUD Operations

  • POST /upsert — HSET + lazy FT.CREATE
  • GET/POST /fetch — HGETALL (by IDs and prefix)
  • DELETE/POST /delete — DEL (by IDs, prefix, and filter)
  • POST /update — atomic vector/data/metadata update
  • GET/POST /range — offset cursor pagination
  • GET/POST /random — random dense vector fetch
  • DELETE/POST /reset — drop index + keys

Phase 3 — Query + Filtering

  • POST /query — FT.SEARCH KNN
  • Score normalization (COSINE, EUCLIDEAN, DOT_PRODUCT)
  • Filter tokenizer
  • Filter parser (recursive descent)
  • Filter evaluator
  • Over-fetch + app-level filter + trim pipeline
  • POST /delete with filter (fetch → filter → delete)

Phase 4 — Namespaces + Info

  • Namespace registry (Redis Set)
  • GET/POST /list-namespaces
  • DELETE/POST /delete-namespace/{ns}
  • POST /rename-namespace
  • GET/POST /info — aggregate stats across namespaces

Phase 5 — Testing + Compatibility

  • Unit tests (filter parser is the big one)
  • Integration tests against Redis Stack in Docker
  • Compatibility tests with the real @upstash/vector SDK against up-vector
  • CI pipeline (GitHub Actions: build → Redis Stack → test)

Phase 6 — Production Hardening

  • Graceful shutdown (request draining via await server.stop(), configurable timeout, double-signal force exit)
  • Connection retry / reconnect (Bun.redis autoReconnect + onconnect/onclose event logging)
  • Request timeout configuration (UPVECTOR_REQUEST_TIMEOUT, Promise.race middleware)
  • Rate limiting — deferred (self-hosted proxy; use reverse proxy like nginx/Caddy instead)
  • Structured JSON logging (src/logger.ts, JSON/text format, request IDs, stderr for warn/error)
  • Prometheus metrics endpoint (GET /metrics, opt-in via UPVECTOR_METRICS=true, counters + histograms)
  • Enhanced health check (GET /health with Redis probe + shutdown state, GET / returns 503 during shutdown)

Phase 7 — Deferred (only if needed)

  • /upsert-data + /query-data (provider-backed: openai / fake / disabled; not Upstash-hosted model catalog)
  • Sparse vector support
  • Hybrid search with fusion algorithms
  • Resumable queries (stateful cursors)
  • Multi-token / multi-index mode (up-redis file-mode equivalent)
  • RedisJSON-based metadata indexing (v2 filter upgrade)
  • /range real SCAN-cursor pagination (known O(N)/OOM risk today; offset-style API preserved)
  • Metadata filter pushdown into RediSearch (v1 over-fetch + JS filter remains)

Docker Setup

docker-compose.yml

services:
  up-vector:
    build: .
    ports:
      - "${UPVECTOR_PORT:-8080}:8080"
    environment:
      - UPVECTOR_TOKEN=${UPVECTOR_TOKEN}
      - UPVECTOR_REDIS_URL=redis://redis:6379
      - UPVECTOR_PORT=8080
      - UPVECTOR_METRIC=${UPVECTOR_METRIC:-COSINE}
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/"]
      interval: 10s
      timeout: 5s
      retries: 3

  redis:
    image: redis/redis-stack-server:latest
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  redis-data:

Dockerfile

FROM oven/bun:alpine AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production=false
COPY . .
RUN bun build src/index.ts --target=bun --outdir=dist --minify

FROM oven/bun:alpine
WORKDIR /app
RUN apk add --no-cache curl
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 8080
CMD ["bun", "run", "dist/index.js"]

Testing Strategy

1. Unit Tests (Bun test)

Focus on the filter parser — it's the most complex and error-prone module:

filter tokenizer: 30+ test cases (strings, numbers, operators, nested parens)
filter parser: 20+ test cases (simple, compound, nested, edge cases)
filter evaluator: 30+ test cases (every operator, dot notation, arrays)
vector encode/decode: roundtrip fidelity, edge cases (NaN, Infinity, empty)
score normalization: all 3 metrics, boundary values

2. Integration Tests

Spin up Redis Stack (in Docker or locally), run operations end-to-end:

  • Upsert → Query → verify results
  • Upsert → Fetch → verify data integrity
  • Upsert → Delete → Fetch → verify gone
  • Namespace isolation
  • Range pagination
  • Reset

3. Compatibility Tests (the up-redis approach)

The tests/compatibility/ suite uses the real @upstash/vector TypeScript SDK as the client and points it at up-vector. This is the ultimate compatibility check for the dense-vector SDK surface because it exercises the same request paths, envelopes, and response parsing production applications use.

Coverage intentionally excludes:

  • Sparse/hybrid payloads and fusion/query-mode options
  • Resumable queries
  • Upstash-specific provisioning APIs

CI Pipeline

# .github/workflows/test.yml
on:
  push:
    paths: [src/**, tests/**, package.json, Dockerfile]
  schedule:
    - cron: '0 12 * * *'  # Daily, same as up-redis

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      redis:
        image: redis/redis-stack-server:latest
        ports: [6379:6379]
    steps:
      - uses: oven/setup-bun@v2
      - run: bun install
      - run: bun test                    # Unit + integration
      - run: bun test:compat             # @upstash/vector SDK suite

Compatibility Notes

What works identically to Upstash

  • All CRUD operations (upsert, fetch, delete, update, range, reset)
  • KNN similarity search with all 3 distance metrics
  • Score normalization (0-1 range)
  • Namespace isolation
  • Metadata storage and retrieval
  • Bearer token authentication
  • JSON request/response envelope format
  • The implemented dense and raw-text @upstash/vector TypeScript SDK calls (just swap the URL and configure embeddings when using data)

Known differences from Upstash

Aspect Upstash up-vector Impact
ANN algorithm DiskANN HNSW (RediSearch) Slightly different recall characteristics at very high scale. Negligible at <100K vectors.
Metadata filtering Server-side (DiskANN-integrated) App-level (v1) / RediSearch (v2) May return slightly different results when filter + topK interact (over-fetch compensates)
Embedding endpoints Built-in hosted models OpenAI-compatible provider, or client-provided vectors No Upstash-hosted model catalog or provisioning parity.
Sparse/hybrid Full support Not supported (v1) Dense-only. See docs/architecture/sparse-hybrid.md before implementation.
Resumable queries Supported Explicit 501 unsupported Use regular query/range; do not expect cursor/session semantics.
Index creation Dashboard/API Automatic (lazy on first upsert) No separate provisioning step needed.
Multi-index Per-database Per-namespace (same Redis) Equivalent functionality via namespaces.

Usage (once built)

Docker Compose

# Clone and start
git clone https://github.com/Coriou/up-vector.git
cd up-vector
cp .env.example .env
# Edit .env: set UPVECTOR_TOKEN
docker compose up -d

With @upstash/vector SDK

import { Index } from "@upstash/vector"

const index = new Index({
  url: "http://localhost:8080",    // up-vector
  token: "your-token-here",
})

// Works exactly like Upstash
await index.upsert([
  { id: "doc-1", vector: embedding, metadata: { title: "Hello" } },
])

const results = await index.query({
  vector: queryEmbedding,
  topK: 5,
  includeMetadata: true,
  filter: "title = 'Hello'",
})

With up-redis (side-by-side in Coolify)

Both services can share the same Redis Stack instance, or run independently — up-redis handles standard Redis commands, up-vector handles vector search:

# Shared Redis Stack setup
services:
  redis-stack:
    image: redis/redis-stack-server:latest

  up-redis:
    image: ghcr.io/coriou/up-redis:latest
    environment:
      UPREDIS_TOKEN: ${UPREDIS_TOKEN}
      UPREDIS_REDIS_URL: redis://redis-stack:6379

  up-vector:
    build: ./up-vector
    environment:
      UPVECTOR_TOKEN: ${UPVECTOR_TOKEN}
      UPVECTOR_REDIS_URL: redis://redis-stack:6379

References