##インクリメンタルサーチとは
文字の入力のたび、自動的に検索が行われる検索方法のことです。
##インデックスでデータの検索を高速化
テーブル内で検索が頻繁に行われるカラムにインデックスを設定することで検索の高速化します。
今回はtweetsテーブルのtextカラムにインデックスを貼ることで、データの検索を高速化します。
インデックスはデータベースの機能の一つで、テーブル内のデータ検索を高速化することができます。インデックスはカラムに対して設定することができ、設定したカラムでの検索が高速になります。
ちなみにインデックスを設定することを、「インデックスを貼る」と言います。
##インデックスを設定
textカラムに対するインデックスを設定するので、tweetsテーブルに対してインデックスを貼るためのマイグレーションファイルを作成します。
ターミナル
$ rails g migration AddIndexToTweets
次に作成したマイグレーションファイルを編集します。
class AddIndexToTweets < ActiveRecord::Migration
def change
add_index :tweets, :text, length: 32
end
end
ターミナル
rails db:migrate
マイグレーションを実行したらtweetsテーブルのtextカラムに対してインデックスが設定できます。
次からはインクリメンタルサーチの実装をしていきます。
##ルーティングなどAPI側の準備
アクションの中でHTMLとJSONなどのフォーマット毎に条件分岐する記述を追加します。
フォーマット毎に処理を分けるには、respond_toを使用します。
【例】
〜省略〜
def index
@tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5)
end
〜省略〜
def search
@tweets = Tweet.search(params[:keyword])
respond_to do |format|
format.html
format.json
end
end
〜省略〜
投稿情報を取得したら、jbuilderを使ってJavaScript側に返します。
検索結果は、複数の投稿情報を表示させ、格納された配列を返すようなjbuilderの記述します。
app/views/tweets/に「search.json.jbuilder」ファイルを作成します。
【例】作成したsearch.json.jbuilderを編集します。今回はすでにtweetカラムに対してtextやimageなど設定済みです
json.array! @tweets do |tweet|
json.id tweet.id
json.text tweet.text
json.image tweet.image
json.user_id tweet.user_id
json.nickname tweet.user.nickname
json.user_sign_in current_user
end
JSON形式のデータを配列で返したい場合は、array!を使用します。
##jbuilder:array!メソッド
jbuilderという拡張子を持つテンプレートでは、JSONという名前のJbuilderオブジェクトが自動的に利用できるようになります。
Jbuilderオブジェクトは、JSON形式の配列で返したい場合はarray!を使用します。
【例】JavaScript側に送る配列
[{
id: 1,
image: "https://~.jpg",
nickname: "たかし",
text: "プログラミングの初学者です",
user_id: 1,
user_sign_in:
{created_at: "2020-03-19T01:23:45.000Z",
email: "aaa@gmail.com",
id: 1,
nickname: "たかし",
updated_at: "2020-03-19T01:23:45.000Z"}
}]
jbuilderを使用するとより少ない記述でJSON形式のデータを作ることができます。
##テキストフィールドを作成
【例】
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "xxxx") do |form| %>
<%= form.text_field :keyword, placeholder: "投稿を検索する", class: "yyyy" %>
<%= form.submit "検索", class: "zzzz" %>
<% end %>
<div class="contents row">
<% @tweets.each do |tweet| %>
<%= render partial: "tweet", locals: { tweet: tweet } %>
<% end %>
<%= paginate(@tweets) %>
</div>
テキストフィールドが入力されるたびにイベントが発火できるように
文字を打ち込み終わって処理をさせたいときはkeyupメソッドを使用します。
keyupメソッドはjQueryオブジェクトで指定した要素にフォーカスがあるとき、キーが離されたら引数のfunctionを実行します。引数にfunctionを設定しない場合は、要素に設定されたfunctionを実行します。
app/assets/javascripts/に「search.js」ファイルを作成します。
'''app/assets/javascripts/search.js
$(function() {
$(".search-input").on("keyup", function() {
var input = $(".yyyy").val();
});
});
今回の実装で使用するテキストフィールドは先ほどの部分になります。
```app/views/tweets/index.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "xxxx") do |form| %>
<%= form.text_field :keyword, placeholder: "投稿を検索する", class: "yyyy" %>
<%= form.submit "検索", class: "zzzz" %>
<% end %>
テキストフィールドのclass名はyyyyです。
クラス名が".yyyy”の部分のテキストフィールドがkeyupしたら、テキストフィールドの文字を取得して変数inputに代入します。
フォームの値を取得するときはval()を使います。
##イベント時に非同期通信できるように
$(function() {
$(".search-input").on("keyup", function() {
var input = $(".search-input").val();
$.ajax({ #追加〜
type: 'GET',
url: '/tweets/search',
data: { keyword: input },
dataType: 'json'
}) #〜追加
});
});
HTTPメソッドはGETで、/tweets/searchのURLに{ keyword: input }を送信します。サーバーから値を返す際は、JSONになります。
ターミナルでrails routesを実行すると、上記のリクエストによって、tweets_controller.rbのsearchアクションが動きます。
〜省略〜
respond_to do |format|
format.html
format.json
end
〜省略〜
$.ajaxのdataTypeでJSONを指定しているので、サーバーはJSON形式で値を返します。
普段のレスポンス(html形式のレスポンス)ではtweets_controller.rbのsearchアクションが実行されたら、app/views/tweets/search.html.erbが読まれますが、JSON形式の場合は、app/views/tweets/search.json.jbuilderが読まれます。
非同期通信の結果を得て、HTMLを作成
非同期通信の結果をdoneの関数の引数から受取り、ビューに追加するためのHTMLを作成します。
$(function() {
$(".search-input").on("keyup", function() {
var input = $(".search-input").val();
$.ajax({
type: 'GET',
url: '/tweets/search',
data: { keyword: input },
dataType: 'json'
})
.done(function(tweets) { #追加〜
$(".contents.row").empty();
if (tweets.length !== 0) {
tweets.forEach(function(tweet){
appendTweet(tweet);
});
}
else {
appendErrMsgToHTML("一致するツイートがありません");
}
}) #〜追加
});
});
.done(function(tweets) {
$(".contents.row").empty();
})
インクリメンタルサーチでは、検索をする直前に投稿情報のリスト(テキストや画像)を削除してあげる必要があります。
〜省略〜
<div class="contents row">
<% @tweets.each do |tweet| %>
<%= render partial: "tweet", locals: { tweet: tweet } %>
<% end %>
<%= paginate(@tweets) %>
</div>
検索結果欄で出力されている投稿情報をemptyメソッドを使用して削除します。
##emptyメソッド
指定したDOM要素の子要素のみを削除するメソッドです。
投稿情報をすべて削除したいので、
なのでsearch.jsの$(".contents.row").empty();で投稿の情報を削除できます。
if (tweets.length !== 0) {
tweets.forEach(function(tweet){
appendTweet(tweet);
});
}
else {
appendErrMsgToHTML("一致するツイートがありません");
}
上記の関数は、jbuilderから得られた値を投稿情報のリストに追加するものです。
tweetsが空ではない場合はtweets.length !== 0を記述します。
forEachメソッドを用いて、tweetsの中身の数だけappendTweet関数を呼び出します。
##forEachメソッド
与えられた関数を配列に含まれる各要素に対して一度ずつ呼び出します。
##tweetsが空の場合
”一致するツイートがありません”という引数を与え、appendErrMsgToHTML関数を呼び出します。
tweetsに投稿の情報が入っている場合のappendTweet関数を定義します。
<div class="content_post" style="background-image: url(<%= tweet.image %>);">
<div class="more">
<span><%= image_tag 'arrow_top.png' %></span>
<ul class="more_list">
<li>
<%= link_to "詳細", tweet_path(tweet.id), method: :get %>
</li>
<% if user_signed_in? && current_user.id == tweet.user_id %>
<li>
<%= link_to '編集', "/tweets/#{tweet.id}/edit", method: :get %>
</li>
<li>
<%= link_to '削除', "/tweets/#{tweet.id}", method: :delete %>
</li>
<% end %>
</ul>
</div>
<%= simple_format(tweet.text) %>
<span class="name">
<a href="/users/<%= tweet.user_id %>">
<span>投稿者</span><%= tweet.user.nickname %>
</a>
</span>
</div>
$(function() {
var search_list = $(".contents.row"); #追加〜
function appendTweet(tweet) {
if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
var current_user = `<li>
<a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
</li>
<li>
<a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
</li>`
} else {
var current_user = ""
}
var html = `<div class="content_post" style="background-image: url(${tweet.image});">
<div class="more">
<span><img src="/assets/arrow_top.png"></span>
<ul class="more_list">
<li>
<a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
</li>
${current_user}
</ul>
</div>
<p>${tweet.text}</p><br>
<span class="name">
<a href="/users/${tweet.user_id}">
<span>投稿者</span>${tweet.nickname}
</a>
</span>
</div>`
search_list.append(html);
} #〜追加
$(".search-input").on("keyup", function() {
var input = $(".search-input").val();
$.ajax({
type: 'GET',
url: '/tweets/search',
data: { keyword: input },
dataType: 'json'
})
.done(function(tweets) {
search_list.empty(); #追加
if (tweets.length !== 0) {
tweets.forEach(function(tweet){
appendTweet(tweet);
});
}
else {
appendErrMsgToHTML("一致するツイートがありません");
}
})
});
});
上記の追加の部分は削除した投稿情報のhtmlをもう一度作成しています
違う部分は<%= %>で出力しているものをjbuilderで取得した値に変えています。
<%= %>で出力しているものは、jbulider取得した値を${}で出力することができます。
次に、tweetsに投稿の情報が入っていない場合のappendErrMsgToHTML関数を定義します。
$(function() {
var search_list = $(".contents.row");
function appendTweet(tweet) {
if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
var current_user = `<li>
<a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
</li>
<li>
<a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
</li>`
} else {
var current_user = ""
}
var html = `<div class="content_post" style="background-image: url(${tweet.image});">
<div class="more">
<span><img src="/assets/arrow_top.png"></span>
<ul class="more_list">
<li>
<a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
</li>
${current_user}
</ul>
</div>
<p>${tweet.text}</p><br>
<span class="name">
<a href="/users/${tweet.user_id}">
<span>投稿者</span>${tweet.nickname}
</a>
</span>
</div>`
search_list.append(html);
}
function appendErrMsgToHTML(msg) { #追加〜
var html = `<div class='name'>${ msg }</div>`
search_list.append(html);
} #〜追加
$(".search-input").on("keyup", function() {
var input = $(".search-input").val();
$.ajax({
type: 'GET',
url: '/tweets/search',
data: { keyword: input },
dataType: 'json'
})
.done(function(tweets) {
search_list.empty();
if (tweets.length !== 0) {
tweets.forEach(function(tweet){
appendTweet(tweet);
});
}
else {
appendErrMsgToHTML("一致するツイートがありません");
}
})
});
});
追加した部分もtweetsに値が入っている場合とやっていることが同じです。
コントローラーで検索をかけ、その投稿情報がなかった場合は、「一致するツイートがありません」という文字列を引数に渡してHTML要素を作成しビューに追加しています。
##エラー時の処理
最後に、通信に失敗した場合の処理を実装します。
アラートで「投稿検索に失敗しました」と表示します。
$(function() {
var search_list = $(".contents.row");
function appendTweet(tweet) {
if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
var current_user = `<li>
<a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
</li>
<li>
<a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
</li>`
} else {
var current_user = ""
}
var html = `<div class="content_post" style="background-image: url(${tweet.image});">
<div class="more">
<span><img src="/assets/arrow_top.png"></span>
<ul class="more_list">
<li>
<a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
</li>
${current_user}
</ul>
</div>
<p>${tweet.text}</p><br>
<span class="name">
<a href="/users/${tweet.user_id}">
<span>投稿者</span>${tweet.nickname}
</a>
</span>
</div>`
search_list.append(html);
}
function appendErrMsgToHTML(msg) {
var html = `<div class='name'>${ msg }</div>`
search_list.append(html);
}
$(".search-input").on("keyup", function() {
var input = $(".search-input").val();
$.ajax({
type: 'GET',
url: '/tweets/search',
data: { keyword: input },
dataType: 'json'
})
.done(function(tweets) {
search_list.empty();
if (tweets.length !== 0) {
tweets.forEach(function(tweet){
appendTweet(tweet);
});
}
else {
appendErrMsgToHTML("一致するツイートがありません");
}
})
.fail(function() { #追加〜
alert('error');
}); #〜追加
});
});
サーバーエラーの場合、このfailの関数が呼ばれます。