Full text search with Thinking Sphinx
Sphinx เป็น full-text search engine ที่รันแยกจาก database — ทำ index จากข้อมูลใน table แล้วตอบ search query ได้เร็วกว่า LIKE '%foo%' ของ SQL มาก Thinking Sphinx เป็น Rails wrapper ที่ทำให้ใช้ Sphinx ผ่าน ActiveRecord ได้สะดวก
ในหัวข้อนี้เราจะมาลองใช้ Sphinx เป็น full text search บน Rails กัน
Prerequisites
- Ruby 3.1.2
- Rails 7.0.4
- Sphinx 3.3.1
- Thinking Sphinx 5.4.0
- MySQL (ใช้เป็น datastore)
Getting Started
ทำการติดตั้ง Sphinx ทำตามขั้นตอนในนี้ได้เลย
สร้าง Rails app โดยใช้ MySQL เป็น database:
1
rails new shopshop --database=mysql
จากนั้นเพิ่มบรรทัดนี้ลงใน Gemfile:
1
gem "thinking-sphinx"
แล้วเรียกคำสั่ง bundle install
ที่นี้เราจะสร้าง model ที่ต้องการจะใช้ค้นหา ในที่นี้ก็จะใช้เป็น product:
1
2
3
rails g scaffold Product name detail:text
rails db:migrate
rails s
เพิ่ม search ใน routes:
1
2
3
4
5
6
7
Rails.application.routes.draw do
resources :products do
collection do
get "search"
end
end
end
เพิ่ม search ใน products controller:
1
2
3
4
5
6
7
8
class ProductsController < ApplicationController
...
def search
@products = Product.search(params[:q])
render :index
end
...
end
Product.search(query) คือ method ที่ Thinking Sphinx เพิ่มให้ — ส่ง query ไปยัง Sphinx แล้วคืน ActiveRecord collection ที่ match ตาม relevance ranking
เพิ่มฟอร์มสำหรับ search ในหน้า index ของ products:
1
2
3
4
<%= form_tag search_products_path, method: :get do %>
<%= text_field_tag :q %>
<%= submit_tag :search %>
<% end %>
กำหนด index
จากนั้นเราจะสร้างไฟล์เพื่อกำหนดว่าจะทำ index กับข้อมูลอะไรบ้างใน model นั้น โดยรูปแบบจะเป็นแบบนี้ app/indices/[modelname]_index.rb:
สร้างไฟล์ index ของ product ใน app/indices/product_index.rb และกำหนดตัวแปรที่จะทำ indexing:
1
2
3
4
ThinkingSphinx::Index.define :product, with: :real_time do
indexes name, sortable: true
indexes detail
end
with: :real_time บอก Sphinx ให้ใช้ real-time index — Sphinx เก็บ index ใน memory และอัปเดตทันทีเมื่อ record เปลี่ยน (insert/update/delete) ทางเลือกคือ SQL-backed index (with: :active_record หรือ default) ที่ต้อง rebuild ทุกครั้งเมื่อข้อมูลเปลี่ยน — เร็วกว่าตอน search แต่ข้อมูลใหม่ไม่ปรากฏจนกว่าจะ rebuild
sortable: true ที่ name ทำให้ใช้ order(name: :asc) กับ result ได้ (Sphinx ต้องเก็บ string เป็น attribute เพิ่มเพื่อ sort)
เริ่มใช้ Sphinx
เรียกใช้คำสั่งเพื่อทำ index แล้วเริ่มใช้ Sphinx:
1
2
3
4
5
6
rails ts:configure
rails ts:stop
rails ts:index
rails ts:start
# or
rails ts:rebuild
แต่ละ task ทำอะไร:
ts:configure— generateconfig/<env>.sphinx.confจาก index file ในapp/indices/ts:stop— หยุด Sphinx daemon (ถ้ารันอยู่)ts:index— สั่ง Sphinx build index file จาก databasets:start— start Sphinx daemon ขึ้นมารอรับ queryts:rebuild— รวมทั้ง 4 ขั้นข้างบนในคำสั่งเดียว สะดวกตอน dev
Auto-index เมื่อ record เปลี่ยน
ถ้าไม่อยากทำ indexing ด้วยมือทุกครั้งที่มีข้อมูลเพิ่ม ให้เราเพิ่มส่วนนี้ลงไปเพื่อจะได้ทำ indexing แบบ real time:
1
2
3
class Product < ApplicationRecord
ThinkingSphinx::Callbacks.append(self, behaviours: [:real_time])
end
Callbacks.append ผูก ActiveRecord callback (after_save, after_destroy) ให้อัปเดต real-time index ของ Sphinx ทันทีที่ record เปลี่ยน ไม่ต้อง rebuild manual
ค้นหาภาษาไทย
ปกติ Sphinx tokenize string โดยตัดที่ space ซึ่งใช้ไม่ได้กับภาษาไทย — ขั้นต่ำที่สุดต้องเพิ่ม Unicode range ของอักษรไทย (U+0E00..U+0E7F) ใน charset_table ให้ Sphinx รู้จัก:
1
2
development:
charset_table: "0..9, A..Z->a..z, _, a..z, U+E00..U+E7F"
การค้นหาภาษาไทยสำหรับ Sphinx จะค้นหาได้ไม่สมบูรณ์นัก เพราะไม่มีฟีเจอร์การตัดคำ ซึ่งภาษาไทยไม่ได้เว้นวรรคแบบภาษาอังกฤษ ซึ่งผมก็ได้สร้าง gem
thbrkสำหรับใช้ตัดคำเพื่อให้ค้นหาด้วย Sphinx ได้ เข้าไปดูแล้วทำตามได้เลย
ทางเลือกอื่นๆ
โพสต์นี้เขียนสมัย 2022 ปัจจุบัน Sphinx ยังใช้ได้ดีในงาน legacy แต่มีทางเลือกที่ active กว่า:
- Searchkick + Elasticsearch/OpenSearch — modern stack ที่ scale ได้ดี มี ecosystem ใหญ่ ดูตัวอย่างที่ OpenSearch setup with Docker Compose and Kamal
- pg_search — full-text search ใน PostgreSQL ไม่ต้องลง service แยก เหมาะกับ app เล็กถึงกลางที่ใช้ Postgres อยู่แล้ว
- Meilisearch / Typesense — search engine รุ่นใหม่ ติดตั้งง่าย มี typo-tolerance built-in
Sphinx ยังเหมาะถ้า — มี legacy code base ที่ใช้ Sphinx อยู่แล้ว, ต้องการ performance สูงและคุม config ระดับลึก, หรือไม่อยากเพิ่ม dependency เป็น JVM (Elasticsearch/OpenSearch)