Skip to content

Commit e357229

Browse files
committed
fix(logical): scores in logical query operators are ignored
Closes #449
1 parent a2de306 commit e357229

File tree

8 files changed

+130
-84
lines changed

8 files changed

+130
-84
lines changed

dist/fuse.d.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ declare class FuseIndex<T> {
112112
declare namespace Fuse {
113113
type FuseGetFunction<T> = (
114114
obj: T,
115-
path: string
115+
path: string | string[]
116116
) => ReadonlyArray<string> | string
117117

118118
export type FuseIndexOptions<T> = {
@@ -231,11 +231,11 @@ declare namespace Fuse {
231231
// weight: 0.7
232232
// }
233233
export type FuseOptionKeyObject = {
234-
name: string | [string]
234+
name: string | string[]
235235
weight: number
236236
}
237237

238-
export type FuseOptionKey = FuseOptionKeyObject | string | [string]
238+
export type FuseOptionKey = FuseOptionKeyObject | string | string[]
239239

240240
export interface IFuseOptions<T> {
241241
isCaseSensitive?: boolean

dist/fuse.js

+59-36
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,10 @@
379379
deepGet(value, path, index + 1);
380380
}
381381
}
382-
};
382+
}; // Backwards compatibility (since path used to be a string)
383+
383384

384-
deepGet(obj, path, 0);
385+
deepGet(obj, isString(path) ? path.split('.') : path, 0);
385386
return arr ? list : list[0];
386387
}
387388

@@ -1920,58 +1921,80 @@
19201921
var _this = this;
19211922

19221923
var expression = parse(query, this.options);
1923-
var records = this._myIndex.records;
1924-
var resultMap = {};
1925-
var results = [];
19261924

