[アップデート] Amazon Bedrock Managed Knowledge Basesでメタデータの項目を検索の対象にできるようになったので試してみた

[アップデート] Amazon Bedrock Managed Knowledge Basesでメタデータの項目を検索の対象にできるようになったので試してみた

Amazon Bedrock Managed Knowledge Basesでメタデータを検索対象にできるようになりました! メタデータフィルタリングとの違いを簡単に整理してみました!
2026.11.09

はじめに

こんにちは、ラーメンが好きなコンサルティング部の神野です。

2026年11月6日のAPI更新でAmazon Bedrock Managed Knowledge Baseのメタデータの項目を検索の対象にできるようになりました!

https://docs.aws.amazon.com/help-panel/bedrock/latest/console/hp-kb-managed-custom-metadata.html

今までもメタデータフィルタリング自体はできていましたよね。何がアップデートしたんでしょうね!

何が嬉しいか

これまでメタデータは検索の前にフィルターで文書を絞り込むために使うものでした。
作成者やプロジェクト名のような情報をメタデータに持たせていても本文に名前がなければその名前を手がかりに順位を上げることはできず、名前で探すにはアプリ側でフィルター条件を組み立てる必要がありました。

メタデータフィルターではアプリが質問からauthorの条件を組み立てて条件に合う文書だけが返る。検索可能なメタデータでは質問文をそのまま検索クエリにして山田花子さんの報告が1位になりほかの文書も下位に残る

今回からはメタデータの項目ごとに検索可能かどうかといった設定が追加されました!
検索可能にした項目は検索クエリと照合されて順位の計算にも使われるので、「山田花子さんが書いた月次報告」のように検索すると本文に名前がない文書まで見つかるのは嬉しいですね!

フィルターと違って条件に合わない文書を切り捨てるのではなく、順位を上げる仕組みです。冒頭の公式ドキュメントでもユーザーが検索に使いそうな作成者・カテゴリ・状態などの項目を検索可能にして絞り込みにしか使わない項目はオフのままにするよう案内されています。

イメージとしてはこんな感じです。

利用者の検索を受けたManaged Knowledge Baseが文書の本文に加えて検索可能にしたメタデータの項目とも照合する図

設定はUpdateKnowledgeBase APIの metadataConfigurations で行います。
作成者(author)とプロジェクト(projects)のメタデータを検索可能にする場合は次のように書きます。

metadataConfigurationsの指定例
metadataConfigurations=[
    {"name": "author", "type": "STRING", "searchability": "ENABLED"},
    {"name": "projects", "type": "STRING_LIST", "searchability": "ENABLED"},
]

項目ごとに名前・型・検索可否(searchability)を指定します。
使用できる型は文字列(STRING)か文字列リスト(STRING_LIST)の2種類で、1つのナレッジベースには最大30項目まで設定できます。

https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_UpdateKnowledgeBase.html

今のところ設定できるのはUpdateKnowledgeBase APIだけで作成時のCreateKnowledgeBase APIでは指定できません。CreateKnowledgeBaseで変わったのはレスポンスに設定内容が含まれるようになった点だけなので、ナレッジベースを作成してから更新で設定する流れになります。作成と同時に設定できるとうれしいのですが・・・近いうちに対応すると期待しています!

試してみる

架空の月次報告を4件用意しました。本文はどれも似た内容にして作成者とプロジェクト名はメタデータに持たせています。

ドキュメントID author(作成者) projects(プロジェクト)
report-01 山田花子 コハク, ルリ
report-02 佐藤健 ヒスイ
report-03 鈴木一郎 メノウ
report-04 高橋美咲 サンゴ

この4件を取り込んだ状態で次の2つを検索して検索可能にする前と後で順位を比べます。

  • 山田花子さんが書いた月次報告
  • コハクの進捗

検証環境

  • リージョンは us-east-1
  • Python 3.15.2
  • boto3 1.43.126 以降が必要です
環境のセットアップ
mkdir kb-metadata-search && cd kb-metadata-search
uv init --bare
uv add "boto3>=1.43.126"

続いて以下のコードを demo.py として保存します。
ナレッジベースの作成から文書の取り込み・検索・設定の変更・後片付けまでをサブコマンドで実行できます。

