How to include a custom SQL function - ruby-on-rails

I'm using find_by_sql to execute an SQL query.
I would like to be able to use Soundex and Levenshtein, but in order to use Levenshtein I need to include the function as a file.
This is my code so far:
info = params[:email].split('#')
name = info[0]
domain = info[1]
levenshtein = File.open("./lib/assets/mysql-function-levenshtein.sql")
results = Domain.find_by_sql(
"" + levenshtein + "
SELECT *
FROM domains
WHERE domain = '" + domain + "'"
)
I have no idea if simply including it in the query is even valid.
What's the best implementation?
By the way the file I'm trying to include is this:
https://github.com/vyper/levenshtein-sql

First off, I think you'd be better off just defining that function in your database with a migration, so you wouldn't have to define it again for every query where you want to use it:
class AddLevenShteinFunctionToDatabase < ActiveRecord::Migration
  def up
levenshtein = File.read("/path/to/levenshtein.sql")
    execute levenshtein
  end
def down
# maybe put some code here to delete the function
end
end
With this done, you could also add a scope to your Domain model for doing these kind of queries:
scope :levenshtein, lambda {|s1, s2| select("levenshein(#{s1}, #{s2})") }
With this, you should be able to write your queries something like this:
results = Domain.levenshtein("LEONARDO", "LEONARDU").where(:domain => domain)

Related

How to Dynamically Build an Active Record Query with Rails Scopes

I am looking for a better way to dynamically build a active record query without making a sql string.
The following method does a new search for every word in the search_str and returns the records that are returned by all the search scopes.
scope :multi_search, ->(search_str){
query = ''
if search_str.present?
search_str.split(' ').each do |x|
query += ".search('#{x}')"
end
eval(query[1..-1])
else
all
end
}
It works, but this is not a clean implementation with the use of eval. Is there a better way of doing this?
In a model method:
def self.multi_search(your_params)
scope = Model.scoped({})
your_params.split(' ').map{|v| scope = scope.search(v)}
scope
end

Multiple word search in one field

I have search functionality with this code in the 'search.rb' file:
votsphonebooks = votsphonebooks.where("address like ?", "%#{address}%") if address.present?
There are multiple fields, this is just one of them.
How can I successfully change this line into something like a map to include multiple words.
Eg. If they type in '123 Fake St' - it will look for exactly that, but I want it to search for '123', 'Fake', 'St'.
First thing you should do is split the address by spaces:
addresses = params[:address].split(" ")
Then what you need is a OR query, you could do it by using ARel.
t = VotsPhoneBook.arel_table # The class name is my guess
arel_query = addresses.reduce(nil) do |q, address|
q.nil? ? t[:address].matches("%#{address}%") : q.or(t[:address].matches("%#{address}%"))
end
results = Post.where(arel_query)
Try using REGEXP instead of LIKE:
address_arr = address.split(" ")
votsphonebooks = votsphonebooks.where('address REGEXP ?',address_arr.join('|')) unless address_arr.blank?

Rails dynamic attribute in where LIKE clause

I have a search method, which takes in a key value pair in argument and searches on an active record model via a LIKE query. But I am unable to get it to work. It doesn't take the key argument properly.
This is what my method looks like:
def search(key,value)
where('? LIKE ?',key,"%#{value}%")
end
The query it fires is ('name' LIKE '%air%') whereas it should fire (name LIKE '%air%')
Is there a way I could get this to work?
Warning: The solution proposed by #MKumar is very dangerous. If key is user-input, you just allowed SQL injection.
def search(key, value)
where("#{key} LIKE ?", "%#{value}%")
end
search("IS_ADMIN == 1 --", "")
Whoops!
The better way to do this would be to use Arel tables.
def search(key, value)
column = Model.arel_table[key.to_sym] # index into the columns, via a symbol
where(column.matches("%#{value}%"))
end
This cannot produce a SQL injection.
Try like this
def search(key,value)
where("#{key} LIKE ?","%#{value}%")
end

How to use ST_GeoHash and ST_MakePoint postgis functions in rails before save?

