# Build and host a Qdrant vector database
URL: https://docs.datarobot.com/en/docs/agentic-ai/genai-code/qdrantdb-vdb.html

# Build and host a Qdrant vector database

This page provides an example of how you can build, validate, and register a vector database to the DataRobot application using DataRobot's Python API client. It describes how to load and host a Qdrant vector store with metadata filtering as part of a custom model. This page is designed for use within a DataRobot [codespace](https://docs.datarobot.com/en/docs/workbench/wb-notebook/codespaces/index.html). DataRobot recommends downloading this notebook and uploading it for use in the application.

## Setup

The following steps outline the necessary configuration to integrate vector databases with the DataRobot platform.

1. This workflow uses the following feature flags. Contact your DataRobot representative or administrator for information on enabling these features.
2. Use a codespace, not a DataRobot Notebook, to ensure this notebook has access to a filesystem.
3. Usepip installto install the packages outlined in the following section if they are not already in the codespace's environment image.
4. Set the notebook session timeout to 180 minutes.
5. Restart the notebook container using at least a "Medium" (16GB RAM) instance.

### Install libraries

Install the following libraries:

```
!pip install "langchain-community==0.4.1" \
             "langchain_text_splitters==1.0.0" \
             "qdrant-client==1.16.1" \
             "sentence-transformers==5.1.2" \
             "datarobotx==0.2.0" \
             "cloudpickle==2.2.1"
```

```
import datarobot as dr
import datarobotx as drx
from datarobot.models.genai.vector_database import CustomModelVectorDatabaseValidation
from datarobot.models.genai.vector_database import VectorDatabase
import os
from pathlib import Path
from qdrant_client import QdrantClient, models
from sentence_transformers import SentenceTransformer
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
import json
import requests
import zipfile
import io
import re
```

### Connect to DataRobot

