こんにちは。
「Today I Learned(今日学んだこと)」を記録するTIL、5月は社内プロジェクトのワインDB開発を中心に、様々な技術的な発見や課題解決がありました。フロントエンドの枠を超えて、データベース設計からインフラ運用まで幅広く触れた月でした。なお、4月に発注したAIマシンは納品が6月になったため、その話題は来月お届けする予定です。以下、ピックアップした内容を紹介します。
2025年5月のピックアップ
ワインDBプロジェクト – 商品データベースの構築 (05/01〜05/09)
弊社のSWAILIFE WINE SHOP(スワイライフワインショップ)の商品データをデータベース化し、他のサービスと連携しやすくするプロジェクトを進めています。5月は基礎となるデータ取り込みから検索機能の実装まで、一通りの開発を行いました。
1. 商品データ取り込み編 (05/01)
まずは既存のワインショップサイトから商品データを取り込む仕組みを構築しました。
サイトマップ(sitemap.xml)をスプレッドシートで読み込んで表形式に変換し、CSVで出力。このURLリストを元に、スクレイピングツールのFirecrawlを使って商品ページの情報を取得しました。取得したデータはDuckDBに保存し、数千件の商品データの登録が完了しました。
2. 検索編 (05/02)
商品データの取り込みが完了したので、次は検索機能の実装です。
DuckDBの全文検索拡張機能を活用し、日本語検索に対応しました。商品名と検索ワードに対して適切な前処理を行うため、WebAssemblyで動作するKuromoji実装のLinderaを採用。正規化、小文字化、分かち書き、カナ化といった処理を実装しました。正直なところ、日本語検索の正しいお作法については試行錯誤の連続でした。
検索APIはHonoフレームワークで構築しました。当初はCloudflare Workersでの運用を検討していましたが、データベースの容量とWebAssemblyの制限により断念。将来的にはベクトル検索も試してみたいと考えています。
3. 商品データ更新編 (05/07)
運用を考えると、商品データの更新機能は必須です。
初期はCSVでページ一覧を管理していましたが、より効率的な運用のためにサイトマップの自動読み込みと差分更新機能を実装しました。サイトマップ内のlastmod(最終更新日)を記録しておき、更新のあったページのみを再スクレイピングする仕組みです。これにより、日々の商品更新に対応できるようになりました。
4. フロント編とDuckDBの所感 (05/09)
検索APIの動作確認用にフロントエンドも用意しました。一旦ここまでで基本的な機能は完成です。

