Runs and Progress
Detection does not run while you edit. A run is started explicitly, processes a scope in the background, and writes its findings into the review screens.
Starting one
| From | How |
|---|---|
| The configuration screen | Save & Detect (incremental) or Save & Full Detection. |
| The review screen | Rerun duplicate analysis (incremental), or Recalculate group for a single reference. |
| The CLI | bin/console bpt:duplicates:analyze |
| A cron | The same command on a schedule. |
A run never executes inline: starting one queues a Generic Execution Engine job and returns
immediately. The work happens on the pimcore_backend_power_tools_duplicates_queue transport, so a
worker must be consuming it. Only one run per scope is allowed at a time; runs on disjoint scopes may
overlap.
bin/console bpt:duplicates:analyze [--scope=objects|assets] [--full]
--scoperestricts the run to one configured scope. Omitted, every enabled scope runs.--fullre-analyses everything instead of only what changed.
Incremental vs full
These are not simply "less work vs more work" — the choice changes four mechanics.
1. Selection. Incremental sweeps only elements changed since the scope's last completed sweep, plus the references of groups something else invalidated. The very first run for a scope has no watermark to select from, so it falls back to a full sweep automatically.
2. Probe depth. Incremental probes up to max probe chunks (3) of an element's chunk vectors; full uses the lower max probe chunks (full run) (1), because a full pass pays that cost once per element across the whole scope. The cap follows the actual selection, not the requested mode — an incremental run that fell back to full also uses the full cap rather than tripling its probes.
3. Retraction. Both modes can remove findings, but only within what they actually looked at. An incremental sweep withdraws the groups of references it just re-evaluated that no longer produce a cluster — those genuinely have no duplicates left — and the reconcile step, which is queued in both modes, always performs its out-of-scope cleanup. What is reserved for a completed full run is the run-wide withdrawal of every group the run did not reproduce: after an incremental sweep "not visited" cannot be told apart from "no longer a duplicate", so applying that rule would delete live findings from the part of the scope the sweep never reached.
4. Floor safeguard. Scores below the candidate floor were never stored, so after lowering a floor, incremental runs are blocked until a full run rewrites the dataset. Raising a floor needs no full run. See Stored vs shown.
Rule of thumb: Detect is the everyday, inexpensive choice, and it only ever retracts findings about elements it re-examined. Full is for after a configuration change that alters what counts as a duplicate, or to clear out findings nothing reproduces any more — which is why its confirmation is styled as destructive.
Following a run
The configuration screen shows a banner while a run is active and the last run's result when idle. For monitoring from outside Studio, the status endpoint behind that banner is:
GET /configurations/{configurationId}/analysis-status
PIMCORE_ADMIN-gated, and deliberately separate from the configuration GET so that polling it never
refetches the configuration while someone is mid-edit in the form.
Why the progress number is trustworthy
Both execution budgets stop a sweep at a page boundary, never mid-page — the runtime budget on
either tier, the element budget on the fuzzy tier only — and the tier that is running records that
boundary in the run's single, tier-tagged resume cursor — for the exact tier the aggregation's
continuation token, for the fuzzy tier the last element id visited, overwriting the exact tier's. scanned therefore always reflects whole pages actually
processed, never a partially applied one.
The cursor is stored per job run. It lets a retried step carry on where it left off inside that run; it is not carried into the next invocation, which starts its sweep from the beginning.
groupsFound is per run, not the scope's whole inventory. It counts only the groups this run
wrote: on a full run it grows to match the review list's own count, on an incremental run only to the
groups that run confirmed.
Cancelling
DELETE /analyze
Use it to stop a run started by mistake, or to recover one flagged staleWorkerSuspected.
It cancels the running job run, and that is all — there is no queue purge. Every duplicate message
handler re-checks the job run's live state at its own entry, so once the state is no longer
RUNNING, anything still queued self-drains as a no-op the moment a worker picks it up.
Cancel stops the run within one page, not instantly. A worker mid-page finishes that page — whose progress is already checkpointed — before the next step message hits the cancelled state and drains.
It is idempotent. Calling it with nothing running is a no-op that still returns 200; there is no
error case for "nothing to cancel".
Cron runs
A run started by a scheduled bpt:duplicates:analyze has no logged-in user to notify, so it never
pushes a Mercure event — the job-run subscriber skips any run owned by the system owner. This does
not affect the status endpoint: polling it reports a cron run's progress and completion exactly like a
Studio-triggered one, because it reads the job run and the run record directly rather than depending
on the notification.