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
curl+jq(CLI)- Browser + JSON Pretty Print Extension
- Kibana Dev Tools Console (แนะนำ)
- 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
- Define Mapping (Data Structure)
- 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 FunctionalityElasticsearch::Model::Callbacks= Auto-update ES เมื่อ Record Updatemappingblock = Index Mappingsearchmethod = 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 Queriesmust= 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 วันนี้ — คุณจะไม่ผิดหวัง!