<?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom">
	<title>レンコン畑でつかまえて</title>
	<subtitle></subtitle>
	<link href="https://www.jyuch.dev/feed.xml" rel="self"/>
	<link href="https://www.jyuch.dev/"/>
	
	<updated>2026-05-05T09:47:55Z</updated>
	<id>https://www.jyuch.dev/</id>
	<author>
		<name>jyuch</name>
		<email></email>
	</author>
	<entry>
		<title>はじめてのDatabricks on AWS</title>
		<link href="https://www.jyuch.dev/posts/2026/05-05-how-to-deploy-databricks-on-aws/"/>
		<updated>2026-05-05T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2026/05-05-how-to-deploy-databricks-on-aws/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;上司から「Databricksを使えるようにしておいて」と言われることは長い人生のなかでは一度くらいはあるかもしれません。&lt;/p&gt;
&lt;p&gt;「まぁ、&lt;a href=&quot;https://docs.databricks.com/aws/en/&quot;&gt;公式リファレンス&lt;/a&gt;を見れば何とかなるやろ」と意気揚々と見に行くも、そもそもどこから読み始めたらよいかよくわからないリファレンスを前に心をへし折られた方もいると思います。私です&lt;/p&gt;
&lt;p&gt;ここでは、私が実際にDatabricksをAWSに展開したときに知っておきたかった前提知識や参考になったページ、約1年程度運用して最初から考慮した方がよかったと思うプラクティスなどを紹介します。&lt;/p&gt;
&lt;h2&gt;前提知識&lt;/h2&gt;
&lt;h3&gt;AWS&lt;/h3&gt;
&lt;p&gt;Databricksは基本的には顧客（つまり私たち）のAWS環境にデータを保存したり計算資源を作成します。
&lt;a href=&quot;https://docs.databricks.com/aws/en/admin/workspace/serverless-workspaces&quot;&gt;Serverless workspace&lt;/a&gt;のようなものもあったりしますが、すべてをServerless workspaceで賄うのはたぶん無理だと思うのでいったん忘れてください。&lt;/p&gt;
&lt;p&gt;Databricksでは主に以下のAWSリソースを使用するため、最低限概要をつかんでおく必要があります。&lt;/p&gt;
&lt;h4&gt;ストレージ（S3）&lt;/h4&gt;
&lt;p&gt;Databricksではあらゆる顧客データをS3のバケットに作成します。&lt;/p&gt;
&lt;p&gt;生データはそのままオブジェクトとして保存されますし、テーブルデータは&lt;a href=&quot;https://docs.databricks.com/aws/en/delta/&quot;&gt;Delta Lake&lt;/a&gt;と呼ばれるOpen Table Format（OTF）として保存されます。
ワークスペースで作成したノートブックなども実体はS3に保存されます。&lt;/p&gt;
&lt;p&gt;OTFにはそのほかにも&lt;a href=&quot;https://iceberg.apache.org/&quot;&gt;Apache Iceberg&lt;/a&gt;などがありますが、この段階では気にする必要はありませんし、&lt;a href=&quot;https://www.databricks.com/jp/company/newsroom/press-releases/databricks-agrees-acquire-tabular-company-founded-original-creators&quot;&gt;そのうち違いは無くなるので&lt;/a&gt;将来的にはそんなこともありましたよねという感じになると思います。&lt;/p&gt;
&lt;p&gt;また、使うのはS3 Tablesのようなものではなく普通のS3です。
Delta Lake自体にバージョニング機能（タイムトラベルクエリ）も持っているため、バケット側のバージョニングも使いません。&lt;/p&gt;
&lt;h4&gt;計算資源（EC2）&lt;/h4&gt;
&lt;p&gt;Databricksには主に&lt;a href=&quot;https://docs.databricks.com/aws/en/compute/clusters-manage&quot;&gt;クラシックコンピュート&lt;/a&gt;と&lt;a href=&quot;https://docs.databricks.com/aws/en/compute/serverless/&quot;&gt;サーバレスコンピュート&lt;/a&gt;の二種類があります。&lt;/p&gt;
&lt;p&gt;そのうち、クラシックコンピュートは顧客のAWSアカウントで動作するEC2を実体としています。
EC2を稼働させるVPCがオンプレミスやほかのAWSリソースに接続しに行ける構成となっていれば、そのままDatabricksからオンプレミスのデータベースに接続することができます。&lt;/p&gt;
&lt;p&gt;サーバレスコンピュートはDatabricks社のAWSアカウント上のKubernetesクラスタ上で動作しているらしいです。
そのため、サーバレスコンピュート用の計算資源は管理する必要はありません。&lt;/p&gt;
&lt;h4&gt;ネットワーク（VPC・Peering・Direct Connect・Site-to-Site VPN）&lt;/h4&gt;
&lt;p&gt;クラッシックコンピュートの実体がEC2であるならば、そのEC2を稼働させるためのVPCも必要になります。&lt;/p&gt;
&lt;p&gt;Databricksからオンプレミスのデータベースに接続する必要があれば、Direct ConnectやSite-to-Site VPNを使用してオンプレミスとの接続性を確保する必要があります。
同様に、他のVPCのリソースに接続しに行く必要があればVPC Peeringなどで接続性を確保する必要があります。&lt;/p&gt;
&lt;p&gt;この辺はDatabricks固有のお話というよりかは一般的なVPCネットワークの設計のお話となります。&lt;/p&gt;
&lt;h4&gt;Databricksコントロールプレーンへの権限の委譲（IAM）&lt;/h4&gt;
&lt;p&gt;AWSのリソースは顧客のAWSアカウントに作成しますが、クラシックコンピュート起動するたびに手動でEC2インスタンスを作成するわけではありません。&lt;/p&gt;
&lt;p&gt;クロスアカウントアクセスが可能なIAMロールをDatabricksに連携しておいて、Databricksの制御側（この制御側をよくコントロールプレーンと呼びます）から必要なEC2インスタンスを作成したり削除してもらいます。&lt;/p&gt;
&lt;p&gt;また、S3バケットに格納されているデータにアクセスするためのIAMロールなども作成して連携する必要があります。
クラシックコンピュートやサーバレスコンピュートはこのIAMロールをAssume Roleしてデータに触りに行きます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/connect/unity-catalog/cloud-services/service-credentials&quot;&gt;Create service credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/connect/unity-catalog/cloud-storage/&quot;&gt;Connect to cloud object storage using Unity Catalog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Databricks&lt;/h3&gt;
&lt;p&gt;Databricksではトップレベルの要素として、Unity Catalogとワークスペースが存在します。
Databricksの論理設計ではUnity Catalogとワークスペースの設計を最初に行います。&lt;/p&gt;
&lt;p&gt;Unity Catalogの階層構造の設計や、ワークスペースの分割設計が今後のDatabricks運用に多大な影響を与えます。
そのため、この部分の設計はふわっとやらない方が良いと思います。&lt;/p&gt;
&lt;h4&gt;Unity Catalog&lt;/h4&gt;
&lt;p&gt;Unity Catalog（UC）はデータを格納しアクセス権を管理するための入れ物となります。&lt;/p&gt;
&lt;p&gt;UCはAWSの場合、リージョン毎に1つしか作成することができません。&lt;/p&gt;
&lt;p&gt;UCでは&lt;strong&gt;常に&lt;/strong&gt;&lt;code&gt;catalog.schema.object&lt;/code&gt;という3タプルの識別子でオブジェクト（テーブル・ボリューム・モデル）を識別します。2つにしたり4つにしたりは出来ません。
そのため、カタログやスキーマの階層構造の設計はその後のデータ整理に多大な影響を及ぼします。
また、運用途中での変更には多大な労力がかかります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/data-governance/unity-catalog/&quot;&gt;What is Unity Catalog?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;ワークスペース&lt;/h4&gt;
&lt;p&gt;UCがデータの入れ物であるならば、ワークスペースはプログラム（ノートブック・Job・パイプライン・ダッシュボード）と計算資源（クラスタ・Vecto Search）の入れ物となります。&lt;/p&gt;
&lt;p&gt;ワークスペースは一つのリージョンの中に複数することができます。
ワークスペースはアクセスの境界にもなるので、組織間のアクセス制御にワークスペースを使うことも多いです。&lt;/p&gt;
&lt;h4&gt;クラシックコンピュートとサーバレスコンピュート&lt;/h4&gt;
&lt;p&gt;クラシックコンピュートとサーバレスコンピュートはワークスペース作成時にどちらを使うか選択するようなものではなく、用途によってそれぞれ使い分けを行います。&lt;/p&gt;
&lt;p&gt;サーバレスは制約は多いですが、起動がめっちゃ早いという1点だけでもかなり便利なので、個人的にはサーバレスでは出来ないこと以外は基本的にはサーバレスを使っています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;クラシックコンピュート
&lt;ul&gt;
&lt;li&gt;メリット
&lt;ul&gt;
&lt;li&gt;VPCの構成によってはオンプレミスのリソースに接続しに行ける&lt;/li&gt;
&lt;li&gt;EC2のインスタンスタイプやクラスタサイズを柔軟に構成できる&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;デメリット
&lt;ul&gt;
&lt;li&gt;起動に時間が掛かる&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;サーバレスコンピュート
&lt;ul&gt;
&lt;li&gt;メリット
&lt;ul&gt;
&lt;li&gt;起動がほぼ一瞬&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;デメリット
&lt;ul&gt;
&lt;li&gt;サイズはいくつかのサイズから選ぶだけで、細かい調整は出来ない&lt;/li&gt;
&lt;li&gt;（&lt;a href=&quot;https://docs.databricks.com/aws/en/security/network/serverless-network-security/pl-to-internal-network&quot;&gt;PrivteLinkの設定をしないと&lt;/a&gt;）オンプレミスのリソースには接続しに行けないうえに、PrivateLinkは追加のコストが掛かる&lt;/li&gt;
&lt;li&gt;GPUは使えない&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;インフラ設計&lt;/h2&gt;
&lt;h3&gt;ワークスペースとAWSリソースの関係&lt;/h3&gt;
&lt;p&gt;ワークスペースにはEC2を動かすためのアベイラビリティゾーンが異なるサブネットが最低2つと、ワークスペースのリソースを保存するためのS3バケットが1つ必要になります。&lt;/p&gt;
&lt;p&gt;ワークスペースを作成するのに指定したバケットはそのまま&lt;a href=&quot;https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-external-locations&quot;&gt;External locations&lt;/a&gt;としても登録されるので、UCを経由してテーブルなどのUCオブジェクトを保存するのにも使用できます。&lt;/p&gt;
&lt;p&gt;また、ワークスペースのバケット以外にも別途バケットを作成してExternal locationとして登録すれば、UCオブジェクトを登録する専用のバケットとして使用することもできます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/05-05-how-to-deploy-databricks-on-aws/workspace-aws-resources.png&quot; alt=&quot;ワークスペースに紐づくAWSリソース&quot;&gt;&lt;/p&gt;
&lt;h3&gt;VPC構成&lt;/h3&gt;
&lt;p&gt;EC2を動かすためのサブネットは最低限インターネットへの接続性があれば大丈夫です。
また、S3用のゲートウェイエンドポイントには費用が掛からないのと、用意しないとS3のアウトバンドコストがまぁまぁ大変なことになるので最低限S3ゲートウェイエンドポイントは用意しておきます。&lt;/p&gt;
&lt;p&gt;あとは、オンプレミスのデータベースやほかのVPCへの接続性のためにDirect ConnectやVPC Peeringを必要に応じて設定します。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/05-05-how-to-deploy-databricks-on-aws/vpc-design-least.png&quot; alt=&quot;最低限の構成&quot;&gt;&lt;/p&gt;
&lt;p&gt;ただし、この構成はクラシックコンピュートとコントロールプレーンの通信はインターネットを経由します。
業界のコンプライアンス要件などでインターネットを経由できない場合、AWSサービスエンドポイントとDatabricksバックエンドサービスへのPrivateLinkを追加で構成することでインターネットへの接続性なしでクラシックコンピュートを運用できます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/05-05-how-to-deploy-databricks-on-aws/vpc-design-full.png&quot; alt=&quot;コントロールプレーンとの通信がインターネットを通らない構成&quot;&gt;&lt;/p&gt;
&lt;p&gt;クラシックコンピュートがインターネットへの接続性を持たない構成にすることもできます。
この場合、ノートブックでPythonライブラリをインターネット経由でインストールすることが出来なくなるので、別途プロキシリポジトリを建てるかwheelパッケージをあらかじめダウンロードしてS3などに配置する必要があります。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/05-05-how-to-deploy-databricks-on-aws/vpc-design-air-gap.png&quot; alt=&quot;エアギャップ構成&quot;&gt;&lt;/p&gt;
&lt;p&gt;厳密なインターネット分離が求められる場合、クラシックコンピュート⇔コントロールプレーン間だけでなくユーザ⇔コントロールプレーン間でもインターネットを経由しないことが求められるかと思います。
その場合、&lt;a href=&quot;https://docs.databricks.com/aws/en/security/network/front-end/front-end-private-connect&quot;&gt;フロントエンドへの接続もインターネットを経由しない構成&lt;/a&gt;にすることができます。&lt;/p&gt;
&lt;p&gt;この辺を真面目にやろうとすると結構大変なので、やる場合は実力のあるサポートベンダに相談した方が良いです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/security/network/classic/privatelink&quot;&gt;Configure classic private connectivity to Databricks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/security/network/front-end/front-end-private-connect&quot;&gt;Configure Inbound PrivateLink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;プラクティス&lt;/h2&gt;
&lt;h3&gt;VPCは新規で作成するか？既存のを流用するか？&lt;/h3&gt;
&lt;p&gt;導入企業の既存のAWS環境の設計やポリシーに強く依存しますが、基本的には分けた方が良いと思います。
VPCが分かれてもVPC PeeringでほかのVPCに触りに行くことが出来ますし、Databricks向けのリソースと他のリソースが混ざる心配もありません。&lt;/p&gt;
&lt;p&gt;もっというと、アカウントを分けるぐらいの勢いで行っても良いと思います。&lt;/p&gt;
&lt;h3&gt;サブネットサイズ設計&lt;/h3&gt;
&lt;p&gt;クラシックコンピュートのインスタンスは対コントロールプレーンに1つと、クラスタ内の通信に1つの計2つのプライベートアドレスを使用します。
また、クラスタの再起動時や拡張・縮退時など稼働しているインスタンスよりも多い数のプライベートアドレスを使用することがあります。&lt;/p&gt;
&lt;p&gt;とはいえ、あるワークスペースに割り当てられたサブネットは他のワークスペースに重複して割り当てが出来ないため、過剰に大きいサブネットに分割してしまうとサブネットが足りなくなる場面も出てきます。私です&lt;/p&gt;
&lt;p&gt;個人的には最小のサブネットを&lt;code&gt;/22&lt;/code&gt;として、稼働予定のインスタンスの倍の数を収容できるサブネットに分割するのがいいのかなと思っています。&lt;/p&gt;
&lt;h3&gt;マルチAZ構成&lt;/h3&gt;
&lt;p&gt;ワークスペースに関連付けるサブネットは最低でも2つ以上のAZで構成しなければいけませんが、高可用構成にするには最低でもNAT Gatewayも1つのAZに1つ作成するなど全体として単一障害点をなくさないといけません。&lt;/p&gt;
&lt;p&gt;運用としてはクラシックコンピュートがワークロードの主体になっており、高可用性が求められるのであれば単一障害点をなくす設計を行わないといけません。&lt;/p&gt;
&lt;p&gt;対して、普段のワークロードはサーバレスコンピュートが主体でクラシックコンピュートはオンプレミスに触りに行くだけとかであれば費用対効果の観点から高可用を捨ててしまってもいいかもしれません。&lt;/p&gt;
&lt;h3&gt;リソースの命名規則&lt;/h3&gt;
&lt;p&gt;DatabricksとAWSという異なる世界のリソースを一緒に扱うため、リソースの命名の一貫性には気を付けています。
過去の経緯から一貫性を欠いているものも一部ありますが、こんな感じで短さよりも分かりやすさを優先して命名をしています。&lt;/p&gt;
&lt;h4&gt;Databricks&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;ワークスペース
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{リージョン名}-{連番}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apn1-0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;AWS&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;VPC
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;databricks-vpc-{リージョン}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;databricks-vpc-apn1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;サブネット
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;databricks-subnet-{ワークスペース}-{AZ}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;databricks-subnet-apn1-0-a&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;バケット（カタログ）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;databricks-catalog-{リージョン}-rootbucket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;databricks-catalog-apn1-rootbucket&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;バケット（ワークスペース）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;databricks-workspace-{ワークスペース名}-rootbucket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;databricks-workspace-apn1-0-rootbucket&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Terraformでのリソースのデプロイ&lt;/h3&gt;
&lt;p&gt;どこまでをTerraformなどのIaCに乗せるかは組織や担当者のIaCへの理解度に依存すると思います。&lt;/p&gt;
&lt;p&gt;私はAWSのリソースはTerraformでデプロイしていますが、Databricksのリソースは手動で展開しています。&lt;/p&gt;
&lt;p&gt;Databricksのリソースはものによっては一度作成したら削除出来ない・一度設定したら削除して再作成しないと変更できないリソースがそこそこあり、無理にTerraform化してよく分からないことになるくらいだったら手動で管理した方が管理しやすいと考えてそのようにしています。&lt;/p&gt;
&lt;h3&gt;カタログ設計&lt;/h3&gt;
&lt;p&gt;UCでは&lt;code&gt;catalog.schema.object&lt;/code&gt;の3タプルでオブジェクトが識別されます。
また、アクセス権は階層構造で設定するので、3タプルの構造がそのままアクセス権の構造に影響します。&lt;/p&gt;
&lt;p&gt;この辺は組織のポリシーやデータの特性に大きく影響を受けるためこれが正解というのはありませんが、一例として以下のようなイメージが考えられます。&lt;/p&gt;
&lt;p&gt;ここでは&lt;a href=&quot;https://www.databricks.com/jp/blog/what-is-medallion-architecture&quot;&gt;メダリオンアーキテクチャ&lt;/a&gt;に従って、&lt;code&gt;販売_bronze&lt;/code&gt;をブロンズ層としてデータの着地点として利用します。&lt;/p&gt;
&lt;p&gt;後続のシルバー層に向けてデータを供給する役目を持つことからブロンズ層はユーザには見せません。
ブロンズ層は可能な限り上流のシステムに近い構成とし、認知負荷を下げます。&lt;/p&gt;
&lt;p&gt;シルバー層ではすべてのユーザが見れる情報と閲覧できる人が限られる情報で分けてカタログに収納します。
閲覧できる人が限られる情報についてはカタログとスキーマで権限を制御します。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;catalog&lt;/th&gt;
      &lt;th&gt;schema&lt;/th&gt;
      &lt;th&gt;table&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td rowspan=&quot;4&quot;&gt;販売_bronze&lt;/td&gt;
      &lt;td rowspan=&quot;3&quot;&gt;販売&lt;/td&gt;
      &lt;td&gt;受注&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;売上&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;商品情報&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CRM&lt;/td&gt;
      &lt;td&gt;顧客情報&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td rowspan=&quot;4&quot;&gt;販売&lt;/td&gt;
      &lt;td rowspan=&quot;3&quot;&gt;販売&lt;/td&gt;
      &lt;td&gt;受注&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;売上&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;商品情報（原価情報なし）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CRM&lt;/td&gt;
      &lt;td&gt;顧客情報（個人情報なし）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td rowspan=&quot;2&quot;&gt;販売_センシティブ&lt;/td&gt;
      &lt;td&gt;原価&lt;/td&gt;
      &lt;td&gt;商品情報（原価情報あり）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CRM&lt;/td&gt;
      &lt;td&gt;顧客情報（個人情報あり）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;ワークスペース設計&lt;/h3&gt;
&lt;p&gt;1年近く運用してみて、ワークスペースは以下のように分割するのがいいのかなという感覚を持っています。
まぁ、あくまでも私が所属している組織で都合が良いというだけで、あらゆる組織に適用できるかというと微妙ですが・・・&lt;/p&gt;
&lt;p&gt;構成としては、ユーザが触る用のワークスペースとデータエンジニアが作業を行うデータの取り込み・パイプライン実行用のワークスペースを最低限分離します。&lt;/p&gt;
&lt;p&gt;これはデータ取り込み用のリソース（ジョブ・パイプライン）と利用者が普段触るリソース（ダッシュボード・Genieスペース）を分離する意図があります。
もちろん、アクセス権を設定すれば触れなくはなりますが、ワークスペースへのアクセス権レベルで制御してしまえば細かいリソース毎のアクセス権は気にしなくてもよくなります。&lt;/p&gt;
&lt;p&gt;また、同時に複数のワークスペースからS3上のDeltaテーブルを変更するとデータが壊れるので、データを変更する操作はなるべく単一のワークスペースに寄せるようにしています。&lt;/p&gt;
&lt;p&gt;また、ある程度の大きさの組織毎にワークスペースを分離します。
ここでの組織とは、組織図に厳密に対応するものではなく、組織の属性に応じたものを考えています。
組織の属性に応じて、どのカタログをアタッチするかの管理を行い、まずはアタッチするカタログレベルでデータへのアクセス権を絞ります。&lt;/p&gt;
&lt;p&gt;もちろん、その先の細かいアクセス権の制御はUnity CatalogなりABACなりで制御を行います。&lt;/p&gt;
&lt;p&gt;同様にDatabricks Appsやダッシュボード・Genieスペースはワークスペースに紐づくため、生産部門が営業部門のアプリに触らないまたはその逆が起こらないという観点からもワークスペースを分離します。&lt;/p&gt;
&lt;p&gt;あとは、たまにやべー挙動をする利用者が出てくると思うので、その監査用に&lt;code&gt;system.audit&lt;/code&gt;スキーマのテーブルを使ってアクティビティ監査を行う専用のワークスペースを置いておきます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/05-05-how-to-deploy-databricks-on-aws/workspace-design.png&quot; alt=&quot;ワークスペース設計&quot;&gt;&lt;/p&gt;
&lt;h3&gt;カタログ専用バケットのデプロイ&lt;/h3&gt;
&lt;p&gt;私の組織ではワークスペースとデータを完全に分離したいという考え方からカタログの保存先としてワークスペースのバケットは使用せず、カタログ保存用のバケットを用意したうえでそこに保存しています。&lt;/p&gt;
&lt;p&gt;こうしておけばどのカタログがどのバケットに入っているかなどは気にしなくて良くなるので、組織のニーズに応じて柔軟にワークスペースの作成と削除を行えます。&lt;/p&gt;
&lt;p&gt;ただし、複数のワークスペースから同じテーブルに変更をかけるとデータが壊れるので、テーブルの更新を行うワークスペースには気を付けた方が良いです。&lt;/p&gt;
&lt;h3&gt;新機能の検証用にアメリカリージョンにもワークスペースが欲しいよね&lt;/h3&gt;
&lt;p&gt;Databricksは過去からの傾向として、新機能のPreviewはアメリカのリージョンから利用可能になることが多いです。&lt;/p&gt;
&lt;p&gt;私の組織ではアメリカの主要リージョンの中で日本からのレイテンシが比較的小さいus-west-2（オレゴン）に機能の評価用ワークスペースを構築しています。&lt;/p&gt;
&lt;h2&gt;その他参考リファレンス&lt;/h2&gt;
&lt;h3&gt;VPC&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/security/network/classic/customer-managed-vpc&quot;&gt;Configure a customer-managed VPC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Storage&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/connect/unity-catalog/cloud-storage/s3/s3-external-location-manual&quot;&gt;Create a storage credential and external location for S3 using Catalog Explorer or SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/delta/s3-limitations&quot;&gt;Delta Lake limitations on S3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Workspace&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/admin/workspace/create-uc-workspace&quot;&gt;Create a workspace with manual AWS configurations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>DatabricksのSpark Declarative Pipelinesを使って簡易的な家計簿を作りたい</title>
		<link href="https://www.jyuch.dev/posts/2026/04-24-databricks-sdp-expenses/"/>
		<updated>2026-04-29T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2026/04-24-databricks-sdp-expenses/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;Databricksには&lt;a href=&quot;https://docs.databricks.com/aws/ja/large-language-models/ai-functions&quot;&gt;AI Functions&lt;/a&gt;があり、ノートブックやSQL Editorなどで使用できます。&lt;/p&gt;
&lt;p&gt;いつからかSpark Declarative Pipelinesでも使えるようになっており、パイプラインだけでOCRと抽出が完結できるようになっていました。&lt;/p&gt;
&lt;p&gt;そこで、今回はボリュームに格納されたレシートから日時と金額を抽出してダッシュボードに表示するまでの処理をSpark Declarative Pipelinesで実装して、Declarative Automation Bundlesでデプロイできるようにしてみました。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jyuch/public-expenses&quot;&gt;jyuch/public-expenses&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;パイプラインの実装&lt;/h2&gt;
&lt;p&gt;以下の流れのパイプラインを実装します。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Volumeからレシートのイメージをロード&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ai_parse_document&lt;/code&gt;でOCR処理を実行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ai_query&lt;/code&gt;で抽出結果を構造化&lt;/li&gt;
&lt;li&gt;構造化データをフラット化してシルバーテーブルに変換&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;基本的に&lt;code&gt;ai_〇〇&lt;/code&gt;は実行にまぁまぁの時間が掛かるので、毎回全部のイメージの再処理をしていたら実行時間がとんでもないことになります。
そのため、ストリーミングテーブルを使用して差分実行を行うようにします。&lt;/p&gt;
&lt;p&gt;この辺も&lt;code&gt;CREATE STREAMING TABLE&lt;/code&gt;と&lt;code&gt;STREAM()&lt;/code&gt;でチェックポイントの管理をしなくても良きにしてくれるのでSDPは大好きです。&lt;/p&gt;
&lt;p&gt;まずはVolumeに格納されているレシートのイメージをロードします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE STREAMING TABLE raw_receipts AS
SELECT
  path,
  modificationTime,
  length,
  content
