I tried building a Japanese ontology with AWS Context Ontology Accelerator (COA) - Support for multilingual search and more

I tried building a Japanese ontology with AWS Context Ontology Accelerator (COA) - Support for multilingual search and more

AWS OSS "Context Ontology Accelerator" v0.2.2 now allows questions in languages without word spacing, such as Japanese, to reach ontology labels. We will share the results of actually testing multilingual search using Japanese data.
2026.08.30

This page has been translated by machine translation. View original

This is Ishikawa from the Cloud Business Division. AWS OSS "Context Ontology Accelerator" has released v0.2.2. The major change is that questions in languages that don't use word spacing (languages that don't separate words with spaces), such as Japanese, can now reach stored labels. I prepared Japanese data and actually tested multilingual search.

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

When I tried v0.2.0 last time, I wrote that I wanted Tier 1 synonym matching to support non-space-delimited languages like Japanese. I also verified how much of that area is now working in v0.2.2.

In the blog I wrote two weeks ago below, I explained in detail about ontology and Context Ontology Accelerator. Today I will only explain the newly added multilingual search. (And yet this is still such a long post...)

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

What is Context Ontology Accelerator

Context Ontology Accelerator (hereafter COA) is an OSS published by AWS. It reads schemas from data sources such as Glue Data Catalog and JDBC databases, induces ontologies (classes, properties, relationships) with an LLM, stores them in Amazon Neptune, and converts natural language questions through that ontology into SQL or SPARQL to provide answers. It also provides an MCP server for agents.

Answering questions is divided into 3 tiers (Tiers). Questions cascade in this order, and if confidence is low, they fall to the next tier.

Tier Role Implementation Core
Tier 1 Resolution of predefined metrics Regular expression matching of names and synonyms
Tier 2 Query against structured data NL→SQL via ontology / Ontop (VKG: Virtual Knowledge Graph)
Tier 3 Document knowledge graph search Vector search + graph traversal + synthesis

The multilingual search in v0.2.2 is a change that entered Tier 2's T-Box fallback and Tier 3's keyword entity search.

Changes in v0.2.2

Organizing the release note headings, they are as follows.

