RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

クエリ実行中断の実装

はじめに

こんにちは、DevOps グループの中島です。

当社ではデータ分析のために Redash を利用しており、
DevOps グループではその管理・運用を行っています。
日々、Redash が重いとかクエリが返ってこない等の問い合わせに対応する中で、
その原因の一部について調査した内容を紹介します。

発生した問題

事象としては発行したクエリが返ってこないという問い合わせで、
調べてみると接続先のデータベース (Aurora MySQL 5.8) の
CPU 使用率が 100% 付近になっていました。
show processlist で実行中のクエリを確認すると、
長時間実行中のクエリが複数存在しており、これが原因でした。

問題の対応

運用者の権限であれば、Redash の画面から実行中のクエリを見ることができ、
ここでクエリ実行のキャンセルをすることが出来ます。
しかし、キャンセルボタンを押してもなかなか消えません。
結局データベースに直接接続して、対象のクエリを KILL することで
CPU 使用率は正常に戻りました。

クエリが中断されない理由

ではなぜキャンセルボタンを押しても実行中のクエリが消えないのか
という点が気になってくるところです。

Redash のコードを見てみたところ、キャンセル処理は SQL の実行を担当する
ワーカープロセスに対して SIGINT を送信することで行っていました。
MySQL 用のクエリ実行処理を抜粋するとおおむね以下のようになっています。
(当社で利用している過去バージョンのものです)

try:
    connection = MySQLdb.connect(...),
    cursor = connection.cursor()
    cursor.execute(query)

    data = cursor.fetchall()

    while cursor.nextset():
        data = cursor.fetchall()
    cursor.close()

except KeyboardInterrupt:
    cursor.close()
    error = "Query cancelled by user."
    json_data = None

finally:
    if connection:
        connection.close()

キャンセル実行時には SIGINTKeyboardInterrupt を発生させ、
cursorconnection を閉じて終了するという意図が読み取れます。
しかし、クエリ実行が中断されないということは、
このコードがうまく動作していないと考えられます。

そこで、以下のような実験用のコードを書いて試してみます。
データベースに select sleep(100) を発行し、
SIGINT を受け取ったら終了するコードです。

import MySQLdb

conn = MySQLdb.connect(...)
cur = conn.cursor()

try:
    sql = "select sleep(100)"
    cur.execute(sql)
    rows = cur.fetchall()
    for row in rows:
         print(row)
except KeyboardInterrupt:
    print('KeyboardInterrupt')

Ctrl + C で SIGINT を送信したところ、プログラムが反応せず、
except KeyboardInterrupt に入ることはありませんでした。
このため、Redash 上からキャンセルボタンを押しても、
クエリの実行は止まらなかったと考えることができます。

MySQLdb は内部で MySQL C API を呼び出すことで MySQL にクエリを発行していますが、
C の処理に制御がある状態では SIGINT のシグナルが受け付けられない、
あるいは C の方にシグナルの処理を明示的に書く必要があるのかもしれません。
上記コードの try の内部において、C で書いた無限ループを呼び出すコードを
試したところ、同様の挙動となったためそのように推測しました。

クエリ実行中断の方法?

話は変わりますが、最近似たような問題がサービス中に発生しました。
内容としては、リクエストを受け付けたアプリケーションが、
データベースへのクエリ実行に時間がかかりすぎていて、
アプリケーションの入り口で設定したタイムアウトになったあとも
クエリがそのまま実行され続け、同じリクエストが何度も行われ
データベースの CPU 使用率が高騰するというものです。

この場合、タイムアウトとなったあとにアプリケーションからクエリを
中断させれば良いのでは、 と 単純にはそう考えられます*1

ここで疑問に思ったのは、そもそもクエリの実行中断というのは
一般的にどのようにするものなのか、ということです。
私が想像していたのは MySQLプロトコルであれば、張ったコネクション上で
それ用のコマンドを送信すればサーバがクエリ実行を
中断してくれるようなものだと思っていました。

クエリ実行中断の一般的な実装

そこで、代表的な例として mysql コマンド ではどのようになっているかを
見てみたところ、クエリ実行中のコネクションとは別の新たなコネクションを確立して、
KILL QUERY コマンドを発行することでクエリを中断していました。(参考)
もう少しエレガントな方法でやっているのかと思っていたのに、
案外泥臭いことをやっているなという感想を持ちました。

一方、PostgreSQL ではどうなっているかを見てみたところ、
C の API である libpq では中断用の APIPQcancel として存在しています。
ただ、この中の処理でも同様に新しいコネクションを確立して
クエリを KILL する実装がされていました。(参考)
こちらは、ドキュメントが存在していて、それによると、
クエリ実行中も入力を随時確認しないといけなくなるので、
処理効率上そのようにしているとのことです。

なるほど、わりと一般的なやり方なのかと認識を改めました。

終わりに

クエリの実行中断について、気になった部分を調べてみました。
この記事が何かのお役に立てば幸いです。

We're hiring!
弊社では、特に DevOps グループでは、
一緒に働いてくださるエンジニアを大募集しています。
rarejob-tech.co.jp

*1:本来はタイムアウトを指定して SQL を発行するなどして、データベース側から切断できるようにするべきです。

Terraform運用の個人的ベストプラクティス

こんにちは、EdTech Labの齋藤です。

スパイダーマン:アクロス・ザ・スパイダーバースをみましたが私には複数の並行世界をどうこうする大いなる力はないので、AWSで複数の環境をいじっている話をしようかなと思います。

というわけで、この一年ちょっとTerraform でAWSリソースの管理・運用をしてきて個人的にやりやすい方法をまとめました。 あくまで私の場合には何が肌に合って・合わなかったかという話なので、そのあたりご了承ください。

なぜTerraform?

AWSをdevelopment・staging・productionのように複数環境で構築してそれを管理したいなと考えたときインフラ素人としては、複数環境をよしなに管理できて・学習コストが低いものが嬉しいなと思いそれらの条件を満たしているのがTerraformだったためTerraformを利用しています。

また、もともとEdTechLabではTerraformで一部のAWSリソースの管理・運用が行われていたということもあり、チーム内に知見がたまっておりとっつきやすかったということも理由の一つです。

個人的ベストプラクティス

module vs workspace

個人的にはmoduleがとても使いやすく、moduleを作成し、利用したい環境分のディレクトリを用意する構成で管理しています。 具体的な構成例は以下のような感じです。

_____
|---aws
|        |---env_name
|        |            |---aws.tf
|        |            |---main.tf
|        |            |---variables.tf
|
|---modules
|        |---resource_name
|        |            |---main.tf
|        |            |---output.tf
|        |            |---variables.tf

env_nameを利用したい環境分作っておき、それぞれのvariables.tfでリソースの数値やvpc_idなどの環境ごとに変えたいものを管理するという方向性です。

このような管理にすることで、env_namevariables.tfの設定を変更するだけで、複数環境に同じリソースを簡単に作成できる上、ちょっとした変更もしやすくなったと感じています。

moduleの設計は 実践Terraform AWSにおけるシステム設計とベストプラクティス に挙げられている

  • Small is beautiful
    • moduleは小さくシンプルにする
  • 疎結合
    • module同士で依存しないようにする
  • 高凝集
    • 変更の理由・タイミングが同じものはまとめ、不要なリソースはmodule化しないようにする
  • 認知的負荷
    • moduleの入力値から必須項目を減らし、オプション項目を増やすことで認知的負荷を減らす

