ThinkingSphinx - Search through has_many association - ruby-on-rails

I have a relationship between two models, Idea and Iteration. Each idea can have many iterations. The models looks like this;
# idea.rb
has_many :iterations, dependent: :destroy
# I also use with: :real_time
after_save ThinkingSphinx::RealTime.callback_for(:idea)
# iteration.rb
belongs_to :idea
after_save ThinkingSphinx::RealTime.callback_for(:idea, [:idea])
My indexes for Idea looks like this:
ThinkingSphinx::Index.define :idea, :with => :real_time do
[...]
indexes iterations.title, as: :iteration_titles
indexes iterations.description, as: :iteration_descriptions
has iterations.id, :as => :iteration_ids, type: :integer
[...]
end
And I search like this:
#ideas = #user.ideas.search ThinkingSphinx::Query.escape(params[:search]),
:with => {:team_id => #teams.collect(&:id)},
:page => params[:page],
:per_page => 10,
:order => 'created_at DESC'
Currently, searching for either Iteration title or description returns 0 hits. And I have performed:
rake ts:rebuild
rake ts:regenerate
rake ts:index
Have I missed something?

One of the differences between real-time indices and SQL-backed indices is that with SQL-backed indices, you refer to associations and columns in your index definition, but with real-time indices, you refer to methods.
And while iterations is an instance method within an idea, title, description and id are not methods on the object returned by iterations.
The easiest way to work through this has two parts. Firstly, add instance methods to Idea that return the data you want for the iteration-related fields and attributes:
def iteration_titles
iterations.collect(&:title).join(' ')
end
def iteration_descriptions
iterations.collect(&:description).join(' ')
end
def iteration_ids
iterations.collect &:id
end
And then use those methods in your index definition:
indexes iteration_titles, iteration_descriptions
has iteration_ids, :type => :integer, :multi => true
And then, run rake ts:regenerate to get it all set up (ts:rebuild and ts:index have no meaning for real-time indices).

Related

Rails 4.1 - Thinking-Sphinx Search with many-to-many through relationships

I'm making an app with three model objects, Team, Users and Member, Member is a join table between Team and Users. So each Team can have many members, and each User can be a member of multiple teams.
The relationship looks like this:
# Team.rb
has_many :members
has_many :users, through: :members
# User.rb
has_many :members
has_many :teams, through: :members
What I want to do is to search for members that are in a specific team. Currently I get no results.
My indexes looks like this:
# user_index
ThinkingSphinx::Index.define :user, :with => :real_time do
indexes name
indexes email
indexes about
has team_id, type: :integer
has created_at, type: :timestamp
has updated_at, type: :timestamp
indexes members.team.name, :as => :teams
end
# team_index
ThinkingSphinx::Index.define :team, :with => :real_time do
indexes name
has created_at, type: :timestamp
has updated_at, type: :timestamp
indexes members.user.name, :as => :members
end
# member_index.rb
ThinkingSphinx::Index.define :member, :with => :real_time do
has user_id, type: :integer
has team_id, type: :integer
has created_at, type: :timestamp
has updated_at, type: :timestamp
end
My members_controller index action (where I perform the search) - looks like this:
def index
#team = Team.find_by_id(params[:team_id])
#users = #team.users.search(params[:search], :page => params[:page], :per_page => 10)
end
I have checked that the team actually has users, but #users always returns 0. Any ideas on how I should do to make it work as I want?
Update
I use Rails: 4.1.4
And thinking-sphink: 3.1.1
My Sphinx query looks like this:
Sphinx Query (0.7ms) SELECT * FROM `member_core` WHERE MATCH('Anders') AND `team_id` = 2 AND `sphinx_deleted` = 0 LIMIT 0, 10
Sphinx Found 0 results
With slightly updated controller code:
#members = #team.members.search(params[:search], :page => params[:page], :per_page => 10)
So, the original cause of this issue was that there was a search query being provided, but no fields to match against. Adding fields fixed that, which is great.
The second issue, noted in the comments above, is that when you search for an email address, a query error is raised. This is because the # character signifies a field name in the query (to limit searches to a specific field - for example "#name Anders" to search for Anders within a field called name).
You have two ways around this... either you escape the query by wrapping it in ThinkingSphinx::Query.escape(params[:query]), or if you're searching for a specific email address, then I would suggest using ActiveRecord instead (given you're almost certainly going to have a unique constraint on email addresses in your database, hence there should only be zero or one matching records). The latter approach also means there's no need to have email as an indexed field, which is a bit more secure if you're letting anyone define your search queries via parameters. Letting someone search for 'gmail' and they get back all your users with gmail addresses is probably not a wise idea.

