thinking-sphinx collect attributes of search result - ruby-on-rails

How can I get collection of attributes for search result?
I.E.
I search through Product model which have many Category:
class Product < ActiveRecord::Base
has_many :categories
Here is my index:
ThinkingSphinx::Index.define :product, with: :active_record do
###
has category(:id), as: :direct_category_id
So, I search through query
products = Product.search params[:query]
categories = Category.find(products.map(&:category_id)) # slow & bad & ugly
But this method is so slow and bad. Is there any way to get all atttributes from search results instead collect?

search = Product.search params[:query]
search.context[:panes] << ThinkingSphinx::Panes::AttributesPane
category_ids = search.collect { |product|
product.sphinx_attributes['direct_category_id']
}
categories = Category.find category_ids
However, keep in mind that if you run this in the console the first line evaluates the search request because IRB renders the result. This means the pane can't be added... so you'll want to add ; '' or similar at the end of the first line (again: only necessary in a Rails console):
search = Product.search params[:query]; ''

okay. I solved by myself. The solution use facets
First of all, we need to add direct_category_id:
has category(:id), as: :direct_category_id, facet: true
After that, we need just to use
category_ids = products.facets[:direct_category_id].keys
categories = Category.where(id: category_ids)

Related

Activeadmin: how to filter for strings that match two or more search terms

Let's say I've got User class with an :email field. And let's say I'm using activeadmin to manage Users.
Making a filter that returns emails that match one string, e.g. "smith", is very simple. In admin/user.rb, I just include the line
filter :email
This gives me a filter widget that does the job.
However, this filter doesn't let me search for the intersection of multiple terms. I can search for emails containing "smith", but not for emails containing both "smith" AND ".edu".
Google tells me that activerecord uses Ransack under the hood, and the Ransack demo has an 'advanced' mode that permits multiple term searches.
What's the easiest way to get a multiple term search widget into activeadmin?
Ideally, I'd like a widget that would allow me to enter smith .edu or smith AND .edu to filter for emails containing both terms.
there is simple solution using ranasckable scopes
So put something like this in your model
class User < ActiveRecord::Base
....
scope :email_includes, ->(search) {
current_scope = self
search.split.uniq.each do |word|
current_scope = current_scope.where('user.email ILIKE ?', "%#{word}%")
end
current_scope
}
def self.ransackable_scopes(auth_object = nil)
[ :email_includes]
end
end
After this you can add filter with AA DSL
Like
filter :email_includes, as: :string, label: "Email"
UPD
should work if change email_contains_any to email_includes
I've figured out a solution but it's not pretty.
The good news is that Ransack has no trouble with multiple terms searches. These searches use the 'predicate' cont_all. The following line works for finding emails containing 'smith' and '.edu'.
User.ransack(email_cont_all: ['smith','.edu'] ).result
Since these searches are easy in Ransack, they're probably straightforward in Activeadmin, right? Wrong! To get them working, I needed to do three things.
I put a custom ransack method (a.k.a. ransacker) into User.rb. I named the ransacker email_multiple_terms.
class User < ActiveRecord::Base
# ...
ransacker :email_multiple_terms do |parent|
parent.table[:path]
end
I declared a filter in my activeadmin dashboard, and associated it with the ransacker. Note that the search predicate cont_all is appended to the ransacker name.
admin/User.rb:
ActiveAdmin.register User do
# ...
filter :email_multiple_terms_cont_all, label: "Email", as: :string
This line creates the filter widget in Activeadmin. We're nearly there. One problem left: Activeadmin sends search queries to ransack as a single string (e.g. "smith .edu"), whereas our ransacker wants the search terms as an array. Somewhere, we need to convert the single string into an array of search terms.
I modified activeadmin to split the search string under certain conditions. The logic is in a method that I added to lib/active_admin/resource_controller/data_access.rb.
def split_search_params(params)
params.keys.each do |key|
if key.ends_with? "_any" or key.ends_with? "_all"
params[key] = params[key].split # turn into array
end
end
params
end
I then called this method inside apply_filtering.
def apply_filtering(chain)
#search = chain.ransack split_search_params clean_search_params params[:q]
#search.result
end
This code is live in my own fork of activeadmin, here: https://github.com/d-H-/activeadmin
So, to get multiple term search working, follow steps 1 and 2 above, and include my fork of A.A. in your Gemfile:
gem 'activeadmin', :git => 'git://github.com/d-H-/activeadmin.git'
HTH.
If anyone's got a simpler method, please share!
Just add three filters to your model:
filter :email_cont
filter :email_start
filter :email_end
It gives you a flexible way to manage your search.
This filter executes next sql code:
SELECT "admin_users".* FROM "admin_users"
WHERE ("admin_users"."email" ILIKE '%smith%' AND
"admin_users"."email" ILIKE '%\.edu')
ORDER BY "admin_users"."id" desc LIMIT 30 OFFSET 0
I expect that exactly what you're looking for.

Listing searchable values after pg_search query

Is there a way of getting the pg_search results from the model and not the indexing table entries? My code works, but I have to loop through the results.
search = PgSearch.multisearch(params[:search])
#items = Array.new
search.find_each do |result|
#items.push(result.searchable)
end
Multisearch will always return PgSearch::Document records.
If you want to query your model directly, you can define a pg_search_scope, e.g.:
pg_search_scope :custom_search, :against => [:title] # Can use multiple fields
And then use it with search = Model.custom_search(params[:search]), which will return Model records.
Also, a cleaner way to write your code above would be:
search = PgSearch.multisearch(params[:search])
#items = search.map(&:searchable)

Ruby on Rails search 2 models

Right... I've spent 3 days trying to do this myself to no a vale.
I have 2 models called Film and Screenings. Screenings belongs_to Film, Film has_many Screenings.
The Film has certain attributes(:title, :date_of_release, :description, :genre).
The Screening has the attributes(:start_time, :date_being_screened, :film_id(foreign key of Film)).
What I am trying to do is create a Search against both of these models.
I want to do something like this...
#films = Film.advanced_search(params[:genre], params[:title], params[:start_time], params[:date_showing])
And then in the Film model...
def self.advanced_search(genre, title, start_time, date)
search_string = "%" + title + "%"
self.find(:all, :conditions => ["title LIKE ? OR genre = ? OR start_time LIKE ? OR date_showing = ?", title, genre, start_time, date], order: 'title')
end
end
I don't think this could ever work quite like this, but I'm hoping my explanation is detailed enough for anyone to understand what im TRYING to do?? :-/
Thanks for any help guys
I would extract the search capability into a separate (non-ActiveRecord) class, such as AdvancedSearch as it doesn't neatly fit into either the Film or Screening class.
Rather than writing a complex SQL query, you could just search the films, then the screenings, and combine the results, for example:
class AdvancedSearch
def self.search
film_matches = Film.advanced_search(...) # return an Array of Film objects
screening_matches = Screening.advanced_search(...) # return an Array of Screening objects
# combine the results
results = film_matches + screening_matches.map(&:film)
results.uniq # may be necessary to remove duplicates
end
end
Update
Let's say your advanced search form has two fields - Genre and Location. So when you submit the form, the params sent are:
{ :genre => 'Comedy', :location => 'London' }
Your controller would then something like:
def advanced_search(params)
film_matches = Film.advanced_search(:genre => params[:genre])
screening_matches = Screening.advanced_search(:location => params[:location])
# remaining code as above
end
i.e. you're splitting the params, sending each to a different model to run a search, and then combining the results.
This is essentially an OR match - it would return films that match the genre or are being screened at that specified venue. (If you wanted and AND match you would need to the work out the array intersection).
I wanted to write something but this cast says all http://railscasts.com/episodes/111-advanced-search-form
Almost the same case as yours.

Finding records which belong to multiple models with HABTM

My Track model has_and_belongs_to_many :moods, :genres, and :tempos (each of which likewise has_and_belongs_to_many :tracks).
I'm trying to build a search "filter" where users can specify any number of genres, moods, and tempos which will return tracks that match any conditions from each degree of filtering.
An example query might be
params[:genres] => "Rock, Pop, Punk"
params[:moods] => "Happy, Loud"
params[:tempos] => "Fast, Medium"
If I build an array of tracks matching all those genres, how can I select from that array those tracks which belong to any and all of the mood params, then select from that second array, all tracks which also match any and all of the tempo params?
I'm building the initial array with
#tracks = []
Genre.find_all_by_name(genres).each do |g|
#tracks = #tracks | g.tracks
end
where genres = params[:genres].split(",")
Thanks.
I'd recommend using your database to actually perform this query as that would be a lot more efficient.
You can try to join all these tables in SQL first and then using conditional queries i.e. where clauses to first try it out.
Once you succeed you can write it in the Active Record based way. I think its fairly important that you write it in SQL first so that you can properly understand whats going on.
This ended up working
#tracks = []
Genre.find_all_by_name(genres).each do |g|
g.tracks.each do |t|
temptempos = []
tempartists = []
tempmoods = []
t.tempos.each do |m|
temptempos.push(m.name)
end
tempartists.push(t.artist)
t.moods.each do |m|
tempmoods.push(m.name)
end
if !(temptempos & tempos).empty? && !(tempartists & artists).empty? && !(tempmoods & moods).empty?
#tracks.push(t)
end
end
end
#tracks = #tracks.uniq

'Splitting' ActiveRecord collection

Let's say I have two models Post and Category:
class Post < ActiveRecord::Base
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :posts
end
Is there a method that will allow me to do something like
posts = Post.find(:all)
p = Array.new
p[1] = posts.with_category_id(1)
p[2] = posts.with_category_id(2)
p[3] = posts.with_category_id(3)
...
or
p = posts.split_by_category_ids(1,2,3)
=> [posts_with_category_id_1,
posts_with_category_id_2,
posts_with_category_id_3]
In other words, 'split' the collection of all posts into arrays by selected category ids
Try the group_by function on Array class:
posts.group_by(&:category_id)
Refer to the API documentation for more details.
Caveat:
Grouping should not performed in the Ruby code when the potential dataset can be big. I use the group_by function when the max possible dataset size is < 1000. In your case, you might have 1000s of Posts. Processing such an array will put strain on your resources. Rely on the database to perform the grouping/sorting/aggregation etc.
Here is one way to do it(similar solution is suggested by nas)
# returns the categories with at least one post
# the posts associated with the category are pre-fetched
Category.all(:include => :posts,
:conditions => "posts.id IS NOT NULL").each do |cat|
cat.posts
end
Something like this might work (instance method of Post, untested):
def split_by_categories(*ids)
self.inject([]) do |arr, p|
arr[p.category_id] ||= []
arr[p.category_id] << p if ids.include?(p.category_id)
arr
end.compact
end
Instead of getting all posts and then doing some operation on them to categorize them which is a bit performance intensive exercise I would rather prefer to use eager loading like so
categories = Category.all(:include => :posts)
This will generate one sql query to fetch all your posts and category objects. Then you can easily iterate over them:
p = Array.new
categories.each do |category|
p[1] = category.posts
# do anything with p[1] array of posts for the category
end
Sure but given your model relationships you I think you need to look at it the other way around.
p = []
1.upto(some_limit) do |n|
posts = Category.posts.find_by_id(n)
p.push posts if posts
end

Resources