12/08/2018, 15:39

Global Autocomplete Search

Trong bài viết này, chúng ta sẽ nói về việc thêm một tính năng autocomplete search vào ứng dụng Rails của bạn. Giống như bạn thấy trên facebook, google (tất nhiên không thể nào được như họ), nơi bạn có thể nhập từ khóa, hoặc một thuật ngữ tìm kiếm hoặc bất cứ điều gì, và nó sẽ trả ra cho bạn thông ...

Trong bài viết này, chúng ta sẽ nói về việc thêm một tính năng autocomplete search vào ứng dụng Rails của bạn. Giống như bạn thấy trên facebook, google (tất nhiên không thể nào được như họ), nơi bạn có thể nhập từ khóa, hoặc một thuật ngữ tìm kiếm hoặc bất cứ điều gì, và nó sẽ trả ra cho bạn thông tin của một số kết quả đầu tiên.

Cơ chế hoạt động của nó sơ lược như sau: mỗi lần bạn nhập vào ô tìm kiếm, nó sẽ kích hoạt một yêu cầu AJAX, máy chủ sẽ thực hiện một số tìm kiếm và kết hợp các kết quả đó vào một đối tượng JSON được trả về trình duyệt và thư viện JavaScript của bạn cho tính năng autocomplete những kết quả đó, và sau đó hiển thị chúng ra nếu bạn muốn. Trong bài viết này ta sử dụng thư viện là EasyAutocomplete để sử dụng tính năng này.

Đầu tiên chung ta cần chuẩn bị một project và cài đặt thư viện để có thể sử dụng. Trong bài viết này chúng ta sẽ sử dụng Rails 5.1, có 2 model là movies và directors - chúng ta sẽ tìm kiếm dựa trên 2 model này. Các bạn cần seed một số dữ liệu mẫu và thêm bootstrap để có một navbar đẹp hơn.

EasyAutocomplete có một số hướng dẫn cài đặt, bạn có thể tải xuống thư viện và có hai tệp CSS và tệp JavaScript, bạn muốn đưa vào JavaScript của mình, sau đó bạn có thể thêm các themes hoặc bạn có thể sử dụng mặc định. Tất nhiên là cần phải đặt thư viện trong application của bạn, và bạn cũng cần phải chắc chắn rằng bạn đã cài đặt jQuery. Bạn có thể làm việc này thông qua CDN hoặc thông qua asset pipeline bằng cách sử dụng jQuery Rails gem - ở đây tôi sẽ lựa chọn cách này vì nó thuận tiện hơn.

# app/assets/javascripts/application.js

//= require jquery easy-autocomplete
# app/assets/stylesheets/appliaction.scss

*= require easy-autocomplete
*= require easy-autocomplete.themes 

Với khai báo trên thì chúng ta có thể truy cập ở tất cả mọi nơi trong ứng dụng. Để chắc chắn bạn có thể kiếm tra thủ công bằng cách check trên trình duyệt của mình, đảm bảo rằng nó được tải và có thể định nghĩa một số tùy chọn.

$("input").easyAutocomplete(options)

Tuy nhiên hãy chắc chắn răng trên tang của bạn chỉ có 1 trường input để test dữ liệu, khi đó easy-autocompletes sẽ pass qua option này.

Nếu tất cả hoạt động chính xác, bạn đã thấy thay đổi này một chút về kiểu dáng vì CSS đã được áp dụng cho tính năng autocomplete và bây giờ nếu chúng ta gõ bất kỳ tùy chọn nào trong số đó, như "green" hoặc "red", nó tự động tạo ra cửa sổ tự động hoàn thành và sau đó nó làm nổi bật từ mà bạn đã gõ vào bất kỳ chỗ nào gặp. Nếu bạn nhập "e", bạn có thể thấy rằng được đánh dấu bằng bốn trong số năm kết quả. Tính năng easy autocomple đang hoạt động và bây giờ chúng tôi cần phải làm việc trên các công cụ tìm kiếm ở back end để có thể chuyển nó vào EasyAutocomplete cho các tùy chọn này. Chúng tôi cần endpoint tìm kiếm chung với tất cả các model muốn tìm kiếm. Như đã nói ở phần chuẩn bị chúng ta sẽ tìm kiếm các bộ phim và đạo diễn, vì vậy cần phỉa có một MainController với methodsearch.

