In today's digital world, we've all experienced the magic of modern search:
This instant, intelligent search experience isn't magic - it's powered by sophisticated search technology. In this guide, we'll explore how to build such powerful search capabilities using Elasticsearch, one of the most popular search engines in the world.
Think of Elasticsearch as a highly efficient digital library system:
# Gemfile
gem 'elasticsearch-model'
gem 'elasticsearch-rails'
# config/initializers/elasticsearch.rb
Elasticsearch::Model.client = Elasticsearch::Client.new(
url: ENV['ELASTICSEARCH_URL'],
transport_options: {
request: { timeout: 5 }
}
)
# app/models/product.rb
class Product < ApplicationRecord
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
# Define the index settings and mappings
settings index: { number_of_shards: 1 } do
mappings dynamic: 'false' do
indexes :name, type: 'text', analyzer: 'english'
indexes :description, type: 'text', analyzer: 'english'
indexes :category, type: 'keyword'
indexes :price, type: 'float'
indexes :created_at, type: 'date'
end
end
# Define the search method
def self.search(query)
__elasticsearch__.search(
query: {
multi_match: {
query: query,
fields: ['name^3', 'description']
}
}
)
end
end
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
def self.search_with_filters(query: nil, filters: {}, page: 1, per_page: 20)
search_definition = {
query: {
bool: {
must: [],
filter: []
}
},
sort: [
{ _score: 'desc' },
{ created_at: 'desc' }
],
from: (page.to_i - 1) * per_page.to_i,
size: per_page.to_i
}
# Add query if present
if query.present?
search_definition[:query][:bool][:must] << {
multi_match: {
query: query,
fields: ['name^3', 'description'],
fuzziness: 'AUTO'
}
}
end
# Add filters
filters.each do |key, value|
next if value.blank?
search_definition[:query][:bool][:filter] << {
term: { key => value }
}
end
__elasticsearch__.search(search_definition)
end
end
end
# app/models/product.rb
def self.search_with_aggregations(query)
search_definition = {
query: {
multi_match: {
query: query,
fields: ['name^3', 'description']
}
},
aggregations: {
categories: {
terms: { field: 'category' }
},
price_ranges: {
range: {
field: 'price',
ranges: [
{ to: 50 },
{ from: 50, to: 200 },
{ from: 200 }
]
}
},
avg_price: {
avg: { field: 'price' }
}
}
}
__elasticsearch__.search(search_definition)
end
# lib/tasks/elasticsearch.rake
namespace :elasticsearch do
desc 'Reindex all Elasticsearch models'
task reindex: :environment do
[Product, Article, User].each do |model|
puts "Reindexing #{model.name}..."
model.__elasticsearch__.create_index! force: true
model.import
puts "Finished reindexing #{model.name}"
end
end
desc 'Monitor Elasticsearch cluster health'
task health: :environment do
health = Elasticsearch::Model.client.cluster.health
puts "Cluster Health:"
puts "Status: #{health['status']}"
puts "Nodes: #{health['number_of_nodes']}"
puts "Active Shards: #{health['active_shards']}"
end
end
# app/models/product.rb
def self.search_with_caching(query, expires_in: 1.hour)
cache_key = "search:#{query}:#{cache_version}"
Rails.cache.fetch(cache_key, expires_in: expires_in) do
search(query).to_a
end
end
def self.cache_version
maximum(:updated_at).to_i
end
# Bulk indexing for better performance
def self.bulk_index(records)
records.each_slice(1000) do |batch|
bulk_body = batch.map do |record|
{ index: { _id: record.id, data: record.as_indexed_json } }
end
__elasticsearch__.client.bulk(
index: index_name,
body: bulk_body
)
end
end
# app/models/product.rb
def self.autocomplete(query)
search_definition = {
suggest: {
text: query,
completion: {
field: 'name.suggest',
fuzzy: {
fuzziness: 'AUTO'
}
}
}
}
__elasticsearch__.search(search_definition)
end
# app/models/product.rb
def self.faceted_search(query, facets = {})
search_definition = {
query: { multi_match: { query: query, fields: ['name^3', 'description'] } },
aggregations: {
categories: { terms: { field: 'category' } },
brands: { terms: { field: 'brand' } },
price_ranges: {
range: {
field: 'price',
ranges: [
{ to: 50 },
{ from: 50, to: 200 },
{ from: 200 }
]
}
}
}
}
__elasticsearch__.search(search_definition)
end
# app/models/store.rb
class Store < ApplicationRecord
include Elasticsearch::Model
mappings do
indexes :name, type: 'text'
indexes :location, type: 'geo_point'
end
def self.near(lat, lon, distance = '10km')
search_definition = {
query: {
bool: {
must: { match_all: {} },
filter: {
geo_distance: {
distance: distance,
location: { lat: lat, lon: lon }
}
}
}
},
sort: [{
_geo_distance: {
location: { lat: lat, lon: lon },
order: 'asc',
unit: 'km'
}
}]
}
__elasticsearch__.search(search_definition)
end
end
A well-implemented search system can transform your application from good to great. When done right, it:
Remember: Just like a good librarian helps visitors find exactly what they need, a well-designed search system helps users discover the right content quickly and effortlessly.
Join our community of developers and get weekly insights, tutorials, and best practices delivered straight to your inbox
Master the principles of API design and domain architecture to create scalable, maintainable applications using the digital city metaphor.