Skip to content

fix(autotag): "featuring" pattern in string_dist matches mid-word "ft"#6843

Open
chuenchen309 wants to merge 3 commits into
beetbox:masterfrom
chuenchen309:fix/distance-featuring-pattern-mid-word-match
Open

fix(autotag): "featuring" pattern in string_dist matches mid-word "ft"#6843
chuenchen309 wants to merge 3 commits into
beetbox:masterfrom
chuenchen309:fix/distance-featuring-pattern-mid-word-match

Conversation

@chuenchen309

Copy link
Copy Markdown

Bug

SD_PATTERNS in beets/autotag/distance.py has this entry:

(r"[\[\(]?(featuring|feat|ft)[\. :].+", 0.1),

The ft alternative has no word boundary before it, so it matches ft embedded inside any ordinary word followed by a space/period/colon — draft, left, gift, craft, soft, swift, etc. — and treats everything after the match as a low-weight "featuring artist" suffix (weight 0.1), effectively deleting it from the comparison.

from beets.autotag.distance import string_dist
string_dist("Draft Beer", "Draft Whiskey")   # 0.05  (near-identical!)
string_dist("Left Field", "Left Symphony")   # 0.067
string_dist("Gift Ideas", "Gift Cards")      # 0.044

Confirmed through the actual matching pipeline (track_distance) too: "Draft Beer" vs. "Draft Whiskey" scores almost as close as an exact match, while a genuinely unrelated title correctly scores much higher. string_dist/add_string backs nearly every fuzzy field comparison used during beet import (title, artist, album, label, catalognum, ...), so this silently degrades match quality for any title/artist containing one of these words.

Fix

Add a word boundary before the alternation:

(r"[\[\(]?\b(featuring|feat|ft)[\. :].+", 0.1),

Verified this doesn't regress the intended "real" cases ("Song feat. Artist", "[feat: Artist]", "(ft. Artist)" — all still match, since they're preceded by whitespace/bracket/string-start, which are real boundaries) while excluding all the false-positive mid-word cases above.

Testing

Added test_featuring_pattern_does_not_match_mid_word (parametrized over draft/left/gift/craft) to test/autotag/test_distance.py, asserting string_dist(...) > 0.3 for these pairs (previously ~0.04-0.07). Verified red on master (git stash the source fix — all 4 cases fail with the near-zero distances shown above), green after. Full test/autotag/test_distance.py (58 passed) and the broader test/autotag/ + test/util/ + test/autotag/test_match.py + test/test_importer.py suites (352 + 142 passed, no regressions) all pass. ruff check/ruff format --check/mypy clean.

Added a changelog entry per CONTRIBUTING.rst's reminder.

Duplicate-work check

Searched open PRs for distance.py/"featuring" — found #6681 (Source class refactor) also touches distance.py, but its diff doesn't touch SD_PATTERNS or this area at all. No overlap.


This PR was drafted with AI assistance (Claude); I independently reproduced the bug against the real, unmodified module (including through the full track_distance matching pipeline) and verified the fix. I reviewed the change and take responsibility for it.

@chuenchen309
chuenchen309 requested a review from a team as a code owner July 16, 2026 00:13
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.59%. Comparing base (2cf20aa) to head (f0bb497).
⚠️ Report is 29 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6843      +/-   ##
==========================================
- Coverage   75.60%   75.59%   -0.02%     
==========================================
  Files         163      163              
  Lines       21299    21299              
  Branches     3358     3358              
==========================================
- Hits        16103    16100       -3     
- Misses       4406     4408       +2     
- Partials      790      791       +1     
Files with missing lines Coverage Δ
beets/autotag/distance.py 83.52% <ø> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread beets/autotag/distance.py Outdated
@chuenchen309

Copy link
Copy Markdown
Author

Applied — added the trailing \b so the boundary is symmetric on both sides. Full test/autotag/test_distance.py still passes (58/58), including the mid-word regression cases and the existing featuring-suffix matches. Thanks for the review!

@snejus snejus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@snejus
snejus enabled auto-merge July 16, 2026 12:13
chuenchen309 and others added 3 commits July 16, 2026 13:14
SD_PATTERNS' featuring/feat/ft regex has no word boundary before the
alternation, so it matches "ft" embedded in ordinary words like
"draft", "left", "gift", "craft" and treats everything after it as a
low-weight suffix. "Draft Beer" vs. "Draft Whiskey" scores 0.05
(near-identical) instead of correctly registering as very different.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply reviewer suggestion (@snejus): add a trailing \b after the
(featuring|feat|ft) alternation so the boundary is explicit on both
sides, matching the leading \b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@snejus
snejus force-pushed the fix/distance-featuring-pattern-mid-word-match branch from 83e39c0 to f0bb497 Compare July 16, 2026 12:14
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