Everything built so far assumes an up-to-date index. It won’t stay that way on its own, a post gets edited an hour after being indexed, another gets unpublished, a third gets deleted outright, and every one of those actions needs the embeddings table to react. An assistant answering from stale, deleted, or unpublished content is arguably worse than one admitting it doesn’t know, since a stale answer looks confident and current when it’s neither.
Lesson 5’s my_plugin_index_post() and its ai_chunk_embeddings table. This lesson
hooks that function into WordPress’s real content lifecycle.
Step 1: Re-indexing on save_post, correctly
save_post fires on every save, including autosaves and revisions, neither of which
should trigger a real re-index:
// File: my-plugin/class-sync.php
add_action( 'save_post', 'my_plugin_sync_on_save', 10, 3 );
function my_plugin_sync_on_save( int $post_id, WP_Post $post, bool $update ) {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
if ( 'publish' !== $post->post_status ) {
// Not published (or no longer published): make sure nothing lingers.
my_plugin_remove_post_from_index( $post_id );
return;
}
my_plugin_index_post( $post_id );
}
Reusing Lesson 5’s my_plugin_index_post(), which already deletes a post’s existing
chunks before re-inserting fresh ones, means calling it again here is safe to run every
time a published post is saved, it never accumulates duplicate rows.
Step 2: Removing embeddings for unpublished or deleted content
The unpublish case is handled inside my_plugin_sync_on_save() above, since a post
moving to draft or private still fires save_post. Outright deletion is a separate
hook:
// File: my-plugin/class-sync.php
add_action( 'before_delete_post', 'my_plugin_remove_post_from_index' );
function my_plugin_remove_post_from_index( int $post_id ) {
global $wpdb;
$wpdb->delete(
$wpdb->prefix . 'ai_chunk_embeddings',
array( 'post_id' => $post_id ),
array( '%d' )
);
}
Using before_delete_post rather than deleted_post means cleanup runs while the post
still exists, which matters if your cleanup logic ever needs to read anything from the
post itself before it’s gone, and it runs reliably even for a post moved straight to the
trash and deleted immediately after.
Step 3: Why this matters more than it looks
Skip this lesson’s hooks entirely and the pipeline still runs, right up until content changes. From then on, every answer risks being grounded in a version of a post that no longer exists: a corrected price, a removed paragraph, a policy that changed last week. The retrieved chunk still scores well against the query, cosine similarity has no concept of “current,” it only knows what’s stored, so a stale row surfaces with exactly the same confidence as a fresh one.
Step 4: Bulk re-indexing existing content with WP-CLI
Everything above handles content going forward. Content that existed before this pipeline was installed needs a one-time bulk pass:
// File: my-plugin/class-sync.php
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'my-plugin reindex', function () {
$posts = get_posts( array(
'post_type' => 'post',
'post_status' => 'publish',
'numberposts' => -1,
'fields' => 'ids',
) );
foreach ( $posts as $post_id ) {
my_plugin_index_post( $post_id );
WP_CLI::log( "Indexed post {$post_id}" );
}
WP_CLI::success( count( $posts ) . ' posts indexed.' );
} );
}
Run it once after installing the pipeline, and again any time you change chunking strategy (Lesson 3) or embedding dimensions (Lesson 2), since either change makes previously-stored embeddings incomparable to newly-generated query embeddings.
Test it: edit a post and confirm the index updates
wp-env run cli wp post update 1 --post_content="Updated content for testing sync."
wp-env run cli wp eval '
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT chunk_text FROM {$wpdb->prefix}ai_chunk_embeddings WHERE post_id = %d", 1 ) );
foreach ( $rows as $row ) { echo $row->chunk_text . "\n"; }
'
The stored chunk text should reflect the update you just made, not whatever the post contained when it was first indexed.
It’s easy to build the indexing pipeline, run it once, ship the assistant, and never
revisit it, the assistant works perfectly in every demo because demo content doesn’t
change. In production, content changes constantly, and without save_post wired up,
every edit silently drifts the index further from reality with no error, no warning,
and no visible symptom until a user asks about the exact thing that changed and gets a
confidently wrong, out-of-date answer.
Recap
save_post, filtered to skip revisions and autosaves and to remove rather than
re-index unpublished content, keeps the embeddings table current as content changes.
before_delete_post removes rows for content that’s gone entirely. A one-time WP-CLI
bulk reindex command handles content that existed before the pipeline did, and needs
re-running whenever chunking or embedding settings change. Without this lesson’s hooks,
every other lesson in this course degrades quietly over time as content drifts out of
sync with what’s stored.
Resources & further reading
- save_post action reference, developer.wordpress.org
- before_delete_post action reference, developer.wordpress.org
- WP-CLI custom commands, make.wordpress.org