every Tech Blog

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

Vue Fes Japan 2024 参加レポート

エブリーは2024年10月19日(土)に開催された Vue Fes Japan 2024 にゴールドスポンサーとして協賛させていただきました。

今回は参加レポートとして、会場の様子やセッションの感想についてお届けします。

イベント概要

Vue Fes Japanは、日本最大のVue.jsカンファレンスです。今年も多くの開発者が集まり、最新のVue.js関連技術や事例について学び、交流する機会となりました。

セッションの感想

1. キーノート

Evan You氏によるキーノートセッションでは、Vue.jsエコシステムの最新動向と将来の展望について、深い洞察が共有されました。

主な注目ポイントは以下の通りです:

  1. Vueフレームワークの最新進展
  2. Nuxt DevToolsの将来像
  3. Viteビルドツールの現状と今後の方向性
    • 現行のesbuild・Rollup・SWC構成から、RolldownとOxcへの移行戦略
    • OxcコンパイラとRolldownバンドラーの性能評価

これらのトピックを中心に、多岐にわたる内容が網羅されました。

個人的に、Evan You 氏が最近設立した Void Zero Inc. に非常に注目しています。

JavaScriptエコシステム全体のために、オープンソースかつ高性能な統合開発ツールチェーンの構築の実現によって、Vue.jsはもとより、JavaScript開発全般にもたらす可能性に大きな期待を寄せています。

2. Vue.js / Nuxt ハンズオン

Vue Fes Japan では、毎年恒例のハンズオン企画として、Vue.js を学び始めたい方向けの教材を提供しています。今年は特別な取り組みがありました。

Nuxt の公式チュートリアル「Nuxt Tutorial」の作者である Anthony Fu 氏と Vue Fes Japan のコラボレーションにより、この公式チュートリアルの日本語版が先行公開されました。このチュートリアルがハンズオン企画の題材として使用されました。

内容は Vue.js の基礎(リアクティビティ、Composition API など)から始まり、Nuxt のコアなコンセプトまでが網羅されていました。

これから Vue.js・Nuxt を学び始めたい方には、このチュートリアルを通じて、より深い理解を得ることができると思います。

learn-nuxt.vuejs-jp.org

3. 次世代フロントエンドクロストーク

次世代フロントエンドクロストークセッションでは、JavaScriptエコシステムの最新動向と課題について活発な議論が展開されました。

主な注目ポイントは以下の通りです:

  1. フロントエンドビルドツールの進化

    • Viteが Vue や React SPA のデファクトスタンダードとして定着
    • Rust製ツール(Oxcコンパイラ、Rolldownバンドラーなど)の台頭
  2. JavaScriptエコシステムのRust化の加速

  3. AIによる大規模コード生成の可能性と課題

これらのトピックを通じて、フロントエンド開発の未来像について多角的な視点が提示されました。

特にRustの重要性が強調されたことで、私自身もRust学習への意欲が大いに刺激されました。このセッションを通じて、フロントエンド開発の将来がより鮮明に見えてきたと感じています。

スポンサーブース紹介

エブリーでは DELISH KITCHEN Web や DELISH KITCHEN チラシ などで Vue.js を採用しています。
いつも Vue コミュニティの恩恵を受けている我々もコミュニティのさらなる盛り上がりに貢献してくべく、スポンサーとして協賛させていただき、ブースも出展させていただきました!

ブース

エブリーでは、今回も弊社が提供するDELISH KITCHENのサービスをイメージしたブースの雰囲気を作りました。多くの方からDELISH KITCHENをを使っていますとの声をかけていただき、とても嬉しかったです。

ノベルティ

今回もDELISH KITCHENにちなんだノベルティを用意させていただきました。

  • ステッカー
  • DELISH KITCHENグッズ
  • CTOブレンドのコーヒーバッグ

DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。参加者の方々にも好評で多くの方に参加していただけました!

アンケート

今回、アンケートでは「Vue について教えて! 」と題して、「Vue の好きなところ」、「Vue の苦労したところ」について回答してもらいました。今回のアンケートでは付箋に自由に記述っしてもらう形式を取り、多くの方から様々な意見をいただくことができました。
回答いただいた多くの皆様、ありがとうございました!