Read more about options for [connecting to DataRobot from the Python client](https://docs.datarobot.com/en/docs/api/api-quickstart/api-qs.html).

### Download sample data

This example references a sample dataset made from the DataRobot english documentation. Feel free to use your own data here.

Note: If you are a self-managed user, you must modify code samples that reference `app.datarobot.com` to the appropriate URL for your instance.

```
# Configuration
QDRANT_DATA_PATH = "qdrant"
EMBEDDING_MODEL_NAME = "all-MiniLM-L6-v2"
COLLECTION_NAME = "my_documents"
SOURCE_DOCUMENTS_ZIP_URL = "https://s3.amazonaws.com/datarobot_public_datasets/ai_accelerators/datarobot_english_documentation_5th_December.zip"
UNZIPPED_DOCS_DIR = "datarobot_english_documentation"
```

```
# Verify docs exist or download them
doc_path = Path(UNZIPPED_DOCS_DIR)
if doc_path.exists():
    txt_files = list(doc_path.rglob("*.txt"))
    print(f"✓ Found {len(txt_files)} .txt files in {UNZIPPED_DOCS_DIR}/")
    if txt_files:
        print(f"  Sample files:")
        for f in txt_files[:5]:
            print(f"    - {f}")
else:
    # Download some example docs
    r = requests.get(SOURCE_DOCUMENTS_ZIP_URL)
    z = zipfile.ZipFile(io.BytesIO(r.content))
    z.extractall()
```

## Create a vector database from documents

Use the following cell to build a vector database from the DataRobot documentation dataset. Note that this notebook uses Qdrant, an open source vector database with metadata filtering support. Additionally, this notebook uses the HuggingFace `all-MiniLM-L6-v2` [embeddings model](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) (also open source).

```
# Create vectorDB
def create_database():
    """Create and populate the Qdrant database"""
    
    # Initialize encoder
    encoder = SentenceTransformer(EMBEDDING_MODEL_NAME)
    
    # Initialize Qdrant client
    client = QdrantClient(path=QDRANT_DATA_PATH)
    
    try:
        # Create collection
        print("Creating collection...")
        client.create_collection(
            collection_name=COLLECTION_NAME,
            vectors_config=models.VectorParams(
                size=encoder.get_sentence_embedding_dimension(),
                distance=models.Distance.COSINE,
            ),
        )

        # Load text files
        print(f"Loading documents from {UNZIPPED_DOCS_DIR}/...")
        docs = []
        doc_path = Path(UNZIPPED_DOCS_DIR)
        
        for file_path in doc_path.rglob("*.txt"):
            loader = TextLoader(str(file_path))
            loaded_docs = loader.load()
            docs.extend(loaded_docs)

        print(f"Loaded {len(docs)} documents")

        # Split documents into chunks
        splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)
        split_docs = splitter.split_documents(docs)
        print(f"Split into {len(split_docs)} chunks")
        
        # Process metadata
        for doc in split_docs:
            # Convert file path to documentation URL
            doc.metadata['source'] = re.sub(
                r'datarobot_english_documentation/datarobot_docs/en/(.+)\.(txt|md)',
                r'https://docs.datarobot.com/en/docs/\1.html',
                doc.metadata.get('source', '')
            )
            # Extract category from source
            doc.metadata["category"] = doc.metadata["source"].split("/")[-1].split(".")[0]

        # Batch encode all documents
        print("Encoding all documents (this may take a few minutes)...")
        all_contents = [doc.page_content for doc in split_docs]
        all_vectors = encoder.encode(
            all_contents, 
            show_progress_bar=False, 
            batch_size=32,
            convert_to_numpy=True
        )
        
        # Create points
        print("Creating point structures...")
        points_to_upload = [
            models.PointStruct(
                id=idx,
                vector=all_vectors[idx].tolist(),
                payload={
                    "content": doc.page_content,
                    "source": doc.metadata.get("source", ""),
                    "category": doc.metadata.get("category", ""),
                    **doc.metadata
                }
            )
            for idx, doc in enumerate(split_docs)
        ]

        # Upload to Qdrant
        print(f"Uploading {len(points_to_upload)} points to Qdrant...")
        client.upload_points(
            collection_name=COLLECTION_NAME,
            points=points_to_upload
        )

        # Verify
        collection_info = client.get_collection(collection_name=COLLECTION_NAME)
        print(f"✓ Collection '{COLLECTION_NAME}' created with {collection_info.points_count} points")
        print(f"✓ Data saved to: {QDRANT_DATA_PATH}/")

    finally:
        # ALWAYS close the client
        client.close()
        print("✓ Database client closed")

# Run the creation (only once)
create_database()
```

### Test the vector database

Use the following cell to test the vector database by having the model perform a similarity search. It returns the top five documents matching the query provided.

```
# Test a query to the vectorDB
def test_database():
    """Test the database with a sample query"""
    
    encoder = SentenceTransformer(EMBEDDING_MODEL_NAME)
    client = QdrantClient(path=QDRANT_DATA_PATH)
    
    try:
        question = "What is MLOps?"
        query_vector = encoder.encode(question).tolist()

        results = client.query_points(
            collection_name=COLLECTION_NAME,
            query=query_vector,
            limit=5,
        ).points

        print(f"Found {len(results)} results for: '{question}'\n")
        for hit in results:
            print(f"Score: {hit.score:.4f}")
            print(f"Content: {hit.payload.get('content', '')[:200]}...")
            print(f"Source: {hit.payload.get('source', '')}")
            print(f"Category: {hit.payload.get('category', '')}\n")
            
    finally:
        # ALWAYS close the client
        client.close()
        print("✓ Test client closed")

# Run the test
test_database()
```

## Define hooks to deploy an unstructured custom model

The following cell defines the methods used to deploy an unstructured custom model. These include loading the custom model and using the model for scoring. In this notebook, the vector database is loaded to DataRobot's infrastructure. Alternatively, you could use a proxy endpoint in `load_model` that points to where the vector database is actually stored.

```
def load_model(input_dir):
    """Custom model hook for loading our Qdrant knowledge base."""
    
    import os
    from qdrant_client import QdrantClient
    from sentence_transformers import SentenceTransformer
    print("Loading model")
    
    EMBEDDING_MODEL_NAME = 'all-MiniLM-L6-v2'
    COLLECTION_NAME = "my_documents"
    
    # When deploying model="qdrant/", the files are at input_dir/qdrant/
    # not directly at input_dir
    if input_dir:
        QDRANT_DATA_PATH = os.path.join(input_dir, "qdrant")
    else:
        QDRANT_DATA_PATH = "qdrant"
    
    print(f'QDRANT_DATA_PATH = {QDRANT_DATA_PATH}')
    print(f'EMBEDDING_MODEL_NAME = {EMBEDDING_MODEL_NAME}')
    print(f'COLLECTION_NAME = {COLLECTION_NAME}')
    
    # Initialize the embedding model (downloads if needed)
    encoder = SentenceTransformer(EMBEDDING_MODEL_NAME)
    
    # Initialize Qdrant client
    client = QdrantClient(path=QDRANT_DATA_PATH)
    
    # Get collection info to verify it loaded
    collection_info = client.get_collection(collection_name=COLLECTION_NAME)
    print(f'Loaded Qdrant collection "{COLLECTION_NAME}" with {collection_info.points_count} points')
    
    return {
        "client": client,
        "encoder": encoder,
        "collection_name": COLLECTION_NAME
    }


def score_unstructured(model, data, **kwargs) -> str:
    """Custom model hook for retrieving relevant docs with our Qdrant knowledge base.

    When requesting predictions from the deployment, pass a dictionary
    with the following keys:
    - 'question' the question to be passed to the vector store retriever
    - 'filter' any metadata filter
    - 'k' the number of results to return (default: 10)

    datarobot-user-models (DRUM) handles loading the model and calling
    this function with the appropriate parameters.

    Returns:
    --------
    rv : str
        Json dictionary with keys:
            - 'question' user's original question
            - 'relevant' the retrieved document contents
            - 'metadata' - metadata for each document including similarity scores
            - 'error' - error message if exception in handling request
    """
    import json
    from qdrant_client import models
    try:

        # Loading data
        data_dict = json.loads(data)
        question = data_dict['question']
        metadata_filter = data_dict.get("filter", None)
        top_k = data_dict.get("k", 10)

        # Defining info
        client = model["client"]
        encoder = model["encoder"]
        collection_name = model["collection_name"]
        
        # Encode the question
        query_vector = encoder.encode(question).tolist()
        
        # Build query parameters
        query_params = {
            "collection_name": collection_name,
            "query": query_vector,
            "limit": top_k,
        }
        
        # Only add filter if it exists
        if metadata_filter is not None:
            query_params["query_filter"] = models.Filter(**metadata_filter)
        
        # Perform the search
        results = client.query_points(**query_params).points
        
        print(f'Returned {len(results)} results')
        
        relevant, metadata = [], []
        for hit in results:
            # Extract the content from payload
            content = hit.payload.get('content', '')
            relevant.append(content)
            
            # Add similarity score to metadata
            hit_metadata = dict(hit.payload)
            hit_metadata["similarity_score"] = hit.score
            metadata.append(hit_metadata)
        
        rv = {
            "question": question,
            "relevant": relevant,
            "metadata": metadata,
        }
    except Exception as e:
        rv = {'error': f"{e.__class__.__name__}: {str(e)}"}
    return json.dumps(rv), {"mimetype": "application/json", "charset": "utf8"}
```

### Test hooks locally

Before proceeding with deployment, use the cell below to test that the custom model hooks function correctly.

```
# Testing to ensure they work locally before deploying
def test_hooks():
    """Test the DataRobot hooks locally"""
    
    # Load the model - pass None so it uses "qdrant" as the path
    model = load_model(None)
    
    try:
        # Test scoring
        print("=" * 80)
        print("TEST: Basic search")
        print("=" * 80)
        result = score_unstructured(
            model,
            json.dumps({
                "question": "What is MLOps?",
                "filter": models.Filter(
                    must=[
                        models.FieldCondition(
                            key="category",
                            match=models.MatchValue(value="datarobot_docs|en|more-info|eli5")
                        )
                    ]
                ).model_dump(), # converting to be json serializable (will be convert back before scoring)
                "k": 3,
            })
        )
        response = json.loads(result[0])
        print(f"Question: {response['question']}")
        print(f"Found {len(response['relevant'])} results:\n")
        
        for idx, (content, meta) in enumerate(zip(response['relevant'], response['metadata'])):
            print(f"{idx+1}. Score: {meta['similarity_score']:.4f}")
            print(f"   Source: {meta.get('source', 'N/A')}")
            print(f"   Category: {meta.get('category', 'N/A')}")
            print(f"   Content: {content[:150]}...")
            print()
            
    finally:
        # ALWAYS close the client after testing
        model["client"].close()
        print("✓ Test client closed")

# Run the test
test_hooks()
```

## Deploy the knowledge base

The cell below uses a convenience method that does the following:

- Builds a new custom model environment containing the contents of qdrant/ .
- Assembles a new custom model with the provided hooks.
- Deploys an unstructured custom model to DataRobot.
- Returns an object that can be used to make predictions.

This example uses a pre-built environment.
You can also provide an `environment_id` and instead use an existing custom model environment for shorter iteration cycles on the custom model hooks. See your account's existing pre-built environments from the [DataRobot Workshop](https://docs.datarobot.com/en/docs/workbench/nxt-registry/nxt-environment-workshop/nxt-drop-in-envs.html).

```
# Get GenAI environment
genai_environment = dr.ExecutionEnvironment.list(search_for="[GenAI] Python 3.11")[0]

# Deploy ONLY the qdrant/ folder (not the parent directory)
deployment = drx.deploy(
    model="qdrant/",
    name="Qdrant Vector Database",
    hooks={
        "score_unstructured": score_unstructured,
        "load_model": load_model
    },
    extra_requirements=[
        "qdrant-client",
        "sentence-transformers",
    ],
    environment_id=genai_environment.id
)

print(f"✓ Deployment complete!")
print(f"Deployment ID: {deployment.dr_deployment.id}")
```

### Test the deployment

Test that the deployment can successfully provide responses to questions using the [datarobot-predict](https://datarobot.github.io/datarobot-predict/1.13/deployment/) library.

```
from datarobot_predict.deployment import predict_unstructured
# Now with metadata filtering
data = {
    "question": "How do I replace a custom model on an existing custom environment?",
    "filter": models.Filter(
        must=[
            models.FieldCondition(
                key="category",
                match=models.MatchValue(value="datarobot_docs|en|modeling|special-workflows|cml|cml-custom-env")
            )
        ]
    ).model_dump(),
    "k": 5,
}

# Prediction request
content, response_headers = predict_unstructured(
    deployment=deployment.dr_deployment,
    data=data,
)

# Check output
content
```

## Validate and create the vector database

These methods execute, validate, and integrate the vector database.

This example associates a Use Case with the validation and creates the vector database within that Use Case.
Set the `use_case_id` to specify an existing Use Case or create a new one with that name.

```
# Use current use case (assuming your working in a DataRobot Codespace)
use_case_id = os.environ['DATAROBOT_DEFAULT_USE_CASE']
use_case = dr.UseCase.get(use_case_id)

# UNCOMMENT if you want to create a new Use Case
# use_case = dr.UseCase.create()
```

The `CustomModelVectorDatabaseValidation.create` function executes the validation of the vector database. Be sure to provide the deployment ID.

```
external_vdb_validation = CustomModelVectorDatabaseValidation.create(
    prompt_column_name="question", 
    target_column_name="relevant",
    deployment_id=deployment.dr_deployment.id,
    use_case=use_case,
    wait_for_completion=True
)
external_vdb_validation
```

```
assert external_vdb_validation.validation_status == "PASSED"
```

After validation completes, use `VectorDatabase.create_from_custom_model()` to integrate the vector database. You must provide the Use Case name (or Use Case ID), a name for the external vector database, and the validation ID returned from the previous cell.

```
vdb = VectorDatabase.create_from_custom_model(
    name="Qdrant Vector Database",
    use_case=use_case,
    validation_id=external_vdb_validation.id
)
vdb
```

```
assert vdb.execution_status == "COMPLETED"
```

```
print(f"Vector Database ID: {vdb.id}")
```

This vector database ID can now be used to alongside [LLM blueprints](https://docs.datarobot.com/en/docs/gen-ai/playground-tools/build-llm-blueprints.html#add-a-vector-database) to create RAG workflows.

---
