Search by

korcontrol / craft-algolia

korcontrol

Algolia search integration plugin for Craft CMS 5

Package info

github.com/korcontrol/craft-algolia

Type:craft-plugin

pkg:composer/korcontrol/craft-algolia

Statistics

Installs: 28

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.2.1 2026-10-29 20:45 UTC

This package is auto-updated.

Last update: 2026-10-29 20:56:07 UTC


README

A Craft CMS 5 plugin for config-driven Algolia search indexing.

Supports entries, assets and Solspace Calendar events via a declarative config file, with a custom indexer escape hatch for complex cases.

Installation

Require the plugin with Composer and install it:

composer require korcontrol/craft-algolia
php craft plugin/install craft-algolia

Environment Variables

Add these to your .env file:

ALGOLIA_APP_ID=your-app-id
ALGOLIA_WRITE_API_KEY=your-write-api-key
ALGOLIA_INDEX_NAME=your-index-name

The write API key must have addObject, deleteObject, and deleteIndex permissions. Never expose it publicly — it stays server-side only.

Algolia Index Configuration

After creating your index, configure the following settings in the Algolia dashboard under Index → Configuration.

Searchable Attributes

Under Searchable attributes, add:

  • title
  • body
  • section
  • elementID

Attributes for Faceting

Under Facets → Attributes for faceting, add:

  • elementID (set to Filter only)
  • siteID (set to Filter only)

Required — the plugin uses deleteBy filters on elementID and siteID when saving and deleting elements. Without these, stale records will not be removed and deletions will silently fail.

Deduplication

Under Deduplication and Grouping:

  • Set Distinct to true
  • Set Attribute for Distinct to elementID

The plugin splits large body fields across multiple Algolia records (e.g. 42-1-0, 42-1-1). Without deduplication, a single entry could appear multiple times in search results.

Configuration

Copy the plugin's src/config.php to your project's config/ directory as algolia.php:

cp vendor/korcontrol/craft-algolia/src/config.php config/algolia.php

Credentials are read from environment variables automatically. The config file is where you define which sections, volumes and calendars to index.

Config Reference

Entries

Index entries from one or more sections:

'entries' => [
    'news' => [
        'bodyFields' => ['editorComponents'],
    ],
    'pages' => [
        'bodyFields' => ['contentBuilder'],
        'bodyMode'   => 'plaintext',
    ],
    'exhibitions' => [
        'bodyFields' => ['description'],
        'dateField'  => 'openingDate',
        'extraFields' => [
            'location' => 'location',
            'ticketUrl' => 'ticketUrl',
        ],
    ],
],

Options

Key Type Default Description
bodyFields array [] Field handles whose content is concatenated into the body attribute
bodyMode string 'search' How body content is extracted — 'search' uses Craft's search keywords extractor (recommended for Matrix/CKEditor fields), 'plaintext' casts the field value directly to string
dateField string 'postDate' Which date field to use for filterDate and filterYear — useful when a section's entries have their own date field (e.g. an exhibition's opening date)
extraFields array [] Map of fieldHandle => algoliaAttribute for additional scalar fields to include

Field handles are the handles as they appear on the entry type's field layout. If a field's handle is overridden on the layout (e.g. an editorComponents field added as body), use the overridden handle (body). Fields that aren't on an entry type's layout are skipped for entries of that type.

Standard fields always indexed for entries

objectID, elementID, section, sectionHandle, title, url, postDate, filterDate, filterYear, body, siteID, siteHandle

Assets

Index assets from one or more volumes:

'assets' => [
    'documents' => [],
    'images' => [
        'extraFields' => [
            'altText' => 'altText',
        ],
    ],
],

Options

Key Type Default Description
extraFields array [] Map of fieldHandle => algoliaAttribute for additional scalar fields to include

Standard fields always indexed for assets

objectID, elementID, section, sectionHandle, title, url, updatedDate, filterDate, filterYear, siteID, siteHandle

Calendar Events

