Skip to content

Add background synchronization for post embeddings - #1041

Open
theaminulai wants to merge 1 commit into
WordPress:developfrom
theaminulai:feature/embedding-background-sync
Open

theaminulai wants to merge 1 commit into
WordPress:developfrom
theaminulai:feature/embedding-background-sync

Conversation

@theaminulai

@theaminulai theaminulai commented Sep 17, 2026

Copy link
Copy Markdown
Member

What?

Part of #962

Adds an Embedding Sync experiment that keeps stored post embeddings up to date in the background as content changes, addressing the "Add background synchronization and large-scale processing" item tracked in #1036.

Why?

The embeddings storage/CRUD layer added in #976 gives the plugin a place to store vectors and a way to check whether a stored vector is stale (Embedding_Repository::get_content_hash()), but nothing in the plugin actually keeps that store synchronized with post content on its own — today the only way to (re-)embed a post is the wp ai embeddings generate WP-CLI command, run by hand, one post (or one string) at a time. A real site needs this to happen automatically, and needs it to not block the editor: embedding a post synchronously inside save_post would mean every save waits on an external provider request, and re-scanning a whole site's content on every change does not scale.

How?

  • includes/Embedding_Sync/Embedding_Sync_Manager.php (new module, parallel to includes/Logging/):
    • Hooks save_postmaybe_queue_post(), which records the post ID in a single wpai_embedding_sync_queue option (a set, not a copy of content) after excluding autosaves/revisions and filtering by configurable post type (default post, page) and status (default publish). Saving a post never waits on a provider request.
    • Registers a wpai_embedding_sync cron schedule (15 minutes by default, filterable, floored at 15 minutes per the platform's cron-interval guidance) and schedules wpai_embedding_sync_process_queue on it.
    • process_queue() resolves the provider and model from this experiment's Developer Options (get_feature_developer_model_config() — the same mechanism Abstract_Ability already uses for developer model overrides), takes a bounded batch off the queue (wpai_embedding_sync_batch_size, default 20), drops any post whose content hash already matches what's stored (no provider request for unchanged content), and embeds the rest in a single batched generate_embeddings() call, storing results via Embedding_Repository::save_many(). A provider error re-queues the whole batch for the next run rather than losing it or storing anything partial.
    • Hooks before_delete_posthandle_post_deleted(), which drops the post from the queue and deletes its stored embeddings across every provider/model via Embedding_Repository::delete_for_object().
    • generate_batch() is a small, separately-overridable method wrapping generate_embeddings(), so tests can substitute a fake generator instead of making live provider requests.
  • includes/Experiments/Embedding_Sync/Embedding_Sync.php: the thin Abstract_Feature glue class, following the exact shape of Experiments/AI_Request_Logging/AI_Request_Logging.php. Registered in Experiments::EXPERIMENT_CLASSES.
  • includes/Admin/Uninstall.php: clears the new cron hook on uninstall (the queue option itself is already covered by the existing wpai_ option-prefix cleanup).
  • docs/experiments/embedding-sync.md: new experiment doc following the established per-experiment template (Summary, Key Hooks, Architecture, Filter/Action Hooks, Testing, Notes).
  • Like every other embedding-consuming code path in this plugin (the CLI command included), this never chooses a provider or model on its own — vectors from different models aren't comparable, so background sync does nothing until one is explicitly configured.

Not in scope for this PR: chunking (the CLI's --chunk flag already covers ad hoc chunked embedding; background sync embeds each post as a single vector), and reacting to a post leaving the synced status set (e.g. unpublishing) — its existing embedding is left as-is rather than removed.

Testing Instructions

  1. composer install
  2. vendor/bin/phpunit --filter "Embedding_Sync_ManagerTest|UninstallTest"
  3. Manual: enable Experiments globally, toggle Embedding Sync, set a provider and model in its Developer Options, publish/edit a post, then trigger the cron event (wp cron event run wpai_embedding_sync_process_queue) and confirm the post now has a stored embedding (e.g. wp ai embeddings compare <id> <id> reports cosine similarity 1). Full steps in docs/experiments/embedding-sync.md#testing.

I wasn't able to run the full WP-integrated PHPUnit suite cleanly in my local environment: Embedding_SchemaTest and Embedding_RepositoryTest (pre-existing, unmodified by this PR) also fail locally with "The embeddings table could not be created" when run inside WP_UnitTestCase's default transactional test isolation, which rewrites CREATE TABLE/DROP TABLE into their TEMPORARY equivalents — dbDelta() doesn't create the table successfully against that rewrite on this local MySQL 8.0.31/Windows setup, though creating it directly (outside that rewrite) works fine. I confirmed this by reproducing the identical failure on an unmodified develop checkout, so it isn't something this PR introduces. My new Embedding_Sync_ManagerTest hits the same four table-dependent cases for the same reason; the other seven (queueing, filtering, deletion, and the no-provider-configured path, which don't need the table) pass. I also temporarily worked around the table-creation issue locally (removing the WP_UnitTestCase temp-table filters, matching the technique UninstallTest.php already uses for the same reason) to confirm all 11 tests pass end to end, including the float-vector round trip through Vector_Codec; that workaround is not part of this diff since Embedding_RepositoryTest.php doesn't use it either and I didn't want to touch an unrelated test file's isolation strategy. phpcs --standard=phpcs.xml.dist and phpstan analyse both pass clean on every changed/new source file.

Changelog Entry

Added - New Embedding Sync experiment that keeps stored post embeddings up to date in the background as content changes, in bounded batches on a recurring cron event.

Open WordPress Playground Preview

Adds an Embedding Sync experiment that keeps stored post embeddings up
to date as content changes, without embedding synchronously inside a
save request and without needing to re-scan a whole site to find what
changed.

save_post queues a post's ID in a single wpai_embedding_sync_queue
option; a recurring cron event (wpai_embedding_sync_process_queue,
every 15 minutes by default) drains a bounded batch of that queue,
skips any post whose content hash already matches what's stored
(Embedding_Repository::get_content_hash()), and embeds the rest in one
batched generate_embeddings() call per run. A provider failure
re-queues the whole batch for the next run instead of losing it.
before_delete_post removes a deleted post's queue entry and its stored
embeddings.

Like every other embedding-consuming code path in this plugin, this
requires an explicit provider and model — configured through this
experiment's Developer Options, the same mechanism abilities already
use for developer model overrides. Nothing is embedded until one is
configured.

Part of WordPress#962.
@theaminulai
theaminulai requested a review from a team September 17, 2026 14:59
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: theaminulai <theaminuldev@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.71014% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.23%. Comparing base (123ab42) to head (2e8e3dd).

Files with missing lines Patch % Lines
includes/Embedding_Sync/Embedding_Sync_Manager.php 82.11% 22 Missing ⚠️
...udes/Experiments/Embedding_Sync/Embedding_Sync.php 57.14% 6 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #1041      +/-   ##
=============================================
- Coverage      80.23%   80.23%   -0.01%     
- Complexity      2998     3042      +44     
=============================================
  Files            124      126       +2     
  Lines          11978    12116     +138     
=============================================
+ Hits            9611     9721     +110     
- Misses          2367     2395      +28     
Flag Coverage Δ
unit 80.23% <79.71%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jeffpaul jeffpaul moved this from Triage to In progress in WordPress AI Roadmap Sep 21, 2026
@jeffpaul jeffpaul added this to the 1.5.0 milestone Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants