Retire nested filter wrong argument - ruby-on-rails

I want to make nested filtering. But return error. Is there another method of filtering? Use of this true?
Error output:
Wrong argument filter
/models/series/filter_season method:
def self.filter_season
s = Tire.search('myindex', type: 'series') do
query do
filtered do
query do
all
end
filter do
nested do
path 'seasons'
query do
bool do
must do
term 'seasons.title', 'season 5'
end
end
end
end
end
end
end
end
s.results.each do|result|
puts result.title
end
end
Mapping setting:
index_name 'myindex'
mapping do
indexes :id, type: 'integer'
indexes :title, type: 'string'
indexes :seasons, type: 'nested' do
indexes :id, type: 'integer'
indexes :title, type: 'string'
end
end

Please look at the following answer on the issue Nested query with filters test integration
filter method expects at least one argument where in your case it's receiving 0. Hence, the error.
Hope the article helps to resolve your issue.

Related

Wish the search method for elasticsearch is working on rails

■The environment
MacOS
RailsServer
Ruby 2.4.1
Ruby on Rails 5.1.7
MySQL
Elasticsearch 7.10.2-SNAPSHOT
kuromoji
Gems
elasticsearch (7.4.0)
elasticsearch-api (7.4.0)
elasticsearch-model (7.1.0 80822d6)
elasticsearch-rails (7.1.0 80822d6)
elasticsearch-transport (7.4.0)
■My wish
Hi I'm japanese.
My wish is that read faster on database to use elasticsearch on rails.
Now i'm trouble with search method on rails.
I will explain the current settings.
First, I think the record on elastic search already input on following below.
curl 'localhost:9200/_cat/indices?v'
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open es_project_development Fpu_adXWT9Gtw7KZTh0aDw 1 1 6804 0 3.7mb 3.7mb
Second, the setting of index and mapping for elasticsearch are the following below.
/models/concerns/project_searchable.rb
module ProjectSearchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
index_name "es_project_#{Rails.env}"
settings do
mappings dynamic: 'false' do
indexes :id, type: 'integer'
indexes :title, type: 'text', analyzer: 'kuromoji'
indexes :contents, type: 'text', analyzer: 'kuromoji'
indexes :industry, type: 'text'
...
def as_indexed_json(*)
attributes
.symbolize_keys
.slice(:id, :title, :contents, :industry, ...)
end
end
class_methods do
def create_index!
client = __elasticsearch__.client
client.indices.delete index: self.index_name rescue nil
client.indices.create(index: self.index_name,
body: {
settings: self.settings.to_hash,
mappings: self.mappings.to_hash
})
end
def es_search(query)
__elasticsearch__.search({
query: {
multi_match: {
fields: %w(id title contents industry ...),
type: 'cross_fields',
query: query,
operator: 'and'
}
}
})
end
end
end
Third, the view and the controller are the following below.
/views/top/index.html.slim
= form_tag search_path, {:method=>"get"}
table border="0"
tr
td
= text_field_tag 'keyword[name]', nil, class: 'write'
td
input.push type="submit" value=""
...
/controllers/projects_controller.rb
def search
...
#keyword = params.dig('keyword', 'name')
params = ''
connection = '?'
...
if #keyword.present?
params = "#{params}#{connection}keyword=#{#keyword}"
connection = '&'
end
...
Last, ProjectSearchable is included in models/project.rb.
/models/project.rb
class Project < ApplicationRecord
include ProjectSearchable
...
■The problem
On rails console, i typed command
Project.es_search('Game')
Response was following.
※Game is included in the record
#<Elasticsearch::Model::Response::Response:0x007fc1a85621a8
#klass=[PROXY] Project (call 'Project.connection' to establish a connection),
#search=
#<Elasticsearch::Model::Searching::SearchRequest:0x007fc1a8562248
#definition=
{:index=>"es_project_development",
:type=>nil,
:body=>
{:query=>
{:multi_match=>
{:fields=>
["id",
"title",
"contents",
"industry",
"required",
...
"comment"],
:type=>"cross_fields",
:query=>"Game",
:operator=>"and"}}}},
#klass=[PROXY] Project (call 'Project.connection' to establish a connection),
#options={}>>
I think elasticsearch doesn't work.
My opinion of this problem is in search method for elasticsearch.
But i don't have enough knowledge about that.
I really need your help.
Thank you.

