🧠

Mastraを使ってRAGに入門してみた

に公開

本記事は「MIXI DEVELOPERS Advent Calendar 2025」の18日目の記事です。

株式会社MIXIでサロンスタッフ予約サービス「minimo」の機械学習やバックエンドの開発をしている鈴木です。
MastraはAIを搭載したアプリケーションやエージェントを構築するためのフレームワークで、TypeScriptで記述します。
MastraではRAG (Retrieval-Augmented Generation) を使用することができるので、Mastraを使ってRAGに入門してみます。

構成

  • Docker Compose
    • Mastraコンテナ
    • ベクトルデータベースコンテナ: OpenSearch 2.19.0
  • embeddingモデル: Amazon Titan Text Embedding v2
    • Amazon Bedrock経由で使用するため、AWSアカウントが必要
    • 料金はap-northeast-1リージョンで0.000029USD/1000入力トークン (本記事執筆時点 / 詳細は https://aws.amazon.com/jp/bedrock/pricing/ 参照)
    • AWS Bedrockで使用できるembeddingモデルの中で最も安いため、このモデルを使用

準備

次のIAMポリシーをアタッチしたAWSのIAMユーザーやIAM Identity Centerの権限セット等を用意します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": "bedrock:InvokeModel",
      "Resource": "arn:aws:bedrock:ap-northeast-1::foundation-model/amazon.titan-embed-text-v2:0"
    }
  ]
}

このIAMユーザー等を手元の環境のAWS CLIで使用できるように aws configure --profile ...aws configure sso 等で設定します。
この際に設定したprofile名を後で使うのでメモしておきましょう。

入門用コード

TODOコメントが付与されている部分は必要に応じて変更してください。

ドキュメント群

documents.ts
import { z } from "zod";
import { documentSchema } from "./lib";

/**
 * ドキュメント群 (ID付き) のスキーマ定義
 */
const documentsWithIDSchema = z.array(
  z
    .object({
      id: z.number(),
    })
    .merge(documentSchema),
);

/**
 * ドキュメント群
 */
export const documents: z.infer<typeof documentsWithIDSchema> = [
  {
    id: 1,
    title: "来月の発注の件について",
    body: "来月はA社に製品bを100個発注予定です。",
  },
  {
    id: 2,
    title: "先月の発注の件について",
    body: "先月はC社に製品dを10個発注しましたが、キャンセルされました。",
  },
];

今回ベクトルデータベースに投入するドキュメント群です。
企業のナレッジデータベースを想定して作りましたが、内容の雑さはスルーしてください。

メインスクリプト

dev.ts
import { z } from "zod";
import { createIndex } from "./createIndex";
import { documentsSchema } from "./lib";
import { upsertEmbeddings } from "./upsertEmbeddings";
import { search } from "./search";

async function main() {
  // ベクトルデータベース用のOpenSearchのindexを作成する
  await createIndex();

  // ドキュメントのembeddingを生成してベクトルデータベースに保存する
  await upsertEmbeddings();

  const question: string = "先月の発注はどのような感じでしたか?";
  console.log(`Q: ${question}`);

  // ベクトルデータベースからドキュメントを検索する
  // id: 2のドキュメントがヒットして欲しい
  const documents: z.infer<typeof documentsSchema> = await search(question, 1);
  console.log(`A: ${JSON.stringify(documents, null, 2)}`);
}

main();

まず大元となるコードです。
コードを見るとわかるように、以下のような流れで処理を行っていきます。

  1. ベクトルデータベース用のindex作成
  2. ドキュメントのembeddingを生成してベクトルデータベースに保存
  3. ベクトルデータベースからドキュメントを検索

documents.ts と見比べるとわかるように、ここでは id: 2 のドキュメントがヒットすることが期待されます。

ベクトルデータベース用のindex作成

createIndex.ts
import { embed } from "ai";
import { embeddingModel, vectorDatabase, vectorDatabaseIndexName } from "./lib";
import type { Embedding } from "ai";

/**
 * ベクトルデータベース用のOpenSearchのindexを作成する
 */
export async function createIndex() {
  // 次元数取得のためのダミーのembedding生成
  const { embedding }: { embedding: Embedding } = await embed({
    value: "ダミー",
    model: embeddingModel,
  });

  if (vectorDatabase === undefined) {
    return;
  }

  // index作成
  await vectorDatabase.createIndex({
    indexName: vectorDatabaseIndexName,

    // 実際に生成したembeddingからembeddingの次元数を求める
    dimension: embedding.length,

    // https://github.com/mastra-ai/mastra/blob/7e6db67f5e2ce58ef62eae324844e32ed314bad1/stores/opensearch/src/vector/index.ts#L54
    // metricのデフォルト値はcosine
    //
    // TODO: OpenSearch 2.19以前を使用する場合はcosineに対応していないため、以下のコメントアウトを解除
    // metric: "euclidean",
  });

  console.log(`Created index: ${vectorDatabaseIndexName} `);
}