Rewrite SQL query with tire

I'm trying to rewrite a query with tire.
This is the model that I have:
class User < ActiveRecord::Base
has_many :bookmarks, :dependent => :destroy
has_many :followed_venues, :through => :bookmarks, :source => :bookmarkable, :source_type => 'Venue'
end
A user can follow venues. And I need to search for venues that are followed by a certain users.
So far I've been doing it with ActiveRecord:
#user.followed_venues.where(["venues.title LIKE ?", "%"+params[:q]+"%"])
This is obviously not ideal, so I added elasticsearch to my app with tire.
How would I search for venues with tire, filtering by the user that is following them?
I'm going to post an answer to my own question.
So it's quite easy to just search venues. Standard Venue.tire.search {...} The problem is how to filter by user that follows venues. Instead of using the Venue model for searching, I decided to index bookmarks.
This is my bookmark model
class Bookmark < ActiveRecord::Base
belongs_to :bookmarkable, :polymorphic => true
def to_indexed_json
{
:title => bookmarkable.display_name,
:user_id => user_id
}.to_json
end
end
After this I have the user_id and the venue name in the index. Now search becomes as simple as this:
Bookmark.tire.search :load => {:include => 'bookmarkable'}, :page => page, :per_page => per_page do
query do
fuzzy :title => { :value => query.downcase, :min_similarity => 0.6, :prefix_length => 2}
end
filter :terms, {:bookmarkable_type => ["Venue"], :user_id => [user.id]}
end
Now this is not a complete solution. And I hope i'm even using filter :terms correctly. The result that I get back now is an array of bookmarks actually. But it's easy to load the actual venues for them, and maybe wrap it in a WillPaginate collection for better pagination on the frontend.
Any problems with this solution? How would it compare to what phoet suggested with putting user ids to the venue index?
i would create a document for each venue and then add a field with an array of all the user-ids that are following.
are you really sure, that this is a proper task for elasticsearch?
i guess it would be way easier to just search the index for the name of the venue and then look up the data you need in your relational database.

Combining the results of multiple Thinking Sphinx queries into a single paginated result

Is there a simple way to combine the results of multiple Thinking Sphinx searches into a single result? All of these searches are on the same model, but the searches have distinct search terms. What I'm trying to do is combine the results so that they can all be sorted by a date column and receive proper pagination.
Say I have a Thinker class and an Idea class.
class Thinker < ActiveRecord::Base
has_many :ideas
end
class Idea < ActiveRecord::Base
belongs_to :thinker
define_index do
indexes text
has created_at
end
end
And say I have two thinkers, Bob, and Alice. I want to combine the following searches:
bob.ideas.search 'pancakes', :order => :created_at, :sort_mode => :desc
alice.ideas.search 'waffles', :order => :created_at, :sort_mode => :desc
...and somehow combine them so that the collection of Bob's (pancake) and Alice's (waffle) ideas are mixed together, sorted by descending created_at, and properly paginated by Thinking Sphinx. In the actual use case, I could have anywhere between 2 and 15 searches to combine in this fashion.
I know that the search method returns a ThinkingSphinx::Search < Array. I thought about manually splicing these objects together, but the fact that I'm looking for both sorting and pagination makes this a bit tricky.
Is there an elegant way to do this in Thinking Sphinx or am I not missing anything and I pretty much have to roll my own?
Thinking Sphinx work with Kaminari.
So you already have kaminari in your gemfile. You just have to do :
result = bob.ideas.search 'pancakes', :order => :created_at, :sort_mode => :desc
result += alice.ideas.search 'waffles', :order => :created_at, :sort_mode => :desc
Result is no longer a ThinkingSphinx::Search. It's an array
result = Kaminari.paginate_array(result)
Your can use pagination and simple sort on result
first_search = bob.ideas.search 'pancakes', :order => :created_at, :sort_mode => :desc
second_search = bob.ideas.search 'pancakes', :order => :created_at, :sort_mode => :desc
results = first_search.flatten + second_search.flatten
you can now sort by date like you want
sorted_results = results.sort_by(&:date)
hope this helps
You can probably do this pretty easily, but you need to rewrite your queries to be more generic and search on the Idea model itself.
Idea.search 'pancakes | waffles', :with => {:thinker_id => [bob.id, alice.id]},
:order => :created_at,
:sort_mode => :desc,
:match_mode => :boolean
And your model would be:
class Idea < ActiveRecord::Base
belongs_to :thinker
define_index do
indexes text
has created_at, thinker_id
end
end

sorting collection by ids from an array

My Thread model has many Posts. Let's say I want to reorder posts by an array containg ids I wish to see my posts sorted by.
thread.posts.collect {|x| x.id} # => [1,2,3]
order = [2,3,1]
posts = thread.posts.sort_by {|x| order.index x.id}
posts.collect {|x| x.id} # => [2,3,1]
thread.update_attributes(:posts => posts) # => true
thread.posts.collect {|x| x.id} # => [1,2,3]
What am I doing wrong? Is sorting by id always preserved in collections and can I somehow disable it?
You should always assume the order of results retrieved from your database as being more or less "random", unless you specifically ask it to sort them. This means that you can not rely on your database to magically store the order of posts associated with a thread (in fact, the code sample you posted would probably not query the database at all, because there is nothing to update).
The easiest way to achieve what you want is to add an order field to your Post model like this:
class AddOrderToPost < ActiveRecord::Migration
def up
change_table :posts do |t|
t.integer :order, :default => 0
end
Post.update_all ["order = ?", 0]
end
def down
remove_column :posts, :order
end
end
In your Thread model:
class Thread < ActiveRecord::Base
# ...
has_many :posts, :order => 'order ASC'
# ...
end
Afterwards you will be able to reorder the posts like this:
thread.posts.zip([2,3,1]) { |p,i| p.order = i }
If you want, you can also use a plugin like acts_as_list which provides this and other useful functionality.

Does writing has_many extensions for ActiveRecord requery the database?

If I have something like this:
class Post < ActiveRecord::Base
has_many :comments, :as => :commentable do
def approved
find(:all, :conditions => {:approved => true})
end
end
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
...when I do this, I get 2 hits to the database (not including finding the post :p):
post = Post.first
post.comments #=> [Comment1, Comment2...]
post.comments.approved #=> [Comment1, Comment7...]
It seems like it should just filter the current comments array in memory, no? Is it doing that? Reason I ask is because the console shows SELECT * FROM ... on post.comments.approved, even though I already called post.comments. Shouldn't this be better optimized in ActiveRecord?
The AR executes a new query for any finder calls inside a association extension method.
You can refer to the cached result set by using self.
has_many :comments, :as => :commentable do
def approved
# uses the cached result set
self.select{|c| c.approved == true}
end
end
It's optional, as in some cases you might only want to load the associated objects when needed. If you want them all loaded into memory, you need to explicitly declare the objects you'd like included with the initial query, using the :include flag. Example:
post = Post.find(:first, :include => :comment)
You might have to rewrite your extension to take advantage of the feature... an approach would be to change your "approved" function to iterate through the comments array attached to each post, and return a new array with the nonapproved comments filtered out. The "find" you have defined explicitly goes back to the database.
If your query is really that simple, then what you want is a named scope:
class Comment
named_scope :approved, :conditions => {:approved => true}
end
Then, you can do:
#post.comments.approved.count #=> 1 DB hit!
#post.comments.count #=> Another DB hit - can't reuse same scope
Look at #scope (#named_scope in Rails 2.3).

Resources