コネヒト開発者ブログ

コネヒト開発者ブログ

【iOS】GoogleSignIn v7.0.0へのアップデート対応方法

こんにちは!2023年1月にiOSエンジニアとしてjoinしました、yoshitakaです!

今後iOS関連の内容で定期的に発信していきたいと思いますので、よろしくお願いいたします!

ママリのアプリではログイン連携でGoogleSignInを使っておりますが、先日GoogleSignInのメジャーアップデート対応を行ったので、内容を簡単にまとめたいと思います。

Google Sign-Inとは

Google Sign-In とは、Googleが提供している認証系ツールで、スマホアプリやWebアプリの認証をGoogleアカウントでできるようにするものです。

developers.google.com

Google Sign-Inを使ってやっていること

ママリユーザーが自分のGoogleアカウントを連携させることで、ママリ内でGoogleアカウントでのログインを可能にしています。

内部の処理としてはユーザーがGoogleログインに成功後その認証情報をサーバで確認、連携済みの場合にママリへのログインを許可しています。

アップデートで変わること

v7.0.0リリースノートより以下3点の変更がされました

  • すべての構成を Info.plist で指定できるように
  • Swift Concurrency のサポート
  • API の改善

アップデートで対応したこと

  • clientIDを Info.plist で指定
  • API の改善による修正

の2点を対応しました。

GoogleログインからidTokenをサーバへ送る際の実装部分(対応前)はこのようになっておりました。

let config = GIDConfiguration(clientID: clientID)
GIDSignIn.sharedInstance.signIn(with: config, presenting: self) {
    [weak self] user, error in
    if error == nil, let idToken = user?.authentication.idToken {
        self?.loginViewModel.loginWithGoogleId(idToken: idToken)
    }
}

Info.plist の修正

まずは、clientIDの指定をInfo.plistに移します。

ドキュメントを参考にGIDClientIDの値を追加しました。

ソースコードを修正する場合はこちらを追加

<key>GIDClientID</key>
<string>${GOOGLE_SIGNIN_ID}</string>

今回はRelease,Develop, Localで値を変えたいため

Build SettingsのAdd User-Defined Settingより環境ごとに値を指定して使います

ログイン部分の修正

次にログインの実装部分を修正します。

Info.plistに追加したことでGIDSignIn.sharedInstance.signInclientIDを渡す必要がなくなりました。

また、ログインが成功したかどうかを表す新しい GIDSignInResult が追加されました。

以前user?.authentication.idTokenから取得していたidToken

signInResult?.user.idToken?.tokenStringとすることで同じ値が取得できました。

GIDSignIn.sharedInstance.signIn(withPresenting: self) { 
    [weak self] signInResult, error in
    if error == nil, let idToken = signInResult?.user.idToken?.tokenString {
        self?.loginViewModel.loginWithGoogleId(idToken: idToken)
    }
}

まとめ

APIの改善により他にも変わっている部分がいくつかありました。

この記事では修正が必要になった部分のみ記載しておりますが、公式ドキュメントに詳しくありましたので対応される際は確認してみてください!

developers.google.com

developers.google.com

ママリ iOSアプリのモジュール分割

こんにちは!コネヒトでiOSエンジニアをやっていますyanamuraです。

ママリのiOSアプリでモジュール分割を行いましたのでその内容について記載しました。

なぜモジュール分割

目的としては大きく2つありました。

1つ目はApp Extensionsをつくるのを楽にしたいためです。

以前ママリでApp Extensionsを使ったプロトタイプで作ろうとした際、App Extensionsからママリアプリの一部のコードを再利用しようとしたところ、依存関係の問題で芋づる式にたくさんのコードを取り込まなければならなくなりとても大変な思いをしました。App Extensionsで共通利用しそうな部分はモジュール化して分割しておくべきだったなという経験からモジュール分割しようという思いに至りました。

2つ目はSwiftUIのプレビューを速くしたいためです。

ママリのiOSアプリではSwiftUIを導入していましたが、Previewがめちゃめちゃ遅いというか場合によってはタイムアウトしてしまい使いものにならないといった問題がありました。モジュール分割することでこの問題を少しでも解消したいという狙いが当初ありました。しかし、MacをApple siliconのものにリプレースして、Rosetta2ではなくApple silicon上でXcodeが動くようにこちらで対応した結果劇的に改善されPreview問題は解決しましたので、こちらの目的は薄くなりました。

モジュール分割の方針

ママリのiOSアプリの構成は、CocoaPodsを使ってパッケージを管理しています。

SwiftPackageManagerを使ってマルチモジュールやパッケージを管理したいところですが、ステップを刻んで少しずつ進めていこうと思い、まず第一歩としてパッケージ管理はCocoaPodsのままで、モジュール分割はEmbedded Frameworkを用いる構成にしました。

モジュールの分け方

モジュールの分け方は、縦(機能ごと)に分けたり横(レイヤーごと)に分けるなどといろいろなやり方がありますが、ママリではアーキテクチャはMVVMになっておりView,ViewModelとModelで2分割しました。

この分け方であれば、目的が満たせる最低限の分割で、最初にはじめるにはよいのではと考えたためです。

モジュール分割する際に起こった問題

いきなりEmbedded Framework化すると変更量が半端ない

ママリの場合1万行くらいの規模をEmbedded Frameworkに切り出すことになったのですが、ビルドが通るようになるまでにかなりの修正が必要で、一旦はじめてしまうと終わるまで結構な時間を要することになります。モジュール分割のために普段の開発を完全に止めるわけではなく、その間もコードの編集が行われるため、同期が大変になります。

これを避けるために、一旦プロジェクト内のディレクトリ構成だけ分割し、Embedde Framework化する前に修正が必要なところ(依存関係の修正やpublic化など)を修正しました。これによりEmbedded Frameworkに実際に分割した際に必要な変更を減らし、スムーズにEmbedded Framework化できました。

structのpublic化

Swiftのstructにはmemberwise initializer(https://github.com/apple/swift-evolution/blob/main/proposals/0018-flexible-memberwise-initialization.md) というstructのpropertiesを初期化するinitializerが自動で生成されます。しかしこのinitializerの可視性はinternalとなっているのでstructをpublicにするとmemberwise initializerを使っていた場合はpublicなinitializerを手動で追加しなければなりませんでした。

手動の追加ですが、Xcodeの機能でstruct名を右クリックしてRefactor->Generate Memberwise Initializer使うとコードを生成することができます。

static framework/libraryのimport問題

アプリを動かすと以下のようなメッセージがログに表示され、simulatorで動かすとクラッシュするといった問題が発生しました。

Class PodsDummy_XXX is implemented in both /xxxBuild/Products/Debug-iphonesimulator/YYY.framework/YYY (0xfffffffff) and xxx/yyy.app/zzz). One of the two will be used. Which one is undefined.

これはアプリとモジュール分割したframework(dynamic)で同じstatic libraryに依存していた場合に発生しました。 Embedded Frameworkを作成するとデフォルトではdynamic libraryに設定されています。アプリとdynamic frameworkの両方で同じstatic libraryに依存するとシンボルの重複が発生してしまいます。

手っ取り早い解決方法がモジュール分割したframeworkをdynamic frameworkではなくstatic frameworkに変えてやることです。Build SettingsのMach-O TypeをStatic Libraryに変更することでstatic framework化することができます。ママリでは現状static frameworkにするデメリットはなさそうでしたのでこちらのやり方で対応しました。

もう一つの方法は、アプリ側のOTHER_LDFLAGSから重複するstatic libraryを消してリンクしないようにする方法です。 CocoaPodsの場合はPodsfileで頑張る必要がありそうでした。https://github.com/CocoaPods/CocoaPods/issues/7126

モジュール分割してよかったこと

よかったことは、責務があやふやなところが炙り出されたことです。 これまでもフォルダ構成で責務は別れていたように見えましたが、いざモジュール分割のためにファイルを振り分けてみると、これは本来こっちじゃないよねといったものや、Model側からのViewへの依存といった問題点などが浮き彫りになり、よいリファクタリングの気づきになりました。

また、差分ビルドの時間を時間を比較してみたところ、10秒くらい速くなりました。(一つメソッドを追加して計測)

差分ビルド時間(sec)
before 40
after 30

まとめ

今回モジュール分割をすることにより、目的は達成することができました。今後はSwiftPackageManagerを使った構成に移行していこうと思っています。


コネヒトでの開発に興味を持っていただいた方はカジュアルにお話しましょう〜
TwitterのDMなどでも大丈夫ですのでお気軽にどうぞ
日本中の家族をITの力で笑顔にしたい、iOSエンジニア募集! - コネヒト株式会社のモバイルエンジニアの採用 - Wantedly

Amazon RDS Blue/Green Deployments利用時に躓いたエラーやハマりポイントなどをまとめてみた

新年あけましておめでとうございます。

初詣への移動中、気がついたら年を越していたインフラエンジニア @sasashuuu です。

2023年1本目の記事をお届けしたいと思います。

本日は、Amazon RDS、Amazon Auoraでリリースされた新しい機能である Amazon RDS Blue/Green Deployments 利用時に躓いたエラーやハマったポイントがいくつかあったので、それらをまとめた内容を取り上げました。本機能を利用する方々の一助になれば幸いです。

はじめに

本記事は2022年12月時点で検証していた際の内容となります。記事公開の時点では当時と状況が変わっている可能性がありますので、最新情報等はAWS公式サイト(Using Amazon RDS Blue/Green Deployments for database updates 等)を参照してください。

Amazon RDS Blue/Green Deploymentsとは

Amazon RDS、Amazon AuroraにおいてBlue/Green デプロイが行える機能です。細かい仕様については触れませんが、ざっくりと機能利用時のワークフローを説明すると、下記のような流れです。

※Auroraの場合の例

  • 対象のAurora(Blue環境)から新しいリソース(Green環境)がクローンで複製される
  • Blue/Green環境間でレプリケーションが開始される
  • クラスターの識別子やエンドポイントの変更なしに、Green環境のリソースをスイッチオーバーで稼働中のBlue環境リソースと入れ替える

https://docs.aws.amazon.com/images/AmazonRDS/latest/AuroraUserGuide/images/blue-green-deployment-aurora.png ※画像は公式ドキュメント Overview of Amazon RDS Blue/Green Deployments for Aurora より転載

このように本番環境からステージング環境を作成し、レプリケーションにより同期、さらにスイッチオーバーでエンドポイントの変更などなしに新規Auroraリソースの本番環境への昇格までもおこなってくれるという便利な機能です。

詳細については 公式 を参考にしていただければと思います。

躓いたエラーやハマりポイント

前提

まず前提として、公式ドキュメント Limitations for blue/green deployments に書かれているように、以下の条件下ではそもそも本機能自体が利用できません。

  • Amazon RDS Proxy
  • Cross-Region read replicas
  • Aurora Serverless v1 DB clusters
  • DB clusters that are part of an Aurora global database
  • AWS CloudFormation

具体的な内容

そしてここからは、実際に本機能を使ってみて躓いたエラーやハマりポイントについてまとめます。ざっくり以下のような内容です。

  1. パラメータグループにおけるバイナリログ出力の有効化が必要(binlog_format ⇒ MIXED)
  2. パラメータグループ変更時の適用(リブート)漏れがあるとGreen環境を作成できない
  3. クラスタに紐づけているサブネットグループにおいて3az以上の指定が必要
  4. Green環境作成前のBlue環境に対する何らかの書き込み操作が必要

ひとつずつ解説していきます。

1. パラメータグループにおけるバイナリログの出力の有効化が必要(binlog_format ⇒ MIXED)

本機能はバイナリログを使用したレプリケーションを伴うので、バイナリログ出力を有効にしておく必要があります。クラスター用のパラメータグループの設定値に binlog_format があるので、その設定値を MIXEDにしておく必要があります。これを行わなければ下記のようなエラーが発生します。

Blue Green Deployments requires cluster parameter group has binlog enabled.

ちなみにこちらは公式ドキュメントの Preparing an Aurora DB cluster for a blue/green deployment で解説されていました。

Before you create a blue/green deployment for an Aurora MySQL DB cluster, the DB cluster must be associated with a custom DB cluster parameter group with binary logging turned on. For example, set the binlog_format  parameter to ROW.

ただ、上記の説明では ROW を例に出していますが、中には ROW だとうまくいかなかった事例などもあるそう(Aurora の Blue/Green デプロイで少し遊んでみた(Aurora MySQL v1 → v3 Blue/Green 失敗編))で、また、公式ブログ(新機能 – Amazon Aurora と Amazon RDS でのフルマネージド型 Blue/Green Deployments)でも MIXED を指定するように促していることから、MIXED を指定する方が良さそうです。

2. パラメータグループ変更時の適用(リブート)漏れがあるとGreen環境を作成できない

Blue環境側でクラスターパラメータグループを変更したのち(適用タイプがstaticのものなど)、本機能を使用してGreen環境を作成しようとすると、下記のエラーが出ることがあります。変更したパラメータグループが適用されていないために起こるエラーです。これは本機能利用以前の話(詳細はAWS公式ドキュメント パラメータグループを使用する などを参照)にはなりますが、失念するとGreen環境が作成できないため、対象クラスター内のインスタンスのリブートを忘れないように行う注意が必要です。

Blue Green Deployments requires writer instance to be in-sync with cluster parameter group.

3. クラスタに紐づけているサブネットグループにおいて3az以上の指定が必要

公式ドキュメント等での言及については見当たらずだったのですが、どうやらクラスターにアタッチしているサブネットグループには3つ以上のAZを指定しないといけないという仕様がある模様です。 Amazon RDSのBlue/Green Deploymentsを実験して気づいたハマりポイントまとめAmazon AuroraでBlue/Greenデプロイを検証する。 のブログの記事で取り上げられていたおかげで問題に対処することができました。

4. Green環境作成前のBlue環境に対する何らかの書き込み操作が必要

Green環境の作成前に、Blue環境で何かしらの書き込み処理を行わなければ、下記のエラーが発生してしまいます。

Correct the replication errors and then switch over.
Read Replica Replication Error - IOError: 1236, reason: Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'

