Agent-Driven Metadata Filtering
This example demonstrates how to implement a Retrieval-Augmented Generation (RAG) system using Mastra, OpenAI embeddings, and PGVector for vector storage. This system uses an agent to construct metadata filters from a user's query to search for relevant chunks in the vector store, reducing the amount of results returned.
OverviewDirect link to Overview
The system implements metadata filtering using Mastra and OpenAI. Here's what it does:
- Sets up a Mastra agent with gpt-4o-mini to understand queries and identify filter requirements
- Creates a vector query tool to handle metadata filtering and semantic search
- Processes documents into chunks with metadata and embeddings
- Stores both vectors and metadata in PGVector for efficient retrieval
- Processes queries by combining metadata filters with semantic search
When a user asks a question:
- The agent analyzes the query to understand the intent
- Constructs appropriate metadata filters (e.g., by topic, date, category)
- Uses the vector query tool to find the most relevant information
- Generates a contextual response based on the filtered results
SetupDirect link to Setup
Environment SetupDirect link to Environment Setup
Make sure to set up your environment variables:
OPENAI_API_KEY=your_openai_api_key_here
POSTGRES_CONNECTION_STRING=your_connection_string_here
DependenciesDirect link to Dependencies
Then, import the necessary dependencies:
import { openai } from "@ai-sdk/openai";
import { Mastra } from "@mastra/core";
import { Agent } from "@mastra/core/agent";
import { PgVector, PGVECTOR_PROMPT } from "@mastra/pg";
import { createVectorQueryTool, MDocument } from "@mastra/rag";
import { embedMany } from "ai";
Vector Query Tool CreationDirect link to Vector Query Tool Creation
Using createVectorQueryTool imported from @mastra/rag, you can create a tool that enables metadata filtering. Each vector store has its own prompt that defines the supported filter operators and syntax:
const vectorQueryTool = createVectorQueryTool({
id: "vectorQueryTool",
vectorStoreName: "pgVector",
indexName: "embeddings",
model: openai.embedding("text-embedding-3-small"),
enableFilter: true,
});
Each prompt includes:
- Supported operators (comparison, array, logical, element)
- Example usage for each operator
- Store-specific restrictions and rules
- Complex query examples
Document ProcessingDirect link to Document Processing
Create a document and process it into chunks with metadata:
const doc = MDocument.fromText(
`The Impact of Climate Change on Global Agriculture...`,
);
const chunks = await doc.chunk({
strategy: "recursive",
size: 512,
overlap: 50,
separator: "\n",
extract: {
keywords: true, // Extracts keywords from each chunk
},
});
Transform Chunks into MetadataDirect link to Transform Chunks into Metadata
Transform chunks into metadata that can be filtered:
const chunkMetadata = chunks?.map((chunk: any, index: number) => ({
text: chunk.text,
...chunk.metadata,
nested: {
keywords: chunk.metadata.excerptKeywords
.replace("KEYWORDS:", "")
.split(",")
.map((k) => k.trim()),
id: index,
},
}));
Agent ConfigurationDirect link to Agent Configuration
The agent is configured to understand user queries and translate them into appropriate metadata filters.
The agent requires both the vector query tool and a system prompt containing:
- Metadata structure for available filter fields
- Vector store prompt for filter operations and syntax
export const ragAgent = new Agent({
name: "RAG Agent",
model: openai("gpt-4o-mini"),
instructions: `
You are a helpful assistant that answers questions based on the provided context. Keep your answers concise and relevant.
Filter the context by searching the metadata.
The metadata is structured as follows:
{
text: string,
excerptKeywords: string,
nested: {
keywords: string[],
id: number,
},
}
${PGVECTOR_PROMPT}
Important: When asked to answer a question, please base your answer only on the context provided in the tool.
If the context doesn't contain enough information to fully answer the question, please state that explicitly.
`,
tools: { vectorQueryTool },
});
The agent's instructions are designed to:
- Process user queries to identify filter requirements
- Use the metadata structure to find relevant information
- Apply appropriate filters through the vectorQueryTool and the provided vector store prompt
- Generate responses based on the filtered context
Note: Different vector stores have specific prompts available. See Vector Store Prompts for details.
Instantiate PgVector and MastraDirect link to Instantiate PgVector and Mastra
Instantiate PgVector and Mastra with the components:
const pgVector = new PgVector({
connectionString: process.env.POSTGRES_CONNECTION_STRING!,
});
export const mastra = new Mastra({
agents: { ragAgent },
vectors: { pgVector },
});
const agent = mastra.getAgent("ragAgent");
Creating and Storing EmbeddingsDirect link to Creating and Storing Embeddings
Generate embeddings and store them with metadata:
const { embeddings } = await embedMany({
model: openai.embedding("text-embedding-3-small"),
values: chunks.map((chunk) => chunk.text),
});
const vectorStore = mastra.getVector("pgVector");
await vectorStore.createIndex({
indexName: "embeddings",
dimension: 1536,
});
// Store both embeddings and metadata together
await vectorStore.upsert({
indexName: "embeddings",
vectors: embeddings,
metadata: chunkMetadata,
});
The upsert operation stores both the vector embeddings and their associated metadata, enabling combined semantic search and metadata filtering capabilities.
Metadata-Based QueryingDirect link to Metadata-Based Querying
Try different queries using metadata filters:
const queryOne = "What are the adaptation strategies mentioned?";
const answerOne = await agent.generate(queryOne);
console.log("\nQuery:", queryOne);
console.log("Response:", answerOne.text);
const queryTwo =
'Show me recent sections. Check the "nested.id" field and return values that are greater than 2.';
const answerTwo = await agent.generate(queryTwo);
console.log("\nQuery:", queryTwo);
console.log("Response:", answerTwo.text);
const queryThree =
'Search the "text" field using regex operator to find sections containing "temperature".';
const answerThree = await agent.generate(queryThree);
console.log("\nQuery:", queryThree);
console.log("Response:", answerThree.text);
View source on GitHub