Creating a separate percolator index for Elasticsearch using Searchkick (Rails) - ruby-on-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

Related

Search for a term on multiple models/Indexes

Description
I have just migrated our application from searchkick to meilisearch however meilisearch doesn't have a way I can search for single term across multiple indexes or models like searchkick does.
Basic example
I want to to be able to search my term on at least one model
example
Meilisearch.search(term, models: [...index_names])
Here is a workaround using parallel gem
# search.rb
module Queries
class Search < BaseQuery
include SearchHelper
type [Types::SearchResultsType], null: true
argument :query, String, required: true
argument :models, [String], required: true, default_value: ['AppUser']
def resolve(**args)
search(args)
end
end
end
# search_helper.rb
module SearchHelper
SEARCH_MODELS = %w[AppUser Market Organisation]
def search(args)
raise Errors::SearchError::BlankQueryError if args[:query].blank?
raise Errors::SearchError::UnpermittedQueryError if args[:query] == '*'
models = args[:models].map { |m| m.tr(' ', '').camelize }
if models.difference(SEARCH_MODELS).any?
raise Errors::SearchError::UnknownSearchModelError
end
Parallel.flat_map(models, in_threads: models.size) do |m|
m.constantize.search(args[:query])
end
end
end
# search_results_type
module Types
class SearchResultsType < Types::BaseUnion
description 'Models which may be searched on'
possible_types(
Types::AppUserType,
Types::MarketType,
Types::OrganisationType,
)
def self.resolve_type(object, context)
if object.is_a?(AppUser)
Types::AppUserType
elsif object.is_a?(Market)
Types::MarketType
else
Types::OrganisationType
end
end
end
end
Graphql query
{
search(query: "Gh", models: ["Market","Organisation"]) {
... on Market {
id
name
}
... on Organisation {
id
name
}
... on AppUser {
id
email
}
}
}

How to get the fast_jsonapi to return the attributes of the relationships

I have a rails api with a number of models that are being serialized by the fast_jsonapi gem.
This is what my models look like:
class Shift < ApplicationRecord
belongs_to :team, optional: true
...
class Team < ApplicationRecord
has_many :shifts
...
This is what the serializer looks like
class ShiftSerializer
include FastJsonapi::ObjectSerializer
...
belongs_to :team
...
end
The serialization works. However, even though I am including the compound team document:
def index
shifts = policy_scope(Shift).includes(:team)
options = {}
options[:include] = [:team, :'team.name', :'team.color']
render json: ShiftSerializer.new(shifts, options)
end
I'm still getting the object formatted like so:
...
relationships: {
team: {
data: {
id: "22",
type: "Team"
}
}
}
Whereas I'm expecting to get also the attributes of my team model.
fast_jsonapi implements json api specification so respond includes "included" key, where serialized data for relationships placed.That's default behavior
If you use the options[:include] you should create a serializer for the included model, and customize what is included in the response there.
in your case if you use
ShiftSerializer.new(shifts, include: [:team]).serializable_hash
you should create a new serializer serializers/team_serializer.rb
class TeamSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :color
end
this way your response will be
{
data: [
{
id: 1,
type: "shift",
relationships: {
team: {
data: {
id: "22",
type: "Team"
}
}
}
}
],
included: [
id: 22,
type: "Team",
attributes: {
name: "example",
color: "red"
}
]
}
and you will find the custom data of your association in the response "included"
If you use like this then maybe solve your problem
class Shift < ApplicationRecord
belongs_to :team, optional:true
accepts_nested_attributes_for :team
end
In your ShiftSerializer.rb please write this code,
attribute :team do |object|
object.team.as_json
end
And you will get custom data that you want.
Reference: https://github.com/Netflix/fast_jsonapi/issues/160#issuecomment-379727174

Advance search with elasticsearch and 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

How to query nested indexes using Retire

I've got an Article model:
class Article < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
settings default_options do
mapping do
indexes :id, index: :not_analyzed
indexes :roles do
indexes :machine_name, analyzer: 'keyword'
end
indexes :published_at, type: 'date', include_in_all: false
end
end
end
where the default_options is:
index: { store: { type: Rails.env.test? ? :memory : :niofs },
analysis: {
analyzer: {
default: {
tokenizer: "standard",
filter: ["asciifolding", "lowercase", "snowball"],
char_filter: ["html_strip"]
}
}
}
I'm simply trying to search articles while filtering roles, but I don't have any idea how to do so. I've been trying something like that without success:
Tire.search("article") do
query { string 'foo bar baz' }
filter :nested, { path:'roles',
query: {
filtered: {
query: {
match_all: {}
},
filter: {
term:{'roles.machine_name' => ['da']}
}
}
}
}
end
This give me that error:
QueryParsingException[[development-oaciq::application-article] [nested] nested object under path [roles] is not of nested type];
After finding this question, it seems the nested filter wasn't required, it could be done like this:
Tire.search("article") do
query do
string 'foo bar baz'
term 'roles.machine_name', 'test'
end
end

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