# config/routes.rb

Rails.application.routes.draw do 
    get :search, controller: :main 
    root to: "main#index"
end

Chúng ta một URL "/ search" mà có thể đi đến action search trong MainController. Ở đây chỉ cần trả về một JSON object nên chỉ cần render json như sau:

# apps/controllers/main_controller.rb

def search 
    render json: {movies: [], directors: []}
end 

Trả về một đối tượng JSON với 2 mảng là movies và directors. Để kiểm tra, bạn có thể thêm ".json" hoặc chúng ta chỉ có thể thực hiện "/search" sẽ luôn luôn nhận được hash ruby đó được chuyển đổi sang JSON. Bạn có thể xử lí tìm kiếm từ cơ sở dữ liệu hoặc Elasticsearch hay bất cứ loại tìm kiếm nào để trả ra dữ liệu tương ứng ở đây.

Để làm việc đó chúng ta có khá nhiều gem hỗ trợ ví dụ như Ransack cho tìm kiếm cơ bản, với Elasticsearch bạn có thể sử dụng searchkick hay pg_search hay bất kí kiểu search nào. Ở đây tôi sẽ chỉ sử dụng gem Ransack để tìm kiếm. Quan trọng tất cả những gì bạn cần để có thể thực hiện, là thực hiện các params, chạy một truy vấn đối với dữ liệu và nhận được mảng các kết quả trở lại, do đó miễn là tìm kiếm có thể làm điều đó, bạn có thể tự do sử dụng bất cứ điều gì bạn thích. Để tăng sức mạnh này, chỉ cần lấy lại những kết quả đó sao cho có thể vượt qua chúng trong MainController.

Chúng ta cần phải tìm kiếm cả movies và directors cùng lúc, và nếu đã quen thuộc với Ransack chúng ta có thể sửa lại hàm search trong MainController như sau:

# app/controllers/main_controller.rb

def search 
    @movies = Movie.ransack(params[:a]).result(distinct: true)
    @directors = Director.ransack(params[:a]).result(distinct: true) 

    render json: {movies: [], directors: []}
end 

Chúng ta có hai mảng của các đối tượng ActiveRecord và cần phải chuyển đổi các đối tượng này sang JSON, và có nhiều cách khác nhau mà bạn có thể chuyển đổi ActiveRecord thành JSON, có thực hiện bằng câu lệnh map, jbuilder hoặc ActiveModel serializers. Nó không thực sự quan trọng lắm, ở đây tôi sử dụng jbuilder để thực hiện việc này như sau:

# app/views/main/search.json.jbuilder

json.movies do
  json.array!(@movies) do |movie|
    json.name movie.name
    json.url movie_path(movie)
  end
end

json.directors do
  json.array!(@directors) do |director|
    json.name director.name
    json.url director_path(director)
  end
end

Chúng ta nên tạo ra JSON phù hợp để chúng tôi có name và URL cho mỗi đối tượng trong đó. Bây giờ chúng ta có thể thử thử bằng cách thêm .json vào URL, và chúng ta sẽ có được danh sách các movies và directors. Ta có thể điều chỉnh lại controller một chút để đảm bảo rằng không cần thiết phải có .json trong URL và chỉ cần /search sẽ luôn hiển thị JSON bất kể định dạng đó là search.html.

# app/controllers/main_controller.rb

before_action :force_json, only: :search 

...

private 

    def force_json 
        request.format = :json 
    end 

