VeiaG

payload algolia search

by VeiaG
Search
v3
^3.37.0
MIT

Overview

Summary generated from the README

Syncs selected Payload collections to an Algolia index whenever documents are created, updated or deleted. It adds a search endpoint and a re-index endpoint, plus a re-index button in the admin panel. Search results can optionally be enriched with fresh, access-controlled documents from Payload. Requires Payload 3.37 or later and an Algolia admin API key; localized fields are not supported.

Features

  • Automatic sync of created, updated and deleted documents to Algolia
  • Per-collection selection of indexed fields
  • Search endpoint that passes Algolia parameters straight through
  • Re-index endpoint and admin button with configurable access control
  • Result enrichment with field selection and depth control per collection
  • Custom field transformers for group and other complex field types

Use cases

  • Add fast frontend search to a blog or content site backed by Payload
  • Index product collections for an ecommerce catalogue search
  • Return access-controlled Payload documents alongside Algolia highlights

From the author

Algolia Search Plugin for Payload CMS

Installation

npm i @veiag/payload-algolia-search

1

Stars

0

Forks

0

Open Issues

1 years ago

Last Updated

0

Weekly Downloads

v1.0.3

npm Version

94.8 KB

Unpacked Size

2

Dependencies

GitHub stars

Trends appear after 7 days of data. Daily snapshots have not started yet.

npm weekly downloads

Trends appear after 7 days of data. Daily snapshots have not started yet.

Each point is npm's rolling 7-day download count.

Health Score

28/100Fair

Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.

Badge for your README

