Skip to content

Commit

Permalink
Introduce combination with NOT
Browse files Browse the repository at this point in the history
The NOT combination can be used to search for documents containing a
term but NOT another, like:

```
// Search for documents containing 'nova' but NOT 'bossa'
ms.search({
  combineWith: 'NOT',
  queries: ['nova', 'bossa']
})
```
  • Loading branch information
lucaong committed Nov 23, 2021
1 parent 8c66bd2 commit b7bc831
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 10 deletions.
16 changes: 15 additions & 1 deletion src/MiniSearch.test.js
Expand Up @@ -529,6 +529,20 @@ describe('MiniSearch', () => {
expect(ms.search('sottomarino vita', { combineWith: 'AND' }).length).toEqual(0)
})

it('combines results with NOT if combineWith is NOT', () => {
const results = ms.search('vita cammin', { combineWith: 'NOT' })
expect(results.length).toEqual(1)
expect(results.map(({ id }) => id)).toEqual([3])
expect(ms.search('vita sottomarino', { combineWith: 'NOT' }).length).toEqual(2)
expect(ms.search('sottomarino vita', { combineWith: 'NOT' }).length).toEqual(0)
})

it('returns empty results for empty search', () => {
expect(ms.search('').length).toEqual(0)
expect(ms.search('', { combineWith: 'AND' }).length).toEqual(0)
expect(ms.search('', { combineWith: 'NOT' }).length).toEqual(0)
})

it('executes fuzzy search', () => {
const results = ms.search('camin memory', { fuzzy: 2 })
expect(results.length).toEqual(2)
Expand Down Expand Up @@ -609,7 +623,7 @@ describe('MiniSearch', () => {
})

describe('when passing a query tree', () => {
it('searches according to the given combination of AND and OR', () => {
it('searches according to the given combination', () => {
const results = ms.search({
combineWith: 'OR',
queries: [
Expand Down
43 changes: 34 additions & 9 deletions src/MiniSearch.ts
Expand Up @@ -2,6 +2,7 @@ import SearchableMap from './SearchableMap/SearchableMap'

const OR = 'or'
const AND = 'and'
const NOT = 'not'

/**
* Search options to customize the search behavior.
Expand Down Expand Up @@ -664,6 +665,13 @@ export default class MiniSearch<T = any> {
* miniSearch.search('motorcycle art', { combineWith: 'AND' })
* ```
*
* ### NOT combinator:
*
* There is also a NOT combinator, that finds documents that match the first
* term, but do not match any of the other terms. This combinator is rarely
* useful with simple queries, and is meant to be used with advanced query
* combinations (see later for more details).
*
* ### Filtering results:
*
* ```javascript
Expand All @@ -676,9 +684,9 @@ export default class MiniSearch<T = any> {
*
* ### Advanced combination of queries:
*
* It is possible to combine different subqueries with OR and AND, and even
* with different search options, by passing a query expression tree object as
* the first argument, instead of a string.
* It is possible to combine different subqueries with OR, AND, and NOT, and
* even with different search options, by passing a query expression tree
* object as the first argument, instead of a string.
*
* ```javascript
* // Search for documents that contain "zen" AND ("motorcycle" OR "archery")
Expand All @@ -692,6 +700,18 @@ export default class MiniSearch<T = any> {
* }
* ]
* })
*
* // Search for documents that contain "zen" AND "art" but NOT "war"
* miniSearch.search({
* combineWith: 'NOT',
* queries: [
* {
* combineWith: 'AND',
* queries: ['zen', 'art']
* },
* 'war'
* ]
* })
* ```
*
* Each node in the expression tree can be either a string, or an object that
Expand Down Expand Up @@ -961,7 +981,7 @@ export default class MiniSearch<T = any> {
})
}

return results.reduce(combinators[OR], {})
return results.reduce(combinators[OR])
}

/**
Expand All @@ -970,7 +990,7 @@ export default class MiniSearch<T = any> {
private combineResults (results: RawResult[], combineWith = OR): RawResult {
if (results.length === 0) { return {} }
const operator = combineWith.toLowerCase()
return results.reduce(combinators[operator], null) || {}
return results.reduce(combinators[operator]) || {}
}

/**
Expand Down Expand Up @@ -1155,10 +1175,10 @@ export default class MiniSearch<T = any> {
const getOwnProperty = (object: any, property: string) =>
Object.prototype.hasOwnProperty.call(object, property) ? object[property] : undefined

type CombinatorFunction = (a: RawResult | null, b: RawResult) => RawResult
type CombinatorFunction = (a: RawResult, b: RawResult) => RawResult

const combinators: { [kind: string]: CombinatorFunction } = {
[OR]: (a: RawResult | null, b: RawResult) => {
[OR]: (a: RawResult, b: RawResult) => {
return Object.entries(b).reduce((combined: RawResult, [documentId, { score, match, terms }]) => {
if (combined[documentId] == null) {
combined[documentId] = { score, match, terms }
Expand All @@ -1171,8 +1191,7 @@ const combinators: { [kind: string]: CombinatorFunction } = {
return combined
}, a || {})
},
[AND]: (a: RawResult | null, b: RawResult) => {
if (a == null) { return b }
[AND]: (a: RawResult, b: RawResult) => {
return Object.entries(b).reduce((combined: RawResult, [documentId, { score, match, terms }]) => {
if (a[documentId] === undefined) { return combined }
combined[documentId] = combined[documentId] || {}
Expand All @@ -1181,6 +1200,12 @@ const combinators: { [kind: string]: CombinatorFunction } = {
combined[documentId].terms = [...a[documentId].terms, ...terms]
return combined
}, {})
},
[NOT]: (a: RawResult, b: RawResult) => {
return Object.entries(b).reduce((combined: RawResult, [documentId, { score, match, terms }]) => {
delete combined[documentId]
return combined
}, a || {})
}
}

Expand Down

0 comments on commit b7bc831

Please sign in to comment.