コネヒト開発者ブログ

コネヒト開発者ブログ

2023年のコネヒト開発組織を振り返る

こんにちは。CTOの永井(shnagai)です。

早いもので今年も残すところ数日ですね。

アドベントカレンダー最終日ということで、技術的なトピックやチームでの工夫はこれまでの記事でみんなが思う存分書いてくれたので、今回は2023年のコネヒト開発組織の1年を自分の視点で振り返っていこうと思います。

この記事は、コネヒトAdvent Calendar2023の25日目の記事です。

adventar.org

開発組織のアップデートと特に目立った技術的なトピックという構成で書いてます。

開発組織のリフレーミングと頼れる多くの仲間のジョイン

FY23のスタートに合わせて、開発組織のリフレーミングを行いました。

それぞれのチームの関係を可視化

期初のタイミングでチームトポロジーを参考に、それぞれのチームの構造を改めて可視化しました。

自分はチームトポロジーの、ストリームアラインドチームが出す価値の総量が開発組織のバリューという考えに非常に感銘を受けており、プロダクトの先にいるユーザーファーストの考えをうまく組織に当てはめた考えだなと思っています。

ママリや自治体向け事業等の事業毎にストリームアラインドチームを配置して、事業部と密に連携しながら開発するスタイルをコネヒトでは採用しています。

コンプリケイテッドサブシステムチームは技術的難易度の高い領域を受け持ち、プラットフォームチームでは、ストリームアラインドチームの認知負荷を減らしプラットフォームエンジニアリングでプロダクトの価値最大化に貢献することをミッションにしています。

下記は、期初戦略で全社に説明した資料の一部です。

※現在はメンバー増によりストリームアラインドが増えています。

ピープルマネジメントをEMに集約 〜エンジニアがよりプロダクト・事業に集中出来る体制へ〜

元々コネヒトでは、各開発グループにグループリーダー(以後、GL)を配置し、そのGLがチームのマネジメント4象限(ピープル/プロダクト/プロジェクト/テクノロジー)を全て担う構造にしていました。

だいたい2,3名のチームにそれぞれGLがいる構造だったのですが、今の組織フェーズ的にはエンジニアがよりプロダクトや事業に集中する時間を作るほうが良いと判断して、ピープルマネジメント業務をチーム横断でEM 2名に集約しました。

全てがうまくいっているわけではありませんが、ピープルマネジメントを集約したことでメンバーからはよりプロダクトや事業に集中出来ているというフィードバックをもらっており、一定うまくいっている施策だなと思っています。

制度やロールは常に見直しながらアップデートしていくのが良いと思っており、これからも組織拡大に合わせて柔軟に見直していき、アジリティの高い開発が出来るようにしていきたいと思っています。

きれいなことばかり書きましたが、テックリードという役割を設けるトライもしたのですが、中々イメージの言語化や定義が追いつかずに、役割を担ってくれたメンバーと対話しながら、半年でまた別のロールにしたりと頼れる同僚とともに試行錯誤しながらやっています。

3人に1人を目指して 多くの仲間のジョイン

今年は、7名の新しいエンジニアがコネヒトにジョインしてくれました。

コネヒトでは、テックビジョンにおいて「3人に1人」という戦術を掲げており、会社としてやりたい事業を全て実現し、テクノロジーやエンジニアリングの恩恵を社内も受け、そしてエンジニア以外でもテクノロジーを使いこなせるような世界を目指しています。

今年ジョインしてくれた仲間の職種も多岐に渡るのですが、それぞれが所属するチームや開発組織でバリューを発揮してくれており全体として活気づいています。

社内のエンジニア比率が高まっていく中で、生成AIの技術革新もあいまってテックビジョンに掲げる世界をより実現フェーズに持っていくために新たな動きも走り始めています。

全社向けに生成AIの活用とテクノロジーを利用した業務改善を推進する「Run with Tech 」プロジェクトで、こちらは、また、どこかの機会で紹介出来ればと思っています。

続いて、今年特に印象的に残った技術的なトピックについても紹介していければと思います。

検索システムの内製化完了

今年技術的なトピックで一番大きなトピックといえば検索システムの内製化が完了したことがまず挙げられます。

これまで、SaaSの検索システムを内部のAPIから呼び出す形式で使っていたものを、フルリプレイスする形で0から検索システムを構築しました。

時系列で見る検索システム内製化の軌跡

  • 2021年10月 構想開始
  • 2022年3月 経営承認
  • 2022年4月 開発開始
  • 2022年8月 最初の機能(新着質問順)のリプレイス完了
  • 2023年6月 全ての機能のリプレイス完了

主な狙いは、検索システムを内部で持つことによるユーザー体験の強化とSaaS入れ替えによる費用削減の2点でした。

時系列で書くとすんなり言ったように見えますが、すべての機能において既存とのABテストを行いママリにおける検索体験のユーザー毀損がないことを細かく確認しながらリプレイスを行いました。

新着順、人気順、サジェスト、関連キーワードと多岐に渡る機能のABテストはSaaS提供と同品質をゴールにしている関係で中々にヒリヒリするもので、メインで担当していた同僚達のやり切る力には脱帽です。

技術的な要素を少し解説すると、検索システム内製化には下記技術要素があります。

  • 内部APIから呼ばれる検索API(Go)
  • 検索エンジンにはOpenSearchを採用
  • 技術的難易度が一番高かったデータ同期の仕組みにはAWS GlueとStepFunctionsでニアリアルタイムな基盤を構築   tech.connehito.com

担当したメンバーは全員検索システムは未経験だったので、みんなでペンギン本を読んで輪読会をしたり、壁にぶつかるたびに議論して一歩ずつ前に進んでいきました。私自身もメンバーとして当初コミットしていて、検索システムは奥深くエンジニアとしてまた一つ成長できた良い機会だったと感じています。

tech.connehito.com

もちろん、リプレイスをして終了ではなく、ようやく検索システムをママリの武器に出来るスタートラインに立てたので、昨今のLLM全盛の時勢も鑑みながら、キーワード検索にとらわれずよりユーザーにとってママリの検索がより使いやすいものであり続けられるように技術的な検証や新たなトライも既に始まっています。

tech.connehito.com

「終わらせることを始めよう」を合言葉に最終的に3ヶ月計画を前倒しし、検索システム内製化という成果に対して社内表彰もされたプロジェクトとなりました。

ユーザーへの価値提供という点では、まさにこれから真価を問われることになりますが、テクノロジーでプロダクトを伸ばす先端事例になるような取組みなので今後がより楽しみです。

Let’s Go

コネヒトのテックビジョンでは、「バックエンドのシステムにGoを積極的に導入する」という戦略を掲げていおりその戦術名が「Let's Go」です。

これまでも、前述の検索APIしかり新規のシステムでのGoの採用は何度か行ってきました。それらを通しての、組織としての知見の習得はそこそこ進んできたかと感じています。

このLet’s Goの作戦は、aboyがボールを持って中心に進めてくれているのですが、今年の大きな進歩として、既存のPHPで書かれたコードベースをGoに置き換えるというプロジェクトが走り出しました。

まずは、Goの特徴である並列処理やワーカー実装のしやすさを活かせる非同期のバッチ処理のリプレイスを実施していますが、メインシステムを適材適所でGoに置き換える動きがスタートしているのは開発組織として大きな一歩だなと感じています。

中期戦略では、2024年中にバッチ処理を全てGoに置き換えるというチャレンジングな目標を掲げており、その実現に向けて日々開発を進めています。

また、Let’s Go Talkという社主催のイベントも定期的に実施しており、Go好きがゆるく繋がれるようなコミュニティも作っていきたいと思っているので興味のある方は是非ご参加ください。

コネヒト/Connehito Inc. - connpass

まとめ

2024年も引き続きプロダクトや事業を伸ばしていくことに注力しつつ、テックビジョンに掲げる「Beyond a Tech Company」の実現に向けてCTOというラベルを活かして色々と策を打っていこうと思います。

テックビジョンの存在が羅針盤からより会社の中で身近なものになっていけば、コネヒトとしてユーザーや社会に約束するありたい世界観の実現に近づいていくと自分は信じています。

最後にですが、エンジニアはもちろん、幅広い職種で採用もしていますので興味持たれた方は是非ご応募いただきカジュアルにお話出来るとうれしいです。

hrmos.co

trivyとGithub Actionsを使用しTerraform設定ファイルのセキュリティスキャンを実行する仕組みを作りました

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

コネヒト Advent Calendar 2023って?
コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。
コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、
ママの一歩を支えるアプリ「ママリ」などを 運営しています。

adventar.org

はじめに

コネヒトのプラットフォームグループでインフラ関連を担当している@yosshiです。 今年の7月に入社してから早いもので半年が経ちました。時が経つのは本当に早いですね。

今回のブログでは、セキュリティスキャンツールであるtrivyを使って、自動的にIaC (Infrastructure as Code)スキャンを実行する仕組みを構築した話をしたいと思います。

弊社ではインフラ構成をTerraform利用して管理するようにしており、それをモノレポの構成で運用しています。

インフラリソースの作成・変更・削除をする際には必ず相互レビューを必須としているものの、人的なチェックのみに依存しているためファイルの設定ミスやセキュリティ上の見落としが潜在的なリスクとなっていました。

これらを事前に検知するツールを調べていたところtrivyの存在を知り、今回導入に至りました。

trivyとは

  • コンテナイメージやアプリケーションの依存ライブラリ・OSのパッケージなどを迅速にスキャンし、セキュリティリスクを効率的に検出するセキュリティツールです。
  • 当初はコンテナのセキュリティ問題に焦点を当てたツールとして開発されたようですが、後にTerraformやKubernetesなどの設定ファイルのチェック機能も追加されました。
  • 設定ファイルのチェックでは、設定ミスやセキュリティのベストプラクティスに沿っていない構成などを検知することができます。

参考:https://github.com/aquasecurity/trivy

(参考)スキャン可能な対象 2023/12時点

  • コンテナイメージ
  • ファイルシステム
  • リモートGitリポジトリ
  • 仮想マシンイメージ
  • Kubernetes
  • AWS

(参考)検出可能な内容 2023/12時点

  • OSパッケージとソフトウェア依存関係(SBOM)
  • 既知の脆弱性(CVE)
  • IaCの問題と設定ミス
  • 機密情報と秘密
  • ソフトウェアライセンス

上記の通りtrivyでは、Terraformのコードだけでなくさまざまなセキュリティスキャン行うことができます。

trivy自体の詳しい説明はここでは割愛するので、詳しくは公式サイトや他の方の記事などをご確認いただけると幸いです。

Github Actionsでの実装

では早速ですが実装内容の説明に移りたいと思います。 今回はTerraformコードのスキャンをCIに組み込んでいます。 弊社では CIツールとしてGithub Actionsを利用しているため、今回もこちらを利用します。

スキャン実行は、trivy公式で用意しているGithub Actions用のツール(tricy-action)があるので、こちらをそのまま利用しています。

背景

まず、前提条件となる弊社のディレクトリ構造を説明します。

弊社では、サービスで共通利用するリソース(base_system)と各サービスで利用するリソース(product_system)とでディレクトリを分けており、 product_sytem以下にはサービスごと関連するリソースが紐づいています。以下のようなイメージです。

