
I tried building a Japanese ontology with AWS Context Ontology Accelerator (COA) - Support for multilingual search and more
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.
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 following blog I wrote two weeks ago, I explained ontologies and Context Ontology Accelerator in detail. Today I will only explain the newly added multilingual search. (And yet it's still this long...)
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, and relationships) using an LLM, stores them in Amazon Neptune, and converts natural language questions into SQL or SPARQL through that ontology to answer them. It also provides an MCP server for agents.
Answers to questions are divided into 3 tiers. Questions cascade in this order, and if confidence is low, they fall to the next level.
| Tier | Role | Core Implementation |
|---|---|---|
| Tier 1 | Resolving predefined metrics | Regular expression matching of names and synonyms |
| Tier 2 | Querying structured data | NL→SQL / Ontop via ontology (VKG: Virtual Knowledge Graph) |
| Tier 3 | Exploring document knowledge graphs | Vector search + graph traversal + synthesis |
The multilingual search in v0.2.2 is a change that went into 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 | Details |
|---|---|
| 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 fixing best / ontop / nl_to_sql / ontop_first / nl_to_sql_first / agentic from the caller side |
| Guardrail observability | Decision dimension (ALLOW / ANONYMIZED / BLOCK / UNKNOWN / MODEL_FILTERED) added to GuardrailInvocations |
| Induction report | droppedTables added to InductionReport, reporting tables where the generative LLM failed |
| Bug fixes | Includes 3 field-reported issues (#92 / #94 / #95). AGPL dependency removed from the document pipeline |
Extracting only the multilingual search-related items, there are 3 points.
- Reverse label 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 (Han / Kana / Thai are explicitly noted as unsupported)
What did multilingual search change?
What was the problem?
COA extracts keywords from the question text and matches them against labels stored in the graph. Matching up to v0.2.1 was only in one 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, v0.2.1's tokenizer was ASCII-only.
# 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 would result in 0 keywords, and both Tier 2's T-Box fallback and Tier 3's keyword search were returning empty before issuing any SPARQL.
Bidirectional label containment
v0.2.2 added the reverse direction while keeping the forward direction.
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 leverages the fact that the graph side holds the correct terms as labels, and searches for the labels within the question text. No Japanese morphological analyzer or particle stop-word list is needed.
The targets for the reverse direction are characters excluding Latin and Greek. The reason is written in the repository's comments.
- Latin characters have words separated by spaces, so the forward direction is sufficient. Including them would cause 2-character labels to match inside English words
- Greek characters rewrite word endings through conjugation, so stored labels don't become substrings of conjugated forms (
νόμοςis not insideνόμου) - Cyrillic uses space delimiters, but case inflection is appended to the stem, so the reverse direction works. Therefore it is included in the targets
- Han / Kana / Thai / Khmer have no spaces between words, and Hangul / Devanagari / Bengali / Arabic / Hebrew have particles and case endings attached directly to words, so they are included
Note that the repository's comments also explicitly state the limitations of the reverse direction. Within a single continuous region, it can also match labels that span word boundaries. For Japanese and Chinese questions, the region becomes the entire question, so there remains the possibility of a 2-character label matching at the junction of adjacent words. The only thing suppressing this is the minimum label length of 2 characters and the row count limit.
Running v0.2.1 and v0.2.2 on the same question
Since it's hard to see the difference from explanation alone, I ran the actual code of both versions through the same questions. I extracted v0.2.1's query_utils.py with git show, and for v0.2.2 I loaded the module from the cloned repository as-is.
% 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 yields 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 the reverse direction).
Look at the Chinese and Thai rows. Because there are no word boundaries, the forward search term becomes the entire question as a single item. In this form forward matching cannot succeed, making the reverse direction essential. Japanese switches between kanji and kana, so grapheme cluster-based tokenization happens to work reasonably well.
Tier 3 also has a sanitization gate right before embedding in SPARQL. I compared this with the same questions.
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 an allowlist of character types.
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)
Get 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, and 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 the verification data
To get Japanese labels into the graph, the table names and column names themselves need to be in Japanese. This is because COA's induction puts table names directly into rdfs:label of owl:Class, and column names directly 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, imagining a trading company dealing in office supplies.
| Table | Columns | Rows |
|---|---|---|
| 商品マスタ (Product Master) | 商品コード / 商品名 / 商品分類 / 標準単価 / 在庫数量 / 取扱開始日 | 20 |
| 顧客マスタ (Customer Master) | 顧客コード / 顧客名 / 顧客区分 / 都道府県 / 登録日 | 15 |
| 受注明細 (Order Details) | 受注番号 / 受注日 / 顧客コード / 商品コード / 数量 / 受注金額 / 配送ステータス | 80 |
I also prepared 2 documents (sales business policy, data glossary) in Japanese. These include information that cannot be read from the schema, such as the 5-value definitions of delivery status, payment terms by customer segment, and reorder points by product category (50 for furniture, 30 for others).
First, I verify whether Japanese table names and column names can be handled in Glue and Athena. If this doesn't work, the whole 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 as-is 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 quotes results in a syntax error.
SELECT "配送ステータス", count(*) AS 件数 FROM "coa_blog_ja"."受注明細" GROUP BY "配送ステータス"
InvalidRequestException: line 1:31: mismatched input '件'. Expecting: <identifier>
This constraint becomes relevant when looking at the SQL generated by the LLM later.
Preparing ground truth data
To cross-check answers, I obtain the ground truth from Athena in advance.
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 below the reorder point (50 for furniture, 30 for others). This rule of "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 had to directly edit the CDK source to change the model used for induction. In v0.2.2, all Bedrock model IDs are keys under 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 editing the source already has Sonnet 5 as the default in v0.2.2. Since I'm deploying to us-east-1 this time, all I needed to set was the initial admin email address.
% aws ssm put-parameter --name /coa/config --type String --overwrite \
--value '{"initialAdminEmail":"xxxxx@example.com"}'
The initial admin is created by CDK at deploy time. If left unset, it's created with the placeholder nobody@amazon.com and no email arrives, so the proper approach is to set it before deploying.
Note that bedrockEmbedModelId and bedrockEmbedDimensions are first-deployment-only settings. The number of dimensions is baked in when the OpenSearch index is created, so changing it after data ingestion requires re-ingestion.
Deployment
For deployment, please refer to the previous blog.
Ontology collapses with Japanese column names
I retrieve the generated proposal TTL (RDF file in Turtle format) 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. This is as intended. However, the 18 properties that should exist are reduced to only 3. One table's columns are all collapsed into a single IRI (ind:entity_), with 7 labels and 7 comments hanging from 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)
Non-ASCII is stripped, so Japanese column names become empty strings here. Property IRIs are constructed 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 gives:
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 the same way, but there is an "Entity" fallback in to_pascal, and pascal_names_for detects collisions and appends hash suffixes. This collision resolution is not implemented on the property side.
The actual harm shows up in queries. The schema context passed to NL→SQL becomes "1 table, 1 column", and the correspondence between labels and comments is also off.
"context_preview": "Table: 受注明細 | Columns: 受注日:integer (受注した個数)
Table: 顧客マスタ | Columns: 登録日:string (与信限度額と請求サイクルの区分...)
Table: 商品マスタ | Columns: 取扱開始日:integer (商品の名称)"
In this state, Tier 2 confidence fell to 0.1, fell back to Tier 3, and hit API Gateway's 29-second limit.
Recreating with ASCII column names only
For the same CSV, I created Glue tables with Japanese table names but ASCII-only column names (with Japanese comments) in a separate database, 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 are as follows.
| Database | Classes | Properties | Class Labels |
|---|---|---|---|
coa_blog_ja (Japanese table names and column names) |
3 | 3 | Japanese |
coa_blog_ja_ascii (ASCII column names only) |
3 | 18 | Japanese |
Properties were 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 at this point it's safer to keep column names in ASCII. The remaining 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 items 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 {"rowCount": 1, "shot": 2, "engine": "athena"}
The result matched the ground truth. However, looking at the trace, the first SQL caused a syntax error in Athena. This was because it forgot to enclose the Japanese table name in quotes, and on the 2nd shot self-correction fixed it to FROM "受注明細". The "Japanese identifiers require double quotes" confirmed during data preparation appeared here directly.
Let's 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, yet the JOIN was assembled from foreign keys inferred by enrichment.
Comparing v0.2.2's options.strategy with Japanese data
In v0.2.2, the Tier 2 strategy can be fixed 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 results were correct, but the quality of generation differs. With ontop, the LLM writes only up to SPARQL, and SQL is mechanically assembled by Ontop from R2RML mappings. Because of this, Japanese table names are correctly quoted as "受注明細" from the start, and no regeneration due to syntax errors occurs. With nl_to_sql, the LLM writes the SQL body itself, so quoting can be forgotten.
What can be said from this actual measurement is that, for schemas with Japanese identifiers, fixing options.strategy to ontop avoids the failure mode of forgotten quoting.
Asking About Rules Written Only in Documents
The reorder point thresholds (50 for fixtures, 30 for everything else) are only written in business documents. Let's ask before adding 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. It's some consolation that the confidence score is low at 0.4, but it still returns 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 check whether 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 stock quantity threshold for replenishing inventory
- Reorder points are set to different values depending on the product category
## Thresholds by Product Category
| Product Category | Reorder Point |
|---|---|
| Fixtures | Below 50 units triggers reorder point (set individually because fixtures are bulky) |
| All other product categories | Below 30 units triggers reorder point |
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 enter the document-side knowledge graph, they can be targets of multilingual search.
Note that queries that reach Tier 3 hit API Gateway's 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 it has a 120-second margin there (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
The Japanese queries so far were answerable with vector search (Cohere Embed v4) and schema context. We'll verify how the reverse label containment added in v0.2.2 actually works against the ontology generated by the real system.
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 assembled from the repository's actual functions (build_query_search_plan / escape_sparql_string_literal / normalize_label_match_text, and on the v0.2.1 side, extract_query_entities and _sparql_escape_string).
The class labels loaded into the graph are three: "受注明細", "商品マスタ", and "顧客マスタ" (since they are already accepted, the same ones are in Neptune).
% python3 scripts/sparql_ab.py artifacts/ontology_ascii.ttl "受注明細表の配送状況を集計してください"
## Query: 受注明細表の配送状況を集計してください
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 querying with the term "受注明細表", the forward direction (label ⊇ search term) does not match. This is because the label "受注明細" does not contain "受注明細表". The reverse direction (query ⊇ label) alone retrieves the hit.
On the other hand, for queries where word boundaries happen to work out correctly, the forward direction also matches.
## Query: 受注明細ごとの金額を教えてください
v0.2.2 forward only : 1 result ['受注明細']
v0.2.2 reverse only : 1 result ['受注明細']
Because Japanese alternates between kanji and kana, grapheme cluster-based tokenization happens to work reasonably well. For "受注明細ごとの金額", the kanji sequences "受注明細" and "金額" can be extracted. However, Chinese has no such alternation.
## Query: 請統計受注明細表的配送狀況
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 query becomes a single search term, so the forward direction cannot match in principle. Only the reverse direction works. The release notes' statement about "being able to reach stored labels that cannot be word-segmented" refers to this structure.
When Is This Path Taken
This is a part where it's worth reading the implementation. The reverse label containment was added in three places.
| Location | Target Graph | Condition for Being Called |
|---|---|---|
Tier 2 T-Box fallback (_fetch_by_entities) |
RDF (Neptune) | 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 in agentic mode selects keyword |
Reading the relevant Tier 2 code, the path that "retrieves all classes when 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 retrieval wins, so this path is not taken. Indeed, the traces for Japanese queries only showed t2.sql.generate in sequence. Tier 3 also defaults to lexical-baseline, so GraphTraverser.traverse is not called either.
In other words, the reverse label containment in v0.2.2 serves as insurance that kicks in "when vector search misses in a large namespace" or "when agentic exploration selects keyword search", and it does not surface in normal queries on small namespaces. The main reason Japanese queries work normally is that the embedding model (Cohere Embed v4) is multilingual and the schema context contains Japanese descriptions.
What Happened to the Tier 1 Word Boundary Issue Noted 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 when particles were attached. Looking at the relevant code 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")
Confirming with the same implementation, Japanese metric names fail to match as soon as a particle is attached.
Metric Name Query 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 a gate that checks whether any modifier words not absorbed by metrics remain. In v0.2.1, it was limited to ASCII, so Korean queries passed through as zero residuals, resulting in confident wrong answers (GitHub issue #95). The release notes also explicitly state that "Han/Kana/Thai tokenization remains unsupported."
Japanese metric synonym matching still does not work in Tier 1 as of v0.2.2 (it remains \b-based). Indeed, all queries this time started with t1.metric_match miss (which is expected since we didn't define metrics, but the point is that even if we did, Japanese names would not match).
Viewing Features in the UI
We also verified through the Web UI.
Sources — Japanese Table Names

Japanese table names appear in the list as-is. Column counts (7/7, 6/6, 5/5) are also approved.
Table Detail — Getting Stuck Here
Clicking a table name resulted in an error on the detail screen.

Calling 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 a lookup 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, table detail retrieval (reviewing AI-generated metadata) is impossible from both the UI and the API. This is the most practically impactful limitation when using Japanese table names at this point.
Explorer — Generated Classes

Inducted classes are listed with Japanese labels.
Playground — Querying in Japanese

It answers Japanese questions with Japanese values, and opening Compiled artifacts lets you check the executed SQL. FROM "商品マスタ" shows the Japanese table name correctly quoted.
Discussion
Key Considerations When Using Japanese Data at This Point
Here is a summary of what we learned from this verification.
| Item | Result |
|---|---|
| Japanese table names | Work in Glue, Athena, and COA alike. The class rdfs:label becomes Japanese |
| Japanese column names | Best avoided. All columns in a table collapse into a single property IRI |
| Japanese column descriptions (Glue Comment) | Effective. Entered as Japanese descriptions in the schema context |
| Japanese queries (Tier 2) | Work. However, LLM may occasionally forget to quote Japanese identifiers, causing one failure |
| Japanese business documents | Work. Propositions are extracted as a mix of Japanese and English |
| Tier 1 metric synonyms | Do not work in Japanese (still \b-based) |
| Table detail API | Returns 404 for non-ASCII table names (path parameter is not URL-decoded) |
"Use Japanese for table names but keep column names ASCII and write descriptions in Japanese" is the practical middle ground 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, you may decide to keep table names ASCII as well and give up on Japanese labels.
The Role of Multilingual Search
The reverse label containment in v0.2.2 did work against real data. However, where it works is limited.
The primary reason Japanese queries work in small namespaces is the multilingual embedding, and reverse matching serves as insurance 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 actually functions as insurance is itself meaningful.
Areas to Hope for Future Improvement
- Resolving property IRI collisions. Since the class side already has a hash suffix mechanism via
pascal_names_for, applying the same idea to properties would enable Japanese column names to be used - Decoding non-ASCII path parameters. The 404 in the table detail API looks like a problem solvable with a single
unquotecall - Multilingual support for Tier 1 synonym matching. Since v0.2.2 addressed Hangul residual tokens, support for Han / Kana / Thai characters would be a natural next step
- Identifier quoting in NL→SQL. First-time generation almost always fails with Japanese identifier schemas. Explicitly stating quoting rules in the prompt could eliminate one round trip
Closing
We tested the multilingual search of Context Ontology Accelerator v0.2.2 with Japanese data.
In v0.2.1, Japanese queries returned 0 results at the keyword extraction stage, and both the Tier 2 T-Box fallback and the Tier 3 keyword search returned empty without issuing SPARQL. v0.2.2 addresses this by switching tokenization to Unicode grapheme cluster units and adding reverse matching that "searches for stored labels within the query." Running the same FILTERs against the ontology generated by the real system, a query that returned 0 results with forward-only matching returned 1 hit with reverse matching. For queries in Chinese, where there are absolutely no word boundaries, forward matching cannot work in principle, and only reverse matching functions.
At the same time, 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 still relies on \b. It is not in a state where "Japanese support" can be described as a blanket capability — the level of support differs by layer.
For those considering using COA with Japanese data, a good starting point would be deciding how to name your tables and columns. Given that so much progress has been made in roughly two weeks since our previous blog post, and that further evolution is expected to continue, we hope you will not make final judgments based on the current state of things.
Further Reading