各社スポンサーブースの様子

会場の1階にはスポンサーブースが展開され、各社の趣向を凝らしたブースに多くの人が足を止めていました。
どのブースも、それぞれの会社の特徴を生かした面白い展示が行われており、飽きることなく見て回ることができました。

GMO インターネットグループさんのブースでは、3種類の生成 AI を使って天秤.AI by GMO の Web 画面を Vue.js で出力させた実装と実際の画面を展示して、好きな出力結果のアンケートを行っていました。 生成 AI の利用はとてもホットなトピックなので興味深かったです。筆者の好みは GPT-4 の出力結果でした!

MedPeer さんのブースでは、「握力で技術的負債を粉砕しよう!」と題して、握力測定をすることでノベルティをもらえるという企画を行っていました。握力測定ができるブースは初めて見たのでとても新鮮でした。ちなみに、筆者は 43.6 kg という結果で、無事に学校で体力測定をしていた頃の過去の自分を超えることができました!

まとめ

Vue Fes Japan 2024 にゴールドスポンサーとして協賛できたことを光栄に思います。このイベントを通じて、Vue.js コミュニティの発展に寄与できたことは、私たちにとって大きな喜びです。

多くの方々にエブリーのブースにお立ち寄りいただき、Vue.js の最新トレンドやエブリーのサービスについて活発な議論を交わせたことに、心から感謝申し上げます。皆様との対話は、私たちにとっても大変刺激的で有意義な経験となりました。

今回のイベントでの経験を糧に、エブリーは今後も Vue.js コミュニティのさらなる発展に貢献していく所存です。Vue.js の最新情報やベストプラクティス、そしてエブリーのサービスを通じた実践的な知見を、継続的に発信してまいります。

Amazon Cognitoのユーザーの移行

はじめに

エブリーでソフトウェアエンジニアをしている本丸です。
最近、Amazon Cognitoのユーザープールから別のユーザープールにユーザーを移行する方法について調査する機会がありました。
Amazon Cognitoに関しては色々な箇所で使われていると思いますが、ユーザーの移行について触れる機会はそれほど多くないかと思いますので紹介しようかと思います。

Amazon Cognitoとは

Amazon Cognito(以降Cognitoと表記します)は、AWSが提供するウェブアプリとモバイルアプリ用のアイデンティティプラットフォームです。ユーザーの認証・承認を行うユーザープールとユーザーにAWSリソースへのアクセスを許可するアイデンティティプールを持っています。

DELISH KITCHENでは、ユーザーのメールアドレスの管理とメールアドレスを用いたサインインにCognitoのユーザープールを利用しています。

Lambdaトリガー

CognitoにはLambdaトリガーという機能があり、ユーザープールに対してサインインなどのイベントが発生した時に、それをトリガーとしてLambdaを呼び出すことができます。

公式ドキュメントからの引用ですが、Lambdaトリガーとして設定できるイベントには下記のようなものがあります。

トリガーの種類 説明
認証前の Lambda トリガー サインインリクエストを承認または拒否するカスタム検証
サインアップ前の Lambda トリガー サインアップリクエストを承認または拒否するカスタム検証を実行する
ユーザー移行の Lambda トリガー 既存のユーザーディレクトリからユーザープールにユーザーを移行する
カスタムメッセージの Lambda トリガー メッセージの高度なカスタマイズとローカライズを実行する

Cognitoのユーザープールへのインポート

ユーザープールへのインポート・移行方法は2つ用意されています。CSVを用いた方法とLambdaトリガーを用いた方法です。

CSVを用いたインポート

CSVを用いたインポートでは、指定されたフォーマットのCSVファイルを使用して一括でユーザーのインポートを行います。公式ドキュメントでは低労力で低コストなオプションとして紹介されていました。
こちらの方法では、セキュリティの観点からパスワードのインポートができないようになっています。そのため、移行の際にユーザー側でパスワードの変更が必要になります。

Lambdaトリガーを用いたインポート

Lambdaトリガーを利用したインポートでは、前述したLambdaトリガーを起点にユーザーの移行を行います(上述の表のユーザー移行の Lambda トリガーが今回説明するトリガーです)。このトリガーは、ユーザーがサインインする時とパスワードのリセットを行うときに発火します。
こちらの方法では、パスワードも連携されるのですが認証フローにUSER_PASSWORD_AUTHまたはADMIN_USER_PASSWORD_AUTHを指定し、ユーザー名とパスワードによる認証を行わなければならない点に注意です。 少しイメージしにくいかと思うので、次でもう少し詳細に説明します。

Lambdaトリガーを用いたユーザーの移行の実装

Lambdaトリガーを用いたユーザー移行の流れはおおよそ図のようになります。

ユーザーを移行したい先のアプリケーションでサインインもしくはパスワードリセットが呼び出されたのをトリガーにしてユーザー移行のLambda(図のuser migration lambda)が呼び出されます。
ユーザー移行のLambdaの実装は次のようになります。

import { 
  CognitoIdentityProviderClient,
  AdminInitiateAuthCommand,
  AdminGetUserCommand,
  CognitoIdentityProviderClientConfig,
  UserNotConfirmedException
} from "@aws-sdk/client-cognito-identity-provider";
import { UserMigrationTriggerHandler } from "aws-lambda";

const userMigration: UserMigrationTriggerHandler = async (event) => {
    const config: CognitoIdentityProviderClientConfig = {
        region: 'ap-northeast-1',
    };
    const client = new CognitoIdentityProviderClient(config);

    if(event.triggerSource == "UserMigration_Authentication") {
        const adminInitiateAuthCommand = new AdminInitiateAuthCommand({
            UserPoolId: ${USER_POOL_ID}, 
            ClientId: ${CLIENT_ID},
            AuthFlow: "ADMIN_USER_PASSWORD_AUTH", 
            AuthParameters: {
                "USERNAME": event.userName,
                "PASSWORD": event.request.password,
            },
        });
        // 認証できるかチェック
        try {
            await client.send(adminInitiateAuthCommand)
        } catch (e) {
            console.log(`user auth failed: ${e.message}`);
            throw e;
        }

        // cognitoに登録するユーザー情報構築
        const adminGetUserCommand = new AdminGetUserCommand({
            UserPoolId: process.env.DK_USER_POOL_ID,
            Username: event.userName,
        });

        try {
            const response = await client.send(adminGetUserCommand);
            // 移行先のユーザーに持たせたい情報を詰め込む
            event.response.userAttributes = {
                "email": response.UserAttributes.find((attr) => attr.Name === "email")?.Value ?? "",
                "email_verified": response.UserAttributes.find((attr) => attr.Name === "email_verified")?.Value ?? "false",
            };
            // 検証メールを送信しないため、下記を指定する
            event.response.messageAction = "SUPPRESS";
            event.response.finalUserStatus = "CONFIRMED";
            return event;
        } catch (e) {
            console.log(`get user failed: ${e.message}`);
            throw e;
        }
    }
    return event;
};

このユーザー移行のLambdaの中で、移行元となるCognitoでユーザーの認証を行い、認証が成功した場合にユーザーの情報を取得します。その情報を移行先のCognitoに返すことでユーザーの移行を行います。 event.responseに渡すデータを変更することで移行先のユーザーに持たせたい情報を変更したり、ユーザーがそのメールアドレスの正当な所有者であるかを確認するメールを送信するかなどを操作することができます。

まとめ

Cognitoのユーザーの移行方法を調査して、ユーザーの移行を行うためにAWS公式で便利な機能が用意されていることを知ることができました。
2つの方法にそれぞれメリット・デメリットがあるかと思うので適切に使うようにしていきたいと思います。

参考資料

Vue Fes Japan 2024直前!エブリーのブース紹介とイベント情報

こんにちは、DevEnableグループの羽馬です。

いよいよ日本最大のVue.js開発者カンファレンス「Vue Fes Japan 2024」の開催が目前に迫りました!

10月19日に開催されるこのイベントは、Vue.jsエコシステムの最新トレンドや先端技術が一堂に会する、開発者必見の祭典です。

エブリーは今年初めてゴールドスポンサーとして協賛し、Vue.jsコミュニティの皆様との交流を心待ちにしています。

この記事では、エブリーのブース内容の詳細や、カンファレンスの前後に開催するVue Fes Japan 2024関連イベントについてご紹介します。Vue.jsに興味がある方、エブリーの技術や文化に触れてみたい方は、ぜひ最後までお読みください!

エブリーのブース紹介

エブリーのブースでは、開発組織やサービスの魅力を存分に感じていただけるよう、様々な工夫を凝らしています。

魅力的なノベルティ

  • DELISH KITCHEN のオリジナルキッチングッズ
  • 先着限定オリジナルタンブラー
  • CTOオリジナルブレンドコーヒーバッグ

特別企画:every CTO Blend

今回特に注目いただきたいのが、オリジナルでブレンドしたコーヒー「every CTO Blend」のコーヒーバッグです。

  • CTOが厳選したブレンド豆使用
  • 香り高い味わいをそのままコーヒーバッグに

コーヒーバッグ制作の裏話や想いは、以下の記事で詳しくご紹介しています。ぜひご覧になってください!

人々へ明るい変化を提供する、オリジナルブレンドコーヒー「every CTO Blend」を制作

エブリーのブースにはお気軽にお立ち寄りください。技術の話題はもちろん、エブリーの魅力や開発文化について、私たちスタッフが熱意を持ってお伝えします。皆様とお会いできることを心より楽しみにしています。

Vue Fes Japan 2024関連イベントのご案内

Vue Fes Japan 2024の前後に、協賛企業の皆様と共催して特別イベントを開催します。これらのイベントは、Vue.jsコミュニティの交流をさらに深める絶好の機会です。

Vue Fes Japan 2024 Pre LT Party

Vue Fes Japan 2024 After Night

両イベントとも公募LT枠をご用意していますので、ぜひご参加ください!

さいごに

Vue Fes Japan 2024は、Vue.js開発者コミュニティにとって貴重な学びと交流の場です。エブリーは、このイベントを通じて日本のVue.js開発の発展に貢献できることを光栄に思います。

Vue.jsに関心のある方、エブリーの技術や文化に興味をお持ちの方、ぜひVue Fes Japan 2024にご参加ください。エブリーのブースでお会いできることを心よりお待ちしています!

生成AIを利用したLP実装の自動生成に挑戦してみた

生成AIを利用したLP実装の自動生成に挑戦してみた

目次

はじめに

こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司(ktanonymous)です。
今回の記事では、生成AIを利用してトモニテが公開しているLPの自動生成に挑戦してみた時の話をしたいと思います。 (※2024年7月下旬〜8月上旬時点での話になります。)

自動生成しようと考えた背景

まずはじめに、なぜLPの自動生成をしようと考えたのか、その背景を説明したいと思います。

トモニテが公開しているLPについて

トモニテでは、企業様と提携したLPを複数公開しています。

LPの例
LPの例

これらのLPは、TypeScript/React/Next.js を利用して実装されており、microCMS1 を利用してコンテンツを管理しています。

各LPは静的ページとしてビルドされ、S3 にデプロイ、CloudFront を経由して配信されます。
また、各LPの仕様書は SpreadSheet/Figma にて管理されており、各LPの実装は仕様書を元に行われています。 基本的には、ビジネスサイドが仕様書を作成し、デザイナーがデザインを作成し、エンジニアが実装を行うというようなフローとなっています。

LP公開までのフロー

既存のLP実装の課題

全てのLPで共通しているパラメータや画像パーツなどのコンテンツは、microCMS を利用して設定できるようになっています。 また、各LPで利用するフォーマットやコンポーネント(プルダウン、選択パネルなど)は一定共通化されていますが、それぞれのLPに合わせて設問や表示方法などが異なっています。
そのため、新しいLPを作成する際には、既存のLPをコピーした後で仕様書を元に細かいチューニングを行う形で開発が行われています。

事業の拡大を目指す中で、LPの数も増えていき、LPの開発・運用に要する人的/時間的リソースの増加がジワジワと事業促進におけるボトルネックとして感じられるようになってきています。
そこで、エンジニアの開発工数やビジネスサイドの確認工数を削減し、事業のスピードアップを図るために生成AIを利用したLPの自動生成を検討しました。