.
├── base_system
│   ├── common
│   │   └── terraform
│   ├── privilege
│   │   └── terraform
│   .
│   .
└── product_system
    ├── (サービス1)
    │   └── terraform
    ├── (サービス2)
    │   └── terraform
    .
    .
    .

実装方針と内容

実装したGithub Actionsのコードは以下の通りです。

主に以下のことをやっています。

  1. シェルスクリプト(sync-updated-dirs-to-work-dir.sh)の実行
    • mainブランチとプルリクエスト中のブランチのコードの差分を検知
    • 差分となっているディレクトリを作業用のディレクトリに同期
  2. 作業ディレクトリに対してスキャン実行
  3. 検出されたスキャン結果をPRのコメントに残す。

Github Actionsのコードは以下の通りです。

name: trivy-scan

on:
  pull_request:
    types: [opened, reopened, synchronize]

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  trivy_scan:
    name: Run Trivy Scan
    runs-on: ubuntu-latest
    steps:
      - name: Clone repo
        uses: actions/checkout@v4

      - name: fetch origin/main for getting diff
        run: git fetch --depth 1 origin $GITHUB_BASE_REF

      - name: Sync Updated Dir to Work Dir
        env:
          GITHUB_TOKEN: ${{ secrets.github_token }}
        run: |
          bash utils/scripts/sync-updated-dirs-to-work-dir.sh

      - name: Trivy Scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          severity: 'HIGH,CRITICAL'
          scan-ref: scan_work_dir
          output: trivy-scan-result.txt

      - name: Format Trivy Scan Result
        run: |
          if [ -s trivy-scan-result.txt ]; then
            # ファイルに内容がある場合
            echo -e "## 脆弱性スキャン結果\n<details><summary>詳細</summary>\n\n\`\`\`\n$(cat trivy-scan-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
          else
            # ファイルが空の場合
            echo -e "## 脆弱性スキャン結果\n脆弱性が検知されませんでした。" > formatted-trivy-result.md
          fi

      - name: Comment PR with Trivy scan results
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          recreate: true
          GITHUB_TOKEN: ${{ secrets.github_token }}
          path: formatted-trivy-result.md

詳しく見ていきます。

まずは変更差分となったディレクトリを調べるため、Github Actionsの中でシェルスクリプトを実行しています。

ここでは一時的なディレクトリを用意し、プルリクエスト上で変更が発生したディレクトリの内容を作業用のディレクトリに同期するという作業を行なっています。

Github Actionsの関連部分は以下です。

- name: Sync Updated Dir to Work Dir
   run: |
     bash utils/scripts/sync-updated-dirs-to-work-dir.sh

sync-updated-dirs-to-work-dir.shはrsyncをwrapしたもので、前述の通りプルリクエスト上で変更が発生したterraformディレクトリを、スキャン実行する一時的な作業用ディレクトリに同期する処理をしています。1

細かな実装は内部事情に特化したものとなっているため割愛しますが、例えば、base_system/common/terraform, base_system/privilege/terraform, product_system/(サービス1)/terraformのディレクトリで変更が発生している場合、このスクリプトを実行することで作業用ディレクトリ(scan_work_dir)が作成され、以下のようなディレクトリ構造となります。

.
├── base_system
│   ├── common
│   │   └── terraform
│   └── privilege
│   │   └── terraform
│   .
│   .
├── product_system
│   ├── (サービス1)
│   │   └── terraform
│   ├── (サービス2)
│   │   └── terraform
│   .
│   .
└── scan_work_dir (ここのディレクトリにtrivyによるスキャンを実行する)
    ├── base_system
    │   ├── common
    │   │   └── terraform
    │   └── privilege
    │   │   └── terraform
    └── product_sytem
        └── (サービス1)
            └── terraform

次に同期してきた作業用ディレクトリに対してスキャンを実行します。
ここでは前述の通り、公式が用意しているtricy-actionを利用しています。

- name: Trivy Scan
  uses: aquasecurity/trivy-action@master
  with:
   scan-type: 'config'
   severity: 'HIGH,CRITICAL'
   scan-ref: scan_work_dir
   output: trivy-scan-result.txt

オプションの内容は以下の通りです。

  • scan-type: 'config'
    • configはTerraformなどの設定ファイルをスキャンする際に使用する。
  • scan-ref: scan_work_dir
    • 作業用ディレクトリ scan_work_dir を指定している。
  • output: trivy-scan-result.txt
    • スキャンした結果をテキストファイルtrivy-scan-result.txtに出力する。
    • このテキストファイルは次のアクションでフォーマットを整形して出力するために使用している。
  • serverity
    • 脆弱性の深刻度で'CLITICAL'と'HIGH'を指定しています。
    • ここのレベルは、CVSS2によって定量化された脆弱性の深刻度をもとに設定されています。

他のオプションなどについてはtricy-actionのページをご確認ください。

次に、前のアクションで出力したテキストファイルを見やすく整形した上で、marocchino/sticky-pull-request-commentを使用しプルリクエストのコメント欄に出力しています。

プルリクエスト更新時には、既存の脆弱性に関するコメントを削除した上で新規コメントを残して欲しかったので、その点でmarocchino/sticky-pull-request-comment(recreate: trueのオプション指定)を利用することで楽に実装することができました。

# スキャンした結果を整える
- name: Format Trivy Scan Result
  run: |
    if [ -s trivy-scan-result.txt ]; then
      # ファイルに内容がある場合
      echo -e "## 脆弱性スキャン結果\n<details><summary>詳細</summary>\n\n\`\`\`\n$(cat trivy-scan-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
    else
      # ファイルが空の場合
      echo -e "## 脆弱性スキャン結果\n脆弱性が検知されませんでした。" > formatted-trivy-result.md
    fi

- name: Comment PR with Trivy scan results
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    recreate: true
    GITHUB_TOKEN: ${{ secrets.github_token }}
    path: formatted-trivy-result.md

スキャン結果

  • 脆弱性が検知されなかった場合

  • 脆弱性が検知された場合

「詳細」部分を開くと以下のような形で表示されています。

今回のケースだとCritical0件、High61件の脆弱性が検知されていることがわかります。

base_system/privilege/terraform/xxx.tf (terraform)
=====================================================================
Tests: 75 (SUCCESSES: 14, FAILURES: 61, EXCEPTIONS: 0)
Failures: 61 (HIGH: 61, CRITICAL: 0)

HIGH: IAM policy document uses sensitive action 'autoscaling:Describe*' on wildcarded resource '*'
════════════════════════════════════════
You should use the principle of least privilege when defining your IAM policies. This means you should specify each exact permission required without using wildcards, as this could cause the granting of access to certain undesired actions, resources and principals.

See https://avd.aquasec.com/misconfig/avd-aws-0057
────────────────────────────────────────
 base_system/privilege/terraform/xxx.tf:376
   via base_system/privilege/terraform/xxx.tf:376 (aws_iam_policy.xxx.xxx)
    via base_system/privilege/terraform/xxx.tf:375-438 (aws_iam_policy.xxx.xxx)
     via base_system/privilege/terraform/xxx.tf:373-439 (aws_iam_policy.xxx)
────────────────────────────────────────
 373   resource "aws_iam_policy" "xxx" {
 ...
 376 [     Version = "2012-10-17",
 ...
 439   }
────────────────────────────────────────
・
・
・(以下省略)

上記の例では、重要度「HIGH」の脆弱性が検知されています。

IAMポリシーは最小特権で付与するべきなので、ワイルドカード使うのはリスクがありますよ、という指摘のようです。

補足

検出された脆弱性の中で、この内容は指摘する対象から除外したい、というケースもあると思います。

その場合は.trivyignoreというファイルをトップディレクトリに置くことで勝手に参照して検出対象から除外してくれます。

以下のような形で記載します。

.trivyignore

AVD-AWS-0057
AVD-AWS-XXXX

(参考)

以下のようにtrivyignoresのオプションを利用することで、トップディレクトリに配置するだけでなく別のディレクトリに配置しているファイルを参照させたり、.trivyignore以外の別のファイル名を指定することもできるようです。

- name: Trivy Scan
  uses: aquasecurity/trivy-action@master
  with:
   scan-type: 'config'
   severity: 'HIGH,CRITICAL'
   scan-ref: trivy_temp_dir
   output: trivy-scan-result.txt
   trivyignores: test-trivyignore

まとめ

今回はtrivyとGitHub Actionsを活用し、Terraformでのセキュリティ上のリスクを効果的に検知する仕組みを構築しました。

今回検知されたものについては、優先度の高いものから順次改善していきたいと思います。

また、冒頭に説明した通り、trivyでは他にも様々なものを検知してくれる機能があるので、 Terraformの設定だけでなく色々な場面での活用を検討していきたいと思います。


  1. 変更が発生したディレクトリの抽出には git diff origin/main --name-only の結果をパースしています。
  2. CVSS( Common Vulnerability Scoring System ) = 共通脆弱性評価システム

ママリiOSアプリで今年取り組んだ3つの改善について

「コネヒト Advent Calendar 2023」の20日目のブログです!

adventar.org

iOSエンジニアのyoshitakaです。 コネヒトに入社してそろそろ1年が経ちます。

コネヒトに入社してからはママリiOSアプリの開発を担当しています。

今回はママリiOSアプリで行った3つの改善をまとめました。

  1. フォルダ構成の変更
  2. 開発中アプリの配布フローの改善
  3. 画面遷移の改善

それぞれどんな課題があったか、どんな改善をしたかを紹介します。

フォルダ構成の変更

課題に感じていたこと

既存のフォルダ構成は機能追加をする際に対象となるコードが見つけにくい状況でした。

前提

  • ママリiOSアプリのアーキテクチャーはMVVMを採用している
  • View・ViewModelModelでモジュール分割している
    • 今回改善したのはView・ViewModel側のフォルダ構成

    ※ モジュール分割の取り組みについてはこちらの記事を見てください。 tech.connehito.com

どんな改善をしたか

変更前のフォルダ構成のイメージがこちらです。

App
├── ViewController
│   ├── 機能AのViewController
│   ├── 機能AのViewController
│   ├── 機能BのViewController
│   └── ...
├── View
│   ├── 機能AのView
│   ├── 機能AのView
│   ├── 機能BのView
│   ├── 機能AB共通のView
│   └── ...
└── ViewModel
    ├── 機能AのViewModel
    ├── 機能AのViewModel
    ├── 機能BのViewModel
    └── ...

アーキテクチャーがわかりやすい構成になっていると思います。

しかし、機能追加をする際に対象となるコードにたどり着くのに時間がかかっていました。

改善案は大きく二つありました。

  1. 現在のフォルダ構成の配下に機能ごとのフォルダを作る
    • メリット: 変更が少なく済む
    • デメリット: 機能ごとのコードが分散する
  2. App配下に機能ごとのフォルダ作り、機能フォルダ内でさらにViewとViewModelのフォルダを作る
    • メリット: 機能ごとのコードがまとまる
    • デメリット: フォルダ構成の変更が大きい

機能改善する際ViewとViewModelをセットで修正することがよくあるので、2のApp配下で機能ごとにフォルダ分けをすることにしました。

改善したフォルダ構成イメージがこちらです。

App
├── Features
│   ├── 機能A
│   │   ├── View:ViewController・DataSource・View置き場
│   │   └── ViewModel
│   ├── 機能B
│   ├── 機能C
│   └── ...
└── Common:共通化されたView・ViewModel置き場
    ├── View
    └── ViewModel

複数の機能で使われているものはCommonフォルダにまとめるようにしました。

フォルダ構成は好みもあるかと思いますが、整理したことで不要なファイルもお掃除でき、良い改善だったと思います。

開発中アプリの配布フローの改善

課題に感じていたこと

特定の開発中ブランチから開発中アプリのを配布する際に、CIでは実行できず、常にローカルでfastlaneを実行する必要がありました。

前提

  • CI/CDはBitriseとfastlaneを使っている
  • 開発中アプリはFirebase Distributionを使い社内に配布している
  • Firebase Distributionへの配布方法は以下の2つ
    • PullRequestをメインブランチにマージすると自動で配布される
    • ローカルでfastlaneを実行して配布する

どんな改善をしたか

CIでfastlaneのアプリ配布のワークフローを実行できるようにすることで、ローカルでのfastlane実行を不要にしました。

トリガーはGitHub Actionsを使い、ブランチを指定して実行するとBitriseのアプリ配布のワークフローが実行されるようにしました。

改善方法は別の記事にまとめております。

tech.connehito.com

この改善により、ローカル環境に依存しないアプリ配布ができるようになり、開発スピードのUPに繋がっています。

画面遷移の改善

課題に感じていたこと

アプリ全体の各ViewConrollerに画面生成と遷移のロジックが実装されていて、コードの見通しが悪く、ViewControllerの肥大化要因にもなっていました。

前提

  • 画面遷移のアーキテクチャーは採用していない
  • 各ViewControllerの任意の箇所でViewControllerの生成と遷移処理を行っている

どんな改善をしたか

画面生成と遷移処理のロジックをなるべく1箇所にまとめる仕組みを作りました。

具体的には画面生成と遷移処理部分で以下のような変更をしました。

let viewController = QuestionContainerViewController.instantiate(
    id: question.id
)
navigationController?.pushViewController(viewController, animated: true)

pushScreen(screen: .questionContainer(
    questionId: question.id
)

Screenというenumを作り、Screenの値を元にViewControllerを生成するようにしました。

enum Screen {
    case questionContainer(
        questionId: Int
    )
    ...
}

extension UIViewController {
    func makeViewController(from screen: Screen) -> UIViewController {
        switch screen {
        case .questionContainer(let questionId):
            return QuestionContainerViewController.instantiate(
                id: questionId
            )
        ...
        }
    }

    func pushScreen(screen: Screen) {
        let viewController = makeViewController(from: screen)

        self.navigationController?.pushViewController(viewController, animated: true)
    }
}

画面遷移のアーキテクチャーパターンの採用も検討しましたが、実装コストとメリットが見合わず、今回はよりライトにできる方法を採用しました。

画面遷移のアーキテクチャー検討について以前外部イベントで発表した資料がありますので、興味がある方はご覧ください。

www.docswell.com

この改善は現在も段階的に置き換えを進めているところですが、すでに実装完了している部分だけでもコードの見通しが良くなり、またテストコードも書きやすくなりました。

まとめ

どの改善も日々の開発業務のスピードを上げることができていると実感する部分が多くありました。

来年はより難易度の高い改善にも取り組めたらと思っております!

ドメインモデリングを通じて起きたチームの変化 ~ytake氏のワークショップ~

この記事はコネヒト Advent Calendarのカレンダー 13日目の記事です。

adventar.org

2023/10/06にytakeさんをお招きして、社内メンバーを対象にドメインモデリングの講義を開催しました。 今回は当日の様子と、その後の変化について紹介したいと思います。

現時点では、定量的に測れる変化までは至っていませんが、業務上定性的な変化が起きていると感じています。 今回、本記事で例示するのは以下の2つです。

  • チーム内でモデルと命名の話題が増えた
  • 「境界づけられたコンテキスト」を基準に設計・実装に反映する

本記事は、以下の構成でお送りします。

ワークショップの目的

ワークショップの目的は、以下の2点です。

  1. コンテキストギャップ埋める方法を身につけ、リリースまでの機動力を高める
  2. 技術コミュニティと新しい学びのループを作る

コンテキストギャップ埋める方法を身につけ、リリースまでの機動力を高める

日々の業務では、個人個人が持つ情報差分や専門職種の特性などから作られるコミュニケーションのコンテキストに違いが生まれます。 その違いが大きいと、コミュニケーションのコストが高くなったり、思わぬ手戻りが発生することで、ボトルネックとなってしまうことがあります。 どの程度のボトルネックかは、個人個人の暗黙的な感覚に依存してしまうため、 まずはワークショップを通じた現状認識と課題の発見を目的としました。

技術コミュニティと新しい学びのループを作る

コネヒトにはスマイル制度という「技術コミュニティになくてはならない開発組織をつくる」をコンセプトにしたアウトプット支援制度があります。 インプットとアウトプットのループを作ることで、コネヒトも技術コミュニティもWin-Winな関係を築いていく狙いがあります。

tech-vision.connehito.com

これまでは研修参加や書籍の購入など、個人で利用されるケースが多かったのですが、複数人で同じことを学び、発信する事例としてこの制度を利用しました。 本制度の多様な活用事例を生み出すことで、より技術コミュニティとWin-Winになること2つめの目的と設定しています。

講義の内容

当日の講義は、座学とワークショップを組み合わせた形式で、合計4時間の内容でした。参加者は事前に目的を共有し希望者を募りました。 ytakeさんに参加者と予定時間を相談の上、半日で収まる内容にチューニングしていただきました。

タイムライン

座学(120min)

  • 14:00 ~ 14:05: イベント概要おさらい
  • 14:05 ~ 14:15: 自己紹介/チェックインタイム
  • 14:15 ~ 15:00: ytakeさん自己紹介・座学
  • 15:00 ~ 15:10: 休憩
  • 15:10 ~ 16:00: 座学
  • 16:00 ~ 16:10: 休憩

イベントストーミング形式ワークショップ(120min)

  • 16:10 ~ 17:00: ワークショップ
  • 17:00 ~ 17:10: 休憩
  • 17:10 ~ 17:50: ワークショップ
  • 17:50 ~ 18:00: チェックアウト

参加者内訳

  • エンジニア:12名
  • デザイナー:1名
  • PdM/PMM: 2名

なお、イベントストーミングは、Alberto Brandolini氏が考案した協働的にドメインモデルを発見していく手法です。 EventStormingのサイトを参照すると以下のような記載があります。

The adaptive nature of EventStorming allows sophisticated cross-discipline conversation between stakeholders with different backgrounds, delivering a new type of collaboration beyond silo and specialisation boundaries.

翻訳:EventStorming の適応的な性質により、異なる背景を持つ関係者間で専門分野を超えた洗練された会話が可能になり、サイロや専門分野の境界を超えた新しいタイプのコラボレーションが実現します。`

今回はこのようなコラボレーションを実現するために、エンジニアとPdM/PMM、デザイナーの3つの職種のメンバーに参加してもらい、この方式を採用しました。

当日の様子

座学

当日は以下の内容を中心にお話ししていただきました。

  • ドメインモデルとはなに?
  • 分析するための考え方
  • ユースケース

座学の間も適宜質問を受けていただき、理解を深めながらお聞きすることができました。 ここで教えていただいた「コンテキストの境界」が今後の変化に繋がっていきます。

わいわいと講義を受けています

イベントストーミング

今回はママリアプリで起こる出来事を題材にイベントストーミングを開催しました。 みんなでわいわいと、出来事や関連する要素を書き出していき、ytakeさんのファシリテーションを受けながら、認識の違いを発見していきました。

グルーピングしたものをもとに議論しています

付箋の色分けは以下の通りです。

  • 画面やUIが絡むようなもの: 青
  • システム的なもの: ピンク
  • サービスの仕様、重要な出来事: オレンジ

当日のサンプル

その後の変化

このようにモデリングを利用し、認識の違いを発見する方法を学んだことで起きたエピソードを2つ紹介します。

チーム内でモデルと命名の話題が増えた

ミーティングで話す用語は、どのように整理すると自然なのかというコミュニケーションが以前より増え、用語集を作る動きがチーム内で生まれています。 筆者の所属するチームが動画コンテンツの改修を担当しているため、まずは動画施策の用語の整理から始めています。

用語集の整理

職種を問わず共同作業ができるように、Notionのデータベースを利用しています。

「境界づけられたコンテキスト」を基準に設計・実装に反映する

コネヒトが管理する動画には、外部サイトへのリンクがあるものないものがあります。この違いをRestAPIリソースとして個別に分けるかの議論がなされました。

miroで整理した様子

ワークショップ開催前は、設計においてアクターが誰かという話題は出てこなかったのですが、ワークショップを通じて、「コンテキストの境界」が重要であることを学んだことで、 この議論では、アクターが同じため同一のリソースとして定義しておこうと判断することができるようになりました。

この他にも

  • チームの担当領域に絞りイベントストーミングを実施する
  • 特定施策のユースケースを書き出してみる

など少しずつモデリング実施する機会が増えてきています。 それらの事例も、今後の変化に繋がっていくと思うので、また別の機会に紹介したいと思います。

まとめ

今回のワークショップでは、まずはモデリングのエッセンスを学ぶことにフォーカスしておりました。 本来の目的であるリリースの機動力を高める目的の実現には、まだまだ課題が多くあり、モデリングの実践とコードへの設計・実装への反映を定期的に繰り返す必要があると感じています。

ytakeさんから「目の前のものに騙されないように本質はなにか分析しましょう!100回くらい分析を繰り返しましょう!」というアドバイスをいただきました。 今後はより日常的にモデリングを実践できるように、常にMiro*1を開いてミーティングに参加しようと思います。

目指せモデリング100回!

最後となりますが、ytakeさん講義の開催ありがとうございました! モデリングにお悩みの場合、最初の一歩として、ytakeさんに相談することをおすすめしたいと思います。

*1:コネヒトではオンラインホワイトボードツールとして利用しています。https://miro.com/ja/

レコメンドで使用する類似アイテムをAmazon Bedrockとitem2vecで計算・比較検証してみた

みなさんこんにちは。MLエンジニアのたかぱい(@takapy0210)です。

最近、久しぶりに機動戦士ガンダムSEEDを見直しました。(来年には劇場版の公開もあります)

地球連合軍第7機動艦隊に所属するパイロットであるムウさんの

「君は出来るだけの力を持っているだろう?なら、出来ることをやれよ」

というセリフが好きです。
相手をリスペクトしつつ、でもお前はもっとできるだろ?という期待も込もった、良い言葉だなと感じます。

さて本日は、レコメンドで使用頻度の高い類似アイテムの計算処理を2パターンで実施し、どんな差分がでるのか?を検証した結果をお話ししようと思います。

この記事はコネヒト Advent Calendarのカレンダー 10日目の記事です。

adventar.org


目次


背景

コネヒトの運営するコミュニティサービスママリでは、様々な部分でレコメンデーション機能が提供されています。 今後もレコメンデーションロジックの改善を継続的に行っていく中で、類似するアイテムをどのように計算するのか?は重要な課題の1つです。

類似アイテムを計算する1つのHowとして、昨今話題になっているLLMを使う方法が考えられます。
LLMを用いることで様々なEmbedding(ベクトル)を取得することができ、このベクトルを用いることでアイテム間の類似度を計算することができます。

本記事では以下の2パターンで算出したベクトルを用いて、類似アイテム(ここで言うアイテム=質問)を抽出し、比較・考察してみようと思います。

  • Amazon Bedrockの埋め込みモデルで取得したベクトル
  • item2vecで計算したベクトル

前半でそれぞれのベクトル取得方法を簡単に説明し、後半では実際にいくつかの質問を用いてどのような類似アイテムが算出できるのか?を見ていこうと思います。

Amazon Bedrockの埋め込みモデルでベクトルを取得する

Amazon Bedrock(以下、Bedrock)とは、テキスト生成AIをはじめとする基盤モデル (Foundation Model) を提供するAWSのサービスです。Bedrockで使用できる基盤モデルには、Amazon自身が開発提供するTitanやAnthropicのテキスト生成AIであるClaudeなどがあります。

aws.amazon.com

以下のようなコードで、Bedrockの埋め込みモデルを使ってテキストのベクトルを取得することができます。

import json
import boto3

bedrock_runtime_client = boto3.client('bedrock-runtime', region_name="ap-northeast-1")

def get_bedrock_embedding(input_str, bedrock_runtime_client):
    bedrock_body = {
        "inputText": input_str
    }
    body_bytes = json.dumps(bedrock_body).encode('utf-8')
    response = bedrock_runtime_client.invoke_model(
        accept="*/*",
        body=body_bytes,
        contentType="application/json",
        modelId="amazon.titan-embed-text-v1",
    )
    response_body = json.loads(response.get("body").read())

    embedding = response_body.get("embedding")
    
    return embedding

text = "帝王切開で出産、退院時に痛み止めもらった方、いつぐらいまで飲んでましたか?"
vec = get_bedrock_embedding(input_str=text, bedrock_runtime_client=bedrock_runtime_client)

今回、Bedrockで取得したEmbeddingはOpenSearchに格納し、その機能を利用して類似質問を抽出しています。 OpenSearchへの格納方法などは、以下のブログに書いていますので、興味のある方はこちらもご覧ください。

tech.connehito.com

item2vecで計算したベクトルを取得する

item2vecとは、自然言語処理におけるword2vecの概念をアイテム推薦などに適用したものです。
word2vecは単語をベクトルとして表現し、これらのベクトルを使って単語間の意味的な関係を捉えることができますが、item2vecでは、この考えを商品や映画、曲などの「アイテム」に適用することができます。*1

今回はママリの質問閲覧ログデータを用いてitem2vecの学習を行いました。学習にはgensimライブラリを用いています。

学習に使用したデータは以下のようなイメージです。

user_id question_id event_dt
100 19064176 2023-10-15
100 19073732 2023-10-16
100 19037730 2023-10-16
101 18892007 2023-10-15
101 18891679 2023-10-16
... ... ...

まずは、このデータをgensimのモデルに入力できるように、系列データに変更していきます。

series_df = pd.DataFrame(df.groupby(['user_id', 'event_dt'])['question_id'].apply(list)).reset_index()
series_df = series_df.rename(columns={'question_id': 'order_question'})

こうすることで、以下の様なDataFrameを取得することができます。

user_id event_dt order_question
100 2023-10-15 [19084888, 18932714, 18925535 …]
100 2023-10-16 [19060467, 19055834, 19047868 …]
101 2023-10-15 [19096491, 19011148, 19095622 …]
101 2023-10-15 [18921942, 18921942, 18921939 …]
... ... ...

最後にこのデータをgensimに渡し、item2vecモデルの学習を行います。

import multiprocessing
from gensim.models import Word2Vec

corpus = series_df['order_question'].values.tolist()
cpu_count = multiprocessing.cpu_count()

model = Word2Vec(
    corpus,
    vector_size=50,
    window=5,
    hs=1,
    min_count=1,
    sg=1,
    workers=cpu_count,
    seed=42
)

類似アイテムの抽出は以下の様に行うことができます。

for i in model.wv.most_similar(target_q_id, topn=3):
    print(f"類似質問ID:{i[0]}, 類似度:{round(i[1], 4)}")

比較検証結果

上記2種類のロジックで取得したベクトルを用いて、どのような類似質問が取得できるのか類似度TOP3を取得して比較します。(今回はコサイン類似度を用いています)
「入力クエリ欄」に記載した質問内容に興味のあるユーザーに対して、どんなものが推薦されるのか?という想定で検証してみます。(Bedrockの場合は入力クエリがテキスト、item2vecの場合入力クエリは質問IDになりますが、ここでは分かりやすいようにテキストで統一して記述します)

※以下で掲示している質問文は一部改変しております

パターン1:ディズニーランドの情報が知りたいユーザー

入力クエリ

ディズニー詳しい方や最近行った方回答お願いします! 10月中旬の平日にディズニーランドへ行くのですが、朝何時から開園並んで何時に入園できましたか? ハロウィンのパレードはプレミアアクセスを購入予定ですが、何時までに入れば買える可能性ありますか?

取得結果

Bedrockの類似アイテムTOP:3

①:10月ディズニーインパについて。 今平日でも入園待ちですごいことになっているみたいですね。10時とかだとスムーズに入園できそうですが、その頃だともうプライオリティパスは取れないですかね?取れても夜の時間とかでしょうか? ハニーハントかモンスターズインクを取る予定です!

②:10/3ディズニー、平日、ハロウィンの情報です!どなたかの参考になれば嬉しいです。6:30ランド到着で一般前から10列目くらいで入場できました。プライオリティパスは入場してすぐで10:10~11:10の回でした!ハモカラ1時間前に地蔵で前から3列目、うちはそのままスプブ待ちでチップ、デール停車位置で前から2列目とれました。

③:皆さんディズニー行く時何時に行きますか?シーもハロウィンだと混むんでしょうか…? 何かYouTubeみて旦那が、7時半には並んだ方がいい!とか言ってますが、子連れでその時間から並ぶって無理あるでしょと思うのですが…笑朝イチじゃないとショーやプライオリティパスもすぐなくなってしまいますか? 来週水曜にディズニーシーに行く予定です。。 全然詳しくないので教えて頂きたいです。

item2vecの類似アイテムTOP:3

①:皆さんの意見を聞かせてください! 何年振りかにディズニーランドに行きます!2歳と4歳の子供を連れていきます! 13日は平日なので多少は空いてる?!チケット料金が安いです。 14日だった場合は親が一緒にこれるので私と旦那の負担が少ない?休日なので混んでる&チケット料金が高いです。 皆さんだったらどちらに行きますか?? 1つ気になるのが13日がシーが早く閉まる?のでランドが混みそうと言う事です。。 ディズニー初心者過ぎて…皆さんの力を貸してください。

②:ここ何年もディズニーに行ってなく、最後に行ったのは10年近く前、、、 その頃は紙のファストパスの時代でしたが今はアプリでとる?んですよね? 5歳3歳1歳の子供連れで久しぶりのディズニーで不安すぎます。 ディズニーランドに行くのですが1日の流れどのようにするのがいいですかね? まったくの無知なのでこうした方がいいよなどあったら教えてほしいです!

③:ここ数日でディズニー行った方教えて下さい! 平日で混んでますか?乗り物の待ち時間どのくらいでしょうか?コロナ禍のガラガラ時以降行ってなくて。。

パターン2:帝王切開後の身体への影響が気になるユーザー

入力クエリ

帝王切開の方にお聞きしたいです。もうすぐ産後1ヶ月になります。骨盤ガタガタで尾てい骨も痛いし足あげるのも痛いし、ゆっくりしか歩けません。みなさんは骨盤ベルトしていましたか?

取得結果

Bedrockの類似アイテムTOP:3

①:臨月になりお尻と足の付け根痛いです。 頭がおりてきていたり、靭帯が緩んだりと原因はいろいろあるみたいですが、これは出産が近いのかな…?? とにかく痛みに対しては骨盤ベルトで抑えてます。 同じように股関節の痛みを感じた方、どの位の期間で出産になりましたか??

②:赤ちゃんがもう産まれても大丈夫な状態らしく、たくさん歩くように言われました。 昨日と今日、いつもより多めに歩いたのですが、先ほどから恥骨がいつもにまして割れるような痛みがきてしまいました。 その前から恥骨は痛かったのですが、歩くのも困難になるほどです。それに加えて、股関節も痛いです。 昨日の検診では赤ちゃん、まだ全然降りてきてないと言われましたが赤ちゃんがおりてきてるんでしょうか?

③:産後50日過ぎましたが、恥骨がまだ痛過ぎます。いつになったら解放されますか? 妊娠後期から恥骨激痛で歩くのやっとで、産んだら解放されるかと思いきや、全然痛い・・・ 骨盤矯正は、先週から行き始めましたが、産後も恥骨痛あった人、いつから無くなりましたか?

item2vecの類似アイテムTOP:3

①:出生後、しばらく哺乳瓶で、その後スムーズに直母授乳に移れますか? 25日の月曜日に帝王切開にて出産しました。 術後の私の回復具合、我が子の体調などなどいろんなことを考慮して、しばらくは哺乳瓶でミルクまたは搾乳した母乳をあたえていくことになり、その方法で1週間近く経ちました。 最初の3日間はミルクを使用、4日目以降搾乳した母乳のみ飲ませています。 同じように、最初は哺乳瓶でその後母乳に移られた方、どうでしたか??特に抵抗なく直母に移れるのもでしょうか?

②:帝王切開で出産、退院時に痛み止めもらった方いつぐらいまで飲んでましたか?

③:出産して入院中なんですが、特に足首から下のむくみがすごく、象の足みたいになってるんですがしばらくはこんな感じなんですかね?

パターン3: ドラム式洗濯機を買うか迷っているユーザー

入力クエリ

縦型洗濯機からドラム式洗濯機に乗り換えた方にお聞きしたいです! 電気代と水道代はどのくらい高くなりましたか? 大体で構わないので教えてください。

取得結果

Bedrockの類似アイテムTOP:3

①:現在、縦型の洗濯機を使っていますが、ドラム式に買い替えることにしました。 明日、電気屋さんに行くのですが、 全く無知なので、おすすめの洗濯機を教えてください! また金額も20万ぐらいかな…と思っているのですが、どのぐらいするのでしょうか?

②:みなさんは洗濯機って縦型使ってますか? それともドラム式洗濯乾燥機ですか? うちは縦型ですが、ドラム式ほしいです。でも高い...

③:洗濯機買って5年経つんですが、縦型で容量が8キロ。 子どもが2人になり洗濯物が増えて、週末は1日に2回まわすこともよくあります。 容量が少ないのがストレスで買い替えたい気持ちもあるのですが、まだ5年だしな〜と迷っています。 もし次買うならドラム式がいいですが高い。。。ドラム式を使われてる方は洗濯後乾燥までされてる方が多いんですかね?縦型、ドラムどちらがおすすめですかね??皆さんどちら使われてるかも教えて欲しいです☺️

item2vecの類似アイテムTOP:3

①:マイホームを購入した時にサービスでつけてもらったオプションや家具家電など参考にさせてもらいたいので教えて下さい

②:コンビニで2,000円の買い物をするのに、 ①楽天カードで支払い ②楽天ペイで支払い(クレカからチャージ) どちらがお得なんでしょうか?

③:年少1人、1歳1人のお子様が居るご家庭の月の食費を教えてください!また旦那さんのお昼ご飯代も込みかどうかも教えて貰えるとありがたいです

パターン4:離乳食の2回食をいつから始めるか気になっているユーザー

入力クエリ

生後5ヶ月半から離乳食を始めました。 2回食にするのは離乳食が始まって2ヶ月後である7ヶ月半くらいかな?と思っていたんですが、6ヶ月の時点で2回食検討中、またはされている話も聞いたりします。 みなさん2回食はいつから始められましたか?

取得結果

Bedrockの類似アイテムTOP:3

①:離乳食についてです。 みなさんいつから2回食に移行しましたか? 5ヶ月から始めてもう1ヶ月と1週間が過ぎました。調べると2回食を始めるのはだいたい7ヶ月からか、始めて1ヶ月経ったらと見ます。 もちろん子どもの様子によって進めていくものだとは思いますが、2回食にするのがすごく気が乗らなくて笑

②:6ヶ月から離乳食を始めた方、いつから2回食にしましたか?5ヶ月で始める時より早めに2回食にした方がいいとかあるんでしょうか、、進め方がよく分からず困っています。

③:5ヶ月から離乳食始めました。 今7週目で(6ヶ月の2週目)いつから2回食を始めるか悩んでます。今日から始めようかな、、と。 みなさん2回目はいつ頃から、何時頃あげてましたか?

item2vecの類似アイテムTOP:3

①:10ヶ月検診で肥満気味と言われました。 男の子で身長73センチの11キロです。離乳食を始めたのが6ヶ月後半からで遅く、量もあまり食べなかったのですが、1ヶ月くらい前から本格的に3回食にして量は小鉢2つ程度です。量は測ったことなかったのですが、 今回測ってみたら25ml+30ml+50mlの冷凍パックしたものを解凍し味噌汁に野菜をプラスしたおかゆとじゃがいもと野菜を潰してコンソメで味付けのポテトサラダで、105g?食べているのかなと思いました。ただ、毎回このくらい食べる時もあれば半分食べたかなくらいで残す時もあります。 本やネットには離乳食の間隔は4時間あけて18時以降は食べさせないと書いてあり、先生には1度に量を食べれなければ間食としてご飯をあげてくださいと言われましたが、その場合はあける時間間隔は無視してもいいのでしょうか。 正直、間食をあげるタイミングもわからないです。 10ヶ月のお子さんがいらっしゃる方は、毎食どのくらい食べているのでしょうか?

②:まだ自分でコップを持って飲めない赤ちゃん、コップ飲みの練習ってどうやりましたか? 現在生後6ヶ月で、生後5ヶ月の頃から離乳食デビューに合わせて麦茶でのコップ飲みを始めました。 ダイソーで売っているトレーニングコップを使用していて、取っ手の部分を握ったりはしますがそのまま自分の口に持っていくのはまだできません。なので私がコップを持って飲ませていますが、麦茶がたくさん口に入るのかむせてしまったりしてイマイチ上手くあげられません。 自分で持って飲んでむせたり溢したりしてだんだんコップに慣れるイメージなのですが、親が飲ませていて練習になるんでしょうか? 1ヶ月続けてもなんの進展もないので不安になってしまいました。 ぜひ教えてください。

③:離乳食をあげてる時間教えてください。 現在生後6ヶ月で5ヶ月の頃から離乳食あげてますがまだ一回食です。 離乳食はモリモリ食べすぎてるくらいなのですが、二回食にするのがめんどくさくて… でもそろそろ二回食を考えなきゃな〜と思ってるので、何時に離乳食をあげているのかみなさんの離乳食スケジュール教えてください。 三回食をもう始めている場合は今後のために三回食の時間も教えて欲しいです。 うちは今10時に離乳食をあげています。 離乳食とミルクはバラバラです。

パターン5:オムツのサイズアップをいつからすれば良いか悩んでいるユーザー

入力クエリ

オムツのサイズアップについてです。 生後1ヶ月で、まだ新生児サイズを使っているのですが、太ももにかなり跡がついています。 ですが、お腹周りはゆるゆるです。 サイズアップした方がいいのかな?と思い、試しに試供品のSサイズを使ってみたら、太ももはぴったりですがお腹の方はオムツがかなり上まで来てゆるゆるです。 このような場合でもサイズアップしたほうがいいのでしょうか?

取得結果

Bedrockの類似アイテムTOP:3

①:オムツの新生児サイズからSサイズへのサイズアップっていつ頃でしたか? また、どんな感じになったらサイズアップした方が良いのか教えて下さい。

②:オムツのサイズについて質問させてください! 生後2ヶ月の男の子がいます。 私の母乳外来受診のため、授乳の様子も見てもらえるということで息子も一緒に受診したのですが、付けているオムツが小さいからサイズアップしたほうがいいと言われました。 体重は約5800gでテープのSサイズを使ってます。 最近ようやく太もも周りがちょうどよくなってきたかなぁと自分的には思ってた矢先助産師さんに小さいと言われたのですが、やはりMサイズへ変えるべきでしょうか?

③:オムツのサイズ別の使用量について。 2400gで生まれ、1ヶ月半新生児用を使用しメリーズファーストプレミアムを5袋使いました。 1ヶ月半となり、4000gになったのでオムツをSサイズにしました! 皆さんはSサイズ、Mサイズ、Lサイズをそれぞれ何ヶ月頃から使用し、何袋使いましたか?? もちろん、赤ちゃんの体型それぞれなのは承知で参考までに。 また、何ヶ月の何キロからSサイズorMサイズからテープオムツをパンツオムツにしましたか??

item2vecの類似アイテムTOP:3

①:生後1ヶ月と半月です。 夜間の授乳が5~6時間くらい初めて空いたのですが、こうやって徐々に夜の時間を延ばしていってもいいんですかね? 最近、日中はずっと起きています。。。早めに昼夜の感覚をつけたいと思ってます!

②:生後1ヶ月 4キロです。 アマゾンセールでおむつを買いたいです。 今使っているものの次のサイズをストックしておくつもりです。 6キロからのMサイズを購入しますがテープタイプとパンツタイプだとどちらが良いのでしょうか?

③:現在生後1ヶ月、まもなく2ヶ月になる息子の育児について相談させてください。ご機嫌に起きている時で、あやすのも授乳も終えている時は皆さんどうされてますか? ご機嫌で1人でおしゃべりしてる時はそのままベッドやハイローチェアで眠るのを待っていても良いのでしょうか?

考察

それぞれのパターンについて簡単にまとめてみます。

パターン Bedrock item2vec
1:ディズニーランドの情報が知りたいユーザー 「プレミアアクセス」や「時間状況」などの単語表現をうまく拾い、類似したアイテムが推薦されている ディズニーランド全般に関する幅広い情報が推薦されている
2:帝王切開後の身体への影響が気になるユーザー 骨盤や股関節の痛みといった、身体への影響に関連するアイテムが推薦されている どちらかと言えば帝王切開に関連したアイテムが推薦されている
3:ドラム式洗濯機を買うか迷っているユーザー 洗濯機を選ぶ基準やおすすめの洗濯機に関連するアイテムが推薦されており、購入を検討しているユーザーには有用そう 家庭生活全般(主にお金関連)に関連するアイテムが推薦されている
4:離乳食の2回食をいつから始めるか気になっているユーザー 離乳食の2回食移行に関連したアイテムが推薦されている 赤ちゃんの健康管理や飲み物の練習など、子育ての幅広い側面をカバーしたアイテムが推薦されている
5:オムツのサイズアップをいつからすれば良いか悩んでいるユーザー オムツのサイズに重点を置いたアイテムが推薦されている パターン4の時と同様に子育ての幅広い側面をカバーしたアイテムが推薦されている

Bedrockによる推薦は、細かいテキスト内容部分も考慮された類似アイテムが計算できていそうです。(テキストをベクトル化して類似度を計算しているので当たり前と言えば当たり前ですが)
一方でitem2vecによる推薦は、ドンピシャな推薦というよりは、同一トピック内ではあるものの、もう少し幅広いアイテムが計算されていそうです。

おわりに

本記事では、Bedrockとitem2vecを用いて類似アイテムの計算と比較検証を行ってみました。

レコメンデーションには大きく分けて「探索 (Exploration)」と「活用 (Exploitation)」があると言われています。

探索とは、何らかのアルゴリズムによってユーザの嗜好に最も合うであろうアイテムが決定される中で、あえて嗜好に最も合うもの"ではない"アイテムを推薦することを言います。 探索の主な目的は新しい知見を得ることです。これにより、ユーザーの好みや興味の変化に適応することができます。
逆に、現時点で最もユーザの嗜好に合うと計算されたアイテムをそのまま推薦することを活用と言います。
一般的にこの2つはトレードオフの関係になっています。

今回の考察結果から、item2vecの推薦はユーザーの興味トピックとしては近いが、そのトピックの中から幅広いアイテムを抽出しているため、どちらかというと探索向けの推薦として利用できそうだなと感じました。
一方でBedrockによる推薦は、興味を持った具体的なアイテムと似ているものをピンポイントで推薦するため、活用向けの推薦に利用できそうだなと感じました。

今後も推薦の目的や状況に合わせたレコメンデーションロジックの検証・改善を繰り返し、より良いユーザー体験を届けていきたいと思います。

「ベクトル検索 vs 全文検索」〜Amazon Bedrockの埋め込みモデルを用いたプロトタイピング〜

※ この記事は、AWS (Amazon Web Services) の技術支援を受けて執筆しています。

はじめに

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

コネヒト Advent Calendar 2023って?
コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。
コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、
ママの一歩を支えるアプリ「ママリ」などを 運営しています。

adventar.org


こんにちは!コネヒトの機械学習エンジニア y.ikenoueです。
突然ですがみなさん、Amazon Bedrockをご存知でしょうか。

aws.amazon.com

Amazon Bedrock(以下、Bedrock)は、テキスト生成AIをはじめとする基盤モデル (Foundation Model)*1を提供するAWSのサービスです。単に基盤モデルを呼び出して出力結果を得るだけにとどまらず、ファインチューニングやナレッジベースの構築によるRAG(検索拡張生成, Retrieval Augmented Generation)*2の実現など、基盤モデルに関する様々な操作を統一的なAPIから利用することができます。Bedrockで使用できる基盤モデルには、Amazon自身が開発提供するTitanの他、Anthropicのテキスト生成AIであるClaude、Stability AIの画像生成AIであるStable Diffusionなど、多様な用途をカバーしたモデルが含まれます。


さらに、Bedrockが提供する基盤モデルの中には埋め込みモデル*3がラインナップされています。埋め込みモデルは、画像や自然言語といったデータを特定の次元のベクトルに変換するためのモデルです。埋め込みモデルによって生み出されたベクトルは元のデータの特徴を反映したものとなり、「機械学習モデルに入力する特徴量として用いる」「ベクトル同士の類似性を計算することで検索やレコメンデーション用途に使う」といった活用が考えられます。

そこで本日は、Bedrockの埋め込みモデルを用いたテキストのベクトル化を実践します。さらに、埋め込みモデルによって生成されたベクトルの活用例として「ベクトル検索」*4システムのプロトタイピングを行います。具体的には、Bedrock埋め込みモデルによって生成されたベクトルデータによるベクトル検索の実行結果と従来の全文検索*5の実行結果を比較することで、埋め込みモデルの性能やベクトル検索の特徴に関する理解を深めることを目的とします。

※ 「手っ取り早くベクトル検索と全文検索の比較結果を見たい!」という方は、こちらからお読みください!

※ 当ブログでは、過去にも埋め込みモデルや検索システムに関する記事を公開しています。興味のある方は、下記のリンクからご覧ください。

tech.connehito.com

tech.connehito.com

tech.connehito.com

プロトタイピングの動機

まずは、今回のプロトタイピングを行うに至った動機についてご説明します。

弊社コネヒトでは、母親向けのQ&Aを主なコンテンツとするママリというコミュニティサービスを運営しています。
ママリには毎月十万件以上の質問が新たに投稿されており、これらの大量の質問データの中からユーザーが求めるものを見つけ出すための検索システムは、AWSのOpenSearch Serviceを用いて自社で構築しています。*6現在の検索システムでは、単語の出現頻度に基づく検索アルゴリズムである全文検索をベースとした手法を採用しているのですが、ここにクエリと文書の意味的な類似性に基づく検索を可能とするベクトル検索技術を取り入れることで、ユーザーの検索体験の改善につなげたいという狙いがあります。

またママリでは、質問のレコメンデーション*7や検閲を行う機械学習モデル*8を運用しています。埋め込みモデルによって獲得したベクトルをこれらのモデルの特徴量として活用することで、モデルの精度向上が期待できるのではないかという仮説をもっています。

このように、ママリにおいて埋め込みモデルの活用範囲は非常に広いと考えています。
さらに、弊社ではインフラにAWSを採用しているため、AWS上の他のサービスとの連携を考慮するとBedrockは有力な選択肢となります。

以上のような背景から、まずはBedrockの埋め込みモデルの性能や使い勝手を確認するための検証第一弾として、今回のプロトタイピングを実施することになりました。

プロトタイピングの概要


続いて、今回のプロトタイピングの概要についてご説明します。
繰り返しになりますが、この記事ではベクトル検索と全文検索を行った際に、両者の検索結果にどのような違いが生じるのかを比較することで、両者の検索手法の強み・弱みを可能な限り明らかにしていきます。
この際、ベクトル検索を実行するために必要なベクトルデータは、Bedrockの埋め込みモデルによって生成します。ベクトル検索の実行結果の妥当性を通して、埋め込みモデルの性能に対する理解も同時に深めていきます。

埋め込み及び検索の対象となるデータにはママリの実データを使用します。具体的には、2023年10月に投稿された十万件以上の質問文を対象とします。

以降は、プロトタイピングの手順を以下3ステップに分け、順番にご説明します。

  • ステップ① Bedrockの埋め込みモデルによるベクトルの生成
    • Bedrockの埋め込みモデルを用いて、ママリの質問データ(2023年10月文)をベクトル化します。
  • ステップ② OpenSearch Serviceによる検索システムの構築
    • ママリの質問文及びステップ①で生成したベクトルデータをAWSのOpenSearch Serviceのマネージド型ドメインに格納することで検索システムを構築します。
  • ステップ③ 「全文検索」と「ベクトル検索」の実行と検索結果の比較
    • ステップ②で構築した検索システムを用いて、ママリの質問文に対する「全文検索」と「ベクトル検索」を実行します。そして、両者の検索結果にどのような違いが生じるのかを定性的に比較します。

ステップ① Bedrockの埋め込みモデルによるベクトルの生成


最初のステップとして、Bedrockの埋め込みモデルを用いたテキストのベクトル化を行います。
まずは、埋め込みモデルの選定基準についてご説明します。 2023年12月8日現在、Bedrockにはテキストデータを対象とした埋め込みモデルとして下記3種類が提供されています。

モデル名 開発元 対応言語 コンテキスト長 埋め込み次元 東京リージョンにおける利用可否
(2023/12/8時点)
Titan Embeddings G1 - Text Amazon 多言語
(日本語含む)
8000 1536
Embed English Cohere 英語 512 1024
Embed Multilingual Cohere 多言語
(日本語含む)
512 1024

※ マルチモーダルモデルであるTitan Multimodal Embeddings G1は比較の対象外としています。

3つのモデルのうち、「Embed English」(Cohere)は対応言語が英語のみとなっていることから、日本語データを対象とする今回のプロトタイピングでは選択肢から外れました。

残った2つのモデルに関しては、主にコンテキスト長の差を重視し「Titan Embeddings G1 - Text」(Amazon)を選択しました。コンテキスト長は、埋め込みモデルが入力として考慮することのできる最大のトークン数を指します。「Embed Multilingual」(Cohere)のコンテキスト長は512までとなっていますが、ママリの質問データにはこれを超えるものが存在することから、コンテキスト長が最大8000トークンまでと余裕のある「Titan Embeddings G1 - Text」 (Amazon)が適しているという判断になります。

次に、BedrockのAPIを呼び出し、テキストをベクトル表現に変換するためのコードをご紹介します。
プログラミング言語には、Pythonを使用しています。

import json
import boto3

# Bedrock runtimeに接続するためのclientを生成
bedrock_runtime_client = boto3.client('bedrock-runtime', region_name="ap-northeast-1")

def bedrock_embedding(input_str):
    bedrock_body = {
        "inputText": input_str
    }
    body_bytes = json.dumps(bedrock_body).encode('utf-8')
    # 埋め込みモデルの呼び出し
    response = bedrock_runtime_client.invoke_model(
        accept="*/*",
        body=body_bytes,
        contentType="application/json",
        modelId="amazon.titan-embed-text-v1",
    )

    return json.loads(response.get("body").read()).get("embedding")

コードの要点を取り上げて解説します。

はじめに、Bedrockのモデルを呼び出す際は、bedrock-runtimeに接続したclientが持つinvoke_model()というメソッドを使用します。このメソッドには、モデルの入力とするテキストをキー"inputText"に対応する値として設定したJSON形式のデータを渡す必要があります。
使用するモデルの種類は、引数modelIdに与えます。先述の通り、今回のプロトタイプでは「Titan Embeddings G1 - Text」(Amazon)を使用するため、このモデルのIDに該当する"amazon.titan-embed-text-v1"を指定しています。
なお、モデル名とIDの正式な対応関係は下記のコードで出力されるモデル一覧から知ることができます。

bedrock = boto3.client(service_name='bedrock')
print(bedrock.list_foundation_models())

invoke_model()メソッドのレスポンスには"embedding"という要素が含まれており、これがテキストの埋め込み結果に該当するベクトルデータです。ちなみに、このレスポンスには"embedding"の他に、元のテキストのトークン長が格納された"inputTextTokenCount"が含まれているため、この値を参照することで埋め込みモデルの利用にかかったコストを個別のデータ単位で正確に把握することができます。

ステップ② OpenSearch Serviceによる検索システムの構築

続いて、AWSのOpenSearch Serviceを用いた検索システムの構築についてご説明します。 このステップでは、特にベクトル検索を実行するためのインデックスの構築方法に焦点を当てています。

なお、このステップのプログラムを実行するには、事前にOpenSearch Serviceのドメインを構築しておく必要がありますが、分量が非常に長くなるためこの記事では説明の対象外とします。ドメインの構築方法については、代わりに下記の公式ドキュメントを御覧ください。

以下は、全文検索とベクトル検索の両者に対応したインデックスを作成するためのPythonコードです。実装にはopensearch-pyというライブラリを用いています。

# 必要なライブラリのインポート
from opensearchpy import OpenSearch, RequestsHttpConnection
from opensearchpy.helpers import bulk
from requests_aws4auth import AWS4Auth

region = 'ap-northeast-1'
service = 'es'

credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)

host = 'XXX.ap-northeast-1.es.amazonaws.com' # XXXには、事前に構築したドメインのエンドポイントを記入します
port = 443

# OpenSearch Serviceに接続するためのクライアントを生成
client = OpenSearch(
    hosts = [{'host': host, 'port': port}],
    http_auth = awsauth,
    use_ssl = True,
    verify_certs = True,
    connection_class = RequestsHttpConnection,
    timeout=1000
)

# ドキュメントとベクトルが格納された辞書データを受け取り、インデックスに登録する関数
def add_documents_to_index(client, index_name, document_dict):
    
    actions = []
    for i, document_id in enumerate(document_dict):
        
        actions.append({
            "_op_type": "index",
            "_index": index_name,
            "_id": document_id,
            "_source": {
                "document": document_dict[document_id]["document"], # 元のテキストデータを追加
                "embedding": document_dict[document_id]["embedding"] # ベクトルデータを追加
            }
        })
        
        if i % 100 == 0:
            # データをひとまとめにして一括で登録
            bulk(client, actions)
            actions = []
    else:
        bulk(client, actions)

上記のコードでは、”document”フィールドにオリジナルのテキストデータ、”embedding”フィールドにテキストの埋め込み表現を追加しており、それぞれのフィールドを全文検索、ベクトル検索の実行時に使用します。

また、データの登録時には関数bulkを用いて複数件のデータを一度にまとめて登録することで、処理にかかる時間を削減しています。一方で、あまりに多くのデータを一度に転送しようとするとbulkの実行時にエラーが発生することがあるため、ここではデータを100件ごとに区切って処理を実行しています。(一度に登録できるデータ件数の上限は一つのデータあたりの容量などに依存します。)

ステップ③ 「全文検索」と「ベクトル検索」の実行と検索結果の比較

最後に、構築したシステムを用いて検索を実行します。
以下は、全文検索、ベクトル検索を実行するためのPythonコードです。

# OpenSearch Serverlessに構築したVector Storeに対してクエリを実行する関数
def search(query_str, search_type, size=5):
    
    if search_type == "vector":
        # 検索クエリを埋め込み表現に変換
        vec, _ = bedrock_embedding(query_str, bedrock_runtime_client)
        # ベクトル検索用のDSLを定義
        query = {
          "size": size,
          "query": {
            "knn": {
              "embedding": {
                "vector": vec,
                "k": 10
              }
            }
          }
        }
    else:
        # 全文検索用のDSLを定義
        query = {
          "size": size,
          "query": {
            "match": {
                "document": query_str
            }
          }
        } 
    # 検索を実行
    results = client.search(index=index_name, body=query)
    return results

関数searchでは、引数search_typeに”vector”を指定した場合はベクトル検索、それ以外の場合は全文検索用のDSL (ドメイン固有言語: Domain Specific Language) を用いるように条件分岐を行っています。

またベクトル検索を行うには、事前に検索クエリの埋め込み表現を獲得する必要があります。そこで、ステップ①で掲載した関数bedrock_embeddingを使ってテキストデータをベクトルに変換する処理を実行しています。

それでは、いよいよ準備が整ったので、全文検索とベクトル検索の検索結果を比較していきましょう。
今回の比較検証では、以下5つのケースを取り上げてそれぞれの検索結果に対する考察を行います。

  • ケース1. クエリが一つの単語で構成される場合
  • ケース2. クエリが複数の単語で構成される場合
  • ケース3. クエリに固有名詞を含む場合
  • ケース4. クエリに誤字を含む場合
  • ケース5. クエリに文章を使用した場合

ケース1. クエリが一つの単語で構成される場合

検索クエリ: 保育園

まずは、検索クエリが一つの単語で構成される場合の例を見ていきます。以下は、保育園を検索クエリとして用いた場合の検索結果です。

検索ランキング 全文検索 ベクトル検索
1 【愛知県瀬戸市の保育園について】瀬戸市の保育園について教えてください (...以下略...) 保育園ってどのくらい前から探すものですか??
2 親の都合で保育園を8月いっぱいで退園させてしまいました (...以下略...) 八幡西区でベビーカー置き場のある保育園はありますか?
3 千葉県香取市の保育園についてまんまる保育園、香西保育園、たまつくり保育園どんな感じか (...以下略...) 滋賀県愛荘町にお住まいの方!保育園の応募なんですが、この辺り出身ではないので (...以下略...)
4 【通勤途中の保育園と家の近くの保育園、どちらに預けるべきかについて】モヤモヤしてます (...以下略...) 山形市内から天童市内周辺まででおすすめの保育園はありますか?0歳児クラスがあるところ (...以下略...)
5 保育園についてです。今生後7ヶ月の娘が保育園に通っているのですが、3回食にならないと保育 (...以下略...) 通園て何が必要ですか?保育園です。

※ 質問文の全体を掲載すると文章が長くなりすぎてしまう場合があるため、質問文の意味や検索クエリとの関連性が正しく伝わる程度に一部の情報を省略して掲載しています。

両者ともに上位五件の検索結果には保育園という単語が含まれており、正常に検索が行われていると言えそうです。保育園という単語だけではユーザーがどういう情報を求めているのかを判断することが難しいため、検索結果の良し悪しについては、今回のケースではこれ以上の評価することができません。

そのうえで両者の検索結果の違いに着目すると、以下のような傾向が見受けられます。

  • 全文検索では、質問内に保育園というワードが複数個含まれている質問が上位に並んでいる
  • ベクトル検索では、保育園というワードを含みつつも、質問の全体の文章量が少ない質問が上位に並んでいる

全文検索では、クエリと文書の一致度を計算する際に、クエリと文書の単語の出現頻度を考慮しているため、クエリと文書の単語の出現頻度が高いほど検索結果の上位に位置づけられる傾向があるためこういった結果が生まれたと推察できます。

一方、ベクトル検索では、単語の出現頻度ではなくクエリと文書の意味的な類似性に基づき検索スコアが計算されます。今回のケースでは、クエリが保育園という一単語のみで構成されているため、文章が長くなるほど(保育園以外の情報が加わるほど)、保育園からは意味が離れていく作用が働いていると考えられます。

ケース2. クエリが複数の単語で構成される場合

検索クエリ: 保育園 お弁当

続いて、クエリ内に複数の単語が含まれる場合を比較しましょう。以下は、ケース1の保育園に一単語を追加した保育園 お弁当を検索クエリとして用いた場合の検索結果です。

検索ランキング 全文検索 ベクトル検索
1 年少さんのお弁当の量ってどんなかんじですか?保育園で初めてお弁当あり (...以下略...) 保育園の園外保育で給食の方、お弁当はどうされていますか?
2 19日に保育園お弁当の日があります。お弁当のおかずについてです。 (...以下略...) 幼稚園 お弁当オススメおかずありますか?
3 保育園の行事でお弁当を持たせる時 普段給食なのでお弁当は作らないので (...以下略...) 保育園の遠足のお弁当。ほんっと下手くそで泣きそうです (...以下略...)
4 年長 遠足のお弁当 今度、保育園の遠足でお弁当持っていきます (...以下略...) 幼稚園お弁当は何入れてますか?
5 保育園に入ってから初めての遠足が明後日あります!初めてのお弁当です (...以下略...) 保育園幼稚園から帰宅後、家でもおやつやジュースを食べますか?

基本的な検索結果の傾向はケース1と似通っているものの、ベクトル検索の結果に興味深い点があります。それは、検索ランキングの二位と四位に保育園ではなく幼稚園を含む文書が出現したことです。これは、保育園幼稚園が意味的に類似しているため、ベクトル検索では保育園幼稚園を同じような意味を持つ単語として扱っているものと考えられます。

また、検索ランキングの五位にお弁当がなくおやつジュースが含まれる質問があることも同様の理由であると言えそうです。

ケース3. クエリに固有名詞を含む場合

検索クエリ: 多摩総合医療センター

ここからは、検索クエリが特殊な特徴をもつ場合の比較検証を行います。まずは、検索クエリに固有名詞を含む場合です。固有名詞は一般名詞と比べて文書内の出現頻度が低い傾向にあり、その言葉の意味を埋め込みモデルが学習することは困難です。そうなると、ベクトル検索では固有名詞を含む文書を正しく検索することが難しいのではないかと考えられますが、どういう結果が生まれるか見ていきましょう。

以下は、検索クエリに多摩総合医療センターという特定の病院名を与えた場合の例です。

検索ランキング 全文検索 ベクトル検索
1 榊原記念病院、多摩総合医療センター、どちらで分娩するか悩んでいます。(...以下略...) 広島県西条の八本松にある高橋ホームクリニックご存知の方に質問です。(...以下略...)
2 - 吹田徳洲会病院でご出産された方いらっしゃいませんか? (...以下略...)
3 - 神奈川県海老名市付近の婦人科検診と不妊治療が行える病院を探しています (...以下略...)
4 - 仙台市太白区付近でセミオープンおすすめの病院を教えてください。(...以下略...)
5 - 熊本県合志市でおすすめの婦人科教えてください (...以下略...)

全文検索では、多摩総合医療センターについて記述されたデータがヒットしています。(多摩総合医療センターを含む質問は一件しか存在しなかったため、検索結果はこれのみ)

一方で、ベクトル検索を用いた場合の検索結果には多摩総合医療センターを含む文書は上位五件以内には現れませんでした。検索結果として並んだ質問を詳細に見ていくと、地域名と病院を含むという共通点が見受けられるため、多摩総合医療センターと意味的に類似する質問がヒットしているとは言えそうですが、直接的に多摩総合医療センターという病院名を含むデータを上位に位置づけることはできませんでした。

一つの例を確認しただけでは結論づけるのは尚早ですが、特定の施設や商品等、固有名詞による検索を必要とするケースでは、ベクトル検索より全文検索のほうが求める検索結果を得られやすい傾向にあるのかもしれません。

ケース4. クエリに誤字を含む場合

検索クエリ: 子供 正確

続いて、クエリに誤字を含む場合の検索結果を比較します。 ここでは、『子供 性格と打つつもりが、子供 正確と打ち間違えてしまった』ケースを想定して検索結果を比較します。

検索ランキング 全文検索 ベクトル検索
1 すぐに測れる体温計でおすすめありませんか?
(...中略...)
非接触型体温計もあるのですが、全く正確に測れません?
小学一年生になる息子がいます。小規模の小学校でクラスに7人しか男の子がいません。息子はおとなしい (...以下略...)
2 小学校一年生の子供にGPSを持たせようと思いますが、正確に場所とか分かるGPS (...以下略...) 子供って、なんて純粋で優しいんだろう…私は子供を (...以下略...)
3 【犬猫のGPSトラッカーについて】犬猫用のGPS 常に正確な位置を携帯アプリで表示 (...以下略...) 幼稚園の先生から見て満3歳児クラスの子でこの子はまだお母さんといた方が良いのではと思う子って (...以下略...)
4 おでことかにあててピッと一瞬で体温が測れる体温計って正確に測れているんでしょうか? (...以下略...) 息子は口が悪くなったり怒りっぽい一方で、優しく穏やかな時もあります。 (...以下略...)
5 【おすすめの体温計について】医療関係者の方 なるべく早くて正確なおすすめの体温計 (...以下略...) 男の子は優しいよって言われたけど (...以下略...)

はじめに全文検索の結果を見てみると、正確を含む質問が並んでいることからクエリとした投げた子供 正確にそのまま素直に一致するデータを返していることがわかります。

一方、興味深いことに、ベクトル検索の結果には子どもの性格に関する質問が上位に並んでいます。
更に、これらの質問は性格という直接的に一致するキーワードを含んでいるわけではなくおとなしい怒りっぽい等、性格と共起性が高いワードを含む文書たちとなっています。ここからは仮説ですが、埋め込みモデルの学習データにも子供 性格子供 正確と間違えて使用していた文書が多く含まれていたことが、こういった結果を引き起こしたと可能性があります。その場合、子供 正確という言葉と「子供がおとなしい」「子供が怒りっぽい」といった子供の性格を表す表現が意味的に類似性が高いものとして学習されていると考えても不思議ではありません。

このように、ベクトル検索を用いることでたとえクエリに誤字が含まれていたとしても、クエリ内の他のキーワードとの文脈が考慮されることによって、ユーザーが本来求めている情報を検索結果として返すことができる可能性が示唆されました。

ケース5. クエリに文章を使用した場合

検索クエリ: 先日、4歳の子供に「サンタさんはお父さんなの?」と聞かれました。本当のことを言うべきでしょうか?

最後に、キーワードではなく文章をクエリとして使用した場合の検索結果を比較します。ベクトル検索は、文章の文脈を含む意味的な類似性を計算することができるという性質から、文章をクエリとして使用した場合にこそ全文検索にはない強みを発揮するのではないかと考えています。
また、テキスト生成AIの応用技術として注目を浴びているRAG (検索拡張生成, Retrieval Augmented Generation)では、ユーザーが入力した文章をそのまま検索クエリとして使用することが多いため、このケースはRAGの実践を見据えた検証としても重要であると言えるでしょう。

以下は、先日、4歳の子供に「サンタさんはお父さんなの?」と聞かれました。本当のことを言うべきでしょうか?という短い文章を検索クエリとして用いた場合の検索結果です。

検索ランキング 全文検索 ベクトル検索
1 小1男子なんですが、まだサンタさん信じてるんですけどその方が珍しいんですかね?友達にサンタはお父さんとお母さんやでって言われたみたいで ほんまなん?って聞かれました...真実を教える年頃ですか? (...以下略...) 5歳年長の娘が「サンタってほんとはお父さんとお母さんじゃない?」と言い出しました、、
(...中略...)
あんまり嘘つくのもよくないかな下の子もいるしあと3〜4年は信じてもらってたいです
2 あと2ヶ月でサンタさん来ますね!?うちはもう欲しいものが決まってて、サンタさんにこれ貰おうねーとよく話すのですが、(...以下略...) 小1男子なんですが、まだサンタさん信じてるんですけどその方が珍しいんですかね?友達にサンタはお父さんとお母さんやでって言われたみたいで ほんまなん?って聞かれました...真実を教える年頃ですか?|5歳年長の娘が「サンタってほんとはお父さんとお母さんじゃない?」と言い出しました、、?
(...中略...)
あんまり嘘つくのもよくないかな 下の子もいるしあと3〜4年は信じてもらってたいです
3 孫に対して(3歳)言うこと聞かないと機嫌悪くなり
(...中略...)
旦那のお父さんは。子供って中々ご飯食べてくれない時どうしてますか?
【クリスマスプレゼントについて】ちょっと時期早めの話題ですが…6歳前後のお子さんをお持ちの方に質問です。
(...中略...)
子どもの夢は壊したくないからサンタさんいないんだよっていいたくはないし ...
4 〈サンタさんについて〉少し早いですが..サンタさんからのプレゼント?を何歳くらいからやりましたか?直接サンタさんから渡される感じではなく子供達が寝た後、枕元に置くプレゼント?です 【旦那がサンタについて嘘をついたことについて】旦那と息子と3人でたわいもない会話をしながら食卓を囲んでいた時に今年のクリスマスプレゼント何が欲しいーの?サンタさんにお願いしようね!っと私が言うと、旦那が笑いながら息子に対してサンタなんか居ないからと言いました。 (...以下略...)
5 5歳年長の娘が「サンタってほんとはお父さんとお母さんじゃない?」と言い出しました、、
(...中略...)
あんまり嘘?つくのもよくないかな 下の子もいるしあと3〜4年は信じてもらってたいです (...以下略...)
子供がもう、サンタさんの話してる笑 サンタさんくる?!って何回も聞いてくる (...以下略...)

検索クエリと同じく「サンタさんは実在しないことを子供に正直に伝えるべきか」をテーマとした質問に該当するものとしては、全文検索は1位と5位の2件、ベクトル検索は1位〜4位の4件がランクインしています。この後、6位以降の結果も見渡したところ「サンタさんは実在しないことを子供に正直に伝えるべきか」をテーマとした質問は、ベクトル検索の実行結果により多いという印象を受けました。

やはり、クエリが長くなるほど (クエリに込められる意味の情報量が増すほど) ベクトル検索はその強みを発揮する傾向があると言えそうです。

「ベクトル検索」と「全文検索」 比較結果のまとめ

ここまでの検証で得られた結果を改めて整理します。

  • キーワードベースのクエリを用いて検索を実行した場合、全文検索ではそのクエリを文書内に多く含むものが上位に並ぶ傾向があり、ベクトル検索では文書の長さが短いものが上位に並ぶ傾向が見受けられた。
  • 誤字を含むクエリを用いて検索を実行した場合、ベクトル検索は誤字ではなく本来検索者が入力しようとしていた情報に関連性の高い文書を検索結果として返すことができる可能性が示唆された。
  • 固有名詞を含むクエリを用いて検索を実行した場合、ベクトル検索ではその固有名詞を含む文書を上位に位置づけることが困難な場合があることが示唆された。
  • 文章をクエリとして用いて検索を実行した場合、ベクトル検索では文章の文脈を考慮することで全文検索以上にユーザーが求める検索結果を返すことができるという可能性が示唆された。

今回の比較検証ではケースごとに1つのクエリの結果しかご紹介していないため、ベクトル検索と全文検索の検索結果の傾向を一概に判断することはできません。そのうえで、クエリに誤字を含む場合やクエリが文章である場合など、ベクトル検索が全文検索より優れた結果を返す場面が存在することが示唆されたと言えるでしょう。

おわりに

以上、この記事ではAmazon Bedrockの埋め込みモデルを用いたベクトル検索システムのプロトタイピング、及びベクトル検索と全文検索の検索結果の比較検証を行ってきました。

近年は「ハイブリッド検索」という検索手法に注目が集まっており、全文検索とベクトル検索の検索スコアをRRF*9などのアルゴリズムによって統合することで、両者のメリットを活かした検索結果を得ることのできる可能性があります。そこで、今後は「ハイブリッド検索」システムのプロトタイピングにも取り組んでいく所存です。

また、今回のプロトタイピング結果から、Bedrockの埋め込みモデルによって獲得されたテキストのベクトル表現の有用性が確認できたため、今後は検索以外の用途にも活用を広げていきたいと考えています。コネヒト開発者ブログでは、今後も積極的に技術検証の結果を発信していきますので、ぜひ引き続きご注目ください!

それでは、今回の記事が皆さんのお役に立てれば幸いです。お読みいただきありがとうございました!

コネヒトCTO永井からのコメント

生成AI分野の進歩は、目覚ましいものがあると日々感じています。コネヒトでは、これまでAWSのサービスを基盤として、事業を着実に成長させてきました。AWSの堅牢で柔軟なインフラは、私たちのプロダクト開発において不可欠なものになっています。

そして、今回のBedrockを用いた検証結果には、この技術を用いることがユーザーの検索体験の改善につながるという手応えがありました。さらに将来的には、今回検証したベクトル検索技術とテキスト生成AIを組み合わせて使うことで、これまでにない新たなユーザー体験を提供することができる可能性も感じています。今後もコネヒトではスピード感をもってプロダクトに実装し実験を繰り返しながら、より良いユーザー体験を創造していきたいと考えています。

*1:「基盤モデル」... 広範な用途に使用することのできる大規模なAIモデルを指します。

*2:「RAG(検索拡張生成, Retrieval Augmented Generation)」... 検索技術を用いて生成AIの出力結果を改善する技術です。

*3:「埋め込みモデル」... データをベクトルに変換するためのモデルを指します。

*4:「ベクトル検索」... ベクトル同士の距離(類似性)を計算することで検索を実現する手法です

*5:「全文検索」... テキストデータの中から特定のキーワードを含む文書を検索する手法です。

*6:https://tech.connehito.com/entry/2022/09/16/165655

*7:https://speakerdeck.com/takapy/komiyuniteisabisuniokerurekomendesiyonfalsebian-qian-tomlpaipurainnituite

*8:https://tech.connehito.com/entry/2022/03/24/173719

*9:「RRF (Reciprocal Rank Fusion)」 ... 複数の検索スコアを統合するアルゴリズムの一つです。

ドメイン駆動設計入門の輪読会をやってました!

「コネヒト Advent Calendar 2023」の7日目のブログです!

コネヒト Advent Calendar 2023って?
コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。
コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、
ママの一歩を支えるアプリ「ママリ」などを 運営しています。

adventar.org


こんにちは。サーバーサイドエンジニアをしている高橋です。

以前からDDDに興味がありましたが、なんとなくしか理解しておらずそんな時に 「ドメイン駆動設計入門」の本をお勧めされました。 DDDの考え方は正解がないものなので、メンバーとワイワイしながら読んでみたいと思いこの輪読会を開催しました。

社内でメンバー募集をした時、サーバーサイドエンジニアに限らずインフラ、Androidエンジニア、機械学習エンジニアなど様々な領域の方が手を挙げてくれました。

既に知見がある方もいれば、DDDに興味があるけどわからないという方もいました。


輪読会の流れ

全15章あり、輪読会を開催したのは11月上旬からで、できれば年内に終わらせたいと思い、2章ずつの合計7回で終わるように設計しました。

2章ずつ読んできてもらい、以下の内容を事前にmiroの付箋に記入してもらいます。

輪読会では以下のように進めました。

  1. 順番に付箋を読み上げていく
  2. 深掘りしたい付箋に一人2票投票する
  3. 票が多かった付箋について深掘りして理解を高める

1時間の輪読会なので、なるべく議論できる時間を作りたいと思い、付箋は事前に書いてきてもらうようにしました。 深掘りタイムでは「自分も同じようにどういうことなんだろうと思った」という意見が多くあったので、DDDに知見のあるメンバーに質問して、疑問点を解消していきました。

具体的にどのような深掘りをしていたか一部紹介します。

本の例にあったものは、名前の登録時のバリデーションについて以下のように記載されていました。

名前にはn文字以上の入力が必須 ⇒ エンティティで実装する

同じ名前は登録できない ⇒ ドメインサービスで実装する

ほみ「名前が何文字以上かはエンティティに記載するのに、重複しているかどうかはエンティティに持たせると不自然になるのはなぜか説明できない」

高谷さん 「本だと「コードで書くと不自然になるから」という説明だったが、あんまりしっくりこなかった」

柳村さん 「インスタンス単体で解決可能かどうかじゃないですかね。たとえば高谷さんに高谷さんって他に存在していますかって聞いても分からないですよね。国の台帳かなんか調べないと分からない。」

aboさん 「値オブジェクトの値はチェックできるが、エンティティの存在チェックは自分でできないという理解をした。」

この会話だと柳村さんの説明でメンバーがなるほどなるほどと納得している様子でした。

DDDは誰かが答えを持っている訳ではないので、メンバー同士の対話を通してより理解を深められたと思いました。

メンバーの声

現状半分まで輪読会を終えて、メンバーに以下のようなアンケートを取ってみました。

この輪読会にどのようなことを期待して参加しましたか。

  • ドメイン駆動設計完全に理解した状態になる。
  • 開発をする上でそこまで意識することは正直ないが、概念として押さえておきたいと思ったから。
  • ドメイン駆動設計の大まかな理解と、可能であれば実務に取り入れていきたい。
  • DDDについての理解を深める。
  • モデリングとの実装のつながりを理解する。
  • 他の方と設計に関する知見交流ができると思ったため。

前半戦を終え、現時点で感じていること(難易度等)教えてください。

  • 実装パターンの話は理解が進んでいる一方で、肝心のドメイン周りは大丈夫?という感じ。でもこの本のテーマ的に、実装パターンから理解してドメイン駆動設計の本丸に進むのを怖くなくするみたいな感じっぽいので、それでいうといい感じ。
  • 業務上直接ドメインを意識することは少ないので、十分に理解した状態には至っていないが、参加メンバーに具体例を出してもらうことで納得できる部分はあり、ほんの少しずつだが理解が進んできている気がする。
  • 設計面はDDDの1側面ではあるので、モデリングとセットで学びたい。
  • 現行のコードに落とし込んでいくには慎重になったほうがよさそう。
  • ドメイン駆動設計を実現するための実装パターンが紹介されてはいるもののOOPのデザインパターンのお話しなど他の知識を知らないとよくわからないみたいな部分も少しあるので知見ない方は理解しずらしかもなと思ってました。

後半の輪読会でもっとこうした方がいいとかあれば教えてください。

  • 議事録をいい感じにみんなで取りたいわね。
  • メンバーで時間いっぱい議論に使えているので、良い会が進められている気がする。
  • 本から学びつつ、ママリの事業で実践するにはという会話を増やせるとよさそう。
  • そのためには一定の図解が必要で、miroで図解しながら話してもおもろいかもですね。
  • 輪読会自体の時間でも良いですし輪読会が終わった後でも良いですが改めて全体で深掘りして聞いてみたいことなどあればお話しできる時間や話せる場所があれば良いのかなと思った。同期的でなくともnotionに非同期で質問書くとでも全然あり。

感想

DDDはバックエンド領域の考え方と思っておりましたが、あくまでそれは実装方法の話であり、ドメインを意識して作っていくという意味ではどの領域のエンジニアにも必要なことだと改めて感じました。

比較的初心者でも読みやすいとは思いつつも、DDDの考え方に慣れていないと結構つまずく部分はありました。このような輪読会を通して、他の方の意見でより理解が深まったと思います。後半戦は年内に終わる予定ですので、引き続きやっていきたいと思っております。