How to build in rails postgis point, then geohash and save them into database before send response to client? I would like to make it through ST_MakePoint and ST_GeoHash function, I prefer to avoid execute SQL and extracting data by [0]["st_makepoint"], if it is possible how to insert this functions to execute them automatically when inserting all attributes? I've installed squeel gem, maybe can I merge this functions to the query?
My current rails code:
before_save :set_geopoint
def set_geopoint
#attributes -> {"latitude" => 51.90,"longitude" => 16.42,"geopoint" => nil}
#self.geopoint = "ST_MakePoint(#{latitude}, #{longitude})")" #not working
#self.geopoint = ActiveRecord::Base.connection.execute("SELECT ST_MakePoint(#{latitude}, #{longitude})")[0]["st_makepoint"]
#self.geohash = "ST_GeoHash(#{self.geopoint})"
#self.geohash = ActiveRecord::Base.connection.execute("SELECT ST_GeoHash(ST_SetSRID(#{self.geopoint},4326),5);").first["st_geohash"]
end
I did it through SQL before trigger function but I'm still looking for rails approach.
CREATE FUNCTION geopoint_trigger() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO posts( geopoint ) VALUES( ST_GeomFromText('POINT(' || NEW.latitude || ' ' || NEW.longitude || ')') );
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
To generate a point that you can save in a postgis enabled database, you need to generate that point with a factory. What you're doing here:
self.geopoint = "ST_MakePoint(#{latitude}, #{longitude})")" #not working
is just setting self.geopoint to a string and trying to save it. As your database geopoint field is (I assume) set to accept a point, it fails.
I suggest you use the rgeo gem to add geo factories to your models. Add it to your Gemfile.
https://github.com/rgeo/rgeo
You need to make sure that in your migration, you are using a point as column type like this:
t.point :geopoint, geographic: true
In your Post model, you then need to specify a factory like this:
RGEO_FACTORY = RGeo::Geographic.spherical_factory(srid: 4326)
And you also need to tell rgeo what factory to use on your geopoint column.
set_rgeo_factory_for_column :geopoint, RGEO_FACTORY
Now in your before_save, simply do:
self.geopoint = RGEO_FACTORY.point(latitude, longitude)
and it should work...
EDIT
If you want to use Postgis functions in your Rails models to get the GeoHash for instance, then you could do something like this:
post_geohash = Post.select("ST_GeoHash(geopoint) as geohash").where(id: some_post_id).geohash
You could also create an instance method that does that on your Post model:
def geohash
Post.select("ST_GeoHash(geopoint) as geohash").where(id: id).geohash
end
Not sure this works as it's not tested but you get the idea.

Performing row operations on ActiveRecord objects prior to an aggregate function

I am trying to calculate a weighted average of a variable in my model based on a second variable in my model and I'm having trouble finding a way to do it through ActiveRecord.
class Employer < ActiveRecord::Base
attr_accessible :name, :number_of_employees, :average_age
def self.wt_avg_age
#return sum(number_of_employee * average_age)/sum(number_of_employees)
end
end
In straight SQL, I would use:
SELECT id, SUM(number_of_employees*average_age)/SUM(number_of_employees)
FROM employer
GROUP BY name
Can I execute something like this on an ActiveRecord relation in an eloquent way (i.e., without pulling down separate arrays and iterating through every record to get my numerator)? I have tried different combinations using .select(), .pluck(), and sum() without any luck. I'm having trouble getting the ActiveRecord object to perform the sumproduct.
You should be able to do something like:
Employer.select("name, (SUM(number_of_employees*average_age)/SUM(number_of_employees)) as sum").group(:name)
That will return Employer instances to you, but they will only have the .name and .sum attributes on them. This will run the exact SQL query that you wanted.
It looks like ActiveRecord::Calculations#sum takes a block:
# File activerecord/lib/active_record/relation/calculations.rb, line 92
def sum(*args)
if block_given?
self.to_a.sum(*args) {|*block_args| yield(*block_args)}
else
calculate(:sum, *args)
end
end
(also see http://api.rubyonrails.org/classes/Enumerable.html#method-i-sum)
So you might try:
def self.wt_avg_age
numerator = self.all.sum { |e| e.number_of_employee * e.average_age }
denominator = self.sum :number_of_employees
return numerator / denominator
end
Take a try, maybe it can works:
def self.wt_avg_age
a = Employer.sum("number_of_employee * average_age")
b = Employer.sum('number_of_employees')
a/b
end

Resources