ElasticSearch & Tire. Search for an object with nested attributes - ruby-on-rails

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.

Related

How to remove N+1 queries

I have a rails API that currently has quite a few N+1 queries that I'd like to reduce.
As you can see it's going through quite a few loops before returning the data.
The relationships are as follows:
Company Model
class Company < ApplicationRecord
has_many :jobs, dependent: :destroy
has_many :contacts, dependent: :destroy
has_many :listings
end
Job Model
class Job < ApplicationRecord
belongs_to :company
has_many :listings
has_and_belongs_to_many :technologies
has_and_belongs_to_many :tools
scope :category, -> ( category ) { where category: category }
end
Listing Modal
class Listing < ApplicationRecord
belongs_to :job, dependent: :destroy
belongs_to :company, dependent: :destroy
scope :is_active, -> ( active ) { where is_active: active }
end
Job Serializer
class SimpleJobSerializer < ActiveModel::Serializer
attributes :id,
:title,
:company_name,
attribute :technology_list, if: :technologies_exist
attribute :tool_list, if: :tools_exist
def technology_list
custom_technologies = []
object.technologies.each do |technology|
custom_technology = { label: technology.label, icon: technology.icon }
custom_technologies.push(custom_technology)
end
return custom_technologies
end
def tool_list
custom_tools = []
object.tools.each do |tool|
custom_tool = { label: tool.label, icon: tool.icon }
custom_tools.push(custom_tool)
end
return custom_tools
end
def tools_exist
return object.tools.any?
end
def technologies_exist
return object.technologies.any?
end
def company_name
object.company.name
end
end
Current query in controller
Job.eager_load(:listings).order("listings.live_date DESC").where(category: "developer", listings: { is_active: true }).first(90)
I've tried to use eager_load to join the listings to the Jobs to make the request more efficient but i'm unsure how to handle this when some of the n+1 queries are coming from inside the serializer as it tries to look at tools and technologies.
Any help would be much appreciated!
You might was well eager load tools and technologies since you know that the serializer is going to use them:
Job.eager_load(:listings, :tools, :technologies)
.order("listings.live_date DESC")
.where(category: "developer", listings: { is_active: true })
.first(90)
After that you really need to refactor that serializer. #each should only be used when you are only interested in the side effects of the iteration and not the return value. Use #map, #each_with_object, #inject etc. These calls can be optimized. return is implicit in ruby so you only explicitly return if you are bailing early.
class SimpleJobSerializer < ActiveModel::Serializer
# ...
def tool_list
object.tools.map { |t| { label: tool.label, icon: tool.icon } }
end
# ...
end
Try nested preload:
Job.preload(:technologies, :tools, company: :listings).order(...).where(...)

Need help constructing a query

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

Ordering elasticsearch results, elasticsearch-rails gem, Rails

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

Serialize a summary of a has_many relationship

How can I include a summary of the associated objects rather than the objects itself. For example, if a client has_many projects I could do this:
class ClientSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :projects
end
But this will return all of the associated projects. I would much rather bring back just a count of the projects, the url to download the full list of projects, the last time a project was updated, etc.
What is the best way to include a summary of the associated objects?
Ideally, for example the resulting JSON would look like this:
{
"id": 10,
"name": "My Client",
"projects": {
"count": 5,
"updated_at": "2014-09-09T13:36:20.000-04:00",
"url": "https://my.baseurl.com/clients/10/projects"
}
I'm not sure if this is the best way to do it, but I got this to work:
class ClientSerializer < ActiveModel::Serializer
attributes :id, :name, :archive, :updated_at, :projects
def projects
collection = object.projects.to_a
{ count: collection.length,
updated_at: collection.map(&:updated_at).max,
url: projects_url }
end
end
You could create an instance method:
class ClientSerializer < ActiveModel::Serializer
has_many :projects
def project_count
projects.size
end
end

Rails: Elasticsearch :through association mapping

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.

Resources