AWS Context Ontology Accelerator(COA)で日本語のオントロジーを構築してみた - 多言語検索等のサポート

AWS Context Ontology Accelerator(COA)で日本語のオントロジーを構築してみた - 多言語検索等のサポート

AWS の OSS「Context Ontology Accelerator」v0.2.2 で、日本語のような分かち書きしない言語の質問がオントロジーのラベルに到達できるようになりました。日本語データを用意して、実際に多言語検索を試してみた結果をお伝えします。
2026.08.30

クラウド事業統括本部の石川です。AWS の OSS「Context Ontology Accelerator」に v0.2.2 がリリースされました。日本語のような分かち書きしない言語(語を空白で区切らない言語)の質問が、格納済みのラベルに到達できるようになったことが大きな変更です。日本語データを用意して、多言語検索を実際に試してみました。

https://x.com/inada_riku/status/2093150194779218343

前回 v0.2.0 を試した際に「Tier 1 の同義語マッチを日本語などの非空白区切り言語に対応させてほしい」と書きました。その周辺が v0.2.2 でどこまで動くようになったのかも、あわせて確認します。

2週間前に書いた以下のブログでは、オントロジー、Context Ontology Acceleratorについて、詳しく解説しています。本日は新たに追加された多言語検索のみを解説します。(それでこのボリュームとは...)

https://dev.classmethod.jp/articles/20260817-aws-context-v020/

Context Ontology Accelerator とは

Context Ontology Accelerator(以下 COA)は、AWS が公開している OSS です。Glue Data Catalog や JDBC データベースなどのデータソースからスキーマを読み取り、LLM でオントロジー(クラス・プロパティ・関連)を誘導(Induction)して Amazon Neptune に格納し、自然言語の質問をそのオントロジーを介して SQL や SPARQL に変換して答えます。エージェント向けに MCP サーバーも提供します。

質問への回答は 3 つの階層(Tier)に分かれています。質問はこの順にカスケードし、確信度が低いと次の段へ落ちます。

Tier 役割 実装の中心
Tier 1 定義済みメトリクスの解決 名前・同義語の正規表現マッチ
Tier 2 構造化データへのクエリ オントロジー経由の NL→SQL / Ontop(VKG: Virtual Knowledge Graph)
Tier 3 文書ナレッジグラフの探索 ベクトル検索 + グラフ探索 + 合成

v0.2.2 の多言語検索は、このうち Tier 2 の T-Box フォールバックと Tier 3 のキーワードエンティティ検索に入った変更です。

v0.2.2 の変更点

リリースノートの見出しを整理すると次のとおりです。

