Introduction

Welcome to the final lesson in our Scaling Up RAG with Vector Databases course! Previously, we explored how to chunk large documents for efficient retrieval, store these chunks in a vector database (such as ChromaDB), and then retrieve them to build prompts for Large Language Models (LLMs). Remember, chunking and storing these text fragments provided the basic scaffolding for a Retrieval-Augmented Generation (RAG) pipeline.

In this lesson, we will expand on that foundation by introducing metadata-based filtering, which allows you to target specific attributes — like category or date — and make your content searches significantly more precise. By the end, you will be able to create queries that focus only on the metadata you care about, such as retrieving documents from specific categories.

Understanding Metadata in RAG Systems

Before we get hands-on, let's talk about the intuition behind metadata:

  • What is Metadata, and Why Does It Matter?
    Metadata includes any labeled information that describes your text chunks. Common examples are category, date, or title. When you have a large collection of documents, a normal text-based similarity search might return results you don't actually want. But by selectively filtering on metadata, you can drastically reduce irrelevant results and ensure only the most pertinent information is retrieved.

  • Real-World Example
    Imagine a large enterprise knowledge base spanning different departments (e.g., Human Resources, Technology, Finance). If you only want to see technology-related documents, applying a simple metadata filter on the category field ensures that your search never strays into HR or Finance content. This becomes particularly useful when you have specialized queries that are domain-specific and need accurate, fast retrieval.

Building the Filter Logic
Generating the Query Embedding

To begin, we need to convert the user's query into an embedding vector, which will allow us to perform a semantic search in the vector database.

    /// Performs a metadata-enhanced search on the given collection.
    async fn metadata_enhanced_search(
        collection: &Collection,
        query_input: &str,
        categories: Option<Vec<String>>,
        top_k: usize,
        embedder: &Embedder,
    ) -> Result<Vec<RetrievedChunk>, SomeErrorType> {

        // Generate the embedding for the query input
        let query_embedding = embedder.embed(query_input).await?;

Here, the embedder.embed function asynchronously transforms the input query string into a vector representation. This vector captures the semantic meaning of the query, enabling similarity-based retrieval from the vector database.

With the query embedding ready, we can now build and execute a search that leverages both the embedding and any metadata filters.

Building and Executing the Query with Metadata Filtering

Now that we have the query embedding, the next step is to construct the query options, apply any metadata filters, and execute the search against the ChromaDB collection.

        // Build the metadata filter (where clause) if categories are provided
        let where_clause = categories.map(|cats| {
            json!({
                "category": { "$in": cats }
            })
        });

        // Set up the query options for ChromaDB
        let query_options = QueryOptions {
            query_texts: None, // We're searching by embedding, not raw text
            query_embeddings: Some(query_embedding),
            n_results: Some(top_k),
            where_metadata: where_clause, // Apply metadata filter if present
            where_document: None,
            include: Some(vec!["documents", "distances", "metadatas"]), // Request these fields in the result
        };

        // Execute the query against the collection
        let result = collection.query(query_options, None).await?;
  • The where_clause is built only if categories are provided, filtering results to those matching the specified categories.
  • QueryOptions configures the search: it uses the query embedding, limits the number of results, and specifies which fields to include in the output.
  • The collection.query call performs the actual search in ChromaDB, returning the most relevant chunks according to the embedding and any metadata filters.

Once the query is executed, we need to extract and structure the results for easy downstream use.

Structuring the Results

After executing the query, we extract the relevant fields from the results and organize them into a vector of RetrievedChunk structs for convenient downstream processing.

        // Extract the first batch of documents, distances, and metadatas (if any)
        let documents = result.documents
            .and_then(|d| d.first().cloned())
            .unwrap_or_default();

        let distances = result.distances
            .and_then(|d| d.first().cloned())
            .unwrap_or_default();

        let metadatas = result.metadatas
            .and_then(|m| m.first().cloned())
            .unwrap_or_default();

        // Combine the results into a vector of RetrievedChunk structs
        let retrieved_chunks = documents.iter().enumerate()
            .map(|(i, chunk)| RetrievedChunk {
                chunk: chunk.clone(),
                doc_id: metadatas.get(i)
                    .and_then(|m| m.as_ref())
                    .and_then(|m| m.get("doc_id"))
                    .and_then(|v| v.as_u64())
                    .map(|id| id as usize)
                    .unwrap_or(i),
                category: metadatas.get(i)
                    .and_then(|m| m.as_ref())
                    .and_then(|m| m.get("category"))
                    .and_then(|v| v.as_str())
                    .map(String::from),
                distance: distances.get(i).copied().unwrap_or(0.0),
            })
            .collect();

        Ok(retrieved_chunks)
    }
  • The code safely extracts the lists of documents, distances, and metadata from the query result, defaulting to empty lists if any are missing.
  • It then iterates over the retrieved documents, pairing each chunk with its corresponding metadata and distance score.
  • Each result is wrapped in a RetrievedChunk struct, making it easy to work with the search results in the rest of your RAG pipeline.

