time() + 2 * MINUTE_IN_SECONDS) { wp_schedule_single_event(time() + MINUTE_IN_SECONDS, Ingest::HOOK); } } /** * @param int|string $postId */ public static function onChange($postId): void { $postId = (int) $postId; if ($postId <= 0 || wp_is_post_revision($postId) || wp_is_post_autosave($postId) || !IndexTable::exists()) { return; } $post = get_post($postId); if (!$post instanceof \WP_Post || $post->post_status === 'auto-draft') { return; } $type = IndexTable::objectType($post); if (Selection::isEligible($post)) { IndexTable::markPending($type, $postId); self::nudge(); } elseif (IndexTable::get($type, $postId) !== null) { IndexTable::markDeleting($type, $postId); self::nudge(); } if ($type === 'post' && Settings::includeAttachments()) { // Images follow their post: unpublishing, protecting or leaving out the post // takes its images out of the store too (and publishing brings them back). foreach (get_children(['post_parent' => $postId, 'post_type' => 'attachment', 'fields' => 'ids', 'post_status' => 'any']) as $childId) { self::onChange((int) $childId); } } } /** * @param int|string $postId */ public static function onDelete($postId): void { $post = get_post((int) $postId); if (!$post instanceof \WP_Post || !IndexTable::exists()) { return; } IndexTable::markDeleting(IndexTable::objectType($post), (int) $postId); self::nudge(); } /** * @param int|string $userId */ public static function onAuthorChange($userId): void { if (in_array('author', Settings::fields(), true) && IndexTable::exists()) { self::requeueAuthor((int) $userId); } } /** * A deleted user's posts are deleted (their vectors go with them) or given to $reassign. * * @param int|string $userId * @param int|string|null $reassign */ public static function onAuthorDeleted($userId, $reassign = null): void { if ($reassign && in_array('author', Settings::fields(), true) && IndexTable::exists()) { self::requeueAuthor((int) $reassign); } } private static function requeueAuthor(int $authorId): void { foreach (Selection::kinds() as $kind) { if ($kind === 'attachment') { continue; } for ($page = 1; ($ids = Selection::ids($kind, $page, 500, $authorId)) !== []; $page++) { IndexTable::markPendingMany('post', $ids); } } self::nudge(); } /** * @param int|string $objectId * @param mixed $terms * @param mixed $ttIds * @param string $taxonomy */ public static function onTerms($objectId, $terms = null, $ttIds = null, $taxonomy = ''): void { if (Settings::taxMode() !== 'none' || in_array('terms', Settings::fields(), true)) { self::onChange($objectId); } } /** * @param mixed $metaId * @param int|string $objectId * @param string $metaKey */ public static function onMeta($metaId, $objectId, $metaKey = ''): void { $watched = [Selection::EXCLUDE_META, '_wp_attachment_image_alt']; if (in_array('custom_fields', Settings::fields(), true)) { $watched = array_merge($watched, Settings::metaKeys()); } if (in_array('meta_description', Settings::fields(), true)) { $watched[] = Backfill::metaDescriptionKey(); } // Never this plugin's own meta (see Settings::sanitize): saving a related list is not an edit. if (strpos((string) $metaKey, '_aipcf_related') !== 0 && in_array((string) $metaKey, $watched, true)) { self::onChange($objectId); } } /** * @param mixed $old * @param mixed $new */ public static function onSettingsChange($old, $new): void { // A key missing from either side is its default, not "changed": the stored option may // predate a setting, or have been written by code without every key. $old = (is_array($old) ? $old : []) + Settings::defaults(); $new = (is_array($new) ? $new : []) + Settings::defaults(); $wasOn = !empty($old['embeddings_enabled']); $isOn = !empty($new['embeddings_enabled']); // Another store or index with the same model: copy the vectors there (no neurons). if (Mover::afterSave()) { self::nudge(); } if (!$isOn) { if ($wasOn) { wp_clear_scheduled_hook(Ingest::HOOK); } return; } $changed = static function (array $keys) use ($old, $new): bool { foreach ($keys as $k) { if (($old[$k] ?? null) != ($new[$k] ?? null)) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- arrays compared by value return true; } } return false; }; if (!$wasOn) { IndexTable::install(); State::resume(); Ingest::startReconcile(); self::hookContent(); self::nudge(); return; } if ($changed(self::CONTENT_KEYS)) { IndexTable::requeueIndexed(); } if ($changed(self::LENGTH_KEYS)) { IndexTable::requeueIndexed(); IndexTable::requeueSkipped(); \WordPress\CloudflareWorkersAiProvider\Features\Related::flushCache(); self::nudge(); } if ($changed(self::SELECTION_KEYS) || $changed(self::CONTENT_KEYS)) { Ingest::startReconcile(); self::nudge(); } } /** * @param string $option * @param mixed $value */ public static function onSettingsAdded($option, $value): void { self::onSettingsChange([], $value); } }