Qdrant 1.15 asymmetric quantization e binary storage: 10x meno VRAM per stesso recall
Il 18 aprile 2026 ho migrato il vector database del mio chatbot RAG interno da uncompressed scalar a binary storage con asymmetric quantization, sfruttando le feature introdotte in Qdrant 1.15 (release luglio 2025, ormai stabile su production pattern). Il dataset: 180.247 embedding di 1.536 dimensioni (text-embedding-3-large OpenAI) su documentazione tecnica italiana, circa 260 MB di vettori raw prima della compressione. Container Qdrant 1.15.3 su un Hetzner CX32 (4 vCPU, 8 GB RAM, 80 GB NVMe) con docker-compose. Prima della migrazione: 1,6 GB di RAM residente per il vector index, P95 query latency 34ms, recall@10 0,942 sulla mia test suite di 500 query ground truth. Dopo la migrazione: 170 MB RAM residenti (riduzione 9,4x), P95 latency 41ms (+20% acceptable), recall@10 0,934 (calo di 0,8 punti percentuali).
Nove volte meno RAM con calo di accuracy sotto l'uno percento. Questo articolo ti mostra il setup completo passo per passo: docker-compose, configurazione collection, migrazione da uncompressed, benchmark e verifica recall. Se gestisci un RAG production con corpus fra 100K e 10M vettori e paghi RAM Hetzner o Scaleway ogni mese, questa è letteralmente moneta che smette di uscire dal tuo budget.
Perché asymmetric quantization e non binary pura
Il binary quantization classico esiste in Qdrant dal 2023: ogni dimensione di un vettore viene ridotta a 1 bit (sign quantization), ottenendo 32x di compressione. Il problema: per vettori con meno di 1.000 dimensioni, il calo di accuracy è significativo, spesso 5-15 punti percentuali su recall@10 su dataset realistici. Per 1.536 dim (OpenAI embedding standard) il problema è meno marcato ma comunque misurabile.
Qdrant 1.15 introduce tre novità che risolvono diversi angoli del problema:
Primo: 2-bit quantization (16x compressione invece di 32x) con gestione esplicita dei valori vicini allo zero, che binary pura comprimerebbe arbitrariamente perdendo segnale.
Secondo: 1,5-bit quantization (24x compressione), bilanciamento tra efficienza binary e accuracy del 2-bit.
Terzo: asymmetric quantization, il pattern che uso in produzione. Storage vettori in binary 1-bit (minimo footprint RAM/disk), ma query vettori in scalar 8-bit. Il rescoring usa la maggiore precisione della query per compensare la perdita dello storage compresso. Pattern ottimale quando il bottleneck è disk I/O o RAM, non CPU.
Per un RAG con 100K-10M vettori OpenAI 1.536-dim, asymmetric binary+scalar è il setup migliore al 2026. Sotto 100K vettori il risparmio RAM è trascurabile; sopra 10M conviene sharding cluster invece di ottimizzazione single-node.
Se gestisci infrastruttura vector DB in produzione per pipeline RAG aziendali e vuoi capire come dimensiono setup ottimali, nel mio hub dedicato all'AI per aziende trovo articoli con la methodology che uso con clienti reali.
Setup docker-compose production-grade
Parto da una configurazione Qdrant 1.15.3 production-ready:
services:
qdrant:
image: qdrant/qdrant:v1.15.3
container_name: qdrant-prod
restart: unless-stopped
ports:
# REST API esposto solo su loopback, la tua app si connette da localhost o private network
- "127.0.0.1:6333:6333"
# gRPC per client high-throughput (Python, Go)
- "127.0.0.1:6334:6334"
environment:
# API key obbligatoria anche in single-tenant per prevenire accessi non autorizzati
- QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY}
# Read-only mode per client che fanno solo query (non ingestion)
- QDRANT__SERVICE__READ_ONLY_API_KEY=${QDRANT_READONLY_KEY}
# Storage on-disk attivo: riduce RAM e sfrutta NVMe
- QDRANT__STORAGE__ON_DISK_PAYLOAD=true
# HNSW parameters tuned per dataset medi (100K-1M vettori)
- QDRANT__STORAGE__HNSW_INDEX__M=16
- QDRANT__STORAGE__HNSW_INDEX__EF_CONSTRUCT=100
volumes:
- ./qdrant_storage:/qdrant/storage
deploy:
resources:
limits:
memory: 4G
cpus: '2.0'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/readyz"]
interval: 30s
timeout: 5s
retries: 3Nota sulla memory limit: 4G è scelto in modo da avere margine oltre i 170 MB che la collezione quantizzata richiederà (ci sono overhead Qdrant core, cache OS, working memory per aggiornamenti). L'effettiva residente sotto carico è circa 450 MB su 4G limit. Se stai planificando multi-tenancy o multiple collezioni, scala proporzionalmente.
Creazione collection con binary storage e scalar query
Qdrant espone la configurazione quantizzazione nel payload di PUT /collections/{name}. Il pattern asymmetric:
<?php
declare(strict_types=1);
namespace App\Qdrant;
use GuzzleHttp\Client;
final class QdrantCollectionManager
{
public function __construct(
private readonly Client $http,
private readonly string $baseUrl,
private readonly string $apiKey,
) {
}
public function createAsymmetricCollection(string $collectionName, int $vectorDim = 1536): void
{
$payload = [
'vectors' => [
// Distanza cosine per embedding OpenAI standard
'size' => $vectorDim,
'distance' => 'Cosine',
'on_disk' => true,
],
'hnsw_config' => [
// Parametri HNSW bilanciati per dataset medi
'm' => 16,
'ef_construct' => 100,
'full_scan_threshold' => 10000,
],
'quantization_config' => [
'binary' => [
// Quantizzazione binary a 1 bit per massima compressione storage
'always_ram' => false,
'encoding' => 'one-bit',
// Query encoding asimmetrico: scalar 8-bit per precisione di rescoring
'query_encoding' => 'scalar8bits',
],
],
'optimizers_config' => [
'default_segment_number' => 2,
// Indicizzo quando il segment raggiunge 20K punti (default 10K troppo frequente)
'indexing_threshold' => 20000,
],
];
$this->http->put("{$this->baseUrl}/collections/{$collectionName}", [
'headers' => [
'api-key' => $this->apiKey,
'Content-Type' => 'application/json',
],
'json' => $payload,
]);
}
}Il campo chiave è quantization_config.binary.query_encoding: scalar8bits. Dichiara esplicitamente il mismatch tra storage (1-bit) e query (8-bit), che definisce il pattern asimmetrico. Senza questo, la query userebbe la stessa precisione dello storage, perdendo il vantaggio del rescoring.
Migrazione da collection uncompressed
Se hai già una collezione uncompressed con 100K+ vettori, la migrazione non è un in-place ALTER. Serve creare una nuova collezione, fare re-ingestion, swap degli alias. Lo script che ho usato:
<?php
declare(strict_types=1);
namespace App\Qdrant;
use GuzzleHttp\Client;
final class CollectionMigrator
{
private const BATCH_SIZE = 500;
public function __construct(
private readonly Client $http,
private readonly string $baseUrl,
private readonly string $apiKey,
) {
}
public function migrateToAsymmetric(string $sourceCollection, string $targetCollection): int
{
$totalMigrated = 0;
$offset = null;
do {
// Scroll API: itero sul source a batch di 500 punti
$response = $this->http->post("{$this->baseUrl}/collections/{$sourceCollection}/points/scroll", [
'headers' => $this->authHeaders(),
'json' => [
'limit' => self::BATCH_SIZE,
'offset' => $offset,
// Includo sia vettori che payload per preservare tutto
'with_vector' => true,
'with_payload' => true,
],
]);
$data = json_decode((string) $response->getBody(), true);
$points = $data['result']['points'];
$offset = $data['result']['next_page_offset'];
if (empty($points)) {
break;
}
// Upsert nel target con payload e vettori identici
$this->http->put("{$this->baseUrl}/collections/{$targetCollection}/points", [
'headers' => $this->authHeaders(),
'json' => [
'points' => array_map(static fn ($p) => [
'id' => $p['id'],
'vector' => $p['vector'],
'payload' => $p['payload'] ?? [],
], $points),
],
]);
$totalMigrated += count($points);
echo "Migrated {$totalMigrated} points so far\n";
} while ($offset !== null);
return $totalMigrated;
}
private function authHeaders(): array
{
return [
'api-key' => $this->apiKey,
'Content-Type' => 'application/json',
];
}
}Per i miei 180K vettori il tempo totale di migrazione è stato 22 minuti. Durante la migrazione il source resta servant (zero downtime sulle query production), poi ho flippato l'alias dalla collection vecchia alla nuova:
curl -X POST http://localhost:6333/collections/aliases \
-H "api-key: ${QDRANT_API_KEY}" \
-d '{"actions":[{"delete_alias":{"alias_name":"docs-prod"}}]}'
curl -X POST http://localhost:6333/collections/aliases \
-H "api-key: ${QDRANT_API_KEY}" \
-d '{"actions":[{"create_alias":{"collection_name":"docs-v2-binary","alias_name":"docs-prod"}}]}'L'applicazione continua a usare l'alias docs-prod, l'infra dietro cambia. Dopo 48 ore di verifica senza regressione, ho eliminato la collection vecchia recuperando gli 1,6 GB.
Benchmark: la misurazione da fare prima di prod
Il punto che la maggior parte dei tutorial tratta male. Prima di flippare in prod, esegui il benchmark di regressione recall sulla tua test suite reale, non sulla paper benchmark. Ho creato un set di 500 query ground truth (query dell'utente + top-k risultati attesi dal corpus di documentazione), replicato la stessa suite sulla collection pre e post quantization.
import asyncio
from qdrant_client import AsyncQdrantClient
import statistics
async def recall_at_k(client, collection, queries, k=10):
"""Calcola recall@k medio su un test set ground truth"""
recalls = []
latencies = []
for query_vec, expected_ids in queries:
import time
start = time.perf_counter()
results = await client.search(
collection_name=collection,
query_vector=query_vec,
limit=k,
)
elapsed_ms = (time.perf_counter() - start) * 1000
latencies.append(elapsed_ms)
retrieved_ids = {r.id for r in results}
true_positive = len(retrieved_ids & set(expected_ids))
recalls.append(true_positive / min(k, len(expected_ids)))
return {
"mean_recall": statistics.mean(recalls),
"p95_latency_ms": statistics.quantiles(latencies, n=20)[18],
"sample_size": len(queries),
}Il mio risultato di regressione:
| Metrica | Pre-quantization | Post asymmetric | Delta |
|---|---|---|---|
| Recall@10 medio | 0,942 | 0,934 | -0,8 pp |
| P95 latency | 34 ms | 41 ms | +21% |
| RAM residente | 1,6 GB | 170 MB | -89% |
| Disk footprint | 310 MB | 78 MB | -75% |
Il calo di recall di 0,8 punti percentuali è sotto la soglia di tolleranza (1%) che avevo fissato a priori con il product owner del chatbot. Il +21% di latency a P95 è assorbibile dato che il chatbot ha latency budget di 2-3 secondi totali end-to-end e il vector search contribuisce solo circa 40ms su quel totale.
Costi reali: il risparmio Hetzner su 12 mesi
Il motivo economico vero per fare questa migrazione è il costo di infrastruttura. Confronto prima e dopo quantization su hardware Hetzner equivalente.
Scenario pre-quantization (1,6 GB RAM richiesta):
- Hetzner CX42 (4 vCPU, 16 GB RAM, 160 GB SSD): 16,90 €/mese
- Totale annuo: 202,80 €
- Backup (20% del prezzo): 40,56 €/anno
- Totale pre: 243,36 €/anno per singola collection
Scenario post-quantization (170 MB richiesta, ma scelgo comunque CX32 per headroom):
- Hetzner CX32 (4 vCPU, 8 GB RAM, 80 GB SSD): 7,90 €/mese
- Totale annuo: 94,80 €
- Backup: 18,96 €/anno
- Totale post: 113,76 €/anno
Risparmio netto annuale: 130 €/collection. Per una PMI con 5-10 collection diverse (tipico di un RAG multi-tenant con un indice per cliente), il saving è 650-1.300 €/anno. Non è cifra enorme in assoluto, ma è puro margine liberato che si somma anno dopo anno. Se poi scali a 50-100 collection (SaaS multi-tenant AI per PMI), il risparmio cumulativo sfiora i 10.000 €/anno, che diventa significativo. E il tempo di migrazione per collection è 20-30 minuti più la verifica recall, quindi ROI immediato.
Il calcolo cambia se sei su provider US (AWS, GCP) dove la RAM è più cara. Su AWS m6i.large (4 vCPU, 8 GB) vs m6i.xlarge (4 vCPU, 16 GB) la differenza è circa 55 $/mese, ben più del saving Hetzner. Su deployment enterprise US, asymmetric quantization può valere 500-800 $/mese per collection.
Confronto con Weaviate e pgvector su questa specifica feature
A domanda inevitabile: perché Qdrant e non Weaviate o pgvector? Risposta breve sull'angolo specifico della quantization.
Weaviate 1.30 (marzo 2026) ha portato multi-vector ColBERT in GA e 1.31 aggiunge MUVERA per ridurre footprint, che è un approccio diverso al problema: invece di comprimere ogni vettore, usa molti vettori più piccoli e rappresentazione late-interaction. Vantaggio: accuracy superiore su retrieval complesso (vocabolario specialistico). Svantaggio: complessità operativa più alta e footprint disk maggiore.
pgvector 0.8.x ha scalar quantization (4-bit, 8-bit) ma non binary quantization né asymmetric mode. Per deployment dove Postgres è già nello stack e serve una soluzione "good enough" senza introdurre un nuovo database, pgvector è la scelta. Per ottimizzazione RAM aggressiva su vector DB dedicato, Qdrant resta avanti.
Milvus al 2026 ha un portfolio più ricco di quantization option (PQ, SQ8, SQ4, binary, asymmetric) ma richiede etcd + minio come dipendenze extra, che moltiplica la superficie operativa per deployment single-tenant PMI.
La scelta pragmatica: Qdrant per deployment medi single-node o small cluster; Milvus per deployment large cluster con team dedicato ops; pgvector se non vuoi aggiungere un servizio al tuo stack e il corpus è sotto 500K vettori; Weaviate se il tuo use case è query-complex con terminologia specialistica.
Considerazioni pratiche finali
Tre avvertenze operative dopo una settimana in produzione.
Prima: pin della versione Qdrant. Non usare latest tag Docker; pin alla versione specifica 1.15.3+. Le feature di asymmetric quantization sono stabili da 1.15.x in poi; nelle versioni più vecchie il flag esisteva ma era flagged experimental.
Seconda: rescoring configuration matters. Il parametro search_params.quantization.rescore: true abilita il rescoring con full-precision sui top-k del binary search. Default è on, ma alcuni template lo disabilitano per speed: verifica che sia on, altrimenti l'accuracy crolla.
Terza: HNSW va ri-tunato. Post-quantization, i parametri HNSW ottimali (M, ef_construct, ef_search) cambiano leggermente. Dopo la migrazione, fai grid search sui tre parametri per ri-ottimizzare il trade-off recall/latency. Io ho trovato M=16, ef=100 già ottimali, ma su dataset diversi potresti dover aumentare M a 32 per mantenere recall.
Nota finale sulla strategia di rollout. Se stai pianificando la migrazione su un ambiente già in produzione con traffico reale, il pattern che suggerisco è "blue-green" con alias Qdrant: crei la collection docs-v2-binary in parallelo a docs-prod, migri, esegui il benchmark di recall, esegui A/B live per 48-72 ore instradando una quota (10%, poi 50%) del traffico sulla nuova collection monitorando metriche di soddisfazione utente (task success rate del chatbot, CSAT, abandon rate). Se le metriche restano stabili, flip completo dell'alias. Se anche una sola metrica regredisce significativamente, rollback via alias switch immediato. Questo pattern richiede che il tuo orchestrator abbia logica di traffic splitting (può essere fatto in 30-40 righe in Laravel middleware o Symfony event listener); non è infrastruttura pesante, è disciplina di deployment.
Se hai RAG production su Qdrant con corpus medio-grande e stai pagando RAM che potresti liberare, o se vuoi un audit del tuo setup vector DB per identificare ottimizzazioni simili, il modulo di preventivo gratuito risponde in due minuti se il tuo scenario rientra nel mio perimetro. Sette domande, niente impegno.