How to use scope in AR request with joins? - ruby-on-rails

In my case I have a model Product has_one Location
I use a geocoder gem to search location near a distance.
The request Location.near([0, 0], 100) is look like this:
SELECT locations.*, 6371.0 * 2 * ASIN(SQRT(POWER(SIN((0 - locations.latitude) * PI() / 180 / 2), 2) + COS(0 * PI() / 180) * COS(locations.latitude * PI() / 180) * POWER(SIN((1 - locations.longitude) * PI() / 180 / 2), 2) )) AS distance, CAST(DEGREES(ATAN2( RADIANS(longitude - 1), RADIANS(latitude - 0))) + 360 AS decimal) % 360 AS bearing FROM \"locations\" WHERE (6371.0 * 2 * ASIN(SQRT(POWER(SIN((0 - locations.latitude) * PI() / 180 / 2), 2) + COS(0 * PI() / 180) * COS(locations.latitude * PI() / 180) * POWER(SIN((1 - locations.longitude) * PI() / 180 / 2), 2) )) <= 20) ORDER BY distance
I want to do something like this:
Product.where(...).joins(:location).dosomething
How can I do it?

Location#near is a named scope? If so you can merge it with your Product scope using the & operator. I think this should work:
class Product
scope :near, lambda { |coord, dist| joins(:location) & Location.near(coord, dist) }
...
end
Then you can just use it like so:
Product.near([0, 0], 100)

Another possibility, since merging the scopes doesn't seem to work:
class Product
scope :near, lambda { |coord, dist| where(:id => Location.near(coord, dist).all.map(&:locationable_id) }
...
end
And usage like in the other answer:
Product.near([0, 0], 100)

Related

Rails - Use self column in sql string query in concern scope

So, I have this concern and scope
module GeocoordinatesHelpers
extend ActiveSupport::Concern
included do
scope :within_range, -> (lat, lon, range) { where("(6371.0 * 2 * ASIN(SQRT(POWER(SIN((self.latitude - :lat) * PI() / 180 / 2), 2) + COS(self.latitude * PI() / 180) * COS(:lat * PI() / 180) * POWER(SIN((self.longitude - :lon) * PI() / 180 / 2), 2)))) <= :range", {lat: lat, lon: lon, range: range})}
end
end
that I need to use across several models
Is it possible to refer to the models column that the scope is being called against? Like, replace the self bits in the where with something that actually works
For reasons, I can't just use the geocoder gem
If you really need the full column name, try changing self to #{table_name}.

How to chain these methods together (Geocoder + Pagination)

I currently have an array of locations using the geocoder gem:
#locations = Location.near([#lat, #lon], 3)
which produces:
=> [#<Location id: 18, address: "555 N 7th Ave, Jamaica, CA 66666", phone: "(623) 555-5555", latitude: 66.666, longitude: -200.082, user_id: 1, created_at: "2015-03-11 15:06:57", updated_at: "2015-03-11 15:06:57">,
#<Location id: 46, address: "555 N 7th Ave, Jamaica, CA 66666", phone: "(623) 555-5555", latitude: 66.666, longitude: -200.082, user_id: 1, created_at: "2015-03-11 15:06:57", updated_at: "2015-03-11 15:06:57">]
I'm trying to add pagination to these results with
#locations = Location.near([#lat, #lon], 3).paginate(:page => params[:page], :per_page => 5)
and in the view (haml):
= will_paginate #locations
But I'm getting the following error:
ActionView::Template::Error (Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '*, 3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((33.457341299999996 - locations.lati' at line 1: SELECT COUNT(locations.*, 3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((33.457341299999996 - locations.latitude) * PI() / 180 / 2), 2) + COS(33.457341299999996 * PI() / 180) * COS(locations.latitude * PI() / 180) * POWER(SIN((-112.07359500000001 - locations.longitude) * PI() / 180 / 2), 2))) AS distance, MOD(CAST((ATAN2( ((locations.longitude - -112.07359500000001) / 57.2957795), ((locations.latitude - 33.457341299999996) / 57.2957795)) * 57.2957795) + 360 AS decimal), 360) AS bearing) FROM `locations` WHERE (locations.latitude BETWEEN 33.41392176506674 AND 33.50076083493325 AND locations.longitude BETWEEN -112.12563828858949 AND -112.02155171141054 AND (3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((33.457341299999996 - locations.latitude) * PI() / 180 / 2), 2) + COS(33.457341299999996 * PI() / 180) * COS(locations.latitude * PI() / 180) * POWER(SIN((-112.07359500000001 - locations.longitude) * PI() / 180 / 2), 2)))) BETWEEN 0.0 AND 3)):
Any idea?
Have you tried rendering #locations separately in your template? This is the pattern I typically use:
= will_paginate
render #locations
= will_paginate
This should at least help you isolate the problem.

How to combine postgres group by count and where

Using Rails 4.2 and Postgres I've created the following query to give me a uniq list of bird ids in the Sightings table ordered by the number of sightings for each bird. Also note I'm using the Kaminari gem for pagination.
Sighting.select("bird_id, COUNT(sightings.id) as sightings_count").group(:bird_id).order('sightings_count DESC').page(1)
This works great returning the ActiveRecordRelation intended. The problem arises when i try to combine it with the geocoder gems .near method
Sighting.near([-31.0, 151.0], 1000, units: :km).select("bird_id, COUNT(sightings.id) as sightings_count").group(:bird_id).order('sightings_count DESC').page(1)
This generates the query and error
SELECT sightings.*, 6371.0 * 2 * ASIN(SQRT(POWER(SIN((-31.0 - sightings.lat) * PI() / 180 / 2), 2) + COS(-31.0 * PI() / 180) * COS(sightings.lat * PI() / 180) * POWER(SIN((151.0 - sightings.lng) * PI() / 180 / 2), 2))) AS distance, MOD(CAST((ATAN2( ((sightings.lng - 151.0) / 57.2957795), ((sightings.lat - -31.0) / 57.2957795)) * 57.2957795) + 360 AS decimal), 360) AS bearing, bird_id, COUNT(sightings.id) as sightings_count FROM "sightings" WHERE (sightings.lat BETWEEN -39.993216059187304 AND -22.006783940812696 AND sightings.lng BETWEEN 140.50821379697885 AND 161.49178620302115 AND (6371.0 * 2 * ASIN(SQRT(POWER(SIN((-31.0 - sightings.lat) * PI() / 180 / 2), 2) + COS(-31.0 * PI() / 180) * COS(sightings.lat * PI() / 180) * POWER(SIN((151.0 - sightings.lng) * PI() / 180 / 2), 2)))) BETWEEN 0.0 AND 1000) GROUP BY bird_id ORDER BY distance ASC, sightings_count DESC LIMIT 25 OFFSET 0
PG::GroupingError: ERROR: column "sightings.id" must appear in the GROUP BY clause or be used in an aggregate function
Adding id to the group by means the bird counts aren't correct and as far as I understood COUNT within select was an aggregate function which does include sightings.id.
How can I successfully combine the two?
Note: I did try the following but this returns a Hash rather than AR Relation.
Sighting.near([#lat, #lng], #range, units: :km, order:nil).group(:bird_id).order('count_id DESC').page(#page).count(:id)
Thanks for any help!!
Creating a custom near scope was the easiest work around as I wasn't using the bearing or distance attributes that get added to each record and hence the whole select sql generated by near I didn't need. Instead of select(options[:select]) I replaced it with the select I wanted.
scope :birds_by_sighting, lambda{ |location, *args|
latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
if Geocoder::Calculations.coordinates_present?(latitude, longitude)
options = near_scope_options(latitude, longitude, *args)
select("bird_id, COUNT(sightings.id) as sightings_count").group(:bird_id).where(options[:conditions]).order('sightings_count DESC')
else
# If no lat/lon given we don't want any results, but we still
# need distance and bearing columns so you can add, for example:
# .order("distance")
select(select_clause(nil, null_value, null_value)).where(false_condition)
end
}

Ruby Geocoder nearbys is slow

Using Rails 3.2, Ruby 1.9, geocoder gem. The following query I have in my controller:
# 1500+ms to load
nearby_shops = (current_shop.nearbys(10, :order => 'overall_rating DESC')
.where(:shop_type => shop_type).includes(:photos, :active_property_list =>
:hotel_image_lists).limit(5))
# SQL
Shop Load (4045.6ms) SELECT shops.*, 3958.755864232 * 2 * ASIN(SQRT(POWER(
SIN((36.111927 - shops.lat) * PI() / 180 / 2), 2) + COS(36.111927 * PI()
/ 180) * COS(shops.lat * PI() / 180) * POWER(SIN((-115.171229 - shops.lng)
* PI() / 180 / 2), 2))) AS distance, CAST(DEGREES(ATAN2( RADIANS(shops.lng
- -115.171229), RADIANS(shops.lat - 36.111927))) + 360 AS decimal) % 360 AS
bearing FROM `shops` WHERE `shops`.`shop_type` = 'food' AND (shops.lat BETWEEN
35.96719521688915 AND 36.25665878311085 AND shops.lng BETWEEN
-115.3503819353204 AND -114.9920760646796 AND 3958.755864232 * 2 *
ASIN(SQRT(POWER(SIN((36.111927 - shops.lat) * PI() / 180 / 2), 2) +
COS(36.111927 * PI() / 180) * COS(shops.lat * PI() / 180) * POWER(SIN((
-115.171229 - shops.lng) * PI() / 180 / 2), 2))) <= 10 AND shops.id != 85155)
ORDER BY overall_rating DESC LIMIT 5
- shops.lat) * PI() / 180 / 2), 2) + COS(48.8582411618 * PI() / 180) *
COS(shops.lat * PI() / 180) * POWER(SIN((2.2945044899 - shops.lng) * PI() /
180 / 2), 2))) <= 100 AND shops.id != 517) ORDER BY distance ASC LIMIT 25
OFFSET 0
The problem lies in the nearbys which does calculation on the longitude and latitude. I have already added indexes to the longitude and latitude columns, but it doesn't improve anything.
How can I improve this?
P/S: I removed unrelated conditions which doesn't contribute to the speed of the query.
This might not be the most exact or elegant solution, but why not just fudge the math a little. You could write a select clause that did something like this:
.select(["id, name, 69.0975851*sqrt(POWER(shops.lat-?,2) + COS(?*PI()/180)*POWER(shops.lon-?,2)) AS DISTANCE", loc.lat, loc.lat, loc.lon])
You don't really need to use that hairy great circle formula if you're just dealing with small (< 500 mile) distances. You'll end up getting the same ~almost~ answers for a fraction of the computational cost.

