Thinking sphinx results based on model preference - ruby-on-rails

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'

Related

Ordering by count with Thinking Sphinx

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.

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 :)

Querying embedded objects in Mongoid/rails 3 ("Lower than", Min operators and sorting)

I am using rails 3 with mongoid.
I have a collection of Stocks with an embedded collection of Prices :
class Stock
include Mongoid::Document
field :name, :type => String
field :code, :type => Integer
embeds_many :prices
class Price
include Mongoid::Document
field :date, :type => DateTime
field :value, :type => Float
embedded_in :stock, :inverse_of => :prices
I would like to get the stocks whose the minimum price since a given date is lower than a given price p, and then be able to sort the prices for each stock.
But it looks like Mongodb does not allow to do it.
Because this will not work:
#stocks = Stock.Where(:prices.value.lt => p)
Also, it seems that mongoDB can not sort embedded objects.
So, is there an alternative in order to accomplish this task ?
Maybe i should put everything in one collection so that i could easily run the following query:
#stocks = Stock.Where(:prices.lt => p)
But i really want to get results grouped by stock names after my query (distinct stocks with an array of ordered prices for example). I have heard about map/reduce with the group function but i am not sure how to use it correctly with Mongoid.
http://www.mongodb.org/display/DOCS/Aggregation
The equivalent in SQL would be something like this:
SELECT name, code, min(price) from Stock WHERE price<p GROUP BY name, code
Thanks for your help.
MongoDB / Mongoid do allow you to do this. Your example will work, the syntax is just incorrect.
#stocks = Stock.Where(:prices.value.lt => p) #does not work
#stocks = Stock.where('prices.value' => {'$lt' => p}) #this should work
And, it's still chainable so you can order by name as well:
#stocks = Stock.where('prices.value' => {'$lt' => p}).asc(:name)
Hope this helps.
I've had a similar problem... here's what I suggest:
scope :price_min, lambda { |price_min| price_min.nil? ? {} : where("price.value" => { '$lte' => price_min.to_f }) }
Place this scope in the parent model. This will enable you to make queries like:
Stock.price_min(1000).count
Note that my scope only works when you actually insert some data there. This is very handy if you're building complex queries with Mongoid.
Good luck!
Very best,
Ruy
MongoDB does allow querying of embedded documents, http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-ValueinanEmbeddedObject
What you're missing is a scope on the Price model, something like this:
scope :greater_than, lambda {|value| { :where => {:value.gt => value} } }
This will let you pass in any value you want and return a Mongoid collection of prices with the value greater than what you passed in. It'll be an unsorted collection, so you'll have to sort it in Ruby.
prices.sort {|a,b| a.value <=> b.value}.each {|price| puts price.value}
Mongoid does have a map_reduce method to which you pass two string variables containing the Javascript functions to execute map/reduce, and this would probably be the best way of doing what you need, but the code above will work for now.

How to coerce type of ActiveRecord attribute returned by :select phrase on joined table?

Having trouble with AR 2.3.5, e.g.:
users = User.all( :select => "u.id, c.user_id", :from => "users u, connections c",
:conditions => ... )
Returns, e.g.:
=> [#<User id: 1000>]
>> users.first.attributes
=> {"id"=>1000, "user_id"=>"1000"}
Note that AR returns the id of the model searched as numeric but the selected user_id of the joined model as a String, although both are int(11) in the database schema.
How could I better form this type of query to select columns of tables backing multiple models and retrieving their natural type rather than String ? Seems like AR is punting on this somewhere. How could I coerce the returned types at AR load time and not have to tack .to_i (etc.) onto every post-hoc access?
It's unfortunately not going to happen very easily. All of the data from the DB connection comes to rails as strings, the conversion of types happens in each of the dynamic attribute methods that rails creates at runtime. It knows which attributes to convert to which type by the table's column-type meta-data that it retrieves when the app starts. Each model only has column meta-data for it's own columns, that's why it's own columns end up with correct type. There is no easy way to auto-convert to the correct types.
You could on the other hand, create a simple conversion method that would take a Hash and automatically convert the attributes.
Something like this:
users = User.all(:select => "cl, comments.c2", ...)
users = convert_columns(users, 'c2' => :integer, 'other_column' => :date)
def convert_columns(records, columns = {})
records.each do |rec|
columns.each do |col, type|
rec[col] = case type
when :int then rec[col].to_i
when :date then ........
....
end
end
end
end
Why are you using :from => "users" inside a User.method ?
The following will do an inner join (which is what you are doing anyways)
users = User.all(:include => :connections, :select => "users.id, connections.user_id", :conditions => {...})
This is going to be very heavy query for the database.
Faster query would be with the outer join though.
This will also return the keys as INT not STRING
A much faster alternative was
Connection.all(:include => :user, :conditions => {...}).collect {|e| [e.user_id, e.id] }
This gives you an array of arrays with the ids. If you are going to select "id, user_id" columns only, then it may not necessarily be as AR object. An array can be faster.
I hope I am not missing some point here. Suggest me, if I am.
If you want quick solution - try to use after_find callback and preset correct attributes types there:
class User < ActiveRecord::Base
after_find :preset_types
private
def preset_types user
user.user_id = user.user_id.to_i
end
end

Ruby on Rails activerecord find average in one sql and Will_Paginate

I have the following model association: a student model and has_many scores.
I need to make a list showing their names and average, min, max scores. So far I am using
student.scores.average(:score) on each student, and I realise that it is doing one sql per student. How can I make the list with one joined sql?
Also how would I use that with Will_Paginate plugin?
Thank you
You want the :group and :select options to Student.find. This should work for you:
students = Student.all(
:select => "
students.*,
AVG(scores.score) as avg_score,
MIN(scores.score) as min_score,
MAX(scores.score) as max_score",
:joins => :scores
:group => 'students.id')
The calculated columns are available just like the real columns though they obviously won't be saved
students.first.avg_score
students.first.min_score
students.first.max_score
For using WillPaginate, just include your :page, :per_page, ... options and call Student.paginate instead of find. If it turns out that the pagination is getting the wrong number of pages because of the :group option, just add this: :total_entries => Student.count to your arguments

Resources