Location based search in Rails using geokit problem - ruby-on-rails

I have a rather complex (or so it seems) geokit based location search on my site. In short, I have a model named "Campaigns" that belongs to another model called "Businesses". When I am searching on the site, users are searching for "Campaigns", but I want all appropriate models to show up in the results if they search for that Campaigns business name. For this I am doing a joins in the search.
I additionally have the geokit plugin and gem installed so that users can get results for these searches only within a set distance from the origin location (which they provide). However, I am getting strange results when I add this location functionality into the site.
If I use the following search (simplified for brevity, but tested in the console):
Campaign.find(:all,
:joins => :business,
:conditions => ['businesses.name LIKE ?', "%Koo Koo Roo%"]
)
I get the appropriate result, which is:
[#<Campaign id: 12, user_id: 4, business_id: 8, created_at: "2011-01-14 16:22:31", updated_at: "2011-01-14 16:25:20", lat: #<BigDecimal:2ad295a9eda8,'0.34154891E2',18(18)>, lng: #<BigDecimal:2ad295a9ed08,'-0.118358834E3',18(18)>>]
But if I try to add geokit based location search parameters onto this search, like so:
Campaign.find(:all,
:joins => :business,
:origin => "90066",
:within => 25,
:conditions => ['businesses.name LIKE ?', "%Koo Koo Roo%"]
)
I get the following result:
[#<Campaign id: 8, user_id: 4, business_id: 8, created_at: "2011-01-14 16:25:20", updated_at: "2011-01-14 16:25:20", lat: #<BigDecimal:2ad29933e618,'0.34154891E2',18(18)>, lng: #<BigDecimal:2ad29933e578,'-0.118358834E3',18(18)>>]
Which is almost identical. The only difference is, for the second result, it seems to be passing the business_id as the Campaign id. I have verified this twice, and both times it is the same thing, the campaign id gets replaced with the business_id.
I should mention, that this is true no matter what :within distance i enter.
What am I doing wrong here? Am I missing something? I can't seem to figure it out, it all looks sound to me, but apparently not! I don't understand how the results could be screwed up like this by geokit.
in my models I simply have:
Business.rb
has_many :campaigns
Campaign.rb
belongs_to :business
Any help would be appreciated. Am I missing some sort of association? I don't have geokit caching turned on.
Thanks!

It's because the "id" field is being overwritten by dodgy join code. When you join, you've got two "id" fields (one for business and one for campaign). without an explicit instruction as to which is "the" id, the DB guesses.
Unfortunately, the one of the versions of rails has a bug where it did not explicitly state that the main Active Record id (in this case Campaign.id) was the id that counts... and the db was guessing the wrong one, overwriting id with the Business.id.
You've already discovered the easy (but more hacky) fix... the other is upgrading rails.

Related

Return name in ActiveRecord relation along with foreign key id

I have a Sub-Component model which can belong to other sub-components. My Model looks like this:
class SubComponent < ApplicationRecord
belongs_to :parent, class_name: "SubComponent", foreign_key: "parent_sub_component_id", optional: true
has_many :child_sub_components, class_name: "SubComponent", foreign_key: "parent_sub_component_id"
validates_presence_of :name
end
This model is fairly simple, it has a name field and a parent_sub_component_id which as the name suggests is an id of another SubComponent.
I'd like to generate a query that returns all of the SubComponents (with their id, name, and parent_sub_component_id) but also includes the actual name of it's parent_sub_component.
This seems like it should be pretty simple but for the life of me I can't figure out how to do it. I'd like for this query to be done in the database rather than doing an each loop in Ruby or something like that.
EDIT:
I'd like for the output to look something like this:
#<ActiveRecord::Relation [#<SubComponent id: 1, name: "Parent Sub", parent_sub_component_id: nil, parent_sub_component_name: nil created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">, #<SubComponent id: 2, name: "Child Sub", parent_sub_component_id: 1, parent_sub_component_name: "Parent Sub" created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">]>
You can do this efficiently using an each loop if you use includes:
SubComponent.all.includes(:parent).each do |comp|
comp.parent.name # this gives you the name of the parent
end
What includes does is it pre-fetches the specified association. That is, ActiveRecord will query all subcomponents, and then in a single query also pull down all the parents of those subcomponents. When you subsequently access comp.parent in the loop, the associated parent will already be loaded, so this will not result in a so-called N+1 query.
The queries that AR will generate for you automatically will look something like this:
SELECT `subcomponents`.* FROM `subcomponents`
SELECT `subcomponents`.* FROM `subcomponents` WHERE `subcomponents`.`id` IN (1, 3, 9, 14)
If you need to use the name of the parent in a where condition, includes will not work and you will have to use joins instead to actually generate an SQL JOIN.
This is untested, but should get you started in the right direction, you can do this in Arel by doing something like
def self.execute_query
parent_table = Arel::Table.new(:sub_component).alias
child_table = Arel::Table.new(:sub_component)
child_table.join(parent_table, Arel::Nodes::OuterJoin).on(child_table[:parent_sub_component_id].eq(parent_table[:id]).project(child_table[:id], child_table[:name], parent_table[:id], parent_table[:name])
end
This results in a query like
SELECT "sub_component"."id", "sub_component"."name", "sub_component_2"."id", "sub_component_2"."name" FROM "sub_component" LEFT OUTER JOIN "sub_component" "sub_component_2" ON "sub_component"."parent_sub_component_id" = "sub_component_2"."id"
this is just off the top of my head by looking at Rails/Arel and probably needs a some work, but the query looks about what I would expect and this should get you going.

Find by multiple conditions in rails

I want to search a table with multiple conditions in Rails. I am using Active record and rails version 3.1.0.
I have Movies object, and want to achieve the equivalent of the following in rails:
Select * from Movies where rating = 'R' OR rating = 'PG'
I tried the following but it does not work
#filtered = Movies.find(:all, :conditions => { :rating => 'R', :rating => 'PG' })
Can you please provide help to write an equivalent of SQL query mentioned above.
One way would be to build an "IN" condition with:
#filtered = Movie.where(:rating => ['R', 'PG']).all
EDIT: I changed your class to "Movie" from "Movies". I assume that's what you will want.
In Rail 4, find with multiple conditions for example consider find Profile with first_name and last_name
Profile.find_by first_name: 'stack', last_name: 'flow'
Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself.
If no record is found, returns nil
Profile.find_by! first_name: 'stack', last_name: 'flow'
Like find_by, except that if no record is found, raises an ActiveRecord::RecordNotFound error.
For more information read Rails Finder Method
1: http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find_byIn Rail 4, find with multiple conditions for example consider find Profile with first_name and last_name
Profile.find_by first_name: 'stack', last_name: 'flow'
Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself.
If no record is found, returns nil
Profile.find_by! first_name: 'stack', last_name: 'flow'
Like find_by, except that if no record is found, raises an ActiveRecord::RecordNotFound error.
For more information read Rails Finder Method
i guess that would be
Movie.where("rating = ? OR rating = ?", 'R', 'PG')
have a look at the guides for more info: http://guides.rubyonrails.org/active_record_querying.html#conditions
i would recommend using an IN statement instead.
You can do it using:
Movie.where(:rating => ['R','PG'])

Searching with thinking_sphinx and filtering results

I have this scenario where I thought it would be pretty basic, but found out that I can't really achieve what I need. This is why I have this question for a thinking_sphinx's expert.
The scenario is this: I need do a search within a list of companies and only return those who has an address (there can be many address by company) which belongs to a particular city or none at all (this I can do).
I have the following models :
class Company < ActiveRecord::Base
has_many :company_addresses
define_index
indexes :name
indexes :description
indexes :keywords
end
end
and
class CompanyAddress < ActiveRecord::Base
end
The CompanyAddress has a city_id property. Without looping through all returned records from a sphinx search, is there a way to achieve the same thing more easily?
I'm using Rails 3.0.3 and thinking_sphinx.
You'll want to add an attribute pointing to the city_id values for the company:
has company_addresses.city_id, :as => :city_ids
And then you can filter on Companies belonging to a specific city:
Company.search 'foo', :with => {:city_ids => #city.id}
If you want both matching to a specific city or has no cities, that's a little trickier, as OR logic for attribute filters is more than a little tricky at best. Ideally what you want is a single attribute that contains either 0, or all city ids. Doing this depends on your database, as MySQL and Postgres functions vary.
As a rough idea, though - this might work in MySQL:
has "IF(COUNT(city_id) = 0, '0', GROUP_CONCAT(city_id SEPARATOR ',')",
:as => :city_ids, :type => :multi
Postgres is reasonably similar, though you may need to use a CASE statement instead of IF, and you'll definitely want to use a couple of functions for the group concatenation:
array_to_string(array_accum(city_id, '0')), ',')
(array_accum is provided by Thinking Sphinx, as there was no direct equivalent of GROUP_CONCAT in PostgreSQL).
Anyway, if you need this approach, and get the SQL all figured out, then your query looks something like:
Company.search 'foo', :with => {:city_ids => [0, #city.id]}
This will match on either 0 (representing no cities), or the specific city.
Finally: if you don't reference the company_addresses association anywhere in your normal fields and attributes, you'll need to force to join in your define_index:
join company_addresses
Hopefully that provides enough clues - feel free to continue the discussion here or on the Google Group.

Sqlite3 activerecord :order => "time DESC" doesn't sort

rails 2.3.4, sqlite3
I'm trying this
Production.find(:all, :conditions => ["time > ?",
start_time.utc], :order => "time DESC",
:limit => 100)
The condition works perfectly, but I'm having problems with the :order => time DESC.
By chance, I discovered that it worked at Heroku (testing with heroku console), which runs PostgreSQL. However, locally, using sqlite3, new entries will be sorted after old ones, no matter what I set time to. Like this (output has been manually stripped): second entry is new:
Production id: 2053939460, time: "2010-04-24 23:00:04", created_at: "2010-04-24 23:00:05"
Production id: 2053939532, time: "2010-04-25 10:00:00", created_at: "2010-04-27 05:58:30"
Production id: 2053939461, time: "2010-04-25 00:00:04", created_at: "2010-04-25 00:00:04"
Production id: 2053939463, time: "2010-04-25 01:00:04", created_at: "2010-04-25 01:00:04"
Seems like it sorts on the primary key, id, not time. Note that the query works fine on heroku, returning a correctly ordered list! I like sqlite, it's so KISS, I hope you can help me...
Any suggestions?
UPDATE/SOLVED:
time is a reserved sqlite3 keyword (date, amongst others, is too). This is why :order => 'time DESC' works in PostgreSQL (non-reserved keyword), but not in sqlite3. The solution is to avoid having sqlite3 keywords as column names if you ever intend to sort on them. Renaming solves the problem.
I've tested with the standard rails pattern updated_at and created_at, which works perfectly.
I still prefer sqlite3 in development, it's so simple and smooth to work with, copy the database and send to your partner. Thanks to #newtover !
It is usually a bad idea to use reserved words without surrounding quotes. time is a built-in function in SQLite, try using the following instead and better get rid of the ambiguity in the first place:
Production.find(:all,
:conditions => ["`time` > ?", start_time.utc],
:order => "`time` DESC",
:limit => 100)
UPD: The problem seems to have appeared on SO:
Rails Active Record find(:all, :order => ) issue

Case-insensitive search in Rails model

My product model contains some items
Product.first
=> #<Product id: 10, name: "Blue jeans" >
I'm now importing some product parameters from another dataset, but there are inconsistencies in the spelling of the names. For instance, in the other dataset, Blue jeans could be spelled Blue Jeans.
I wanted to Product.find_or_create_by_name("Blue Jeans"), but this will create a new product, almost identical to the first. What are my options if I want to find and compare the lowercased name.
Performance issues is not really important here: There are only 100-200 products, and I want to run this as a migration that imports the data.
Any ideas?
You'll probably have to be more verbose here
name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first
model ||= Product.create(:name => name)
This is a complete setup in Rails, for my own reference. I'm happy if it helps you too.
the query:
Product.where("lower(name) = ?", name.downcase).first
the validator:
validates :name, presence: true, uniqueness: {case_sensitive: false}
the index (answer from Case-insensitive unique index in Rails/ActiveRecord?):
execute "CREATE UNIQUE INDEX index_products_on_lower_name ON products USING btree (lower(name));"
I wish there was a more beautiful way to do the first and the last, but then again, Rails and ActiveRecord is open source, we shouldn't complain - we can implement it ourselves and send pull request.
If you are using Postegres and Rails 4+, then you have the option of using column type CITEXT, which will allow case insensitive queries without having to write out the query logic.
The migration:
def change
enable_extension :citext
change_column :products, :name, :citext
add_index :products, :name, unique: true # If you want to index the product names
end
And to test it out you should expect the following:
Product.create! name: 'jOgGers'
=> #<Product id: 1, name: "jOgGers">
Product.find_by(name: 'joggers')
=> #<Product id: 1, name: "jOgGers">
Product.find_by(name: 'JOGGERS')
=> #<Product id: 1, name: "jOgGers">
You might want to use the following:
validates_uniqueness_of :name, :case_sensitive => false
Please note that by default the setting is :case_sensitive => false, so you don't even need to write this option if you haven't changed other ways.
Find more at:
http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of
Several comments refer to Arel, without providing an example.
Here is an Arel example of a case-insensitive search:
Product.where(Product.arel_table[:name].matches('Blue Jeans'))
The advantage of this type of solution is that it is database-agnostic - it will use the correct SQL commands for your current adapter (matches will use ILIKE for Postgres, and LIKE for everything else).
In postgres:
user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])
Quoting from the SQLite documentation:
Any other character matches itself or
its lower/upper case equivalent (i.e.
case-insensitive matching)
...which I didn't know.But it works:
sqlite> create table products (name string);
sqlite> insert into products values ("Blue jeans");
sqlite> select * from products where name = 'Blue Jeans';
sqlite> select * from products where name like 'Blue Jeans';
Blue jeans
So you could do something like this:
name = 'Blue jeans'
if prod = Product.find(:conditions => ['name LIKE ?', name])
# update product or whatever
else
prod = Product.create(:name => name)
end
Not #find_or_create, I know, and it may not be very cross-database friendly, but worth looking at?
Similar to Andrews which is #1:
Something that worked for me is:
name = "Blue Jeans"
Product.find_by("lower(name) = ?", name.downcase)
This eliminates the need to do a #where and #first in the same query. Hope this helps!
Another approach that no one has mentioned is to add case insensitive finders into ActiveRecord::Base. Details can be found here. The advantage of this approach is that you don't have to modify every model, and you don't have to add the lower() clause to all your case insensitive queries, you just use a different finder method instead.
Upper and lower case letters differ only by a single bit. The most efficient way to search them is to ignore this bit, not to convert lower or upper, etc. See keywords COLLATION for MSSQL, see NLS_SORT=BINARY_CI if using Oracle, etc.
Find_or_create is now deprecated, you should use an AR Relation instead plus first_or_create, like so:
TombolaEntry.where("lower(name) = ?", self.name.downcase).first_or_create(name: self.name)
This will return the first matched object, or create one for you if none exists.
An alternative can be
c = Product.find_by("LOWER(name)= ?", name.downcase)
Case-insensitive searching comes built-in with Rails. It accounts for differences in database implementations. Use either the built-in Arel library, or a gem like Squeel.
There are lots of great answers here, particularly #oma's. But one other thing you could try is to use custom column serialization. If you don't mind everything being stored lowercase in your db then you could create:
# lib/serializers/downcasing_string_serializer.rb
module Serializers
class DowncasingStringSerializer
def self.load(value)
value
end
def self.dump(value)
value.downcase
end
end
end
Then in your model:
# app/models/my_model.rb
serialize :name, Serializers::DowncasingStringSerializer
validates_uniqueness_of :name, :case_sensitive => false
The benefit of this approach is that you can still use all the regular finders (including find_or_create_by) without using custom scopes, functions, or having lower(name) = ? in your queries.
The downside is that you lose casing information in the database.
You can also use scopes like this below and put them in a concern and include in models you may need them:
scope :ci_find, lambda { |column, value| where("lower(#{column}) = ?", value.downcase).first }
Then use like this:
Model.ci_find('column', 'value')
If you're using postgres (probably others), I like this solution.
Product.find_by("name ilike 'bLue JEaNS'")
I like this better for a couple reasons.
Clearer connection to database action -> you can just copy paste that into where ...
If you choose to add a wildard %, it's straightforward.
Assuming that you use mysql, you could use fields that are not case sensitive: http://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html
user = Product.where(email: /^#{email}$/i).first
Some people show using LIKE or ILIKE, but those allow regex searches. Also you don't need to downcase in Ruby. You can let the database do it for you. I think it may be faster. Also first_or_create can be used after where.
# app/models/product.rb
class Product < ActiveRecord::Base
# case insensitive name
def self.ci_name(text)
where("lower(name) = lower(?)", text)
end
end
# first_or_create can be used after a where clause
Product.ci_name("Blue Jeans").first_or_create
# Product Load (1.2ms) SELECT "products".* FROM "products" WHERE (lower(name) = lower('Blue Jeans')) ORDER BY "products"."id" ASC LIMIT 1
# => #<Product id: 1, name: "Blue jeans", created_at: "2016-03-27 01:41:45", updated_at: "2016-03-27 01:41:45">
You can use like this in model
scope :matching, lambda { |search, *cols|
where cols.flatten.map{|col| User.arel_table[col].matches("%#{search}%") }.inject(:or)
}
and use wherever you like this
User.matching(params[:search], :mobile_number, :name, :email)
You can pass multiple column for search
for single column search you can use like this
User.where(User.arel_table[:column].matches("%#{search}%"))
So far, I made a solution using Ruby. Place this inside the Product model:
#return first of matching products (id only to minimize memory consumption)
def self.custom_find_by_name(product_name)
##product_names ||= Product.all(:select=>'id, name')
##product_names.select{|p| p.name.downcase == product_name.downcase}.first
end
#remember a way to flush finder cache in case you run this from console
def self.flush_custom_finder_cache!
##product_names = nil
end
This will give me the first product where names match. Or nil.
>> Product.create(:name => "Blue jeans")
=> #<Product id: 303, name: "Blue jeans">
>> Product.custom_find_by_name("Blue Jeans")
=> nil
>> Product.flush_custom_finder_cache!
=> nil
>> Product.custom_find_by_name("Blue Jeans")
=> #<Product id: 303, name: "Blue jeans">
>>
>> #SUCCESS! I found you :)

Resources