生成AIによるLP実装の自動生成

LP自動生成のためのアプローチ

今回検討した生成AIによるLPの自動生成では、以下のようなフローをイメージしました。

  1. 既存のLPの仕様書および実装を AI モデルに embedding で学習させる。
  2. 新規LPの仕様書およびプロンプトを AI モデルに入力し、LPの実装を生成する。

LP自動生成フロー

生成AIを利用するにあたって、詳細なモデルを決定する以前に、ChatGPT2のようなオープンなモデルを利用するのかローカルLLMを利用するのかという観点があります。 ローカルLLMはモデルの性能が低めであったり実際に使用しているマシンのスペックが影響したりするため精度は落ちがちですが、今回は新しくコストをかけたくないという要望があったので、ローカルLLMを利用することにしました。

実際に自動生成してみて

実際にLPの自動生成を行ってみた時のことについて説明したいと思います。

ローカルLLMに関しては、様々なモデルをシンプルに利用することのできる Ollama3 を採用しました。Ollama は、モデルの重みを量子化して推論を高速化することで CPU レベルのスペックでも LLM を扱えるようにした Llama.cpp4 をラップしてローカルでも動かせるようにしてくれるツールです。 ちなみに、 Ollama で利用できるモデルはこちらから確認することができます。

実際に Ollama を組み込むために、以下の2つのパターンを試してみました。

  1. Open WebUI5 (ローカルで動かせる ChatGPT のような AI チャットツール)
  2. LlamaIndex.TS6 (LLM を組み込んだアプリケーションを開発するための TypeScript 向けフレームワーク)

OpenWebUI を利用した場合

Open WebUI + Ollama による LLM 開発環境を構築できるコンテナイメージが公開されているため、以下のコマンドで簡単にローカル環境を構築できます7

$ docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama

上記のコマンドでコンテナを起動しておいてブラウザから http://localhost:3000 にアクセスするとOpen WebUI を利用した AI チャットツールが利用できます。 設定画面から Embedding のようなモデルの設定などが行えます。

Open WebUI のチャット画面
Open WebUI のチャット画面(Open WebUI 公式サイト

モデルやプロンプトに関しては色々と調整してみましたが、以下のプロンプトを llama3.1 に渡した時に一番良さそうなレスポンスが返ってきました。

プロンプト

`ExistingLP.tsx` was created referencing `existing_lp_specification.csv`.\nLike that, create `NewLP.tsx` referencing `new_lp_specification.csv`.\nYou must show me full implementation.

生成された実装

import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import EntryAPI from '../api/EntryAPI';

export default function NewLP({ campaign }: Props) {
  const [isError, ToggleError] = useState(false);
  const [isSubmit, ToggleSubmit] = useState(false);
  const router = useRouter();
  const methods = useForm({ shouldFocusError: false });
  const watch = methods.watch();
  const entryAPI = new EntryAPI();

  const onSubmit = (data: DoNotUseAnyType) => {
    ToggleSubmit(true);
    const refererParams = getCommonEventParams();
    const answers = {
      email: data.email,
      gender: data.gender,
      ...  // レスポンスではコードが記述されていましたが省略しています
      // Additional fields from the CSV file
      venue: data.venue,
      experience: data.experience,
    };

    entryAPI.submitEntry(answers, refererParams)
      .then((response) => {
        console.log(response);
        router.push('/thank-you');
      })
      .catch((error) => {
        ToggleError(true);
        console.error(error);
      });
  };

  return (
    <div>
      {isSubmit ? (
        'Thank you for submitting your entry!'
      ) : (
        <form onSubmit={methods.handleSubmit(onSubmit)}>

          ...  // レスポンスではコードが記述されていましたが省略しています

          <label>
            Email:
            <input type="email" name="email" {...methods.register('email')} />
          </label>

          <label>
            Gender:
            <select name="gender" {...methods.register('gender')}>
              {genders.map((gender) => (
                <option key={gender} value={gender}>
                  {gender}
                </option>
              ))}
            </select>
          </label>

          ...  // レスポンスではコードが記述されていましたが省略しています

          <button type="submit">Submit</button>
        </form>
      )}
    </div>
  );
}

とりあえず画面を表示するくらいはできそうな実装が返ってきました。 実際のLPの実装や仕様書をお見せできないので伝わりにくいのですが、既存の実装を踏襲できていない部分や仕様書の内容を反映できていない部分が多くありました。

LlamaIndex.TS を利用した場合

次に、LlamaIndex.TS を利用してローカルで LLM を利用する方法を試してみました。 既存のLPがTypeScriptで実装されているので、組み込みやすいように TypeScript 向けのフレームワークを利用することにしました。 LlamaIndex.TS を利用して TypeScript で Ollama を動かす方法に関しては公式のチュートリアル8が参考になります(Node.js v18からしか対応していません9)。
ここでは、新しいページを作成して、そのページでLLMとのやりとりを行えるようにしようと考えました。 実際の画面や詳細は割愛しますが、このやり方でもあまり良い結果は得られませんでした。

新しいページでLLMを動かすための実装 結果が芳しくなかったので、クエリをページ上から入力できるようにすることまではしていません。

LLM とのチャットページの実装 (src/pages/llm.tsx)

import { useState } from "react";

const LpGenerator = () => {
  // ボタンをクリックするとmain関数が実行されるページ
  const [query, setQuery] = useState("What did the author do in college?");
  const [result, setResult] = useState("");

  const onClick = async (e: any) => {
    e.preventDefault();
    const response = await fetch('/api/query_llm', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ query }),
    });
    const data = await response.json();
    setResult(data.result);
    console.log(result);
  }

  return(
    <>
      <p>LP Generator</p>
      <button onClick={onClick}>Run LLM Query</button>
      {result && <p>{result}</p>}
    </>
  )
};

