Rails + PostgreSQL - Multiple Keyword Search using Full Text Search - ruby-on-rails

I have a simple Ruby on Rails app CookBook, with recipes. I have 2 tables (recipes and ingredients). One recipe has many ingredients.
I am trying to implement a simple search box so that I can filter recipes containing certain ingredients....
These are the tables:
create table recipes (id int, recipe_name varchar(100));
create table ingredients(id int, ingredient_name varchar(100), recipe_id int);
insert into recipes values
(1, 'my recipe 1'),
(2, 'my recipe 2');
(3, 'my recipe 3');
(4, 'my recipe 4');
insert into ingredients values
(1, 'Banana', 1),
(2, 'Milk', 1),
(3, 'Banana', 2),
(4, 'Milk', 2),
(5, '4 ½ teaspoons baking powder', 2);
(6, '1 ½ cups whole wheat flour', 4),
(7, 'Heavy Cream', 4);
(8, '⅓ cup packed brown sugar', 3),
(9, 'Milk', 3);
(10, '2 teaspoons packed brown sugar', 4);
Let's say I type in search box two ingredients, for example "banana" and "milk". Then I am expecting to get recipes with rows from ingredients table that contain those words...
This is the code in my recipes_controller I currently have:
def search
if params[:search].blank?
redirect_to recipes_path and return
else
#parameter = params[:search].downcase
#recipe_ids = Ingredient.search_ingredient(#parameter).pluck(:recipe_id)
#results = Recipe.where(id: #recipe_ids).paginate(page: params[:page], per_page: 26)
end
end
code in ingredient.rb:
class Ingredient < ApplicationRecord
belongs_to :recipe
include PgSearch::Model
pg_search_scope :search_ingredient,
against: { ingredient_name: 'A' },
using: {
tsearch: {
dictionary: 'english', tsvector_column: 'searchable'
}
}
end
The problem with this is that if I type 'Banana milk' in search box, I will not get anything in result set, because they are in separate rows in ingredients table, belonging to a recipe. In this case, I should get at least recipes with id 1 and 2, because they both have those ingredients
If I search for only one ingredient, like "milk" then I get all the recipes containing that ingredient.
How do I accomplish this?
Thank you in advance

I advise you to use any_word attribute, look at the documentation here - https://github.com/Casecommons/pg_search#any_word
pg_search_scope :search_ingredient,
against: { ingredient_name: 'A' },
using: {
tsearch: {
any_word: true,
dictionary: 'english',
tsvector_column: 'searchable'
}
}

Related

How can I find each ascendent of a record in a self-referential table without N+1

I am working with a self-referential model (Category) in a Ruby on Rails app, which has the following columns:
id, name, level, parent_id
belongs_to :parent, class_name: 'Category', optional: true
The concept is that a level 1 category can have a level 2 subcategory, which can have a level 3 subcategory, etc.
For example:
Category id: 1, name: 'Dessert', level: 1, parent_id: nil
Category id: 2, name: 'Cold', level: 2, parent_id: 1
Category id: 3, name: 'Cake', level: 3, parent_id: 2
Category id: 4, name: 'Ice Cream', level: 3, parent_id: 2
Category id: 5, name: 'Sponge', level: 4, parent_id: 3
I'd like to find each ascendent of a record, regardless of how many levels deep it is. I then want to concatenate all the names, in ascending order, into one string.
i.e., if I'm starting with Sponge, I'd like a method which returns "Dessert - Cold - Cake - Sponge"
What I have so far works but is an n+1 and doesn't feel very Railsy:
def self.concatenate_categories(order)
category = order.category
categories_array = []
order.category.level.times do
categories_array.push category.name
category = Category.find(category.parent_id) if category.parent_id.present?
end
categories_array.reverse.join(' - ')
end
If this order is for Sponge, I get "Dessert - Cold - Cake - Sponge".
If the order is for Cake, I get "Dessert - Cold - Cake".
You can try a recursive CTE to get each category parent based on its parent_id:
WITH bar AS (
WITH RECURSIVE foo AS (
SELECT
categories.id,
categories.name,
categories.parent_id
FROM categories
WHERE categories.id = 5
UNION
SELECT
p.id,
p.name,
p.parent_id
FROM categories p
INNER JOIN foo f
ON f.parent_id = p.id
) SELECT name FROM foo ORDER BY id
) SELECT STRING_AGG(name, ' - ') FROM bar
How about this? I haven't tested that code, but you get the idea, join as many times as there are levels and query once. Depending on how many levels there are, your solution can be faster than too many joins.
def self.concatenate_categories(order)
scope = order.category
categories_array = if category.level > 1
scope = scope.select('categories.name')
order.category.level.downto(1) do |l|
scope = scope.joins("JOIN categories as c#{l} ON categories.id = c#{l}.parent_id")
.select("c#{l}.name")
end
scope.to_a
else
Array.wrap(scope.name)
end
categories_array.reverse.join(' - ')
end

Convert simple string into array

I'm wanting to use the Code by Zapier action to convert a string like this: "product 1, product 2, product 3" into an array like this [product 1, product 2, product 3] so the subsequent Zapier actions will run for as many times as there are values in the array. What code do I need to enable this?
You can use the str.split method to achieve that:
>>> products_str = "product 1, product 2, product 3"
>>> products = products_str.split(', ')
>>> products
['product 1', 'product 2', 'product 3']

Rails - Query table with array of IDs and order based on the array