With your results now structured, you can seamlessly integrate them into downstream tasks or present them to users as part of your RAG workflow.

Practical example

Next, we can integrate this metadata-based search into our workflow. Let's try running a sample query with and without metadata filtering:

let query_input = "Recent advancements in AI and their impact on teaching";

println!("\n======== WITHOUT CATEGORY FILTER ========");
let no_filter_results = metadata_enhanced_search(
    &collection,
    query_input,
    None,
    3,
    &embedder
).await?;

for chunk in no_filter_results {
    println!("Doc ID: {}, Category: {}, Distance: {:.4}",
             chunk.doc_id,
             chunk.category.unwrap_or_else(|| "Unknown".to_string()),
             chunk.distance
    );
    println!("Chunk: {}\n", chunk.chunk);
}

println!("\n======== WITH CATEGORY FILTER (Education) ========");
let filter_results = metadata_enhanced_search(
    &collection,
    query_input,
    Some(vec!["Education".to_string()]),
    3,
    &embedder
).await?;

for chunk in filter_results {
    println!("Doc ID: {}, Category: {}, Distance: {:.4}",
             chunk.doc_id,
             chunk.category.unwrap_or_else(|| "Unknown".to_string()),
             chunk.distance
    );
    println!("Chunk: {}\n", chunk.chunk);
}

When you run the example query in the code provided using our data/corpus.json, you obtain the following output:

======== WITHOUT CATEGORY FILTER ========
Doc ID: 64, Category: Education, Distance: 1.0530
Chunk: The integration of technology in education is revolutionizing traditional teaching methods. Digital tools and interactive platforms are making learning more engaging. Educators are adapting to these changes to enhance student outcomes.

Doc ID: 24, Category: Education, Distance: 1.1431
Chunk: Universities are rethinking traditional education models to better prepare students for a dynamic global job market. Innovative teaching methods, including online and hybrid courses, are gaining traction. These reforms aim to create more engaging and effective learning environments.

Doc ID: 1, Category: Technology, Distance: 1.1532
Chunk: Artificial intelligence is transforming the way we approach complex problems in computing. Recent breakthroughs in machine learning have enabled faster data processing and smarter algorithms. The future of technology is expected to integrate AI into every facet of life.

======== WITH CATEGORY FILTER (Education) ========
Doc ID: 64, Category: Education, Distance: 1.0530
Chunk: The integration of technology in education is revolutionizing traditional teaching methods. Digital tools and interactive platforms are making learning more engaging. Educators are adapting to these changes to enhance student outcomes.

Doc ID: 24, Category: Education, Distance: 1.1431
Chunk: Universities are rethinking traditional education models to better prepare students for a dynamic global job market. Innovative teaching methods, including online and hybrid courses, are gaining traction. These reforms aim to create more engaging and effective learning environments.

Doc ID: 63, Category: Education, Distance: 1.2630
Chunk: Modern classrooms are benefiting from innovative pedagogical approaches that encourage active learning. Educators are integrating technology to create interactive lessons. These methods aim to foster critical thinking and creativity among students.

By default (no filter), you see documents that span both “Education” and “Technology.” Notice how Doc ID 1, despite mentioning AI, focuses more on broad computing challenges rather than teaching. Once the “Education” filter is applied, Doc ID 1 is excluded, and Doc ID 63 appears instead, emphasizing “modern classrooms” and strategies that involve integrating technology. Given the query about “AI and its impact on teaching,” Doc ID 63 is more specifically aligned with education-focused content, underscoring how metadata-based filtering can help narrowing down your search to only the most relevant subsets of your data.

Conclusion and Next Steps

In this lesson, you learned how to harness metadata-based filtering to refine search results in your RAG pipeline. You've seen that by storing category information (or any other descriptor) alongside your text chunks, you can easily pinpoint the data most relevant to your query. This makes your system significantly more robust and efficient, especially as your document collection grows.

Next, you will have the chance to practice implementing these ideas on your own in the upcoming exercises. Good luck, and keep exploring the power of metadata in RAG!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal