Implementing RAG for Product Search using MastraAI

Accurate and intuitive product search capabilities are crucial for user experience and conversion rates in e-commerce. Traditional keyword based search often fails when dealing with natural language queries or semantic understanding. This blog post explores how I implemented a Retrieval Augmented Generation (RAG) system using MastraAI to enhance product search in a Next.js Commerce application.

You can view the application code on GitHub if you're in a rush, but I recommend following along for a deeper understanding.

Live demo: https://nextjs-commerce-nu-eight-83.vercel.app/

Tech Stack

Our solution leverages a modern tech stack:

  • Next.js Commerce: As our foundational e-commerce framework
  • MastraAI: For workflow orchestration and RAG pipeline management
  • OpenAI: Providing text-embedding-3-small for semantic embeddings
  • PgVector: PostgreSQL with vector extensions for similarity search
  • Shopify: As our product data source
  • TypeScript: Ensuring type safety throughout the application

The RAG Workflow Architecture

The workflow is implemented as a three-step process using MastraAI's workflow orchestration:

  1. Product Data Fetching:
    • Fetches product data from Shopify using GraphQL
    • Validates data using Zod schema
    • Processes product metadata including titles, descriptions, and variants
  2. Embedding Generation:
    • Utilizes OpenAI's text-embedding-3-small model
    • Generates embeddings for product descriptions and metadata
    • Handles batch processing for multiple products
  3. Vector Storage:
    • Stores embeddings in PgVector with product metadata
    • Creates and maintains vector indices for efficient similarity search
    • Handles upserts to keep product data current

The vector search implementation is built on PgVector:

const pgVector = new PgVector(connectionString);
await pgVector.createIndex('products', 1536); // Initialize 1536-dimensional index

export async function searchProducts(query?: string) {
  if (!query) return { products: [], scores: [] };

  const { embedding } = await embed({
    value: query,
    model: openai.embedding('text-embedding-3-small')
  });

  const results = await pgVector.query('products', embedding, 10);
  return {
    products: results.map((result) => result.metadata as Product),
    scores: results.map((result) => result.score as number)
  };
}

Building the Search Experience

The search experience is implemented with several key features:

  1. Real-time Search:
    • Immediate feedback as users type
    • Handles both direct matches and semantic search results
  2. Result Ranking:
    • Combines exact matches with semantic search results
    • Implements confidence scoring (threshold > 0.4 for high confidence)
    • Sorts results by relevance score
  3. UI Components:
    • Clean, responsive search interface
    • Results filtering and sorting options
    • Rich product cards with images and pricing

Key Implementation Details

Data Validation:

const shopifyProductSchema = z.object({
  id: z.string(),
  handle: z.string(),
  title: z.string(),
  description: z.string(),
  // ... other fields
});

Workflow Definition:

export const shopifyRagWorkflow = new Workflow({
  name: 'shopify-rag-workflow',
  triggerSchema: z.object({
    inputValue: shopifyProductSchema
  })
});

Vector Storage:

export async function storeProductEmbedding(embeddings: number[][], products: ProductMetadata[]) {
  return await pgVector.upsert(
    'products',
    embeddings,
    products.map((product) => ({
      id: product.id,
      // ... other fields
    }))
  );
}

Implementation Walkthrough

Let's walk through how the RAG workflow is implemented using MastraAI's workflow orchestration. The workflow consists of three main steps that work together to create a seamless product search experience:

Step 1: Fetching Products (fetchProductsStep)

const fetchProductsStep = new Step({
  id: 'fetchProductsStep',
  input: z.object({
    inputValue: shopifyProductSchema.optional()
  }),
  output: z.array(shopifyProductSchema),
  execute: async ({ context }) => {
    if (context?.triggerData && Object.keys(context.triggerData).length > 0) {
      return context.triggerData;
    }

    const shopify = new ShopifyClient(
      process.env.SHOPIFY_STORE_DOMAIN!,
      process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!
    );

    try {
      return await shopify.fetchProducts();
    } catch (error) {
      console.error('Error in fetchProductsStep:', error);
      throw error;
    }
  }
});

This step either uses existing product data from the context or fetches fresh data from Shopify. The data is validated against a Zod schema to ensure type safety and data integrity.

Step 2: Generating Embeddings (generateEmbeddingsStep)

const generateEmbeddingsStep = new Step({
  id: 'generateEmbeddingsStep',
  input: z.array(shopifyProductSchema),
  output: z.array(productWithEmbeddingSchema),
  execute: async ({ context }) => {
    const fetchProductsResult = context?.steps?.fetchProductsStep;
    if (!fetchProductsResult || fetchProductsResult.status !== 'success') {
      throw new Error('Previous step failed or not completed');
    }

    const products = fetchProductsResult.output;
    if (!products || !Array.isArray(products)) {
      throw new Error('Products not found in step results or invalid format');
    }

    const results = [];
    for (const product of products) {
      let imageDescription = '';
      if (product.featuredImage?.url) {
        imageDescription = await getImageDescription(product.featuredImage.url);
      }

      const productText = [
        product.title,
        product.description,
        imageDescription,
        ...(product.tags || [])
      ]
        .filter(Boolean)
        .join(' ');

      const embedding = await generateEmbedding(productText);
      results.push({
        ...product,
        embedding
      });
    }
    return results;
  }
});

This step enriches product data by:

  1. Generating detailed image descriptions using GPT-4o
  2. Combining product metadata into a rich text description
  3. Creating embeddings using OpenAI's text-embedding-3-small model

Step 3: Storing Embeddings (storeEmbeddingsStep)

const storeEmbeddingsStep = new Step({
  id: 'storeEmbeddingsStep',
  input: z.array(productWithEmbeddingSchema),
  output: z.object({ success: z.boolean() }),
  execute: async ({ context }) => {
    const generateEmbeddingsResult = context?.steps?.generateEmbeddingsStep;
    if (!generateEmbeddingsResult || generateEmbeddingsResult.status !== 'success') {
      throw new Error('Previous step failed or not completed');
    }

    const productsWithEmbeddings = generateEmbeddingsResult.output;
    if (!productsWithEmbeddings || !Array.isArray(productsWithEmbeddings)) {
      throw new Error('Products not found in step results or invalid format');
    }

    const embeddings = productsWithEmbeddings.map((p) => p.embedding);
    const products = productsWithEmbeddings.map(({ embedding, ...product }) => product);

    await storeProductEmbedding(embeddings, products);
    return { success: true };
  }
});

This final step stores the product embeddings and metadata in PgVector for efficient similarity search. The upsert operation ensures that:

  • New products are added to the vector store
  • Existing products are updated with fresh embeddings
  • Product metadata is kept in sync

Workflow Orchestration

The workflow is orchestrated using MastraAI's workflow engine:

shopifyRagWorkflow
  .step(fetchProductsStep)
  .then(generateEmbeddingsStep)
  .then(storeEmbeddingsStep)
  .commit();

This creates a linear pipeline where:

  1. Products are fetched from Shopify
  2. Embeddings are generated for each product
  3. Results are stored in the vector database

The workflow can be triggered manually or scheduled to run periodically to keep the search index fresh. Each step's output is validated against its schema, ensuring type safety throughout the pipeline.

Benefits and Real World Impact

  1. Improved Search Accuracy:
    • Better handling of natural language queries
    • Understanding of semantic relationships between products
    • Reduced "no results found" scenarios
  2. Enhanced Product Discovery:
    • Surfaces relevant products even with imperfect queries
    • Handles synonyms and related terms effectively
    • Improves cross-selling through semantic relationships
  3. Performance:
    • Fast query response times through vector indexing
    • Efficient batch processing of product updates
    • Scalable architecture for large product catalogs

Conclusion

Implementing RAG for product search using MastraAI has significantly improved our e-commerce search capabilities. The combination of Next.js Commerce, OpenAI embeddings, and PgVector provides a robust foundation for semantic search that enhances the shopping experience.

Key learnings from this implementation:

  • Vector search provides more intuitive results than traditional text search
  • Proper workflow orchestration is crucial for maintaining data consistency
  • Type safety and data validation are essential for reliable operations
  • Confidence scoring helps balance precision and recall in search results

For developers looking to implement similar solutions, the full source code demonstrates how to integrate these technologies effectively while maintaining code quality and type safety.

I'd love to hear about your experience with MastraAI! Connect with me on X (formerly Twitter) or LinkedIn to share your thoughts and questions.


AI should drive results, not complexity. AgentemAI helps businesses build scalable, efficient, and secure AI solutions. See how we can help.