Skip to content

Qdrant Deployment (Air-Gapped) — Vector DB for assistant-service

Qdrant is the vector database assistant-service's intelligence layer uses for the memory / mem0 feature (long-term agent memory: stores text embeddings, searches by similarity). It is not a subchart — it's an independent service referenced by URL.

OPTIONAL — the air-gap deployment runs WITHOUT Qdrant

The qdrant and memory_ingestion config blocks are optional (| None in the schema). When both are omitted, get_memory_ingestion_service() returns None and the memory feature is simply off — assistant-service starts and runs fine without Qdrant. (The on-prem production reference deploys this way.)

For air-gap bring-up: do NOT deploy Qdrant. Omit the […intelligence.qdrant] and […intelligence.memory_ingestion] TOML blocks, and drop QDRANT_SECRET / QDRANT_BASIC_PASSWORD from the secret seed. Only follow the steps below if you specifically want the long-term memory / mem0 feature.

What assistant-service expects (only when memory IS enabled)

From intelligence.qdrant in the config:

[<env>.intelligence.qdrant]
url = "https://qdrant.ask.mod.auh1.dev.dir"
basic_user = "qdrant"
api_key       = "@format {env[QDRANT_SECRET]}"          # Qdrant API key
basic_password = "@format {env[QDRANT_BASIC_PASSWORD]}"  # ingress basic-auth (optional)
[<env>.intelligence.memory_ingestion]
embedding_model_name = "BAAI/bge-m3"
embedding_model_dims = 1024
collection_name = "memories_bge_m3"
assistant-service creates the collection itself on first use — Qdrant just needs to be up, persistent, and authenticated.


Check whether it already exists

kubectl get pods,svc -A | grep -i qdrant
kubectl get application -n argocd | grep -i qdrant
Empty → build it (below).


Prerequisites

  • [x] Image mirrored: harbor.cl1.sq4.aegis.internal/dockerhub/qdrant/qdrant:v1.12.5 (added to scripts/harbor-mirror/mirror-config.yaml)
  • [x] Ceph RBD storage class (csi-rbd-sc) for the collection volume
  • [x] cert-manager internal-ca-issuer for the ingress TLS
  • [x] Vault key QDRANT_SECRET (API key) seeded; optional QDRANT_BASIC_PASSWORD

Manifests (ArgoCD app — ask namespace)

A StatefulSet is enough (single node; Qdrant clustering is optional). Add these under mod-ask-devops/applications/qdrant/ and wire an Application like the others.

1. Secret (API key) — via ESO from Vault:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: qdrant-secret, namespace: ask }
spec:
  refreshInterval: 1m
  secretStoreRef: { kind: ClusterSecretStore, name: vault-backend }
  target: { name: qdrant-secret, creationPolicy: Owner }
  data:
    - secretKey: api-key
      remoteRef: { key: ask/assistant-service/auh1-dev, property: QDRANT_SECRET }

2. StatefulSet + Service:

apiVersion: apps/v1
kind: StatefulSet
metadata: { name: qdrant, namespace: ask }
spec:
  serviceName: qdrant
  replicas: 1
  selector: { matchLabels: { app: qdrant } }
  template:
    metadata: { labels: { app: qdrant } }
    spec:
      nodeSelector: { node.kubernetes.io/role: ask71 }
      imagePullSecrets: [{ name: harbor-pull-secret }]
      containers:
        - name: qdrant
          image: harbor.cl1.sq4.aegis.internal/dockerhub/qdrant/qdrant:v1.12.5
          ports: [{ name: http, containerPort: 6333 }, { name: grpc, containerPort: 6334 }]
          env:
            - name: QDRANT__SERVICE__API_KEY
              valueFrom: { secretKeyRef: { name: qdrant-secret, key: api-key } }
          volumeMounts: [{ name: storage, mountPath: /qdrant/storage }]
          readinessProbe: { httpGet: { path: /readyz, port: http }, initialDelaySeconds: 10 }
          livenessProbe:  { httpGet: { path: /livez,  port: http }, initialDelaySeconds: 20 }
          resources:
            requests: { cpu: 500m, memory: 2Gi }
            limits:   { memory: 4Gi }
  volumeClaimTemplates:
    - metadata: { name: storage }
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: csi-rbd-sc
        resources: { requests: { storage: 20Gi } }
---
apiVersion: v1
kind: Service
metadata: { name: qdrant, namespace: ask }
spec:
  selector: { app: qdrant }
  ports: [{ name: http, port: 6333 }, { name: grpc, port: 6334 }]

3. Ingress (so url = https://qdrant.ask.mod.auh1.dev.dir resolves; API-key auth is in-app):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: qdrant
  namespace: ask
  annotations:
    cert-manager.io/cluster-issuer: internal-ca-issuer
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls: [{ secretName: qdrant-tls, hosts: [qdrant.ask.mod.auh1.dev.dir] }]
  rules:
    - host: qdrant.ask.mod.auh1.dev.dir
      http: { paths: [{ path: /, pathType: Prefix, backend: { service: { name: qdrant, port: { number: 6333 } } } }] }

In-cluster vs ingress URL

assistant-service runs in-cluster, so it can use http://qdrant.ask.svc.cluster.local:6333 directly (no TLS, no ingress) — simpler and avoids the internal-CA trust dance. Use the ingress only if you need external/admin access. Set intelligence.qdrant.url accordingly.


Validate

# pod ready
kubectl get pods -n ask -l app=qdrant
# API reachable with the key
kubectl exec -n ask qdrant-0 -- sh -c 'wget -qO- --header="api-key: $QDRANT__SERVICE__API_KEY" http://localhost:6333/collections'
Expect {"result":{"collections":[...]},"status":"ok"}. After assistant-service runs, the memories_bge_m3 collection appears here.


Production / air-gap notes

  • Single-node is fine for dev; for HA, Qdrant supports clustering (raft) — needs ≥3 replicas + a headless service.
  • Back up /qdrant/storage (snapshots API or volume snapshots) — it holds all embeddings.
  • The embedding model (BAAI/bge-m3, 1024-dim) must be served by Foundry/vLLM — Qdrant only stores the vectors; assistant-service computes them via Foundry. See Foundry.