Áp dụng ElasticSearch để tìm kiếm thông minh hơn trong ứng dụng Rails

Trong một ứng dụng web, di động hay bất kì ứng dụng này thì phần tìm kiếm là sẽ xuất hiện rất nhiều và là một phần không thể thiếu, nó tiết kiệm thời gian sử dụng cho người dùng cũng như làm cho ứng dụng của chính chúng ta thông minh hơn, và việc tìm kiếm càng thông minh và càng đưa ra những gợi sát nghĩa với cụm từ người dùng muốn thì sẽ giúp nâng cao trải nghiệm của người dùng lên rất nhiều. Ví dụ nếu bạn viết sai chính tả những gì bạn đang cố gắng tìm kiếm, thì việc đưa ra gợi ý sát với những gì chúng ta muốn tìm hoặc tự động sửa là những tính năng tuyệt vời, như chúng ta có thể thấy trên các trang web như Google hoặc Facebook.

Để thực hiện tất cả các tính năng này chỉ sử dụng một cơ sở dữ liệu quan hệ như MySQL hoặc Postgres thì không đơn giản. Vì lý do này, tôi đang sử dụng Elasticsearch, mà bạn có thể nghĩ đến như một cơ sở dữ liệu được xây dựng và tối ưu hóa đặc biệt cho tìm kiếm. Nó là mã nguồn mở và nó được xây dựng trên Apache Lucene.

Một trong những tính năng hay nhất của Elasticsearch là cho thấy chức năng của nó bằng cách sử dụng REST API, do đó, nó có thể sử dụng với hầy hết tất cả các ngôn ngữ lập trình và với Ruby on Rails framework này đã có sẵn gem để có thể sử dụng với Elasticsearch.

Elasticsearch là gì, vì sao nó được sử dụng để tìm kiếm full-text

Trong bài viết trước của mình ở đây mình đã tóm tắt cấu trúc, cách nó tìm kiếm, cách sử dụng của Elasticsearch với Rest cũng như điểm mạnh điểm yếu của cộng cụ này. Các bạn costheer sử dụng bài viết của mình để thao khảo hoặc vào chính trang chủ của Elasticsearch để biết thêm nhiều tính năng nâng cao hơn cho phù hợp với dự án của mình.

Áp dụng cho project

Elasticsearch sử dụng cho tìm kiếm full-text vì vậy mình sẽ bắt tay vào làm mợt dự án nhỏ về blog app và người dùng có thể tìm kiếm thông minh dựa vào nội dung văn bản.

Khỏi tạo Rails App

$ rails new blog
$ cd blog
$ bundle install
$ rails s

Tạo Articles Controller

Tạo Articles Controller bằng cách sử dụng Rails generator, thêm các routes vào config/routes.rb và thêm các phương thức để hiển thị, tạo và liệt kê danh sách các bài viết.

$ rails g controller articles

Mở config/routes.rb và thêm Articles resources vào

Blog::Application.routes.draw do
  root to: 'articles#index'
  resources :articles
end

Thêm các phương thức to create, show, and index vào Articles Controller

def index
  @articles = Article.all
end

def show
  @article = Article.find params[:id]
end

def new
end

def create
  @article = Article.new article_params
  if @article.save
    redirect_to @article
  else
    render 'new'
  end
end

private
  def article_params
    params.require(:article).permit :title, :text
  end

Article Model

Tạo một model đơn giản cho một bài viết

$ rails g model Article title:string text:text
$ rake db:migrate

Views

Tạo form để tạo bài viết mới (app/views/articles/new.html.erb)

<h1>New Article</h1>  

<%= form_for :article, url: articles_path do |f| %>

  <% if not @article.nil? and @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited
      this article from being saved:</h2>
    <ul>
    <% @article.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>

  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

<%= link_to '<- Back', articles_path %>

Xem nội dung một bài viết (app/views/articles/show.html.erb)

<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>

<p>
  <strong>Text:</strong>
  <%= @article.text %>
</p>

<%= link_to '<- Back', articles_path %>

