Best Techniques to Retrieve Data from Vector Database in 1-2 Seconds

Why Vector Database Speed Matters

When building AI-powered applications — RAG pipelines, semantic search, recommendation engines or AI chatbots — the vector database is often the bottleneck. A user waiting more than 2 seconds for a response will abandon the interaction. Achieving sub-second retrieval from a vector store requires understanding how these databases work and applying the right optimisation techniques.

This guide covers battle-tested methods used in production systems to keep vector search fast regardless of whether you are using FAISS, ChromaDB, Pinecone, Weaviate, Qdrant or pgvector.


1. Choose the Right Index Type (HNSW vs IVF vs Flat)

The index structure is the single biggest factor in retrieval speed. The wrong index will make even small datasets slow.

HNSW (Hierarchical Navigable Small World)

HNSW is the gold standard for production vector search. It builds a multi-layer graph where each node connects to its nearest neighbours. Search traverses from the top layer downward, narrowing candidates at each level.

  • Query time: O(log n) — extremely fast even at millions of vectors
  • Recall: 95-99% with default parameters
  • Memory: Higher than IVF — plan for ~100 bytes per vector
  • Best for: Production APIs, real-time search, RAG pipelines
# FAISS HNSW index
import faiss
import numpy as np

d = 1536  # OpenAI embedding dimension
M = 32    # connections per node — higher = better recall, more memory

index = faiss.IndexHNSWFlat(d, M)
index.hnsw.efSearch = 64   # search time accuracy — tune per use case
index.hnsw.efConstruction = 200  # build time quality

IVF (Inverted File Index)

IVF clusters vectors into buckets (Voronoi cells) using k-means. At query time, only the nearest nprobe clusters are searched.

  • Query time: Much faster than flat search — sub-linear
  • Memory: Lower than HNSW
  • Key parameter: nprobe — higher = better recall but slower. Start at 10-20 for 1M vectors
# IVF + PQ (best memory/speed ratio)
nlist = 1000  # number of clusters — rule of thumb: sqrt(n_vectors)
m = 8         # sub-quantizer count for PQ
bits = 8      # bits per sub-quantizer

quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, bits)
index.nprobe = 20  # search 20 clusters out of 1000

2. Use Approximate Nearest Neighbour (ANN) Instead of Exact Search

Exact nearest neighbour search (brute force) checks every vector — O(n) complexity. At 100k+ vectors this becomes too slow for real-time use. ANN algorithms trade a tiny accuracy loss (1-5%) for 100x speed gains.

AlgorithmSpeedRecall@10MemoryBest Use
HNSWVery Fast98%+HighProduction APIs
IVF-PQFast90-95%LowLarge datasets, memory constrained
ScaNNFastest95%+MediumGoogle-scale search
Flat (exact)Slow100%MediumSmall datasets, benchmarks only

3. Apply Vector Quantization to Compress Embeddings

OpenAI text-embedding-3-large produces 3072-dimensional float32 vectors — that is 12KB per vector. At 1 million documents you need 12GB just for the vectors. Quantization compresses this dramatically.

Product Quantization (PQ)

PQ splits each vector into m sub-vectors and encodes each with a codebook. A 1536-dim float32 vector (6KB) compressed to 64 bytes — a 96x reduction with minimal recall loss.

Scalar Quantization (SQ)

Simpler than PQ — converts float32 (4 bytes) to int8 (1 byte) per dimension. 4x memory reduction with very high recall preservation (99%+).

# ChromaDB — uses HNSW automatically, configure via settings
import chromadb
from chromadb.config import Settings

client = chromadb.Client(Settings(
    chroma_db_impl="duckdb+parquet",
    persist_directory="./chroma_db",
    anonymized_telemetry=False
))

collection = client.get_or_create_collection(
    name="documents",
    metadata={
        "hnsw:space": "cosine",       # cosine similarity for text
        "hnsw:M": 32,                  # connections per node
        "hnsw:ef_construction": 200,   # build quality
        "hnsw:ef": 100,                # query accuracy
    }
)

4. Implement a Multi-Layer Caching Strategy

The fastest query is one that never hits the vector database. A well-designed cache can serve 60-80% of production traffic from memory.

Layer 1 — Exact Query Cache (Redis)

Cache the embedding vector and results for queries you have seen before. Identical questions from different users (e.g. "What is RAG?") hit the cache instantly.

import redis
import hashlib
import json
import numpy as np

redis_client = redis.Redis(host='localhost', port=6379, db=0)

def cached_vector_search(query_text: str, collection, top_k: int = 5):
    # Create cache key from query text
    cache_key = f"vsearch:{hashlib.md5(query_text.encode()).hexdigest()}:{top_k}"

    # Check cache first
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)  # return in ~0.1ms

    # Cache miss — do vector search
    results = collection.query(query_texts=[query_text], n_results=top_k)

    # Cache for 1 hour
    redis_client.setex(cache_key, 3600, json.dumps(results))
    return results

Layer 2 — Embedding Cache

Embedding generation (calling OpenAI API) typically takes 200-400ms. Cache embeddings separately so re-queries of the same text skip the API call entirely.