分類 内容
US 外へのデプロイ Bedrock のモデル ID がすべて SSM のデプロイ設定キーになった。解決済みの値がコンテナ環境変数・IAM 許可・コストダッシュボードを一箇所から駆動する
多言語クエリ理解 Tier 2 / Tier 3 のラベル照合が双方向になった。トークナイズが UAX #29 の書記素クラスタ単位に変わった
Tier 2 の戦略指定 options.strategybest / ontop / nl_to_sql / ontop_first / nl_to_sql_first / agentic を呼び出し側から固定できる
ガードレールの可観測性 GuardrailInvocationsDecision ディメンション(ALLOW / ANONYMIZED / BLOCK / UNKNOWN / MODEL_FILTERED)が追加
誘導レポート InductionReportdroppedTables が追加され、生成 LLM が失敗したテーブルが報告されるようになった
バグ修正 フィールドから報告された 3 件(#92 / #94 / #95)を含む。ドキュメントパイプラインから AGPL 依存が消えた

多言語検索まわりだけを取り出すと、次の 3 点です。

  • Tier 2 の T-Box フォールバックと Tier 3 のキーワードエンティティ検索に、逆方向のラベル包含(reverse containment)が入った
  • トークナイズが Unicode の拡張書記素クラスタ(UAX #29)単位になった
  • Tier 1 の残差修飾子ゲートがハングル音節を扱うようになった(Han / 仮名 / タイ文字は未対応と明記されている)

https://github.com/aws/context-ontology-accelerator/releases/tag/v0.2.2

多言語検索は何を変えたのか

何が問題だったのか

COA は質問文からキーワードを取り出し、グラフに格納されたラベルと照合します。v0.2.1 までの照合は順方向の 1 本だけでした。

CONTAINS(LCASE(?label), "検索語")     -- 格納済みラベルが検索語を含むか

この形が成立するには、質問文から「検索語」を切り出せている必要があります。ところが v0.2.1 のトークナイザは ASCII 前提でした。

# v0.2.1: packages/context-manager/src/coa_serve/query_utils.py
_ENTITY_RE = re.compile(r"\b[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\b")

Tier 3 側には、SPARQL に埋め込む直前のサニタイズゲートもありました。

# v0.2.1: packages/context-manager/src/coa_serve/tier3/graph_traverser.py
_SAFE_ENTITY_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")

どちらも非 ASCII を通しません。 日本語の質問はキーワードが 0 件になり、Tier 2 の T-Box フォールバックも Tier 3 のキーワード検索も、SPARQL を発行する前に空で返っていました。

双方向のラベル包含

v0.2.2 は、順方向はそのままに逆方向を足しました。

CONTAINS("質問文の連続領域", LCASE(STR(?label)))   -- 質問文が格納済みラベルを含むか

発想が逆転しています。分かち書きしない言語で「質問文をどこで切るか」を解くのではなく、グラフ側が正解の語をラベルとして持っていることを利用して、ラベルの方を質問文の中から探します。日本語用の形態素解析器も、助詞のストップワード表も要りません。

逆方向の対象になるのは、ラテン文字とギリシャ文字を除いた文字です。リポジトリのコメントに理由が書かれています。

  • ラテン文字は空白で語が区切れるので順方向で足りる。入れると 2 文字のラベルが英単語の内側に当たってしまう
  • ギリシャ文字は活用で語尾が書き換わるため、格納済みラベルが活用形の部分文字列にならない(νόμοςνόμου の中にない)
  • キリル文字は空白区切りだが、格変化が語幹に付加されるので逆方向が効く。そのため対象に含める
  • 漢字・仮名・タイ文字・クメール文字は語間に空白がなく、ハングル・デーヴァナーガリー・ベンガル文字・アラビア文字・ヘブライ文字は助詞や格語尾が語に直接くっつくため対象

なお、リポジトリのコメントは逆方向の限界も明記しています。ひとつの連続領域の中では、語の境界をまたいだラベルにも当たり得ます。日本語や中国語の質問では領域が質問文まるごとになるため、2 文字のラベルが隣り合う語の継ぎ目に一致する可能性が残ります。これを抑えているのは、ラベル 2 文字以上という最低長と行数上限だけです。

v0.2.1 と v0.2.2 を同じ質問で動かしてみる

説明だけでは差がわからないので、両バージョンの実物のコードを同じ質問に通しました。v0.2.1 の query_utils.pygit show で取り出し、v0.2.2 は clone したリポジトリのモジュールをそのまま読み込んでいます。

% git -C coa show v0.2.1:packages/context-manager/src/coa_serve/query_utils.py > scripts/query_utils_v021.py
% coa/.venv/bin/python scripts/compare_tokenizer.py
lang  question                                       v0.2.1 entities        v0.2.2 terms / containers
--------------------------------------------------------------------------------------------------------
ja    商品分類ごとの在庫数量を教えてください            []   terms=['商品分類', '在庫数'] containers=1
ja    配送ステータスが遅延している注文はいくつありますか  []   terms=['配送', 'ステータス', '遅延', '注文'] containers=1
ja    顧客区分別の売上合計を出してください              []   terms=['顧客区分別', '売上合計'] containers=1
zh    每个商品分类的库存数量是多少                      []   terms=['每个商品分类的库存数量是多少'] containers=1
ko    상품분류별 재고수량을 알려주세요                   []   terms=['상품분류별', '재고수량을', '알려주세요'] containers=3
th    จำนวนสินค้าคงคลังของแต่ละหมวดหมู่สินค้าคือเท่าใด  []   terms=['จำนวนสินค้าคงคลัง...'] containers=1
en    How many products are in stock per product category
                              ['products', 'stock', 'per', 'product', 'category']
                              terms=['products', 'stock', 'per', 'product', 'category'] containers=0

v0.2.1 は日本語・中国語・韓国語・タイ語のすべてで検索語が 0 件です。この時点で SPARQL は発行されず、空で返ります。v0.2.2 は語を取り出し、さらに逆方向用の連続領域(containers)を持ちます。英語は containers=0(ラテン文字は逆方向の対象外)です。

中国語と タイ語の行に注目してください。語の切れ目がないため、順方向の検索語は「質問全体」ひとつになります。この形では順方向の照合が成立しないので、逆方向が必須になります。日本語は漢字と仮名が切り替わるため、書記素クラスタ単位のトークナイズが偶然そこそこ効いています。

Tier 3 には、SPARQL に埋め込む直前のサニタイズゲートもあります。ここも同じ質問で比較しました。

keyword        v0.2.1     v0.2.2
----------------------------------------
商品分類           DROP       keep
在庫数            DROP       keep
顧客区分           DROP       keep
products       keep       keep
stock-level    keep       keep
Straße         DROP       keep
재고수량           DROP       keep
库存数量           DROP       keep

v0.2.1 の ^[a-z0-9][a-z0-9_-]{0,63}$ は、アクセント付きラテン文字(Straße)も落とします。v0.2.2 は「危険な文字(制御文字・行区切り)を弾く」方式に変わり、文字種の許可リストではなくなりました。

やってみた

前提条件

  • 検証環境: バージニア北部リージョン(us-east-1)
  • 使用タグ: v0.2.2
  • 検証日: 2026 年 8 月 30 日
  • AWS CLI v2(検証時のバージョンは 2.36.29)

リポジトリを取得します。

% git clone https://github.com/aws/context-ontology-accelerator.git coa
% cd coa && git checkout v0.2.2
% git describe --tags
v0.2.2

ローカルの要件は Python 3.12、Node.js 22 以上、pnpm、uv、Java 17 以上、Docker、AWS CLI v2 です。

% for c in node npm pnpm uv java python3 aws git docker; do printf '%-8s: ' "$c"; $c --version 2>&1 | head -1; done
node    : v22.23.2
npm     : 10.9.8
pnpm    : 10.30.3
uv      : uv 0.8.14 (Homebrew 2025-08-28)
java    : openjdk 25.0.2 2026-01-20
python3 : Python 3.12.7
aws     : aws-cli/2.36.29 Python/3.14.6 Darwin/25.6.0 exe/arm64
git     : git version 2.53.0
docker  : docker version 5.6.0

検証データの準備

日本語のラベルをグラフに入れるには、テーブル名とカラム名そのものを日本語にする必要があります。COA の誘導(Induction)は、テーブル名を owl:Classrdfs:label に、カラム名をプロパティの rdfs:label にそのまま入れるためです。

# packages/ontology-engine/src/coa_ontology/inducer/strategies/table_to_ontology.py
g.add((table_cls, RDFS.label, Literal(table.name)))
...
g.add((prop_uri, RDFS.label, Literal(col.name)))

そこで、オフィス用品を扱う商社を想定した 3 テーブルを日本語で用意しました。

テーブル カラム 行数
商品マスタ 商品コード / 商品名 / 商品分類 / 標準単価 / 在庫数量 / 取扱開始日 20
顧客マスタ 顧客コード / 顧客名 / 顧客区分 / 都道府県 / 登録日 15
受注明細 受注番号 / 受注日 / 顧客コード / 商品コード / 数量 / 受注金額 / 配送ステータス 80

業務ルールの文書 2 件(販売業務ポリシー、データ用語集)も日本語で用意しました。配送ステータスの 5 値の定義、顧客区分ごとの支払いサイト、商品分類ごとの発注点(什器は在庫数量 50、それ以外は 30)など、スキーマからは読み取れない情報を入れています。

まず、日本語のテーブル名・カラム名が Glue と Athena で扱えるのかを確かめます。ここが通らないと計画自体が成立しません。

glue.create_table(DatabaseName="coa_ja_probe", TableInput={
    "Name": "商品マスタ",
    "StorageDescriptor": {"Columns": [{"Name": "商品コード", "Type": "string"},
                                      {"Name": "商品名", "Type": "string"},
                                      {"Name": "商品分類", "Type": "string"}], ...},
    ...})
create_table OK   : 商品マスタ
create_table OK   : products_ja_cols

Glue Data Catalog には日本語のテーブル名・カラム名をそのまま登録できました。Athena からも読めます。

SELECT "商品名", "商品分類" FROM "coa_ja_probe"."商品マスタ" LIMIT 5
商品名           | 商品分類
ワイヤレスマウス   | 周辺機器
USB-Cハブ       | 周辺機器

ただし識別子は必ずダブルクォートで囲む必要があります。エイリアスを引用符なしで日本語にすると構文エラーです。

SELECT "配送ステータス", count(*) AS 件数 FROM "coa_blog_ja"."受注明細" GROUP BY "配送ステータス"
InvalidRequestException: line 1:31: mismatched input '件'. Expecting: <identifier>

この制約は、後で LLM が生成する SQL を見るときに効いてきます。

正解データの用意

回答を突き合わせるために、Athena で先に正解を出しておきます。

SELECT "配送ステータス", count(*) AS "件数", sum("受注金額") AS "受注金額合計"
FROM "coa_blog_ja"."受注明細" GROUP BY "配送ステータス" ORDER BY 2 DESC
配送ステータス | 件数 | 受注金額合計
配達完了     | 33  | 4526160
出荷済      | 22  | 4066120
出荷準備中    | 13  | 2625860
受注済      | 7   | 742440
遅延       | 5   | 381280

発注点(什器は 50、それ以外は 30)を下回る商品は 7 件です。この「什器だけしきい値が違う」というルールは文書にしか書いていません。

商品名                  | 商品分類 | 在庫数量
ワイヤレスマウス           | 周辺機器 | 0
書画カメラ               | 映像機器 | 7
USB 電源アダプタ          | 電源機器 | 12
昇降デスク               | 什器   | 18
4K ウェブカメラ           | 映像機器 | 22
ネットワークスイッチ 8ポート  | 通信機器 | 29
電子ホワイトボード          | 什器   | 42

モデル ID の設定が SSM に移った

前回の v0.2.0 では、誘導(Induction)に使うモデルを変えるために CDK のソースを直接編集しました。v0.2.2 ではすべての Bedrock モデル ID が SSM の /{prefix}/config のキーになっています。

設定キー 対象 既定値
bedrockLlmModelId Serve のクエリ LLM(NL→SPARQL、合成) us.anthropic.claude-sonnet-5
bedrockEmbedModelId すべての埋め込み us.cohere.embed-v4:0
bedrockEmbedDimensions 埋め込みの次元数 1024
bedrockInductionLlmModelId 誘導(Induction)、グラウンディング、説明生成 us.anthropic.claude-sonnet-5
bedrockChatModelId ソースのエンリッチメント、制約推論、文書抽出 us.anthropic.claude-haiku-4-5-20251001-v1:0

前回ソースを書き換えて Claude Sonnet 5 にした誘導(Induction)モデルは、v0.2.2 では既定値がすでに Sonnet 5 です。今回 us-east-1 に立てるので、設定するのは初期管理者のメールアドレスだけで済みました。

% aws ssm put-parameter --name /coa/config --type String --overwrite \
  --value '{"initialAdminEmail":"xxxxx@example.com"}'

初期管理者は CDK がデプロイ時に作ります。未設定だと nobody@amazon.com というプレースホルダーで作られてメールが届かないため、デプロイ前に入れておくのが正規ルートです。

なお bedrockEmbedModelIdbedrockEmbedDimensions初回デプロイ限定の設定です。次元数は OpenSearch のインデックス作成時に焼き込まれるため、データ投入後の変更は再取り込みになります。

デプロイ

デプロイについては、以前のブログをご覧ください。

https://dev.classmethod.jp/articles/20260817-aws-context-v020/#%25E3%2583%2587%25E3%2583%2597%25E3%2583%25AD%25E3%2582%25A4

日本語カラム名でオントロジーが潰れる

生成された提案(proposal)の TTL(Turtle 形式の RDF ファイル)を取得して中身を見ます。

% aws s3 cp "s3://coa-dev-ontology-artifacts-123456789012/proposals/$NS/$PID/latest/ontology.ttl" -

ind:entity_ a owl:DatatypeProperty ;
    rdfs:label "受注日",
        "受注番号",
        "受注金額",
        "商品コード",
        "数量",
        "配送ステータス",
        "顧客コード" ;
    rdfs:comment "受注した個数",
        "受注を登録した日",
        "受注明細の主キー。O + 4桁の連番",
        ... ;
    rdfs:domain ind:Entity ;
    rdfs:range xsd:integer,
        xsd:string .

ind:Entity a owl:Class ;
    rdfs:label "受注明細" .

ind:Entity_b121102e a owl:Class ;
    rdfs:label "顧客マスタ" .

ind:Entity_fd703afe a owl:Class ;
    rdfs:label "商品マスタ" .

ラベルは日本語のまま入りました。 ここは狙いどおりです。ところが、18 個あるはずのプロパティが 3 個しかありません。1 テーブルのカラムが全部ひとつの IRI(ind:entity_)に潰れ、7 個のラベルと 7 個のコメントがぶら下がっています。

原因は IRI のローカル名を作る関数です。

# packages/ontology-engine/src/coa_ontology/inducer/strategies/table_to_ontology.py
def _to_pascal(s: str) -> str:
    return "".join(w.capitalize() for w in re.split(r"[\s_\-]+", re.sub(r"[^a-zA-Z0-9\s_\-]", "", s)) if w)

非 ASCII を除去するため、日本語のカラム名はここで空文字になります。プロパティ IRI は ns[f"{local}_{_to_camel(column_name)}"] で組み立てられるので、同じテーブルのカラムが全部同じ IRI になるわけです。

同じ関数を手元で動かすと、こうなります。

column '商品コード'   -> _to_camel=''  property local = 'entity_'
column '商品名'      -> _to_camel=''  property local = 'entity_'
column '在庫数量'     -> _to_camel=''  property local = 'entity_'
column 'product_code' -> _to_camel='productCode'  property local = 'entity_productCode'

クラス側は無事です。テーブル名も同じく空文字になりますが、こちらは to_pascal"Entity" フォールバックがあり、さらに pascal_names_for衝突を検出してハッシュ接尾辞を付けるためです。プロパティ側にはこの衝突解決が入っていません。

実害はクエリに出ます。NL→SQL に渡るスキーマ文脈が「1 テーブル 1 カラム」になり、しかもラベルとコメントの対応もずれました。

"context_preview": "Table: 受注明細 | Columns: 受注日:integer (受注した個数)
                    Table: 顧客マスタ | Columns: 登録日:string (与信限度額と請求サイクルの区分...)
                    Table: 商品マスタ | Columns: 取扱開始日:integer (商品の名称)"

この状態では Tier 2 が確信度 0.1 まで落ち、Tier 3 へフォールバックして API Gateway の 29 秒制限に当たりました。

カラム名だけ ASCII にして作り直す

同じ CSV に対して、テーブル名は日本語のまま、カラム名だけ ASCII(日本語のコメント付き)の Glue テーブルを別データベースに作り、もう一度誘導(Induction)しました。

% python3 scripts/create_glue_tables_ascii.py
created table: coa_blog_ja_ascii.商品マスタ (6 columns)
created table: coa_blog_ja_ascii.顧客マスタ (5 columns)
created table: coa_blog_ja_ascii.受注明細 (7 columns)

結果は次のとおりです。

データベース クラス数 プロパティ数 クラスのラベル
coa_blog_ja(テーブル名・カラム名とも日本語) 3 3 日本語
coa_blog_ja_ascii(カラム名のみ ASCII) 3 18 日本語

プロパティが正しく 18 個生成され、クラスのラベルは日本語のままです。日本語で検索したいなら、日本語にすべきはテーブル名(=クラスのラベル)で、カラム名は ASCII にしておくのが現時点では無難という結論になりました。以降の検証はこちらのネームスペースで進めます。

日本語で問い合わせる

scripts/run_query.py は私が今回の検証用に書いたローカルのヘルパーです。まず、構造化データだけで答えられる質問です。正解は Athena で出した 5 件でした。

% python3 scripts/run_query.py "配送ステータスが遅延している受注は何件ありますか"
tier      : 2.0
confidence: {"score": 0.9, "rationale": "LLM-generated SQL from ontology retrieval"}
rowCount  : 1
rows      : [{"order_count": "5"}]

queryUsed:
SELECT COUNT(*) AS order_count
FROM "受注明細"
WHERE delivery_status = '遅延'

--- トレース ---
  routing.select             success        0ms  {"gating": "source_composition", "hasStructuredSource": true, "hasUnstructuredSource": false, "skipped": ["tier3_vector_search"]}
  t1.metric_match            miss         123ms  {"query_length": 24}
  query.embed                success      223ms [bedrock]  {"dimensions": 1024}
  t2.sql.generate            success     3362ms [bedrock]  {"confidence": 1.0, "tables": ["受注明細", "商品マスタ", "顧客マスタ"]}
  t2.sql.authorize           allow         71ms [cedar]
  t2.sql.firewall            success       71ms [sql-firewall]
  t2.sql.execute             error        120ms  {"error": "InvalidRequestException", "message": "... line 2:6: mismatched input '", "shot": 1}
  t2.sql.generate            success     1827ms [bedrock]  {"confidence": 0.9, "correction_shot": 2}
  t2.sql.authorize           allow          1ms [cedar]
  t2.sql.firewall            success        1ms [sql-firewall]
  t2.sql.execute             success     1789ms  {"rowCount": 1, "shot": 2, "engine": "athena"}

正解と一致しました。 ただしトレースを見ると、1 回目の SQL が Athena で構文エラーになっています。日本語のテーブル名を引用符で囲み忘れたためで、2 ショット目の自己修正で FROM "受注明細" と直って通りました。データ準備のときに確認した「日本語の識別子はダブルクォート必須」がそのまま出ています。

集約と JOIN も試します。

% python3 scripts/run_query.py "商品分類ごとの在庫数量の合計を教えてください"
rows : [{"product_category": "通信機器", "total_stock_quantity": "470"},
        {"product_category": "映像機器", "total_stock_quantity": "310"},
        {"product_category": "什器",   "total_stock_quantity": "230"},
        {"product_category": "電源機器", "total_stock_quantity": "222"},
        {"product_category": "記憶装置", "total_stock_quantity": "195"},
        {"product_category": "周辺機器", "total_stock_quantity": "192"},
        {"product_category": "音響機器", "total_stock_quantity": "120"}]

% python3 scripts/run_query.py "顧客区分別の受注金額の合計を多い順に教えてください"
rows : [{"customer_segment": "官公庁",   "total_order_amount": "4451960"},
        {"customer_segment": "教育機関",  "total_order_amount": "2864980"},
        {"customer_segment": "個人事業主", "total_order_amount": "2179420"},
        {"customer_segment": "法人小口",  "total_order_amount": "1992460"},
        {"customer_segment": "法人大口",  "total_order_amount": "853040"}]

queryUsed:
SELECT c.customer_segment, COALESCE(SUM(o.order_amount), 0) AS total_order_amount
FROM "受注明細" o
JOIN "顧客マスタ" c ON o.customer_code = c.customer_code
GROUP BY c.customer_segment
ORDER BY total_order_amount DESC

どちらも Athena で出した正解と一致しました。外部キー制約は一切定義していませんが、エンリッチメントが推論した外部キーから JOIN が組み立てられています。

v0.2.2 の options.strategy を日本語データで比べる

v0.2.2 では Tier 2 の戦略を呼び出し側から固定できるようになりました。同じ質問を ontop(VKG 経由)と nl_to_sql(フラットな NL→SQL)で比べます。

% python3 scripts/run_query_direct.py "顧客区分別の受注金額の合計を多い順に教えてください" --strategy ontop
tier: 2.0 | confidence: {"score": 0.8, "rationale": "NL-to-SPARQL translation"}

queryUsed:
SELECT V5."customer_segment1m3" AS "customer_segment1m3", SUM(V5."order_amount1m17") AS "sum1"
FROM (SELECT DISTINCT ... FROM "受注明細" AS V1, "受注明細" AS V2, "顧客マスタ" AS V3 WHERE ...) AS V5
GROUP BY V5."customer_segment1m3" ORDER BY SUM(V5."order_amount1m17") DESC

--- トレース ---
  t2.vkg.context             success      149ms [tbox-builder]     "3 classes, 18 properties"
  t2.vkg.translate           success     5184ms [bedrock]          "confidence=0.85"
  t2.vkg.validate            success       50ms [sparql-validator] "passed"
  t2.vkg.compile             success      255ms [ontop]            {"dialect": "trino", "tables": ["受注明細", "顧客マスタ"]}
  t2.vkg.execute             success     1857ms [sql-engine]       {"row_count": 5}

結果はどちらも正解でしたが、生成の質が違いますontop は LLM が書くのは SPARQL までで、SQL は Ontop が R2RML マッピングから機械的に組み立てます。そのため日本語のテーブル名も最初から "受注明細" と正しく引用され、構文エラーによる再生成が起きませんでした。nl_to_sql は SQL 本文まで LLM が書くため、引用忘れが起こります。

日本語識別子のスキーマでは、options.strategyontop を固定すると引用忘れの失敗モードを避けられる、というのが今回の実測から言えることです。

文書にしか書いていないルールを聞く

発注点のしきい値(什器は 50、それ以外は 30)は、業務文書にしか書いていません。まず文書を入れる前に聞いてみます。

% python3 scripts/run_query.py "発注点を下回っている商品を教えてください"
tier      : 2.0
confidence: {"score": 0.4, "rationale": "LLM-generated SQL from ontology retrieval"}
rowCount  : 1
rows      : [{"product_code": "P001", "product_name": "ワイヤレスマウス", "stock_quantity": "0"}]

queryUsed:
SELECT DISTINCT product_code, product_name, stock_quantity
FROM "商品マスタ"
WHERE stock_quantity <= 0

「発注点」を「在庫 0」と解釈しました。 正解は 7 件です。確信度が 0.4 と低く出ている点は救いですが、エラーではなく答えとして返ってきます。

日本語の業務文書 2 件をドキュメントソースとして追加します。

% curl -sS -X POST "$API/namespaces/$NS/sources" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{
    "sourceType": "DOCUMENTS",
    "documentSource": {"name": "ja-business-docs",
      "sourceBucketArn": "arn:aws:s3:::coa-blog-ja-123456789012-us-east-1",
      "s3Prefixes": ["docs/"]}}'

取り込みは 7 分ほどかかりました。

16:36:40Z  REGISTERED
16:36:57Z  SCANNING
16:37:48Z  SCANNING_ENTITY_EXTRACTION
16:43:42Z  COMPLETED

Tier 3 を明示して、文書側の知識が取れているか確認します。

% python3 scripts/run_query_direct.py "発注点の定義と、商品分類ごとのしきい値を教えてください" --tier 3 --mode agentic
tier: 3.0 | confidence: {"score": 0.85, "rationale": "Agentic synthesis"}
## 発注点の定義

データグロッサリー(data-glossary-ja.txt)によると、「発注点」は以下のように定義されています。
- 発注点とは、在庫を補充するための在庫数量のしきい値である
- 発注点は商品分類によって異なる値が設定される

## 商品分類ごとのしきい値

| 商品分類 | 発注点 |
|---|---|
| 什器 | 50個未満で発注点(什器は嵩張るため個別に設定) |
| その他すべての商品分類 | 30個未満で発注点 |

文書に書いたルールを正しく拾えました。

supportingContent を見ると、抽出された命題は日本語と英語が混在しています。

{"topic": "青葉オフィスサプライ販売業務ポリシーの配送ステータス定義",
 "statements": ["受注済 is the state immediately after registering an order",
                "受注明細の配送ステータス is a classification system"]}
{"topic": "Product Classification and Inventory Management Policies",
 "statements": ["Fixtures have large volume, therefore the reorder point is set when inventory quantity falls below 50 units.",
                "All other product classifications have an inventory reorder point of 30 units."]}

前回 v0.2.0 を試したときは日本語文書の命題がすべて英語に正規化されていました。今回は日本語の用語(受注済、配送ステータス、発注点など)がそのまま残った命題も生成されています。文書側のナレッジグラフに日本語のラベルが入るため、多言語検索の対象になり得ます。

なお、Tier 3 まで到達するクエリは API Gateway の 29 秒制限に当たります。実際に何度か 504 を受けました。

HTTP 504
{"message": "Endpoint request timed out"}

MCP サーバーが Context Manager を呼ぶ経路には API Gateway が入らないため、そちらは 120 秒の余裕があります(CM_INVOKE_TIMEOUT_S)。今回は同じ経路(AgentCore Runtime の invocations エンドポイント + Bearer JWT)を直接叩いて確認しました。agentic モードの実測は 117 秒でした。

逆方向のラベル包含を実データで検証する

ここまでの日本語クエリは、ベクトル検索(Cohere Embed v4)とスキーマ文脈で回答できました。v0.2.2 が追加した逆方向のラベル包含が実際にどう効くのかを、実機が生成したオントロジーに対して確かめます。

提案の TTL をローカルに取得し、rdflib に読み込んで、v0.2.1 の FILTER と v0.2.2 の FILTER を同じデータに対して実行しました。FILTER の文字列は、どちらもリポジトリ本体の関数(build_query_search_plan / escape_sparql_string_literal / normalize_label_match_text、v0.2.1 側は extract_query_entities_sparql_escape_string)から組み立てています。

読み込んだグラフに入っているクラスのラベルは「受注明細」「商品マスタ」「顧客マスタ」の 3 つです(accept 済みなので Neptune にも同じものが入っています)。

% python3 scripts/sparql_ab.py artifacts/ontology_ascii.ttl "受注明細表の配送状況を集計してください"

## 質問: 受注明細表の配送状況を集計してください
  v0.2.1 FILTER      : (検索語ゼロ件のため SPARQL を発行せず終了)
  v0.2.1 ヒット       : 0 []
  v0.2.2 順方向のみ      : 0 []
    FILTER: CONTAINS(LCASE(?label), "受注明細表") || CONTAINS(LCASE(?label), "配送状況") || CONTAINS(LCASE(?label), "集計")
  v0.2.2 逆方向のみ      : 1 ['受注明細']
    FILTER: (STRLEN(STR(?label)) >= 2 && CONTAINS("受注明細表の配送状況を集計してください", LCASE(STR(?label))))
  v0.2.2 実装(双方向)    : 1 ['受注明細']

「受注明細表」という語で聞くと、順方向(ラベル ⊇ 検索語)では当たりません。ラベル「受注明細」は「受注明細表」を含まないからです。逆方向(質問 ⊇ ラベル)が単独でヒットを取り戻しています。

一方、語の区切りが偶然うまくいく質問では順方向でも当たります。

## 質問: 受注明細ごとの金額を教えてください
  v0.2.2 順方向のみ      : 1 件 ['受注明細']
  v0.2.2 逆方向のみ      : 1 件 ['受注明細']

日本語は漢字と仮名が切り替わるため、書記素クラスタ単位のトークナイズが偶然そこそこ効きます。「受注明細ごとの金額」なら漢字の連続として「受注明細」「金額」が取れます。ところが中国語には切り替わりがありません。

## 質問: 請統計受注明細表的配送狀況
  v0.2.1 ヒット       : 0 件 []
  v0.2.2 順方向のみ      : 0 件 []
    FILTER: CONTAINS(LCASE(?label), "請統計受注明細表的配送狀況")
  v0.2.2 逆方向のみ      : 1 件 ['受注明細']
  v0.2.2 実装(双方向)    : 1 件 ['受注明細']

質問全体がひとつの検索語になり、順方向は原理的に当たりません。 逆方向だけが機能します。リリースノートが「分かち書きできない格納済みラベルに到達できる」と書いているのは、この構造のことです。

この経路はいつ通るのか

ここは実装を読んでおいたほうがよい部分です。逆方向のラベル包含が入ったのは次の 3 か所です。

位置 対象グラフ 呼ばれる条件
Tier 2 の T-Box フォールバック(_fetch_by_entities RDF(Neptune) クラス 200 個以上かつベクトル検索のヒットが 0 件のとき
Tier 3 の GraphTraverser.traverse RDF(Neptune) TIER3_STRATEGY=hand-rolled のとき
agentic Tier 3 の graph_traversal(keyword モード) プロパティグラフ(graphrag) agentic モードでプランナーが keyword を選んだとき

Tier 2 の該当箇所を読むと、まず「クラス数がしきい値(200)未満なら全クラスを取得する」経路が優先されます。

full_context = await self._try_full_namespace_context(namespace, mapped)
if full_context is not None:
    classes, properties = full_context
elif ontology_hits:
    classes, properties = await self._fetch_ontology_context(ontology_hits, namespace, mapped)
elif query:
    # Fallback: use query entities to fetch context
    classes, properties = await self._fetch_by_entities(query, namespace, mapped)

今回のような 3 クラスのネームスペースでは全件取得が勝つため、この経路は通りません。実際、日本語クエリのトレースにも t2.sql.generate が並ぶだけでした。Tier 3 も既定は lexical-baseline なので、GraphTraverser.traverse は呼ばれません。

つまり v0.2.2 の逆方向のラベル包含は「大規模なネームスペースでベクトル検索が外したとき」や「agentic 探索がキーワード検索を選んだとき」に効く保険であり、小さなネームスペースの通常クエリでは表に出てきません。日本語の質問が普通に通るようになった主因は、埋め込みモデル(Cohere Embed v4)が多言語対応であることと、スキーマ文脈が日本語の説明を持っていることです。

前回指摘した Tier 1 の語境界はどうなったか

前回の記事では、Tier 1(メトリクス解決)の同義語マッチが \b(単語境界)ベースで、助詞が付いた日本語ではマッチしないことを書きました。v0.2.2 で該当箇所を見ると、実装は変わっていません。

# packages/context-manager/src/coa_serve/tier1/metric_resolver.py
def _name_pattern(name: str) -> re.Pattern:
    escaped = re.escape(name).replace("_", r"[\s_]")
    return re.compile(rf"\b{escaped}\b")

同じ実装で確かめると、日本語のメトリクス名は助詞が付いた時点で外れます。

メトリクス名       質問                                   マッチ
--------------------------------------------------------------
売上合計         売上合計は?                               miss
売上合計         顧客区分別の売上合計を教えてください                   miss
在庫数量         商品分類ごとの在庫数量を教えてください                  miss
total_sales  show me the total sales by customer segment HIT
total_sales  what is the total sales?                    HIT

v0.2.2 で Tier 1 に入った多言語対応は、残差修飾子ゲートのハングル対応だけです。

# 残差トークンの抽出(v0.2.2)
_RESIDUAL_TOKEN_RE = re.compile(r"[a-z0-9_%$\uac00-\ud7a3]+")

これは「メトリクスに吸収されなかった修飾語が残っていないか」を見るゲートで、v0.2.1 では ASCII 限定だったため韓国語の質問が残差ゼロとして素通りし、自信のある誤答になっていました(GitHub issue #95)。リリースノートも「Han/仮名/タイ文字のトークナイズは未対応のまま」と明記しています。

日本語のメトリクス同義語マッチは、v0.2.2 時点でも Tier 1 では機能しません。 実際、今回のクエリはすべて t1.metric_match miss から始まっていました(メトリクスを定義していないので当然ではありますが、定義しても日本語名では当たらないということです)。

機能を画面で見る

Web UI からも確認しました。

Sources — 日本語のテーブル名

20260830-coa022-03-source-detail

日本語のテーブル名がそのまま一覧に出ます。カラム数(7/7、6/6、5/5)も承認済みです。

テーブル詳細 — ここで詰まる

テーブル名をクリックすると、詳細画面がエラーになりました。

20260830-coa022-04-table-detail-error

API を直接叩くと原因が見えます。一覧は日本語のテーブル ID を正しく返すのに、詳細は 404 で、エラーメッセージにパーセントエンコードされたままの ID が出ています

% GET /namespaces/{ns}/sources/{sid}/tables
HTTP 200 ['coa_blog_ja_ascii.顧客マスタ', 'coa_blog_ja_ascii.商品マスタ', 'coa_blog_ja_ascii.受注明細']

% GET /namespaces/{ns}/sources/{sid}/tables/coa_blog_ja_ascii.%E5%95%86%E5%93%81%E3%83%9E%E3%82%B9%E3%82%BF
HTTP 404: {"error": "Table coa_blog_ja_ascii.%E5%95%86%E5%93%81%E3%83%9E%E3%82%B9%E3%82%BF not found"}

ハンドラは pathParameters の値をそのまま検索キーに使っています。

# packages/sources/src/coa_sources/api/sources_handler.py
table_id = path_params.get("tableId", "")

coa_sources 配下に unquote の呼び出しはありません。非 ASCII のテーブル名では、テーブル詳細の取得(AI が生成したメタデータのレビュー)が画面からも API からもできないことになります。日本語テーブル名を使う場合の、現時点でいちばん実務に響く制約です。

Explorer — 生成されたクラス

20260830-coa022-05-explorer

誘導(Induction)されたクラスが日本語ラベルで並びます。

Playground — 日本語で問い合わせる

20260830-coa022-07-playground-sql

日本語の質問に日本語の値で答え、Compiled artifacts を開くと実行された SQL が確認できます。FROM "商品マスタ" と日本語のテーブル名が正しく引用されています。

考察

日本語データで使うときの現時点の勘所

今回の検証で分かったことを整理します。

項目 結果
日本語のテーブル名 Glue・Athena・COA いずれも通る。クラスの rdfs:label が日本語になる
日本語のカラム名 避けたほうがよい。 1 テーブルのカラムが 1 つのプロパティ IRI に潰れる
日本語のカラム説明(Glue の Comment) 有効。スキーマ文脈に日本語の説明として入る
日本語の質問(Tier 2) 通る。ただし LLM が日本語識別子の引用を忘れて 1 回失敗することがある
日本語の業務文書 通る。命題は日本語と英語が混在して抽出される
Tier 1 のメトリクス同義語 日本語では機能しない\b ベースのまま)
テーブル詳細 API 非 ASCII のテーブル名では 404(パスパラメータが URL デコードされない)

「日本語にするならテーブル名まで、カラム名は ASCII にして説明を日本語で書く」というのが、v0.2.2 時点での落としどころです。ただしテーブル名を日本語にすると、上記のとおりテーブル詳細画面が開けなくなります。AI 生成メタデータのレビューを画面で回したいなら、テーブル名も ASCII にしてラベルの日本語化は諦めるという判断もあり得ます。

多言語検索の位置づけ

v0.2.2 の逆方向のラベル包含は、実データに対して確かに効きました。ただし効く場所は限定的です。

小規模なネームスペースで日本語クエリが通る主因は多言語埋め込みのほうで、逆方向の照合はベクトル検索が外したときの保険です。とはいえ、v0.2.1 ではその保険が「日本語だと必ず 0 件」という状態でした。保険が保険として機能するようになったこと自体に意味があります。

今後に期待したい点

  • プロパティ IRI の衝突解決。クラス側には pascal_names_for によるハッシュ接尾辞の仕組みがあるので、同じ考え方をプロパティにも入れれば日本語カラム名が使えるようになります
  • 非 ASCII のパスパラメータのデコード。テーブル詳細 API の 404 は、unquote 一行で解決する種類の問題に見えます
  • Tier 1 の同義語マッチの多言語対応。v0.2.2 でハングルの残差トークンには対応したので、Han / 仮名 / タイ文字も続いてほしいところです
  • NL→SQL の識別子引用。日本語識別子のスキーマでは初回生成がほぼ失敗します。プロンプト側で引用規則を明示すれば 1 往復減らせます

最後に

Context Ontology Accelerator v0.2.2 の多言語検索を、日本語データで試しました。

v0.2.1 では、日本語の質問はキーワード抽出の時点で 0 件になり、Tier 2 の T-Box フォールバックも Tier 3 のキーワード検索も SPARQL を発行する前に空で返っていました。 v0.2.2 は、トークナイズを Unicode の書記素クラスタ単位に変え、「質問の中に格納済みラベルを探す」逆方向の照合を足すことで、この経路を通るようにしています。実機が生成したオントロジーに対して同じ FILTER を実行して比べたところ、順方向だけでは 0 件だった質問が逆方向で 1 件ヒットしました。中国語のように語の切れ目がまったくない質問では、順方向は原理的に当たらず、逆方向だけが機能します。

一方で、日本語のカラム名がプロパティ IRI を潰すこと、非 ASCII のテーブル名でテーブル詳細 API が 404 になること、Tier 1 の同義語マッチが依然 \b ベースであることも分かりました。「日本語対応」と一括りにできる状態ではなく、レイヤーごとに対応状況が違います。

日本語データで COA を検討している方は、まずテーブル名とカラム名の付け方を決めるところから始めるとよさそうです。前回のブログ執筆から2週間程度でこれほど多くの進化を遂げており、まだまだ進化は止まらないことが予想されますので、今日の状況で判断しないでいただきたい限りです。

合わせて読みたい

https://dev.classmethod.jp/articles/20260817-aws-context-v020/

https://github.com/aws/context-ontology-accelerator/releases/tag/v0.2.2

https://github.com/aws/context-ontology-accelerator


AI白書2026 配布中

クラスメソッドが独自に行なったAI診断調査をもとに、企業のAI活用の現在地を調査レポートとしてまとめました。企業規模別の活用度傾向に加え、規模を超えてAI活用を進める企業に共通する取り組みまで、自社の現在地を捉えるためのヒントにぜひ。

AI白書2026

無料でダウンロードする

この記事をシェアする

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

関連記事