
Snowflake AI_EXTRACT で複数の文書種を構造化抽出してみた
こんにちは、データ事業本部のまっきーです。
Snowflake AI_EXTRACT で、検査レポートに加えて技術ホワイトペーパー用の抽出スキーマを追加し、同じ仕組みで複数の文書種を扱う設計を試しました。題材は Snowflake 公式の Create a Document Processing Pipeline with AI_EXTRACT ハンズオンの拡張です。
結論を先に
PDF を Stage に置いて extract_document_data('@stage', 'file.pdf', 'TEMPLATE_ID') を叩くと、抽出スキーマに沿った構造化 JSON が返ってきます。新しい文書種を増やしたいときは、prompt_templates テーブルに抽出ルールを INSERT で 1 行足すだけ。SharePoint や S3 にたまっていく業務文書(提案書 / 議事録 / ホワイトペーパー / 検査レポート等)からメタデータを抜き出して検索・分析できる状態にする、ナレッジベース構築のコアになる設計です。
AI_EXTRACT とは
AI_EXTRACT は Snowflake Cortex AI の SQL 関数で、ドキュメントやテキストから構造化データを抽出します。抽出したい項目を JSON Schema(
responseFormat)で指定すると、Cortex 側の LLM が該当する値を JSON で返してくれます。
旧 Document AI のように モデルを自分で学習させる必要がなく、SQL を書けば即座に試せる のが特徴です。2025-10-16 に GA に到達した機能です。
「抽出したい項目を JSON Schema で書く」=「ナレッジベースで PDF に持たせたいメタデータ項目を設計する」 という構造になっていて、設計フェーズの議論がそのまま実装に直結します。
本記事での拡張ポイント
公式ハンズオンは検査レポート 1 種類だけが対象ですが、実務では複数の文書種を扱いたい場面が多いはずです。そこで本記事では、公式ハンズオンの設計を活かしつつ技術ホワイトペーパー用の抽出スキーマを追加してみました。
| 項目 | 公式ハンズオン | 本記事(拡張版) |
|---|---|---|
| 抽出対象 | 検査レポート 1 種類 | 検査レポート + 技術ホワイトペーパー の 2 種類 |
| 抽出スキーマ | INSPECTION_REVIEWS のみ |
INSPECTION_REVIEWS + TECHNICAL_WHITEPAPER |
| ラッパー関数 | 単一テンプレ固定 | テンプレ ID 引数で切り替え可能 |
| Stage | my_pdf_stage 1 つ |
my_pdf_stage + whitepaper_stage の 2 つ |
手順
1. 環境セットアップ
DB / Schema / Stage / 関数の作成権限を持つロール(SYSADMIN 等)で進めます。
CREATE DATABASE makita_doc_ai_db;
CREATE SCHEMA makita_doc_ai_db.doc_ai_schema;
USE DATABASE makita_doc_ai_db;
USE SCHEMA doc_ai_schema;
USE WAREHOUSE COMPUTE_WH;
2. 抽出スキーマをテーブルに切り出して管理
この設計が今回のキモです。AI_EXTRACT に渡す JSON Schema を SQL コードにハードコードせず、専用テーブルに切り出します。
CREATE TABLE IF NOT EXISTS prompt_templates (
template_id VARCHAR PRIMARY KEY, -- テンプレートID(例: INSPECTION_REVIEWS)
response_format VARIANT -- JSON Schema 本体
);
INSERT INTO prompt_templates
SELECT 'INSPECTION_REVIEWS',
PARSE_JSON('{
"schema": {
"type": "object",
"properties": {
"list_of_units": {
"description": "Extract the table showing all units and conditions",
"type": "object",
"properties": {
"unit_name": {"type": "array"},
"condition": {"type": "array"}
}
},
"inspection_date": {"type": "string"},
"inspection_grade": {"type": "string"},
"inspector": {"type": "string"}
}
}
}');
response_format カラムは VARIANT 型(半構造化データを構造を保ったまま格納できる Snowflake の型)で定義しています。
VARIANT 型に JSON を入れるときは
PARSE_JSON()で「これは JSON だよ」と Snowflake に教える必要があります。これを忘れるとただの文字列として保存されてしまい、キーパスアクセス(例:response_format:schema.type)ができません。
実務でナレッジベースを設計するときは「どの文書種に、どの抽出項目を持たせるか」を顧客と合意していく作業が発生します。その議論結果が そのまま INSERT 文として実装に落ちる のがこの設計の旨みです。
3. Stage 作成と AI_EXTRACT ラッパー関数
PDF の置き場所(Stage)と、AI_EXTRACT を呼び出すためのラッパー関数を作ります。
CREATE STAGE IF NOT EXISTS my_pdf_stage
DIRECTORY = (ENABLE = TRUE)
ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE');
CREATE OR REPLACE FUNCTION extract_document_data(
stage_name STRING,
file_path STRING
)
RETURNS VARIANT
LANGUAGE SQL
AS $$
SELECT AI_EXTRACT(
file => TO_FILE(stage_name, file_path),
responseFormat => (
SELECT response_format
FROM prompt_templates
WHERE template_id = 'INSPECTION_REVIEWS'
)
):response
$$;
AI_EXTRACT を直接呼ぶと毎回ファイル指定 + スキーマ取得を SQL で書く必要があります。1 関数にまとめておけば、利用側は extract_document_data('@stage', 'file.pdf') のように短く呼べます。
AI_EXTRACT のシグネチャ変更や、前後処理(プロンプト編集 / ログ記録など)が必要になったときも、ラッパー関数の中身だけ直せば全利用箇所に反映できます。
4. 2 種類目の抽出スキーマを追加(技術ホワイトペーパー用)
ここから本記事独自の拡張です。prompt_templates テーブルにホワイトペーパー用のスキーマを INSERT します。
INSERT INTO prompt_templates
SELECT 'TECHNICAL_WHITEPAPER',
PARSE_JSON('{
"schema": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "ドキュメントのタイトル"},
"product_name": {"type": "string", "description": "紹介されているプロダクト・サービス名"},
"key_features": {"type": "array", "description": "主要機能のリスト"},
"use_cases": {"type": "array", "description": "想定ユースケース"},
"target_audience": {"type": "string", "description": "想定読者(エンジニア / マネジメント / 経営層など)"},
"publication_date": {"type": "string", "description": "発行日(記載があれば)"}
}
}
}');
次にラッパー関数を テンプレート ID を引数で受け取る汎用版 に改修します。Step 3 ではテンプレ ID をハードコードしていましたが、引数で渡せるようにすれば 1 関数で複数の文書種に対応できます。
引数名は カラム名 template_id と衝突しない別名 にします。理由は後述の「ハマったところ」で。
CREATE OR REPLACE FUNCTION extract_document_data(
stage_name STRING,
file_path STRING,
p_template_id STRING -- ← 引数名はカラム名 template_id と衝突させない
)
RETURNS VARIANT
LANGUAGE SQL
AS $$
SELECT AI_EXTRACT(
file => TO_FILE(stage_name, file_path),
responseFormat => (
SELECT response_format
FROM prompt_templates
WHERE template_id = p_template_id
)
):response
$$;
これで利用側は 同じ関数を呼びつつ、template_id を切り替えるだけ で文書種に応じた抽出ができます。
SELECT extract_document_data('@my_pdf_stage', 'inspection.pdf', 'INSPECTION_REVIEWS');
SELECT extract_document_data('@whitepaper_stage', 'snowflake_guide.pdf', 'TECHNICAL_WHITEPAPER');
5. ホワイトペーパー専用の Stage 作成
文書種類ごとに Stage を分けておくと、後で「ホワイトペーパー全部を AI_EXTRACT に通したい」みたいな処理が書きやすくなります。
CREATE STAGE IF NOT EXISTS whitepaper_stage
DIRECTORY = (ENABLE = TRUE)
ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE');
6. PDF アップロードと初回 AI_EXTRACT 実行
題材は Snowflake 公式の「The New Essential Guide to Data Engineering」(22 ページの ebook)です。Snowsight の「Data → Databases → MAKITA_DOC_AI_DB → DOC_AI_SCHEMA → Stages → WHITEPAPER_STAGE」を開いて「+ Files」から PDF をアップロード。
ALTER STAGE whitepaper_stage REFRESH;
SELECT * FROM DIRECTORY('@whitepaper_stage');
SELECT extract_document_data(
'@whitepaper_stage',
'ebook-the-essential-guide-to-data-engineering-updated.pdf',
'TECHNICAL_WHITEPAPER'
) AS result;
実行すると 1 分前後で結果が返ってきました。以下は本記事で使用した PDF (Snowflake 公式 ebook「The New Essential Guide to Data Engineering」) に対する実際の抽出結果です。
{
"title": "Snowflake",
"product_name": "Snowflake",
"key_features": [
"Lineage tracing",
"Data cataloging",
"Access privileges",
"Change management"
],
"use_cases": [
"高頻率取引",
"詐欺検出"
],
"target_audience": "エンジニア",
"publication_date": "None"
}
use_cases の「高頻率取引」は AI_EXTRACT が実際に返してきた表記です(一般的な日本語では「高頻度取引」(high-frequency trading)が正しい表記)。AI 出力の言語選択や表記揺れは、後述の description の言語影響と合わせて、設計時に考慮すべきポイントです。
JSON を列に展開したい場合は :キー 記法でキーパスアクセスします。
WITH extracted AS (
SELECT extract_document_data(
'@whitepaper_stage',
'ebook-the-essential-guide-to-data-engineering-updated.pdf',
'TECHNICAL_WHITEPAPER'
) AS data
)
SELECT
data:title::STRING AS title,
data:product_name::STRING AS product_name,
data:key_features AS key_features,
data:use_cases AS use_cases,
data:target_audience::STRING AS target_audience,
data:publication_date::STRING AS publication_date
FROM extracted;
「無い情報」は文字列 "None" で返ってくる
題材 PDF には発行日の記載がなかったのですが、AI_EXTRACT は publication_date に文字列 "None" を返してきました。書かれていない情報を無理に作らない挙動はハルシネーション対策として有効ですが、NULL ではなく文字列なので、後段のテーブル設計で「未取得は NULL として扱いたい」場合は変換処理が必要です。
7. メタデータテーブルとして永続化
毎回 AI_EXTRACT を呼び直さなくて済むよう、結果を CTAS でテーブル化します。
CREATE OR REPLACE TABLE document_metadata AS
SELECT
RELATIVE_PATH AS file_name,
SIZE AS file_size,
LAST_MODIFIED AS last_modified,
FILE_URL AS snowflake_file_url,
extract_document_data(
'@whitepaper_stage',
RELATIVE_PATH,
'TECHNICAL_WHITEPAPER'
) AS extracted_data
FROM DIRECTORY('@whitepaper_stage');
新しい PDF を Stage に追加して CREATE OR REPLACE TABLE を流し直せば、全件再抽出も簡単です。ファイル追加のたびに自動で AI_EXTRACT を走らせたい場合は Stream + Task を組み合わせる方法がありますが、本記事のスコープからは外しています。
ハマったところ
Single-row subquery returns more than one row. エラー
Step 4 でラッパー関数を汎用版に改修したあと、AI_EXTRACT を実行したら次のエラーが出ました。
Single-row subquery returns more than one row.
原因は カラム名と引数名がどちらも template_id で完全に同じ だったこと。
WHERE template_id = template_id -- ★ カラム = カラム と解釈されて常に TRUE
SQL のスコープ解決では同名の場合カラムが優先されるため、この条件式は「カラム = カラム」と解釈されて常に TRUE になり、prompt_templates の全行(2 行)がマッチします。サブクエリは 1 行返ることが前提なのでエラーになります。
引数名を別名(例: p_template_id)に変えることで解消します。カラム名と関数引数で同名を使ってしまう書き方は SQL あるあるなので、レビュー観点として頭に置いておくと良さそうです。
description の言語が出力 JSON の言語に影響する
本記事の検証では、抽出スキーマの description を日本語で書いた状態(Step 4 のスキーマ参照)で英語 PDF を AI_EXTRACT に渡したところ、返却 JSON に 日本語値が混ざる 結果が確認されました。
{
"use_cases": ["高頻率取引", "詐欺検出"],
"target_audience": "エンジニア"
}
PDF 本体は英語なので、ユーザーとしては「英語値で揃えたい」または「日本語値で揃えたい」のどちらかに統一したい場面です。description の言語を統一する / システムプロンプト的な指示を追加する / 後段で翻訳を挟む等、要件に応じて整える必要があります。なお、AI_EXTRACT の responseFormat 仕様の詳細は AI_EXTRACT 公式ドキュメント を参照してください。
まとめ
AI_EXTRACTは SQL 1 行で PDF を構造化 JSON に変換できる関数で、responseFormatで抽出項目を JSON Schema として宣言する。- 抽出ルールをテーブル(
prompt_templates)に切り出し、ラッパー関数の引数でテンプレ ID を切り替える設計にしておくと、文書種類が増えてもINSERT1 行で対応 できる。 - 抽出スキーマの設計 = 「ナレッジベースで PDF に持たせたいメタデータ項目を顧客と合意する」作業そのもの。設計フェーズの議論がそのまま実装に直結する。
- 引数名とカラム名が同じだと SQL がカラムを優先して解釈する、という落とし穴に注意。
- 「無い情報」は NULL ではなく文字列
"None"で返ってくる。
次は AI_PARSE_DOCUMENT(PDF → Markdown / テキストへの構造化変換)を触って、AI_EXTRACT との使い分けや、両者を組み合わせたパイプライン構成を整理してみたいと思います。
参考
- Create a Document Processing Pipeline with AI_EXTRACT ー 公式ハンズオン(本記事の手順元)
- AI_EXTRACT 公式ドキュメント ー 関数仕様
- AI_PARSE_DOCUMENT 公式ドキュメント ー 関連関数