これは一般的にはレプリケーション先(Green側)が参照しようとするバイナリログがレプリケーション元(Blue側)に存在しないことで起きてしまうエラーであり、Amazon RDS Blue/Green DeploymentsにおいてAWS側の内部動作上の問題として認知されているそうです。(※サポートへ確認済み)

対策として、バイナリログの出力を有効にしたのち、Green環境作成前にBlue側のデータベースに対して何かしらの書き込み処理を行う必要があるとのことです。(任意のテーブルに任意のレコードINSERT等)

その他のエラーやハマりポイントについて

本記事で紹介した事例以外に他にもいくつか取り上げられているものがあるみたいです。下記のブログなどが参考になるので、併せて参考にしていただければと思います。

おわりに

Amazon RDS Blue/Green Deploymentsは、リリースされて日が浅い新機能ですので、オープンにはなっていない細かい部分の仕様などによる不具合が見られますが、これから改善されていく見込みかと思われます。弊社では本機能を使用し、Aurora MySQL 5.6 (Amazon Aurora MySQL Version 1)のEOL対応(詳細は Amazon Aurora MySQL 互換エディションバージョン 1 のサポート終了に向けて準備する などを参照)などに活用していければと思っている期待の機能です。皆様も機会があればぜひ利用してみてください。

カーディナリティが低いINDEXのお掃除をしました

こんにちは!サーバーサイドエンジニアをやっている @otukutun です。

最近slow query起因のレイテンシアラートが発生していてRDB(MySQL)が不安定になってしまう事象が発生し、それはカーディナリティが低いINDEXが使われたことが原因で発生していました。今回は、その対応としてカーディナリティが低いINDEXのお掃除を行ったのでそれについて書きたいと思います。

なぜカーディナリティが低いINDEXを削除したのか?

一般的にカーディナリティが低いINDEXの弊害などはいろんな方が言われていますが、

などがあると思っております。詳しくはSQLアンチパターン「12章 インデックスショットガン」をみてみてください。

今回はカーディナリティが低いINDEXを削除した理由ですが、

  • カーディナリティの統計情報がおかしくなっていた。flag的なフィールドで20ほどの値しかないはずだが、数値はその数百倍の7000ほどになっていた
  • そのINDEXがまれに使われて、実態としては検索パフォーマンスをあげないのでslow queryになっていた
  • その結果、MySQLのパフォーマンスが悪化しサービスが不安定になることが稀にあった

などがあります。対処方法ですが、OPTIMIZE TABLEを実行しカーディナリティを更新することも考えましたが、今回はDROP INDEXすることにしました。OPTIMIZE TABLEとDROP INDEXは5.6.17以降はIn Placeで実行されるため実行時の挙動は同じですが、Tableの再構築が行われるかの違いがあります。今回は元々が効果がないINDEXであることはSQLとアプリケーションコードから把握できていたこと、Tableの再構築は必要ないことからDROP INDEXすることにしました。

やったこと

まずは一旦、一次対応としてインデックスヒントを与える対応をしました。その後、DROP INDEXの準備を進めていきました。

前述したようにMySQL5.6.17以降(InnoDB)ではDROP INDEXはオンラインDDL対応でIn Placeで実行されるため、テーブルロックはされません(公式ページより)。ただ、レプリカでのbinlogの反映の影響が遅延することを考慮してアクセスが少ない早朝に作業することにしました。今回はslow queryの原因になっているINDEXと他2つのカーディナリティがとても低いものを合わせた3つのINDEXをDROP対応しました。

その際,以下手順で順次DROP INDEX対応を行いました。

  1. DROP INDEXの実行
  2. AWS上のアラートの確認
  3. 念の為 SHOW SLAVE STATUS でSeconds_Behind_Masterの値を確認

最終的に何も問題なく(replica lagも発生しませんでした)、1時間ほどで作業を終えることができました。

おわりに

slow queryの原因が解消され、無事平穏な生活が戻ってきました。(ただし、INSERTクエリの改善は前後比較だと明らかな改善は見られませんでした)

今回はslow queryを発生させているINDEXのDROP対応をしました。他にも効果的でないINDEXが行われている箇所が見つかったので削除していけたらと思います。INDEXはRDBを扱う上で便利な機能ですが、無駄なINDEXを張ってしまうことでの弊害もあります。用法要領を守って正しくお使いくださいませ。

チームを異動して成果を出すため取り組んだこと

この記事はコネヒトアドベントカレンダー2022の21日目の記事です。

こんにちは!コネヒト歴1年3ヶ月のWebエンジニアの古市(@takfjp)です。今年になってから入社して初めてチームを移る経験をしたのですが、その時に「はじめの一歩」として自分から取り組んだことを書き残したいと思います

入社して初めての異動

今年の7月に新CMSを制作するチームから、ママリユーザー向けのキャンペーンサービスを開発するチームに異動しました。

チームが変わるとKPIや触るリポジトリが変わるのはもちろんなのですが、スクラムイベントの内容や、カンバンの運用方法なども変わってきます。

また、求められるタスクへのコミットメント内容も大きく異なる部分が発生します。そういった状況の中で、新しいチームのメンバーとの信頼関係の構築や、チームが扱うタスクや問題にどうキャッチアップしていったかを振り返りたいと思います。

すぐに取り組めるIssueに取り掛かる

現在のチームに異動して初めて取り組んだのがGAS(Google Apos Script)の改修でした。異動したタイミングで新たにGASを作成する必要があり、以前のチームではフロントエンド開発でTypeScript(JavaScript)を書く時間が多かったため「このIssueなら自分でもすぐに携われそう」と思い、最初のプランニングの時間で手を挙げてすぐ取り掛かりました。

個人的な肌感覚ですが、新しいリポジトリのIssueにすぐ取り掛かろうとすると、ディレクトリの構成や技術スタックが異なるためキャッチアップだけで時間がかかってしまい、元々所属しているメンバーに比べてどうしてもIssueの解決に時間がかかってしまいます。その点、GASの作成であればサービスのビジネスモデルへの理解が途中の段階でも、自分が得意とするJSの知識をすぐ活かしてコミットできるという自負がありました。

自分の見通し通り、GASの作成タスクは異動後の初めてのスプリントで完成させることができ、メンバーからポジティブなフィードバックを多くもらうことができました。

わからないことをメモにして公開する

異動後のチームのスクラムイベントで、リファインメントに営業 / マーケ / PdM / エンジニアと、異なる職種のメンバーが一体となって参加しています。その中でKPIや売上目標の数値などの共有が行われるのですが、あまり耳にしたことがないマーケティング用語や、Google系マーケティングツールの略称が多く飛び交うため最初は内容を理解するのに苦心しました。

そこで、現時点で「自分はここがわからない」「調べて理解した」というのを開示したいと思い、メモ代わりにNotion*1を用いてチーム内でよく使われる用語集を個人的に作成しました。

リファインメントや数値説明があるタイミングで毎回開き、「今日は新しい単語が出てきた」「これは前回聞いたけどまだ調べてなかったな」など確認しつつ、追記していくことでビジネスモデルへの理解度を深めるのに役立ちました。また、作成した用語集を全体に公開することで、より詳しいメンバーからのフォローが入ることもあり、前のめりな姿勢を示すことができ取り組んでよかったなと思えました。

モブプロで一緒にタスクを進める

チームを異動したタイミングで、前回自分が携わったタスクを他の現チームのメンバーにお願いする場面がありました。普段はあまり使わないリポジトリでの作業だったのですが、まだ自分の記憶が鮮明だったため、担当するメンバーとまずペアプロ形式でセットアップなどから始めていきました。Slackのハドル機能が充実し、画面共有やミーティング上のスレッドをフロー情報として残すこともできようになったため、今でもペアプロやリアルタイムのコードレビューで活用しています。

モブプロに関しては自発的に始めたというより、所属メンバーが新メンバーのひとりである私に対して、今後のタスクを円滑に進められるよう機会を設けてくれました。最初はPHPの各Controllerの役割やModelの説明、バッチの動かし方の解説から始まり、エンジニア全員で順を追いながら、改修箇所にコードを追加していきつつタスクを完遂させる方式で行いました。

この時の体験がよかったため、最近はチーム内でのフロントエンドの開発が比率的にも多くなってきたこともあり、自分から知識をお裾分けできるよう他のメンバーを巻き込んでモブプロでReactを書いています。

新しいチームでの信頼関係の築き方

そういえば、ここまで書いてみて「信頼関係をどう築いたか」ということにあまり触れてきませんでした。その理由として、元々コネヒトでは時間がある社員が有志でZoom上に集まって行う朝会や、新メンバーとの親睦を深めるためのシャッフルランチ、コネヒトのビジョン*2を少し広い視点で考える「コネヒトワークショップ」など、社内メンバーが活発に交流し合うイベントが不定期的に実施されています。

そのおかげで、チームを移った時点で「全く話したことがない人」がいない状態でした。そのため、自分自身も「他のメンバーのことをある程度知っている」という状態から出発し、元々の所属メンバーからも名前と顔をあらかじめ知ってもらえていることから、上記のような取り組みを行うことでさらに信頼度・親密度が共に増したのかなと個人的に考えています。

新しいチームに参加してすぐやると良いこと

