12/08/2018, 14:40

Common Usage Of Searchkick In Rails

Sự tăng trưởng về kích thước cũng như độ phức tạp của dữ liệu đang tăng lên từng ngày, nhu cầu tìm kiếm thông tin của người dùng càng ngày càng khắt khe dường như làm cho search engine cơ bản của SQL đuối sức. Trong bối cảnh đó search engine mọc ra như nấm, ví như "cây nấm" nổi tiếng Elasticsearch. ...

Sự tăng trưởng về kích thước cũng như độ phức tạp của dữ liệu đang tăng lên từng ngày, nhu cầu tìm kiếm thông tin của người dùng càng ngày càng khắt khe dường như làm cho search engine cơ bản của SQL đuối sức. Trong bối cảnh đó search engine mọc ra như nấm, ví như "cây nấm" nổi tiếng Elasticsearch. Khi mà nhà nhà nói về ES (Elasticsearch), người người áp dụng ES vào các hệ thống lớn, thì Ruby developer cũng khao khát đem ES vào các dự án bất cứ khi nào có thể. Hiện nay có 2 gem phổ biến để integrate ES đó là elasticsearch-rails và searchkick nhưng nếu với nhu cầu mỳ ăn liền, nhanh gọn cũng như không cần phải tốn quá nhiều thời gian công sức học ES thì searchkick là một lựa chọn khôn ngoan và quen thuộc với Rails dev hơn. Ngoài ra giữa chúng không hề có sự khác biệt về performance !

Installation

Include gem searchkick vào trong Gemfile

gem "searchkick"

Khai báo mapping cho model muốn tích hợp ES

class Post < ApplicationRecord
   searchkick
end

Usage

Câu query cơ bản có dạng như sau:

Post.search "text"

Tất cả các trường của model Post sẽ được mapping theo default của ES: curl -XGET 'localhost:9200/_all/_mapping/post?pretty'

{
  "posts_development_20170202170757313" : {
    "mappings" : {
      "post" : {
        "dynamic_templates" : [ ],
        "properties" : {
          "author_id" : {
            "type" : "long"
          },
          "content" : {
            "type" : "keyword",
            "fields" : {
              "analyzed" : {
                "type" : "text"
              }
            },
            "include_in_all" : true,
            "ignore_above" : 256
          },
          "created_at" : {
            "type" : "date"
          },
          "id" : {
            "type" : "long"
          },
          "updated_at" : {
            "type" : "date"
          }
        }
      },...

Tuy nhiên trên đây chỉ là trường hợp đơn giản nhất khi model không có mối quan hệ nào, trong thực tế khi muốn search một bản ghi nào đó lại thường dựa rất nhiều vào thông tin cha con của nó. Giả sử có 3 models Comment > Post > Author có quan hệ 1-n với nhau và người dùng muốn search các Post dựa trên các thông tin của Author hay Comment. Khi đó ta cần phải khai báo custome mapping cho model như sau:

class Post < ApplicationRecord
  belongs_to :author
  has_many :comments
  searchkick mappings : {
    post:  {
      properties: {
        content: {type: "string", index: "not_analyzed"}
      }
    }
  }
  def search_data
    {
       content: content
    }
  end
end

Để search post dựa trên tên của lớp cha author, bắt buộc phải tạo 1 method trung gian để lấy ra tên tác giả từ model post: app/models/post.rb

class Post < ApplicationRecord
  belongs_to :author
  has_many :comments
  searchkick mappings: {
    post: {
      properties: {
        content: {"type": "string", "index": "not_analyzed"},
        author_name: {"type": "string","index": "not_analyzed"}
      }
    }
  }
  
  def search_data
    {
      content: content,
      author_name: author_name
    }
  end
  
  def author_name
    author.try :name
  end
end

Note: thay đổi data sẽ được đánh index bằng search_data method và ghi nhớ reindex lại document sau mỗi lần thay đổi mapping hoặc search_data bằng Post.reindex hoặc `rake searchkick:reindex CLASS

{
  "posts_development_20170203091125396" : {
    "mappings" : {
      "post" : {
        "properties" : {
          "author_id" : {
            "type" : "long"
          },
          "author_name" : {
            "type" : "keyword"
          },
          "content" : {
            "type" : "keyword"
          },...
        }
      }
    }
  }
}

Kiểm tra lại mapping author_name đã được include vào properties của index post như một column thông thường. Tuy nhiên một khi đã dùng custom mapping, chúng ta bắt buộc phải dùng custom search với option body, điều đó cũng có nghĩa mọi option khác nằm ngoài body đều bị ignore. Hãy cùng xem ví dụ tìm các post có tên tác giả là "Nicolette Hintz":

Post.search body: {query: {bool: {must: [{term: {author_name: "Nicolette Hintz"}}]}}}

Post Search (12.9ms)  curl http://localhost:9200/posts_development/_search?pretty -d '{"query":{"bool":{"must":[{"term":{"author_name":"Nicolette Hintz"}}]}}}'
 => <Searchkick::Results:0x00000004078f60 @klass=Post(id: integer, content: text, author_id: integer, created_at: datetime, 
 updated_at: datetime), @response={"took"=>5, "timed_out"=>false, "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0}, 
 "hits"=>{"total"=>4, "max_score"=>2.0794415, "hits"=>[{"_index"=>"posts_development_20170203095543938", "_type"=>"post",
 "_id"=>"41", "_score"=>2.0794415, "_source"=>{"content"=>"Ut inventore voluptates. A magni nesciunt ex sunt nam. 
 Consequatur accusantium molestias eaque.", "author_name"=>"Nicolette Hintz"}}, {"_index"=>"posts_development_20170203095543938",
 "_type"=>"post", "_id"=>"42", "_score"=>2.0794415, "_source"=>{"content"=>"Dolorem quis quam accusamus quae distinctio 
 velit. Soluta aut delectus ea quia itaque iusto consequatur. Quis natus quas pariatur repellendus nihil.", 
 "author_name"=>"Nicolette Hintz"}}, {"_index"=>"posts_development_20170203095543938", "_type"=>"post", "_id"=>"4", "_score"=>1.9924302, "_source"=>{"content"=>"Et labore quae quia eveniet et. Ratione qui eius dolores sequi. Excepturi aut sed sit odio doloribus alias.", "author_name"=>"Nicolette Hintz"}}, {"_index"=>"posts_development_20170203095543938", "_type"=>"post", "_id"=>"1", "_score"=>1.89712, "_source"=>{"content"=>"Consequatur laboriosam pariatur laborum voluptas ad velit. At eum accusamus quam doloribus. Inventore reprehenderit blanditiis omnis sed atque voluptatem optio.", "author_name"=>"Nicolette Hintz"}}]}}, @options={:page=>1, :per_page=>1000, :padding=>0, :load=>true, :includes=>nil, :json=>true, :match_suffix=>"analyzed", :highlighted_fields=>[]}>

Cú pháp trên đây tuân theo full Query DSL mà ES cung cấp cho phép định nghĩa câu lệnh tìm kiếm theo kiểu JSON.

Searchkich hay nói đúng hơn là ES cũng hỗ trợ search theo nested attributes hay nói cách khác search theo thuộc tính của lớp con. Trong ví dụ trên, để search Post dựa trên thuộc tính của Comment ta có thể làm như sau:

class Post < ApplicationRecord
  belongs_to :author
  has_many :comments
  searchkick mappings : {
    post:  {
      properties: {
        content: {type: "string", index: "not_analyzed"},
        comments: {
          type: "nested",
          properties: {
            content: {
              type: "string",
              index: "not_analyzed"
            }
          }
        }
      }
    }
  }
  def search_data
    {
      content: content,
      author_name: author_name,
      comments: comments
    }
  end
end

Trong mappings của Post chúng ta cần khai báo thêm kiểu "nested" cho thuộc tính comments cũng như các trường muốn search trong comments. Sau khi reindex lại trong mappings đã xuất hiện các trường của comments:

{
  "posts_development_20170206093907338" : {
    "mappings" : {
      "post" : {
        "properties" : {
          "author_name" : {
            "type" : "keyword"
          },
          "comments" : {
            "type" : "nested",
            "properties" : {
              "content" : {
                "type" : "keyword"
              }
            }
          },
          "content" : {
            "type" : "keyword"
          }
        }
      }
    }
  }
}

Câu query sẽ có dạng như sau:

Post.search body: {query: {bool: {must: [
  {term: {author_name: "Nicolette Hintz"}},
  {
    nested: {
      path: "comments", 
      query: {
        bool: {
          must: [{term: {content: "something"}}]
        }
      }
    }
  }
]}}}

Trong đó path trùng với nested attribute và field query phải nằm trong danh sách các trường đã được khai báo mapping ở trên.

Một yêu cầu thường gặp nữa đó là search đa ngôn ngữ. Để làm được điều này, cách đơn giản nhất là khai báo một trường với 2 kiểu analyzer khác nhau. Ví dụ:

{
  properties: {
    content: {
      type: "string",
      fields: {
        ja: {type: "string", analyzer: "japanese analyzer"},
        en: {type: "string", analyzer: "english analyzer"}
      }
    }
  }
}

Câu quyery sẽ như sau:

Post.search body: {query: {bool: {must: [
  {multi_match: {query: "some string", fields: ["content.en", "content.ja"], operator: "and"}}
]}}}
0