every Tech Blog

株式会社エブリーのTech Blogです。

Databricks Mosaic AIによるLLM アプリケーションの評価

この記事は every Tech Blog Advent Calendar 2024 の 16日目の記事です。

はじめに

こんにちは。
株式会社エブリーの開発本部データ&AIチーム(DAI)でデータエンジニアをしている吉田です。

今回は、Databricks Mosaic AIによるLLM アプリケーションの評価についてのお話です。

背景

近年、LLMを利用したアプリケーションが増えており、DELISH KITCHENでもAIによる料理アシスタントとして「デリッシュAI」の提供を開始しました。
そのような状況の中で、サービスにLLMアプリケーションを組み込む際には、アプリケーションの評価がますます重要な課題となっています。

しかし、LLMアプリケーションの品質は、データの質、モデルの性能、プロンプト、retrieverの性能など複数の要素が影響するため、評価は複雑で難しい課題です。
また、アプリケーションの評価に利用するユーザーからフィードバックを収集するためには、レビュー環境の構築やテスト人員の確保、さらに収集したフィードバックの分析といった作業が必要となります。

そこで、Databricks Mosaic AI Agent Evaluationを利用することで、アプリケーション評価にかかる負担を軽減することが可能です。

Databricks Mosaic AI Agent Evaluationとは

Databricksドキュメント、Mosaic AIエージェント評価とは? によると、以下のように説明されています。

Agent Evaluationは、開発者がRAGアプリケーションやチェーンを含む エージェントAIアプリケーションの品質、コスト、およびレイテンシを評価するのに役立ちます。 エージェント評価は、品質の問題を特定し、それらの問題の根本原因を特定するように設計されています。 Agent Evaluation の機能は、 MLOps ライフサイクルの開発フェーズ、ステージングフェーズ、本番運用フェーズ全体で統合されており、すべての評価メトリクスとデータは MLflowランに記録されます。

https://docs.databricks.com/ja/generative-ai/agent-evaluation/index.html

主に以下のような機能が提供されています。

  • Review App作成 : ユーザからフィードバックを効率よく収集できるアプリケーションを簡単に作成できます
  • 評価と原因分析:集めたフィードバックを活用して、評価指標を可視化し、問題の根本原因を明確にします

モデルのデプロイに合わせてReview Appが作成されることで、ユーザからのフィードバックを効率的に収集できる仕組みが提供されます。
また、Databricks Model ServingやVector Search、Unity Catalogと組み合わせることで、LLMアプリケーションの構築、運用、評価を単一のプラットフォームで行うことができます。

LLMアプリケーションの評価

今回は、RAGアプリケーションを例にLLMアプリケーションの評価を行います。
テストにはGenerative AI Cookbook10 minute demo of Mosaic AI Agent Framework & Agent Evaluation上のコードを利用しました。

Unity CatalogにRAGモデルが登録されている前提で、以下の手順で評価を行います。

  1. Serving EndpointにRAGモデルを登録 & Review Appの作成
  2. Review Appを利用して、ユーザからフィードバックを収集
  3. 収集したフィードバックを利用して、評価と根本原因分析

Review Appの作成

Review Appの作成は、以下のコードを実行することで行います。

import mlflow
from databricks import agents

mlflow.set_registry_uri('databricks-uc')

model_name = "{catalog}.{schema}.{model_name}"

# Unity Catalogにモデルを登録
model_register_info = mlflow.register_model(model_uri="{model_uri}", name=model_name)

# Serving Endpointにモデルをデプロイ & Review Appが作成される
deployment_info = agents.deploy(model_name=model_name, model_version=model_register_info.version)

agent.deployを実行することで、Serving Endpointにモデルがデプロイされます。
このとき、Review App用のfeedbackというモデルが同時に作成され、自動的にReview Appが作成されます。

フィードバックの収集

Review Appを活用することで、ユーザからフィードバックを迅速に収集できる環境を整えられます。
これにより、モデルデプロイ後数分から十数分でフィードバックの収集をスタートでき、URL共有するだけでユーザの評価を収集する仕組みが構築されます。
ドメイン知識を持つ社内メンバーやステークホルダーからのフィードバックを収集することで、モデルの品質向上に寄与する重要なインサイトを得ることができます。

Review Appのフィードバック入力

