Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เพิ่มประสิทธิภาพ Rails Apps: เชี่ยวชาญ Full-Text Search ด้วย Elasticsearch

Elasticsearch เป็น Search Engine ยอดนิยมมาก — ใช้ใน Production ของ Netflix, Medium, GitHub และยักษ์ใหญ่หลายแห่ง

Elasticsearch ทรงพลัง — Use Cases หลัก: Full-Text Search, Real-time Log Analysis, Security Analysis

น่าเสียดายที่ Rails Community ให้ความสนใจ Elasticsearch น้อย — บทความนี้เป้าหมาย 2 จุด: 1) Introduce Elasticsearch Concepts 2) แสดงการใช้งานกับ Ruby on Rails

Source Code โปรเจกต์ตัวอย่าง: GitHub Repo — Commit History ตรงกับลำดับ Sections

Introduction: Elasticsearch คืออะไร?

มุมมองกว้าง: Elasticsearch คือ Search Engine ที่:

  • สร้างบน Apache Lucene
  • จัดเก็บและ Index JSON Documents ได้อย่างมีประสิทธิภาพ
  • Open Source
  • มี REST APIs สำหรับ Interaction ทุกประเภท
  • Default: ไม่มี Security (Anyone can query via Public Endpoints)
  • Scale Horizontally ได้ดีมาก

Core Concepts

  • Index ≈ Table ใน Relational DB — ที่เก็บ Documents
  • Document ≈ Row — ชุด Fields (JSON)
  • Mapping ≈ Schema Definition — กำหนด Fields และ Types (Explicit หรือ Guess ได้ — แนะนำ Define ล่วงหน้า)

ติดตั้ง Elasticsearch

macOS (Homebrew)

brew tap elastic/tap
brew install elastic/tap/elasticsearch-full

Docker (Cross-platform)

docker run \
  -p 127.0.0.1:9200:9200 \
  -p 127.0.0.1:9300:9300 \
  -e "discovery.type=single-node" \
  docker.elastic.co/elasticsearch/elasticsearch:7.16.2

Options อื่นๆ: Official Reference

Default Port: 9200 — ทดสอบ: curl https://localhost:9200

APIs

REST APIs สำหรับทุก Task — ตัวอย่างสร้าง Document:

curl -X POST https://localhost:9200/my-index/_doc \
  -H 'Content-Type: application/json' \
  -d '{"title": "Banana Cake"}'
  • my-index = Index Name (Auto-create หากไม่มี)
  • _doc = System Route (ขึ้นต้น _)

Ways to Interact

  1. curl + jq (CLI)
  2. Browser + JSON Pretty Print Extension
  3. Kibana Dev Tools Console (แนะนำ)
  4. Chrome Extensions

บทความนี้ใช้ Gem — ไม่ Interaction APIs โดยตรง

เริ่ม Rails App ใหม่

Song Lyrics App — Public Dataset 26K+ เพลง (Title, Artist, Genre, Lyrics) — Full-text Search ด้วย Elasticsearch

rails new songs_api --api -d postgresql
  • --api = API-only (Limit Middlewares)
  • -d postgresql = Database

Scaffold:

bin/rails generate scaffold Song title:string artist:string genre:string lyrics:text

Migrate & Start:

bin/rails db:create db:migrate
bin/rails server

Test: curl https://localhost:3000/songs → Empty Array (ยังไม่มี Data)

นำ Elasticsearch เข้า Rails

Gem: elasticsearch-model (Official, Integrates กับ Rails Models)

# Gemfile
gem 'elasticsearch-model'

Default Connect: localhost:9200 — เปลี่ยนได้:

Song.__elasticsearch__.client = Elasticsearch::Client.new host: 'myserver.com', port: 9876

ทำ Model Indexable

  1. Define Mapping (Data Structure)
  2. Construct Search Request

Gem ทำได้ทั้งคู่ — สร้าง Concern: app/models/concerns/searchable.rb

# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern
  included do
    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks
    mapping do
      # mapping definition
    end
    def self.search(query)
      # build and run search
    end
  end
end

Key Points:

  • Elasticsearch::Model = ES Functionality
  • Elasticsearch::Model::Callbacks = Auto-update ES เมื่อ Record Update
  • mapping block = Index Mapping
  • search method = Custom Search Query (Gem มี search ง่ายๆ แต่เราจะใช้ Query DSL)

Include ใน Model:

# app/models/song.rb
class Song < ApplicationRecord
  include Searchable
end

Mappings (Schema Definition)

# app/models/concerns/searchable.rb
mapping do
  indexes :artist, type: :text
  indexes :title, type: :text
  indexes :lyrics, type: :text
  indexes :genre, type: :keyword
end
  • text = Indexed สำหรับ Full-Text Search (artist, title, lyrics)
  • keyword = Exact Value Filtering (genre)