Payload Directory health badge
Markdown
[![Payload Directory](https://payloaddirectory.dev/badge/1035672939-root.svg)](https://payloaddirectory.dev/plugins/1035672939-root)
HTML
<a href="https://payloaddirectory.dev/plugins/1035672939-root"><img src="https://payloaddirectory.dev/badge/1035672939-root.svg" alt="Payload Directory"></a>
Updated Aug 11, 2025Created Aug 10, 2025Published Aug 11, 2025

README

Payload Algolia Search Plugin

npm version

A powerful, feature-rich plugin to sync your Payload CMS collections with Algolia to enable fast and extensive search capabilities.

Table of Contents

Overview

The Payload Algolia Search Plugin bridges your Payload CMS with Algolia's powerful search infrastructure, providing:

  • šŸ”„ Automatic Syncing: Real-time synchronization of your Payload collections with Algolia
  • ⚔ Fast Search: Lightning-fast search capabilities powered by Algolia
  • šŸŽÆ Flexible Configuration: Granular control over which collections and fields to index
  • šŸ”’ Secure: Built-in access control and secure API endpoints
  • šŸ› ļø Developer-Friendly: Comprehensive customization options and hooks

Key Features

  • Automatic Syncing: Documents are automatically synced when created, updated, or deleted
  • Collection-Specific Configuration: Choose exactly which collections and fields to index
  • Admin UI Integration: Built-in re-index button in the Payload admin panel
  • RESTful Endpoints: Dedicated endpoints for search and re-indexing operations
  • Result Enrichment: Option to fetch fresh, access-controlled data from Payload
  • Custom Transformers: Transform complex field types for optimal search indexing
  • Access Control: Fine-grained permissions for re-indexing operations
  • Auto-Configuration: Automatic Algolia index setup on server start

Installation

Install the plugin using your preferred package manager:

# pnpm (recommended)
pnpm add @veiag/payload-algolia-search

# npm
npm install @veiag/payload-algolia-search

# yarn
yarn add @veiag/payload-algolia-search

Quick Start

1. Basic Setup

Add the plugin to your payload.config.ts:

import { buildConfig } from 'payload/config'
import { algoliaSearchPlugin } from '@veiag/payload-algolia-search'

export default buildConfig({
  // ... your existing config
  plugins: [
    algoliaSearchPlugin({
      credentials: {
        appId: process.env.ALGOLIA_APP_ID!,
        apiKey: process.env.ALGOLIA_API_KEY!, // Admin API Key
        indexName: process.env.ALGOLIA_INDEX_NAME!,
      },
      collections: [
        {
          slug: 'posts',
          indexFields: ['title', 'content', 'tags'],
        },
      ],
    }),
  ],
})

2. Environment Variables

Create a .env file with your Algolia credentials:

ALGOLIA_APP_ID=your_app_id
ALGOLIA_API_KEY=your_admin_api_key
ALGOLIA_INDEX_NAME=your_index_name

3. Start Your Server

The plugin will automatically:

  • Configure your Algolia index (if it exists)
  • Set up search and re-index endpoints

To index existing documents, use re-index endpoints (or button in admin UI)

Configuration

Plugin Options

The plugin accepts a configuration object with the following options:

OptionTypeRequiredDefaultDescription
credentialsPluginAlgoliaCredentialsāœ…-Algolia connection details
collectionsCollectionAlgoliaConfig[]āœ…-Collections to sync with Algolia
searchEndpointstring | falseāŒ'/search'Search endpoint path (set to false to disable)
overrideAccessbooleanāŒfalseIf true, the plugin will override access control when enriching search results
reindexEndpointstring | falseāŒ'/reindex'Re-index endpoint path (set to false to disable)
configureIndexOnInitbooleanāŒtrueAuto-configure Algolia index on startup
hideReindexButtonbooleanāŒfalseHide re-index button in admin UI
reindexAccessfunctionāŒ( req ) => !!req.userAccess control for re-index operations
fieldTransformersRecord<string, FieldTransformer>āŒ-Custom field transformation functions
disabledbooleanāŒfalseDisable the plugin entirely

Collection Configuration

Each collection in the collections array supports:

interface CollectionAlgoliaConfig {
  slug: string;           // Collection slug
  indexFields: string[];  // Fields to index in Algolia
}

Environment Variables

VariableDescriptionRequired
ALGOLIA_APP_IDYour Algolia Application IDāœ…
ALGOLIA_API_KEYYour Algolia Admin/Write API Keyāœ…
ALGOLIA_INDEX_NAMETarget Algolia index nameāœ…

āš ļø Security Note: The ALGOLIA_API_KEY should be your Admin/Write API Key and must be kept secret. Never expose it in client-side code.

API Reference

Search Endpoint

Perform search queries against your Algolia index.

Endpoint: GET /search (or your configured searchEndpoint)

Query Parameters
ParameterTypeDescription
querystringSearch term
enrichResultsbooleanFetch fresh documents from Payload
selectobjectField selection for enriched results
depthobjectDepth options for enriched results
hitsPerPagenumberNumber of results per page
filtersstringAlgolia filters
Any other Algolia search parametervariesPassed directly to Algolia
Basic Search Example
// Simple search
const response = await fetch('/search?query=javascript&hitsPerPage=10');
const results = await response.json();

Re-index Endpoint

Manually trigger a full re-index of a collection.

Endpoint: POST /reindex/:collectionSlug

Example
// Re-index the 'posts' collection
const response = await fetch('/reindex/posts', { method: 'POST' });
const result = await response.json();

Advanced Features

Result Enrichment

By default, search results come directly from Algolia for maximum speed. However, you can enable result enrichment to get fresh, access-controlled data from your Payload database.

Why Use Enrichment?
  • Data Freshness: Guaranteed up-to-date information from your database
  • Security: Respects Payload's access control rules
  • Metadata Preservation: Keeps Algolia's search metadata (highlights, snippets)
How It Works

Add enrichResults=true to your search query:

const response = await fetch('/search?query=javascript&enrichResults=true');
const { hits, enrichedHits, ...algoliaMetadata } = await response.json();

// hits: Original Algolia results with search metadata
// enrichedHits: Fresh documents from Payload (keyed by ID)
Response Structure
{
  "hits": [
    {
      "objectID": "60c7c5d5f1d2a5001f6b0e3d",
      "title": "JavaScript Basics",
      "_highlightResult": { "title": { "value": "<em>JavaScript</em> Basics" } }
    }
  ],
  "enrichedHits": {
    "60c7c5d5f1d2a5001f6b0e3d": {
      "id": "60c7c5d5f1d2a5001f6b0e3d",
      "title": "JavaScript Basics",
      "content": "Full article content...",
      "author": { "name": "John Doe" },
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  },
  "nbHits": 1,
  "page": 0
}

Field Selection

Control which fields are returned in enriched results to optimize response size and performance.

Using Field Selection
import qs from 'qs-esm';

// Include only specific fields
const selectFields = {
  posts: { title: true, slug: true },
  authors: { name: true, email: true }
};

const params = {
  query: 'javascript',
  enrichResults: true,
  select: selectFields
};

const url = `/search?${qs.stringify(params)}`;
Selection Strategies

Inclusion (recommended):

{
  posts: { title: true, content: true },
  authors: { name: true }
}

Exclusion:

{
  posts: { internalNotes: false, draft: false }
}

Depth Control

Control the depth of relationship population in enriched results to optimize performance and control data fetching.

Why Use Depth Control?
  • Performance Optimization: Prevent over-fetching of deeply nested relationships
  • Bandwidth Control: Reduce response payload size
  • Granular Control: Different depth levels per collection type
How It Works

Add depth parameters to your search query to specify how deeply relationships should be populated for each collection:

import qs from 'qs-esm';

// Set different depths for different collections
const depthConfig = {
  posts: 3,    // Populate posts to depth 3
  authors: 1,  // Populate authors to depth 1
  categories: 2 // Populate categories to depth 2
};

const params = {
  query: 'javascript',
  enrichResults: true,
  depth: depthConfig
};

const url = `/search?${qs.stringify(params)}`;

// URL EXAMPLE
/search?query=javascript&enrichResults=true&depth[posts]=3&depth[authors]=1&depth[categories]=2
Combined with Field Selection

You can combine depth control with field selection for maximum optimization:

const params = {
  query: 'javascript',
  enrichResults: true,
  select: {
    posts: { title: true, content: true, author: true },
    authors: { name: true, bio: true }
  },
  depth: {
    posts: 2,  // Populate author relationship
    authors: 0 // Don't populate further relationships in authors
  }
};

Default Behavior

  • If no depth is specified for a collection, it defaults to depth 1
  • Invalid depth values are ignored and fall back to the default
  • Depth must be a non-negative integer (0, 1, 2, 3, etc.)

Custom Field Transformers

Transform complex field types into searchable formats before indexing in Algolia.

When to Use Transformers
  • Group Fields: Flatten nested data structures
  • Custom Fields: Handle proprietary field types
  • Complex Data: Convert objects/arrays to searchable strings
Transformer Function Signature
type FieldTransformer = (
  value: unknown,
  fieldConfig: Field,
  collectionSlug: string
) => string | number | boolean | string[] | null;
Example: Group Field Transformer
// Collection with group field
const Posts: CollectionConfig = {
  slug: 'posts',
  fields: [
    {
      name: 'authorDetails',
      type: 'group',
      fields: [
        { name: 'name', type: 'text' },
        { name: 'title', type: 'text' },
        { name: 'bio', type: 'textarea' },
      ],
    },
  ],
};

// Plugin configuration
algoliaSearchPlugin({
  // ... other config
  collections: [
    {
      slug: 'posts',
      indexFields: ['title', 'authorDetails'], // Include the group field
    },
  ],
  fieldTransformers: {
    group: (value, fieldConfig, collectionSlug) => {
      if (fieldConfig.name === 'authorDetails' && value) {
        const { name, title, bio } = value as any;
        return [name, title, bio].filter(Boolean).join(' ');
      }
      return null; // Don't index other group fields
    },
  },
});
Built-in Transformers

The plugin includes default transformers for:

  • text: Just return the value as-is, or in case of an array, join the elements with a comma
  • richText: Converts rich text to plain text
  • relationship: Extracts related document titles or names (returns value.title, value.name, value.slug, or String(value.id))
  • upload: Indexes file names and metadata (uses value?.filename, value?.alt, value?.title, or null)
  • select: Handles select field values
  • array: Joins array elements into a comma-separated string; if elements are objects, their values are concatenated into a single string before joining.

Access Control

Control who can trigger re-indexing operations.

Default Access Control

By default, any authenticated user can trigger re-indexing:

const defaultAccess = ( req: PayloadRequest ) => !!req.user;
Custom Access Control

Restrict access to specific user roles:

algoliaSearchPlugin({
  // ... other config
  reindexAccess: ( req ) => {
    return req.user?.role === 'admin' || req.user?.role === 'editor';
  },
});
Disable Re-indexing UI

Hide the re-index button while keeping the endpoint active:

algoliaSearchPlugin({
  // ... other config
  hideReindexButton: true,
});

Examples

Basic Blog Setup

import { algoliaSearchPlugin } from '@veiag/payload-algolia-search';

export default buildConfig({
  collections: [Posts, Authors, Categories],
  plugins: [
    algoliaSearchPlugin({
      credentials: {
        appId: process.env.ALGOLIA_APP_ID!,
        apiKey: process.env.ALGOLIA_API_KEY!,
        indexName: process.env.ALGOLIA_INDEX_NAME!,
      },
      collections: [
        {
          slug: 'posts',
          indexFields: ['title', 'excerpt', 'content', 'tags'],
        },
        {
          slug: 'authors',
          indexFields: ['name', 'bio'],
        },
      ],
    }),
  ],
});

E-commerce Setup

algoliaSearchPlugin({
  credentials: {
    appId: process.env.ALGOLIA_APP_ID!,
    apiKey: process.env.ALGOLIA_API_KEY!,
    indexName: process.env.ALGOLIA_INDEX_NAME!,
  },
  collections: [
    {
      slug: 'products',
      indexFields: ['title', 'description', 'category', 'brand', 'sku','specifications'],
    },
  ],
  fieldTransformers: {
    group: (value, fieldConfig) => {
      if (fieldConfig.name === 'specifications' && value) {
        return Object.entries(value)
          .map(([key, val]) => `${key}: ${val}`)
          .join(' ');
      }
      return null;
    },
  },
});

Frontend Search Implementation

// React search component example with depth control
const SearchResults = ({ query }) => {
  const [results, setResults] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const searchProducts = async () => {
      if (!query) return;
      
      setLoading(true);
      try {
        const params = {
          query,
          enrichResults: true,
          hitsPerPage: 20,
          depth: {
            products: 2,  // Include category and brand relationships
            categories: 1 // Don't over-fetch category relationships
          },
          select: {
            products: { title: true, description: true, price: true, category: true, brand: true },
            categories: { name: true, slug: true }
          }
        };

        const response = await fetch(`/search?${qs.stringify(params)}`);
        const data = await response.json();
        setResults(data);
      } catch (error) {
        console.error('Search failed:', error);
      } finally {
        setLoading(false);
      }
    };

    searchProducts();
  }, [query]);

  if (loading) return <div>Searching...</div>;
  if (!results) return null;

  return (
    <div>
      <p>{results.nbHits} results found</p>
      {results.hits.map((hit) => {
        const enrichedData = results.enrichedHits[hit.objectID];
        return (
          <div key={hit.objectID}>
            <h3 dangerouslySetInnerHTML={{ 
              __html: hit._highlightResult.title.value 
            }} />
            {enrichedData && (
              <>
                <p>{enrichedData.description}</p>
                {enrichedData.category && (
                  <span>Category: {enrichedData.category.name}</span>
                )}
              </>
            )}
          </div>
        );
      })}
    </div>
  );
};

Troubleshooting

Common Issues

Plugin Not Syncing Documents

Symptoms: Documents aren't appearing in Algolia after creation/updates.

Solutions:

  1. Verify your Algolia credentials are correct
  2. Check that indexFields includes existing fields
  3. Ensure the API key has write permissions
  4. Check server logs for error messages
Search Endpoint Returns 404

Symptoms: Search requests fail with 404 errors.

Solutions:

  1. Verify searchEndpoint is not set to false
  2. Check your server is running and the plugin is loaded
  3. Ensure the endpoint path doesn't conflict with existing routes
Re-index Button Not Appearing

Symptoms: No re-index button in the admin panel.

Solutions:

  1. Check that hideReindexButton is not set to true
  2. Ensure the collection is configured in the plugin
Enriched Results Empty

Symptoms: enrichedHits is empty even with enrichResults=true.

Solutions:

  1. Verify documents exist in your Payload database
  2. Check access control permissions for the requesting user
  3. Ensure document IDs in Algolia match Payload document IDs
Localized Content Issues

Current Limitation: This plugin does not currently support Payload's localization features. Localized fields will not be indexed correctly.

Workarounds:

  1. Single Locale: Configure your collections to use only one locale for now
  2. Manual Field Mapping: Create separate non-localized fields specifically for search indexing

Performance Optimization

Large Collections

For collections with many documents:

  1. Use field selection to limit response size
  2. Implement pagination with hitsPerPage
  3. Consider indexing only essential fields initially
Search Performance
  • Use enrichment sparingly for better performance
  • Cache search results on the frontend when appropriate
  • Consider using Algolia's faceting for filters instead of enrichment

License

MIT


Contributing

We welcome contributions!

Similar plugins