この記事は、Bill One開発Unit ブログリレー2025の第18弾です。

技術本部 Bill One Engineering Unitの向井です。
2025年11月、Bill Oneは「AI自動照合」という機能をリリース*1しました。この機能は、これまで人手で行っていた請求書の内容と納品や検収の照合、発注データとの総額・明細単位での照合を自動化するもので、業務の大幅な効率化を実現します。今回は、この機能を支える請求書明細データ基盤の技術背景、その中でもデータストアとして リレーショナルデータベース(AlloyDB)を選択した理由について解説します。
請求書明細データ化の概要
AI自動照合を実現するためには、請求書の明細表から正確にデータを抽出し、構造化する必要があります。グループ会社であるILU(株式会社言語理解研究所)との共同開発により、請求書明細のデータ化技術を実装しました。これにより、Bill One上で高精度に構造化された請求書明細データを扱えるようになりました。

請求書明細データ基盤のアーキテクチャ
請求書明細のデータ化結果はILUで保持しています。この構造をBill Oneでそのまま扱うのであれば、請求書明細データ基盤をBill Oneに構築せず、必要な都度ILUからデータを取得することも選択肢として考えられます。しかし、このデータ構造はAI自動照合や今後実装される連携先の機能での利用を考えたときに、Bill Oneとして扱いやすい構造である必要があります。そこで、拡張性やスケーラビリティ観点での扱いやすさを考え、Bill One側でデータを保持し、請求書明細データ基盤として構築することが望ましいと判断しました。
請求書明細データ基盤は主に以下のコンポーネントと連携します。
- 請求書明細データ化エンジン: ILUの技術を利用した高精度な請求書明細データ化を実行する環境
- AI自動照合エンジン: 発注情報と請求書明細データ基盤の持つ情報を解析することで、対応関係を自動抽出する環境
請求書明細データ基盤を構築することで、ILUが持つ請求書明細データ化エンジンはデータ化に専念してもらい、請求書明細データ基盤はスケーラビリティ高く、各コンポーネントに対して柔軟なデータフォーマットで連携できます。

データストアの選定
請求書明細データ基盤を構築するに当たってはデータストアが一番のポイントでした。選定に当たって、データストアにおける要件を次の通り整理しました。
保持しているデータ構造は頻繁に変更が入る可能性が高い
請求書明細のデータ構造はBill Oneで扱いやすい形式を独自で定義しています。この構造は今後も請求書明細データ化エンジンの精度や機能が拡張されると変化していく可能性が高いです。 請求書明細データは表構造となっており、この構造は安定している一方で、データ化時に付与したさまざまなメタデータは一定の拡張性が求められます。
保持する請求書あたりの請求書明細データのサイズが大きくなるケースがある
請求書明細データのサイズは請求書の記載内容に依存します。請求書データのサイズには制限を設けていますが、明細の数が多い場合はデータ化した結果のサイズも大きくなることが想定されます。
請求書明細データの内容を使った複雑な検索は考慮しない
請求書明細データは基本的には他のコンポーネントに対して請求書単位で連携する想定なので、請求書明細に対する複雑な検索は現時点では要件に入りません。
加えて、データのアクセス特性では下記の前提をおいています。
請求書明細データの保存は非同期で実行する
データ化された請求書明細データの加工や保存は非同期で実行するため、保存時に高いパフォーマンス要件は要求されません。
請求書明細データ基盤から各コンポーネントへのデータ連携はオンデマンドに実行される
AI自動照合などの機能が利用するタイミングで請求書明細データを連携します。 そのため、アクセス数のスパイクや、利用機能が増えることによるアクセス数の増加が想定されます。
Bill Oneではデータストアにリレーショナルデータベースを採用するケースが多いのですが、表構造を正規化するとレコード数が膨大になることが懸念されます。リレーショナルデータベース以外のスキーマレスなデータストアを使うべきか悩みました。そこで、Bill Oneでは主にGoogle Cloudを採用していることも踏まえて、データストアの候補として次の選択肢を比較検討しました。
- Google Cloud Storage
- Google Cloud Datastore
- リレーショナルデータベース(Cloud SQLやAlloyDB)
Google Cloud Storage
Google Cloud Storageはサイズの大きなデータを扱いやすく、単一のファイルを読み込むだけなら大きな問題はなさそうでした。
しかし、請求書明細のデータの構造変更が発生した際に、データマイグレーションをし続けない限りデータ構造の断片化が進みます。データマイグレーションをせずにアプリケーションで差分を吸収するという方法も考えられますが、過去のデータ構造に対する実装をすべて維持する必要があり、保守性の低下が懸念されます。また、請求書明細に対する検索も視野に入っており、Google Cloud Storage上のオブジェクトに対するファイル内容の検索が難しいことも今後の懸念として考えられます。
Google Cloud Datastore
Google Cloud Datastoreはリレーショナルデータベースのようなデータ量の増大によるパフォーマンス低下が発生しにくく、今後大量の請求書の明細データ化を扱っていくうえでは相性がよいです。また、Google Cloud Storageのようなオブジェクトストレージと比べて柔軟にデータを扱えます。
しかし、請求書明細データのサイズがネックになります。Google Cloud DatastoreのEntityは最大で約 1MiB*2であり、この制限を超過する場合は分割するなどの対応が必要になります。分割するにあたっては、請求書明細の行や列単位とすることを検討しましたが、データ全体として整合性を保つことが難しいことをリスクと捉えました。加えて、Google Cloud Storageと同様にデータマイグレーションの課題も考えられます。
リレーショナルデータベース(Cloud SQLやAlloyDB)
請求書明細データ基盤では明細データの管理に加えて、データ化プロセス(データ化の進捗状況や契約情報などの管理)の制御も担います。こういった整合性を厳密に扱う処理ではリレーショナルデータベースとの相性がよいです。加えて、データ構造に変更が発生した場合にテーブル構造をマイグレーションすることでデータをマイグレーションしやすいこともメリットです。
しかし表構造を正規化する場合、レコード数が多くなるためINSERTのコストが大きくなることや、データアクセス時のコストが大きくなることが懸念として挙げられます。加えて、データの増加に伴ってクエリのパフォーマンスが劣化するおそれもあります。
INSERTコストに関しては請求書明細のデータ化処理は非同期で実行しているため、大きな問題ではないとしました。とはいえ、トランザクションが長くなってしまうことは好ましくないですが、リレーショナルデータベースとしてPostgreSQLを使うのであればCOPYコマンドにより効率化が可能であることから対処可能と判断しています。
クエリのパフォーマンスに関しては、検証を通じてインデックス効率の高い設計や読み出し単位の限定で、一定のパフォーマンスを維持できることを確認しました。
検討結果
各サービスを比較して簡単にまとめると、次のようになります。
| メリット | デメリット | |
|---|---|---|
| Google Cloud Storage | 大きなデータを扱いやすい。 | データ構造の変更を既存データに対して適用する際に手間がかかる。 |
| Google Cloud Datastore | 動的なスキーマに対して柔軟にデータを保存できる。スケーラビリティが高い。 | ドキュメントあたりの最大サイズに制限がある。データ構造の変更を既存データに対して適用する際に手間がかかる。 |
| リレーショナルデータベース | トランザクションを使った整合性が取りやすい。データ構造を変更したときに、データマイグレーションをしやすい。 | データが大きくなったときにパフォーマンスが劣化する懸念がある。 |
中長期的に考えるとNoSQLであるGoogle Cloud Datastoreが有利ではあるものの、機能の立ち上げ初期は変更頻度が高いことが想定されるためリレーショナルデータベースを採用しました。しかしながら、中長期的に耐えられない仕組みというわけではなく、検証を通じてインデックスの最適化や部分的な非正規化を組み合わせることによりパフォーマンスの維持は一定可能であると判断しています。また、リレーショナルデータベースのマネージドサービスとしてAlloyDBを採用することで高機能なリードレプリカ(Read Pool)を使えます。リードレプリカをスケールアウトさせることで、クエリの実行数に対してスケーラビリティを持たせることもできると考えています。
リレーショナルデータベースを使うときの正規化の方針
明細データを正規化するにあたっては、どこまで正規化するかというのが課題になりました。いくつか案を考えましたが、最終的には表構造は正規化し、それ以外の構造は部分的な正規化に留める判断をしました。
例えば次のような請求書明細データを扱う場合を考えます。
| 商品名 | 金額 |
|---|---|
| 鉛筆 | 100円 |
| 消しゴム | 200円 |
この請求書明細データを正規化する場合は、次のテーブルをつくります。
表テーブル
表ID 請求書ID table1 invoice1 列テーブル
列ID 表ID(表テーブル) 列名 column1 table1 商品名 column2 table1 金額 行テーブル
行ID 表ID(表テーブル) row1 table1 row2 table1 セルテーブル
セルID 列ID(列テーブル) 行ID(行テーブル) セルの値 cell1 column1 row1 鉛筆 cell2 column2 row1 100円 cell3 column1 row2 消しゴム cell4 column2 row2 200円
一方で、行単位で非正規化する場合は下記のような JSONB カラム(データ列)を使ったテーブル構造になるイメージです。(列単位の場合は軸を変えただけの構造になります)
表テーブル
表ID 請求書ID table1 invoice1 行テーブル
行ID 表ID(表テーブル) 行の値 row1 table1 [ { “商品名”: “鉛筆” }, { “金額”: “100円” } ]row2 table1 [ { “商品名”: “消しゴム” }, { “金額”: “200円” } ]
このような非正規化をすると、レコード数が少なくなるためパフォーマンスで優位な可能性があります。しかし、JSONとして保持しているため、セル単位のメタデータ付与をテーブル同士の関連で表現することが難しくなります。
結果として表構造は非正規化せずに、メタデータのあらゆる単位に対応しやすい構造とすることを優先しました。表構造が正規化されていることで、セルや行などに対するメタデータを適切なスコープに対して関連付けることができます。メタデータの中には構造上1:Nとなっているものの、データ特性上ひとまとまりとしても変化に対するリスクが少ないものもあり、そういったデータ構造は部分的に非正規化することでレコード数が増えすぎないようにしています。
まとめ
一般的には、データ構造が多様なために定まりにくいケースではNoSQLなどのスキーマレスなデータベースが有利です。一方で、定義した構造がプロダクトの成長に伴って変化していくケースでは、マイグレーションが容易なリレーショナルデータベースのほうが有利と考えることもできます。今回は、データの整合性を維持し続けることを重視した結果としてAlloyDBを採用しました。
長期的に考えるとパフォーマンスが問題になる可能性もあります。データストアの移行には大きなコストがかかりますが、その時点では機能が安定してスキーマの変更頻度も減り、NoSQLやSpannerなどのNewSQLでも扱いやすい状態になっているはずです。スキーマが安定していれば、コストをかけて移行する判断を取りやすくなります。そのタイミングで改めて移行を検討すれば十分と考えました。
機能の立ち上げ時にどれだけのスケーラビリティが求められるか不透明で、かつ機能やデータの変更が多い状況においては、あえてリレーショナルデータベースを選択することで安定した開発を実現できることもあるかと思います。
Sansan技術本部ではカジュアル面談を実施しています
Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。