@rowboat/mongoose-fuzzy-searching
Version:
Mongoose fuzzy searching plugin
220 lines (175 loc) • 5.55 kB
JavaScript
const {
config: { DEFAULT_MIN_SIZE, DEFAULT_PREFIX_ONLY, validMiddlewares },
createFields,
createNGrams,
isFunction,
isObject,
isString,
removeFuzzyElements,
setTransformers,
nGrams,
} = require('./helpers');
const parseArguments = (args, i1, i2) => {
let options = {};
let callback = null;
if (args[i1] && isFunction(args[i1])) {
callback = args[i1];
} else if (args[i1] && isObject(args[i1])) {
options = args[i1];
}
if (!callback && typeof isFunction(args[i2])) {
callback = args[i2];
}
return { options, callback };
};
const validateItem = (item) => {
if (isObject(item) && item.keys && !Array.isArray(item.keys) && !isString(item.keys)) {
throw new TypeError('Key must be an array or a string.');
}
};
const validateMiddlewares = (middlewares) => {
if (!middlewares) {
return;
}
if (!isObject(middlewares)) {
throw new TypeError('Middlewares must be an object.');
}
if (!Object.keys(middlewares).every((key) => validMiddlewares.includes(key))) {
throw new TypeError(`Middleware key should be one of: [${validMiddlewares.join(', ')}].`);
}
if (!Object.values(middlewares).every(isFunction)) {
throw new TypeError('Middleware must be a Function.');
}
};
const getMiddleware = (middlewares, name) => {
return middlewares && middlewares[name] ? middlewares[name] : null;
};
const getDefaultValues = (item) => {
const checkPrefixOnly = isObject(item) ? item.prefixOnly : DEFAULT_PREFIX_ONLY;
const defaultNgamMinSize = isObject(item) ? item.minSize : DEFAULT_MIN_SIZE;
return {
checkPrefixOnly,
defaultNgamMinSize,
};
};
const getArgs = (queryArgs) => {
let queryString = queryArgs;
let exact = false;
if (isObject(queryArgs)) {
({ query: queryString, exact } = queryArgs);
}
return { queryString, exact: !!exact };
};
function fuzzySearch(...args) {
const queryArgs = Object.values(args);
if (queryArgs.length === 0 || (!isString(queryArgs[0]) && !isObject(queryArgs[0]))) {
throw new TypeError(
'Fuzzy Search: First argument is mandatory and must be a string or an object.',
);
}
const { exact, queryString } = getArgs(queryArgs[0]);
if (!queryString) {
return this.find();
}
const { checkPrefixOnly, defaultNgamMinSize } = getDefaultValues(queryArgs[0]);
const query = exact
? `"${queryString}"`
: nGrams(queryString, false, defaultNgamMinSize, checkPrefixOnly).join(' ');
const { callback, options } = parseArguments(queryArgs, 1, 2);
let search;
if (!isObject(options)) {
search = {
$text: {
$search: query,
},
};
} else {
search = {
$and: [{ $text: { $search: query } }, options],
};
}
return this.find.apply(this, [
search,
{ confidenceScore: { $meta: 'textScore' } },
{ sort: { confidenceScore: { $meta: 'textScore' } } },
callback,
]);
}
/**
* Plugin's main function. Creates the fuzzy fields on the collection, set's a pre save middleware to create the Ngrams for the fuzzy fields
* and creates the instance methods `fuzzySearch` which finds the guesses.
* @param {object} schema - Mongo Collection
* @param {object} options - plugin options
*/
module.exports = function (schema, pluginOptions) {
if (!pluginOptions || (pluginOptions && !pluginOptions.fields)) {
throw new Error('You must set at least one field for fuzzy search.');
}
const { fields, middlewares } = pluginOptions;
if (!Array.isArray(fields)) {
throw new TypeError('Fields must be an array.');
}
fields.forEach(validateItem);
validateMiddlewares(middlewares);
const { indexes, weights } = createFields(schema, fields);
schema.index(indexes, { weights, name: 'fuzzy_text' });
const hideElements = removeFuzzyElements(fields);
const { toJSON, toObject } = setTransformers(hideElements)(schema);
schema.options = {
...schema.options,
toObject,
toJSON,
};
function thenable(fn, cb, attr) {
if (!fn) {
return cb();
}
return Promise.resolve(fn.bind(this)(attr)).then(cb);
}
function saveMiddleware(next) {
const attributes = this;
return function () {
createNGrams(attributes, fields);
next();
};
}
function updateMiddleware(next) {
const attributes = this._update;
return function () {
createNGrams(attributes, fields);
next();
};
}
function insertMany(next, docs) {
return function () {
docs.forEach((doc) => {
createNGrams(doc, fields);
});
next();
};
}
function preUpdate(fnName) {
const fn = getMiddleware(middlewares, fnName);
return function (next) {
return thenable.bind(this)(fn, updateMiddleware.bind(this)(next));
};
}
schema.pre('save', function (next) {
const fn = getMiddleware(middlewares, 'preSave');
return thenable.bind(this)(fn, saveMiddleware.bind(this)(next));
});
schema.pre('insertMany', function (next, docs) {
const fn = getMiddleware(middlewares, 'preInsertMany');
return thenable.bind(this)(fn, insertMany.bind(this)(next, docs), docs);
});
schema.pre('update', preUpdate('preUpdate'));
schema.pre('updateOne', preUpdate('preUpdateOne'));
schema.pre('findOneAndUpdate', preUpdate('preFindOneAndUpdate'));
schema.pre('updateMany', preUpdate('preUpdateMany'));
schema.statics.fuzzySearch = function (...args) {
return fuzzySearch.apply(this, args);
};
schema.query.fuzzySearch = function (...args) {
return fuzzySearch.apply(this, args);
};
};