สร้าง Index ใน ES:

Song.__elasticsearch__.create_index!

__elasticsearch__ = Gateway ไป ES (Methods มากมาย)

Import ข้อมูล (Dataset)

Download CSV (Creative Commons Attribution 4.0) — Import ลง PostgreSQL + ES:

require 'csv'
class Song < ApplicationRecord
  include Searchable
  def self.import_csv!
    filepath = "/path/to/your/file/tcc_ceds_music.csv"
    res = CSV.parse(File.read(filepath), headers: true)
    res.each_with_index do |s, ind|
      Song.create!(
        artist: s["artist_name"],
        title: s["track_name"],
        genre: s["genre"],
        lyrics: s["lyrics"]
      )
    end
  end
end

Rails Console: Song.import_csv! (ใช้เวลาสักพัก) — Bulk Import เร็วกว่า แต่แบบนี้ Ensure ทั้ง PG และ ES

Search ข้อมูล

Gem เพิ่ม search method — ใช้ใน Concern:

def self.search(query)
  self.__elasticsearch__.search(query)
end

Console: res = Song.search('genesis') — ดู res.response["hits"]["hits"]

Controller Index:

def index
  query = params["query"] || ""
  res = Song.search(query)
  render json: res.response["hits"]["hits"]
end

Test: curl https://localhost:3000/songs?query=genesis — Response มี _source (Data), _score (Relevance)

Query DSL (Complex Queries)

Search Field เฉพาะ (Artist)

def self.search(query)
  params = {
    query: {
      match: { artist: query }
    }
  }
  self.__elasticsearch__.search(params)
end

Query "genesis" → ได้เพลงของ Band "Genesis" เท่านั้น (ไม่รวม Title/Lyrics)

Multi-Match (หลาย Fields)

def self.search(query)
  params = {
    query: {
      multi_match: {
        query: query,
        fields: [ :title, :artist, :lyrics ]
      }
    }
  }
  self.__elasticsearch__.search(params)
end

Filtering (Genre)

def self.search(query, genre = nil)
  params = {
    query: {
      bool: {
        must: [
          { multi_match: { query: query, fields: [ :title, :artist, :lyrics ] } }
        ],
        filter: [
          { term: { genre: genre } }
        ]
      }
    }
  }
  self.__elasticsearch__.search(params)
end
  • bool = Combine Queries
  • must = Contributes to Score (Full-text Search)
  • filter = ไม่ Contributes Score, แค่ Filter (Exact Match = SQL WHERE)
  • term = Exact Value Filter (Genre)

Scoring (Relevance)

Results เรียงตาม _score — คะแนนสูง = Relevant มาก

ปัญหา: Search "genesis" → เพลง Grimes ขึ้นก่อน Band Genesis — Fix ด้วย Boost

def self.search(query)
  params = {
    query: {
      bool: {
        should: [
          { match: { title: query }},
          { match: { artist: { query: query, boost: 5 } }},
          { match: { lyrics: query }},
        ]
      }
    }
  }
  self.__elasticsearch__.search(params)
end
  • should = OR Logic (อย่างน้อย 1 ตรง)
  • boost: 5 = คูณ Score 5 เท่า หาก Match Artist Field

ทดสอบอีกครั้ง → Genesis Band ขึ้นก่อน! 🎉

Highlighting

Highlight Matched Phrases ใน Document — ES ใส่ <em> Tag ให้อัตโนมัติ

def self.search(query)
  params = {
    query: { ... },  # same as above
    highlight: {
      fields: { title: {}, artist: {}, lyrics: {} }
    }
  }
  self.__elasticsearch__.search(params)
end

Response จะมี Field highlight — Matched Phrases Wrapped in <em>

Fuzziness (Typo Tolerance)

พิมพ์ "benesis" แทน "genesis" → ไม่เจอ — เปิด Fuzzy Search:

# เปลี่ยน artist match ให้เพิ่ม fuzziness
{ match: { artist: { query: query, boost: 5, fuzziness: "AUTO" } } }

fuzziness: "AUTO" = ES ปรับระยะห่าง (Edit Distance) เองตามความยาว Term — Config ได้เพิ่มเติมใน Docs

Where to Next?

Elasticsearch ทรงพลัง — ควรใช้เมื่อต้องการ Non-trivial Search — Resources เพิ่มเติม:

Resources

  • Official Elasticsearch Reference
  • Ruby Gem (elasticsearch-ruby)
  • Rails Gem (elasticsearch-rails)
  • หนังสือ: "Elasticsearch: The Definitive Guide" (Practical Knowledge)
  • Building Autocomplete

Alternative Gems

  • Searchkick (Simple, Convention over Configuration)
  • Chewy (Advanced, DSL-heavy)

ลอง Elasticsearch กับ Rails วันนี้ — คุณจะไม่ผิดหวัง!