私が行った3つの取り組みとその効果を1段階抽象化すると、

  • 自分が得意・出来ると思うことが見つかったら進んで取り組む
    • 適度なサイズですぐ着手でき、他のメンバーが気にしていることを率先してやれるとベター
  • 自分がわからないこと・調べてわかったことを開示する
    • 最初はわからないことが多く尻込みしたり、パフォーマンスが低下して自分のモチベーションを保つのが難しい場面もあるが、それを逆手に取り成長していく姿勢を示す
  • 自分が持っている知識・スキルをチームに還元する
    • 自己開示がなによりも大事。 自分が得意なことでソロプレーに走るより、全体に還元することでチームそのものが前進できる

こういった内容になるかと思います。新しいチーム・現場での自分の動き方に悩んでいる方は是非実践してみてください。

来年に向けて

来年は逆に、自分が受け入れる側になる場面を想定して、以下のことを事前に用意していきたいと思います。

  • リポジトリや開発環境構築のドキュメントを更新
    • 古くなっている情報を頼ると時間が吸われるので、クイックスタートができるようにする
  • Good First Issueの整備
    • 日々の開発の中で「やりたいけど時間がない」という理由で敬遠しているIssueの中で、重要度が相対的に低く、すぐ取り掛かってもらえそうなものをお願いすることでチーム内での成功体験を積んでもらう
  • 通常タスクはまずモブプロから始める
    • チーム内での会話量を増やしながら孤立せず効率的にタスクを進められる場を設け、信頼関係をハイペースで構築していく

2023年はどんな新しい出会いが待っているのかワクワクしながらこの記事を終えたいと思います。読んでいただきありがとうございました!

*1:コネヒトでのNotion活用についてはこちらの記事をご覧ください。 https://tech.connehito.com/entry/2022/12/07/182847

*2:コネヒトのビジョンについてはこちらのコーポレートサイトをご覧ください.

私たちについて | コネヒト株式会社

脱RosettaしてiOSアプリのビルド時間を短縮

この記事はコネヒトアドベントカレンダー2022の19日目の記事です。

こんにちは!コネヒトでiOSエンジニアをやっていますyanamuraです。

Apple Siliconを搭載したMacをつかって開発していますが、ママリのiOSアプリはXcodeではRosetta2上でしかビルドが通らずCPU性能を生かしきれていませんでした。

なぜRosetta2上でしかビルドが通らなかったのか

原因はいくつかのライブラリでソースコードではなくバイナリで提供されているものがあり、それらがsimulator 向けarm64のバイナリを含んでいなかったためです。

Apple SiliconはiPhoneのCPUと同じARM系チップの64bit CPUなので、fat binaryにarm64のバイナリが含まれていれば普通にsimulatorでも動くだろうと思っていたのですが、なんとそうはいきません。

なぜなら、arm64向けのbinaryでも、simulatorとdevice用では若干違い(LC_BUILD_VERSION、LC_VERSION_MIN_IPHONEOSの有無)があるためdevice用のarm64のバイナリをsimulatorで利用することはできません。また、これらの両方のarm64バイナリを含んだfat binaryが生成できないため、XCFrameworkにしてかつApple Silicon Macでビルド1してバイナリを生成する必要があるのです。

この辺の話の詳細が知りたい場合はこちらの記事を読んでみてください。 https://bogo.wtf/arm64-to-sim.html

ママリiOSアプリでやったこと

基本的な手順としては以下になります

  1. XcodeをApple Silicon上で起動(Open using Rosettaのチェックをはずす)してビルド
  2. Ld: XXX, building for iOS simulator, but linking in object file built for iOS, file YYY for architecture arm64 のエラーがでたframeworkをXCFramework版に変更する

2の対応で一部ライブラリがXCFrameworkに対応していないという問題が発生しました。

具体的には、2022年12月現在GoogleMapsSDKがXCFrameworkに対応していませんでした。

GoogleMapsSDKのXCFramework対応

正式版ではXCFrameworkにまだ対応していませんが、ベータ版が公開されていたのでこれを開発用にだけ利用することにしました。

ママリではCocoaPodsを利用しているので、CocoaPodsでのやり方を紹介します。

本番用では以下のようにReleaseビルドのときだけGoogleMapsSDKの正式版を使うようにし

pod 'GoogleMaps', '~> 7.2.0’, :configurations => ['Release']

開発用ではベータ版をダウンロードしたものをprivateリポジトリに作成してつかっています。

pod 'GoogleMaps-dev',  :configurations => ['Debug']

改善結果

RosettaとApple Siliconでビルドした時間を比べてみました。

結果としては約140secだったのが約90secとかなりの改善効果を得ることができました。

ビルド時間(sec)
Rosetta 140
Apple Silicon 90

ビルド時間が早くなるとビルド中にTwitterを見る時間は減りますが生産性があがるので脱Rosettaはおすすめです!


