Custom Observers
Observers enable real-time synchronization between WordPress and your search index.
Overview
Observers watch for WordPress events (post saves, deletes, meta changes) and trigger index updates. Each index type needs an observer to stay synchronized.
Architecture
WordPress Event (post_save, delete, etc.)
↓
Observer::observe() hooks into events
↓
Event handler validates and processes
↓
Queue or direct sync to search engine
Creating a Custom Observer
Extend BaseObserver and implement the observe() method:
<?php
namespace MyPlugin\Observers;
use CelerSearch\DataTransfer\IndexCandidate;
use CelerSearch\Observers\BaseObserver;
class EventObserver extends BaseObserver {
public function observe(): void {
// Hook into WordPress events
add_action('wp_after_insert_post', [$this, 'on_event_saved'], 10, 2);
add_action('before_delete_post', [$this, 'on_event_deleted']);
add_action('trashed_post', [$this, 'on_event_trashed']);
// Watch meta changes
add_action('updated_post_meta', [$this, 'on_meta_updated'], 10, 4);
}
public function on_event_saved(int $post_id, $post = null): void {
// Skip autosaves
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
$post = $post ?? get_post($post_id);
if (!$post) {
return;
}
// Check if this index supports this post
$candidate = new IndexCandidate($post);
if (!$this->index->is_supported($candidate)) {
return;
}
// Allow filtering
if (!apply_filters('my_plugin_should_sync_event', true, $post, $this->index)) {
return;
}
// Use queue if available, otherwise sync directly
if ($this->should_use_queue()) {
$this->queue_sync('post', $post->ID);
} else {
$this->sync_item($post);
}
}
public function on_event_deleted(int $post_id): void {
$post = get_post($post_id);
if (!$post) {
return;
}
$candidate = new IndexCandidate($post);
if (!$this->index->is_supported($candidate)) {
return;
}
if ($this->should_use_queue()) {
$this->queue_delete('post', $post->ID);
} else {
$this->delete_item($post);
}
}
public function on_event_trashed(int $post_id): void {
// Treat trash as delete
$this->on_event_deleted($post_id);
}
public function on_meta_updated($meta_id, $post_id, $meta_key, $meta_value): void {
// Only watch specific meta keys
$watched_keys = ['start_date', 'end_date', 'venue', 'is_featured'];
if (!in_array($meta_key, $watched_keys, true)) {
return;
}
// Re-sync the post
$this->on_event_saved($post_id);
}
}
Registering Your Observer
Use the celersearch_observer_for_index filter:
add_filter('celersearch_observer_for_index', function($observer, $index) {
if ($index instanceof \MyPlugin\Indices\CustomEventsIndex) {
return new \MyPlugin\Observers\EventObserver($index);
}
return $observer;
}, 10, 2);
BaseObserver Methods
Your observer inherits these methods from BaseObserver:
sync_item($item)
Syncs a raw item (WP_Post, etc.) to the index:
// Called internally
$this->sync_item($post);
// What it does:
// 1. Wraps item in IndexCandidate
// 2. Checks is_supported() and should_index()
// 3. Calls get_candidate_objects()
// 4. Sends to search engine
delete_item($item)
Deletes an item from the index:
$this->delete_item($post);
// Handles split records automatically
delete_post_records($post_id)
Deletes all records for a post ID (handles splits):
$this->delete_post_records($post_id);
// Deletes: 123-0, 123-1, 123-2, etc.
queue_sync($item_type, $item_id)
Schedules a sync via Action Scheduler:
$this->queue_sync('post', $post->ID);
// Schedules with 5-second debounce
queue_delete($item_type, $item_id)
Schedules a delete via Action Scheduler:
$this->queue_delete('post', $post->ID);
// Runs immediately (no debounce)
should_use_queue()
Checks if queue system is enabled:
if ($this->should_use_queue()) {
$this->queue_sync('post', $id);
} else {
$this->sync_item($post);
}
Handling Non-Post Content
For custom database tables or external data:
class CustomTableObserver extends BaseObserver {
public function observe(): void {
// Hook into your custom events
add_action('my_table_row_inserted', [$this, 'on_row_saved']);
add_action('my_table_row_updated', [$this, 'on_row_saved']);
add_action('my_table_row_deleted', [$this, 'on_row_deleted']);
}
public function on_row_saved($row): void {
$candidate = new IndexCandidate($row);
if (!$this->index->is_supported($candidate)) {
return;
}
if ($this->should_use_queue()) {
$this->queue_sync('custom_row', $row->id);
} else {
$this->sync_item($row);
}
}
public function on_row_deleted($row_id): void {
// For queue, we need the full object
// For direct delete, we can use the ID
if ($this->should_use_queue()) {
$this->queue_delete('custom_row', $row_id);
} else {
// Create minimal object for deletion
$row = (object) ['id' => $row_id];
$this->delete_item($row);
}
}
}
Queue Handler Registration
For non-post items, you need to handle the queue processing:
// In your index class, implement these for queue support:
public function get_item_for_sync(string $item_type, $item_id) {
if ($item_type === 'custom_row') {
global $wpdb;
return $wpdb->get_row(
$wpdb->prepare("SELECT * FROM my_table WHERE id = %d", $item_id)
);
}
return null;
}
public function delete_from_index($item_type, $item_id): void {
if ($item_type === 'custom_row') {
$this->get_service()->delete_item(
$this->get_index_uid(),
(string) $item_id
);
}
}
Best Practices
Check Support First
Always verify is_supported() before processing to avoid unnecessary work:
$candidate = new IndexCandidate($item);
if (!$this->index->is_supported($candidate)) {
return;
}
Use Queue When Available
The queue system provides debouncing and reliability:
if ($this->should_use_queue()) {
$this->queue_sync('post', $id);
} else {
$this->sync_item($item);
}
Skip Autosaves
Always skip WordPress autosaves:
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}