Bây giờ chúng ta phải chỉnh sửa movies và directors bởi vì nếu thêm vào một cụm từ truy vấn ở đây, như "Cure". "Cure for wellness" nên là kết quả được lọc ở đây, nhưng chúng ta không thực sự thấy rằng việc lọc đúng, vì vậy chúng ta cần phải giải quyết vấn đề đó, và đảm bảo rằng chúng ta có công cụ tìm kiếm của chúng ta hoạt động, để khi truyền "q=" bất cứ điều gì, hệ thống lấy đó và làm một tìm kiếm thích hợp trên model.

Trước hết, điều chúng tôi muốn làm là hạn chế kết quả (khoảng 5 kết quả), nếu không danh sách trả về sẽ rất dài, và điều đó sẽ không cần thiết và khá xấu. Chúng ta chỉ muốn bạn biết từ ba đến năm kết quả cho mỗi truy vấn . Đó có thể là kết quả tốt nhất, bạn cần thiết kế việc tìm kiếm một cách phức tạp hơn như tìm kiếm trên nhiều attributes và các cách khác nhau. Những gì chúng ta muốn làm để có những params q - bất cứ điều gì người dùng nhập vào, và muốn phù hợp với điều đó đối với name. Chúng ta có thể sử dụng name_cont: params [: q]. Bạn có thể vượt qua nó trong elasticsearch hoặc bất cứ thứ gì tương tự như vậy, miễn là bạn nhận được những kết quả đó, và bạn có thể hạn chế chúng đến một con số hợp lý, như 3-5, sau đó bạn có thể gán chúng cho điều này, và Jbuilder JSON của bạn sẽ tự động thu những bản ghi đó và chuyển chúng sang cột thích hợp, và url cho mỗi một trong số đó. Đây là cách chúng ta sẽ thực hiện nó với Ransack, việc triển khai của bạn có thể khác một chút, nếu bạn đang sử dụng một cơ chế tìm kiếm khác, nhưng ở đây chúng ta có thể nói điều gì đó giống như q = s và chúng ta sẽ chỉ nhận được kết quả có chữ "s" trong đó, nhưng chúng ta thậm chí có thể đi xa hơn thế. Vì vậy chúng ta hãy làm "split", vì vậy "Sp", và chúng ta sẽ tìm thấy những bộ phim có "Sp" trong đó, đó chỉ là Split, và không có đạo diễn nào có "Sp" trong tên của họ trong danh sách mà tôi có trong cơ sở dữ liệu. Bây giờ chúng ta biết rằng tìm kiếm của chúng tôi đang hoạt động thích hợp, có nghĩa là chúng ta có thể kết nối kết nối dễ dàng với URL tìm kiếm và xây dựng mã JavaScript của chúng tôi để làm cho công việc này trở nên dễ dàng và dễ dàng hơn, khi chúng ta làm xong tất cả những gì chúng tôi phải làm. Bây giờ là để thêm các tùy chọn của chúng tôi vào cách mà chúng tôi gọi là dễ dàng tự động hoàn thành khi chúng ta thiết lập trang.

Chúng ta thêm file search.js để viết tất cả các code js.

# app/assets/javascripts/search.js

document.addEventListener("turbolinks:load", function() {
  $input = $("[data-behavior='autocomplete']")

  var options = {
  }

  $input.easyAutocomplete(options)
});

Chúng tôi muốn lấy phần tử đó trên trang

# app/views/layouts/_navbar.html.erb

<form class="form-inline my-2 my-lg-0">
    <input class="form-control mr-sm-2" type="text" placeholder="Search", data-behavior="autocomplete">
</form>

Chúng ta sẽ đi qua các lựa chọn, vì vậy chúng ta sẽ thiết lập các lựa chọn ở đây, và điều này sẽ khá dài bởi vì chúng ta phải xác định tất cả những thứ muốn thực hiện. Chúng ta đang trỏ form này vào đường dẫn tìm kiếm và có nghĩa là action search sẽ respone lại với HTML và không chỉ JSON. Điều khác là chúng ta muốn là local = true vì theo mặc định chúng cũng gửi như AJAX.