Let me describe with simple example.
I have a list of numbers:
ex: list = [ 3, 1, 2 ]
and I have a table in DB named Products which have 3 rows with product_id = 1, 2and 3.
Now I need to query Products sort by list values (3,1,2) so the result will be:
product_3
product_1
product_2
Query will be like:
product.sort(list) or product.order(list) or some other alternative pls
This should work for you:
list = [3, 1, 2]
Product.where(product_id: list).sort_by { |p| list.find_index(p.product_id) })

Single Postgres query to update many records using a local hash/array

I want to use a single query to update many records in my Postgres database using a Ruby hash or array, rather than having to iterate through each record and call a separate update.
# {:id => :color}
my_hash = {
1 => 'red',
2 => 'blue',
3 => 'green'
}
How I don't want to do it because it does three serial queries:
my_hash.each do |id, color|
MyModel.where(id: id).update_all(color: color)
end
How I do want to do it:
MyModel.connection.execute <<-SQL
UPDATE my_models
SET color=something
FROM somehow_using(my_hash)
WHERE maybe_id=something
SQL
You can use case:
update my_models
set color = case id
when 1 then 'red'
when 2 then 'blue'
when 3 then 'green'
end;
or save the hash in a separate table:
create table my_hash (id int, color text);
insert into my_hash values
(1, 'red'),
(2, 'blue'),
(3, 'green');
update my_models m
set color = h.color
from my_hash h
where h.id = m.id;
One more option, if you know the way to select the hash as jsonb:
with hash as (
select '{"1": "red", "2": "blue", "3": "green"}'::jsonb h
)
update my_models
set color = value
from hash, jsonb_each_text(h)
where key::int = id;
OP ruby-fying klin's third option:
sql = <<-SQL
with hash as (
select '#{my_hash.to_json}'::jsonb h
)
update my_models
set color = value
from hash, jsonb_each_text(h)
where key::int = id;
SQL
ActiveRecord::Base.connection.execute(sql)
For readers that might encounter this question in 2020+, it looks like upsert_all does what the OP wanted in Rails 6:
MyModel.upsert_all([{ id: 1, color: "red" }, { id: 2, color: "blue" }, { id: 3, color: "green" }])
Will generate something like
# Bulk Insert (26.3ms) INSERT INTO `my_models`(`id`,`color`)
# VALUES (1, 'red')...
# ON DUPLICATE KEY UPDATE `color`=VALUES(`color`)
Example inspired by this blog.
Another solution is to concatenate a series of updates into a single string and send that all at once. Downside is that it sends more data across the wire, but on the other hand PG doesn't have to deserialize and process the JSON.
ActiveRecord::Base.connection.execute(
my_hash.collect{|id,color| "UPDATE my_models SET color=#{color} WHERE id=#{id};"}.join('')
)
# And don't forget to sanitize the :id and :color values
You can do this with the Postgres VALUES function:
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES (1, 'red'), (2, 'blue'), (3, 'green'))
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
This works reasonably well even with hundreds of values. To do this in Ruby from a hash, use something like:
values = my_hash.map { |id, color| "(#{id}, '#{color}')" }.join(', ')
# => (1, 'red'), (2, 'blue'), (3, 'green')
stmt = <<~SQL
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES #{values})
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
SQL
MyModel.connection.exec_update(stmt)
Note though that you really don't want to do this with user input unless you can sanitize it first or you like SQL injection attacks. I imagine something like this would work, although I haven't actually tried it:
values = my_hash.keys.map { |id| "(#{id}, ?)" }.join(', ')
# => (1, ?), (2, ?), (3, ?)
sql = <<~SQL
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES #{values})
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
SQL
stmt = ActiveRecord::Base.sanitize_sql([sql, *my_hash.values])
MyModel.connection.exec_update(stmt)

Rails, converting an integer to array offset

Based on feedback, I am revising this question. How do i convert an integer array to a set of values displayed on the view page using the Constants (defined in the model). I can do it on my form page but have not figured it out for the Index.
On an Index Page (if dbase has grades: [0, 1, 2], the page should display as A+, A, B)
something like what is done for days of the week (e.g. http://hightechsorcery.com/2010/02/16/ruby-arrays-and-hashes-and-days-of-the-week/
....
<h4 class="h3"><%= #gradestemp %>
CONTROLLER Labels Controller
def index
#labels = current_user.labels
grad = []
#gradestemp = Contact::GRADES.each_with_index { |x, i| grad << [x, i] }
render
end
MODEL NB: GRADES is a constant - I am trying to also use in Labels
class Label < ActiveRecord::Base
NB: this is in the CONTACT model
GRADES = [["A+",0 ], ["A",1], ["B", 2], [ "C",3], [ "D",4], [ "-",5]]
Am i able to access the Contact GRADES while in the Labels controller?
i have found this SO - which is similar to what i am trying to do:
Ruby: How to store and display a day of the week?
Based suggestion below, this did the trick:
<h4 class="h3"><%= print_campaign.grades.compact.map{|idx| Contact::GRADES[idx][0]}.join(' ') %>
[0, 1, 2].map {|g| Contact::GRADES.select.map {|letter, val| val == g; letter}
Really though GRADES seems much better off as a Hash:
GRADES = {
"A+" => 0,
"A" => 1,
..
}
Then you lookup would be much simpler
[0, 1, 2].map {|g| Contact::GRADES.key(g) }

Resources