Skip to content

Commit

Permalink
Merge pull request #42 from atom-ide-community/independent-tree
Browse files Browse the repository at this point in the history
  • Loading branch information
aminya authored Nov 9, 2020
2 parents bd467dd + a47ed63 commit b1a177b
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 69 deletions.
90 changes: 82 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ 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.

Fuzzaldrin-plus-fast also provides an additional `filterTree` function which allows to fuzzy filter text in nested tree-like objects.
### Extra featuers
Fuzzaldrin-plus-fast:
- provides `filterTree` function which allows to fuzzy filter text in nested tree-like objects.
- allows setting the candidates only once using `ArrayFilterer` and `TreeFilterer` classes, and then, perform `filter` multiple times. This is much more efficient than calling the `filter` or `filterTree` functions directly every time.

# 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.
Expand Down Expand Up @@ -63,6 +66,41 @@ candidates = [
results = filter(candidates, 'me', {key: 'name'}) // [{name: 'Me', id: 2}, {name: 'Maybe', id: 3}]
```

**Performance Note**: use `ArrayFilterer` class if you call the `filter` function multiple times on a certain set of candidates. `filter` internally uses this class, however, in each call it sets the candidates from scratch which can slow down the process.

### ArrayFilterer class

ArrayFilterer is a class that allows to set the `candidates` only once and perform filtering on them multiple times. This is much more efficient than calling the `filter` function directly.
```typescript
export class ArrayFilterer<T> {
constructor()

/** The method to set the candidates that are going to be filtered
* @param candidates An array of tree objects.
* @param dataKey (optional) if `candidates` is an array of objects, pass the key in the object which holds the data. dataKey can be the options object passed to `filter` method (but this is deprecated).
*/
setCandidates<T>(candidates: Array<T>, dataKey?: string): void

/** The method to perform the filtering on the already set candidates
* @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.
*/
filter(query: string, options: IFilterOptions<T>): Array<T>
}
```

Example:
```Javascript
const { ArrayFilterer } = require('fuzzaldrin-plus-fast')

const arrayFilterer = new ArrayFilterer()
arrayFilterer.setCandidates(['Call', 'Me', 'Maybe']) // set candidates only once
// call filter multiple times
arrayFilterer.filter('me')
arrayFilterer.filter('all')
```

### filterTree(candidates, query, dataKey, childrenKey, options = {})

Sort and filter the given Tree candidates by matching them against the given query.
Expand Down Expand Up @@ -96,6 +134,49 @@ const candidates = [
results = filter(candidates, 'hello', {key: 'name'}) // [ { data: 'hello', index: 2, level: 0 }, { data: 'helloworld', index: 0, level: 0 } ]
```

**Performance Note**: use `TreeFilterer` class if you call the `filterTree` function multiple times on a certain set of candidates. `filterTree` internally uses this class, however, in each call it sets the candidates from scratch which can slow down the process.

### TreeFilterer class
`TreeFilterer` is a class that allows to set the `candidates` only once and perform filtering on them multiple times. This is much more efficient than calling the `filterTree` function directly.

```typescript
export class TreeFilterer<T> {
constructor()

/** The method to set the candidates that are going to be filtered
* @param candidates An array of tree objects.
* @param dataKey the key of the object (and its children) which holds the data (defaults to `"data"`)
* @param childrenKey the key of the object (and its children) which hold the children (defaults to `"children"`)
*/
setCandidates<T>(candidates: Array<T>, dataKey?: string, childrenKey?: string): void

/** The method to perform the filtering on the already set candidates
* @param query A string query to match each candidate against.
* @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`.
*/
filter(query: string, options: IFilterOptions<object>): TreeFilterResult[]
}
```

Example:
```Javascript
const { TreeFilterer } = require('fuzzaldrin-plus-fast')

const arrayFilterer = new TreeFilterer()

const candidates = [
{data: "bye1", children: [{data: "hello"}]},
{data: "Bye2", children: [{data: "_bye4"}, {data: "hel"}]},
{data: "eye"},
]
arrayFilterer.setCandidates(candidates, "data", "children") // set candidates only once

// call filter multiple times
arrayFilterer.filter('hello')
arrayFilterer.filter('bye')
```

### score(string, query, options = {})

Score the given string against the given query.
Expand Down Expand Up @@ -155,13 +236,6 @@ In all the above functions, you can pass an optional object with the following k
}
```

### New()
Initializes the native binding
```js
const { New } = require('fuzzaldrin-plus-fast')
New()
```

# Info for Developers
## How to release the package to npm?

Expand Down
63 changes: 46 additions & 17 deletions fuzzaldrin.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,27 @@ function parseOptions(options, query) {
return options
}

class FuzzaldrinPlusFast {
/** Array Filter */

export class ArrayFilterer {
constructor() {
this.obj = new binding.Fuzzaldrin()
}

setCandidates(candidates, options = {}) {
setCandidates(candidates, dataKey = undefined) {
this.candidates = candidates
if (options.key)
candidates = candidates.map((item) => item[options.key])
return this.obj.setCandidates(candidates)

if (dataKey) {
if (typeof dataKey == "string") {
candidates = candidates.map((item) => item[dataKey])
}
// @deprecated pass the key as the second argument as a string
else if (dataKey.key) { // an object (options) containing the key
candidates = candidates.map((item) => item[dataKey.key])
}
}

return this.obj.setArrayFiltererCandidates(candidates)
}

filter(query, options = {}) {
Expand All @@ -34,30 +45,46 @@ class FuzzaldrinPlusFast {
Boolean(options.usePathScoring), Boolean(options.useExtensionBonus))
return res.map((ind) => 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()
/**
* @deprecated use ArrayFilterer or TreeFilterer instead class instead
*/
export const New = () => new ArrayFilterer()

export function filter (candidates, query, options = {}) {
if (!candidates || !query)
return []
const obj = new FuzzaldrinPlusFast()
obj.setCandidates(candidates, options)
return obj.filter(query, options)
const arrayFilterer = new ArrayFilterer()
arrayFilterer.setCandidates(candidates, options)
return arrayFilterer.filter(query, options)
}

/** Tree Filter */

export class TreeFilterer {
constructor() {
this.obj = new binding.Fuzzaldrin()
}

setCandidates(candidates, dataKey = "data", childrenKey = "children") {
this.candidates = candidates
return this.obj.setTreeFiltererCandidates(candidates, dataKey, childrenKey)
}

filter(query, options = {}) {
options = parseOptions(options)
return this.obj.filterTree(query, options.maxResults,
Boolean(options.usePathScoring), Boolean(options.useExtensionBonus))
}
}

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)
const treeFilterer = new TreeFilterer()
treeFilterer.setCandidates(candidatesTrees, dataKey, childrenKey)
return treeFilterer.filter(query, options)
}

export function score (candidate, query, options = {}) {
Expand All @@ -68,6 +95,8 @@ export function score (candidate, query, options = {}) {
Boolean(options.usePathScoring), Boolean(options.useExtensionBonus))
}

/** Other functions */

export function match (string, query, options = {}) {
if (!string || !query)
return []
Expand Down
11 changes: 11 additions & 0 deletions spec/array-filterer-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { ArrayFilterer } = require('../fuzzaldrin-dist')

describe('ArrayFilterer', function () {
it('is possible to set candidates only once and filter multiple times', function(){
const arrayFilterer = new ArrayFilterer()
arrayFilterer.setCandidates(['Call', 'Me', 'Maybe']) // set candidates only once
// call filter multiple times
expect(arrayFilterer.filter('me')).toEqual(['Me', 'Maybe'])
expect(arrayFilterer.filter('all')).toEqual(['Call'])
})
});
36 changes: 36 additions & 0 deletions spec/tree-filterer-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { TreeFilterer } = require("../fuzzaldrin-dist")
const DeepEqual = require('deep-equal');

describe("ArrayFilterer", function() {
it('is possible to set candidates only once and filter multiple times', function() {

const arrayFilterer = new TreeFilterer()

const candidates = [
{data: "bye1", children: [{data: "hello"}]},
{data: "Bye2", children: [{data: "_bye4"}, {data: "hel"}]},
{data: "eye"},
]
arrayFilterer.setCandidates(candidates, "data", "children") // set candidates only once


// call filter multiple times

expect(DeepEqual(
arrayFilterer.filter('hello'),
[ { data: 'hello', index: 0, level: 1 } ]
)).toBe(true)


expect(DeepEqual(
arrayFilterer.filter('bye'),
[
{ data: 'bye1', index: 0, level: 0 },
{ data: '_bye4', index: 0, level: 1 },
{ data: 'Bye2', index: 1, level: 0 }
]
)).toBe(true)


})
})
Loading

0 comments on commit b1a177b

Please sign in to comment.