Full-Text Search trong Ruby on Rails sử dụng Elasticsearch

Ngày nay, rất nhiều trang web cung cấp cho người dùng tính năng tìm kiếm, khi người dùng nhập từ khoá, hệ thống sẽ đưa ra các gợi ý hay là các kết quả với từ khoá mà người dùng nhập vào sẽ được highlight lên trông rất trực quan. Nếu người dùng nhập sai chính tả thì việc tự động giúp người dùng sửa lại các từ đó sẽ là một tính năng tuyệt vời, như ta thường thấy trên Google hay Facebook, ...

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

Một trong những tính năng tuyệt vời nhất của Elasticsearch là đưa ra các chức năng của nó bằng cách sử dụng REST API, do đó có các thư viện đóng gói các chức năng đó cho hầu hết các ngôn ngữ lập trình. Trong bài viết này, mình sẽ hướng dẫn các bạn cách cài đặt Full-Text Search trong Ruby on Rails sử dụng Elasticsearch.

Giới thiệu về Elasticsearch

Như trên đó, mình đã đề cập rằng Elasticsearch giống như một cơ sở dữ liệu dành cho việc tìm kiếm. Việc tìm hiểu sẽ dễ dàng hơn nếu bạn quen với một số thuật ngữ như:

  • Field: Một field (trường) giống như một cặp key - value (từ khoá - giá trị). Giá trị có thể là một giá trị đơn giản (chuỗi, số nguyên, ngày tháng) hoặc một cấu trúc phức tạp như một mảng hoặc một đối tượng. Một trường tương tự như một cột trong một bảng của một cơ sở dữ liệu quan hệ.
  • Document: Một document (tài liệu) là một danh sách các trường. Tài liệu được lưu trữ trong Elasticsearch có dạng JSON. Nó giống như một hàng trong một bảng của một cơ sở dữ liệu quan hệ. Mỗi tài liệu được lưu trữ trong một chỉ mục và có một loại và một id duy nhất.
  • Type: Một type (kiểu) giống như một bảng trong cơ sở dữ liệu quan hệ. Mỗi loại có một danh sách các lĩnh vực có thể được chỉ định cho các tài liệu của loại đó.
  • Index: Một index (chỉ mục) tương đương với cơ sở dữ liệu quan hệ. Nó chứa định nghĩa của nhiều loại và lưu trữ nhiều tài liệu.

Trong Elasticsearch, khi bạn viết một tài liệu trong một chỉ mục, các trường của tài liệu sẽ được phân tích từng chữ một để giúp cho việc tìm kiếm trở nên dễ dàng và nhanh chóng hơn. Elasticsearch cũng hỗ trợ geolocation (vị trí địa lý), do đó bạn có thể tìm kiếm các tài liệu nằm trong một khoảng cách nhất định so với một vị trí nào đó.

Elasticsearch đã được xây dựng với khả năng mở rộng cao, vì vậy rất dễ dàng để xây dựng cluster với nhiều máy chủ và có tính sẵn sàng cao ngay cả khi một số máy chủ bị hỏng.

Cài đặt Elasticsearch

Trong bài viết này, mình sẽ hướng dẫn các bạn cài đặt Elasticsearch trên Ubuntu 16.04 nhé.

Trước tiên, bạn chạy các lệnh sau để cài đặt Elasticsearch về máy:

sudo apt-get update

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.1.deb

sudo dpkg -i elasticsearch-6.2.1.deb

(6.2.1 là phiên bản mới nhất tại thời điểm bài viết này được publish).

Sau đó chạy lệnh sudo systemctl start elasticsearch.service để khởi động Elasticsearch. Nếu bạn muốn Elasticsearch tự động khởi chạy khi hệ thống khởi động thì chạy lệnh sudo systemctl enable elasticsearch.service nhé.

Lưu ý là để chạy được Elasticsearch thì bạn phải cài đặt java cho hệ thống trước.

Tạo mới một ứng dụng Rails

Trong bài viết này, mình sẽ tạo một ứng dụng Rails thật đơn giản để làm ví dụ. Ứng dụng của mình sẽ có 1 bảng posts lưu trữ các bài đăng và mình sẽ sử dụng Elasticsearch để thực hiện việc tìm kiếm các bài đăng có trong cơ sở dữ liệu.

Trước tiên, chúng ta sẽ tạo mới 1 ứng dụng Rails:

rails new demo-elasticsearch