export default LpGenerator;

API ハンドラーの実装 (src/pages/api/query_llm.ts)

import type { NextApiRequest, NextApiResponse } from 'next';
import QueryLLMAPI from 'api/query_llm';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { query } = req.body;
  const queryLLMAPI = new QueryLLMAPI();
  const result = await queryLLMAPI.post(query);
  res.status(200).json({ result });
}

LLM とのやりとりを行う API の実装 (src/api/query_llm.ts)

import fs from "fs/promises";
import { Document, HuggingFaceEmbedding, Ollama, Settings, VectorStoreIndex } from 'llamaindex';

interface IQueryLLMAPI {
  post(query: string): Promise<string>;
}

export default class QueryLLMAPI implements IQueryLLMAPI {
  constructor() {
    Settings.llm = new Ollama({ model: "llama3.1:8b" });
    Settings.embedModel = new HuggingFaceEmbedding({ modelType: "BAAI/bge-small-en-v1.5", quantized: false });
  }
  async post(query: string): Promise<string> {
    // テキストの読み込みと処理
    console.log("Reading text...");
    const path = "node_modules/llamaindex/examples/abramov.txt";
    const essay = await fs.readFile(path, "utf-8");

    // Documentの生成とインデックスの作成
    console.log("Creating document and index...");
    const document = new Document({ text: essay, id_: path });
    const index = await VectorStoreIndex.fromDocuments([document]);

    // クエリエンジンでクエリを実行
    console.log("Querying...");
    const queryEngine = index.asQueryEngine();
    const response = await queryEngine.query({ query: query });

    // 結果をクライアントに返す
    return response.toString();
  }
}

そのほかのアプローチ

ローカル LLM である Ollama を利用する以外にも、GitHub copilot や OpenAI を利用したアプローチも検討しました(弊社では既に利用可能な状況だったため、一旦「新しい」コストは発生していないという体で考えていました)。 しかし、残念ながら、これでも工数改善に繋がるようなクオリティの生成結果を得ることはできませんでした。

まとめ

今回の記事では、生成AIを利用してトモニテが公開しているLPの自動生成に挑戦してみた時の話をしました。
結果としては、今回の検証では、生成AIを利用してLPの自動生成を行うことは難しいという結論に至りました。 しかし、仕様書のフォーマットや既存実装の渡し方、推論プロセスなど改善できそうな点はまだあると思っています。

