Skip to content

fix(fieldNorm): count word-starts instead of space transitions to avoid over-counting leading/trailing spaces#830

Merged
krisk merged 1 commit into
krisk:mainfrom
JSap0914:fix/fieldnorm-leading-trailing-space
Jun 22, 2026
Merged

fix(fieldNorm): count word-starts instead of space transitions to avoid over-counting leading/trailing spaces#830
krisk merged 1 commit into
krisk:mainfrom
JSap0914:fix/fieldnorm-leading-trailing-space

Conversation

@JSap0914

Copy link
Copy Markdown
Contributor

Summary

fieldNorm's word-counting loop started numTokens at 1 and incremented on every space-to-non-space transition. This means a leading space was treated as a transition from an implicit word, over-counting the number of words:

Input Old numTokens New numTokens
" hello" 2 1 ✅
"hello " 2 1 ✅
" hello world " 3 2 ✅
" " 2 1 ✅
"hello world" 2 ✅ 2 ✅
"" 1 ✅ 1 ✅

The inflated count produces a lower norm value (e.g. 0.707 instead of 1.0 for a single-word field), which in turn raises the final multiplicative score — penalising any document whose stored field value happens to contain leading or trailing whitespace.

Fix

Replace the space-transition counter with a word-start counter: increment only when encountering a non-space character that follows a space or the start of the string. This is equivalent to the old logic for well-trimmed strings (same result for "hello world", "hello world", etc.) and correctly ignores stray boundary whitespace. Strings with zero real words (empty or all-whitespace) are clamped to numTokens = 1 to preserve the existing fallback.

Tests

A new test — "does not count leading or trailing spaces as word boundaries" — was failing before the fix and passes after. All 371 existing tests continue to pass.

AI-assist: this fix was generated with AI assistance.

Leading and trailing spaces were silently inflating numTokens by one
boundary each, producing a lower norm value (0.707 instead of 1.0 for a
single-word field) and therefore a higher (worse) final search score for
any document whose field value contained stray whitespace.

Root cause: the original loop started numTokens at 1 and incremented on
every space→non-space transition. A leading space was treated as a
transition from an implicit word, so ' hello' gave numTokens=2 instead
of 1. Similarly, 'hello ' transitioned word→space near the end, giving
numTokens=2 even though only one word is present.

Fix: rewrite the counter to tally word-starts (non-space character that
follows a space or the string start). This is equivalent for typical
strings — 'hello world' still yields 2 — but correctly ignores leading
and trailing whitespace. Empty and all-whitespace strings are clamped to
1 to preserve the existing fallback behaviour.

Test: add a failing test that demonstrates the bug for ' hello',
'hello ', ' hello world ', and ' ', then verify it passes after fix.
Copilot AI review requested due to automatic review settings June 22, 2026 00:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@krisk krisk left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nice catch, this one's real. " hello" was counting as 2 tokens since values reach norm.get() untrimmed and only all-whitespace gets filtered. Fix looks good and trimmed strings come out the same as before.

Tiny thing: the table says old count for " hello world " is 3, it's actually 4.

Approving, thanks!

@krisk
krisk merged commit 2946f97 into krisk:main Jun 22, 2026
krisk pushed a commit to chatman-media/Fuse that referenced this pull request Jul 3, 2026
The word counter added in krisk#830 only checked for charCode 32 (plain
ASCII space), so any field that separated words with a tab or
newline instead was scored as a single long word. That gives it an
artificially better field-length norm than a same-content field
using regular spaces - e.g. 'foo\tbar\tbaz' scores 20x better than
'foo bar baz' even though they're the same three words.

Added a small isWordSeparator helper covering the usual whitespace
range (tab/LF/VT/FF/CR, space, NBSP) instead of the single charCode
check, plus a test that fails without the fix.
krisk pushed a commit that referenced this pull request Jul 3, 2026
The word counter added in #830 only checked for charCode 32 (plain
ASCII space), so any field that separated words with a tab or
newline instead was scored as a single long word. That gives it an
artificially better field-length norm than a same-content field
using regular spaces - e.g. 'foo\tbar\tbaz' scores 20x better than
'foo bar baz' even though they're the same three words.