Index Solspace Calendar events from one or more calendars, keyed by calendar handle. This is for events managed in the Calendar plugin; entries in a section that happens to hold events are configured under entries. Calendar is optional: the plugin doesn't require it, and the events key is ignored (with a logged warning) when Calendar isn't installed.

'events' => [
    // Calendar handle => options
    'community' => [],
    'workshops' => [
        'bodyFields'  => ['description', 'agenda'],
        'extraFields' => [
            'organiser' => 'organiser',
        ],
    ],
],

An event is indexed when it's enabled for the site and, if its calendar's field layout has a searchable field, that field is on. Calendar events have no pending or expired state, and their post date doesn't hide them, so neither affects indexing.

Options

Key Type Default Description
bodyFields array The calendar's description field Field handles whose plain text is concatenated into the body attribute, replacing the default
extraFields array [] Map of fieldHandle => algoliaAttribute for additional scalar fields to include

Fields that aren't on the calendar's field layout are skipped, with a warning in the log.

Standard fields always indexed for Calendar events

Attribute Value
elementID, title, url, siteLanguage As for entries
calendar, calendarHandle The event's calendar name and handle
section, sectionHandle Also the calendar name and handle, so facets on sectionHandle work across entries and Calendar events
postDate Unix timestamp, or null
startDate, endDate Unix timestamps (UTC) of the event itself (its first occurrence, for recurring events)
allDay true for all-day events
filterDate, filterYear From startDate
body Plain text of the calendar's description field (set in the calendar's settings), or of bodyFields when configured; chunked like entry bodies
location Plain text of the calendar's location field
repeats, repeatFrequency Whether the event repeats, and how: DAILY, WEEKLY, MONTHLY, YEARLY or SELECT_DATES (null when it doesn't repeat)
occurrenceDates Unix start timestamps of occurrences from today up to 12 months ahead, at most 100 (override occurrenceWindow() / occurrenceLimit() to change), refreshed automatically as they run out. A non-repeating event has just its own start
lastDate When the final occurrence ends, or null for an event that repeats forever

objectID, siteID and siteHandle are added too, as for every record.

Recurring Calendar events

A recurring event is one record per site, not one per occurrence. Calendar stores it as a single element, and occurrenceDates lists its upcoming dates. Algolia matches a numeric filter on an array if any value matches, so to show upcoming events (including ones on today), filter on the start of today:

filters: `occurrenceDates >= ${startOfTodayTimestamp}`

As time passes, a recurring event's indexed dates would run out, so the plugin rebuilds its record automatically: when the event's last indexed occurrence starts, a delayed queue job re-indexes it with the next set of dates and queues the following refresh. Between refreshes, past dates stay in occurrenceDates; they don't affect the filter above. This needs a queue that supports long delays (see How Indexing Works).

Finished Calendar events stay in the index, like old entries do; filter them out as above if you only want upcoming ones.

Uninstalling Calendar doesn't remove its events' records from the index. Clear them first (e.g. delete the events, or deleteBy on calendarHandle after adding it as a filter-only facet), or clear the index and reindex.

The searchable Field

If an entry, asset or Calendar event field layout contains a field with the handle searchable (a Lightswitch field), the plugin uses it as an opt-in gate. The element is only indexed if the field is enabled. If no searchable field exists on the layout, all elements are indexed by default.

Custom Indexers

For complex cases — relational fields, Commerce products, or any logic that can't be expressed in config — register a custom indexer class:

'custom' => [
    \mynamespace\indexers\CareersIndexer::class,
],

Custom indexers are checked first and take priority over config-driven ones.

Writing a Custom Entry Indexer

Extend BaseEntryIndexer and implement sectionHandle() and getIndexData():

<?php

namespace mynamespace\indexers;

use craft\base\Element;
use craft\elements\Entry;
use korcontrol\craftalgolia\services\indexers\BaseEntryIndexer;

final class CareersIndexer extends BaseEntryIndexer
{
    public function sectionHandle(): string
    {
        return 'careers';
    }

    public function getIndexData(Element $element): array
    {
        /** @var Entry $entry */
        $entry = $element;

        return [
            'objectID'      => (string) $entry->id,
            'elementID'     => (string) $entry->id,
            'section'       => 'Careers',
            'sectionHandle' => 'careers',
            'title'         => (string) $entry->title,
            'url'           => (string) $entry->getUrl(),
            'department'    => (string) $entry->getFieldValue('department'),
            'body'          => $this->getSearchKeywordsForField($entry, 'jobDescription'),
        ];
    }
}

Writing a Custom Asset Indexer

Extend BaseAssetIndexer and implement volumeHandle() and getIndexData():

<?php

namespace mynamespace\indexers;

use craft\base\Element;
use craft\elements\Asset;
use korcontrol\craftalgolia\services\indexers\BaseAssetIndexer;

final class ReportsIndexer extends BaseAssetIndexer
{
    public function volumeHandle(): string
    {
        return 'reports';
    }

    public function getIndexData(Element $element): array
    {
        /** @var Asset $asset */
        $asset = $element;

        return [
            'objectID'      => (string) $asset->id,
            'elementID'     => (string) $asset->id,
            'section'       => 'Reports',
            'sectionHandle' => 'reports',
            'title'         => (string) $asset->title,
            'url'           => (string) $asset->getUrl(),
            'year'          => (string) $asset->getFieldValue('reportYear'),
        ];
    }
}

Writing a Custom Calendar Event Indexer

When the events config can't express what you need, extend BaseSolspaceEventIndexer and implement calendarHandle() and getIndexData(). It's only loaded when you extend it, so Calendar stays optional.

<?php

namespace mynamespace\indexers;

use craft\base\Element;
use korcontrol\craftalgolia\services\indexers\BaseSolspaceEventIndexer;
use Solspace\Calendar\Elements\Event;

final class CommunityCalendarIndexer extends BaseSolspaceEventIndexer
{
    public function calendarHandle(): string
    {
        return 'community';
    }

    public function getIndexData(Element $element): array
    {
        /** @var Event $event */
        $event = $element;

        return array_merge($this->getStandardEventData($event), [
            // A relation the `events` config can't express (extraFields are scalar)
            'categories' => $event->getFieldValue('eventCategories')->collect()->pluck('title')->all(),
        ]);
    }
}

Register it under custom, one indexer per calendar:

'custom' => [
    \mynamespace\indexers\CommunityCalendarIndexer::class,
],

getStandardEventData() returns the standard Calendar event fields; merge your own fields into it.

Reindexing

Since the plugin indexes on save, use Craft's built-in resave commands to index existing content:

# Reindex all entries in a section
php craft resave/entries --section=news

# Reindex all assets in a volume
php craft resave/assets --volume=documents

# Reindex all entries across all sections
php craft resave/entries

# Reindex a single entry (or a comma-separated list of IDs)
php craft resave/entries --elementId=42

Calendar events have their own resave command:

php craft calendar/events/resave

How Indexing Works

  • On save — when a live entry, an asset, or an enabled Calendar event is saved, the plugin deletes any existing Algolia records for that element and site, then writes fresh records
  • On delete — all Algolia records for the element are removed across all sites
  • Chunking — if the body field content exceeds Algolia's record size limit, it is automatically split into multiple records with suffixed objectIDs (e.g. 42-1-0, 42-1-1)
  • Multi-site — each site variant is indexed as a separate record with its own objectID (elementId-siteId), siteID, and siteHandle
  • Scheduled re-indexing — an entry with a future post date is indexed when it goes live, and removed when its expiry date passes; a recurring Calendar event is re-indexed before its indexed occurrence dates run out (see Recurring Calendar events). Both use delayed queue jobs, so the queue must support long delays: Craft's default database queue and Redis queues do; Amazon SQS caps delays at 15 minutes

Running Tests

The plugin includes a test suite using Codeception with a Docker-based Postgres instance.

# Start the test database
docker compose up -d

# Run all tests
vendor/bin/codecept run Unit