├── test ├── index.js ├── n-gram.js ├── utils.js └── int │ └── index.js ├── src ├── index.js ├── n-gram.js ├── plugin.js └── utils.js ├── rollup.js ├── .github └── workflows │ └── test.yml ├── LICENSE ├── package.json ├── .gitignore ├── readme.md ├── index.js └── index.cjs /test/index.js: -------------------------------------------------------------------------------- 1 | import './n-gram.js'; 2 | import './utils.js'; 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {plugin} from './plugin.js'; 2 | 3 | export default plugin; 4 | -------------------------------------------------------------------------------- /rollup.js: -------------------------------------------------------------------------------- 1 | import node from '@rollup/plugin-node-resolve'; 2 | import cjs from '@rollup/plugin-commonjs'; 3 | 4 | export default { 5 | input: './src/index.js', 6 | output: [{ 7 | file: 'index.cjs', 8 | format: 'cjs', 9 | exports: 'default' 10 | }, { 11 | file: 'index.js', 12 | format: 'es' 13 | }], 14 | plugins: [node(), cjs()] 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | services: 16 | mongo: 17 | image: mongo:4.2.6 18 | ports: 19 | - 27017:27017 20 | 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | node-version: [ 12.x, 14.x ] 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v1 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - run: npm ci 34 | - run: npm run build --if-present 35 | - run: npm test 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RENARD Laurent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-fuzzy-search", 3 | "version": "0.0.4", 4 | "description": "fuzzy search based on trigrams for mongoose odm", 5 | "main": "./index.js", 6 | "type": "module", 7 | "exports": { 8 | "import": "./index.js", 9 | "require": "./index.cjs" 10 | }, 11 | "directories": { 12 | "test": "test" 13 | }, 14 | "keywords": [ 15 | "mongoose", 16 | "mongodb", 17 | "fuzzy", 18 | "trigram", 19 | "ngram", 20 | "mongo", 21 | "search", 22 | "fuzzy search" 23 | ], 24 | "files": [ 25 | "index.js", 26 | "index.cjs" 27 | ], 28 | "scripts": { 29 | "build": "rollup -c rollup.js", 30 | "test:unit": "node test/index.js", 31 | "test:int": "node test/int/index.js", 32 | "test": "npm run test:unit && npm run test:int" 33 | }, 34 | "author": "", 35 | "license": "ISC", 36 | "devDependencies": { 37 | "@rollup/plugin-commonjs": "~15.1.0", 38 | "@rollup/plugin-node-resolve": "~9.0.0", 39 | "lodash.deburr": "~4.1.0", 40 | "lodash.set": "~4.3.2", 41 | "mongoose": "~5.10.9", 42 | "rollup": "~2.32.1", 43 | "zora": "~4.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/n-gram.js: -------------------------------------------------------------------------------- 1 | import {compose, getWords} from './utils.js'; 2 | 3 | const leftPad = (length, {symbol = ' '} = {}) => (string) => string.padStart(length + string.length, symbol); 4 | const rightPad = (length, {symbol = ' '} = {}) => (string) => string.padEnd(length + string.length, symbol); 5 | const addPadding = (length, opts = {}) => compose(leftPad(length, opts), rightPad(length, opts)); 6 | export const removeDuplicate = (array) => [...new Set(array)]; 7 | 8 | export const nGram = (n, {withPadding = false} = {withPadding: false}) => { 9 | 10 | const wholeNGrams = (string, accumulator = []) => { 11 | if (string.length < n) { 12 | return accumulator; 13 | } 14 | accumulator.push(string.slice(0, n)); 15 | return wholeNGrams(string.slice(1), accumulator); 16 | }; 17 | 18 | const pad = addPadding(n - 1); 19 | 20 | return withPadding ? compose(removeDuplicate, wholeNGrams, pad) : compose(removeDuplicate, wholeNGrams); 21 | }; 22 | 23 | export const trigram = nGram(3, {withPadding: true}); 24 | 25 | const combineTriGrams = (string) => getWords(string) 26 | .map(trigram) 27 | .reduce((acc, curr) => acc.concat(curr), []); 28 | 29 | export const sentenceTrigrams = compose(removeDuplicate, combineTriGrams); 30 | 31 | -------------------------------------------------------------------------------- /test/n-gram.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import {nGram, sentenceTrigrams, trigram} from '../src/n-gram.js'; 3 | 4 | test(`nGram`, (t) => { 5 | 6 | t.test('with no padding', (t) => { 7 | const bigram = nGram(2); 8 | t.test('should return an array of matching bigrams sorted from left to right', (t) => { 9 | t.eq(bigram('laurent'), ['la', 'au', 'ur', 're', 'en', 'nt']); 10 | }); 11 | t.test('should return an empty array if the length of the string is too small', (t) => { 12 | t.eq(bigram('l'), []); 13 | }); 14 | }); 15 | 16 | t.test('trigram', (t) => { 17 | t.test('should return trigrams of the padded input', (t) => { 18 | t.eq(trigram('laurent'), [' l', ' la', 'lau', 'aur', 'ure', 'ren', 'ent', 'nt ', 't ']); 19 | }); 20 | t.test('should have trigrams of the padded input even for short strings', (t) => { 21 | t.eq(trigram('l'), [' l', ' l ', 'l ']); 22 | }); 23 | t.test('should remove duplicates trigram', (t) => { 24 | t.eq(trigram('lolol'), [' l', ' lo', 'lol', 'olo', 'ol ', 'l ']); 25 | }); 26 | }); 27 | 28 | t.test(`draw trigrams from sentence`, (t) => { 29 | t.eq(sentenceTrigrams(' éléphant! '), [' e', ' el', 'ele', 'lep', 'eph', 'pha', 'han', 'ant', 'nt ', 't ']); 30 | t.eq(sentenceTrigrams(' Comment? ça va?'), [' c', ' co', 'com', 'omm', 'mme', 'men', 'ent', 'nt ', 't ', ' ca', 'ca ', 'a ', ' v', ' va', 'va ']); 31 | t.eq(sentenceTrigrams('Laurent Renard'), [' l', ' la', 'lau', 'aur', 'ure', 'ren', 'ent', 'nt ', 't ', ' r', ' re', 'ena', 'nar', 'ard', 'rd ', 'd ']); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import {sentenceTrigrams} from './n-gram.js'; 2 | import set from 'lodash.set'; 3 | import { 4 | buildMatchClause, 5 | buildSimilarityClause, 6 | createSchemaFields, 7 | normalizeFields, 8 | normalizeInputFactory 9 | } from './utils.js'; 10 | 11 | const saveMiddleware = (fields) => function (next) { 12 | for (const [key, fn] of Object.entries(fields)) { 13 | this.set(key, sentenceTrigrams(fn(this))); 14 | } 15 | next(); 16 | }; 17 | 18 | const insertManyMiddleware = (fields) => function (next, docs) { 19 | const Ctr = this; 20 | docs.forEach((doc) => { 21 | for (const [path, fn] of Object.entries(fields)) { 22 | // we create an instance so the document given to the getter will have the Document API 23 | const instance = new Ctr(doc); 24 | // mutate doc in place only at trigram paths 25 | set(doc, path, sentenceTrigrams(fn(instance))); 26 | } 27 | }); 28 | next(); 29 | }; 30 | 31 | export function plugin(schema, {fields = {}, select = false} = {}) { 32 | 33 | const normalizedFields = normalizeFields(fields); 34 | const normalizeInput = normalizeInputFactory(fields); 35 | const preSave = saveMiddleware(normalizedFields); 36 | const preInsertMany = insertManyMiddleware(normalizedFields); 37 | 38 | schema.add(createSchemaFields({fields, select})); 39 | schema.pre('save', preSave); 40 | schema.pre('insertMany', preInsertMany); 41 | 42 | schema.statics.fuzzy = function (input) { 43 | const normalizedInput = normalizeInput(input); 44 | const matchClause = buildMatchClause(normalizedInput); 45 | const similarity = buildSimilarityClause(normalizedInput); 46 | return this.aggregate([ 47 | {$match: matchClause}, 48 | { 49 | $project: { 50 | _id: 0, 51 | document: '$$CURRENT', 52 | similarity 53 | } 54 | } 55 | ]); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mongoose-fuzzy-search 2 | 3 | [Mongoose](https://mongoosejs.com) plugin which adds fuzzy search capabilities on a Model based on [trigrams](https://en.wikipedia.org/wiki/Trigram) sequence similarity 4 | 5 | ## Usage 6 | 7 | ### installation 8 | 9 | ``npm i --save mongoose-fuzzy-search`` 10 | 11 | ### Apply to a model 12 | 13 | ```Javascript 14 | import fuzzy from 'mongoose-fuzzy-search'; 15 | 16 | const schema = new mongoose.Schema('User', { 17 | firstname: String, 18 | lastname: String 19 | }); 20 | 21 | // add the plugin to the model and specify new fields on your schema to hold the trigrams projected 22 | schema.plugin(fuzzy, { 23 | fields:{ 24 | lastname_tg: 'lastname', // equivalent to (doc) => doc.get('lastname') 25 | fullname_tg: (doc) => [doc.get('firstname'), doc.get('lastname') ].join(' ') 26 | } 27 | }) 28 | const User = mongoose.model('User', schema); 29 | 30 | const user = new User({ 31 | firstname: 'Laurent', 32 | lastname: 'Renard', 33 | }) 34 | 35 | 36 | await user.save(); 37 | ``` 38 | 39 | The saved document will be: 40 | 41 | ```JSON 42 | { 43 | "firstname": "Laurent", 44 | "lastname": "Renard", 45 | "lastname_tg":[" r"," re","ren","ena","nar","ard","rd ","d "], 46 | "fullname_tg":[" l"," la","lau","aur","ure","ren","ent","nt ","t "," r"," re","ren","ena","nar","ard","rd ","d "] 47 | } 48 | ``` 49 | 50 | Note: when using a string, it is equivalent to a function returning the value of the document at the matching path. 51 | 52 | ### Search 53 | 54 | The ``fuzzy`` static method returns a [Aggregate](https://mongoosejs.com/docs/api/aggregate.html) matching the documents which have at least one matching trigram with the query and their [similarity score](#similarity-score). You can then decide to extend the pipeline: filter out, sort them, etc 55 | 56 | ```Javascript 57 | const result = await User.fuzzy('renart') // (.sort(), etc) 58 | // > [{ document: , similiarity: }] 59 | ``` 60 | 61 | #### similarity score 62 | 63 | The _similarity score_ is calculated by dividing the size of the intersection set between the query and the document field trigrams, and the size of the trigrams set for the query. 64 | 65 | #### change the weight of the different fields 66 | 67 | When passing a string, the pipeline calculate the similarity for each trigram field and return the mean. 68 | However, you can combine various queries and give different weights to each of them: 69 | 70 | ```Javascript 71 | const results = await User.fuzzy({ 72 | lastname_tg: { 73 | searchQuery: 'Renard' 74 | }, 75 | fullname_tg: { 76 | searchQuery: 'repnge', 77 | weight: 20 78 | } 79 | }); 80 | ``` 81 | 82 | ### Notes 83 | 84 | This plugin does not: 85 | * add any index: it is up to you 86 | * remove stop words (which are usually language specific): you can still transform an argument before you pass it to the trigram function using the field options 87 | 88 | This plugin does: 89 | * add **Document** middleware on ``save`` and ``insertMany`` middleware in order to update the trigram fields on your documents on insert/update. 90 | * lowercase, deburr, split in words and concat each word trigram into a unique set 91 | 92 | This plugin is adapted for searches when relative strings length difference does not matter much (ideal for short string like emails, names, titles, etc), when the strings have no or very little semantic (like names etc). 93 | Otherwise, you might consider using another solution such as the [native mongodb text index](https://docs.mongodb.com/manual/text-search/) or a different database 94 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import deburr from 'lodash.deburr'; 2 | import {sentenceTrigrams} from './n-gram.js'; 3 | 4 | export const compose = (...fns) => (arg) => fns.reduceRight((acc, curr) => curr(acc), arg); 5 | 6 | const replaceNonAlphaNumeric = (string) => string.replace(/\W+/g, ' '); 7 | 8 | const split = (string) => string.split(' '); 9 | 10 | const filter = (predicate) => (items) => items.filter(predicate); 11 | 12 | const toLowerCase = (string) => string.toLowerCase(); 13 | 14 | const trim = (string) => string.trim(); 15 | 16 | export const normalizeFields = (options = {}) => { 17 | return Object.fromEntries( 18 | Object 19 | .entries(options) 20 | .map(([key, value]) => [ 21 | key, 22 | typeof value === 'string' ? 23 | (doc) => doc.get(value) : 24 | value 25 | ]) 26 | ); 27 | }; 28 | 29 | export const createSchemaFields = ({fields = {}, select = false} = {}) => Object.fromEntries( 30 | Object 31 | .keys(fields) 32 | .map((key) => [key, {type: [String], select}]) 33 | ); 34 | 35 | export const getWords = compose( 36 | filter(Boolean), 37 | split, 38 | replaceNonAlphaNumeric, 39 | deburr, 40 | toLowerCase, 41 | trim 42 | ); 43 | 44 | export const normalizeInputFactory = (fields) => { 45 | const fieldNames = Object.keys(fields); 46 | return (input) => { 47 | if (typeof input === 'string') { 48 | return Object.fromEntries( 49 | fieldNames.map((key) => [key, { 50 | searchQuery: input, 51 | weight: 1 52 | }]) 53 | ); 54 | } 55 | 56 | return Object.fromEntries( 57 | Object 58 | .entries(input) 59 | .filter(([key]) => fieldNames.includes(key)) 60 | .map(([key, value]) => { 61 | const inputObject = typeof value === 'string' ? {searchQuery: value} : value; 62 | if (!(inputObject && inputObject.searchQuery)) { 63 | throw new Error(`you must provide at least "searchQuery" property for the query field ${key}. Ex: 64 | { 65 | ${key}:{ 66 | searchQuery: 'some query string', 67 | // weight: 4, // and eventually a weight 68 | } 69 | } 70 | `); 71 | } 72 | 73 | return [key, { 74 | searchQuery: inputObject.searchQuery, 75 | weight: inputObject.weight || 1 76 | }]; 77 | }) 78 | ); 79 | }; 80 | }; 81 | 82 | export const totalWeight = (normalizedInput) => Object 83 | .values(normalizedInput) 84 | .reduce((acc, curr) => acc + curr.weight, 0); 85 | 86 | const individualSimilarityClause = ([path, input]) => { 87 | const trigram = sentenceTrigrams(input.searchQuery); 88 | return { 89 | $multiply: [input.weight, { 90 | $divide: [{ 91 | $size: { 92 | $setIntersection: [`$${path}`, trigram] 93 | } 94 | }, 95 | trigram.length] 96 | }] 97 | }; 98 | }; 99 | 100 | export const buildSimilarityClause = (normalizedInput) => { 101 | return { 102 | $divide: [{$add: Object.entries(normalizedInput).map(individualSimilarityClause)}, totalWeight(normalizedInput)] 103 | }; 104 | }; 105 | 106 | export const buildMatchClause = (normalizedInput) => Object.fromEntries( 107 | Object 108 | .entries(normalizedInput) 109 | .map(([key, value]) => [key, {$in: sentenceTrigrams(value.searchQuery)}]) 110 | ); 111 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import { 3 | buildMatchClause, 4 | buildSimilarityClause, 5 | createSchemaFields, 6 | normalizeInputFactory, 7 | totalWeight 8 | } from '../src/utils.js'; 9 | 10 | test(`utils`, (t) => { 11 | t.test(`create schema fields`, (t) => { 12 | t.eq(createSchemaFields({ 13 | fields: { 14 | foo: 'bar', 15 | bim: (document) => document.get('woot') 16 | } 17 | }), { 18 | foo: {type: [String], select: false}, 19 | bim: {type: [String], select: false} 20 | }, 'trigrams should not be part of the projection by default'); 21 | 22 | t.eq(createSchemaFields({ 23 | fields: { 24 | foo: 'bar', 25 | bim: (document) => document.get('woot') 26 | }, 27 | select: true 28 | }), { 29 | foo: {type: [String], select: true}, 30 | bim: {type: [String], select: true} 31 | }, 'trigrams should be part of the projection if option flag is true'); 32 | }); 33 | 34 | t.test(`normalizeInput`, (t) => { 35 | const normalize = normalizeInputFactory({ 36 | foo: 'whatever', 37 | woot: 'anotherone' 38 | }); 39 | t.test('it should spread input with equal weight to every field, with input string', (t) => { 40 | t.eq(normalize('hello'), { 41 | foo: { 42 | searchQuery: 'hello', 43 | weight: 1 44 | }, 45 | woot: { 46 | searchQuery: 'hello', 47 | weight: 1 48 | } 49 | 50 | }); 51 | }); 52 | 53 | t.test(`it should add a default weight with object notation`, (t) => { 54 | t.eq(normalize({ 55 | foo: { 56 | searchQuery: 'some query on foo' 57 | }, 58 | woot: { 59 | searchQuery: 'woot query', 60 | weight: 5 61 | }, 62 | extra: { 63 | searchQuery: 'blahblah', 64 | weight: 666 65 | } 66 | }), { 67 | foo: { 68 | searchQuery: 'some query on foo', 69 | weight: 1 70 | }, 71 | woot: { 72 | searchQuery: 'woot query', 73 | weight: 5 74 | } 75 | }); 76 | }); 77 | }); 78 | 79 | t.test(`total weight`, (t) => { 80 | t.eq(totalWeight({ 81 | foo: { 82 | searchQuery: 5, 83 | weight: 2 84 | }, 85 | bar: { 86 | searchQuery: 5, 87 | weight: 5 88 | } 89 | }), 7); 90 | }); 91 | 92 | t.test(`build match clause`, (t) => { 93 | t.test(`with a single field`, (t) => { 94 | t.eq(buildMatchClause({ 95 | foo: { 96 | searchQuery: 'hello', 97 | weight: 66 98 | } 99 | }), {foo: {'$in': [' h', ' he', 'hel', 'ell', 'llo', 'lo ', 'o ']}} 100 | ); 101 | }); 102 | 103 | t.test(`with multiple fields`, (t) => { 104 | t.eq(buildMatchClause({ 105 | foo: { 106 | searchQuery: 'hello', 107 | weight: 1 108 | }, 109 | bar: { 110 | searchQuery: 'what?', 111 | weight: 4 112 | } 113 | }), { 114 | foo: {'$in': [' h', ' he', 'hel', 'ell', 'llo', 'lo ', 'o ']}, 115 | bar: {'$in': [' w', ' wh', 'wha', 'hat', 'at ', 't ']} 116 | } 117 | ); 118 | }); 119 | }); 120 | 121 | t.test(`buildSimilarityClause`, (t) => { 122 | t.test(`with a single field`, (t) => { 123 | t.eq(buildSimilarityClause({ 124 | foo: { 125 | searchQuery: 'hello', 126 | weight: 1 127 | } 128 | }), { 129 | $divide: [ 130 | { 131 | $add: [{ 132 | $multiply: [ 133 | 1, { 134 | $divide: [{ 135 | $size: { 136 | $setIntersection: ['$foo', [' h', ' he', 'hel', 'ell', 'llo', 'lo ', 'o ']] 137 | } 138 | }, 7] 139 | }] 140 | }] 141 | }, 1] 142 | } 143 | ); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /test/int/index.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import mongoose from 'mongoose'; 3 | import {plugin} from '../../src/plugin.js'; 4 | 5 | const LaurentRENARD = { 6 | lastname_tg: [' r', ' re', 'ren', 'ena', 'nar', 'ard', 'rd ', 'd '], 7 | fullname_tg: [' l', ' la', 'lau', 'aur', 'ure', 'ren', 'ent', 'nt ', 't ', ' r', ' re', 'ena', 'nar', 'ard', 'rd ', 'd '], 8 | firstname: 'Laurent', 9 | lastname: 'Renard' 10 | }; 11 | 12 | const BobREPONGE = { 13 | 'lastname_tg': [' r', ' re', 'rep', 'epo', 'pon', 'ong', 'nge', 'ge ', 'e '], 14 | 'fullname_tg': [' b', ' bo', 'bob', 'ob ', 'b ', ' r', ' re', 'rep', 'epo', 'pon', 'ong', 'nge', 'ge ', 'e '], 15 | 'firstname': 'Bob', 16 | 'lastname': 'Reponge' 17 | }; 18 | 19 | const LaurenCANARD = { 20 | lastname_tg: [' c', ' ca', 'can', 'ana', 'nar', 'ard', 'rd ', 'd '], 21 | fullname_tg: [' l', ' la', 'lau', 'aur', 'ure', 'ren', 'en ', 'n ', ' c', ' ca', 'can', 'ana', 'nar', 'ard', 'rd ', 'd '], 22 | firstname: 'Lauren', 23 | lastname: 'Canard' 24 | } 25 | ; 26 | 27 | test(`integration tests`, async t => { 28 | try { 29 | const schema = new mongoose.Schema({ 30 | firstname: String, 31 | lastname: String 32 | }, { 33 | versionKey: false 34 | }); 35 | 36 | schema.plugin(plugin, { 37 | fields: { 38 | lastname_tg: 'lastname', 39 | fullname_tg: (doc) => [doc.get('firstname'), doc.get('lastname')].join(' ') 40 | } 41 | }); 42 | 43 | const User = mongoose.model('User', schema, 'users'); 44 | 45 | await mongoose.connect('mongodb://localhost:27017/fuzzy-search-test', { 46 | useNewUrlParser: true, 47 | useUnifiedTopology: true 48 | }); 49 | 50 | await User.deleteMany(); 51 | 52 | await t.test(`middleware`, async (t) => { 53 | 54 | await t.test(`should create trigrams on insert many`, async (t) => { 55 | const users = await User.insertMany([{ 56 | firstname: 'Laurent', 57 | lastname: 'Renard' 58 | }, { 59 | firstname: 'Bob', 60 | lastname: 'Reponge' 61 | }]); 62 | 63 | const documents = users 64 | .map((user) => user.toObject()) 65 | .map(({_id, ...rest}) => rest); 66 | 67 | t.eq(documents, [LaurentRENARD, BobREPONGE]); 68 | }); 69 | 70 | await t.test(`should create trigram when document is created with save`, async (t) => { 71 | const newUser = new User({ 72 | firstname: 'Lauren', 73 | lastname: 'Canard' 74 | }); 75 | 76 | await newUser.save(); 77 | 78 | const {_id, ...rest} = newUser.toObject(); 79 | 80 | t.eq(rest, LaurenCANARD); 81 | }); 82 | 83 | await t.test(`should update trigram when user is updated with save`, async (t) => { 84 | const user = await User.findOne({lastname: 'Renard'}); 85 | 86 | user.lastname = 'lolo'; 87 | 88 | await user.save(); 89 | 90 | t.eq([...user.lastname_tg], [' l', ' lo', 'lol', 'olo', 'lo ', 'o ']); 91 | t.eq([...user.fullname_tg], [' l', ' la', 'lau', 'aur', 'ure', 'ren', 'ent', 'nt ', 't ', ' lo', 'lol', 'olo', 'lo ', 'o ']); 92 | 93 | user.lastname = 'Renard'; 94 | 95 | await user.save(); 96 | 97 | t.eq([...user.lastname_tg], [' r', ' re', 'ren', 'ena', 'nar', 'ard', 'rd ', 'd ']); 98 | t.eq([...user.fullname_tg], [' l', ' la', 'lau', 'aur', 'ure', 'ren', 'ent', 'nt ', 't ', ' r', ' re', 'ena', 'nar', 'ard', 'rd ', 'd ']); 99 | }); 100 | }); 101 | 102 | await t.test('search', async (t) => { 103 | 104 | const searchAndTransform = async (input) => (await User.fuzzy(input).sort({ 105 | similarity: -1 106 | })).map(({similarity, document}) => { 107 | const {firstname, lastname} = document; 108 | return { 109 | similarity: Math.trunc(similarity * 100), 110 | firstname, 111 | lastname 112 | }; 113 | }); 114 | 115 | await t.test(`search on every field`, async (t) => { 116 | const results = await searchAndTransform('renard'); 117 | t.eq(results, [ 118 | {similarity: 100, firstname: 'Laurent', lastname: 'Renard'}, 119 | {similarity: 56, firstname: 'Lauren', lastname: 'Canard'}, 120 | {similarity: 25, firstname: 'Bob', lastname: 'Reponge'} 121 | ] 122 | ); 123 | }); 124 | 125 | await t.test(`search on a particular field`, async (t) => { 126 | const results = await searchAndTransform({ 127 | lastname_tg: 'Leponge' 128 | }); 129 | t.eq(results, [ 130 | {similarity: 66, firstname: 'Bob', lastname: 'Reponge'} 131 | ]); 132 | }); 133 | 134 | await t.test(`give different weight`, async (t) => { 135 | const results = await searchAndTransform({ 136 | lastname_tg: { 137 | searchQuery: 'Renard' // exact match 138 | }, 139 | fullname_tg: { 140 | searchQuery: 'repnge', // typo but with higher weight 141 | weight: 20 142 | } 143 | }); 144 | t.eq(results, [ 145 | {similarity: 72, firstname: 'Bob', lastname: 'Reponge'}, 146 | {similarity: 28, firstname: 'Laurent', lastname: 'Renard'} 147 | ]); 148 | }); 149 | }); 150 | 151 | } finally { 152 | mongoose.connection.close(); 153 | } 154 | }); 155 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 2 | 3 | /** 4 | * lodash (Custom Build) 5 | * Build: `lodash modularize exports="npm" -o ./` 6 | * Copyright jQuery Foundation and other contributors 7 | * Released under MIT license 8 | * Based on Underscore.js 1.8.3 9 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 10 | */ 11 | 12 | /** Used as references for various `Number` constants. */ 13 | var INFINITY = 1 / 0; 14 | 15 | /** `Object#toString` result references. */ 16 | var symbolTag = '[object Symbol]'; 17 | 18 | /** Used to match Latin Unicode letters (excluding mathematical operators). */ 19 | var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; 20 | 21 | /** Used to compose unicode character classes. */ 22 | var rsComboMarksRange = '\\u0300-\\u036f\\ufe20-\\ufe23', 23 | rsComboSymbolsRange = '\\u20d0-\\u20f0'; 24 | 25 | /** Used to compose unicode capture groups. */ 26 | var rsCombo = '[' + rsComboMarksRange + rsComboSymbolsRange + ']'; 27 | 28 | /** 29 | * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and 30 | * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). 31 | */ 32 | var reComboMark = RegExp(rsCombo, 'g'); 33 | 34 | /** Used to map Latin Unicode letters to basic Latin letters. */ 35 | var deburredLetters = { 36 | // Latin-1 Supplement block. 37 | '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', 38 | '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', 39 | '\xc7': 'C', '\xe7': 'c', 40 | '\xd0': 'D', '\xf0': 'd', 41 | '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', 42 | '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', 43 | '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', 44 | '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', 45 | '\xd1': 'N', '\xf1': 'n', 46 | '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', 47 | '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', 48 | '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', 49 | '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', 50 | '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', 51 | '\xc6': 'Ae', '\xe6': 'ae', 52 | '\xde': 'Th', '\xfe': 'th', 53 | '\xdf': 'ss', 54 | // Latin Extended-A block. 55 | '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', 56 | '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', 57 | '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', 58 | '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', 59 | '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', 60 | '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', 61 | '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', 62 | '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', 63 | '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', 64 | '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', 65 | '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', 66 | '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', 67 | '\u0134': 'J', '\u0135': 'j', 68 | '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', 69 | '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', 70 | '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', 71 | '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', 72 | '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', 73 | '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', 74 | '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', 75 | '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', 76 | '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', 77 | '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', 78 | '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', 79 | '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', 80 | '\u0163': 't', '\u0165': 't', '\u0167': 't', 81 | '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', 82 | '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', 83 | '\u0174': 'W', '\u0175': 'w', 84 | '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', 85 | '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', 86 | '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', 87 | '\u0132': 'IJ', '\u0133': 'ij', 88 | '\u0152': 'Oe', '\u0153': 'oe', 89 | '\u0149': "'n", '\u017f': 'ss' 90 | }; 91 | 92 | /** Detect free variable `global` from Node.js. */ 93 | var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; 94 | 95 | /** Detect free variable `self`. */ 96 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self; 97 | 98 | /** Used as a reference to the global object. */ 99 | var root = freeGlobal || freeSelf || Function('return this')(); 100 | 101 | /** 102 | * The base implementation of `_.propertyOf` without support for deep paths. 103 | * 104 | * @private 105 | * @param {Object} object The object to query. 106 | * @returns {Function} Returns the new accessor function. 107 | */ 108 | function basePropertyOf(object) { 109 | return function(key) { 110 | return object == null ? undefined : object[key]; 111 | }; 112 | } 113 | 114 | /** 115 | * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A 116 | * letters to basic Latin letters. 117 | * 118 | * @private 119 | * @param {string} letter The matched letter to deburr. 120 | * @returns {string} Returns the deburred letter. 121 | */ 122 | var deburrLetter = basePropertyOf(deburredLetters); 123 | 124 | /** Used for built-in method references. */ 125 | var objectProto = Object.prototype; 126 | 127 | /** 128 | * Used to resolve the 129 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 130 | * of values. 131 | */ 132 | var objectToString = objectProto.toString; 133 | 134 | /** Built-in value references. */ 135 | var Symbol = root.Symbol; 136 | 137 | /** Used to convert symbols to primitives and strings. */ 138 | var symbolProto = Symbol ? Symbol.prototype : undefined, 139 | symbolToString = symbolProto ? symbolProto.toString : undefined; 140 | 141 | /** 142 | * The base implementation of `_.toString` which doesn't convert nullish 143 | * values to empty strings. 144 | * 145 | * @private 146 | * @param {*} value The value to process. 147 | * @returns {string} Returns the string. 148 | */ 149 | function baseToString(value) { 150 | // Exit early for strings to avoid a performance hit in some environments. 151 | if (typeof value == 'string') { 152 | return value; 153 | } 154 | if (isSymbol(value)) { 155 | return symbolToString ? symbolToString.call(value) : ''; 156 | } 157 | var result = (value + ''); 158 | return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; 159 | } 160 | 161 | /** 162 | * Checks if `value` is object-like. A value is object-like if it's not `null` 163 | * and has a `typeof` result of "object". 164 | * 165 | * @static 166 | * @memberOf _ 167 | * @since 4.0.0 168 | * @category Lang 169 | * @param {*} value The value to check. 170 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 171 | * @example 172 | * 173 | * _.isObjectLike({}); 174 | * // => true 175 | * 176 | * _.isObjectLike([1, 2, 3]); 177 | * // => true 178 | * 179 | * _.isObjectLike(_.noop); 180 | * // => false 181 | * 182 | * _.isObjectLike(null); 183 | * // => false 184 | */ 185 | function isObjectLike(value) { 186 | return !!value && typeof value == 'object'; 187 | } 188 | 189 | /** 190 | * Checks if `value` is classified as a `Symbol` primitive or object. 191 | * 192 | * @static 193 | * @memberOf _ 194 | * @since 4.0.0 195 | * @category Lang 196 | * @param {*} value The value to check. 197 | * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. 198 | * @example 199 | * 200 | * _.isSymbol(Symbol.iterator); 201 | * // => true 202 | * 203 | * _.isSymbol('abc'); 204 | * // => false 205 | */ 206 | function isSymbol(value) { 207 | return typeof value == 'symbol' || 208 | (isObjectLike(value) && objectToString.call(value) == symbolTag); 209 | } 210 | 211 | /** 212 | * Converts `value` to a string. An empty string is returned for `null` 213 | * and `undefined` values. The sign of `-0` is preserved. 214 | * 215 | * @static 216 | * @memberOf _ 217 | * @since 4.0.0 218 | * @category Lang 219 | * @param {*} value The value to process. 220 | * @returns {string} Returns the string. 221 | * @example 222 | * 223 | * _.toString(null); 224 | * // => '' 225 | * 226 | * _.toString(-0); 227 | * // => '-0' 228 | * 229 | * _.toString([1, 2, 3]); 230 | * // => '1,2,3' 231 | */ 232 | function toString(value) { 233 | return value == null ? '' : baseToString(value); 234 | } 235 | 236 | /** 237 | * Deburrs `string` by converting 238 | * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) 239 | * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) 240 | * letters to basic Latin letters and removing 241 | * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). 242 | * 243 | * @static 244 | * @memberOf _ 245 | * @since 3.0.0 246 | * @category String 247 | * @param {string} [string=''] The string to deburr. 248 | * @returns {string} Returns the deburred string. 249 | * @example 250 | * 251 | * _.deburr('déjà vu'); 252 | * // => 'deja vu' 253 | */ 254 | function deburr(string) { 255 | string = toString(string); 256 | return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); 257 | } 258 | 259 | var lodash_deburr = deburr; 260 | 261 | const compose = (...fns) => (arg) => fns.reduceRight((acc, curr) => curr(acc), arg); 262 | 263 | const replaceNonAlphaNumeric = (string) => string.replace(/\W+/g, ' '); 264 | 265 | const split = (string) => string.split(' '); 266 | 267 | const filter = (predicate) => (items) => items.filter(predicate); 268 | 269 | const toLowerCase = (string) => string.toLowerCase(); 270 | 271 | const trim = (string) => string.trim(); 272 | 273 | const normalizeFields = (options = {}) => { 274 | return Object.fromEntries( 275 | Object 276 | .entries(options) 277 | .map(([key, value]) => [ 278 | key, 279 | typeof value === 'string' ? 280 | (doc) => doc.get(value) : 281 | value 282 | ]) 283 | ); 284 | }; 285 | 286 | const createSchemaFields = ({fields = {}, select = false} = {}) => Object.fromEntries( 287 | Object 288 | .keys(fields) 289 | .map((key) => [key, {type: [String], select}]) 290 | ); 291 | 292 | const getWords = compose( 293 | filter(Boolean), 294 | split, 295 | replaceNonAlphaNumeric, 296 | lodash_deburr, 297 | toLowerCase, 298 | trim 299 | ); 300 | 301 | const normalizeInputFactory = (fields) => { 302 | const fieldNames = Object.keys(fields); 303 | return (input) => { 304 | if (typeof input === 'string') { 305 | return Object.fromEntries( 306 | fieldNames.map((key) => [key, { 307 | searchQuery: input, 308 | weight: 1 309 | }]) 310 | ); 311 | } 312 | 313 | return Object.fromEntries( 314 | Object 315 | .entries(input) 316 | .filter(([key]) => fieldNames.includes(key)) 317 | .map(([key, value]) => { 318 | const inputObject = typeof value === 'string' ? {searchQuery: value} : value; 319 | if (!(inputObject && inputObject.searchQuery)) { 320 | throw new Error(`you must provide at least "searchQuery" property for the query field ${key}. Ex: 321 | { 322 | ${key}:{ 323 | searchQuery: 'some query string', 324 | // weight: 4, // and eventually a weight 325 | } 326 | } 327 | `); 328 | } 329 | 330 | return [key, { 331 | searchQuery: inputObject.searchQuery, 332 | weight: inputObject.weight || 1 333 | }]; 334 | }) 335 | ); 336 | }; 337 | }; 338 | 339 | const totalWeight = (normalizedInput) => Object 340 | .values(normalizedInput) 341 | .reduce((acc, curr) => acc + curr.weight, 0); 342 | 343 | const individualSimilarityClause = ([path, input]) => { 344 | const trigram = sentenceTrigrams(input.searchQuery); 345 | return { 346 | $multiply: [input.weight, { 347 | $divide: [{ 348 | $size: { 349 | $setIntersection: [`$${path}`, trigram] 350 | } 351 | }, 352 | trigram.length] 353 | }] 354 | }; 355 | }; 356 | 357 | const buildSimilarityClause = (normalizedInput) => { 358 | return { 359 | $divide: [{$add: Object.entries(normalizedInput).map(individualSimilarityClause)}, totalWeight(normalizedInput)] 360 | }; 361 | }; 362 | 363 | const buildMatchClause = (normalizedInput) => Object.fromEntries( 364 | Object 365 | .entries(normalizedInput) 366 | .map(([key, value]) => [key, {$in: sentenceTrigrams(value.searchQuery)}]) 367 | ); 368 | 369 | const leftPad = (length, {symbol = ' '} = {}) => (string) => string.padStart(length + string.length, symbol); 370 | const rightPad = (length, {symbol = ' '} = {}) => (string) => string.padEnd(length + string.length, symbol); 371 | const addPadding = (length, opts = {}) => compose(leftPad(length, opts), rightPad(length, opts)); 372 | const removeDuplicate = (array) => [...new Set(array)]; 373 | 374 | const nGram = (n, {withPadding = false} = {withPadding: false}) => { 375 | 376 | const wholeNGrams = (string, accumulator = []) => { 377 | if (string.length < n) { 378 | return accumulator; 379 | } 380 | accumulator.push(string.slice(0, n)); 381 | return wholeNGrams(string.slice(1), accumulator); 382 | }; 383 | 384 | const pad = addPadding(n - 1); 385 | 386 | return withPadding ? compose(removeDuplicate, wholeNGrams, pad) : compose(removeDuplicate, wholeNGrams); 387 | }; 388 | 389 | const trigram = nGram(3, {withPadding: true}); 390 | 391 | const combineTriGrams = (string) => getWords(string) 392 | .map(trigram) 393 | .reduce((acc, curr) => acc.concat(curr), []); 394 | 395 | const sentenceTrigrams = compose(removeDuplicate, combineTriGrams); 396 | 397 | /** 398 | * lodash (Custom Build) 399 | * Build: `lodash modularize exports="npm" -o ./` 400 | * Copyright jQuery Foundation and other contributors 401 | * Released under MIT license 402 | * Based on Underscore.js 1.8.3 403 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 404 | */ 405 | 406 | /** Used as the `TypeError` message for "Functions" methods. */ 407 | var FUNC_ERROR_TEXT = 'Expected a function'; 408 | 409 | /** Used to stand-in for `undefined` hash values. */ 410 | var HASH_UNDEFINED = '__lodash_hash_undefined__'; 411 | 412 | /** Used as references for various `Number` constants. */ 413 | var INFINITY$1 = 1 / 0, 414 | MAX_SAFE_INTEGER = 9007199254740991; 415 | 416 | /** `Object#toString` result references. */ 417 | var funcTag = '[object Function]', 418 | genTag = '[object GeneratorFunction]', 419 | symbolTag$1 = '[object Symbol]'; 420 | 421 | /** Used to match property names within property paths. */ 422 | var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, 423 | reIsPlainProp = /^\w*$/, 424 | reLeadingDot = /^\./, 425 | rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; 426 | 427 | /** 428 | * Used to match `RegExp` 429 | * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). 430 | */ 431 | var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; 432 | 433 | /** Used to match backslashes in property paths. */ 434 | var reEscapeChar = /\\(\\)?/g; 435 | 436 | /** Used to detect host constructors (Safari). */ 437 | var reIsHostCtor = /^\[object .+?Constructor\]$/; 438 | 439 | /** Used to detect unsigned integer values. */ 440 | var reIsUint = /^(?:0|[1-9]\d*)$/; 441 | 442 | /** Detect free variable `global` from Node.js. */ 443 | var freeGlobal$1 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; 444 | 445 | /** Detect free variable `self`. */ 446 | var freeSelf$1 = typeof self == 'object' && self && self.Object === Object && self; 447 | 448 | /** Used as a reference to the global object. */ 449 | var root$1 = freeGlobal$1 || freeSelf$1 || Function('return this')(); 450 | 451 | /** 452 | * Gets the value at `key` of `object`. 453 | * 454 | * @private 455 | * @param {Object} [object] The object to query. 456 | * @param {string} key The key of the property to get. 457 | * @returns {*} Returns the property value. 458 | */ 459 | function getValue(object, key) { 460 | return object == null ? undefined : object[key]; 461 | } 462 | 463 | /** 464 | * Checks if `value` is a host object in IE < 9. 465 | * 466 | * @private 467 | * @param {*} value The value to check. 468 | * @returns {boolean} Returns `true` if `value` is a host object, else `false`. 469 | */ 470 | function isHostObject(value) { 471 | // Many host objects are `Object` objects that can coerce to strings 472 | // despite having improperly defined `toString` methods. 473 | var result = false; 474 | if (value != null && typeof value.toString != 'function') { 475 | try { 476 | result = !!(value + ''); 477 | } catch (e) {} 478 | } 479 | return result; 480 | } 481 | 482 | /** Used for built-in method references. */ 483 | var arrayProto = Array.prototype, 484 | funcProto = Function.prototype, 485 | objectProto$1 = Object.prototype; 486 | 487 | /** Used to detect overreaching core-js shims. */ 488 | var coreJsData = root$1['__core-js_shared__']; 489 | 490 | /** Used to detect methods masquerading as native. */ 491 | var maskSrcKey = (function() { 492 | var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); 493 | return uid ? ('Symbol(src)_1.' + uid) : ''; 494 | }()); 495 | 496 | /** Used to resolve the decompiled source of functions. */ 497 | var funcToString = funcProto.toString; 498 | 499 | /** Used to check objects for own properties. */ 500 | var hasOwnProperty = objectProto$1.hasOwnProperty; 501 | 502 | /** 503 | * Used to resolve the 504 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 505 | * of values. 506 | */ 507 | var objectToString$1 = objectProto$1.toString; 508 | 509 | /** Used to detect if a method is native. */ 510 | var reIsNative = RegExp('^' + 511 | funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') 512 | .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' 513 | ); 514 | 515 | /** Built-in value references. */ 516 | var Symbol$1 = root$1.Symbol, 517 | splice = arrayProto.splice; 518 | 519 | /* Built-in method references that are verified to be native. */ 520 | var Map = getNative(root$1, 'Map'), 521 | nativeCreate = getNative(Object, 'create'); 522 | 523 | /** Used to convert symbols to primitives and strings. */ 524 | var symbolProto$1 = Symbol$1 ? Symbol$1.prototype : undefined, 525 | symbolToString$1 = symbolProto$1 ? symbolProto$1.toString : undefined; 526 | 527 | /** 528 | * Creates a hash object. 529 | * 530 | * @private 531 | * @constructor 532 | * @param {Array} [entries] The key-value pairs to cache. 533 | */ 534 | function Hash(entries) { 535 | var index = -1, 536 | length = entries ? entries.length : 0; 537 | 538 | this.clear(); 539 | while (++index < length) { 540 | var entry = entries[index]; 541 | this.set(entry[0], entry[1]); 542 | } 543 | } 544 | 545 | /** 546 | * Removes all key-value entries from the hash. 547 | * 548 | * @private 549 | * @name clear 550 | * @memberOf Hash 551 | */ 552 | function hashClear() { 553 | this.__data__ = nativeCreate ? nativeCreate(null) : {}; 554 | } 555 | 556 | /** 557 | * Removes `key` and its value from the hash. 558 | * 559 | * @private 560 | * @name delete 561 | * @memberOf Hash 562 | * @param {Object} hash The hash to modify. 563 | * @param {string} key The key of the value to remove. 564 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 565 | */ 566 | function hashDelete(key) { 567 | return this.has(key) && delete this.__data__[key]; 568 | } 569 | 570 | /** 571 | * Gets the hash value for `key`. 572 | * 573 | * @private 574 | * @name get 575 | * @memberOf Hash 576 | * @param {string} key The key of the value to get. 577 | * @returns {*} Returns the entry value. 578 | */ 579 | function hashGet(key) { 580 | var data = this.__data__; 581 | if (nativeCreate) { 582 | var result = data[key]; 583 | return result === HASH_UNDEFINED ? undefined : result; 584 | } 585 | return hasOwnProperty.call(data, key) ? data[key] : undefined; 586 | } 587 | 588 | /** 589 | * Checks if a hash value for `key` exists. 590 | * 591 | * @private 592 | * @name has 593 | * @memberOf Hash 594 | * @param {string} key The key of the entry to check. 595 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 596 | */ 597 | function hashHas(key) { 598 | var data = this.__data__; 599 | return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key); 600 | } 601 | 602 | /** 603 | * Sets the hash `key` to `value`. 604 | * 605 | * @private 606 | * @name set 607 | * @memberOf Hash 608 | * @param {string} key The key of the value to set. 609 | * @param {*} value The value to set. 610 | * @returns {Object} Returns the hash instance. 611 | */ 612 | function hashSet(key, value) { 613 | var data = this.__data__; 614 | data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; 615 | return this; 616 | } 617 | 618 | // Add methods to `Hash`. 619 | Hash.prototype.clear = hashClear; 620 | Hash.prototype['delete'] = hashDelete; 621 | Hash.prototype.get = hashGet; 622 | Hash.prototype.has = hashHas; 623 | Hash.prototype.set = hashSet; 624 | 625 | /** 626 | * Creates an list cache object. 627 | * 628 | * @private 629 | * @constructor 630 | * @param {Array} [entries] The key-value pairs to cache. 631 | */ 632 | function ListCache(entries) { 633 | var index = -1, 634 | length = entries ? entries.length : 0; 635 | 636 | this.clear(); 637 | while (++index < length) { 638 | var entry = entries[index]; 639 | this.set(entry[0], entry[1]); 640 | } 641 | } 642 | 643 | /** 644 | * Removes all key-value entries from the list cache. 645 | * 646 | * @private 647 | * @name clear 648 | * @memberOf ListCache 649 | */ 650 | function listCacheClear() { 651 | this.__data__ = []; 652 | } 653 | 654 | /** 655 | * Removes `key` and its value from the list cache. 656 | * 657 | * @private 658 | * @name delete 659 | * @memberOf ListCache 660 | * @param {string} key The key of the value to remove. 661 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 662 | */ 663 | function listCacheDelete(key) { 664 | var data = this.__data__, 665 | index = assocIndexOf(data, key); 666 | 667 | if (index < 0) { 668 | return false; 669 | } 670 | var lastIndex = data.length - 1; 671 | if (index == lastIndex) { 672 | data.pop(); 673 | } else { 674 | splice.call(data, index, 1); 675 | } 676 | return true; 677 | } 678 | 679 | /** 680 | * Gets the list cache value for `key`. 681 | * 682 | * @private 683 | * @name get 684 | * @memberOf ListCache 685 | * @param {string} key The key of the value to get. 686 | * @returns {*} Returns the entry value. 687 | */ 688 | function listCacheGet(key) { 689 | var data = this.__data__, 690 | index = assocIndexOf(data, key); 691 | 692 | return index < 0 ? undefined : data[index][1]; 693 | } 694 | 695 | /** 696 | * Checks if a list cache value for `key` exists. 697 | * 698 | * @private 699 | * @name has 700 | * @memberOf ListCache 701 | * @param {string} key The key of the entry to check. 702 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 703 | */ 704 | function listCacheHas(key) { 705 | return assocIndexOf(this.__data__, key) > -1; 706 | } 707 | 708 | /** 709 | * Sets the list cache `key` to `value`. 710 | * 711 | * @private 712 | * @name set 713 | * @memberOf ListCache 714 | * @param {string} key The key of the value to set. 715 | * @param {*} value The value to set. 716 | * @returns {Object} Returns the list cache instance. 717 | */ 718 | function listCacheSet(key, value) { 719 | var data = this.__data__, 720 | index = assocIndexOf(data, key); 721 | 722 | if (index < 0) { 723 | data.push([key, value]); 724 | } else { 725 | data[index][1] = value; 726 | } 727 | return this; 728 | } 729 | 730 | // Add methods to `ListCache`. 731 | ListCache.prototype.clear = listCacheClear; 732 | ListCache.prototype['delete'] = listCacheDelete; 733 | ListCache.prototype.get = listCacheGet; 734 | ListCache.prototype.has = listCacheHas; 735 | ListCache.prototype.set = listCacheSet; 736 | 737 | /** 738 | * Creates a map cache object to store key-value pairs. 739 | * 740 | * @private 741 | * @constructor 742 | * @param {Array} [entries] The key-value pairs to cache. 743 | */ 744 | function MapCache(entries) { 745 | var index = -1, 746 | length = entries ? entries.length : 0; 747 | 748 | this.clear(); 749 | while (++index < length) { 750 | var entry = entries[index]; 751 | this.set(entry[0], entry[1]); 752 | } 753 | } 754 | 755 | /** 756 | * Removes all key-value entries from the map. 757 | * 758 | * @private 759 | * @name clear 760 | * @memberOf MapCache 761 | */ 762 | function mapCacheClear() { 763 | this.__data__ = { 764 | 'hash': new Hash, 765 | 'map': new (Map || ListCache), 766 | 'string': new Hash 767 | }; 768 | } 769 | 770 | /** 771 | * Removes `key` and its value from the map. 772 | * 773 | * @private 774 | * @name delete 775 | * @memberOf MapCache 776 | * @param {string} key The key of the value to remove. 777 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 778 | */ 779 | function mapCacheDelete(key) { 780 | return getMapData(this, key)['delete'](key); 781 | } 782 | 783 | /** 784 | * Gets the map value for `key`. 785 | * 786 | * @private 787 | * @name get 788 | * @memberOf MapCache 789 | * @param {string} key The key of the value to get. 790 | * @returns {*} Returns the entry value. 791 | */ 792 | function mapCacheGet(key) { 793 | return getMapData(this, key).get(key); 794 | } 795 | 796 | /** 797 | * Checks if a map value for `key` exists. 798 | * 799 | * @private 800 | * @name has 801 | * @memberOf MapCache 802 | * @param {string} key The key of the entry to check. 803 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 804 | */ 805 | function mapCacheHas(key) { 806 | return getMapData(this, key).has(key); 807 | } 808 | 809 | /** 810 | * Sets the map `key` to `value`. 811 | * 812 | * @private 813 | * @name set 814 | * @memberOf MapCache 815 | * @param {string} key The key of the value to set. 816 | * @param {*} value The value to set. 817 | * @returns {Object} Returns the map cache instance. 818 | */ 819 | function mapCacheSet(key, value) { 820 | getMapData(this, key).set(key, value); 821 | return this; 822 | } 823 | 824 | // Add methods to `MapCache`. 825 | MapCache.prototype.clear = mapCacheClear; 826 | MapCache.prototype['delete'] = mapCacheDelete; 827 | MapCache.prototype.get = mapCacheGet; 828 | MapCache.prototype.has = mapCacheHas; 829 | MapCache.prototype.set = mapCacheSet; 830 | 831 | /** 832 | * Assigns `value` to `key` of `object` if the existing value is not equivalent 833 | * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) 834 | * for equality comparisons. 835 | * 836 | * @private 837 | * @param {Object} object The object to modify. 838 | * @param {string} key The key of the property to assign. 839 | * @param {*} value The value to assign. 840 | */ 841 | function assignValue(object, key, value) { 842 | var objValue = object[key]; 843 | if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || 844 | (value === undefined && !(key in object))) { 845 | object[key] = value; 846 | } 847 | } 848 | 849 | /** 850 | * Gets the index at which the `key` is found in `array` of key-value pairs. 851 | * 852 | * @private 853 | * @param {Array} array The array to inspect. 854 | * @param {*} key The key to search for. 855 | * @returns {number} Returns the index of the matched value, else `-1`. 856 | */ 857 | function assocIndexOf(array, key) { 858 | var length = array.length; 859 | while (length--) { 860 | if (eq(array[length][0], key)) { 861 | return length; 862 | } 863 | } 864 | return -1; 865 | } 866 | 867 | /** 868 | * The base implementation of `_.isNative` without bad shim checks. 869 | * 870 | * @private 871 | * @param {*} value The value to check. 872 | * @returns {boolean} Returns `true` if `value` is a native function, 873 | * else `false`. 874 | */ 875 | function baseIsNative(value) { 876 | if (!isObject(value) || isMasked(value)) { 877 | return false; 878 | } 879 | var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; 880 | return pattern.test(toSource(value)); 881 | } 882 | 883 | /** 884 | * The base implementation of `_.set`. 885 | * 886 | * @private 887 | * @param {Object} object The object to modify. 888 | * @param {Array|string} path The path of the property to set. 889 | * @param {*} value The value to set. 890 | * @param {Function} [customizer] The function to customize path creation. 891 | * @returns {Object} Returns `object`. 892 | */ 893 | function baseSet(object, path, value, customizer) { 894 | if (!isObject(object)) { 895 | return object; 896 | } 897 | path = isKey(path, object) ? [path] : castPath(path); 898 | 899 | var index = -1, 900 | length = path.length, 901 | lastIndex = length - 1, 902 | nested = object; 903 | 904 | while (nested != null && ++index < length) { 905 | var key = toKey(path[index]), 906 | newValue = value; 907 | 908 | if (index != lastIndex) { 909 | var objValue = nested[key]; 910 | newValue = customizer ? customizer(objValue, key, nested) : undefined; 911 | if (newValue === undefined) { 912 | newValue = isObject(objValue) 913 | ? objValue 914 | : (isIndex(path[index + 1]) ? [] : {}); 915 | } 916 | } 917 | assignValue(nested, key, newValue); 918 | nested = nested[key]; 919 | } 920 | return object; 921 | } 922 | 923 | /** 924 | * The base implementation of `_.toString` which doesn't convert nullish 925 | * values to empty strings. 926 | * 927 | * @private 928 | * @param {*} value The value to process. 929 | * @returns {string} Returns the string. 930 | */ 931 | function baseToString$1(value) { 932 | // Exit early for strings to avoid a performance hit in some environments. 933 | if (typeof value == 'string') { 934 | return value; 935 | } 936 | if (isSymbol$1(value)) { 937 | return symbolToString$1 ? symbolToString$1.call(value) : ''; 938 | } 939 | var result = (value + ''); 940 | return (result == '0' && (1 / value) == -INFINITY$1) ? '-0' : result; 941 | } 942 | 943 | /** 944 | * Casts `value` to a path array if it's not one. 945 | * 946 | * @private 947 | * @param {*} value The value to inspect. 948 | * @returns {Array} Returns the cast property path array. 949 | */ 950 | function castPath(value) { 951 | return isArray(value) ? value : stringToPath(value); 952 | } 953 | 954 | /** 955 | * Gets the data for `map`. 956 | * 957 | * @private 958 | * @param {Object} map The map to query. 959 | * @param {string} key The reference key. 960 | * @returns {*} Returns the map data. 961 | */ 962 | function getMapData(map, key) { 963 | var data = map.__data__; 964 | return isKeyable(key) 965 | ? data[typeof key == 'string' ? 'string' : 'hash'] 966 | : data.map; 967 | } 968 | 969 | /** 970 | * Gets the native function at `key` of `object`. 971 | * 972 | * @private 973 | * @param {Object} object The object to query. 974 | * @param {string} key The key of the method to get. 975 | * @returns {*} Returns the function if it's native, else `undefined`. 976 | */ 977 | function getNative(object, key) { 978 | var value = getValue(object, key); 979 | return baseIsNative(value) ? value : undefined; 980 | } 981 | 982 | /** 983 | * Checks if `value` is a valid array-like index. 984 | * 985 | * @private 986 | * @param {*} value The value to check. 987 | * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. 988 | * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. 989 | */ 990 | function isIndex(value, length) { 991 | length = length == null ? MAX_SAFE_INTEGER : length; 992 | return !!length && 993 | (typeof value == 'number' || reIsUint.test(value)) && 994 | (value > -1 && value % 1 == 0 && value < length); 995 | } 996 | 997 | /** 998 | * Checks if `value` is a property name and not a property path. 999 | * 1000 | * @private 1001 | * @param {*} value The value to check. 1002 | * @param {Object} [object] The object to query keys on. 1003 | * @returns {boolean} Returns `true` if `value` is a property name, else `false`. 1004 | */ 1005 | function isKey(value, object) { 1006 | if (isArray(value)) { 1007 | return false; 1008 | } 1009 | var type = typeof value; 1010 | if (type == 'number' || type == 'symbol' || type == 'boolean' || 1011 | value == null || isSymbol$1(value)) { 1012 | return true; 1013 | } 1014 | return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || 1015 | (object != null && value in Object(object)); 1016 | } 1017 | 1018 | /** 1019 | * Checks if `value` is suitable for use as unique object key. 1020 | * 1021 | * @private 1022 | * @param {*} value The value to check. 1023 | * @returns {boolean} Returns `true` if `value` is suitable, else `false`. 1024 | */ 1025 | function isKeyable(value) { 1026 | var type = typeof value; 1027 | return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') 1028 | ? (value !== '__proto__') 1029 | : (value === null); 1030 | } 1031 | 1032 | /** 1033 | * Checks if `func` has its source masked. 1034 | * 1035 | * @private 1036 | * @param {Function} func The function to check. 1037 | * @returns {boolean} Returns `true` if `func` is masked, else `false`. 1038 | */ 1039 | function isMasked(func) { 1040 | return !!maskSrcKey && (maskSrcKey in func); 1041 | } 1042 | 1043 | /** 1044 | * Converts `string` to a property path array. 1045 | * 1046 | * @private 1047 | * @param {string} string The string to convert. 1048 | * @returns {Array} Returns the property path array. 1049 | */ 1050 | var stringToPath = memoize(function(string) { 1051 | string = toString$1(string); 1052 | 1053 | var result = []; 1054 | if (reLeadingDot.test(string)) { 1055 | result.push(''); 1056 | } 1057 | string.replace(rePropName, function(match, number, quote, string) { 1058 | result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match)); 1059 | }); 1060 | return result; 1061 | }); 1062 | 1063 | /** 1064 | * Converts `value` to a string key if it's not a string or symbol. 1065 | * 1066 | * @private 1067 | * @param {*} value The value to inspect. 1068 | * @returns {string|symbol} Returns the key. 1069 | */ 1070 | function toKey(value) { 1071 | if (typeof value == 'string' || isSymbol$1(value)) { 1072 | return value; 1073 | } 1074 | var result = (value + ''); 1075 | return (result == '0' && (1 / value) == -INFINITY$1) ? '-0' : result; 1076 | } 1077 | 1078 | /** 1079 | * Converts `func` to its source code. 1080 | * 1081 | * @private 1082 | * @param {Function} func The function to process. 1083 | * @returns {string} Returns the source code. 1084 | */ 1085 | function toSource(func) { 1086 | if (func != null) { 1087 | try { 1088 | return funcToString.call(func); 1089 | } catch (e) {} 1090 | try { 1091 | return (func + ''); 1092 | } catch (e) {} 1093 | } 1094 | return ''; 1095 | } 1096 | 1097 | /** 1098 | * Creates a function that memoizes the result of `func`. If `resolver` is 1099 | * provided, it determines the cache key for storing the result based on the 1100 | * arguments provided to the memoized function. By default, the first argument 1101 | * provided to the memoized function is used as the map cache key. The `func` 1102 | * is invoked with the `this` binding of the memoized function. 1103 | * 1104 | * **Note:** The cache is exposed as the `cache` property on the memoized 1105 | * function. Its creation may be customized by replacing the `_.memoize.Cache` 1106 | * constructor with one whose instances implement the 1107 | * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) 1108 | * method interface of `delete`, `get`, `has`, and `set`. 1109 | * 1110 | * @static 1111 | * @memberOf _ 1112 | * @since 0.1.0 1113 | * @category Function 1114 | * @param {Function} func The function to have its output memoized. 1115 | * @param {Function} [resolver] The function to resolve the cache key. 1116 | * @returns {Function} Returns the new memoized function. 1117 | * @example 1118 | * 1119 | * var object = { 'a': 1, 'b': 2 }; 1120 | * var other = { 'c': 3, 'd': 4 }; 1121 | * 1122 | * var values = _.memoize(_.values); 1123 | * values(object); 1124 | * // => [1, 2] 1125 | * 1126 | * values(other); 1127 | * // => [3, 4] 1128 | * 1129 | * object.a = 2; 1130 | * values(object); 1131 | * // => [1, 2] 1132 | * 1133 | * // Modify the result cache. 1134 | * values.cache.set(object, ['a', 'b']); 1135 | * values(object); 1136 | * // => ['a', 'b'] 1137 | * 1138 | * // Replace `_.memoize.Cache`. 1139 | * _.memoize.Cache = WeakMap; 1140 | */ 1141 | function memoize(func, resolver) { 1142 | if (typeof func != 'function' || (resolver && typeof resolver != 'function')) { 1143 | throw new TypeError(FUNC_ERROR_TEXT); 1144 | } 1145 | var memoized = function() { 1146 | var args = arguments, 1147 | key = resolver ? resolver.apply(this, args) : args[0], 1148 | cache = memoized.cache; 1149 | 1150 | if (cache.has(key)) { 1151 | return cache.get(key); 1152 | } 1153 | var result = func.apply(this, args); 1154 | memoized.cache = cache.set(key, result); 1155 | return result; 1156 | }; 1157 | memoized.cache = new (memoize.Cache || MapCache); 1158 | return memoized; 1159 | } 1160 | 1161 | // Assign cache to `_.memoize`. 1162 | memoize.Cache = MapCache; 1163 | 1164 | /** 1165 | * Performs a 1166 | * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) 1167 | * comparison between two values to determine if they are equivalent. 1168 | * 1169 | * @static 1170 | * @memberOf _ 1171 | * @since 4.0.0 1172 | * @category Lang 1173 | * @param {*} value The value to compare. 1174 | * @param {*} other The other value to compare. 1175 | * @returns {boolean} Returns `true` if the values are equivalent, else `false`. 1176 | * @example 1177 | * 1178 | * var object = { 'a': 1 }; 1179 | * var other = { 'a': 1 }; 1180 | * 1181 | * _.eq(object, object); 1182 | * // => true 1183 | * 1184 | * _.eq(object, other); 1185 | * // => false 1186 | * 1187 | * _.eq('a', 'a'); 1188 | * // => true 1189 | * 1190 | * _.eq('a', Object('a')); 1191 | * // => false 1192 | * 1193 | * _.eq(NaN, NaN); 1194 | * // => true 1195 | */ 1196 | function eq(value, other) { 1197 | return value === other || (value !== value && other !== other); 1198 | } 1199 | 1200 | /** 1201 | * Checks if `value` is classified as an `Array` object. 1202 | * 1203 | * @static 1204 | * @memberOf _ 1205 | * @since 0.1.0 1206 | * @category Lang 1207 | * @param {*} value The value to check. 1208 | * @returns {boolean} Returns `true` if `value` is an array, else `false`. 1209 | * @example 1210 | * 1211 | * _.isArray([1, 2, 3]); 1212 | * // => true 1213 | * 1214 | * _.isArray(document.body.children); 1215 | * // => false 1216 | * 1217 | * _.isArray('abc'); 1218 | * // => false 1219 | * 1220 | * _.isArray(_.noop); 1221 | * // => false 1222 | */ 1223 | var isArray = Array.isArray; 1224 | 1225 | /** 1226 | * Checks if `value` is classified as a `Function` object. 1227 | * 1228 | * @static 1229 | * @memberOf _ 1230 | * @since 0.1.0 1231 | * @category Lang 1232 | * @param {*} value The value to check. 1233 | * @returns {boolean} Returns `true` if `value` is a function, else `false`. 1234 | * @example 1235 | * 1236 | * _.isFunction(_); 1237 | * // => true 1238 | * 1239 | * _.isFunction(/abc/); 1240 | * // => false 1241 | */ 1242 | function isFunction(value) { 1243 | // The use of `Object#toString` avoids issues with the `typeof` operator 1244 | // in Safari 8-9 which returns 'object' for typed array and other constructors. 1245 | var tag = isObject(value) ? objectToString$1.call(value) : ''; 1246 | return tag == funcTag || tag == genTag; 1247 | } 1248 | 1249 | /** 1250 | * Checks if `value` is the 1251 | * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) 1252 | * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 1253 | * 1254 | * @static 1255 | * @memberOf _ 1256 | * @since 0.1.0 1257 | * @category Lang 1258 | * @param {*} value The value to check. 1259 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 1260 | * @example 1261 | * 1262 | * _.isObject({}); 1263 | * // => true 1264 | * 1265 | * _.isObject([1, 2, 3]); 1266 | * // => true 1267 | * 1268 | * _.isObject(_.noop); 1269 | * // => true 1270 | * 1271 | * _.isObject(null); 1272 | * // => false 1273 | */ 1274 | function isObject(value) { 1275 | var type = typeof value; 1276 | return !!value && (type == 'object' || type == 'function'); 1277 | } 1278 | 1279 | /** 1280 | * Checks if `value` is object-like. A value is object-like if it's not `null` 1281 | * and has a `typeof` result of "object". 1282 | * 1283 | * @static 1284 | * @memberOf _ 1285 | * @since 4.0.0 1286 | * @category Lang 1287 | * @param {*} value The value to check. 1288 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 1289 | * @example 1290 | * 1291 | * _.isObjectLike({}); 1292 | * // => true 1293 | * 1294 | * _.isObjectLike([1, 2, 3]); 1295 | * // => true 1296 | * 1297 | * _.isObjectLike(_.noop); 1298 | * // => false 1299 | * 1300 | * _.isObjectLike(null); 1301 | * // => false 1302 | */ 1303 | function isObjectLike$1(value) { 1304 | return !!value && typeof value == 'object'; 1305 | } 1306 | 1307 | /** 1308 | * Checks if `value` is classified as a `Symbol` primitive or object. 1309 | * 1310 | * @static 1311 | * @memberOf _ 1312 | * @since 4.0.0 1313 | * @category Lang 1314 | * @param {*} value The value to check. 1315 | * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. 1316 | * @example 1317 | * 1318 | * _.isSymbol(Symbol.iterator); 1319 | * // => true 1320 | * 1321 | * _.isSymbol('abc'); 1322 | * // => false 1323 | */ 1324 | function isSymbol$1(value) { 1325 | return typeof value == 'symbol' || 1326 | (isObjectLike$1(value) && objectToString$1.call(value) == symbolTag$1); 1327 | } 1328 | 1329 | /** 1330 | * Converts `value` to a string. An empty string is returned for `null` 1331 | * and `undefined` values. The sign of `-0` is preserved. 1332 | * 1333 | * @static 1334 | * @memberOf _ 1335 | * @since 4.0.0 1336 | * @category Lang 1337 | * @param {*} value The value to process. 1338 | * @returns {string} Returns the string. 1339 | * @example 1340 | * 1341 | * _.toString(null); 1342 | * // => '' 1343 | * 1344 | * _.toString(-0); 1345 | * // => '-0' 1346 | * 1347 | * _.toString([1, 2, 3]); 1348 | * // => '1,2,3' 1349 | */ 1350 | function toString$1(value) { 1351 | return value == null ? '' : baseToString$1(value); 1352 | } 1353 | 1354 | /** 1355 | * Sets the value at `path` of `object`. If a portion of `path` doesn't exist, 1356 | * it's created. Arrays are created for missing index properties while objects 1357 | * are created for all other missing properties. Use `_.setWith` to customize 1358 | * `path` creation. 1359 | * 1360 | * **Note:** This method mutates `object`. 1361 | * 1362 | * @static 1363 | * @memberOf _ 1364 | * @since 3.7.0 1365 | * @category Object 1366 | * @param {Object} object The object to modify. 1367 | * @param {Array|string} path The path of the property to set. 1368 | * @param {*} value The value to set. 1369 | * @returns {Object} Returns `object`. 1370 | * @example 1371 | * 1372 | * var object = { 'a': [{ 'b': { 'c': 3 } }] }; 1373 | * 1374 | * _.set(object, 'a[0].b.c', 4); 1375 | * console.log(object.a[0].b.c); 1376 | * // => 4 1377 | * 1378 | * _.set(object, ['x', '0', 'y', 'z'], 5); 1379 | * console.log(object.x[0].y.z); 1380 | * // => 5 1381 | */ 1382 | function set(object, path, value) { 1383 | return object == null ? object : baseSet(object, path, value); 1384 | } 1385 | 1386 | var lodash_set = set; 1387 | 1388 | const saveMiddleware = (fields) => function (next) { 1389 | for (const [key, fn] of Object.entries(fields)) { 1390 | this.set(key, sentenceTrigrams(fn(this))); 1391 | } 1392 | next(); 1393 | }; 1394 | 1395 | const insertManyMiddleware = (fields) => function (next, docs) { 1396 | const Ctr = this; 1397 | docs.forEach((doc) => { 1398 | for (const [path, fn] of Object.entries(fields)) { 1399 | // we create an instance so the document given to the getter will have the Document API 1400 | const instance = new Ctr(doc); 1401 | // mutate doc in place only at trigram paths 1402 | lodash_set(doc, path, sentenceTrigrams(fn(instance))); 1403 | } 1404 | }); 1405 | next(); 1406 | }; 1407 | 1408 | function plugin(schema, {fields = {}, select = false} = {}) { 1409 | 1410 | const normalizedFields = normalizeFields(fields); 1411 | const normalizeInput = normalizeInputFactory(fields); 1412 | const preSave = saveMiddleware(normalizedFields); 1413 | const preInsertMany = insertManyMiddleware(normalizedFields); 1414 | 1415 | schema.add(createSchemaFields({fields, select})); 1416 | schema.pre('save', preSave); 1417 | schema.pre('insertMany', preInsertMany); 1418 | 1419 | schema.statics.fuzzy = function (input) { 1420 | const normalizedInput = normalizeInput(input); 1421 | const matchClause = buildMatchClause(normalizedInput); 1422 | const similarity = buildSimilarityClause(normalizedInput); 1423 | return this.aggregate([ 1424 | {$match: matchClause}, 1425 | { 1426 | $project: { 1427 | _id: 0, 1428 | document: '$$CURRENT', 1429 | similarity 1430 | } 1431 | } 1432 | ]); 1433 | }; 1434 | } 1435 | 1436 | export default plugin; 1437 | -------------------------------------------------------------------------------- /index.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 4 | 5 | /** 6 | * lodash (Custom Build) 7 | * Build: `lodash modularize exports="npm" -o ./` 8 | * Copyright jQuery Foundation and other contributors 9 | * Released under MIT license 10 | * Based on Underscore.js 1.8.3 11 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 12 | */ 13 | 14 | /** Used as references for various `Number` constants. */ 15 | var INFINITY = 1 / 0; 16 | 17 | /** `Object#toString` result references. */ 18 | var symbolTag = '[object Symbol]'; 19 | 20 | /** Used to match Latin Unicode letters (excluding mathematical operators). */ 21 | var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; 22 | 23 | /** Used to compose unicode character classes. */ 24 | var rsComboMarksRange = '\\u0300-\\u036f\\ufe20-\\ufe23', 25 | rsComboSymbolsRange = '\\u20d0-\\u20f0'; 26 | 27 | /** Used to compose unicode capture groups. */ 28 | var rsCombo = '[' + rsComboMarksRange + rsComboSymbolsRange + ']'; 29 | 30 | /** 31 | * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and 32 | * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). 33 | */ 34 | var reComboMark = RegExp(rsCombo, 'g'); 35 | 36 | /** Used to map Latin Unicode letters to basic Latin letters. */ 37 | var deburredLetters = { 38 | // Latin-1 Supplement block. 39 | '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', 40 | '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', 41 | '\xc7': 'C', '\xe7': 'c', 42 | '\xd0': 'D', '\xf0': 'd', 43 | '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', 44 | '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', 45 | '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', 46 | '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', 47 | '\xd1': 'N', '\xf1': 'n', 48 | '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', 49 | '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', 50 | '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', 51 | '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', 52 | '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', 53 | '\xc6': 'Ae', '\xe6': 'ae', 54 | '\xde': 'Th', '\xfe': 'th', 55 | '\xdf': 'ss', 56 | // Latin Extended-A block. 57 | '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', 58 | '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', 59 | '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', 60 | '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', 61 | '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', 62 | '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', 63 | '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', 64 | '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', 65 | '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', 66 | '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', 67 | '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', 68 | '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', 69 | '\u0134': 'J', '\u0135': 'j', 70 | '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', 71 | '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', 72 | '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', 73 | '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', 74 | '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', 75 | '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', 76 | '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', 77 | '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', 78 | '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', 79 | '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', 80 | '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', 81 | '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', 82 | '\u0163': 't', '\u0165': 't', '\u0167': 't', 83 | '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', 84 | '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', 85 | '\u0174': 'W', '\u0175': 'w', 86 | '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', 87 | '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', 88 | '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', 89 | '\u0132': 'IJ', '\u0133': 'ij', 90 | '\u0152': 'Oe', '\u0153': 'oe', 91 | '\u0149': "'n", '\u017f': 'ss' 92 | }; 93 | 94 | /** Detect free variable `global` from Node.js. */ 95 | var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; 96 | 97 | /** Detect free variable `self`. */ 98 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self; 99 | 100 | /** Used as a reference to the global object. */ 101 | var root = freeGlobal || freeSelf || Function('return this')(); 102 | 103 | /** 104 | * The base implementation of `_.propertyOf` without support for deep paths. 105 | * 106 | * @private 107 | * @param {Object} object The object to query. 108 | * @returns {Function} Returns the new accessor function. 109 | */ 110 | function basePropertyOf(object) { 111 | return function(key) { 112 | return object == null ? undefined : object[key]; 113 | }; 114 | } 115 | 116 | /** 117 | * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A 118 | * letters to basic Latin letters. 119 | * 120 | * @private 121 | * @param {string} letter The matched letter to deburr. 122 | * @returns {string} Returns the deburred letter. 123 | */ 124 | var deburrLetter = basePropertyOf(deburredLetters); 125 | 126 | /** Used for built-in method references. */ 127 | var objectProto = Object.prototype; 128 | 129 | /** 130 | * Used to resolve the 131 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 132 | * of values. 133 | */ 134 | var objectToString = objectProto.toString; 135 | 136 | /** Built-in value references. */ 137 | var Symbol = root.Symbol; 138 | 139 | /** Used to convert symbols to primitives and strings. */ 140 | var symbolProto = Symbol ? Symbol.prototype : undefined, 141 | symbolToString = symbolProto ? symbolProto.toString : undefined; 142 | 143 | /** 144 | * The base implementation of `_.toString` which doesn't convert nullish 145 | * values to empty strings. 146 | * 147 | * @private 148 | * @param {*} value The value to process. 149 | * @returns {string} Returns the string. 150 | */ 151 | function baseToString(value) { 152 | // Exit early for strings to avoid a performance hit in some environments. 153 | if (typeof value == 'string') { 154 | return value; 155 | } 156 | if (isSymbol(value)) { 157 | return symbolToString ? symbolToString.call(value) : ''; 158 | } 159 | var result = (value + ''); 160 | return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; 161 | } 162 | 163 | /** 164 | * Checks if `value` is object-like. A value is object-like if it's not `null` 165 | * and has a `typeof` result of "object". 166 | * 167 | * @static 168 | * @memberOf _ 169 | * @since 4.0.0 170 | * @category Lang 171 | * @param {*} value The value to check. 172 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 173 | * @example 174 | * 175 | * _.isObjectLike({}); 176 | * // => true 177 | * 178 | * _.isObjectLike([1, 2, 3]); 179 | * // => true 180 | * 181 | * _.isObjectLike(_.noop); 182 | * // => false 183 | * 184 | * _.isObjectLike(null); 185 | * // => false 186 | */ 187 | function isObjectLike(value) { 188 | return !!value && typeof value == 'object'; 189 | } 190 | 191 | /** 192 | * Checks if `value` is classified as a `Symbol` primitive or object. 193 | * 194 | * @static 195 | * @memberOf _ 196 | * @since 4.0.0 197 | * @category Lang 198 | * @param {*} value The value to check. 199 | * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. 200 | * @example 201 | * 202 | * _.isSymbol(Symbol.iterator); 203 | * // => true 204 | * 205 | * _.isSymbol('abc'); 206 | * // => false 207 | */ 208 | function isSymbol(value) { 209 | return typeof value == 'symbol' || 210 | (isObjectLike(value) && objectToString.call(value) == symbolTag); 211 | } 212 | 213 | /** 214 | * Converts `value` to a string. An empty string is returned for `null` 215 | * and `undefined` values. The sign of `-0` is preserved. 216 | * 217 | * @static 218 | * @memberOf _ 219 | * @since 4.0.0 220 | * @category Lang 221 | * @param {*} value The value to process. 222 | * @returns {string} Returns the string. 223 | * @example 224 | * 225 | * _.toString(null); 226 | * // => '' 227 | * 228 | * _.toString(-0); 229 | * // => '-0' 230 | * 231 | * _.toString([1, 2, 3]); 232 | * // => '1,2,3' 233 | */ 234 | function toString(value) { 235 | return value == null ? '' : baseToString(value); 236 | } 237 | 238 | /** 239 | * Deburrs `string` by converting 240 | * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) 241 | * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) 242 | * letters to basic Latin letters and removing 243 | * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). 244 | * 245 | * @static 246 | * @memberOf _ 247 | * @since 3.0.0 248 | * @category String 249 | * @param {string} [string=''] The string to deburr. 250 | * @returns {string} Returns the deburred string. 251 | * @example 252 | * 253 | * _.deburr('déjà vu'); 254 | * // => 'deja vu' 255 | */ 256 | function deburr(string) { 257 | string = toString(string); 258 | return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); 259 | } 260 | 261 | var lodash_deburr = deburr; 262 | 263 | const compose = (...fns) => (arg) => fns.reduceRight((acc, curr) => curr(acc), arg); 264 | 265 | const replaceNonAlphaNumeric = (string) => string.replace(/\W+/g, ' '); 266 | 267 | const split = (string) => string.split(' '); 268 | 269 | const filter = (predicate) => (items) => items.filter(predicate); 270 | 271 | const toLowerCase = (string) => string.toLowerCase(); 272 | 273 | const trim = (string) => string.trim(); 274 | 275 | const normalizeFields = (options = {}) => { 276 | return Object.fromEntries( 277 | Object 278 | .entries(options) 279 | .map(([key, value]) => [ 280 | key, 281 | typeof value === 'string' ? 282 | (doc) => doc.get(value) : 283 | value 284 | ]) 285 | ); 286 | }; 287 | 288 | const createSchemaFields = ({fields = {}, select = false} = {}) => Object.fromEntries( 289 | Object 290 | .keys(fields) 291 | .map((key) => [key, {type: [String], select}]) 292 | ); 293 | 294 | const getWords = compose( 295 | filter(Boolean), 296 | split, 297 | replaceNonAlphaNumeric, 298 | lodash_deburr, 299 | toLowerCase, 300 | trim 301 | ); 302 | 303 | const normalizeInputFactory = (fields) => { 304 | const fieldNames = Object.keys(fields); 305 | return (input) => { 306 | if (typeof input === 'string') { 307 | return Object.fromEntries( 308 | fieldNames.map((key) => [key, { 309 | searchQuery: input, 310 | weight: 1 311 | }]) 312 | ); 313 | } 314 | 315 | return Object.fromEntries( 316 | Object 317 | .entries(input) 318 | .filter(([key]) => fieldNames.includes(key)) 319 | .map(([key, value]) => { 320 | const inputObject = typeof value === 'string' ? {searchQuery: value} : value; 321 | if (!(inputObject && inputObject.searchQuery)) { 322 | throw new Error(`you must provide at least "searchQuery" property for the query field ${key}. Ex: 323 | { 324 | ${key}:{ 325 | searchQuery: 'some query string', 326 | // weight: 4, // and eventually a weight 327 | } 328 | } 329 | `); 330 | } 331 | 332 | return [key, { 333 | searchQuery: inputObject.searchQuery, 334 | weight: inputObject.weight || 1 335 | }]; 336 | }) 337 | ); 338 | }; 339 | }; 340 | 341 | const totalWeight = (normalizedInput) => Object 342 | .values(normalizedInput) 343 | .reduce((acc, curr) => acc + curr.weight, 0); 344 | 345 | const individualSimilarityClause = ([path, input]) => { 346 | const trigram = sentenceTrigrams(input.searchQuery); 347 | return { 348 | $multiply: [input.weight, { 349 | $divide: [{ 350 | $size: { 351 | $setIntersection: [`$${path}`, trigram] 352 | } 353 | }, 354 | trigram.length] 355 | }] 356 | }; 357 | }; 358 | 359 | const buildSimilarityClause = (normalizedInput) => { 360 | return { 361 | $divide: [{$add: Object.entries(normalizedInput).map(individualSimilarityClause)}, totalWeight(normalizedInput)] 362 | }; 363 | }; 364 | 365 | const buildMatchClause = (normalizedInput) => Object.fromEntries( 366 | Object 367 | .entries(normalizedInput) 368 | .map(([key, value]) => [key, {$in: sentenceTrigrams(value.searchQuery)}]) 369 | ); 370 | 371 | const leftPad = (length, {symbol = ' '} = {}) => (string) => string.padStart(length + string.length, symbol); 372 | const rightPad = (length, {symbol = ' '} = {}) => (string) => string.padEnd(length + string.length, symbol); 373 | const addPadding = (length, opts = {}) => compose(leftPad(length, opts), rightPad(length, opts)); 374 | const removeDuplicate = (array) => [...new Set(array)]; 375 | 376 | const nGram = (n, {withPadding = false} = {withPadding: false}) => { 377 | 378 | const wholeNGrams = (string, accumulator = []) => { 379 | if (string.length < n) { 380 | return accumulator; 381 | } 382 | accumulator.push(string.slice(0, n)); 383 | return wholeNGrams(string.slice(1), accumulator); 384 | }; 385 | 386 | const pad = addPadding(n - 1); 387 | 388 | return withPadding ? compose(removeDuplicate, wholeNGrams, pad) : compose(removeDuplicate, wholeNGrams); 389 | }; 390 | 391 | const trigram = nGram(3, {withPadding: true}); 392 | 393 | const combineTriGrams = (string) => getWords(string) 394 | .map(trigram) 395 | .reduce((acc, curr) => acc.concat(curr), []); 396 | 397 | const sentenceTrigrams = compose(removeDuplicate, combineTriGrams); 398 | 399 | /** 400 | * lodash (Custom Build) 401 | * Build: `lodash modularize exports="npm" -o ./` 402 | * Copyright jQuery Foundation and other contributors 403 | * Released under MIT license 404 | * Based on Underscore.js 1.8.3 405 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 406 | */ 407 | 408 | /** Used as the `TypeError` message for "Functions" methods. */ 409 | var FUNC_ERROR_TEXT = 'Expected a function'; 410 | 411 | /** Used to stand-in for `undefined` hash values. */ 412 | var HASH_UNDEFINED = '__lodash_hash_undefined__'; 413 | 414 | /** Used as references for various `Number` constants. */ 415 | var INFINITY$1 = 1 / 0, 416 | MAX_SAFE_INTEGER = 9007199254740991; 417 | 418 | /** `Object#toString` result references. */ 419 | var funcTag = '[object Function]', 420 | genTag = '[object GeneratorFunction]', 421 | symbolTag$1 = '[object Symbol]'; 422 | 423 | /** Used to match property names within property paths. */ 424 | var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, 425 | reIsPlainProp = /^\w*$/, 426 | reLeadingDot = /^\./, 427 | rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; 428 | 429 | /** 430 | * Used to match `RegExp` 431 | * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). 432 | */ 433 | var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; 434 | 435 | /** Used to match backslashes in property paths. */ 436 | var reEscapeChar = /\\(\\)?/g; 437 | 438 | /** Used to detect host constructors (Safari). */ 439 | var reIsHostCtor = /^\[object .+?Constructor\]$/; 440 | 441 | /** Used to detect unsigned integer values. */ 442 | var reIsUint = /^(?:0|[1-9]\d*)$/; 443 | 444 | /** Detect free variable `global` from Node.js. */ 445 | var freeGlobal$1 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; 446 | 447 | /** Detect free variable `self`. */ 448 | var freeSelf$1 = typeof self == 'object' && self && self.Object === Object && self; 449 | 450 | /** Used as a reference to the global object. */ 451 | var root$1 = freeGlobal$1 || freeSelf$1 || Function('return this')(); 452 | 453 | /** 454 | * Gets the value at `key` of `object`. 455 | * 456 | * @private 457 | * @param {Object} [object] The object to query. 458 | * @param {string} key The key of the property to get. 459 | * @returns {*} Returns the property value. 460 | */ 461 | function getValue(object, key) { 462 | return object == null ? undefined : object[key]; 463 | } 464 | 465 | /** 466 | * Checks if `value` is a host object in IE < 9. 467 | * 468 | * @private 469 | * @param {*} value The value to check. 470 | * @returns {boolean} Returns `true` if `value` is a host object, else `false`. 471 | */ 472 | function isHostObject(value) { 473 | // Many host objects are `Object` objects that can coerce to strings 474 | // despite having improperly defined `toString` methods. 475 | var result = false; 476 | if (value != null && typeof value.toString != 'function') { 477 | try { 478 | result = !!(value + ''); 479 | } catch (e) {} 480 | } 481 | return result; 482 | } 483 | 484 | /** Used for built-in method references. */ 485 | var arrayProto = Array.prototype, 486 | funcProto = Function.prototype, 487 | objectProto$1 = Object.prototype; 488 | 489 | /** Used to detect overreaching core-js shims. */ 490 | var coreJsData = root$1['__core-js_shared__']; 491 | 492 | /** Used to detect methods masquerading as native. */ 493 | var maskSrcKey = (function() { 494 | var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); 495 | return uid ? ('Symbol(src)_1.' + uid) : ''; 496 | }()); 497 | 498 | /** Used to resolve the decompiled source of functions. */ 499 | var funcToString = funcProto.toString; 500 | 501 | /** Used to check objects for own properties. */ 502 | var hasOwnProperty = objectProto$1.hasOwnProperty; 503 | 504 | /** 505 | * Used to resolve the 506 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 507 | * of values. 508 | */ 509 | var objectToString$1 = objectProto$1.toString; 510 | 511 | /** Used to detect if a method is native. */ 512 | var reIsNative = RegExp('^' + 513 | funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') 514 | .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' 515 | ); 516 | 517 | /** Built-in value references. */ 518 | var Symbol$1 = root$1.Symbol, 519 | splice = arrayProto.splice; 520 | 521 | /* Built-in method references that are verified to be native. */ 522 | var Map = getNative(root$1, 'Map'), 523 | nativeCreate = getNative(Object, 'create'); 524 | 525 | /** Used to convert symbols to primitives and strings. */ 526 | var symbolProto$1 = Symbol$1 ? Symbol$1.prototype : undefined, 527 | symbolToString$1 = symbolProto$1 ? symbolProto$1.toString : undefined; 528 | 529 | /** 530 | * Creates a hash object. 531 | * 532 | * @private 533 | * @constructor 534 | * @param {Array} [entries] The key-value pairs to cache. 535 | */ 536 | function Hash(entries) { 537 | var index = -1, 538 | length = entries ? entries.length : 0; 539 | 540 | this.clear(); 541 | while (++index < length) { 542 | var entry = entries[index]; 543 | this.set(entry[0], entry[1]); 544 | } 545 | } 546 | 547 | /** 548 | * Removes all key-value entries from the hash. 549 | * 550 | * @private 551 | * @name clear 552 | * @memberOf Hash 553 | */ 554 | function hashClear() { 555 | this.__data__ = nativeCreate ? nativeCreate(null) : {}; 556 | } 557 | 558 | /** 559 | * Removes `key` and its value from the hash. 560 | * 561 | * @private 562 | * @name delete 563 | * @memberOf Hash 564 | * @param {Object} hash The hash to modify. 565 | * @param {string} key The key of the value to remove. 566 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 567 | */ 568 | function hashDelete(key) { 569 | return this.has(key) && delete this.__data__[key]; 570 | } 571 | 572 | /** 573 | * Gets the hash value for `key`. 574 | * 575 | * @private 576 | * @name get 577 | * @memberOf Hash 578 | * @param {string} key The key of the value to get. 579 | * @returns {*} Returns the entry value. 580 | */ 581 | function hashGet(key) { 582 | var data = this.__data__; 583 | if (nativeCreate) { 584 | var result = data[key]; 585 | return result === HASH_UNDEFINED ? undefined : result; 586 | } 587 | return hasOwnProperty.call(data, key) ? data[key] : undefined; 588 | } 589 | 590 | /** 591 | * Checks if a hash value for `key` exists. 592 | * 593 | * @private 594 | * @name has 595 | * @memberOf Hash 596 | * @param {string} key The key of the entry to check. 597 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 598 | */ 599 | function hashHas(key) { 600 | var data = this.__data__; 601 | return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key); 602 | } 603 | 604 | /** 605 | * Sets the hash `key` to `value`. 606 | * 607 | * @private 608 | * @name set 609 | * @memberOf Hash 610 | * @param {string} key The key of the value to set. 611 | * @param {*} value The value to set. 612 | * @returns {Object} Returns the hash instance. 613 | */ 614 | function hashSet(key, value) { 615 | var data = this.__data__; 616 | data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; 617 | return this; 618 | } 619 | 620 | // Add methods to `Hash`. 621 | Hash.prototype.clear = hashClear; 622 | Hash.prototype['delete'] = hashDelete; 623 | Hash.prototype.get = hashGet; 624 | Hash.prototype.has = hashHas; 625 | Hash.prototype.set = hashSet; 626 | 627 | /** 628 | * Creates an list cache object. 629 | * 630 | * @private 631 | * @constructor 632 | * @param {Array} [entries] The key-value pairs to cache. 633 | */ 634 | function ListCache(entries) { 635 | var index = -1, 636 | length = entries ? entries.length : 0; 637 | 638 | this.clear(); 639 | while (++index < length) { 640 | var entry = entries[index]; 641 | this.set(entry[0], entry[1]); 642 | } 643 | } 644 | 645 | /** 646 | * Removes all key-value entries from the list cache. 647 | * 648 | * @private 649 | * @name clear 650 | * @memberOf ListCache 651 | */ 652 | function listCacheClear() { 653 | this.__data__ = []; 654 | } 655 | 656 | /** 657 | * Removes `key` and its value from the list cache. 658 | * 659 | * @private 660 | * @name delete 661 | * @memberOf ListCache 662 | * @param {string} key The key of the value to remove. 663 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 664 | */ 665 | function listCacheDelete(key) { 666 | var data = this.__data__, 667 | index = assocIndexOf(data, key); 668 | 669 | if (index < 0) { 670 | return false; 671 | } 672 | var lastIndex = data.length - 1; 673 | if (index == lastIndex) { 674 | data.pop(); 675 | } else { 676 | splice.call(data, index, 1); 677 | } 678 | return true; 679 | } 680 | 681 | /** 682 | * Gets the list cache value for `key`. 683 | * 684 | * @private 685 | * @name get 686 | * @memberOf ListCache 687 | * @param {string} key The key of the value to get. 688 | * @returns {*} Returns the entry value. 689 | */ 690 | function listCacheGet(key) { 691 | var data = this.__data__, 692 | index = assocIndexOf(data, key); 693 | 694 | return index < 0 ? undefined : data[index][1]; 695 | } 696 | 697 | /** 698 | * Checks if a list cache value for `key` exists. 699 | * 700 | * @private 701 | * @name has 702 | * @memberOf ListCache 703 | * @param {string} key The key of the entry to check. 704 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 705 | */ 706 | function listCacheHas(key) { 707 | return assocIndexOf(this.__data__, key) > -1; 708 | } 709 | 710 | /** 711 | * Sets the list cache `key` to `value`. 712 | * 713 | * @private 714 | * @name set 715 | * @memberOf ListCache 716 | * @param {string} key The key of the value to set. 717 | * @param {*} value The value to set. 718 | * @returns {Object} Returns the list cache instance. 719 | */ 720 | function listCacheSet(key, value) { 721 | var data = this.__data__, 722 | index = assocIndexOf(data, key); 723 | 724 | if (index < 0) { 725 | data.push([key, value]); 726 | } else { 727 | data[index][1] = value; 728 | } 729 | return this; 730 | } 731 | 732 | // Add methods to `ListCache`. 733 | ListCache.prototype.clear = listCacheClear; 734 | ListCache.prototype['delete'] = listCacheDelete; 735 | ListCache.prototype.get = listCacheGet; 736 | ListCache.prototype.has = listCacheHas; 737 | ListCache.prototype.set = listCacheSet; 738 | 739 | /** 740 | * Creates a map cache object to store key-value pairs. 741 | * 742 | * @private 743 | * @constructor 744 | * @param {Array} [entries] The key-value pairs to cache. 745 | */ 746 | function MapCache(entries) { 747 | var index = -1, 748 | length = entries ? entries.length : 0; 749 | 750 | this.clear(); 751 | while (++index < length) { 752 | var entry = entries[index]; 753 | this.set(entry[0], entry[1]); 754 | } 755 | } 756 | 757 | /** 758 | * Removes all key-value entries from the map. 759 | * 760 | * @private 761 | * @name clear 762 | * @memberOf MapCache 763 | */ 764 | function mapCacheClear() { 765 | this.__data__ = { 766 | 'hash': new Hash, 767 | 'map': new (Map || ListCache), 768 | 'string': new Hash 769 | }; 770 | } 771 | 772 | /** 773 | * Removes `key` and its value from the map. 774 | * 775 | * @private 776 | * @name delete 777 | * @memberOf MapCache 778 | * @param {string} key The key of the value to remove. 779 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 780 | */ 781 | function mapCacheDelete(key) { 782 | return getMapData(this, key)['delete'](key); 783 | } 784 | 785 | /** 786 | * Gets the map value for `key`. 787 | * 788 | * @private 789 | * @name get 790 | * @memberOf MapCache 791 | * @param {string} key The key of the value to get. 792 | * @returns {*} Returns the entry value. 793 | */ 794 | function mapCacheGet(key) { 795 | return getMapData(this, key).get(key); 796 | } 797 | 798 | /** 799 | * Checks if a map value for `key` exists. 800 | * 801 | * @private 802 | * @name has 803 | * @memberOf MapCache 804 | * @param {string} key The key of the entry to check. 805 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 806 | */ 807 | function mapCacheHas(key) { 808 | return getMapData(this, key).has(key); 809 | } 810 | 811 | /** 812 | * Sets the map `key` to `value`. 813 | * 814 | * @private 815 | * @name set 816 | * @memberOf MapCache 817 | * @param {string} key The key of the value to set. 818 | * @param {*} value The value to set. 819 | * @returns {Object} Returns the map cache instance. 820 | */ 821 | function mapCacheSet(key, value) { 822 | getMapData(this, key).set(key, value); 823 | return this; 824 | } 825 | 826 | // Add methods to `MapCache`. 827 | MapCache.prototype.clear = mapCacheClear; 828 | MapCache.prototype['delete'] = mapCacheDelete; 829 | MapCache.prototype.get = mapCacheGet; 830 | MapCache.prototype.has = mapCacheHas; 831 | MapCache.prototype.set = mapCacheSet; 832 | 833 | /** 834 | * Assigns `value` to `key` of `object` if the existing value is not equivalent 835 | * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) 836 | * for equality comparisons. 837 | * 838 | * @private 839 | * @param {Object} object The object to modify. 840 | * @param {string} key The key of the property to assign. 841 | * @param {*} value The value to assign. 842 | */ 843 | function assignValue(object, key, value) { 844 | var objValue = object[key]; 845 | if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || 846 | (value === undefined && !(key in object))) { 847 | object[key] = value; 848 | } 849 | } 850 | 851 | /** 852 | * Gets the index at which the `key` is found in `array` of key-value pairs. 853 | * 854 | * @private 855 | * @param {Array} array The array to inspect. 856 | * @param {*} key The key to search for. 857 | * @returns {number} Returns the index of the matched value, else `-1`. 858 | */ 859 | function assocIndexOf(array, key) { 860 | var length = array.length; 861 | while (length--) { 862 | if (eq(array[length][0], key)) { 863 | return length; 864 | } 865 | } 866 | return -1; 867 | } 868 | 869 | /** 870 | * The base implementation of `_.isNative` without bad shim checks. 871 | * 872 | * @private 873 | * @param {*} value The value to check. 874 | * @returns {boolean} Returns `true` if `value` is a native function, 875 | * else `false`. 876 | */ 877 | function baseIsNative(value) { 878 | if (!isObject(value) || isMasked(value)) { 879 | return false; 880 | } 881 | var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; 882 | return pattern.test(toSource(value)); 883 | } 884 | 885 | /** 886 | * The base implementation of `_.set`. 887 | * 888 | * @private 889 | * @param {Object} object The object to modify. 890 | * @param {Array|string} path The path of the property to set. 891 | * @param {*} value The value to set. 892 | * @param {Function} [customizer] The function to customize path creation. 893 | * @returns {Object} Returns `object`. 894 | */ 895 | function baseSet(object, path, value, customizer) { 896 | if (!isObject(object)) { 897 | return object; 898 | } 899 | path = isKey(path, object) ? [path] : castPath(path); 900 | 901 | var index = -1, 902 | length = path.length, 903 | lastIndex = length - 1, 904 | nested = object; 905 | 906 | while (nested != null && ++index < length) { 907 | var key = toKey(path[index]), 908 | newValue = value; 909 | 910 | if (index != lastIndex) { 911 | var objValue = nested[key]; 912 | newValue = customizer ? customizer(objValue, key, nested) : undefined; 913 | if (newValue === undefined) { 914 | newValue = isObject(objValue) 915 | ? objValue 916 | : (isIndex(path[index + 1]) ? [] : {}); 917 | } 918 | } 919 | assignValue(nested, key, newValue); 920 | nested = nested[key]; 921 | } 922 | return object; 923 | } 924 | 925 | /** 926 | * The base implementation of `_.toString` which doesn't convert nullish 927 | * values to empty strings. 928 | * 929 | * @private 930 | * @param {*} value The value to process. 931 | * @returns {string} Returns the string. 932 | */ 933 | function baseToString$1(value) { 934 | // Exit early for strings to avoid a performance hit in some environments. 935 | if (typeof value == 'string') { 936 | return value; 937 | } 938 | if (isSymbol$1(value)) { 939 | return symbolToString$1 ? symbolToString$1.call(value) : ''; 940 | } 941 | var result = (value + ''); 942 | return (result == '0' && (1 / value) == -INFINITY$1) ? '-0' : result; 943 | } 944 | 945 | /** 946 | * Casts `value` to a path array if it's not one. 947 | * 948 | * @private 949 | * @param {*} value The value to inspect. 950 | * @returns {Array} Returns the cast property path array. 951 | */ 952 | function castPath(value) { 953 | return isArray(value) ? value : stringToPath(value); 954 | } 955 | 956 | /** 957 | * Gets the data for `map`. 958 | * 959 | * @private 960 | * @param {Object} map The map to query. 961 | * @param {string} key The reference key. 962 | * @returns {*} Returns the map data. 963 | */ 964 | function getMapData(map, key) { 965 | var data = map.__data__; 966 | return isKeyable(key) 967 | ? data[typeof key == 'string' ? 'string' : 'hash'] 968 | : data.map; 969 | } 970 | 971 | /** 972 | * Gets the native function at `key` of `object`. 973 | * 974 | * @private 975 | * @param {Object} object The object to query. 976 | * @param {string} key The key of the method to get. 977 | * @returns {*} Returns the function if it's native, else `undefined`. 978 | */ 979 | function getNative(object, key) { 980 | var value = getValue(object, key); 981 | return baseIsNative(value) ? value : undefined; 982 | } 983 | 984 | /** 985 | * Checks if `value` is a valid array-like index. 986 | * 987 | * @private 988 | * @param {*} value The value to check. 989 | * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. 990 | * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. 991 | */ 992 | function isIndex(value, length) { 993 | length = length == null ? MAX_SAFE_INTEGER : length; 994 | return !!length && 995 | (typeof value == 'number' || reIsUint.test(value)) && 996 | (value > -1 && value % 1 == 0 && value < length); 997 | } 998 | 999 | /** 1000 | * Checks if `value` is a property name and not a property path. 1001 | * 1002 | * @private 1003 | * @param {*} value The value to check. 1004 | * @param {Object} [object] The object to query keys on. 1005 | * @returns {boolean} Returns `true` if `value` is a property name, else `false`. 1006 | */ 1007 | function isKey(value, object) { 1008 | if (isArray(value)) { 1009 | return false; 1010 | } 1011 | var type = typeof value; 1012 | if (type == 'number' || type == 'symbol' || type == 'boolean' || 1013 | value == null || isSymbol$1(value)) { 1014 | return true; 1015 | } 1016 | return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || 1017 | (object != null && value in Object(object)); 1018 | } 1019 | 1020 | /** 1021 | * Checks if `value` is suitable for use as unique object key. 1022 | * 1023 | * @private 1024 | * @param {*} value The value to check. 1025 | * @returns {boolean} Returns `true` if `value` is suitable, else `false`. 1026 | */ 1027 | function isKeyable(value) { 1028 | var type = typeof value; 1029 | return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') 1030 | ? (value !== '__proto__') 1031 | : (value === null); 1032 | } 1033 | 1034 | /** 1035 | * Checks if `func` has its source masked. 1036 | * 1037 | * @private 1038 | * @param {Function} func The function to check. 1039 | * @returns {boolean} Returns `true` if `func` is masked, else `false`. 1040 | */ 1041 | function isMasked(func) { 1042 | return !!maskSrcKey && (maskSrcKey in func); 1043 | } 1044 | 1045 | /** 1046 | * Converts `string` to a property path array. 1047 | * 1048 | * @private 1049 | * @param {string} string The string to convert. 1050 | * @returns {Array} Returns the property path array. 1051 | */ 1052 | var stringToPath = memoize(function(string) { 1053 | string = toString$1(string); 1054 | 1055 | var result = []; 1056 | if (reLeadingDot.test(string)) { 1057 | result.push(''); 1058 | } 1059 | string.replace(rePropName, function(match, number, quote, string) { 1060 | result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match)); 1061 | }); 1062 | return result; 1063 | }); 1064 | 1065 | /** 1066 | * Converts `value` to a string key if it's not a string or symbol. 1067 | * 1068 | * @private 1069 | * @param {*} value The value to inspect. 1070 | * @returns {string|symbol} Returns the key. 1071 | */ 1072 | function toKey(value) { 1073 | if (typeof value == 'string' || isSymbol$1(value)) { 1074 | return value; 1075 | } 1076 | var result = (value + ''); 1077 | return (result == '0' && (1 / value) == -INFINITY$1) ? '-0' : result; 1078 | } 1079 | 1080 | /** 1081 | * Converts `func` to its source code. 1082 | * 1083 | * @private 1084 | * @param {Function} func The function to process. 1085 | * @returns {string} Returns the source code. 1086 | */ 1087 | function toSource(func) { 1088 | if (func != null) { 1089 | try { 1090 | return funcToString.call(func); 1091 | } catch (e) {} 1092 | try { 1093 | return (func + ''); 1094 | } catch (e) {} 1095 | } 1096 | return ''; 1097 | } 1098 | 1099 | /** 1100 | * Creates a function that memoizes the result of `func`. If `resolver` is 1101 | * provided, it determines the cache key for storing the result based on the 1102 | * arguments provided to the memoized function. By default, the first argument 1103 | * provided to the memoized function is used as the map cache key. The `func` 1104 | * is invoked with the `this` binding of the memoized function. 1105 | * 1106 | * **Note:** The cache is exposed as the `cache` property on the memoized 1107 | * function. Its creation may be customized by replacing the `_.memoize.Cache` 1108 | * constructor with one whose instances implement the 1109 | * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) 1110 | * method interface of `delete`, `get`, `has`, and `set`. 1111 | * 1112 | * @static 1113 | * @memberOf _ 1114 | * @since 0.1.0 1115 | * @category Function 1116 | * @param {Function} func The function to have its output memoized. 1117 | * @param {Function} [resolver] The function to resolve the cache key. 1118 | * @returns {Function} Returns the new memoized function. 1119 | * @example 1120 | * 1121 | * var object = { 'a': 1, 'b': 2 }; 1122 | * var other = { 'c': 3, 'd': 4 }; 1123 | * 1124 | * var values = _.memoize(_.values); 1125 | * values(object); 1126 | * // => [1, 2] 1127 | * 1128 | * values(other); 1129 | * // => [3, 4] 1130 | * 1131 | * object.a = 2; 1132 | * values(object); 1133 | * // => [1, 2] 1134 | * 1135 | * // Modify the result cache. 1136 | * values.cache.set(object, ['a', 'b']); 1137 | * values(object); 1138 | * // => ['a', 'b'] 1139 | * 1140 | * // Replace `_.memoize.Cache`. 1141 | * _.memoize.Cache = WeakMap; 1142 | */ 1143 | function memoize(func, resolver) { 1144 | if (typeof func != 'function' || (resolver && typeof resolver != 'function')) { 1145 | throw new TypeError(FUNC_ERROR_TEXT); 1146 | } 1147 | var memoized = function() { 1148 | var args = arguments, 1149 | key = resolver ? resolver.apply(this, args) : args[0], 1150 | cache = memoized.cache; 1151 | 1152 | if (cache.has(key)) { 1153 | return cache.get(key); 1154 | } 1155 | var result = func.apply(this, args); 1156 | memoized.cache = cache.set(key, result); 1157 | return result; 1158 | }; 1159 | memoized.cache = new (memoize.Cache || MapCache); 1160 | return memoized; 1161 | } 1162 | 1163 | // Assign cache to `_.memoize`. 1164 | memoize.Cache = MapCache; 1165 | 1166 | /** 1167 | * Performs a 1168 | * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) 1169 | * comparison between two values to determine if they are equivalent. 1170 | * 1171 | * @static 1172 | * @memberOf _ 1173 | * @since 4.0.0 1174 | * @category Lang 1175 | * @param {*} value The value to compare. 1176 | * @param {*} other The other value to compare. 1177 | * @returns {boolean} Returns `true` if the values are equivalent, else `false`. 1178 | * @example 1179 | * 1180 | * var object = { 'a': 1 }; 1181 | * var other = { 'a': 1 }; 1182 | * 1183 | * _.eq(object, object); 1184 | * // => true 1185 | * 1186 | * _.eq(object, other); 1187 | * // => false 1188 | * 1189 | * _.eq('a', 'a'); 1190 | * // => true 1191 | * 1192 | * _.eq('a', Object('a')); 1193 | * // => false 1194 | * 1195 | * _.eq(NaN, NaN); 1196 | * // => true 1197 | */ 1198 | function eq(value, other) { 1199 | return value === other || (value !== value && other !== other); 1200 | } 1201 | 1202 | /** 1203 | * Checks if `value` is classified as an `Array` object. 1204 | * 1205 | * @static 1206 | * @memberOf _ 1207 | * @since 0.1.0 1208 | * @category Lang 1209 | * @param {*} value The value to check. 1210 | * @returns {boolean} Returns `true` if `value` is an array, else `false`. 1211 | * @example 1212 | * 1213 | * _.isArray([1, 2, 3]); 1214 | * // => true 1215 | * 1216 | * _.isArray(document.body.children); 1217 | * // => false 1218 | * 1219 | * _.isArray('abc'); 1220 | * // => false 1221 | * 1222 | * _.isArray(_.noop); 1223 | * // => false 1224 | */ 1225 | var isArray = Array.isArray; 1226 | 1227 | /** 1228 | * Checks if `value` is classified as a `Function` object. 1229 | * 1230 | * @static 1231 | * @memberOf _ 1232 | * @since 0.1.0 1233 | * @category Lang 1234 | * @param {*} value The value to check. 1235 | * @returns {boolean} Returns `true` if `value` is a function, else `false`. 1236 | * @example 1237 | * 1238 | * _.isFunction(_); 1239 | * // => true 1240 | * 1241 | * _.isFunction(/abc/); 1242 | * // => false 1243 | */ 1244 | function isFunction(value) { 1245 | // The use of `Object#toString` avoids issues with the `typeof` operator 1246 | // in Safari 8-9 which returns 'object' for typed array and other constructors. 1247 | var tag = isObject(value) ? objectToString$1.call(value) : ''; 1248 | return tag == funcTag || tag == genTag; 1249 | } 1250 | 1251 | /** 1252 | * Checks if `value` is the 1253 | * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) 1254 | * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 1255 | * 1256 | * @static 1257 | * @memberOf _ 1258 | * @since 0.1.0 1259 | * @category Lang 1260 | * @param {*} value The value to check. 1261 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 1262 | * @example 1263 | * 1264 | * _.isObject({}); 1265 | * // => true 1266 | * 1267 | * _.isObject([1, 2, 3]); 1268 | * // => true 1269 | * 1270 | * _.isObject(_.noop); 1271 | * // => true 1272 | * 1273 | * _.isObject(null); 1274 | * // => false 1275 | */ 1276 | function isObject(value) { 1277 | var type = typeof value; 1278 | return !!value && (type == 'object' || type == 'function'); 1279 | } 1280 | 1281 | /** 1282 | * Checks if `value` is object-like. A value is object-like if it's not `null` 1283 | * and has a `typeof` result of "object". 1284 | * 1285 | * @static 1286 | * @memberOf _ 1287 | * @since 4.0.0 1288 | * @category Lang 1289 | * @param {*} value The value to check. 1290 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 1291 | * @example 1292 | * 1293 | * _.isObjectLike({}); 1294 | * // => true 1295 | * 1296 | * _.isObjectLike([1, 2, 3]); 1297 | * // => true 1298 | * 1299 | * _.isObjectLike(_.noop); 1300 | * // => false 1301 | * 1302 | * _.isObjectLike(null); 1303 | * // => false 1304 | */ 1305 | function isObjectLike$1(value) { 1306 | return !!value && typeof value == 'object'; 1307 | } 1308 | 1309 | /** 1310 | * Checks if `value` is classified as a `Symbol` primitive or object. 1311 | * 1312 | * @static 1313 | * @memberOf _ 1314 | * @since 4.0.0 1315 | * @category Lang 1316 | * @param {*} value The value to check. 1317 | * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. 1318 | * @example 1319 | * 1320 | * _.isSymbol(Symbol.iterator); 1321 | * // => true 1322 | * 1323 | * _.isSymbol('abc'); 1324 | * // => false 1325 | */ 1326 | function isSymbol$1(value) { 1327 | return typeof value == 'symbol' || 1328 | (isObjectLike$1(value) && objectToString$1.call(value) == symbolTag$1); 1329 | } 1330 | 1331 | /** 1332 | * Converts `value` to a string. An empty string is returned for `null` 1333 | * and `undefined` values. The sign of `-0` is preserved. 1334 | * 1335 | * @static 1336 | * @memberOf _ 1337 | * @since 4.0.0 1338 | * @category Lang 1339 | * @param {*} value The value to process. 1340 | * @returns {string} Returns the string. 1341 | * @example 1342 | * 1343 | * _.toString(null); 1344 | * // => '' 1345 | * 1346 | * _.toString(-0); 1347 | * // => '-0' 1348 | * 1349 | * _.toString([1, 2, 3]); 1350 | * // => '1,2,3' 1351 | */ 1352 | function toString$1(value) { 1353 | return value == null ? '' : baseToString$1(value); 1354 | } 1355 | 1356 | /** 1357 | * Sets the value at `path` of `object`. If a portion of `path` doesn't exist, 1358 | * it's created. Arrays are created for missing index properties while objects 1359 | * are created for all other missing properties. Use `_.setWith` to customize 1360 | * `path` creation. 1361 | * 1362 | * **Note:** This method mutates `object`. 1363 | * 1364 | * @static 1365 | * @memberOf _ 1366 | * @since 3.7.0 1367 | * @category Object 1368 | * @param {Object} object The object to modify. 1369 | * @param {Array|string} path The path of the property to set. 1370 | * @param {*} value The value to set. 1371 | * @returns {Object} Returns `object`. 1372 | * @example 1373 | * 1374 | * var object = { 'a': [{ 'b': { 'c': 3 } }] }; 1375 | * 1376 | * _.set(object, 'a[0].b.c', 4); 1377 | * console.log(object.a[0].b.c); 1378 | * // => 4 1379 | * 1380 | * _.set(object, ['x', '0', 'y', 'z'], 5); 1381 | * console.log(object.x[0].y.z); 1382 | * // => 5 1383 | */ 1384 | function set(object, path, value) { 1385 | return object == null ? object : baseSet(object, path, value); 1386 | } 1387 | 1388 | var lodash_set = set; 1389 | 1390 | const saveMiddleware = (fields) => function (next) { 1391 | for (const [key, fn] of Object.entries(fields)) { 1392 | this.set(key, sentenceTrigrams(fn(this))); 1393 | } 1394 | next(); 1395 | }; 1396 | 1397 | const insertManyMiddleware = (fields) => function (next, docs) { 1398 | const Ctr = this; 1399 | docs.forEach((doc) => { 1400 | for (const [path, fn] of Object.entries(fields)) { 1401 | // we create an instance so the document given to the getter will have the Document API 1402 | const instance = new Ctr(doc); 1403 | // mutate doc in place only at trigram paths 1404 | lodash_set(doc, path, sentenceTrigrams(fn(instance))); 1405 | } 1406 | }); 1407 | next(); 1408 | }; 1409 | 1410 | function plugin(schema, {fields = {}, select = false} = {}) { 1411 | 1412 | const normalizedFields = normalizeFields(fields); 1413 | const normalizeInput = normalizeInputFactory(fields); 1414 | const preSave = saveMiddleware(normalizedFields); 1415 | const preInsertMany = insertManyMiddleware(normalizedFields); 1416 | 1417 | schema.add(createSchemaFields({fields, select})); 1418 | schema.pre('save', preSave); 1419 | schema.pre('insertMany', preInsertMany); 1420 | 1421 | schema.statics.fuzzy = function (input) { 1422 | const normalizedInput = normalizeInput(input); 1423 | const matchClause = buildMatchClause(normalizedInput); 1424 | const similarity = buildSimilarityClause(normalizedInput); 1425 | return this.aggregate([ 1426 | {$match: matchClause}, 1427 | { 1428 | $project: { 1429 | _id: 0, 1430 | document: '$$CURRENT', 1431 | similarity 1432 | } 1433 | } 1434 | ]); 1435 | }; 1436 | } 1437 | 1438 | module.exports = plugin; 1439 | --------------------------------------------------------------------------------