Category Content
Deployment outside the US All Bedrock model IDs became SSM deployment configuration keys. Resolved values drive container environment variables, IAM permissions, and the cost dashboard from a single location
Multilingual query understanding Label matching in Tier 2 / Tier 3 became bidirectional. Tokenization changed to UAX #29 grapheme cluster units
Tier 2 strategy specification options.strategy allows the caller to fix best / ontop / nl_to_sql / ontop_first / nl_to_sql_first / agentic
Guardrail observability Decision dimension (ALLOW / ANONYMIZED / BLOCK / UNKNOWN / MODEL_FILTERED) added to GuardrailInvocations
Induction report droppedTables added to InductionReport, so tables where generation LLM failed are now reported
Bug fixes Includes 3 field-reported issues (#92 / #94 / #95). AGPL dependency removed from document pipeline

Extracting only the multilingual search-related items, there are 3 points.

  • Reverse containment was added to Tier 2's T-Box fallback and Tier 3's keyword entity search
  • Tokenization changed to Unicode extended grapheme cluster (UAX #29) units
  • Tier 1's residual modifier gate now handles Hangul syllables (it is explicitly noted that Han / Kana / Thai characters are not yet supported)

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

What did multilingual search change

What was the problem

COA extracts keywords from questions and matches them against labels stored in the graph. Matching up to v0.2.1 was only in the forward direction.

CONTAINS(LCASE(?label), "search term")     -- Does the stored label contain the search term?

For this form to work, the "search term" needs to be extractable from the question text. However, the tokenizer in v0.2.1 assumed 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")

On the Tier 3 side, there was also a sanitization gate right before embedding in 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}$")

Neither passes non-ASCII. Japanese questions result in 0 keywords, and both Tier 2's T-Box fallback and Tier 3's keyword search were returning empty before issuing SPARQL.

Bidirectional label containment

v0.2.2 added the reverse direction while keeping the forward direction as-is.

CONTAINS("continuous region of question text", LCASE(STR(?label)))   -- Does the question text contain the stored label?

The idea is reversed. Rather than solving "where to split the question text" in languages without word spacing, it uses the fact that the graph side holds the correct words as labels, and searches for the label within the question text. No morphological analyzer for Japanese or stop-word list for particles is needed.

The target of the reverse direction is characters excluding Latin and Greek letters. The reason is written in the repository comments.

  • Latin characters have words separated by spaces, so the forward direction suffices. Including them would cause 2-character labels to match inside English words
  • Greek characters have word endings rewritten by inflection, so stored labels do not become substrings of inflected forms (νόμος is not inside νόμου)
  • Cyrillic uses space separation, but case inflection is appended to the stem, so the reverse direction works. Therefore it is included
  • CJK / Kana / Thai / Khmer have no spaces between words, and Hangul / Devanagari / Bengali / Arabic / Hebrew have particles or case endings directly attached to words, so they are targeted

Note that the repository comments also explicitly state the limitations of the reverse direction. Within a single continuous region, it may also match labels that span word boundaries. For Japanese and Chinese questions, the region becomes the entire question, so the possibility remains that a 2-character label could match at the junction between adjacent words. The only things suppressing this are the minimum label length of 2 characters and the row count limit.

Running v0.2.1 and v0.2.2 with the same question

Just explaining it doesn't make the difference clear, so I ran the actual code of both versions through the same questions. I extracted query_utils.py from v0.2.1 with git show, and for v0.2.2 I loaded the module from the cloned repository directly.

% 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 returns 0 search terms for all of Japanese, Chinese, Korean, and Thai. At this point SPARQL is not issued and it returns empty. v0.2.2 extracts terms and also holds continuous regions (containers) for the reverse direction. English has containers=0 (Latin characters are excluded from reverse direction).

Pay attention to the Chinese and Thai rows. Because there are no word boundaries, the forward search term is just the entire question as a single item. In this form, forward matching does not work, so the reverse direction becomes essential. For Japanese, because kanji and kana alternate, grapheme cluster-based tokenization happens to work reasonably well by coincidence.

Tier 3 also has a sanitization gate right before embedding in SPARQL. I compared this with the same questions as well.

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's ^[a-z0-9][a-z0-9_-]{0,63}$ also drops accented Latin characters (Straße). v0.2.2 changed to a method of "rejecting dangerous characters (control characters, line separators)", and is no longer a character type allowlist.

Let's try it

Prerequisites

  • Verification environment: US East (N. Virginia) region (us-east-1)
  • Tag used: v0.2.2
  • Verification date: August 30, 2026
  • AWS CLI v2 (version at time of verification: 2.36.29)

Clone the repository.

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

Local requirements are Python 3.12, Node.js 22 or higher, pnpm, uv, Java 17 or higher, 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

Preparing verification data

To put Japanese labels into the graph, it is necessary to make the table names and column names themselves Japanese. This is because COA's Induction puts table names directly into rdfs:label of owl:Class and column names into rdfs:label of properties.

# 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)))

So I prepared 3 tables in Japanese, assuming a trading company dealing in office supplies.

Table Columns Rows
商品マスタ (Product Master) 商品コード / 商品名 / 商品分類 / 標準単価 / 在庫数量 / 取扱開始日 20
顧客マスタ (Customer Master) 顧客コード / 顧客名 / 顧客区分 / 都道府県 / 登録日 15
受注明細 (Order Details) 受注番号 / 受注日 / 顧客コード / 商品コード / 数量 / 受注金額 / 配送ステータス 80

I also prepared 2 business rule documents (Sales Operations Policy, Data Glossary) in Japanese. These contain information that cannot be read from the schema, such as the 5-value definition of delivery status, payment terms by customer segment, and reorder points by product category (50 for furniture, 30 for others).

First, I verify that Japanese table names and column names can be handled by Glue and Athena. If this doesn't work, the entire plan falls apart.

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

Japanese table names and column names could be registered directly in the Glue Data Catalog. They can also be read from Athena.

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

However, identifiers must always be enclosed in double quotes. Using Japanese for aliases without quotation marks results in a syntax error.

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

This constraint comes into play later when looking at SQL generated by the LLM.

Preparing ground truth data

I generate ground truth answers in Athena in advance to compare against.

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

There are 7 products that fall below the reorder point (50 for furniture, 30 for others). This rule where only furniture has a different threshold is only written in the document.

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

Model ID configuration moved to SSM

In the previous v0.2.0, I directly edited the CDK source to change the model used for Induction. In v0.2.2, all Bedrock model IDs are keys in SSM's /{prefix}/config.

Configuration Key Target Default Value
bedrockLlmModelId Serve query LLM (NL→SPARQL, synthesis) us.anthropic.claude-sonnet-5
bedrockEmbedModelId All embeddings us.cohere.embed-v4:0
bedrockEmbedDimensions Embedding dimensions 1024
bedrockInductionLlmModelId Induction, grounding, description generation us.anthropic.claude-sonnet-5
bedrockChatModelId Source enrichment, constraint inference, document extraction us.anthropic.claude-haiku-4-5-20251001-v1:0

The Induction model that I previously changed to Claude Sonnet 5 by rewriting the source now has Sonnet 5 as the default value in v0.2.2. Since I'm deploying in us-east-1 this time, only the initial admin email address needed to be configured.

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

The initial admin is created by CDK at deployment time. If not set, it is created with a placeholder nobody@amazon.com and the email won't arrive, so the proper approach is to set it before deployment.

Note that bedrockEmbedModelId and bedrockEmbedDimensions are first-deployment-only settings. The number of dimensions is baked in at OpenSearch index creation time, so changing them after data ingestion requires re-ingestion.

Deployment

For deployment, please refer to the previous blog.

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

Ontology gets collapsed with Japanese column names

I retrieve the generated proposal's TTL (Turtle-format RDF file) and look at its contents.

% 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 "商品マスタ" .

The labels were stored in Japanese as intended. However, the 18 properties that should exist have been collapsed to just 3. One table's columns are all collapsed into a single IRI (ind:entity_), with 7 labels and 7 comments hanging off it.

The cause is the function that creates the local name of the 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)

