Sử dụng gem pg_search để xây dựng chức năng tìm kiếm trong PostgreSQL
Tìm kiếm là một trong những chức năng phổ biến nhất mà bất kỳ trang web nào cũng được tích hợp. Có rất nhiều giải pháp đã được đưa ra để giải bài toán tìm kiếm trong ứng dụng của bạn. Sau thời gian dài làm việc với MySQL, gần đây mình làm quen với PostgreSQL để đổi gió và mình thấy có gem ...
Tìm kiếm là một trong những chức năng phổ biến nhất mà bất kỳ trang web nào cũng được tích hợp. Có rất nhiều giải pháp đã được đưa ra để giải bài toán tìm kiếm trong ứng dụng của bạn.
Sau thời gian dài làm việc với MySQL, gần đây mình làm quen với PostgreSQL để đổi gió và mình thấy có gem pg_search có khả năng hỗ trợ tìm kiếm rất hay trên PostgreSQL.
Trong bài viết này mình sẽ xây dựng một ứng dụng đơn giản và sử dụng gem pg_search để làm chức năng tìm kiếm cho ứng dụng đó.
Khởi tạo
Môi trường làm việc :
- Rails 5.0.1
- PostgreSQL (nếu chưa cài đặt thì bạn có thể xem hướng dẫn)
Hãy cùng thực hiện nhanh các bước chuẩn bị này vì nó rất quen thuộc.
Trước tiên, tạo một ứng dụng Rails mới sử dụng tìm kiếm trong Postgres :
rails new autocomplete --database=postgresql
Chuẩn bị cấu hình trong file config/database.yml để kết nối tới DB.
development: adapter: postgresql database: autocomplete host: localhost user: username password: password
Tạo một bảng đơn giản để tiến hành việc tìm kiếm với 2 trường name, surname
rails g model User name:string surname:string rails db:migrate
Tạo một vài dữ liệu mẫu cho bảng users, tốt nhất là nên tạo ra các use có trường name là phân biệt. Bạn có thể dùng gem Faker tạo ra file seed đơn giản như sau :
50.times do User.create({name: Faker::Name.first_name, surname: Faker::Name.last_name}) end
Cuối cùng, tạo controller, view và trỏ router đến controller
- users_controller.rb
class UsersController < ApplicationController def index @users = User.all end end
- views/users/index.html.erb
<ul> <% @users.each do |user| %> <li><%= user.name %> <%= user.surname %></li> </ul>
- config/routes.rb
resources :users, only: [:index] root to: "users#index"
Như vậy là chúng ta đã chuẩn bị xong môi trường. Hãy tiến hành thêm chức năng tìm kiếm vào nhé.
Tìm kiếm users
Trước tiên, hãy thêm một box search vào view như sau :
<%= form_tag users_path, method: :get do %> <%= text_field_tag "term", params[:term], placeholder: "Enter user's name or surname" %> <%= submit_tag "Search!"" %> <% end
Bây giờ là lúc cần cài đặt gem pg_search cho ứng dụng.
Gem pg_search hỗ trợ chế độ pg_search_scope (tương tự như scope trong ActiveRecord) với cú pháp pg_search_scope SCOPE_NAME, against: OPTIONS trong đó :
- OPTIONS là danh sách các trường mà chúng ta cần tìm kiếm
Áp dụng vào model User cho việc tìm kiếm với cả 2 trường name và surname như sau :
# ... include PgSearch pg_search_scope :search_by_full_name, against: [:name, :surname]
Sửa lại action index trong controller để sử dụng scope vừa khai báo như sau :
def index if params[:term] @users = User.search_by_full_name(params[:term]) else @users = User.all end end
Đến đây thì bạn hãy chạy server lên để xem kết quả đầu tiên nhé.
Tìm kiếm với options nâng cao
Chức năng tìm kiếm của chúng ta đã làm việc, tuy nhiên hiện tại, nó chỉ đơn thuần là so sánh = giữa text mình truyền vào box search với trường name hoặc surname của các bản ghi trong BD.
Hãy nâng cấp khả năng tìm kiếm cho ứng dụng bằng việc sử dụng các kỹ thuật tìm kiếm mà pg_search cung cấp :
- tsearch - full text search, được tích hợp vào PostgreSQL
- trigram - yêu cầu phải có phần mở rộng trigram
- dmetaphone - yêu cầu phái có phần mở rộng fuzzystrmatch
Sẽ được khai báo trong param using của scope tìm kiếm đã được định nghĩa ở trên. Trong bài viết này mình chỉ trình bày về tsearch, hai kỹ thuật còn lại bạn có thể tham khảo thêm tại đây
PostgreSQL tích hợp full text search với các hỗ trợ : trọng số, tìm kiếm tiền tố,...
- Trọng số
Với các trường cho phép tìm kiếm thì sẽ được gán các trọng số A > B > C > D, ưu tiên theo thứ tự trên. Trọng số có thể được khai báo trong key against dưới dạng Hash, Array như sau :
class Article < ActiveRecord::Base include PgSearch pg_search_scope :search_full_text1, against: { title: "A", subtitle: "B", content: "C" } pg_search_scope :search_full_text2, against: [ [:title, "A"], [:subtitle, "B"], [:content, "C"] ] pg_search_scope :search_full_text3, against: [ [:title, "A"], {subtitle: "B"}, :content ] end
Như ở trên thì thứ tự ưu tiên khi tìm kiếm sẽ là title > subtitle > content
- :prefix
Chỉ hỗ trợ cho PostgreSQL 8.4 trở lên.
Mặc định thì full text search sẽ tìm kiếm trong toàn bộ từ, nếu bạn chỉ muốn tìm kiếm một phần thì bạn sẽ thêm key prefix: true vào trong tsearch như sau :
class Superhero < ActiveRecord::Base include PgSearch pg_search_scope :whose_name_starts_with, against: :name, using { tsearch: { prefix: true } } end
Khi đó ta sẽ tìm kiếm được như sau :
batman = Superhero.create name: "Batman" batgirl = Superhero.create name: "Batgirl" robin = Superhero.create name: "Robin" Superhero.whose_name_starts_with("Bat") # => [batman, batgirl]
- :negation
Mặc định thì full text search sẽ tìm kiếm với tất cả các cụm từ đầu vào, nếu muốn tìm kiếm loại trừ một số cụm từ thì bạn có thể khai báo negation: true bên trong tsearch. Khi tìm kiếm thì đầu vào của bạn chỉ cần thêm ký tự ! trước cụm từ bạn muốn loại bỏ khi tìm kiếm :
class Animal < ActiveRecord::Base include PgSearch pg_search_scope :with_name_matching, against: :name, using: { tsearch: { negation: true } } end one_fish = Animal.create(:name => "one fish") two_fish = Animal.create(:name => "two fish") red_fish = Animal.create(:name => "red fish") blue_fish = Animal.create(:name => "blue fish") Animal.with_name_matching("fish !red !blue") # => [one_fish, two_fish]
Ngoài ra còn có một số option khác cũng khá hay nữa như dictionary, normalization, highlight... mà việc kết hợp sử dụng linh hoạt giữa chúng sẽ giúp ta xây dựng một ứng dụng tìm kiếm rất hiệu quả.
Tham khảo
- https://github.com/Casecommons/pg_search
- https://www.sitepoint.com/search-autocomplete-rails-apps/
Hi vọng bài viết sẽ giúp bạn có thêm lựa chọn để xây xựng tính năng tìm kiếm cho ứng dụng của mình.
Cám ơn bạn đã theo dõi bài viết
tribeo.