Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。

新卒から見たZealsエンジニア組織について

f:id:dainiizawa:20200316104115p:plain


はじめまして!現在Zealsで内定者インターンをしている玉城です!
私は大学の在学期間中、GoとGCPを使ったWebアプリケーションの作成や時間短縮ツールなどを独学で作成したり、自然言語処理と機械学習を用いた研究を行っていました。Zealsには、利用している技術や提供しているサービス、熱量の高さから、自分がやりたいことを達成できる環境だと思い入社することを決めました。
現在、内定者インターンという形でジョインしてからおよそ3ヶ月ほど経ちましたが、とても凄い環境で圧倒されています。
そこで今回は、新卒から見たZealsの開発組織と環境について書いていきます。
特に、

  • 私がZealsでどのように働いているのか
  • エンジニアが働きやすい環境か
  • 新卒でも主体的に動ける環境があるか

こういった問いを持つ方に、少しでも参考になれば幸いです。

続きを読む

GKE環境のRedashでPostgreSQLサーバのディスクが溢れたので、Persistent Volumeをいじって何とかした

f:id:lead-zep0324teruhisa:20200202211533p:plain

こんにちは、分析基盤を担当している鍵本です。

本日は昨年の10月に GKE に移行した Redash の PostgreSQL サーバでディスクがいっぱいになってサービス停止してしまった 時のお話をします。

GKE に移行した話については2019年11月15日に公開したテックブログ Redash 分析環境のGKE移設&ver.3から7へのアップデート手順を公開 を御覧ください。

tech.zeals.co.jp

続きを読む

fast_jsonapi gemを1.2から1.5へアップデートしたら躓いた点と対応策

f:id:zeals-engineer:20200124000233p:plain

こんにちは、分析基盤を担当しつつ Rails エンジニアのサポートもやっている鍵本です。

本日は Fast JSON API のバージョンアップをした時に突然テストが落ちて困った話をします。

github.com

  • 背景
  • テストが落ちる状態の再現
  • エラーの原因
  • 対応方法
  • まとめ
  • 最後に

背景

Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、WebのForm体験をチャット上で完結させることができます。この Zeals のアーキテクチャは同じくテックブログの記事『進化し続ける「Zeals」サービスを支えるアーキテクチャについて』でご紹介しているとおり、バックエンドを Ruby on Rails と React + Redux (& TypeScript) で実装しています。

tech.zeals.co.jp

Rails は DB から必要なデータを準備し JSON 形式に変換したものをフロント側の React に渡しています。その JSON 形式に変換する部分に Fast JSON API を用いています。アップデート前はバージョン 1.2 を用いておりテストコードも問題なく通っていたのですが、1.5 に gem のアップデートをしたところ突然テストが通らなくなりました。

テストが落ちる状態の再現

サンプルプログラムを使ってテストが落ちた状態を再現してみます。

属性として size, color を持つ Shirt モデルを作成します。

class Shirt < ApplicationRecord
  enum size: { 'XS': 0, 'S': 1, 'M': 2, 'L': 3, 'XL': 4 }
end

Shirt モデルのためのシリアライザを以下のように作成します。

class ShirtSerializer < ActiveModel::Serializer
  include FastJsonapi::ObjectSerializer

  attributes :size, :color
end

Shirt モデルをシリアライズして JSON を返すコントローラを作成します。

class ShirtController < ApplicationController
  def show
    @shirt = Shirt.find(params[:id])

    shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]

    render json: shirt_json
  end
end

ShirtController#show は {size: サイズ, color: 色} を返すので、例えば size: 'M' となる Shirt を生成して、その値が正しく得られることをテストするコードを用意します。

require 'rails_helper'

RSpec.describe ShirtController, type: :controller do
  describe '#show' do
    let(:shirt) { create(:shirt, size: 'M', color: 'black') }

    subject { JSON.parse(response.body, symbolize_names: true) }

    example 'response の size が M である' do
      get :show, params: { id: shirt.id }

      expect(subject[:size]).to eq shirt.size
    end
  end
end

fast_jsonapi のバージョンを 1.2 としてテストすると問題なく成功します。

$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
.

Finished in 0.02659 seconds (files took 1.39 seconds to load)
1 example, 0 failures

ここで fast_jsonapi のバージョンを 1.5 にアップデートすると以下のような結果を得ます。

$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
F

Failures:

  1) ShirtController#show response の size が M である
     Failure/Error: shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]

     NoMethodError:
       undefined method `each' for #<Shirt:0x00007f9478a3f318>
     # ./app/controllers/shirt_controller.rb:5:in `show'
     # ./spec/controllers/shirt_controller_spec.rb:10:in `block (3 levels) in <top (required)>'

Finished in 0.01942 seconds (files took 1.43 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/controllers/shirt_controller_spec.rb:9 # ShirtController#show response の size が M である

エラーの原因

fast_jsonapi の serializable_hash メソッドは ActiveRecord::Relation オブジェクト か否かで処理を分けているのですが、その判定方法が 1.2 以前とそれ以降で変わってしまったことが原因でした。具体的には 1.3 以降では以下のような条件で判定しています。

      resource.respond_to?(:size) && !resource.respond_to?(:each_pair)

我々の Shirt モデルには enum で size という属性を持っているため size メソッドが存在します。そのため ActiveRecord::Relation オブジェクトであると間違って判定され、

@shirt.each do |s|
  ...
end

のような処理が実行され、each メソッドが存在しない というエラーが出たということでした。因みに 1.2 以前では以下のような判定だったので問題なかったというわけです。

      resource.respond_to?(:each) && !resource.respond_to?(:each_pair)

対応方法

テーブルスキーマを変更して列名を size から別のものに変更すればいいのですが、影響範囲が大きいためシリアライズする際にオプションを渡すことで対応することとしました。すなわち、

class ShirtController < ApplicationController
  def show
    @shirt = Shirt.find(params[:id])
    options = { is_collection: false }

    shirt_json = ShirtSerializer.new(@shirt, options).serializable_hash[:data][:attributes]

    render json: shirt_json
  end
end

@shirt が ActiveRecord::Relation オブジェクトでないことがわかっているから採用できる方法です。これで先のテストコードが成功することがわかります。

続きを読む

TypeScriptでconditionベースのサブセットタイプを作成する

f:id:zeals-engineer:20191229220455p:plain Photo by Alvaro Reyes on Unsplash

究極の謎を解くためのタイピングシステムの詳細について

こちらの記事は翻訳記事となります。 原著者の許諾を得て翻訳・公開しております。

medium.com

続きを読む

会話分析におけるGraphDBの活用

f:id:newton30000:20191215174040p:plain

こんにちは。ZealsでCTOをしている佐藤です。
Zealsでは AdventCalendarを開催しており、そちらの15日目の記事となります!

  • はじめに
    • 会話分析とは?
    • 何がしたいか
  • Neo4j
    • 技術選定について
    • 環境構築・実行
  • 振り返り
  • さいごに

はじめに

会話分析とは?

私達が開発しているZealsは「ネットにおもてなし革命を!」をコンセプトにした、ユーザーと会話して商品を案内するチャットコマースのサービス です。

チャットコマースについて詳細が知りたい方は、同じくZeals AdventCalenarで公開された以下の記事をご覧ください!

qiita.com

このZealsの会話体験を向上させていく中で、ユーザーとチャットボットとの 会話データ を分析していく必要があります。

現在ではMySQLやBigQueryにデータを格納することで、 ETL分析や機械学習のためのマスターデータ を用意しています。 (機会があればこのテーマも記事にします)

www.ashisuto.co.jp

何がしたいか

f:id:newton30000:20191215173727p:plain

例えば会話構造の一部をサンキーチャートで表すと、上記のように表示されます。

ja.wikipedia.org

KPIの取得や特徴量の把握に関しては運用上の問題は特に発生していないのですが、機能が拡張する中で分析しきれない(あるいはしづらい)領域が出てきてしまいます。

例えば上記のサンキーチャートですが、再帰的な構造の会話には対応することができません。

  • ユーザー毎の会話の分岐を構造化
  • 会話が再帰的な構造に対するクエリ発行
  • ユーザーと会話の関係を表すデータが多様
  • 将来的にテーブルが肥大化することによって性能が悪化する

このような問題に対応するため、 RDB以外でデータ格納や分析を行うアプローチ を検討しました。

続きを読む

pytestのparametrizeの使い方とその有用性について

f:id:RyotaAraki:20191211175222j:plain

※こちらはZeals AdventCalendar 12日目の記事です。

こんにちは!

Pythonエンジニアの荒木です。
夏頃に社内の開発プロジェクトにPythonメンバーとしてアサインされ、本格的にPythonを業務で書くようになり、最近はほぼ毎日Pythonを書いています。

Pythonの業務としては主に新機能実装とDBリファクタリングをやっています。

今日はpytestのparametrizeの基本的な使い方
parametrizeでちょっと詰まったところがあったので、それらについて書きます。

目次は以下のようになります。

  • 前提
  • parametrizeについて
    • parametrize とは
    • parametrize の使い方
  • fixtureをparametrizeする
    • fixtureとは
    • fixtureのparametrize
  • 終わりに

前提

まず、以前にこちらの記事で紹介したように、Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、サービスは大きく分けると、

  1. チャットボットの管理用Webアプリケーション
  2. チャットボットへのメッセージ送受信サービス
  3. 1と2から共通処理を切り出したマイクロサービス群

の3つに分かれています。
詳細はこちらの記事をご覧ください。

tech.zeals.co.jp

その中の 2.メッセージ送受信機能フレームワークを採用しないピュアなPythonで実装しており、チャットボットに流入したエンドユーザーに対して、設定されたシナリオの応答や配信処理 を行います。
実際に、10万人を超えるユーザーに対する配信もここで行っています。

このようにZealsにおけるPythonコードは直接エンドユーザーとやりとりするので、誤った挙動をしてしまうとそのまま配信事故につながるリスクが大きいです。
例えば、想定していた配信先と異なる配信先にメッセージを送信して閉まった場合は、そのまま配信事故に繋がることになります。

そのためにこの配信機能の開発はより慎重さが求められ、当然テストコードの重要性も高くなります

parametrizeについて

parametrize とは

Zealsでは、Pythonのテストフレームワークとしてpytestを導入しています。

parametrizeとはpytestで使えるデコレータの1つで変数と結果をパラメータでテストコードに渡すことで、1つのテストメソッドで複数パターンのテストを行うことができます。

parametrizeを使ってテストコードを書くことで
テストコードをDRYにして改修コストを下げることができます。

parametrize の使い方

実際にparametrizeを使ってみます。

import pytest


numeric_datas = [
    (2, 5, 10),
    (3, 6, 18),
    (5, 9, 45)
]


@pytest.mark.parametrize("a, b, expect", numeric_datas)
def test_multiple(a, b, expect):
    assert a*b == expect

与えた2つの変数a, bの積がexpectであるかどうかを判別するテストです。 これを実行してみると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.01s ==============================

となり、いずれの計算結果も正しかったことが分かりました。
このブログのコンソールのUIは白黒ですが、実際はテストが通ると1番下のラインが綺麗な緑色になります。

試しに与えるexpectを誤ったものに変えてみます。

numeric_datas = [
    (2, 5, 8),
    (3, 6, 18),
    (5, 9, 45)
]

この内容でテストを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py F..                                                [100%]

================================== FAILURES ==================================
____________________________ test_multiple[2-5-8] ____________________________

a = 2, b = 5, expect = 8

    @pytest.mark.parametrize("a, b, expect", numeric_datas)
    def test_multiple(a, b, expect):
>       assert a*b == expect
E       assert (2 * 5) == 8

test_parametrize.py:16: AssertionError
======================== 1 failed, 2 passed in 0.05s =========================

このように、どのパラメータを与えた時にテストが落ちたのかが分かります。
あるパラメータでテストが通過しなくても、他のパラメータが別で独立してテストを続行してくれるところはありがたいですね。

parametrizeについてのドキュメントはこちらです。

docs.pytest.org

実際のZealsにおけるparametrizeのユースケースとしては
LINEとFacebook Messengerで共通した処理のテストをLINE, Facebook Messengerそれぞれの変数をパラメータで渡したり、個人情報に関するメッセージの情報の種類(郵便番号や住所など)をパラメータとして渡すことで活用しています。

fixtureをparametrizeする

さて、次はfixtureのparametrizeについてです。

fixtureとは

fixtureとはpytestで使い回すことができる、テスト用のインスタンスを定義しているメソッドのことです。

実際にpytestでテストメソッドを回す際には、fixtureをテストメソッドの引数に与えてテストを回すことが多いです。
引数として与えられたfixtureはテストメソッドが実行される前に必ず実行されるようになっています。

fixtureのparametrize

やりたかったことは「このfixtureをパラメータに渡してテストメソッドを回したい」ということでした。

そのため、↓のようにfixtureを定義してメソッドを実行してみます。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect):
    assert letter == expect