What explicit type casts should I add to make a no operator method work

I'm trying to use the near method of the Ruby geocoder gem in a Rails application with a postgres database.
#users = Contact.near(params[:search], params[:distance])
That line produces the 'operator does not exist error' you see below. The search parameter will be an address and the dist
In the Contact.rb model I do this
geocoded_by :full_address
def full_address
self.address + self.city + self.province + self.postal
end
Can you tell me what it is that I might be doing wrong? The error says, 'You might need to add explicit type casts.'
Address, city, province and postal are all strings.
PG::Error: ERROR: operator does not exist: numeric - character varying LINE 1: ...8.755864232 * 2 * ASIN(SQRT(POWER(SIN((43.7058645 - contacts... ^ HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts. : SELECT contacts.*, 3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((43.7058645 - contacts.latitude) * PI() / 180 / 2), 2) + COS(43.7058645 * PI() / 180) * COS(contacts.latitude * PI() / 180) * POWER(SIN((-79.3687137 - contacts.longitude) * PI() / 180 / 2), 2))) AS distance, CAST(DEGREES(ATAN2( RADIANS(contacts.longitude - -79.3687137), RADIANS(contacts.latitude - 43.7058645))) + 360 AS decimal) % 360 AS bearing FROM "contacts" WHERE (contacts.latitude BETWEEN 43.676918143377826 AND 43.73481085662217 AND contacts.longitude BETWEEN -79.40875589075141 AND -79.3286715092486 AND 3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((43.7058645 - contacts.latitude) * PI() / 180 / 2), 2) + COS(43.7058645 * PI() / 180) * COS(contacts.latitude * PI() / 180) * POWER(SIN((-79.3687137 - contacts.longitude) * PI() / 180 / 2), 2))) <= '2') ORDER BY distance ASC LIMIT 1

Resources