Qdrant: A Beginner's Tutorial to Vector Search
Welcome to this tutorial on Qdrant, an open-source vector database and vector similarity search engine. Qdrant helps you build the next generation of AI-powered applications by providing a powerful, production-ready service to store, search, and manage vector embeddings.
In this notebook, we will cover the fundamental concepts of Qdrant and walk through a practical example of building a simple semantic search engine. We'll explore:
- Setup and Installation: Getting your environment ready.
- Connecting to Qdrant: Initializing the Qdrant client.
- Creating Collections: Setting up a space for our vectors.
- Generating and Uploading Vectors: Preparing and storing our vector data.
- Performing Searches: Running semantic and filtered searches.
1. Setup and Installation
First, you'll need to install the necessary Python libraries. We'll use qdrant-client
to interact with Qdrant, sentence-transformers
to generate our vector embeddings, and python-dotenv
to manage our API keys and environment variables.
You can install them by running the following command in your terminal:
#pip install qdrant-client sentence-transformers python-dotenv -q
Managing Environment Variables
It's a best practice to manage sensitive information like API keys securely. We'll use a .env
file to store these credentials. Create a file named .env
in the same directory as this notebook and add the following lines:
QDRANT_API_KEY="your_qdrant_api_key_here"
QDRANT_URL="your_qdrant_url_here"
For this tutorial, you can easily set up a Qdrant cloud cluster and get the above env variables.
import os
from dotenv import load_dotenv
from qdrant_client import QdrantClient, models
from sentence_transformers import SentenceTransformer
# Load environment variables from .env file
load_dotenv()
# Get the API key and URL from environment variables
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
QDRANT_URL = os.getenv("QDRANT_URL")
2. Connecting to Qdrant
The QdrantClient
is our entry point to interacting with the Qdrant service. Qdrant can be run in different ways:
- In-memory: Perfect for quick experiments and testing, as all data is cleared when the process finishes.
- On-disk storage: A local, persistent storage option.
- Docker: Running Qdrant as a standalone server.
- Qdrant Cloud: A fully managed, scalable cloud solution.
For simplicity, we'll use the Qdrant cloud in this tutorial.
# Initialize the Qdrant client for in-memory storage
# client = QdrantClient(":memory:")
# If you were connecting to Qdrant Cloud, you would use:
client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
3. Creating a Collection
In Qdrant, a collection is a named set of points (vectors with a payload). Think of it as a table in a SQL database. When creating a collection, you need to define the configuration for the vectors it will store, such as their size (dimensionality) and the distance metric for similarity search.
Common distance metrics include:
- Cosine: Measures the angle between two vectors. Great for text embeddings.
- Euclidean: The straight-line distance between two points.
- Dot Product: A measure of similarity that also considers vector magnitude.
my_collection = "my_first_collection"
client.recreate_collection(
collection_name=my_collection,
vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE),
)
/var/folders/pv/g_b0j0n53rz5fm8yrlw3jg040000gn/T/ipykernel_48560/1209000986.py:3: DeprecationWarning: `recreate_collection` method is deprecated and will be removed in the future. Use `collection_exists` to check collection existence and `create_collection` instead. client.recreate_collection(
True
4. Generating and Uploading Vectors
To perform semantic search, we need to convert our text data into numerical representations called vector embeddings. We'll use a pre-trained model from sentence-transformers
for this task. The model all-MiniLM-L6-v2
is a good choice for its balance of speed and quality, and it outputs vectors of size 384, matching our collection's configuration.
# Load a pre-trained sentence transformer model
model = SentenceTransformer('all-MiniLM-L6-v2')
# Let's create some sample documents
documents = [
{"id": 1, "text": "Qdrant is a vector database for building AI applications.", "metadata": {"type": "tech"}},
{"id": 2, "text": "The Eiffel Tower is a famous landmark in Paris, France.", "metadata": {"type": "travel"}},
{"id": 3, "text": "A vector database indexes vectors for easy search and retrieval.", "metadata": {"type": "tech"}},
{"id": 4, "text": "The Great Wall of China is one of the world's wonders.", "metadata": {"type": "travel"}},
{"id": 5, "text": "Artificial intelligence is transforming many industries.", "metadata": {"type": "tech"}}
]
# Generate embeddings for our documents
embeddings = model.encode([doc["text"] for doc in documents])
/Users/devon/.pyenv/versions/3.10.14/lib/python3.10/site-packages/torch/nn/modules/module.py:1520: FutureWarning: `encoder_attention_mask` is deprecated and will be removed in version 4.55.0 for `BertSdpaSelfAttention.forward`. return forward_call(*args, **kwargs)
Upserting Points
Now that we have our embeddings, we'll upload them to our Qdrant collection. In Qdrant, a point is the central entity, consisting of a vector, a unique ID, and an optional payload (a JSON object for metadata). We use the upsert
operation, which will add new points or update existing ones with the same ID.
client.upsert(
collection_name=my_collection,
points=[
models.PointStruct(
id=doc["id"],
vector=embedding.tolist(),
payload=doc["metadata"]
)
for doc, embedding in zip(documents, embeddings)
],
wait=True,
)
UpdateResult(operation_id=0, status=<UpdateStatus.COMPLETED: 'completed'>)
5. Performing Searches
Semantic Search
The core functionality of a vector database is finding the most similar vectors to a given query vector. This is semantic search. We'll take a query, encode it into a vector using the same model, and then use Qdrant to find the closest matches.
query = "What is a vector database?"
query_vector = model.encode(query).tolist()
search_results = client.query_points(
collection_name=my_collection,
query=query_vector,
limit=2, # Return the top 2 most similar results
with_payload=True,
)
# Map IDs to original document text so we can display it with results
id_to_text = {doc["id"]: doc["text"] for doc in documents}
print("Semantic Search Results:")
for result in search_results.points:
print(f"- ID: {result.id}, Score: {result.score:.4f}")
text = id_to_text.get(result.id)
if text is not None:
print(f" Text: {text}")
Semantic Search Results: - ID: 3, Score: 0.6311 Text: A vector database indexes vectors for easy search and retrieval. - ID: 1, Score: 0.5301 Text: Qdrant is a vector database for building AI applications.
Search with a Filter
Qdrant's real power comes from its ability to combine vector search with filtering on the payload. This allows you to narrow down your search to only the points that match certain metadata criteria. Here, we'll search for the same query but only within documents of the "travel" type.
# Create a payload index for the string field 'type' so we can filter on it
# Qdrant requires an index of type KEYWORD for exact-match string filters
try:
client.create_payload_index(
collection_name=my_collection,
field_name="type",
field_schema=models.PayloadSchemaType.KEYWORD,
)
print("Created payload index for 'type' (KEYWORD).")
except Exception as e:
# Safe to ignore if already exists and we're re-running cells
if "already exists" in str(e).lower():
print("Payload index for 'type' already exists.")
else:
raise
Created payload index for 'type' (KEYWORD).
filtered_search_results = client.query_points(
collection_name=my_collection,
query=query_vector,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="type",
match=models.MatchValue(value="travel")
)
]
),
limit=2,
with_payload=True,
)
print("Filtered Search Results (type='travel'):")
for result in filtered_search_results.points:
print(f"- ID: {result.id}, Score: {result.score:.4f}, Payload: {result.payload}")
text = id_to_text.get(result.id)
if text is not None:
print(f" Text: {text}")
Filtered Search Results (type='travel'): - ID: 2, Score: 0.0032, Payload: {'type': 'travel'} Text: The Eiffel Tower is a famous landmark in Paris, France. - ID: 4, Score: -0.0419, Payload: {'type': 'travel'} Text: The Great Wall of China is one of the world's wonders.
Conclusion
Congratulations! You've successfully built a small semantic search engine using Qdrant. You've learned how to:
- Set up your environment and connect to Qdrant.
- Create collections with specific vector configurations.
- Generate vector embeddings from text data.
- Upsert points with vectors and metadata payloads.
- Perform both semantic and filtered searches.
This is just the beginning. From here, you can explore more advanced topics like:
- Hybrid Search: Combining keyword-based (sparse) and semantic (dense) vectors for more accurate results.
- Scalability: Using Docker or Qdrant Cloud for larger, production-level applications.
- Advanced Filtering: Creating more complex filtering conditions.
Happy building!