Advance search with elasticsearch and rails - ruby-on-rails

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

Related

Creating a separate percolator index for Elasticsearch using Searchkick (Rails)

I'm trying to create a separate percolator index in Elasticsearch using Searchkick. I'd like SavedSearch to be able to percolate the Product index and thus (I believe) need the SavedSearch and Product mappings to be the same with the addition of the percolator property in the SavedSearch mapping.
The solution below seems to be working, but it also seems clumsy. Does anyone have any suggestions for a better way to accomplish this?
class Product < ApplicationRecord
searchkick callbacks: false, batch_size: 200
def self.mappings_for_percolator
_mapping_name, full_mapping = Product.search_index.mapping.max_by { |k, _v|
Time.parse(k[/\d+/])
}
mapping = full_mapping["mappings"]["product"]
mapping["properties"]["query"] = { "type" => "percolator" }
mapping
end
end
class SavedSearch < ApplicationRecord
searchkick merge_mappings: true, mappings: {
saved_search: Product.mappings_for_percolator
}
validates :query, presence: true
validate :query_is_valid
def self.percolate_product(id)
q = {
query: {
constant_score: {
filter: {
percolate: {
field: "query",
index: Product.search_index.name,
type: "product",
id: id
}
}
}
}
}
search(body: q.to_json)
end
def query_is_valid
result = Searchkick.client.perform_request(
"GET",
"#{Product.search_index.name}/_validate/query",
{},
{ query: query }.to_json
).body
return if result["valid"]
errors.add(:query, "is invalid")
end
end

Rails Elasticsearch error: [400] No handler found for uri [//items] and method [PUT]

I'm trying to implement elasticsearch with two models account.rb and item.rb. The account model contains the address info and the item model contains the searchable item.
I'm also using the gem geocoded in order to get the latitude and longitude coordinates when the address is inserted to the account.rb.
With the following setup I'm receiving this error:
Elasticsearch::Transport::Transport::Errors::BadRequest in ShowcaseController#index
[400] No handler found for uri [//items] and method [PUT]
on these two lines item.rb and showcase_controller.rb respectively:
Item.__elasticsearch__.client.indices.create \
#items = Item.all
item.rb
require 'elasticsearch/model'
class Item < ApplicationRecord
mount_uploader :image, ImageUploader
belongs_to :category
belongs_to :store
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mapping do
indexes :location, type: 'geo_point'
end
end
def location
[latitude, longitude]
end
def self.search()
__elasticsearch__.search(
{
query: {
match_all: {}
},
sort: [{'_geo_distance' => {
location: {
latitude: 0,
longitude: 0
}
}
}]
}
)
end
end
Item.__elasticsearch__.client.indices.delete index: Item.index_name rescue nil
Item.__elasticsearch__.client.indices.create \
index: Item.index_name,
body: { settings: Item.settings.to_hash, mappings: Item.mappings.to_hash }
Item.import
showcase_controller.rb
class ShowcaseController < ApplicationController
def index
#items = Item.all
end
private
def front_params
params.require(:front).permit(:title, :description, :price, :image, :location )
end
end
Any ideas on how to solve this error?

NoMethodError (undefined method `highlight' for #<Elasticsearch::Model::Response::Result>

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

Jbuilder not wrapping single object in collection

I have a index view in my rails application that allows filtering via search params. When a group op articles are returned its is wropped in an articles colllection like {"articles":[{"article":{"id":341,"updated":"2015-08-18T13:05:08.427Z","title":". But if only a single object is found the articles level is missing, {"article":{"id":398,"updated":"2015-08-07T11:37:26.200Z","title":. How can I fix it so that a single object behaves like multiple?
_articles.list.json.jbuilder
require 'uri'
require 'publish_on'
json.cache! ['v1', articles] do
json.articles articles do |article|
json.cache! ['v1', article] do
json.article do
json.id article.id
json.updated as_ns_date(article.updated_at)
json.title article.label
json.numberOfViews article.view_mappings.count
json.numberOfFavorites article.favorite_mappings.count
json.imageURLs article.images if article.images.any?
json.youtubeURL article.youtube unless article.youtube.blank?
json.tags article.categories.map(&:label)
json.isFeatured article.featured
json.isPublished article.is_published
json.published as_ns_date(article.publish_on)
end
end
end
end
index.json.jbuilder
json.partial! 'articles/articles_list', articles: #articles
articles_controller.rb
def index
#articles = SearchArticlesCommand.new(params).execute
render :index
end
search_articles_command.rb
class SearchArticlesCommand
def initialize(params = {})
#since = params[:since_date]
#keys = params[:search_query]
#category = params[:category]
end
def execute
Article.unscoped do
query = if #since.present?
Article.article.since_date(#since)
else
Article.published_article
end
query = query.search_by_keywords(#keys) if #keys.present?
query = query.search_by_category(#category) if #category.present?
query.select(:id, :updated_at, :label, :is_published, :featured, :slug, :created_at).order(created_at: :desc)
end
end
end
article.rb
class Article < Comfy::Cms::Page
include PgSearch
include ActionView::Helpers::SanitizeHelper
HOSTNAME = ENV['HOSTNAME'] || Socket.gethostname
has_many :view_mappings, dependent: :destroy
has_many :favorite_mappings, dependent: :destroy
pg_search_scope :search_by_keywords, against: [:content_cache, :label], using: { tsearch: { any_word: true, prefix: true } }
pg_search_scope :search_by_category, associated_against: {
categories: [:label]
}
scope :since_date, -> (date) { where('created_at > ? OR updated_at > ? ', date, date) if date.present? }
scope :folder, -> { where.not(layout_id: ENV['ARTICLE_LAYOUT_ID']) }
scope :published_article, -> { published.article }
scope :article, -> { where(layout_id: ENV['ARTICLE_LAYOUT_ID']) }
It is what i suspected. If you want the same behavior your query should return the same type of object when it finds one or many articles. The problem is that either you are returning an ActiveRecordRelation or a Article object depending on your params.
#articles = Article.all # => ActiveRecordRelation, an array per se
#articles = Article.find(1) # => Article object
When it comes to jbuilder to construct the JSON it checks if it is an array of objects and then wrap the json with a { keyword => array }. WHen it is a single object, it defaults to a single object {article: {}}.
The solution is simple, you can tweak your SearchArticlesCommand to always return an ActiveRecordRelation, even if it finds only one object.

Elasticsearch rails/ Elasticsearch model search model association

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

Resources