Sau đó vào thư mục project mới được tạo để tạo model với migration:

cd demo-elasticsearch

rails g model Post title:string content:text

rails db:create db:migrate

Để có dữ liệu cho việc tìm kiếm, mình sẽ cập nhật file db/seed.rb với nội dung như sau và chạy rails db:seed:

Post.create title: 'Elixir (programming language)', content: 'Elixir is a functional, concurrent, general-purpose programming language that runs on the Erlang virtual machine (BEAM). Elixir builds on top of Erlang to provide distributed, fault-tolerant, soft real-time, non-stop applications but also extends it to support metaprogramming with macros and polymorphism via protocols.'
Post.create title: 'Elasticsearch', content: 'Elasticsearch is a search server based on Lucene. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents. Elasticsearch is developed in Java and is released as open source under the terms of the Apache License. Elasticsearch is the most popular enterprise search engine followed by Apache Solr, also based on Lucene. Shay Banon created Compass in 2004.[2] While thinking about the third version of Compass he realized that it would be necessary to rewrite big parts of Compass to "create a scalable search solution".[2] So he created "a solution built from the ground up to be distributed" and used a common interface, JSON over HTTP, suitable for programming languages other than Java as well.[2] Shay Banon released the first version of Elasticsearch in February 2010. Elasticsearch BV was founded in 2012 to provide commercial solutions around Elasticsearch and related software.[4] In June 2014, the company announced raising $70 million in a Series C funding round, just 18 months after forming the company. The round was led by New Enterprise Associates (NEA). Additional funders include Benchmark Capital and Index Ventures. This round brings total funding to $104M.'
Post.create title: 'Ruby (programming language)', content: 'Ruby is a dynamic, reflective, object-oriented, general-purpose programming language. It was designed and developed in the mid-1990s by Yukihiro "Matz" Matsumoto in Japan. According to its creator, Ruby was influenced by Perl, Smalltalk, Eiffel, Ada, and Lisp.[12] It supports multiple programming paradigms, including functional, object-oriented, and imperative. It also has a dynamic type system and automatic memory management.'
Post.create title: 'Ruby on Rails', content: 'Ruby on Rails, or simply Rails, is a web application framework written in Ruby under MIT License. Rails is a model–view–controller (MVC) framework, providing default structures for a database, a web service, and web pages. It encourages and facilitates the use of web standards such as JSON or XML for data transfer, and HTML, CSS and JavaScript for display and user interfacing. In addition to MVC, Rails emphasizes the use of other well-known software engineering patterns and paradigms, including convention over configuration (CoC), don\'t repeat yourself (DRY), and the active record pattern.'
Post.create title: 'MySQL', content: 'MySQL (officially pronounced as /maɪ ˌɛskjuːˈɛl/ "My S-Q-L",[5]) is an open-source relational database management system (RDBMS);[6] in July 2013, it was the world\'s second most[a] widely used RDBMS, and the most widely used open-source client–server model RDBMS.[9] It is named after co-founder Michael Widenius\'s daughter, My.[10] The SQL abbreviation stands for Structured Query Language. The MySQL development project has made its source code available under the terms of the GNU General Public License, as well as under a variety of proprietary agreements. MySQL was owned and sponsored by a single for-profit firm, the Swedish company MySQL AB, now owned by Oracle Corporation.[11] For proprietary use, several paid editions are available, and offer additional functionality. MySQL is a popular choice of database for use in web applications, and is a central component of the widely used LAMP open-source web application software stack (and other "AMP" stacks). LAMP is an acronym for "Linux, Apache, MySQL, Perl/PHP/Python". Free-software open-source projects that require a full-featured database management system often use MySQL. Applications that use the MySQL database include: TYPO3, MODx, Joomla, WordPress, phpBB, MyBB, Drupal and other software. MySQL is also used in many high-profile, large-scale websites, including Google[12][13] (though not for searches), Facebook,[14][15][16] Twitter,[17] Flickr,[18] and YouTube. On all platforms except Windows, MySQL ships with no GUI tools to administer MySQL databases or manage data contained within the databases. Users may use the included command line tools,[20][21] or install MySQL Workbench via a separate download. Many third party GUI tools are also available.'
Post.create title: 'PostgreSQL', content: 'PostgreSQL, often simply Postgres, is an object-relational database management system (ORDBMS) with an emphasis on extensibility and standards-compliance. As a database server, its primary function is to store data securely, supporting best practices, and to allow for retrieval at the request of other software applications. It can handle workloads ranging from small single-machine applications to large Internet-facing applications with many concurrent users. PostgreSQL implements the majority of the SQL:2011 standard,[9][10] is ACID-compliant and transactional (including most DDL statements) avoiding locking issues using multiversion concurrency control (MVCC), provides immunity to dirty reads and full serializability; handles complex SQL queries using many indexing methods that are not available in other databases; has updateable views and materialized views, triggers, foreign keys; supports functions and stored procedures, and other expandability,[11] and has a large number of extensions written by third parties. In addition to the possibility of working with the major proprietary and open source databases, PostgreSQL supports migration from them, by its extensive standard SQL support and available migration tools. Proprietary extensions in databases such as Oracle can be emulated by built-in and third-party open source compatibility extensions. Recent versions also provide replication of the database itself for availability and scalability. PostgreSQL is cross-platform and runs on many operating systems including Linux, FreeBSD, OS X, Solaris, and Microsoft Windows. On OS X, PostgreSQL has been the default database starting with Mac OS X 10.7 Lion Server,[12][13][14] and PostgreSQL client tools are bundled with in the desktop edition. The vast majority of Linux distributions have it available in supplied packages. PostgreSQL is developed by the PostgreSQL Global Development Group, a diverse group of many companies and individual contributors.[15] It is free and open-source software, released under the terms of the PostgreSQL License, a permissive free-software license.'
Post.create title: 'Amazon Web Services', content: 'Amazon Web Services (AWS), is a collection of cloud computing services that make up the on-demand computing platform offered by Amazon.com. These services operate from 12 geographical regions[2] across the world. The most central and well-known of these services arguably include Amazon Elastic Compute Cloud, also known as "EC2", and Amazon Simple Storage Service, also known as "S3". Amazon markets AWS as a service to provide large computing capacity quicker and cheaper than a client company building an actual physical server farm.[3]'
Post.create title: 'Heroku', content: 'Heroku is a cloud Platform-as-a-Service (PaaS) supporting several programming languages. Heroku was acquired by Salesforce.com in 2010.[1] Heroku, one of the first cloud platforms[citation needed], has been in development since June 2007, when it supported only the Ruby programming language, but has since added support for Java, Node.js, Scala, Clojure, Python, PHP, and Go.[2] [3]'
Post.create title: 'ImageMagick', content: 'ImageMagick is a free and open-source[3] software suite for displaying, converting, and editing raster image and vector image files. It can read and write over 200 image file formats. ImageMagick is licensed under the Apache 2.0 license.'
Post.create title: 'Redis', content: 'Redis is a data structure server. It is open-source, networked, in-memory, and stores keys with optional durability. The development of Redis has been sponsored by Redis Labs since June 2015.[3] Before that, it was sponsored by Pivotal Software[4] and by VMware.[5][6] According to the monthly ranking by DB-Engines.com, Redis is the most popular key-value database.[7] Redis has also been ranked the #1 NoSQL (and #4 database) in User Satisfaction and Market Presence based on user reviews,[8] the most popular NoSQL database in containers,[9] and the #1 NoSQL among Top 50 Developer Tools & Services.[10] The name Redis means REmote DIctionary Server.[11]'