ベクトルデータベース用のOpenSearchのindexを作成します。

indexを作成する際にembeddingの次元数を指定する必要があります。
Mastraのドキュメントでは次元数を直接指定していますが、本コードではダミーのembeddingを生成し、そこからembeddingの次元数を求めるようにしています。
こうすることで、embeddingモデルを差し替えた際に次元数の設定が自動的に変更されるようにしています。

https://github.com/mastra-ai/mastra/blob/7e6db67f5e2ce58ef62eae324844e32ed314bad1/stores/opensearch/src/vector/index.ts#L54
また、 metric のデフォルト値は cosine になっています。
https://zenn.dev/opensearch/articles/opensearch-2-19-explore-new-features#faiss-ベクトルエンジンでコサインベースの類似検索を適用
しかし、 cosine に対応したのは2.19からであり、それ以前のバージョンでは対応していないようなので、2.19以前を使いたい場合は euclidean 等の別の metric を指定しましょう。

ドキュメントのembeddingを生成とベクトルデータベースへの保存

upsertEmbeddings.ts
import { embedMany } from "ai";
import { z } from "zod";
import { MDocument } from "@mastra/rag";
import { documents } from "./documents";
import {
  embeddingModel,
  documentSchema,
  vectorDatabase,
  vectorDatabaseIndexName,
} from "./lib";
import type { Embedding } from "ai";

/**
 * ドキュメントのembeddingを生成してベクトルデータベースに保存する
 */
export async function upsertEmbeddings() {
  for (const document of documents) {
    const vectorTexts: string[] = [document.title, document.body];

    // ドキュメントを初期化してチャンク作成
    const chunks = await MDocument.fromText(vectorTexts.join(" ")).chunk({
      strategy: "recursive",
    });

    // embedding生成
    const { embeddings }: { embeddings: Embedding[] } = await embedMany({
      values: chunks.map(({ text }) => text), // 各チャンクのテキストを渡す
      model: embeddingModel,
    });

    // embeddingのメタデータのスキーマ定義
    const embeddingMetadataSchema = documentSchema.merge(
      z.object({
        documentID: z.number().describe("ドキュメントID"),
        chunkID: z.number().describe("チャンクID"),
        embeddingsText: z.string().describe("チャンクのテキスト (デバッグ用)"),
      }),
    );

    if (vectorDatabase === undefined) {
      return;
    }

    // embeddingをベクトルデータベースに保存
    await vectorDatabase.upsert({
      indexName: vectorDatabaseIndexName,
      ids: chunks.map((_, i) => `${document.id}_${i}`),
      vectors: embeddings,

      // デフォルトではベクトルデータベースにはIDとembeddingしか保存されない
      // IDとembedding以外の検索時に取得したいドキュメントの内容はメタデータとして保存する
      metadata: chunks.map(
        ({ text }, i): z.infer<typeof embeddingMetadataSchema> => ({
          documentID: document.id,
          chunkID: i,
          title: document.title,
          body: document.body,
          embeddingsText: text,
        }),
      ),
    });
  }
}

ドキュメントのembeddingを生成してベクトルデータベースに保存します。
ここではドキュメントの titlebody を結合した上でチャンクに区切り、それらのembeddingを作成しています。

これをベクトルデータベースに保存するのですが、ここで注意が必要なのがメタデータです。
ベクトルデータベースに保存されるのはIDとembeddingのみであり、これだけでは検索してもどのドキュメントがヒットしたのか (少なくとも人間には) よくわからないです。
そのため、IDとembedding以外の検索時に取得したいドキュメントの内容はメタデータとして保存する必要があります。
本コードではドキュメントの内容に加え、デバッグ用に各チャンクのテキストも embeddingsText として保存しています。

ドキュメントの検索

search.ts
import { embed } from "ai";
import { z } from "zod";
import {
  embeddingModel,
  documentSchema,
  vectorDatabase,
  vectorDatabaseIndexName,
  documentsSchema,
} from "./lib";
import type { Embedding } from "ai";
import type { QueryResult } from "@mastra/core";

const optionalDocumentsSchema = documentsSchema.optional();

/**
 * ベクトルデータベースからドキュメントを検索する
 * @param question 自然言語による質問
 * @param count 取得件数
 * @return 取得結果のドキュメント群
 */
export async function search(
  question: string,
  count: number,
): Promise<z.infer<typeof optionalDocumentsSchema>> {
  try {
    // embedding生成
    const { embedding }: { embedding: Embedding } = await embed({
      value: question,
      model: embeddingModel,
    });

    if (vectorDatabase === undefined) {
      return;
    }

    // 検索
    const queryResults: QueryResult[] = await vectorDatabase.query({
      indexName: vectorDatabaseIndexName,
      queryVector: embedding,
      topK: count,
    });

    // 検索結果のメタデータをドキュメントのスキーマ定義に当てはめて返す
    return queryResults.map(({ metadata }) => documentSchema.parse(metadata));
  } catch (error) {
    console.error("Error in Search:", error);
    throw error;
  }
}

ベクトルデータベースを検索します。

search では引数として questioncount をとっています。
question は自然言語による質問であり、すなわち日本語での質問をそのまま与えます。
count は取得件数です。

question のembeddingを生成し、それを使ってベクトルデータベースを検索しています。
そして、その結果のメタデータをドキュメントのスキーマに当てはめて返します。

共通ライブラリ

lib.ts
import { z } from "zod";
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
import { fromIni } from "@aws-sdk/credential-providers";
import { OpenSearchVector } from "@mastra/opensearch";
import type { AmazonBedrockProvider } from "@ai-sdk/amazon-bedrock";
import type { EmbeddingModelV2 } from "@ai-sdk/provider";
import type { AwsCredentialIdentity } from "@smithy/types";

/**
 * ドキュメントのスキーマ定義
 */
export const documentSchema = z.object({
  title: z.string(),
  body: z.string(),
});

/**
 * ドキュメント群のスキーマ定義
 */
export const documentsSchema = z.array(documentSchema);

/**
 * Amazon BedrockのProvider
 * embeddingモデルを使用するために必要
 */
const bedrockProvider: AmazonBedrockProvider = createAmazonBedrock({
  region: process.env.AWS_REGION || "ap-northeast-1",

  // 環境変数 AWS_PROFILE にセットされたprofile名からAWSの認証情報を取得
  ...(process.env.AWS_PROFILE && {
    credentialProvider: async (): Promise<{
      accessKeyId: string;
      secretAccessKey: string;
      sessionToken?: string;
    }> => {
      const identity: AwsCredentialIdentity = await fromIni({
        profile: process.env.AWS_PROFILE,
      })();
      return {
        accessKeyId: identity.accessKeyId,
        secretAccessKey: identity.secretAccessKey,
        sessionToken: identity.sessionToken,
      };
    },
  }),
});

/**
 * embeddingモデル
 */
export const embeddingModel: EmbeddingModelV2<string> =
  bedrockProvider.embedding("amazon.titan-embed-text-v2:0");

/**
 * ベクトルデータベース
 */
export const vectorDatabase: OpenSearchVector | undefined = process.env
  .OPENSEARCH_URL
  ? new OpenSearchVector({
      url: process.env.OPENSEARCH_URL,
    })
  : undefined;

/**
 * ベクトルデータベースのindex名を生成する
 * embeddingモデルによってembeddingの次元数が異なるため、embeddingモデルを差し替えた際に別のindexが生成されるようにしている
 * @param embeddingModel embeddingモデル
 */
const createIndexName = (embeddingModel: EmbeddingModelV2<string>): string =>
  ["embeddings", embeddingModel.provider, embeddingModel.modelId]
    .join("_")
    .replace(/[^a-zA-Z0-9_]/g, "_");

/**
 * ベクトルデータベースのindex名
 */
export const vectorDatabaseIndexName: string = createIndexName(embeddingModel);

documentSchemadocumentSchema は検索結果として返すドキュメント (群) のスキーマ定義です。
他のドキュメント関連のスキーマ定義のベースにもなっています。

bedrockProvider はAmazon BedrockのProviderです。
ここでは環境変数 AWS_PROFILE にセットされたprofile名からAWSの認証情報を取得し、それらをセットしています。

embeddingModel では bedrockProvider を使ってembeddingモデルを取得しています。

vectorDatabase はベクトルデータベースです。

createIndexName ではベクトルデータベースのindex名を生成しています。
embeddingモデルによってembeddingの次元数が異なるため、embeddingモデルを差し替えた際に別のindexが生成されるようにしています。

vectorDatabaseIndexName では createIndexName を使って生成したindex名を代入しています。

その他実行に必要なファイル群

.env
# Bedrock Configuration (for local development)
# TODO: 記載する
AWS_PROFILE=your-profile-name
# TODO: ap-northeast-1以外のリージョンを使う場合は以下のコメントアウトを解除し記載する
# AWS_REGION=ap-northeast-1

