RTX 3050 - Order Now
Home / Blog / Tutorials / AI Search: Embeddings + Qdrant
Tutorials

AI Search: Embeddings + Qdrant

Build a semantic search engine using GPU-accelerated embeddings and Qdrant vector database that returns results by meaning rather than keywords.

You will build a semantic search system that understands meaning, not just keywords. Users type natural language queries and get results ranked by conceptual relevance. A technical documentation site with 15,000 pages sees search success rates jump from 34% (keyword search) to 78% (semantic search) after switching to this pipeline. The embedding model runs on a dedicated GPU server alongside Qdrant for sub-50ms query latency.

Search Architecture

ComponentToolPurpose
Embedding modelBGE-large-en-v1.5Convert text to 1024-dim vectors
Vector databaseQdrantStore and query vectors
RerankerBGE-reranker-v2Refine top-K results
API layerFastAPISearch endpoint

GPU-Accelerated Embeddings

from sentence_transformers import SentenceTransformer
import torch

model = SentenceTransformer("BAAI/bge-large-en-v1.5", device="cuda")

def embed_documents(texts: list[str], batch_size: int = 64) -> list:
    """Embed documents with instruction prefix for retrieval."""
    prefixed = [f"Represent this document for retrieval: {t}" for t in texts]
    embeddings = model.encode(prefixed, batch_size=batch_size,
                              show_progress_bar=True,
                              normalize_embeddings=True)
    return embeddings.tolist()

def embed_query(query: str) -> list:
    """Embed query with query-specific prefix."""
    prefixed = f"Represent this sentence for searching relevant passages: {query}"
    embedding = model.encode(prefixed, normalize_embeddings=True)
    return embedding.tolist()

BGE-large produces 1024-dimensional embeddings optimised for retrieval. The instruction prefixes differentiate document embeddings from query embeddings, improving search accuracy by 8-12%.

Qdrant Setup and Indexing

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct

client = QdrantClient(host="localhost", port=6333)

# Create collection
client.create_collection(
    collection_name="docs",
    vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
    optimizers_config={"indexing_threshold": 20000}
)

def index_documents(documents: list[dict]):
    """Index documents with metadata."""
    texts = [d["content"] for d in documents]
    embeddings = embed_documents(texts)
    points = [
        PointStruct(id=i, vector=emb,
                    payload={"title": d["title"], "url": d["url"],
                             "category": d["category"],
                             "content": d["content"][:500]})
        for i, (emb, d) in enumerate(zip(embeddings, documents))
    ]
    client.upsert(collection_name="docs", points=points, batch_size=256)

Qdrant uses HNSW indexing for approximate nearest neighbour search. With 15,000 documents, queries complete in 5-15ms. For larger collections, adjust the HNSW parameters (m=16, ef_construct=200) to balance speed and recall.

Two-Stage Retrieval with Reranking

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3", device="cuda")

def search(query: str, top_k: int = 10, rerank_top: int = 50) -> list:
    query_vec = embed_query(query)
    # Stage 1: vector search (fast, broad recall)
    results = client.search(
        collection_name="docs", query_vector=query_vec,
        limit=rerank_top, score_threshold=0.5
    )
    # Stage 2: cross-encoder reranking (slow, precise)
    pairs = [(query, r.payload["content"]) for r in results]
    scores = reranker.predict(pairs)
    ranked = sorted(zip(results, scores), key=lambda x: x[1], reverse=True)
    return [{"title": r.payload["title"], "url": r.payload["url"],
             "score": float(s), "snippet": r.payload["content"]}
            for r, s in ranked[:top_k]]

The two-stage approach retrieves 50 candidates by vector similarity, then reranks them with a cross-encoder for precision. This catches results that vector search alone might rank lower due to vocabulary differences.

FastAPI Search Endpoint

from fastapi import FastAPI

app = FastAPI()

@app.get("/search")
async def search_endpoint(q: str, limit: int = 10):
    results = search(q, top_k=limit)
    return {"query": q, "results": results, "count": len(results)}

The endpoint supports filtering by category and date range using Qdrant’s payload filters, enabling faceted search without separate infrastructure.

Scaling and Production

An RTX 5090 (24 GB) handles both the embedding model and reranker simultaneously, processing 500+ queries per minute. For initial bulk indexing of 15,000 documents, GPU embedding completes in under 10 minutes compared to 3+ hours on CPU. Deploy on private infrastructure for sensitive document collections. Incremental indexing (new documents only) runs as a background job. See vLLM hosting to add RAG chatbot capabilities on top of the search index, GDPR compliance for personal data in searchable documents, more tutorials, and use cases.

AI Search GPU Servers

Run embedding models and vector search on dedicated UK GPU infrastructure with sub-50ms query latency.

Browse GPU Servers

Need a Dedicated GPU Server?

Deploy from RTX 3050 to RTX 5090. Full root access, NVMe storage, 1Gbps — UK datacenter.

Browse GPU Servers

gigagpu

We benchmark, deploy, and optimise GPU infrastructure for AI workloads. All data in our guides comes from real-world testing on our UK-based dedicated GPU servers.

Ready to deploy your AI workload?

Dedicated GPU servers from our UK datacenter. NVMe storage, 1Gbps networking, full root access.

Browse GPU Servers Contact Sales

Have a question? Need help?