Vậy là công đoạn chuẩn bị đã xong, chúng ta đi vào chức năng chính của ứng dụng demo này nhé

Thêm tính năng tìm kiếm

Chúng ta sẽ bắt đầu bằng cách thêm vào Gemfile hai gem của Elasticsearch:

gem 'elasticsearch-model'
gem 'elasticsearch-rails'

Mình cần thêm 1 form tìm kiếm ở trên cùng của website, do đó, mình sẽ tạo 1 partial app/views/search/_form.html.erb với nội dung như sau:

<%= form_for :term, url: search_path, method: :get do |form| %>
  <p>
    <%= text_field_tag :term, params[:term] %>
    <%= submit_tag "Search", name: nil %>
  </p>
<% end %>

Và chỉnh sửa lại 1 chút trong app/views/layouts/application.html.erb để render form mình vừa tạo:

...
<body>
  <%= render "search/form" %>
  <%= yield %>
</body>
...

Bây giờ, mình sẽ tạo ra 1 controller app/controllers/search_controller.rb để thực hiện việc tìm kiếm và hiển thị kết quả:

class SearchController < ApplicationController
  def index
    @posts =
      if params[:term].nil?
        Array.new
      else
        Post.search params[:term]
      end
  end
end

Hiện tại mình chưa định nghĩa phương thức search cho model Post nên sẽ gặp lỗi nếu thực hiện việc tìm kiếm. Và mình cũng chưa thêm route cho việc tìm kiếm này, nên mình sẽ cập nhật file config/routes.rb trước như sau:

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

