natural 0.5.0 → 0.5.1
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/.travis.yml +1 -2
- package/.vscode/temp.sql +0 -0
- package/README.md +18 -6
- package/bayes_classifier.json +1 -0
- package/lib/natural/classifiers/bayes_classifier.js +1 -1
- package/lib/natural/classifiers/classifier.js +268 -7
- package/lib/natural/distance/jaro-winkler_distance.js +60 -59
- package/lib/natural/inflectors/noun_inflector.js +1 -0
- package/lib/natural/phonetics/double_metaphone.js +42 -42
- package/lr_classifier.json +1 -0
- package/package.json +6 -4
package/.travis.yml
CHANGED
package/.vscode/temp.sql
ADDED
|
File without changes
|
package/README.md
CHANGED
|
@@ -289,6 +289,15 @@ var restoredClassifier = natural.BayesClassifier.restore(JSON.parse(raw));
|
|
|
289
289
|
console.log(restoredClassifier.classify('i should sell that'));
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
+
__Note:__ if using the classifier for languages other than English you may need
|
|
293
|
+
to pass in the stemmer to use. In fact, you can do this for any stemmer including
|
|
294
|
+
alternate English stemmers. The default is the `PorterStemmer`.
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
const PorterStemmerRu = require('./node_modules/natural/lib/natural/stemmers/porter_stemmer_ru');
|
|
298
|
+
var classifier = new natural.BayesClassifier(PorterStemmerRu);
|
|
299
|
+
```
|
|
300
|
+
|
|
292
301
|
## Phonetics
|
|
293
302
|
|
|
294
303
|
Phonetic matching (sounds-like) matching can be done with the [SoundEx](http://en.wikipedia.org/wiki/Soundex),
|
|
@@ -954,7 +963,7 @@ This is best constructed with an array of tokens from a corpus, but a simple lis
|
|
|
954
963
|
|
|
955
964
|
```javascript
|
|
956
965
|
var corpus = ['something', 'soothing'];
|
|
957
|
-
var spellcheck = new Spellcheck(corpus);
|
|
966
|
+
var spellcheck = new natural.Spellcheck(corpus);
|
|
958
967
|
```
|
|
959
968
|
|
|
960
969
|
It uses the trie datastructure for fast boolean lookup of a word
|
|
@@ -977,19 +986,22 @@ algorithm. Transformation rules are specified in external files.
|
|
|
977
986
|
|
|
978
987
|
### Usage
|
|
979
988
|
```javascript
|
|
980
|
-
var natural = require("
|
|
989
|
+
var natural = require("natural");
|
|
990
|
+
var path = require("path");
|
|
981
991
|
|
|
982
|
-
var base_folder = "
|
|
983
|
-
var rulesFilename = base_folder + "/data/tr_from_posjs.txt";
|
|
984
|
-
var lexiconFilename = base_folder + "/data/lexicon_from_posjs.json";
|
|
992
|
+
var base_folder = path.join(path.dirname(require.resolve("natural")), "brill_pos_tagger");
|
|
993
|
+
var rulesFilename = base_folder + "/data/English/tr_from_posjs.txt";
|
|
994
|
+
var lexiconFilename = base_folder + "/data/English/lexicon_from_posjs.json";
|
|
985
995
|
var defaultCategory = 'N';
|
|
986
996
|
|
|
987
997
|
var lexicon = new natural.Lexicon(lexiconFilename, defaultCategory);
|
|
988
|
-
var rules = new natural.
|
|
998
|
+
var rules = new natural.RuleSet(rulesFilename);
|
|
989
999
|
var tagger = new natural.BrillPOSTagger(lexicon, rules);
|
|
990
1000
|
|
|
991
1001
|
var sentence = ["I", "see", "the", "man", "with", "the", "telescope"];
|
|
992
1002
|
console.log(JSON.stringify(tagger.tag(sentence)));
|
|
1003
|
+
// [["I","NN"],["see","VB"],["the","DT"],["man","NN"],["with","IN"],["the","DT"],["telescope","NN"]]
|
|
1004
|
+
|
|
993
1005
|
```
|
|
994
1006
|
|
|
995
1007
|
### Lexicon
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"classifier":{"classFeatures":{"computing":{"0":2,"1":2,"2":2,"3":3,"4":2,"5":2},"literature":{"2":2,"6":4,"7":2,"8":2}},"classTotals":{"computing":4,"literature":4},"totalExamples":7,"smoothing":1},"docs":[{"label":"computing","text":["fix","box"]},{"label":"computing","text":["write","code"]},{"label":"computing","text":["nasti","script","code"]},{"label":"literature","text":["write","book"]},{"label":"literature","text":["read","book"]},{"label":"literature","text":["studi","book"]}],"features":{"fix":1,"box":1,"write":2,"code":2,"nasti":1,"script":1,"book":3,"read":1,"studi":1},"stemmer":{},"lastAdded":6,"events":{"domain":null,"_events":{},"_eventsCount":0}}
|
|
@@ -45,7 +45,7 @@ function restore(classifier, stemmer) {
|
|
|
45
45
|
function load(filename, stemmer, callback) {
|
|
46
46
|
Classifier.load(filename, function(err, classifier) {
|
|
47
47
|
if (err) {
|
|
48
|
-
callback(err);
|
|
48
|
+
return callback(err);
|
|
49
49
|
}
|
|
50
50
|
else {
|
|
51
51
|
callback(err, restore(classifier, stemmer));
|
|
@@ -21,8 +21,10 @@ THE SOFTWARE.
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
var PorterStemmer = require('../stemmers/porter_stemmer'),
|
|
24
|
+
Threads = require('webworker-threads'),
|
|
24
25
|
util = require('util'),
|
|
25
|
-
events = require('events')
|
|
26
|
+
events = require('events')
|
|
27
|
+
os = require('os');
|
|
26
28
|
|
|
27
29
|
var Classifier = function(classifier, stemmer) {
|
|
28
30
|
this.classifier = classifier;
|
|
@@ -34,15 +36,15 @@ var Classifier = function(classifier, stemmer) {
|
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
function addDocument(text, classification) {
|
|
37
|
-
|
|
38
|
-
// Ignore further processing if classification is undefined
|
|
39
|
+
|
|
40
|
+
// Ignore further processing if classification is undefined
|
|
39
41
|
if(typeof classification === 'undefined') return;
|
|
40
|
-
|
|
41
|
-
// If classification is type of string then make sure it's dosen't have blank space at both end
|
|
42
|
+
|
|
43
|
+
// If classification is type of string then make sure it's dosen't have blank space at both end
|
|
42
44
|
if(typeof classification === 'string'){
|
|
43
45
|
classification = classification.trim();
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
if(typeof text === 'string')
|
|
47
49
|
text = this.stemmer.tokenizeAndStem(text);
|
|
48
50
|
|
|
@@ -105,6 +107,28 @@ function textToFeatures(observation) {
|
|
|
105
107
|
return features;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
function docsToFeatures(docs) {
|
|
111
|
+
var parsedDocs = [];
|
|
112
|
+
|
|
113
|
+
for (var i = 0; i < docs.length; i++) {
|
|
114
|
+
var features = [];
|
|
115
|
+
|
|
116
|
+
for (var feature in FEATURES) {
|
|
117
|
+
if (docs[i].observation.indexOf(feature) > -1)
|
|
118
|
+
features.push(1);
|
|
119
|
+
else
|
|
120
|
+
features.push(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
parsedDocs.push({
|
|
124
|
+
index: docs[i].index,
|
|
125
|
+
features: features
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return JSON.stringify(parsedDocs);
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
function train() {
|
|
109
133
|
var totalDocs = this.docs.length;
|
|
110
134
|
for(var i = this.lastAdded; i < totalDocs; i++) {
|
|
@@ -117,12 +141,246 @@ function train() {
|
|
|
117
141
|
this.classifier.train();
|
|
118
142
|
}
|
|
119
143
|
|
|
144
|
+
function trainParallel(numThreads, callback) {
|
|
145
|
+
if (!callback) {
|
|
146
|
+
callback = numThreads;
|
|
147
|
+
numThreads = undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (isNaN(numThreads)) {
|
|
151
|
+
numThreads = os.cpus().length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var totalDocs = this.docs.length;
|
|
155
|
+
var threadPool = Threads.createPool(numThreads);
|
|
156
|
+
var docFeatures = {};
|
|
157
|
+
var finished = 0;
|
|
158
|
+
var self = this;
|
|
159
|
+
|
|
160
|
+
// Init pool; send the features array and the parsing function
|
|
161
|
+
threadPool.all.eval('var FEATURES = ' + JSON.stringify(this.features));
|
|
162
|
+
threadPool.all.eval(docsToFeatures);
|
|
163
|
+
|
|
164
|
+
// Convert docs to observation objects
|
|
165
|
+
var obsDocs = [];
|
|
166
|
+
for (var i = this.lastAdded; i < totalDocs; i++) {
|
|
167
|
+
var observation = this.docs[i].text;
|
|
168
|
+
if (typeof observation === 'string')
|
|
169
|
+
observation = this.stemmer.tokenizeAndStem(observation);
|
|
170
|
+
obsDocs.push({
|
|
171
|
+
index: i,
|
|
172
|
+
observation: observation
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Called when a batch completes processing
|
|
177
|
+
var onFeaturesResult = function(docs) {
|
|
178
|
+
setTimeout(function() {
|
|
179
|
+
self.events.emit('processedBatch', {
|
|
180
|
+
size: docs.length,
|
|
181
|
+
docs: totalDocs,
|
|
182
|
+
batches: numThreads,
|
|
183
|
+
index: finished
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
for (var j = 0; j < docs.length; j++) {
|
|
188
|
+
docFeatures[docs[j].index] = docs[j].features;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Called when all batches finish processing
|
|
193
|
+
var onFinished = function(err) {
|
|
194
|
+
if (err) {
|
|
195
|
+
threadPool.destroy();
|
|
196
|
+
return callback(err);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (var j = self.lastAdded; j < totalDocs; j++) {
|
|
200
|
+
self.classifier.addExample(docFeatures[j], self.docs[j].label);
|
|
201
|
+
self.events.emit('trainedWithDocument', {
|
|
202
|
+
index: j,
|
|
203
|
+
total: totalDocs,
|
|
204
|
+
doc: self.docs[j]
|
|
205
|
+
});
|
|
206
|
+
self.lastAdded++;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
self.events.emit('doneTraining', true);
|
|
210
|
+
self.classifier.train();
|
|
211
|
+
|
|
212
|
+
threadPool.destroy();
|
|
213
|
+
callback(null);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Split the docs and start processing
|
|
217
|
+
var batchSize = Math.ceil(obsDocs.length / numThreads);
|
|
218
|
+
var lastError;
|
|
219
|
+
|
|
220
|
+
for (var i = 0; i < numThreads; i++) {
|
|
221
|
+
var batchDocs = obsDocs.slice(i * batchSize, (i+1) * batchSize);
|
|
222
|
+
var batchJson = JSON.stringify(batchDocs);
|
|
223
|
+
|
|
224
|
+
threadPool.any.eval('docsToFeatures(' + batchJson + ')', function(err, docs) {
|
|
225
|
+
lastError = err || lastError;
|
|
226
|
+
finished++;
|
|
227
|
+
|
|
228
|
+
if (docs) {
|
|
229
|
+
docs = JSON.parse(docs);
|
|
230
|
+
onFeaturesResult(docs);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (finished >= numThreads) {
|
|
234
|
+
onFinished(lastError);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function trainParallelBatches(options) {
|
|
241
|
+
var numThreads = options && options.numThreads;
|
|
242
|
+
var batchSize = options && options.batchSize;
|
|
243
|
+
|
|
244
|
+
if (isNaN(numThreads)) {
|
|
245
|
+
numThreads = os.cpus().length;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isNaN(batchSize)) {
|
|
249
|
+
batchSize = 2500;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
var totalDocs = this.docs.length;
|
|
253
|
+
var threadPool = Threads.createPool(numThreads);
|
|
254
|
+
var docFeatures = {};
|
|
255
|
+
var finished = 0;
|
|
256
|
+
var self = this;
|
|
257
|
+
|
|
258
|
+
var abort = false;
|
|
259
|
+
var onError = function(err) {
|
|
260
|
+
if (!err || abort) return;
|
|
261
|
+
abort = true;
|
|
262
|
+
threadPool.destroy(true);
|
|
263
|
+
self.events.emit('doneTrainingError', err);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Init pool; send the features array and the parsing function
|
|
267
|
+
var str = JSON.stringify(this.features);
|
|
268
|
+
threadPool.all.eval('var FEATURES = ' + str + ';', onError);
|
|
269
|
+
threadPool.all.eval(docsToFeatures, onError);
|
|
270
|
+
|
|
271
|
+
// Convert docs to observation objects
|
|
272
|
+
var obsDocs = [];
|
|
273
|
+
for (var i = this.lastAdded; i < totalDocs; i++) {
|
|
274
|
+
var observation = this.docs[i].text;
|
|
275
|
+
if (typeof observation === 'string')
|
|
276
|
+
observation = this.stemmer.tokenizeAndStem(observation);
|
|
277
|
+
obsDocs.push({
|
|
278
|
+
index: i,
|
|
279
|
+
observation: observation
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Split the docs in batches
|
|
284
|
+
var obsBatches = [];
|
|
285
|
+
var i = 0;
|
|
286
|
+
while (true) {
|
|
287
|
+
var batch = obsDocs.slice(i * batchSize, (i+1) * batchSize);
|
|
288
|
+
if (!batch || !batch.length) break;
|
|
289
|
+
obsBatches.push(batch);
|
|
290
|
+
i++;
|
|
291
|
+
}
|
|
292
|
+
obsDocs = null;
|
|
293
|
+
self.events.emit('startedTraining', {
|
|
294
|
+
docs: totalDocs,
|
|
295
|
+
batches: obsBatches.length
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Called when a batch completes processing
|
|
299
|
+
var onFeaturesResult = function(docs) {
|
|
300
|
+
self.events.emit('processedBatch', {
|
|
301
|
+
size: docs.length,
|
|
302
|
+
docs: totalDocs,
|
|
303
|
+
batches: obsBatches.length,
|
|
304
|
+
index: finished
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
for (var j = 0; j < docs.length; j++) {
|
|
308
|
+
docFeatures[docs[j].index] = docs[j].features;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Called when all batches finish processing
|
|
313
|
+
var onFinished = function() {
|
|
314
|
+
threadPool.destroy(true);
|
|
315
|
+
abort = true;
|
|
316
|
+
|
|
317
|
+
for (var j = self.lastAdded; j < totalDocs; j++) {
|
|
318
|
+
self.classifier.addExample(docFeatures[j], self.docs[j].label);
|
|
319
|
+
self.events.emit('trainedWithDocument', {
|
|
320
|
+
index: j,
|
|
321
|
+
total: totalDocs,
|
|
322
|
+
doc: self.docs[j]
|
|
323
|
+
});
|
|
324
|
+
self.lastAdded++;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
self.events.emit('doneTraining', true);
|
|
328
|
+
self.classifier.train();
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Called to send the next batch to be processed
|
|
332
|
+
var batchIndex = 0;
|
|
333
|
+
var sendNext = function() {
|
|
334
|
+
if (abort) return;
|
|
335
|
+
if (batchIndex >= obsBatches.length) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
sendBatch(JSON.stringify(obsBatches[batchIndex]));
|
|
340
|
+
batchIndex++;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Called to send a batch of docs to the threads
|
|
344
|
+
var sendBatch = function(batchJson) {
|
|
345
|
+
if (abort) return;
|
|
346
|
+
threadPool.any.eval('docsToFeatures(' + batchJson + ');', function(err, docs) {
|
|
347
|
+
if (err) {
|
|
348
|
+
return onError(err);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
finished++;
|
|
352
|
+
|
|
353
|
+
if (docs) {
|
|
354
|
+
docs = JSON.parse(docs);
|
|
355
|
+
setTimeout(onFeaturesResult.bind(null, docs));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (finished >= obsBatches.length) {
|
|
359
|
+
setTimeout(onFinished);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setTimeout(sendNext);
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Start processing
|
|
367
|
+
for (var i = 0; i < numThreads; i++) {
|
|
368
|
+
sendNext();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
120
372
|
function retrain() {
|
|
121
373
|
this.classifier = new (this.classifier.constructor)();
|
|
122
374
|
this.lastAdded = 0;
|
|
123
375
|
this.train();
|
|
124
376
|
}
|
|
125
377
|
|
|
378
|
+
function retrainParallel(numThreads, callback) {
|
|
379
|
+
this.classifier = new (this.classifier.constructor)();
|
|
380
|
+
this.lastAdded = 0;
|
|
381
|
+
this.trainParallel(numThreads, callback);
|
|
382
|
+
}
|
|
383
|
+
|
|
126
384
|
function getClassifications(observation) {
|
|
127
385
|
return this.classifier.getClassifications(this.textToFeatures(observation));
|
|
128
386
|
}
|
|
@@ -153,7 +411,7 @@ function load(filename, callback) {
|
|
|
153
411
|
|
|
154
412
|
fs.readFile(filename, 'utf8', function(err, data) {
|
|
155
413
|
var classifier;
|
|
156
|
-
|
|
414
|
+
|
|
157
415
|
if(!err) {
|
|
158
416
|
classifier = JSON.parse(data);
|
|
159
417
|
}
|
|
@@ -166,7 +424,10 @@ function load(filename, callback) {
|
|
|
166
424
|
Classifier.prototype.addDocument = addDocument;
|
|
167
425
|
Classifier.prototype.removeDocument = removeDocument;
|
|
168
426
|
Classifier.prototype.train = train;
|
|
427
|
+
Classifier.prototype.trainParallel = trainParallel;
|
|
428
|
+
Classifier.prototype.trainParallelBatches = trainParallelBatches;
|
|
169
429
|
Classifier.prototype.retrain = retrain;
|
|
430
|
+
Classifier.prototype.retrainParallel = retrainParallel;
|
|
170
431
|
Classifier.prototype.classify = classify;
|
|
171
432
|
Classifier.prototype.textToFeatures = textToFeatures;
|
|
172
433
|
Classifier.prototype.save = save;
|
|
@@ -27,70 +27,71 @@ THE SOFTWARE.
|
|
|
27
27
|
// s1 is the first string to compare
|
|
28
28
|
// s2 is the second string to compare
|
|
29
29
|
function distance(s1, s2) {
|
|
30
|
-
if (typeof(s1)
|
|
31
|
-
if (s1.length == 0 || s2.length == 0)
|
|
30
|
+
if (typeof(s1) !== "string" || typeof(s2) !== "string") {
|
|
32
31
|
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (s1.length === 0 || s2.length === 0) {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
s1 = s1.toLowerCase(), s2 = s2.toLowerCase();
|
|
39
|
+
|
|
34
40
|
var matchWindow = (Math.floor(Math.max(s1.length, s2.length) / 2.0)) - 1;
|
|
35
41
|
var matches1 = new Array(s1.length);
|
|
36
42
|
var matches2 = new Array(s2.length);
|
|
37
43
|
var m = 0; // number of matches
|
|
38
44
|
var t = 0; // number of transpositions
|
|
45
|
+
var i = 0; // index for string 1
|
|
46
|
+
var k = 0; // index for string 2
|
|
39
47
|
|
|
40
48
|
//debug helpers
|
|
41
49
|
//console.log("s1: " + s1 + "; s2: " + s2);
|
|
42
50
|
//console.log(" - matchWindow: " + matchWindow);
|
|
43
51
|
|
|
44
|
-
// find
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// check for an exact match
|
|
49
|
-
if (s1[i] == s2[i]) {
|
|
50
|
-
matches1[i] = matches2[i] = matched = true;
|
|
51
|
-
m++
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// check the "match window"
|
|
55
|
-
else {
|
|
56
|
-
// this for loop is a little brutal
|
|
57
|
-
for (k = (i <= matchWindow) ? 0 : i - matchWindow;
|
|
58
|
-
(k <= i + matchWindow) && k < s2.length && !matched;
|
|
59
|
-
k++) {
|
|
60
|
-
if (s1[i] == s2[k]) {
|
|
61
|
-
if(!matches1[i] && !matches2[k]) {
|
|
62
|
-
m++;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
matches1[i] = matches2[k] = matched = true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
52
|
+
for (i = 0; i < s1.length; i++) { // loop to find matched characters
|
|
53
|
+
var start = Math.max(0, (i - matchWindow)); // use the higher of the window diff
|
|
54
|
+
var end = Math.min((i + matchWindow + 1), s2.length); // use the min of the window and string 2 length
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
for (k = start; k < end; k++) { // iterate second string index
|
|
57
|
+
if (matches2[k]) { // if second string character already matched
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (s1[i] !== s2[k]) { // characters don't match
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
// assume match if the above 2 checks don't continue
|
|
65
|
+
matches1[i] = true;
|
|
66
|
+
matches2[k] = true;
|
|
67
|
+
m++;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if(s1[i] != s2[k] && k < matches2.length) {
|
|
82
|
-
t++;
|
|
83
|
-
}
|
|
72
|
+
// nothing matched
|
|
73
|
+
if (m === 0) {
|
|
74
|
+
return 0.0;
|
|
75
|
+
}
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
k = 0; // reset string 2 index
|
|
78
|
+
for(i = 0; i < s1.length; i++) { // loop to find transpositions
|
|
79
|
+
if (!matches1[i]) { // non-matching character
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
while(!matches2[k]) { // move k index to the next match
|
|
83
|
+
k++;
|
|
84
|
+
}
|
|
85
|
+
if (s1[i] !== s2[k]) { // if the characters don't match, increase transposition
|
|
86
|
+
t++;
|
|
87
|
+
}
|
|
88
|
+
k++; // iterate k index normally
|
|
87
89
|
}
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
//console.log(" - matches: " + m);
|
|
91
|
-
//console.log(" - transpositions: " + t);
|
|
90
|
+
|
|
91
|
+
// transpositions divided by 2
|
|
92
92
|
t = t / 2.0;
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
return ((m / s1.length) + (m / s2.length) + ((m - t) / m)) / 3.0;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
// Computes the Winkler distance between two string -- intrepreted from:
|
|
@@ -99,18 +100,18 @@ function distance(s1, s2) {
|
|
|
99
100
|
// s2 is the second string to compare
|
|
100
101
|
// dj is the Jaro Distance (if you've already computed it), leave blank and the method handles it
|
|
101
102
|
function JaroWinklerDistance(s1, s2, dj) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
103
|
+
if (s1 === s2) {
|
|
104
|
+
return 1;
|
|
105
|
+
} else {
|
|
106
|
+
var jaro = (typeof(dj) === 'undefined') ? distance(s1,s2) : dj;
|
|
107
|
+
var p = 0.1; // default scaling factor
|
|
108
|
+
var l = 0 // length of the matching prefix
|
|
109
|
+
while(s1[l] === s2[l] && l < 4) {
|
|
110
|
+
l++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return jaro + l * p * (1 - jaro);
|
|
114
|
+
}
|
|
115
115
|
}
|
|
116
116
|
module.exports = JaroWinklerDistance;
|
|
117
|
+
|
|
@@ -62,6 +62,7 @@ var NounInflector = function() {
|
|
|
62
62
|
this.addIrregular("tooth", "teeth");
|
|
63
63
|
this.addIrregular("goose", "geese");
|
|
64
64
|
this.addIrregular("ephemeris", "ephemerides");
|
|
65
|
+
this.addIrregular("cloth", "clothes");
|
|
65
66
|
|
|
66
67
|
// see if it is possible to unify the creation of both the singular and
|
|
67
68
|
// plural regexes or maybe even just have one list. with a complete list
|
|
@@ -32,13 +32,13 @@ function isVowel(c) {
|
|
|
32
32
|
function truncate(string, length) {
|
|
33
33
|
if(string.length >= length)
|
|
34
34
|
string = string.substring(0, length);
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
return string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function process(token, maxLength) {
|
|
40
40
|
token = token.toUpperCase();
|
|
41
|
-
var primary = '', secondary = '';
|
|
41
|
+
var primary = '', secondary = '';
|
|
42
42
|
var pos = 0;
|
|
43
43
|
maxLength == maxLength || 32;
|
|
44
44
|
|
|
@@ -67,9 +67,9 @@ function process(token, maxLength) {
|
|
|
67
67
|
|
|
68
68
|
function handleC() {
|
|
69
69
|
|
|
70
|
-
if(pos >= 1 && !isVowel(token[pos - 2])
|
|
70
|
+
if(pos >= 1 && !isVowel(token[pos - 2])
|
|
71
71
|
&& token[pos - 1] == 'A' && token[pos + 1] == 'H'
|
|
72
|
-
&&
|
|
72
|
+
&& token[pos + 2] != 'I'
|
|
73
73
|
|| subMatch(-2, 4, ['BACHER', 'MACHER'])) {
|
|
74
74
|
add('K');
|
|
75
75
|
pos++;
|
|
@@ -85,8 +85,8 @@ function process(token, maxLength) {
|
|
|
85
85
|
if(pos > 0 && token.substring(pos + 2, pos + 4) == 'AE') {
|
|
86
86
|
addSecondary('K', 'X');
|
|
87
87
|
pos++;
|
|
88
|
-
} else if(pos == 0
|
|
89
|
-
&& (subMatch(1, 6, ['HARAC', 'HARIS'])
|
|
88
|
+
} else if(pos == 0
|
|
89
|
+
&& (subMatch(1, 6, ['HARAC', 'HARIS'])
|
|
90
90
|
|| subMatch(1, 4, ['HOR', 'HUM', 'HIA', 'HEM']))
|
|
91
91
|
&& token.substring(pos + 1, pos + 5) != 'HORE') {
|
|
92
92
|
add('K');
|
|
@@ -95,7 +95,7 @@ function process(token, maxLength) {
|
|
|
95
95
|
if((subMatchAbsolute(0, 3, ['VAN', 'VON']) || token.substring(0, 3) == 'SCH')
|
|
96
96
|
|| subMatch(-2, 4, ['ORCHES', 'ARCHIT', 'ORCHID'])
|
|
97
97
|
|| subMatch(2, 3, ['T', 'S'])
|
|
98
|
-
|| ((subMatch(-1, 0, ['A', 'O', 'U', 'E']) || pos == 0)
|
|
98
|
+
|| ((subMatch(-1, 0, ['A', 'O', 'U', 'E']) || pos == 0)
|
|
99
99
|
&& subMatch(2, 3, ['B', 'F', 'H', 'L', 'M', 'N', 'R', 'V', 'W']))) {
|
|
100
100
|
add('K');
|
|
101
101
|
} else if(pos > 0) {
|
|
@@ -103,15 +103,15 @@ function process(token, maxLength) {
|
|
|
103
103
|
if(token.substring(0, 2) == 'MC') {
|
|
104
104
|
add('K');
|
|
105
105
|
} else {
|
|
106
|
-
addSecondary('X', 'K');
|
|
106
|
+
addSecondary('X', 'K');
|
|
107
107
|
}
|
|
108
108
|
} else {
|
|
109
109
|
add('X');
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
pos++;
|
|
113
|
-
}
|
|
114
|
-
} else if(token.substring(pos, pos + 2) == 'CZ'
|
|
113
|
+
}
|
|
114
|
+
} else if(token.substring(pos, pos + 2) == 'CZ'
|
|
115
115
|
&& token.substring(pos - 2, pos + 1) != 'WICZ') {
|
|
116
116
|
addSecondary('S', 'X');
|
|
117
117
|
pos++;
|
|
@@ -119,7 +119,7 @@ function process(token, maxLength) {
|
|
|
119
119
|
add('X');
|
|
120
120
|
pos += 2;
|
|
121
121
|
} else if(token[pos + 1] == 'C' && pos != 1 && token[0] != 'M') {
|
|
122
|
-
if(['I', 'E', 'H'].indexOf(token[pos + 2]) > -1
|
|
122
|
+
if(['I', 'E', 'H'].indexOf(token[pos + 2]) > -1
|
|
123
123
|
&& token.substring(pos + 2, pos + 4) != 'HU') {
|
|
124
124
|
if(pos == 1 && token[pos - 1] == 'A'
|
|
125
125
|
|| subMatch(-1, 4, ['UCCEE', 'UCCES'])) {
|
|
@@ -138,19 +138,19 @@ function process(token, maxLength) {
|
|
|
138
138
|
pos++;
|
|
139
139
|
} else if(['E', 'I', 'Y'].indexOf(token[pos + 1]) > -1) {
|
|
140
140
|
if(subMatch(1, 3, ['IA', 'IE', 'IO'])) {
|
|
141
|
-
addSecondary('S', 'X');
|
|
141
|
+
addSecondary('S', 'X');
|
|
142
142
|
} else {
|
|
143
143
|
add('S');
|
|
144
144
|
}
|
|
145
145
|
pos++;
|
|
146
|
-
} else {
|
|
146
|
+
} else {
|
|
147
147
|
add('K');
|
|
148
148
|
if(token[pos + 1] == ' ' && ['C', 'Q', 'G'].indexOf(token[pos + 2])) {
|
|
149
149
|
pos += 2;
|
|
150
150
|
} else if(['C', 'K', 'Q'].indexOf(token[pos + 1]) > -1
|
|
151
151
|
&& !subMatch(1, 3, ['CE', 'CI'])) {
|
|
152
152
|
pos++;
|
|
153
|
-
}
|
|
153
|
+
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -165,7 +165,7 @@ function process(token, maxLength) {
|
|
|
165
165
|
}
|
|
166
166
|
} else if(token[pos + 1] == 'T') {
|
|
167
167
|
add('T');
|
|
168
|
-
pos++;
|
|
168
|
+
pos++;
|
|
169
169
|
} else
|
|
170
170
|
addCompressedDouble('D', 'T');
|
|
171
171
|
}
|
|
@@ -182,7 +182,7 @@ function process(token, maxLength) {
|
|
|
182
182
|
add('K');
|
|
183
183
|
}
|
|
184
184
|
pos++;
|
|
185
|
-
} else if(pos > 1
|
|
185
|
+
} else if(pos > 1
|
|
186
186
|
&& (['B', 'H', 'D'].indexOf(token[pos - 2]) > -1
|
|
187
187
|
|| ['B', 'H', 'D'].indexOf(token[pos - 3]) > -1
|
|
188
188
|
|| ['B', 'H'].indexOf(token[pos - 4]) > -1)) {
|
|
@@ -213,7 +213,7 @@ function process(token, maxLength) {
|
|
|
213
213
|
} else if(token.substring(pos + 1, pos + 3) == 'LI' && !slavoGermanic) {
|
|
214
214
|
addSecondary('KL', 'L');
|
|
215
215
|
pos++;
|
|
216
|
-
} else if(pos == 0 && (token[pos + 1] == 'Y'
|
|
216
|
+
} else if(pos == 0 && (token[pos + 1] == 'Y'
|
|
217
217
|
|| subMatch(1, 3, ['ES', 'EP', 'EB', 'EL', 'EY', 'IB', 'IL', 'IN', 'IE', 'EI', 'ER']))) {
|
|
218
218
|
addSecondary('K', 'J')
|
|
219
219
|
} else {
|
|
@@ -226,22 +226,22 @@ function process(token, maxLength) {
|
|
|
226
226
|
if((pos == 0 || isVowel(token[pos - 1])) && isVowel(token[pos + 1])) {
|
|
227
227
|
add('H');
|
|
228
228
|
pos++;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
231
|
|
|
232
232
|
function handleJ() {
|
|
233
233
|
var jose = (token.substring(pos + 1, pos + 4) == 'OSE');
|
|
234
234
|
|
|
235
235
|
if(san || jose) {
|
|
236
|
-
if((pos == 0 && token[pos + 4] == ' ')
|
|
236
|
+
if((pos == 0 && token[pos + 4] == ' ')
|
|
237
237
|
|| san) {
|
|
238
|
-
add('H');
|
|
238
|
+
add('H');
|
|
239
239
|
} else
|
|
240
240
|
add('J', 'H');
|
|
241
241
|
} else {
|
|
242
242
|
if(pos == 0/* && !jose*/) {
|
|
243
243
|
addSecondary('J', 'A');
|
|
244
|
-
} else if(isVowel(token[pos - 1]) && !slavoGermanic
|
|
244
|
+
} else if(isVowel(token[pos - 1]) && !slavoGermanic
|
|
245
245
|
&& (token[pos + 1] == 'A' || token[pos + 1] == 'O')) {
|
|
246
246
|
addSecondary('J', 'H');
|
|
247
247
|
} else if(pos == token.length - 1) {
|
|
@@ -262,14 +262,14 @@ function process(token, maxLength) {
|
|
|
262
262
|
pos++;
|
|
263
263
|
return;
|
|
264
264
|
}
|
|
265
|
-
pos++;
|
|
265
|
+
pos++;
|
|
266
266
|
}
|
|
267
267
|
add('L');
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
function handleM() {
|
|
271
271
|
addCompressedDouble('M');
|
|
272
|
-
if(token[pos - 1] == 'U' && token[pos + 1] == 'B' &&
|
|
272
|
+
if(token[pos - 1] == 'U' && token[pos + 1] == 'B' &&
|
|
273
273
|
((pos == token.length - 2 || token.substring(pos + 2, pos + 4) == 'ER')))
|
|
274
274
|
pos++;
|
|
275
275
|
}
|
|
@@ -277,10 +277,10 @@ function process(token, maxLength) {
|
|
|
277
277
|
function handleP() {
|
|
278
278
|
if(token[pos + 1] == 'H') {
|
|
279
279
|
add('F');
|
|
280
|
-
pos++;
|
|
280
|
+
pos++;
|
|
281
281
|
} else {
|
|
282
282
|
addCompressedDouble('P');
|
|
283
|
-
|
|
283
|
+
|
|
284
284
|
if(token[pos + 1] == 'B')
|
|
285
285
|
pos++;
|
|
286
286
|
}
|
|
@@ -292,7 +292,7 @@ function process(token, maxLength) {
|
|
|
292
292
|
&& !subMatch(-4, -3, ['ME', 'MA'])) {
|
|
293
293
|
addSecondary('', 'R');
|
|
294
294
|
} else
|
|
295
|
-
addCompressedDouble('R');
|
|
295
|
+
addCompressedDouble('R');
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
function handleS() {
|
|
@@ -312,7 +312,7 @@ function process(token, maxLength) {
|
|
|
312
312
|
addSecondary('S', 'X');
|
|
313
313
|
}
|
|
314
314
|
pos++;
|
|
315
|
-
} else if((pos == 0 && ['M', 'N', 'L', 'W'].indexOf(token[pos + 1]) > -1)
|
|
315
|
+
} else if((pos == 0 && ['M', 'N', 'L', 'W'].indexOf(token[pos + 1]) > -1)
|
|
316
316
|
|| token[pos + 1] == 'Z') {
|
|
317
317
|
addSecondary('S', 'X');
|
|
318
318
|
if(token[pos + 1] == 'Z')
|
|
@@ -326,18 +326,18 @@ function process(token, maxLength) {
|
|
|
326
326
|
} else if(pos == 0 && !isVowel(token[3]) && token[3] != 'W') {
|
|
327
327
|
addSecondary('X', 'S');
|
|
328
328
|
} else {
|
|
329
|
-
add('X');
|
|
330
|
-
}
|
|
329
|
+
add('X');
|
|
330
|
+
}
|
|
331
331
|
} else if(['I', 'E', 'Y'].indexOf(token[pos + 2]) > -1) {
|
|
332
332
|
add('S');
|
|
333
333
|
} else {
|
|
334
334
|
add('SK');
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
pos += 2;
|
|
337
|
+
pos += 2;
|
|
338
338
|
} else if(pos == token.length - 1
|
|
339
339
|
&& subMatch(-2, 0, ['AI', 'OI'])) {
|
|
340
|
-
addSecondary('', 'S');
|
|
340
|
+
addSecondary('', 'S');
|
|
341
341
|
} else if(token[pos + 1] != 'L' && (
|
|
342
342
|
token[pos - 1] != 'A' && token[pos - 1] != 'I')) {
|
|
343
343
|
addCompressedDouble('S');
|
|
@@ -353,12 +353,12 @@ function process(token, maxLength) {
|
|
|
353
353
|
} else if(subMatch(1, 3, ['IA', 'CH'])) {
|
|
354
354
|
add('X');
|
|
355
355
|
pos += 2;
|
|
356
|
-
} else if(token[pos + 1] == 'H'
|
|
356
|
+
} else if(token[pos + 1] == 'H'
|
|
357
357
|
|| token.substring(1, 2) == 'TH') {
|
|
358
|
-
if(subMatch(2, 4, ['OM', 'AM'])
|
|
358
|
+
if(subMatch(2, 4, ['OM', 'AM'])
|
|
359
359
|
|| ['VAN ', 'VON '].indexOf(token.substring(0, 4)) > -1
|
|
360
360
|
|| token.substring(0, 3) == 'SCH') {
|
|
361
|
-
add('T');
|
|
361
|
+
add('T');
|
|
362
362
|
} else
|
|
363
363
|
addSecondary('0', 'T');
|
|
364
364
|
pos++;
|
|
@@ -373,7 +373,7 @@ function process(token, maxLength) {
|
|
|
373
373
|
function handleX() {
|
|
374
374
|
if(pos == 0) {
|
|
375
375
|
add('S');
|
|
376
|
-
} else if(!(pos == token.length - 1
|
|
376
|
+
} else if(!(pos == token.length - 1
|
|
377
377
|
&& (['IAU', 'EAU', 'IEU'].indexOf(token.substring(pos - 3, pos)) > -1
|
|
378
378
|
|| ['AU', 'OU'].indexOf(token.substring(pos - 2, pos)) > -1))) {
|
|
379
379
|
add('KS');
|
|
@@ -387,7 +387,7 @@ function process(token, maxLength) {
|
|
|
387
387
|
} else if (isVowel(token[1])) {
|
|
388
388
|
addSecondary('A', 'F');
|
|
389
389
|
}
|
|
390
|
-
} else if((pos == token.length - 1 && isVowel(token[pos - 1])
|
|
390
|
+
} else if((pos == token.length - 1 && isVowel(token[pos - 1])
|
|
391
391
|
|| subMatch(-1, 4, ['EWSKI', 'EWSKY', 'OWSKI', 'OWSKY'])
|
|
392
392
|
|| token.substring(0, 3) == 'SCH')) {
|
|
393
393
|
addSecondary('', 'F');
|
|
@@ -401,11 +401,11 @@ function process(token, maxLength) {
|
|
|
401
401
|
function handleZ() {
|
|
402
402
|
if(token[pos + 1] == 'H') {
|
|
403
403
|
add('J');
|
|
404
|
-
pos++;
|
|
405
|
-
} else if(subMatch(1, 3, ['ZO', 'ZI', 'ZA'])
|
|
404
|
+
pos++;
|
|
405
|
+
} else if(subMatch(1, 3, ['ZO', 'ZI', 'ZA'])
|
|
406
406
|
|| (slavoGermanic && pos > 0 && token[pos - 1] != 'T')) {
|
|
407
407
|
addSecondary('S', 'TS');
|
|
408
|
-
pos++;
|
|
408
|
+
pos++;
|
|
409
409
|
} else
|
|
410
410
|
addCompressedDouble('Z', 'S');
|
|
411
411
|
}
|
|
@@ -421,7 +421,7 @@ function process(token, maxLength) {
|
|
|
421
421
|
while(pos < token.length) {
|
|
422
422
|
|
|
423
423
|
switch(token[pos]) {
|
|
424
|
-
case 'A': case 'E': case 'I': case 'O': case 'U': case 'Y':
|
|
424
|
+
case 'A': case 'E': case 'I': case 'O': case 'U': case 'Y':
|
|
425
425
|
case 'Ê': case 'É': case 'É': case'À':
|
|
426
426
|
if(pos == 0)
|
|
427
427
|
add('A');
|
|
@@ -502,7 +502,7 @@ function compare(stringA, stringB) {
|
|
|
502
502
|
var encodingsA = process(stringA),
|
|
503
503
|
encodingsB = process(stringB);
|
|
504
504
|
|
|
505
|
-
return encodingsA[0] == encodingsB[0] ||
|
|
505
|
+
return encodingsA[0] == encodingsB[0] ||
|
|
506
506
|
encodingsA[1] == encodingsB[1];
|
|
507
507
|
};
|
|
508
508
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"classifier":{"examples":{"computing":[[1,1,0,0,0,0,0,0,0],[0,0,1,1,0,0,0,0,0],[0,0,0,1,1,1,0,0,0]],"literature":[[0,0,1,0,0,0,1,0,0],[0,0,0,0,0,0,1,1,0],[0,0,0,0,0,0,1,0,1]]},"features":[],"featurePositions":{},"maxFeaturePosition":0,"classifications":["computing","literature"],"exampleCount":6,"theta":[{"elements":[2.0057203693422903,2.0057203693422903,0.0904766662626176,3.447364098636818,0.9696521506748157,0.9696521506748157,-4.840706080886848,-1.2267353995937322,-1.2267353995937322]},{"elements":[-2.0057203693422894,-2.0057203693422894,-0.09047666626261801,-3.4473640986368186,-0.9696521506748156,-0.9696521506748156,4.84070608088685,1.2267353995937313,1.2267353995937313]}]},"docs":[{"label":"computing","text":["fix","box"]},{"label":"computing","text":["write","code"]},{"label":"computing","text":["nasti","script","code"]},{"label":"literature","text":["write","book"]},{"label":"literature","text":["read","book"]},{"label":"literature","text":["studi","book"]}],"features":{"fix":1,"box":1,"write":2,"code":2,"nasti":1,"script":1,"book":3,"read":1,"studi":1},"stemmer":{},"lastAdded":6,"events":{"domain":null,"_events":{},"_eventsCount":0}}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natural",
|
|
3
3
|
"description": "General natural language (tokenizing, stemming (English, Russian, Spanish), classification, inflection, phonetics, tfidf, WordNet, jaro-winkler, Levenshtein distance, Dice's Coefficient) facilities for node.",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"homepage": "https://github.com/NaturalNode/natural",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"apparatus": ">= 0.0.9",
|
|
15
15
|
"sylvester": ">= 0.0.12",
|
|
16
|
-
"underscore": ">=1.3.1"
|
|
16
|
+
"underscore": ">=1.3.1",
|
|
17
|
+
"webworker-threads": ">=0.6.2"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
20
|
+
"uubench": "0.0.x",
|
|
21
|
+
"sinon": "^1.12.2",
|
|
22
|
+
"jasmine-node": "~1.13.1"
|
|
21
23
|
},
|
|
22
24
|
"scripts": {
|
|
23
25
|
"test": "NODE_PATH=. node_modules/jasmine-node/bin/jasmine-node spec/"
|