を意識して設計すると良いものになると思います。

さて、このような構成に至る前にworkspaceも利用したのですが、個人的に以下のことが気になりました。

自分が今どのワークスペースにいるのかぱっと見でわかりにくい

terraform workspace showして違うならselect で環境を切り替えるなどapplyにたどり着くまでのコマンド量が地味に多くめんどくさいなと感じることがありました。

というのも、自分が利用しているターミナルの設定ではカレントディレクトリが常に表示されているようにしているので、そこを見ればいまどの環境にapplyされるのかがわかるという状況が好ましいと思っていたためです。

環境ごとに違う設定にしたいときの柔軟性が低い

作業の都合で特定の環境のみにつけたい権限などが発生した場合にcount = terraform.workspace == env_nameみたいな判定を入れる必要があったという点と、このやり方で制御してしまうと本番環境のみに適用したいけどいきなり本番環境はちょっと心配という状況で変更箇所がむやみに増えてしまう上に、countで環境別のリソース制御を行っていくのは見通しが悪いと感じていました。

また、変更を入れたい環境のディレクトリにいてもworkspaceは切り替わっていないためここでも「自分が今どのワークスペースにいるのかぱっと見でわかりにくい」問題が発生しました。

この2点を解消したのがmoduleでの管理+ディレクトリを環境ごとに分離という方法でした。

s3にtfstateを保存しておく

なんというか、当然のことではあると思うのですが一度設定を忘れて辛い目にあったことがあるので戒めを込めて書きます。

環境を作り始めたころ作っていた環境を破壊しかけて、tfstateをS3に保存していなかったばかりに環境を復元するのに無駄な時間を使ってしまい悲しい思いをしました。

なので各環境でローカルではない場所にちゃんとtfstateを保存してバージョンなどを適切に管理しましょう。また、その際管理するバケットはTerraform公式ドキュメントでも記載してある通り、Terraformで管理しないようにしてください。Terraform触っていてミスって消してしまったりしたら元も子もなくなってしまうので...

最後に

いろいろやり方あると思いますが、複数環境を管理するという条件では今のところこのような形がしっくりきています。moduleとしてもterraform importはできるので構成さえ決まってしまえば、コンソールから作成したものや既存のリソースであってもそこそこ楽にmodule化できるので「とりあえず触ってみる」がやり易いのもいい点でした。

いろいろな職種で一緒に働いてくださるエンジニアを募集していますので興味ある方は是非! rarejob-tech.co.jp

LangChainのふんいき(LangChain触ってみたN番煎じ)

導入

こんにちは。

プロダクト開発部 PROGOS・SMART Method 開発グループの奥山です。

レアジョブテクノロジーズでは月1回、知見発表会があります。
月毎に部署が割り当てられ、各々が知見を発表していきます。
この会で紹介する内容に縛りはなく、最近自分が遊んだ技術を紹介することも可能です。
今回は、この知見発表会でゆるく話したLangChainのスライドをそのままだとネタスライドがあって出せないのでブログ記事に改変してお送りします。

何が書いてあるか?

LangChainの小さなコードを積み上げていって、1回だけ質問に答えてくれるコードを書きます。

読む意味はあるか?

私の理解はLangChainのドキュメントの土地勘がちょっと生えたレベルです。
よって、LangChainドキュメントの土地勘を生やすのに少し役立つかもしれません。
網羅的に扱っていないため、全てを解説したドキュメントをお探しの場合は公式をご覧ください。

注意点

ゆるいです。

概要

  1. LangChainのイメージをゆるく説明します
  2. 環境構築します
  3. コードを小さく積み上げていきます

LangChainのゆるい説明

ウェルカムページにはこう書かれてます。

LangChain is a framework for developing applications powered by language models. We believe that the most powerful and differentiated applications will not only call out to a language model

言語モデルを利用して開発する際に利用するフレームワークとして開発されたようです。

言語モデルといえば、OpenAIのGPT系などでしょうか。

フレームワークといえば、開発する際に色々と便利なツールや機能のカタマリ、みたいなイメージでしょうか。

言語モデルを用いた開発が手軽にできるんだな〜というイメージを一旦持っておくことにします。

https://python.langchain.com/en/latest/index.html

環境構築

python3系が入っていることが前提です。 自分は下記で構築しました。

pip install langchain
pip install openai
export OPENAI_API_KEY="..."

OPENAI_API_KEYはご自身でOpenAIのアカウントを作成し入手してください。

おすすめの読み方

ドキュメントの土地勘をふんわり掴むことを念頭に置いてるので、貼られたリンクを踏みながら目次の場所を眺めます。
そのあとサンプルコードをコピペか写経するかして動かしてみてください。

Step1 Language Modelsを利用する

公式はこちらです。
https://python.langchain.com/docs/modules/model_io/models/

Language Modelsの項目にはLLMsとChatModelsが見えていると思います。

LangChain Language Models 目次
両方とも、言語モデルを提供しているAPIをラップしています。 実際にこのStepで紹介するOpenAIクラスを覗いてみます。

OpenAIクラス(597行目)の実装を見てみると、BaseOpenAIクラス(123行目)を継承してるので、そこを読みにいきます。 https://github.com/hwchase17/langchain/blob/e9877ea8b1d301efafb7d2fda5f9105b005774b8/langchain/llms/openai.py

利用している言語モデルはtext-davinci-003。

model_name: str = "text-davinci-003"

エンドポイントはcompletionのようです。

values["client"] = openai.Completion

https://platform.openai.com/docs/api-reference/completions

では、LLMsを実際に利用します。

from langchain.llms import OpenAI

llm = OpenAI(temperature=0.9)

prompt = "英会話サービス運営企業のバックオフィス用に開発したチャットボットに名前をつけてください。"
print(llm(prompt))

結果例

NiceTalker.

LangChainにはLLMsの他に、ChatModelsも用意されてます。

https://python.langchain.com/docs/modules/model_io/models/chat/

今回利用するChatOpenAIクラスは、gpt3.5-turboモデルがデフォで、chat/completionsをラップしているようです。

ChatModelクラスは下記の通りに利用できます。

from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage

chat = ChatOpenAI(temperature=0.0)

prompt = "英会話サービス運営企業のバックオフィス用に開発したチャットボットに名前をつけてください。"

result = chat([HumanMessage(content=prompt)])

print(result)

結果例

content='「ChatPal」という名前を提案します。' additional_kwargs={} example=False

また、LLMsもChatModelsも、引数temperatureを0に設定すると、何度実行しても同じ結果が戻ります。

Step2 PromptTemplateを利用する

公式ドキュメントはこちらです。 https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/

説明するよりサンプルをご覧になった方が早いかと思います。

from langchain.prompts import PromptTemplate

prompt = PromptTemplate.from_template("{product}の名前は何が良いでしょうか?")
resutl1 = prompt.format(product="子供向け英会話サービス")

prompt = PromptTemplate.from_template("{adjective}{product}の名前は何が良いでしょうか?")
result2 = prompt.format(adjective="かわいい", product="子供向け英会話サービス")

print(resutl1)
print(result2)

結果例

子供向け英会話サービスの名前は何が良いでしょうか?
かわいい子供向け英会話サービスの名前は何が良いでしょうか?

プロンプトの一部を虫食い状態にできるので、プロンプトを抽象化して利用可能であることがポイントです。

Step3 Chainsを利用する

公式ドキュメントはこちらです。

https://python.langchain.com/docs/modules/chains/

このサンプルではLLMChainというクラスを利用します。 引数にModelsとTemplateを渡して動かします。 Modelsは、単に私がChatOpenAIクラスをなんとなく好きなので利用してます。

from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.chat_models import ChatOpenAI

prompt = PromptTemplate(
    input_variables=["product", "something"],
    template="{product}の名前は何が良いでしょうか?また、{something}も考えてください。",
)

llm = ChatOpenAI(temperature=0.9)

chain = LLMChain(llm=llm, prompt=prompt)

print(chain.run({
    "product": "必ず喋れることをコンセプトにした英会話サービス", 
    "something": "コンセプト"
    }))

結果例

1. "SureSpeak": コンセプトは、自信を持って会話ができることを強調し、利用者に自信と満足感を与えること。
2. "TalkMaster":コンセプトは、利用者が英語のマスターになり、自在に会話ができることを強調。
3. "FluentChat":コンセプトは、流暢な英会話ができることを強調し、利用者が自然な会話を楽しめること。
4. "ConverseEase":コンセプトは、利用者が英会話を簡単に行えることを強調し、利用者の学習体験をスムーズにすること。
5. "TalkSure":コンセプトは、利用者が自信を持って英会話ができることを強調し、利用者の自己表現能力向上を支援すること。

ModelsとPromptTemplatesをChainsのコンストラクタ引数として渡すことで利用可能です。

Step4 Document Loadersを利用する

公式ドキュメントはこちらです。

https://python.langchain.com/docs/modules/data_connection/document_loaders/

名前の通り、ドキュメントをロードします。
まずは利用方法を見てみます。
今回はWebからドキュメントを取得してみます。
弊社の主力プロダクト、レアジョブ英会話のLPから取得してみます。

www.rarejob.com

下準備として、下記コマンドを打っておきます。 このコマンドは、今回利用するPlaywrightURLLoaderのドキュメントから拝借してます。

https://python.langchain.com/docs/modules/data_connection/document_loaders/integrations/url

pip install "playwright"
pip install "unstructured"
playwright install
from langchain.document_loaders import PlaywrightURLLoader

urls = [
    "https://www.rarejob.com/"
]
loader = PlaywrightURLLoader(urls=urls, remove_selectors=["header", "footer"])
data = loader.load()

print(data)

出力例

