Appearance
Events Reference 
The Craft Search with Elastic plugin provides a comprehensive event system that allows developers to extend and customize the plugin's functionality. Events are triggered at key points during search, indexing, and management operations.
Event Registration 
Events follow Craft's standard event pattern. Register event listeners in your module, plugin, or bootstrap file:
php
use yii\base\Event;
use pennebaker\searchwithelastic\services\ElasticsearchService;
use pennebaker\searchwithelastic\events\SearchEvent;
Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_BEFORE_SEARCH,
    function(SearchEvent $event) {
        // Your event handler code
    }
);Search Events 
SearchEvent 
Namespace: pennebaker\searchwithelastic\events\SearchEvent
Triggered when search queries are executed against Elasticsearch.
Properties 
| Property | Type | Description | 
|---|---|---|
$query | string|array | The search query (can be modified by event handlers) | 
$params | array | Additional search parameters | 
$siteId | ?int | The site ID where the search is being performed | 
$skipDefaultSearch | bool | Whether the default search execution should be skipped | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_SEARCH | ElasticsearchService | Before executing a search query | 
EVENT_AFTER_SEARCH | ElasticsearchService | After executing a search query | 
Usage Examples 
Modify search query before execution:
php
use yii\base\Event;
use pennebaker\searchwithelastic\services\ElasticsearchService;
use pennebaker\searchwithelastic\events\SearchEvent;
Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_BEFORE_SEARCH,
    function(SearchEvent $event) {
        // Add site-specific boost
        if ($event->siteId === 1) {
            $event->params['boost'] = 1.5;
        }
        
        // Sanitize query
        $event->query = trim(strip_tags($event->query));
        
        // Add automatic filters
        if (!isset($event->params['filters'])) {
            $event->params['filters'] = [];
        }
        $event->params['filters']['status'] = 'live';
    }
);Custom search analytics:
php
Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_AFTER_SEARCH,
    function(SearchEvent $event) {
        // Log search analytics
        $resultCount = count($event->params['results'] ?? []);
        
        \Craft::info("Search performed: '{$event->query}' on site {$event->siteId}, {$resultCount} results", 'search-analytics');
        
        // Store in custom analytics table
        (new \craft\db\Query())
            ->createCommand()
            ->insert('{{%search_analytics}}', [
                'query' => $event->query,
                'siteId' => $event->siteId,
                'resultCount' => $resultCount,
                'dateCreated' => \craft\helpers\Db::prepareDateForDb(new \DateTime())
            ])
            ->execute();
    }
);ConnectionTestEvent 
Namespace: pennebaker\searchwithelastic\events\ConnectionTestEvent
Triggered when testing connections to Elasticsearch.
Properties 
| Property | Type | Description | 
|---|---|---|
$endpoint | string | The Elasticsearch endpoint being tested | 
$config | array | Connection configuration (passwords masked) | 
$result | ?bool | The test result (null initially, set during test) | 
$errorMessage | ?string | Error message if test failed | 
$skipDefaultTest | bool | Whether to skip the default test | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_CONNECTION_TEST | ElasticsearchService | Before testing connection | 
EVENT_AFTER_CONNECTION_TEST | ElasticsearchService | After testing connection | 
Usage Examples 
Custom connection validation:
php
use pennebaker\searchwithelastic\events\ConnectionTestEvent;
Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_BEFORE_CONNECTION_TEST,
    function(ConnectionTestEvent $event) {
        // Perform custom validation
        if (strpos($event->endpoint, 'localhost') !== false && \Craft::$app->getConfig()->general->devMode === false) {
            $event->result = false;
            $event->errorMessage = 'Localhost connections not allowed in production';
            $event->skipDefaultTest = true;
        }
    }
);Indexing Events 
IndexElementEvent 
Namespace: pennebaker\searchwithelastic\events\IndexElementEvent
Triggered when indexing or removing elements from Elasticsearch.
Properties 
| Property | Type | Description | 
|---|---|---|
$element | Element | The element being indexed or removed | 
$documentData | array | The document data that will be indexed (modifiable) | 
$indexName | string | The Elasticsearch index name | 
$isNew | bool | Whether the element is being newly created | 
$skipDefaultOperation | bool | Whether to skip the default operation | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_INDEX_ELEMENT | ElementIndexerService | Before indexing an element | 
EVENT_AFTER_INDEX_ELEMENT | ElementIndexerService | After indexing an element | 
EVENT_BEFORE_REMOVE_ELEMENT | ElementIndexerService | Before removing an element | 
EVENT_AFTER_REMOVE_ELEMENT | ElementIndexerService | After removing an element | 
Usage Examples 
Add custom fields to index:
php
use pennebaker\searchwithelastic\services\ElementIndexerService;
use pennebaker\searchwithelastic\events\IndexElementEvent;
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_BEFORE_INDEX_ELEMENT,
    function(IndexElementEvent $event) {
        $element = $event->element;
        
        // Add custom metadata
        $event->documentData['customCategory'] = $element->getFieldValue('category')?->title ?? 'Uncategorized';
        $event->documentData['authorName'] = $element->getAuthor()?->fullName ?? 'Anonymous';
        $event->documentData['wordCount'] = str_word_count(strip_tags($event->documentData['content'] ?? ''));
        
        // Add computed fields
        if ($element instanceof \craft\elements\Entry) {
            $event->documentData['entryAge'] = time() - $element->postDate->getTimestamp();
            $event->documentData['isRecent'] = $event->documentData['entryAge'] < (30 * 24 * 60 * 60); // 30 days
        }
        
        // Add tags from related elements
        $tags = [];
        foreach ($element->getFieldValue('relatedTags')->all() as $tag) {
            $tags[] = $tag->title;
        }
        $event->documentData['tags'] = $tags;
    }
);Custom indexing logic:
php
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_BEFORE_INDEX_ELEMENT,
    function(IndexElementEvent $event) {
        // Skip indexing draft entries in production
        if (!Craft::$app->getConfig()->general->devMode && $event->element->getIsDraft()) {
            $event->skipDefaultOperation = true;
            return;
        }
        
        // Custom privacy handling
        if ($event->element->getFieldValue('isPrivate')) {
            $event->documentData['content'] = '[Private content]';
            $event->documentData['searchable'] = false;
        }
    }
);Post-indexing operations:
php
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_AFTER_INDEX_ELEMENT,
    function(IndexElementEvent $event) {
        // Invalidate related caches
        \Craft::$app->getCache()->delete("search_cache_site_{$event->element->siteId}");
        
        // Update related counters
        if ($event->element instanceof \craft\elements\Entry) {
            $this->updateCategoryCounter($event->element->getFieldValue('category'));
        }
        
        // Send webhook notification
        $this->sendWebhookNotification('element_indexed', [
            'elementId' => $event->element->id,
            'elementType' => get_class($event->element),
            'siteId' => $event->element->siteId,
            'indexName' => $event->indexName
        ]);
    }
);ContentExtractionEvent 
Namespace: pennebaker\searchwithelastic\events\ContentExtractionEvent
Triggered when extracting indexable content from HTML.
Properties 
| Property | Type | Description | 
|---|---|---|
$rawContent | string | The raw HTML content to extract from | 
$extractedContent | string | The extracted content (modifiable) | 
$element | ?Element | The element being indexed (optional context) | 
$skipDefaultExtraction | bool | Whether to skip the default extraction | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_CONTENT_EXTRACTION | ElementIndexerService | During content extraction from HTML | 
Usage Examples 
Custom content extraction:
php
use pennebaker\searchwithelastic\services\ElementIndexerService;
use pennebaker\searchwithelastic\events\ContentExtractionEvent;
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_CONTENT_EXTRACTION,
    function(ContentExtractionEvent $event) {
        // Custom extraction for specific element types
        if ($event->element instanceof \craft\elements\Entry && $event->element->section->handle === 'products') {
            // Extract product-specific information
            $dom = new \DOMDocument();
            @$dom->loadHTML($event->rawContent);
            
            $xpath = new \DOMXPath($dom);
            
            // Extract product specifications
            $specs = $xpath->query('//div[@class="product-specs"]//text()');
            $specText = '';
            foreach ($specs as $spec) {
                $specText .= trim($spec->nodeValue) . ' ';
            }
            
            // Extract reviews
            $reviews = $xpath->query('//div[@class="reviews"]//p/text()');
            $reviewText = '';
            foreach ($reviews as $review) {
                $reviewText .= trim($review->nodeValue) . ' ';
            }
            
            $event->extractedContent = trim($specText . ' ' . $reviewText);
            $event->skipDefaultExtraction = true;
        }
    }
);Content filtering and enhancement:
php
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_CONTENT_EXTRACTION,
    function(ContentExtractionEvent $event) {
        // Remove unwanted content
        $event->extractedContent = preg_replace('/\bPassword:\s*\S+/i', '[Password Removed]', $event->extractedContent);
        $event->extractedContent = preg_replace('/\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/', '[Card Number Removed]', $event->extractedContent);
        
        // Enhance content with metadata
        if ($event->element) {
            $author = $event->element->getAuthor();
            if ($author) {
                $event->extractedContent .= " Author: {$author->fullName}";
            }
            
            // Add field values to searchable content
            $category = $event->element->getFieldValue('category');
            if ($category) {
                $event->extractedContent .= " Category: {$category->title}";
            }
        }
    }
);Management Events 
IndexManagementEvent 
Namespace: pennebaker\searchwithelastic\events\IndexManagementEvent
Triggered during index management operations.
Properties 
| Property | Type | Description | 
|---|---|---|
$siteId | int | The site ID | 
$indexName | string | The Elasticsearch index name | 
$indexConfig | array | The index configuration (modifiable) | 
$operation | string | The operation type ('create', 'delete', 'recreate') | 
$indexExisted | bool | Whether the index existed before the operation | 
$skipDefaultOperation | bool | Whether to skip the default operation | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_CREATE_INDEX | IndexManagementService | Before creating an index | 
EVENT_AFTER_CREATE_INDEX | IndexManagementService | After creating an index | 
EVENT_BEFORE_DELETE_INDEX | IndexManagementService | Before deleting an index | 
EVENT_AFTER_DELETE_INDEX | IndexManagementService | After deleting an index | 
EVENT_BEFORE_RECREATE_INDEX | IndexManagementService | Before recreating an index | 
EVENT_AFTER_RECREATE_INDEX | IndexManagementService | After recreating an index | 
Usage Examples 
Custom index configuration:
php
use pennebaker\searchwithelastic\services\IndexManagementService;
use pennebaker\searchwithelastic\events\IndexManagementEvent;
Event::on(
    IndexManagementService::class,
    IndexManagementService::EVENT_BEFORE_CREATE_INDEX,
    function(IndexManagementEvent $event) {
        // Add custom analyzers
        $event->indexConfig['settings']['analysis']['analyzer']['custom_html'] = [
            'type' => 'custom',
            'tokenizer' => 'standard',
            'char_filter' => ['html_strip'],
            'filter' => ['lowercase', 'stop']
        ];
        
        // Add custom field mappings
        $event->indexConfig['mappings']['properties']['customField'] = [
            'type' => 'text',
            'analyzer' => 'custom_html'
        ];
        
        // Increase shards for high-traffic sites
        if ($event->siteId === 1) {
            $event->indexConfig['settings']['number_of_shards'] = 3;
            $event->indexConfig['settings']['number_of_replicas'] = 1;
        }
    }
);Index lifecycle hooks:
php
Event::on(
    IndexManagementService::class,
    IndexManagementService::EVENT_AFTER_CREATE_INDEX,
    function(IndexManagementEvent $event) {
        // Set up index aliases
        $connection = \pennebaker\searchwithelastic\SearchWithElastic::getConnection();
        $aliasName = "craft-site-{$event->siteId}";
        
        $connection->post(['_aliases'], [], json_encode([
            'actions' => [
                ['add' => ['index' => $event->indexName, 'alias' => $aliasName]]
            ]
        ]));
        
        // Log index creation
        \Craft::info("Created index {$event->indexName} for site {$event->siteId}", 'elasticsearch');
    }
);Queue Events 
QueueEvent 
Namespace: pennebaker\searchwithelastic\events\QueueEvent
Triggered during queue management operations.
Properties 
| Property | Type | Description | 
|---|---|---|
$elementModels | array | Array of IndexableElementModel instances | 
$operation | string | The operation type ('enqueue', 'clear') | 
$jobIds | ?array | Array of job IDs (set after enqueue) | 
$skipDefaultOperation | bool | Whether to skip the default operation | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_ENQUEUE_JOBS | ReindexQueueManagementService | Before enqueuing jobs | 
EVENT_AFTER_ENQUEUE_JOBS | ReindexQueueManagementService | After enqueuing jobs | 
EVENT_BEFORE_CLEAR_QUEUE | ReindexQueueManagementService | Before clearing queue | 
EVENT_AFTER_CLEAR_QUEUE | ReindexQueueManagementService | After clearing queue | 
Usage Examples 
Custom queue management:
php
use pennebaker\searchwithelastic\services\ReindexQueueManagementService;
use pennebaker\searchwithelastic\events\QueueEvent;
Event::on(
    ReindexQueueManagementService::class,
    ReindexQueueManagementService::EVENT_BEFORE_ENQUEUE_JOBS,
    function(QueueEvent $event) {
        // Filter out certain element types during peak hours
        if (date('G') >= 9 && date('G') <= 17) { // Business hours
            $event->elementModels = array_filter($event->elementModels, function($model) {
                return $model->type !== \craft\elements\Asset::class;
            });
        }
        
        // Prioritize certain element types
        usort($event->elementModels, function($a, $b) {
            $priority = [
                \craft\elements\Entry::class => 1,
                \craft\elements\Category::class => 2,
                \craft\elements\Asset::class => 3,
            ];
            
            return ($priority[$a->type] ?? 999) <=> ($priority[$b->type] ?? 999);
        });
    }
);Model Events 
ModelEvent 
Namespace: pennebaker\searchwithelastic\events\ModelEvent
Triggered during model creation operations.
Properties 
| Property | Type | Description | 
|---|---|---|
$element | Element | The Craft element | 
$siteId | int | The site ID | 
$model | IndexableElementModel | The indexable element model (modifiable) | 
$skipDefaultCreation | bool | Whether to skip default model creation | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_CREATE_MODEL | ModelService | Before creating a model | 
EVENT_AFTER_CREATE_MODEL | ModelService | After creating a model | 
Record Events 
RecordEvent 
Namespace: pennebaker\searchwithelastic\events\RecordEvent
Triggered during record operations.
Properties 
| Property | Type | Description | 
|---|---|---|
$element | Element | The element | 
$documentId | ?string | The document ID | 
$operation | string | The operation type ('save', 'delete') | 
$result | bool | The operation result | 
$skipDefaultOperation | bool | Whether to skip the default operation | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_SAVE_RECORD | RecordService | Before saving a record | 
EVENT_AFTER_SAVE_RECORD | RecordService | After saving a record | 
EVENT_BEFORE_DELETE_RECORD | RecordService | Before deleting a record | 
EVENT_AFTER_DELETE_RECORD | RecordService | After deleting a record | 
Query Events 
QueryEvent 
Namespace: pennebaker\searchwithelastic\events\QueryEvent
Triggered during query building operations.
Properties 
| Property | Type | Description | 
|---|---|---|
$siteId | int | The site ID | 
$elementType | string | The element type class name | 
$query | mixed | The query builder instance (modifiable) | 
Events 
| Event Constant | Service | Trigger Point | 
|---|---|---|
EVENT_BEFORE_BUILD_QUERY | QueryService | Before building a query | 
EVENT_AFTER_BUILD_QUERY | QueryService | After building a query | 
Usage Example 
Custom query modifications:
php
use pennebaker\searchwithelastic\services\QueryService;
use pennebaker\searchwithelastic\events\QueryEvent;
Event::on(
    QueryService::class,
    QueryService::EVENT_BEFORE_BUILD_QUERY,
    function(QueryEvent $event) {
        // Add custom filtering for specific sites
        if ($event->siteId === 2 && $event->elementType === \craft\elements\Entry::class) {
            $event->query->section('news'); // Only news section for site 2
        }
        
        // Add date-based filtering
        if ($event->elementType === \craft\elements\Entry::class) {
            $event->query->postDate('>= ' . (new \DateTime('-1 year'))->format('Y-m-d'));
        }
    }
);Best Practices 
Event Handler Performance 
Keep event handlers lightweight and fast:
php
// Good: Quick operations
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    $event->data['customField'] = 'value';
});
// Avoid: Heavy operations in events
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    // Don't do this - it will slow down indexing
    $heavyData = $this->performExpensiveApiCall();
    $event->data['heavyData'] = $heavyData;
});Error Handling 
Always include error handling in event listeners:
php
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    try {
        // Your event handling code
        $event->data['processed'] = $this->processData($event->data);
    } catch (\Exception $e) {
        \Craft::error("Event handler failed: " . $e->getMessage(), 'search-plugin');
        // Don't re-throw - let the operation continue
    }
});Conditional Processing 
Use conditional logic to avoid unnecessary processing:
php
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    // Skip processing for certain element types
    if (!$event->element instanceof \craft\elements\Entry) {
        return;
    }
    
    // Skip processing for disabled sites
    if (!$event->element->getSite()->enabled) {
        return;
    }
    
    // Your processing code here
});Memory Management 
For events that process many elements, be mindful of memory usage:
php
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    // Process data
    $processedData = $this->processElement($event->element);
    $event->data = array_merge($event->data, $processedData);
    
    // Clear references to prevent memory leaks
    unset($processedData);
});