Monoxerで画像や音声が付いた問題をインポートする仕組み

モノグサ開発チームの伊東です。今回は、MonoxerのBookをCSVファイルから作成する時に、画像や音声ファイル(以下メディアファイルと呼称)をシステムの内部でどのように扱っているかについて説明します。

Bookインポート機能とは

BookとはMonoxerにおける問題集のようなものです。MonoxerではBookを作成する主な方法として、大きく分けて2通りを提供しています。

  • WEB管理画面上で作成する方法
  • CSVファイルを作成しておいて、それをWEB管理画面から読み込ませる方法(Bookインポート機能)

WEB管理画面上で作成する方法は、ブラウザ上でマウスとキーボードをつかってBookを作成できるので比較的始めやすい方法です。

CSVファイルを作成する方法は、定められたフォーマットのファイルを書く必要がありますが、大量に問題を作成したい場合は、便利な方法です。特に同じ形式の問題をたくさん作る場合は楽になります。

【参考】

モノグサ管理マニュアル:Book(問題集)を作成する

※Bookインポート用のCSVファイルの例

・通常の英単語など

・音声を使うBook

・画像を使うBook

Bookインポート機能でメディアファイルを使いたい場合、CSVファイルとは別に、メディアファイルをアップロードする必要があります。 CSVファイルでBookを作成する前に、まずメディアファイルをアップロードしておきます。 CSVファイル中には、アップロード済みのファイル名を書くことで、ファイルを指定する仕組みにしています。

www.notion.so 使用するCSVファイルを①にインポートします。画像や音声を使う場合は②にインポートします。([.jpeg]と[.mp3]に対応しています) f:id:corp_monoxer:20211001171217p:plain

※ファイル一式をZIPファイル1つにまとめてアップロードするという方法も考えられますが、Bookを作成するユーザにとっては、ZIPファイルにまとめる手間がかかります。 CSVファイルは、Book完成までに何回か試しにインポートしてみて、修正を繰り返すのが普通ですので、その度にZIPファイルのまとめるのは面倒すぎます。 (実際のところサービスを実装する上でもファイルの処理が煩雑になって面倒そうです。)

このあたりの仕組みについてもう少し詳しく説明していきます。

アップロードされたファイルは、クラウド上のストレージ(オブジェクトストレージ)に格納されます。この際に、元のファイルの名前を使わず、新しいファイル名を作ってストレージに保存しています。なぜ元のファイル名を使わないのでしょうか?

名前を付け替えないと?

元のファイル名のままストレージ上に保存するというやり方だと、以下のような不都合があります。

  • 名前が容易に衝突する。そして上書きされると、既存Bookが壊れる。
  • 上書きされないような名前をユーザに付けてもらうのは不便
  • 中身が同じ別名のファイルが発生しうる

作成中のBookについては、一度アップロードしたファイルの差し替えが出来ることは望ましいのですが、以前に作成したBookには影響を及ぼさない方が良い。というわけで、名前を付け替えるのがよいです。

※インポート前に予測可能な規則を定めて、ユーザがアップロード時にファイル名を付与する方法も考えられます。例えば、エントリ番号を名前に付けて123.jpegなど。これはCSVファイル中でファイル名を書かなくても良くなるかもしれない(規則的なファイル名を暗黙的に指定できる)という利点がありますが、 以下のような欠点がありそうです。

  • ユーザがBookに対応したファイル名をBook毎につけ直すのは面倒
  • インポート前に毎回ファイルをアップロードし直す必要があるかもしれない
  • 実質的に同一のファイルを、システム上で同一視できない
  • 中身が同じファイルでもストレージ上で重複して保存される

さて、新しいファイル名の作り方です。すぐ思いつきそうなやり方としては、

  • 連番を振る方法
  • ランダムなファイル名を付ける方法

が考えられます。

これでファイル名が重複することは(ほぼ)ないのでよさそうな方法ですが、これらは事前に予測が難しいという問題点があります。これらをCSVファイル中にそのまま指定することは現実的ではありません。 インポートでメディアファイルを指定するためには、CSVファイルを用意する段階で、ファイルを特定できる情報(ファイル名)が予測可能であり、CSVファイル上で指定できる必要があります。 また、同じファイルを何度もアップロードした場合でも、別の名前のファイルとして、ストレージに保存されることになります。ストレージ容量が無駄になります。

同一オブジェクトの同一視と、重複排除の手法

オブジェクトストレージ上のオブジェクトのファイル名(以降、オブジェクト識別子)としては、中身が同じファイルを複数保存しないためには、実質的に同一とみなすべきオブジェクトの持つ特徴的な情報を識別子として利用するのがよいです。

