Search by Algolia
Introducing new developer-friendly pricing
algolia

Introducing new developer-friendly pricing

Hey there, developers! At Algolia, we believe everyone should have the opportunity to bring a best-in-class search experience ...

Nick Vlku

VP of Product Growth

What is online visual merchandising?
e-commerce

What is online visual merchandising?

Eye-catching mannequins. Bright, colorful signage. Soothing interior design. Exquisite product displays. In short, amazing store merchandising. For shoppers in ...

Catherine Dee

Search and Discovery writer

Introducing the new Algolia no-code data connector platform
engineering

Introducing the new Algolia no-code data connector platform

Ingesting data should be easy, but all too often, it can be anything but. Data can come in many different ...

Keshia Rose

Staff Product Manager, Data Connectivity

Customer-centric site search trends
e-commerce

Customer-centric site search trends

Everyday there are new messages in the market about what technology to buy, how to position your company against the ...

Piyush Patel

Chief Strategic Business Development Officer

What is online retail merchandising? An introduction
e-commerce

What is online retail merchandising? An introduction

Done any shopping on an ecommerce website lately? If so, you know a smooth online shopper experience is not optional ...

Vincent Caruana

Sr. SEO Web Digital Marketing Manager

5 considerations for Black Friday 2023 readiness
e-commerce

5 considerations for Black Friday 2023 readiness

It’s hard to imagine having to think about Black Friday less than 4 months out from the previous one ...

Piyush Patel

Chief Strategic Business Development Officer

How to increase your sales and ROI with optimized ecommerce merchandising
e-commerce

How to increase your sales and ROI with optimized ecommerce merchandising

What happens if an online shopper arrives on your ecommerce site and: Your navigation provides no obvious or helpful direction ...

Catherine Dee

Search and Discovery writer

Mobile search UX best practices, part 3: Optimizing display of search results
ux

Mobile search UX best practices, part 3: Optimizing display of search results

In part 1 of this blog-post series, we looked at app interface design obstacles in the mobile search experience ...

Vincent Caruana

Sr. SEO Web Digital Marketing Manager

Mobile search UX best practices, part 2: Streamlining search functionality
ux

Mobile search UX best practices, part 2: Streamlining search functionality

In part 1 of this series on mobile UX design, we talked about how designing a successful search user experience ...

Vincent Caruana

Sr. SEO Web Digital Marketing Manager

Mobile search UX best practices, part 1: Understanding the challenges
ux

Mobile search UX best practices, part 1: Understanding the challenges

Welcome to our three-part series on creating winning search UX design for your mobile app! This post identifies developer ...

Vincent Caruana

Sr. SEO Web Digital Marketing Manager

Teaching English with Zapier and Algolia
engineering

Teaching English with Zapier and Algolia

National No Code Day falls on March 11th in the United States to encourage more people to build things online ...

Alita Leite da Silva

How AI search enables ecommerce companies to boost revenue and cut costs
ai

How AI search enables ecommerce companies to boost revenue and cut costs

Consulting powerhouse McKinsey is bullish on AI. Their forecasting estimates that AI could add around 16 percent to global GDP ...

Michelle Adams

Chief Revenue Officer at Algolia

What is digital product merchandising?
e-commerce

What is digital product merchandising?

How do you sell a product when your customers can’t assess it in person: pick it up, feel what ...

Catherine Dee

Search and Discovery writer

Scaling marketplace search with AI
ai

Scaling marketplace search with AI

It is clear that for online businesses and especially for Marketplaces, content discovery can be especially challenging due to the ...

Bharat Guruprakash

Chief Product Officer

The changing face of digital merchandising
e-commerce

The changing face of digital merchandising

This 2-part feature dives into the transformational journey made by digital merchandising to drive positive ecommerce experiences. Part 1 ...

Reshma Iyer

Director of Product Marketing, Ecommerce

What’s a convolutional neural network and how is it used for image recognition in search?
ai

What’s a convolutional neural network and how is it used for image recognition in search?

A social media user is shown snapshots of people he may know based on face-recognition technology and asked if ...

Catherine Dee

Search and Discovery writer

What’s organizational knowledge and how can you make it accessible to the right people?
product

What’s organizational knowledge and how can you make it accessible to the right people?

How’s your company’s organizational knowledge holding up? In other words, if an employee were to leave, would they ...

Catherine Dee

Search and Discovery writer

Adding trending recommendations to your existing e-commerce store
engineering

Adding trending recommendations to your existing e-commerce store

Recommendations can make or break an online shopping experience. In a world full of endless choices and infinite scrolling, recommendations ...

Ashley Huynh

Looking for something?

Integrating third-party APIs into GraphQL with Apollo Client
facebookfacebooklinkedinlinkedintwittertwittermailmail

