Help us understand the problem. What is going on with this article?

インクリメンタルサーチを噛み砕く

近況報告

やあ。テックキャンプのリモートワーク化がはじまって四週間目に突入しました。我がチームはついに最終課題をパスし間も無く就職活動に入ります。そこで大きな壁,履歴書の自己開示。わたしが一番苦手とする場所です。「わたしはプログラミングでご飯食べていくってきめたの!」って叫んで内定決まればいいのに笑

さて,今日の記事は個人アプリにインクリメンタルサーチを実装のついでの忘備録です。実況感覚で書いていきます。

インクリメンタルサーチ

なにそれ

 英語でincremental search,直訳は増加検索ですね。通常の検索は検索ボタンを押すとページが更新されて検索結果が出てきますが,この検索は一文字入れるごとに非同期通信で検索結果がぽんぽん出てきます。youtubeやGoogleとか検索の予測がでますね。あれです。文字を打つごとに予測される結果が減るからデクリメンタルサーチでもいいんじゃね。ちなみに,予測の変換に履歴が出てくるのはcookieがかかわるので少々異なります。

非同期通信・・・ユーザーの望むタイミングとは別で行われる通信です。通信自体は行われています 。railsなら,ユーザーのリクエストをMVCで逐一処理してそれに応じたビューをユーザーに届けています。ただ,例えばgoogle検索で「ツイッ」まで書いたところでリダイレクトして予測変換「ツイッター」が記載されたビューに更新されたとします。検索フォームの部分だけ変わればいいのに,変化のないGoogleのロゴとかも更新されるの無駄な気がしません?痒いところに届くのが非同期通信です。私のイメージは,必要な情報だけをビューに上書きする通信です。

なんで必要?

 いちいち検索ボタンを押して読み込むより早く検索結果をユーザーに届けることができます。個人アプリのような小さい規模ではそこまで破壊力があるわけではありません。ただ,ロゴが舌なめずりしている某サイトだと通信が0.1秒遅れると売り上げが1%落ちると聞きました。1兆円の売り上げなら100億円の損失です。まさに時は金なりですね!

環境(前提)

・rails 2.5.2
・ruby 5.2.3
gem 'jquery-rails'
DBに検索対象のレコードが入っている
検索機能の実装完了前提

今回はjqueryのgemを用いるから

gem 'jquery-rails'

からのbundle install。
application.jsでjqueryが使えるように,

//= require jquery

の記述の有無を確認。

これで準備おっけー

実装(実況感覚)

検索フォームに入力するとテーブルのnameカラムを検索する機能を実装していきます。

インデックスの実装

インデックスってなんだっけ?笑

カリキュラムを見直すと,検索の際にどれを検索すればいいのかをピックアップしてくれる機能なんだってさ。
UFOキャッチャーで例えると,UFOが検索するための端末としたらインデックスは商品(レコード)が引っかかりやすくなるために付いているリングみたいな役割なのかな。 ワカリニクイタトエデース

ただ,インデックスは検索に用いるカラムのみを複製して1つのテーブルみたいにしてるから,作りすぎるとデータベースの容量の圧迫になるみたい。

さて,インデックスを追加していきます

$ rails g migration AddIndexTo検索したいテーブルs

実行したら,マイグレーションファイルに飛んで

class AddIndexTo検索したいテーブル名 < ActiveRecord::Migration
  def change
    add_index :検索したいテーブルs, :検索したいカラム, length: 32(最大文字数)
  end
end

を記述したらマイグレート。ミスったらDB:rollbackなれdb:dropとなれ。

APIの準備

インクリメンタルサーチの実装の順番はrailsの実装とあんまり変わりません。
コントローラーに条件分岐の記入

reviews_controller.rb
  def search
    @reviews = Reviews.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end

formatごとのレスポンスで場合分けします。
format.htmlはhtml全部読み込みますよ,format.jsonはJS形式のみ読み込みますよって感じ。この分岐のおかげで非同期通信が可能になります。

コントローラーで条件分岐したら,DBから該当のでーたをビューまで運ぶ箱,jbuilder(パラムスに似てるね)を作成する。検索しているテーブルのビューファイルにフォルダsearch.json.jbuilderを作成。jbuilderを用いると,そのままJSでも用いることができる。

search.json.jbuilder
json.array! @reviews do |review| ←検索に引っかかった各インスタンスを配列でバラすぜ!中身は以下の通りだ!
  json.id review.id
  json.content review.content 
  json.image review.image 
  json.taste review.taste
  json.fragrance review.fragrance
  json.individuality review.individuality
  json.fruity review.fruity
  json.smorky review.smorky
  json.age review.age
  json.user_id review.user_id 
  json.user_sign_in current_user
end

テキストフィールドの実装

次はビューの作成をしていきます。検索フォームはこんな感じ↓

