新しいWeb UIをデフォルトで有効にしました

こんにちは、技術書典のWeb担当 @vvakame です。
01月24日にお知らせした新しいWeb UIをデフォルトで有効にしました!
…することができました……!

Opt-Inでリリースしてから約半月、特に大きな問題もなく、さらなる細かい改善もたくさん盛り込むことができました。
開発チームはこれに自信を得て、今日デフォルト化に踏み切りました!
新UIにOpt-Inし、利用してくださった方々、大変ありがとうございました!

新UIをリリースしましたが、不測の事態に備えて旧UIに戻す設定も残してあります。
運営事務局からのサポート時など、限られた状態で使う想定です。
今後、技術書典8までの1ヶ月足らずの間にも新UIにしかない設定項目などが増えていく予定もあります。
よって、旧UIからの操作は基本的に行わないでいただきたいです。
ご協力よろしくおねがいします。

もし、不具合など発見した場合はお問い合わせ窓口までご連絡ください。

リニューアル第一弾となる新しいWeb UIのおしらせ

こんにちは、技術書典のWeb担当 @vvakame です。
そしてこの記事の編集を担当した技術書典代表 @mhidaka です。

一年の大半という長きに渡り、燻っていましたが遂にアーキテクチャを刷新した新しいWeb UIをリリースしました!
大規模変更となるためOpt-Inでの公開です。一週間くらい様子を見て問題がなさそうであれば移行Stepを進め、Opt-Outに切り替えます。

ここまで大きな変化が技術書典WebのUIに起こったことはありません。不安に感じるかもしれませんが、
ユーザーにとっては変化がないことのほうが改善が行われないリスクだと判断しました。今後もやきもきさせてしまうことがあるかもしれませんが、暖かく見守ってください。

今回は何もかもを作り直しており、さらなる前進のための基盤を整えました。
アーキテクチャの刷新は大変に大きな変更なのでUIの構造自体は旧来のものを維持しています。少しでもインパクトを小さくするためです。
今回の変更では特に何も変わっていないなぁ、と皆さんに思ってもらえれば成功かもしれません。
これからどういう変化が加えられていくのか、随時発信していく予定です。今後の更新・技術書典の新しい姿を楽しみにお待ちください。

どこが変わったのかの話

変更点をいくつか紹介したいと思いますが、UI的にはどこも変わっていません。

旧UIの技術書典08イベントページ
新UIの技術書典08イベントページ

ほぼ変わってない!はずです。だいたいの画面は今までと同じ場所にありますし、表示されているデータも変わりないはずです。
コンポーネント単位で見るとデザインの変化など細かい違いを見て取れるかもしれません。

ユーザー設定から新UIを利用するにチェックを入れ送信すると新UIに切り替わります。
旧UIに戻したいときも、同じ位置に設定項目があります。

新UIを利用する

なぜ変えたのかの話

開発リソースの集約のため、というのが当初からある目的のひとつです。いままではWeb UIはAngularで作っていました。
そして、イベント当日用アプリ(主にかんたん後払いに使われている)のAndroid/iOS版は現在はReact Nativeで作られています。
これらを個別に開発し続け双方を充実させていくには深刻にリソースが足りません。
よって、どちらかに集約したいという要求が生じます。

かんたん後払いで必要な、QRコードの軽量かつ連続した読み取りをサポートするためには、ネイティブの実装が現時点では不可欠です。
よって、開発をReact NativeとReact Native for Webに寄せることにしました。これは @Nkzn という他に考えられないレベルの仲間が得られたためです。

おれたちはReactに全振りしたらぁ…!!やったらぁよ…!!

といったかもしれませんし、言ってないかもしれません。しかし心の声は確かに聞こえました!この変更によって今後の計画も前に進むでしょう。詳細についてはまだ秘密です!
公開できる日が楽しみです。

使っている技術や設計思想的な話

1年間弱、がんばってきたプロジェクトなので、どういう構成になったか技術的なところに触れておきたいと思います。

React Native + React Native for Web

どうも、@Nkznです。フロントエンドの実装を担当しました。
以前からかんたん後払いシステムのモバイルアプリ担当として関わらせてもらっています。そこで使っていたReact Nativeの周辺エコシステムが技術書典のロードマップと上手く噛み合ったため、技術書典Webのリアーキテクチャにも携わる運びとなりました。

ざっくりと次のようなツール構成で作っています。

こだわりの点として、すべてのコンポーネントを関数コンポーネント(Function Component)として実装しました。状態管理もReact 16.8で導入されたHooksを利用しています。これは大成功で、従来のクラスコンポーネントと比べて見通しがよくなりました。