To add fast and feature-rich third-party APIs into any codebase, developers need to work within an ecosystem designed to include these self-sufficient APIs.

Developers also need to rely on technologies like GraphQL with Apollo Client and React to help them manage the variety of functionality and data sources.

In most situations, GraphQL is a godsend. As a flexible data layer, it helps centralize and standardize the data exchanges between the back and front ends.

However, requiring all data exchanges to flow through GraphQL can sometimes hinder an external API from reaching its full potential as a critical functional component. In our case, we wanted to integrate a cloud-based search API that performed substantially better than GraphQL.

The problem we solve in this article is how you can use an external API on its own terms, side-by-side with GraphQL that is, without going through GraphQL.

Why we decided to bypass GraphQL with Apollo Client and How we achieved interoperability

We divide this article into two parts:

  • The explanation for why we decided to bypass GraphQL to allow our front end to make direct calls to an external API’s cloud service
  • The code we used to build the interoperability between the external API and GraphQL with Apollo Client

Feel free to jump to the code. But the backstory may help those faced with the same question: How do you combine GraphQL with self-sufficient APIs that provide their own set of data?

Why GraphQL? Why an external search API?

To say our online service is data-intensive is an understatement. Openbase helps developers find open-source libraries and packages perfectly tailored to their needs. They can search for and compare open-source packages with powerful metrics and user reviews. This information comes from many sources, including Github and our own databases.

And there are other concerns:

  • Weekly downloads
  • Maintenance
  • Bundle size
  • Categorization

Using GraphQL with Apollo Client

Openbase is a GraphQL shop. We access our data with @apollo/client for data-fetching and use React for rendering. A number of our React components are built using @apollo/client#readFragment to reference data from our GraphQL api. This makes our components tightly coupled to @apollo/client and GraphQL and less re-usable with other data sources.

Example:

Code for PackageTile.tsx:

import { useApolloClient } from '@apollo/client'
import React, { FC } from 'react'

// Generated using GraphQL Code Generator
import { AlgoliaProductFragment, AlgoliaProductFragmentDoc } from '@openbase/graphql'

interface PackageTileProps {
	packageId: string
}

const PackageTile: FC<PackageTileProps> = ({ packageId }) => {
	const apolloClient = useApolloClient()

	const packageFragment = apolloClient.readFragment<AlgoliaProductFragment>({
		fragment: AlgoliaProductFragmentDoc,
		id: `Package:${packageId}`
	})

	return (
		<div>
			<div>{packageFragment.name}</div>
			<div>{packageFragment.description}</div>
			<div>{packageFragment.bundleSize.gzipSizeInBytes}</div>
		</div>
	)
}

Building search with a third-party, cloud-based search API

With all that data, search is therefore essential to our business. Our developer-users start with a search bar. But they do more than that they categorize, filter, and surf through multiple results and category pages, all of which comes with the external search API. On top of that, our product provides ratings and recommendations and allows users to personalize their results to their favorite languages. Much of this additional functionality comes directly from an external Search API’s results, in combination with our own datasources accessible via GraphQL.

The two options for combining GraphQL and an external search API

There are two mutually-exclusive approaches to integrating an external API into a GraphQL front-end layer:

1. Use the API on the front end as a secondary datasource alongside @apollo/client
2. Use the API on the back end behind our GraphQL API, and maintain a single front-end datasource

Option 1 – the reason for bypassing GraphQL

The external search API, provided by Algolia, is fast, displaying search results instantaneously as the user types. Putting Algolia behind our GraphQL API would bottleneck the search and slow it down considerably.

  1. Algolia’s API shows an average search speed of _32ms across over 300,000 requests (at the time of this writing)
  2. Our GraphQL API requests can take anywhere between 100-200ms sometimes

This delay is untenable in comparison.

To get that additional speed, which is critical to our product, we really needed to call the API directly.

Option 2 – the reasons to put the external API behind GraphQL

We had two concerns with bypassing GraphQL:

  • Caching. Existing components built upon GraphQL’s cache aren’t aware of records retrieved through our search API.
  • Schema-less. The search API’s records are schema-less. Its indexes are flat and contain the minimum set of attributes necessary for search, display, and ranking. While necessary for speed, relevance, and scalability, a flat, schema-less set of data does not align easily with the object-types from our GraphQL schema, which our components are built upon.

Additionally, putting search functionality behind GraphQL that integrates the search results into the front-end components just works out of GraphQL’s box: the front end just makes requests to GraphQL, retrieving the same object-types, and the developer experience stays consistent.

The decision

We were therefore faced with the following choice: either we go for the easier solution (plug the API into GraphQL) or we work a little harder to get the full functionality we expect from the API.

We decided with little hesitation to offer our users instantaneous search results as they type.

How we integrated the external API into our GraphQL front-end components

Algolia uses indexes that are not part of our GraphQL layer. We’ll be referring to algolia from this point forward.

Our GraphQL client is called @apollo/client.

Based on the above considerations, we’ve introduced an algolia-to-@apollo/client interoperability layer so that we are able to:

  1. Consume the API directly on the frontend (to stay fast)
  2. Use our existing react components built against GraphQL to maintain a developer and user experience that is consistent with the rest of our application

At a high level, the interoperability works like this:

  1. Records are queried from the algolia-indices
  2. The data from the records are written to our @apollo/client cache using custom mapper functions and GraphQL fragments generated from @graphql-codegen/cli
  3. All of the components work out of the box reading data using GraphQL from @apollo/client‘s cache

Here is how we’ve built our indices to achieve this (built with typescript):

Code for algoliaToGraphQL.tsx:

import type { ObjectWithObjectID, SearchOptions, SearchResponse } from '@algolia/client-search'
import type { RequestOptions } from '@algolia/transporter'
import type { ApolloClient, DocumentNode } from '@apollo/client'
import type { SearchClient, SearchIndex } from 'algoliasearch'

// Narrowly type our indices for intellisense
export type AlgoliaTypedIndex<TRecord> = Omit<SearchIndex, 'search'> & {
	search: (query: string) => Readonly<Promise<SearchResponse<TRecord & ObjectWithObjectID>>>
}

// Define the schema for configuring the custom mappings
export interface AlgoliaToGraphQLFieldConfig<
	TIndex extends ObjectWithObjectID,
	TFragment extends Record<string, any>
> {
	__typename: string
	fragment: DocumentNode
	fragmentName?: string
	id?: (value: TIndex) => string
	write: (value: TIndex) => MaybePromise<Maybe<TFragment>>
}

// Define how the custom mapping will type-translate to narrowly-typed indices
export interface AlgoliaToGraphQLFields {
	[indexName: string]: AlgoliaToGraphQLFieldConfig<any, any>
}

export type AlgoliaToGraphQLResult<T extends AlgoliaToGraphQLFields> = {
	[P in keyof T]: T[P] extends AlgoliaToGraphQLFieldConfig<infer I, any>
		? AlgoliaTypedIndex<I>
		: never
}

// Create algolia-indices that will inject hits data to our `@apollo/client` cache
const createIndex = async <
	TIndex extends ObjectWithObjectID,
	TFragment extends Record<string, any>
>(
	algolia: SearchClient,
	apollo: ApolloClient<object>,
	name: string,
	config: AlgoliaToGraphQLFieldConfig<TIndex, TFragment>
): Promise<AlgoliaTypedIndex<TIndex>> => {
	const {
		__typename,
		fragment,
		fragmentName,
		id = (value) => `${__typename}:${value.objectID}`,
		write,
	} = config

	const writeFragment = async (value: TIndex): Promise<void> => {
		const fragmentData = await write(value)

		!!fragmentData && apollo.writeFragment<TFragment>({
			fragment,
			fragmentName,
			data: { __typename, ...fragmentData },
			id: id(value),
		})
	}

	const index = algolia.initIndex(name) as AlgoliaTypedIndex<TIndex>

	return {
		...index,
		// Override search to write everything into cache.
		async search(query, opts) {
			const result = await index.search(query, opts)

			await Promise.all(result.hits.map(async (hit) => writeFragment(hit)))

			return result
		},
	}
}

// Generate all of the new algolia indices from a config
export const algoliaToGraphQL = async <T extends AlgoliaToGraphQLFields>(
	algolia: SearchClient,
	apollo: ApolloClient<object>,
	config: T
): Promise<AlgoliaToGraphQLResult<T>> => {
	const indices = await Promise.all(
		Object.entries(config).map(async ([indexName, fieldConfig]) => {
			const index = await createIndex(algolia, apollo, indexName, fieldConfig)

			return [indexName, index] as readonly [string, AlgoliaTypedIndex<any>]
		})
	)

	return indices.reduce(
		(acc, [indexName, index]) => ({ ...acc, [indexName]: index }),
		{} as AlgoliaToGraphQLResult<T>
	)
}

Once we have the means to inject algoliasearch data to our @apollo/client cache, we simply type the shape of the Algolia record, define the GraphQL fragment to be written, map the record to the fragment, and build the new indices.

Code for types.ts:

import type { ObjectWithObjectID } from '@algolia/client-search'

// Types for our algolia records (for demonstration only)

interface BaseAlgoliaTypes {
	AlgoliaProduct: {
		name: string // String!
		description?: Maybe<string> // String
		bundleSize?: Maybe<number> // Int
		starRating: number // Float!
	}
}

export type AlgoliaTypes = {
	[P in keyof BaseAlgoliaTypes]: BaseAlgoliaTypes[P] & ObjectWithObjectID
}