コネヒトでの開発に興味を持っていただいた方はカジュアルにお話しましょう〜
TwitterのDMなどでも大丈夫ですのでお気軽にどうぞ
日本中の家族をITの力で笑顔にしたい、iOSエンジニア募集! - コネヒト株式会社のモバイルエンジニアの採用 - Wantedly


  1. Apple Silicon Macでビルドする必要が本当にあるかは未確認です。クロスコンパイルできると思うので不要そうな気はしています。

日本語サジェスト機能の実装にあたり試行錯誤した話

こんにちは。エンジニアの永井(shnagai)です。

今回は、現在進めている検索システム内製化プロジェクトの中で、日本語サジェストを実装するために試行錯誤した話を書こうと思います。

内容は、ざっくり下記の構成になっています。

  • 日本語サジェストの難しいところ
  • よりよい日本語サジェストのために試行錯誤した点

この記事はコネヒトアドベントカレンダー2022の16日目の記事です。

日本語サジェストの難しいところ

日本語のサジェストをOpenSearch(Elasticsearch)で実装するにあたりいくつか難しい点がありました。

この話の前提として、コネヒトではインデックス作成に使えるデータとして下記を持っています。

  • 検索ログデータ
  • 検索対象のテキストデータ

以後の話はこのデータを使いサジェストを実装する際に、どのような点を意識したかについて話していきます。

①よみがなの考慮

サジェストの実装を考えるに当たり、「赤ちゃん」「妊娠」のような1語として意味をなす入力に対するサジェストは比較的簡単に出来ました。(elastic社のElasticsearchで日本語のサジェストの機能を実装するを参考に実装することでそれなりのベースラインとなるサジェスト機能を作ることが出来ました。良記事に感謝)

ですがそう一筋縄ではいかず、よみがなの部分で苦戦しました。参考実装では、ローマ字入力を考慮し、カナをすべてローマ字にするchar_filterを噛ます実装になっていました。

ママリはスマホアプリなのでローマ字の考慮は不要なのですが、「あかち」「にん」のようなかなの入力途中の語に対して、当然ユーザの検索を補助するためにサジェスト候補を表示する必要があります。

つまり、「赤ちゃん」というワードに対して、「赤ちゃん」「あかちゃん」という2パターンのトークンを持つ必要があります。

後で詳細は述べますが、「あかち」でヒットさせるためにedge_ngramを使いトークンを先頭からn文字という形に分割させることで「赤ちゃん」「あかち」の両方にヒットするような工夫をしています。

また、ユーザの入力途中のワードを分かち書きしてしまうと意図せぬ分割が起こるため、よみがな用のアナライザーを用意する必要があります。(後述)

整理して書くと当たり前だよなって気もするのですが、腑に落ちるまではある程度の時間がかかりました。

②単語の区切り

これはサジェストだけに限った話ではなく日本語の解析全般にいえることなのですが、英語等のスペース区切りで単語が分割される言語と異なり、日本語は単語の区切りが曖昧なためルールベースで単語分割することが難しく、形態素解析が必要になります。

形態素解析することで、単語分割とよみがな振りが出来、トークンとして検索システムで使える形になります。

検索ログ(スペースで単語が区切られた日本語)をデータとして使う場合に、どうトークナイズするのが出したい結果に対してベストなのか色々と試行錯誤しました。

よりよい日本語サジェストのために試行錯誤した点

日本語サジェストの実装にあたっては、先程の紹介した下記のブログが非常に参考になりここで紹介されているマッピング定義をベースに試行錯誤しながらより理想に近づける作業を行いました。

Implementing Japanese autocomplete suggestions in Elasticsearch

ここからは、いくつか理想に近づけるためにチューニングを行った内容を紹介していきます。

これらはまだ本番リリースまではしてないので検証の中で得られた知見の共有という点にご留意ください。

検索ログ活用のために、kuromojiで形態素解析はせずkeywordトークナイザーを使う

既に述べましたが、今回使えるデータとしては下記2つがありました。

  • 検索ログデータ
  • 検索対象のテキストデータ

検索ログの件数をソートの要素に使いたいという理由から今回検索ログデータを利用してサジェストを実装することにしました。

この検索ログデータですが、「つわり△いつから」「保育園△見学」「義実家」のように、単語もしくはスペース区切りのある程度整ったデータが格納されています。

※ちなみに文中の「つわり△いつから」△はスペースを表しています。

また、データパイプラインで全角/半角スペースの正規化とログデータなのでノイズも多いため、しきい値を設けて特定件数以上のワードをデータとして使うことでノイズ除去しています。

最初このデータを、参考実装に則りkuromojiトークナイザを使い分かち書きする手法で検証を進めていました。

そこで、問題にぶち当たります。

これが一番わかりやすい例で、「ほい」という入力が来たらママリだったら「保育園」をサジェストしたいですが、「ほっけ △いつから」がヒットしました。

※チーム内では「ほっけいつから問題」と言われしばらくホットワードになっていました。