また、後述のとおり、今回はGraphQLをAPIサーバーのプロトコルとして採用しているのですが、React向けのGraphQLクライアントである、Apollo ClientのHooks拡張がとても良かったので言及しておきます。

GraphQLサーバーと通信するコンポーネントは、次のように定義できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React from 'react';
import { View, Text } from 'react-native';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import { LoadingView, ErrorView } from './utils';

// (2)クエリ文字列を静的解析して生成されたTypeScript型定義
import { EventPageQuery } from './path/to/EventPageQuery';

// (1)クエリ文字列を定義
const query = gql`
query EventPageQuery {
event {
id
name
}
# any other data structure
}
`;

// コンポーネント定義
export const EventPage: React.FC = () => {
// (3)型安全なリクエスト(dataはEventPageQuery型になる)
const { data, loading, error } = useQuery<EventPageQuery>(query);

if (loading) {
// 通信待ちの間は読み込み中のUIを表示
return <LoadingView />;
}

if (error) {
// エラーがあった場合は内容を表示
return <ErrorView error={error} />;
}

return (
<View>
<Text>イベント名: {data.event.name}</Text>
</View>
);
}

これを実行すると、読み込み中に LoadingView が一瞬表示された後に、イベント名: 技術書典8 のような表示が画面に出ます。

(1)でGraphQLリクエストのためのクエリ文字列を作成するわけですが、これは別途用意したCLIツールによって静的解析され、(2)のTypeScript型定義ファイルを生成します。これを(3)の useQuery にジェネリクスの型パラメータとして渡すと、戻り値の data がリクエスト時に指定したデータ構造を型として持てるようになるのです。これには型安全教徒の私もvvakameさんもニッコリです。

この方式だとUI更新と通信の実装が隣り合ってしまう感じがして、最初は少し抵抗がありましたが、よく見るとUIから関心がある範囲に限定されたデータをインターフェースとして持っているので、とても使いやすいものだと思えるようになりました。

さて、話は変わりますが、将来React Nativeのモバイルアプリとソースコードの統合を予定しているということで、できる限りネイティブ版でも利用できるソースコードを多く用意する必要がありました。実際どのくらいになったでしょうか。

手元でざっくり数えてみたところ、だいたいコードベースの70%くらいが共通で利用できる状態になっているようです。コンポーネント(UI)が45%、それ以外(主に自動生成した型定義)が25%という内訳でした。モバイルアプリ版を作り始めるとネイティブ専用コンポーネントが増えていくので、将来は共通部分の比率は下がると思われますが、現時点ではこんなところです。

React NativeやFlutterといったクロスプラットフォーム系のテクノロジは、人の手が足りない組織で多くのプラットフォームに向けてサービスを提供したい場合に上手くマッチします。その中でもReactという「本来ブラウザのために作られたフレームワーク」を軸にしているReact Nativeは、ブラウザに手を広げた場合にも、里帰りしたかのような安心感を伴うことがわかってきました。

この挑戦的なプロジェクトは今後も続いてきます。今後の機能追加にご期待ください!

Fastly

CDNにFastlyを導入しました。すでに去年の終わり頃からリクエストをFastlyでさばくようになっています。
製作者のvvakameは5年ぶりくらいにVCL書きました。その柔軟な表現力でもって、新UIと旧UIの出し分けなどはFastly上で行っています。

設定の管理はTerraformで記述し、GitHub Actionsで反映させています。
慣れるとめっちゃ便利ですね。

GraphQL

APIがOpen API v2(Swagger)ベースのものからGraphQLベースのものになりました。
サーバが動いている場所は以前と変わらずAppEngine+Goなので、vvakameもコミッタを務めるgqlgenを利用しています。

GraphQLのおかげでサーバ側のコードを書くときに煩わしいパートが少なくなりましたし、クライアント側もSwagger時代以上に強固に型が付き、開発が楽しくなりました。

クライアント側をTypeScriptで書く場合、GraphQLがアプリにとって最良の選択肢であると思います。おすすめです!
それ以外の言語でもわりと高い割合で満足できる選択です。今後はGraphQLベースで開発を進めていきます。

フィードバックについて

今回は見た目は変わっていませんが、おかしなところがあるかもしれません。技術書典のWebサイトは豊かなサークル活動を支えるためにたくさんの機能を持っています。
テストカバー率100%でリリースすることを涙が出るほど望んでいましたが我々は完璧ながら遅いリリースより素早くリリースし、ユーザーからのフィードバックをもらうことを望みました(すでにすごい時間かかったし!)。

もし何かあったらお問い合わせ窓口にご連絡ください。
すべてに返信することは難しいですが、スタッフで共有します。もしかしたら追加情報をお願いするかもしれませんので、その際は修正にご協力いただければと思います。

それでは、よい執筆を!

サークル半自動配置の導入

こんにちは、@shuhei_fujiwara です。

技術書典5から会場を変更し、参加サークルも約246から470超(企業スペースを含め調整中です)と劇的に増加しました。元々サークルの配置決めは非常に時間が掛かっていた作業ですが、さすがに今回は手動では乗り切れない規模になりました。運営事務局は指数的に増えていくサークル配置の組合せを見て絶望に包まれていました。

そこで技術書典5からサークル配置の(半)自動化を検討することになり、@shuhei_fujiwaraが実装を担当しました。
皆さんの「どういう工程を経て現在の配置にたどり着いたのだろう?」という疑問に答えるため、簡単に仕組みと背景を解説します。

ことのあらまし

僕自身も今回から配置作業に参加して知ったことですが、技術書典はちょっと引くくらい配置にこだわっています。考慮すべき制約は大量にあるのですが、大まかな方針としては以下の3つに気をつけて配置を決めています。

  1. 近いジャンルのサークルが同じブロックに集まるように
  2. ジャンルの境界でなければ両隣のサークルのジャンルには気を配る
  3. 特に珍しいジャンルのサークルの同志は近くに集まるようにする

1と2が自明なのに対し、3は見落としがちですが重要な制約です。技術書典が技術の良い出会いの場所であるために、これは可能な限り実現したいポイントでした。

検討段階では機械学習で配置を何とか改善したいという要望から出発しましたが、最終的には数理最適化ベースの手法へ落ち着きました。機械学習を採用しなかった背景は機械学習で扱える問題に落とし込む難しかったということと、前述のように珍しいジャンルを見落とさずに扱うのが難しいだろうという判断でした。

半自動配置の手順

技術書典では、ざっくり次のような手順で行っています:

  1. 全サークル間の類似度を定義する(定義の仕方は後述)
  2. 良い感じのアルゴリズムでサークルをグループに分割する
  3. 各グループ内での配置順を決めて通し番号を付与する
  4. グループ単位で配置場所を決定、人間が動線を考慮して調整する。この時点で叩き台となる
  5. 配置単位で調整をおこない、最終版が完成する

今回は手順3までを自動化し、手順4以降で人間による調整を行っています。各工程のうちグループ分割を行う手順2と配置順を決定する手順3は補足が必要です。少し分かりにくいかもしれませんが、以下の図のようなイメージです。

技術書典5のサークル座席配置の手順

手順2のグループ分割は各サークルがどのブロック(ブロックとは「あ-X」のように同じひらがなでまとめられる集団を指した自動化用の概念です)に属するかを決める作業です。実際には会場の形を考慮して会場の配置区分(あ、い、うなどで表現されているもの)よりも細かく分けました。グループ分割の工程では、グループ内のサークル間類似度の総和が最大になることを目標とします。

手順3の配置順の決定とは、具体的にグループ内での各サークル配置順序を決めていきます。会場の形状に依存しますが同一グループは基本的には一直線上に並ぶことを仮定して、サークルの両隣との類似度の総和最大化を目標とします。

技術書典4では手順4の叩き台と同程度のものができるまでに丸々2日掛かっていましたが、
技術書典5では自動化の恩恵により 2時間程度(ただし@shuhei_fujiwaraの工数を除く)で可能になりました。サークル数が倍近くに増えたにもかかわらずです!

浮いた丸2日をすべて投入し、今回は前回以上に細かい調整を行いました!あれ?何か楽になってなくておかしいぞ…?

アルゴリズムの詳細

サークル間類似度の定義

各サークル間の類似度を機械的に計算するために、まずはサークルごとに次のようなジャンルのタグづけを行います。

tag1 tag2 tag3 tag4 tag5
サークル1 機械学習 数学 R
サークル2 機械学習 ハードウェア IoT 数学
サークルN Android IoT iOS

タグはスタッフがジャンル詳細を見たり、各サークルのホームページや記憶を漁って人力でつけています。
温かみのある手作業も良いのですが、次回以降は何とかしたいところです

※編集注:技術書典では自動化にかかるコストと得られるパフォーマンスをかなりキチンと判定しています。初回が手作業であっても施策に効果があれば自動化していき、どんどん工数を浮かせて、浮いた分を別の企画や改善につぎ込んでいきます。

サークル間の類似度はタグの集合同士の類似度という形で定義します。

  • 共通のタグを持っている集合同士は類似度が高い
  • 珍しいタグを持っている集合同士は特に類似度が高い
  • タグをたくさん持っている集合が極端に有利になることを避けたい

まずは集合間で共通要素があったときのスコアを定義します。「機械学習」というタグが共通要素として存在していた場合のスコアは今回では次のように定義しています。

1
1 / (log [機械学習タグを持つサークルの総数] + 1)

複数のタグが共通している場合はスコアが一番高いもの(つまり一番珍しいタグ)を使って計算し、これを集合間の類似度としています。

たとえばサークル1とサークル2の類似度を計算する場合は {機械学習, 数学, R} 集合と {機械学習, ハードウェア, IoT, 数学} 集合の類似度を考えます。
集合の共通要素は {機械学習, 数学} なので次のように類似度を計算できます。

1
2
3
4
similarity = max(
1 / (log [機械学習タグを持つサークルの総数] + 1),
1 / (log [数学タグを持つサークルの総数] + 1)
)

このとき、次の条件(配置上の気持ち)を反映させるために定義しています。

  • タグが多いサークルが極端に有利になることを避けたい
  • 珍しいタグが共通している場合は類似度が高くなるようにしたい

こなれておらず、まだ変更の余地がある部分なので次回はもう少し調整するかもしれません。

なにはともあれ類似度を定義したので次のような類似度表を得ることができました (数値は実際のものとは異なります)。

サークル1 サークル2 サークルN
サークル1 0.3 0
サークル2 0.3 0.5
サークルN 0 0.5

グループ分け問題の定式化と解法

前述までに定義し、算出した類似度を元に似たサークルが集まるようグループ分けを行います。ここでいうグループは「あ-X」のような同じひらがなで括られるようなブロックに近いものですが実際には会場の形を考慮してもう少し細かくするなどの調整を行い、最終的にはだいたい20サークルずつのグループに分けることにしました。

少し手を抜いた表記ですが、次のように同じグループに属しているサークル間の類似度の総和を最大化することがゴールになります。

1
max Σ y_i_j * s_i_j

y_i_j はサークルiとサークルjが同じグループに属していれば1、そうでなければ0とします。

s_i_j はサークルiとサークルjの類似度です。

各グループには次のような制約があり、この制約下で前述の目的関数を最大化していきます。

  • グループごとに収容可能なサークル数の上限が存在する
  • サークルは1つのグループにしか所属することができない

最適化アルゴリズムは「ランダムにサークルを配置したあとグループ間でサークルの交換を検討し、目的関数値が改善するならば交換を行う」というシンプルなものを採用しました。数理最適化出身の人間として、ここはいずれもっと良いものを用意したいなと密かに考えています。

グループ内配置の定式化と解法

グループ分けが済んだら今度はグループ内での配置を決定します。グループ内で各サークルは一直線に並ぶという仮定をおき、今度は両隣のサークルとの類似度の総和が最大になるような並び順を求めるという定式化をしました。

ここもアルゴリズムは同様で、ランダムに並び順の入れ替えを検討して目的関数値が改善するなら並び替えを実行するという手法で実装しています。
この部分は問題の規模も小さいのできちんと解き切ることもできたはずですが、言い訳として配置発表予定日の3日前の夜の会話を置いておきます。

1
2
3
@vvakame 「グループ分け良い感じなんだけど、グループ内の細かい配置もないとつらくない?」
@shuhei_fujiwara 「次回までに欲しいですね」
@vvakame 「明日までに欲しくない?」

NG集

結果としてはかなりシンプルな方法になりましたが、ここに着地するまでにそれなりの紆余曲折があったのでNG集をお楽しみください。

  • サークル数500程度なら整数計画問題を解き切れるのでは?
    • 全然だめでした
  • それなら連続緩和して適当に丸めればなんとかなるのでは?
    • 緩和したら完全にダメなタイプの問題でした
    • 1つのサークルが小数点以下の単位に細かく千切られてすべてのブロックに配置されていきました
  • せめてグラフの問題に落とし込みたい
    • 何か上手いモデリングができないか考え中です
  • 量子コンピュータってやつでなんとかして!!
    • まさかのなんとかなるらしいことが判明
    • 技術書典6の配置は量子コンピュータで算出されます(嘘です)!

サークル参加の皆さんへのお願い

ここまで読んでくださった方はおそらく気付いているかと思いますが、自動配置の精度には各サークルにジャンルのタグが適切に付いているか重要です。
タグの自動付与なども検討はしていますが、やはり自己申告してもらうのが一番確実です。
次回以降、もしかしたら申込みの際にジャンルを申告することを求められるかもしれませんが、その際はより良い配置のためと協力して頂けると大変助かります。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×