インテージテクノスフィア技術ブログ

株式会社インテージテクノスフィアの社員達がシステム開発や仕事に関する技術情報を随時掲載いたします

Snowflakeで非構造化データを扱う:Cortex AISQLとFILE型による画像OCR実践ガイド

近年のSnowflakeは、データクラウドサービスからAI Data Cloudへと進化を遂げつつあります。

これまでの構造化データ分析に加え、AIを活用した非構造化データ処理自然言語によるデータ分析をSnowflake上で完結できるようになってきました。

今回は、その中でもAIを活用した非構造化データ処理に焦点を当ててご紹介します。

はじめに

こんにちは、藤平です。

2025年のSnowflake Summitでは、新機能 「Cortex AISQL」 が大きな注目を集めました。 この機能は、AIモデルを従来のSQLから自然に呼び出せる点が革新的です。

本記事では、非構造化データ(画像・PDF・音声など)をSnowflake上で扱う方法を中心に、FILE型データとAISQLを組み合わせた実践的な画像OCR手順を、実際のコード例を交えながら解説します。

この記事は、次のような方が対象です。

・Snowflakeを日常的に利用しているものの、AISQL機能はまだ試したことがない方
・非構造化データをSnowflake上でどうやって扱うのかを知りたい方
・BIやDWHの領域で、AIを使ったデータ分析の新しいアプローチに興味がある方

本記事を通じて、Snowflakeにおけるマルチモーダルデータ分析の仕組みと実践イメージの理解に少しでもお役立ていただければ幸いです。

目次

Cortex AISQLとは

Cortex AISQL とは、Snowflake上でAI機能を SQL関数として直接実行できる仕組みです。

LLMモデルのAPIを構築する必要なく、SELECT 文などの中で自然にAI処理を呼び出せる点が特徴です。

現在利用可能な代表的な関数と、それぞれが扱う主なデータ形式は以下の通りです。

2025年10月時点

非構造化データを扱う仕組み

Snowflakeで画像やPDFなどの非構造化データを扱う上で鍵となるのが、9月にGAされた FILE型 という新しいデータ型です。

この型は、ファイルそのものをバイナリデータとしてSnowflake内に保存するのではなく、内部または外部ステージ上のファイルへの参照パスとメタデータをテーブルに保持します。

つまりFILE型は、実体としては「どこに何があるか」を指すポインタのような存在ですが、Cortex AISQL関数を使用すればテーブル内の非構造化データに対して直接AI処理を行っているように見える形で、参照先のファイル内容を処理できるようになります。

FILE型の作成方法

非構造化データを FILE 型データとして扱うには、ステージ上のファイルパスをto_file()関数で指定することで作成します。

ファイルパスはディレクトリテーブルのfile_urlから取得できます。 (スコープ付きURLなど他にも方法はありますが今回は省略します。)

ディレクトリテーブル:
ステージ内のデータファイルに関するメタデータを取得できるテーブル

例えばステージ @my_stage に格納したファイルをFILE型に変換するには以下のクエリになります。

SELECT 
    TO_FILE(file_url)
FROM 
   DIRECTORY(@my_images);

ちなみに余談ですが、ディレクトリテーブルにはファイル内容に基づいて付与される一意のハッシュ値であるMD5カラムが含まれており、 ファイル名が異なっても中身が同じファイルを識別したい場合に便利です。

私はFILE型変換と同時に、このMD5をキー項目としてテーブルにINSERTしていました。

実践

では早速、実際に非構造化データをAISQLで分析する手順を見ていきましょう。 今回の記事では、Snowflake上で非構造化データを取り込み、AISQLで文字抽出して構造化するまでの一連の流れを実践形式で紹介します。

利用環境

前提として、今回の実行環境は以下の通りです。

・ AWS Tokyoリージョン
・ Enterpriseエディション
(2025年10月時点での実行)

準備:ステージの作成~FILE型変換

①まずは非構造化データを格納するステージを作成します。

内部ステージ・外部ステージのどちらでも利用可能ですが、外部ステージの場合は事前にストレージ統合が必要です。

(今回はストレージ統合の作成手順の詳細は割愛します。)