Because it removes non-ASCII, Japanese column names become empty strings here. Property IRIs are built as ns[f"{local}_{_to_camel(column_name)}"], so all columns in the same table end up with the same IRI.

Running the same function locally, the result is as follows:

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'

The class side is fine. Table names also become empty strings in the same way, but there to_pascal has an "Entity" fallback, and furthermore pascal_names_for detects collisions and appends a hash suffix. This collision resolution is not applied on the property side.

The real impact shows up in queries. The schema context passed to NL→SQL becomes "1 table, 1 column", and the correspondence between labels and comments also gets misaligned.

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

In this state, Tier 2 drops to a confidence of 0.1, falls back to Tier 3, and hits the API Gateway 29-second limit.

Rebuilding with ASCII column names only

For the same CSV, I created Glue tables in a separate database with table names still in Japanese but only column names in ASCII (with Japanese comments), and ran Induction again.

% 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)

The results were as follows.

Database Class Count Property Count Class Labels
coa_blog_ja (both table names and column names in Japanese) 3 3 Japanese
coa_blog_ja_ascii (only column names in ASCII) 3 18 Japanese

Properties are correctly generated as 18, and class labels remain in Japanese. The conclusion from this is: if you want to search in Japanese, what should be in Japanese is the table name (= class label), and it is currently safer to keep column names in ASCII. Subsequent verification proceeds with this namespace.

Querying in Japanese

scripts/run_query.py is a local helper I wrote for this verification. First, a question that can be answered with structured data alone. The ground truth was 5 records obtained from Athena.

% 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 = '遅延'

--- Trace ---
  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  {"athena": "engine", "rowCount": 1, "shot": 2, "engine": "athena"}

The result matched the ground truth. However, looking at the trace, the first SQL resulted in a syntax error in Athena. This was because the Japanese table name was not enclosed in quotes, and the self-correction in the 2nd shot fixed it to FROM "受注明細" and it passed. The constraint verified during data preparation — "Japanese identifiers require double quotes" — appeared here directly.

I also try aggregation and 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

Both matched the ground truth obtained from Athena. No foreign key constraints were defined at all, but the JOIN was constructed from foreign keys inferred by enrichment.

Comparing v0.2.2's options.strategy with Japanese data

