Save a penny and find recipes with ingredients you already have at home 🥘.
- Website: https://savepenny.herokuapp.com/
- Git
- Ruby 3.1
- Postgres 13
- Heroku CLI
$ git clone git@github.com:frodsan/recipes.git
Run the following commands to install dependencies and setup the database.
$ bin/setup
You can start the server using the command given below.
$ bin/dev
And now you can visit the site with the URL http://localhost:3000.
The application help users to find recipes by the ingredients they have.
The dataset can be found in db/seeds/recipes-en.json
and it has been populated into the database by rails db:seed
. You can find the
code here: db/seeds.rb.
The format of the JSON objects found in the dataset is:
{
"title": "Golden Sweet Cornbread",
"cook_time": 25,
"prep_time": 10,
"ingredients": [
"1 cup all-purpose flour",
"1 cup yellow cornmeal",
"⅔ cup white sugar",
"1 teaspoon salt",
"3 ½ teaspoons baking powder",
"1 egg",
"1 cup milk",
"⅓ cup vegetable oil"
],
"ratings": 4.74,
"cuisine": "",
"category": "Cornbread",
"author": "bluegirl",
"image": "https://images.url/image.jpg"
}
I've created the recipes
table with the same structure as the JSON object for simplicity.
I've decided to use jsonb
as the type for the ingredients
field, so it's straightforward
to read it from Rails, but also, there is no need to do any data transformation since I planned
to use Postgres' search features.
Since the ingredients field uses jsonb
, it is possible to parse
each string in the JSON array into a tsvector
, a sorted list of
normalized lexemes:
# select to_tsvector('english', '[
# "1 cup all-purpose flour",
# "1 cup yellow cornmeal"
# ]') as ts_vector;
ts_vector
--------------------------------------------------------------------------------
'1':1,7 'all-purpos':3 'cornmeal':10 'cup':2,8 'flour':6 'purpos':5 'yellow':9
Then, I decided to use the websearch_to_tsquery
function, which provides
search capabilities sort of like the ones used by search engines:
# -- Search by cornmeal and flour
# select to_tsvector('english', '[
# "1 cup all-purpose flour",
# "1 cup yellow cornmeal"
# ]') @@ websearch_to_tsquery('cornmeal flour') as must_be_true;
must_be_true
--------------
t
# -- Search by cornmeal but not flour
# select to_tsvector('english', '[
# "1 cup all-purpose flour",
# "1 cup yellow cornmeal"
# ]') @@ websearch_to_tsquery('cornmeal -flour') as must_be_false;
must_be_false
--------------
f
This is how is implemented in the model:
class Recipe < ApplicationRecord
scope :by_ingredients, -> (query) {
where("to_tsvector('english', ingredients) @@ websearch_to_tsquery(?)", query)
}
end
Also, I've created an index on the function result to speed up the queries:
add_index :recipes, "to_tsvector('english', ingredients)", using: :gin
Explaining the query shows that the it uses the index:
> Recipe.by_ingredients('pasta, eggplant').explain
EXPLAIN for: SELECT "recipes".* FROM "recipes" WHERE (to_tsvector('english', ingredients) @@ websearch_to_tsquery('pasta, eggplant'))
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Index Scan using index_recipes_on_to_tsvector_english_ingredients on recipes (cost=0.11..6.11 rows=2 width=525)
Index Cond: (to_tsvector('english'::regconfig, ingredients) @@ websearch_to_tsquery('pasta, eggplant'::text))
(2 rows)
The rest is standard Rails code which can be found here: