Rails

Autocomplete sử dụng Typeahead và Searchkick trong Rails

Thư viện sử dụng

  • Gem Searchkick cho việc tìm kiếm
  • Gem ElasticSearch cho Full Text Search
  • Thư viện javascript Typeahead cho việc autocomplete

Cài đặt Searchkick
Tạo 1 project Rails 5 và thêm vào gem Searchkick
gem \'searchkick\'
Chạy lệnh bunlde và tạo 1 resource có tên là article
rails g scaffold article title content:text
Thêm searchkick và model article

class Article < ApplicationRecord
  searchkick
end

Tạo database
Tạo 1 vài database trong file seeds.rb:

Article.destroy_all
data = [{ title: \'Star Wars\', content: \'Wonderful adventure in the space\' }, 
        { title: \'Lord of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Man of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Woman of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Dog of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Daddy of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Mommy of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Duck of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Drug Lord of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Native of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Naysayer of the Rings\', content: \'Lord that became a ring\' },
        { title: \'Tab Wars\', content: \'Lord that became a ring\' },
        { title: \'Drug Wars\', content: \'Lord that became a ring\' },
        { title: \'Cheese Wars\', content: \'Lord that became a ring\' },
        { title: \'Dog Wars\', content: \'Lord that became a ring\' },
        { title: \'Dummy Wars\', content: \'Lord that became a ring\' },
        { title: \'Dummy of the Rings\', content: \'Lord that became a ring\' }
        ]
Article.create(data)

Chạy migrate và chạy file seed:

rails db:migrate
rails db:seed

Kết nối thử nghiệm với ElasticSearch
Chỉ mục các dữ liệu article ở trong elasticsearch
rake searchkick:reindex CLASS=Article
Giờ đây chúng ta có thể dùng rails console để xác minh các tính năng tìm kiếm

> results = Article.search(\'War\')
  Article Search (11.7ms)  curl http://localhost:9200/articles_development/_search?pretty -d \'{"query":{"dis_max":{"queries":[{"match":{"_all":{"query":"War","boost":10,"operator":"and","analyzer":"searchkick_search"}}},{"match":{"_all":{"query":"War","boost":10,"operator":"and","analyzer":"searchkick_search2"}}},{"match":{"_all":{"query":"War","boost":1,"operator":"and","analyzer":"searchkick_search","fuzziness":1,"prefix_length":0,"max_expansions":3,"fuzzy_transpositions":true}}},{"match":{"_all":{"query":"War","boost":1,"operator":"and","analyzer":"searchkick_search2","fuzziness":1,"prefix_length":0,"max_expansions":3,"fuzzy_transpositions":true}}}]}},"size":1000,"from":0,"fields":[]}\'
 => #<Searchkick::Results:0x007fcf42475dd8 @klass=Article (call \'Article.connection\' to establish a connection), @response={"took"=>9, "timed_out"=>false, "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0}, "hits"=>{"total"=>6, "max_score"=>0.37037593, "hits"=>[{"_index"=>"articles_development_20160518103333170", "_type"=>"article", "_id"=>"16", "_score"=>0.37037593}, {"_index"=>"articles_development_20160518103333170", "_type"=>"article", "_id"=>"15", "_score"=>0.37037593}, {"_index"=>"articles_development_20160518103333170", "_type"=>"article", "_id"=>"12", "_score"=>0.3074455}, {"_index"=>"articles_development_20160518103333170", "_type"=>"article", "_id"=>"14", "_score"=>0.3074455}, {"_index"=>"articles_development_20160518103333170", "_type"=>"article", "_id"=>"1", "_score"=>0.21875}, {"_index"=>"articles_development_20160518103333170", "_type"=>"article", "_id"=>"13", "_score"=>0.21875}]}}, @options={:page=>1, :per_page=>1000, :padding=>0, :load=>true, :includes=>nil, :json=>false, :match_suffix=>"analyzed", :highlighted_fields=>[]}>

Chúng ta có thể kết nối tới máy elasticsearch server bằng việc sử dụng thư viện searchkick và lấy kết quả tìm kiếm

> results.class
 => Searchkick::Results

Kết quả là đối tượng Searchkick :: Results. Chúng ta có 6 data trong kết quả.

> results.size
  Article Load (0.4ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (16, 15, 12, 14, 1, 13)
 => 6

Tích hợp thư viện Typehead
Hãy thêm form tìm kiếm vào trang index artciles

<%= form_tag articles_path, method: :get do %>
  <%= text_field_tag :query, params[:query], class: \'form-control\' %>
  <%= submit_tag \'Search\' %>
<% end %>

Giờ bạn có thể tìm kiếm trong trang index nhưng không có autocomplete. Hãy cùng tạo tính năng autocomplete. Tải thư viện typehead.js version 0.11.1 và chuyển nó vào thư mục vendor/assets/javascripts. Thêm typehead.js vào trong application.js :

//= require typeahead

Thêm vào trong controller hàm tìm kiếm với autocomplete:

def autocomplete
  render json: Article.search(params[:query], autocomplete: true, limit: 10).map(&:title)
end

Định nghĩa route:

Rails.application.routes.draw do
  resources :articles do
    get :autocomplete
  end
end

Thêm id và thuộc tính autocomplete vào trong trường tìm kiếm ở trong trang index

<%= text_field_tag :query, params[:query], class: \'form-control\', id: "article_search" %>

Tạo file xử lí article.js:

var ready;
ready = function() {
    var engine = new Bloodhound({
        datumTokenizer: function(d) {
            console.log(d);
            return Bloodhound.tokenizers.whitespace(d.title);
        },
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
            url: \'../articles/autocomplete?query=%QUERY\',
            wildcard: \'%QUERY\'
        }
    });

    var promise = engine.initialize();

    promise
        .done(function() { console.log(\'success!\'); })
        .fail(function() { console.log(\'err!\'); });

    $(\'.typeahead\').typeahead(null, {
        name: \'engine\',
        displayKey: \'title\',
        source: engine.ttAdapter()
    });
}

$(document).ready(ready);
$(document).on(\'page:load\', ready);

Nếu bạn không cung cấp kí tự đại diện, bạn sẽ gặp lỗi như sau:

GET http://localhost:3000/search/autocomplete?query=%QUERY 400 (Bad Request)

Trong console của cửa sổ trình duyệt:

HTTP parse error, malformed request puma

Tách vấn đề
Bạn dùng curl để cô lập các vấn đề cho front-end và back-end
curl http://localhost:3000/articles?query=\'dog\'
Trong log, bạn có thể nhìn thấy:

Article Search (19.4ms)  curl http://localhost:9200/articles_development/_search?pretty -d \'{"query":{"dis_max":{"queries":[{"match":{"_all":{"query":"dog","boost":10,"operator":"and","analyzer":"searchkick_search"}}},{"match":{"_all":{"query":"dog","boost":10,"operator":"and","analyzer":"searchkick_search2"}}},{"match":{"_all":{"query":"dog","boost":1,"operator":"and","analyzer":"searchkick_search","fuzziness":1,"prefix_length":0,"max_expansions":3,"fuzzy_transpositions":true}}},{"match":{"_all":{"query":"dog","boost":1,"operator":"and","analyzer":"searchkick_search2","fuzziness":1,"prefix_length":0,"max_expansions":3,"fuzzy_transpositions":true}}}]}},"size":1000,"from":0,"fields":[]}\'
  Rendering articles/index.html.erb within layouts/application

Đầu ra trong terminal:

<!DOCTYPE html>
<html>
  <head>
    <title>Autoc</title>
    <meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="O9rx6qf0ik6ae" />
    <link rel="stylesheet" media="all" href="/assets/articles.self-e3b04b855.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/scaffolds.self-c8daf17deb4.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/application.self-a9e16886.css?body=1" data-turbolinks-track="reload" />
    <script src="/assets/jquery.self-35bf4c.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/jquery_ujs.self-e87806d0cf4489.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/typeahead.self-7d0ec0be4d31a26122.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/turbolinks.self-979a09514ef27c8.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/articles.self-ca74ce155498e7f0.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/action_cable.self-97a1acc11db.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/cable.self-6e05142.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/application.self-afe802b04eaf.js?body=1" data-turbolinks-track="reload"></script>
  </head>
  <body>
    <p id="notice"></p>
<form action="/articles" accept-charset="UTF-8" method="get"><input name="utf8" type="hidden" value="&#x2713;" />
  <input type="text" name="query" id="article_search" value="dog" class="form-control" />
  <input type="submit" name="commit" value="Search" data-disable-with="Search" />
</form>
<h1>Articles</h1>
<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Content</th>
      <th colspan="3"></th>
    </tr>
  </thead>
  <tbody>
      <tr>
        <td>Dog Wars</td>
        <td>Lord that became a ring</td>
        <td><a href="/articles/15">Show</a></td>
        <td><a href="/articles/15/edit">Edit</a></td>
        <td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/articles/15">Destroy</a></td>
      </tr>
      <tr>
        <td>Dog of the Rings</td>
        <td>Lord that became a ring</td>
        <td><a href="/articles/5">Show</a></td>
        <td><a href="/articles/5/edit">Edit</a></td>
        <td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/articles/5">Destroy</a></td>
      </tr>
  </tbody>
</table>
<a href="/articles/new">New Article</a>
  </body>
</html>

đây không phải là autocomplete, nếu bạn tìm kiếm 1 cái gì đó bạn sẽ gặp lỗi:
ActiveRecord::RecordNotFound (Couldn\'t find Article with \'id\'=autocomplete):
Bở vì chúng ta không định tuyến trong routes, hãy kiểm tra bằng rake routes
article_autocomplete GET /articles/:article_id/autocomplete(.:format) articles#autocomplete
route không đúng, giờ hãy sửa lại như sau:

Rails.application.routes.draw do
  resources :articles do
    collection do
      get :autocomplete
    end
  end
end

Triển khai autocomplete
Cấu hình autocomplete trong model article:
searchkick autocomplete: [\'title\']
Triển khai autocomplete trong controller:

def autocomplete
  render json: Article.search(params[:query], autocomplete: false, limit: 10).map do |book|
    { title: book.title, value: book.id }
  end
end

Bạn cần thêm class typehead và trong form tìm kiếm:

<%= text_field_tag :query, params[:query], class: \'form-control typeahead\' %>

Giờ thì bạn hãy thử F5 trình duyệt và thử tìm kiếm, nó sẽ tự động autocomplete cho bạn
Style Autocomplete DropDown
Tạo typehead.scss và thêm vào:

.tt-query {
  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
     -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}

.tt-hint {
  color: #999
}

.tt-menu {    /* used to be tt-dropdown-menu in older versions */
  width: 422px;
  margin-top: 4px;
  padding: 4px 0;
  background-color: #fff;
  border: 1px solid #ccc;
  border: 1px solid rgba(0, 0, 0, 0.2);
  -webkit-border-radius: 4px;
     -moz-border-radius: 4px;
          border-radius: 4px;
  -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
     -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
          box-shadow: 0 5px 10px rgba(0,0,0,.2);
}

.tt-suggestion {
  padding: 3px 20px;
  line-height: 24px;
}

.tt-suggestion.tt-cursor,.tt-suggestion:hover {
  color: #fff;
  background-color: #0097cf;

}

.tt-suggestion p {
  margin: 0;
}

Bắt việc search ở trong trang index:

def index
  @articles = if params[:query].present?
    Article.search(params[:query])
  else
    Article.all
  end
end

Bây giờ bạn có thể nhìn thấy autocomplete với hiệu ứng đẹp mắt.

Registration Login
Sign in with social account
or
Lost your Password?
Registration Login
Sign in with social account
or
A password will be send on your post
Registration Login
Registration