26
loading...
This website collects cookies to deliver better user experience
class Property < ApplicationRecord
include PropertyElastic
has_many :rooms, dependent: :destroy
has_many :booking_rooms, through: :rooms
has_and_belongs_to_many :property_options
end
class Room < ApplicationRecord
belongs_to :property
has_and_belongs_to_many :room_options
has_many :booking_rooms, dependent: :destroy
end
class BookingRoom < ApplicationRecord
belongs_to :room
has_many :booking_room_availabilities, dependent: :destroy
enum status: %i[published draft]
end
class BookingRoomAvailability < ApplicationRecord
belongs_to :booking_room
enum status: %i[available closed booked]
validates_uniqueness_of :booking_room, scope: %i[day]
end
class CreateBookingRoomAvailabilities < ActiveRecord::Migration[5.2]
def change
create_table :booking_room_availabilities do |t|
t.references :booking_room
t.date :day
t.integer :price
t.integer :status
t.timestamps
end
end
end
gem 'elasticsearch-model', '7.1.1'
gem 'elasticsearch-rails', '7.1.1'
require 'elasticsearch/model'
module PropertyElastic
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
settings index: { number_of_shards: 1 } do
mappings dynamic: 'false' do
indexes :id
indexes :search_body, analyzer: 'snowball'
indexes :property_options
indexes :rooms, type: 'nested', properties: {
'name' => {'type' => 'text'},
'id' => {'type' => 'long'},
'room_options' => {'type' => 'keyword'},
'guests' => { 'type' => 'long'},
'availability' => { 'type' => 'nested', properties: {
'day' => { 'type' => 'date' },
'id' => { 'type' => 'long' },
'price' => {'type' => 'long'},
'status' => { 'type' => 'keyword' },
'booking_room_id' => { 'type' => 'long'}
} }
}
end
end
end
def as_indexed_json(_options = {})
as_json(
only: %i[id updated_at]
).merge({
property_options: property_options.pluck(:id),
search_body: title,
rooms: availabilities,
})
end
def availabilities
if booking_rooms.any?
booking_rooms.map do |br|
{
name: br.title,
id: br.id,
room_options: br.room.room_options.pluck(:id).uniq,
guests: br.guests,
availability: br.booking_room_availabilities.where("day >= ? AND day < ?", Date.today, Date.today + 6.months).map do |s|
{day: s.day.to_s, price: s.price, status: s.status, room_id: br.id, id: s.id}
end
}
end
else
rooms.map do |r|
{
name: r.title,
id: r.id,
room_options: r.room_options.pluck(:id).uniq,
}
end
end
end
end
class BookingSearch
def self.perform(params, page = 1)
@page = page || 1
@per_page = params[:per_page]
@query = params[:q]&.strip
@params = params
search
end
def self.search
salt = @params[:salt] || "salt"
terms = {
sort: {
_script: {
script: "(doc['_id'] + '#{salt}').hashCode()",
type: "number",
order: "asc"
}
},
query: {
bool: {
must: search_terms,
filter: {
bool: {
must: generate_term + generate_nested_term
}
}
}
},
size: @per_page,
from: (@page.to_i - 1) * @per_page
}
Property.__elasticsearch__.search(terms)
end
def self.generate_nested_term
terms_all = []
terms = []
if @params[:room_options]
@params[:room_options].split(",")&.map(&:to_i)&.each do |ro|
terms.push({terms: { "rooms.room_options" => [ro] }})
end
end
terms.push({range: { "rooms.guests" => {'gte' => @params[:guests].to_i}}})
n = {nested: {
path: "rooms",
query: {
bool: {
must: terms
}
},
inner_hits: { name: 'room' }
}}
terms_all.push(n)
Date.parse(@params[:from]).upto(Date.parse(@params[:to]) - 1.day) do |d|
terms = []
terms.push(match: { "rooms.availability.status" => 'available' })
terms.push(match: { "rooms.availability.day" => d.to_s })
n = {nested: {
path: "rooms.availability",
query: {
bool: {
must: terms
}
},
inner_hits: { name: d.to_s }
}
}
terms_all.push(n)
end
terms_all
end
def self.generate_term
terms = []
if @params[:property_options].present?
@params[:property_options].split(',').each do |lo|
terms.push(term: { property_options: lo })
end
end
terms
end
def self.search_terms
match = [@query.blank? ? { match_all: {} } : { multi_match: { query: @query, fields: %w[search_body], operator: 'and' } }]
match.push( { ids: { values: @params[:ids] } }) if @params[:ids]
match
end
end
class DataDemo
PROPERTIES = ['White', 'Blue', 'Yellow', 'Red', 'Green']
PROPERTY_OPTIONS = ['WiFi', 'Parking', 'Swimming Pool', 'Playground']
ROOM_OPTIONS = ['Kitchen', 'Kettle', 'Work table', 'TV']
ROOM_TYPES = ['Standard', 'Comfort']
def self.search
params = {}
from_search = Date.today + rand(20).days
params[:from] = from_search.to_s
params[:to] = (from_search + 3.days).to_s
params[:per_page] = 10
property_options = PROPERTY_OPTIONS.sample(1).map{|po| PropertyOption.find_by title: "po}"
room_options = ROOM_OPTIONS.sample(1).map{|ro| RoomOption.find_by title: "ro}"
params[:property_options] = property_options.map(&:id).join(',')
params[:room_options] = room_options.map(&:id).join(',')
params[:guests] = 2
res = BookingSearch.perform(params)
puts "Search for dates: #{params[:from]}..#{params[:to]}"
puts "Property options: #{property_options.map(&:title).to_sentence}"
puts "Room options: #{room_options.map(&:title).to_sentence}"
res.response.hits.hits.each do |hit|
puts "Property: #{hit._source.search_body}"
available_rooms = {}
# dive into inner hits to get detailed search data
# here we transform search result into more structured way
hit.inner_hits.each do |key, inner_hit|
if key != 'room'
inner_hit.hits.hits.each do |v|
available_rooms[v._source.room_id.to_s] ||= []
available_rooms[v._source.room_id.to_s] << { day: v._source.day, price: v._source.price }
end
else
puts "Rooms: #{inner_hit.hits.hits.count}"
end
end
# printing results
available_rooms.each do |key, ar|
booking_room = BookingRoom.find key
puts "Room: #{booking_room.room.title} / #{booking_room.title}"
total_price = 0
ar.each do |day|
puts "#{day[:day]}: $#{day[:price]}/night"
total_price += day[:price]
end
puts "Total price for #{ar.count} #{'night'.pluralize(ar.count)}: $#{total_price}"
puts "----------------------------\n"
end
end
res.response.hits["total"].value
end
def self.delete
Property.destroy_all
PropertyOption.destroy_all
RoomOption.destroy_all
end
def self.run
delete
PROPERTY_OPTIONS.each { |po| PropertyOption.create(title: po) }
ROOM_OPTIONS.each { |ro| RoomOption.create(title: ro) }
5.times do |i|
p = Property.create(title: PROPERTIES[i])
rooms = rand(2) + 1
p.property_options = PROPERTY_OPTIONS.sample(2).map{ |po| PropertyOption.find_by title: po }
rooms.times do |j|
room = p.rooms.create({title: "#{ROOM_TYPES[rand(2)]} #{j}" })
room.room_options = ROOM_OPTIONS.sample(2).map{ |po| RoomOption.find_by title: po }
(rand(2) + 1).times do |k|
booking_room = room.booking_rooms.create title: "Room #{k+1}", status: :published, guests: rand(4) + 1
30.times do |d|
booking_room.booking_room_availabilities.create day: Date.today + d.days, status: :available, price: [100, 200, 300][rand(3)]
end
end
end
end
Property.__elasticsearch__.delete_index!
Property.__elasticsearch__.create_index!
Property.__elasticsearch__.import
end
end
DataDemo.run
DataDemo.search