FROM
  STREAM(READ_FILES(&#39;/Volumes/expenses/default/receipt_images/&#39;, format =&amp;gt; &#39;binaryFile&#39;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そうしたら、&lt;code&gt;ai_parse_document&lt;/code&gt;でOCRを実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE STREAMING TABLE parsed_receipts AS
SELECT
  path,
  modificationTime,
  length,
  content,
  ai_parse_document(content, Map(&#39;version&#39;, &#39;2.0&#39;)) AS parsed_receipt
FROM
  STREAM(raw_receipts);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ai_query&lt;/code&gt;の&lt;code&gt;responseFormat&lt;/code&gt;にJSON Schemaを指定して構造化出力を行わせます。
本当は&lt;code&gt;STRUCT&amp;lt;&amp;gt;&lt;/code&gt;で構造体の定義を指定できるはずなのですが、「Spark Declarative Pipelinesのバグにエンカウントしたのでバグ報告ヨロ」みたいなエラーが表示されるので、あきらめてJSON Schemaを指定しています。&lt;/p&gt;
&lt;p&gt;個人的にはJSON Schemaは書式が冗長なので、できれば&lt;code&gt;STRUCT&amp;lt;&amp;gt;&lt;/code&gt;の方の構文を使いたいです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE STREAMING TABLE structured_receipts AS
SELECT
  path,
  ai_query(
    &#39;databricks-qwen3-next-80b-a3b-instruct&#39;,
    CONCAT(
      &#39;レシートをパースした結果が与えられます。そこから購入日と合計金額を抽出しなさい。&#39;,
      parsed_receipt
    ),
    responseFormat =&amp;gt;
      &#39;{
        &amp;quot;type&amp;quot;: &amp;quot;json_schema&amp;quot;,
        &amp;quot;json_schema&amp;quot;: {
          &amp;quot;name&amp;quot;: &amp;quot;structured_food_receipts&amp;quot;,
          &amp;quot;schema&amp;quot;: {
            &amp;quot;type&amp;quot;: &amp;quot;object&amp;quot;,
            &amp;quot;properties&amp;quot;: {
              &amp;quot;purchase_date&amp;quot;: {
                &amp;quot;type&amp;quot;: &amp;quot;string&amp;quot;,
                &amp;quot;format&amp;quot;: &amp;quot;date&amp;quot;
              },
              &amp;quot;total_amount&amp;quot;: {
                &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
              }
            },
            &amp;quot;required&amp;quot;: [
              &amp;quot;purchase_date&amp;quot;,
              &amp;quot;total_amount&amp;quot;
            ]
          },
          &amp;quot;strict&amp;quot;: true
        }
      }&#39;
  ) AS structured_receipt
FROM
  STREAM(parsed_receipts);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抽出したパスからレシートの種類（食費とか日用品とか）を付与しています。また、JSON形式の文字列から&lt;code&gt;STRUCT&amp;lt;&amp;gt;&lt;/code&gt;に変換します。&lt;/p&gt;
&lt;p&gt;購入日とかを抽出するタイミングでカテゴライズをしてもいいのですが、精度が出なかったのでひとまずファイルパスで識別させています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE STREAMING TABLE flatten_receipts AS
SELECT
  path,
  regexp_extract(path, &#39;/([^/]+)/[^/]+$&#39;, 1) AS kind,
  try_cast(
    try_parse_json(structured_receipt) AS STRUCT&amp;lt;purchase_date DATE, total_amount INT&amp;gt;
  ) AS receipt
FROM
  STREAM(structured_receipts);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最後にフラット化すればひとまずは家計簿テーブルが完成します。
名前とやっていることが微妙に一致してませんが、まぁ、その・・・。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE OR REPLACE MATERIALIZED VIEW receipts AS
SELECT
  path,
  kind,
  receipt.purchase_date,
  receipt.total_amount
FROM
  flatten_receipts;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DABでの展開&lt;/h2&gt;
&lt;p&gt;パイプラインを手動でデプロイしてもいいのですが、全部自動でできた方がうれしいですよね。&lt;/p&gt;
&lt;p&gt;ということでDABで一発デプロイできるようにします。&lt;/p&gt;
&lt;p&gt;特にひねりのない&lt;code&gt;databricks.yml&lt;/code&gt;と&lt;code&gt;resources/expenses.pipeline.yml&lt;/code&gt;を定義します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;bundle:
  name: expenses
  uuid: 00000000-0000-0000-0000-000000000000

include:
  - resources/*.yml

variables:
  catalog:
    description: The catalog to use
  schema:
    description: The schema to use
  ingest_from:
    description: The path log ingestion

targets:
  dev:
    mode: development
    default: true
    variables:
      catalog: expenses
      schema: dev
  prod:
    mode: production
    workspace:
      root_path: /Workspace/Bundles/${bundle.name}/${bundle.target}
    variables:
      catalog: expenses
      schema: prod
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;resources:
  pipelines:
    expenses:
      name: expenses
      catalog: ${var.catalog}
      schema: ${var.schema}
      serverless: true
      root_path: &amp;quot;../src/expenses&amp;quot;

      libraries:
        - glob:
            include: ../src/expenses/transformations/**

      environment:
        dependencies:
          - --editable ${workspace.file_path}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;あとは🚀ボタンで一発デプロイです。&lt;/p&gt;
&lt;h2&gt;ダッシュボードの作成&lt;/h2&gt;
&lt;p&gt;あとはお好みの種類のグラフでダッシュボードを作るだけです。&lt;/p&gt;
&lt;p&gt;ダッシュボードもDABでデプロイ出来ます。
お仕事ではダッシュボードもパイプラインと同じようにDABでデプロイしているのですが、小回りが利かなくなるので趣味ならこの辺はお好みでいいと思います。&lt;/p&gt;
&lt;h2&gt;おわりに&lt;/h2&gt;
&lt;p&gt;SQLだけでレシートのOCR ⇒ 情報の抽出が完結しました。
また、差分更新も勝手にやってくれます。&lt;/p&gt;
&lt;p&gt;宣言的に実装からデプロイまでできるので管理しやすいです。皆様のお仕事のお供のぜひぜひ&lt;/p&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>Databricks Zerobus Ingestでサーバレスでリアルタイムにデータを流し込みたかった</title>
		<link href="https://www.jyuch.dev/posts/2026/04-04-databricks-zerobus/"/>
		<updated>2026-04-04T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2026/04-04-databricks-zerobus/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;ついにDatabricksのZerobus IngestがGAとなりました。&lt;/p&gt;
&lt;p&gt;Preview版ではデフォルトストレージは対応していないとありましたが、GAになったタイミングでその表示が消えたので、Databricks Free Editionで試してみることにしました。&lt;/p&gt;
&lt;h2&gt;テーブルの作成&lt;/h2&gt;
&lt;p&gt;Databricks上のテーブルからProtobufの定義を生成出来るようなので、まずはテーブルを作成します。&lt;/p&gt;
&lt;p&gt;ここではひとまず私が必要そうなデータ型に絞ってテーブルを作成します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE OR REPLACE TABLE workspace.default.zerobus_sample (
    id INT,
    name STRING,
    create_at TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Protobufの定義生成&lt;/h2&gt;
&lt;p&gt;テーブルを作成したら、テーブル定義からProtbufの定義を生成します。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/databricks/zerobus-sdk&quot;&gt;Zerobus Ingest SDK&lt;/a&gt;に生成するためのツール（&lt;a href=&quot;https://github.com/databricks/zerobus-sdk/tree/main/rust/tools/generate_files&quot;&gt;generate_files&lt;/a&gt;）があるので、それを使って生成します。&lt;/p&gt;
&lt;p&gt;ツールはサービスプリンシパル経由で触りに行くタイプなので、サービスプリンシパルを作成してシークレットを生成しておきます。&lt;/p&gt;
&lt;p&gt;また、テーブル定義も触りに行く必要があるので、サービスプリンシパルに権限を振るのを忘れないようにして下さい。（1敗）&lt;/p&gt;
&lt;p&gt;ツールはRustを使って実装されているので、Rustの処理系をあらかじめインストールしておきます。
あとは以下のコマンドで生成できます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;cargo run -- &#92;
  --uc-endpoint &amp;quot;&amp;lt;your_uc_endpoint&amp;gt;&amp;quot; &#92;
  --client-id &amp;quot;your-client-id&amp;quot; &#92;
  --client-secret &amp;quot;your-client-secret&amp;quot; &#92;
  --table &amp;quot;workspace.default.zerobus_sample&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;クライアントの実装&lt;/h2&gt;
&lt;p&gt;今回はRustで書いていきます。&lt;/p&gt;
&lt;p&gt;必要なライブラリは以下の通りなのですが、&lt;a href=&quot;https://crates.io/crates/databricks-zerobus-ingest-sdk&quot;&gt;databricks-zerobus-ingest-sdk&lt;/a&gt;のprost系は&lt;code&gt;0.13&lt;/code&gt;に依存しているので、prostだけは&lt;code&gt;0.13&lt;/code&gt;を明示的に指定してインストールする必要があります。（1敗）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[dependencies]
# 必須
databricks-zerobus-ingest-sdk = &amp;quot;1.0.1&amp;quot;
prost = &amp;quot;0.13.5&amp;quot;
prost-types = &amp;quot;0.13.5&amp;quot;
tokio = { version = &amp;quot;1&amp;quot;, features = [&amp;quot;full&amp;quot;] }
# お好みで
anyhow = &amp;quot;1&amp;quot;
chrono = &amp;quot;0.4&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;あとは&lt;a href=&quot;https://github.com/databricks/zerobus-sdk/blob/main/rust/examples/proto/single/src/main.rs&quot;&gt;公式のサンプル&lt;/a&gt;をベースに簡単な書き込み処理を実装していきます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use crate::samples::TableZerobusSample;
use databricks_zerobus_ingest_sdk::{
    ProtoMessage, StreamConfigurationOptions, TableProperties, ZerobusSdk, ZerobusStream,
};
use prost::Message;
use prost_types::{DescriptorProto, FileDescriptorSet};
use std::fs;

pub mod samples {
    include!(&amp;quot;../output/zerobus_sample.rs&amp;quot;);
}

const SERVER_ENDPOINT: &amp;amp;str = &amp;quot;&amp;lt;workspace id&amp;gt;.zerobus.&amp;lt;region&amp;gt;.cloud.databricks.com&amp;quot;;
const DATABRICKS_WORKSPACE_URL: &amp;amp;str = &amp;quot;https://dbc-12345678-90ab.cloud.databricks.com&amp;quot;;
const DATABRICKS_CLIENT_ID: &amp;amp;str = &amp;quot;00000000-0000-0000-0000-000000000000&amp;quot;;
const DATABRICKS_CLIENT_SECRET: &amp;amp;str = &amp;quot;dose12345678901234567890123456789012&amp;quot;;

#[tokio::main]
async fn main() -&amp;gt; anyhow::Result&amp;lt;()&amp;gt; {
    let descriptor_proto = load_descriptor(
        &amp;quot;output/zerobus_sample.descriptor&amp;quot;,
        &amp;quot;zerobus_sample.proto&amp;quot;,
        &amp;quot;table_zerobus_sample&amp;quot;,
    );

    let table_properties = TableProperties {
        table_name: &amp;quot;workspace.default.zerobus_sample&amp;quot;.to_string(),
        descriptor_proto: Some(descriptor_proto),
    };

    let stream_configuration_options = StreamConfigurationOptions {
        max_inflight_requests: 100,
        // RecordType::Proto is the default.
        ..Default::default()
    };

    let sdk_handle = ZerobusSdk::builder()
        .endpoint(SERVER_ENDPOINT)
        .unity_catalog_url(DATABRICKS_WORKSPACE_URL)
        .build()?;

    let mut stream = sdk_handle
        .create_stream(
            table_properties.clone(),
            DATABRICKS_CLIENT_ID.to_string(),
            DATABRICKS_CLIENT_SECRET.to_string(),
            Some(stream_configuration_options),
        )
        .await?;

    ingest_with_offset_api(&amp;amp;mut stream).await?;

    stream.close().await?;
    println!(&amp;quot;Stream closed successfully&amp;quot;);
    Ok(())
}

/// Recommended API: returns offset directly after queuing.
async fn ingest_with_offset_api(stream: &amp;amp;mut ZerobusStream) -&amp;gt; anyhow::Result&amp;lt;()&amp;gt; {
    println!(&amp;quot;=== Offset-based API (Recommended) ===&amp;quot;);

    let now = chrono::Utc::now().timestamp();

    // 1. Auto-encoding: ProtoMessage - pass message directly, SDK handles encoding.
    let order = TableZerobusSample {
        id: Some(1),
        name: Some(&amp;quot;test&amp;quot;.to_string()),
        create_at: Some(now),
    };

    let offset_id = stream.ingest_record_offset(ProtoMessage(order)).await?;
    println!(&amp;quot;[Auto-encoding] Record sent with offset ID: {}&amp;quot;, offset_id);
    stream.wait_for_offset(offset_id).await?;
    println!(
        &amp;quot;[Auto-encoding] Record acknowledged with offset ID: {}&amp;quot;,
        offset_id
    );

    Ok(())
}

// Load descriptor from generated files
fn load_descriptor(path: &amp;amp;str, file: &amp;amp;str, msg: &amp;amp;str) -&amp;gt; DescriptorProto {
    let bytes = fs::read(path).expect(&amp;quot;Failed to read descriptor&amp;quot;);
    let file_set = FileDescriptorSet::decode(bytes.as_ref()).unwrap();

    let file_desc = file_set
        .file
        .into_iter()
        .find(|f| f.name.as_deref() == Some(file))
        .unwrap();

    file_desc
        .message_type
        .into_iter()
        .find(|m| m.name.as_deref() == Some(msg))
        .unwrap()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;実装できたら早速実行してみましょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Unsupported table kind. Tables created in default storage are not supported. Error Code: 4024, Error State: 0.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;無慈悲&lt;/p&gt;
&lt;h2&gt;おわりに&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/ingestion/zerobus-limits#workspace-and-target-table&quot;&gt;Zerobus Ingest connector limitations - Workspace and Target table&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;書いてある場所が移動しただけで、使えないんすね。&lt;/p&gt;
&lt;p&gt;Free EditionでもS3を外部ロケーションとして登録してカタログをそっちに作成すれば使えるとは思いますが・・・&lt;/p&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>Hugging FaceのSentence TransformersモデルをUnity Catalogに登録してServing endpointsにデプロイしたい</title>
		<link href="https://www.jyuch.dev/posts/2026/02-26-register-hf-mode-to-uc/"/>
		<updated>2026-02-25T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2026/02-26-register-hf-mode-to-uc/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;大体タイトル通りです。&lt;/p&gt;
&lt;p&gt;Databricksに最初から登録されている埋め込みモデルは英語のみで日本語に対応していないので、Serving endpointにデプロイするためにHugging Faceから好きなモデルを引っ張ってきてUnity Catalogに登録する方法を確認してみました。&lt;/p&gt;
&lt;p&gt;ちなみにモデルを登録するだけなら以下のノートブックを使えば一発で登録できると思います。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jyuch.dev/img/2026/02-26-register-hf-mode-to-uc/register-hf-model-to-uc.html&quot;&gt;🤗HFのモデルをUCに登録する君&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;🤗HFのモデルをUCに登録する君&lt;/h2&gt;
&lt;h3&gt;環境のセットアップ&lt;/h3&gt;
&lt;p&gt;ノートブックにServerlessコンピュートをアタッチして、以下のライブラリをインストールします。&lt;/p&gt;
&lt;p&gt;MLflowのSentence Transformersを使用したいため、&lt;code&gt;mlflow[sentence-transformers]&lt;/code&gt;を指定してインストールします。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mlflow[sentence-transformers]==3.9.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sentence-transformers==5.2.3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;今回はConfigurationのDependenciesから追加しましたが、たぶん&lt;code&gt;%pip&lt;/code&gt;コマンドを使ってインストールして&lt;code&gt;dbutils.library.restartPython()&lt;/code&gt;しても良いと思います。&lt;/p&gt;
&lt;h3&gt;モデル名の入力とかスキーマの作成とか&lt;/h3&gt;
&lt;p&gt;このノートブックでは登録先のカタログ名とHugging Faceのモデル名をWidgetから受け付けます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;dbutils.widgets.text(&amp;quot;uc_catalog_name&amp;quot;, &amp;quot;&amp;quot;, &amp;quot;UC Catalog name&amp;quot;)
dbutils.widgets.text(&amp;quot;hf_model_name&amp;quot;, &amp;quot;&amp;quot;, &amp;quot;Hugging Face Model name&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登録先のスキーマ名とモデル名は入力されたHugging Faceモデルから組み立てています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;UC_CATALOG_NAME = dbutils.widgets.get(&amp;quot;uc_catalog_name&amp;quot;)
HF_MODEL_NAME = dbutils.widgets.get(&amp;quot;hf_model_name&amp;quot;)
UC_SCHEMA_NAME = HF_MODEL_NAME.split(&amp;quot;/&amp;quot;)[0].replace(&amp;quot;-&amp;quot;, &amp;quot;_&amp;quot;).replace(&amp;quot;.&amp;quot;, &amp;quot;_&amp;quot;)
UC_MODEL_NAME = HF_MODEL_NAME.split(&amp;quot;/&amp;quot;)[1].replace(&amp;quot;-&amp;quot;, &amp;quot;_&amp;quot;).replace(&amp;quot;.&amp;quot;, &amp;quot;_&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そうしたら登録先のスキーマを作成しておきます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;spark.sql(f&amp;quot;CREATE SCHEMA IF NOT EXISTS {UC_CATALOG_NAME}.{UC_SCHEMA_NAME}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MLflowへのモデルのロギング&lt;/h3&gt;
&lt;p&gt;Sentence Transformers経由でHugging Faceのモデルを取得し、&lt;a href=&quot;https://mlflow.org/docs/latest/ml/deep-learning/sentence-transformers/&quot;&gt;MLflowのSentence Transformersフレーバー&lt;/a&gt;を使用してモデルをロギングします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import mlflow
from sentence_transformers import SentenceTransformer
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;mlflow.set_registry_uri(&amp;quot;databricks-uc&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model = SentenceTransformer(HF_MODEL_NAME)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;input_example = [
    &amp;quot;RFC 1034とRFC 1035は、Domain Name System（DNS）の基礎を定義する最も重要な仕様である。&amp;quot;,
    &amp;quot;Paul Mockapetrisによって1987年に策定され、インターネットにおける名前解決の仕組み全体を規定している。&amp;quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with mlflow.start_run():
    model_info = mlflow.sentence_transformers.log_model(
        model=model,
        name=UC_MODEL_NAME,
        input_example=input_example,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ロギングされたモデルにはモデルの実行に必要なライブラリが含まれるのですが、たまに必要な依存関係が足りずにServing endpointsへのデプロイに失敗することがあるので、事前に一度モデルが動作することを確認します。&lt;/p&gt;
&lt;p&gt;大体10分位掛かるので、気長に待ちましょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;mlflow.models.predict(
    model_uri=model_info.model_uri,
    input_data=input_example,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Unity Catalogへの登録&lt;/h3&gt;
&lt;p&gt;モデルが正常に動作することが確認出来たらUnity Catalogへ登録して完了です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;mlflow.register_model(
    model_info.model_uri, f&amp;quot;{UC_CATALOG_NAME}.{UC_SCHEMA_NAME}.{UC_MODEL_NAME}&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>Databricksのread_files関数でもExcelファイルを読み込みたい</title>
		<link href="https://www.jyuch.dev/posts/2026/01-17-excel-read-files/"/>
		<updated>2026-01-17T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2026/01-17-excel-read-files/</id>
		<content type="html">&lt;h2&gt;2月8日追記&lt;/h2&gt;
&lt;p&gt;現時点ではフリガナ問題は解決しています。&lt;/p&gt;
&lt;p&gt;また、より大きいファイルでも取り込めるようになっていました。
ですが、調子に乗って大きなサイズを放り込むと処理が終わらないので、その辺はいい感じにする必要があります。&lt;/p&gt;
&lt;h2&gt;注意事項&lt;/h2&gt;
&lt;p&gt;本検証は2026年1月17時点の内容で検証しています。
リリース版は挙動が異なる場合があります。&lt;/p&gt;
&lt;p&gt;また、Databricks Free Editionで評価しています。
そのため、商用版と挙動が異なる場合があります。&lt;/p&gt;
&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;Databricksの&lt;code&gt;read_files&lt;/code&gt;関数はCSVやJSONフォーマットをいい感じに読み込んでくれる関数です。
その&lt;code&gt;read_files&lt;/code&gt;関数がExcelファイルの読み込みをサポートしました。&lt;/p&gt;
&lt;p&gt;ですが、Excelフォーマットをプログラムで扱う際のトラップに悩まされてきた生産性アプリケーション開発者も多いのではないでしょうか。&lt;/p&gt;
&lt;p&gt;そこで、今回はトラップになりそうな部分の評価を行ってみました。&lt;/p&gt;
&lt;h2&gt;Office Open XMLフォーマット&lt;/h2&gt;
&lt;p&gt;今回はExcelフォーマットのうち、Office Open XMLフォーマット（&lt;code&gt;xlsx&lt;/code&gt;）を評価してみます。
そのOOXMLフォーマットのうち、今回の検証で触れる内容について軽く解説をしていきます。&lt;/p&gt;
&lt;p&gt;OOXMLはXMLの名前がついている通り、主にzipで圧縮されたXMLファイルで構成されています。&lt;/p&gt;
&lt;p&gt;たとえば、以下のようなExcelファイルをzip展開すると、以下のような構造となっています。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/example.xlsx.png&quot; alt=&quot;example.xlsxの見た目&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;example.xlsx
│
│  [Content_Types].xml
│
├─docProps
│      app.xml
│      core.xml
│
├─xl
│  │  sharedStrings.xml
│  │  styles.xml
│  │  workbook.xml
│  │
│  ├─printerSettings
│  │      printerSettings1.bin
│  │
│  ├─theme
│  │      theme1.xml
│  │
│  ├─worksheets
│  │  │  sheet1.xml
│  │  │
│  │  └─_rels
│  │          sheet1.xml.rels
│  │
│  └─_rels
│          workbook.xml.rels
│
└─_rels
        .rels
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そのうち、内容を確認する際に見るのが以下の二つのファイルになります。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;xl&#92;worksheets&#92;sheet1.xml&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;xl&#92;worksheets&lt;/code&gt;以下にはシート毎にシートの内容が含まれているXMLファイルが格納されています。&lt;/p&gt;
&lt;p&gt;上記の例だと、こんな感じに内容になるはずです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;yes&amp;quot;?&amp;gt;
&amp;lt;worksheet xmlns=&amp;quot;http://schemas.openxmlformats.org/spreadsheetml/2006/main&amp;quot;
    xmlns:r=&amp;quot;http://schemas.openxmlformats.org/officeDocument/2006/relationships&amp;quot;
    xmlns:mc=&amp;quot;http://schemas.openxmlformats.org/markup-compatibility/2006&amp;quot;
    mc:Ignorable=&amp;quot;x14ac xr xr2 xr3&amp;quot;
    xmlns:x14ac=&amp;quot;http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac&amp;quot;
    xmlns:xr=&amp;quot;http://schemas.microsoft.com/office/spreadsheetml/2014/revision&amp;quot;
    xmlns:xr2=&amp;quot;http://schemas.microsoft.com/office/spreadsheetml/2015/revision2&amp;quot;
    xmlns:xr3=&amp;quot;http://schemas.microsoft.com/office/spreadsheetml/2016/revision3&amp;quot;
    xr:uid=&amp;quot;{00000000-0001-0000-0000-000000000000}&amp;quot;&amp;gt;
    &amp;lt;dimension ref=&amp;quot;A1:B2&amp;quot; /&amp;gt;
    &amp;lt;sheetViews&amp;gt;
        &amp;lt;sheetView tabSelected=&amp;quot;1&amp;quot; workbookViewId=&amp;quot;0&amp;quot; /&amp;gt;
    &amp;lt;/sheetViews&amp;gt;
    &amp;lt;sheetFormatPr defaultRowHeight=&amp;quot;18.75&amp;quot; /&amp;gt;
    &amp;lt;sheetData&amp;gt;
        &amp;lt;row r=&amp;quot;1&amp;quot; spans=&amp;quot;1:2&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;A1&amp;quot; t=&amp;quot;s&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;0&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
        &amp;lt;row r=&amp;quot;2&amp;quot; spans=&amp;quot;1:2&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;B2&amp;quot; t=&amp;quot;s&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;1&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
    &amp;lt;/sheetData&amp;gt;
    &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;pageMargins left=&amp;quot;0.7&amp;quot; right=&amp;quot;0.7&amp;quot; top=&amp;quot;0.75&amp;quot; bottom=&amp;quot;0.75&amp;quot; header=&amp;quot;0.3&amp;quot; footer=&amp;quot;0.3&amp;quot; /&amp;gt;
    &amp;lt;pageSetup paperSize=&amp;quot;9&amp;quot; orientation=&amp;quot;portrait&amp;quot; r:id=&amp;quot;rId1&amp;quot; /&amp;gt;
&amp;lt;/worksheet&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;A1&lt;/code&gt;と&lt;code&gt;B2&lt;/code&gt;セルにデータが含まれているのが分かりますね。
ですが、謎の数字&lt;code&gt;0&lt;/code&gt;と&lt;code&gt;1&lt;/code&gt;があるだけで、内容は特には記述されていません。&lt;/p&gt;
&lt;p&gt;テキスト情報はどこに行ったのでしょう？&lt;/p&gt;
&lt;p&gt;その答えはShared String Table（SST、&lt;code&gt;sharedStrings.xml&lt;/code&gt;）の中にあります。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;xl&#92;sharedStrings.xml&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;SSTはその名の通り、複数の場所から参照されるテキスト情報を格納するためテーブルです。&lt;/p&gt;
&lt;p&gt;上記の例だとシートはSSTのインデックス番号を参照していたことになります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;yes&amp;quot;?&amp;gt;
&amp;lt;sst xmlns=&amp;quot;http://schemas.openxmlformats.org/spreadsheetml/2006/main&amp;quot; count=&amp;quot;2&amp;quot; uniqueCount=&amp;quot;2&amp;quot;&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;Hello Excel&amp;lt;/t&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;こんにちはExcel&amp;lt;/t&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
&amp;lt;/sst&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;なお、文字情報は必ずSSTに格納しなければならない訳ではなく、Excelを出力するライブラリはシートのXMLに直接文字を格納する場合が多いです。&lt;/p&gt;
&lt;h2&gt;Excelファイルの読み込み&lt;/h2&gt;
&lt;p&gt;というわけで、Excelの深淵を覗く準備ができたところで&lt;code&gt;read_files&lt;/code&gt;関数でファイルを読み込んでみましょう。&lt;/p&gt;
&lt;h3&gt;単純な例&lt;/h3&gt;
&lt;p&gt;以下のような単純なExcelファイルを用意します。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/01_simple_files.xlsx.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;最低限のオプションだけ設定すると以下のように出力されます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/01_simple_files.xlsx&amp;quot;,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_01.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;スキーマを推論してくれるので、日付や数字もそれぞれの型として認識してくれています。&lt;/p&gt;
&lt;p&gt;ですが、小数が少し怪しいですね。
内部的にはこの形で格納されているので、&lt;code&gt;read_files&lt;/code&gt;関数が悪いわけではなさそうです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;yes&amp;quot;?&amp;gt;
&amp;lt;worksheet&amp;gt;
    &amp;lt;sheetData&amp;gt;
        &amp;lt;row r=&amp;quot;1&amp;quot; spans=&amp;quot;1:4&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;C1&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;1.1000000000000001&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
        &amp;lt;row r=&amp;quot;2&amp;quot; spans=&amp;quot;1:4&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;C2&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;2.2000000000000002&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
        &amp;lt;row r=&amp;quot;3&amp;quot; spans=&amp;quot;1:4&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;C3&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;3.3&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
        &amp;lt;row r=&amp;quot;4&amp;quot; spans=&amp;quot;1:4&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;C4&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;4.4000000000000004&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
        &amp;lt;row r=&amp;quot;5&amp;quot; spans=&amp;quot;1:4&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;C5&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;5.5&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
    &amp;lt;/sheetData&amp;gt;
&amp;lt;/worksheet&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;型が既知なのであれば、あらかじめスキーマヒントを与えておいたほうが良いかもしれません。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/01_simple_files.xlsx&amp;quot;,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;,
    schemaHints =&amp;gt; &amp;quot;_c2 decimal(5,1)&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_02.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;ちなみに、&lt;code&gt;inferColumnTypes&lt;/code&gt;はExcel読み込みでは考慮されないようです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/01_simple_files.xlsx&amp;quot;,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;,
    inferColumnTypes =&amp;gt; false
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_03.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;スキーマヒントを与えて強制的に文字列とすると、to_stringをしたような形式になります。&lt;/p&gt;
&lt;p&gt;また、日付は&lt;code&gt;MM/DD/YY&lt;/code&gt;形式になります。USロケールなのかもしれません。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/01_simple_files.xlsx&amp;quot;,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;,
    schemaHints =&amp;gt; &amp;quot;_c1 string, _c2 string, _c3 string&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_04.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;日本語を含む例&lt;/h3&gt;
&lt;p&gt;次はよくオフィスで流通していそうなファイルを読み込んでみます。&lt;/p&gt;
&lt;p&gt;今回の検証で初めて知ったのですが、Excelは変換時の元テキストをフリガナとして格納してくれるっぽいです。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/02_%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%82%92%E5%90%AB%E3%82%80%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.xlsx.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;関係ないセルを読まないよう、&lt;code&gt;dataAddress&lt;/code&gt;で読み込む場所を明示してあげます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/02_日本語を含むファイル.xlsx&amp;quot;,
    dataAddress =&amp;gt; &amp;quot;&#39;お供　給与表&#39;!B2:C5&amp;quot;,
    headerRows =&amp;gt; 1,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_05.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;良くないですね。&lt;/strong&gt;
フリガナも拾ってきてしまっています。&lt;/p&gt;
&lt;p&gt;少なくとも日本語だと意図してフリガナを拾ってほしいケースはあまりないと思うので、GAまでにフリガナを拾わないか、オプションで選択出来るようになるといいなと思います。&lt;/p&gt;
&lt;p&gt;ちなみに、この例だとSSTはこのようになっています。
シート名として入力したテキストもフォネティックを拾っているとは思いませんでした。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;yes&amp;quot;?&amp;gt;
&amp;lt;sst xmlns=&amp;quot;http://schemas.openxmlformats.org/spreadsheetml/2006/main&amp;quot; count=&amp;quot;8&amp;quot; uniqueCount=&amp;quot;8&amp;quot;&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;お供&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;1&amp;quot; eb=&amp;quot;2&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;トモ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;きび団子支給数&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;2&amp;quot; eb=&amp;quot;4&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ダンゴ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;4&amp;quot; eb=&amp;quot;7&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;シキュウスウ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;猿&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;0&amp;quot; eb=&amp;quot;1&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;サル&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;犬&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;0&amp;quot; eb=&amp;quot;1&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;イヌ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;雉&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;0&amp;quot; eb=&amp;quot;1&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;キジ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;令和8年1月17日改定&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;0&amp;quot; eb=&amp;quot;2&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;レイワ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;3&amp;quot; eb=&amp;quot;4&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ネン&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;5&amp;quot; eb=&amp;quot;6&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ガツ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;8&amp;quot; eb=&amp;quot;9&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ニチ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;9&amp;quot; eb=&amp;quot;11&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;カイテイ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;※変更時はおじいさんとおばあさんの承認を得ること&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;1&amp;quot; eb=&amp;quot;4&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ヘンコウジ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;17&amp;quot; eb=&amp;quot;19&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ショウニン&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;20&amp;quot; eb=&amp;quot;21&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;エ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;全然関係ないデータ&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;0&amp;quot; eb=&amp;quot;4&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ゼンゼンカンケイ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
&amp;lt;/sst&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;手動でフリガナを消してあげると意図した感じに取り込んでくれます。&lt;/p&gt;
&lt;p&gt;ただし、手動で消すのは結構めんどくさいので、前処理でフリガナを消すプログラムを動かすとかでしょうか・・・&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/03_日本語を含むファイル_フリガナ除去.xlsx&amp;quot;,
    dataAddress =&amp;gt; &amp;quot;&#39;お供　給与表&#39;!B2:C5&amp;quot;,
    headerRows =&amp;gt; 1,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_06.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;yes&amp;quot;?&amp;gt;
&amp;lt;sst xmlns=&amp;quot;http://schemas.openxmlformats.org/spreadsheetml/2006/main&amp;quot; count=&amp;quot;8&amp;quot; uniqueCount=&amp;quot;8&amp;quot;&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;令和8年1月17日改定&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;0&amp;quot; eb=&amp;quot;2&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;レイワ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;3&amp;quot; eb=&amp;quot;4&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ネン&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;5&amp;quot; eb=&amp;quot;6&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ガツ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;8&amp;quot; eb=&amp;quot;9&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ニチ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;9&amp;quot; eb=&amp;quot;11&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;カイテイ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;※変更時はおじいさんとおばあさんの承認を得ること&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;1&amp;quot; eb=&amp;quot;4&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ヘンコウジ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;17&amp;quot; eb=&amp;quot;19&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ショウニン&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;rPh sb=&amp;quot;20&amp;quot; eb=&amp;quot;21&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;エ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;全然関係ないデータ&amp;lt;/t&amp;gt;
        &amp;lt;rPh sb=&amp;quot;0&amp;quot; eb=&amp;quot;4&amp;quot;&amp;gt;
            &amp;lt;t&amp;gt;ゼンゼンカンケイ&amp;lt;/t&amp;gt;
        &amp;lt;/rPh&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;お供&amp;lt;/t&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;きび団子支給数&amp;lt;/t&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;猿&amp;lt;/t&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;犬&amp;lt;/t&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
    &amp;lt;si&amp;gt;
        &amp;lt;t&amp;gt;雉&amp;lt;/t&amp;gt;
        &amp;lt;phoneticPr fontId=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/si&amp;gt;
&amp;lt;/sst&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;計算式を含む例&lt;/h3&gt;
&lt;p&gt;次は式を含むExcelファイルを読み込ませてみます。&lt;/p&gt;
&lt;p&gt;ドキュメントページでは&lt;a href=&quot;https://docs.databricks.com/aws/ja/query/formats/excel&quot;&gt;評価された数式を取り込みます&lt;/a&gt;とありますが、どのような意味なのでしょうか。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/04_formula.xlsx.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/04_formula.xlsx&amp;quot;,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_07.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;エクセルで数式セルを作成すると、内部的には数式と共に計算済みの値が格納されています。
ドキュメントの「評価された数式を取り込みます」とはおそらく内部的に保持されている計算済みの値を取り込むということを言っているのだと思われます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;yes&amp;quot;?&amp;gt;
&amp;lt;worksheet&amp;gt;
    &amp;lt;sheetData&amp;gt;
        &amp;lt;row r=&amp;quot;1&amp;quot; spans=&amp;quot;1:3&amp;quot;&amp;gt;
            &amp;lt;c r=&amp;quot;A1&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;1&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
            &amp;lt;c r=&amp;quot;B1&amp;quot;&amp;gt;
                &amp;lt;v&amp;gt;2&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
            &amp;lt;c r=&amp;quot;C1&amp;quot;&amp;gt;
                &amp;lt;f&amp;gt;A1+B1&amp;lt;/f&amp;gt;
                &amp;lt;v&amp;gt;3&amp;lt;/v&amp;gt;
            &amp;lt;/c&amp;gt;
        &amp;lt;/row&amp;gt;
    &amp;lt;/sheetData&amp;gt;
&amp;lt;/worksheet&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;では、計算済みの値が格納されないケースがあるのでしょうか。&lt;/p&gt;
&lt;p&gt;全部のケースを確認した訳ではありませんが、ライブラリを使用してExcelファイルをプログラムから作成する場合は数式のみが格納されて計算済みの値が格納されない場合が多いです。&lt;/p&gt;
&lt;p&gt;たとえば、以下のようなコードを使用して上記のExcelと同じようなファイルを作成したとします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import openpyxl


def main():
    wb = openpyxl.Workbook()
    sheet = wb.active

    for it in range(1, 5):
        sheet.cell(it, 1).value = it
        sheet.cell(it, 2).value = it + 1
        sheet.cell(it, 3).value = f&amp;quot;=A{it}+B{it}&amp;quot;

    wb.save(&amp;quot;05_formula_no_value.xlsx&amp;quot;)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Excelで開くと未計算のセルは計算されて表示されますが、&lt;code&gt;read_files&lt;/code&gt;関数では計算済みの値が無いため、正しく取得できていません。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/05_formula_no_value.xlsx.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/05_formula_no_value.xlsx&amp;quot;,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_08.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;複数のファイル&lt;/h3&gt;
&lt;p&gt;問題ないとは思いますが、念のため複数のファイルの読み込みを試してみます。&lt;/p&gt;
&lt;p&gt;以下のようなコードで100行×100ファイルのExcelを生成して読み込んでみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random
import string

import openpyxl


def randomname(n):
    return &amp;quot;&amp;quot;.join(random.choices(string.ascii_letters + string.digits, k=n))


def main():
    for i in range(1, 101):
        print(f&amp;quot;06_excel_files&#92;&#92;book_{i:0&amp;gt;3}.xlsx&amp;quot;)
        wb = openpyxl.Workbook()
        sheet = wb.active

        for r in range(1, 101):
            sheet.cell(r, 1).value = i
            sheet.cell(r, 2).value = r

            for c in range(3, 6):
                sheet.cell(r, c).value = randomname(10)

            for c in range(6, 9):
                sheet.cell(r, c).value = random.randint(0, 100)

        wb.save(f&amp;quot;06_excel_files&#92;&#92;book_{i:0&amp;gt;3}.xlsx&amp;quot;)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  *
FROM
  read_files(
    &amp;quot;/Volumes/excel_read_files/default/files/06_excel_files/&amp;quot;,
    format =&amp;gt; &amp;quot;excel&amp;quot;,
    schemaEvolutionMode =&amp;gt; &amp;quot;none&amp;quot;
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/output_09.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;良さそうですね。&lt;/p&gt;
&lt;h3&gt;サイズの大きいファイル&lt;/h3&gt;
&lt;p&gt;JavaでExcelを読み書きする代表的なライブラリと言えば&lt;a href=&quot;https://poi.apache.org/&quot;&gt;Apache POI&lt;/a&gt;があります。&lt;/p&gt;
&lt;p&gt;POIはUser API（Excelファイルをインメモリで展開するAPI）で巨大なファイルを扱おうとするとOOMでプログラムが爆散するという悲しき事故が発生しがちです。&lt;/p&gt;
&lt;p&gt;SparkはScala、すなわちJVMで動作するので同じ事故が発生するか否かは割と気になりますよね。&lt;/p&gt;
&lt;p&gt;以下のようなコードでそれぞれの行数を持つExcelファイルを作って、順番に読み込ませてみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random
import string

import openpyxl

ROWS = [
    10000,
    10100,
    10101,
    10102,
    10103,
    10104,
    10105,
    10106,
    10107,
    10108,
    10109,
    10110,
    10120,
    10130,
    10140,
    10150,
    10160,
    10170,
    10180,
    10190,
    10200,
    10300,
    10400,
    10500,
    11000,
]


def randomname(n):
    return &amp;quot;&amp;quot;.join(random.choices(string.ascii_letters + string.digits, k=n))


def main():
    for row_count in ROWS:
        print(f&amp;quot;07_large_excel_files&#92;&#92;book_{row_count}.xlsx&amp;quot;)
        wb = openpyxl.Workbook()
        sheet = wb.active

        for r in range(1, row_count + 1):
            sheet.cell(r, 1).value = row_count
            sheet.cell(r, 2).value = r

            for c in range(3, 6):
                sheet.cell(r, c).value = randomname(10)

            for c in range(6, 9):
                sheet.cell(r, c).value = random.randint(0, 100)

        wb.save(f&amp;quot;07_large_excel_files&#92;&#92;book_{row_count}.xlsx&amp;quot;)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;詳細は省きますが、私の環境では10102行のファイルまでなら読み込んでくれましたが、10103行のファイルからは関数が返ってこなくなりました。&lt;/p&gt;
&lt;p&gt;関数の内部的な制限に引っかかったのか、それともDatabricks Free Editionのなんらかのレートリミットに引っかかったのかは分かりませんが、あまり大量の行数を有するファイルの読み込みには向かないようです。&lt;/p&gt;
&lt;h2&gt;おわりに&lt;/h2&gt;
&lt;p&gt;いくつか気になる点はありましたが、ベータ版の機能なのでGAまでには修正されると思います。&lt;/p&gt;
&lt;p&gt;Pythonでロジックを書かなくてもいい感じにExcelファイルが読み込めると、ファイルサーバ内に眠っているExcelファイルのデータの利活用のハードルも低くなると思います。&lt;/p&gt;
&lt;p&gt;GAが待ち遠しい機能ですね。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jyuch/excel-read-files&quot;&gt;jyuch / excel-read-files&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;余談&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;ファイルをドラッグアンドドロップでテーブルを作れる機能も内部的に&lt;code&gt;read_files&lt;/code&gt;関数を使っているのか、現時点では同様に日本語のフリガナを拾ってしまいます。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;こちらも解決済みです。やったぜ&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2026/01-17-excel-read-files/gui_ingestion.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>2025年の振り返りとか</title>
		<link href="https://www.jyuch.dev/posts/2025/12-31-look-back-2025/"/>
		<updated>2025-12-31T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2025/12-31-look-back-2025/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;2025年の振り返りです。&lt;/p&gt;
&lt;h2&gt;Databricks&lt;/h2&gt;
&lt;p&gt;今年はお仕事でDatabricksをかなり触ってきました。&lt;/p&gt;
&lt;p&gt;特にDatabricksの環境を完全にゼロから（新規契約のAWSアカウントから）構築するのはあまりない経験だと思うので、珍しい経験が出来たなと思います。&lt;/p&gt;
&lt;p&gt;他にも基幹システムからのデータ取り込みやデータ加工パイプラインの構築・ガバナンス設計など&lt;s&gt;運用の全部を一人でやらさせられてる&lt;/s&gt;大変貴重な経験を積ませていただいており大変うれしく思います。&lt;/p&gt;
&lt;p&gt;個人的にはDatabricksは少なくとも数年はデータとAI領域でのトップを走るプロダクトだと思っているので、しばらくはDababricksのお仕事を続けたいなとは思ってます。&lt;/p&gt;
&lt;h2&gt;生成AI&lt;/h2&gt;
&lt;p&gt;データ基盤を触っているとAI絡みのお話を頂くので、RAGや&lt;a href=&quot;https://www.jyuch.dev/posts/2025/12-28-dspy-kasu-no-uso/&quot;&gt;プロンプト自動最適化&lt;/a&gt;などのテクノロジはちょこちょこと触っていました。
特にDSPyとMLflowはかなり注目しているので、2026年もこれらのプロダクトを追いかけたいと思っています。&lt;/p&gt;
&lt;p&gt;ただ、AIエージェントは技術的な領域としては面白いのですが、全社的なワークロードを動かしたときの性能面・コスト面の監視や基幹システムとの連接など考えないといけないことが多いなと現時点でも感じています。
2026年はこの辺の本格的な開発・運用が始まりそうなので2026年はつらみポイントが高い年になりそうです。&lt;/p&gt;
&lt;p&gt;この辺を真面目にやろうとすると事業会社の内部でどうこう出来るレベルを超えそうと思っているのですが、皆さんどう考えてるんでしょうね？&lt;/p&gt;
&lt;h2&gt;資格とか&lt;/h2&gt;
&lt;p&gt;今年は&lt;a href=&quot;https://www.jyuch.dev/posts/2025/07-13-2025-spring-nw/&quot;&gt;ネットワークスペシャリスト試験&lt;/a&gt;と&lt;a href=&quot;https://www.jyuch.dev/posts/2025/07-04-saa-c03/&quot;&gt;SAA&lt;/a&gt;の2つを合格・取得できました。&lt;/p&gt;
&lt;p&gt;ネットワークスペシャリスト試験で勉強した内容は普通に日常の業務&lt;s&gt;やテキトーなことを吹っ掛けてくる自称有識者をボコすの&lt;/s&gt;に役に立つので、ネットワークに触る人はみんな勉強すると良いと思います。&lt;/p&gt;
&lt;p&gt;IPAの試験体系が改定されて高度試験が消える前にデータベーススペシャリストもとっておこうかなと思ってます。
セキュリティスペシャリストはどうなるんでしょうね？あれだけ体系が違うっぽいので残る気がしますが。&lt;/p&gt;
&lt;p&gt;AWS系の資格について特にコンプとかは考えていないのですが、SAPを持ってると貰えるお小遣いが増えるので受けてもいいかなと思っています。
まぁ、弊社は全体的に資格難易度と金額が明らかに乖離しているのでにゃーん（社会性フィルタ）&lt;/p&gt;
&lt;p&gt;また、Databricks Certified Data Engineer Associateとかはあまり難しくなさそうなので試しに取ってみたいなとは思ってるのですが、受験料がｸｯｿ高いのと、弊社内でお小遣いが貰える資格ではないのでちょっと迷ってます。&lt;/p&gt;
&lt;h2&gt;コミュニティ&lt;/h2&gt;
&lt;p&gt;東京に勤務することになってから今年はDatabricksのユーザグループであるJEDAIのミートアップから始まり、結構な数の勉強会やもくもく会にお邪魔させていただきました。&lt;/p&gt;
&lt;p&gt;とくに&lt;a href=&quot;https://mito-web-engineer.connpass.com/&quot;&gt;Ibaraki.dev&lt;/a&gt;におかれましては、&lt;a href=&quot;https://speakerdeck.com/jyuch/start-your-own-dns-forwarder-with-hickory-dns&quot;&gt;いきなりDNSの話をし始めたり誰も使ってないDNSのライブラリの話をし始めたり&lt;/a&gt;と大変ご迷惑をお掛けしました。&lt;/p&gt;
&lt;p&gt;いろんな人と交流するとそれぞれの組織の考え方が分かったり、どんな組織でも大体似たようなことで悩んでるんだなというのが分かって楽しいですね。&lt;/p&gt;
&lt;h2&gt;おわりに&lt;/h2&gt;
&lt;p&gt;2026年も飛躍の年にしたいですね。&lt;/p&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>ダウナー系生成AIに毎日カスの嘘を流し込まれたい</title>
		<link href="https://www.jyuch.dev/posts/2025/12-28-dspy-kasu-no-uso/"/>
		<updated>2025-12-28T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2025/12-28-dspy-kasu-no-uso/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;最近は仕事でもAIエージェントの話を聞くことが多くなってきました。&lt;/p&gt;
&lt;p&gt;AIエージェントの性能はプロンプトの記述で大きく変わってきます。
ですが、プロンプトが少し変わったり、ナレッジカットオフが変わっただけで挙動が大きく変わるなんて話も聞きます。&lt;/p&gt;
&lt;p&gt;プロンプトの人力での調整はつらみポイントがとても高いので、個人的にはDSPyのようなプロンプト自動最適化に大きな期待を寄せています。&lt;/p&gt;
&lt;p&gt;そこで、今回は与えられたキーワードからいわゆる「カスの嘘」を生成するためのプロンプトをDSPyを使っての最適化を試してみます。&lt;/p&gt;
&lt;h2&gt;カスの嘘&lt;/h2&gt;
&lt;p&gt;まず、生成AIに生成させたいカスの嘘の要件を確認してみましょう。&lt;/p&gt;
&lt;p&gt;ここで生成したいカスの嘘とは以下のようなものです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;雀が電線で感電しないのは、みんなで乗ることでダメージを分散しているからなんだよ&lt;/li&gt;
&lt;li&gt;地球儀は風水で最強なので、どこにいくつ置いてもいいんだよ&lt;/li&gt;
&lt;li&gt;一度開封したみりんを常温で保管すると、違法なんだって&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これらのカスの嘘の特徴をまとめてみると、&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一見して分かるようなあからさまな嘘ではないこと&lt;/li&gt;
&lt;li&gt;科学・文化・法律に依拠したうそであること&lt;/li&gt;
&lt;li&gt;「〇〇、〇〇」のフォーマットに従っており、20文字から40文字程度であること&lt;/li&gt;
&lt;li&gt;落ち着いた穏やかな口調であること&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;であることが分かります。&lt;/p&gt;
&lt;h2&gt;DSPyを使用した最適化&lt;/h2&gt;
&lt;p&gt;手作業でのプロンプトの最適化では、上記に挙げた特徴に沿った出力を行うようにプロンプトを手作業で最適化する必要があります。&lt;/p&gt;
&lt;p&gt;DSPyでは代わりに生成AIを使用した処理をシグネチャとモジュールとして定義し、用意したデータセットと評価関数をを用いてスコアが高くなるようにプロンプトの最適化を行います。&lt;/p&gt;
&lt;p&gt;今回使用する&lt;a href=&quot;https://arxiv.org/abs/2507.19457&quot;&gt;GEPA&lt;/a&gt;では評価関数のメトリックだけでなく、評価関数が返すフィードバックテキストを元にプログラムの最適化を行います。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2025/12-21-dspy-kasu-no-uso/%E5%85%A8%E4%BD%93%E6%A7%8B%E6%88%90%E5%9B%B3.png&quot; alt=&quot;全体構成図&quot;&gt;&lt;/p&gt;
&lt;h2&gt;カスの嘘を吐くDSPyプログラム&lt;/h2&gt;
&lt;p&gt;今回は以下のようなシグネチャとモジュールを定義します。
キーワードからカスを嘘を出力するとても単純なものです。&lt;/p&gt;
&lt;p&gt;また、DSPyではコメントドキュメントをプロンプトとして使用するのですが、自動プロンプト最適化を使用する場合は非常に非常にざっくりとしたものでもいい感じに最適化してくれます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class LieGenerate(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;与えられたキーワードからカスの嘘を生成します&amp;quot;&amp;quot;&amp;quot;

    keyword: str = dspy.InputField(desc=&amp;quot;キーワード&amp;quot;)
    lie: str = dspy.OutputField(desc=&amp;quot;生成されたカスの嘘&amp;quot;)


class GenerateLie(dspy.Module):
    def __init__(self):
        super().__init__()
        self.extractor = dspy.ChainOfThought(LieGenerate)

    def forward(self, keyword):
        return self.extractor(keyword=keyword)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;評価関数&lt;/h2&gt;
&lt;p&gt;今回はカスの嘘を上記の観点で判定させるため、LLM as Judgeで評価を行います。&lt;/p&gt;
&lt;p&gt;生成AIは放っておくとファンタジーな嘘をつき始めるので、ファンタジー要素はあからさまなSF要素は減点するように評価をさせています。&lt;/p&gt;
&lt;p&gt;この評価用のプロンプトはある程度試行錯誤しないといけないんですよね・・・&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def create_lie_metric(reflection_lm: dspy.LM):
    class StyleEvaluation(dspy.Signature):
        &amp;quot;&amp;quot;&amp;quot;応答のスタイルを評価&amp;quot;&amp;quot;&amp;quot;

        response = dspy.InputField(desc=&amp;quot;評価対象の応答&amp;quot;)
        criteria = dspy.InputField(desc=&amp;quot;評価基準&amp;quot;)
        score = dspy.OutputField(desc=&amp;quot;スコア（0-20）&amp;quot;, format=int)
        explanation = dspy.OutputField(desc=&amp;quot;詳細な項目毎のスコアとその理由&amp;quot;)

    evaluator = dspy.ChainOfThought(StyleEvaluation)

    def lie_metric(gold, pred, trace=None, pred_name=None, pred_trace=None):
        criteria = &amp;quot;&amp;quot;&amp;quot;
        以下の基準で0-20点で評価してください:
        1. キーワードに沿った嘘となっているか（2点）
        2. 20-60文字で、「〇〇は、〇〇」や「〇〇はね、〇〇」という形式となっているか（2点）
        3. あからさまな嘘ではなく、一見すると嘘とわからないようなもっともらしい嘘となっているか（6点）
            - 魔法やドラゴン・妖精といった空想的存在に基づいた嘘ではないか
            - 宇宙人や火星といった存在が確認されていないものや現在の科学水準で達成できない事柄を含んでいないか
            - 食品が必修科目として学ばれるなど、キーワードと嘘の内容が乖離していないか
            - 存在しない税が免除されるといった一つの文の中に複数の嘘が入っている場合は減点
        4. 余計な解説や文が入っておらず、嘘のみの出力となっているか（2点）
        5. 語頭に「ね、知ってる？」や「実は」を適切に使っているか（2点）
        6. 語尾に「（な）んだよ」や「（な）んだって」、「らしいよ」を自然に接続して適切に使っているか（2点）
        7. 日本語として自然で読みやすいか（2点）
        8. 穏やかで落ち着いた口調か（2点）
        &amp;quot;&amp;quot;&amp;quot;
        with dspy.context(lm=reflection_lm):
            eval_result = evaluator(response=pred.lie, criteria=criteria)
            score = min(20, max(0, float(eval_result.score))) / 20.0
            explanation = eval_result.explanation
            return dspy.Prediction(score=score, feedback=explanation)

    return lie_metric
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最適化&lt;/h2&gt;
&lt;p&gt;今回はリフレクションLLMに&lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt;を使用し、タスクを実行するためのLLMとして以下のモデルを使って評価をします。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gpt-oss-120b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gpt-oss-20b&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本当は Claude Opus 4.5 を使いたかったのですが、トレーニングを1回通しで実行するだけで50ドルくらい消費しそうになったためビビッて Sonnet に変えたという経緯があります。&lt;/p&gt;
&lt;p&gt;学習1回あたり2時間30分程度、コストとしては10ドル程度掛かっていました。&lt;/p&gt;
&lt;h2&gt;生成出来るカスの嘘&lt;/h2&gt;
&lt;p&gt;各モデルがつく嘘は以下のような感じです。&lt;/p&gt;
&lt;p&gt;gpt-ossはやたら江戸時代の話題をしたがりますが、全体的にそれっぽい嘘をつけているのではないでしょうか。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;symbol&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;keyword&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;lie&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;おみくじ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、おみくじは江戸時代に寺の守護神が結果を予測したんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;おみくじ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;おみくじは、実は江戸時代に寺院で神の預言として引かれたものらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;おみくじ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、おみくじは元々、神社の祭りの日に参加者の役割を決めるためのくじだったんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;お餅&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、お餅は江戸時代に、米ではなく大麦の粉を蒸して作っていたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;お餅&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;お餅は、実は江戸時代の神社で祭祀用の供え物として使われていたんだよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;お餅&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、お餅は昔、神社の儀式で使う供え物として作られたのが始まりで、食べ物として広まったのは江戸時代らしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;ちくわ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;ちくわは実は江戸時代に、魚のすり身ではなく、山形県の里山で取れた大根の根を乾燥させたものらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;ちくわ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;ちくわは、実は平安時代の貴族が魚の内臓を使い、皮を巻いて焼いたお菓子らしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;ちくわ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、ちくわは昔、竹の筒に詰めて蒸すことから『竹輪』という名前がついたんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;チョコレート&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;チョコレートは実は19世紀末のスイスで、当初はカカオ豆の発酵を防ぐための保存料として開発されたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;チョコレート&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;チョコレートは、実は16世紀にスペインの商人が日本に持ち込んだ貴族用甘味料として使われていたんだよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;チョコレート&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、チョコレートは昔、王族の儀式用の飲料として神聖視されていたんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;懐中電灯&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;懐中電灯は実は、第一次世界大戦時にドイツ軍が潜水艦用に赤外線光源を内蔵していたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;懐中電灯&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;懐中電灯は、実は江戸時代に武士が夜の暗闇で作戦指示を確認するために携帯した小型の灯りで、取手に銀の刻印が付いていたんだよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;懐中電灯&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、懐中電灯は元々、鉱山での作業中に両手を自由にするために懐に入れて使う照明として開発されたんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;乾電池&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、乾電池はかつて日本の昭和30年代に、アルカリ電解質ではなく銀酸化物が主成分だったらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;乾電池&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;乾電池は、実は地下鉄の電力供給に使われていたと、当時の軍部秘書が記録していたんだよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;乾電池&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、乾電池は元々、懐中電灯ではなく、医療用の刺激装置として開発されたんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;軽トラ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、軽トラは昭和60年代に農業用から貨物輸送専用に改良されたんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;軽トラ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;軽トラは、実は昭和30年代に発明された、山間部の農家が手作りで改造した小型自動車の原型だって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;軽トラ&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、軽トラは元々、郵便配達員が使うために開発された専用車両なんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;初詣&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、初詣は江戸時代に商人が売り上げを祈願するために始めたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;初詣&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;初詣は、実は江戸時代から新年のご飯をお供えすると、その年の収穫が倍増すると信じられていたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;初詣&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、初詣は江戸時代に商人たちが商売繁盛を祈るために始めた習慣なんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;生成AI&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;生成AIは実は1970年代に日本の研究所で、音声合成だけでなく感情推定までできると宣伝されたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;生成AI&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;生成AIは、実は19世紀末のドイツ工学者がパターン生成装置として開発したらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;生成AI&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、生成AIって、元々は人間の脳の神経回路を模倣するために開発された技術なんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-120b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;東京タワー&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、東京タワーは当初、通信塔ではなく観測用の気象観測所として建てられたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;gpt-oss-20b&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;東京タワー&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;東京タワーは、実は昭和初期に東京郊外の灯台の設計図を活用して作られたらしいよ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;haiku-4-5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;東京タワー&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;実は、東京タワーは元々、戦後の電波障害を調査するための実験施設だったんだって&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;クロス評価&lt;/h2&gt;
&lt;p&gt;それぞれのモデルに最適なプロンプトが求められたので、プロンプトをそれぞれモデルに適用してスコアがどのようになるか評価してみました。&lt;/p&gt;
&lt;p&gt;どのモデルでもモデルに最適化されたプロンプトを使用したときが最も高いスコアを出していることが分かります。
また、gpt-oss-20bのようにパラメータ数が小さいモデルでは最適なプロンプトとそれ以外のプロンプトでのスコアの差が大きくなることが分かります。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2025/12-21-dspy-kasu-no-uso/cross-eval.png&quot; alt=&quot;クロス評価結果&quot;&gt;&lt;/p&gt;
&lt;h2&gt;一通り試してみての感想&lt;/h2&gt;
&lt;p&gt;ここからは一通り試してみての感想です。&lt;/p&gt;
&lt;h3&gt;学習データの準備が一番大変&lt;/h3&gt;
&lt;p&gt;この辺はよく言われると思います。
今回はしょうもない嘘をつかせるという非常に単純なタスクだったので、学習データは30程度でも割と何とかなってます。
ですが、業務レベルの複雑さの場合、とにかく学習データを用意するのがとても大変になると思います。&lt;/p&gt;
&lt;h3&gt;評価関数のチューニングが大事&lt;/h3&gt;
&lt;p&gt;今回のタスクはキーワードから嘘をつくという、創作的なタスクを行わせています。
そのため、与えられた入力からどれだけ期待する出力化という観点では評価がしづらく、LLM as Judgeで評価をさせています。&lt;/p&gt;
&lt;p&gt;評価関数で自分が欲しい嘘に対して高いスコアを返せるよういかに嘘の条件を言語化してLLM as Judgeで評価するのが大事でした。&lt;/p&gt;
&lt;h3&gt;APIの使用料金もそれなりに掛かる&lt;/h3&gt;
&lt;p&gt;GEPAはフィードバックを基にプロンプトを最適化しますが、そのプロンプトの最適化には出来るだけ高性能なLLMを使ったほうがいいらしいです。&lt;/p&gt;
&lt;p&gt;そんな高性能なLLMをすごい勢いで呼び出すので、当然APIの使用料金も高くなります。
かといってプロンプトの最適化に安価なモデルを使用すると明らかにカスの嘘のクオリティも下がるので、この辺はクオリティと費用のバランスをうまくとる必要がありそうです。&lt;/p&gt;
&lt;h3&gt;試行錯誤が無くなるわけではない&lt;/h3&gt;
&lt;p&gt;LLM as Judgeを使う以上、ジャッジをさせるプロンプトそのものの最適化は人間がやらなくてはいけません。
今回の例ですと、LLMはやたらとファンタジーな嘘をつきたがるので、それをいかにフィードバックで消すかというところに労力がかかっています。&lt;/p&gt;
&lt;p&gt;LLM as Judge自体をMIPROなりGEPAなりで最適化する手法もありますが、その辺の評価をどうするかや学習データの整備にも手間と時間が掛かるので、試行錯誤自体を消せるわけではないのかなという気がしています。&lt;/p&gt;
&lt;h2&gt;おわりに&lt;/h2&gt;
&lt;p&gt;生成AIにしょうもない嘘をつかせるのも安くはないですね・・・&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.jyuch.dev/img/2025/12-21-dspy-kasu-no-uso/%E3%82%B3%E3%82%B9%E3%83%88.png&quot; alt=&quot;カスの嘘をつかせるために28ドル掛かりました&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jyuch/dspy-kasu-no-uso&quot;&gt;jyuch / dspy-kasu-no-uso&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;おわり&lt;/p&gt;
&lt;h2&gt;参考資料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mahm/dspy-demo&quot;&gt;mahm / dspy-demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://speakerdeck.com/masahiro_nishimi/dspy-meetup-tokyo-number-1-hazimetenodspy&quot;&gt;はじめてのDSPy - 株式会社ジェネラティブエージェント 西見公宏&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
	</entry>
	<entry>
		<title>DSPyを使用してLLMを使ったレシートの読み取り精度を向上させたい</title>
		<link href="https://www.jyuch.dev/posts/2025/10-26-improve-precision-using-dspy/"/>
		<updated>2025-10-26T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2025/10-26-improve-precision-using-dspy/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;簡単な家計簿をつけているのですが、レシートの内容を確認して転記するのがめんどくさいなと感じていました。&lt;/p&gt;
&lt;p&gt;そこで、LLMを使って情報を抽出できないかなと考えていたのですが、せっかくなら&lt;a href=&quot;https://dspy.ai/&quot;&gt;DSPy&lt;/a&gt;を使って構造化出力するのと、プロンプト最適化を行ってみたいなということで試してみました。&lt;/p&gt;
&lt;h2&gt;シグネチャの準備&lt;/h2&gt;
&lt;p&gt;DSPyでは入力値と出力値をコードとして表現します。
ここではレシート画像から購入日と合計金額を抽出することにします。&lt;/p&gt;
&lt;p&gt;入力値と出力値はシグネチャとして表現され、&lt;code&gt;dspy.Signature&lt;/code&gt;のサブクラスとして実装します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import date

import dspy


class ExtractReceiptInfo(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;Extract total amount from receipt image.&amp;quot;&amp;quot;&amp;quot;

    image: dspy.Image = dspy.InputField(desc=&amp;quot;Receipt image.&amp;quot;)
    purchase_date: date = dspy.OutputField(desc=&amp;quot;Purchase date of payment.&amp;quot;)
    total_amount: int = dspy.OutputField(desc=&amp;quot;Total amount of payment.&amp;quot;)


class ReceiptExtractor(dspy.Module):
    def __init__(self):
        super().__init__()
        self.extractor = dspy.ChainOfThought(ExtractReceiptInfo)

    def forward(self, image):
        return self.extractor(image=image)


def extraction_metric(gold, pred, trace=None):
    metric = 0

    if gold.total_amount == pred.total_amount:
        metric += 1
    if gold.purchase_date == pred.purchase_date:
        metric += 1

    if trace is None:
        return metric / 2.0
    else:
        return metric == 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;学習データの準備&lt;/h2&gt;
&lt;p&gt;DSPyでは教師データを使用して入力データから期待する出力をするようにプロンプトを最適化します。&lt;/p&gt;
&lt;p&gt;そのため、最初にある程度のレシート画像と、その画像からどのような結果を出力してほしいかの期待値をひたすら列挙する必要があります。&lt;/p&gt;
&lt;p&gt;ここでは、以下のような教師データを家中のあるだけのレシートを使って作成します。
スキャナでレシートを読み込み、エクセルでレシート画像と期待値の組み合わせをひたすら入力します。&lt;/p&gt;
&lt;p&gt;このデータそのまま家計簿に突っ込めばよくね？とか考えてはいけません。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;image&lt;/th&gt;
&lt;th style=&quot;text-align:right&quot;&gt;purchase_date&lt;/th&gt;
&lt;th style=&quot;text-align:right&quot;&gt;total_amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;20251019_000.jpg&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;2025-08-28&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;162&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;20251019_001.jpg&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;2025-08-19&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;170&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;20251019_002.jpg&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;2025-09-09&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;162&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;...&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;...&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;LM Studioの準備&lt;/h2&gt;
&lt;p&gt;タスクを実行するLLMはLM Studioを使ってローカルで実行するので、LM Studioをインストールしておきます。
モデルはGemma 3を使います。&lt;/p&gt;
&lt;h2&gt;Databricks Free Editionの準備&lt;/h2&gt;
&lt;p&gt;MIPROv2では教師として高性能なLLMを使用するのですが、ここではDatabricksでホストされているLlama 4 Maverickを利用します。&lt;/p&gt;
&lt;p&gt;Databricks Free Editionではレートリミットなどの制限はありますが、無償で使わせてくれるのでありがたく使います。Databricksさんはなんて太っ腹なんでしょう！（ステマその１）&lt;/p&gt;
&lt;p&gt;Settings → User → Developer → Access tokensからトークンを発行しておきます。
また、MLFlowも使いたいので、併せてExperimentsも作成しておきます。
Experimentsを作成すると、外部からExperimentsを使うにはみたいな画面が表示されるので、表示された環境変数をコピーしておきます。&lt;/p&gt;
&lt;p&gt;最終的に以下の環境変数を登録します。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;環境変数&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;DATABRICKS_API_BASE&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;https://dbc-12345678-abcd.cloud.databricks.com/serving-endpoints&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;DATABRICKS_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Databricksのシークレット&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;DATABRICKS_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;https://dbc-12345678-abcd.cloud.databricks.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;DATABRICKS_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Databricksのシークレット&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;MLFLOW_EXPERIMENT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;123456789012345&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;MLFLOW_REGISTRY_URI&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;databricks-uc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;MLFLOW_TRACKING_URI&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;databricks&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;DATABRICKS_API_BASE&lt;/code&gt;と&lt;code&gt;DATABRICKS_API_KEY&lt;/code&gt;はDatabricksのServing endpointsにアクセスする用で、それ以外はMLFlowにメトリックを送る用です。&lt;/p&gt;
&lt;h2&gt;トレーニング&lt;/h2&gt;
&lt;p&gt;必要なものがそろったらいよいよプロンプト最適化を実行します。
ここではとりあえずMIPROv2を使っていきます。&lt;/p&gt;
&lt;p&gt;余談ですが、DSPyはLLMにアクセスするために&lt;a href=&quot;https://www.litellm.ai/&quot;&gt;LiteLLM&lt;/a&gt;というライブラリを使用しているようです。&lt;/p&gt;
&lt;p&gt;LiteLLMではプレフィックスでどのプロバイダーのAPI形式（OpenAI互換やAnthropic互換など）を判断しているようです。
また、LM StudioはOpenAI互換です。&lt;/p&gt;
&lt;p&gt;そのため、OpenAI互換としてAPIを叩いてほしいのですが、&lt;code&gt;openai/google/gemma-3-12b&lt;/code&gt;とかいう各方面から怒られそうなモデル名で指定をしないといけません。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import csv
import os
from datetime import datetime
from typing import List

import dspy
import mlflow

from program import ReceiptExtractor, extraction_metric

LMSTUDIO_API_BASE = os.environ[&amp;quot;LMSTUDIO_API_BASE&amp;quot;]

teacher_llm = dspy.LM(
    &amp;quot;databricks/databricks-llama-4-maverick&amp;quot;,
    temperature=1.0,
)

student_llm = dspy.LM(
    &amp;quot;openai/google/gemma-3-12b&amp;quot;,
    api_base=LMSTUDIO_API_BASE,
    api_key=&amp;quot;dummy&amp;quot;,
)


def run_prompt_optimizer(train_examples: List[dspy.Example]):
    student_program = ReceiptExtractor()
    optimizer = dspy.MIPROv2(
        metric=extraction_metric, prompt_model=teacher_llm, task_model=student_llm
    )
    compiled_program = optimizer.compile(student_program, trainset=train_examples)
    compiled_program.save(&amp;quot;./program.json&amp;quot;, save_program=False)


def main():
    mlflow.dspy.autolog(
        log_compiles=True,
        log_evals=True,
        log_traces_from_compile=True,
    )

    dspy.configure(lm=student_llm)

    train_examples: List[dspy.Example]
    with open(&amp;quot;./dataset/training.csv&amp;quot;, encoding=&amp;quot;utf_8&amp;quot;) as f:
        reader = csv.DictReader(f)
        train_examples = [
            dspy.Example(
                image=dspy.Image.from_file(f&amp;quot;./dataset/{row[&#39;image&#39;]}&amp;quot;),
                purchase_date=datetime.strptime(
                    row[&amp;quot;purchase_date&amp;quot;], &amp;quot;%Y-%m-%d&amp;quot;
                ).date(),
                total_amount=int(row[&amp;quot;total_amount&amp;quot;]),
            ).with_inputs(&amp;quot;image&amp;quot;)
            for row in reader
        ]

    run_prompt_optimizer(train_examples)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;評価&lt;/h2&gt;
&lt;p&gt;学習が終わったら、とりあえずどのくらい違うのか評価してみましょう。&lt;/p&gt;
&lt;p&gt;ここでは、学習前と学習後、LLMのモデル、パラメータ数を変えてどのくらい差があるのかを確認しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import csv
import os
from datetime import datetime
from typing import List

import dspy
from dspy.evaluate.evaluate import Evaluate

from program import ReceiptExtractor, extraction_metric

LMSTUDIO_API_BASE = os.environ[&amp;quot;LMSTUDIO_API_BASE&amp;quot;]

gemma_3_12b = dspy.LM(
    &amp;quot;openai/google/gemma-3-12b&amp;quot;,
    api_base=LMSTUDIO_API_BASE,
    api_key=&amp;quot;dummy&amp;quot;,
)

gemma_3_27b = dspy.LM(
    &amp;quot;openai/google/gemma-3-27b&amp;quot;,
    api_base=LMSTUDIO_API_BASE,
    api_key=&amp;quot;dummy&amp;quot;,
)

llama_4_maverick = dspy.LM(
    &amp;quot;databricks/databricks-llama-4-maverick&amp;quot;,
)


def main():
    dspy.configure(lm=gemma_3_12b)
    original = ReceiptExtractor()
    trained = ReceiptExtractor()
    trained.load(&amp;quot;./program.json&amp;quot;)

    train_examples: List[dspy.Example]
    with open(&amp;quot;./dataset/training.csv&amp;quot;, encoding=&amp;quot;utf_8&amp;quot;) as f:
        reader = csv.DictReader(f)
        train_examples = [
            dspy.Example(
                image=dspy.Image.from_file(f&amp;quot;./dataset/{row[&#39;image&#39;]}&amp;quot;),
                purchase_date=datetime.strptime(
                    row[&amp;quot;purchase_date&amp;quot;], &amp;quot;%Y-%m-%d&amp;quot;
                ).date(),
                total_amount=int(row[&amp;quot;total_amount&amp;quot;]),
            ).with_inputs(&amp;quot;image&amp;quot;)
            for row in reader
        ]

    evaluate = Evaluate(
        devset=train_examples, num_threads=1, display_progress=True, display_table=0
    )

    with dspy.context(lm=gemma_3_12b):
        evaluate(original, metric=extraction_metric)
        evaluate(trained, metric=extraction_metric)

    with dspy.context(lm=gemma_3_27b):
        evaluate(original, metric=extraction_metric)
        evaluate(trained, metric=extraction_metric)

    with dspy.context(lm=llama_4_maverick):
        evaluate(original, metric=extraction_metric)
        evaluate(trained, metric=extraction_metric)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;モデル&lt;/th&gt;
&lt;th style=&quot;text-align:right&quot;&gt;最適化前&lt;/th&gt;
&lt;th style=&quot;text-align:right&quot;&gt;最適化後&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;google/gemma-3-12&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;84.0%&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;96.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;google/gemma-3-27b&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;80.2%&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;97.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;databricks-llama-4-maverick&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;100.0%&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;100.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;あー、うん、まぁ、ねぇ。&lt;/p&gt;
&lt;h2&gt;おわりに&lt;/h2&gt;
&lt;p&gt;プロンプト最適化の部分については確かに最適化すれば性能は上がりましたが、最初から高性能なモデルを使えばそりゃ精度は高いよねというのを如実に見せつけられました。&lt;/p&gt;
&lt;p&gt;ですが、個人的には構造化出力をコードとして表現できるのはうれしみがありますね。
プロンプトにJSON Schemaをくっつけて、出力をJSONパーサに食わせて正常にパース出来るのを祈るという作業から解放されるだけでもDSPyを使ううれしみがあると思います。&lt;/p&gt;
&lt;p&gt;教師データだけ用意しておけば、他のLLMが出てきたときは最適化と検証のループを回して、今までよりも成績が良ければ入れ替えるというサイクルをほぼ自動で回せます。&lt;/p&gt;
&lt;p&gt;ギョームでLLMを使う場合はこの辺のサイクルを回せるようにしておくと後々のつらみポイントを軽減出来ると思うので、みなさんDatabricksと合わせて使ってみてはいかがでしょうか。（ステマその２）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jyuch/extract-receipt/tree/master&quot;&gt;jyuch / extract-receipt&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>PostgreSQL 18 の uuidv7 関数は単調性が保証されているから安心だねってお話</title>
		<link href="https://www.jyuch.dev/posts/2025/10-12-pg18-uuidv7-is-monotonically-increasing/"/>
		<updated>2025-10-12T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2025/10-12-pg18-uuidv7-is-monotonically-increasing/</id>
		<content type="html">&lt;h2&gt;長いので先にまとめ&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;UUIDv7は&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9562&quot;&gt;RFC9562&lt;/a&gt;上ではミリ秒精度のタイムスタンプを持つことを要求している&lt;/li&gt;
&lt;li&gt;また、オプショナルとして&lt;code&gt;rand_a&lt;/code&gt;及び&lt;code&gt;rand_b&lt;/code&gt;の生成方法を工夫して単調増加を保証しても良いとされている&lt;/li&gt;
&lt;li&gt;PostgreSQL 18 の&lt;code&gt;uuidv7()&lt;/code&gt;関数はRFC9562のSection 6.2 Method 3で提案されているタイムスタンプをサブミリ秒まで拡張する方法で単調増加を保証している
&lt;ul&gt;
&lt;li&gt;サブミリ秒レベルでタイムスタンプが衝突した場合は、最小精度でインクリメントすることでタイムスタンプの衝突を回避している&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;最近リリースされたPostgreSQL 18 で UUIDv7 の生成がサポートされました。&lt;/p&gt;
&lt;p&gt;個人的にはUUIDが生成順にソート可能になるうれしさがよく分からないのですが、いざ使うことになった際に困らないように調べてみることにしました。&lt;/p&gt;
&lt;h2&gt;UUIDv7&lt;/h2&gt;
&lt;p&gt;そもそも、UUIDv7はどのような構造をしているのでしょうか。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7&quot;&gt;5.7. UUID Version 7&lt;/a&gt;からビットレイアウトを引用して確認してみましょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                           unix_ts_ms                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          unix_ts_ms           |  ver  |       rand_a          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |var|                        rand_b                             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                            rand_b                             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                   Figure 11: UUIDv7 Field and Bit Layout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;バージョン（&lt;code&gt;ver&lt;/code&gt;）とバリアント（&lt;code&gt;var&lt;/code&gt;）を除くと、以下の要素から構成されていることが分かります。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;フィールド&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;説明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;unix_ts_ms&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;48ビットで表現されたミリ秒精度のUnixエポックタイムスタンプ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;rand_a&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;12ビットのランダムフィールド&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;rand_b&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;62ビットのランダムフィールド&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RFCが最低限求めている仕様では、タイムスタンプはミリ秒精度となっています。
そのため、同一ミリ秒内で複数回UUIDが生成された場合、生成された順序の並べ替えを保証出来なくなってしまいます。&lt;/p&gt;
&lt;p&gt;そのため、&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9562#name-monotonicity-and-counters&quot;&gt;6.2. Monotonicity and Counters&lt;/a&gt;では、高頻度でのUUIID生成環境下での単調増加性を保証するための方法が提案されています。&lt;/p&gt;
&lt;h2&gt;PostgreSQL 18 の UUIDv7&lt;/h2&gt;
&lt;p&gt;PostgreSQL 18 の&lt;code&gt;uuidv7()&lt;/code&gt;関数はミリ秒タイムスタンプ + サブミリ秒タイムスタンプ + ランダム値で計算されていることが&lt;a href=&quot;https://www.postgresql.org/docs/18/functions-uuid.html#FUNC_UUID_GEN_TABLE&quot;&gt;ドキュメントに記載されています。&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;具体的には&lt;code&gt;uuid.c&lt;/code&gt;の以下の&lt;code&gt;generate_uuidv7()&lt;/code&gt;関数で実装されています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/*
 * Generate UUID version 7 per RFC 9562, with the given timestamp.
 *
 * UUID version 7 consists of a Unix timestamp in milliseconds (48 bits) and
 * 74 random bits, excluding the required version and variant bits. To ensure
 * monotonicity in scenarios of high-frequency UUID generation, we employ the
 * method &amp;quot;Replace Leftmost Random Bits with Increased Clock Precision (Method 3)&amp;quot;,
 * described in the RFC. This method utilizes 12 bits from the &amp;quot;rand_a&amp;quot; bits
 * to store a 1/4096 (or 2^12) fraction of sub-millisecond precision.
 *
 * unix_ts_ms is a number of milliseconds since start of the UNIX epoch,
 * and sub_ms is a number of nanoseconds within millisecond. These values are
 * used for time-dependent bits of UUID.
 *
 * NB: all numbers here are unsigned, unix_ts_ms cannot be negative per RFC.
 */
static pg_uuid_t *
generate_uuidv7(uint64 unix_ts_ms, uint32 sub_ms)
{
	pg_uuid_t  *uuid = palloc(UUID_LEN);
	uint32		increased_clock_precision;

	/* Fill in time part */
	uuid-&amp;gt;data[0] = (unsigned char) (unix_ts_ms &amp;gt;&amp;gt; 40);
	uuid-&amp;gt;data[1] = (unsigned char) (unix_ts_ms &amp;gt;&amp;gt; 32);
	uuid-&amp;gt;data[2] = (unsigned char) (unix_ts_ms &amp;gt;&amp;gt; 24);
	uuid-&amp;gt;data[3] = (unsigned char) (unix_ts_ms &amp;gt;&amp;gt; 16);
	uuid-&amp;gt;data[4] = (unsigned char) (unix_ts_ms &amp;gt;&amp;gt; 8);
	uuid-&amp;gt;data[5] = (unsigned char) unix_ts_ms;

	/*
	 * sub-millisecond timestamp fraction (SUBMS_BITS bits, not
	 * SUBMS_MINIMAL_STEP_BITS)
	 */
	increased_clock_precision = (sub_ms * (1 &amp;lt;&amp;lt; SUBMS_BITS)) / NS_PER_MS;

	/* Fill the increased clock precision to &amp;quot;rand_a&amp;quot; bits */
	uuid-&amp;gt;data[6] = (unsigned char) (increased_clock_precision &amp;gt;&amp;gt; 8);
	uuid-&amp;gt;data[7] = (unsigned char) (increased_clock_precision);

	/* fill everything after the increased clock precision with random bytes */
	if (!pg_strong_random(&amp;amp;uuid-&amp;gt;data[8], UUID_LEN - 8))
		ereport(ERROR,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg(&amp;quot;could not generate random values&amp;quot;)));

#if SUBMS_MINIMAL_STEP_BITS == 10

	/*
	 * On systems that have only 10 bits of sub-ms precision,  2 least
	 * significant are dependent on other time-specific bits, and they do not
	 * contribute to uniqueness. To make these bit random we mix in two bits
	 * from CSPRNG. SUBMS_MINIMAL_STEP is chosen so that we still guarantee
	 * monotonicity despite altering these bits.
	 */
	uuid-&amp;gt;data[7] = uuid-&amp;gt;data[7] ^ (uuid-&amp;gt;data[8] &amp;gt;&amp;gt; 6);
#endif

	/*
	 * Set magic numbers for a &amp;quot;version 7&amp;quot; (pseudorandom) UUID and variant,
	 * see https://www.rfc-editor.org/rfc/rfc9562#name-version-field
	 */
	uuid_set_version(uuid, 7);

	return uuid;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;詳しくは実装を読んでもらえればと思いますが、&lt;code&gt;rand_a&lt;/code&gt;フィールドの12bitにナノ秒（Linux）を埋め込むか、マイクロ秒 + ランダム値（Windows・mac）を埋め込んでいます。&lt;/p&gt;
&lt;p&gt;また、現在時刻を供給している&lt;code&gt;get_real_time_ns_ascending()&lt;/code&gt;関数内で、前回との時刻の差分が&lt;code&gt;rand_a&lt;/code&gt;フィールドに埋め込むタイムスタンプの精度以下の場合は、最小精度（Linuxの場合は245マイクロ秒）を加算することでサブミリ秒内でタイムスタンプが衝突することを防いでいます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/*
 * Get the current timestamp with nanosecond precision for UUID generation.
 * The returned timestamp is ensured to be at least SUBMS_MINIMAL_STEP greater
 * than the previous returned timestamp (on this backend).
 */
static inline int64
get_real_time_ns_ascending()
{
	static int64 previous_ns = 0;
	int64		ns;

	/* Get the current real timestamp */

#ifdef	_MSC_VER
	struct timeval tmp;

	gettimeofday(&amp;amp;tmp, NULL);
	ns = tmp.tv_sec * NS_PER_S + tmp.tv_usec * NS_PER_US;
#else
	struct timespec tmp;

	/*
	 * We don&#39;t use gettimeofday(), instead use clock_gettime() with
	 * CLOCK_REALTIME where available in order to get a high-precision
	 * (nanoseconds) real timestamp.
	 *
	 * Note while a timestamp returned by clock_gettime() with CLOCK_REALTIME
	 * is nanosecond-precision on most Unix-like platforms, on some platforms
	 * such as macOS it&#39;s restricted to microsecond-precision.
	 */
	clock_gettime(CLOCK_REALTIME, &amp;amp;tmp);
	ns = tmp.tv_sec * NS_PER_S + tmp.tv_nsec;
#endif

	/* Guarantee the minimal step advancement of the timestamp */
	if (previous_ns + SUBMS_MINIMAL_STEP_NS &amp;gt;= ns)
		ns = previous_ns + SUBMS_MINIMAL_STEP_NS;
	previous_ns = ns;

	return ns;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そのため、実用的かどうかと言われるとｱﾚですが、&lt;code&gt;uuidv7()&lt;/code&gt;関数で生成されたUUIDv7からサブミリ秒のタイムスタンプを抽出することが出来ます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// サブミリ秒が12ビットで供給されている環境用
fn uuidv7_to_timestamp(uuidv7: &amp;amp;str) -&amp;gt; (i64, i64) {
    let uuid = uuidv7.replace(&amp;quot;-&amp;quot;, &amp;quot;&amp;quot;);
    let uuid = u128::from_str_radix(&amp;amp;*uuid, 16).unwrap();
    let ms = (uuid &amp;gt;&amp;gt; 80) as i64;

    let uuid = uuid.to_be_bytes();
    let increased_clock_precision = (((uuid[6] &amp;amp; 0x0fu8) as i64) &amp;lt;&amp;lt; 8) | uuid[7] as i64;
    let ns = increased_clock_precision * SUBMS_MINIMAL_STEP_NS;

    (ms, ns)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jyuch/pg-uuidv7-to-timestamp&quot;&gt;jyuch/pg-uuidv7-to-timestamp&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;PostgreSQL 18 でのUUIDv7生成例&lt;/h2&gt;
&lt;p&gt;最後に実際に&lt;code&gt;uuidv7()&lt;/code&gt;関数を使ってUUIDv7を生成して、単調増加しているかを確認してみましょう。&lt;/p&gt;
&lt;p&gt;以下のようなテーブルを作成したうえで、&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create table uuidv7_test_table
(
    i     int,
    clock timestamp,
    value uuid
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下のクエリで全力でUUIDv7を生成します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;begin transaction;
do
$do$
  begin
    for i in 1..1000000
      loop
        insert into uuidv7_test_table(i, clock, value) 
        values (i, clock_timestamp(), uuidv7());
      end loop;
    end
$do$;
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;すると、以下のような結果となります。
上記の&lt;code&gt;uuidv7_to_timestamp()&lt;/code&gt;の結果を併記しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select i, value, uuid_extract_timestamp(value), clock
from uuidv7_test_table
order by value;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;i&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;value&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;uuid_extract_timestamp&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;clock&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;uuidv7_to_timestamp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;0199d67d-81a7-713d-aed5-6bbd22a476c2&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479000 +00:00&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479071&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479 UTC 77665&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;0199d67d-81a7-7be4-ba18-b1d95ac5835f&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479000 +00:00&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479741&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479 UTC 745780&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;0199d67d-81a7-7c31-9ca1-df81b59c2e69&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479000 +00:00&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479762&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479 UTC 764645&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;0199d67d-81a7-7c45-b183-8ebf38326e24&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479000 +00:00&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479766&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479 UTC 769545&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;0199d67d-81a7-7c55-893a-c1685851f5f7&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479000 +00:00&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479770&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;2025-10-12 03:36:13.479 UTC 773465&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;uuid_extract_timestamp&lt;/code&gt;関数がミリ秒までの精度しか返していませんが、仕様上はミリ秒精度があれば良いとされていることと、外部で生成されたUUIDv7でも対応できるようにこのようになっています。&lt;/p&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>PostgreSQL 18 をソースコードからビルドしたい</title>
		<link href="https://www.jyuch.dev/posts/2025/10-11-build-postgresql-18/"/>
		<updated>2025-10-11T00:00:00Z</updated>
		<id>https://www.jyuch.dev/posts/2025/10-11-build-postgresql-18/</id>
		<content type="html">&lt;h2&gt;はじめに&lt;/h2&gt;
&lt;p&gt;PostgreSQL 18 がリリースされたので、ソースコードからビルドする手順を確認してみました。&lt;/p&gt;
&lt;p&gt;まぁ、基本的には公式リファレンスと過去の自分のブログをなぞっているだけです。&lt;/p&gt;
&lt;p&gt;LLVMのサポートと、lz4・zstd圧縮のサポートを有効にしてビルドします。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.postgresql.org/docs/18/installation.html&quot;&gt;Chapter 17. Installation from Source Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jyuch.dev/posts/2021/build-pg-on-linux/&quot;&gt;PostgreSQLを野良ビルドしてローカルインストールしたい&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;環境&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;ソフトウェア&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;バージョン&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;OS&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Ubuntu 24.04.3 LTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;PostgreSQL&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;18.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;また、ソースコードは&lt;code&gt;$HOME/src&lt;/code&gt;に展開するものとし、バイナリは&lt;code&gt;$HOME/.local/pg18.0&lt;/code&gt;にインストールするものとします。&lt;/p&gt;
&lt;h2&gt;コンパイラ・ライブラリのインストール&lt;/h2&gt;
&lt;p&gt;ビルドするだけなら以下のパッケージを入れるだけでOKです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo apt install &#92;
  build-essential &#92;
  flex &#92;
  bison &#92;
  libreadline-dev &#92;
  zlib1g-dev &#92;
  liblz4-dev &#92;
  libzstd-dev &#92;
  llvm-20 &#92;
  clang-20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ドキュメントを含めたフルビルドが必要なら以下のパッケージも必要になります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo apt install &#92;
  docbook-xml &#92;
  docbook-xsl &#92;
  libxml2-utils &#92;
  xsltproc &#92;
  fop
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ソースコードのダウンロード・展開&lt;/h2&gt;
&lt;p&gt;以下のコマンドよりソースコードをダウロード・展開します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;cd $HOME/src
curl -OL https://ftp.postgresql.org/pub/source/v18.0/postgresql-18.0.tar.gz
tar zxvf postgresql-18.0.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ビルド・インストール&lt;/h2&gt;
&lt;p&gt;以下のコマンドより、&lt;code&gt;configure&lt;/code&gt;を流します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;cd postgresql-18.0
mkdir build_temp &amp;amp;&amp;amp; cd build_temp
$HOME/src/postgresql-18.0/configure &#92;
  --prefix=$HOME/.local/pg18.0 &#92;
  --with-icu &#92;
  --with-lz4 &#92;
  --with-zstd &#92;
  --with-llvm &#92;
  LLVM_CONFIG=&#39;/usr/bin/llvm-config-20&#39; &#92;
  CLANG=&#39;/usr/bin/clang-20&#39; &#92;
  CC=&#39;/usr/bin/clang-20&#39; &#92;
  CXX=&#39;/usr/bin/clang-20&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;バイナリ系をビルドするだけなら以下のコマンドを実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;make world-bin
make install-world-bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ドキュメントを含めてすべてビルドするなら以下のコマンドを実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;make world
make install-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;おわり&lt;/p&gt;</content>
	</entry>
</feed>