demo.pyの全体
demo.py
import json
import sys
import time
from pathlib import Path

import boto3

REGION = "us-east-1"
STATE = Path("state.json")
agent = boto3.client("bedrock-agent", region_name=REGION)
runtime = boto3.client("bedrock-agent-runtime", region_name=REGION)
iam = boto3.client("iam", region_name=REGION)

# 本文には作成者もプロジェクト名も書かず、メタデータにだけ持たせる
REPORTS = [
    {
        "id": "report-01",
        "author": "山田花子",
        "projects": ["コハク", "ルリ"],
        "text": "10月の月次報告です。問い合わせ対応の件数は前月より12%減りました。FAQページを改修した影響と見ています。来月はチャットボットの回答精度の改善に取り組みます。",
    },
    {
        "id": "report-02",
        "author": "佐藤健",
        "projects": ["ヒスイ"],
        "text": "10月の月次報告です。問い合わせ対応の件数は前月より5%増えました。新機能のリリース直後に質問が集中したためです。来月はリリースノートの書き方を見直します。",
    },
    {
        "id": "report-03",
        "author": "鈴木一郎",
        "projects": ["メノウ"],
        "text": "10月の月次報告です。問い合わせ対応の件数は前月とほぼ同じでした。夜間の一次回答をチャットボットに任せる準備を進めています。来月は回答テンプレートを整理します。",
    },
    {
        "id": "report-04",
        "author": "高橋美咲",
        "projects": ["サンゴ"],
        "text": "10月の月次報告です。問い合わせ対応の件数は前月より8%減りました。よくある質問を社内ポータルに掲載したためです。来月は担当者向けの研修資料を作ります。",
    },
]

QUERIES = ["山田花子さんが書いた月次報告", "コハクの進捗"]

def save(name, data):
    Path("evidence").mkdir(exist_ok=True)
    Path("evidence", name + ".json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2, default=str)
    )

def state():
    return json.loads(STATE.read_text())

def wait_kb(kb_id):
    for _ in range(120):
        kb = agent.get_knowledge_base(knowledgeBaseId=kb_id)["knowledgeBase"]
        print("kb status", kb["status"], flush=True)
        if kb["status"] == "ACTIVE":
            return kb
        if kb["status"] in ("FAILED", "UPDATE_UNSUCCESSFUL"):
            raise RuntimeError(kb.get("failureReasons"))
        time.sleep(10)
    raise RuntimeError("timed out")

def setup():
    if STATE.exists():
        raise RuntimeError("state.json already exists")
    account = boto3.client("sts").get_caller_identity()["Account"]
    name = "kb-meta-search-" + str(int(time.time()))
    trust = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "bedrock.amazonaws.com"},
                "Action": "sts:AssumeRole",
                "Condition": {
                    "StringEquals": {"aws:SourceAccount": account},
                    "ArnLike": {
                        "aws:SourceArn": f"arn:aws:bedrock:{REGION}:{account}:knowledge-base/*"
                    },
                },
            }
        ],
    }
    role = iam.create_role(RoleName=name, AssumeRolePolicyDocument=json.dumps(trust))
    s = {"name": name, "roleArn": role["Role"]["Arn"]}
    STATE.write_text(json.dumps(s))
    time.sleep(15)
    kb = agent.create_knowledge_base(
        name=name,
        roleArn=s["roleArn"],
        knowledgeBaseConfiguration={
            "type": "MANAGED",
            "managedKnowledgeBaseConfiguration": {"embeddingModelType": "MANAGED"},
        },
    )["knowledgeBase"]
    s["knowledgeBaseId"] = kb["knowledgeBaseId"]
    STATE.write_text(json.dumps(s))
    save("created-kb", kb)
    wait_kb(s["knowledgeBaseId"])
    ds = agent.create_data_source(
        knowledgeBaseId=s["knowledgeBaseId"],
        name="custom",
        dataSourceConfiguration={
            "type": "MANAGED_KNOWLEDGE_BASE_CONNECTOR",
            "managedKnowledgeBaseConnectorConfiguration": {
                "connectorParameters": {"type": "CUSTOM", "version": "1"}
            },
        },
    )["dataSource"]
    s["dataSourceId"] = ds["dataSourceId"]
    STATE.write_text(json.dumps(s))
    print(json.dumps(s))

