Im started to use Elasticsearh in my project, and have problem with result ordering.
In fact I need to sort my records by hstore record in connected (belongs_to) model.
More details:
So, I have a Model that I want to be searchable. This model have connections with another models, here the code:
class PropertyObject < ActiveRecord::Base
belongs_to :country, :counter_cache => true
belongs_to :region, :counter_cache => true
belongs_to :city, :counter_cache => true
belongs_to :property_object_type, :counter_cache => true
belongs_to :property_object_state, :counter_cache => true
has_one :property_object_parameter_pack, dependent: :destroy
has_one :property_object_feature_pack, dependent: :destroy
has_one :property_object_description, dependent: :destroy
has_one :property_object_seo_field, dependent: :destroy
end
I want to include to my search results next fields:
Model PropertyObject:
:code :string
Model Country
:title_translations :hstore
Model Region
:title_translations :hstore
Model City
:title_translations :hstore
Model PropertyObjectDescription
:title_translations :hstore
:main_text_translations :hstore
Model PropertyObjectParameterPack
:price :hstore (example: {min => 10, max=>100})
To make this work I had create concern Searchable and add it to my model PropertyObject.
Here the code of it:
module Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
mapping do
indexes :property_object_parameter_pack, type: 'nested' do
indexes :price do
indexes :min, type: :integer
end
end
end
# Customize the JSON serialization for Elasticsearch
def as_indexed_json(options={})
self.as_json(
include: {
country: {only: :title_translations},
region: {only: :title_translations},
city: {only: :title_translations},
property_object_description: {only: [:title_translations, :main_text_translations]},
property_object_parameter_pack: {only: [:price, :area, :rooms]}
})
end
end
end
Controller part where search is calling
def search
pagen = params[:page] || 1
#property_objects = PropertyObject.search(params[:q]).page(pagen).records
end
So now searching working and all seems good. But I need sort results of search by min price.
I had try order method that works in my another orders - but no luck.
As I understand I need to use Elasticsearch sorting , to get result already sorted - but spend a lot of hours trying to implement this and fail.
What you can suggest me?
UPDATE
Had try this code:
pagen = params[:page] || 1
query = params[:q]
params[:order] ||= 'asc'
property_objects = PropertyObject.search(query) do |s|
s.query do |q|
q.string query
end
s.sort { by :property_object_parameter_pack.price.min, params[:sort]}
end
#property_objects = property_objects.page(pagen).records
With different variants
s.sort by
by :price
by :price.min
by :price[:min]
by :property_object_parameter_pack.price.min
by :property_object_parameter_pack.price[:min]
and no luck with ordering.
In the end I decide to understand how Elasticsearch works, and start to read Elasticsearch: The Definitive Guide - where the answer was founded.
First off all I recommend to read this guide and install Marvel
. After this all becomes much more clearer then using CURL. In fact I discover my index structure with Marvel, and just implement it to search query of elasticsearch-rails gem.
Next thing that I did - I had rebuild price from hstore to separate integer columns : like price_min and price_max.
So in short the answer code is:
sq = {
"query": {
"multi_match": {
"query": "Prague",
"fields": [ "country.title_translations.en",
"region.title_translations.en",
"city.title_translations.en",
"property_object_description.main_text_translations.en",
"property_object_description.title_translations.en"
]
}
},
"track_scores": true,
"sort": {
"property_object_parameter_pack.price_min":
{ "order": "desc",
"missing" : "_last"
}
}} PropertyObject.search (sq)
In fact Im sure that it will work with hstore. Because I store translations in hstore and it indexing fine - just need to point right field (in this task Marvel helps with autocomplete).
Did you try this?
def search
options = { :page => (params[:page] || 1) }
#property_objects = PropertyObject.search, options do |f|
f.query { string params[:q] }
f.sort { by :price, 'desc' }
end
#property_objects.records
end
Related
I've a model that has a nested model of skills. Its a common has_many example. Elastic search is indexing the skills as an array of strings.
My question is, I am attempting to match on those skills by way of two different inputs.
Required skills and bonus skills.
So if I have two query terms one for required and one for bonus, I want to query the skills attribute with required input, if none found, query with the bonus input.
I'm using elasticsearch-rails gem. Didn't think I needed to post any code as this is more theory.
UPDATE
class Profile
has_many :skills
...
end
class Skill
belongs_to :profile
end
Mappings
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
...
mapping dynamic: 'false' do
indexes :skills, analyzer: 'keyword'
end
...
end
Overriden as_json
def as_indexed_json(options={})
hash = self.as_json(
include: {location: { methods: [:coordinates], only: [:coordinates] },
locations_of_interest: { methods: [:coordinates], only: [:coordinates]}
})
hash['skills'] = self.skills.map(&:name)
hash['interests'] = self.interests.map(&:name)
hash
end
I guess in essence i'm looking to perform the reverse of a multi_match on multiple fields and boosting one but instead searching one field with multiple inputs (required and bonus) and depending no the results of required search with bonus input. Does this makes things more clear?
This is my query so far, first attempt.
if options[:required_skills].present? && options[:bonus_skills].present?
bool do
must do
term skills: options[:required_skills]
end
should do
term skills: options[:bonus_skills]
end
end
end
class SkillContainer < ActiveRecord::Base
has_many :skill_links, dependent: :destroy, inverse_of: :skill_container
has_many :skills, through: :skill_links
has_many
end
##################################
create_table :skill_link do |t|
t.references :skill
t.references :skill_container
t.boolean :required
t.boolean :bonus
end
##################################
class SkillLink
belongs_to :skill_container
belongs_to :skill
scope :required, -> {
where(required: true)
}
scope :bonus, -> {
where(bonus: true)
}
end
class Skill < ActiveRecord::Base
has_many :skill_links, dependent: :destroy, inverse_of: :skill
has_many :skills, through: :skill_links
end
#required skills from any skill container
SkillContainer.last.skills.merge(SkillLink.required)
#bonus skills from any skill container
SkillContainer.last.skills.merge(SkillLink.bonus)
scopes can be combined with your elastic search
I'm trying to index a model when I have a has_many, :through association, but no results are being displayed.
class Business < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
def self.search(params)
tire.search(load: true) do
query { string params[:q]} if params[:q].present?
end
end
mapping do
indexes :service_name
indexes :service_description
indexes :latitude
indexes :longitude
indexes :services do
indexes :service
indexes :description
end
end
def to_indexed_json #returns json data that should index (the model that should be searched)
to_json(methods: [:service_name, :service_description], include: { services: [:service, :description]})
end
def service_name
services.map(&:service)
end
def service_description
services.map(&:description)
end
has_many :professionals
has_many :services, :through => :professionals
end
Then this is Service model
class Service < ActiveRecord::Base
attr_accessible :service, :user_id, :description
belongs_to :professional
belongs_to :servicable, polymorphic: true
end
I've also reindex using this:
rake environment tire:import CLASS=Business FORCE=true
I can search for the items in Business, but when I tried to search something in Service, I get an empty result.
After struggling with mapping, I created a gem to make search a bit easier. https://github.com/ankane/searchkick
You can use the search_data method to accomplish this:
class Business < ActiveRecord::Base
searchkick
def search_data
{
service_name: services.map(&:name),
service_description: services.map(&:description)
}
end
end
I do not believe there is a way to do mapping on associations with Tire. What you will want to do instead is define easily searchable fields with the :as method and a proc. This way you can also get rid of the to_indexed_json method (you will actually need to)
mapping do
indexes :service_name
indexes :service_description
indexes :latitude
indexes :longitude
indexes :service_name, type: 'string', :as => proc{service_name}
indexes :service_description, type: 'string', :as => proc{service_description}
end
Tire can associate with associations, I've used it to index on has_many association but have not tried has_many, :through yet. Try index on object?
mapping do
indexes :service_name
indexes :service_description
indexes :latitude
indexes :longitude
indexes :services, type: 'object',
properties: {
service: {type: 'string'}
description: {type: 'string'}
}
end
Also, it might be good to have a touch method :
class Service < ActiveRecord::Base
attr_accessible :service, :user_id, :description
belongs_to :professional, touch: true
belongs_to :servicable, polymorphic: true
end
and after_touch callback to update the index.
I've spent several hours trying to solve the problem. I hope someone can help me.
First, here's some code to illustrate the structure:
class Company < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
attr_accessible :name, :website, :addresses_attributes
has_many :addresses, dependent: :destroy
accepts_nested_attributes_for :addresses, allow_destroy: true
after_touch() { tire.update_index }
include_root_in_json = false
mapping do
indexes :name, boost: 10
indexes :addresses do
indexes :street
end
end
def self.search(params)
s = tire.search(load: true) do
query do
boolean do
must { string params[:query] } if params[:query]
must { term "addresses.street", params[:location] } if params[:location]
end
end
s.results
end
def to_indexed_json
to_json( include: { addresses: { only: [:street] } } )
end
end
class Address < ActiveRecord::Base
# Attributes
attr_accessible :street
# Associations
belongs_to :company, touch: true
end
When I'm trying to search for company with particular street name (calling Company.search({location: 'example_street_name', query: 'example_name'}) I get no results.
Indexing looks fine for me. That's one of the requests:
curl -X POST "http://localhost:9200/companies/company/1351" -d '{
"created_at": "2012-12-29T13:41:17Z",
"name": "test name",
"updated_at": "2012-12-29T13:41:17Z",
"website": "text.website.com",
"addresses": [
{
"street": "test street name"
}
]
}'
I've tried many things to get the results. Nothing worked. It seems I must have missed something basic.
You're using a term query for the "addresses.street" field, which is not analyzed, so you must pass in lowercased etc. values.
Try using the match query.
For Tire (ElasticSearch wrapper gem), how do you query and filter out indexed records that has nil/null value for a certain attribute. For example, if I have this relationship
class Article < ActiveRecord::Base
belongs_to :topic
end
I have article indexed but I want to avoid pulling back records with topic_id = nil. I tried this code blow but it didn't work.
class Article
belongs_to :topic
def search(q)
tire.search do
...
filter :missing, :field => :topic_id, { :existence => false, :null_value => false }
...
### filter :range, :topic_id => { :gt => 0 } # ==> I tried this as well but didn't work
...
end
end
end
I think you should use exists filter if you want to filter only documents with existing topic_id value.
class Article
belongs_to :topic
def search(q)
tire.search do
...
filter :exists, :field => :topic_id
...
end
end
end
Or you can use query string with _exists_:topic_id query.
Please try:
class Article
belongs_to :topic
def search(q)
tire.search do
...
filter :not, :missing => {:field => :topic_id, { :null_value => true } }
...
end
end
end
This should work. existence is required if you want to check if the field exists or not. I guess you just need the null_value check.
#articles = Article.where('topic_id is not ?' , nil)
I've not seen this feature as a plug in and was wondering how to achieve it, hopefully using rails.
The feature I'm after is the ability to rate one object (a film) by various attributes such as plot, entertainment, originality etc etc on one page/form.
Can anyone help?
I don't think you need a plugin to do just that... you could do the following with AR
class Film < ActiveRecord::Base
has_many :ratings, :as => :rateable
def rating_for(field)
ratings.find_by_name(field).value
end
def rating_for=(field, value)
rating = nil
begin
rating = ratigns.find_by_name(field)
if rating.nil?
rating = Rating.create!(:rateable => self, :name => field, :value => value)
else
rating.update_attributes!(:value => value)
end
rescue ActiveRecord::RecordInvalid
self.errors.add_to_base(rating.errors.full_messages.join("\n"))
end
end
end
class Rating < ActiveRecord::Base
# Has the following field:
# column :rateable_id, Integer
# column :name, String
# column :value, Integer
belongs_to :rateable, :polymorphic => true
validates_inclusion_of :value, :in => (1..5)
validates_uniqueness_of :name, :scope => :rateable_id
end
Of course, with this approach you would have a replication in the Rating name, something that is not that bad (normalize tables just for one field doesn't cut it).
You can also use a plugin ajaxfull-rating
Here's another, possibly more robust rating plugin...it's been around for a while and has been revised to work with Rails 2
http://agilewebdevelopment.com/plugins/acts_as_rateable