ActiveAdmin Filter on postgres Array field

I added the following filter in ActiveAdmin.
filter :roles, as: :select, collection Model::ROLES, multiple: true
but when i choose the filter value to search the roles. it gives me following error
PG::InvalidTextRepresentation: ERROR: malformed array literal: "teacher"LINE 1: ...ted" = $1 AND roles" IN('teacher
DETAIL: Array value must start with "{" or dimension information. ^
Any idea ? How we can search/Filter ARRAY field using AA filters? I'm using Rails 4.2.4,
ruby 2.2.2p95
I came up to a solution slightly different (and inspired by) this one over here: https://stackoverflow.com/a/45728004/1170086
Mine involves some changes (and prevent breaking contains operator in other cases). So, you're going to basically create two initializer files:
This one is for Arel, in order to support #> operator (array's contain operator in PG) for a given table column.
# config/initializers/arel.rb
module Arel
class Nodes::ContainsArray < Arel::Nodes::Binary
def operator
:"#>"
end
end
class Visitors::PostgreSQL
private
def visit_Arel_Nodes_ContainsArray(o, collector)
infix_value o, collector, ' #> '
end
end
module Predications
def contains(other)
Nodes::ContainsArray.new self, Nodes.build_quoted(other, self)
end
end
end
The other file aims to create a new Ransack predicate but I also decided to support the :array type (that's not natively supported in Ransack in terms of predicates).
# config/initializers/ransack.rb
module Ransack
module Nodes
class Value < Node
alias_method :original_cast, :cast
def cast(type)
return Array(value) if type == :array
original_cast(type)
end
end
end
end
Ransack.configure do |config|
config.add_predicate 'contains_array',
arel_predicate: 'contains',
formatter: proc { |v| "{#{v.join(',')}}" },
validator: proc { |v| v.present? },
type: :array
end
And in other to use it. All you need to do is:
User.ransack(roles_contains_array: %i[admin manager])
Or as a filter in ActiveAdmin (which is my case):
ActiveAdmin.register User do
# ...
filter :roles_contains_array, as: :select, collection: User.roles_for_select
# ...
end
I hope it works for you as it worked for me. ;)
You can set up a custom ransacker method to first collect the ids you want returned using a regular postgres search, and then return the results based on those ids:
class User < ApplicationRecord
ransacker :roles,
formatter: proc { |str|
data = where("? = ANY (roles)", str).map(&:id)
data.present? ? data : nil
} do |parent|
parent.table[:id]
end
end
If your filter is a select drop-down, then this should work fine. If you have a free-form text box, then make sure to use the "in" predicate:
filter :roles_in, as: :string
leandroico solutions works well.
But if you add the predicate with this formatter
formatter: proc { |v| "{#{v.join(', ')}}" }, (note the space after the comma)
Then you could use the multiple: true keyword in the filter input and filter by more than one value:
filter :roles_contains_array, as: :select, multiple: true, collection: User.roles_for_select
I used the answer from #leandroico to come up with the below wiki-type approach to doing this.
How to Create Custom SQL Searches for ActiveAdmin (using Arel and Ransack)
In ActiveAdmin, filters are declared in app/admin/model.rb like:
ActiveAdmin.register Model do
filter 'column_name', label: 'column_name', as: :string
end
That will make a searchbox available on the front-end with options to choose between
contains
equals
starts with
ends with
You can even do something like...
filter 'column_name_contains', label: 'column_name', as: :string
...to only have a contains type search available on the front-end.
You can also (after defining some custom methods elsewhere) specify other, non-built-in search methods, like:
filter 'column_name_custom_contains', label: 'column_name', as: :string
The rest of this doc will be about how to define this custom search method, custom_contains
Within config/initializers/arel.rb, define the following:
module Arel
# this example of custom_contains will cast the SQL column as ::text and then do a wildcard-wrapped ILIKE
class Nodes::CustomContains < Arel::Nodes::Binary
def operator
'::text ILIKE'.to_sym
end
end
class Visitors::PostgreSQL
private
def visit_Arel_Nodes_CustomContains(o, collector)
infix_value o, collector, '::text ILIKE '
end
end
module Predications
def custom_contains(column_value)
column_value = self.relation.engine.column_types[self.name.to_s].type_cast_for_database(column_value)
column_value = "%#{self.relation.engine.send(:sanitize_sql_like, column_value)}%" # wrap escaped value with % wildcard
column_value = Nodes.build_quoted(column_value, self)
Nodes::CustomContains.new(self, column_value)
end
end
end
module ActiveRecord::QueryMethods
def custom_contains(predicates)
return none if predicates.length == 0
predicates.map{ |column_name, column_value|
column_value = table.engine.column_types[column_name.to_s].type_cast_for_database(column_value)
column_value = "%#{table.engine.send(:sanitize_sql_like, column_value)}%" # wrap escaped value with % wildcard
column_value = Arel::Nodes.build_quoted(column_value)
where Arel::Nodes::CustomContains.new(table[column_name], column_value)
}.inject(:merge)
end
end
module ActiveRecord::Querying
delegate :custom_contains, :to => :all
end
Within config/initializers/ransack.rb, define the following:
Ransack.configure do |config|
config.add_predicate(
'custom_contains',
arel_predicate: 'custom_contains',
formatter: proc { |v| v.to_s },
validator: proc { |v| v.present? },
type: :string
)
end
The above has accomplished a couple of things:
1) You can use the custom_contains method that was delegate'd to all ActiveRecord models:
puts Model.custom_contains(column_name: 'search for me').to_sql
2) You can use Ransack to search against the Arel predicates that were defined:
puts Model.ransack(column_name_custom_contains: 'search for me').result.to_sql
However, in order to do the below in ActiveAdmin...
filter 'column_name_custom_contains', label: 'column_name', as: :string
...we must add a scope to Model so that there is a method, column_name_custom_contains, on Model
scope_name = "#{column_name}_custom_contains".to_sym
unless Model.methods.include?(scope_name)
Model.scope(
scope_name,
->(value) {
Model.custom_contains({column_name.to_sym => value})
}
)
end
Voila!

Why multi-field mapping is not working with tire gem for elasticsearch?

I'm using elastic search to enhance search capabilities in my app. Search is working perfectly, however sorting is not for fields with multiple words.
When I try to sort the search by log 'message', I was getting the error:
"Can't sort on string types with more than one value per doc, or more than one token per field"
I googled the error and find out that I can use multi-fields mapping on the :message field (one analyzed and the other one not) to sort them. So I did this:
class Log < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
tire.mapping do
indexes :id, index: :not_analyzed
indexes :source, type: 'string'
indexes :level, type: 'string'
indexes :created_at, :type => 'date', :include_in_all => false
indexes :updated_at, :type => 'date', :include_in_all => false
indexes :message, type: 'multi_field', fields: {
analyzed: {type: 'string', index: 'analyzed'},
message: {type: 'string', index: :not_analyzed}
}
indexes :domain, type: 'keyword'
end
end
But, for some reason is not passing this mapping to ES.
rails console
Log.index.delete #=> true
Log.index.create #=> 200 : {"ok":true,"acknowledged":true}
Log.index.import Log.all #=> 200 : {"took":243,"items":[{"index":{"_index":"logs","_type":"log","_id":"5 ... ...
# Index mapping for :message is not the multi-field
# as I created in the Log model... why?
Log.index.mapping
=> {"log"=>
{"properties"=>
{"created_at"=>{"type"=>"date", "format"=>"dateOptionalTime"},
"id"=>{"type"=>"long"},
"level"=>{"type"=>"string"},
"message"=>{"type"=>"string"},
"source"=>{"type"=>"string"},
"updated_at"=>{"type"=>"date", "format"=>"dateOptionalTime"}}}}
# However if I do a Log.mapping I can see the multi-field
# how I can fix that and pass the mapping correctly to ES?
Log.mapping
=> {:id=>{:index=>:not_analyzed, :type=>"string"},
:source=>{:type=>"string"},
:level=>{:type=>"string"},
:created_at=>{:type=>"date", :include_in_all=>false},
:updated_at=>{:type=>"date", :include_in_all=>false},
:message=>
{:type=>"multi_field",
:fields=>
{:message=>{:type=>"string", :index=>"analyzed"},
:untouched=>{:type=>"string", :index=>:not_analyzed}}},
:domain=>{:type=>"keyword"}}
So, Log.index.mapping is the current mapping in ES which doesn't contain the multi-field that I created. Am I missing something? and why the multi-field is shown in Log.mapping but not in Log.index.mapping?
I have changed the workflow from:
Log.index.delete; Log.index.create; Log.import
to
Log.index.delete; Log.create_elasticsearch_index; Log.import
The MyModel.create_elasticsearch_index creates the index with proper mapping from model definition. See Tire's issue #613.

Having trouble with mixed complex boolean with Tire (and ElasticSearch)

I've been trying to figure out how to do mixed boolean searches that use nested objects using Tire. All the simple examples I've found don't include a more complex query (when searching on other attributes).
My search involves finding a Team that 'needs' a specific type of person. When trying to build a football team, the team needs to fill the roster with certain types of players of a given weight class, with the option of excluding one term or the other.
Other parameters such as 'region' or 'kind' have to do with where the team plays, and what kind of team (casual, competitive, etc) it is.
My current setup:
mapping do
indexes :region, index: :not_analyzed
indexes :kind, index: :not_analyzed
indexes :role_requirements do
indexes :need, type: 'boolean'
indexes :weight_class_id, type: 'integer'
indexes :role_id, type: 'integer'
end
.
.
.
end
def self.search(params)
team_params = params[:team_search]
tire.search(page: params[:page], per_page: 10) do
query do
boolean do
must { string team_params[:query], default_operator: "AND" } if team_params[:query].present?
must { term :kind, team_params[:kind] } if team_params[:kind].present?
must { term :region, team_params[:region] } if team_params[:region].present?
if team_params[:weight_class_id].present? || team_params[:role_id].present?
must { term 'role_requirements.need', true }
end
must { term 'role_requirements.weight_class_id', team_params[:job_id].to_i } if team_params[:weight_class_id].present?
must { term 'role_requirements.role_id', team_params[:role_id].to_i } if team_params[:role_id].present?
.
.
.
end
end
end
end
I've tried a number of ways, but there usually seems to be a problem either with ElasticSearch not able to parse things or Tire not having the method within the scope:
With this implementation, here is the generated to_json: https://gist.github.com/8a615e701eb31ff2e250
Which are currently not giving me any results.
All the different ways I've tried: https://gist.github.com/907c9571caa0e87bad27
None are really able to give me full results.
You seem to be missing the nested type in the mapping:
mapping do
indexes :region, index: :not_analyzed
indexes :kind, index: :not_analyzed
indexes :role_requirements, type: 'nested' do
indexes :need, type: 'boolean'
indexes :weight_class_id, type: 'integer'
indexes :role_id, type: 'integer'
end
# ..more mappings..
end
Then you can build your query like this:
tire.search(page: params[:page], per_page: 10) do
query do
boolean do
must { string team_params[:query], default_operator: "AND" } if team_params[:query].present?
must { term :kind, team_params[:kind] } if team_params[:kind].present?
must { term :region, team_params[:region] } if team_params[:region].present?
must do
nested path: 'role_requirements' do
query do
boolean do
if team_params[:weight_class_id].present? || team_params[:role_id].present?
must { term 'role_requirements.need', true }
end
must { term 'role_requirements.weight_class_id', team_params[:job_id].to_i } if team_params[:weight_class_id].present?
must { term 'role_requirements.role_id', team_params[:role_id].to_i } if team_params[:role_id].present?
end
end
end
end
end
end
end
Here are some examples in Tire's integration tests.
Hope this helps :)

