AI

Solving a dinnertime dilemma with Algolia
facebooklinkedintwittermail

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:

  • How can I effectively squeeze every last bit of functionality out of my toolset?
  • Why are events so useful? How can I easily implement them?
  • How does the React library for Algolia Recommend work?
  • How can I sanitize and normalize a complex dataset for an Algolia index?
  • How do I whip up an Algolia proof-of-concept quickly?

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.

Building the index

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.

Building the GUI

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:

  1. The imports — here we’re bringing in the Algolia search, recommendation, insights, and autocomplete libraries, equipped with their necessary JavaScript and React add-ons. Then it imports React itself, some CSS, and a few more sensitive variables I defined in a separate file.
  2. The clients — here we give our Algolia credentials to the libraries and get back the main object we’ll be performing searches with.
  3. 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.
  4. 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:

  1. First we set up a ref that the Algolia autocomplete implementation will sit in and a state variable that defines what ingredient we’re looking at right now.
  2. Next, a 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.
  3. Lastly, the layout of our app. There’s some description, the autocomplete container, and depending on whether we’ve selected a flavor yet, recommended flavor pairings or more description.

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.

tl;dr

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.

  • How can I effectively squeeze every last bit of functionality out of my toolset?Even though Algolia gives specific usecases for tools like Search and Recommend, we’re not saying that’s the only way you can use them. As you can imagine, we cater to the widest portion of our audience by creating the most generic example apps and instructions we can, but our tools have nearly unlimited potential to help you accomplish your business goals. If you’re having trouble applying Search or Recommend to an unorthodox usecase, give us a shout! We’re here to help.
  • Why are events so useful? How can I easily implement them?Events power everything from analytics to AI-based recommendations. You can send them back to Algolia’s servers with REST calls or a straightforward client library in your language of choice. Really, there’s no reason not to be sending events, even if you don’t plan to use them now. The potential game-changing insights gained by implementing this far outweigh the 10 minutes it will cost your engineering team.
  • How does the React library for Algolia Recommend work?For each form of recommendations, the React library contains a hook to access that functionality and a default component that utilizes the hook. In prototypes, you’ll probably be fine using the default components, but for anything custom or complex, the hooks are probably the way to go.
  • How can I sanitize and normalize a complex dataset for an Algolia index?Strip out as much information as doesn’t pertain to the search, and try to flatten it into a single-layer object if possible.
  • How do I whip up an Algolia proof-of-concept quickly?Algolia builds simple UIs for you, if all you’re looking to do is show off an index or AI model to your manager or mess around with a new idea. The UI widgets are suggested right alongside where you actually create anything new in Algolia. For anything more complex, we have generic example apps that you can trim down to demo your exact usecase like I did in this article.

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!

About the authorJaden Baptista

Jaden Baptista

Recommended Articles

Powered by Algolia AI Recommendations

Adding trending recommendations to your existing e-commerce store
Engineering

Adding trending recommendations to your existing e-commerce store

Ashley Huynh

Ashley Huynh

Building a Store Locator in React using Algolia, Mapbox, and Twilio – Part 3
Engineering

Building a Store Locator in React using Algolia, Mapbox, and Twilio – Part 3

Clément Sauvage

Clément Sauvage

Software Engineer, Freelance
Creating an omnibar with Autocomplete
Engineering

Creating an omnibar with Autocomplete

Bryan Robinson

Bryan Robinson

Senior Developer Relations Specialist