[Document(page_content='オンライン英会話No.1 レアジョブ英会話\n\n無料登録\n\nログイン\n\n法人向け|\n\n学校向け|\n\n初めての方へ\n                        体験レッスンとは\n                        講師について\n                        ご利用の流れ\n                        レッスンルームについて\n                        お客様の声\n\n料金プラン\n\nサービス\n                        日常英会話コース\n                        ビジネス英会話コース\n                        中学・高校生コース\n                        スピーキングテスト\n                        レッスンチケット\n                        英会話コーチングのスマートメソッド®\n\n教材\n\n予約・講師検索\n\n初めての方へ\n                    \n                        体験レッスンとは\n                        講師について\n                        ご利用の流れ\n                        レッスンルームについて\n                        お客様の声\n\n料金プラン\n\nサービス\n                    \n                        オンライン英会話サービス\n                        日常英会話コース\n                        ビジネス英会話コース\n                        中学・高校生コース\n                        スピーキングテスト\n

(以下省略)

Document Loadersの魅力は、色々なドキュメントを扱えるよう整備されていることです。

Document Loaders 目次

他にもConfluenceやGoogle Driveもサポートしてます。
個人的な感想ですが、こうやって気楽に色々なドキュメントを読み込ませて、LLM利用の可能性を探っていきやすいのが魅力的だなと思います。

Step 5 Text Splitterを利用する

公式ドキュメントはこちらです。 https://python.langchain.com/docs/modules/data_connection/document_transformers/

先ほどロードしたドキュメントを分割するのに利用します。

from langchain.document_loaders import PlaywrightURLLoader
from langchain.text_splitter import CharacterTextSplitter

urls = [
    "https://www.rarejob.com/"
]
loader = PlaywrightURLLoader(urls=urls, remove_selectors=["header", "footer"])
docs = loader.load()

text_splitter = CharacterTextSplitter(        
    separator = "\n",
    chunk_size = 1000,
    chunk_overlap = 200
)

texts = text_splitter.split_documents(docs)

print('=======')
print(texts[0])
print('=======')
print(texts[1])

出力例

=======
page_content='オンライン英会話No.1 レアジョブ英会話\n無料登録\nログイン\n法人向け|\n学校向け|\n初めての方へ\n                        体験レッスンとは\n                        講師について\n                        ご利用の流れ\n                        レッスンルームについて\n                        お客様の声\n料金プラン\nサービス\n                        日常英会話コース\n                        ビジネス英会話コース\n                        中学・高校生コース\n                        スピーキングテスト\n                        レッスンチケット\n                        英会話コーチングのスマートメソッド®\n教材\n予約・講師検索\n初めての方へ\n                    \n                        体験レッスンとは\n                        講師について\n                        ご利用の流れ\n                        レッスンルームについて\n                        お客様の声\n料金プラン\nサービス\n                    \n                        オンライン英会話サービス\n                        日常英会話コース\n                        ビジネス英会話コース\n                        中学・高校生コース\n                        スピーキングテスト\n                        レッスンチケット\n                        英会話コーチングサービス\n                        スマートメソッド®\n教材\n予約・講師検索\nまずは無料体験レッスンへ\n叶えたいのは、英語が話せること' metadata={'source': 'https://www.rarejob.com/'}
=======
page_content='スピーキングテスト\n                        レッスンチケット\n                        英会話コーチングサービス\n                        スマートメソッド®\n教材\n予約・講師検索\nまずは無料体験レッスンへ\n叶えたいのは、英語が話せること\n本当に英会話力が上がる英会話サービスを作る\nレアジョブ英会話にはその使命があります\n週に1回しか通えない教室より毎日使えるオンライン\n楽しいだけでなく、適切なフィードバックをできるプロの講師\n日々研究する中で15年間の集大成が詰まっています\nレアジョブ英会話をうまく使って英会話力を伸ばしてください\nレアジョブ英会話だから実現した最適な学習サイクル\nオンラインで毎日話せるからこそ、英会話力は伸びます。さらにレアジョブ英会話では、レッスンを提供するだけでなく、「迷わず学べる・話せるようになる」学習体験を実現しました。\n詳細を見る\n継続するなら「話せるようになる」レアジョブ英会話へ\nレッスンを受けたいと思ったそのときに、外出先でも予約ができます。あなたのライフスタイルに合わせて、いつでも気軽に英会話を学べます。\n5,000教材から選べる\n                        \n                        view more\n朝6時~深夜1時毎日開校\n                        \n                        view more\n日本人講師によるレッスン・学習相談\n                        \n                        view more\nスマホアプリで受けられる\n                        \n                        view more\n自動録音機能で復習ができる\nインプット学習をアプリで「ソロトレ」\n※Webとスマートフォンアプリでは一部の仕様が異なります。\n※自動録音機能は一部の講師のみとなります。\n※自動録音機能は、一部の環境で音声が再生できない場合があります。詳細は\n                    こちらをご確認ください。\n質の違い、体験でわかります' metadata={'source': 'https://www.rarejob.com/'}

Step6 EmbeddingsとVectorStoreを利用する

Embeddingsの説明について、LangChainのドキュメントの説明を引用します。

https://python.langchain.com/docs/modules/data_connection/text_embedding/

The Embeddings class is a class designed for interfacing with text embedding models. There are lots of embedding model providers (OpenAI, Cohere, Hugging Face, etc) - this class is designed to provide a standard interface for all of them. Embeddings create a vector representation of a piece of text. This is useful because it means we can think about text in the vector space, and do things like semantic search where we look for pieces of text that are most similar in the vector space.

テキストをベクトルで表現する際に利用するモデルのことをEmbedding Modelsと呼んでます。
Embeddings Models自体も同様に、さまざまなプロバイダーから提供されているようです。
今回はOpenAIのEmbedding Modelsを利用します。

https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/openai

実際に動かしてみます。

from langchain.embeddings import OpenAIEmbeddings

embedding_model = OpenAIEmbeddings()

embeddings = embedding_model.embed_documents(
    [
        "牛丼",
        "カツ丼",
        "豚丼",
        "たまご丼",
        "親子丼",
    ]
)

embedded_query = embedding_model.embed_query("豚")

print(embedded_query)

出力例

[-0.012570846825838089, -0.015062803402543068, -0.005081093404442072, -0.02415601722896099, -0.025752535089850426, 0.002058120444417, -0.03506787493824959, -0.015479287132620811, -0.04137064889073372, -0.029237110167741776, -0.0027921719010919333, 0.029181579127907753, 0.005153977777808905, -0.018200309947133064, -0.019283166155219078, 0.03720581904053688, 0.022184664383530617, 0.006268070079386234, 0.03512340411543846,

(省略)

VectorStoreの説明をします。

https://python.langchain.com/docs/modules/data_connection/vectorstores/

VectorStoreの説明をLangChainの公式ドキュメントから引用します。

One of the most common ways to store and search over unstructured data is to embed it and store the resulting embedding vectors, and then at query time to embed the unstructured query and retrieve the embedding vectors that are 'most similar' to the embedded query. A vector store takes care of storing embedded data and performing vector search for you.

与えられた非構造化データをベクトルに変換して保存する、クエリもベクトル化することで近いベクトルを検索できる、というふんわりとした理解で行きます。

今回はなんとなくFAISSを利用します。

https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/faiss

github.com

下記コマンドで下準備します。

pip install faiss-cpu

下記サンプルは
①レアジョブ英会話のLPからドキュメントを取得する
②テキストを分割してEmbeddingsを利用してベクトル変換し、VectorStoreに格納する
③「レアジョブ英会話」という単語で打った際、どんなドキュメントが出てくるか観察する
というコードになります。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain.document_loaders import PlaywrightURLLoader

urls = [
    "https://www.rarejob.com/"
]
loader = PlaywrightURLLoader(urls=urls, remove_selectors=["header", "footer"])
data = loader.load()

text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
docs = text_splitter.split_documents(data)

embeddings = OpenAIEmbeddings()

# FAISSにOpenAI Embeddingsを用いて取得したベクトルを格納する
db = FAISS.from_documents(docs, embeddings)
db.save_local("rarejob-eikaiwa")

# 格納した先を開く
new_db = FAISS.load_local("faiss_index", embeddings)

# レアジョブ英会話と距離が近い値を持つドキュメントを取得する
query = "レアジョブ英会話"
docs = new_db.similarity_search(query)

print(docs[0].page_content)

結果例

本当に英会話力が上がる英会話サービスを作る

レアジョブ英会話にはその使命があります

週に1回しか通えない教室より毎日使えるオンライン

楽しいだけでなく、適切なフィードバックをできるプロの講師

日々研究する中で15年間の集大成が詰まっています

レアジョブ英会話をうまく使って英会話力を伸ばしてください

レアジョブ英会話だから実現した最適な学習サイクル

オンラインで毎日話せるからこそ、英会話力は伸びます。さらにレアジョブ英会話では、レッスンを提供するだけでなく、「迷わず学べる・話せるようになる」学習体験を実現しました。

(省略)

Step7 1回だけ質問に答えてくれるコードを書く

今まで
・LLMs、ChatModels
・Prompt Templates
・Chains
・Document Loaders
・Text Splitters
・Embeddings
・VectorStore
とやってきました。

最後に、Chainsの1つであるload_qa_with_sources_chainに、レアジョブ英会話のLPから取得したテキストを渡して、 レアジョブ英会話について教えてもらうことにします。

https://python.langchain.com/docs/use_cases/question_answering/

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.chat_models import ChatOpenAI
from langchain.chains.qa_with_sources import load_qa_with_sources_chain

embeddings = OpenAIEmbeddings()
llm = ChatOpenAI(temperature=0.9)

new_db = FAISS.load_local("faiss_index", embeddings)
docs = new_db.similarity_search("レアジョブ英会話", k=3, return_only_outputs=True)

chain = load_qa_with_sources_chain(llm=llm, chain_type="stuff")
question = "レアジョブ英会話の特徴について教えてください。日本語で回答してください。"
answer = chain({"input_documents": docs, "question": question}, return_only_outputs=True)
print(answer)

出力例

1回目

{'output_text': 'レアジョブ英会話の特徴は以下の通りです:\n- 週に1回しか通えない教室よりも、毎日使えるオンライン英会話サービスです。\n- プロの講師による楽しいレッスンを提供し、適切なフィードバックも受けることができます。\n- 15年間の研究に基づいた集大成が詰まったプログラムです。\n- オンラインで毎日話すことで英会話力が伸びる特長があります。\n- 自動録音機能により、復習を行うこともできます。\n- 講師の厳しい審査と専門的な研修により、高品質なレッスンを提供しています。\n- 日本人講師とフィリピン人講師の両方がおり、学習者のニーズに合わせたレッスンが選べます。\n- レベルや目的に合わせたオリジナル教材が提供されています。\n\nソース: https://www.rarejob.com/\n'}

2回目

{'output_text': 'レアジョブ英会話の特徴は以下の通りです。\n- 週に1回しか通えない教室より毎日使えるオンライン\n- 適切なフィードバックをできるプロの講師\n- 15年間の研究成果が詰まった英会話サービス\n- レアジョブ英会話を使って英会話力を伸ばすことができる\n- 実現した最適な学習サイクル\n- スマホアプリで受けられる\n- 自動録音機能で復習ができる\n- 無料体験レッスンがある\n- 教材が選べる\n- TESOLに基づいた研修を監修している講師陣がいる\n- フィリピン人講師と日本人講師がいる\nSOURCES: https://www.rarejob.com/'}

3回目

{'output_text': 'レアジョブ英会話の特徴は以下のとおりです。\n- 週に1回しか通えない教室より毎日使えるオンラインサービスです。\n- 適切なフィードバックをできるプロの講師が提供されます。\n- 15年間の研究を経て集大成された教材があります。\n- 自動録音機能で復習ができます。\n- 日本人講師によるレッスンと学習相談が受けられます。\n- スマホアプリで受講可能です。\n- 無料体験レッスンが用意されています。\nSOURCES: https://www.rarejob.com/'}

どれも公式見解ではないのでご注意ください。

終わりに

気楽にLLMで遊ぶにはもってこいのツールなので、ご興味ある方は是非触ってみてください。

採用

現在、弊社は一緒に働いてくださるエンジニアを大募集しています。
穏やかな紳士・淑女の多い職場です。
ご興味ありましたら、是非カジュアル面談にお越しください。 rarejob-tech.co.jp

プロダクトごとの AWS コスト可視化

はじめに

こんにちは、DevOps グループの中島です。
最近また円安に振れてきて、コストが気になるようになってきましたね。

弊社はレアジョブ英会話以外にもいくつかプロダクトを開発しておりますが、 各プロダクトを開発しているチームでもコスト意識を持ってもらおうと思い、プロダクトごとの AWS コスト可視化を行いはじめました。

今回はそのときの知見をいくつか紹介したいと思います。

コスト管理の方法

AWS のコストは コスト配分タグ を利用することで、各リソースにかかったコストをタグを用いて分類することができます。 コストはコスト配分レポートとしてCSV 形式でダウンロード できます。

一般的な従量課金のレコードは以下のようになります。

項目名
usage_type APN1-BoxUsage:t3.large
item_description $0.1088 per On Demand Linux ...
usage_quantity 利用時間
rate 利用時間あたりのコスト
cost usage_quantity * rate = 合計コスト
コスト配分タグ1 RareJobEikaiwa
コスト配分タグ2 StudentSite

弊社では AWS のコストを若干値引き可能なためクラスメソッド様のサービスを利用させて頂いておりますが、 このようなクラウドサービスのリセラーを利用していると、コスト配分タグが複数作成できない、キー値が指定されているなどの制限がある場合があるため、注意が必要です。

また、タグを付与できないリソースや、タグを付与しても集計対象にできないリソースや処理も存在するため、 正確に把握したい場合はアカウントを分けるのが最善と考えられます。

ただし、アカウントを分けても後から集計単位を変更したくなった場合や さらに細かい単位で集計したくなった場合はコスト配分タグを利用することになるため、 将来的に同じ問題に直面する可能性があります。

コスト配分タグ 1 種類でも複数の軸で集計する

前述のような理由で、使用可能なコスト配分タグが 1 種類という制限がある場合、 タグの値に複数の情報を含めることで複数軸による集計をすることができます。
例えば、プロダクトの他に機能ごとに集計したいと考えた場合、 タグの値を Product_Function のように何らかのセパレータで区切り、別の項目として集計します。

このようなワークアラウンドでコスト集計に対する問題の回避は可能ですが、 やはりタグが分かれないことで、AWS の提供するリソースのフィルタリングがこのタグに対しては効かないなどのデメリットはあります。

リザーブインスタンスの考慮

リザーブインスタンスが適用された場合は、以下のようにコスト 0 のレコードとなります。

項目名
usage_type APN1-HeavyUsage:t3.large
item_description Linux/UNIX (Amazon VPC), t3.large reserved instance applied
usage_quantity 利用時間
rate 0
cost usage_quantity * rate = 0

上記 (と利用時間あたりのコスト) によってどれぐらいの削減効果があったかをタグごとに知ることができます。 しかし、購入したリザーブインスタンスはどのインスタンスに適用するかを指定することができません。 このため、正確に (平等に) 利用料金を求めるためには、同じインスタンスタイプのすべてのレコードから利用時間を用いて按分するなどして再計算する必要があります。

Savings Plans の考慮

こちらはリザーブインスタンスと異なり、コストがマイナス値の値引きレコードとして表現されます。 通常の従量課金レコードに対して、使用割合で按分して均等に値引きを適用させるなどで値引き額の按分が実現できます。 レコードは以下のようになります。

項目名
usage_type APN1-BoxUsage:t3.large
item_description SavingsPlanNegation used by AccountId : ... and UsageSku : ...
usage_quantity 利用時間
rate 利用時間あたりの値引き額 (マイナス値)
cost usage_quantity * rate = 値引き額 (マイナス値)

ECS へのタグの付与

コンテナの実行にかかる料金をコストデータに反映させるためには、ECS タスクにタグを付与する必要があります。 ECS タスクへのタグ付与は、サービス定義propagateTags パラメータを指定することで、 ECS タスクの生成と同時にタグが伝搬して付与されるようになります。

注意点として、サービスから伝搬させるためにはサービスにタグを付与する必要がありますが、 ARN が古い サービスは、タグ付与するためにサービスを再作成する必要があります。 本番環境のサービスを削除するのはなかなか難しいので、タスク定義から伝搬させることになることが多いと思います。

コスト配分タグの効果が得られないリソース

タグを付与してもコストデータに反映されないリソースは、逐一 AWS に確認しないと分かりません。 ここでは弊社の AWS 利用のうち料金が目立っているものを 2 点紹介します。

CloudTrail のコスト

Trail ごとにタグを付与することが可能ですが、コスト配分レポートには反映されません。
プロダクトごとに用意するようなものではないのであまり気になりませんが、結構なコストがかかります。

RDS スナップショットからの復元コスト

本番の RDS スナップショットに対して、データ分析などのためマスキングを行った上で別アカウントにコピーを行っています。 データが大きいこともあり、このときのスナップショットの作成 (S3へのエクスポート) にコストがかかっているのですが、 こちらもコスト配分レポートに反映されません。AWS に問い合わせたところ、反映する方法はないそうです。

このようなコストは共通としてカウントするか、各プロダクトに按分するしかありません。

終わりに

改善のためにまずは計測からということでこのような施策を行いました。 すぐには効果が得られないと思いますが、地道に見守っていきたいと思います。

We're hiring!
弊社では、特に DevOps グループでは、一緒に働いてくださるエンジニアを大募集しています。
rarejob-tech.co.jp

ChatGPTをプロダクトへ組み込んでいくステップ

どうもCTOの羽田です。 この度下記のリリースを実施しました!

prtimes.jp

取材もいただきまして、弊社もChatGPTのビッグウェーブに乗り、GenerativeAIの大海原へと漕ぎ出しました。

xtech.nikkei.com

このプロジェクトですが、最初のアクションからリリースまでなんと一ヶ月かからず爆速で完了しました。 主にメンバーや関係者の頑張りでこのスピード感を出せたんですが、社内でChatGPTを使ったプロジェクトを起案・推進するためにPdM兼CTOとして考えたこと・動いたことを共有できればと思います。

「これからChatGPTを使ったプロジェクトを立ち上げようと持っていた人」「何かしらお題としてプロダクトへの導入を検討している人」向けに書きます。

1 アイデアを固めよう

ChatGPTは言わずもがな注目度は高く、どの企業も活用に躍起になっていると思います。 弊社でも同様でして、特に学習体験を大きく改善するには活用するしか無い、これは大きくマナビをアップデートしてくれるものだと思っています。(マナビ・アップデートは弊社のミッションです。)

一方で「ChatGPTでなにかしよう」という話は、プロダクトアウトになりがちで「とりあえず注目を集めるようなプロモーション機能」や「新しい価値だけど顧客課題を解決していないもの」になってしまうことがあると思います。これも一概に悪いことではなく、導入してみてわかる自分たちのプロダクトやサービスへのフィット感・手触り感を得ることはとても大事だと思っています。

ただ意図や意思、明確に「顧客の課題を解決できるもの」という前提を持たない限り施策の後のことが思いつかない&価値検証以外の成果が得られないので、今回我々においては課題設定は非常に重要だと考えました。

そこで企画・案件を考えるため下記が満たすべき要件だと考えました

  • 弊社だからできることか
  • 弊社でしかできないものであるか
  • ニュースバリューが作れるか
  • 明確に解決されるユーザー課題があるか
  • 効果は多少こじつけでもいいから定量化可能か
  • 次の意思決定はできるか
  • かといって開発コストがそこまで膨らまない、1ヶ月程度の施策

これを元にいくつかの案を出しました。またそれを他の執行役員や興味関心を持ってくれていたメンバーと相談し方針を決めました。 他社のプレスリリースを見て最初は何をすべきか考えていたのですが、結果的には「どのユーザー課題を解決しようか」という課題ベースで考え直しました。 その課題をChatGPTだったらどう解決できるのかというのを考えて弊社ではレッスンルーム(英会話レッスンでのレッスンを実施する機能)への組み込みを決めました。

2 「PRD: プロダクト要求仕様書」を書いてみる

初めて書くPRD(プロダクト要求仕様書)|Miz Kushida|note

案があるとすぐ仕様書を書きたくなってしまう・ワイヤーを書きたくなるところですが、今回はそもそも課題や解決方法自体の解像度が低い(プロンプトやチャット方式への課題が曖昧な部分が多かった)のでPRDですり合わせを進めました。

実際に書いた項目はこちら。

私は自分が「新しい面白そうな技術大好き芸人」だと理解しているので、自分の意思や意図が正しいのかの検証のためにもPRDを書いてみて、違和感や不整合がないかを確認しました。 この時点で書いていて、記載に無理があったり、実際の課題のファクトとなるようなユーザーの意見やデータがないのであれば自分の都合のいい妄想でしかないのでこれは書いてよかったです。 また抽象度の高いレベルでのすり合わせをして具体化はメンバーにまかせることもできました。

3 関係各所とすり合わせる

今回はβ版での展開を考え、早い段階から法務、監査担当、CS、経営層、社内の自社プロダクトのヘビーユーザー、エンジニア、デザイナー、機械学習エンジニア(このときはプロンプトの設計をしてもらいました)とのすり合わせをPRDを用いで実施しました。 順番としては

  1. 経営層(抽象度の高いレベルでやること・やらないことのすり合わせ
  2. エンジニア、デザイナー、研究開発エンジニア(工期、実現可能性のすり合わせ
  3. 法務、監査担当、CS(規約やリスクのすり合わせ
  4. 社内の自社プロダクトのヘビーユーザー(簡単なユーザーニーズの調査

という順番。後続のステップと並列して相談していたのですが、最初はPRD一本で説明したのですが私も言語化・文書化が十分でないところもいくつかあり、2から3に移るときは自分で作ったプロトタイプを用いて説明をしました。 説明の順番が下に行くほど具体的であることが求められるので、React + Typescriptでサクッと作った下記のようなチャットサンプルに設計してもらったプロンプトを組み込んで実際に触ってもらったりしながら説明しました。また実際に動くものを見てもらうことで、法務からのリスク指摘や備えておくべきことなどの解像度を上げることができました。

ワイヤーでもFigmaのデザインでも良いんですが、今回ばかりは「ChatGPTを使った機能」を取り扱うことが未経験の人も多く目線をすり合わせるためにプロトタイプを工夫しました。

4 各種設計・デザイン・実装

このステップで難しかったのが「プロンプトの設計を依頼する」というところです。皆さんはやったことありますか?私はなかったです。 自分でそれなりに触ってきたつもりですが、それでも「何を言語化して依頼すべきか」がお手本が少なく、ディスカッションしながら入出力の期待値をすり合わせました。 運良くPrompt Engineering Guideの日本語版もマージされこれが考える参考になりました。

www.promptingguide.ai

ここでは弊社の機械学習エンジニアの山城さんが工夫してくれて、私のふわっとした依頼を実際に試せるプロンプトをjupyter notebookで用意してくれてほしいプロンプトへの近道を作ってくれました。 弊社のプロダクトでは複数のプロンプトを組み合わせており、ケースによっては多段でリクエストを投げるなどここで想像していなかった課題が多く見つかりました。

5 リリースに向けて

検証環境で触れるようになったら「# 3 関係各所とすり合わせる」をもう一周します。 これは私が心配性なだけでもあるんですが、抽象度の高いレベルですり合わせていたものが具体に落ちたときにも齟齬内容に改めて目線を合わせました。

一方でこの時点でOpenAI APIのレスポンス速度の課題があり、プロンプトの見直しをしました。 内容を見直し、数秒のレスポンス速度の改善をしてもらいました。それでも時間が結構かかってしまうので stream api なども検討しましたがまだリリースして見えてきそうな改善課題もありそうだったので今回は一旦プロンプトの改善のみでリリースをしました。(ここはまだまだ速度課題があり、ぜひ知見のある人教えてほしい・・・)

ChatGPTのストリーミング(SSE)APIを試してみた(Go実装) | DevelopersIO

あとは忘れてはいけないのが、OpenAI APIの上限引き上げ設定です。 Hard Limitの上限はデフォルトで$120になっており、これは申請しないと上がらないです。申請は1日くらいで通ります。 コスト感はTokenizerを使って調べましょう。もしくはAPIのレスポンスにtoken数が含まれるので参考にしてください。

https://platform.openai.com/tokenizer

最後に

触っていく中でChatGPTが持つ豊富な仕組みや、それをより便利にするサービスやライブラリ群も知り改めて注目度・ポテンシャルの高さに圧倒されました。 また本プロジェクトと並行してうちのCIOが頑張ってくれて、安全快適に社内向けでChatGPTを利用するためのアプリケーションも爆速開発リリース・ガイドラインも展開してくれました。

まだまだ弊社のプロダクトや機能、社内での運用改善で使えるところが多すぎるので、引き続きマナビ・アップデートしていこうと思います! 今期も全方位採用進めております〜気になった方はぜひ話をさせてください!

rarejob-tech.co.jp

CDK インポート を試してみる

はじめに

こんにちは、DevOps グループの中島です。
弊社では AWS CDK を用いて AWS リソースを管理しています。
過去にコンソールから作成したリソースを CDK に取り込みたいと思い、
CDK インポートを利用する機会があったので紹介します。
使ってみた系記事では触れられてない機能の紹介もあるので参考にしてみてください。

CDK インポートとは

cdk import は CDK 以外の手段で作成した AWS リソースを CDK 管理下に置くことができる機能です。

使い方

CDK インポートの使い方は以下のようになります。
こちらの記事 で詳しく書かれており参考にさせて頂きました。

  1. インポートしたいリソースを用意する
  2. インポート先の Cloudformation スタックを CDK で作成する
  3. インポートしたいリソースの CDK コードを書く
  4. cdk import を実行する

リスナールールをインポートする

CDK インポートを利用したいモチベーションは、現在 100 以上ある ALB のリスナールール (上限緩和済み) を CDK で管理したいというものでした。リスナーの数が多く、コンソールから変更や追加を行うのは苦しいものです。

とはいえ、以下は aws elbv2 describe-rules コマンドで出力されるリスナールール 1 つ分ですが、これ自体を入力とする CDK コードを書いても管理が煩わしいように感じられます。

{
    "RuleArn": "arn:aws:elasticloadbalancing:ap-northeast-1:...",
    "Priority": "200",
    "Conditions": [
        {
            "Field": "path-pattern",
            "Values": [
                "/hogehoge/"
            ],
            "PathPatternConfig": {
                "Values": [
                    "/hogehoge/"
                ]
            }
        },
        {
            "Field": "host-header",
            "Values": [
                "hoge-www.rarejob.com"
            ],
            "HostHeaderConfig": {
                "Values": [
                    "hoge-www.rarejob.com"
                ]
            }
        }
    ],
    "Actions": [
        {
            "Type": "forward",
            "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:...",
            "ForwardConfig": {
                "TargetGroups": [
                    {
                        "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:...",
                        "Weight": 1
                    }
                ],
                "TargetGroupStickinessConfig": {
                    "Enabled": false
                }
            }
        }
    ],
    "IsDefault": false
}

そこで今回は、なるべく見やすい json ファイルを入力にする CDK コードを書きました。
1 ルールにつき 7 〜 8 行です。これくらいなら許容範囲でしょう。
フォーマッターを使うと崩れてしまいますが、見やすさを優先しています。

{
  "targetGroups": {
    "my-web": "my-web ターゲットグループの ARN"
  },
  "rules": [
    {
      "priority": 200,
      "if": {
        "hostHeaders": ["my-web.rarejob.com"],
        "httpHeadersCookie": ["*my_cookie=1*"]
      },
      "then": { "forward": [{"target": "my-web"}] }
    },
    {
      "priority":400,
      "if": {
        "pathPatterns": ["/hogehoge/*"]
      },
      "then": { "redirect": "https://#{host}:443/hoge/?#{query}", "status": 302 }
    }
  ]
}

aws elbv2 describe-rules で出力した結果をパースして、上記の json にするプログラムも別途書いています。
また、例は jsonですが、コメントを入れられるように (json5)https://json5.org/ が読み込めるようにしました。

CDK インポートしてみる

それでは cdk インポートをしてみます。

$ npx cdk import
The 'cdk import' feature is currently in preview.
TestStack
TestStack/rule200/Resource (AWS::ElasticLoadBalancingV2::ListenerRule): enter RuleArn to import (empty to skip):

すると、上記の最終行のように CDK コード上の Construct ID rule200 に対して、AWS 上に存在するどのルールをマッピングするかを聞かれます。
ルールの ARN を入力することになるのですが、これを正確に 100 以上も手入力するのはかなり苦しいです。苦しみから開放されるためにエンジニアしているはずでは?と自問しながら入力してはエラーとなり何度もやりなおしました。

resource-mapping

しかし実はターミナル上で入力しなくても良い機能が実装されています。
-rマッピングのテンプレートを出力して、-m で取り込むことが出来ます。
少し前のバージョンでは -r がうまく動かなかったので、 cdk import がまだプレビューであることを感じられます。

$ cdk import --help
.
.
  -r, --record-resource-mapping  If specified, CDK will generate a mapping of
                                 existing physical resources to CDK resources to
                                 be imported as. The mapping will be written in
                                 the given file path. No actual import operation
                                 will be performed                      [string]
  -m, --resource-mapping         If specified, CDK will use the given file to
                                 map physical resources to CDK resources for
                                 import, instead of interactively asking the
                                 user. Can be run from scripts          [string]
.
.

リスナールールインポートの顛末

ここまでやったのですが、結局リスナールールのインポートはしていません。
対象の ALB は BlueGreen デプロイを CodeDeploy で実装しており、
デプロイ時に CodeDeploy がリスナールールのフォワード先を Blue のターゲットグループから Green のターゲットグループに書き換えてしまうからです。
CDK のコードと AWS 上の設定が異なると都合が悪いので、CDK で管理することは見送りました。

終わりに

CDK インポートを利用してリスナールールを CDK 管理にする試みと、 大量リソースをインポートする際に役立つ機能を紹介しました。
似たようなユースケースで参考になる部分があれば幸いです。

We're hiring!
弊社では、特に DevOps グループでは、一緒に働いてくださるエンジニアを大募集しています。
rarejob-tech.co.jp

Bubbleで開発したサービスがリリースされました

こんにちは。新会社になってから初の投稿です。
レアジョブテクノロジーズCEOの山田です。
レアジョブ時代は執行役員CTOを担っておりましたが、分社化に伴いCEOに就任しました。

先日、新サービスとして、グローバルビジネスで必要な英語コミュニケーションスキルをシーンごとに習得できるオンラインプログラム「グローバルスキルPowerトレーニング(以下、Power)」をリリースしました。
「Power」は、開発にNoCodeのBubbleを採用しましたので、選定理由や実装した機能、そしてメリット・デメリットを紹介していきます。

なお、本サービスは、現業と平行して捻出出来る時間だけでPowerの開発に参加出来る、というイレギュラーな形でメンバーを募集する新たな試みのプロジェクトチームで推進してきました。

www.rarejob.co.jp

実現するサービス要件

Powerでは、システム化する前にテストマーケティングしフィードバックを得ながら教材やレッスン内容を改善しつつ、ニーズを探ってきたこともあり、 ある程度の需要は見込めたものの不確実性が高く、また実運用に乗せないと見えないことも多かったので、まずはMVPとしてお客様と講師に提供し、FBを得ながら拡張していくことにしました。
サービスが提供する機能は、1 on 1オンラインレッスンを提供するためのスケジュールと進捗管理が主となり、実現するサービス仕様は以下のようなものでした。

  • レアジョブ英会話を提供している講師でPowerでのレッスン提供の認定を受けた講師がレッスンを提供
  • 講師はPowerの開講時間を登録でき、その期間はレアジョブ英会話、Powerが混在するスケジュールとなる
  • 生徒は、空き時間の検索、レッスンの予約・キャンセル、教材のDLが出来る
  • また、レッスンはカリキュラムで指定された順番に受講していく
  • 講師が来ないなどのトラブル報告が出来る
  • 運用者の役割は、生徒、講師の登録、スケジュール管理、トラブル管理など多岐に渡る
  • 日本、フィリピンなど利用者のタイムゾーンが違う など

Bubbleの選定理由

まず、NoCodeを選定した背景としては、各プロジェクトの状況からアサイン出来るメンバーがほとんどいないが需要に応えるために早期にシステム化する必要がある、 NoCodeでもMVPで定義した機能は実現出来そうの2点でした。
Bubbleは学習コストは高いものの、

  • 複雑な処理を実装でき、DBもあり、想定する機能は実現出来る
  • 学習するためのコンテンツが豊富にある
    • How to build では有名サービスをBubbleでどう実装するか説明されている
  • 外部APIとの連携も簡易に行える
  • Pluginが豊富で実装すべき機能を軽減出来る
  • バージョン管理、バージョン間のマージができ、運用にも耐えられる
  • 独自ドメインの設定が出来る
  • 情報量も多く、コミュニティも大きい

など、比較したNoCodeサービスより提供されている機能が豊富で、MVPとMVP以降の拡張を実現出来る可能性が高いため採用に至りました。
ただし、コスト面では他のNoCodeサービスより(アプリケーション毎の課金のため)少々高く、また、どのNoCodeサービスもそうかもしれませんがパフォーマンスに不安がありました。

構成

Powerシステム構成

本システムでは、講師が複数サービスにレッスンを提供するためスケジュール管理を一元化することが前提ということもあり、弊社独自のプラットフォームをベースに Bubbleでは、サービス独自の情報を保持し、ユーザーへのフロントエンド部分を実装するという構成になっています。
また、アカウント管理もプラットフォームが担っているため、ログイン機能はOAuth2を採用しています。

実装した機能

ログイン、検索、予約、進捗可視化をメイン機能として実装しており、以下が出来ることでそれらの機能が実現出来ています。

  • OAuth認証
  • プラットフォームAPI利用
  • Bubble DBに対するCRUD処理
  • 時間による表示内容、処理の制御
    • Local Timezoneでの時間制御
  • バージョン毎に環境変数を定義し、環境変数に応じた処理の制御、表示の制御
    • 定義値の管理機能も有しています
  • Bubbleアプリケーション間のAPIを介したデータ同期
    • BubbleアプリケーションでもAPIを外部公開できる
  • スケジューラーによるバックグランド処理
  • 運用システムのIPアドレス制限 などあり、デザインを実現するための部品も揃っており、またHTMLやJSなども挿入可能です。

利用Plugin

Plugin 用途
API Connector 外部API利用向けのインターフェース
Browser Push Notifications ブラウザプッシュ通知
Browser Timezone and Locale タイムゾーンの取得
Data Converter 変数の型変換(テキスト型⇒日付型など)
Ipiphy 利用者IPアドレス取得
CSV Creator 取得したデータからCSVファイル作成しDL
Sidebar Menu サイドメニュー

実装で苦労した点

絶対的にこれが出来ないというものは、これまで触ってきた中でなさそうですが、ひと手間かけないとやれないことは割とあると感じました。

  • 日付型の扱い
    • プラットフォームAPIでは、RFC3339をサポートしており、一発で変換出来ないのでいくつかの処理に分けて実装した
    • 使われるタイムゾーンが分かりにくくデバックしながら正しい状態にするのにかなり時間を要した
  • 繰り返し処理 ※一番の不満
    • 例えば、APIからリストを取得し、一件毎に何か処理しながらDBに登録ということが簡単に出来ない
    • これを実現するには、私が知る限りでバックグランド処理に渡すのが実現方法
  • APIのHTTP Statusによるエラーハンドリングが出来ない
    • 4 xxと5xxで処理を分けるということが出来ないので、エラーだったらというざっくりした処理で回避するしかない
  • OAuthは動的に変更不可
    • 弊社のように環境毎に指定するURLが異なる場合は、管理方法を厳密なルール設計が必要
    • 本件は、運用リスクが大きいため、Bubble社へリクエストしています
  • 定期実行処理の最小単位が日単位
    • 時間や分単位でスケジュール決めて処理をしたい場合、イベントトリガーでスケジュールイベントとして日時を指定して、処理を登録する

メリット・デメリット

所感として、機能的にシンプルで、小規模であればかなり有用だと思いますので、今回のようなMVPには適していると思います。
サービス規模が拡大していく中で、継続するかの判断が必要となりますので、将来的に別システムにスイッチすることも視野に入れて選定した方がいいと考えています。
Powerでは、プラットフォームにサービスの重要なマスタデータは、保持しているので同じ構成であればリプレイスは難易度として高くないので、その点もNoCodeを採用したポイントとなっています。

メリット

当然、BubbleはNoCodeツールなので、すべての開発スキルを有している必要はなくアプリケーション開発を行えることが出来るのでエンジニアコストを抑えて開発することが可能です。
繰り返しの内容も含みますが、私が使ってみて実感した代表的なメリットは次の通りです。

  • (機能を把握し使い方に慣れれば)初期開発費用を抑えて短期間でリリースが可能
  • ページ、機能やAPIを追加してデプロイするまでのプロセスが短いので運用工数が少ない
  • Pluginも充実しており、やりたいことを実現する機能がカバーされている
  • ナレッジが豊富、学習コンテンツが充実している
  • サポートが丁寧に対応してくれる
    • 時差があるので返信はすぐには来ないです
    • Bubble本体のバグがあり指摘したら、早期に修正しリリースしてくれました

デメリット

前述の「実装で苦労した点」以外で、以下のデメリットもありますので、Bubbleの採用を検討する際の参考にして頂ければと思います。

  • 非エンジニアだと難易度が高い(学習コストが高い)
    • カバーしている機能が広い反面、使いこなすまである程度の時間が必要
    • 無料でもやれる範囲が広いので、無料で実装慣れしてから有料化するのが効率的
    • ドキュメントも豊富にあるが、公式How to動画を見るのが楽
    • 正しく使うには、設計力が重要
  • 処理速度が遅く、チューニングが難しい
    • Pluginで処理が遅くなることが多いので、Pluginの比較も重要
  • デバック出来る部分が限られている
    • ワークフローで定義した処理はデバッグモードとログで詳細がわかるが、それ以外(条件に応じて表示内容を変えるなど)の不具合調査は、試行錯誤することになる
  • 処理内容の可読性が弱い
    • GUIで処理定義していくが、どのような処理が組み込まれているかひと目でわからない
    • 設定処理毎にコメント残す、ドキュメント化するなどの工夫が必要
    • App search toolというどこで何が使われているか検索するツールを駆使する
    • 大人数でやるには向かない

今後の展望

事業計画上は、当面Bubbleアプリケーションでも問題ないと考えているので、要望対応をしつつ、AIを活用した機能拡張を考えています。
VoicePenbyword_VocionなどBubbleで作られた生成AIサービスも出てきており、Azure OpenAI pluginもリリースされるということなので、動きをウォッチしつつ色々と試していきたいと思います。

最後に

今回紹介したBubbleのようなNoCode、話題のChatGPTに代表される生成AIで、技術選定の選択肢が増え、また想像を超える早さで進化しています。
弊社は、「マナビ、アップデート」をミッションに掲げ、我々が提供するプロダクトで学びを進化させ社会貢献していきます。
同時に、新しい技術をTry&Errorして、組織としての「マナビ、アップデート」も行い、プロダクトの成長に繋げていきます。
ChatGPTに関する発表は近々あるのでお楽しみに。

ということで、一緒にチャレンジしてくれる人を募集しています!
少しでも興味ありましたら、カジュアル面談でも如何でしょうか?
rarejob-tech.co.jp