UNPKG

sentence-similarity

Version:
238 lines (197 loc) 6.02 kB
"use strict"; let Helper = require("helper-clockmaker").Helper; let debug = require("debug")("SentenceSimilarity"); let deepcopy = require("clone"); /** * Order similarity should only depend on the number of matches, since * unmatched terms are already factored into the match similarity. */ let orderSimilarity = function(v, otherLength) { //compute the offset since the whole phrase might //actually be offset by a few words, but be in the //correct order. let offset = 0; let osCount = 0; v.forEach((c, i) => { if (c >= 0) { osCount++; offset += c - i; } }); offset = offset / osCount; let mL = Math.max(v.length, otherLength); let orderSimilarity = 0; v.forEach((c, i) => { if (c >= 0) { orderSimilarity += 1.0 - Math.abs(c - i - offset) / mL; } }); if (osCount == 0) return 0.0; return (orderSimilarity / osCount - 0.5) / 0.5; }; /** * Create a table and return both the table and the * best similarity match for each term. Averages the metaphone * and levenshtein-damaru scores to produce the final result. * * @param a is the word vector used as the row of the table * a should be the wild word vector of say ["wer","r","the","pigs"] * @param b is the word vector used as the columns of the table * b should be the controlled word vector (the one that is 'correct') * ["where","are","the"] */ let similarityTable = function(a, b, options) { let table = []; let best = []; for (let i = 0; i < b.length; i++) { table.push([]); if (!b[i].match(Helper.betweenParentheses)) { for (let j = 0; j < a.length; j++) { let score = options.f(a[j], b[i], options.options); table[i].push(score); } } else { for (let j = 0; j < a.length; j++) { table[i].push(0); } } } debug(table); return table; }; let bestMatch = function(table) { let matchedColumn = new Map(); let matchedRow = new Map(); let unMatchedColumn = new Set(); let unMatchedRow = new Set(); for (let i = 0; i < table.length; i++) { unMatchedColumn.add(i); } for (let i = 0; i < table[0].length; i++) { unMatchedRow.add(i); } let shrunk = true; while (shrunk && (unMatchedRow.size && unMatchedColumn.size)) { shrunk = false; for (let i of unMatchedRow) { if (unMatchedColumn.size == 0) { matchedRow.set(i, { column: -1, score: 0 }); continue; } //find the max in the columns let columnMax = -1; let columnScoreMax = 0; for (let j of unMatchedColumn) { let val = table[j][i]; if (val > columnScoreMax) { columnScoreMax = val; columnMax = j; } } //for that column find the maximum row let rowMax = -1; let rowScoreMax = 0; if (columnMax >= 0) { for (let k of unMatchedRow) { let val = table[columnMax][k]; if (val > rowScoreMax) { rowScoreMax = val; rowMax = k; } } } if (rowMax == i && rowMax >= 0) { //rowScoreMax and columnScoreMax should be identical. matchedRow.set(rowMax, { column: columnMax, score: rowScoreMax }); matchedColumn.set(columnMax, { row: rowMax, score: rowScoreMax }); shrunk = true; if (rowMax >= 0) unMatchedRow.delete(rowMax); if (columnMax >= 0) unMatchedColumn.delete(columnMax); } } } return { matchedRow: matchedRow, matchedColumn: matchedColumn }; }; //Number of scores that were exact matches let exactScore = function(bm, a, b) { let score = 0; for (let i of bm.values()) { if (i.score == 1) { score = score + 1; } } debug("bm", bm); return score; }; //Total score including partial matches let matchScore = function(bm, a, b) { let score = 0; for (let i of bm.values()) { score = score + i.score; } debug("bm", bm); return score; }; let lengthScore = function(a, b) { let pCount = 0; b.forEach(val => { if (val.match(Helper.betweenParentheses)) { pCount++; } }); return 1.0 / (b.length - pCount); }; let computeVectors = function(bm, a, b) { let matchVector = []; let matchScore = []; for (let i = 0; i < a.length; i++) { let ans = bm.get(i); //console.log('ans',ans) if (ans) { matchVector.push(ans.column); matchScore.push(ans.score); } else { matchVector.push(-1); matchScore.push(0); } } //Ok, produce a word order score as well return { matched: matchVector, matchScore: matchScore }; }; /** * Computes the similarity between 2 sentence vectors a and b. * a and b are expected to be pre-processed before reaching this * state. * * @param a is the word vector whos similarity we are testing * @param b is the word vector we are comparing a to * @param threshold is the value below which the similarity is set to 0 * * @return an object {matched : [], matchScore : [], score : } * where matched is a vector containing indexes of the matched * words in b. matchScore is a vector of the score for each * match (0 is no match, 1 is perfect match). */ let similarity = function(ain, bin, options) { //You need to do this so that cleanArray does affect the final output //i.e. you don't want lowercase and missing commas etc in the final //result, only in the comparison. let a = deepcopy(ain); let b = deepcopy(bin); //Get rid of punctuation and capitalization for the comparison phase. a = Helper.cleanArray(a); b = Helper.cleanArray(b); debug("a", a); debug("b", b); let table = similarityTable(a, b, options); let bm = bestMatch(table); let exact = exactScore(bm.matchedRow, a, b); let score = matchScore(bm.matchedRow, a, b); let vectors = computeVectors(bm.matchedRow, a, b); vectors.exact = exact; vectors.score = score; vectors.order = orderSimilarity(vectors.matched, b.length); vectors.size = lengthScore(a, b); return vectors; }; module.exports = similarity;