DuckDBを使ってみた感想としては、基本的な使い勝手はかなり良好です。DuckDB UIを使えばクエリの試し書きも簡単にできます。ただし、数千件程度のデータ量では、DuckDBの高速性を体感することはまだできていません。
ワインDBは「溜め込み → 加工 → 展開 → 分析」という流れにぴったりな使い方ができそうです。例えば、CI環境でデータベース更新を定期実行し、更新・加工後に自動でデプロイする仕組みも構築できそうです。
今後の目標は、商品詳細のベクトル検索(あいまい検索や類似商品検索)の実装と、スクレイピングで取得したテキストデータの構造化です。現状は商品情報がただのテキストになっているので、より検索しやすい形に整理したいと考えています。
仮想マシンのCPU割り当てとCPU Pinning (05/14)
Proxmox VEでは、物理コアを複数の仮想マシンで同時に使用できます。例えば、物理4コアのマシンで仮想4コア割り当ての仮想マシンを複数立ち上げることが可能です。実際の処理はスケジューラーによって適切に割り当てられますが、当然パフォーマンスは落ちます。
CPU Pinningという機能を使えば、特定の仮想マシンのコアと物理コアを1対1で対応させることができます。これによりパフォーマンスが向上する場合があるようです。(実際の設定はしていませんが)
TypeScriptでswitch文を使わずに、条件分岐を漏れなく書く (05/15)
役に立つ記事を見つけたので共有します。
参考記事: https://zenn.dev/bmth/articles/do-not-use-switch-case
文字列ユニオン型の変数ですべてのパターンに対して処理をしたい場合、switch文の代わりにオブジェクトを使う手法です。ユニオン型をキーに、対応する関数を値にしたオブジェクトを用意して呼び出すようにします。
この手法の最大のメリットは、switch文では条件を省略できてしまいますが、オブジェクトの場合は省略できないため、実装漏れが防げることです。TypeScriptの型システムを活用した、より安全なコーディングができます。
RAGでドメイン固有な表現のギャップを埋める (05/23)
最近、SWAILIFE WINE SHOP(スワイライフワインショップ)にAIソムリエ機能が追加されました。この機能の課題と解決方法についてです。
AIソムリエでは、商品情報のテイスティング表現はワインの世界では一般的なものですが、汎用的な埋め込みモデルではこうした専門用語での検索が困難でした。例えば、「さっぱりしたワイン」という問い合わせに対して、「酸味しっかり」「切れ味抜群」「グレープフルーツ」「ステンレスタンク」「火打石のニュアンス」といった専門用語で表現された商品を見つける必要があります。
検討した解決方法は2つ:
1. 埋め込みモデルのファインチューニング
- クエリ(質問)とコーパス(回答)、スコア(正解度)のデータセットを用意
- 質問に対して回答がどのくらい正しいかを学習させる
- データセット準備のコストが高い
2. クエリ変換、類語辞書
- RAG検索時のクエリをテイスティング用語に変換
- 事前にいくつかの例を入れておく
- 文字数制限があり、1回の検索で表現できる範囲が狭い
現在のAIソムリエは外部サービスを利用しており、独自の埋め込みモデルを使うことができないため、クエリ変換で簡易的に対応しています。将来的には1から独自開発して、埋め込みモデルのファインチューニングをしたいと考えています。
サーバー上で削除されたファイルが残り続けた問題 (05/26)
社内用のセルフホストアプリケーションの運用中に発生した、やや特殊な問題について共有します。
DockerでアプリケーションをホストしているLinux環境で、ストレージがいっぱいになりました。調査すると/var/lib/docker/overlay2/.../merged
の容量が異常に大きくなっていました。コンテナやイメージの容量はそこまで使用していないはずなのに…。
原因は、削除済みファイルをNode.jsプロセスが掴んだままになっていて、容量が解放されていなかったことでした。バックアップ用のzip圧縮時に生成される一時ファイルが残っていたのです。lsof | grep deleted
コマンドで確認すると、大量のzipファイルとnodeプロセスが見つかりました。
今回はコンテナの再起動で解決しましたが、根本的な原因は不明です(Docker + Node.js + jszipの組み合わせの何か)。Linuxではプロセスを止めないと削除してもファイルの容量が解放されないケースがあるようです。有効な対策は定期的なコンテナの再起動しかないのかもしれません。
Postmanで郵便番号・デジタルアドレスAPIを試す (05/28)
日本郵便の新しいAPIをPostmanで試す際に遭遇した問題と、その解決方法を共有します。
API利用トークンの取得APIでは、リクエストボディのContent-typeが`application/json`である必要があります。しかし、Postmanの認可機能で”OAuth 2.0″を選択すると、Content-typeが自動的に`application/x-www-form-urlencoded`になってしまい、APIがエラーを返してしまいます。
この問題を回避するために、トークン取得は通常のPOSTリクエストとして実行し、Post-responseスクリプトでトークンを保存する方法を採用しました:
const response = pm.response.json();
pm.collectionVariables.set("token", response.token);
Postmanを使ってAPIをテストする際は、こういった細かい仕様の違いに注意が必要ですね。
おわり
5月は社内プロジェクトのワインDB開発を中心に、データベース技術からインフラ運用まで幅広く経験できた月でした。6月にはAIマシンも無事納品され、現在稼働中です。ワインDBもさらなる改良を進めています。次回のTILでは、AIマシンの活用事例も含めて紹介しますので、お楽しみに。