Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dist/fuse.basic.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,17 @@ const Config = Object.freeze({

//#endregion
//#region src/tools/fieldNorm.ts
function isWordSeparator(code) {
return code >= 9 && code <= 13 || code === 32 || code === 160;
}
function norm(weight = 1, mantissa = 3) {
const cache = /* @__PURE__ */ new Map();
const m = Math.pow(10, mantissa);
return {
get(value) {
let numTokens = 0;
let inWord = false;
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) !== 32) {
for (let i = 0; i < value.length; i++) if (!isWordSeparator(value.charCodeAt(i))) {
if (!inWord) {
numTokens++;
inWord = true;
Expand Down
2 changes: 1 addition & 1 deletion dist/fuse.basic.min.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/fuse.basic.min.mjs

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion dist/fuse.basic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,17 @@ const Config = Object.freeze({

//#endregion
//#region src/tools/fieldNorm.ts
function isWordSeparator(code) {
return code >= 9 && code <= 13 || code === 32 || code === 160;
}
function norm(weight = 1, mantissa = 3) {
const cache = /* @__PURE__ */ new Map();
const m = Math.pow(10, mantissa);
return {
get(value) {
let numTokens = 0;
let inWord = false;
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) !== 32) {
for (let i = 0; i < value.length; i++) if (!isWordSeparator(value.charCodeAt(i))) {
if (!inWord) {
numTokens++;
inWord = true;
Expand Down
5 changes: 4 additions & 1 deletion dist/fuse.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,17 @@ const Config = Object.freeze({

//#endregion
//#region src/tools/fieldNorm.ts
function isWordSeparator(code) {
return code >= 9 && code <= 13 || code === 32 || code === 160;
}
function norm(weight = 1, mantissa = 3) {
const cache = /* @__PURE__ */ new Map();
const m = Math.pow(10, mantissa);
return {
get(value) {
let numTokens = 0;
let inWord = false;
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) !== 32) {
for (let i = 0; i < value.length; i++) if (!isWordSeparator(value.charCodeAt(i))) {
if (!inWord) {
numTokens++;
inWord = true;
Expand Down
2 changes: 1 addition & 1 deletion dist/fuse.min.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/fuse.min.mjs

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion dist/fuse.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,17 @@ const Config = Object.freeze({

//#endregion
//#region src/tools/fieldNorm.ts
function isWordSeparator(code) {
return code >= 9 && code <= 13 || code === 32 || code === 160;
}
function norm(weight = 1, mantissa = 3) {
const cache = /* @__PURE__ */ new Map();
const m = Math.pow(10, mantissa);
return {
get(value) {
let numTokens = 0;
let inWord = false;
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) !== 32) {
for (let i = 0; i < value.length; i++) if (!isWordSeparator(value.charCodeAt(i))) {
if (!inWord) {
numTokens++;
inWord = true;
Expand Down
2 changes: 1 addition & 1 deletion dist/fuse.worker.min.mjs

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion dist/fuse.worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,17 @@ const Config = Object.freeze({

//#endregion
//#region src/tools/fieldNorm.ts
function isWordSeparator(code) {
return code >= 9 && code <= 13 || code === 32 || code === 160;
}
function norm(weight = 1, mantissa = 3) {
const cache = /* @__PURE__ */ new Map();
const m = Math.pow(10, mantissa);
return {
get(value) {
let numTokens = 0;
let inWord = false;
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) !== 32) {
for (let i = 0; i < value.length; i++) if (!isWordSeparator(value.charCodeAt(i))) {
if (!inWord) {
numTokens++;
inWord = true;
Expand Down
24 changes: 18 additions & 6 deletions src/tools/fieldNorm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { NormInterface } from '../types'

// Whitespace charCodes treated as word separators by the counter below:
// tab (9), newline (10), vertical tab (11), form feed (12), CR (13),
// space (32), non-breaking space (160).
function isWordSeparator(code: number): boolean {
return (code >= 9 && code <= 13) || code === 32 || code === 160
}

// Field-length norm: the shorter the field, the higher the weight.
// Set to 3 decimals to reduce index size.
export default function norm(
Expand All @@ -11,15 +18,20 @@ export default function norm(

return {
get(value: string): number {
// Count words by tallying word-starts (transitions from space/start to
// non-space). This avoids allocating a regex match array and correctly
// handles leading and trailing spaces, which the old transition-counter
// (starting at 1 and incrementing on every space boundary) would
// over-count by 1 for each stray boundary.
// Count words by tallying word-starts (transitions from separator/start
// to non-separator). This avoids allocating a regex match array and
// correctly handles leading and trailing separators, which the old
// transition-counter (starting at 1 and incrementing on every boundary)
// would over-count by 1 for each stray boundary.
//
// A separator is any whitespace character (space, tab, newline, CR,
// vertical tab, form feed, or non-breaking space) — not just plain
// ASCII space. Checking charCode 32 alone missed tabs and newlines, so
// e.g. a tab- or newline-joined field was scored as a single word.
let numTokens = 0
let inWord = false
for (let i = 0; i < value.length; i++) {
if (value.charCodeAt(i) !== 32) {
if (!isWordSeparator(value.charCodeAt(i))) {
if (!inWord) {
numTokens++
inWord = true
Expand Down
11 changes: 11 additions & 0 deletions test/internals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ describe('fieldNorm', () => {
// A string of only spaces should fall back to 1 token, same as empty
expect(n.get(' ')).toBe(n.get(''))
})

test('treats tabs and newlines as word separators, like plain spaces', () => {
const n = norm(1, 3)
// The word counter only checked charCode 32 (plain space), so a field
// using tabs or newlines between words was scored as a single word,
// giving it an artificially better (lower) norm than it should have.
expect(n.get('hello\tworld')).toBe(n.get('hello world'))
expect(n.get('hello\nworld')).toBe(n.get('hello world'))
expect(n.get('one\ttwo\tthree')).toBe(n.get('one two three'))
expect(n.get('one\ntwo\nthree')).toBe(n.get('one two three'))
})
})

describe('InvertedIndex', () => {
Expand Down