Thus far I have implemented a basic search with eleasticsearch. Insert a keyword and find the item according to the title/description.
Now I'm trying to sort the results according to distance from the users current location, but don't know how.
I have two models, the items.rb contains the item info and the account.rb has the address of the user that uploaded the item.
This is what I have done thus far:
item.rb
require 'elasticsearch/model'
class Item < ApplicationRecord
mount_uploader :image, ImageUploader
belongs_to :category
belongs_to :user
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
settings index: { number_of_shards: 1 } do
mappings dynamic: 'false' do
indexes :title, analyzer: 'english', index_options: 'offsets'
indexes :description, analyzer: 'english'
end
end
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['title^10', 'description']
}
},
highlight: {
pre_tags: ['<em>'],
post_tags: ['</em>'],
fields: {
title: {},
description: {},
}
}
}
)
end
end
# Delete the previous articles index in Elasticsearch
Item.__elasticsearch__.client.indices.delete index: Item.index_name rescue nil
# Create the new index with the new mapping
Item.__elasticsearch__.client.indices.create \
index: Item.index_name,
body: { settings: Item.settings.to_hash, mappings: Item.mappings.to_hash }
# Index all Item records from the DB to Elasticsearch
Item.import
account.rb
require 'elasticsearch/model'
class Account < ApplicationRecord
geocoded_by :full_address
after_validation :geocode, if: ->(obj){ obj.full_address.present? }
def full_address
[street, city, state, zip_code, country].join(",")
end
def location
[longitude.to_f, latitude.to_f]
end
ELASTICSEARCH_MAX_RESULTS = 25
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
include Elasticsearch::Model::Indexing
mapping do
indexes :location, type: 'geo_point'
indexes :city, type: 'string'
indexes :state, type: 'string'
end
def as_indexed_json(_options = {})
as_json(only: %w(city state))
.merge(location: {
lat: lat.to_f, lon: lon.to_f
})
end
def self.search(query = nil, options = {})
options ||= {}
# empty search not allowed, for now
return nil if query.blank? && options.blank?
# define search definition
search_definition = {
query: {
bool: {
must: []
}
}
}
unless options.blank?
search_definition[:from] = 0
search_definition[:size] = ELASTICSEARCH_MAX_RESULTS
end
# query
if query.present?
search_definition[:query][:bool][:must] << {
multi_match: {
query: query,
fields: %w(city state),
operator: 'and'
}
}
end
# geo spatial
if options[:lat].present? && options[:lon].present?
options[:distance] ||= 100
search_definition[:query][:bool][:must] << {
filtered: {
filter: {
geo_distance: {
distance: "#{options[:distance]}mi",
location: {
lat: options[:lat].to_f,
lon: options[:lon].to_f
}
}
}
}
}
end
__elasticsearch__.search(search_definition)
end
end
# Delete the previous articles index in Elasticsearch
Account.__elasticsearch__.client.indices.delete index: Account.index_name rescue nil
# Create the new index with the new mapping
Account.__elasticsearch__.client.indices.create \
index: Item.index_name,
body: { settings: Account.settings.to_hash, mappings: Account.mappings.to_hash }
# Index all Item records from the DB to Elasticsearch
Account.import
search_controller.rb
class SearchController < ApplicationController
def show
#items = Item.find(params[:id])
end
def search
if params[:q].nil?
#items = []
else
#items = Item.search params[:q]
end
end
end
search.html.erb
<%= form_for search_path, method: :get do |f| %>
<p>
<%= f.label "Search for" %>
<%= text_field_tag :q, params[:q] %>
<%= submit_tag "Nearby", name: nil %>
</p>
<% end %>
<ul>
<% #items.each do |item| %>
<li>
<h3>
<%= link_to item.try(:highlight).try(:title) ? item.highlight.title[0].html_safe : item.title,
controller: "search",
action: "show",
id: item._id%>
</h3>
<% if item.try(:highlight).try(:description) %>
<% item.highlight.description.each do |snippet| %>
<p><%= snippet.html_safe %>...</p>
<% end %>
<% end %>
</li>
<% end %>
You should index the lat/lng on each item using the referenced account, don't index another document for the account. Though ES can express some relations, remember it isn't a relational DB.
Related
I'm creating a form_for in which one of the field fetches the drop-down list from the database. I'm interpolating the data to display the string but I want to store its id back into other database which is linked with my form.
class FlightsController < ApplicationController
def new
#flight = Flight.new
#airplane = #flight.airplane
#options = Airport.list
end
def create
#flight = Flight.new(flight_params)
if #flight.save!
flash[:success] = "Flight created successfully."
redirect_to #flight
else
flash[:danger] = "Flight not created."
redirect_to :new
end
end
private
def flight_params
params.require(:flight).permit(:name, :origin, :destination, :depart, :arrive, :fare, :airplane_id)
end
end
<%= form_for(#flight) do |f| %>
...
<div class="row">
<div class="form-group col-md-6">
<%= f.label :origin %>
<%= f.select :origin, grouped_options_for_select(#options), { include_blank: "Any", class: "form-control selectpicker", data: { "live-search": true } } %>
</div>
</div>
...
<% end %>
class Airport < ApplicationRecord
def self.list
grouped_list = {}
includes(:country).order("countries.name", :name).each do |a|
grouped_list[a.country.name] ||= [["#{a.country.iso} #{a.country.name}", a.country.iso]]
grouped_list[a.country.name] << ["#{a.iata} #{a.name} (#{a.city}, #{a.country.name})", a.id]
end
grouped_list
end
end
class Flight < ApplicationRecord
belongs_to :origin, class_name: "Airport"
belongs_to :destination, class_name: "Airport"
belongs_to :airplane
has_many :bookings, dependent: :destroy
has_many :passengers, through: :bookings
end
The following error is showing,
Airport(#69813853361360) expected, got "43" which is an instance of String(#47256130076180)
The output of Airport.list when run in a console is shown below:
=> {"India"=>[["IN India", "IN"], ["AGX Agatti Airport (Agatti, India)", 3], ["IXV Along Airport (Along, India)", 5], ["AML Aranmula International Airport (Aranmula, India)", 6], ["IXB Bagdogra International Airport (Siliguri, India)", 50]]}
Parameters: {"utf8"=>"✓", "authenticity_token"=>"+Z8+rkrJkkgaTznnwyTd/QjEoq3kR4ZmoUTp+EpM+320fNFg5rJm+Izx1zBODo/H7IIm3D+yg3ysnVUPmy7ZwQ==", "flight"=>{"name"=>"Indigo", "origin"=>"49", "destination"=>"11", "depart"=>"2019-02-21T21:30", "arrive"=>"2019-02-22T01:30", "fare"=>"2500", "airplane_id"=>"3"}, "commit"=>"Create Flight"}
I tried using to_i but it didn't work.
if you're interpolating a string with space delimiter you can try this.
'1 one'.split(' ').first.to_i
grouped_options_for_select is sending a.id as string value. Convert it to integer in your create action.
def create
#flight = Flight.new(flight_params)
#flight.origin = #flight.origin.to_i ## <== add this line
if #flight.save!
...
Convert string into integer in rails:
user_id = "123"
#user_id = user_id.to_i
puts #user_id
#user_id = 123
Convert integer into string in rails:
user_id = 456
#user_id = user_id.to_s
puts #user_id
#user_id = "456"
Convert column type string into integer in rails migration :
def change
change_column :webinars, :user_id, :integer, using: 'user_id::integer'
end
Convert column type integer into string in rails migration:
def change
change_column :webinars, :user_id, :string, using: 'user_id::string'
end
Your problem is not integer versus string, your problem is (and the error is telling you) the field is expecting an Airport object, but it's getting an airport id...
<%= f.select :origin, grouped_options_for_select(#options), { include_blank: "Any", class: "form-control selectpicker", data: { "live-search": true } } %>
You're trying to select origin which is an airport object. You really are just returning the ID of an airport object (origin_id).
Change it to
<%= f.select :origin_id, grouped_options_for_select(#options), { include_blank: "Any", class: "form-control selectpicker", data: { "live-search": true } } %><%= f.select :origin, grouped_options_for_select(#options), { include_blank: "Any", class: "form-control selectpicker", data: { "live-search": true } } %>
And don't forget to update your flight_params
def flight_params
params.require(:flight).permit(:name, :origin_id, :destination, :depart, :arrive, :fare, :airplane_id)
end
I would guess you'll have a similar problem with destination
I'm going to use elastic search for my ruby on rails project. I get this error when I search some of the word that It's used in my article too much.
NoMethodError (undefined method `highlight' for #<Elasticsearch::Model::Response::Result:0x007f062ed26708>)
i got this in the log production. this is what everything that i did:
in controller:
# POST /search/article
def search
render json: Article.search(params[:query]), each_serializer: ElasticsearchResultsSerializer
end
this is my article.rb model
#default_scope { order('created_at DESC') }
scope :visible, -> { where(enabled: true) }
after_commit on: [:create] do
self.keywords = self.keywords.each {|str| str.force_encoding("UTF-8")}
__elasticsearch__.index_document if self.enabled?
end
after_commit on: [:update] do
self.keywords = self.keywords.each {|str| str.force_encoding("UTF-8")}
__elasticsearch__.update_document if self.enabled?
end
after_commit on: [:destroy] do
__elasticsearch__.delete_document
end
settings index: { number_of_shards: 1, number_of_replicas: 0 }
mappings dynamic: 'false' do
indexes :content, type: "string", index_options: 'offsets'
indexes :title, type: "string"
indexes :description, type: "string"
indexes :category, type: "string"
indexes :created_at, type: "date"
indexes :keywords, type: "string"
end
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['title^10', 'content^5', 'description^2', 'keywords', 'category']
}
},
highlight: {
pre_tags: ['<em>'],
post_tags: ['</em>'],
fields: { title: {}, content: {} }
}
}
)
end
def as_indexed_json(options={})
as_json(
only: [:content, :title, :id, :category, :keywords, :description]
)
end
and also i used serializer
class ElasticsearchResultsSerializer < ActiveModel::Serializer
attributes :_id, :highlight, :_score, :_source
def _source
#article = object._index.singularize.capitalize.constantize.find(object._id)
#serializer = "#{object._index.singularize.capitalize}Serializer".constantize
#serializer.new(#article)
end
end
Maybe is a silly observation, but did you tried to change the value
:highlight to :_highlight ?
class ElasticsearchResultsSerializer < ActiveModel::Serializer
attributes :_id, :_highlight, :_score, :_source
def _source
#article = object._index.singularize.capitalize.constantize.find(object._id)
#serializer = "#{object._index.singularize.capitalize}Serializer".constantize
#serializer.new(#article)
end
end
I would try switching fields to:
fields: {
:"*" => {}
}
Just to see if that gets rid of the error. See the following: https://github.com/elastic/elasticsearch-rails/issues/446
I want to use ElasticSearch to search with multiple parameters (name, sex, age at a time).
what I've done so far is included elastic search in my model and added a as_indexed_json method for indexing and included relationship.
require 'elasticsearch/model'
class User < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
belongs_to :product
belongs_to :item
validates :product_id, :item_id, :weight, presence: true
validates :product_id, uniqueness: {scope: [:item_id] }
def as_indexed_json(options = {})
self.as_json({
only: [:id],
include: {
product: { only: [:name, :price] },
item: { only: :name },
}
})
end
def self.search(query)
# i'm sure this method is wrong I just don't know how to call them from their respective id's
__elasticsearch__.search(
query: {
filtered: {
filter: {
bool: {
must: [
{
match: {
"product.name" => query
}
}
],
must: [
{
match: {
"item.name" => query
}
}
]
}
}
}
}
)
end
end
User.import force: true
And In controller
def index
#category = Category.find(params[:category_id])
if params[:search].present? and params[:product_name].present?
#users = User.search(params[:product_name]).records
end
if params[:search].present? and params[:product_price].present?
#users = User.search(params[:product_price]).records
end
if params[:search].present? and params[:item].present?
if #users.present?
#users.search(item: params[:item], product: params[:product_name]).records
else
#users = User.search(params[:item]).records
end
end
end
There are basically 3 inputs for searching with product name , product price and item name, This is what i'm trying to do like if in search field only product name is present then
#users = User.search(params[:product_name]).records
this will give me records but If user inputs another filter say product price or item name in another search bar then it's not working. any ideas or where I'm doing wrong :/ stucked from last 3 days
I have a model named Movie that looks like this:
class Movie < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
has_many :actors, after_add: [ lambda {|a,c| a.__elasticsearch__.index_document}],
after_remove: [ lambda {|a,c| a.__elasticsearch__.index_document}]
settings index: {number_of_shards: 1} do
mappings dynamic: 'false' do
indexes :title, analyzer: 'snowball', boost: 100
indexes :actors
end
end
def as_indexed_json(options={})
self.as_json(
include: {
actors: { only: :name}
}
)
end
end
When i do Movie.first.as_indexed_json , I get:
{"id"=>6, "title"=>"Back to the Future ",
"created_at"=>Wed, 03 Dec 2014 22:21:24 UTC +00:00,
"updated_at"=>Fri, 12 Dec 2014 23:40:03 UTC +00:00,
"actors"=>[{"name"=>"Michael J Fox"}, {"name"=>"Christopher Lloyd"},
{"name"=>"Lea Thompson"}]}
but when i do Movie.search("Christopher Lloyd").records.first i get: => nil .
What changes can i make to the index to search movies associated with the searched actor?
I used filtering query to solve this, first I created an ActiveSupport::Concern called searchable.rb, the concern looks like this:
module Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
index_name [Rails.application.engine_name, Rails.env].join('_')
settings index: { number_of_shards: 3, number_of_replicas: 0} do
mapping do
indexes :title, type: 'multi_field' do
indexes :title, analyzer: 'snowball'
indexes :tokenized, analyzer: 'simple'
end
indexes :actors, analyzer: 'keyword'
end
def as_indexed_json(options={})
hash = self.as_json()
hash['actors'] = self.actors.map(&:name)
hash
end
def self.search(query, options={})
__set_filters = lambda do |key, f|
#search_definition[:post_filter][:and] ||= []
#search_definition[:post_filter][:and] |= [f]
end
#search_definition = {
query: {},
highlight: {
pre_tags: ['<em class="label label-highlight">'],
post_tags: ['</em>'],
fields: {
title: {number_of_fragments: 0}
}
},
post_filter: {},
aggregations: {
actors: {
filter: {bool: {must: [match_all: {}]}},
aggregations: {actors: {terms: {field: 'actors'}}}
}
}
}
unless query.blank?
#search_definition[:query] = {
bool: {
should: [
{
multi_match: {
query: query,
fields: ['title^10'],
operator: 'and'
}
}
]
}
}
else
#search_definition[:query] = { match_all: {} }
#search_definition[:sort] = {created_at: 'desc'}
end
if options[:actor]
f = {term: { actors: options[:taxon]}}
end
if options[:sort]
#search_definition[:sort] = { options[:sort] => 'desc'}
#search_definition[:track_scores] = true
end
__elasticsearch__.search(#search_definition)
end
end
end
I have the above concern in the models/concerns directory.
In movies.rb I have:
class Movie < ActiveRecord::Base
include Searchable
end
In movies_controller.rb I am doing searching on the index action and the action looks like this:
def index
options = {
actor: params[:taxon],
sort: params[:sort]
}
#movies = Movie.search(params[q], options).records
end
Now when i go to http://localhost:3000/movies?q=future&actor=Christopher I get all records which have the word future on their title and has an actor with a name Christopher. You can have more than one filter as shown by the expert template of the example application templates found here .
You can try add method search to your model like this:
class Movie < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
# ...
def self.search(query, options = {})
es_options =
{
query: {
query_string: {
query: query,
default_operator: 'AND',
}
},
sort: '_score',
}.merge!(options)
__elasticsearch__.search(es_options)
end
# ...
end
Here is some examples of method search: http://www.sitepoint.com/full-text-search-rails-elasticsearch/
And now you can search in all your indexed fields.
You need to specify the fields in the the search method, like:
def self.search query
__elasticsearch__.search(
query: {
multi_match: {
query: query,
fields: %w[title actor.name]
}
}
)
end
Try this
indexes :actors do
indexes :name, type: "string"
end
I got a problem defining new dynamic ProductFilters for Spree 2.0.4
My customer has got Categories (e.g. Taxons). A Product belongs to one category and has maximum 8 Properties BUT the Properties also depend on the Category each Product is in AND the Position of the Property is also important.
My Solution was to extend the Database in an unnormalized way:
module Spree
Taxon.class_eval do
belongs_to :prop1, class_name: "Spree::Property",
foreign_key: "p1_id"
belongs_to :prop2, class_name: "Spree::Property",
foreign_key: "p2_id"
belongs_to :prop3, class_name: "Spree::Property",
foreign_key: "p3_id"
belongs_to :prop4, class_name: "Spree::Property",
foreign_key: "p4_id"
belongs_to :prop5, class_name: "Spree::Property",
foreign_key: "p5_id"
belongs_to :prop6, class_name: "Spree::Property",
foreign_key: "p6_id"
belongs_to :prop7, class_name: "Spree::Property",
foreign_key: "p7_id"
belongs_to :prop8, class_name: "Spree::Property",
foreign_key: "p8_id"
attr_accessible :prop1, :prop2, :prop3, :prop4, :prop5, :prop6, :prop7, :prop8
def properties
prop = []
prop << prop1
prop << prop2
prop << prop3
prop << prop4
prop << prop5
prop << prop6
prop << prop7
prop << prop8
return prop
end
def applicable_filters
fs = []
fs << Spree::Core::ProductFilters.price_filter if Spree::Core::ProductFilters.respond_to?(:price_filter)
#fs << Spree::Core::ProductFilters.brand_filter if Spree::Core::ProductFilters.respond_to?(:brand_filter)
fs
end
end
end
So that I'm able to get the possible properties in a taxon for the corresponding product. I now made 8 Filters for each taxon property (prop1 .. prop8) because some values are numerical and should be handled different from text values, so even that's not completely DRY I came to the following solution:
module Spree
module Core
module ProductFilters
if Spree::Property.table_exists?
Spree::Product.add_search_scope :selective_prop1_any do |*opts|
conds = opts.map {|o| ProductFilters.selective_prop1_filter[:conds][o]}.reject {|c| c.nil?}
scope = conds.shift
conds.each do |new_scope|
scope = scope.or(new_scope)
end
Spree::Product.where(scope)
end
def ProductFilters.selective_prop1_filter(taxon = nil, locale = 'en')
return if taxon.nil? #||= Spree::Taxon.find_by_permalink!("categories")
property = taxon.prop1
scope = Spree::ProductProperty.where(:property_id => property, :locale => locale).
joins(:product => :taxons).
where("#{Spree::Taxon.table_name}.id" => [taxon] + taxon.descendants).
scoped
brands = scope.pluck(:value).uniq
{
:name => property.presentation,
:scope => :selective_prop1_any,
:labels => brands.sort.map { |k| [k,k] }
}
end
end
if Spree::Property.table_exists?
Spree::Product.add_search_scope :selective_prop2_any do |*opts|
conds = opts.map {|o| ProductFilters.selective_prop2_filter[:conds][o]}.reject {|c| c.nil?}
Rails.logger.debug conds.inspect
scope = conds.shift
Rails.logger.debug scope.inspect
conds.each do |new_scope|
scope = scope.or(new_scope)
end
Rails.logger.debug scope.inspect
Spree::Product.where(scope)
end
# ... other filters cut out for brevity
if Spree::Property.table_exists?
Spree::Product.add_search_scope :selective_prop8_any do |*opts|
[..]
end
def ProductFilters.selective_prop8_filter(taxon = nil, locale = 'en')
[..]
end
end
Spree::Product.add_search_scope :price_range_any do |*opts|
conds = opts.map {|o| Spree::Core::ProductFilters.price_filter[:conds][o]}.reject {|c| c.nil?}
scope = conds.shift
conds.each do |new_scope|
scope = scope.or(new_scope)
end
Spree::Product.joins(:master => :default_price).where(scope)
end
def ProductFilters.format_price(amount)
Spree::Money.new(amount)
end
def ProductFilters.price_filter
v = Spree::Price.arel_table
conds = [ [ Spree.t(:under_price, :price => format_price(10)) , v[:amount].lteq(10)],
[ "#{format_price(10)} - #{format_price(15)}" , v[:amount].in(10..15)],
[ "#{format_price(15)} - #{format_price(18)}" , v[:amount].in(15..18)],
[ "#{format_price(18)} - #{format_price(20)}" , v[:amount].in(18..20)],
[ Spree.t(:or_over_price, :price => format_price(20)) , v[:amount].gteq(20)]]
{ :name => Spree.t(:price_range),
:scope => :price_range_any,
:conds => Hash[*conds.flatten],
:labels => conds.map {|k,v| [k,k]}
}
end
end
end
end
Due to the fact, that even the values of the Properties should be localized, a made a column locale in the ProductProperties table. My selective filters pass the locale Variable to retrieve the correct ProductProperty.
Because of the MVC restrictions not being able to pass the locale from session[:locale] and the current taxon to the model I overwrote the original display logic which is using the applicable_filters method for the taxon in the CONTROLLER(!) like so:
TaxonsController.class_eval do
def show
#taxon = Taxon.find_by_permalink(params[:id])
return unless #taxon
if #taxon
#filters = []
#filters << Spree::Core::ProductFilters.selective_prop1_filter(#taxon, locale) unless #taxon.prop1.nil?
[...]
#filters << Spree::Core::ProductFilters.selective_prop8_filter(#taxon, locale) unless #taxon.prop8.nil?
#filters.concat(#taxon.applicable_filters)
else
#filters = Spree::Core::ProductFilters.all_taxons
end
p = params.merge(:taxon => #taxon.id)
#searcher = Spree::Config.searcher_class.new(params)
#searcher.current_user = try_spree_current_user
#searcher.current_currency = current_currency
#products = #searcher.retrieve_products
end
end
And here's the mainly left original view code:
<% unless #filters.empty? %>
<%= form_tag '', :method => :get, :id => 'sidebar_products_search' do %>
<% params[:search] ||= {} %>
<%= hidden_field_tag 'per_page', params[:per_page] %>
<% #filters.each do |filter| %>
<% labels = filter[:labels] || filter[:conds].map {|m,c| [m,m]} %>
<% next if labels.empty? %>
<div class="" data-hook="navigation">
<h5 class="filter-title"> <%= filter[:name] %> </h5>
<% labels.each do |nm,val| %>
<% label = "#{filter[:name]}_#{nm}".gsub(/\s+/,'_') %>
<label for="<%= label %>" class="checkbox" style="display:block;"><%= nm %><input type="checkbox"
id="<%= label %>"
name="search[<%= filter[:scope].to_s %>][]"
value="<%= val %>"
<%= params[:search][filter[:scope]] && params[:search][filter[:scope]].include?(val.to_s) ? "checked" : "" %> />
</label>
<% end %>
</div>
<% end %>
<%= submit_tag Spree.t(:search), :name => nil, :class => 'button' %>
<% end %>
<% end %>
The display works like it should: Depending on the taxon the user is in, the values from ProductProperties in the session[:locale] are fetched and displayed correctly, but now there comes the Problem:
The Search doesn't work work anymore. Even the not modified :price_range_any filter does not work. All Products belonging to the current taxon are always displayed. The search hash in params hash is built correct by the form search => {"price_range_any":["10.00 € EUR - 15.00 € EUR"]}
I've no idea. If I switch back to the original files by removing the overwrites everything is working.
How can I get my filters to run correctly?