Post

Full text search with Thinking Sphinx

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 — generate config/<env>.sphinx.conf จาก index file ใน app/indices/
  • ts:stop — หยุด Sphinx daemon (ถ้ารันอยู่)
  • ts:index — สั่ง Sphinx build index file จาก database
  • ts:start — start Sphinx daemon ขึ้นมารอรับ query
  • ts: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)

References

This post is licensed under CC BY 4.0 by the author.