開発するときに心がけていること 実装編

この記事は株式会社SUPER STUDIOのAdvent Calendar 2023の21日目の記事です。

初めに

先日チームメンバーと設計の話をした時に「どういう時にクラスを分けるのか?」という話をチームでした。自分なりの判断基準はあるのだが、それを明文化したことがなかったのでしてみようと思う。 今時点での思考なので、この先変わる事もあり得るので「こういう考えもあるな」くらいの気持ちでとらえていただけると嬉しい。

分割

ケース1: このクラス、やる事多くない?

SOLID原則の「単一責務の原則」を基準に考えて、一つのクラスでやることが多いと感じたらクラスを分ける。 例えば商品の値段を計算する処理と商品の割引の値段を計算する処理、送料を計算する処理が一つのクラスの中でやっていたとすると、それらは別々のクラスにする。 割引の処理に新しいロジックが追加された時、予めその処理を別のクラスに抽出していたら変更する範囲はそのクラスの中だけになるし、そのクラスのテストをちゃんと書いておけば良くなる。 以前ある人から聞いたのだが、「このクラスはXXをYYするクラスです」と一言で説明できる単位が良いのだろうと思う。

ケース2: このメソッド、色々やりすぎじゃない?

例えばメソッドの名前が「get~~」ってなっていて確かにその値をreturnしているけど、中身で他の処理(データの更新など)をしていたりするとメソッドを分割することを検討すると思う。 副作用なども考えると思う。

通化

ケース1: これって処理同じだから共通化しようか?

ただ同じだからという理由で処理を抽出するということはしないと思う。 ドメインロジックは共通処理として切り出すが、それ以外については結構慎重になる。 共通化しても後々の開発でその共通処理に分岐が 入って逆に処理が複雑化することもある。 重複するところがあるが作りがシンプルになるのであれば、敢えて重複は許容します。

ケース2: そうだ継承しよう

継承を使って処理を共通化しようというケースもある。それがハマる時は良いが、後々処理が追加された時に継承していることによって逆に複雑度が上がる場合がある。 例えばRailsSTIと言う仕組みがある。Imageモデルがあり、それを継承してCustomerImageとAdminImageがあるとする。 CustomerImageとAdminImageもどちらもImageテーブルにデータが入りtypeと言うカラムで二つのデータの区別をする。 当初は良かったが、途中からAdminImageだけに別のデータを持つことになりカラムhogeを追加したとする。 するとCustomerImageの場合はhogenilにAdminImageは何かしら値が入る。そういう拡張が少しずつ増える・・・・。 そうなると、そもそも共通化する意味があったのか。継承したことでCustomerImageとAdminImageを分けたくても出来ない。 そういうことが起こる場合がある。 継承するくらいなら処理を別クラスに移譲し各々のクラスでそれを使うという方が良いかもしれない。

ケース3: テストも共通化しよう

テストの可読性を上げようということで共通化するといこともある。RailsRspecで開発している場合にそのための仕組みもある。 で、私も色々と試行錯誤したが、結果テストでDRYを追求する必要はないと思っている。テストでは「この処理はどういうデータが必要で結果どうなるのか」それが1ファイル(1context)で完結するのが良いと思っている。 shared_contextを使うと確かにテストはスッキリするが、テストの内容を把握するのに共通部分として切り出したところを読み、そしてそれを踏まえてテストファイルの方を見る。 それよりもテストの内容は1ファイルで、1contextで完結していた方が必要なものを把握し内容が理解しやすいと感じた。 これは個人差があると思うが、他にもそういう風に感じている人はいそうだった。 ポイントは、数ヶ月後に自分が見ても内容が把握しやすいテストであれば良いと思っている。

まとめ

色々あるが、根底にあるのは「数ヶ月後に見ても理解しやすいか」ということ。 メンテし続けるコードなら、そのためにどうあるべきかを考えている。これからもそのことを念頭に開発していきたいと思う。 (他にも何か書こうと思ったので、思い出したら追記します)

チームリーダとしてやってきたことの振り返り

この記事は株式会社SUPER STUDIOのAdvent Calendar 2023の 12/8の記事です。

初めに

昨年8月に転職して新しい職場になった。新しい環境では今までと変わらずコードを書いているけどチームマネージメントに関しては、やる量が増えてきた。もともとそういうマネージメントということに関して抵抗はなかったので、新しいチャレンジと思って色々やってきたので、今回はその振り返りをしたいと思う。

やったこと

やったことを羅列してみる - 振り返り方法を幾つか試してみた。 開発プロセスアジャイル開発、スクラムを採用しているのでスプリントの終わりには振り返りがある。が、その振り返りがチームとしての振り返りになっていなかった気がした。少しマンネリ化しているようにも思えたので新しい振り返りをやってみて、そのマンネリ化を変えてみた。

  • モブプロ・ペアプロ

    • 若いエンジニアが多いので、モブ設計、モブプロ、を行った。そこで色々議論しながら、どういう風に作るべきか指針を話、チームで共通認識を持ちながら進めるようにした。
  • テストコードを後回しにしない

    • テストコード作成が開発チケットと別で切られることが多く、後回しになりがちだった。テストコード作成も開発タスクの一部なので、チケットの完了の定義に「RSpecが作成されていること」を入れるようにしたり、PRレビュー時にテストを作るように指摘し続けた。結果RSpecの作成が当たり前になった。
  • チームメンバーの相互理解のためにドラッカー風エクササイズをやった

    • 前職でやって良かったので、ここに来て同じようにやってみた。それぞれどういう価値観を持って仕事をしているのか知れるよい機会になった。
  • 1on1や朝の雑談などコミニケーションを取ることを意識した

    • リモート環境で仕事しているので、テキストのコミニケーションだけではなく、実際に声を交わして会話するようにした。1on1も定期的にやっているが、それだけではなく朝仕事を始める前に雑談する時間を設けていた。会話は大事です。

上記以外にも実装方針、方法の提案をしたり、勉強会で発表したりなどなど色々やった。

やったこと詳細

振り返り方法

試した振り返り方法は主に以下の3つ。

  • KPT
  • YWT
  • Fun Done Learn

それぞれ特徴があるので、どれが良いということはなく、チームの状況によるとは思う。チームが立ち上げ時期の時はYWT。チームが良い感じで動き始めたらKPTが良いとは思う。個人的にはFun Done Learnが好きだ(チームメンバーが今どういうところに楽しさを覚えて仕事しているかが分かるから)。今はKPTをやっている。途中チームメンバーから別の振り返り方法をやってみたいという提案があったので、それもやってみた。こういう提案も振り返りを通してより良いチームの進め方を考えてのことなので嬉しい提案だった。 KPTやYWT、Fun Done Learnについては、こちら にも書いてみました。

モブプロ・ペアプロ

チームで認識を合わせながら進めるという点でモブプロは結構有効だった。開発予定の機能について、自分はよく知っているが自分以外のチームメンバーは知っていることが少ないので、どう設計して開発すると良いか知っている知識を共有しながら一緒に設計してみた。そしてそれを何度か繰り返した結果、メンバーに知識が浸透していると感じた瞬間があった。チームとしての成長を感じた瞬間だった。 また別の利点として重要な機能の開発をモブプロすることで、後にソロで開発をしても認識のずれがなく進められたということがあった。今後もモブプロ・ペアプロは続けたい。 ちなみにリモート環境でモブプロ・ペアプロをどうやったかでいうと使ったツールは以下のもの。

  • VSCode Live Share
  • Slack Haddle (最近はこっちも使っている)
  • Metalife or oVice (会話や画像共有で使っている)

相互理解のためにドラッカー風エクササイズをやった

エクササイズの流れ
上で示したような進め方でやってみた。人数は5人だったが内容を記載した後の各自の発表だけで1時間は使ってしまった。色々新しい発見もあり盛り上がった。で、最後の全体を見ての振り返りは別日で30分とってやった。リモート環境でチームで仕事していると、なかなかメンバーの考え方、価値観の理解が難しいと思うが、こういうエクササイズをやって相互理解が進むならやって損はないと思う。今後も新しいメンバーが入った時にでもやってみたいと思う。

最後に

今また新しい役割を担当している。ということで引き続き色々とチャレンジしていきたいと思う。

