@pulsar-edit/fuzzy-native 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -48
- package/binding.gyp +5 -2
- package/lib/main.d.ts +119 -0
- package/package.json +3 -2
- package/src/MatcherBase.cpp +35 -89
- package/src/fuzzy-native.cpp +155 -0
- package/src/fuzzy-native.h +20 -0
- package/src/binding.cpp +0 -233
package/README.md
CHANGED
|
@@ -8,54 +8,7 @@ The scoring algorithm is heavily tuned for file paths, but should work for gener
|
|
|
8
8
|
|
|
9
9
|
## API
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export type MatcherOptions = {
|
|
13
|
-
// Default: false
|
|
14
|
-
caseSensitive?: boolean,
|
|
15
|
-
|
|
16
|
-
// Default: infinite
|
|
17
|
-
maxResults?: number,
|
|
18
|
-
|
|
19
|
-
// Maximum gap to allow between consecutive letters in a match.
|
|
20
|
-
// Provide a smaller maxGap to speed up query results.
|
|
21
|
-
// Default: unlimited
|
|
22
|
-
maxGap?: number;
|
|
23
|
-
|
|
24
|
-
// Default: 1
|
|
25
|
-
numThreads?: number,
|
|
26
|
-
|
|
27
|
-
// Default: false
|
|
28
|
-
recordMatchIndexes?: boolean,
|
|
29
|
-
|
|
30
|
-
// can be either "fuzzaldrin" or anything else to use the file-based option
|
|
31
|
-
algorithm?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type MatchResult = {
|
|
35
|
-
id: number,
|
|
36
|
-
value: string,
|
|
37
|
-
|
|
38
|
-
// A number in the range (0-1]. Higher scores are more relevant.
|
|
39
|
-
// 0 denotes "no match" and will never be returned.
|
|
40
|
-
score: number,
|
|
41
|
-
|
|
42
|
-
// Matching character index in `value` for each character in `query`.
|
|
43
|
-
// This can be costly, so this is only returned if `recordMatchIndexes` was set in `options`.
|
|
44
|
-
matchIndexes?: Array<number>,
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class Matcher {
|
|
48
|
-
constructor(candidates: Array<string>) {}
|
|
49
|
-
|
|
50
|
-
// Returns all matching candidates (subject to `options`).
|
|
51
|
-
// Will be ordered by score, descending.
|
|
52
|
-
match: (query: string, options?: MatcherOptions) => Array<MatchResult>;
|
|
53
|
-
|
|
54
|
-
addCandidates: (ids: Array<number>, candidates: Array<string>) => void;
|
|
55
|
-
removeCandidates: (ids: Array<number>) => void;
|
|
56
|
-
setCandidates: (ids: Array<number>, candidates: Array<string>) => void;
|
|
57
|
-
}
|
|
58
|
-
```
|
|
11
|
+
Read `lib/main.d.ts` for the API of the `Matcher` class.
|
|
59
12
|
|
|
60
13
|
See also the [spec](spec/fuzzy-native-spec.js) for basic usage.
|
|
61
14
|
|
package/binding.gyp
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
'targets': [
|
|
3
3
|
{
|
|
4
4
|
'target_name': 'fuzzy-native',
|
|
5
|
-
'include_dirs': [
|
|
5
|
+
'include_dirs': [
|
|
6
|
+
'<!@(node -p "require(\'node-addon-api\').include")'
|
|
7
|
+
],
|
|
8
|
+
'defines': ['NAPI_DISABLE_CPP_EXCEPTIONS'],
|
|
6
9
|
'cflags': [
|
|
7
10
|
'-std=c++20',
|
|
8
11
|
'-O3',
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
'MACOSX_DEPLOYMENT_TARGET': '10.7',
|
|
17
20
|
},
|
|
18
21
|
'sources': [
|
|
19
|
-
'src/
|
|
22
|
+
'src/fuzzy-native.cpp',
|
|
20
23
|
'src/fuzzaldrin_score.cpp',
|
|
21
24
|
'src/score_match.cpp',
|
|
22
25
|
'src/MatcherBase.cpp',
|
package/lib/main.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* The options that can be passed to {@link Matcher#match}.
|
|
4
|
+
*/
|
|
5
|
+
export type MatcherOptions = {
|
|
6
|
+
/** Whether matching is case-sensitive. Defaults to `false`. */
|
|
7
|
+
caseSensitive?: boolean;
|
|
8
|
+
|
|
9
|
+
/** How many results to return at the maximum. Defaults to no limit. */
|
|
10
|
+
maxResults?: number;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum “gap” to allow between consecutive letters for a match candiate.
|
|
14
|
+
* Provide a smaller value to speed up query results. Defaults to no limit.
|
|
15
|
+
*/
|
|
16
|
+
maxGap?: number;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How many threads to use while searching. Defaults to `1`.
|
|
20
|
+
*/
|
|
21
|
+
numThreads?: number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether to return metadata about the indices of the characters that
|
|
25
|
+
* matched in each returned max. Defaults to `false`.
|
|
26
|
+
*/
|
|
27
|
+
recordMatchIndexes?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The algorithm to use for fuzzy-matching. If `"fuzzaldrin"`, will use that
|
|
31
|
+
* algorithm for legacy support. Any other value, including the default of
|
|
32
|
+
* `undefined`, will trigger use of the default algorithm.
|
|
33
|
+
*/
|
|
34
|
+
algorithm?: 'fuzzaldrin' | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A single result returned by {@link Matcher#match}.
|
|
39
|
+
*/
|
|
40
|
+
export type MatchResult = {
|
|
41
|
+
/** A unique identifier for the match. */
|
|
42
|
+
id: number;
|
|
43
|
+
|
|
44
|
+
/** The string value of the match. */
|
|
45
|
+
value: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A number in the range (0, 1] — i.e., the maximum value is `1` and the
|
|
49
|
+
* minimum value is the smallest possible positive value. Higher scores mean
|
|
50
|
+
* more relevant matches. `0` means “no match” and will never be returned.
|
|
51
|
+
*/
|
|
52
|
+
score: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Matching charcter index in `value` for each character in `query`. This can
|
|
56
|
+
* be costly, so this information is returned only when
|
|
57
|
+
* {@link MatcherOptions.recordMatchIndexes} is `true`.
|
|
58
|
+
*/
|
|
59
|
+
matchIndexes?: number[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class Matcher {
|
|
63
|
+
/**
|
|
64
|
+
* Construct a new {@link Matcher} object.
|
|
65
|
+
*
|
|
66
|
+
* You may specify candidates at instantiation time (with the same arguments
|
|
67
|
+
* used by {@link addCandidates} and {@link setCandidates}) or you may wait
|
|
68
|
+
* and add candidates later.
|
|
69
|
+
*
|
|
70
|
+
* @param ids A list of numeric IDs. Must correspond to the candidates
|
|
71
|
+
* themselves.
|
|
72
|
+
* @param candidates A list of candidates against which we will be matching.
|
|
73
|
+
*/
|
|
74
|
+
constructor();
|
|
75
|
+
constructor(ids: number[], candidates: string[]);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find all candidates that match the given query.
|
|
79
|
+
*
|
|
80
|
+
* @param query The input against which candidates will be searched.
|
|
81
|
+
* @param options Any {@link MatcherOptions}.
|
|
82
|
+
*/
|
|
83
|
+
match(query: string, options?: MatcherOptions): MatchResult[];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Add candidates to the list.
|
|
87
|
+
*
|
|
88
|
+
* You are responsible for ensuring that the IDs you use do not match the IDs
|
|
89
|
+
* of any candidates that are already present in the `Matcher`. Any
|
|
90
|
+
* candidates whose IDs already exist in the `Matcher` are silently ignored.
|
|
91
|
+
*
|
|
92
|
+
* @param ids A list of numeric IDs. Must correspond to the candidates
|
|
93
|
+
* themselves.
|
|
94
|
+
* @param candidates A list of candidates against which we will be matching.
|
|
95
|
+
*/
|
|
96
|
+
addCandidates(ids: number[], candidates: string[]): void;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove candidates from the list.
|
|
100
|
+
*
|
|
101
|
+
* @param ids The unique identifiers for each of the candidates you want to
|
|
102
|
+
* remove. Must be an array; if you want to remove only one candidate, wrap
|
|
103
|
+
* the value in an array first.
|
|
104
|
+
*/
|
|
105
|
+
removeCandidates(ids: number[]): void;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set a complete list of candidates, removing any candidate that may already
|
|
109
|
+
* be defined.
|
|
110
|
+
*
|
|
111
|
+
* If you want to add candidates instead without removing any that may
|
|
112
|
+
* already exist, use {@link addCandidates}.
|
|
113
|
+
*
|
|
114
|
+
* @param ids A list of numeric IDs. Must correspond to the candidates
|
|
115
|
+
* themselves.
|
|
116
|
+
* @param candidates A list of candidates against which we will be matching.
|
|
117
|
+
*/
|
|
118
|
+
setCandidates(ids: number[], candidates: string[]): void;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulsar-edit/fuzzy-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Native C++ implementation of a fuzzy string matcher.",
|
|
5
5
|
"main": "lib/main.js",
|
|
6
|
+
"types": "lib/main.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "jasmine-node --captureExceptions spec"
|
|
8
9
|
},
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
"repository": "https://github.com/pulsar-edit/fuzzy-native",
|
|
22
23
|
"license": "MIT",
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
+
"node-addon-api": "^8.8.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"jasmine-node": "^1.14.5",
|
package/src/MatcherBase.cpp
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#include "MatcherBase.h"
|
|
2
|
-
#include "score_match.h"
|
|
3
2
|
#include "fuzzaldrin_score.h"
|
|
3
|
+
#include "score_match.h"
|
|
4
4
|
|
|
5
5
|
#include <algorithm>
|
|
6
6
|
#include <atomic>
|
|
@@ -34,7 +34,7 @@ inline uint64_t letter_bitmask(const std::string &str) {
|
|
|
34
34
|
|
|
35
35
|
inline string str_to_lower(const std::string &s) {
|
|
36
36
|
string lower(s);
|
|
37
|
-
for (auto&
|
|
37
|
+
for (auto &c : lower) {
|
|
38
38
|
if (c >= 'A' && c <= 'Z') {
|
|
39
39
|
c += 'a' - 'A';
|
|
40
40
|
}
|
|
@@ -42,9 +42,7 @@ inline string str_to_lower(const std::string &s) {
|
|
|
42
42
|
return lower;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
bool is_slash(char c) {
|
|
46
|
-
return c == '/' || c == '\\';
|
|
47
|
-
}
|
|
45
|
+
bool is_slash(char c) { return c == '/' || c == '\\'; }
|
|
48
46
|
|
|
49
47
|
int num_dirs(const std::string &path) {
|
|
50
48
|
int num = 0;
|
|
@@ -86,12 +84,8 @@ int score_based_root_path(const MatchOptions &options,
|
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
// Push a new entry on the heap while ensuring size <= max_results.
|
|
89
|
-
void push_heap(ResultHeap &heap,
|
|
90
|
-
|
|
91
|
-
int score_based_root_path,
|
|
92
|
-
uint32_t id,
|
|
93
|
-
const std::string *value,
|
|
94
|
-
size_t max_results) {
|
|
87
|
+
void push_heap(ResultHeap &heap, float score, int score_based_root_path,
|
|
88
|
+
uint32_t id, const std::string *value, size_t max_results) {
|
|
95
89
|
MatchResult result(score, score_based_root_path, id, value);
|
|
96
90
|
if (heap.size() < max_results || result < heap.top()) {
|
|
97
91
|
heap.push(std::move(result));
|
|
@@ -101,26 +95,17 @@ void push_heap(ResultHeap &heap,
|
|
|
101
95
|
}
|
|
102
96
|
}
|
|
103
97
|
|
|
104
|
-
vector<MatchResult> finalize(const string &query,
|
|
105
|
-
const string &query_case,
|
|
98
|
+
vector<MatchResult> finalize(const string &query, const string &query_case,
|
|
106
99
|
const MatchOptions &options,
|
|
107
|
-
bool record_match_indexes,
|
|
108
|
-
ResultHeap &&heap) {
|
|
100
|
+
bool record_match_indexes, ResultHeap &&heap) {
|
|
109
101
|
vector<MatchResult> vec;
|
|
110
102
|
while (heap.size()) {
|
|
111
103
|
const MatchResult &result = heap.top();
|
|
112
104
|
if (record_match_indexes) {
|
|
113
105
|
result.matchIndexes.reset(new vector<int>(query.size()));
|
|
114
106
|
string lower = str_to_lower(*result.value);
|
|
115
|
-
score_match(
|
|
116
|
-
|
|
117
|
-
lower.c_str(),
|
|
118
|
-
query.c_str(),
|
|
119
|
-
query_case.c_str(),
|
|
120
|
-
options,
|
|
121
|
-
0.0,
|
|
122
|
-
result.matchIndexes.get()
|
|
123
|
-
);
|
|
107
|
+
score_match(result.value->c_str(), lower.c_str(), query.c_str(),
|
|
108
|
+
query_case.c_str(), options, 0.0, result.matchIndexes.get());
|
|
124
109
|
}
|
|
125
110
|
vec.push_back(result);
|
|
126
111
|
heap.pop();
|
|
@@ -129,18 +114,11 @@ vector<MatchResult> finalize(const string &query,
|
|
|
129
114
|
return vec;
|
|
130
115
|
}
|
|
131
116
|
|
|
132
|
-
void thread_worker(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
std::atomic<float>* min_score,
|
|
138
|
-
size_t max_results,
|
|
139
|
-
vector<MatcherBase::CandidateData> &candidates,
|
|
140
|
-
size_t start,
|
|
141
|
-
size_t end,
|
|
142
|
-
ResultHeap &result
|
|
143
|
-
) {
|
|
117
|
+
void thread_worker(const string &query, const string &query_case,
|
|
118
|
+
const MatchOptions &options, bool use_last_match,
|
|
119
|
+
std::atomic<float> *min_score, size_t max_results,
|
|
120
|
+
vector<MatcherBase::CandidateData> &candidates, size_t start,
|
|
121
|
+
size_t end, ResultHeap &result) {
|
|
144
122
|
uint64_t bitmask = letter_bitmask(query_case.c_str());
|
|
145
123
|
for (size_t i = start; i < end; i++) {
|
|
146
124
|
auto &candidate = candidates[i];
|
|
@@ -149,30 +127,19 @@ void thread_worker(
|
|
|
149
127
|
}
|
|
150
128
|
if ((bitmask & candidate.bitmask) == bitmask) {
|
|
151
129
|
float score;
|
|
152
|
-
if(query == "") {
|
|
130
|
+
if (query == "") {
|
|
153
131
|
score = 1;
|
|
154
|
-
} else if(options.fuzzaldrin) {
|
|
132
|
+
} else if (options.fuzzaldrin) {
|
|
155
133
|
score = fuzzaldrin_score(candidate.value, query);
|
|
156
134
|
score = fuzzaldrin_basename_score(candidate.value, query, score);
|
|
157
135
|
} else {
|
|
158
|
-
score = score_match(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
query.c_str(),
|
|
162
|
-
query_case.c_str(),
|
|
163
|
-
options,
|
|
164
|
-
min_score->load()
|
|
165
|
-
);
|
|
136
|
+
score = score_match(candidate.value.c_str(),
|
|
137
|
+
candidate.lowercase.c_str(), query.c_str(),
|
|
138
|
+
query_case.c_str(), options, min_score->load());
|
|
166
139
|
}
|
|
167
140
|
if (score > 0) {
|
|
168
|
-
push_heap(
|
|
169
|
-
|
|
170
|
-
score,
|
|
171
|
-
score_based_root_path(options, candidate),
|
|
172
|
-
candidate.id,
|
|
173
|
-
&candidate.value,
|
|
174
|
-
max_results
|
|
175
|
-
);
|
|
141
|
+
push_heap(result, score, score_based_root_path(options, candidate),
|
|
142
|
+
candidate.id, &candidate.value, max_results);
|
|
176
143
|
if (result.size() == max_results) {
|
|
177
144
|
float current_max = result.top().score;
|
|
178
145
|
float min_score_value = min_score->load();
|
|
@@ -232,8 +199,9 @@ vector<MatchResult> MatcherBase::findMatches(const std::string &query,
|
|
|
232
199
|
ResultHeap combined;
|
|
233
200
|
std::atomic<float> min_score(0);
|
|
234
201
|
if (num_threads == 0 || candidates_.size() < 10000) {
|
|
235
|
-
thread_worker(new_query, query_case, matchOptions, use_last_match,
|
|
236
|
-
max_results, candidates_, 0, candidates_.size(),
|
|
202
|
+
thread_worker(new_query, query_case, matchOptions, use_last_match,
|
|
203
|
+
&min_score, max_results, candidates_, 0, candidates_.size(),
|
|
204
|
+
combined);
|
|
237
205
|
} else {
|
|
238
206
|
vector<ResultHeap> thread_results(num_threads);
|
|
239
207
|
vector<thread> threads;
|
|
@@ -244,19 +212,10 @@ vector<MatchResult> MatcherBase::findMatches(const std::string &query,
|
|
|
244
212
|
if (i < candidates_.size() % num_threads) {
|
|
245
213
|
chunk_size++;
|
|
246
214
|
}
|
|
247
|
-
threads.emplace_back(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
ref(matchOptions),
|
|
252
|
-
use_last_match,
|
|
253
|
-
&min_score,
|
|
254
|
-
max_results,
|
|
255
|
-
ref(candidates_),
|
|
256
|
-
cur_start,
|
|
257
|
-
cur_start + chunk_size,
|
|
258
|
-
ref(thread_results[i])
|
|
259
|
-
);
|
|
215
|
+
threads.emplace_back(thread_worker, ref(new_query), ref(query_case),
|
|
216
|
+
ref(matchOptions), use_last_match, &min_score,
|
|
217
|
+
max_results, ref(candidates_), cur_start,
|
|
218
|
+
cur_start + chunk_size, ref(thread_results[i]));
|
|
260
219
|
cur_start += chunk_size;
|
|
261
220
|
}
|
|
262
221
|
|
|
@@ -264,26 +223,15 @@ vector<MatchResult> MatcherBase::findMatches(const std::string &query,
|
|
|
264
223
|
threads[i].join();
|
|
265
224
|
while (thread_results[i].size()) {
|
|
266
225
|
auto &top = thread_results[i].top();
|
|
267
|
-
push_heap(
|
|
268
|
-
|
|
269
|
-
top.score,
|
|
270
|
-
top.score_based_root_path,
|
|
271
|
-
top.id,
|
|
272
|
-
top.value,
|
|
273
|
-
max_results
|
|
274
|
-
);
|
|
226
|
+
push_heap(combined, top.score, top.score_based_root_path, top.id,
|
|
227
|
+
top.value, max_results);
|
|
275
228
|
thread_results[i].pop();
|
|
276
229
|
}
|
|
277
230
|
}
|
|
278
231
|
}
|
|
279
232
|
|
|
280
|
-
return finalize(
|
|
281
|
-
|
|
282
|
-
query_case,
|
|
283
|
-
matchOptions,
|
|
284
|
-
options.record_match_indexes,
|
|
285
|
-
move(combined)
|
|
286
|
-
);
|
|
233
|
+
return finalize(new_query, query_case, matchOptions,
|
|
234
|
+
options.record_match_indexes, std::move(combined));
|
|
287
235
|
}
|
|
288
236
|
|
|
289
237
|
void MatcherBase::addCandidate(uint32_t id, const string &candidate) {
|
|
@@ -295,10 +243,10 @@ void MatcherBase::addCandidate(uint32_t id, const string &candidate) {
|
|
|
295
243
|
data.id = id;
|
|
296
244
|
data.value = candidate;
|
|
297
245
|
data.bitmask = letter_bitmask(lowercase.c_str());
|
|
298
|
-
data.lowercase = move(lowercase);
|
|
246
|
+
data.lowercase = std::move(lowercase);
|
|
299
247
|
data.last_match = true;
|
|
300
248
|
data.num_dirs = num_dirs(candidate);
|
|
301
|
-
candidates_.emplace_back(move(data));
|
|
249
|
+
candidates_.emplace_back(std::move(data));
|
|
302
250
|
}
|
|
303
251
|
}
|
|
304
252
|
|
|
@@ -324,6 +272,4 @@ void MatcherBase::reserve(size_t n) {
|
|
|
324
272
|
lookup_.reserve(n);
|
|
325
273
|
}
|
|
326
274
|
|
|
327
|
-
size_t MatcherBase::size() const {
|
|
328
|
-
return candidates_.size();
|
|
329
|
-
}
|
|
275
|
+
size_t MatcherBase::size() const { return candidates_.size(); }
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#include "fuzzy-native.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
|
|
5
|
+
using namespace Napi;
|
|
6
|
+
|
|
7
|
+
#define CHECK(env, cond, msg) \
|
|
8
|
+
if (!(cond)) \
|
|
9
|
+
{ \
|
|
10
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException(); \
|
|
11
|
+
return; \
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#define CHECK_OR_RETURN(env, cond, msg, retval) \
|
|
15
|
+
if (!(cond)) \
|
|
16
|
+
{ \
|
|
17
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException(); \
|
|
18
|
+
return retval; \
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Matcher::Matcher(const Napi::CallbackInfo &info) : ObjectWrap<Matcher>(info)
|
|
22
|
+
{
|
|
23
|
+
if (info.Length() > 0) {
|
|
24
|
+
AddCandidates(info);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Napi::Value Matcher::Match(const Napi::CallbackInfo &info)
|
|
29
|
+
{
|
|
30
|
+
Napi::Env env = info.Env();
|
|
31
|
+
CHECK_OR_RETURN(env, info.Length() >= 1, "Wrong Number of arguments", env.Null());
|
|
32
|
+
CHECK_OR_RETURN(env, info[0].IsString(), "First argument should be a query string", env.Null());
|
|
33
|
+
auto query = info[0].As<Napi::String>().Utf8Value();
|
|
34
|
+
MatcherOptions options;
|
|
35
|
+
if (info.Length() > 1) {
|
|
36
|
+
CHECK_OR_RETURN(env, info[1].IsObject(), "Second argument should be an options object", env.Null());
|
|
37
|
+
Napi::Object options_obj = info[1].As<Napi::Object>();
|
|
38
|
+
|
|
39
|
+
options.case_sensitive = options_obj.Get("caseSensitive").ToBoolean();
|
|
40
|
+
options.smart_case = options_obj.Get("smartCase").ToBoolean();
|
|
41
|
+
options.num_threads = options_obj.Get("numThreads").ToNumber().Uint32Value();
|
|
42
|
+
options.max_results = options_obj.Get("maxResults").ToNumber().Uint32Value();
|
|
43
|
+
options.max_gap = options_obj.Get("maxGap").ToNumber().Uint32Value();
|
|
44
|
+
options.record_match_indexes = options_obj.Get("recordMatchIndexes");
|
|
45
|
+
options.fuzzaldrin = options_obj.Get("algorithm").ToString().Utf8Value() == "fuzzaldrin";
|
|
46
|
+
options.root_path = options_obj.Get("rootPath").ToString();
|
|
47
|
+
}
|
|
48
|
+
auto idKey = Napi::String::New(env, "id");
|
|
49
|
+
auto valueKey = Napi::String::New(env, "value");
|
|
50
|
+
auto scoreKey = Napi::String::New(env, "score");
|
|
51
|
+
auto matchIndexesKey = Napi::String::New(env, "matchIndexes");
|
|
52
|
+
|
|
53
|
+
std::vector<MatchResult> matches = this->_impl.findMatches(query, options);
|
|
54
|
+
|
|
55
|
+
auto result = Napi::Array::New(env);
|
|
56
|
+
size_t result_count = 0;
|
|
57
|
+
|
|
58
|
+
for (const auto &match : matches) {
|
|
59
|
+
auto obj = Napi::Object::New(env);
|
|
60
|
+
obj.Set(idKey, Napi::Number::New(env, match.id));
|
|
61
|
+
obj.Set(scoreKey, Napi::Number::New(env, match.score));
|
|
62
|
+
obj.Set(valueKey, Napi::String::New(env, *match.value));
|
|
63
|
+
|
|
64
|
+
if (match.matchIndexes != nullptr) {
|
|
65
|
+
Napi::Array array = Napi::Array::New(env, match.matchIndexes->size());
|
|
66
|
+
for (size_t i = 0; i < array.Length(); i++) {
|
|
67
|
+
array.Set(i, Napi::Number::New(env, match.matchIndexes->at(i)));
|
|
68
|
+
}
|
|
69
|
+
obj.Set(matchIndexesKey, array);
|
|
70
|
+
}
|
|
71
|
+
result.Set(result_count++, obj);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void Matcher::AddCandidates(const Napi::CallbackInfo &info)
|
|
77
|
+
{
|
|
78
|
+
auto env = info.Env();
|
|
79
|
+
if (info.Length() > 0) {
|
|
80
|
+
CHECK(env, info[0].IsArray(), "Expected an array of unsigned 32-bit integer ids as the first argument");
|
|
81
|
+
CHECK(env, info[1].IsArray(), "Expected an array of strings as the second argument");
|
|
82
|
+
|
|
83
|
+
auto ids = info[0].As<Napi::Array>();
|
|
84
|
+
auto values = info[1].As<Napi::Array>();
|
|
85
|
+
|
|
86
|
+
CHECK(env, ids.Length() == values.Length(), "Expected ids array and values array to have the same length");
|
|
87
|
+
|
|
88
|
+
// Create a random permutation so that candidates are shuffled.
|
|
89
|
+
std::vector<size_t> indexes(ids.Length());
|
|
90
|
+
for (size_t i = 0; i < indexes.size(); i++)
|
|
91
|
+
{
|
|
92
|
+
indexes[i] = i;
|
|
93
|
+
if (i > 0)
|
|
94
|
+
{
|
|
95
|
+
std::swap(indexes[rand() % i], indexes[i]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this->_impl.reserve(this->_impl.size() + ids.Length());
|
|
99
|
+
for (auto i : indexes) {
|
|
100
|
+
auto id_value = ids.Get(i);
|
|
101
|
+
double raw = id_value.As<Napi::Number>().DoubleValue();
|
|
102
|
+
CHECK(env, raw >= 0 && raw == std::floor(raw), "Expected first array to only contain unsigned 32-bit integer ids");
|
|
103
|
+
uint32_t id = id_value.As<Napi::Number>().Uint32Value();
|
|
104
|
+
auto value = values.Get(i).As<Napi::String>();
|
|
105
|
+
this->_impl.addCandidate(id, value.Utf8Value());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
void Matcher::RemoveCandidates(const Napi::CallbackInfo &info)
|
|
111
|
+
{
|
|
112
|
+
auto env = info.Env();
|
|
113
|
+
if (info.Length() > 0) {
|
|
114
|
+
CHECK(env, info[0].IsArray(), "Expected an array of unsigned 32-bit integer ids");
|
|
115
|
+
auto ids = info[0].As<Napi::Array>();
|
|
116
|
+
for (size_t i = 0; i < ids.Length(); i++) {
|
|
117
|
+
auto id_value = ids.Get(i);
|
|
118
|
+
double raw = id_value.As<Napi::Number>().DoubleValue();
|
|
119
|
+
CHECK(env, raw >= 0 && raw == std::floor(raw), "Expected array to only contain unsigned 32-bit integer ids");
|
|
120
|
+
uint32_t id = id_value.As<Napi::Number>().Uint32Value();
|
|
121
|
+
this->_impl.removeCandidate(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
void Matcher::SetCandidates(const Napi::CallbackInfo &info)
|
|
127
|
+
{
|
|
128
|
+
this->_impl.clear();
|
|
129
|
+
AddCandidates(info);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Napi::FunctionReference Matcher::constructor;
|
|
133
|
+
|
|
134
|
+
Napi::Function Matcher::GetClass(Napi::Env env) {
|
|
135
|
+
Napi::Function func = DefineClass(
|
|
136
|
+
env, "Matcher",
|
|
137
|
+
{InstanceMethod("match", &Matcher::Match),
|
|
138
|
+
InstanceMethod("addCandidates", &Matcher::AddCandidates),
|
|
139
|
+
InstanceMethod("removeCandidates", &Matcher::RemoveCandidates),
|
|
140
|
+
InstanceMethod("setCandidates", &Matcher::SetCandidates)});
|
|
141
|
+
constructor = Napi::Persistent(func);
|
|
142
|
+
constructor.SuppressDestruct();
|
|
143
|
+
|
|
144
|
+
return func;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports)
|
|
148
|
+
{
|
|
149
|
+
Napi::String matcher = Napi::String::New(env, "Matcher");
|
|
150
|
+
exports.Set(matcher, Matcher::GetClass(env));
|
|
151
|
+
|
|
152
|
+
return exports;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
NODE_API_MODULE(addon, Init)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <napi.h>
|
|
4
|
+
#include "MatcherBase.h"
|
|
5
|
+
|
|
6
|
+
class Matcher : public Napi::ObjectWrap<Matcher>
|
|
7
|
+
{
|
|
8
|
+
private:
|
|
9
|
+
static Napi::FunctionReference constructor;
|
|
10
|
+
MatcherBase _impl;
|
|
11
|
+
|
|
12
|
+
public:
|
|
13
|
+
Matcher(const Napi::CallbackInfo &info);
|
|
14
|
+
Napi::Value Match(const Napi::CallbackInfo &info);
|
|
15
|
+
void AddCandidates(const Napi::CallbackInfo &info);
|
|
16
|
+
void RemoveCandidates(const Napi::CallbackInfo &info);
|
|
17
|
+
void SetCandidates(const Napi::CallbackInfo &info);
|
|
18
|
+
|
|
19
|
+
static Napi::Function GetClass(Napi::Env);
|
|
20
|
+
};
|
package/src/binding.cpp
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
#include <nan.h>
|
|
2
|
-
#include <vector>
|
|
3
|
-
#include <unordered_map>
|
|
4
|
-
#include <chrono>
|
|
5
|
-
|
|
6
|
-
#include "MatcherBase.h"
|
|
7
|
-
|
|
8
|
-
using namespace Nan;
|
|
9
|
-
|
|
10
|
-
#define CHECK(cond, msg) \
|
|
11
|
-
if (!(cond)) \
|
|
12
|
-
{ \
|
|
13
|
-
ThrowTypeError(msg); \
|
|
14
|
-
return; \
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
template <typename T>
|
|
18
|
-
T get_property(const v8::Local<v8::Object> &object, const char *name)
|
|
19
|
-
{
|
|
20
|
-
auto prop = Nan::Get(object, Nan::New(name).ToLocalChecked());
|
|
21
|
-
if (prop.IsEmpty())
|
|
22
|
-
{
|
|
23
|
-
return T();
|
|
24
|
-
}
|
|
25
|
-
Nan::Maybe<T> result = Nan::To<T>(prop.ToLocalChecked());
|
|
26
|
-
if (result.IsNothing())
|
|
27
|
-
{
|
|
28
|
-
return T();
|
|
29
|
-
}
|
|
30
|
-
return result.FromJust();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* This saves one string copy over using v8::String::Utf8Value.
|
|
35
|
-
*/
|
|
36
|
-
std::string to_std_string(const v8::Local<v8::String> &v8str)
|
|
37
|
-
{
|
|
38
|
-
v8::Isolate *isolate = v8::Isolate::GetCurrent();
|
|
39
|
-
std::string str(v8str->Utf8Length(isolate), ' ');
|
|
40
|
-
v8str->WriteUtf8(isolate, &str[0]);
|
|
41
|
-
return str;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
std::string get_string_property(const v8::Local<v8::Object> &object,
|
|
45
|
-
const char *name)
|
|
46
|
-
{
|
|
47
|
-
auto prop =
|
|
48
|
-
Nan::Get(object, Nan::New(name).ToLocalChecked());
|
|
49
|
-
if (!prop.IsEmpty())
|
|
50
|
-
{
|
|
51
|
-
auto propLocal = prop.ToLocalChecked();
|
|
52
|
-
if (propLocal->IsNull() || propLocal->IsUndefined())
|
|
53
|
-
{
|
|
54
|
-
return std::string("");
|
|
55
|
-
}
|
|
56
|
-
if (!propLocal->IsString())
|
|
57
|
-
{
|
|
58
|
-
std::string msg =
|
|
59
|
-
std::string("property ") +
|
|
60
|
-
name +
|
|
61
|
-
std::string(" must be a string");
|
|
62
|
-
ThrowTypeError(msg.c_str());
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return to_std_string(Nan::To<v8::String>(propLocal).ToLocalChecked());
|
|
66
|
-
}
|
|
67
|
-
return std::string("");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
Persistent<v8::Function> MatcherConstructor;
|
|
71
|
-
|
|
72
|
-
class Matcher : public ObjectWrap
|
|
73
|
-
{
|
|
74
|
-
public:
|
|
75
|
-
static void Init(v8::Local<v8::Object> exports)
|
|
76
|
-
{
|
|
77
|
-
// Prepare constructor template
|
|
78
|
-
v8::Local<v8::FunctionTemplate> tpl =
|
|
79
|
-
New<v8::FunctionTemplate>(Matcher::Create);
|
|
80
|
-
tpl->SetClassName(New("Matcher").ToLocalChecked());
|
|
81
|
-
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
82
|
-
|
|
83
|
-
// Prototype
|
|
84
|
-
SetPrototypeMethod(tpl, "match", Match);
|
|
85
|
-
SetPrototypeMethod(tpl, "addCandidates", AddCandidates);
|
|
86
|
-
SetPrototypeMethod(tpl, "removeCandidates", RemoveCandidates);
|
|
87
|
-
SetPrototypeMethod(tpl, "setCandidates", SetCandidates);
|
|
88
|
-
|
|
89
|
-
v8::Local<v8::Context> context = Nan::GetCurrentContext();
|
|
90
|
-
|
|
91
|
-
MatcherConstructor.Reset(tpl->GetFunction(context).ToLocalChecked());
|
|
92
|
-
Set(exports, Nan::New("Matcher").ToLocalChecked(), tpl->GetFunction(context).ToLocalChecked());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
static void Create(const Nan::FunctionCallbackInfo<v8::Value> &info)
|
|
96
|
-
{
|
|
97
|
-
CHECK(info.IsConstructCall(), "Use 'new' to construct Matcher");
|
|
98
|
-
auto obj = new Matcher();
|
|
99
|
-
obj->Wrap(info.This());
|
|
100
|
-
AddCandidates(info);
|
|
101
|
-
info.GetReturnValue().Set(info.This());
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
static void Match(const FunctionCallbackInfo<v8::Value> &info)
|
|
105
|
-
{
|
|
106
|
-
if (info.Length() < 1)
|
|
107
|
-
{
|
|
108
|
-
Nan::ThrowTypeError("Wrong number of arguments");
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
CHECK(info[0]->IsString(), "First argument should be a query string");
|
|
113
|
-
std::string query(to_std_string(Nan::To<v8::String>(info[0]).ToLocalChecked()));
|
|
114
|
-
|
|
115
|
-
MatcherOptions options;
|
|
116
|
-
if (info.Length() > 1)
|
|
117
|
-
{
|
|
118
|
-
CHECK(info[1]->IsObject(), "Second argument should be an options object");
|
|
119
|
-
auto options_obj = Nan::To<v8::Object>(info[1]).ToLocalChecked();
|
|
120
|
-
options.case_sensitive = get_property<bool>(options_obj, "caseSensitive");
|
|
121
|
-
options.smart_case = get_property<bool>(options_obj, "smartCase");
|
|
122
|
-
options.num_threads = get_property<int>(options_obj, "numThreads");
|
|
123
|
-
options.max_results = get_property<int>(options_obj, "maxResults");
|
|
124
|
-
options.max_gap = get_property<int>(options_obj, "maxGap");
|
|
125
|
-
options.record_match_indexes =
|
|
126
|
-
get_property<bool>(options_obj, "recordMatchIndexes");
|
|
127
|
-
options.fuzzaldrin =
|
|
128
|
-
get_string_property(options_obj, "algorithm") == "fuzzaldrin";
|
|
129
|
-
options.root_path = get_string_property(options_obj, "rootPath");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
auto idKey = New("id").ToLocalChecked();
|
|
133
|
-
auto valueKey = New("value").ToLocalChecked();
|
|
134
|
-
auto scoreKey = New("score").ToLocalChecked();
|
|
135
|
-
auto matchIndexesKey = New("matchIndexes").ToLocalChecked();
|
|
136
|
-
|
|
137
|
-
auto matcher = Unwrap<Matcher>(info.This());
|
|
138
|
-
std::vector<MatchResult> matches =
|
|
139
|
-
matcher->impl_.findMatches(query, options);
|
|
140
|
-
auto result = New<v8::Array>();
|
|
141
|
-
size_t result_count = 0;
|
|
142
|
-
for (const auto &match : matches)
|
|
143
|
-
{
|
|
144
|
-
auto obj = New<v8::Object>();
|
|
145
|
-
Set(obj, idKey, New<v8::Uint32>(match.id));
|
|
146
|
-
Set(obj, scoreKey, New(match.score));
|
|
147
|
-
Set(obj, valueKey, New(*match.value).ToLocalChecked());
|
|
148
|
-
|
|
149
|
-
v8::Local<v8::Context> context = Nan::GetCurrentContext();
|
|
150
|
-
if (match.matchIndexes != nullptr)
|
|
151
|
-
{
|
|
152
|
-
auto array = New<v8::Array>(match.matchIndexes->size());
|
|
153
|
-
for (size_t i = 0; i < array->Length(); i++)
|
|
154
|
-
{
|
|
155
|
-
array->Set(context, i, New(match.matchIndexes->at(i)));
|
|
156
|
-
}
|
|
157
|
-
Set(obj, matchIndexesKey, array);
|
|
158
|
-
}
|
|
159
|
-
result->Set(context, result_count++, obj);
|
|
160
|
-
}
|
|
161
|
-
info.GetReturnValue().Set(result);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
static void AddCandidates(const FunctionCallbackInfo<v8::Value> &info)
|
|
165
|
-
{
|
|
166
|
-
auto matcher = Unwrap<Matcher>(info.This());
|
|
167
|
-
if (info.Length() > 0)
|
|
168
|
-
{
|
|
169
|
-
CHECK(info[0]->IsArray(), "Expected an array of unsigned 32-bit integer ids as the first argument");
|
|
170
|
-
CHECK(info[1]->IsArray(), "Expected an array of strings as the second argument");
|
|
171
|
-
|
|
172
|
-
auto ids = v8::Local<v8::Array>::Cast(info[0]);
|
|
173
|
-
auto values = v8::Local<v8::Array>::Cast(info[1]);
|
|
174
|
-
|
|
175
|
-
CHECK(ids->Length() == values->Length(), "Expected ids array and values array to have the same length");
|
|
176
|
-
|
|
177
|
-
// Create a random permutation so that candidates are shuffled.
|
|
178
|
-
std::vector<size_t> indexes(ids->Length());
|
|
179
|
-
for (size_t i = 0; i < indexes.size(); i++)
|
|
180
|
-
{
|
|
181
|
-
indexes[i] = i;
|
|
182
|
-
if (i > 0)
|
|
183
|
-
{
|
|
184
|
-
std::swap(indexes[rand() % i], indexes[i]);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
matcher->impl_.reserve(matcher->impl_.size() + ids->Length());
|
|
188
|
-
|
|
189
|
-
v8::Local<v8::Context> context = Nan::GetCurrentContext();
|
|
190
|
-
for (auto i : indexes)
|
|
191
|
-
{
|
|
192
|
-
auto id_value = ids->Get(context, i).ToLocalChecked();
|
|
193
|
-
CHECK(id_value->IsUint32(), "Expected first array to only contain unsigned 32-bit integer ids");
|
|
194
|
-
auto id = v8::Local<v8::Uint32>::Cast(id_value)->Value();
|
|
195
|
-
auto value = to_std_string(Nan::To<v8::String>(values->Get(context, i).ToLocalChecked()).ToLocalChecked());
|
|
196
|
-
matcher->impl_.addCandidate(id, value);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
static void RemoveCandidates(const FunctionCallbackInfo<v8::Value> &info)
|
|
202
|
-
{
|
|
203
|
-
auto matcher = Unwrap<Matcher>(info.This());
|
|
204
|
-
if (info.Length() > 0)
|
|
205
|
-
{
|
|
206
|
-
CHECK(info[0]->IsArray(), "Expected an array of unsigned 32-bit integer ids");
|
|
207
|
-
auto ids = v8::Local<v8::Array>::Cast(info[0]);
|
|
208
|
-
|
|
209
|
-
v8::Local<v8::Context> context = Nan::GetCurrentContext();
|
|
210
|
-
for (size_t i = 0; i < ids->Length(); i++)
|
|
211
|
-
{
|
|
212
|
-
auto id_value = ids->Get(context, i).ToLocalChecked();
|
|
213
|
-
CHECK(id_value->IsUint32(), "Expected array to only contain unsigned 32-bit integer ids");
|
|
214
|
-
auto id = v8::Local<v8::Uint32>::Cast(id_value)->Value();
|
|
215
|
-
matcher->impl_.removeCandidate(id);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
static void SetCandidates(const FunctionCallbackInfo<v8::Value> &info)
|
|
221
|
-
{
|
|
222
|
-
auto matcher = Unwrap<Matcher>(info.This());
|
|
223
|
-
matcher->impl_.clear();
|
|
224
|
-
AddCandidates(info);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private:
|
|
228
|
-
MatcherBase impl_;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
void Init(v8::Local<v8::Object> exports) { Matcher::Init(exports); }
|
|
232
|
-
|
|
233
|
-
NAN_MODULE_WORKER_ENABLED(addon, Init)
|