How can I scope a Sunspot query? - ruby-on-rails

My trouble with this snippet is that it's returning an integer -1 which means a universal Message that is shared in other accounts as well.
def build_results
search = Sunspot.new_search(Message) do
any_of do
member.lists.map { |list| with :enterprise_list_id, list.search_id }
end
How can I add on to this statement to query all Message's with -1 as a search_id but scope it so that it belongs exclusively to member.account ?
I'm trying to scope it as so :
searchable :include => :repliable do
integer :account_id do
repliable.try(:account_id)
end
and..
def build_results
search = Sunspot.new_search(Message) do
with :account_id, member.account_id
But this returns nothing even though I know for a fact that there are search results that should be returned because they share a commont account_id.

If I understood your question correctly then you just need to add this statements in your Message searchable block.
searchable do
integer :some_search_ids, :multiple => true do
member.lists.map { |list| list.search_id} if member.present?
end
integer :member_account_id do
member.account_id if member.present?
end
end
then,
def build_results
search = Sunspot.new_search(Message) do
with(:some_search_ids, some_list_search_id)
with(:member_account_id, some_member_account_id)
end
end

Related

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!

Getting rails3-autocomplete-jquery gem to work nicely with Simple_Form with multiple inputs

So I am trying to implement multiple autocomplete using this gem and simple_form and am getting an error.
I tried this:
<%= f.input_field :neighborhood_id, collection: Neighborhood.order(:name), :url => autocomplete_neighborhood_name_searches_path, :as => :autocomplete, 'data-delimiter' => ',', :multiple => true, :class => "span8" %>
This is the error I get:
undefined method `to_i' for ["Alley Park, Madison"]:Array
In my params, it is sending this in neighborhood_id:
"search"=>{"neighborhood_id"=>["Alley Park, Madison"],
So it isn't even using the IDs for those values.
Does anyone have any ideas?
Edit 1:
In response to #jvnill's question, I am not explicitly doing anything with params[:search] in the controller. A search creates a new record, and is searching listings.
In my Searches Controller, create action, I am simply doing this:
#search = Search.create!(params[:search])
Then my search.rb (i.e. search model) has this:
def listings
#listings ||= find_listings
end
private
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline)
listings = listings.includes(:neighborhood).where("listings.headline like ? or neighborhoods.name like ?", key, key) if keywords.present?
listings = listings.where(neighborhood_id: neighborhood_id) if neighborhood_id.present?
#truncated for brevity
listings
end
First of all, this would be easier if the form is returning the ids instead of the name of the neighborhood. I haven't used the gem yet so I'm not familiar how it works. Reading on the readme says that it will return ids but i don't know why you're only getting names. I'm sure once you figure out how to return the ids, you'll be able to change the code below to suit that.
You need to create a join table between a neighborhood and a search. Let's call that search_neighborhoods.
rails g model search_neighborhood neighborhood_id:integer search_id:integer
# dont forget to add indexes in the migration
After that, you'd want to setup your models.
# search.rb
has_many :search_neighborhoods
has_many :neighborhoods, through: :search_neighborhoods
# search_neighborhood.rb
belongs_to :search
belongs_to :neighborhood
# neighborhood.rb
has_many :search_neighborhoods
has_many :searches, through: :search_neighborhoods
Now that we've setup the associations, we need to setup the setters and the attributes
# search.rb
attr_accessible :neighborhood_names
# this will return a list of neighborhood names which is usefull with prepopulating
def neighborhood_names
neighborhoods.map(&:name).join(',')
end
# we will use this to find the ids of the neighborhoods given their names
# this will be called when you call create!
def neighborhood_names=(names)
names.split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
# view
# you need to change your autocomplete to use the getter method
<%= f.input :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, input_html: { data: { delimiter: ',', multiple: true, class: "span8" } %>
last but not the least is to update find_listings
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline).includes(:neighborhood)
if keywords.present?
listings = listings.where("listings.headline LIKE :key OR neighborhoods.name LIKE :key", { key: "#{keywords}")
end
if neighborhoods.exists?
listings = listings.where(neighborhood_id: neighborhood_ids)
end
listings
end
And that's it :)
UPDATE: using f.input_field
# view
<%= f.input_field :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, data: { delimiter: ',' }, multiple: true, class: "span8" %>
# model
# we need to put [0] because it returns an array with a single element containing
# the string of comma separated neighborhoods
def neighborhood_names=(names)
names[0].split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
Your problem is how you're collecting values from the neighborhood Model
Neighborhood.order(:name)
will return an array of names, you need to also collect the id, but just display the names
use collect and pass a block, I beleive this might owrk for you
Neighborhood.collect {|n| [n.name, n.id]}
Declare a scope on the Neighborhood class to order it by name if you like to get theat functionality back, as that behavior also belongs in the model anyhow.
edit>
To add a scope/class method to neighborhood model, you'd typically do soemthing like this
scope :desc, where("name DESC")
Than you can write something like:
Neighborhood.desc.all
which will return an array, thus allowing the .collect but there are other way to get those name and id attributes recognized by the select option.

Ransack: How to use existing scope?

Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:
class Post < ActiveRecord::Base
scope :year, lambda { |year|
where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'")
}
end
So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:
ransacker :year,
:formatter => proc {|v|
year(v)
}
But this does not work:
Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation
I tried some variations of the ransacker declaration, but none of them work. I Need some help...
UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.
ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.
From manual
Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:
class Employee < ActiveRecord::Base
scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
def self.hired_since(date)
where('start_date >= ?', date)
end
private
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
%i(activated hired_since salary_gt)
else
# allow other users to search on `activated` and `hired_since` only
%i(activated hired_since)
end
end
end
Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates
Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34
I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.
You can read full explanation here. Meanwhile here's the gist of it
The View
= form_for #product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for #product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
```
ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:
The Controller
# products_controller.rb
def index
#product_search = ProductSearch.new(params[:product_search])
#products ||= #product_formobject.result.page(params[:page])
end
The Form Object
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are Product.scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
#params = params || {}
super
#q = Product.search( #params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end

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!

ruby object array... or hash

I have an object now:
class Items
attr_accessor :item_id, :name, :description, :rating
def initialize(options = {})
options.each {
|k,v|
self.send( "#{k.to_s}=".intern, v)
}
end
end
I have it being assigned as individual objects into an array...
#result = []
some loop>>
#result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>
But instead of assigning my singular object to an array... how could I make the object itself a collection?
Basically want to have the object in such a way so that I can define methods such as
def self.names
#items.each do |item|
item.name
end
end
I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.
A few observations before I post an example of how to rework that.
Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.
One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:
class Item
attr_accessor :item_id, :name, :description, :rating
def initialize(options = { })
options.each do |k,v|
method = :"#{k}="
# Check that the method call is valid before making it
if (respond_to?(method))
self.send(method, v)
else
# If not, produce a meaningful error
raise "Unknown attribute #{k}"
end
end
end
end
class ItemsCollection < Array
# This collection does everything an Array does, plus
# you can add utility methods like names.
def names
collect do |i|
i.name
end
end
end
# Example
# Create a custom collection
items = ItemsCollection.new
# Build a few basic examples
[
{
:item_id => 1,
:name => 'Fastball',
:description => 'Faster than a slowball',
:rating => 2
},
{
:item_id => 2,
:name => 'Jack of Nines',
:description => 'Hypothetical playing card',
:rating => 3
},
{
:item_id => 3,
:name => 'Ruby Book',
:description => 'A book made entirely of precious gems',
:rating => 1
}
].each do |example|
items << Item.new(example)
end
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
Do you know the Ruby key word yield?
I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:
class Items
#items = []
class << self
attr_accessor :items
end
attr_accessor :name, :description
def self.each(&args)
#items.each(&args)
end
def initialize(name, description)
#name, #description = name, description
Items.items << self
end
def each(&block)
yield name
yield description
end
end
a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}
This outputs
mug
cup
mug
a big cup
Did you ask for something like Items.each or a.each or for something completely different?
Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by
def method_missing(symbol_s, *arguments)
symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
if s == 's' and arguments.empty?
select do |i|
i.respond_to?(symbol) && i.instance_variables.include?("##{symbol}")
end.map {|i| i.send(symbol)}
else
super
end
end
For his example data you will get following outputs:
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems
As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.
The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.
Add 'return self' as the last line of initialize and you're golden.
Class Item
def initialize(options = {})
## Do all kinds of stuff.
return self
end
end

Resources