備忘録的な感じです。Railsは学び始めて10日目。
目的
現在の理工展ホームページでは検索機能を実装しています。ここら辺について、先輩からは「コピペすればいい」といわれたのですが、いつかフレームワークを理工展に導入する可能性を踏まえて、自分で研究することにしました。
もともと先輩が「Mroongaはうまく導入できなかった」とか言っていたのを小耳にはさんでいたので、きっとMySQLの機能を使ったのだろうと思っていました。
実際その通りで、MATCH ... AGAINST ...を使っているとわかりました。MySQL 5.7で対応した、とてもすごい機能とのことで、いろいろと研究してみます。
内容について
Railsを使わずMySQLクエリを実行した場合の記事をもとにRailsに落とし込みました。
環境
- MySQL Server 8.0.18 (ローカル)
- Ruby on Rails 6.0.2 (ローカル)
- Ruby 2.6.5 (MSYS2 / MinGW)
- Windows 10
- Visual Studio Code
Railsアプリを作る
mysql_testアプリを作ります。もちろん名前は自由。
rails new mysql_test -d mysql
database.ymlを見たらわかる通り、開発版はmysql_test_developmentというデータベースが使われることがわかります。(rails db:createというコマンドを打っていないのでまだ実際にはMySQLには生成されていない)
データベースを作る
この記事では基本的にRails側で作業します。
rails db:create
を実行し、database.ymlに書いてある条件の通りのデータベースを作ります(このときはまだ中身は空)。
テーブル / モデルを作る
モデル名は自由、今回はmysql_match_testとおきました。
rails g model mysql_match_test
でモデルを作ります。このとき、MySQL側にはmysql_match_testsというテーブルが、Rails側にはモデルとマイグレーションファイルができます。
このマイグレーションファイルに、テーブルについての設定みたいなものを書いていきます。カラムを作るなどよく使う内容はActiveRecordsという、Railsからのみ使える書きやすい記法があるのでそれで書いていきます。
まずは、フルテキストインデックス(検索に必要なインデックス)を作る対象になるカラムを作ります。対象となるカラムが存在しないとインデックスが生成できません。
class CreateMysqlMatchTests < ActiveRecord::Migration[6.0]
def change
create_table :mysql_match_tests do |t|
t.text :name
t.text :short_desc
t.text :desc
t.timestamps
end
end
end
まずこのマイグレーションファイルをマイグレートします。
rails db:migrate
これで、フルテキストインデックスを作りたいカラムが作られました。このあと、諸々の設定を挟んで、のちの章でフルテキストインデックスを生成します。
(ここになんか間違って次の章の文章が入っていました、やばい編集ミスですね。)
データ挿入
seeds.rbにいろいろデータを挿入します。なお自分でデータを作ると逆に自然さが失われるかなと考え、データにはWikipediaの各記事の、冒頭にある概要文章を用いました。(Wikipediaの記事作成に携わった皆様にこの場を借りて感謝申し上げます。)
MysqlMatchTest.create(:name => '錦織 圭', :desc => '錦織 圭(にしこり けい、1989年12月29日?- )は、島根県松江市出身の男子プロテニス選手。')
MysqlMatchTest.create(:name => '大坂 なおみ', :desc => '大坂 なおみ(おおさか なおみ、英: Naomi Osaka、1997年10月16日 - )は、大阪府大阪市中央区出身の女子プロテニス選手[4][5]。')
MysqlMatchTest.create(:name => '羽生 結弦', :desc => '羽生 結弦(はにゅう ゆづる、英語: Yuzuru Hanyu、1994年(平成6年)12月7日[8][11] - )は、宮城県仙台市泉区出身のフィギュアスケート選手(男子シングル)、栄典:紫綬褒章 受賞者(2014年・2018年)、 国民栄誉賞 最年少受賞者(2018年)。')
MysqlMatchTest.create(:name => '髙橋 聡文', :desc => '髙橋 聡文(たかはし あきふみ、1983年5月29日 - )は、福井県大飯郡高浜町出身の元プロ野球選手(投手)。')
MysqlMatchTest.create(:name => '福留 孝介', :desc => '福留 孝介(ふくどめ こうすけ、1977年4月26日 - )は、阪神タイガースに所属する鹿児島県曽於郡大崎町出身のプロ野球選手(外野手)。右投左打。')
MysqlMatchTest.create(:name => '藤井 聡太', :desc => '藤井 聡太(ふじい そうた、2002年7月19日 - )は将棋棋士[1]。杉本昌隆八段門下[1]。棋士番号は307[1]。愛知県瀬戸市出身[1]。')
MysqlMatchTest.create(:name => '東京タワー', :desc => '東京タワー(とうきょうタワー、英: Tokyo Tower)は、東京都港区芝公園にある総合電波塔の愛称である。正式名称は日本電波塔(にっぽんでんぱとう)。')
MysqlMatchTest.create(:name => '東京スカイツリー', :desc => '東京スカイツリー(とうきょうスカイツリー、英: TOKYO SKYTREE)は、東京都墨田区押上一丁目にある電波塔(送信所)である。観光・商業施設やオフィスビルが併設されており、電波塔を含め周辺施設は「東京スカイツリータウン」と呼ばれている。2012年(平成24年)5月に電波塔・観光施設として開業した。')
MysqlMatchTest.create(:name => '渋谷スクランブルスクエア', :desc => '渋谷スクランブルスクエア(しぶやスクランブルスクエア、英称:Shibuya Scramble Square)は、東京都渋谷区渋谷2丁目にある渋谷駅に直結した複合施設型超高層ビル[4]。東棟はセルリアンタワーを抜いて渋谷で最も高いビルである。')
そのうえで
rails db:seed
を実行し、データを挿入します。
MySQLの検索設定を日本語向けに変える
今回使う全文検索の機能は、日本語を単語として認識するのではなく、ある一定字数ごとに切り分けて、検索の言葉と合致するものを探すという方法を用いています(N-gram)。
2文字で分かち書きするとすると、例えば、「テニス」は「テニ」と「ニス」、「渋谷スクランブルスクエア」は…ものすごいことになりますね。字数-1個に分かち書きされます。
既定では3か4だったはずですが、漢字の熟語が主流の日本語では、単語は2字程度で形成されるものが多いので、基準字数を2文字とします。
まずはMySQL Serverのインストールされたディレクトリを探し、その中のetcフォルダを開きます。この中にmy.cnfというファイルを作りましょう。
この中には、
[mysqld]
innodb_ft_min_token_size=2
ft_min_word_len=2
と書きます。これで2文字ずつで分けるようになります。ここでMySQLを再起動するのを忘れずに。再起動が終わってから次の作業に行きます。
フルテキストインデックスを作る
フルテキストインデックスを生成したいカラムは前の項で作りました。また、検索に関する設定もできたので、フルテキストインデックスを作ることができます。
まずはマイグレーションファイルを新たに作ります。今あるマイグレーションファイルに追加で書くのはNG。開発中のデータベースには反映されないし、(開発版と実用版でデータベースが異なるなどの面で)Railsではそれをするととてもやばいです。
なお、マイグレーションファイルの名前は自由。適当にfulltext_indexとしました。
rails g migrate fulltext_index
これでフルテキストインデックスを作るためのマイグレーションファイルを作ります。次はこのマイグレーションファイルの中に書き込みます。なお、こちらで指定した(変更可能な)名前は以下の通りです。
- desc_fulltext_idx: フルテキストインデックスの名前。自由。descのフルテキストインデックスなのでこういう名前にしました。
- mysql_match_tests: インデックス生成対象のカラムが存在するテーブル名(データベースはRails側がすでに選んでいるため、必要ありません。SQLクエリで言うと USE ***; を書いているという感じですね)。
- desc: インデックス生成対象のカラム。
class FulltextIndex < ActiveRecord::Migration[6.0]
def change
execute("create fulltext index `desc_fulltext_idx` on `mysql_match_tests` (`desc`) with parser ngram")
end
end
フルテキストインデックスを生成するというのはさすがにActiveRecordsでサポートされていません。
だからといって、Railsでは何もできずMySQLを直接コマンドプロンプトでいじらないといけない、とかではなく、RailsのマイグレーションファイルからもSQLクエリをそのまま実行できます。executeというコマンドです。
あとはこのマイグレーションファイルの内容を反映させましょう。
rails db:migrate
これで未反映のマイグレーションファイルのみマイグレーションが行われ、フルテキストインデックスが作られます。あとから追加したデータについても、追加した時点でインデックスをMySQLが勝手に追加してくれるようになります(あとからのデータ追加もこの後の項で検証します。)。とても便利。ここまで来たら、あと少しです。
検索のテスト(MySQLクエリ)
ターミナルで
mysql -u root -p
と実行し、パスワードを入力します。
パスワードが正しかったらログインできます。
まずは対象のデータベースを選択します。
use mysql_test_development;
次に、検索を試してみる。
select * from `mysql_match_tests` where match (`desc`) against ('テニス');
これを行うと、1番目の錦織選手と2番目の大坂選手が表示されます。
あとからのデータ挿入
(MySQLを開いたターミナルは、この後の検証のために残しておきます。)
Railsのseeds.rbの中身を一度消して(そのままもう一度実行すると同じ内容が2回挿入されてしまいます。seeds.rbはマイグレーションファイルとかとは異なり、重複とかの管理はないようで、rails db:seedを打つごとに後ろにどんどん挿入していってしまいます。)、以下の文を書きます。
MysqlMatchTest.create(:name => '松岡 修造', :desc => '松岡 修造(まつおか しゅうぞう、1967年11月6日 - )は、東京都出身の日本の元男子プロテニス選手、兼スポーツキャスター[1]、タレント、スポーツ解説者。現日本テニス協会理事強化本部副部長[2]。')
再度検索を実行してみましょう。
select * from `mysql_match_tests` where match (`desc`) against ('テニス');
すると、10番の松岡さんも表示され、錦織選手、大坂選手とともに3人のデータが表示されます。フルテキストインデックスは、データを追加するごとに更新されることがわかりました。とっても便利!
このあとやりたいこと
テーブルの内容をすべてビューで表示することには成功しているので、あとは検索して絞った内容を表示に回せば目的クリアです。おそらくfind_by_sqlでできるのですが、眠いのと、表示の方法も含めてまとめて表示したいのでまたあとで。続編出来たらここに書きます。というか、間違いなく続編書きます。
P.S. 翌日午前で続編書きました。こっちは意外と前知識使うだけで、簡単でした。モデルって本当に便利ですね。
MySQLの全文検索を使い、Railsで検索フォームと結果を表示する(ビュー系全般)
お疲れさまでした。