Categorization with NLP
Categorization with NLP
Ivan Sagalaev,
July 2026
Since launching my categorization tool Shoppy I've had some fun analyzing collected data which resulted in considerable complication of the prediction model. And now I feel the urge to write a deeper dive into its inner workings. I'm not sure how useful it would be for anyone who isn't a part of the Grocery Categorization industry, but hopefully some NLP tricks could be at least interesting to any general practitioner.
Please note that I'm by no means an NLP expert! Part of the reason for writing this kind of posts is to try and nerd-snipe someone who knows more into sharing their expertise.
Oh, and it's not a short one, this! Settle down :-)
Problem description
Just to remind everyone, what I'm trying to solve for my slowly developing shopping list app is suggesting grocery categories for products. So that it knows that "Milk" is dairy, "Apples" is produce and so on. This categorization helps with grouping and sorting, and it also looks nicer with colors and appropriate icons.
The usual approach to solving such problems is Machine Learning, and specifically — classification. That doesn't work for me though, as I couldn't come up with any means of collecting enough data myself, and hiring a consultancy is way above the budget of a tiny personal project.
So instead I'm manually crafting a clever algorithm with explicitly handled edge cases.
Lexing input
The first step is turning free-form input into something more formal and predictable: a set of lexemes. Step by step, it looks like this:
Lowercase the input string
Normalize it into the [NFD Unicode form][] — the one that separates accents from their characters, which lets me get rid of the former (not everyone bothers typing "crème fraîche" properly).
Split the string into "words", ignoring everything else like punctuation and whitespace. In my case words are defined as alpha-numeric characters and an apostrophe (because I want things like "7'up" to be one word).
For each word, get rid of apostrophes and stem them.
Sort the resulting list of word stems.
This gives me a normalized, stable input key independent of basic morphological variants:
Whipping cream |<br>whip cream | => ["cream", "whip"]<br>Cream (whipping) |
A note on stemming. I'm using the original Porter algorithm. It's widely supported, but is also the most simplistic. I don't really care about the correctness of the result from the point of view of the English language. As long as the algorithm used to produce the data is the same as the one used for checking against it, I'm good.
Terms database
The straightforward design for a database is just a mapping from a whole key to a category: ["cream", "whip"] => "dairy". But that would require listing all likely real-world products: all sorts of apples, peppers, beans, etc. Which doesn't work for me since, as I mentioned, I don't have a firehose of data to fill it up.
But you'll notice that mostly all such product are defined by one word: an apple is an apple and is produce, regardless of the sort. So let's reify this in the form of a CSV file:
appl,produce<br>cream,dairy<br>soap,body<br>...
These one-word keys are called unigrams. To match a search query, we can simply look up every separate unigram from it in the database.
It works for surprisingly many cases, but it breaks on things like "apple juice", because despite having "apple" in it, it's a beverage, not produce.
This is fixed by making the order of rows in the database significant, so that juic,drink goes before appl,produce. Then, if several of the unigrams in a search query match, the earliest one wins.
This might smell to you like something prone to potentially irresolvable ordering cycles, but I actually found that I only really need two groups of significance:
# Derivations
juic,drink<br>milk,dairy<br>oil,pantry
# Raw
appl,produce<br>oliv,snack<br>oat,pantry
Things like "juice", "milk" and "oil" are usually derived from something, as in "apple juice", "oat milk" and "olive oil". Keeping those derivations above raw ingredients ensures those tings are categorized correctly.
And don't take the word "raw" too literally. There are things like "ketchup" in there! But since nobody puts "tomato ketchup" on their shopping list, it is considered "raw" in my domain.
Bigrams
The next problem is combinations of words. "Spaghetti squash" is not a pasta, but a kind of squash. And "apple sauce" is neither produce, nor a condiment, but a snack! In both of these examples no single unigram is enough to correctly identify the product. This means I need to consider two-word terms called bigrams:
# Derivations
appl sauc,snack<br>sauc,condiment
# Raw
spaghetti squash,produce<br>appl,produce<br>spaghetti,pantry<br>squash,produce
All bigrams come before unigrams, as their purpose is to serve as more specific disambiguations of conflicting unigrams. But this ordering is less significant than the groups (Derivations and Raw), so each of...