Next.js(App router)における開発しやすいディレクトリ構成の例

初めまして、テックドクターでフロントエンド開発を担当している大瀧です。

ディレクトリ構成はコードの可読性やスケーラビリティに関わる重要な要素であると思っています。

しかし、フロントエンドのディレクトリ構成はベストプラクティスが確立されておらず、わりと悩むポイントです。

そこで今回は、Next.jsのApp routerにおいて、弊社で採用しているディレクトリ構成を共有します。この記事がディレクトリ構成に悩む開発者の助けになれば幸いです。

ディレクトリ構成の自由度が高すぎる問題

さきほど「フロントエンドのディレクトリ構成はベストプラクティスが確立されていない」と書きましたが、特にApp routerのディレクトリ構成については、公式ドキュメントで以下のように記載されています。

There is no "right" or "wrong" way when it comes to organizing your own files and folders in a Next.js project.
 
Next.js プロジェクトでファイルやフォルダを整理する方法には「正しい」「間違っている」はありません。

引用元:Routing: Project Organization | Next.js(日本語訳:プロジェクト編成とファイル・コロケーション | Next.js 公式ドキュメント 日本語翻訳プロジェクト

正解も不正解もないと明記されているものの、これは自由度を高める一方で、開発者からすると悩ましいです。実際のプロジェクトにおいては、ある程度ルール化しておかなければコードの品質が保てませんし、保守しづらいコードは開発速度低下を招いてしまいます。

そこで本記事では、テックドクターで私が手がけているHealth Portalというプロダクトの構成を共有します。Health Portalは、クライアント(ヘルスケアアプリやSaMDを開発したい企業)がデジタルバイオマーカー*1を利活用するためのプラットフォームです。JavaScriptフレームワークにNext.js(App router)を採用しています。

Health Portalの技術スタック

前提として、フロントエンドで採用している主なライブラリはざっくり以下のとおりです。

  • Next.js v14
  • Tailwind CSS
  • React Hook Form
  • zod
  • keycloak.js

またAPIはFastAPIで実装しており、通信プロトコルにはREST APIを採用しています。

弊社のディレクトリ構成とその解説

ディレクトリ構成と各ディレクトリの役割

まず、ディレクトリ構成の全体像は以下のとおりです。

src
|
+-- app        # Routing files(Next.jsが標準で提供するファイル)
|
+-- components # 横断的(ドメインに依存しない)なUIコンポーネント
|
+-- features   # 特定のドメイン・機能に関係するコンポーネント
|
+-- hooks      # ドメインに依存しない、横断的なhooks
|
+-- providers  # アプリケーションプロバイダー
|
+-- utils      # 横断的な汎用関数
|
+-- constants  # 横断的な定数
|
+-- types      # 横断的な型定義
|
+-- styles     # スタイリング(css)に関するファイル
|
+-- lib        # ライブラリの処理や標準処理を共通化したコード
|
+-- tests      # 自動テスト関連

 

  • app: ルーティングの責務を持つディレクトリ。Routing files(Next.jsが標準で提供するファイル。page.tsxやlayout.tsxなど)のみを配置します
  • components: ボタンやモーダルなど、再利用可能でドメインに依存しないUIコンポーネントを格納します。
  • features: 特定の機能に関連するファイルを集約し、機能ごとのサブディレクトリに格納しています。例えば、ユーザー管理機能に関連するコンポーネント、hooks、ユーティリティ関数、型定義などをこのディレクトリ内でまとめます。
  • hooks: グローバルに使用されるカスタムフックを格納します。ドメインに依存しない汎用的なロジックはここにまとめます。
  • providers: ReactのContext APIやその他のプロバイダーを配置します。グローバルな状態管理やテーマ設定など、アプリケーション全体で使用されるプロバイダーをここに集約します。
  • utils: 簡潔で再利用可能なユーティリティ関数を格納します。これには、日付処理、文字列操作、API通信など、特定のドメインに依存しない関数が含まれます。
  • constants: アプリケーション全体で使用される定数を格納します。
  • types: TypeScriptの型定義をまとめます。
  • styles: グローバルスタイルやテーマ設定、Tailwind CSSのカスタマイズを含むスタイルシートをここに配置します。
  • lib: プロジェクト内で再利用されるライブラリや、外部ライブラリの実装をここに格納します。
  • tests: ユニットテストや統合テストなど、自動テストに関連するファイルを格納します。

基本的には、Reactアプリ開発のベストプラクティスと言われるbulletproof-reactを参考にしてディレクトリ構成を考えています。

bulletproof-reactについてはmejinさんの記事がとてもわかりやすくまとまっているのでご参照ください。
Reactベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件

肝となるのがfeaturesディレクトリで、ここには機能ごとのファイルが格納されます(詳細は後述)

以前はコンポーネントの粒度毎にディレクトリを分割するAtomic Designが流行ってた印象ですが、ドメイン毎にディレクトリを分類するほうが、フレームワークへの依存度が少なく学習コストやフレームワークの移行ハードルが低いように思います。

またClean Architectureの著者であるRobert C. Martinさんも記事の中で以下のように述べています。

Your architectures should tell readers about the system, not about the frameworks you used in your system.
 
あなたのアーキテクチャが読み手に伝えるべきなのはシステムのことであって、使用したフレームワークのことではありません。

引用元:Clean Coder Blog(日本語訳は筆者)

featuresの切り方について

featuresディレクトリの構成は、名のとおり機能毎にディレクトリを分割しています。

ディレクトリ分割の判断基準としては、オブジェクト指向UIというUI設計思想に基づき、分類しています。 オブジェクト指向UIとは、簡単にいうとオブジェクトを起点にユーザーのアクションが設計されたUIのことで、ここでいうオブジェクトとは「ユーザーが関心を寄せるモノ、ユーザーの操作対象となる名詞」を指します。このオブジェクト(ユーザーの関心事)ごとにディレクトリを分割しています。

例えば、「ユーザーの作成」や「ユーザーの削除」等のアクションがある場合、オブジェクトは「ユーザー」となり、関連するソースコードはuserディレクトリに格納することになります。 オブジェクト指向UIの詳細については、ぜひこちらを読んでみてください。

オブジェクトごとのディレクトリ配下の構造についても、基本的にbulletproof-reactに則っており、下記の図のとおり特定のドメインに依存するコンポーネントを格納します。

features            
|
+-- user            # 特定のドメインに関するディレクトリ
|   |
|   +-- hooks       # 特定のドメインに関するhooks
|   +-- utils       # 特定のドメインに関する汎用関数
|   +-- components  # 特定のドメインに関するコンポーネント
|   +-- types       # 特定のドメインに関する型
|   +-- providers   # 特定のドメインに関するプロバイダー
|   +-- pages       # appディレクトリ配下から呼び出されるページコンポーネントを格納する
|       +-- users_page.tsx
|       +-- user_new_page.tsx
|       +-- ...
+-- ...

しかし、オブジェクトごとのディレクトリすべてが完全に同じ構造を持つ必要はなく、その機能に必要なディレクトリのみを作成します。

appディレクトリとfeaturesディレクトリの対応関係について

features/xxx/pagesに格納されたページコンポーネントがエントリポイントとなり、appディレクトリ配下の対応するpage.tsxから呼び出されます。

app                 
|
+-- users            
|   | 
|   +-- page.tsx     # `features/user/users_page.tsx`を呼び出す
|
features
|
+-- user        
|   |
|   +-- pages 
|       +-- users_page.tsx
|       +-- ...
+-- ...

細かなルールについて

異なるfeature間での依存はNG

各featureは独立して機能するべきであり、他のfeatureに依存しないことで低結合・高凝集なソースコードを実現することができるため、異なるfeature間での依存はNGとしています。

横断的なコンポーネントに特定ドメインの要素を含ませるのはNG

コンポーネントを汎用化し、かつfeatureの凝集度を高めるために、横断的なコンポーネントに特定ドメインの要素を含めるのはNGとしています。

簡単な例ですが、以下のようにButtonコンポーネントにuserドメインを含めると汎用性が落ちてしまいます。

type ButtonProps = {
  user: {
    role: 'admin' | 'user';
  };
} & ButtonHTMLAttributes<HTMLButtonElement>;

const Button: FC<PropsWithChildren<ButtonProps>> = ({ user, children, ...props }) => {
  const buttonClass = user.role === 'admin' ? 'bg-red-500' : 'bg-blue-500';

  return (
    <button {...props} className={buttonClass}>
      {children}
    </button>
  );
};
ファイル名にパスカルケース・キャメルケースを使用しない

MacWindowsではファイル名の大文字小文字を区別しませんが、Linuxではこれを区別するのでファイル名はスネークケースで統一してます。

本構成のメリット

コードリーディング時の認知負荷が下がる

単一の機能毎にディレクトリが分かれているため、コンテキストが統一されコードの理解がし易いと個人的には思っています。

他の開発者とコンフリクトしづらくなる

機能毎にディレクトリが分かれているため、機能修正や追加の際にコンフリクトが起きにくくなります。大規模な開発体制の場合は開発生産性が結構上がるんじゃないかと思います。

フロントエンドエンジニア以外も多少コードリーディングが楽になる

これは実際に確認したわけではないのですが、フロントエンド特有のしきたりではなくドメイン毎にディレクトリが分かれているため、プロダクトそのものの知識があればコードリーディングが楽になるような気がしています。

デメリット・改善点

やっぱりfeaturesディレクトリ配下の切り方が曖昧

オブジェクト指向UIの考え方を参考にfeatureを分類することで、開発者間で一応は共通認識を持つことは出来ているような気はしつつ、明確な線引があるわけではないのでたまに悩んでしまいます。ラクスさんの場合は画面仕様書単位で切り分けているようです。

最終的な画面表示のロジックはfeatures/xxx/pagesではなく、app配下で実装してもいいかも

features/xxx/pagesを作成すると責務の分離ができる反面、該当のページがどのURLに対応するかが分かりづらくなってしまいます。
なので、pageのレンダリング処理に関してはapp配下で実装してもいいかもしれないです。
実際、元VercelのエンジニアであるSteven Teyさんが開発している、dubというプロダクトではapp配下にpage.tsxpage-client.tsxを格納する設計になってます(以下を参照ください)
dub/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/tokens at 35973de925857f4952d7910a2424ffab478ad2cb · dubinc/dub · GitHub

正直なところ、ルーティングとレンダリングの責務を分離することに今のところ大きなメリットを感じていないので、こっちのほうが分かりやすいかも知れません。

*1:ウェアラブル端末やスマホから取得できるデジタルの情報をもとに、病気や治療の変化を可視化するための指標

データ解析で伝える「調子」~心拍・運動・睡眠データから得られる意味

こんにちは。データサイエンスチームの坂本と申します。TechBlog第四回では、日常生活中の心拍や睡眠などのデータを解析する意義や可能性について、分析者の視点からお話しします。

私たちは、スマートウォッチを代表とするウェアラブル端末で測定されたデータの解析を行っています。このデータ解析という行為を少し抽象的に言い換えると、「数字の羅列から意味を見つけ出す技術」であると表現できます。

そこから得られる「意味」とは、いったい何であり、何の役に立つのでしょうか?

心や身体の「調子のバロメータ

私たちが主に取り扱う心拍・運動・睡眠のデータ*1は、いわば心や身体の「調子のバロメータ」です。

これは体温計のようなものだと考えると馴染みやすいかもしれません。37度の微熱だとか、39度近い高熱だといったように、自分の体調やしんどさを家族やお医者さんに伝える時の「言葉」になるものです。

もちろん、体温だけではわからないことも多いです。ですが咳が出る等の周辺の情報と組み合わせることで、その人の心や身体の状態への理解が深まり、病気の診断や、適切な薬の処方に貢献します。

デジタル生体データも、これによく似ています。自分の体の変化を数字でとらえることで、客観的に体調の変化を表すことができます。

病気と健康の「第三の情報源」

これまで、心身の調子は主に「本人の感覚」や「病院での検査」などで捉えられてきました。もちろん現在も、これらは様々な研究や病気の診断などで、最も重要なものとして考慮されています。

しかしその一方で、本人でさえ気づいていない心身の変化や、病院では見えてこない日常生活のしんどさといったものも存在します。それらは見落とされたり、診察の時間中だけで伝えきることが難しいために取りこぼされてきました。実はここに、「本来必要な情報の不足」が発生しています。

デジタル生体データは、いろいろな条件でデータを抽出したりすることで、本人も気づかないような「日々の調子」を捉えることができます。例えば「眠っている時に心拍数が急激に変化している」などは、本人であっても自覚が困難ですが、病気の診断や予防に重要な役割を果たしそうですよね。私たちデータ解析者は、こういった情報をうまく抽出し、わかりやすく活用できるよう、様々な解析をしています。ひとつひとつの開発と検証が進む中で、デジタル生体データは、「病気と健康の第三の情報源」として確立したものになってゆくだろうと予感しています。

図1. デジタル生体データは病気と健康の第三の情報源

データ解析の面白さと様々な工夫

さきほど、デジタル生体データは調子のバロメータであり、体温計のようなものであると表現しました。しかし、体温計の場合、「熱がある」というだけでは、原因が何かまではわかりません。

他方、デジタル生体データの場合、「心拍数が高くなっている」という情報から、その背景理由を推測したり、将来起きる出来事を予測できる場合があります。心拍数が上がる理由の代表例は「運動」「飲酒」「発熱」などですが、細かく分析を進めてゆくと、心拍数の「上昇のしかた」が全て異なっているということを発見(!)したりします。

データ解析では、こういった心拍数の「上昇のしかた」を特定し、データを上手に加工することで、「運動」「飲酒」「発熱」を判別できるようにしたりします。すると、「先月、何日間ほど発熱で寝込んでいたか」や「1ヶ月〜1年でどれくらいお酒を飲んでいたか」が分かるようになります。「風邪を引いて治ったと思っていたけれど、まだ完全には身体の状態が戻っていなかった」ということにも気がついたりします。こういった技術は、本人が生活を振り返ったり、誰かに伝える場合だけでなく、研究などにも応用できると考えられます。

図2. 発熱に反応する心拍バロメータの例

また、スマートウォッチ等で測定されるデジタル生体データには、新しい第三の情報源としての利点がある一方で、いくつかの弱点もあります。着脱などによる測定データの欠落がその一例です。こういったデジタル生体データの特徴に対して、データ解析の視点からは「解析結果の品質を守るための取り組み」が必要になります。

例えば「睡眠不足」という情報を抽出したい時には、毎日の睡眠データが必要になります。しかし、寝ている時にスマートウォッチを外した日や、睡眠中に電池が切れてしまった時には、データの欠落が発生してしまいます。このような場合、データ解析者は「欠損補完」という技術でデータや解析結果の品質をカバーしようと試みることがあります。

欠損補完にも様々な手法が存在し、ケースバイケースで最適な方法が異なります。そこで、「わざとデータを欠落させて、欠損補完をした時の復元度合いを確認する」といった方法で、最適な欠損補完方法を検証したりしています。

図3. 欠損補完の性能比較(一部の手法のみ掲載)

このように生体データ解析では、工夫すればするほど様々な価値が出てきます。株式会社テックドクターには、心理学を勉強してきた私を一例として、様々なバックグラウンド/専門分野を持つデータサイエンティストが所属しています。それぞれの分析者が持つユニークな視点や、得意な技術を活かすことができる領域であり、そんなところも魅力のひとつです。

デジタル生体データの解析は可能性の宝庫

ここまでで紹介できたのはほんの一例に過ぎませんが、デジタル生体データの解析は可能性に満ちています。これからたくさんの発見があり、一つ一つの発見が実用化につながってゆくと予感しています。

実用化の領域は、一人ひとりが関心を持っている事柄(執筆者の場合は、ランニングで心肺機能が回復したかを確認したりすること)をセルフチェックしたり、病院での診察場面や、病気の予防や治療効果に関する研究など、様々だと考えられます。どのような応用例であっても、データや分析結果の品質を守る取り組みは、丁寧に進めていきたいですね。

自分自身の心身の調子を、家族や友人、お医者さんに話すとき。あるいは、研究という取組を通じて、一人ひとりの体験を世界に伝えてゆくときに。デジタル生体データが、「人と人とが理解し合うための言葉」として使われることを願っています。


書いた人:坂本

*1:※ 他にも、様々な種類のデジタル生体指標や、検査データ、アンケートデータなどを幅広く取り扱っています。

B+treeで効率良く後方一致検索をする方法

初めまして、テックドクターのバックエンドエンジニアの魚木です。

私が担当するプロジェクトに、データベースのテーブルのあるカラムを前方一致検索する機能があります。そこに部分・後方一致検索もしたいという要望がありました。

そのデータベースはB+treeインデックスが使用されていますが、B+tree (または同系統のtree) インデックスは部分・後方一致検索は効率良くできないと言われています。
結果的に機能追加は見送られたのですが、前方・部分・後方一致検索の違いについて考えるよい機会になりました。

本記事は、B+treeが部分・後方一致検索を効率よくできない理由と、その代替手段として後方一致検索を高効率でする方法を説明します。

MySQLを前提としますが、インデックスの構造が同系統であれば同じような結論になります。

なぜ前方一致検索のみ高効率なのか

まずはtree構造が前方一致検索を効率よく処理できるしくみを説明します。ここではシンプルに、二分探索木で考えてみましょう。

下記のような二分探索木があるとします。

  • 葉ノードのみ実データを持ち、それ以外のノードはキーのみを持つ
  • 隣接する葉ノードは相互にリンクされている
  • キーは8個存在し、それぞれ "aec", "afd", "agc", "ahd", "bec", "bfd", "bgc", "bhd" のデータを持っており、辞書順でソート済みであるものとする。

図にすると、このようになります。

図1:二分探索木の例

完全一致検索の場合

まず、完全一致検索について考えます。完全一致検索は前方一致検索の一種であるため、高効率に処理することができるはずです。

上記のtreeから、あるキーを持つ葉ノードを検索した場合……

  1. 根ノードの "ahd" と比較し、入力が "ahd" 以下であれば左のブランチを進みます。"ahd" より大きければ右のブランチを進みます。1つ階層を降りた時点で、入力と同じキーをもつ可能性のある葉ノードは8個から4個に絞られます。
  2. 同様の操作を深さ1のノードで行うと、候補がさらに4個から2個に絞られ、
  3. 同様の操作を深さ2のノードで行うと入力と同じキーを持つノードが特定されるか、存在しないことが判明します。
図2:二分探索木の完全一致検索の様子(ここでは "bec" を検索)

このように、treeの階層をひとつ降りるたびに候補が1/2ずつに絞られることが二分探索木の探索の効率の良さを生んでいます。

データベースでよく使われるB+treeは各ノードの保持するキーが複数になりますが、階層を降りる毎に候補が1/n (二分探索木の場合はnが2) に絞られるという点で同じです。

前方一致検索の場合

次に前方一致検索について考えます。

同じtreeで、"b" から始まるデータを全て取得したいとします。

下記の手順で、"b" から始まるキーを持つ葉ノードの中で、辞書順で一番早いものを1回の探索で特定することができます。

  1. 最初は根ノードで "ahd" と "b" を比較し、"b" の方が辞書順で遅いので右のブランチを進みます。
  2. 次は "bfd" と "b" の比較で "b" の方が辞書順で早いので左のブランチを進み、
  3. 同様に "bec" のノードでも左のブランチを進むことで左から5番目の葉ノードである "bec" が特定されます。
図3:前方一致検索の様子("b" から始まるデータを検索)

そこから図3の緑色の矢印のように、隣接ノードへのリンクを右方向に、"b" から始まらないキーを持つ葉ノードが出現するか (※)、終わりまで走査し終わるかのいずれかになるまで進みます。ここでは "bec"、"bfd"、"bgc"、"bhd" の4件が該当しました。

※葉ノードが辞書順でソート済みであるので、一度 "b" 以外から始まる葉ノードに到達したら、それ以降に "b" から始まる葉ノードは絶対に存在しません


今回は1文字の前方一致の例を示しましたが、前方一致検索の条件が2文字以上になっても基本的な考えは変わりません。肝心なことは、前方一致する辞書順で一番早いキーを持つ葉ノードを1回の探索で特定できることと、葉ノードの並びが辞書順でソート済みであるため効率的に処理できるということです。

後方一致検索の場合

最後に後方一致検索について考えてみます。

例えば "c" で終わるキーを持つ葉ノードを取得したいとしましょう。

上で説明したように、根ノードから左に降りることは条件に合致する候補を1~4番目に限定することです。また、右に降りることは5~8番目に限定することを意味します。

しかし、下の図4からわかるとおり、"c" で終わる葉ノードは左から1、3、5、7番目に存在し、どちらに降りたとしても条件に合致するキーが反対側にも存在することになってしまいます。

とりあえず、全てのノードで左に進んだとしましょう。偶然、一番左の葉ノードのキーが "aec" と "c" で終わっているので、前方一致検索と同様にそこから右のリンクを進んでみましょう。そうすると次の葉ノードのキーは "afd" であり "c" で終わりませんが、それで走査を打ち切れるでしょうか?

 先に述べたように左から3、5、7番目にも "c" で終わるキーを持つ葉ノードが存在するので、走査を打ち切ってしまうと正しい結果は得られません。

図4:後方一致検索は効率的にできない

別のデータセットであれば、偶然1番目にしか "c" で終わるキーを持つ葉ノードが存在しないこともあるかもしれませんが、いずれにせよ葉ノードを全て調べて結果的にわかることであって、tree構造自体が保証するものではありません。

これらのことから、後方一致検索は葉ノードを全走査しなければならず、大抵の場合とても非効率になります。部分一致検索も同じ理由で非効率になります。

(実際にはこの例のようにデータ数が少ない場合は全走査した方が効率が良い可能性が高いですが、データ数が増えたときのことを想像してみてください)

B+treeで高効率で (擬似的に) 後方一致検索をする方法

これまで述べたように、B+tree (または同系統のtree) で後方一致検索を効率よく行うことは原理的に不可能です。しかし、B+treeで後方一致検索と同じ結果を高効率で得る方法もあります。以下のような手順で実現することができます。

  1. 後方一致検索をしたいカラムを反転したカラムを追加する
    • Generated Column (本記事はこれを使う)
    • トリガー
    • ...
  2. 反転したカラムのインデックスを作成する
  3. 後方一致検索対象の文字列を反転し、そのカラムを条件に前方一致検索する

後方一致検索をしたいカラムを反転したカラムを作成し、それに対してインデックスを作成することによって、後方一致検索を前方一致検索に変換することができます。上で説明したように前方一致検索は高効率で行えるので、事実上の後方一致検索を高速に行うことができます。

実際にやってみる

今回はGenerated Columnを使った方法で説明します。(後方一致検索をしたいカラムを反転したカラムさえあればいいので、トリガーでも実現可能です。)

Generated Columnの詳細は公式ドキュメントを参照していただきたいですが、ざっくりいうとカラム定義に式を使えるようにする機能です。

例えば、メールアドレスを保存するカラムが元からあるとして、それを参照しつつ反転したカラムを作成することができます。

このカラムは、参照元カラムの更新も反映されますし、インデックスを作ることもできます。

例として以下のような`email`カラムと`reverse_email`カラムを持ったusersテーブルを作成します。

CREATE TABLE users (
    id INT AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE,
    reverse_email VARCHAR(255) GENERATED ALWAYS AS (REVERSE(email)) VIRTUAL,
    PRIMARY KEY (id),
    index email_index(email),
    index reverse_emai_index(reverse_email)
);

INSERT INTO users (email) VALUES
    ('test1@test.com'),
    ('test2@test.jp'),
    ('test3@test.com'),
    ('test4@test.jp'),
    ('test5@test.com'),
    ('test6@test.jp'),
    ('test7@test.com'),
    ('test8@test.jp')
;

SELECT * FROM users;
+----+----------------+----------------+
| id | email          | reverse_email  |
+----+----------------+----------------+1 | test1@test.com | moc.tset@1tset |
|  2 | test2@test.jp  | pj.tset@2tset  |
|  3 | test3@test.com | moc.tset@3tset |
|  4 | test4@test.jp  | pj.tset@4tset  |
|  5 | test5@test.com | moc.tset@5tset |
|  6 | test6@test.jp  | pj.tset@6tset  |
|  7 | test7@test.com | moc.tset@7tset |
|  8 | test8@test.jp  | pj.tset@8tset  |
+----+----------------+----------------+
8 rows in set (0.00 sec)

emailreverse_emailに対するインデックスを作成しており、SELECTの結果から想定通りにreverse_emailemailを反転したものであることがわかります。

まずは、emailの「.com」後方一致検索をしてみます。

SELECT * FROM users WHERE email LIKE '%.com';
+----+----------------+----------------+
| id | email          | reverse_email  |
+----+----------------+----------------+1 | test1@test.com | moc.tset@1tset |
|  3 | test3@test.com | moc.tset@3tset |
|  5 | test5@test.com | moc.tset@5tset |
|  7 | test7@test.com | moc.tset@7tset |
+----+----------------+----------------+
4 rows in set (0.09 sec)

EXPLAIN SELECT * FROM users WHERE email LIKE '%.com'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: users
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 8
     filtered: 12.50
        Extra: Using where
1 row in set, 1 warning (0.01 sec)

取得結果は正しいですが、EXPLAINのtypeをみるとALLとなっており、全表走査していることが分かります。

次に、後方一致検索用の文字列を反転し前方一致検索用の文字列にして、reverse_emailに対して検索します。

SELECT * FROM users WHERE reverse_email LIKE REVERSE('%.com');
+----+----------------+----------------+
| id | email          | reverse_email  |
+----+----------------+----------------+1 | test1@test.com | moc.tset@1tset |
|  3 | test3@test.com | moc.tset@3tset |
|  5 | test5@test.com | moc.tset@5tset |
|  7 | test7@test.com | moc.tset@7tset |
+----+----------------+----------------+
4 rows in set (0.13 sec)

EXPLAIN SELECT * FROM users WHERE reverse_email LIKE REVERSE('%.com')\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: users
   partitions: NULL
         type: range
possible_keys: reverse_emai_index
          key: reverse_emai_index
      key_len: 1023
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using where
1 row in set, 1 warning (0.07 sec)

emailに対する後方一致検索と同じ結果が得られました。また、EXPLAINのtypeがrange、keyがreverse_email_indexとなっていて、インデックスを使っていることがわかります。

このように、Generated Columnを使えば後方一致検索を高効率で行うことができます。

(例ではデータ数が小さいので、emailに対する後方一致検索の方が早いですが、データ数が大きくなるとreverse_emailに対する前方一致検索の方が早くなります)

注意点

この方法で事実上の後方一致検索を高効率でできますが、別のコストが増加する場合もあるので注意が必要です。

  • インデックスの追加による更新系クエリの性能の悪化
  • アプリケーションで後方一致検索の要求をうまく処理する必要がある

実装にあたってはこれらのコストを考慮して、本当に後方一致検索が必要かどうかを判断する必要があります。

ちなみに部分一致検索はこの方法ではできませんし、私の知る限りB+tree (または同系統のtree) で高効率で部分一致検索をする方法はありません。



この記事を読んで、少しでもテックドクターに興味を持っていただけたら嬉しく思います。
今後も技術系の記事を書いていく予定なので、よろしくお願いします。


書いた人:魚木

データサイエンス部のお仕事~データのもつ可能性を最大限に引き出す

初めまして、テックドクターでデータサイエンス部を統括している深見です。

本エントリでは、私たちデータサイエンス部のメンバーが日々どんなデータをどのように分析しているのかをご紹介します。

24時間365日の活動を測るウェアラブルデータ

テックドクターは、ウェアラブルバイスのデータを分析することで、医療現場で活用されるデジタルバイオマーカーの開発をめざしています。

※デジタルバイオマーカー……デジタルデバイスで測定した『日常データ』をもとにした、病気の早期発見や治療につながる客観的指標(前回記事参照

これまで医療現場で扱われるデータは、検査や健康診断など、病院に来院したときにだけ計測される一時点のデータでした。これに対しウェアラブルデータは、その人の24時間365日の活動を継続的に測定したデータです。この性質のちがいにより、ウェアラブルデータは「これまでわかっていなかった、病気と人の行動の関係」を明らかにする可能性を秘めているのです。

データサイエンス部ではそうしたウェアラブルデータのもつ可能性を最大限に引き出すため、その加工方法や分析手法を日々模索しています。

私が使用しているウェアラブルバイス。普段からデータ取得ができるように、左右の腕に別のデバイスを2個付けしています(ブログ用の演出ではありません!)

扱っているデータ・分析事例

私たちが計測に使っているウェアラブル端末には、みなさんが使用している一般的なスマートウォッチも含まれます。それらは睡眠・脈拍・活動量に関するデータを収集することができます。

たとえば、睡眠の質は自分ではわかりにくいものですが、収集した睡眠データを使えば計測できます。同じように、日頃の睡眠量や生活の規則正しさを定量的に知ることもできます。

いっぽう活動量のデータでは歩数を分単位で計測できるため、運動不足や運動習慣の有無などが可視化されます。

こういった行動のデータにくわえて、脈拍データからは心臓の拍動を通して、自律神経の動きを評価できると言われています。

さらにそれらを組み合わせることによって、睡眠中/運動中の脈拍数など、生活シーンを念頭においた分析ができるようになります。
下の図は、睡眠・脈拍・活動量(歩数)のデータを可視化した例です。

 

脈拍数のグラフに睡眠や活動量の状況を重ねたもの

こうして単純にグラフにしただけでも、なかなか興味深いものです。
しかし私たちは、データサイエンスの力を使って、このデータからさらに多くのことを知ろうとしています。わかりやすい例を2点ほどご紹介しましょう。

分析事例1.長期変動が見えてくる

テックドクターには創業間もないころからウェアラブル端末を着けつづけているメンバー、Mさん(男性)がおり、数年単位のデータが揃っているため絶好の解析対象となっています。

彼の数年にわたる脈拍データを解析した結果が下の図です。

測定した脈拍データをもとに、心拍変動という数値を算出し、STL分解したもの

STL分解……時系列データを季節成分、トレンド成分、残差成分に分ける分析手法

トレンドのグラフに注目してほしいのですが、時間の経過とともに数値が減少しています。これはMさんの身体が年齢を重ねていることを表しています。

この心拍変動は年齢とともに減少することが論文で報告されていますが、それは多数の被験者から統計的に導かれたものです。一人の人間の数値をこれだけ細かく、長期に可視化した例は珍しいのではないでしょうか。 

分析事例2.飲酒と脈拍

お酒を飲むと脈拍が上昇すると感じる方は多いと思います。ウェアラブルからの脈拍データを見ても、そのようすは明らかです。

この現象は飲酒習慣を定量化するには非常に役立つのですが、ときに困ることもあります。たとえば病気と脈拍との関連を解析したいとき、脈拍上昇の要因が病気のせいなのか飲酒のせいなのか判別がつかず、ノイズになるのです。

テックドクター社では、収集した飲酒日のデータと脈拍データを分析することで、飲酒判定モデルを構築しました。このモデルを使用すると、脈拍データのみをもとに飲酒日を推定することができます。

ピンクのハイライトが脈拍データから推定した飲酒日の予測。丸が実際の飲酒量

飲酒日が見事に推定されている様子がわかるでしょうか。このモデルは、先述した分析ノイズを除去するために役立てることができます。

どんな人がいるの? メンバーのバックグラウンドはさまざま

データサイエンス部には現在5人のメンバーがいます。

一貫して分析を経験してきたメンバーはむしろ少数派で、さまざまなバックグラウンドを持ちながら、その経験値をそれぞれの分析に活かしています。実はウェアラブルデータ自体がまだまだ新しいデータのため、分析経験がある人は世の中にほとんどいません。そのため全員テックドクターに入社後に初めて実分析を経験しています。

ふだんの業務では、毎日の朝会で業務の進捗確認や困りごとの相談を行っています。日々メンバー間でフレッシュな情報を共有しつつ、他のメンバーの分析方法を参考にしたりアドバイスをもらったりして、相互に助け合って業務をこなしています。

また定例MTGや個別案件の相談などは、一元化したりなるべくマネージャが受けることにして、メンバーにはなるべく多くの時間を分析にあててもらっています。

例として、私ともう一人のメンバーについて、そのバックグラウンドと一日の過ごし方をご紹介します。

👤マネージャ(私、入社3年以上、データ分析歴10年以上)

バックグラウンド

  • 計算理工学専攻卒
  • アンケートデータ分析
  • 事業開発(M&A)
  • Q&Aデータのテキスト分析
  • 位置情報分析
  • ウェブ広告データ分析
  • imp/click予測モデル

ある一日のスケジュール

時刻 内容
6:30 出社
  タスク整理・雑務
9:00 朝会
  分析定例
12:00 昼食
13:00 データ整理
  案件相談
  1on1
  定例会議
  全社会議
18:30 退社

私の仕事デスクです。分割キーボードに興味のある方大歓迎!笑

👤フレッシュメンバー(入社8ヶ月目)

バックグラウンド

ある一日のスケジュール

時刻 内容
8:00 出社
  雑務
9:00 朝会
  分析
12:00 昼食
13:00 分析
17:00 家事(業務外)
20:00 分析
21:00 終業

 

データの可能性を信じ、価値を引き出せる人と仕事がしたい

テックドクター社のデータサイエンス部では、技術面のスキルだけでなく、ウェアラブルデータに興味を持ち、人のために何かをしたいという思いを持った方々とともに働きたいと考えています。
具体的には、こんな人です。

データを見る/集める/分析することが好き

データには無限の可能性があり、どういった人がどういった関心を持って分析するかで結果が大きく変わってくるものです。分析要件や指示をこなすだけではなく、データの可能性を信じて、その価値を引き出せるようなマインドを重視しています。

ウェアラブルデータはまだまだ顧客でも理解が追いついていない面があります。データを見ている最中にふと思いついた分析内容が大いに評価される、といったことも多々あるのです。

ヘルステックへの興味・関心がある

ヘルステックの領域では、病気一つ一つに膨大なドメイン知識が存在しています。それらの中には解明が進んでいる病気もあれば、精神疾患のようにまだまだ未知の部分が残っている病気もあります。そういったドメイン知識を貪欲に吸収することができると、データ分析の精度・信頼度が向上していきます。

地道な努力ができる/一つ一つのデータを丁寧に根気強く確認することができる

データ分析をしていると、機械学習モデルの構築のような花形な業務よりも、手前のデータクレンジングに時間をとられることが多いです。弊社も例外ではなく、とりわけウェアラブルデータという24時間365日、いついかなる状況かに関わらず採取されたデータから結果を導くには、ことさら地味な作業が重要です。

これまでの分析をふりかえっても、きちんと医師や専門家の意見を聞き、細かなノイズを除去する作業を1年近くに渡って繰り返した結果、高精度な分類モデルの構築に成功する、といった例も多いです。

データを代表値として扱わず、個々の数値の意味を追求できる

分析作業を言われたことをこなす業務としてとらえてしまうと、検定で統計的な差異が出た/出なかった、機械学習モデルの精度が良かった/悪かった、で終わってしまいがちです。

しかしながら、うまくいかなかった時はもちろん、うまくいった時でも、なぜうまくいったのか、偶然ではなく汎用的な結果となっているのかを追求する姿勢はとても大事です。部内では、数百人規模のデータが対象であっても、分析を重ねていくうちに気がついたら一人一人のデータを細かく確認していたという例がよくありますし、そういった姿勢が重要だと考えています。

 

以上です。
このエントリを読んで、少しでもテックドクターに興味を持っていただけたら、そして困っている人のためにデータ分析をしたいと思っていただけたら、嬉しいです。

次回のデータサイエンス部の記事では、具体的な分析手法や、事例の紹介をしていきたいと思っています!

 

書いた人:深見

TechDoctor開発者Blogをはじめます

初めまして、テックドクターでCTOをしている、佐藤大樹と申します。このたび、弊社の技術ブログ「TechDoctor開発者Blog」を立ち上げることにしました。

みなさんはテックドクターが何をしている会社かご存じでしょうか。私たちテックドクターは、デジタルバイオマーカーの開発をしています。

デジタルバイオマーカーってなに?

たとえば健康診断の血液検査で測定するHbA1cヘモグロビンA1c)という数値。この値が高いと糖尿病の疑いがあるとされます。このような指標は(「デジタル」のつかない)バイオマーカーと呼ばれています。

私たちが開発しているデジタルバイオマーカーは、デジタルデータを活用して作成したバイオマーカーです。健康診断のような特別な機会にしか測定できないデータではなく、日常的に使用しているウェアラブルバイスやIoTデバイスなどに蓄積された『日常データ』を活用します。それらを統計処理して状態定量化することで、病気の早期発見や治療につながる客観的指標を得ることができます。そうして得られた指標のことをデジタルバイオマーカーと呼びますが、これをデータドリブンで開発していくのが私たちテックドクターです。

デジタルバイオマーカーのイメージ

デジタルバイオマーカーを活用すると、たとえばこんなことができます。

  • 患者さんに測定デバイスを付けてもらうことで、病勢を把握し、医師とのコミュニケーションを円滑にする
  • 希少疾患の発作の発病をデジタルバイオマーカーを使って予測することで、患者さんの薬のタイミングを調整する
  • まだ病気に至っていない段階でも、このままの生活を続けると病気になりそうだという予兆をつかむ

上記はほんの一例で、他にもさまざまな方法でみなさんの健康に貢献できます。

エンジニアが何をしているのか発信したい

こうしてデジタルバイオマーカーの開発を続けてきて、ありがたいことに狭い業界ではだんだん知名度も上がり、口コミ等を通して指名でお仕事をいただける機会も増えてきました。

非常にやりがいや社会的意義がある仕事だと日々感じていますが、いっぽうでCTOの立場からは現状にひとつ問題意識も感じています。

私たちの会社にいるエンジニアがどんなことをやっているのか、外から見えないのです。

仕事の合間に談笑する弊社エンジニアたち(左から2番目が筆者)

私は昔、Unohという会社で「ウノウラボ」というブログの立ち上げを経験しました。当時まだ珍しかった企業のエンジニアブログの走り的なブログです。

ブログの運営には大きなメリットがありました。ウノウラボが技術情報の発信源として評価された結果、Unohの採用には優秀なエンジニアがたくさん応募してきてくれたのです。

テックドクターでもウノウラボの成功経験を生かし積極的な情報発信を行うことを目的として、このブログを開設しました。

ただ反面で、エンジニアとしての本業があるかたわらでのブログ執筆は、予想以上に負担がかかる業務でした。今回は運営メンバーに編集者を迎えることで、エンジニアの負担を軽減しながらブログを書くことができないかという試みも行っていきます。

在籍当時のUnohの社内サーバー

テックドクターのエンジニア組織

せっかくなので、私たちテックドクターのエンジニア組織についても紹介させてください。

テックドクターでは現在、プロダクトチームとデータサイエンティストチームの2つのチームに分かれ、互いに連携しながら革新的なソリューションを開発しています。

プロダクトチームでは、ユーザーのニーズを把握し、それにもとづいた製品やサービスの設計・開発をおこないます。ミッションは使いやすく高機能なアプリケーションを提供すること。プロダクトマネージャー、フロントエンドエンジニア、バックエンドエンジニアが一体となって、定期的なユーザーテストやフィードバックセッションを通じて、プロダクトの改善を続けています。

一方、データサイエンティストチームは、膨大なビッグデータを分析し、その結果をもとに統計分析や機械学習を駆使して、デジタルバイオマーカーの開発と最適化を行っています。こうして開発したデジタルバイオマーカーは、先ほどご紹介したとおり患者さんの病勢判断や治療効果の評価に活用されています。それ以外にもデータサイエンティストの業務は論文を読んだり、追実験を行ったりと、大学の研究室にかなり近いと感じています。

オフィスの様子

「もうやっちゃいました」精神で獣道を作りたい

テックドクターでは医療や健康、データサイエンス、機械学習といった事業ドメインに興味がある方と一緒に働きたいと思っています。

また社風の点では下記のようなエンジニアの行動指針を定めています。 

なかでも、特に気に入っているものをいくつか紹介します。

 

獣道を作ろう

まだだれもやったことない方法を使って、だれもやったことのない事をやって、医療という課題に向き合っていこう。

 

喧々諤々しよう

誰でも対等に忌憚なく意見が言えるようにしよう、そういう環境を作ろう。

 

もうやっちゃいました

すぐにできることなら、断りなくどんどん改善していこう。たとえばCI/CDの改善などはそれぞれの積極的に担当者が自身の判断で行っており、最近は入社2年目の担当者の発案でCodeRabbitをテスト導入したりしています。

 

フルマラソンチャンピオン

事業自体、比較的にロングスパンで動かしていくことになるので、長い距離でも走りきってチャンピオンになれるように、積極的にリファクタリングを行ったりテストを書いていこう。例えばデジタルバイオマーカー開発プラットフォーム「SelfBase」のプロジェクトでは、新機能の実装をしながら、平行してDDDへのリアーキテクチャなども進めています。

 

このエントリを読んで、少しでもテックドクターに興味をもっていただけたら嬉しく思います。 

次回からは実際の事業内容に沿った技術的な記事や、コードやアーキテクチャの話、データサイエンスや分析手法の話などを記事にできたらと思っております。お楽しみに!

 

テックドクターをよろしくおねがいします



 

書いた人:佐藤