With v0.2.2, it is now possible to fix the Tier 2 strategy from the caller side. I compare the same question using ontop (via VKG) and nl_to_sql (flat 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

--- Trace ---
  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}

Both strategies produced correct results, but the quality of generation differs. With ontop, the LLM only writes up to SPARQL, and Ontop mechanically assembles the SQL from R2RML mappings. As a result, the Japanese table name is correctly quoted as "受注明細" from the start, and no regeneration due to syntax errors occurs. With nl_to_sql, the LLM writes the entire SQL body, so quote omissions can happen.

What can be said from this actual measurement is that for schemas with Japanese identifiers, fixing options.strategy to ontop can avoid the failure mode of forgotten quotes.

Asking About Rules Written Only in Documents

The reorder point thresholds (50 for fixtures, 30 for everything else) are written only in business documents. First, let's ask before inserting the documents.

% 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

It interpreted "reorder point" as "stock 0." The correct answer is 7 items. The confidence score coming out at 0.4 is a saving grace, but it returns as an answer rather than an error.

We'll add 2 Japanese business documents as document sources.

% 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/"]}}'

Ingestion took about 7 minutes.

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

We explicitly specify Tier 3 to verify that knowledge from the document side is being retrieved.

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

According to the data glossary (data-glossary-ja.txt), "reorder point" is defined as follows:
- A reorder point is a threshold for the quantity of inventory at which stock should be replenished
- Reorder points are set to different values depending on the product category

## Thresholds by Product Category

| Product Category | Reorder Point |
|---|---|
| Fixtures | Reorder point when below 50 units (set individually because fixtures are bulky) |
| All other product categories | Reorder point when below 30 units |

The rules written in the documents were correctly retrieved.

Looking at supportingContent, the extracted propositions contain a mix of Japanese and English.

{"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."]}

When we tested v0.2.0 previously, propositions from Japanese documents were all normalized to English. This time, propositions that retain Japanese terms (受注済, 配送ステータス, 発注点, etc.) are also being generated. Since Japanese labels are included in the document-side knowledge graph, they can become targets of multilingual search.

Note that queries reaching Tier 3 hit the API Gateway 29-second limit. We actually received 504 errors several times.

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

The route where the MCP server calls the Context Manager does not go through API Gateway, so that route has 120 seconds of headroom (CM_INVOKE_TIMEOUT_S). This time, we verified by directly hitting the same route (AgentCore Runtime's invocations endpoint + Bearer JWT). The actual measured time for agentic mode was 117 seconds.

Verifying Reverse Label Containment with Real Data

Up to this point, Japanese queries were answerable through vector search (Cohere Embed v4) and schema context. We verify against the ontology generated by the actual machine how the reverse label containment added in v0.2.2 actually works.

We retrieved the proposed TTL locally, loaded it into rdflib, and ran both the v0.2.1 FILTER and the v0.2.2 FILTER against the same data. The FILTER strings were constructed from functions in the repository itself (build_query_search_plan / escape_sparql_string_literal / normalize_label_match_text, and for v0.2.1, extract_query_entities and _sparql_escape_string).

The class labels in the loaded graph are 「受注明細」, 「商品マスタ」, and 「顧客マスタ」 (since they are accepted, the same ones are in Neptune).

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

## Question: 受注明細表の配送状況を集計してください
  v0.2.1 FILTER      : (No search terms found, exits without issuing SPARQL)
  v0.2.1 hits        : 0 results []
  v0.2.2 forward only   : 0 results []
    FILTER: CONTAINS(LCASE(?label), "受注明細表") || CONTAINS(LCASE(?label), "配送状況") || CONTAINS(LCASE(?label), "集計")
  v0.2.2 reverse only   : 1 result ['受注明細']
    FILTER: (STRLEN(STR(?label)) >= 2 && CONTAINS("受注明細表の配送状況を集計してください", LCASE(STR(?label))))
  v0.2.2 implementation (bidirectional): 1 result ['受注明細']

When asking with the term "受注明細表", the forward direction (label ⊇ search term) does not match. This is because the label "受注明細" does not contain "受注明細表". The reverse direction (question ⊇ label) alone recovers the hit.

On the other hand, for questions where word boundaries happen to work out, the forward direction also matches.

## Question: 受注明細ごとの金額を教えてください
  v0.2.2 forward only   : 1 result ['受注明細']
  v0.2.2 reverse only   : 1 result ['受注明細']

In Japanese, kanji and kana alternate, so tokenization at the grapheme cluster level happens to work reasonably well by coincidence. For "受注明細ごとの金額", "受注明細" and "金額" can be extracted as continuous kanji sequences. However, Chinese has no such alternation.

## Question: 請統計受注明細表的配送狀況
  v0.2.1 hits        : 0 results []
  v0.2.2 forward only   : 0 results []
    FILTER: CONTAINS(LCASE(?label), "請統計受注明細表的配送狀況")
  v0.2.2 reverse only   : 1 result ['受注明細']
  v0.2.2 implementation (bidirectional): 1 result ['受注明細']

The entire question becomes a single search term, making it fundamentally impossible for the forward direction to match. Only the reverse direction works. When the release notes say "stored labels that cannot be word-segmented can be reached," this is the structure they are referring to.

When Does This Path Get Used

This is a part worth reading the implementation carefully. The reverse label containment was added in 3 places.

Location Target Graph Conditions for being called
Tier 2 T-Box fallback (_fetch_by_entities) RDF (Neptune) When there are 200 or more classes AND vector search returns 0 hits
Tier 3 GraphTraverser.traverse RDF (Neptune) When TIER3_STRATEGY=hand-rolled
Agentic Tier 3 graph_traversal (keyword mode) Property graph (graphrag) When the planner chooses keyword in agentic mode

Reading the Tier 2 relevant section, the path that "fetches all classes if the class count is below the threshold (200)" takes priority first.

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)