Review Appを利用することでユーザはLLMアプリケーションを利用しつつ、フィードバックを提供できます。
主に以下のようなフィードバックを収集します。

  • 回答に対する正誤
  • フィードバックの理由
  • 期待される回答
  • 参照されたドキュメントへの正誤

収集されたフィードバックは、自動的にDeltaテーブルに記録され後続の分析に活用することが可能です。

評価と根本原因分析

収集したフィードバックを活用し、以下のような評価を行います。

  • 回答は、ユーザの質問に対応しているか
  • 回答は、期待される回答と比較して適切か
  • 回答は、取得されたドキュメントを元にしているか
  • 取得されたドキュメントは適切か

収集されたデータは以下のような形式で記録されています。一部のカラムを抜粋しています。

  • {catalog_name}.{schema_name}.{model_name}_payload_assessment_logs : フィードバックの詳細
    • text_assessment : LLMからの回答に対する評価
      • ratings : 評価
      • suggested_output : 期待される回答
    • retrieval_assessment : 取得されたドキュメントに対する評価
  • {catalog_name}.{schema_name}.{model_name}_payload_request_logs : ユーザのリクエストとアプリケーションの回答
    • request : ユーザのリクエスト
    • response : アプリケーションの回答

これらのデータを利用して、以下のような評価データセットを作成し、mlflow.evaluateを利用して評価を行います。

import mlflow
import pandas as pd

eval_df = pd.DataFrame([
{
    "request_id": "{ID}",
    "request": "{ユーザのリクエスト}",
    "response": "{アプリケーションの回答}",
    "expected_retrieved_context" : "[{期待されるドキュメント}]",
    "expected_response" : "{期待される回答}",
}
])

eval_results = mlflow.evaluate(
    data=eval_df,
    model={評価したいモデル},
    model_type="databricks-agent",
)

mlflow.evaluateを実行することで、モデルの評価を行います。

Evaluation Results

Evaluation Result Detail

モデル出力と期待結果の比較を行うことで、モデルの性能を評価しています。
評価に対する根拠や、推奨される改善点が表示されるため、モデルの品質向上に寄与する重要な情報を得ることができます。
また、モデル間で評価を比較することで、改善の方向性を検討することも可能です。

終わりに

LLMアプリケーションの評価は、サービスの品質向上やユーザ体験の最適化において非常に重要なプロセスです。
Databricks Mosaic AI Agent Evaluationを活用することで、従来は複雑だった評価作業を効率化し、フィードバックの収集から分析まで一貫したプロセスで実施できる強力なフレームワークが提供されます。

iPadOS 18のタブバーのデザイン変更に対応する

この記事は every Tech Blog Advent Calendar 2024 の 15 日目の記事です。

iPadOS 18の新しいタブバー

iPadOS 18では、タブバーのデザインが一新され、これまで画面下部にあったタブバーが画面上部のナビゲーションバー内に移動しています。これによってコンテンツを表示するスペースがより広くなる利点があります。

新デザインはほぼ強制的に適用されるため、タブバーを持つ既存アプリで何らかの対処が必要になる場合があります。トモニテアプリでの対応をご紹介します。

参考

新しいタブバーデザインの適用条件

iPadOS 17上で動作 iPadOS 18上で動作
Xcode 15.xでビルド 旧デザイン 旧デザイン
Xcode 16でビルド 旧デザイン 新デザイン タブバーが画面上部にある

Xcode 16以降でビルドしたアプリをiPadOS 18以降で動作させた場合だけ、新規デザインが適用されます。

Xcode 16への移行スケジュール

2025年4月以降、App Store ConnectにアップロードするアプリはiOS 18、iPadOS 18に対応したSDKでビルドする必要があります。 そのため2025年3月中にXcode 16に移行する必要があり、新しいタブバーデザインへの対応もそれまでに完了しておく必要があります。

https://developer.apple.com/jp/news/?id=utw4yhtp

既存デザインを維持する方法

タブバーの旧デザインを選択する公式の方法はありませんが、 traitOverrides.horizontalSizeClass を利用して強制的にiPhoneと同じ表示にすることで変化を回避できるようです。

SwiftUI

TabView {
    ...
}
.environment(\.horizontalSizeClass, .compact)

UIKit

class MyTabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        traitOverrides.horizontalSizeClass = .compact
    }
}

引用 https://stackoverflow.com/questions/78631030

このコードでは MyTabBarController の配下のViewControllerも影響を受けるため、それらのViewControllerが想定と異なる動作になってしまう可能性があります。また、MacOSでは別の問題があります。

将来のiPadOSでもこの方法が有効な保証はないため推奨できませんが、一時的な回避策としてこの方法の採用を検討しても良いかもしれません。

新しいタブバーデザインに対応

トモニテアプリでは navigationItem.titleView に検索文字列を入力するためのテキストフィールドを置いていました。

Xcode 16 + iPadOS 18では以下のように、 navigationItem.titleView に配置したビューとタブが重なって表示されてしまいます。

トモニテアプリでは今後、iPadOSでは navigationItem.titleView を使わないこととして、ビューを他の箇所に移動することにしました。ここでは navigationItem.rightBarButtonItems にボタンとして配置することにしました。

Xcode 16 + iPadOS 18ではタイトルの文字列が以下のように表示されます。ナビゲーションバーが太くなり、2段目にタイトルが表示されます。

無駄にスペースを消費するため、iPadOSでは今後、重要でない場合はタイトルに表示しないなどの対応をするのが望ましいと思います。トモニテでは、情報が重複する箇所ではタイトルを表示しない対応をしました。

その他

その他、タブバーの旧デザインに依存した箇所を見直す必要があるかもしれません。

  • タブアイコンの画像が表示されないため視認性が悪化する可能性がある
  • タブバーのカスタマイズをしている場合、カスタマイズができなくなる

最後に

Xcode 16 + iPadOS 18のタブバーデザイン変更への対応は、UXが大きく変わり影響範囲が広くなる可能性があります。デザイナー/PdMと連携して十分余裕を持ったスケジュールで対応する必要があると感じました。

この記事が、同様の課題に直面している開発者の方々の参考になれば幸いです。

デリッシュAIのアーキテクチャ

この記事は every Tech Blog Advent Calendar 2024 の 14 日目の記事です。

はじめに

こんにちは。
開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。
直近開発に取り組んでいるデリッシュAIのアーキテクチャについてご紹介します。

DELISH KITCHENでは 「作りたい!が見つかる」をサービスのコンセプトとして、様々な機能を提供してきました。
一方、ユーザーひとりひとりの多様なニーズに合わせたレシピを提案していくには既存機能だけでは難しい部分があります。 そこで、AIによる料理アシスタントとして「デリッシュAI」ベータ版を一部ユーザーから提供し始めています。

アーキテクチャ

AI Server

  1. あらかじめレシピのタイトル、手順、タグ情報などを持ったデータをEmbedding化するバッチを組んでおく
  2. そのレシピEmbedding情報をベクトル検索できるようにMosaic AI Vector Searchを用いたベクトルストアを用意
  3. RAGの仕組みを活用して、ユーザのクエリをベクトル検索し、検索して得られたレシピのコンテキストをプロンプトに入れ込んで、LLMにリクエスト
  4. LLMはOpenAI APIを利用し、Structured Outputs機能を活用して、レシピの提案コメントとレシピIDを返す
  5. これらのコードをmlflow.pyfunc.PythonModelを継承したクラスとして定義し、mlflow.pyfunc.log_modelでモデルを保存(後述する今度の課題>コード管理参照)
  6. Mosaic AI Foundation Model Servingの機能で、保存したモデルをデプロイし、Seving EndpointsのURLを取得

     例)
     https://{サブドメイン}.cloud.databricks.com/serving-endpoints/{モデル}/invocations
    
  7. API GatewayにSeving EndpointsのURLを登録しておく

Delish Server

  1. ユーザからのリクエストを受け取る
  2. 過去の会話履歴に基づいてレシピ提案するため、レシピデータと会話履歴を取得し、レシピのメタデータを持った会話履歴データを作成
  3. API Gatewayにユーザのクエリと会話履歴データをリクエスト
  4. API Gatewayのマッピングテンプレート機能を使って、リクエストのフォーマットを変換し、DatabricksのModel Servingにリクエスト
  5. Model Serving Endpointからのレスポンス(AI検索結果)を受け取り、レシピIDがハルシネーションしていないかなど、レスポンスが適切かをチェック
  6. ユーザにレシピを提案するレスポンスを返す
  7. 同時に会話履歴を踏まえたレシピの提案をするために会話履歴を保存
  8. 同様に蓄積したデータを分析するために、event logを保存

意図

職責による実装の分離

弊社では、データ&AIチームがデータの収集・分析基盤の開発・運用を担当し、事業部のサーバーサイドエンジニアがユーザに提供するためのAPIやバックエンドシステムの開発・運用を担当していることが多いです。
今回のデリッシュAIの開発も同様に、データ&AIチームが機械学習モデルを開発し、事業部のサーバーサイドエンジニアがこれらのモデルをサービスに統合するという役割分担をしました。
これにより、それぞれのチームが普段使い慣れている技術をベースに開発を進めることができ、短期間でデリッシュAIを提供することができています。

API Gatewayの活用

Delish ServerとDatabricksの繋ぎ込み部分は、Amazon API Gateway マッピングテンプレートと Amazon SageMaker を使用して機械学習を搭載した REST API の作成を参考に、API Gatewayを採用しています。これは、以前のPoC実績をベースにしています。

API Gatewayを利用することで、以下の2点の恩恵を受けられると考えています。

1. ログやメトリクスの拡充

ログやメトリクスは、DatabricksのMosaic AI Foundation Model Servingでも確認できるのですが、詳細を見ることができる情報量がAPI GatewayからCloundWatchに送られるログの方が多いです。実際に開発時にも、CloundWatchのログからエラーの原因を特定することができた実績もあります。

2. リクエスト/レスポンスの柔軟なフォーマット変換

「モデルのI/O」と「APIのリクエスト/レスポンス」が必ずしも一致しない場合があります。 また、何らかの理由でフォーマットを変更したい可能性もあります。 そのとき、マッピングテンプレートを変更することで、「モデルのI/O」と「APIのリクエスト/レスポンス」のどちらかを変更したい場合でも、柔軟な対応が可能になります。

今後の課題

コード管理

デリッシュAIのロジックであるRAGの実装部分は以下のようなコード構成になっています。 これまでは、これをDatabricksの1ノートブック内に全てコーディングする管理をしていました。 しかし、これから開発がさらに進むことを想定して、ある程度コードを共通化したいニーズが生まれて来ており、新たなコード管理の整備が必要だと感じています。

import mlflow
from mlflow.models import infer_signature

def generate_answer(query, conversation_history):
    # ここでRAGの仕組みを活用してレシピの提案コメントを生成
    return response

class DelishAI(mlflow.pyfunc.PythonModel):  
    def predict(self, context, input):
        query = str(input['query'][0])
        conversation_history = list(input['conversationHistory'][0])
        return generate_answer(query, conversation_history)

# サンプルデータ
query_sample = "もっとスパイシーなカレーのレシピ教えて"
conversation_history_sample = [...]
input_sample = {
    "query": query_sample,
    "conversationHistory": conversation_history_sample
}
output_sample = generate_answer(query_sample, seed_sample, conversation_history_sample)

# シグネチャの作成(モデルのI/Oを保存時に付与)
signature_sample = infer_signature(input_sample, output_sample)

# Unity Catalogで登録するモデル名
model_name = f"{catalog}.{schema}.{model}"

# モデルの保存
with mlflow.start_run():
    model_info = mlflow.pyfunc.log_model(
        artifact_path="model",
        python_model=DelishAI(),
        signature = signature_sample,
        registered_model_name=model_name,
    )

評価データの収集

Databricksでは、Mosaic AI Agent Frameworkを使用してAIエージェントを作ることができます。
AIエージェントをデプロイすると、Databricks上で簡単に自分たちが作成したモデルをレビューアプリとして利用することができます。
レビューアプリはAIエージェントの品質に関するフィードバックを利用者から集めることができ、社内で公開することで、toC向けの機能だとしても品質向上に役立てることができると考えています。
デリッシュAIも、Slackbotで社内提供させるところから始まって改善を繰り返しており、実際にどんな使われ方をしているかを把握することは、開発する上で重要だと感じています。

ただ、AIエージェントの利用には入出力のスキーマが特定のフォーマットである必要があります。ここでも柔軟にフォーマット変更できるマッピングテンプレートが活きてくると考えています。

