Building the right search request with gem Tire and ElasticSearch - ruby-on-rails

I am trying to implement simple search request for my Flower Store, but as I am new to Tire and ElasticSearch, I cannot get how to do it.
I have searchable model Product, and habtm models Category, Colour, Flower. What I want is checkboxes for each association, producing request similiar to this:
http://example.com/search?q=some%20query&category_names[]=bouquet&category_names[]=marriage&colour_names[]=red&colour_names[]=yellow&colour_names[]=white&flower_names[]=rose&flower_names[]=tulip&price_min=100&price_max=1000
But I need to show only those products, which:
a) prestent in any of the requested categories (OR);
b) have all of the requested colours (AND);
c) have all of the requested flowers (AND);
d) enter the price range between price_min and price_max.
The q parameter is to search any text, entered to the text field, in the names of associations (let's say, red roses bouquet), and show it with the upper criteria too.
For now I have only
class Product
include Mongoid::Document
include Tire::Model::Search
include Tire::Model::Callbacks
has_and_belongs_to_many :categories
has_and_belongs_to_many :colours
has_and_belongs_to_many :flowers
def self.search(params)
tire.search(load: true, page: params[:page], per_page: 8) do |search|
search.query { string params[:query] } if params[:query].present?
end
end
def to_indexed_json
self.to_json(methods: [:category_names, :colour_names])
end
def category_names
self.categories.collect { |c| c.name }
end
def colour_names
self.colours.collect { |c| c.name }
end
end
and do not know how to configure it. I read about filters and facets, but because English is not my native I cannot uderstand what is what and how to use them.

I did it so:
def self.search(params)
tire.search(load: true, page: params[:page], per_page: 8) do
query do
boolean do
must { string params[:query], default_operator: "AND" } if params[:query].present?
must { string params[:category_names] } if params[:category_names].present?
must { string params[:colour_names], default_operator: "AND" } if params[:colour_names].present?
must { range :price, { gte: params[:price_min], lte: params[:price_max] } } if params[:price_min].present? && params[:price_max].present?
end
end
# raise to_curl
end
end

Related

tire and elasticsearch_autocomplete accents and facets

I'm using elasticsearch_autocomplete gem for autocomplete feature.
I have a problem with special characters ñ and accents áéíóú.
Model:
class Car
ac_field :name, :description, :city, :skip_settings => true
def self.ac_search(params, options={})
tire.search load: true, page: params[:page], per_page: 9 do
query do
boolean do
must { string params[:query], default_operator: "AND" } if params[:query].present?
must { term :city, params[:city] } if params[:city].present?
end
end
filter :term, city: params[:city] if params[:city].present?
facet "city" do
terms :city
end
end
end
end
This version works fine with special characters.
e.g.: Query with Martin I get all results with Martín, martín, martin, Martin
With this approach this is the problem:
Now what results is individual words. e.g. A city tagged ["San Francisco", "Madrid"] will end up having three separate tags. Similarly, if I do a query to search on "san francisco" (must { term 'city', params[:city] }), that will fail, while a query on "San" or "Francisco" will succeed. The desired behaviour here is that the tag should be atomic, so only a "San Francisco" (or "Madrid") tag search should succeed.
To fix this problem I create my custom mapping:
model = self
settings ElasticsearchAutocomplete::Analyzers::AC_BASE do
mapping _source: {enabled: true, includes: %w(name description city)} do
indexes :name, model.ac_index_config(:name)
indexes :description, model.ac_index_config(:description)
indexes :city, :type => 'string', :index => :not_analyzed
end
end
With this mapping the problem with multi-words is fixed, and now facets with city field works fine:
Instead of getting the type facets San and Francisco Now I get San Francisco
Now, the problem is that with this mapping inside of the model the search doesn't find results with special characters
e.g.: Query with Martin I get only results with Martin martin
I'm using mongoid instead active record.
How can I fix this problem? I think that the problem is with asciifolding tokenfilter.
I fixed the problem with:
class User
include Mongoid::Document
field :city, :type => String
has_one: car
end
class Car
ac_field :name, :description, :user_city, :skip_settings => true
def self.ac_search(params, options={})
tire.search load: true, page: params[:page], per_page: 9 do
query do
boolean do
must { term :user_city, params[:user_city] } if params[:user_city].present?
end
end
facet "cities" do
terms :user_city
end
end
end
model = self
settings ElasticsearchAutocomplete::Analyzers::AC_BASE do
mapping _source: {enabled: true, includes: %w(car_city name description)} do
indexes :car_city, :type => 'string', :index => :not_analyzed
end
end
def to_indexed_json
to_json(methods: [:user_city])
end
def user_city
user.city
end
end

Using filters with elasticsearch and tire to mimic named scopes in rails

I have an Exhibition model that has three named scopes:
scope :opening, -> { where(start_date: (Date.today .... }
scope :closing, -> { where(end_date: .... }
scope :reception, -> { where("reception is not null")... }
I have the following tire mapping inside the same model:
tire.mapping do
...
indexes :start_date, type: "date"
indexes :end_date, type: "date"
indexes :reception, type: "date"
...
end
And my search method:
def self.search(params)
tire.search(load: true) do
query { string params[:query] } if params[:query].present?
...
end
Inside my exhibitions_controller.rb I have the following (and the same for closing and reception)
def opening_this_week
#exhibitions = Exhibition.opening.search(params)
...
end
When I actually search for one of the indexed fields I get back the correct response of just the exhibitions that meet the query and are opening this week, however, when I want all opening exhibitions and do not pass a search query I get ALL exhibitions, not just those that are opening this week.
What am I doing wrong here? How do I transition from a named scope to a facet? Or should I be doing it as a filter?

How to boost query depends of attribute's value in ElasticSearch?

I have a Profile class. In ES index I have company_type attribute.
class Profile
...
include Tire::Model::Search
include Tire::Model::Callbacks
def to_indexed_json
{ name: self.name,
company_type: self.company.company_type
}.to_json
end
end
Tire.search('profiles') do
query do
custom_filters_score do
query { all }
filter do
filter :range, last_contact_at: { gte: 7.days.ago }
boost 1
end
score_mode :total
end
end
end.results
I would like to boost by 10 queries if company_type == 'intern'.
did you try adding
filter do
filter :term, company_type: "intern"
boost 10.0
end
to your custom_filters_score filter?

Elastic Search/Tire: How do I filter a boolean attribute?

I want to filter the private boolean of my class so it only shows resources that aren't private but it's not working for me. (I dumbed down the code tremendously)
mapping do
indexes :private, type: "boolean"
indexes :name, type: "string"
end
end
def self.search(params)
tire.search(load: true, page: params[:page], per_page: 20) do
query { string params[:query] } if params[:query].present?
# So far I've tried...
# filter :bool, :private => ["false"]
# filter :bool, private: false
end
end
How do I do this correctly?
filter :term, :private => false
Should do the trick. Depending on whether you want to do stuff with facets it may be more efficient to do your filtering a filtered query rather than at the top level, ie
tire.search(...) do
query do
filtered do
query { string, params[:query] }
filter :term, :private => false
end
end
end
It shouldn't change the results though.
You can also do this with a bool filter, but not quite how you tried - within a bool filter you need to build up a structure that says what's optional and what's not
For example
tire.search(load: true, page: params[:page], per_page: 20) do
query { string params[:query] } if params[:query].present
filter :bool, :must => {:term => {:private => true}}
end
A bool filter is slower than using an and filter (which is what tire does behind the scenes if you specify multiple filters) but obviously gives you more flexibility.
You can try:
tire.search(load: true, page: params[:page], per_page: 20) do
query do
boolean do
must { string params[:query] } if params[:query].present?
must { term :private, true }
end
end
end
According to the elasticsearch - guide, booleans are stored as T or F, so I would try filtering by T or F.
For example
filter :terms, :private => ['T']
I haven't actually used tires, this is just based on some research on the guide and in the examples.

How to implement 'search function' for Mailboxer's each box

Now I'm using a gem called 'mailboxer' for messaging system.https://github.com/ging/mailboxer
I'd like to implement 'keyword search function' for inbox, sentbox, and trash.
If I don't care about search keyword, it is fetching the result without any problem by coding like this
inbox... #messages = current_user.mailbox.inbox.page(params[:page]).per(10)
sentbox... #messages = current_user.mailbox.sentbox.page(params[:page]).per(10)
trash... #messages = current_user.mailbox.trash.page(params[:page]).per(10)
But I sometimes want to filter the result with search keyword.
Assume search keyword was 'test' this time, the result should be only the records that contain 'test' within body attribute in notifications table.
How can I do this for each above such as inbox, sentbox, and trash??
I tried this but it didn't work at all :(
#messages = current_user.mailbox.inbox.search_messages(#search).page(params[:page]).per(10)
#messages = current_user.mailbox.sentbox.search_messages(#search).page(params[:page]).per(10)
#messages = current_user.mailbox.trash.search_messages(#search).page(params[:page]).per(10)
The solution is to monkey patch both the Mailboxer::Receipt and Mailboxer::Models::Messageable module to enable searching by box type:
class Mailboxer::Receipt < ActiveRecord::Base
...
if Mailboxer.search_enabled
searchable do
text :subject, :boost => 5 do
message.subject if message
end
text :body do
message.body if message
end
integer :receiver_id
# Add mailbox_type to sunspot search field
string :mailbox_type
boolean :trashed
boolean :deleted
end
end
end
and under the Messageable module, I've added new search__messages method for each mailbox type:
module Mailboxer
module Models
module Messageable
def search_messages(query)
# Replaces 'search' with alias 'solr_search' since ransack search clashes with solr search method
#search = Mailboxer::Receipt.solr_search do
fulltext query
with :receiver_id, self.id
end
#search.results.map { |r| r.conversation }.uniq
end
# Adds additional Mailboxer search functionality to search by box type
def search_inbox_messages(query)
#search = Mailboxer::Receipt.solr_search do
fulltext query
with :receiver_id, self.id
with(:mailbox_type).equal_to("inbox")
end
#search.results.map { |r| r.conversation }.uniq
end
def search_sent_messages(query)
#search = Mailboxer::Receipt.solr_search do
fulltext query
with :receiver_id, self.id
with(:mailbox_type).equal_to("sentbox")
end
#search.results.map { |r| r.conversation }.uniq
end
def search_trash_messages(query)
#search = Mailboxer::Receipt.solr_search do
fulltext query
with :receiver_id, self.id
with :trashed, true
with :deleted, false
end
#search.results.map { |r| r.conversation }.uniq
end
end
end
end
With this, in your controller you can simply use the newly defined methods to do the box type searching.
current_user.search_inbox_messages(query[:keywords])
current_user.search_sentbox_messages(query[:keywords])
current_user.search_trash_messages(query[:keywords])
IMPORTANT NOTE: Make sure to regenerate the Solr indexes after adding the new search fields, otherwise nothing will show up:
bundle exec rake sunspot:solr:reindex
Hope this helps!

Resources