For a namespace with only 3 classes like this one, the full fetch wins, so this path is not taken. In fact, the traces for Japanese queries showed only t2.sql.generate in sequence. Tier 3 also defaults to lexical-baseline, so GraphTraverser.traverse is not called.

In other words, the reverse label containment in v0.2.2 serves as insurance for when vector search misses in large namespaces or when agentic exploration chooses keyword search; it does not surface in ordinary queries against small namespaces. The main reason Japanese queries now work normally is that the embedding model (Cohere Embed v4) supports multiple languages and that the schema context contains Japanese descriptions.

What Happened to the Tier 1 Word Boundary Issue Reported Last Time

In the previous article, we noted that Tier 1 (metrics resolution) synonym matching was \b (word boundary) based and would not match Japanese with particles attached. Looking at the relevant section in v0.2.2, the implementation has not changed.

# 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")

Verifying with the same implementation, Japanese metric names fail to match once particles are attached.

Metric name    Question                                   Match
--------------------------------------------------------------
売上合計         売上合計は?                               miss
売上合計         顧客区分別の売上合計を教えてください                   miss
在庫数量         商品分類ごとの在庫数量を教えてください                  miss
total_sales  show me the total sales by customer segment HIT
total_sales  what is the total sales?                    HIT

The multilingual support added to Tier 1 in v0.2.2 is only Hangul support for the residual modifier gate.

# Residual token extraction (v0.2.2)
_RESIDUAL_TOKEN_RE = re.compile(r"[a-z0-9_%$\uac00-\ud7a3]+")