今回はオブジェクトの本体のダイジェストハッシュ値を用いることにしました。つまり本体がバイト単位で完全一致した場合に同一オブジェクトとみなす方法です。

これにより、いつアップロードされたかにかかわらず、中身がファイルならば、システム内部で同じオブジェクトであるという扱いができるという利点があります。 これによりシステム内部で同一オブジェクト(画像や音声)は同一視されます。

  • オブジェクト識別子は、Bookの中で記憶度を検索するためのキーの一部としても利用されているため、同一のオブジェクトとその記憶度とを一対一対応させることができます
  • ストレージ上で重複排除されます

※実装ではハッシュ関数としてSHA256を用い、同一でないオブジェクト同士で識別子の衝突が発生することがないように留意しています。同様の手法はgit等で広く使われています(gitではSHA1)。

f:id:corp_monoxer:20211001171238p:plain
オブジェクトストレージ上の画像を入れるバケツ

インポート時のメディアファイルの特定方法

ユーザがCSVファイルを用意する時に、事前にアップロードしたときのメディアファイル名を、CSVファイル中に指定してもらいます。 そして、Bookインポート時に、CSVファイル中に指定されたファイル名を、ストレージ上のオブジェクト識別子に読み替えます。このために、ファイル名とオブジェクトの対応表を用意しています。この対応表は、メディアファイルをアップロードした段階で更新してあります。

f:id:corp_monoxer:20211001171248p:plain
ファイル名とオブジェクトの対応表

一度作ったBookの中の画像や音声は、サーバ上ではオブジェクト識別子(≒ストレージ上のファイル名)を使って表現されています。(元のファイル名とストレージ上のファイル名の対応表を参照して作成されます。) この仕組みにより、Bookインポート後のメディアファイルアップロード操作の影響は受けません。つまり同一ファイル名で、中身が異なるファイルをアップロードしても作成済みBookの画像や音声には影響を及ぼしません。

もし、画像や音声を変更したい場合は、別のファイル名でアップロードし直して、CSVファイル中でその別のファイル名を指定することで差し替えることができます。

メディアファイルのアップロード機能と、CSVファイルのインポート機能は、それぞれ独立しています。つまり、一度アップロードしたら、ブラウザをリロードしたりしても影響せず、その後のインポートで何度でも利用可能です。したがって、一度メディアファイルをアップロードすれば、あとは、CSVファイルだけ修正とインポートを繰り返すことができます。

もちろん、都度メディアファイルアップロードしてもらっても問題ありません。 上で説明したように、中身が同じファイルは1個だけオブジェクトストレージに保存されます。さらに、クラウドサーバでは一般的にアップロード方向の通信量には課金されないのでサービス運用側としては同じファイルを何度アップロードしてもらっても特にデメリットはないのです。

Bookインポート機能の内部処理のまとめ

1.メディアファイルのアップロード時

  • オブジェクト識別子を決定する(ダイジェストハッシュ値を計算)
  • オブジェクト本体をストレージに保存する
  • ファイル名とオブジェクトの対応表に、ファイル名とオブジェクト識別子を登録する

2.CSVファイルによるBookインポート時

  • ファイル名とオブジェクトの対応表を参照して、CSV中で指定されたファイル名から、オブジェクト識別子を取得する
  • Bookで使うオブジェクトをダウンロードするためのキーとして、オブジェクト識別子を使う
  • 記憶度を保存、検索するためのキーの一部として、オブジェクト識別子を使う

今回は、Bookインポートでメディアファイルを使う仕組みについて解説しました。私からは以上です。

モノグサ株式会社では一緒に働く仲間を募集しています。

少しでも興味を持っていただけた方はぜひお話しましょう!

careers.monoxer.com

文系出身の私がモノグサでエンジニアを始めた理由

f:id:corp_monoxer:20210905154159j:plain

モノグサ株式会社のTobisatis(@tobisatis)です。2021年2月にソフトウェアエンジニアとして入社しました。

当社には大学や大学院で計算機科学を専攻していたような、技術的に非常に優秀なエンジニアが多く集まっています。そんななか、計算機科学どころか理系ですらない、経営学部出身の私がモノグサで働きはじめる話をしたいと思います。

大学卒業まで

高校卒業まで、私は理系の生徒でした。といっても、理科や数学が得意だったわけではありません。在学中、理系から文系は変えられるけれどその逆は難しいと聞いたからでした。理科の中でも物理ではなく生物を選んでいたので、典型的な理系の生徒像とは少し違うかもしれません。

高校時代に勉強していた生物は割と楽しかったので、特に根拠もなく「iPS細胞を応用して、食糧問題や環境問題、エネルギー問題などのあらゆる問題を解決する!」と意気込むようになりました。大学受験は第一志望に落ちたため、合格した大学の経営学部に進学することにしました。

