概要
- 検索にオートコンプリートをつけます
- 読み仮名(ひらがな)でもオートコンプリートできるようにします。
動作イメージ
環境
- rails 6.0.0beta3
- dbはsqliteなのでローカルで適当に試してください。
- gem
- fast_jsonapi オートコンプリート用の結果をjsonで返すときにつかう
- rubyfuri 読み仮名を取得
- dotenv-rails yahoo japanのapi keyを管理するため
- javascript libary
- selectize.js フロント側のオートコンプリート表示を制御
heroku
コード
ローカルで試す場合
gitから https://github.com/junara/fuchu-nosanbutsu/releases/tag/1.1.1 をクローンして下さい。
- bundle install
bundle install
- migarate
bin/rails db:migrate
- データを取り込む
bin/rails c
> Store.import_csv('db/30chokubaijo_map.csv')
- Storeデータからキーワードを登録
> importer = ItemImporter.new(Store.pluck(:description))
> importer.execute
- ふりがなを登録
-
.env
にyahoo japanのdeveloper apiを取得して書き込んでください。 - その後以下を事項
-
bin/rake item:name_hiragana
- サーバーを起動
bin/rail s
- webpacker-dev-serverを起動
bin/webpack-dev-server
下記にアクセス
http://localhost:3000
手順
- サーバーサイド(オートコンプリートエンドポイント作成)
- データ取り込み
- ふりがなデータ取得
- レスポンスのjson出力
- エンドポイント
- 複数キーワードで検索できるように修正
- フロントエンド
- javascript環境のセットアップ
- jquery, selectizeのインストール
- オートコンプリートの実装
- javascript環境のセットアップ
サーバーサイド(オートコンプリートエンドポイント作成)
データ取り込み
オートコンプリート対象となるテーブル Item
を作成します。
class CreateItems < ActiveRecord::Migration[6.0]
def change
create_table :items do |t|
t.string :name
t.timestamps
end
end
end
bin/rails db:migrate
インポート用の ItemImporterを実装します。これは、Store.descriptionをparseしてItem.nameに取り込むクラスです。
class ItemImporter
def initialize(descriptions)
@descriptions = descriptions
end
def execute
ActiveRecord::Base.transaction do
Item.delete_all
list = []
item_names.uniq.reject(&:blank?).each do |item_name|
list << Item.new(name: item_name)
end
Item.import list
end
end
private
def item_names
current_item_names = Item.pluck(:name)
@descriptions.each_with_object(current_item_names) do |str, res|
res.concat(custom_split(str))
end
end
def custom_split(str)
return [] if str.nil?
str.gsub(/ほか|など|あり/, '').split(/・|\(|(|)|、|:|\n| |\)| /)
end
end
これで取り込みを行います。
rails c
> importer = ItemImporter.new(Store.pluck(:description))
> importer.execute
これで、item.nameにstore.descriptionのデータからパースされたキーワードが保存されています。
Item.pluck(:name)
などで確認して下さい。
ふりがなデータ取得
これだけだと ナス
や 茄子
を なす
でオートコンプリートすることができません。そのために、カタカナをひらがなに、さらに茄子を なすに対応させます。
まずふりがな(ひらがな)を格納するカラムをマイグレーションします。
class AddNameFuriToItem < ActiveRecord::Migration[6.0]
def change
add_column :items, :name_hiragana, :string
end
end
bin/rails db:migrate
フリガナを追加するためのメソッドを追加します。
Gemfileに下記のgemを追加します。rubyfuriはyahoo japanのapiを叩いて、読み仮名を取得するgemですdotenv-railsはapiキーを管理するためです。
gem 'dotenv-rails'
gem 'rubyfuri'
で、yahoo japanのdeveloper apiキーを取得します。 https://developer.yahoo.co.jp/webapi/jlp/ このあたりから取得してください。(取得方法は、各自調べて下さい。)
取得したapiキーを .env
ファイルに記載します。(gitignoreに記載しているので大丈夫だと思いますが、こちらのAPIキーは決してgithubにアップロードしないで下さい!)
YAHOO_JAPAN_DEVELOPER_CLIENT_ID=あなたのAPIキー
良みこむためのメソッドをItemに付け加えます。
class Item < ApplicationRecord
def update_name_hiragana(force: false)
rubyfuri = Rubyfuri::Client.new(ENV['YAHOO_JAPAN_DEVELOPER_CLIENT_ID'])
update(name_hiragana: rubyfuri.furu(name))
end
end
先ほど実装したメソッドを利用してitem.name_hiraganaに読み仮名を追加するためのrake taskを作ります。
namespace :item do
desc 'item.nameに読み仮名をつける'
# rake item:name_hiragana
task name_hiragana: :environment do
Item.all.each {|item| item.update_name_hiragana}
end
end
上記rake taskを実行します。
> rake item:name_hiragana
Item.pluck(:name_hiragana)
等で読み仮名が格納されたことを確認して下さい。
レスポンスのjson出力
ユーザーが入力したキーワード(フロント側から送られてきたキーワード)に従って、オートコンプリート候補となるレスポンスを返します。レスポンスの形式はなんでもいいです。jbuilderで書いてもいいのですが、意外と面倒。
なので、簡単にいい感じのJSONを返してくれるgemをつかいます。
active_model_serializers が有名ですが、今回はactive_model_serializersと同様の使い勝手で、active_model_serializersよりも速い!とされる、 fast_jsonapi を使います。
まずgemをインストールします。
Gemfileに記述して
gem 'fast_jsonapi'
ふつーにbundle install。
bundle install
fast_jsonapi でJSONを返すためには、 include FastJsonapi::ObjectSerializer
したクラスを作り、出力したいカラムを attributes :id, :name, :name_hiragana
のように書きます。これは、一番シンプルな例ですが、has_manyなどの関連でもできます。そちらをやりたい場合は公式のREADME読んでくださいませ。
class ItemSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name, :name_hiragana
end
上記は単純に単純にid, name, name_hiraganaを返します。
なお、返ってくるjsonはこんなかんじ。(この構造はフロント側のコードで使うので頭の片隅に置いておいてください。)
{
data: [
{
id: "167",
type: "item",
attributes: {
id: 167,
name: "トマト",
name_hiragana: "とまと"
}
},
{
id: "168",
type: "item",
attributes: {
id: 168,
name: "きゅうり",
name_hiragana: "きゅうり"
}
}
]
}
このjsonを返すコントローラのアクションはこちらです。フロント側から params[:keyword]
として入力した文字列がやってきます。その文字列を検索して、結果をItemSerializerに渡してJSONで返します。 render json: ItemSerializer.new(items)
class AjaxController < ApplicationController
def item_autocomplete
items = Item.ransack(name_or_name_hiragana_cont: params[:keyword]).result
render json: ItemSerializer.new(items)
end
end
エンドポイント
オートコンプリートとは関係無いですが、複数のキーワードによる検索に対応できるようにします。
split_queries(str)
がそれです。keywordが ,
で区切りだったら分割してransackに渡します。 description_cont_all
とすることで、複数のキーワード(and検索)に対応しています。ransackの細かなところは こちら を見ましょう
class HomeController < ApplicationController
def index
@stores = Store.ransack(description_cont_all: split_queries(params[:keyword])).result
end
private
def split_queries(str)
return nil unless str
str.split(',')
end
end
エンドポイントをroutes.rbに書きます。これで ajax/item_autocomplete
でアクセスできます。(js由来だけに制限したい!とかったら適宜コード書き換えましょう。今回は緩ーく書いています)
Rails.application.routes.draw do
root to: 'home#index'
namespace :ajax do # このあたりを追加
get :item_autocomplete
end
end
フロントエンド
javascript環境のセットアップ
(rails 6なので最初からwebpacker入っています。rails 5系だったら 先にwebpackerを先にインストールしておいて下さい。)
jquery, selectizeのインストール
yarn add selectize
yarn add jquery
selectizeのcssを設定。( application.css だったら application.scss にリネームして下さい。)
ちなみに、bopotstrap3だと node_moudles/selectize/dist/css/
を見てbootstrap3用を選択するといいかも。
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
@import "selectize/dist/css/selectize.default";
オートコンプリートの実装
ここら辺はrailsでseletize.jsの使うための手順。
sprocketにのっかって書いてもいいけど、ES6で書きたいのでwebpackerつかった実装にしました。
rails 5系で webpackerが入っていない場合はこちらのQiitaの記事 Rails & Webpackerでフロントエンド開発環境を整えるを参考に事前にインストールして下さい。
view
selectizeのターゲットとなるフォームにidを付与します。今回は、 store_search
というidを追加しました。それぞれのプロジェクトであわせてください。
こちら後で説明しますが、javascriptを読み込むための javascript_pack_tag
を書きます。今の段階でページを表示するとjavascriptがないのでエラーになります。
<%= javascript_pack_tag 'home' %>
<div>
<%= form_with url: root_path, method: :get, local: true do |f| %>
<%= f.text_field :keyword, value: params[:keyword], autocomplete: 'off', id: 'store_search' %>
<%= f.submit '検索' %>
<% end %>
</div>
javascript
ようやく最後。Javascriptを書きます!
webpackerのエントリーポイントはデフォルトは app/javascript/packs
。こちらにjavascript書きます。ファイル名は何でもいいのですが、viewとあわせるとわかりやすいので home.js
にしました。
で、実際のコードは app/javascript/home
以下に書くのでここでは読み込むだけのコードを書きます。
require('home')
で、下記のコードが実行されます。ファイル名はindex.jsにしてください。( require('home')
だとデフォルトでindex.jsを読み込むので。)
import $ from 'jquery';
require('selectize');
const url = (query) => {
return `/ajax/item_autocomplete?keyword=${encodeURIComponent(query)}`
};
const items = (data) => {
return data.map((item) => {
return {
text: item['attributes']['name'],
sub_text: item['attributes']['name_hiragana'], // ひらがなでも検索結果を絞り込めるようにする
label: item['attributes']['name'],
value: item['attributes']['name']
}
})
};
const renderOption = (item, escape) => {
return `<div><span>${escape(item.text ? item.text : '')}</span></div>`
};
const renderOptionCreate = (item, escape) => {
return `<div><span>新規キーワード... </span><span>${escape(item.input) ? item.input : ''}</span></div>`
};
const getAutocomplete = (query, callback) => {
$.ajax({
url: url(query),
type: 'GET',
error: () => {
callback()
},
success: (res) => {
callback(items(res.data))
}
})
};
const addSelectize = () => {
$('#store_search').selectize({
searchField: ['text', 'sub_text'], // 入力値のフィルター対象、ひらがなでも検索結果を絞り込めるようにする
labelField: 'label', // 表示させるラベル
valueField: 'value', // inputのvalue
closeAfterSelect: true,
create: true, // 新規単語を追加を許可
createOnBlur: true, // 画面外をタッチすると新規単語を追加
render: {
option: (item, escape) => {
return renderOption(item, escape)
},
option_create: (item, escape) => {
return renderOptionCreate(item, escape)
}
},
load: (query, callback) => {
if (!query.length) return callback();
getAutocomplete(query, callback)
}
})
};
const initialize = function () {
addSelectize();
};
window.onload = function () {
initialize();
};
細かく解説します。
import $ from 'jquery';
require('selectize');
ここで、ライブラリを読み込みます。raisl 6は最初からjqueryがないので、 jqueyをimportして、selectizeも読み込みます。
const addSelectize = () => {
$('#store_search').selectize({
searchField: ['text', 'sub_text'], // 入力値のフィルター対象、ひらがなでも検索結果を絞り込めるようにする
labelField: 'label', // 表示させるラベル
valueField: 'value', // inputのvalue
closeAfterSelect: true,
create: true, // 新規単語を追加を許可
createOnBlur: true, // 画面外をタッチすると新規単語を追加
render: {
option: (item, escape) => {
return renderOption(item, escape)
},
option_create: (item, escape) => {
return renderOptionCreate(item, escape)
}
},
load: (query, callback) => {
if (!query.length) return callback();
getAutocomplete(query, callback)
}
})
};
ここら辺がselectizeの中心部。
$('#store_search')
はjqueryのいつものやつ。 .selectize
でselectizeを付与。
load: (query, callback) => {
if (!query.length) return callback();
getAutocomplete(query, callback)
}
ここで、キーワードが入力されたときに実行されます。ここで、 ajaxリクエスト送ります。
const getAutocomplete = (query, callback) => {
$.ajax({
url: url(query),
type: 'GET',
error: () => {
callback()
},
success: (res) => {
callback(items(res.data))
}
})
};
みたままですが、queryがあったら下記のurlにリクエストを送ります。
const url = (query) => {
return `/ajax/item_autocomplete?keyword=${encodeURIComponent(query)}`
};
レスポンスは下記で処理。
success: (res) => {
callback(items(res.data))
}
レスポンスで得られたJSONを使いやすいようにマッピングし直します。
const items = (data) => {
return data.map((item) => {
return {
text: item['attributes']['name'],
sub_text: item['attributes']['name_hiragana'], // ひらがなでも検索結果を絞り込めるようにする
label: item['attributes']['name'],
value: item['attributes']['name']
}
})
};
マッピングし直したkeyをここで指定します。意味の詳細は、公式のusage で調べましょう。簡易的には下記のようにコメント書きました。
searchField: ['text', 'sub_text'], // 入力値のフィルター対象、ひらがなでも検索結果を絞り込めるようにする
labelField: 'label', // 表示させるラベル
valueField: 'value', // inputのvalue
オートコンプリートリストおよび、フォームに表示される内容は下記で指定します。
const renderOption = (item, escape) => {
return `<div><span>${escape(item.text ? item.text : '')}</span></div>`
};
const renderOptionCreate = (item, escape) => {
return `<div><span>新規キーワード... </span><span>${escape(item.input) ? item.input : ''}</span></div>`
};
既に記述していますが、このjavascriptを使いたいところで読み込むために
<%= javascript_pack_tag 'home' %>
をviewに記述します。
今回は、 app/views/home/index.html.erb
記述しました。
最後、webpackerでjavascriptをアセットコンパイルしてもらいます。
bin/webpack-dev-server
http://localhost:3000 にアクセスするとOK
以上。
長文読んでくれてありがとうです。