Ordering by count with Thinking Sphinx - ruby-on-rails

I want my search engine to be able to order Lawyers on the count of cases of a certain case type. The most a lawyer has finalized cases of a certain type, the higher he will be ranked.
lawyer.rb
has_many :cases
has_many :case_types, :through => :cases
define_index do
indexes case_types.name, :as => :case_types
has case_types(:id), :as => :case_types_id
has "SUM(case_types)", :as => :case_type_count #this line gives an error, as my lawyer table does't have a case_type column, also, I need to count DISTINCT case_types
end
In my search_controller.rb, I would like to do something like that, suggestion being the name of a case type
#lawyers = Lawyer.search params[:suggestion], :order => "#case_type_count DESC"
Am I going the wrong way? should I think of a less Sphinx oriented method? The problem is I need to do an each_with_geodist on #lawyers, so I would need to get my lawyers through a Sphinx search.

Add the following to your define_index:
has "COUNT(case_types.id)", :as => :case_type_count, :type => :integer
join case_types
Then retrieve by case_count:
Lawyer.search("", :order => "case_type_count desc")
I have found it useful to read the sql code in development.sphinx.conf which allows me to see the column names being generated.

Related

How to add infix and prefix indexes for nested model based on condition in sphinx search

I have a note model with polymorphic association which is like below
incident has one symptom -> (note model)
incident has many comments -> (note model)
and
I would like to do the infix indexing for the note with the note type "symptom" and want to do the prefix indexing for the note with type "comment"
I've tried the below code in Sphinx index
ThinkingSphinx::Index.define(:incident, DEFAULT_INDEX_OPTIONS.merge(name: "incident_prefix"), &Searchable.beetilable_index('Incident', index_count: incident_index_count, index_id: i) {
set_property :min_infix_length => 3
indexes notes.note, :as => :notes, :source => :query
notes.where(note_type: "Symptom")
}
ThinkingSphinx::Index.define(:incident, DEFAULT_INDEX_OPTIONS.merge(name: "incident_infix"), &Searchable.beetilable_index('Incident', index_count: incident_index_count, index_id: i) {
set_property :min_prefix_length => 3
indexes notes.note, :as => :notes, :source => :query
notes.where.not(note_type: "Symptom")
}
the above code is just doing the indexing with INFIX option and ignores the PREFIX one. I guess something wrong with my where condition, can someone let me know the way to achieve this?
I'm afraid you can't use Arel/ActiveRecord querying methods within the index definition to build on top of associations - you can only use associations and columns.
If you only want notes of certain types, then the best approach here is to create associations that have those filters applied, and then reference the association in your index:
# in the model
has_many :symptom_notes,
lambda { where(:note_type => "Symptom") },
:class_name => "Note"
# in the index:
indexes symptom_notes.note, :as => :notes

Thinking sphinx results based on model preference

I have two models: 'A' and 'B', and want to search objects from both of them using Thinking sphinx, but I want all results of model 'A' first and then 'B'. How can I do that?
I pass the following options to sphinx query
{:match_mode=>:extended, :sort_mode=>:extended, :star=>true, :order=>"#relevance DESC", :ignore_errors=>true, :populate=>true, :per_page=>10, :retry_stale=>true, :classes => [A,B]}
And then get search results using:
ThinkingSphinx.search "*xy*", options
But it gives results in mixed ordering, whereas I need all 'A' objects first. How can I do that?
The easiest way is to add an attribute to both models' indices:
has "1", :as => :sort_order, :type => :integer
The number within the string should be different per model. And then your :order argument becomes:
:order => 'sort_order ASC, #relevance DESC'

Indexing fields + custom text in with Thinking Sphinx

I've got indexes on a few different models, and sometimes the user might search for a value which exists in multiple models. Now, if the user is really only interested in data from one of the models I'd like the user to be able to pre/postfix the query with something to limit the scope.
For instance, if I only want to find a match in my Municipality model, I've set up an index in that model so that the user now can query "xyz municipality" (in quotes):
define_index do
indexes :name, :sortable => true
indexes "name || ' municipality' name", :as => :extended_name, :type => :string
end
This works just fine. Now I also have a Person model, with a relation to Municipality. I'd like, when searching only on the Person model, to have the same functionality available, so that I can say Person.search("xyz municipality") and get all people connected to that municipality. This is my current definition in the Person model:
has_many :municipalities, :through => :people_municipalities
define_index do
indexes [lastname, firstname], :as => :name, :sortable => true
indexes municipalities.name, :as => :municipality_name, :sortable => true
end
But is there any way I can create an index on this model, referencing municipalities, like the one I have on the Municipality model itself?
If you look at the generated SQL in the sql_query setting of config/development.sphinx.conf for source person_core_0, you'll see how municipalities.name is being concatenated together (I'd post an example, but it depends on your database - MySQL and PostgreSQL handle this completely differently).
I would recommend duplicating the field, and insert something like this (SQL is pseudo-code):
indexes "GROUP_CONCAT(' municipality ' + municipalities.name)",
:as => :extended_municipality_names
Also: there's not much point adding :sortable true to either this nor the original field from the association - are you going to sort by all of the municipality names concat'd together? I'm guessing not :)

RoR top five ratings

I have a model that has ratings in it for an post.
I want to display a list of the top 5 ratings for a given post, but I am completely lost on where to start. I figure the find method might have something useful, but I'm unsure. I've even considered looping through each record getting its size, adding it to a hash, sorting the hash, etc., but that seems too complicated.
Does anyone know how I might accomplish something like this?
Thank you
Edit: I found this to get all the posts that have the rating of agree:
Post.find(:all, :include => :post_ratings, :condtions => ['post_ratings.agree = ?', true])
The only problem is I can't figure out how to get the top five ratings from this query.
Might be worth giving a little more of an example of the code you're working with but I'll answer with a few assumptions.
If you have:
class Post
has_many :post_ratings
end
class PostRating
belongs_to :post
# Has a 'rating' attribute
end
You can find the top five post ratings with:
p = Post.find(:first) # For example
p.post_ratings.find(:all, :limit => 5, :order => 'rating desc')
To get the top five post ratings overall you can do:
PostRating.find(:all, :limit => 5, :order => 'rating desc')
UPDATE:
Following your edit it seems you have an 'agree' and a 'disagree' column. Not sure how that works in combination so I'll stick with the 'agree' column. What you'll need to do is count the ratings with agree flagged. Something like:
count_hsh PostRating.count(:group => 'post_id',
:order => 'count(*) desc',
:conditions => { :agree => true },
:limit => 5)
This will return you a hash mapping the post id to the count of agree ratings. You can then use that post_id to locate the posts themselves. The ratings are provided by the counts so the individual ratings are (I think) of no use though you could access them by calling post.post_ratings.
So, to get the top five posts:
#top_five_posts = []
count_hsh.each_pair do |post_id, ratings|
p = Post.find(post_id)
p[:rating_count] = ratings
#top_five_posts << p
end
This is probably more verbose than it could be but is hopefully illustrative. The p[:rating_count] is a virtual attribute which isn't in the database but will allow you to access the .rating_count method on the posts in your view if you wish to.
Assuming the same Post and PostRating from Shadwell's answer:
class Post
has_many :post_ratings
end
class PostRating
belongs_to :post
# Has a 'rating' attribute
end
To get the top five ratings for all Posts:
post_ratings.find(:all, :limit => 5, :order => 'rating desc')
To get the top five ratings for a specific Post you can:
p = Post.find(:first)
post_ratings.find_all_by_post_id(p.id, :limit => 5, :order => 'rating desc')
To find all posts sorted by average rating, you can use ActiveRecord::Calculations.
PostRating.average(:rating, :group => :post_id, :include => :post, :order => 'average desc')

Rails find by *all* associated tags.id in

Say I have a model Taggable has_many tags, how may I find all taggables by their associated tag's taggable_id field?
Taggable.find(:all, :joins => :tags, :conditions => {:tags => {:taggable_id => [1,2,3]}})
results in this:
SELECT `taggables`.* FROM `taggables` INNER JOIN `tags` ON tags.taggable_id = taggables.id WHERE (`tag`.`taggable_id` IN (1,2,3))
The syntax is incredible but does not fit my needs in that the resulting sql returns any taggable that has any, some or all of the tags.
How can I find taggables with related tags of field taggable_id valued 1, 2 and 3?
Thanks for any advice. :)
UPDATE:
I have found a solution which I'll post for others, should they end up here. Also though for the attention of others whose suggestions for improvement I'd happily receive. :)
Taggable.find(:all, :joins => :tags, :select => "taggables.*, tags.count tag_count", :conditions => {:tags => {:taggable_id => array_of_tag_ids}}, :group => "taggables.id having tag_count = #{array_of_tag_ids.count}"))
Your question is a bit confusing ('tags' seemed to be used quite a bit :)), but I think you want the same thing I needed here:
Taggable.find([1,2,3], :include => :tags).tags.map { |t| t.taggables }
or (if you want the results to be unique, which you probably do):
Taggable.find([1,2,3], :include => :tags).tags.map { |t| t.taggables }.flatten.uniq
This gets at all the taggables that have the same tags as the taggables that have ids 1,2, and 3. Is that what you wanted?

Resources