Version: Latest

Integrate Qdrant with Enterprise Search Policy

This guide shows how to configure Rasa to use Qdrant as a vector store for the Enterprise Search Policy.
Setup for Qdrant will be done locally using Docker or Podman.

Setting Up Qdrant

You can set up Qdrant using either Docker Desktop or Podman. Choose the method that best suits your environment.

Setting Up Qdrant with Docker Desktop

Follow these instructions to install Docker Desktop.

Use this Docker Compose File for Qdrant

Create a file named docker-compose.yml:

services:
qdrant:
image: qdrant/qdrant:v1.0.0
container_name: qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- ./qdrant_storage:/qdrant/storage
environment:
QDRANT__SERVICE__HOST: "0.0.0.0"
QDRANT__SERVICE__PORT: 6333
QDRANT__SERVICE__GRPC_PORT: 6334
QDRANT__STORAGE__PATH: "/qdrant/storage"

Ingesting Documents into Qdrant

You can ingest documents into Qdrant using a custom script. Below, we provide options for LangChain-based ingestion and a lightweight LiteLLM-based ingestion.

This script:

  • Extracts documents from a folder.
  • Splits documents into chunks with overlap for better retrieval.
  • Embeds these chunks using OpenAI embeddings.
  • Stores the resulting vectors in Qdrant.

Ingesting Documents with LangChain

Script: ingest.py

import logging
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Qdrant
from langchain_community.document_loaders import DirectoryLoader, UnstructuredFileLoader
from pathlib import Path
from langchain.schema import Document
from langchain.schema.embeddings import Embeddings
from typing import List
import yaml
from argparse import ArgumentParser
import os
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
DEFAULT_COLLECTION_NAME = "rasa"
def extract_documents(docs_folder: str) -> List[Document]:
"""Extract documents from a given folder.
Args:
docs_folder: The folder containing the documents.
Returns:
the list of documents
"""
logger.debug(f"Extracting files from: {Path(docs_folder).absolute()}")
# check if directory exists
if not Path(docs_folder).exists():
raise SystemExit(f"Directory '{docs_folder}' does not exist.")
# change the loader_cls for a different document type or extension
# https://python.langchain.com/docs/modules/data_connection/document_loaders/file_directory
loader = DirectoryLoader(
docs_folder, loader_cls=UnstructuredFileLoader, show_progress=True
)
return loader.load()
def create_chunks(documents: List[Document], chunk_size: int, chunk_overlap: int) -> List[Document]:
"""Splits the documents into chunks with RecursiveCharacterTextSplitter.
Args:
documents: The documents to split.
chunk_size: The size of the chunks.
chunk_overlap: The overlap of the chunks.
Returns:
The list of chunks.
"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
return text_splitter.split_documents(documents)
def embeddings_factory(type: str, config: dict = None):
if type.lower() == "openai":
return OpenAIEmbeddings(**config)
def create_qdrant_collection(
embeddings: Embeddings,
docs: List[Document],
connection_args: dict,
) -> None:
"""Creates a Qdrant collection from the documents.
Args:
embeddings: embeddings model object
docs: The documents to store as a List of document chunks
connection_args: The connection arguments.
Returns:
The Qdrant collection.
"""
host = connection_args.get("host", None)
port = connection_args.get("port", 6333)
path = connection_args.get("path", None)
collection_name = connection_args.get("collection", DEFAULT_COLLECTION_NAME)
return Qdrant.from_documents(
docs,
embeddings,
host=host,
port=port,
collection_name=collection_name,
path=path,
)
def validate_destination(destination: str):
if destination.lower() not in ["qdrant"]:
raise SystemExit(f"Destination '{destination}' not supported.")
def validate_embeddings_type(embeddings_type: str):
if embeddings_type.lower() not in ["openai"]:
raise SystemExit(f"Embeddings type '{embeddings_type}' not supported.")
elif embeddings_type.lower() == "openai":
# check if OPENAI_API_KEY is set
if not "OPENAI_API_KEY" in os.environ:
raise SystemExit("OPENAI_API_KEY environment variable not set.")
def main():
parser = ArgumentParser(
prog="ingest.py",
description="Extract documents from a folder and load them into a vector store.",
epilog="Example: python ingest.py --config config.yaml",
)
parser.add_argument('-c', '--config', required=True, help='config file path')
args = parser.parse_args()
opt = yaml.load(open(args.config), Loader=yaml.FullLoader)
opt.update(vars(args))
# parse config file
docs_folder = opt.get("docs_folder", "data/documents")
chunk_size = opt.get("chunk_size", 1000)
chunk_overlap = opt.get("chunk_overlap", 20)
embeddings_type = opt.get("embeddings", "openai")
destination = opt.get("destination")
try:
openai_config = opt["openai_config"]
except KeyError:
raise SystemExit("config not found in config file.")
# do some validation
try:
connection_args = opt["connection_args"]
except KeyError:
raise SystemExit("connection_args not found in config file.")
validate_destination(destination)
validate_embeddings_type(embeddings_type)
# extract documents
docs = extract_documents(docs_folder)
logger.info(f"{len(docs)} documents extracted.")
# create chunks
chunks = create_chunks(docs, chunk_size, chunk_overlap)
logger.info(f"{len(chunks)} chunks created.")
for i, chunk in enumerate(chunks[:3]):
logger.info(f"chunk {i}")
logger.info(chunk)
# create embeddings
embeddings = embeddings_factory(embeddings_type, openai_config)
# create qdrant collection
if destination.lower() == "qdrant":
create_qdrant_collection(
embeddings=embeddings,
docs=chunks,
connection_args=connection_args,
)
logger.info(f"Qdrant collection created with arguments {connection_args}")
else:
raise SystemExit(f"Destination '{destination}' not supported. Only qdrant is supported.")
if __name__ == "__main__":
main()

Run the Ingestion Script

Below is an example qdrant_config.yml:

docs_folder: "/path/to/docs" # replace with your path to docs file
embeddings: "openai"
chunk_size: 1000
chunk_overlap: 100
openai_config:
model: "text-embedding-ada-002" # try different models if desired
destination: "qdrant"
connection_args:
host: "localhost"
port: 6333
collection: "rasa"

Run the ingestion:

python ingest.py --config qdrant_config.yml

Re-Ingesting Documents

If you want to re-ingest documents (for example, after adjusting the ingestion script or changing your documents):

  • Stop the Rasa server if it is running.
  • Delete the existing Qdrant collection:
curl -X DELETE "http://localhost:6333/collections/rasa"
  • Re-run the ingestion script:

Integrating Qdrant with Rasa

For more details, refer to the Rasa Pro documentation on the Enterprise Search Policy

Configuring config.yml

policies:
- name: EnterpriseSearchPolicy
llm:
model_name: gpt-4o
request_timeout: 20
temperature: 0
max_tokens: 800

Configuring endpoints.yml

vector_store:
type: qdrant
host: "localhost"
port: 6333
collection: "rasa"
prefer_grpc: False
content_payload_key: page_content
metadata_payload_key: metadata

Summary

By following this guide, you have:

  • Set up Qdrant locally using Docker Desktop or Podman.
  • Ingested documents into Qdrant using LangChain or LiteLLM.
  • Integrated Qdrant with Rasa’s EnterpriseSearchPolicy to enhance contextual relevance.

Feel free to explore alternative embeddings, distance metrics, or chunking strategies to further refine your bot’s retrieval performance.