def get_cached_embedding(text: str, model="text-embedding-3-small"):
    cache_key = f"emb:{hashlib.md5(text.encode()).hexdigest()}"
    cached = redis_client.get(cache_key)
    if cached:
        return np.frombuffer(cached, dtype=np.float32)

    # Generate embedding
    import openai
    response = openai.embeddings.create(input=text, model=model)
    embedding = np.array(response.data[0].embedding, dtype=np.float32)

    # Cache for 24 hours — embeddings are deterministic
    redis_client.setex(cache_key, 86400, embedding.tobytes())
    return embedding

5. Use Hybrid Search (Vector + BM25 Keyword)

Pure vector search misses exact keyword matches. Pure keyword search misses semantic meaning. Hybrid search combines both and is consistently faster because the BM25 stage pre-filters candidates before the vector comparison.

# Weaviate hybrid search — combines BM25 + vector in one query
import weaviate

client = weaviate.Client("http://localhost:8080")

result = (
    client.query
    .get("Document", ["content", "title", "_additional {score}"])
    .with_hybrid(
        query="how to optimize vector search",
        alpha=0.75,    # 0 = pure BM25, 1 = pure vector, 0.75 = vector-heavy
        fusion_type=weaviate.gql.get.HybridFusion.RELATIVE_SCORE
    )
    .with_limit(5)
    .do()
)

The alpha parameter controls the blend. For technical documentation, 0.7-0.8 favours vector similarity. For product search, 0.4-0.6 gives better keyword precision.


6. Pre-filter with Metadata Before Vector Search

Searching 10,000 vectors is 100x faster than searching 1,000,000. Use metadata filters to narrow the candidate set before running ANN.

# ChromaDB — filter by metadata first
results = collection.query(
    query_texts=["explain HNSW indexing"],
    n_results=5,
    where={                          # pre-filter — reduces search space
        "$and": [
            {"category": {"$eq": "ai"}},
            {"year": {"$gte": 2023}}
        ]
    },
    include=["documents", "distances", "metadatas"]
)

# Qdrant — highly optimised filtered search
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue

client = QdrantClient("localhost", port=6333)
results = client.search(
    collection_name="knowledge_base",
    query_vector=query_embedding,
    query_filter=Filter(
        must=[FieldCondition(key="category", match=MatchValue(value="ai"))]
    ),
    limit=5
)

7. Batch Embeddings and Use Smaller Models

OpenAI text-embedding-3-large (3072 dims) is overkill for most RAG use cases. text-embedding-3-small (1536 dims) provides 95%+ of the quality at half the storage and faster search.

ModelDimensionsMTEB ScoreCostSpeed
text-embedding-3-large307264.6$0.13/1M tokensSlow
text-embedding-3-small153662.3$0.02/1M tokensFast
BAAI/bge-small-en-v1.538462.0Free (local)Very Fast
all-MiniLM-L6-v238456.3Free (local)Fastest

For self-hosted production systems, BAAI/bge-small-en-v1.5 offers the best balance of quality, speed and cost — it runs in 50ms on CPU.


8. Async Architecture — Never Block the Main Thread

Vector search should never block your API response. Use async workers to pre-compute results and stream partial responses.

# FastAPI async vector search
from fastapi import FastAPI
from contextlib import asynccontextmanager
import asyncio

app = FastAPI()

@app.get("/search")
async def search(query: str, top_k: int = 5):
    # Run blocking vector search in thread pool
    loop = asyncio.get_event_loop()
    results = await loop.run_in_executor(
        None,                          # default thread pool
        lambda: vector_db.query(       # blocking call in separate thread
            query_texts=[query],
            n_results=top_k
        )
    )
    return {"results": results, "query": query}

Real-World Performance Benchmarks

TechniqueDataset SizeQuery TimeRecall@10
Flat (brute force)100k vectors450ms100%
IVF-Flat (nprobe=20)100k vectors45ms97%
HNSW (M=32, ef=64)100k vectors8ms98%
HNSW + Redis cache100k vectors0.5ms98%
HNSW (M=32, ef=64)1M vectors25ms97%
IVF-PQ (nprobe=50)1M vectors30ms92%

Production Checklist for Sub-Second Vector Search

  • Use HNSW index — tune M=32, efSearch=64 as baseline
  • Cache embeddings in Redis with 24h TTL
  • Cache query results in Redis with 1h TTL
  • Apply metadata pre-filters to reduce search space
  • Use text-embedding-3-small or bge-small for lower latency
  • Run vector search in async thread pool — never block main thread
  • Consider hybrid BM25 + vector for better precision
  • Monitor p95/p99 latency — not just average
  • Use IVF-PQ if memory is constrained at scale
  • Benchmark with ann-benchmarks.com for your specific dataset

Conclusion

Achieving 1-2 second end-to-end response times in a RAG application is very achievable with the right techniques. The typical production stack looks like this:

  1. Check Redis cache — return in 0.5ms if hit
  2. HNSW ANN search — 10-25ms for up to 1M vectors
  3. LLM generation — 500-1500ms (the actual bottleneck)

Vector retrieval itself should never be your bottleneck. With HNSW indexing and a Redis cache layer, retrieval stays under 30ms even at millions of documents — leaving your latency budget for the LLM where it matters.

Ready to Test Your Knowledge?

Put your skills to the test with our comprehensive quiz platform

Feedback