この事象、形態素解析に明るい方なら勘づかれるかもしれませんが、search_analyzerに指定したkuromojiが「ほい」を ほ/い にトークン化して検索をかけにいくため「保育園」ではなく「ほっけ△いつから」が候補としてヒットしていました。

中々頭で理解するのが難しいので、自分の中で手書きで分解して何が起きてるのかを理解しようと下記のキャプチャのようなことをいくつか繰り返し理解しました。手で書いて整理すると不思議と理解出来て不思議なものです。

この件でとっかかりを掴み、analyzerの設計方針が固まりました。suggest_analyzer(読みではなく入力ワードに対するサジェスト)では、searchとindexでアナライザーを分けているので、その内容も簡単に解説します。

search_analyzer

search_analyzerの役割はユーザの入力文字列を最低限の正規化だけすることです。入力途中の文字が入ってくることが前提なので、形態素解析はせずkeywordトークナイザーを使ってスペースも1語として扱います。

例えば、「つわり△い」のようなワードが来た時に、そのままの形で検索を行うのが役目。出来るだけそのままの形にするという部分が大事なことに気づいたのが大きな転換点でした。

index_analyzer

index_analyzerの役目は、入力データである検索ログを正規化し、edge_ngramを使って入力途中の文字列にもヒットするようなトークンを転置インデックスとして格納することです。

こちらのトークナイザーも最初はkuromojiに始まり、whitespace、textと試し、結果的には検索ログをそのままの形で格納するのが一番良いという結論になりkeywordトークナイザーを使うことにしました。

具体例がわかりやすいと思うので説明すると、

「つわり△いつから」という入力に対して、それぞれ下記のようにトークン化され転置インデックスとして登録されます。

keywordトークナイザーだと つ/つわ/つわり/つわり△/つわり△い/つわり△いつ/つわり△いつか/つわり△いつから

whitespaceトークナイザーだと つ/つわ/つわり/つわり/い/いつ/いつか/いつから

この場合に、サジェスト観点で検証すると明確な差が出ます。

  • 「つわり」という入力に対してはどちらもヒットする
  • 「つわり△い」という入力に対してはwhitespaceトークナイザーだとヒットしない

サジェストやオートコンプリートと言われる入力補助の機能にとっては、「つわり△い」に対して、「つわり△いつから」「つわり△いつまで」のような補助をしてユーザの検索体験をサポートしていきたいのでkeywordトークナイザーを採用しています。

よみがなで当てるためのアナライザーでは、search_analyzerは同じくkeywordトークナイザーを使い、index_analyzerには、カスタム辞書のくだりでも話したよみがなでのヒットさせる必要があるのでkuromojiトークナイザーを使う形にしました。この当たりの全体像はまた機会があれば書こうと思います。

カスタム辞書にカナを振る

今使っているKuromojiのカスタム辞書は、カナを使う機会がなかったのもありカナ振り作業を後回しにしていました。

ですが、 kuromoji_readingform filterで単語のよみがなを使う必要が出てきたので、カスタム辞書へのカナ振りが必要になりました。

初期のカスタム辞書を作るフェーズでは、よみがなを使わないケースでの導入だったためその時のツケを払う形だったが中々に骨の折れる作業でした。

# before
ママリ公式,ママリ公式,ママリ公式,カスタム名詞
# after
ママリ公式,ママリ公式,ママリコウシキ,カスタム名詞

よみがなを振ったおかげで、「ママリ公式」という単語がこのような形で読みでもヒットするようになりました。つまり「ママリこ」という入力に対して、「ママリ公式」がヒットするようになります。

before: マ/ママ/ママリ/ママリ公/ママリ公式

after: マ/ママ/ママリ/ママリコ/ママリコウ/ママリコウシ/ママリコウシキ

※最終的に、カタカナでトークン化するtoken filterも入れているのでトークンはカタカナに

他にもいくつか試行錯誤した点はあるのですが、それらの紹介はまたの機会に取っておきます。記事にすると意外とあっさりした内容になりますが、1つ1つ動作確認しながらのチューニングになるので時間はかなりかかるしやろうと思えばいくらでもチューニング出来るので、そこがこの分野の面白さだなと実感しています。

プロジェクトとしては、ある程度の性能のサジェストを作ることが出来たのでこれからABテストに向かうところです。良い結果が出ることを願いつつここらで筆を置こうと思います。

この試行錯誤のログが日本語サジェストを実装しようとする方の一助になれば幸いです。

明日は @takapy0210による記事です!お楽しみに。

最後にコネヒトでは一緒に働く仲間を募集しています。

今回のテーマの検索システムだけでなく様々な技術要素でチャレンジ出来る環境があるので、少しでも興味を持っていただけたら気軽にご連絡いただければ幸いです。

www.wantedly.com