非構造化データを扱うステージでは、ディレクトリテーブルの有効化サーバーサイド暗号化が必須要件となります。

-- @my_stageの作成
CREATE OR REPLACE STAGE my_stage
  DIRECTORY = (ENABLE = TRUE)                -- ディレクトリテーブルの有効化
  ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE')      -- サーバーサイド暗号化
  STORAGE_INTEGRATION = my_integration;      -- ※外部ステージの場合のみ
;

画像は内部ステージで作成

②ステージが作成できたら、内部ステージまたは外部ストレージに非構造化データを格納します。

今回はreceiptディレクトリを切ってレシート画像を3枚入れてみました。

今回使用するレシートたち

DIRECTORY関数でステージ上のファイル一覧を取得し、TO_FILE関数を使って FILE 型に変換してテーブル化します。

-- FILE型で非構造化データを持つimage_tableを作成
CREATE OR REPLACE TABLE image_table AS
SELECT 
  MD5 AS image_id,
  RELATIVE_PATH AS file_name,
  TO_FILE(file_url) AS file_data
FROM DIRECTORY(@my_stage);
;

作成されたテーブル
※なお、テーブルを作成せずにサブクエリから直接AISQLに渡すことも可能です。

実践:レシート画像のOCR(AI_COMPLETE関数)

では、非構造化データをSQLで処理する準備が整ったので、早速 AISQL を使ってみたいと思います。

AISQLの中には画像や文書の解析・抽出に特化した関数はいくつかありますが、今回はAI_COMPLETE関数を使ってみたいと思います。 AI_COMPLETE はSnowflake Cortexの中でも汎用的な生成モデル関数で、プロンプト次第で自由度の高い解析や構造化が可能な点が大きな魅力です。

なぜ敢えてAI_COMPLETE関数を指定したかというと、以下のような理由があります。

・日本語に強い Claude-Sonnet モデルを指定できる
・レシートは定型フォーマットが無く、店舗によってレイアウト・表示情報がバラバラなため柔軟に抽出したい
・入れ子(ネスト)形式で商品明細を取得したい

※現在AWS TokyoリージョンではClaudeやOpenAIモデルは非対応なので、使いたい場合は事前にクロスリージョンをアカウントで有効にしておく必要があります。

ALTER ACCOUNT SET CORTEX_ENABLED_CROSS_REGION = 'ANY_REGION';

プロンプトは以下の通りで、レシートの店舗情報・日付・商品明細をJSON形式で出力するよう指示しました。

具体的な記載箇所は一切明示せず、抜き出してほしい項目だけ羅列しています。

あなたは日本のレシートを解析するAIアシスタントです。  
入力はレシート画像です。形式やレイアウトが異なっても内容を抽出し、  
以下のJSON形式で出力してください。

{
  "store_name": "",
  "store_address": "",
  "phone_number": "",
  "purchase_date": "YYYY-MM-DD",
  "items": [
    {
      "index": 1,
      "name": "",
      "jan_code": "",
      "quantity": 1,
      "unit_price": "",
      "discount": "",
      "total_price": ""
    }
  ],
  "subtotal": "",
  "discount": "",
  "tax": "",
  "total": "",
  "payment_method": "現金"
}

**ルール**
- クレジット明細・クーポン・広告・会員情報は無視する  
- 金額はカンマなしの整数値
- 不明な項目は空文字 `""` にする  
- 上記以外のキーは追加しない

プロンプトが長いので省略しますが、AISQLを実行して一旦JSONを格納するテーブルを作成します。

CREATE TABLE receipt_ocr_json
AS
SELECT
        image_id,
        file_name,
        file_data,
        TRY_PARSE_JSON(
            AI_COMPLETE(
                'claude-3-5-sonnet',
                'ここにプロンプトを入れる {0}', file_data
            )
        ) as ocr_json, 
    FROM image_table
;

3レシートだと15秒で処理が完了しました

JSONデータの構造化

無事JSON形式でレシート情報を抽出できたので、分析に扱いやすいよう構造化形式に整えたいと思います。

まず、入れ子になっていない商品情報以外の基本項目については、VARIANT 型カラム内のJSONキーを カラム名:キー名 の形式で指定します。

また、形式の揺れや欠損値に備えて、安全に変換を試みるために TRY_ 系関数を使用しています。

SELECT
  image_id,
  NULLIF(ocr_json:store_name::STRING, '')          AS store_name,
  NULLIF(ocr_json:store_address::STRING, '')       AS store_address,
  NULLIF(ocr_json:phone_number::STRING, '')        AS phone_number,
  TRY_TO_DATE(NULLIF(ocr_json:purchase_date::STRING, '')) AS purchase_date,
  TRY_TO_NUMBER(NULLIF(ocr_json:subtotal::STRING, ''))     AS subtotal,
  TRY_TO_NUMBER(NULLIF(ocr_json:discount::STRING, '')) AS discount,
  TRY_TO_NUMBER(NULLIF(ocr_json:tax::STRING, ''))           AS tax,
  TRY_TO_NUMBER(NULLIF(ocr_json:total::STRING, ''))         AS total,
  NULLIF(ocr_json:payment_method::STRING, '')               AS payment_method
FROM receipt_ocr_json;

結果

購入商品は、JSON内の "items" キーで配列形式で格納されています。

このような入れ子構造のデータは、FLATTEN 関数を使うことで「1商品=1行」に展開することができます。

SELECT
  image_id,
  item.value:index::INT                                AS item_index,
  NULLIF(item.value:name::STRING, '')                  AS item_name,
  NULLIF(item.value:jan_code::STRING, '')              AS jan_code,
  TRY_TO_NUMBER(NULLIF(item.value:quantity::STRING, '')) AS quantity,
  TRY_TO_NUMBER(NULLIF(item.value:unit_price::STRING, '')) AS unit_price,
  TRY_TO_NUMBER(NULLIF(item.value:total_price::STRING, '')) AS total_price,
  TRY_TO_NUMBER(NULLIF(item.value:discount::STRING, '')) AS discount
FROM receipt_ocr_json as r, 
     LATERAL FLATTEN(input => ocr_json:items) AS item;

結果

抽出精度を確認

無事にJSONを構造化できたので、実際のレシート画像と照らし合わせて、どの程度正確に文字が読み取れているかを確認してみます。

レシート一枚目
ご覧の通り、かなり良い精度で情報が抽出できています。

店舗名が●●店まで読み取れなかったのは惜しいものの、ミルクティーの行で商品名の-440と割引額、単価あたりが誤認識せず正確に項目を分類している点が非常に優秀と感じました。

レシート2枚目
先ほどの例に比べて商品点数が多く、文字サイズも小さめで、 さらにJANコードが印字され、店舗情報がレシート下部に記載されているパターンです。

それでもご覧の通り、AISQLはレイアウトの違いに左右されることなく、 各項目を正確に抽出できています。

レシート3枚目
最後は比較的文字が大きく、印字も明瞭なレシートを使用しました。 こちらはご覧の通り、全ての項目で完璧に抽出できています。

今回のような明瞭なレシートであればかなり精度よく抽出できることが確認できた一方で、一般的なOCRと同様に、文字が小さい・影がかかっている・レシートが折れている場合などは、一部の項目で認識精度がやや低下するようです。

より安定した精度を出すには事前の画像トリミングなどの前処理が必要になってくるかもしれません。

また、今回は AI_COMPLETE 関数を利用しましたが、画像の種類や形式によっては AI_EXTRACT や AI_PARSE_DOCUMENT の方が適しているケースもあります。 ユースケースに応じて関数を使い分けていきたいですね。

まとめ

本記事では、Cortex AISQL と FILE型を活用し、Snowflake上で非構造化データを取り扱う方法を紹介しました。 実践例として、AI_COMPLETE 関数を用いたレシート画像のOCR処理を行い、画像から構造化データとして扱えることを確認しました。

今後も、他のCortex AISQLとの使い分けや組み合わせも試しながら、 非構造化データをより効率的に分析できるパターンを探っていきたいと思います。

関連資料

Cortex AISQLが発表されたSnowflake Summit2025の参加レポはこちら! note.intage-technosphere.co.jp