Code for our GraphQL fragments for mapping:

fragment AlgoliaProduct on Package {
id
name
description
bundleSize {
gzipSizeInBytes
}
starRating
}

Code for indices.ts:

import type { ApolloClient } from '@apollo/client'
import type { SearchClient } from 'algoliasearch'
import { algoliaToGraphQL, AlgoliaToGraphQLFieldConfig } from './algoliaToGraphQL'
import type { AlgoliaTypes } from './types'

// Generated using GraphQL Code Generator
import { AlgoliaProductFragment, AlgoliaProductFragmentDoc } from '@openbase/graphql'

// Map algolia package records to our `AlgoliaProduct` fragment in GraphQL
const packages: AlgoliaToGraphQLFieldConfig<
	AlgoliaTypes['AlgoliaProduct'],
	AlgoliaProductFragment
> = {
	__typename: 'Package',
	fragment: AlgoliaProductFragmentDoc,
	write(value: AlgoliaTypes['AlgoliaProduct']): AlgoliaProductFragment {
		return {
			id: value.objectID,
			name: value.name,
			description: value.description,
			bundleSize: {
				gzipSizeInBytes: value.bundleSize,
			},
			starRating: value.starRating,
		}
	},
}

// Create all of our indices
export const getIndices = (algolia: SearchClient, apollo: ApolloClient<object>) => {
	return algoliaToGraphQL(algolia, apollo, {
		packages,
		// Different sortings using algolia virtual replicas
		packages_by_bundleSize_desc: packages,
		packages_by_starRating_desc: packages,
	})
}

Code for AlgoliaApolloProvider.tsx:

import { useApolloClient } from '@apollo/client'
import algolia from 'algoliasearch'
import React, {
	createContext,
	FC,
	ReactNode,
	useContext,
	useEffect,
	useState
} from 'react'
import { getIndices } from './indices'

export type AlgoliaApolloContextIndex = InferFromPromise<ReturnType<typeof getIndices>>

const AlgoliaApolloContext = createContext<Maybe<AlgoliaApolloContextIndex>>(null)

export interface AlgoliaApolloProviderProps {
	children?: ReactNode
}

// Wrap your application with this, to be able to use the interoperability anywhere
export const AlgoliaApolloProvider: FC<AlgoliaApolloProviderProps> = ({
	children,
}) => {
	const [index, setIndex] = useState<Maybe<AlgoliaApolloContextIndex>>(null)

	const apollo = useApolloClient()

	useEffect(() => {
		const newClient = algolia(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_API_KEY)

		getIndices(client, apollo).then((newIndices) => setIndex(newIndices))
	}, [apollo])

	return (
		<AlgoliaApolloContext.Provider value={index}>
			{children}
		</AlgoliaApolloContext.Provider>
	)
}

// Hook to use in any component that needs to search algolia
export const useAlgoliaIndex = () => useContext(AlgoliaApolloContext)

From this point on, whenever a search is made to Algolia, components that use @apollo/client‘s cache will work out of the box:

import { NextPage } from 'next'
import React from 'react'
import { useAlgoliaIndex } from './AlgoliaApolloProvider'

const Page: NextPage = () => {
	const index = useAlgoliaIndex()
	const [hits, setHits] = useState<any[]>([])

	useEffect(() => {
		index?.packages
			.search('react', { length: 20, offset: 0 })
			.then((results) => setHits(results.hits.slice()))
	}, [index])

	return (
		<div>
			{!!hits && hits.map((hit) => (
				<PackageTile key={hit.objectID} packageId={hit.objectID} />
			))}
		</div>
	)
}

export default Page

With this, we achieved the goals we’ve set for a fast search, with a developer and user experience that is consistent with the rest of Openbase. The API for our modified indices are the same as the non-modified indices from algoliasearch, and our components can stay unmodified specific to our single @apollo/client data-source.

Coming up with a high-quality and relevant set of Github repos is an infinite challenge. Algolia’s algoliasearch eliminates a significant complexity for Openbase to build an experience that allows for users to find the best open-source tooling to fit their needs. The approach your GraphQL shop takes with integrating Algolia or any other API may depend on your own requirements and conditions.

About the author
David Lee

Sr. Frontend Engineer at Openbase

github

Recommended Articles

Powered byAlgolia Algolia Recommend

Part 4: Supercharging search for ecommerce solutions with Algolia and MongoDB — Frontend implementation and conclusion
engineering

Soma Osvay

Full Stack Engineer, Starschema

GraphQL search and indexing with Algolia
engineering

Charly Poly

Freelance Front-end Architect

Centralizing state and data handling with React Hooks: on the road to reusable components
engineering

Emmanuel Krebs

Sr. Software Engineer