大きめの実装を実際に運用できるようなクオリティで生成させることは簡単ではないかと思いますが、今後の AI 技術の発展も含めて、引き続き期待したい領域だと感じました。

LP運用に関しては、引き続き改善を進めていきたいと考えているので、進展があった時にはまた記事にできたらと思います。
最後まで読んでいただき、ありがとうございました。

SQLでそれっぽく異常検知してみる

レシピメディアにおいて、たとえば検索数推移のような時系列的なデータを扱っていると、急激に検索数が伸びているワードを捕捉したいシーンがあります。 要因はものによって違いますが、これをSQLだけで完結してなるべく楽したい。が今回の目的です。

要するに異常検知をすることが目的なのですが、「上昇率がX%以上を検知する」ような単純なモデルではないが、ある程度統計的な根拠をもとに検知が可能で、かつPythonのライブラリをつかうほど複雑ではなくSQL上でわかりやすく書けることを主眼に置きます。

方針としては以下です。

  • 特定ワードにおける検索数の時系列データが正規分布に従うと仮定する
  • 特定ウィンドウにおける検索数の移動平均と、標準偏差を抽出し、有意水準を5%などとし、逸脱したものを異常値とみなす

イメージはこんな感じです。各週ごとに適正な範囲を求めて、そこを逸脱した値を異常値としてみなします

本来は標準化すべきですが、簡単のため標準化処理は行わずに進めます。 と、いうことで早速やっていきます。異常検知までの全体的なステップは以下です。

  1. 特定ワードにおける週ごとの検索数を抽出する
  2. 4週間ごとの検索数の移動平均を抽出
  3. 4週間ごとの検索数の標準偏差(σ)を抽出
  4. 移動平均±2σを抽出
  5. 現在週の値を判定する

まず今回想定するデータソースはこんな感じです。仮に search_data というテーブルに入っているとして進めて行きます。

  • date: 日付
  • query: 検索ワード
  • num_of_searches: 検索数

まず特定ワード xxx の検索数を抽出します。今回は最終的に週ごとの移動平均を取りたいので、週ごとの集計にします。 なお実行環境はPrestoとします。適宜ご自身の扱っているSQLに読み替えてご確認ください。

▼検索数の抽出

with
    weekly_data as (
        select
            week(date) as week
          , sum(num_of_searches) as total_searches
        from
            search_data
        where
            q = 'xxx'
        group by
            week
    )

次に移動平均と標準偏差を抽出します。ウィンドウ関数を使います。

▼移動平均と標準偏差の抽出

    stats as (
        select 
            week
            total_searches
          , avg(total_searches) over (order by week rows between 4 preceding and 1 preceding row) as moving_average
          , stddev(total_searches) over (order by week rows between 4 preceding and 1 preceding row) as moving_stddev
        from 
            weekly_data
    )

次に移動平均±2σを抽出します。先程のクエリで同時に計算してもいいですが、若干わかりづらくなるので、こちらで移動平均±2σを抽出します。これにより、異常値の範囲を明確にすることができます。

▼移動平均±2σの抽出

  , bounds as (
        select 
            week
          , total_seaches
          , moving_average
          , moving_stddev
          , moving_average + 2 * moving_stddev as upper_bound
          , moving_average - 2 * moving_stddev as lower_bound
        from 
            stats
    )

これで準備ができました。過去4週間の移動平均±2σと今週の値を見比べてみましょう。現在週の検索数がこの範囲を超えているかどうかを確認することで、異常値を検知することができます。

▼異常検知する

select
    total_searches > upper_bound or total_searches < lower_bound as is_anomaly
from
    bounds

これで異常値を検出できます! 今回のクエリを実行することで、現在の検索数が過去のトレンドから逸脱しているかどうかを簡単に確認できます。今回はSQLでやりましたが、どちらかと言うとスプレッドシートのほうがより簡単にできそうな気もしてきました。スプレッドシートでは、関数を使って同様の計算を行うことができるため、視覚的にデータを確認しやすいという利点があります。

是非ご自身で試してみてください。データの異常検知は、ビジネスの意思決定において非常に重要な要素ですので、さまざまな手法を試してみることをお勧めします。