Chỉnh sửa lại MainController như sau:

# app/controllers/main_controller.rb

def search 
    @movies = Movie.ransack(params[:a]).result(distinct: true)
    @directors = Director.ransack(params[:a]).result(distinct: true) 

    respond_to do |format| 
        format.html {}
        format.json do
            @movies = @movies.limit(5)
            @directors = @directors.limit(5)
        end
    end
end 

Chúng ta có thể xóa đi method force_json vì nó không còn thực sự cần thiêt nữa.

Để đảm bảo sự tách biệt hơn giữa response html (để truy vấn) và json (để trả về dữ liệu JSON) bạn có thể tạo thêm ra một phương thức là autocomplete trong MainController - điều này thực sự là một việc nên làm. Ví dụ: nếu tìm kiếm của bạn kết thúc tìm kiếm nhiều model hơn là bạn thực sự autocomplete, sau đó bạn có thể thêm vào một số truy vấn khác ở đây hoặc thay đổi cách làm việc cho tìm kiếm HTML, trong khi autocomplete của bạn sẽ được rất tốt được nâng cấp và duy trì độc lập. Đây là điều bạn cần phải thay đổi và dĩ nhiên bạn phải thay đổi routes một chút. Hoặc cũng có thể chia ra các đinhj dạng khác nhau như trên.

Ví dụ: facebook, tính năng autocomplete sẽ tìm kiếm những điều phổ biến như people hoặc group, nhưng nếu bạn thực sự nhấn tìm kiếm và đi đến kết quả thực sự có thể liệt kê các doanh nghiệp hoặc các sự kiện hoặc địa điểm. Họ có tính năng autocomplete mà họ thực sự hoạt động tách biệt với tìm kiếm thực và vì vậy bạn có thể thực hiện chính xác ở đây nhưng vì chúng tôi chỉ có hai model, tôi sẽ giữ chúng lại với nhau:

# app/assets/javascripts/search.js

document.addEventListener("turbolinks:load", function() {
  $input = $("[data-behavior='autocomplete']")

var options = {
        getValue: "name",
        url: function(phrase) {
          return "/search.json?q=" + phrase;
        },
        categories: [
          {
                listLocation: "movies",
                header: "<strong>Movies</strong>",
          },
          {
                listLocation: "directors",
                header: "<strong>Directors</strong>",
          }
        ],
        list: {
            onChooseEvent: function() {
            var url = $input.getSelectedItemData().url
            $input.val("")
            Turbolinks.visit(url)
          }
        }
    }

  $input.easyAutocomplete(options)
});

Ok, chúng ta hãy đi qua các tùy chọn trên, ta sẽ chạy qua khá nhanh (bạn có thể xem tài liệu về các tùy chọn này)

  1. GetValue: được gán cho name của đối tượng trong JSON của chúng ta

  2. Url: là một function, nó sẽ cho chúng ta cụm từ nhập vào, và phải trả về một chuỗi cho /search

  3. categories: một mảng các categories. Mỗi loại sẽ có một vị trí. Bạn cũng có thể thêm tùy chọn tiêu đề, vì vậy bạn có thể có một tiêu đề

  4. List: xử lý các sự kiện click, bằng cách đi qua một danh sách tùy chọn

Cuối cùng nhưng không kém phần quan trọng là nếu bạn nhập một từ và bạn không click vào kết quả, và gửi form tìm kiếm, bạn sẽ được đưa đến url tìm kiếm giống nhau sẽ có truy vấn trong đó, giống như Url autocomplete định dạng JSON, điều này sẽ có thể tạo kết quả dạng HTML để cung cấp cho người dùng tìm kiếm của họ. Đó là cách bạn thực hiện autocomplete và search form. Điều này cho phép bạn có cả chức năng của cả hai nếu quan tâm, bạn luôn có thể chia hai bước này cho hành động autocomplete và hành động search. Xử lý những khác nhau một chút trong từng trường hợp.

0