実際に実行すると、いずれのテストも失敗します。
テストケース3つともすべて同じ原因で失敗をするのですが、エラーメッセージは以下のように表示されます。

____________________________ test_capitalize[a-A] ____________________________

letter = 'a', expect = 'A'

    @pytest.mark.parametrize("letter, expect", characters)
    def test_capitalize(letter, expect):
>       assert letter == expect
E       AssertionError: assert 'a' == 'A'
E         - a
E         + A

test_parametrize.py:47: AssertionError

letterがfixtureとして認識されていないために、変数 'a' として返されていることが分かります。

そこで、与えた変数をfixtureに変換してあげる必要があります。
その変換のためのメソッドが getfixturevalue() です。

公式ドキュメントは↓です。

docs.pytest.org

これを用いて変数を変換してみましょう。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


@pytest.fixture
def get_fixture_values(request):
    def _get_fixture(fixture):
        return request.getfixturevalue(fixture)
    return _get_fixture


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect, get_fixture_values):
    letter = get_fixture_values(letter)
    assert letter == expect

これを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/mbp028/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.02s ==============================

テストが問題なく通過しました。
ちなみに、Zealsではこのメソッドをpluginで実装しており、Pythonアプリケーション内の様々なファイルから呼び出すことができるように工夫しています。

終わりに

Pythonは日本語の情報が少なく、ましてやpytestやparametrizeと
より細かい領域の話であれば、情報を集めるのになおさらハードルが上がってしまうと感じます。

続きを読む