I'd like to do a complex search with thinking sphinx:
Search for users which:
-> live in a city (city_id attribute)
-> or has hability to move to a city (mobile_cities association)
-> or live at a maximum distance from a lat/long point, the maximum distance is different for each user and set in a mobility_distance attribute.
For now I did that with 3 differents search, I volontary set a big per_page number, then i merge the 3 results on a single array, an then paginate this array :
#users living in the #city
search_set_living = search_set.merge({:city_id => #city.id })
users_living = User.search :with => search_set_living.dup,
:page => 1, :per_page => 1000
#users declaring hability to move to the #city
search_set_mobile = search_set.merge({:mobile_cities_ids => #city.id })
users_mobile = User.search :with => search_set_mobile.dup, :page => 1, :per_page => 1000
#users living at a maximum distance from the origin point(custom distance for each user, max 30km)
search_set_around = search_set.merge({"#geodist" => 0.0..30_000.0})
users_around = User.search :geo => [#search_latitude * Math::PI / 180 , #search_longitude * Math::PI / 180],
:with => search_set_around.dup,
:page => 1, :per_page => 1000
users_around_filtered = users_around.dup.delete_if{|user| (user.mobility_distance * 1000 )< user.sphinx_attributes['#geodist'] }
#merge the 3 results in a array
all_users = (users_mobile.flatten + users_around_filtered.flatten).uniq
#look for facets and paginate the array
#facets = User.facets :with => {:user_id => all_users.map(&:id)}
#users_to_display = all_users.paginate(:page => params[:page], :per_page => 10)
This is working fine but i'm not satisfied:
-performance are not so good,
-I want the ability to sort on multiple attributes like this :order => "created_at DESC, #relevance DESC"
I want to do the exact same search but in a single sphinx's search.
I know that I should use the "OR Logic with Attribute Filters" from the docs but I don't know how to mix it with a geo_search call...
I really have no idea how to do that,
can you guys help me ?
Many thanks,
The :sphinx_select option is definitely your friend here, as you've guessed. Let's piece it together bit by bit:
logic = [
"city_id = #{#city.id}",
"IN(mobile_cities_ids, #{#city.id}",
"GEODIST(lat, lng, #{lat}, #{lng}) < (mobility_distance * 1000)"
]
User.search :sphinx_select => "*, #{logic.join(" OR ")}) AS valid",
:with => {:valid => true}
Add pagination as you like, tweak the attribute names if needed (maybe your lat/lng attributes are named something else). I don't think you need the IF call around that custom attribute like in the docs, but if things aren't working when they should be, maybe give it a shot. Should be good in a facets call too.
Great ! Thank you so much. I just needed to correct a little your syntax (some parenthesis missing) in order to get it work.
I had to add per_page and page arguments too, don't know really why.
logic = ["city_id = #{#city.id}",
"IN(mobile_cities_ids, #{#city.id})",
"GEODIST(latitude, longitude, #{#search_latitude * Math::PI / 180}, #{#search_longitude * Math::PI / 180}) < (mobility_distance * 1000)"]
search_set_logic = search_set.merge({:valid => true})
#users_to_display = User.search :sphinx_select => "*, (#{logic.join(" OR ")}) AS valid",
:with => search_set_logic.dup,
:sort_mode => :extended,
:order => "visibility DESC, last_login_at DESC",
:page => params[:page], :per_page => 10
Related
In my view, I've got a fiddly loop which creates 500 SQL queries (to get the info for 500 books). How can I avoid lots of SQL queries by loading a variable up in the controller?
My current (pseudo) code:
controller index action:
#books = Book.scoped.where(:client_id => #client.id).text_search(params[:query])
#feature_root = Book.multiple_summary_details_by_category( #books )
#...returns a hash of books
#features = #feature_root.to_a.paginate(:page => params[:page], :per_page => 4)
index.html.haml
= render :partial => "feature", :locals => { :features => #features }
_features.html.haml
- features.each_with_index do |(cat_name, array_of_books), i|
%h2
= cat_name
- array_of_books[0..10].each do |feature|
= link_to image_tag(feature[:cover], :class => "product_image_tiny"), book_path(feature[:book])
# more code
- array_of_books.sort_by{ |k, v| k["Author"] }.each do |feature|
- feature.each do |heading,value|
%span.summary_title
= heading + ':'
%span.summary_value
= value
What have you tried so far? It should be quite easy with standard ActiveRecord queries as documented in http://guides.rubyonrails.org/active_record_querying.html.
Also, instead of
array_of_books.sort_by{ |k, v| k["Author"] }
try something like
Book.order("author DESC")
(not sure about your exact model here) to let the db do the sorting rather than putting them in an array and let ruby handle it.
Hey I'm trying to figure something out..
I want to get all Carts that have a cart_stage.stage equal to '35' (this is a separate table and a cart has many stages)
the cart_stage table is a bit like
id ----- cart_id ----- stage
1 ------- 123 ---------- 20
2 ------- 123 ---------- 35
3 ------- 102 ---------- 35
I am trying this at the moment:
# Cart model
has_one :top_stage, :foreign_key => 'cart_id', :class_name => "CartStage", :order => 'stage'
# Cart controller
#carts = Cart.find :all, :order => 'created_at DESC', :include => :top_stage, :conditions => ["top_stage.stage = ?", 35]
This gives me :
SQLite3::SQLException: no such column: top_stage.stage: SELECT DISTINCT "carts".id FROM "carts" WHERE (top_stage.stage = 35) ORDER BY created_at DESC LIMIT 40 OFFSET 0
Hope it all makes sense and any help would be greatly appreciated,
Alex
That should probably be:
#carts = Cart.find(:all, :order => 'carts.created_at DESC', :include => :top_stage, :conditions => { "cart_stages.stage" => 35 })
Remember that you use the name of the table in the conditions, not the name of the association, however you do use the name of the association in the include option.
Whenever possible, you should probably use the hash method for expressing conditions to keep your declarations simple. It's when you need complicated OR or > type logic that the array-style proves necessary.
A better way of expressing it for Rails 3 is:
#carts = Cart.order('carts.created_at DESC').include(:top_stage).where('top_stages.stage' => 35)
It's not clear why your cart_stages table isn't being added with a JOIN.
Anywhere that you include literal SQL, you need to use the real table and column names, not the Rails association names. So you need to have: :conditions => ["cart_stage.stage = ?", 35] instead of top_stage
I managed to work it out my self actually.. What I wanted was -
#carts = Cart.find :all, :order => 'created_at DESC', :joins => :top_stage, :conditions => { :cart_stages => { :stage => 35 } }
Thanks for all the help guys! :)
The following query returns the collection of AR objects that I want to update:
Variant.all(:joins => { :candy_product => :candy }, :conditions => "candies.name = 'Skittles'")
I'm trying to do something like the following:
Variant.update_all(:price => 5, :joins => { :candy_product => :candy }, :conditions => "candies.name = 'Skittles'")
This should only update the price for the variants returned from original query. Is this possible with AR or will I have to write the SQL? This is a pretty large collection, so anything that iterates is out.
Using Rails 2.3.4.
As #François Beausoleil pointed correctly we should use scoped
Variant.scoped(:joins => { :candy_product => :candy }, :conditions => "candies.name = 'Skittles'").update_all(:price => 5)
on my site, I'm trying to display the hiking trails with the latest posted pictures (Scroll to the "Picture section: http://www.trailheadfinder.com/trail_search/latest_trails). However, the current query I use, is showing the trails in order of the "first" picture posted. So when a new picture is added at a later date, the trail does not show at the top. I have a trail table and a trailpicture table that are linked. Here is the current query I use:
#trails_pictures = Trailpicture.find(:all,
:limit => 20,
:include => [:trail],
:select => 'trailpictures.trail_id, trails.name, trails.short_description, trails.city, trails.state, trails.country',
:group => 'trailpictures.trail_id',
:conditions => ["trailpictures.parent_id is NULL"],
:order => 'trailpictures.id DESC')
Any help would be greatly appreciated!
Thank You,
Nick,
You need to order by created_at. :)
I finally figured it out. This is how it needs to be written:
#trails_pictures = Trail.find(:all,
:joins => 'INNER JOIN trailpictures ON trails.id = trailpictures.trail_id',
:limit => 20,
:conditions => ["trailpictures.parent_id is NULL"],
:select => 'trails.id, trails.name, trails.short_description, trails.city, trails.state, trails.country, max(trailpictures.id)',
:group => 'trails.id',
:order => 'max(trailpictures.id) DESC')
I have a an method that retrieves Groups that are present in certain areas. Groups are given a country_id, region_id and city_id
The UI gives three select boxes to choose a country, a region from that country and then a city from that region. To find all groups in a particular city, I have this code:
#groups = Group.find(:all, :conditions => {:city_id => params[:city_id]})
This all works fine, but I also want it to find all groups in an area when the lower criteria isn't specified. For example, If a country and region are given, but not city, I'd like to find it by the region.
What I'm doing is this:
if !params[:city_id].nil?
#groups = Group.find(:all, :conditions => {:city_id => params[:city_id]})
else
if !params[:region_id].nil?
#groups = Group.find(:all, :conditions => {:region_id => params[:region_id]})
else
#groups = Group.find(:all, :conditions => {:country_id => params[:country_id]})
end
end
This works perfectly well, but it seems like it's a little inefficient. Am I doing it the best way or can I streamline a little?
One idea I had was to have a single find checking against all parameters, but I could not work out how to effectively 'ignore' parameters that were nil - my main thought was to check which ones were not set and set them to something like '*' or 'true', but that's not how SQL plays the game.
Sounds like a job for named scopes:
class Group < ActiveRecord::Base
named_scope :in_city, lambda { |city_id| {
:conditions => { :city_id => city_id }
}}
named_scope :in_region, lambda { |region_id | {
:conditions => { :region_id => region_id }
}}
named_scope :in_country, lambda { |country_id | {
:conditions => { :country_id => country_id }
}}
end
This establishes some simple scopes for restricting the Group records. Presumably you have indexed your database properly so these are quick to resolve.
The controller is much easier to implement then:
def index
#group_scope = Group
if (!params[:city_id].blank?)
#group_scope = #group_scope.in_city(params[:city_id])
elsif (!params[:region_id].blank?)
#group_scope = #group_scope.in_region(params[:region_id])
elsif (!params[:country_id].blank?)
#group_scope = #group_scope.in_country(params[:country_id])
end
#groups = #group_scope.all
end
Generally you should be testing for .blank? instead of .nil? as some form elements can send in empty results, such as a select with something akin to "All" as the default.
You could use some Ruby idioms to get something a little more succinct.
Try something like this: (untested code!)
def index
#groups = Group.find :all, :conditions => [:city_id, :region_id, :country_id].inject {} do |conditions, name|
conditions[name] = params[name] unless params[name].blank?
conditions
end
end
If every value in params is a candidate for :conditions you can just do this:
#groups = Group.all(:conditions => params.reject { |idx, val| val.nil? })
This just throws out nil values from params and uses the remaining values for conditions.
If you don't want to use all of the values in params, you have two options. You can just get rid of a bunch of redundancy in your original code:
conditions = if !params[:city_id].nil?
{ :city_id => params[:city_id] }
elsif !params[:region_id].nil?
{ :region_id => params[:region_id] }
else
{ :country_id => params[:country_id] }
end
#groups = Group.all(:conditions => conditions)
You can knock of a few more lines like this, but it sacrifices a bit of readability IMO:
conditions = if !params[:city_id].nil? then { :city_id => params[:city_id] }
elsif !params[:region_id].nil? then { :region_id => params[:region_id] }
else { :country_id => params[:country_id] }
end
Or you can do something like this:
conditions = [:city_id, :region_id, :country_id].inject({}) do |hsh, sym|
hsh[sym] = params[sym] unless params[sym].nil?
hsh
end
#groups = Group.all(:conditions => conditions)
This has the advantage that you don't need to add another condition for each symbol.