Liệt kê danh sách các bài viết (app/views/articles/index.html.erb)

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <h3>
        <%= article.title %>
      </h3>
      <p>
        <%= article.text %>
      </p>
    </li>
  <% end %>
</ul>
<%= link_to 'New Article', new_article_path %>

Lòng vòng mãi mình mới nhớ sao không dùng

$ rails generate scaffold Article title:string text:text

luôn cho bài demo của mình, có phải đỡ mất thời gian hơn không ^^!

Sau đó các bạn hãy khởi động lại server và thêm một vài bài viết để xem thử. Phần trên đó chỉ mới là việc cái rails và nó là đơn giản. Tiếp theo chúng ta hãy cùng cài Elasticsearch vào áp dụng nó vào project nhỏ này.

Cài đặt Elasticsearch

Các bạn vào đây để cài tải bản mới nhất phù hợp với hệ điều hành mình đang sử dụng, hãy nhớ máy bạn đã cài sẵn java để Elasticsearch có thể hoạt động được.

Với ubuntu bạn hãy tải bản deb về và sử dụng lệnh dưới đây để cài

$ sudo dpkg -i elasticsearch-[version].deb

Bạn có thể config hoặc để mặc định sau đó để khỏi động service của nó hãy sử dụng lệnh

$ sudo systemctl enable elasticsearch.service
$ sudo systemctl start elasticsearch.service

Mở trình duyệt và truy cập vào địa chỉ: http://localhost:9200 nếu thành công màn hình sẽ xuất hiện nội dung tương tự như sau:

