UNPKG

node-lemmatizer

Version:

A lemmatization library for Node.js to retrieve a base form from an inflected form word in English.

452 lines (400 loc) 12.6 kB
/* * node-lemmatizer ©lamblabo * https://github.com/lamblabo/node-lemmatizer * MIT License * * based on the following project: * JavaScript Lemmatizer v0.0.2 by Takafumi Yamano * https://github.com/takafumir/javascript-lemmatizer * MIT License */ // programStorage let Storage = function() { this.storage = new Object(); } Storage.prototype = { getItem: function(key){ return this.storage[key]; }, setItem: function(key, data){ this.storage[key] = data; } } let programStorage = new Storage(); // extend String and define String#endsWith if (typeof String.endsWith !== "function") { String.prototype.endsWith = function(suffix) { return this.indexOf(suffix, this.length - suffix.length) !== -1; }; } // Lemmatizer constructor let Lemmatizer = function() { this.wn_files = { noun: [ './dict/index.noun.json', './dict/noun.exc.json' ], verb: [ './dict/index.verb.json', './dict/verb.exc.json' ], adj: [ './dict/index.adj.json', './dict/adj.exc.json' ], adv: [ './dict/index.adv.json', './dict/adv.exc.json' ] }; this.morphological_substitutions = { noun: [ ['ies', 'y' ], ['ves', 'f' ], ['men', 'man'] ], verb: [ ['ies', 'y'], ['ied', 'y'], ['cked', 'c'], ['cked', 'ck'], ['able', 'e'], ['able', ''], ['ability', 'e'], ['ability', ''] ], adj: [ ['er', '' ], ['est', '' ], ['er', 'e'], ['est', 'e'], ['ier', 'y'], ['iest', 'y'] ], adv: [ ['er', '' ], ['est', '' ], ['er', 'e'], ['est', 'e'], ['ier', 'y'], ['iest', 'y'] ] }; this.wordlists = {}; this.exceptions = {}; // initialize wordlists and exceptions for (let key in this.morphological_substitutions) { this.wordlists[key] = {}; this.exceptions[key] = {}; } // store dictionary data to programStorage from wn_files for (let pos in this.wn_files) { this.load_wordnet_files(pos, this.wn_files[pos][0], this.wn_files[pos][1]); } // fetch dictionary data from programStorage, then set up wordlists and exceptions for (let pos in this.wn_files) { this.setup_dic_data(pos); } }; // Lemmatizer properties Lemmatizer.prototype = { form: '', idx: '_idx', exc: '_exc', lems: [], // -> [ ["lemma1", "verb"], ["lemma2", "noun"]... ] // ************************************************** // public // ************************************************** // reuturn Array of ["lemma", "pos"] pairs // like [ ["lemma1", "verb"], ["lemma2", "noun"]... ] lemmas: function(form, pos) { let self = this; this.lems = []; this.form = form; let parts = ['verb', 'noun', 'adj', 'adv']; if ( pos && !parts.includes( pos ) ) { console.log("warning: pos must be 'verb' or 'noun' or 'adj' or 'adv'."); return; } if (!pos) { parts.forEach( function(pos) { self.irregular_bases(pos); } ); parts.forEach( function(pos) { self.regular_bases(pos); } ); // when lemma not found and the form is included in wordlists. if ( this.is_lemma_empty() ) { parts .filter( function(pos) { return self.wordlists[pos][form]; } ) .forEach( function(pos) { self.lems.push([ form, pos ]); } ); } // when lemma not found and the form is not included in wordlists. if ( this.is_lemma_empty() ) { this.lems.push([ form, '' ]); } } else { this.base_forms(pos); if ( this.is_lemma_empty() ) { this.lems.push([ form, pos ]); } } const sortBy = (key) => { return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0); }; // sort to verb -> noun -> adv -> adj return this.uniq_lemmas(this.lems).concat().sort(sortBy( function(val) { return val[1]; } )).reverse(); }, // return only uniq lemmas without pos like [ 'high' ] or [ 'leave', 'leaf' ] only_lemmas: function(form, pos) { let result = this.lemmas(form, pos).map( function(val) { return val[0]; } ); return [...new Set(result)]; }, // ************************************************** // private // The following properties(methods) are only used by // Lemmatizer inside, so don't call them from outside. // ************************************************** is_lemma_empty: function() { return this.lems.length === 0; }, // set up dictionary data load_wordnet_files: function(pos, list, exc) { let key_idx = pos + this.idx; this.open_file(key_idx, list); let key_exc = pos + this.exc; this.open_file(key_exc, exc); }, setup_dic_data: function(pos) { let self = this; let key_idx = pos + this.idx; this.fetch_data(key_idx).forEach( function(w) { self.wordlists[pos][w] = w; }); let key_exc = pos + this.exc; this.fetch_data(key_exc).forEach( function(item) { let w = item[0]; let s = item[1]; self.exceptions[pos][w] = s; }); }, open_file: function(key, file) { if (!programStorage.getItem(key)) { let data = require(`${file}`); this.store_data(key, JSON.stringify(data)); } }, store_data: function(key, data) { programStorage.setItem(key, data); }, fetch_data: function(key) { let data = JSON.parse(programStorage.getItem(key)); return data; }, // end of set up dictionary data base_forms: function(pos) { this.irregular_bases(pos); this.regular_bases(pos); }, // build array lemmas(this.lems) like [ [lemma1, "verb"], [lemma2, "noun"]... ] irregular_bases: function(pos) { if (this.exceptions[pos][this.form] && this.exceptions[pos][this.form] !== this.form) { this.lems.push( [this.exceptions[pos][this.form], pos] ); } }, // build array lemmas(this.lems) like [ [lemma1, "verb"], [lemma2, "noun"]... ] regular_bases: function(pos) { let bases = null; // bases -> [ [lemma1, lemma2, lemma3...], pos ] switch (pos){ case 'verb': bases = this.possible_verb_bases(); break; case 'noun': bases = this.possible_noun_bases(); break; case 'adj': bases = this.possible_adj_adv_bases('adj'); break; case 'adv': bases = this.possible_adj_adv_bases('adv'); break; default: break; } if (bases) { this.check_lemmas(bases); } }, // check if possible bases are include in lemma wordlists and push check_lemmas: function(bases) { let self = this; // bases -> [ [lemma1, lemma2, lemma3...], pos ] let lemmas = bases[0]; let pos = bases[1]; lemmas.forEach( function(lemma) { if ( self.wordlists[pos][lemma] && self.wordlists[pos][lemma] === lemma ) { self.lems.push( [lemma, pos] ); } }); }, possible_verb_bases: function() { let form = this.form; let lemmas = []; if ( this.ends_with_es() ) { // goes -> go let verb_base = form.slice( 0, -2 ); lemmas.push( verb_base ); if ( !this.wordlists['verb'][verb_base] || this.wordlists['verb'][verb_base] !== verb_base ) { // opposes -> oppose lemmas.push( form.slice( 0, -1 ) ); } } else if ( this.ends_with_verb_vowel_ys() ) { // annoys -> annoy lemmas.push( form.slice( 0, -1 ) ); } else if ( form.endsWith('ed') && !form.endsWith('ied') && !form.endsWith('cked') ) { // saved -> save let past_base = form.slice( 0, -1 ); lemmas.push( past_base ); if ( !this.wordlists['verb'][past_base] || this.wordlists['verb'][past_base] !== past_base ) { // talked -> talk, but not push like coded -> cod lemmas.push( form.slice( 0, -2 ) ); } } else if ( form.endsWith('ed') && this.double_consonant('ed') ) { // dragged -> drag lemmas.push( form.slice( 0, -3 ) ); // added -> add lemmas.push( form.slice( 0, -2 ) ); // pirouetted -> pirouette lemmas.push( form.slice( 0, -2 ) + 'e' ); } else if ( form.endsWith('ing') && this.double_consonant('ing') ) { // dragging -> drag lemmas.push( form.slice( 0, -4 ) ); // adding -> add lemmas.push( form.slice( 0, -3 ) ); // pirouetting -> pirouette lemmas.push( form.slice( 0, -3 ) + 'e' ); } else if ( form.endsWith('ing') && !this.exceptions['verb'][form] ) { // coding -> code let ing_base = form.slice( 0, -3 ) + 'e'; lemmas.push( ing_base ); if ( !this.wordlists['verb'][ing_base] || this.wordlists['verb'][ing_base] !== ing_base ) { // talking -> talk, but not push like coding -> cod lemmas.push( form.slice( 0, -3 ) ); } } else if ( form.endsWith('able') && this.double_consonant('able') ) { lemmas.push( form.slice( 0, -5 ) ); } else if ( form.endsWith('ability') && this.double_consonant('ability') ) { lemmas.push( form.slice( 0, -8 ) ); } else if ( form.endsWith('s') ) { lemmas.push( form.slice( 0, -1 ) ); } this.morphological_substitutions["verb"].forEach( function(entry) { let morpho = entry[0]; let origin = entry[1]; if ( form.endsWith(morpho) ) { lemmas.push( form.slice( 0, -(morpho.length) ) + origin ); } }); lemmas.push(form); return [ lemmas, 'verb' ]; }, possible_noun_bases: function() { let form = this.form; let lemmas = []; if ( this.ends_with_es() ) { // watches -> watch let noun_base = form.slice( 0, -2 ); lemmas.push( noun_base ); if ( !this.wordlists['noun'][noun_base] || this.wordlists['noun'][noun_base] !== noun_base ) { // horses -> horse lemmas.push( form.slice( 0, -1 ) ); } } else if ( form.endsWith('s') ) { lemmas.push( form.slice( 0, -1 ) ); } this.morphological_substitutions["noun"].forEach( function(entry) { let morpho = entry[0]; let origin = entry[1]; if ( form.endsWith(morpho) ) { lemmas.push( form.slice( 0, -(morpho.length) ) + origin ); } }); // to push a word like 'us' as it is lemmas.push(form); return [ lemmas, 'noun' ]; }, possible_adj_adv_bases: function(pos) { let form = this.form; let lemmas = []; if ( form.endsWith('est') && this.double_consonant('est') ) { // biggest -> big lemmas.push( form.slice( 0, -4 ) ); } else if ( form.endsWith('er') && this.double_consonant('er') ) { // bigger -> bigger lemmas.push( form.slice( 0, -3 ) ); } this.morphological_substitutions[pos].forEach( function(entry) { let morpho = entry[0]; let origin = entry[1]; if ( form.endsWith(morpho) ) { lemmas.push( form.slice( 0, -(morpho.length) ) + origin ); } }); // to push a word like 'after' as it is lemmas.push(form); return [ lemmas, pos ]; }, double_consonant: function(suffix) { // for like bigger -> big let form = this.form; // length after removing suffix from form let len = form.length - suffix.length; return this.is_vowel(form[len - 3]) && !this.is_vowel(form[len - 2]) && form[len - 2] === form[len - 1]; }, is_vowel: function(letter) { return ["a", "e", "i", "o", "u"].includes(letter); }, // [ ["leave", "verb"], ["leaf", "noun"], ["leave", "verb"], ["leave", "noun"] ]; // -> [ ["leave", "verb"], ["leaf", "noun"], ["leave", "noun"] ]; uniq_lemmas: function(lemmas) { let u_lemmas = []; let len = lemmas.length; for (let i = 0; i < len; i++) { let val = lemmas[i]; if (!this.is_include(u_lemmas, val) && val[0].length > 1) { u_lemmas.push(val); } } return u_lemmas; }, is_include: function(lemmas, target) { let len = lemmas.length; for (let i = 0; i < len; i++) { if (lemmas[i][0] === target[0] && lemmas[i][1] === target[1]) { return true; } } return false; }, ends_with_es: function() { let result = false; let form = this.form; let ends = ['ches', 'shes', 'oes', 'ses', 'xes', 'zes']; ends.forEach( function(end) { if ( form.endsWith(end) ) { result = true; } }); return result; }, ends_with_verb_vowel_ys: function() { let result = false; let form = this.form; let ends = ['ays', 'eys', 'iys', 'oys', 'uys']; ends.forEach( function(end) { if ( form.endsWith(end) ) { result = true; } }); return result; } }; let lemmatizer = new Lemmatizer(); module.exports = lemmatizer;