Autocomplete sử dụng Typeahead và Searchkick trong Rails
Bài đăng này đã không được cập nhật trong 6 năm
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="✓" />
<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.
All rights reserved