久しぶりにRailsのPRを眺める

初めに

久しぶりにRailsのPRを眺めてみることにした。すると一つ目についたものがあったので、備忘録の意味も込めて書くことにした

今回眺めたPR

今回見たのは、このPR。2022/12/28に見たときはまだDraftだった。今後これがどうなるか分からないが、このPRの中でActiveRecordwithメソッドという見慣れないものを使っていたので、これは何だろうかと少し調べてみた

withメソッドいつ実装された??

このPRで追加されたみたい。2022/12/28時点でmainブランチに取り込まれているのは確認した。

github.com

ただまだRailsの7.0.4には取り込まれていなかったので、mainブランチを使って検証してみました。

検証

やることは単純で例としてUserモデルを作ってみて、それに対しwithメソッドを使ってみる。どんなSQLが発行されるのか、見てみたいと思う。 下の例はnameとageというカラムを持つUserテーブルを作成し、それに対するmodelを作る。で、ageが10歳の人を抽出する処理をwithメソッドの中に書く。そしてto_sqlで発行されるSQLを確認。

3.1.1 :001 > User.with(ten_age_user: User.where('age = ?', 10))
  User Load (0.2ms)  WITH "ten_age_user" AS (SELECT "users".* FROM "users" WHERE (age = 10)) SELECT "users".* FROM "users" /* loading for pp */ LIMIT ?  [["LIMIT", 11]]
 => []
3.1.1 :002 > User.with(ten_age_user: User.where('age = ?', 10)).to_sql
 => "WITH \"ten_age_user\" AS (SELECT \"users\".* FROM \"users\" WHERE (age = 10)) SELECT \"users\".* FROM \"users\""

SQLのwith句が使えるようになるみたい。 試しにデータを追加して、このwithメソッドを使ってデータを抽出してみる。

3.1.1 :012 > ten_uesr = User.with(ten_age_user: User.where('age = ?', 10)).joins('join ten_age_user on ten_age_user.id = users.id')
  User Load (2.4ms)  WITH "ten_age_user" AS (SELECT "users".* FROM "users" WHERE (age = 10)) SELECT "users".* FROM "users" join ten_age_user on ten_age_user.id = users.id /* loading for pp */ LIMIT ?  [["LIMIT", 11]]
 => [#<User:0x0000000115266270 id: 3, name: "hoge3", age: 10, created_at: Wed, 28 Dec 2022 04:29:01.991160000 UTC +00:00, updated_at: Wed, 28 Dec 2022 04:29:01.991160000 UTC +00:00>]

これがあるとActiveRecordを使った抽出処理がSQLを書くイメージにより近づいたのかなと思う。 早くリリースされると良いなー。

最後に

久しぶりにrailsのPR見たけど、勉強になる。これは暫く続けるべきだと思った。

二つの会社でリモートワークをしてみて感じたこと

初めに

今年も残すところ残りわずかですね。この記事は株式会社SUPER STUDIOのAdvent Calendar 2022の 12/16の記事です。 公開が遅れてしまい、すみません。

さて今年は私個人にとって仕事面で大きな変化があった年でした。転職をしたからです。前職のClassi株式会社で約7年勤めてきましたので、新しい環境になり今までとは違うこと、文化に触れることが出来てます。毎日刺激的です。ただ働き方は変わらずリモートワークです。ですから周りから見るとそれほど以前と変わっていないように見えると思います。では今年転職前と後でどう働き方が変わったのか簡単に振り返ってみたいと思います。

変わらないところ

上でも述べましたが、基本前の職場も今の職場もリモートワークです。そこは変わらずです。最近会社に行くことがあったのですが、久しぶりの通勤で結構大変なことを毎日していたんだなと感じました。一方で仕事仲間と直に会って会話したり、ランチを一緒に行ったりする体験は楽しいもので、これはこれで良いなと思いました。 話を戻します。リモートワークで朝の開始時間も前職と同じ9時半から10時です。で、時間になると一応気持ちを切り替えるために「行ってきます」と家族に宣言してから仕事部屋に向かいます。

違うところ

コミニケーションについて

前職ではSlackやmeetでコミニケーションをとることが多かったのですが、今の職場ではバーチャルオフィスがあり基本そこでコミニケーションをとるという点が違います。前職は仕事開始したらSlackでハドルなど使って直接会話したりmeetで直接顔を見せて会話する方式でしたが、今はバーチャルオフィスに出勤していてバーチャルオフィス内の特定の場所でチームメンバーと集まって会話したり、質問があればその人の近くに行って話し掛けたりします。 この体験は初だったので、新鮮です。離れていても仕事仲間をより身近に感じることが出来ています。

次に違うところは雑談が割と多いという点です。リモートワークをする前は雑談が大事ということを特に感じなかったのですが、リモートワークになってから仕事仲間と会話することが急に減りました。朝の挨拶、ランチに一緒に行く、など以前まで普通にあったことがリモートワークに切り替わってからなくなったからです。雑談が減ると話す時間が減るのでお互いを知るという機会がなくなります。そうなると仕事をする上でもコミニケーション上の問題が起きやすくなります。この人はどういう価値観で仕事しているのかなども分からなくなります。テキストだけのコミニケーションになると変な誤解を生まれストレスがたまります。そうなるとチームの生産性という点で支障が出てきます。 前職でも会話を増やそうということから「雑談タイム」というのを設けて決まった時間(週に15分ほど)にチームメンバーと話すようにしていました。今の職場では朝会が終わった後に時間が余ったら雑談タイムを入れて会話しています。多い週はほぼ毎日話しています。テーマは自由で例えば「昨日の晩御飯は何」や「武勇伝は何」など様々です。各テーマに対しチームメンバーが自分のことを話すので割とすぐチームメンバーの人柄を知りチームに馴染むことが出来ました。

開発

前職では開発は基本モブプロやペアプロでやっていました。その時はSlackで画面の共有をしつつVSCodeのLive Share機能を使ってペアプロをしていました。これはなかなか良い開発体験でコードレビューのコストが抑えられたと思っています。 一方今の会社ではまだモブプロやペアプロは行っていません。個人的にはやっていきたいので、これからという思いはあります。今は個人でタスクをこなすことが多く不明な事があった場合は先ほど紹介したバーチャルオフィスで画面を映しながら相談したりしています。

スクラムイベント

前職も今も開発プロセススクラムを採用しています。スクラムイベントの進め方は今も昔も大きな違いはなく出来ています。オンラインとで比べた時に出来ないなと思っていることはスプリントレビューの時の成果発表で成果が出た時にメンバーと一緒にお菓子を食べるというセレモニーがないという点でしょうか。

まとめ

振り替えてみて改めて思ったのはコミニケーションのところが大きく変わったなという点です。そしてリモートワークはこのコミニケーションがよく課題として挙げられるのですが、その課題を感じる場面が今の職場では少ないというところにリモートワーク環境でも割と短期間で会社に馴染められた要因だと考えています。

Vueを使ってみたパート2

はじめに

前回はプロジェクトを作って簡単に動作確認したところまでです。 今回はその続きでルーティングを追加します。

やること

vue-router(https://github.com/vuejs/router)を使うので、それをinstallします。

コンポーネントの作成

ページ遷移した時の元のページと遷移先のページ、それぞれに該当するコンポーネントを作ります。 今回作るのはタスク一覧ページ用のコンポーネントとタスク作成用のコンポーネントです。 src/components直下にそれぞれ作ります。

<template>
  <div>
    <h2>タスク一覧ページ</h2>
    <router-link to="/createTask">タスク作成ページ</router-link>
  </div>
  <div>
    <router-link to="/">トップへ</router-link>
  </div>
</template>a
<template>
  <div>
    <h2>新規タスク追加</h2>
    <router-link to="/taskList">タスク一覧へ</router-link>
  </div>
</template>

続いてrouter/index.jsを作成しルーティングの定義を記載する

こちら勉強で参考にしたサイトでrouter/index.jsを作成してルーティングを記載していたので、それに合わせています。もしかしたら、この先勉強を進めていったら変わるかもしれませんが、一旦は同じようにやってみました。

import { createRouter, createWebHistory } from 'vue-router'
import TaskList from '@/components/TaskList.vue'
import CreateTask from '@/components/CreateTask.vue'

const routes = [
  {
    path: '/',
    name: 'top',
    component: TaskList
  },
  {
    path: '/taskList',
    name: 'taskList',
    component: TaskList
  },
  {
    path: '/createTask',
    name: 'createTask',
    component: CreateTask
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

main.jsにrouter/index.jsを追加

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

createApp(App).use(router).mount('#app')

App.vueに<router-view />を追加

最後に<router-view />をApp.vueに<router-view />を追加します。するとページングが表示されます。

vueにルーティングの追加が出来ましたので、次のステップに進みたいと思います。 まだまだ先は長いです

vue cliを使ってフロントエンドの実装を進めてみる

この記事は何?

以前Railsで作ったサンプルアプリのフロントエンド側の実装としてvueを使ってみようと思い、今更ではありますがvueの勉強をしてます。 幾つかサンプルを作成したので、今度はvue cliを使ってプロジェクトを作成し、より本格的な実装にしていこうと思い進めていました。 ただ序盤から躓いたので、忘れないために簡単にメモを残そうと思いました。

環境

いつもの通り環境構築から。 ただ最初にいきなり躓いた。

 npm install -g @vue/cli

上記コマンドでvue cliをインストールしようと思ったが、

changed 903 packages, and audited 904 packages in 34s

91 packages are looking for funding
  run `npm fund` for details

9 vulnerabilities (3 moderate, 6 high)

To address all issues possible (including breaking changes), run:
  npm audit fix --force

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.

となり、ちゃんとinstall出来なかった。で、情けないことではあるが、試行錯誤してしまい、結局npm updateをした後に再度installコマンドを実行したら、無事installされた。よかった。

プロジェクト作ってみる

今回サンプルプロジェクトの名前をpinejuiceにしているので、それと同じ名前でプロジェクトを作成した。

vue create pinejuice

?  Your connection to the default yarn registry seems to be slow.
   Use https://registry.npmmirror.com for faster installation? Yes


Vue CLI v5.0.7
┌─────────────────────────────────────────┐
│                                         │
│   New version available 5.0.7 → 5.0.8   │
│    Run npm i -g @vue/cli to update!     │
│                                         │
└─────────────────────────────────────────┘

? Please pick a preset: Default ([Vue 3] babel, eslint)
? Pick the package manager to use when installing dependencies: Yarn


Vue CLI v5.0.7
✨  Creating project in /Users/shinichirounakamura/project/pinejuice_vue_frontend/pinejuice.
⚙️  Installing CLI plugins. This might take a while...

yarn install v1.22.19
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...

success Saved lockfile.
✨  Done in 132.84s.
🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.22.19
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
✨  Done in 39.33s.
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project pinejuice.
👉  Get started with the following commands:

 $ cd pinejuice
 $ yarn serve

プロジェクトが作成し終わったら、直下にプロジェクト名と同じ名前のフォルダがあるので、そこに移動し、サーバを実行してみます。今回の場合はyarnを使っているのでyarn serve です。

routingを追加する

ます最初にVue Routerを導入します。

npm install vue-router@4

vueの3系以降はrouterは4を使うようです。(github: https://github.com/vuejs/router)

これで準備は出来ました。そうしたら、やることは以下の点です。 - router用のindex.jsファイルを作成し、そこにroutingの定義を書く。書く内容は、どのパスの時にどのコンポーネントを使うかです。 - 各パスで使うvueコンポーネントの定義

ざっくりいうと上記2点です。

一旦今日はここまでにします。 続きは次回。

Elasticsearchで辞書登録が出来ない場合の検索精度向上について

2019 elasticsearchのAdvent calendarの7日目の記事です。qittaに書いていたものを、自分のブログに引越ししました。

今仕事で携わっている教育プラットフォームでは検索にAmazon Elasticsearch Serviceを使ってます。検索対象はテスト、問題、アップロードされたファイル等になります。 ただ使っているバージョンが2.3と古いものでした。こちらのアップデート対応時に出た検索精度の問題について今回書こうと思います。

前提

使っている環境はAmazon Elasticsearch Serviceです。EC2上に構築することもあると思いますが、運用の負荷など考えてAmazon Elasticsearch Serviceで構築・運用することにしました。 またAmazon Elasticsearch Serviceへ検索する処理を行なっているのはRailsで実装されたAPIサーバになります。このRailsが使っているElasticsearch用のgemとRails自体のバージョンの関係で今回は6.8系まであげるようにしました。(Railsのバージョンが6系になたら、Elasticsearch用のgemもバージョンアップしてElasticsearchの7系も使えるようになりますが、それはまだ少し先になりそうです)

課題

今回のバージョンアップするに当たって、そもそも解決したい問題がありました。それが上でも述べた検索精度についてです。 これは以前からあった問題で、あるキーワードで検索すると検索結果にヒットしないが、そのキーワードに含まれる一部のワードだとヒットするというものでした。これは原因は明らかでkuromojiプラグインの辞書にその検索ワードが含まれていないためです。

$ curl -XGET "localhost:9200/_analyze?pretty" -H 'Content-Type: application/json' -d'
{ "analyzer": "kuromoji", "text": "ホゲホゲ星人" }'

{
  "tokens" : [
    {
      "token" : "ホゲホゲ",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "星",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "人",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "word",
      "position" : 2
    }
  ]
}

じゃ、辞書に追加すれば良いということになりますが、使っているのがAmazon Elasticsearch Serviceであるためそうはいきません。辞書の追加が出来ないためQueryで何とか解決しないといけません。 友人に相談したところngramを使うのはどうかという助言をいただき、検討した結果その方法を採用することにしました。

やったこと

ngram使う

対策としてやったことは、まずtokenizerにkuromojiだけでなくngramも併用して使うようにしました。text search対象のfieldでtitleというのがあるするとtitle2みたいなのを追加で定義してそこのanalyzerはngramにします。

indexes :title,  type: 'text', analyzer: 'kuromoji_analyzer'
indexes :title2, type: 'text', analyzer: 'ngram_analyzer'

そして次に検索時にはこの二つのfieldを見るようにして対応しました。

{"min_score":0.1,
  "query":{"function_score":
      {
        "score_mode":"sum",
        "boost_mode":"multiply",
        "query":{
          "bool":{
            "must":[
                {"term":{"user_id":139}},
                {"simple_query_string":
                    {"query":"/'#{keyword}/'",
                     "fields":
                    ["title^10","title2^3"],
                     "default_operator":"or"
                    }
                }
             ]
           }
        },
        "functions":[]
      }
    },
  }

ここで1点補足です。query中で ["title10","title23"],と書いてますが、この「10」は何を表すかというと検索結果の重み付けを表します。上の例だとtitleフィールドの検索の方に10、title2フィールドの方に3の重み付けをしています。 どうしてこのような重み付けしているかというと検索結果の最適化のためです。ngramの検索は検索対象のデータを機械的にN語で分割し転置インデックスを作成します。そのため最近の言葉、特殊な言葉など一般的な言葉でなくても機械的に分割して転置インデックス作るので検索することは出来ます。ただ本来意図しないデータも検索で引っかかることがあります。 対してkuromojiを採用している形態素解析は辞書を使って文字列分割します。そのため辞書に単語がないとダメなのですが、精度は良いです。 以上の点からただ併用して使えば良いという訳ではなく、チューニング作業が必要になります。

実験

ここで少し実験してみようと思います。

検索Query

.{"min_score":0.1,
  "query":{"function_score":
      {
        "score_mode":"sum",
        "boost_mode":"multiply",
        "query":{
          "bool":{
            "must":[
                {"term":{"user_id":139}},
                {"simple_query_string":
                    {"query":"/'#{keyword}/'",
                     "fields":
                    ["title^10","title2^3",
                     "photos.description^8","photos.description2^3"],
                     "default_operator":"or"
                    }
                }
             ]
           }
        },
        "functions":[]
      }
    },
  }

テストデータ

rubyで検証用のテストを実装したので、そのコードの抜粋になります。

      let(:album1) { create(:album, title: '映画仮面ライダーとホゲホゲ星人の戦い', user: user) }
      let(:photo1_1) { create(:photo, description: 'ホゲホゲ星人との場面1',
                              album: album1, user: user) }
      let(:photo1_2) { create(:photo, description: 'ホゲホゲ星人との場面2',
                               album: album1, user: user) }
      let(:album2) { create(:album, title: 'ターミネータVS仮面ライダー', user: user) }
      let(:photo2_1) { create(:photo, description: 'ターミネータとホゲホゲ星人の戦いの場面',
                              album: album2, user: user) }
      let(:photo2_2) { create(:photo, description: 'ホゲホゲ星人を助ける仮面ライダーの場面',
                              album: album2, user: user) }
      let(:album3) { create(:album, title: '鉄仮面のライダー', user: user) }
      let(:album4) { create(:album, title: 'ライダーが仮面を被ったら', user: user) }
      let(:album5) { create(:album, title: '仮面を被ったライダーの写真', user: user) }
      let(:photo5_1) { create(:photo, description: 'ライダーの写真',
                              album: album5, user: user) }
      let(:album6) { create(:album, title: 'ライダーの写真集', user: user) }
      let(:photo6_1) { create(:photo, description: '仮面をつけたライダーも写ってます',
                              album: album6, user: user) }
      let(:other_album) { create(:album, title: 'ホゲホゲ星人の冒険', user: other_user) }
      let(:other_photo) { create(:photo, description: '冒険の記録1',
                                  album: other_album, user: other_user) }

結果

右の数値は検索時に算出されるscoreになります。

keywordを「仮面」で検索した場合

鉄仮面のライダー: 11.709004
ライダーが仮面を被ったら: 11.371845
仮面を被ったライダーの写真: 10.375977
映画仮面ライダーとホゲホゲ星人の戦い: 8.224535
ターミネータVS仮面ライダー: 2.6144085

仮面というワードに近いものがscoreが高くなっていて、仮面ライダーは下の方になりますね。

一方keywordを「仮面ライダー」で検索した場合だと

ターミネータVS仮面ライダー: 40.157257
映画仮面ライダーとホゲホゲ星人の戦い: 17.230406
鉄仮面のライダー: 10.584366
ライダーが仮面を被ったら: 9.0324745
仮面を被ったライダーの写真: 8.719972
ライダーの写真集: 8.502096

ngramでもみているので、最後の二つも引っかかります。もしこの結果をよしとしないのであれば、min_scoreを設定して最低スコアを定義して調整するのも良いかと思っています。

keywordを「ライダー」で検索した場合

鉄仮面のライダー: 14.960871
ライダーの写真集: 14.960871
ライダーが仮面を被ったら: 13.746138
仮面を被ったライダーの写真: 12.806761
映画仮面ライダーとホゲホゲ星人の戦い: 10.416438
ターミネータVS仮面ライダー: 6.8164654

この場合も「仮面ライダー」は下の方に言って「ライダー」が上位に来てます。 ではもし重みつけをしないで検索した場合、どのようになるのでしょうか。実際試して見ると次のようになりました。

鉄仮面のライダー: 4.146576
ライダーの写真集: 4.146576
ライダーが仮面を被ったら: 3.7416654
仮面を被ったライダーの写真: 3.590652
映画仮面ライダーとホゲホゲ星人の戦い: 3.1220098
ターミネータVS仮面ライダー: 2.9388218

そこまで各検索結果でスコアに差は出ませんでした。重みつけをすることで、どの結果をより上位に出したいかscoreに反映させられます。 最後に辞書に含まれてない単語を検索した場合です。キーワードに「ホゲホゲ星人」にして検索した場合、以下のようになります。

映画仮面ライダーとホゲホゲ星人の戦い: 48.348312

kuromojiだけだと辞書にないから検索出来ないのですが、ngramを使うことで辞書にない言葉でも検索できるようになりました。これも踏まえ、どのようなデータを検索結果として優先するか重みつけやscore計算処理を使ってスコアを算出しチューニングすることでより検索したいデータを検索結果上位に表示していくことが出来ます。

今後

検索精度については他の方々も同じようなことをされていると思い、今回の対応方法は目新しい物ではなかったかもしれません。ただkuromojiとngramを併用して検索させるという方法があまり情報として公開されてなかったので今回書くことにしました。 何かしら役に立つことがあれば幸いです