Thêm mới file app/views/search/index.html.erb với nội dung:

<h1>Posts</h1>

<% if @posts %>
  <ul>
    <% @posts.each do |post| %>
      <li>
        <h3>
          <%= link_to post.title, "#" %>
        </h3>
        <p><%= post.content %></p>
      </li>
    <% end %>
  </ul>
<% else %>
  <p>Your search did not match any documents.</p>
<% end %>

Theo như hướng dẫn ở elasticsearch-rails, để sử dụng được Elasticsearch cho model Post, chúng ta phải include hai modules vào model Post như sau:

require "elasticsearch/model"

class Post < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
end

Module thứ nhất cung cấp cho model của chúng ta phương thức search để thực hiện việc tìm kiếm. Module thứ 2 kết hợp với các callback của ActiveRecord để đánh index mỗi bản ghi mà chúng ta lưu vào cơ sở dữ liệu hay mỗi khi chúng ta cập nhật hoặc xoá một bản ghi.

Nếu có các bản ghi đã tồn tại trước khi cài đặt Elasticsearch, bạn chỉ cần vào rails c và chạy lệnh Post.import để đánh index cho các bản ghi đấy. Bây giờ thử tìm kiếm với 1 từ khoá nào đó, ví dụ ruby, kết quả sẽ như sau:

Highlight từ khoá tìm kiếm

Như bạn thấy ở các website lớn thì khi tìm kiếm từ khoá nào đó, ở phần kết quả tìm kiếm sẽ highlight từ khoá lên. Và với Elasticsearch, chúng ta cũng có thể làm được như vậy. Trước tiên, chúng ta cần override lại phương thức search trong model Post bằng cách sửa file app/models/post.rb như sau:

require "elasticsearch/model"

class Post < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks

  class << self
    def search query
      __elasticsearch__.search(
        {
          query: {
            multi_match: {
              query: query,
              fields: ['title', 'content']
            }
          },
          highlight: {
            pre_tags: ['<span class="highlight">'],
            post_tags: ['</span>'],
            fields: {
              title: {},
              content: {}
            }
          }
        }
      )
    end
  end
end

Mặc định, phương thức search được định nghĩa bởi gem elasticsearch-models, và __elasticsearch__ là một proxy object được cung cấp để có thể truy cập đến lớp đóng gói của Elasticsearch API. Vì vậy, chúng ta có thể thay đổi truy vấn mặc định bằng cách sử dụng các tùy chọn dạng JSON được cung cấp ở https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html.

Bây giờ, phương thức search sẽ bọc các kết quả khớp với truy vấn bằng các thẻ HTML như đã được chỉ định. Do đó, chúng ta cũng cần phải sửa lại file kết quả tìm kiếm để có thể hiển thị các thẻ HTML một cách an toàn. Để làm như vậy, hãy sửa file app/views/search/index.html.erb như sau:

<h1>Search Results</h1>

<% if @posts %>
  <ul class="search_results">
    <% @posts.each do |post| %>
      <li>
        <h3>
          <%= link_to post.try(:highlight).try(:title) ?
            post.highlight.title[0].html_safe : post.title, "#" %>
        </h3>
        <% if post.try(:highlight).try(:content) %>
          <% post.highlight.content.each do |snippet| %>
            <p><%= snippet.html_safe %>...</p>
          <% end %>
        <% end %>
      </li>
    <% end %>
  </ul>
<% else %>
  <p>Your search did not match any documents.</p>
<% end %>

Và thêm chút css cho trang này bằng cách thêm mới 1 file app/assets/stylesheets/search.scss với nội dung:

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

Thử tìm kiếm lại với code đã được cập nhật:

Kết luận

Trên đây là một số điều cơ bản về Elasticsearch mình muốn giới thiệu cho các bạn. Tuy nhiên, còn tuỳ vào nhu cầu sử dụng của bạn, hãy tìm hiểu thêm về cách truy vấn dựa theo tài liệu của Elasticsearch đã được cung cấp ở https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html.

Cảm ơn các bạn đã quan tâm đến bài viết của mình.


All Rights Reserved