Extending¶
Custom Engines¶
Register additional search engines by listening to the EVENT_REGISTER_ENGINE_TYPES event on IndexesController:
use cogapp\searchindex\controllers\IndexesController;
use cogapp\searchindex\events\RegisterEngineTypesEvent;
use yii\base\Event;
Event::on(
IndexesController::class,
IndexesController::EVENT_REGISTER_ENGINE_TYPES,
function(RegisterEngineTypesEvent $event) {
$event->types[] = \myplugin\engines\MyCustomEngine::class;
}
);
Your engine class must implement cogapp\searchindex\engines\EngineInterface. You can extend cogapp\searchindex\engines\AbstractEngine for a head start, which provides environment variable parsing, index name prefixing, and default batch method implementations.
The interface requires these methods:
// -- Lifecycle -----------------------------------------------------------
/**
* Create a new index (collection/table) in the search engine.
*
* @param Index $index The index model containing handle and field mappings.
* @return void
*/
public function createIndex(Index $index): void;
/**
* Push updated settings or mappings to an existing index.
*
* @param Index $index The index model whose settings should be synced.
* @return void
*/
public function updateIndexSettings(Index $index): void;
/**
* Delete an index and all of its documents from the search engine.
*
* @param Index $index The index to delete.
* @return void
*/
public function deleteIndex(Index $index): void;
/**
* Check whether the index already exists in the search engine.
*
* @param Index $index The index to check.
* @return bool True if the index exists.
*/
public function indexExists(Index $index): bool;
// -- Document CRUD -------------------------------------------------------
/**
* Add or update a single document in the index.
*
* @param Index $index The target index.
* @param int $elementId The Craft element ID used as the document key.
* @param array<string, mixed> $document The document body as an associative array.
* @return void
*/
public function indexDocument(Index $index, int $elementId, array $document): void;
/**
* Add or update multiple documents in a single batch operation.
*
* @param Index $index The target index.
* @param array<int, array<string, mixed>> $documents Array of document bodies, each containing an 'objectID' key.
* @return void
*/
public function indexDocuments(Index $index, array $documents): void;
/**
* Remove a single document from the index by element ID.
*
* @param Index $index The target index.
* @param int $elementId The Craft element ID of the document to remove.
* @return void
*/
public function deleteDocument(Index $index, int $elementId): void;
/**
* Remove multiple documents from the index in a single batch operation.
*
* @param Index $index The target index.
* @param int[] $elementIds Array of Craft element IDs to remove.
* @return void
*/
public function deleteDocuments(Index $index, array $elementIds): void;
/**
* Remove all documents from the index without deleting the index itself.
*
* @param Index $index The index to flush.
* @return void
*/
public function flushIndex(Index $index): void;
// -- Document retrieval ---------------------------------------------------
/**
* Retrieve a single document from the index by its ID.
*
* @param Index $index The index to query.
* @param string $documentId The document ID to retrieve.
* @return array<string, mixed>|null The document as an associative array, or null if not found.
*/
public function getDocument(Index $index, string $documentId): ?array;
// -- Search --------------------------------------------------------------
/**
* Search within facet values for the given fields.
*
* Each engine should use its native facet search API where available
* (e.g. Meilisearch facetSearch, Algolia searchForFacetValues, Typesense
* facet_query). The AbstractEngine provides a fallback that searches with
* the query and returns facets from matching documents.
*
* @param Index $index The index to search.
* @param string[] $facetFields The facet field names to search within.
* @param string $query The query to match against facet values.
* @param int $maxPerField Maximum values to return per field.
* @param array<string, mixed> $filters Optional filters to narrow the facet value context.
* @return array<string, array<array{value: string, count: int}>> Grouped by field name.
*/
public function searchFacetValues(Index $index, array $facetFields, string $query, int $maxPerField = 5, array $filters = []): array;
/**
* Execute a search query against the index.
*
* Unified pagination options (`page` and `perPage`) are extracted automatically.
* Engine-native pagination keys (e.g. `from`/`size`, `offset`/`limit`) still
* work and take precedence when provided.
*
* @param Index $index The index to search.
* @param string $query The search query string.
* @param array<string, mixed> $options Search options — supports unified `page`/`perPage` plus engine-specific keys.
* @return SearchResult Normalised result with hits, pagination, facets, and raw response.
*/
public function search(Index $index, string $query, array $options = []): SearchResult;
/**
* Find documents related to a given document ("More Like This").
*
* Returns similar documents based on the content of the source document.
* Engines with native MLT support (Elasticsearch, OpenSearch) use their
* built-in queries. Other engines fall back to keyword extraction + search.
*
* @param Index $index The index to search.
* @param string $documentId The source document ID to find similar content for.
* @param int $perPage Maximum number of related documents to return.
* @param string[] $fields Optional field names to base similarity on. Defaults to text fields.
* @return SearchResult Normalised result with related hits.
*/
public function relatedSearch(Index $index, string $documentId, int $perPage = 5, array $fields = []): SearchResult;
/**
* Return the total number of documents stored in the index.
*
* @param Index $index The index to count.
* @return int Document count.
*/
public function getDocumentCount(Index $index): int;
/**
* Return all document IDs stored in the engine for the given index.
* Used by orphan cleanup to find stale documents.
*
* @param Index $index The index to query.
* @return string[] Array of document ID strings.
*/
public function getAllDocumentIds(Index $index): array;
/**
* Execute multiple search queries in a single batch request.
*
* Each query is an array with 'index' (Index model), 'query' (string),
* and optional 'options' (array). Returns one SearchResult per query
* in the same order.
*
* @param array<int, array{index: Index, query: string, options?: array<string, mixed>}> $queries Array of ['index' => Index, 'query' => string, 'options' => array]
* @return SearchResult[] One result per query, in the same order.
*/
public function multiSearch(array $queries): array;
// -- Schema --------------------------------------------------------------
/**
* Retrieve the current schema/settings for the index from the engine.
*
* Returns engine-specific information about the index structure,
* such as field mappings, searchable attributes, or collection schema.
*
* @param Index $index The index to inspect.
* @return array<string, mixed> Engine-specific schema/settings array.
*/
public function getIndexSchema(Index $index): array;
/**
* Extract a normalised list of field names and types from the live engine schema.
*
* Returns an array of associative arrays, each with 'name' (string) and
* 'type' (a FieldMapping::TYPE_* constant) keys.
*
* @param Index $index The index to inspect.
* @return array<array{name: string, type: string}> Normalised field list.
*/
public function getSchemaFields(Index $index): array;
/**
* Map a generic plugin field type constant to the engine-native field type.
*
* @param string $indexFieldType A FieldMapping::TYPE_* constant.
* @return mixed The engine-specific type representation.
*/
public function mapFieldType(string $indexFieldType): mixed;
/**
* Build a complete schema definition from the given field mappings.
*
* @param array<int, \cogapp\searchindex\models\FieldMapping> $fieldMappings Array of FieldMapping models.
* @return array<string, mixed> Engine-specific schema/settings array.
*/
public function buildSchema(array $fieldMappings): array;
// -- Atomic swap ----------------------------------------------------------
/**
* Whether this engine supports atomic index swapping for zero-downtime refresh.
*
* @return bool
*/
public function supportsAtomicSwap(): bool;
/**
* Return the handle to use for the temporary swap index.
*
* Alias-based engines (ES, OpenSearch, Typesense) alternate between
* `{handle}_swap_a` and `{handle}_swap_b` so the production alias can be
* atomically re-pointed. Direct-rename engines (Algolia, Meilisearch)
* simply use `{handle}_swap`.
*
* @param Index $index The production index.
* @return string The swap index handle.
*/
public function buildSwapHandle(Index $index): string;
/**
* Perform an atomic swap between the production index and a temporary index.
*
* Only called when supportsAtomicSwap() returns true. The temporary index
* has already been created and populated with documents.
*
* @param Index $index The production index.
* @param Index $swapIndex A cloned index with modified handle pointing to the temp index.
* @return void
*/
public function swapIndex(Index $index, Index $swapIndex): void;
// -- Info ----------------------------------------------------------------
/**
* Return the human-readable display name of the engine (e.g. "Elasticsearch").
*
* @return string
*/
public static function displayName(): string;
/**
* Return the Composer package name required by this engine.
*
* @return string e.g. "algolia/algoliasearch-client-php"
*/
public static function requiredPackage(): string;
/**
* Whether the engine's required PHP client library is installed.
*
* @return bool
*/
public static function isClientInstalled(): bool;
/**
* Return the per-index configuration field definitions for the CP UI.
*
* @return array<string, array<string, mixed>> Associative array of field handles to field config arrays.
*/
public static function configFields(): array;
/**
* Test whether the plugin can reach the search engine with the current settings.
*
* @return bool True if the connection is healthy.
*/
public function testConnection(): bool;
Custom Field Resolvers¶
Register additional field resolvers by listening to the EVENT_REGISTER_FIELD_RESOLVERS event on FieldMapper:
use cogapp\searchindex\services\FieldMapper;
use cogapp\searchindex\events\RegisterFieldResolversEvent;
use yii\base\Event;
Event::on(
FieldMapper::class,
FieldMapper::EVENT_REGISTER_FIELD_RESOLVERS,
function(RegisterFieldResolversEvent $event) {
// Map a Craft field class to your resolver class
$event->resolvers[\myplugin\fields\MyField::class] = \myplugin\resolvers\MyFieldResolver::class;
}
);
Your resolver must implement cogapp\searchindex\resolvers\FieldResolverInterface:
interface FieldResolverInterface
{
/**
* Resolve the indexable value for a given element and field.
*
* @param Element $element The Craft element to extract data from.
* @param FieldInterface|null $field The Craft field instance, or null for attribute-based resolvers.
* @param FieldMapping $mapping The field mapping configuration.
* @return mixed The resolved value suitable for indexing, or null if unavailable.
*/
public function resolve(Element $element, ?FieldInterface $field, FieldMapping $mapping): mixed;
/**
* Return the list of Craft field type classes this resolver supports.
*
* @return array<int, class-string> List of fully qualified field class names.
*/
public static function supportedFieldTypes(): array;
}
Events¶
FieldMapper::EVENT_REGISTER_FIELD_RESOLVERS¶
Fired when building the resolver map. Use this to add or override field resolvers.
- Event class:
cogapp\searchindex\events\RegisterFieldResolversEvent - Property:
resolvers--array<string, string>mapping field class names to resolver class names.
FieldMapper::EVENT_BEFORE_INDEX_ELEMENT¶
Fired after a document is resolved but before it is sent to the engine. Use this to modify the document data.
- Event class:
cogapp\searchindex\events\ElementIndexEvent - Properties:
element(the Craft element),index(the Index model),document(the resolved document array -- mutable).
use cogapp\searchindex\services\FieldMapper;
use cogapp\searchindex\events\ElementIndexEvent;
use yii\base\Event;
Event::on(
FieldMapper::class,
FieldMapper::EVENT_BEFORE_INDEX_ELEMENT,
function(ElementIndexEvent $event) {
// Add a custom field to every document
$event->document['customField'] = 'custom value';
}
);
IndexesController::EVENT_REGISTER_ENGINE_TYPES¶
Fired when building the list of available engine types. Use this to register custom engines.
- Event class:
cogapp\searchindex\events\RegisterEngineTypesEvent - Property:
types--string[]array of engine class names.
Index Lifecycle Events¶
Fired by the Indexes service during index save/delete operations:
| Constant | When | Event class |
|---|---|---|
Indexes::EVENT_BEFORE_SAVE_INDEX |
Before an index is saved. | cogapp\searchindex\events\IndexEvent |
Indexes::EVENT_AFTER_SAVE_INDEX |
After an index is saved. | cogapp\searchindex\events\IndexEvent |
Indexes::EVENT_BEFORE_DELETE_INDEX |
Before an index is deleted. | cogapp\searchindex\events\IndexEvent |
Indexes::EVENT_AFTER_DELETE_INDEX |
After an index is deleted. | cogapp\searchindex\events\IndexEvent |
Document Sync Events¶
Fired by the Sync service after documents are indexed or deleted. Use these to trigger side effects (e.g. cache invalidation, analytics, webhooks).
| Constant | When | Fired from |
|---|---|---|
Sync::EVENT_AFTER_INDEX_ELEMENT |
After a single document is indexed. | IndexElementJob |
Sync::EVENT_AFTER_DELETE_ELEMENT |
After a single document is deleted. | DeindexElementJob |
Sync::EVENT_AFTER_BULK_INDEX |
After a batch of documents is indexed. | BulkIndexJob |
- Event class:
cogapp\searchindex\events\DocumentSyncEvent - Properties:
index(Index model),elementId(int),action('upsert'or'delete').
use cogapp\searchindex\services\Sync;
use cogapp\searchindex\events\DocumentSyncEvent;
use yii\base\Event;
Event::on(
Sync::class,
Sync::EVENT_AFTER_INDEX_ELEMENT,
function(DocumentSyncEvent $event) {
// $event->index — the Index model
// $event->elementId — the Craft element ID
// $event->action — 'upsert'
}
);