This is the gate that checks whether modifier words not absorbed by a metric remain, and in v0.2.1 it was ASCII-only, causing Korean questions to pass through as zero residuals, resulting in confident wrong answers (GitHub issue #95). The release notes also explicitly state "Han/Kana/Thai tokenization remains unsupported."

Japanese metric synonym matching still does not work in Tier 1 as of v0.2.2. In fact, all queries this time started with t1.metric_match miss (which is expected since no metrics were defined, but the point is that even if defined, Japanese names would not match).

Viewing Features in the UI

We also verified via the Web UI.

Sources — Japanese Table Names

20260830-coa022-03-source-detail

Japanese table names appear as-is in the list. Column counts (7/7, 6/6, 5/5) are also approved.

Table Details — Getting Stuck Here

Clicking a table name caused the detail screen to error out.

20260830-coa022-04-table-detail-error

Hitting the API directly reveals the cause. The list correctly returns Japanese table IDs, but the detail returns 404, and the error message shows the ID still percent-encoded.

% 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"}

The handler uses the pathParameters value directly as the search key.

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

There is no unquote call under coa_sources. For non-ASCII table names, retrieving table details (reviewing AI-generated metadata) is not possible either from the UI or from the API. This is the most practically impactful constraint at the current moment when using Japanese table names.

Explorer — Generated Classes

20260830-coa022-05-explorer

Induced classes are listed with Japanese labels.

Playground — Querying in Japanese

20260830-coa022-07-playground-sql

It answers Japanese questions with Japanese values, and opening the Compiled artifacts lets you verify the executed SQL. FROM "商品マスタ" shows the Japanese table name correctly quoted.

Discussion

Key Points for Using with Japanese Data at This Time

Here is a summary of what we learned from this verification.

Item Result
Japanese table names Works in Glue, Athena, and COA alike. The class rdfs:label becomes Japanese
Japanese column names Best avoided. Columns from one table collapse into a single property IRI
Japanese column descriptions (Glue Comment) Effective. Enters the schema context as Japanese descriptions
Japanese questions (Tier 2) Works. However, the LLM may forget to quote Japanese identifiers and fail once
Japanese business documents Works. Propositions are extracted with a mix of Japanese and English
Tier 1 metric synonyms Does not work in Japanese (still \b-based)
Table detail API Returns 404 for non-ASCII table names (path parameters are not URL-decoded)

"Use Japanese up to table names, keep column names ASCII, and write descriptions in Japanese" is the practical compromise point as of v0.2.2. However, using Japanese table names means the table detail screen cannot be opened, as noted above. If you want to review AI-generated metadata through the UI, there is also the option of keeping table names ASCII and forgoing Japanese labels.

The reverse label containment in v0.2.2 demonstrably worked against real data. However, where it works is limited.

The main reason Japanese queries work in small namespaces is the multilingual embedding, and the reverse matching is insurance for when vector search misses. That said, in v0.2.1, that insurance was in a state of "always returning 0 results for Japanese." The fact that the insurance now functions as insurance is itself meaningful.

Points to Hope for Going Forward

  • Property IRI collision resolution. Since the class side has a hash suffix mechanism via pascal_names_for, applying the same thinking to properties would enable Japanese column names to be used
  • Decoding of non-ASCII path parameters. The 404 in the table detail API looks like the kind of problem that can be solved with a single line of unquote
  • Multilingual support for Tier 1 synonym matching. Since v0.2.2 addressed residual tokens for Hangul, it would be great to see Han / Kana / Thai follow
  • Identifier quoting in NL→SQL. First-pass generation almost always fails for schemas with Japanese identifiers. Explicitly stating quoting rules in the prompt could eliminate one round trip

Conclusion

We tested the multilingual search of Context Ontology Accelerator v0.2.2 with Japanese data.

In v0.2.1, Japanese questions returned 0 results at the keyword extraction stage, and both the Tier 2 T-Box fallback and the Tier 3 keyword search returned empty results before ever issuing SPARQL. v0.2.2 addresses this by changing tokenization to Unicode grapheme cluster units and adding reverse matching that "searches for stored labels within the question," enabling these paths to be taken. Running the same FILTERs against the ontology generated by the actual machine and comparing results showed that a question returning 0 results with the forward direction alone yielded 1 hit with the reverse direction. For questions in languages like Chinese where there are no word boundaries at all, the forward direction is fundamentally incapable of matching, and only the reverse direction works. When the release notes say "stored labels that cannot be word-segmented can be reached," this is the structure they are referring to.

On the other hand, we also found that Japanese column names collapse property IRIs, that the table detail API returns 404 for non-ASCII table names, and that Tier 1 synonym matching remains \b-based. "Japanese support" cannot be treated as a blanket statement; the level of support differs layer by layer.

For those considering COA with Japanese data, it would be best to start by deciding on naming conventions for table names and column names. Given that such significant evolution has occurred in approximately two weeks since writing the previous blog post, and further evolution is expected to continue, please do not make judgments based on the current state of affairs.

Further Reading

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

無料でダウンロードする

Share this article

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