大学生活は特に変わったことをせず、しばらく過ごしていたのですが、大学4年でいわゆるレールから外れることになりました。

大学での社会科学の勉強を通してさまざまな思考を巡らせた結果、大雑把に表現すると「裕福でない子どもたちにも、よりよい教育を提供する必要がある」という結論に達し、これに使命感を抱くようになりました。しかし、この目標はビジネス的にはあまり歓迎されるものではないと気づき、就職によってこれを達成しようとすることはやめました。新卒での就職をしないことに決めたのです。

就活をせずに時間があったため、なんとなく始めたことがありました。それが競技プログラミングです。競技プログラミングとは、与えられた問題を解くためのプログラムを提出して得点や速度を競う競技です。これまで、確率の問題を解くためにJavaScriptを使って簡単なシミュレーションをしたり、無料レンタルサーバーを使ってPHPで遊んでみたりとプログラミングに触れる機会があったので、競技プログラミングを始めたのは自分にとって自然な成り行きだったな、と思います。

大学卒業後

先のような使命感から、「自分で塾を経営する」という計画を立てていました。しばらくの生活費を稼ぐためにフリーランスのPHPエンジニアとして8ヶ月ほど働いたのち、2020年5月、私の両親が神奈川県茅ヶ崎市で経営する「カフェのあるめがね屋さん一丁目四番地」内で「総合教室Salite(サリーテ)」をオープンしました。

しかし、新型コロナウイルスの影響もあり、宣伝が思うようにできませんでした。約1時間のマンツーマン授業を990円で受講できる破格のプランを掲げていても、問い合わせはなかなか増えませんでした。この塾の人気が上がる見込みを期待できない状況だったため、悶々とした日々を送っていました。

モノグサとの出会い

そんななか、Wantedly(ビジネス向けSNS)を経由して、モノグサ株式会社から直接、ソフトウエアエンジニア採用のお誘いをもらい、「教育の業界を少しでも勉強できたらラッキー」くらいの軽い気持ちで面接を受けることにしました。

そのときは、あっけなく落ちるだろうと思っていました。チームでの実務経験が高々8ヶ月。塾の営業に必要なホームページやカレンダー関連のシステムは自作していたものの、職業エンジニアとしてはブランクがある状態。転職市場ではほとんど評価されない経歴だろうと根拠はないものの感じており、胸を張って面接に臨むことはできませんでした。

結果的に内定を得ることができ、そのときは信じられませんでした。技術面接ではひたすらに自分のエンジニア領域の知識のなさを気付かされ、恥ずかしさのようなものだけを感じながら帰路についた覚えがあります。ただ、そんななか振り返ると、競技プログラミングで培った問題解決能力が高く評価されたのだろうと感じています。

競技プログラミングで得た力

私はAtCoderというサイトで競技プログラミングに参加していました。AtCoderにはレーティングのシステムがあり、定期的に開催されるコンテストで良い結果を出すほど高いレートが付きます。コンテストは出題される問題の難易度によって、ABC、ARC、AGCという種類に分かれています。

コードを書くのはとても遅いものの難しい問題の考察に取り組むことが好きだった私は、最も難易度の高いAGCに出場して2問だけ選んでじっくり解く、という戦略をよくとっていました。

業務における開発では、ひとつの問題にじっくりと取り組む力が重要です。このため、大学院で熱心に研究に打ち込んでいるような学生は、モノグサでも高く評価される傾向にあります。私は大学院に行っておらず、学部での研究も卒業論文の提出の他にはなかったのですが、競技プログラミングを通してこの力を身につけることができました。

f:id:corp_monoxer:20210905153625j:plain

入社を決めた理由

モノグサのエンジニア採用は、技術面接をすべて通過すると最終面接に移る、というプロセスです。最終面接ではエンジニアだけでなく、さまざまな部署のメンバーと面接をします。

就職をすることは、そもそも私にとって目的ではありませんでした。したがって、技術面接を通過したあとも、面接の準備や練習を一切することなく最終面接に行きました。

最終面接では、自分がどのような思いで塾の先生をやっているか、すなわち、先に述べたビジネス性がないと結論付けた目標をひたすら正直に語りました。おそらくこのときは生徒が多く集まらない孤独感から話を聞いて欲しい思いが強くなってしまっていたのだろうと振り返ると、顔が赤くなります。

モノグサに入社を決めたのは、内定をもらったからに他なりません。メールボックスにモノグサからの内定通知が届いたとき、道を間違ってはいなかった、と感じたのを覚えています。

現代に生まれたものとして、掲げる理想だけでも高潔でいたい。経済的な成長のためだけにそれを犠牲にしたくない。こんな思い全てを明かしても私を迎え入れてくれるこの会社でなら、自身の尊厳を毀損せずに生きられるだろう。

