diff --git a/README.md b/README.md index 3685c059..aba6e7d3 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ Fast fuzzy-search - the native replacement for `fuzzaldrin-plus` * Fuzzaldrin plus is an awesome library that provides fuzzy-search that is more targeted towards filenames. * Fuzzaldrin-plus-fast is a rewrite of the library in native C++ to make it fast. The goal is to make it a few hundred millisecond filter times for a dataset with 1M entries. This performance is helpful in Atom's fuzzy finder to open files from large projects such as Chrome/Mozilla. -This project potentially solves the following Atom fuzzy-finder issues if used. -https://github.com/atom/fuzzy-finder/issues/271 -https://github.com/atom/fuzzy-finder/issues/88 +Fuzzaldrin-plus-fast also provides an additional `filterTree` function which allows to fuzzy filter text in nested tree-like objects. # How performance is improved? Fuzzaldrin-plus-fast achieves 10x-20x performance improvement over Fuzzaldrin plus for chromium project with 300K files. This high performance is achieved using the following techniques. @@ -16,6 +14,9 @@ Fuzzaldrin-plus-fast achieves 10x-20x performance improvement over Fuzzaldrin pl * Use multiple threads to parallelize computation to achieve another 4x performance benefit. Currently, up to 8 threads are used if there are more than 10K candidates to filter. * Some miscellaneous improvements provide additional benefit. +This project potentially solves the following Atom fuzzy-finder issues if used. +https://github.com/atom/fuzzy-finder/issues/271 and https://github.com/atom/fuzzy-finder/issues/88 + # Is the API the same? API is backward compatible with Fuzzaldrin and Fuzzaldrin-plus. Additional functions are provided to achieve better performance that could suit your needs @@ -42,6 +43,7 @@ Sort and filter the given candidates by matching them against the given query. * `candidates` - An array of strings or objects. * `query` - A string query to match each candidate against. +* `options` options. You should provide a `key` in the options if an array of objects are passed. Returns an array of candidates sorted by best match against the query. @@ -61,6 +63,39 @@ candidates = [ results = filter(candidates, 'me', {key: 'name'}) // [{name: 'Me', id: 2}, {name: 'Maybe', id: 3}] ``` +### filterTree(candidates, query, dataKey, childrenKey, options = {}) + +Sort and filter the given Tree candidates by matching them against the given query. + +A **tree object** is an object in which each entry stores the data in its dataKey and it has (may have) some children (with a similar structure) in its childrenKey. See the following example. + +* `candidates` An array of tree objects. +* `query` A string query to match each candidate against. +* `dataKey` the key of the object (and its children) which holds the data +* `childrenKey` the key of the object (and its children) which hold the children +* `options` options +* `returns` An array of candidate objects in form of `{data, index, level}` sorted by best match against the query. Each objects has the address of the object in the tree using `index` and `level`. + +```js +const { filterTree } = require('fuzzaldrin-plus-fast') + +candidates = [ + {data: "bye1", children: [{data: "hello"}]}, + {data: "Bye2", children: [{data: "_bye4"}, {data: "hel"}]}, + {data: "eye"}, +] + +filterTree(candidates, "he", "data", "children") // [ { data: 'hel', index: 1, level: 1 }, { data: 'hello', index: 0, level: 1 }] + +// With an array of objects (similar to providing `key` to `filter` function) +const candidates = [ + {data: "helloworld"}, + {data: "bye"}, + {data: "hello"}, +] +results = filter(candidates, 'hello', {key: 'name'}) // [ { data: 'hello', index: 2, level: 0 }, { data: 'helloworld', index: 0, level: 0 } ] +``` + ### score(string, query, options = {}) Score the given string against the given query. diff --git a/binding.gyp b/binding.gyp index 867e7578..d5fc5435 100644 --- a/binding.gyp +++ b/binding.gyp @@ -2,13 +2,30 @@ "targets": [ { "target_name": "fuzzaldrinplusfast", - "sources": [ "src/fuzzaldrin.cc", "src/scorer.cc", "src/path_scorer.cc", "src/filter.cc", "src/query.cc", "src/matcher.cc" ], - "cflags!": [ "-fno-exceptions" ], - "cflags_cc!": [ "-fno-exceptions" ], + "sources": [ "src/fuzzaldrin.cc", "src/scorer.cc", "src/path_scorer.cc", "src/filter.cc", "src/query.cc", "src/matcher.cc", "src/tree.h" ], "include_dirs": [ " this.candidates[ind]) } + + filterTree(candidatesTrees, query, dataKey = "data", childrenKey = "children", options = {}) { + options = parseOptions(options) + return this.obj.filterTree(candidatesTrees, query, dataKey, childrenKey, options.maxResults, + Boolean(options.usePathScoring), Boolean(options.useExtensionBonus)) + } } export const New = () => new FuzzaldrinPlusFast() @@ -46,6 +52,14 @@ export function filter (candidates, query, options = {}) { return obj.filter(query, options) } + +export function filterTree(candidatesTrees, query, dataKey = "data", childrenKey = "children", options = {}) { + if (!candidatesTrees || !query) + return [] + const obj = new FuzzaldrinPlusFast() + return obj.filterTree(candidatesTrees, query, dataKey, childrenKey, options) +} + export function score (candidate, query, options = {}) { if (!candidate || !query) return 0 diff --git a/package-lock.json b/package-lock.json index 7e8b3a0a..3fc52fec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5068,6 +5068,36 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "deep-equal": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.4.tgz", + "integrity": "sha512-BUfaXrVoCfgkOQY/b09QdO9L3XNoF2XH0A3aY9IQwQL/ZjLOe8FQgCNVl1wiolhsFo8kFdO9zdPViCPbmaJA5w==", + "dev": true, + "requires": { + "es-abstract": "^1.18.0-next.1", + "es-get-iterator": "^1.1.0", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -5458,6 +5488,48 @@ "string.prototype.trimstart": "^1.0.1" } }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -6670,6 +6742,18 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz", + "integrity": "sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==", + "dev": true + }, + "is-boolean-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", + "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", + "dev": true + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -6829,6 +6913,12 @@ "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", "dev": true }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": true + }, "is-nan": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.0.tgz", @@ -6856,6 +6946,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "dev": true + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -6906,12 +7002,24 @@ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", "dev": true }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "dev": true + }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "dev": true }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, "is-svg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", @@ -6975,6 +7083,18 @@ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "dev": true }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, + "is-weakset": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz", + "integrity": "sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -10594,6 +10714,37 @@ "safe-regex": "^1.1.0" } }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, "regexpu-core": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", @@ -11028,6 +11179,16 @@ "shelljs": "^0.8.4" } }, + "side-channel": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", + "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", + "dev": true, + "requires": { + "es-abstract": "^1.18.0-next.0", + "object-inspect": "^1.8.0" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -12433,6 +12594,31 @@ "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz", + "integrity": "sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==", + "dev": true, + "requires": { + "is-bigint": "^1.0.0", + "is-boolean-object": "^1.0.0", + "is-number-object": "^1.0.3", + "is-string": "^1.0.4", + "is-symbol": "^1.0.2" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, "which-typed-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", diff --git a/package.json b/package.json index 1808ce34..8b8b3383 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "devDependencies": { "babel-preset-atomic": "^3.0.0", "cross-env": "^7.0.2", + "deep-equal": "^2.0.4", "fuzzaldrin-plus": "^0.6.0", "growl": ">=1.10.5", "jasmine-node": "^3.0.0", diff --git a/spec/filter-tree-spec.js b/spec/filter-tree-spec.js new file mode 100644 index 00000000..3e3b7575 --- /dev/null +++ b/spec/filter-tree-spec.js @@ -0,0 +1,115 @@ +const {filterTree} = require('../fuzzaldrin-dist') +const DeepEqual = require('deep-equal'); + +describe("filterTree", () => { + it("can fuzzy search in an array tree objects", () =>{ + candidates = [ + {data: "bye1", children: [{data: "hello"}]}, + {data: "Bye2", children: [{data: "_bye4"}, {data: "hel"}]}, + {data: "eye"}, + ] + + expect(DeepEqual( + filterTree(candidates, "hello", "data", "children"), + [ { data: 'hello', index: 0, level: 1 } ] + )).toBe(true) + + expect(DeepEqual( + filterTree(candidates, "hel", "data", "children"), + [ + { data: 'hel', index: 1, level: 1 }, + { data: 'hello', index: 0, level: 1 } + ] + )).toBe(true) + + expect(DeepEqual( + filterTree(candidates, "he", "data", "children"), + [ + { data: 'hel', index: 1, level: 1 }, + { data: 'hello', index: 0, level: 1 } + ] + )).toBe(true) + + expect(DeepEqual( + filterTree(candidates, "bye", "data", "children"), + [ + { data: 'bye1', index: 0, level: 0 }, + { data: '_bye4', index: 0, level: 1 }, + { data: 'Bye2', index: 1, level: 0 } + ] + )).toBe(true) + + expect(DeepEqual( + filterTree(candidates, "ye", "data", "children"), + process.platform !== "darwin" + ? + [ + { data: 'eye', index: 2, level: 0 }, + { data: 'bye1', index: 0, level: 0 }, + { data: 'Bye2', index: 1, level: 0 }, + { data: '_bye4', index: 0, level: 1 } + ] + : + [ + { data: 'eye', index: 2, level: 0 }, + { data: 'Bye2', index: 1, level: 0 }, + { data: 'bye1', index: 0, level: 0 }, + { data: '_bye4', index: 0, level: 1 } + ] + )).toBe(true) + + + // test maxResults + expect(DeepEqual( + filterTree(candidates, "bye", "data", "children", {maxResults: 2}), + [ + { data: 'bye1', index: 0, level: 0 }, + { data: '_bye4', index: 0, level: 1 }, + ] + )).toBe(true) + + + expect(DeepEqual( + filterTree(candidates, "ye", "data", "children", {maxResults: 3}), + process.platform !== "darwin" + ? + [ + { data: 'eye', index: 2, level: 0 }, + { data: 'bye1', index: 0, level: 0 }, + { data: 'Bye2', index: 1, level: 0 }, + ] + : + [ + { data: 'eye', index: 2, level: 0 }, + { data: 'Bye2', index: 1, level: 0 }, + { data: 'bye1', index: 0, level: 0 } + ] + )).toBe(true) + + }) + + it("can search in an array of children-less objects", () =>{ + const candidates = [ + {data: "helloworld"}, + {data: "bye"}, + {data: "hello"}, + ] + expect(DeepEqual( + filterTree(candidates, "hello", "data", "children"), + [ + { data: 'hello', index: 2, level: 0 }, + { data: 'helloworld', index: 0, level: 0 }, + ] + )).toBe(true) + + // test default values + expect(DeepEqual( + filterTree(candidates, "hello"), + [ + { data: 'hello', index: 2, level: 0 }, + { data: 'helloworld', index: 0, level: 0 }, + ] + )).toBe(true) + }) + +}) diff --git a/src/common.h b/src/common.h index f15a2842..9d447e79 100644 --- a/src/common.h +++ b/src/common.h @@ -1,3 +1,4 @@ +#pragma once #include #include #include diff --git a/src/fuzzaldrin.cc b/src/fuzzaldrin.cc index fd3001b2..f174cbc0 100644 --- a/src/fuzzaldrin.cc +++ b/src/fuzzaldrin.cc @@ -48,6 +48,73 @@ Napi::Value Fuzzaldrin::SetCandidates(const Napi::CallbackInfo& info) { return Napi::Boolean(); } +void Fuzzaldrin::SetCandidates(vector const &candidates) { + const size_t N = candidates.size(); // different + const size_t num_chunks = (N < 1000 * kMaxThreads) ? (N / 1000 + 1) : kMaxThreads; + candidates_.clear(); + candidates_.resize(num_chunks); + size_t cur_start = 0; + for (size_t i = 0; i < num_chunks; i++) { + size_t chunk_size = N / num_chunks; + // Distribute remainder among the chunks. + if (i < N % num_chunks) { + chunk_size++; + } + for (size_t j = cur_start; j < cur_start + chunk_size; j++) { + candidates_[i].push_back(candidates[j].data); // different + } + cur_start += chunk_size; + } +} + +/** (tree: Array, query: string, dataKey: string, childrenKey: string, options: Options) */ +Napi::Value Fuzzaldrin::FilterTree(const Napi::CallbackInfo& info) { + + // parse arguments + if (info.Length() != 7 + || !info[0].IsArray() + || !info[1].IsString() || !info[2].IsString() || !info[3].IsString() + || !info[4].IsNumber() || !info[5].IsBoolean() || !info[6].IsBoolean() + ) { + Napi::TypeError::New(info.Env(), "Invalid arguments").ThrowAsJavaScriptException(); + return Napi::Array::New(info.Env()); + } + auto const jsTreeArray = info[0].As(); + std::string query = info[1].As(); + + string const dataKey = info[2].As(); + string const childrenKey = info[3].As(); + + size_t maxResults = info[4].As().Uint32Value(); + bool usePathScoring = info[5].As(); + bool useExtensionBonus = info[6].As(); + + // create Tree and set candidates + auto tree = Tree(jsTreeArray, dataKey, childrenKey); + Fuzzaldrin::SetCandidates(tree.entriesArray); + + // create options + Options options(query, maxResults, usePathScoring, useExtensionBonus); + const auto matches = filter(candidates_, query, options); + + // filter + Napi::Array filteredCandidateObjects = Napi::Array::New(info.Env()); // array of candidate objects (with their address in index and level) + for (uint32_t i = 0, len = matches.size(); i < len; i++) { + auto entry = tree.entriesArray[matches[i]]; // + + // create {data, index, level} + auto obj = Napi::Object::New(info.Env()); + obj.Set("data", entry.data); + obj.Set("index", entry.index); + obj.Set("level", entry.level); + + filteredCandidateObjects[i] = obj; + } + return filteredCandidateObjects; +} + + + Napi::Number score(const Napi::CallbackInfo& info) { if (info.Length() != 4 || !info[0].IsString() || !info[1].IsString() || !info[2].IsBoolean() || !info[3].IsBoolean()) { @@ -109,6 +176,7 @@ Napi::Object Fuzzaldrin::Init(Napi::Env env, Napi::Object exports) { Napi::Function func = DefineClass(env, "Fuzzaldrin", { InstanceMethod("filter", &Fuzzaldrin::Filter), + InstanceMethod("filterTree", &Fuzzaldrin::FilterTree), InstanceMethod("setCandidates", &Fuzzaldrin::SetCandidates) }); diff --git a/src/fuzzaldrin.h b/src/fuzzaldrin.h index e089fc02..99c3716e 100644 --- a/src/fuzzaldrin.h +++ b/src/fuzzaldrin.h @@ -1,8 +1,10 @@ +#pragma once #ifndef FUZZALDRIN_H #define FUZZALDRIN_H #include #include "common.h" +#include "tree.h" // Converted from the example at // https://github.com/nodejs/node-addon-examples/blob/master/6_object_wrap/node-addon-api/ @@ -14,9 +16,12 @@ class Fuzzaldrin : public Napi::ObjectWrap { Napi::Value Filter(const Napi::CallbackInfo& info); Napi::Value SetCandidates(const Napi::CallbackInfo& info); + void SetCandidates(vector const& candidates); + + Napi::Value FilterTree(const Napi::CallbackInfo& info); private: vector> candidates_; }; -#endif // FUZZALDRIN_H +#endif // FUZZALDRIN_H \ No newline at end of file diff --git a/src/tree.h b/src/tree.h new file mode 100644 index 00000000..1a24967b --- /dev/null +++ b/src/tree.h @@ -0,0 +1,75 @@ +#pragma once +#include +#include +#include "common.h" + +/** Get the children of a jsTree (Napi::Object) */ +std::optional getChildren(Napi::Object const& jsTree, string const& childrenKey) { + Napi::Array childrenArray; + + // determine if it has children + bool hasChildren = false; + if (jsTree.HasOwnProperty(childrenKey)) { + auto childrenRaw = jsTree.Get(childrenKey); + if (childrenRaw.IsArray()) { + childrenArray = childrenRaw.As(); + if (childrenArray.Length() != 0) { + hasChildren = true; + } + } + } + if (hasChildren) + return childrenArray; + return {}; +} + + +struct CandidateObject { + CandidateString data; + size_t level = 0; + int32_t index = -1; + + CandidateObject(CandidateString const data, size_t const level, int32_t const index) + : data{ data }, level{ level }, index{ index } {}; +}; + +template +struct Tree { + T jsTreeArrayOrObject; + string dataKey; + string childrenKey; + + + /** an array of the CandidateObject which includes the data and its address (index, level) in the tree for each */ + vector entriesArray; + + /** Recursive function that fills the entriesArray from the given jsTreeArray */ + void makeEntriesArray(Napi::Array const& jsTreeArray, size_t const level) { + for (uint32_t iEntry = 0, len = jsTreeArray.Length(); iEntry < len; iEntry++) { + auto jsTree = jsTreeArray[iEntry].As(); + makeEntriesArray(jsTree, level, iEntry); + } + } + + /** 1st argument is a single object */ + void makeEntriesArray(Napi::Object const& jsTree, size_t const level, int32_t const iEntry = -1) { + // get the current data + CandidateString data = jsTree.Get(dataKey).ToString().Utf8Value(); + entriesArray.push_back(CandidateObject(data, level, iEntry)); + + // add children if any + auto mayChildren = getChildren(jsTree, childrenKey); + if (mayChildren.has_value()) { + // recurse + makeEntriesArray(mayChildren.value(), level + 1); + } + } + + /** create a Tree object and make an entries array */ + Tree(T const _jsTreeArrayOrObject, string const _dataKey, string const _childrenKey) { + dataKey = _dataKey; + childrenKey = _childrenKey; + jsTreeArrayOrObject = _jsTreeArrayOrObject; + makeEntriesArray(jsTreeArrayOrObject, 0); + } +}; diff --git a/types/fuzzaldrin-plus-fast.d.ts b/types/fuzzaldrin-plus-fast.d.ts index b7abb6fc..e7fc63da 100644 --- a/types/fuzzaldrin-plus-fast.d.ts +++ b/types/fuzzaldrin-plus-fast.d.ts @@ -36,11 +36,30 @@ export type IFilterOptions = IOptions & { /** Sort and filter the given candidates by matching them against the given query. * @param candidates An array of strings or objects. * @param query A string query to match each candidate against. +* @param options options * @return returns an array of candidates sorted by best match against the query. */ export function filter( - data: T[], + candidates: T[], + query: string, + options?: IFilterOptions +): T[] + + +/** Sort and filter the given Tree candidates by matching them against the given query. +* A tree object is an object in which each entry stores the data in its dataKey and it has (may have) some children (with a similar structure) in its childrenKey +* @param candidates An array of tree objects. +* @param query A string query to match each candidate against. +* @param dataKey the key of the object (and its children) which holds the data +* @param childrenKey the key of the object (and its children) which hold the children +* @param options options +* @return An array of candidate objects in form of `{data, index, level}` sorted by best match against the query. Each objects has the address of the object in the tree using `index` and `level`. + */ +export function filterTree( + candidates: T[], query: string, + dataKey: string, + childrenKey: string, options?: IFilterOptions ): T[]