1927-
var evaluateExpression = function evaluateExpression(node, item, idx) {
1928-
if (node.children) {
1929-
var operator = node.operator;
1930-
var res = [];
1925+
var evaluate = function evaluate(node, item, idx) {
1926+
if (!node.children) {
1927+
var keyId = node.keyId,
1928+
searcher = node.searcher;
19311929

1932-
for (var k = 0; k < node.children.length; k += 1) {
1933-
var child = node.children[k];
1934-
var matches = evaluateExpression(child, item, idx);
1930+
var matches = _this._findMatches({
1931+
key: _this._keyStore.get(keyId),
1932+
value: _this._myIndex.getValueForItemAtKeyId(item, keyId),
1933+
searcher: searcher
1934+
});
19351935

1936-
if (matches && matches.length) {
1937-
res.push({
1938-
idx: idx,
1939-
item: item,
1940-
matches: matches
1941-
});
1936+
if (matches && matches.length) {
1937+
return [{
1938+
idx: idx,
1939+
item: item,
1940+
matches: matches
1941+
}];
1942+
}
1943+
1944+
return [];
1945+
}
1946+
/*eslint indent: [2, 2, {"SwitchCase": 1}]*/
19421947

1943-
if (operator === LogicalOperator.OR) {
1944-
// Short-circuit
1945-
break;
1948+
1949+
switch (node.operator) {
1950+
case LogicalOperator.AND:
1951+
{
1952+
var res = [];
1953+
1954+
for (var i = 0, len = node.children.length; i < len; i += 1) {
1955+
var child = node.children[i];
1956+
var result = evaluate(child, item, idx);
1957+
1958+
if (result.length) {
1959+
res.push.apply(res, _toConsumableArray(result));
1960+
} else {
1961+
return [];
1962+
}
19461963
}
1947-
} else if (operator === LogicalOperator.AND) {
1948-
res.length = 0; // Short-circuit
19491964

1950-
break;
1965+
return res;
19511966
}
1952-
}
19531967

1954-
return res;
1955-
} else {
1956-
var keyId = node.keyId,
1957-
searcher = node.searcher;
1968+
case LogicalOperator.OR:
1969+
{
1970+
var _res = [];
19581971

1959-
var value = _this._myIndex.getValueForItemAtKeyId(item, keyId);
1972+
for (var _i = 0, _len = node.children.length; _i < _len; _i += 1) {
1973+
var _child = node.children[_i];
19601974

1961-
return _this._findMatches({
1962-
key: _this._keyStore.get(keyId),
1963-
value: value,
1964-
searcher: searcher
1965-
});
1975+
var _result = evaluate(_child, item, idx);
1976+
1977+
if (_result.length) {
1978+
_res.push.apply(_res, _toConsumableArray(_result));
1979+
1980+
break;
1981+
}
1982+
}
1983+
1984+
return _res;
1985+
}
19661986
}
19671987
};
19681988

1989+
var records = this._myIndex.records;
1990+
var resultMap = {};
1991+
var results = [];
19691992
records.forEach(function (_ref3) {
19701993
var item = _ref3.$,
19711994
idx = _ref3.i;
19721995

19731996
if (isDefined(item)) {
1974-
var expResults = evaluateExpression(expression, item, idx);
1997+
var expResults = evaluate(expression, item, idx);
19751998

19761999
if (expResults.length) {
19772000
// Dedupe when adding

docs/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Of course! Fuse.js has no DOM dependencies.
7777

7878
### Who's using Fuse.js these days?
7979

80-
Plenty of people. It's hard to say an exact number, since it's free. But a good indication is the number of [dependents](https://www.npmjs.com/package/fuse.js?activeTab=dependents) on NPM and [stargazers](https://github.com/krisk/Fuse/stargazers) on Github.
80+
Plenty of people. It's hard to say an exact number, since it's free. But a good indication is the number of [dependents](https://www.npmjs.com/package/fuse.js?activeTab=dependents) on NPM, and the [dependency graph](https://github.com/krisk/Fuse/network/dependents) and [stargazers](https://github.com/krisk/Fuse/stargazers) on Github.
8181

8282
Read the [stories](/stories.html) to learn how various products are using Fuse.js to tackle a growing number of use cases.
8383

docs/api/options.md

+9-6
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,16 @@ When `true`, it enables the use of unix-like search commands. See [example](/exa
106106
### `getFn`
107107

108108
- Type: `Function`
109-
- Default: `(obj: T, path: string) => string[] | string`
109+
- Default: `(obj: T, path: string | string[]) => string | string[]`
110110

111-
The function to use to retrieve an object's value at the provided path (i.e, key). The default will search nested paths.
111+
The function to use to retrieve an object's value at the provided path. The default will also search nested paths.
112112

113-
::: danger
114-
There aren't many cases where you'd want to use your own `getFn`.
115-
:::
113+
### `sortFn`
114+
115+
- Type: `Function`
116+
- Default: `(a, b) => number`
117+
118+
The function to use to sort all the results. The default will sort by ascending relevance score, ascending index.
116119

117120
### `ignoreFieldNorm`
118121

@@ -122,5 +125,5 @@ There aren't many cases where you'd want to use your own `getFn`.
122125
When `true`, the calculation for the relevance score (used for sorting) will ignore the [field-length norm](/concepts/scoring-theory.html#fuzziness-score).
123126

124127
:::tip
125-
The only time it might make sense `ignoreFieldNorm` to `false` is when it does not matter how many terms there are, but only that the query term exists.
128+
The only time it makes sense to set `ignoreFieldNorm` to `true` is when it does not matter how many terms there are, but only that the query term exists.
126129
:::

src/core/index.js

+46-33
Original file line numberDiff line numberDiff line change
@@ -134,53 +134,66 @@ export default class Fuse {
134134

135135
const expression = parse(query, this.options)
136136

137-
const records = this._myIndex.records
138-
const resultMap = {}
139-
const results = []
140-
141-
const evaluateExpression = (node, item, idx) => {
142-
if (node.children) {
143-
const operator = node.operator
144-
let res = []
137+
const evaluate = (node, item, idx) => {
138+
if (!node.children) {
139+
const { keyId, searcher } = node
145140

146-
for (let k = 0; k < node.children.length; k += 1) {
147-
let child = node.children[k]
148-
let matches = evaluateExpression(child, item, idx)
141+
const matches = this._findMatches({
142+
key: this._keyStore.get(keyId),
143+
value: this._myIndex.getValueForItemAtKeyId(item, keyId),
144+
searcher
145+
})
149146

150-
if (matches && matches.length) {
151-
res.push({
147+
if (matches && matches.length) {
148+
return [
149+
{
152150
idx,
153151
item,
154152
matches
155-
})
156-
if (operator === LogicalOperator.OR) {
157-
// Short-circuit
158-
break
159153
}
160-
} else if (operator === LogicalOperator.AND) {
161-
res.length = 0
162-
// Short-circuit
163-
break
164-
}
154+
]
165155
}
166156

167-
return res
168-
} else {
169-
const { keyId, searcher } = node
170-
171-
const value = this._myIndex.getValueForItemAtKeyId(item, keyId)
157+
return []
158+
}
172159

173-
return this._findMatches({
174-
key: this._keyStore.get(keyId),
175-
value,
176-
searcher
177-
})
160+
/*eslint indent: [2, 2, {"SwitchCase": 1}]*/
161+
switch (node.operator) {
162+
case LogicalOperator.AND: {
163+
const res = []
164+
for (let i = 0, len = node.children.length; i < len; i += 1) {
165+
const child = node.children[i]
166+
const result = evaluate(child, item, idx)
167+
if (result.length) {
168+
res.push(...result)
169+
} else {
170+
return []
171+
}
172+
}
173+
return res
174+
}
175+
case LogicalOperator.OR: {
176+
const res = []
177+
for (let i = 0, len = node.children.length; i < len; i += 1) {
178+
const child = node.children[i]
179+
const result = evaluate(child, item, idx)
180+
if (result.length) {
181+
res.push(...result)
182+
break
183+
}
184+
}
185+
return res
186+
}
178187
}
179188
}
180189

190+
const records = this._myIndex.records
191+
const resultMap = {}
192+
const results = []
193+
181194
records.forEach(({ $: item, i: idx }) => {
182195
if (isDefined(item)) {
183-
let expResults = evaluateExpression(expression, item, idx)
196+
let expResults = evaluate(expression, item, idx)
184197

185198
if (expResults.length) {
186199
// Dedupe when adding

src/helpers/get.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export default function get(obj, path) {
3232
}
3333
}
3434

35-
deepGet(obj, path, 0)
35+
// Backwards compatibility (since path used to be a string)
36+
deepGet(obj, isString(path) ? path.split('.') : path, 0)
3637

3738
return arr ? list : list[0]
3839
}

src/index.d.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ declare class FuseIndex<T> {
109109
declare namespace Fuse {
110110
type FuseGetFunction<T> = (
111111
obj: T,
112-
path: string
112+
path: string | string[]
113113
) => ReadonlyArray<string> | string
114114

115115
export type FuseIndexOptions<T> = {
@@ -228,11 +228,11 @@ declare namespace Fuse {
228228
// weight: 0.7
229229
// }
230230
export type FuseOptionKeyObject = {
231-
name: string | [string]
231+
name: string | string[]
232232
weight: number
233233
}
234234

235-
export type FuseOptionKey = FuseOptionKeyObject | string | [string]
235+
export type FuseOptionKey = FuseOptionKeyObject | string | string[]
236236

237237
export interface IFuseOptions<T> {
238238
isCaseSensitive?: boolean

test/logical-search.test.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,12 @@ describe('Multiple nested conditions', () => {
130130
]
131131

132132
const options = {
133+
includeScore: true,
133134
useExtendedSearch: true,
134135
keys: ['title', 'author.firstName', 'author.lastName', 'author.age']
135136
}
136-
const fuse1 = new Fuse(list1, options)
137137

138+
const fuse1 = new Fuse(list1, options)
138139
const fuse2 = new Fuse(list2, options)
139140

140141
test('Search: nested AND + OR', () => {
@@ -170,6 +171,8 @@ describe('Multiple nested conditions', () => {
170171
})
171172

172173
expect(result.length).toBe(1)
174+
expect(result[0]).toHaveProperty('score')
175+
expect(result[0].score).toBeGreaterThan(0)
173176
})
174177

175178
test('Search: deep nested AND + OR', () => {
@@ -189,6 +192,8 @@ describe('Multiple nested conditions', () => {
189192
})
190193

191194
expect(result.length).toBe(1)
195+
expect(result[0]).toHaveProperty('score')
196+
expect(result[0].score).toBeGreaterThan(0)
192197
})
193198
})
194199

@@ -206,6 +211,7 @@ describe('Logical search with dotted keys', () => {
206211

207212
const options = {
208213
useExtendedSearch: true,
214+
includeScore: true,
209215
keys: [
210216
'title',
211217
['author', 'first.name'],

0 commit comments

Comments
 (0)