def ingest():
    s = state()
    documents = [
        {
            "content": {
                "dataSourceType": "CUSTOM",
                "custom": {
                    "customDocumentIdentifier": {"id": r["id"]},
                    "sourceType": "IN_LINE",
                    "inlineContent": {
                        "type": "TEXT",
                        "textContent": {"data": r["text"]},
                    },
                },
            },
            "metadata": {
                "type": "IN_LINE_ATTRIBUTE",
                "inlineAttributes": [
                    {
                        "key": "author",
                        "value": {"type": "STRING", "stringValue": r["author"]},
                    },
                    {
                        "key": "projects",
                        "value": {
                            "type": "STRING_LIST",
                            "stringListValue": r["projects"],
                        },
                    },
                ],
            },
        }
        for r in REPORTS
    ]
    params = {k: s[k] for k in ["knowledgeBaseId", "dataSourceId"]}
    agent.ingest_knowledge_base_documents(**params, documents=documents)
    ids = [{"dataSourceType": "CUSTOM", "custom": {"id": r["id"]}} for r in REPORTS]
    for _ in range(60):
        details = agent.get_knowledge_base_documents(
            **params, documentIdentifiers=ids
        )["documentDetails"]
        statuses = [d["status"] for d in details]
        print("documents", statuses, flush=True)
        if all(x == "INDEXED" for x in statuses):
            return
        time.sleep(10)
    raise RuntimeError("timed out")

def search(label="search"):
    s = state()
    out = {}
    for q in QUERIES:
        resp = runtime.retrieve(
            knowledgeBaseId=s["knowledgeBaseId"],
            retrievalQuery={"text": q},
            retrievalConfiguration={
                "managedSearchConfiguration": {"numberOfResults": 4}
            },
        )
        out[q] = resp["retrievalResults"]
        print("Q:", q)
        for i, r in enumerate(resp["retrievalResults"], 1):
            m = r.get("metadata", {})
            print(
                f"  [{i}] score={r.get('score', 0):.4f} author={m.get('author')} projects={m.get('projects')}"
            )
    save(label, out)

def enable():
    s = state()
    kb = agent.get_knowledge_base(knowledgeBaseId=s["knowledgeBaseId"])[
        "knowledgeBase"
    ]
    resp = agent.update_knowledge_base(
        knowledgeBaseId=s["knowledgeBaseId"],
        name=kb["name"],
        roleArn=kb["roleArn"],
        knowledgeBaseConfiguration=kb["knowledgeBaseConfiguration"],
        metadataConfigurations=[
            {"name": "author", "type": "STRING", "searchability": "ENABLED"},
            {"name": "projects", "type": "STRING_LIST", "searchability": "ENABLED"},
        ],
    )
    save("update", resp)
    kb = wait_kb(s["knowledgeBaseId"])
    save("kb-after-update", kb)
    print(json.dumps(kb.get("metadataConfigurations"), ensure_ascii=False))

def cleanup():
    s = state()
    agent.delete_knowledge_base(knowledgeBaseId=s["knowledgeBaseId"])
    for _ in range(180):
        try:
            agent.get_knowledge_base(knowledgeBaseId=s["knowledgeBaseId"])
        except agent.exceptions.ResourceNotFoundException:
            break
        time.sleep(5)
    iam.delete_role(RoleName=s["name"])
    STATE.unlink()
    print("Knowledge Base and IAM role deleted")

if __name__ == "__main__":
    cmd = sys.argv[1]
    if cmd == "search":
        search(sys.argv[2] if len(sys.argv) > 2 else "search")
    else:
        {"setup": setup, "ingest": ingest, "enable": enable, "cleanup": cleanup}[cmd]()

メタデータは取り込み時に文書ごとに付け、文字列のリストは STRING_LIST 型で渡します。

demo.py(メタデータを付ける箇所の抜粋)
"metadata": {
    "type": "IN_LINE_ATTRIBUTE",
    "inlineAttributes": [
        {"key": "author", "value": {"type": "STRING", "stringValue": "山田花子"}},
        {
            "key": "projects",
            "value": {"type": "STRING_LIST", "stringListValue": ["コハク", "ルリ"]},
        },
    ],
},