{
  "name" : "5cP3-lz",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "uWSboHpfSUiGjAjiEuzyrw",
  "version" : {
    "number" : "6.3.2",
    "build_flavor" : "default",
    "build_type" : "deb",
    "build_hash" : "053779d",
    "build_date" : "2018-07-20T05:20:23.451332Z",
    "build_snapshot" : false,
    "lucene_version" : "7.3.1",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

Cài ElasticSearch vào ứng dụng Rails

Thêm 2 gem này vào ứng gem file

gem "elasticsearch-model"
gem "elasticsearch-rails"

và sau đó nãy nhớ sử dụng bundle install và khới đọng lại rails

Search Controller

Tạo Search Controller

$ rails g controller search

Và thêm method search vào Search Controller

def search
  if params[:term].nil?
    @articles = []
  else
    @articles = Article.search params[:term]
  end
end

Tích hợp ElasticSearch vào model

Sửa lại model Article tương tự như sau

require "elasticsearch/model"

class Article < ActiveRecord::Base
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
end
Article.import # tự động đồng bộ với ElasticSearch

Search Route

Thêm đoạn code vào routes file

get "search", to: "search#search"

Search View

Tạo mợt file app/views/search/search.html.erb

<h1>Articles Search</h1>
<%= form_for search_path, method: :get do |f| %>
  <p>
    <%= f.label "Search for" %>
    <%= text_field_tag :q, params[:term] %>
    <%= submit_tag "Go", name: nil %>
  </p>
<% end %>
<ul>
  <% @articles.each do |article| %>
    <li>
      <h3>
        <%= link_to article.title, controller: "articles", action: "show",
          id: article._id%>
      </h3>
    </li>
  <% end %>
</ul>

Đến đây thành quả chúng ta đặt được có dạng như sau

Highlighting kết quả tìm kiếm

Trên nhiều trang web, bạn có thể xem trên trang kết quả tìm kiếm các cụm từ bạn đã tìm kiếm được đánh dấu bằng cách tô sáng hoặc gạch chân. Điều này là rất dễ dàng khi sử dụng Elasticsearch.

Thay đối phương thức tìm kiếm mặc định trong app/models/article.rb

def self.search(query)
  __elasticsearch__.search(
    {
      query: {
        multi_match: {
          query: query,
          fields: ['title', 'text']
        }
      },
      highlight: {
        pre_tags: ['<em>'],
        post_tags: ['</em>'],
        fields: {
          title: {},
          text: {}
        }
      }
    }
  )
end

Bây giờ phương thức tìm kiếm sẽ bao bọc các kết quả phù hợp với truy vấn nhưng nó có chứa cả các thẻ HTML được chỉ định(<em>). Vì lý do này, chúng ta cũng cần cập nhật trang kết quả tìm kiếm để có thể hiển thị thẻ HTML một cách chính xác. Để làm như vậy, hãy chỉnh sửa app/views/search/search.html.erb

<h1>Search Results</h1>
 
<% if @articles %>
  <ul class="search_results">
    <% @articles.each do |article| %>
      <li>
        <h3>
          <%= link_to article.try(:highlight).try(:title) ?
              article.highlight.title[0].html_safe : article.title,
              controller: "articles", action: "show", id: article._id %>
        </h3>
        <% if article.try(:highlight).try(:text) %>
          <% article.highlight.text.each do |snippet| %>
          <p><%= snippet.html_safe %>...</p>
        <% end %>
      <% end %>
    </li>
  <% end %>
</ul>
<% else %>
  <p>Your search did not match any documents.</p>
<% end %>

Trang trí cho kết quả tìm kiếm bằng một vài thuộc tính css. Tạo file app/assets/stylesheets/search.scss cho highlighted tag

.search_results em {
  background-color: yellow;
  font-style: normal;
  font-weight: bold;
}

Bây giờ ta sẽ được kết quả kìm kiếm là dạng như sau Chắc hẵn chúng ta vẫn chưa tháy tìm kiếm này thông minh hơn ở điểm nào chỉ có điều dễ nhận thấy là việc highlight kết quả tìm kiếm khá là dễ dàng. Bây giờ chúng ta tiếp tục để nâng cao sự thông minh của công cụ tìm kiếm.

Sử dụng gem searchkick

Đầu tiên hãy thêm gem "searchkick" vào project của chúng ta

gem "searchkick"

Searchkick là gem đi liền với Elasticsearch giúp cho việc tìm kiếm thông minh và tiện lợi cho ngời dùng hơn rất nhiều, nó có thể gợi ý cho người dùng kết quả tìm kiếm ngay cả khi người dùng có viết sai chính tả ngoài ra còn có rất nhiều ưu điểm khác. Các bạn có thể đọc tại đây.

Trong model Article ta thêm searchkick vào tương tự như sau:

class Article < ActiveRecord::Base
  searchkick
end

Chúng ta cần phải reindex các bài viết một lần nữa

rake searchkick:reindex CLASS=Article

Chúng ta sửa method search trong Search Controller thành

class SearchController < ApplicationController
  def search
    if params[:term].nil?
      @articles = []
    else
      term = params[:term]
      @articles = Article.search term, fields: [:text], highlight:  true
    end
  end
end

Tiếp đó chính ta sửa file views/search/search.html.erb thành

<h2>Search Results for: <i><%= params[:term] %></i></h2>
 
<% if @articles %>
<ul class="search_results">
  <% @articles.with_details.each do |article, details| %>
    <li>
      <h3>
        <%= link_to article.title, controller: "articles", action: "show", 
          id: article.id %>
      </h3>
      <p><%= details[:highlight][:text].html_safe %>...</p>
    </li>
  <% end %>
</ul>
<% else %>
  <p>Your search did not match any documents.</p>
<% end %>

Vậy là đã xong, hãy cùng xem kết quả

Kết luận

Bài viết này mình đã giới thiệu với các bạn cách kết hợp ElasticSearch - một công cụ tìm kiếm mạnh với framework Ruby on Rails, bài viết này chỉ đề cập một phần chức năng nhỏ của tìm kiếm với ElasticSearch và gem searchkick thực tế nó hiện đại và thông minh hơn rất nhiều nữa. Trong bài viết tiếp theo chúng ta cùng tiếp tục tìm hiểu những kỹ thuật nâng cao hơn của gem searchkick và ElasticSearch và cùng với đó mình sẽ làm thêm chức năng Autosuggest cho các bài viết.Mọng mọi người sẽ ủng hộ mình trong những bài viết tiếp theo.