まとめ

デリッシュAIのアーキテクチャについてご紹介しました。 今後も、ユーザのニーズに合わせた提案していくために、デリッシュAIの機能を拡充していく予定です。

Android で性別に応じて文法を変更する方法について

この記事は every Tech Blog Advent Calendar 2024 13 日目の記事です。

はじめに

こんにちは、DELISH KITCHEN でクライアントエンジニアを担当している kikuchi です。

普段会話をする際に、話す相手は誰か、言及する対象は人であるか物であるか、性別はどうか、といった様々な情報から微妙にニュアンスを変えて話すことがありますが、
もしアプリでユーザの特性によって文言を出し分ける、というような機能を実装する場合は、条件分岐が複雑化するなど多くの手間がかかってしまいます。
今回はそのユーザの特性の中でもユーザの性別 (文法上の性別) によって、簡単にアプリ上で表示する文言を切り替えることができる Grammatical Inflection API という機能を紹介したいと思います。

Grammatical Inflection API を使用することで、性別による複雑な条件分岐を実装する手間を省くことができます。

文法上の性別とは

一言で「文法上の性別」と記載しても少し分かりづらいと思いますので、具体例を記載したいと思います。

Android Developer サイト で本件について「フランス語でサービスに登録されていることをユーザに知らせるメッセージの例」があるため引用します。

  • Masculine-inflected form: 「Vous êtes abonné à...」 (English: 「You are subscribed to...」)
  • Feminine-inflected form: 「Vous êtes abonnée à...」 (English: 「You are subscribed to...」)
  • Neutral phrasing that avoids inflection: 「Abonnement à...activé」 (English: 「Subscription to ... enabled」)

この様に英語では文法が変わりませんが、フランス語では微妙に文法が変わっている (「abonné」と「abonnée」で差異がある) ことが分かります。
日本語でも「あなたは〇〇に登録しています」といった統一の文法になると思いますが、上記の様に文法上の性別への対応が必要な言語も存在するため、
多言語対応をする場合は言語に合わせて適切な文法を設定することがユーザに対しての適切なアプローチとなります。

Grammatical Inflection API の概要

こちらの API で提供される機能は大きく分けて 2 つあります。

  1. 文法上の性別を選択する
  2. 性別によって文字列のリソースを分ける

まず 1 についてですが、こちらで選択できる性別の修飾子は

  • 女性的 (feminine)
  • 男性的 (masculine)
  • 中性的 (neuter)

の 3 つが存在します。

そして 2 についてですが、Android は以前から言語で文字列のリソースを分けることができますが、そこに更に性別でもリソースを分けることができる様になります。
先程のフランス語を例にすると

  • フランス語、かつ女性的 : res/values-fr-feminine/strings.xml
  • フランス語、かつ男性的 : res/values-fr-masculine/strings.xml
  • フランス語、かつ中性的 : res/values-fr-neuter/strings.xml

といった分け方ができ、例えば 1 の機能で女性的 (feminine) を選択していると、res/values-fr-feminine/strings.xml のリソースから文字列が読み込まれる様になります。

なお、本 API ですが、

  • Android 14 以降の端末のみサポートされる
  • Android Studio Giraffe Canary 7 以降の環境のみ、性別のリソースの修飾子 (values-fr-masculine の masculine の部分) がサポートされる

といった制約があるため、事前に対象の OS を絞る、開発環境を新しくするといった対応が必要となります。

実装方法

本項目では具体的な実装方法について説明したいと思います。

文法上の性別を選択する

文法上の性別を選択する API の実装方法について説明します。

まずは AndroidManifest.xml で API を実施する Activity に宣言を追加します。

<activity
    android:name=".TestActivity"
    android:configChanges="grammaticalGender"  ← こちらを追加する
    android:exported="true">
</activity>

そして次に性別を選択する API を以下の様に実装します。

val gIM = getSystemService(requireContext(), GrammaticalInflectionManager::class.java)
gIM?.setRequestedApplicationGrammaticalGender(Configuration.GRAMMATICAL_GENDER_FEMININE)

GrammaticalInflectionManager のサービスにアクセスし、setRequestedApplicationGrammaticalGender メソッドを呼ぶのみとなります。
こちらで指定できる値は

  • Configuration.GRAMMATICAL_GENDER_FEMININE : 女性的
  • Configuration.GRAMMATICAL_GENDER_MASCULINE : 男性的
  • Configuration.GRAMMATICAL_GENDER_NEUTRAL : 中性的

となります。

本 API ですが、アプリの初回起動時のアンケート、あるいは設定画面などで性別を選択する UI を用意し、ユーザが選択したタイミングで性別を選択する API を実行する、といった使い方ができるかと思います。

なお、選択した性別を取得する API は以下の様に実装します。

val gIM = getSystemService(requireContext(), GrammaticalInflectionManager::class.java)
val grammaticalGender = gIM?.applicationGrammaticalGender

こちらも選択のケースと同様で、GrammaticalInflectionManager サービスにアクセスし applicationGrammaticalGender で値を取得するのみとなります。

性別によって文字列のリソースを分ける

次に性別によって文字列のリソースを分ける方法ですが、こちらは特にコードを書く必要はありません。
概要で説明した通り、性別のリソースファイルを用意するのみとなります。

●res/values-fr/strings.xml (言語のリソースファイル)
<resources>
    <string name="test">test</string>
</resources>

●res/values-fr-feminine/strings.xml (女性的のリソースファイル)
<resources>
    <string name="test">test_feminine</string>
</resources>

●res/values-fr-masculine/strings.xml (男性的のリソースファイル)
<resources>
    <string name="test">test_masculine</string>
</resources>

●res/values-fr-neuter/strings.xml (中性的のリソースファイル)
<resources>
    <string name="test">test_neuter</string>
</resources>

文法上の性別を選択する API で Configuration.GRAMMATICAL_GENDER_FEMININE を設定していた場合、test の識別子のリソースにアクセスすると
女性的のリソースファイルにアクセスするため、「test_feminine」という文字列が取得できる様になります。
なお、文法上の性別を選択する API を実行していない場合は言語のリソースファイルにアクセスするため「test」という文字列が取得できます。

実装としては以上となります。

注意点

言語のリソースファイルには全てのリソースの識別子が網羅されており、性別によって変化させたい文字列のみ性別のリソースファイルに定義する必要があります。
また言語のリソースファイルに定義されていないリソースの識別子を性別のリソースファイルに定義はできません。

具体例を記載します。

●res/values-fr/strings.xml (言語のリソースファイル)
<resources>
    <string name="test">test</string>
</resources>

●res/values-fr-feminine/strings.xml (性別のリソースファイル)
<resources>
    <string name="test">test_feminine</string>
</resources>

●res/values-fr-masculine/strings.xml (性別のリソースファイル)
<resources>
    <string name="test_aaa">test_masculine</string>
</resources>

このようなケースの場合、

  • values-fr-masculine に 「test」が無いが、values-fr に定義されているのでエラーにはならない (values-fr の「test」が読み込まれる)
  • values-fr-masculine に 「test_aaa」が定義されているが、values-fr に定義されていないためエラーになる

となるため、エラーを回避する場合は values-fr にも test_aaa を定義する必要があります。
こちらは大本のリソースファイルと言語のリソースファイル (values/strings.xml と values-fr/strings.xml) の関係と同様のルールとなります。

また、values-fr-neuter が存在しませんが、values-fr が存在しているのでエラーとはなりません。

まとめ

翻訳自体はかなり専門的な知識が必要となりますが、私自身海外のアプリを使用する際は翻訳が雑だと怪しいと感じ、逆に翻訳が行き届いていると丁寧なアプリだと感じることがあるので、
こういった細かい部分を丁寧に対応することがユーザの獲得、継続利用に繋がると考えています。

また日本語の場合でも、性別によって言葉を変えることでよりターゲットを絞った訴求ができることも考えられるため、一度この機能の導入を検討してみてはいかがでしょうか。

iOSプロジェクトからApolloを削除した話 - GraphQLクライアントの自前実装への移行

はじめに

この記事はevery Tech Blog Advent Calendar 2024の12日目の記事です。

DELISH KITCHENのiOSアプリ開発を担当している池田です。今回はiOSプロジェクトでのGraphQLクライアントをApollo iOSから自前実装へ移行した経験についてお話しします。

背景