ナレッジベースを作って4件を取り込みます。

リソースの作成と文書の取り込み
uv run demo.py setup
uv run demo.py ingest

検索可能にする前

まずは何も設定していない状態で検索してみます。

検索
uv run demo.py search before
実行結果
Q: 山田花子さんが書いた月次報告
  [1] score=0.3517 author=鈴木一郎 projects=['メノウ']
  [2] score=0.2934 author=高橋美咲 projects=['サンゴ']
  [3] score=0.2141 author=山田花子 projects=['コハク', 'ルリ']
Q: コハクの進捗
  [1] score=0.3342 author=佐藤健 projects=['ヒスイ']
  [2] score=0.2883 author=高橋美咲 projects=['サンゴ']
  [3] score=0.1614 author=鈴木一郎 projects=['メノウ']

山田花子さんの報告は3位にとどまり本文が似ているほかの報告が上位になりました。
「コハクの進捗」の検索では山田花子さんの報告はヒットしません。

メタデータの項目を検索可能にする

UpdateKnowledgeBase APIでauthorとprojectsの検索可否を ENABLED にします。

demo.py(設定を変更する箇所の抜粋)
kb = agent.get_knowledge_base(knowledgeBaseId=s["knowledgeBaseId"])["knowledgeBase"]
agent.update_knowledge_base(
    knowledgeBaseId=s["knowledgeBaseId"],
    name=kb["name"],
    roleArn=kb["roleArn"],
    knowledgeBaseConfiguration=kb["knowledgeBaseConfiguration"],
    metadataConfigurations=[
        {"name": "author", "type": "STRING", "searchability": "ENABLED"},
        {"name": "projects", "type": "STRING_LIST", "searchability": "ENABLED"},
    ],
)

UpdateKnowledgeBaseでは名前・ロール・構成も必須なのでGetKnowledgeBaseで取得した今の値をそのまま渡しています。あとから項目を足すときは既存の項目も一緒に指定します。metadataConfigurations は渡したリストで丸ごと置き換わります。

設定の変更
uv run demo.py enable
実行結果
kb status UPDATING
kb status ACTIVE
[{"name": "author", "type": "STRING", "searchability": "ENABLED"}, {"name": "projects", "type": "STRING_LIST", "searchability": "ENABLED"}]

コマンドの実行から約12秒で ACTIVE に戻りました。文書の取り込み直しはせずにこのまま検索してみます。

検索可能にした後

検索
uv run demo.py search after
実行結果
Q: 山田花子さんが書いた月次報告
  [1] score=0.4371 author=山田花子 projects=['コハク', 'ルリ']
  [2] score=0.1890 author=鈴木一郎 projects=['メノウ']
  [3] score=0.1535 author=高橋美咲 projects=['サンゴ']
Q: コハクの進捗
  [1] score=0.4840 author=山田花子 projects=['コハク', 'ルリ']
  [2] score=0.1538 author=佐藤健 projects=['ヒスイ']
  [3] score=0.1536 author=鈴木一郎 projects=['メノウ']

どちらの検索でも山田花子さんの報告が1位になりました!

検索可能にする前と後の順位の比較。山田花子さんが書いた月次報告の検索では3位から1位へ上がりコハクの進捗の検索では圏外から1位へ上がった

山田花子さんの報告のスコアは0.21から0.44に上がり、コハクの方も圏外から0.48になって2位以下と差が付いています。

projectsは文字列のリストですが2つの値のうち「コハク」だけをクエリに含めた場合でも正しく照合されていますね。取り込み済みの文書にも設定の変更がすぐ反映されたのはよいですね。

言い換えでも検索可能?

ここで気になったのはメタデータも意味の近さで検索されるのかどうか気になりました。
ここまでの検索はメタデータの値と文字が一致していたので一致しない言い換えでも試してみました。

前半のナレッジベースを削除したあとで部署(department)の項目を足したdemo.pyを使ってナレッジベースを新しく作り直し、4件に人事部・経理部・法務部・情報システム部を持たせました。何も設定していない状態で検索してから、author・projects・departmentの3項目をまとめて検索可能にしてもう一度検索しています。

