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
| Component | Tool | Purpose |
|---|---|---|
| Embedding model | BGE-large-en-v1.5 | Convert text to 1024-dim vectors |
| Vector database | Qdrant | Store and query vectors |
| Reranker | BGE-reranker-v2 | Refine top-K results |
| API layer | FastAPI | Search 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