DELISH KITCHENのAPIの一部でGraphQLを利用しており、開発効率向上のためにApollo iOSを導入していました。これにより、GraphQLの利用をより簡単に行える環境を整えていました。導入時の詳細については以下の記事をご参照ください。

tech.every.tv

GraphQLについて

GraphQLでは、必要な情報だけを取得できたり複数のエンドポイントのリクエストをひとつにまとめたりできる柔軟なデータ取得が特徴です。クライアント側で必要なデータを宣言的に指定できるため、データの過不足なく効率的な通信が可能になります。一方で、新しい技術仕様の習得やクエリ設計のベストプラクティスの理解など、学習コストが比較的高いことが課題として挙げられます。

Apollo iOSを使う利点

Apollo iOSには主に以下の利点があります:

  • GraphQLのスキーマとクエリからSwiftコードを自動生成できることによる開発効率の向上
  • クライアントサイドでのキャッシュ管理の簡略化とパフォーマンスの最適化
  • 型安全性の保証による実行時エラーの防止

DELISH KITCHENでは、特にコードの自動生成による開発効率向上を目的としてApollo iOSを導入していました。

自前実装の経緯

Apollo iOSは、コード自動生成機能により当初の目的であった開発効率化を実現していました。しかし、DELISH KITCHENのAPIの大半はRESTfulで、GraphQLの利用は限定的であったため、実際の効率化の効果は想定を下回っていました。

さらに、今後もGraphQLの利用を積極的に拡大しない方針が決定されたことで、Apollo iOSを維持するコストが相対的に高くなってきました。具体的には以下の課題が浮き彫りになりました:

  • iOSプロジェクトのApollo iOSへの依存関係の管理
  • コード自動生成に必要な非Swiftファイルの維持
  • 自動生成されたファイルによるプロジェクト管理上の複雑さ

これらの状況を踏まえ、Apollo iOSへの依存を解消し、必要最小限の機能に特化したGraphQLクライアントを自前で実装することを決定しました。

GraphQLクライアントの自前実装

GraphQLは本質的にはHTTP POSTリクエストであり、適切なリクエストボディを構築することで実装が可能です。ここでのクエリ文字列自体はApollo iOS使用時と同様にスキーマを元に構築する必要があります。以下に基本的な実装例を示します:

// GraphQLのエンドポイントURL
let url = URL(string: "https://api.example.com/graphql")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

// スキーマを元に構築したGraphQLのクエリGraphQLのクエリ
let query = """
    mutation CreateOrder($productId: ID!, $quantity: Int!, $shippingAddress: AddressInput!) {
      createOrder(productId: $productId, quantity: $quantity, shippingAddress: $shippingAddress) {
        orderId
        totalPrice
        estimatedDeliveryDate
        status
      }
    }
    """

// 変数の定義
let variables: [String: Any] = [
    "productId": "prod_123456",
    "quantity": 2,
    "shippingAddress": [
        "street": "123 Main St",
        "city": "Tokyo",
        "postalCode": "100-0001",
        "country": "Japan"
    ]
]

// リクエストボディの構築
let body: [String: Any] = [
    "query": query,
    "variables": variables
]

// リクエストの実行
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)

do {
    let (data, response) = try await URLSession.shared.data(for: urlRequest)
    
    // レスポンス処理
    if let httpResponse = response as? HTTPURLResponse {
        switch httpResponse.statusCode {
        case 200:
            let json = try JSONSerialization.jsonObject(with: data)
            print("注文が成功しました:", json)
        default:
            print("エラーが発生しました。ステータスコード:", httpResponse.statusCode)
        }
    }
} catch {
    print("リクエストエラー:", error)
}

このように、GraphQLクライアントの基本的な機能は標準的なネットワーキング処理で実装することができます。実際のプロジェクトでは、この基本実装をベースに、既存のRESTful APIクライアントと同じインターフェースで利用できるよう設計し、ネットワーキング層の実装を共通化しました。

おわりに

今回は、Apollo iOSから自前実装への移行について紹介しました。GraphQLの利用範囲や開発方針に応じて、時にはサードパーティライブラリへの依存を見直し、シンプルな実装へ移行することも選択肢のひとつとなり得ることが分かりました。

この記事が、同様の課題に直面している開発者の方々の参考になれば幸いです。