# OpenSearch Configuration (for local development)
OPENSEARCH_INITIAL_ADMIN_PASSWORD=pnssWord@2

IAMユーザー等のAWS CLIでのprofile名を指定します。
また、 ap-northeast-1以外のリージョンを使う場合は AWS_REGION を設定します。

.gitignore
node_modules
.env
.DS_Store

Gitで管理する場合は .env 等を .gitignore に入れます。

package.json
{
  "name": "mastra-rag",
  "scripts": {
    "dev": "tsx dev.ts"
  },
  "dependencies": {
    "@ai-sdk/amazon-bedrock": "3.0.66",
    "@aws-sdk/credential-providers": "3.943.0",
    "@mastra/core": "0.24.3",
    "@mastra/opensearch": "0.11.18",
    "@mastra/rag": "1.3.6",
    "ai": "5.0.109",
    "tsx": "4.21.0",
    "zod": "3.25.76"
  },
  "devDependencies": {
    "@ai-sdk/provider": "2.0.0",
    "@smithy/types": "4.9.0",
    "@types/node": "24.10.3",
    "prettier": "3.7.4",
    "typescript": "5.9.3"
  }
}

npm run devdev.ts が実行されるようになっています。
また、本筋とは関係ないですが、本記事のコードは prettier でフォーマットを修正しています。

Dockerfile
FROM node:24.12.0-bookworm-slim@sha256:04d9cbb7297edb843581b9bb9bbed6d7efb459447d5b6ade8d8ef988e6737804 AS base

# Create non-root user for security
RUN groupadd -r mastra && useradd -r -g mastra -m mastra

# Set working directory
WORKDIR /app

COPY . .

# dependenciesのみインストール(tsxでTypeScriptを直接実行)
RUN npm ci --omit=dev && npm cache clean --force

# Change ownership to non-root user
RUN chown -R mastra:mastra /app

# Switch to non-root user
USER mastra

CMD ["npm", "run", "dev"]

RAGを動かすDockerイメージの Dockerfile です。

.dockerignore
node_modules
Dockerfile
.dockerignore
.git
.gitignore
.env
.DS_Store

Dockerイメージ上に余分なファイルが載らないように .dockerignore を設定しておきます。

docker-compose.yml
services:
  mastra:
    build:
      context: .
      dockerfile: Dockerfile
    env_file:
      - .env
    environment:
      - OPENSEARCH_URL=http://opensearch:9200
    volumes:
      - ~/.aws:/home/mastra/.aws:ro
    depends_on:
      opensearch:
        condition: service_healthy

  opensearch:
    image: opensearchproject/opensearch:2.19.0
    environment:
      node.name: "es01"
      cluster.initial_master_nodes: "es01"
      plugins.security.disabled: "true"
    env_file:
      - .env
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200/_cluster/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s

RAGを動かすDocker Composeの定義です。
RAGを動かす mastra コンテナと opensearch コンテナで構成されており、 opensearch コンテナの起動が完了すると mastra コンテナが起動します。

tsconfig.json
{
  "compilerOptions": {
    "skipLibCheck": true,
    "noEmit": true
  }
}

本筋の実行とは関係ないですが、 tsc で型チェックが行えるよう、 tsconfig.json を設定しておきます。

実行方法

  1. (IAM Identity Centerの権限セットを使用する場合のみ) aws sso login --profile ... で今回使用する権限セットにログインします。

  2. npm install を実行し、 package-lock.json を生成します。

  3. docker compose up --build を実行します。
    しばらく待つと次のように出力されるでしょう。

    mastra-1      | > dev
    mastra-1      | > tsx dev.ts
    mastra-1      |
    mastra-1      | Created index: embeddings_amazon_bedrock_amazon_titan_embed_text_v2_0 
    mastra-1      | Q: 先月の発注はどのような感じでしたか?
    mastra-1      | A: [
    mastra-1      |   {
    mastra-1      |     "title": "先月の発注の件について",
    mastra-1      |     "body": "先月はC社に製品dを10個発注しましたが、キャンセルされました。"
    mastra-1      |   }
    mastra-1      | ]
    mastra-1 exited with code 0
    
  4. 結果を確認したら Ctrl + C でDocker Composeを停止します。

最後に

Mastraの公式ドキュメントにも入門用のコードはありますが、それだけではうまく動かなかったため、コピペすればとりあえず入門できる状態を目指して書いてみました。
この記事を元にRAGに入門してみてはいかがでしょうか。

参考文献

MIXI DEVELOPERS Tech Blog

Discussion