Plan to Eat Web — Nutrition Calculator
The Problem
Plan to Eat did a survey of its customers asking what features they were interested in seeing down the road. One of the most requested features by far was a way to calculate the Nutrition Facts for the recipes in the app.
The Solution
I developed a couple of new tables in the Plan to Eat database: nutritions
, which was our local copy of imported JSON data from the USDA’s public database, and nutrition_calculateds
, which associated the USDA’s data with a recipe ingredient a user wanted the Nutrition Facts for and contained the data I had selected from the USDA.
On the front end, the user can click a “Calculate Nutrition Facts” button in the web and mobile apps. If any nutritional information existed on the recipe before the calculation, they must confirm that they want to overwrite the changes with the data provided. In the web app, users can also flag an ingredient’s nutrition for review by our team if it looks inaccurate, as it is very possible that the wrong nutrition was associated with the ingredient, or that the data was scaled improperly.
In the web app’s admin portal, admins can review flagged nutritions and update them so more accurate data is presented to users. A nutrition can be flagged by a user, and it can also be flagged during the calculation process if it appears that important data was missing. Calculated nutritions can be associated with many ingredients based on their stemmed titles, so reviewing one nutrition can potentially update many ingredients for many users.
Challenges
To avoid unnecessary network requests and potential rate limiting, I chose to import the USDA’s data into Plan to Eat’s
nutritions
table. This import process was complex for a host of reasons.I wanted to be able to not only import the data, but also update it if the USDA revised the data over time. I therefore developed a rake task that may either create a new record in the table or update an existing one, depending on whether the USDA’s ID was already present in the database.
The USDA does not have one nutrition database, it has four, and they store nutritional data differently. One of them stores Nutrition Facts the way you might expect to see them on a label at a grocery store, but the other three store nutrients per 100 grams (for solids) or per 100 milliliters (for liquids). They do not have densities stored directly on the nutritions, but instead have “portions” that can be used to translate a volumetric measurement (a cup of flour) or an instance measurement (six almonds) into a mass measurement.
Nutrition Facts from the USDA had multiple fields for the same nutrient on a food. It required additional research to understand how the Atwater General calculation of calories differs from the Atwater Specific calculation, or how carbohydrates can be measured by difference or by summation.
Matching recipe ingredients to nutritions based on user input was exceptionally challenging.
It was clear from the start that it would be inefficient to recalculate the same nutrition over and over for the same ingredient. Therefore, I associated a stemmed title with each
nutrition_calculated
. Before calculating the nutrition for an ingredient, the app searches the database for an existing calculation with the same stemmed title. Plan to Eat already used stemmed titles to associate planned recipe ingredients as one shopping list item, so I reused most of that logic to make “onion,” “onions,” “chopped onions,” and “diced onions” all refer to the same Nutrition Facts.It also became clear that having a way to correct the data over time would be essential to the Nutrition Facts feature, because it was so difficult to get the correct results. I migrated the database so recipe ingredients can each belong to a calculated nutrition, and added the appropriate belongs_to and has_many relationships in the Rails models.
Searching for existing calculations was handled by ActiveRecord and MySQL directly, because all it had to do was return the first matching entry. Searching for USDA nutritional data was handled by Searchkick, a Rails gem that integrates with Elasticsearch. This allowed for performant search that could be configured to permit errors and discrepancies (such as minor typos or a missing word), result limits from the ~400,000 nutrition entries, and to consider various heuristics. For example, in the USDA database, the first result when searching for “steak” is typically “steak sauce,” because the word “steak” appears first in the string “steak sauce.” But if a user typed “steak,” I configured Searchkick to understand they probably meant “beef steak,” which was the name of most entries for “steak” in the USDA’s database.
Even if the right nutrition is found, scaling can often be difficult.
As mentioned previously, many entries in the database have portions rather than a specific density, so I had to define an approach to parsing them for density conversions.
Sometimes a user would enter information relevant to the ingredient’s unit in the title or notes field. I had to consider all of the fields for a given ingredient to return accurate results. For example, an ingredient might have an amount of “4,” an empty unit, and a title of “6-ounce steaks.” In this case, I need to know the nutrition for 24 ounces of steak, then divide that by the number of servings in the recipe, so that had to be parsed out of the ingredient.
Even if the title was stemmed and irrelevant words were stripped, sometimes the results did not include conversion information. However, every food had a category, so I provided default densities for each category as a fallback.
Technologies Used
Ruby on Rails
Rails MVC architecture
Rails service model
Rspec testing
Memo Wise caching library
Pundit gem for security policies in controllers