Added a small isWordSeparator helper covering the usual whitespace
range (tab/LF/VT/FF/CR, space, NBSP) instead of the single charCode
check, plus a test that fails without the fix.
OIRNOIR pushed a commit to OIRNOIR/YouTube-Helper-Server that referenced this pull request Jul 21, 2026
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [fuse.js](http://fusejs.io) ([source](https://github.com/krisk/Fuse)) | imports | minor | [`7.4.2` -> `7.5.0`](https://renovatebot.com/diffs/npm/fuse.js/7.4.2/7.5.0) |

---

### Release Notes

<details>
<summary>krisk/Fuse (fuse.js)</summary>

### [`v7.5.0`](https://github.com/krisk/Fuse/blob/HEAD/CHANGELOG.md#750-2026-07-13)

[Compare Source](krisk/Fuse@v7.4.2...v7.5.0)

##### ⚠️ Behavior changes

Every change in this release is a bug fix, but each one corrects a **scoring or ranking** bug. Scores and result ordering will shift for some queries. That is why this ships as a minor rather than a patch: the public API is unchanged and upgrading is a drop-in, but the results you get back can differ, and that should not arrive silently in a patch bump.

If you assert on exact `score` values or on a specific result order, expect those assertions to need updating. Re-baseline them against 7.5.0 rather than pinning to 7.4.x, since the 7.4.x behavior was wrong in the cases below.

- **Field-length normalisation now counts words correctly.** Tabs and newlines were not treated as word separators, so a multi-line or tab-delimited field was scored as though it were one long word, making it look far shorter than it is. Fields containing `\t`, `\n`, or `\r` now score differently ([#&#8203;830](krisk/Fuse#830)).
- **Key weights are now normalised in object and keyless-logical search.** Weights that did not sum to `1` were applied unnormalised, skewing the relative influence of each key. If your `keys` weights do not already sum to `1`, your relative ranking changes ([#&#8203;833](krisk/Fuse#833)).
- **`limit` now returns the correct top-N when scores tie.** A tie at the cutoff boundary could evict a result that should have been kept, so `limit` could return the *wrong* items, not merely the right items in a different order ([#&#8203;835](krisk/Fuse#835)).
- **Bitap respects `minMatchCharLength` in the exact-match shortcut.** Matches shorter than `minMatchCharLength` were still reported via the exact-match fast path, so the `matches` array could contain entries it was configured to exclude ([#&#8203;831](krisk/Fuse#831)).

##### Bug Fixes

- **bitap:** respect minMatchCharLength in exact-match shortcut ([dbb98b6](krisk/Fuse@dbb98b6)), closes [#&#8203;831](krisk/Fuse#831)
- **fieldNorm:** count tabs and newlines as word separators ([6fe85b0](krisk/Fuse@6fe85b0)), closes [#&#8203;830](krisk/Fuse#830)
- **fieldNorm:** count word-starts instead of space transitions ([2946f97](krisk/Fuse@2946f97))
- **scoring:** normalise key weights in object and keyless-logical search ([e164b61](krisk/Fuse@e164b61)), closes [#&#8203;833](krisk/Fuse#833)
- **search:** keep the correct top-N under limit when scores tie ([437f8f3](krisk/Fuse@437f8f3)), closes [#&#8203;835](krisk/Fuse#835), thanks [@&#8203;spokodev](https://github.com/spokodev) for the report and the fix

##### [7.4.2](krisk/Fuse@v7.4.1...v7.4.2) (2026-06-05)

##### Bug Fixes

- **types:** emit CommonJS declarations (.d.cts) for node16/nodenext ([#&#8203;780](krisk/Fuse#780)) ([33f5d29](krisk/Fuse@33f5d29))

##### [7.4.1](krisk/Fuse@v7.4.0...v7.4.1) (2026-06-02)

##### Bug Fixes

- **types:** add TypeScript declarations for fuse.js/worker-script ([6ef6c33](krisk/Fuse@6ef6c33)), closes [#&#8203;828](krisk/Fuse#828)
- **types:** ship TypeScript declarations for fuse.js/worker ([572ad1e](krisk/Fuse@572ad1e)), closes [#&#8203;828](krisk/Fuse#828)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI2MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://git.oirnoir.dev/OIRNOIR/YouTube-Helper-Server/pulls/27
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.

3 participants