そう確信し、私は入社を決めました。

入社してからは、周りのエンジニアの優秀さに圧倒される日々が続いています。

それでもできることを自分なりに探しながら、「記憶を日常に。」のスローガンの元で自分の信念に基づいて仕事をできている今は、過去になく充実しています。

モノグサ株式会社では一緒に働く仲間を募集しています。

少しでも興味を持っていただけた方はぜひお話しましょう!

careers.monoxer.com

『記憶を日常に。』を実現するための大切な仕組み。Monoxerのオフライン学習とは

モノグサエンジニアの井岡です。 モノグサでも技術的な情報を技術ブログとして発信していきます。プロダクトのMonoxerに関連することだけでなく、モノグサ社員が趣味で行っている内容等についても紹介していく予定です。

記念すべき第1回は、Monoxerの特徴的な機能であるオフライン学習についてご紹介します。

Monoxerとは

Monoxerは解いて憶える記憶アプリです。憶えたい内容を登録すると、記憶している状況(=記憶度)に応じてアプリが問題を生成します。まったく憶えていないときは写経形式(薄く答えが出ている状態で回答)で、少し憶えているときは択一形式で、すでに記憶していそうであれば自由入力形式で、といったように記憶度に応じて出題形式が変化します。そのため無理なく自然に憶えることができます。

出題形式をコントロールするための重要な機能として記憶の一元管理*1があります。簡単に説明すると、問題を要素に分解し、それぞれの記憶度を管理するための機能です。例えば、数式の「2+3」であれば整数「2」、「3」と、「足し算」の要素に分解できます。これらを記憶している状況で、「3+4」を出題する際には、整数「3」と「足し算」は記憶しているとみなして、出題形式を決定しています。

管理機能も充実しており、塾や学校の先生が生徒に課題(=タスク)を配信できます。配信したタスクを生徒が学習すると、その学習結果や記憶度を逐次サーバーに送信していきます。そのため、生徒ごとの進捗や記憶度をリアルタイムに確認することができます。

f:id:corp_monoxer:20210819163503p:plain

オフライン学習の仕組み

Monoxerを用いた記憶は日常的に行われるものであり、飛行機や地下鉄の中、山でのキャンプ中等のように通信が不安定な環境下でも学習できる必要があります。もちろん、タスク配信時にはサーバーからデータを取得する必要がありますが、学習については可能な限りオフラインでも実行できるような仕組みにしています。

Monoxerでの学習は以下のような流れで行います。

  1. サーバーからタスクをダウンロード (初回のみ)
  2. タスクを学習
  3. 学習結果・記憶度をローカルに保存
  4. 学習結果・記憶度をサーバーに送信 f:id:corp_monoxer:20210819163527p:plain

サーバーに確実にデータが保存された状態にするために、データがサーバーに保存されたかどうかをdirtyフラグを用いて管理しています。ローカルに保存するタイミングではdirtyフラグをtrueにし、サーバーへの保存が成功した場合にfalseに変更します。オフライン時には上記4.のサーバーへの送信が失敗するため、dirtyフラグがtrueのままローカルに保存された状態になります。 f:id:corp_monoxer:20210819163530p:plain

その後、端末がオンラインに変わったタイミング*2で、ローカルに保存されている学習結果や記憶度のうちdirtyフラグがtrueのものをすべて取得し、あらためてサーバーに送信します。

f:id:corp_monoxer:20210819163523p:plain

また、記憶度は最新のものがサーバーに保存された状態になっています。Monoxerでは複数端末での学習も可能なため、保存されているものと受け取ったもののうち新しいものが保存されるようにタイムスタンプのチェックも行っています。

他にもサーバーでの保存は成功しているが、レスポンスを受け取れないということも起こり得ます。この場合、ローカルに保存されている学習結果のdirtyフラグはtrueのままであるため再度サーバーにデータが送信されますが、サーバーではすでに同じデータが保存されているため、重複して保存されないようにする必要があります。そのため、サーバーで管理しているIDとは別に、ローカルでユニークIDを生成し、これを比較して保存済みかを判断しています。

まとめ

Monoxerでは日常的に記憶するために様々な機能を提供しています。今回はその中のオフライン学習についてご紹介しました。様々な状況での学習に対応するために、整合性を取れるようにデータを管理しています。 他の機能については、別の機会にご紹介できればと思います。

*1:記憶の一元管理の詳細はCTOのインタビュー記事をご確認ください。

*2:iOSではReachabilityのwhenReachableおよびwhenUnreachableを用いて通信可否の変更を検知しています。Androidではandroid.net.ConnectivityManagerのNetworkCallbackを用いて通信可否の変更を検知しています。