@nguyen-thanh-tung/minisearch-synonyms
Version:
Use MiniSearch with synonyms
131 lines (104 loc) • 3.56 kB
text/typescript
import { tokenize } from './tokenize';
export class MiniSearchSynonyms {
private groups: string[][];
private wordmap: Map<string, string[]>;
constructor(groups?: string[][]) {
this.groups = [];
this.wordmap = new Map();
if (groups) {
for (const group of groups) {
this.addSynonyms(group);
}
}
}
addSynonyms(group: string[]) {
if (group.length < 2) {
throw new Error('Synonym must have at least 2 words');
}
let uniqueWords: string[] = [];
for (const word of group) {
if (this.wordmap.has(word)) {
continue;
}
if (uniqueWords.find(w => w === word)) {
continue;
}
uniqueWords.push(word);
}
const newGroup = group.map(w => w.toLowerCase()).sort();
this.groups.push(newGroup);
for (const word of newGroup) {
this.wordmap.set(word, newGroup);
}
}
removeSynonyms(word: string) {
const lowerCaseWord = word.toLowerCase();
const groupIndex = this.groups.findIndex(group => group.find(w => w === lowerCaseWord));
if (groupIndex >= 0) {
const group = this.groups[groupIndex];
for (const w of group) {
this.wordmap.delete(w);
}
this.groups.splice(groupIndex, 1);
}
}
getSynonyms(word: string) {
const lowerCaseWord = word.toLowerCase();
const synonyms = this.wordmap.get(lowerCaseWord) || [];
return synonyms.filter(synonym => synonym !== lowerCaseWord);
}
expandQuery(query: string) {
if (!query || query.length === 0) {
return query;
}
const tokens = tokenize(query, [...this.wordmap.keys()]);
const synonymOptions: { [index: string]: string[] } = {};
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'synonym') {
const word = token.value.toLowerCase();
const synonyms = this.getSynonyms(word);
if (synonyms.length > 0) {
synonymOptions[i.toString()] = [word, ...synonyms];
}
}
}
const synonymCombinations: { [index: string]: string }[] = this.generateKeywordCombinations(synonymOptions, []);
const queries = synonymCombinations.map(combination => {
return tokens.map((token, i) => {
const iStr = i.toString();
return synonymOptions[iStr] ? combination[iStr] : token.value;
}).join('');
});
if (queries.length === 0) {
return query;
}
return {
combineWith: 'OR',
queries
};
}
private generateKeywordCombinations(synonymOptions: { [index: string]: string[] }, combinations: { [index: string]: string }[]): { [index: string]: string }[]{
const keys = Object.keys(synonymOptions);
if (keys.length === 0) {
return combinations;
}
const key = keys[0];
const newCombinations: { [index: string]: string }[] = [];
if (Object.keys(combinations).length === 0) {
for (const option of synonymOptions[key]) {
newCombinations.push({ [key]: option });
}
} else {
for (const option of synonymOptions[key]) {
for (const combination of combinations) {
newCombinations.push({ ...combination, [key]: option });
}
}
}
const remainingOptions = { ...synonymOptions };
delete remainingOptions[key];
return this.generateKeywordCombinations(remainingOptions, newCombinations);
}
}
export default MiniSearchSynonyms;