RoR: Elasticsearch sort by mapped field

I have Player and Player have fullname from Profil. I use elasticsearch on Player. I need default sort by fullname. How can I set it? My code from Player class file:
......
mapping do
indexes :id, type: 'integer'
indexes :fullname, boost: 10
indexes :name
indexes :surname
indexes :position_name
indexes :info
indexes :club_id, type: 'integer'
indexes :club_name
end
def self.search(params)
tire.search(load: true, page: params[:page], per_page: 20) do
query do
boolean do
if params[:query].present?
must { string params[:query], default_operator: "AND" }
else
must {string '*'}
end
must { term :position_id, params[:position_id]} if params[:position_id].present?
must { term :club_id, params[:club_id]} if params[:club_id].present?
end
end
# filter :term, user_id: params[:user_id] if params[:user_id].present?
# sort { by Item.column_names.include?(params[:sort]) ? params[:sort] : "created_at", %w[asc desc].include?(params[:direction]) ? params[:direction] : "desc" } if params[:query].blank?
sort { by Player.column_names.include?(params[:sort]) ? params[:sort] : :id ,%w[asc desc].include?(params[:direction]) ? params[:direction] : "desc"}
facet "positions" do
terms :position_id
end
facet "clubs" do
terms :club_id
end
# raise to_curl
end
end
def to_indexed_json
to_json(methods: [:fullname, :name, :surname, :position_name, :club_name])
end
def fullname
self.profil.fullname
end
......
If I changed :id to :fullname in sort I have this error:
500 : {"error":"SearchPhaseExecutionException[Failed to execute phase [query], total failure; shardFailures {[DBgvi6JiQAi0FTwhonq8Ag][players][0]: QueryPhaseExecutionException[[players][0]: query[ConstantScore(NotDeleted(cache(_type:player)))],from[0],size[20],sort[<custom:\"fullname\": org.elasticsearch.index.field.data.strings.StringFieldDataType$1#33a626ac>!]: Query Failed [Failed to execute main query]]; nested: IOException[Can't sort on string types with more than one value per doc, or more than one token per field]; }{[DBgvi6JiQAi0FTwhonq8Ag][players][4]: QueryPhaseExecutionException[[players][4]: query[ConstantScore(NotDeleted(cache(_type:player)))],from[0],size[20],sort[<custom:\"fullname\": org.elasticsearch.index.field.data.strings.StringFieldDataType$1#3274eb8a>!]: Query Failed [Failed to execute main query]]; nested: IOException[Can't sort on string types with more than one value per doc, or more than one token per field]; }]","status":500}
I got it! After dumb amounts of research I found this article here. The first answer mentions:
The field user by default gets analyzed and broken down into one or more tokens. If it gets broken down into more than one token, then you can't really sort on it. If you want to sort on it, you either need to set it i(in the mappings) as not analyzed
In your situation, all you would need to do is:
mapping do
indexes :id, type: 'integer'
indexes :fullname, :index => 'not_analyzed'
indexes :name
indexes :surname
indexes :position_name
indexes :info
indexes :club_id, type: 'integer'
indexes :club_name
end
And it should work.
sort { by Player.column_names.include?(params[:sort]) ? params[:sort] : :fullname ,%w[asc desc].include?(params[:direction]) ? params[:direction] : "desc"}
Changing 'id' to 'fullname' should work.

Resources