index.html.erb
<%= form_with(url: review_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>

ではJSの記述に入ります。記述量が多いので少しずつ刻んでいきます。

search.js
$(function() {
  $(".search-input").on("keyup", function() { ←フォームに何か打ち込まれたら作動するよ
    var input = $(".search-input").val(); ←inputを打ちこまれた文字と定義するよ
    console.log(input); ←JSが動いているか確認。うまく表示されていたら消す。
  });
});

フォームの情報をJSON形式でコントローラーに送る

今までやったことの確認をすると,
・respond_toで行き先決定
・jbuilderでデータベースの情報の引き出し準備
・ビューでフォームの情報をJSで抽出する
うん,MVCだね。railsは非同期通信でも大まかなやることは同じです。もしこれ行こうエラーが発生したらMVCに則ってエラーを解消していきます。

search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
//以下を追加
    $.ajax({ ←ajaxは非同期通信するよ!っていう合図
      type: 'GET', ←HTTPメソッドはDBを得るからGET
      url: '/review/search', ←非同期通信が行われるurl
      data: { keyword: input }, ←さーばーに送信するキー。これを元にインデックスから引っ張ってくる。
      dataType: 'json'←データの形式
    })
  });
});

これでフォームの情報はJSON形式で送られ,コントローラーのrespond_toでJSONに振り分けられてモデルに進みます。多分。

非同期通信がビューまで届いているか確認します。メソッドdoneを用いて届いた情報をみてみる。

search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/review/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(reviews) {
      console.log(reviews);
    })
  });
});

事故発生。読み込めてない。。。順番に確認していこう。
JSは読み込めていたから,クラスのミスはない。
ajaxの読み込みミスは、、、url間違えていました、、、
分岐は誤字なし,jbuilderは、、、nameを書き忘れる痛恨のミス!何調べようとしてたんだ

も一回検索フォームにキーワードを打ち込む、、、うん、うまくいった!!!

スクリーンショット 2020-04-21 19.21.46.png

さて,次に進みます。次はJSを利用してHTMLを上書きします。
まずdone以下を以下のように編集します。

search.js
~省略~
    .done(function(reviews) { ←reviewを引張ってきたから以下の実行するよ
      $(".showZone").empty(); ←該当クラスの中の子要素全て消すね
      if (reviews.length !== 0) { ←もし1でもあったら
        reviews.forEach(function(review){ ←1つずつ並べて
          appendReview(review); ←そのレコードを出現させるよ
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
~省略~

次はappendReview(review)に該当する定義を作成してクラスshowZoneに上書きするHTMLを作成します。

search.js

$(function() {

  var search_list = $(".showZone"); ←こういうやつは先に定義にしとく

  function appendReview(review) {
    var html = `<div class="reviewWhisky">
            <a href="/reviews/${review.id}">
            <div class="topZone">
              <div class="topZone__image">
                <img id="goShowPage" src="${review.image.url}" width="180" height="180">
              </div>
             <div class="bottomZone">
             <p>${review.name}</p>
            </div></div></a></div>`
   //htmlは検証からコピーが最速。データベースからの読み込み表示は上の書き方参考にしてね
    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: '/reviews/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(reviews) {
      search_list.empty();
      if (reviews.length !== 0) {
        reviews.forEach(function(review){
          appendReview(review); ←appendHTML召喚のメソッドと覚えておけばOK
        });
      }
      else {
        appendErrMsgToHTML("一致するのがありません");
      }
    })
  });
});

では,もし何も投稿が該当しなかった時用のを用意しましょ。いま一枚もない場合,何も写っていない状態だから,バグじゃないよーてわかるような記述を書きますappendErrMsgToHTMLに意味を持たせます)。

search.js
~省略~
    search_list.append(html);
  }
  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`←新しい要素なので必要ならCSS
    search_list.append(html);
~省略~
    .fail(function() {←うまく行かなかった時用にエラー表示を準備
      alert('error');
    });
  });
});

これで一通り完成かな。ちなみに中の人はキータ執筆と同時進行で行なっていたのですが,search.jsファイルが読み込まれなくなる不具合に遭遇し2時間詰みました笑

解決策としては別のファイルを作成して作り直したらうまくいきまっした、、よかった、、、ただ,何が原因のエラーだったんだろう。。。

 おわりに

 まあなんとか実装できました。非同期通信のjsはコード1つ1つがわかりやすいので比較的サクッといけますね。非同期通信で重要なのは,通信の根本はMVCであること!

余談

 非同期通信とturbolinks(以下TB)は仲が悪いみたいです。TBってなんやねんって話ですよね。カリキュラムでも無下に扱われていました。この子は名前の通り,リンク先に早くたどり着くため手助けをしてくれます。ただ,早くする方法というのは,余計な動きを削ること,ここではJSを起動しないことです。非同期通信はJSを用いた通信なので,turbolinksが邪魔をしてしまう可能性がある(実際邪魔する)訳ですねー。

ただ悪い子ではないので見つけ次第gemをコメントアウト!とかしないであげてください。
$(document).on('turbolinks:load', function(){
とJSの初めに打ってあげたり,link_toにつづけて
data:{"turbolinks": :false}
と打ってあげると,JSが動いてくれます

aaabb6211
TECHCAMP72期卒業(since 23/04/2020)⇨インフラ系エンジニア 趣味はウィスキーを飲むこと。twitter : @silverchair_pg
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした