Skip to content

perf(entity_quality): 2x faster validate_entity_slug for repair plans - #274

Merged
jack-arturo merged 1 commit into
verygoodplugins:developfrom
dexhunter:perf/entity-tag-validation
Nov 2, 2026
Merged

jack-arturo merged 1 commit into
verygoodplugins:developfrom
dexhunter:perf/entity-tag-validation

Conversation

@dexhunter

@dexhunter dexhunter commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

  • This PR makes entity tag validation in automem/utils/entity_quality.py faster (validate_entity_slug and its private helpers).
  • It splits the slug once, passes the parts to each helper, and skips regex calls that cannot match.
  • Benchmark: one build_repair_plan call on 708 rows takes 72.6 ms before and 35.1 ms after (median), 51.6% faster.
  • Tests: pytest tests gives 953 passed, 12 skipped, the same as on develop.

What & Why

build_repair_plan calls validate_entity_tag for every entity tag. Before this change, each check split the same slug again. The action-status check also built a new set of word forms for every root. The people check ran a regex on each part and seven membership tests per part.

Now validate_entity_slug splits the canonical slug once and passes the parts list to each helper. The action-status check removes each known suffix and looks up the root directly. Clean stored slugs skip the slugify regex. Cheap length or first-character checks run before regex calls that cannot match. The vocabulary check uses isdisjoint on each vocabulary set.

The output stays identical. Each shortcut only skips work when the old code would give the same answer. The same rejection reasons are returned in the same order, and _accepted() / _reject() still build every result.

Changes

  • validate_entity_slug: split the canonical slug once and pass the parts to the helpers.
  • _looks_like_action_status_slug: undo each inflection (-s, -ed, -d, -ing, -ied) and look up the root once.
  • _looks_like_unicode_escape_slug and the people name check: use one regex over the whole slug.
  • _looks_like_duration_or_count_slug, _looks_like_generated_fragment_slug, _has_internal_camelcase: return early when a pattern cannot match.
  • People vocabulary check: use isdisjoint for each vocabulary set instead of seven in tests per part.
  • _looks_tool_or_org_like: pass the whole suffix tuple to one endswith call.

No public API, configuration or docs change. Only private helper signatures change, and only validate_entity_slug calls them.

Results

Median of 5 alternating runs on an isolated CPU core (Python 3.12):

Situation Before After Change
build_repair_plan, 708 benchmark rows 72.57 ms 35.13 ms 51.6% faster
build_repair_plan, 8 rows from the existing test 0.25 ms 0.12 ms 51.2% faster
5000 first-last entity:people tags 76.11 ms 32.31 ms 57.5% faster
5000 single-name entity:people tags 83.89 ms 28.97 ms 65.5% faster
5000 junk entity:people tags (rejected) 67.26 ms 27.29 ms 59.4% faster
5000 tags of other categories 50.45 ms 23.23 ms 54.0% faster
5000 non-entity tags (rejected before these checks) 10.70 ms 10.64 ms no change

Why this is safe and easy to maintain

The change is small and stays in one file. It keeps the existing regular expressions and the _accepted() / _reject() builders. Behaviour and output are unchanged: 200,000 random slugs, categories and tags give identical results before and after. tests/test_entity_quality.py, tests/test_repair_entity_tags.py and tests/test_entity_identities.py cover these paths.

Validation

  • pytest tests/test_entity_quality.py tests/test_repair_entity_tags.py tests/test_entity_identities.py: 165 passed.
  • pytest tests: 953 passed, 12 skipped (same as develop).
  • black --check, isort --check and the CI flake8 selection pass on the changed file.
  • Benchmark: build_repair_plan(rows, mode="canonicalize-safe") on 708 rows of mixed entity:people tags (the four rows of test_plan_suppresses_single_name_people_unsafe_structural_targets plus 700 generated rows). Before: 72.6 ms per call. After: 35.1 ms per call (224 ms -> 111 ms for 3 calls). Three repeated measurements gave 0.496, 0.502 and 0.492 of the old time.
  • Short reproduction (run from the repository root):
import importlib.util, timeit
spec = importlib.util.spec_from_file_location("r", "scripts/lab/repair_entity_tags.py")
r = importlib.util.module_from_spec(spec); spec.loader.exec_module(r)
rows = [{"id": str(i), "tags": [f"entity:people:{n}"], "tag_prefixes": [], "metadata": {}, "content": ""}
        for i, n in enumerate(["alex-beck", "maria", "docker", "phase-five", "updated", "x2bc91f"] * 200)]
print(min(timeit.repeat(lambda: r.build_repair_plan(rows, mode="canonicalize-safe"), number=5, repeat=5)))

No linked issue.

AI assistance

This PR was prepared with AI assistance from Aiden. Candidates were tracked with Weco Observe.

Weco share link: https://dashboard.weco.ai/share/yYsMiWbvWBbxNasQaArg0DiL0pH8MxX2

Split the canonical slug once in validate_entity_slug and pass the
parts to the private helpers instead of re-splitting it in each one.
Look up action-status roots by removing known suffixes, skip the
slugify regex for clean stored slugs, check people vocabulary with
isdisjoint, and return early before regex calls that cannot match.

Output is unchanged. build_repair_plan(mode="canonicalize-safe") on
708 mixed entity:people rows drops from 72.6 ms to 35.1 ms per call.
pytest tests: 953 passed, 12 skipped.

Co-Authored-By: Aiden <aiden@weco.ai>
@jack-arturo

Copy link
Copy Markdown
Member

Thanks @dexhunter :)

@jack-arturo
jack-arturo merged commit 90f41af into verygoodplugins:develop Nov 2, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants