Boost Payload CMS with Search: Step-by-Step Tutorial
Posted on: August 11 2025

One of the key essential features in any modern web applications is the ability to search for specific
content. The Payload CMS Search plugin provides a simple and flexible way to optimize search on
your site.
The plugin can efficiently query and retrieve relevant content by indexing fields intelligently and supporting advanced filtering and data synchronization.
Overview
In this post, I'll show how to setup and leverage this plugin to super charge your site's search capabilities, including:
- What is the Payload CMS Search Plugin?
- How does the search plugin actually work?
- How to optimize a search model for multiple types of documents.
- How to customize searchable fields.
- How to create a global search bar to query your site's content.
Helping users find what they are looking for could be the difference in promoting your site's potential and profitability.
So, let's dive into this plugin and see how to optimize search on your site.
Installation and setup
The search plugin requires the following Payload plugin library to installed.
pnpm add @payloadcms/plugin-search
or
npm i @payloadcms/plugin-search
Search Plugin Configuration
The plugin configuration has a few options:
- collections: An array of collections to enabled search on.
- beforeSync: A function to run to sync and update the search data.
- searchOverrides: Override any Payload config setting and also sets the fields the search Collection should define.
- defaultPriorities: An object of Collection keys and the priority level they get in the search results. Can either be a number or derived function.
- reindexBatchSize: Sets the batch size increments when re-indexing a Collection to avoid lengthy transaction locks.
export default buildConfig({
plugins: [
searchPlugin({
collections: ['pages', 'posts', 'movies'],
beforeSync: () => { }),
searchOverrides: {
fields: ({ defaultFields }) => {
return [
...defaultFields,
...customSearchFields
]
},
},
defaultPriorities: {
'movies': 60,
'pages': 50,
'posts': 30
},
reindexBatchSize: 50
}),
})
How Does the Payload CMS Search Plugin Work?
The search plugin creates its own fully optimized search Collection that can be searched on more efficiently, rather than querying on the original Collections directly.
These key fields are inserted into a new search Collection document, which is optimized by synchronizing and indexing.
Each new document added to the search Collection contains:
- Key search fields from the original Collection document.
- A relationship reference doc to the original document.
- The original docUrl path.
Making your website searchable
Now, let's build a simple example to illustrate how the search plugin works. I'll define some new collections to hold a movie database and seed it with some useful data.
Here's the movies Collection config.
Simple movies Collection
import type { CollectionConfig } from 'payload'
export const Movies: CollectionConfig = {
slug: 'movies',
admin: {
useAsTitle: 'title',
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
{
name: 'year',
label: 'Year',
type: 'number',
},
{
name: 'runtime',
label: 'Runtime',
type: 'number',
},
{
name: 'releaseDate',
label: 'Release Date',
type: 'text',
},
{
name: 'categories',
label: 'Categories',
type: 'relationship',
relationTo: 'categories',
hasMany: true,
},
{
name: 'storyline',
label: 'Storyline',
type: 'text',
},
],
}
And here's the categories relationship field.
Categories relationship field config
import type { CollectionConfig } from 'payload'
export const Categories: CollectionConfig = {
slug: 'categories',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
label: 'Category Name',
type: 'text',
},
],
}
Default search fields
The Payload Search plugin defines some default fields that are passed to the searchOverrides function in the plugin config.
- title: doc field
- priority: doc priority
- doc: A relationTo reference to the actual doc, in this case it's a particular movies doc.
- docUrl: The doc path url to the actual
These fields will be defined in the the search Collection by default unless overriden.
Which fields should I use?
A good rule of thumb, for which fields to add could be as follows:
- Any common searchable fields like, title, description, tag or keywords from each Collection in the plugin collections array.
- If you're using the SEO plugin also define the standard meta fields.
- Any unique fields that are essential for searching on.
The goal is to create a union of fields that are common and essential to search on.
Not every Collection will use the same search fields, for example, the storyline will probably only pertain to the movies Collection but it will be added so it can be searched on.
Search overrides
The following additional search fields will be added:
- meta: fields for the SEO info which are automatically added to collections if using the SEO plugin.
- categories: fields for the categories relationship field.
- storyline: The storyline only pertains to movies but it's added so it can be queried.
It's important to set text fields to index: true
, so Payload can index and optimize the database schema for searching.
The categories field can just use a where conditional query to find matches so it doesn't really need to be indexed.
/src/search/fieldOverrides.ts
export const searchFields: Field[] = [
{
name: 'slug',
type: 'text',
index: true,
admin: {
readOnly: true,
},
},
{
name: 'meta',
label: 'Meta',
type: 'group',
index: true,
admin: {
readOnly: true,
},
fields: [
{
type: 'text',
name: 'title',
label: 'Title',
},
{
type: 'text',
name: 'description',
label: 'Description',
},
{
name: 'image',
label: 'Image',
type: 'upload',
relationTo: 'media',
},
],
},
{
label: 'Categories',
name: 'categories',
type: 'array',
admin: {
readOnly: true,
},
fields: [
{
name: 'relationTo',
type: 'text',
},
{
name: 'id',
type: 'text',
},
{
name: 'title',
type: 'text',
},
],
},
{
name: 'storyline',
type: 'text',
index: true,
admin: {
readOnly: true,
},
},
]
Configuring search overrides
Any search overrides need to be added to the search plugin config as follows:
Adding custom search fields.
export default buildConfig({
plugins: [
searchPlugin({
collections: ['pages', 'posts', 'movies'],
searchOverrides: {
fields: ({ defaultFields }) => {
return [
...defaultFields,
...searchFields
]
}
},
})
})
Synchronizing your search data
Now that the fields have been identified, we can add the interactive built-in synchronization to keep everything updated.
The search plugin adds a Collection level AfterChange hook on each of the target collections defined in the config.
Any changes to documents that are managed by the search plugin, will call the beforeSync function to update the search Collection.
So basically the document that changes, needs to copy over any updates to the search Collection to keep it in sync.
BeforeSync example
The beforeSync function is passed the following args:
The beforeSync function copies search field values from the orginalDoc into the searchDoc, so it can be saved.
Here's an example of how to do the update.
/src/search/beforeSync.ts
import { BeforeSync, DocToSync } from '@payloadcms/plugin-search/types'
export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc }) => {
const {
doc: { relationTo: collection },
} = searchDoc
const { id, slug, meta, title, categories, storyline } = originalDoc
const modifiedDoc: DocToSync = {
...searchDoc,
slug,
meta: {
...meta,
title: meta?.title || title,
image: meta?.image?.id || meta?.image,
description: meta?.description,
},
title,
storyline,
categories: [],
}
if (categories && Array.isArray(categories) && categories.length > 0) {
try {
const flattenedCategories = categories.map((category) => {
const { id, title } = category
return {
relationTo: 'categories',
id,
title,
}
})
modifiedDoc.categories = flattenedCategories
} catch (_err) {
console.error(
`Failed: syncing categories collection '${collection}' with id: '${id}' to search.`,
)
}
}
return modifiedDoc
}
The beforeSync function needs to be added to search plugin config as follows.
Configuring the beforeSync hook
export default buildConfig({
plugins: [
searchPlugin({
beforeSync: beforeSyncWithSearch
})
})
The Search Results page
Payload CMS will automatically generate a readonly Search Results page in the admin section with the following benefits:
- Provides extensive keyword search and filters.
- Select which columns to show in the result.
- Change the priority in the config to see preferred document sorting in results.
- The page is secured in the admin section and can easily open any found document in edit mode.
- Any Collection can be reindexed at any time by using the dropdown menu at the top of the page.
The only catch is you need to be logged in and have admin access to view the Search Results page.
Also, any results from a search, will open the document for editing which is not exactly ideal for public facing users.
Creating a public facing search page
Since the Search Results page is only accessible for logged users, how can we use the search plugin to build a public facing search bar for a web application?
The Payload Search plugin was designed for this very purpose, to allow developers to create custom search pages for any purpose needed.
Let's take a look at an example of how to query the new search Collection by building a simple page for users to lookup movies.
Search page example
Here's the interface I'll create, so that when a user types some keywords, it searches all of the available documents.
Building a search bar interface
Let's start with the page.tsx which will be a server component in Next.js.
This page will take advantage of Payload's Local API to query the search Collection and pass the results to a client component.
The directory structure
The search page will use the following app route.
Search page folder
└── src
└── app
└── (yoursite) // route group
└── search
├── SearchBar.tsx
└── page.tsx
Create a new search page
Let's start by creating a React Server Page (RSC) so all of the querying can be done on the server side. The page will be responsible for the following:
- Looks for the search param called
q
that has the search phrase entered in the search input field.
- Uses the Payload Local API to construct a query based on the search phase.
- Passes the results of the search to the SearchBar component which is a client component.
Here's the search page code.
/app/(yoursite)/search/page.tsx
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React, { Suspense } from 'react'
import { SearchBar } from './SearchBar'
type Props = {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export default async function Page({ searchParams }: Props) {
const { q } = await searchParams
const results = await searchByQuery({
q: q ? (Array.isArray(q) ? q.join(' ') : q) : undefined,
})
return (
<div>
<Suspense>
<SearchBar results={results} />
</Suspense>
</div>
)
}
const searchByQuery = async ({ q }: { q?: string }) => {
if (!q) return []
const payload = await getPayload({ config: configPromise })
try {
const result = await payload.find({
collection: 'search',
limit: 100,
pagination: false,
overrideAccess: false,
where: {
or: [
{
title: {
like: q,
},
},
{
storyline: {
like: q,
},
},
],
},
})
return result.docs || []
} catch (e) {
return []
}
}
Create the SearchBar client component
The client SearchBar component will do the following:
- Sets the query phrase state using a hook.
- Adds a simple debounce to add a delay for input field key strokes.
- Builds a link with a public facing URL path and opens the selected document in a new tab.
/app/(yoursite)/search/SearchBar.tsx
'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Search } from '@payload-types'
type SearchProps = {
results?: Search[]
}
export const SearchBar: React.FC<SearchProps> = ({ results }) => {
const [value, setValue] = useState('')
const router = useRouter()
const searchValue = useDebounce(value)
useEffect(() => {
router.push(`/search${searchValue ? `?q=${searchValue}` : ''}`)
}, [searchValue, router])
return (
<div className="relative">
<div id="overlay" className="bg-black/10 absolute w-screen h-screen">
<div className="bg-white border border-gray-300 my-10 mx-auto w-[70%] z-50">
<div className="border-b border-gray-300 p-3">
<form
onSubmit={(e) => {
e.preventDefault()
}}
>
<div className="flex justify-start items-center gap-5">
<label htmlFor="search" className="sr-only">
Search
</label>
<input
name="search"
onChange={(event) => {
setValue(event.target.value)
}}
placeholder="Search"
className="w-full h-15 p-5"
/>
<button type="submit" className="sr-only">
submit
</button>
</div>
</form>
</div>
{results && results?.length > 0 ? (
<div className="overflow-y-scroll max-h-[80vh]">
<div className="px-5">
<h1 className="text-xl ">Search Results</h1>
</div>
<div>
<ul className="w-full">
{results.map(({ slug, title, storyline, doc: { relationTo, value } }: Search, i) => {
const linkToDoc = `/${relationTo}/${slug}`
return (
<li key={i} className="border-b border-gray-300 p-5">
<div className="flex flex-col w-full">
<div className="flex flex-col justify-start items-start">
<a
href={linkToDoc}
target="_blank"
rel="noopener noreferrer"
className="text-lg underline"
>
{title}
</a>
</div>
<div>{storyline}</div>
</div>
</li>
)
})}
</ul>
</div>
</div>
) : (
<div className="flex justify-center p-5">No search results.</div>
)}
</div>
</div>
</div>
)
}
export function useDebounce<T>(value: T, delay = 200): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
Linking to documents
How you build the links from the search documents is entirely up to the needs of your app.
The search results will provide sufficient data to build a link to point to your preferred view page.
Sample search doc value
const {
slug,
title,
storyline,
doc: {
relatedTo,
value
},
docUrl
} = result;
const linkToDoc = `/${relationTo}/${slug}`
What are the advantages of the search plugin?
So let's take a minute to recap on some key points and also analyze why the search plugin is beneficial.
-
Manages any Collection: The plugin can manage any Collection by simply adding it to the collections array in the config.
-
No direct queries on Collection: There's no need to search directly on the actual Collection but instead run queries on the search Collection which is indexed and light weight.
-
Access control: By searching on the search Collection directly, which is essentially public by default, you won't need to modify your existing Collection's access control for public search access.
-
Automatic synchronization: Your routine updating of docs automatically syncs the search Collection with the latest changes so your search data is always acurrate. Plus you can reindex when needed.
In Conclusion
So I hope this helps to shed some light on what this plugin actually does.
This plugin really takes the complexity out of building your own search method and provides a powerful tool for excellent search capabilities.
When coupled together with the SEO plugin, you can get just about full search coverage on your documents internally as well as publicly.
The idea is to use this as a simple way to organize what fields are essential for searching your documents. This is a must have when building large apps.