A few months ago in a planning meeting, we were coming up with fun usecases for Algolia. Our typical process is to just throw a bunch of ideas at the wall at see what sticks, often before thinking through how we might actually accomplish some of these random suggestions. Somebody said, “let’s replace sommeliers” — I just laughed, but then an eerie quiet set in as the gears started turning, with all of us thinking about how this might actually be a feasible demonstration…
Let me be clear off the bat, this little exploratory jaunt is not meant to be a tutorial or a manual. If we manage to hold your attention through till the end though, you’ll have learned a couple key concepts that’ll help you in whatever you’re building with Algolia:
Before we jump right into building, let’s lay out exactly what we’re trying to make. Originally we had dreamt this up as a database of food and wine pairings, but the idea got simplified as a proof-of-concept. So instead, let’s create a database of all the useful, recognizable flavors, and then use Algolia Recommend to train an AI model to suggest pairings between those flavors. We’ll want some way for the dataset to evolve over time, so we’ll allow users to reinforce the strength of those combos in the AI model by “liking” certain recommended pairings. This is going to make it so that nobody can add ridiculous pairings in the model (I can already imagine somebody trying to force the model into recommending beer and spearmint or chocolate and peas or something like that).
Let’s take this in two steps, then: first, we’ll pull in all of the data and turn it into an index, and then we’ll build a GUI for it that incorporates our like button idea.
For this, we’ll need two lists: one of ingredients, and one of recipes that use those ingredients. I went online and combined a bunch of open-source lists on GitHub and ended up with a sorry mess of unstandardized data, and that wasn’t going to do, so I parsed it all with a few bodged JavaScript functions and some manual checking. The goal here was (a) to strip out all of the information that doesn’t directly affect the search (that information can be put in a database and connected to the Algolia index by objectID
if needed) and (b) to flatten the resulting data so all of the key pieces of information are at the root level of the object.
Here’s my list of ingredients after I found an Unsplash image for each of them. It’s essentially just a big JSON array, with each item in the array structured like this:
{
"name": "kiwi",
"objectID": "kiwi",
"image": "<https://images.unsplash.com/photo-1585059895524-72359e06133a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=Mnw0MDU1OTh8MHwxfHNlYXJjaHwxfHxraXdpfGVufDB8fHx8MTY3NTI4OTA4MQ&ixlib=rb-4.0.3&q=80&w=1080>"
}
Then I wrote a little JavaScript function to take each recipe in the lists I found, come up with a recipe ID and random recent timestamp for it, extract the ingredient list, remove the ingredients we didn’t have in our index already, and map each remaining ingredient left to a line in a new CSV file containing the ingredient’s name, the recipe ID, the timestamp, and this string: conversion,Ingredient Used
. Each line in the CSV looks like this:
allspice,121072214,1673629271,conversion,Ingredient Used
The CSV I ended up with is 17.6k lines long, and each of those lines is an event. We don’t care about exactly when they occurred — which is why the timestamps are random — but all of the other data is going to be used to train our AI model. The recipe ID we created is actually the analogue to a userToken
, which in a normal usecase, would represent a single checkout in an ecommerce application. All of the products (ingredients) bought (used) together in the same purchase (recipe) will be connected together inside the AI, making it slightly more probable that one of those products (ingredients) will be recommended as a user views another.
Let’s give Algolia all our data and get the AI model training! First, we need an Algolia account:
Next, we need to create a new application. Every plan includes Recommend out of the box, but you’ll notice that the free plan is capped. In your project, it’s likely that your initial testing phase will do just fine on the free plan, but you’ll probably need to upgrade to the pay-as-you-go plan for production because you’ll hit 10K requests fairly quickly with any reasonable user base.
It asks immediately for me to create my first index (because what’s the point in an Algolia account without one), so I just uploaded my flavors.json file.
On the far left of the screen, you’ll see the Search and Recommend tabs. If they’re minimized, they’ll look like this:
The pink one at the bottom is Recommend. In there, I just selected the Frequently Bought Together model, chose the applicable index, uploaded my CSV, let it train, and I’m done! It gives me some statistics as soon as training is done; as it turns out, my completely unrelated datasets and mediocre standardization actually did quite well!
In the preview, I can give this a whirl without having created my GUI yet. Apparently even watercress was represented in the model as a flavor worthy of combining with papaya, ham, and radish — I could get behind that. Personally, I would have never thought of watercress and papaya, but apparently this is a pretty common flavor combination, so it looks like the app is already doing its job!
Earlier, I mentioned that our “liking” system would allow user input to reinforce the data from the recipes, but it’ll actually allow new flavors to pop up in the recommendation lists too. Note what happens when I search for walnuts:
One of the options is watercress! There are no perfectly symmetrical relationships here — walnut and watercress have a match score of 35.75 per our most recent model, which gives watercress 3rd place in walnut’s rankings, but walnut 4th place in watercress’ rankings. So if walnut and watercress proved to be a super popular combination, users could use the like button on the watercress result of a walnut search to raise that match score enough for walnuts to beat out radishes in the initial search. That sounds pretty boring when we’re talking about leafing drupes and drooping leaves, but imagine these were products in your ecommerce store. As users themselves refine the AI model by making purchases, you’ll start to see particular items being shown to the exact type of customer to whom they’d most appeal. It’s a recipe for increased cart sizes, impulse buys, and therefore, revenue. And if you’re thinking of pitching this to the team at your company, you can build this exact proof-of-concept in just a few minutes and use Algolia’s built-in previews to demonstrate the clear benefits. I’m going to go a step further here and build a functioning GUI outside of my Algolia dashboard, something that I could pass around to my colleagues if they’d like to take a look outside of the pitch meeting.
Let’s move on to building a spot for us to display all of this.
<aside>💡 There are easy templates available if you’d like to speedrun this part for your demo — admittedly, it’s often easier to take a larger demo like this one and pare out all of the stuff you don’t need to show the product managers. In fact, that’s what I did here. The resulting repo is much simpler to parse, so I’ll just explain how it works.</aside>
The app I worked up to add a UI to our Recommend-powered functionality is a fairly generic Next.JS app. Here’s the base layout of the main src/App.tsx
file, the only page in the app:
import algoliarecommend from '@algolia/recommend';
import { useFrequentlyBoughtTogether } from '@algolia/recommend-react';
import { createAlgoliaInsightsPlugin } from '@algolia/autocomplete-plugin-algolia-insights';
import algoliasearch from 'algoliasearch';
import insights from 'search-insights';
import {
autocomplete,
getAlgoliaResults
} from '@algolia/autocomplete-js';
import React, {
createElement,
Fragment,
ReactElement,
useEffect,
useRef,
useState
} from 'react';
import { render } from 'react-dom';
import '@algolia/autocomplete-theme-classic';
import '@algolia/ui-components-horizontal-slider-theme';
import './App.css';
import {
appId,
apiKey,
indexName
} from './config.js';
const searchClient = algoliasearch(appId, apiKey);
const recommendClient = algoliarecommend(appId, apiKey);
insights('init', { appId, apiKey, useCookie: true });
const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({ insightsClient: insights });
const IngredientSuggestions = ({ currentObjectID, name }) => { };
const App = () => { };
export default App;
This seems like a lot, but the lines split into a few tidy categories:
IngredientSuggestions
is a component to encapsulate our Recommend functionality. It’s not really needed to separate it out since we’re not reusing it anywhere, but it makes the code easier to read.App
is our main rendering component containing the entire page.Zooming in a little further, this is the App
component:
const App = () => {
const [selectedResult, setSelectedResult] = useState(null);
const autocompleteContainerRef = useRef(null);
useEffect(() => {
if (!autocompleteContainerRef.current) return undefined;
const search = autocomplete({
container: autocompleteContainerRef.current,
renderer: { createElement, Fragment },
render({ children }, root) {
render(children as ReactElement, root);
},
placeholder: "Search for an ingredient",
plugins: [algoliaInsightsPlugin],
openOnFocus: true,
defaultActiveItemId: 0,
getSources: ({ query }) => [
{
sourceId: 'suggestions',
getItems: () => getAlgoliaResults({
searchClient,
queries: [
{
indexName,
query,
params: {
hitsPerPage: 12,
clickAnalytics: true
}
}
]
}),
getItemInputValue: ({ item }) => item.name,
onSelect: ({ item }) => {
setSelectedResult({
objectID: item.objectID,
name: item.name,
image: item.image
});
},
templates: {
item({ item, components, state, html }) {
return createProductItemTemplate({
hit: item,
components,
insights: state.context.algoliaInsightsPlugin.insights,
html
});
},
item: ({ item, ...params }) => (
<div className="result autocompleteSuggestion">
<img src={item.image} />
<span>{item.name}</span>
</div>
)
}
}
]
});
console.log(search)
return () => {
search.destroy();
};
}, []);
return (
<>
<p id="intro">Welcome to the flavor pairing database! Search for flavors below.</p>
<div ref={autocompleteContainerRef} />
{selectedResult ? (
<IngredientSuggestions
currentObjectID={selectedResult.objectID}
name={selectedResult.name}
/>
) : (
<p id="getStarted">Start typing in the name of an ingredient to see what other ingredients are commonly used with it!</p>
)}
</>
);
};
Again it might seem like a lot, but it breaks down into tidy pieces:
useEffect
hook. Here, we instantiate the autocomplete functionality, passing it the container to hydrate, the React tools to render the component, a link to our insights plugin, what to do when the user selects an option, how to actually display each option in the autocomplete list, and a couple other miscellaneous details.The IngredientSuggestions
component is where the actual flavor pairing logic happens that we drew up earlier:
const IngredientSuggestions = ({ currentObjectID, name }) => {
const { recommendations, queryID } = useFrequentlyBoughtTogether({
recommendClient,
indexName,
objectIDs: [currentObjectID],
maxRecommendations: 3,
queryParameters: {
analytics: true,
clickAnalytics: true
}
});
return (
<>
<h1 id="heading">What flavors pair well with {name}?</h1>
{recommendations.length != 0
? (
<div id="usedWith">
{recommendations.map(({name, image, objectID}) =>
<div className="result" key={objectID}>
<img src={image} />
<span>{name}</span>
<div
title="I like this combo"
onClick={e => {
e.target.classList.add("liked");
insights(
'convertedObjectIDsAfterSearch',
{
eventName: 'Pairing Liked',
index: indexName,
objectIDs: [currentObjectID, objectID],
queryID
}
);
}}
>👍</div>
</div>
)}
</div>
)
: (
<p>We don't have any data on this.</p>
)
}
</>
)
};
This component skips the default FrequentlyBoughtTogether
component and jumps right to the hook that powers it, replacing the default component altogether. A recent update in Algolia’s Recommend JS implementation allows us to fetch both the recommended results and the query ID from this hook, so then in the JSX that this component actually renders, we can include the like button with an onClick
event that sends the query ID back to Algolia, this time in the form of a conversion event that reinforces the connection between the flavor we’re viewing and the flavor we’re clicking the like button on.
That’s it! Here’s the repo if you’d like to see the rest of the repo, like how the config file is structured or what the CSS looks like.
At the beginning of the article, I made a couple promises about what you’d learn; let’s see if we can recap the lessons.
And sidenote: if you do end up building a cool demo and convincing your team to go with Algolia based off of it, shoot us an email! We’d love to partner up and make some content about it 🙂 Happy building!
Jaden Baptista
Powered by Algolia AI Recommendations