demo.pyからの変更点と実行結果

REPORTS の各報告に部署を足して取り込み時のメタデータと検索可能にする項目にも加えました。検索結果の表示にも部署を出しています。

demo.py(変更箇所の抜粋)
# REPORTS の各報告に部署を追加(report-01から順に人事部・経理部・法務部・情報システム部)
"department": "経理部",

# ingest() の inlineAttributes に追加
{"key": "department", "value": {"type": "STRING", "stringValue": r["department"]}},

# 検索するクエリ
QUERIES = [
    "経理部の月次報告",
    "お金の出入りを管理する部署の月次報告",
    "契約書をチェックする部署の報告",
    "琥珀の進捗",
    "Hanako Yamada report",
]

# enable() の metadataConfigurations に追加
{"name": "department", "type": "STRING", "searchability": "ENABLED"},

# search() の表示に部署を追加
print(
    f"  [{i}] score={r.get('score', 0):.4f} author={m.get('author')} dept={m.get('department')} projects={m.get('projects')}"
)

実行順は前半と同じです。

追加検証の実行順
uv run demo.py setup
uv run demo.py ingest
uv run demo.py search before
uv run demo.py enable
uv run demo.py search after
uv run demo.py cleanup
検索可能にする前(抜粋)
Q: 経理部の月次報告
  [3] score=0.1592 author=佐藤健 dept=経理部 projects=['ヒスイ']
Q: お金の出入りを管理する部署の月次報告
  [3] score=0.1820 author=佐藤健 dept=経理部 projects=['ヒスイ']
Q: 契約書をチェックする部署の報告
  [3] score=0.1780 author=鈴木一郎 dept=法務部 projects=['メノウ']
検索可能にした後(抜粋)
Q: 経理部の月次報告
  [1] score=0.4642 author=佐藤健 dept=経理部 projects=['ヒスイ']
  [2] score=0.2342 author=高橋美咲 dept=情報システム部 projects=['サンゴ']
Q: お金の出入りを管理する部署の月次報告
  [1] score=0.3474 author=佐藤健 dept=経理部 projects=['ヒスイ']
  [2] score=0.3278 author=高橋美咲 dept=情報システム部 projects=['サンゴ']
Q: 契約書をチェックする部署の報告
  [3] score=0.1749 author=鈴木一郎 dept=法務部 projects=['メノウ']
Q: 琥珀の進捗
  [1] score=0.3938 author=高橋美咲 dept=情報システム部 projects=['サンゴ']
  [2] score=0.2020 author=佐藤健 dept=経理部 projects=['ヒスイ']
  [3] score=0.1685 author=鈴木一郎 dept=法務部 projects=['メノウ']
Q: Hanako Yamada report
  [1] score=0.3558 author=高橋美咲 dept=情報システム部 projects=['サンゴ']
  [2] score=0.2760 author=鈴木一郎 dept=法務部 projects=['メノウ']
  [3] score=0.1510 author=佐藤健 dept=経理部 projects=['ヒスイ']

検索クエリと狙ったメタデータの値の組み合わせごとの結果。文字が一致する経理部の月次報告は3位から1位に上がり言い換えのクエリはお金の出入りを除いて順位が変わらない

文字が一致する「経理部」は3位から1位に上がりましたが、一方で法務部・琥珀・ローマ字の言い換えでは順位が変わりませんでした。「お金の出入り」では経理部の報告が1位になったものの2位との差は0.02です。

今回の検証では文字が一致するクエリで順位が上がりましたが、「琥珀」「Hanako Yamada」のような言い換えは狙った報告が検索結果に現れませんでした・・・
表記が分かれそうな項目は検索で使われそうな表記を値に入れておくのがよさそうです。

あくまで今回私が試した範囲の所感ですので、実際にメタデータを設計および検索する際に有用かどうかはご判断ください!

後片付け

最後にナレッジベースとIAMロールを削除します。

リソースの削除
uv run demo.py cleanup

おわりに

フィルター条件を組み立てなくても作成者やプロジェクト名で文書を探せるようになりました!
検索条件として使用したいケースは一度試してみるのが良いと思います!

本記事が参考になりましたら幸いです!最後までご覧いただきありがとうございましたー!!

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事