From 40562cc35592ae337c8b9d18757930eb18063e03 Mon Sep 17 00:00:00 2001 From: Tommaso Allevi Date: Tue, 19 Sep 2023 18:02:44 +0200 Subject: [PATCH] Implements enum[] (#482) --- packages/docs/pages/usage/create.mdx | 1 + packages/docs/pages/usage/search/facets.mdx | 17 ++ packages/docs/pages/usage/search/filters.mdx | 11 +- packages/orama/src/components/defaults.ts | 11 + packages/orama/src/components/facets.ts | 19 +- packages/orama/src/components/index.ts | 73 +++--- packages/orama/src/components/sorter.ts | 3 + packages/orama/src/errors.ts | 1 + packages/orama/src/methods/insert.ts | 2 +- packages/orama/src/trees/flat.ts | 22 +- packages/orama/src/types.ts | 12 +- packages/orama/tests/enum.test.ts | 210 ++++++++++++++++++ packages/orama/tests/facets.test.ts | 63 +++++- .../test/index.test.ts | 92 +++++++- 14 files changed, 484 insertions(+), 53 deletions(-) diff --git a/packages/docs/pages/usage/create.mdx b/packages/docs/pages/usage/create.mdx index 2d5483e31..3550450b3 100644 --- a/packages/docs/pages/usage/create.mdx +++ b/packages/docs/pages/usage/create.mdx @@ -22,6 +22,7 @@ Orama supports the following types: | `string[]` | An array of strings. | `['red', 'green', 'blue']` | | `number[]` | An array of numbers. | `[42, 91, 28.5]` | | `boolean[]` | An array of booleans. | `[true, false, false]` | +| `enum[]` | An array of enums. | `['comedy', 'action', 'romance']` | | `vector[]` | A vector of numbers to perform vector search on. | `[0.403, 0.192, 0.830]` | A database can be as simple as: diff --git a/packages/docs/pages/usage/search/facets.mdx b/packages/docs/pages/usage/search/facets.mdx index ba14abf17..67869dc89 100644 --- a/packages/docs/pages/usage/search/facets.mdx +++ b/packages/docs/pages/usage/search/facets.mdx @@ -178,6 +178,22 @@ In the search result, `boolean` facets will be returned as an `object` with the } ``` +### Enum facets + +If a property is specified as `enum` in the schema, no configuration is required. +In the search result, `enum` facets will be returned as an `object` with the following properties: + +```js +{ + count: 9, // Total number of values + values: { + 'Action': 4, // Number of documents that have this value + 'Adventure': 3, // Number of documents that have this value + 'Comedy': 2, // Number of documents that have this value + } +} +``` + ### How facets works on array fields Orama treats each array element as a single element of the facet: @@ -209,3 +225,4 @@ const results = await search(db, { } } ``` + diff --git a/packages/docs/pages/usage/search/filters.mdx b/packages/docs/pages/usage/search/filters.mdx index 1d2ab27ee..7d0eb339c 100644 --- a/packages/docs/pages/usage/search/filters.mdx +++ b/packages/docs/pages/usage/search/filters.mdx @@ -113,10 +113,19 @@ const results = await search(db, { ## Enum operators -The numeric properties support the following operators: +The enum properties support the following operators: | Operator | Description | Example | | --------- | ------------------------------ | --------------------------------- | | `eq` | Equal to | `genre: { eq: 'drama' }` | | `in` | Contained in the given array | `genre: { in: ['drama', 'horror'] }` | | `nin` | Not contained in the given array | `genre: { nin: ['commedy'] }` | + + +## Enum[] operators + +The enum properties support the following operators: + +| Operator | Description | Example | +| --------- | ------------------------------ | --------------------------------- | +| `containsAll` | Contains all the given values | `genre: { containsAll: ['comedy', 'action'] }` | diff --git a/packages/orama/src/components/defaults.ts b/packages/orama/src/components/defaults.ts index ce2039e89..c6b14118c 100644 --- a/packages/orama/src/components/defaults.ts +++ b/packages/orama/src/components/defaults.ts @@ -46,6 +46,15 @@ export async function validateSchema = { 'string[]': true, 'number[]': true, 'boolean[]': true, + 'enum[]': true, } const INNER_TYPE: Record = { 'string[]': 'string', 'number[]': 'number', 'boolean[]': 'boolean', + 'enum[]': 'enum', } export function isVectorType(type: unknown): type is Vector { diff --git a/packages/orama/src/components/facets.ts b/packages/orama/src/components/facets.ts index 43bf285e5..2dd507559 100644 --- a/packages/orama/src/components/facets.ts +++ b/packages/orama/src/components/facets.ts @@ -1,3 +1,4 @@ +import { createError } from '../errors.js' import type { AnyOrama, FacetResult, @@ -10,6 +11,8 @@ import type { } from '../types.js' import { getNested } from '../utils.js' +type FacetValue = string | boolean | number + function sortingPredicate(order: FacetSorting = 'desc', a: [string, number], b: [string, number]) { if (order.toLowerCase() === 'asc') { return a[1] - b[1] @@ -75,19 +78,23 @@ export async function getFacets( break } case 'boolean': + case 'enum': case 'string': { - calculateBooleanOrStringFacet(facets[facet].values, facetValue as string | boolean, propertyType) + calculateBooleanStringOrEnumFacet(facets[facet].values, facetValue as FacetValue, propertyType) break } case 'boolean[]': + case 'enum[]': case 'string[]': { const alreadyInsertedValues = new Set() const innerType = propertyType === 'boolean[]' ? 'boolean' : 'string' - for (const v of facetValue as Array) { - calculateBooleanOrStringFacet(facets[facet].values, v, innerType, alreadyInsertedValues) + for (const v of facetValue as Array) { + calculateBooleanStringOrEnumFacet(facets[facet].values, v, innerType, alreadyInsertedValues) } break } + default: + throw createError('FACET_NOT_SUPPORTED', propertyType) } } } @@ -137,10 +144,10 @@ function calculateNumberFacet( } } -function calculateBooleanOrStringFacet( +function calculateBooleanStringOrEnumFacet( values: Record, - facetValue: string | boolean, - propertyType: 'string' | 'boolean', + facetValue: FacetValue, + propertyType: 'string' | 'boolean' | 'enum', alreadyInsertedValues?: Set, ) { // String or boolean based facets diff --git a/packages/orama/src/components/index.ts b/packages/orama/src/components/index.ts index ed3748b7b..862a82f8e 100644 --- a/packages/orama/src/components/index.ts +++ b/packages/orama/src/components/index.ts @@ -1,3 +1,22 @@ +import type { + AnyIndexStore, + AnyOrama, + ArraySearchableType, + BM25Params, + ComparisonOperator, + EnumArrComparisonOperator, + EnumComparisonOperator, + IIndex, + ScalarSearchableType, + SearchableType, + SearchableValue, + SearchContext, + Tokenizer, + TokenScore, + TypedDocument, + VectorIndex, + VectorType, +} from '../types.js' import { createError } from '../errors.js' import { create as avlCreate, @@ -12,6 +31,7 @@ import { import { create as flatCreate, filter as flatFilter, + filterArr as flatFilterArr, insert as flatInsert, removeDocument as flatRemoveDocument, FlatTree, @@ -23,24 +43,7 @@ import { Node as RadixNode, removeDocumentByWord as radixRemoveDocument, } from '../trees/radix.js' -import type { - AnyIndexStore, - AnyOrama, - ArraySearchableType, - BM25Params, - ComparisonOperator, - EnumComparisonOperator, - IIndex, - ScalarSearchableType, - SearchableType, - SearchableValue, - SearchContext, - Tokenizer, - TokenScore, - TypedDocument, - VectorIndex, - VectorType -} from '../types.js' + import { intersect, safeArrayPush } from '../utils.js' import { BM25 } from './algorithms.js' import { getMagnitude } from './cosine-similarity.js' @@ -76,6 +79,7 @@ export type TreeType = export type TTree = { type: T, node: N + isArray: boolean } export type Tree = @@ -234,25 +238,27 @@ export async function create( vectors: {}, } } else { + const isArray = /\[/.test(type as string) switch (type) { case 'boolean': case 'boolean[]': - index.indexes[path] = { type: 'Bool', node: { true: [], false: [] } } + index.indexes[path] = { type: 'Bool', node: { true: [], false: [] }, isArray } break case 'number': case 'number[]': - index.indexes[path] = { type: 'AVL', node: avlCreate(0, []) } + index.indexes[path] = { type: 'AVL', node: avlCreate(0, []), isArray } break case 'string': case 'string[]': - index.indexes[path] = { type: 'Radix', node: radixCreate() } + index.indexes[path] = { type: 'Radix', node: radixCreate(), isArray } index.avgFieldLength[path] = 0 index.frequencies[path] = {} index.tokenOccurrences[path] = {} index.fieldLengths[path] = {} break case 'enum': - index.indexes[path] = { type: 'Flat', node: flatCreate() } + case 'enum[]': + index.indexes[path] = { type: 'Flat', node: flatCreate(), isArray } break default: throw createError('INVALID_SCHEMA_TYPE', Array.isArray(type) ? 'array' : type, path) @@ -468,7 +474,7 @@ export async function search>( context: SearchContext, index: Index, - filters: Record, + filters: Record, ): Promise { const filterKeys = Object.keys(filters) @@ -487,7 +493,7 @@ export async function searchByWhereClause(sharedInternalDocumentStore: InternalDoc const vectorIndexes: Index['vectorIndexes'] = {} for (const prop of Object.keys(rawIndexes)) { - const { node, type } = rawIndexes[prop] + const { node, type, isArray } = rawIndexes[prop] switch (type) { case 'Radix': indexes[prop] = { type: 'Radix', - node: loadRadixNode(node) + node: loadRadixNode(node), + isArray } break case 'Flat': indexes[prop] = { type: 'Flat', - node: loadFlatNode(node) + node: loadFlatNode(node), + isArray } break default: @@ -684,14 +696,15 @@ export async function save(index: Index): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const savedIndexes: any = {} for (const name of Object.keys(indexes)) { - const {type, node} = indexes[name] + const {type, node, isArray} = indexes[name] if (type !== 'Flat') { savedIndexes[name] = indexes[name] continue } savedIndexes[name] = { type: 'Flat', - node: saveFlatNode(node) + node: saveFlatNode(node), + isArray, } } diff --git a/packages/orama/src/components/sorter.ts b/packages/orama/src/components/sorter.ts index c8b5e8809..314e8f68f 100644 --- a/packages/orama/src/components/sorter.ts +++ b/packages/orama/src/components/sorter.ts @@ -83,6 +83,9 @@ function innerCreate( } break case 'enum': + // We don't allow to sort by enums + continue + case 'enum[]': case 'boolean[]': case 'number[]': case 'string[]': diff --git a/packages/orama/src/errors.ts b/packages/orama/src/errors.ts index d0f46427b..0387cc730 100644 --- a/packages/orama/src/errors.ts +++ b/packages/orama/src/errors.ts @@ -33,6 +33,7 @@ const errors = { INVALID_VECTOR_VALUE: `Vector value must be a number greater than 0. Got "%s" instead.`, INVALID_INPUT_VECTOR: `Property "%s" was declared as a %s-dimentional vector, but got a %s-dimentional vector instead.\nInput vectors must be of the size declared in the schema, as calculating similarity between vectors of different sizes can lead to unexpected results.`, WRONG_SEARCH_PROPERTY_TYPE: `Property "%s" is not searchable. Only "string" properties are searchable.`, + FACET_NOT_SUPPORTED: `Facet doens't support the type "%s".`, } export type ErrorCode = keyof typeof errors diff --git a/packages/orama/src/methods/insert.ts b/packages/orama/src/methods/insert.ts index f610f612f..d8c6756cc 100644 --- a/packages/orama/src/methods/insert.ts +++ b/packages/orama/src/methods/insert.ts @@ -63,7 +63,7 @@ async function innerInsert( continue } - if (expectedType === 'enum' && (actualType === 'string' || actualType === 'number')) { + if ((expectedType === 'enum' || expectedType === 'enum[]') && (actualType === 'string' || actualType === 'number')) { continue } diff --git a/packages/orama/src/trees/flat.ts b/packages/orama/src/trees/flat.ts index d2f26dace..355c04995 100644 --- a/packages/orama/src/trees/flat.ts +++ b/packages/orama/src/trees/flat.ts @@ -1,5 +1,6 @@ import { InternalDocumentID } from "../components/internal-document-id-store.js" -import { EnumComparisonOperator, Nullable, ScalarSearchableValue } from "../types.js" +import { EnumArrComparisonOperator, EnumComparisonOperator, Nullable, ScalarSearchableValue } from "../types.js" +import { intersect } from "../utils.js" export interface FlatTree { numberToDocumentId: Map @@ -93,3 +94,22 @@ export function filter(root: FlatTree, operation: EnumComparisonOperator): Inter throw new Error('Invalid operation') } + +export function filterArr(root: FlatTree, operation: EnumArrComparisonOperator): InternalDocumentID[] { + const operationKeys = Object.keys(operation) + + if (operationKeys.length !== 1) { + throw new Error('Invalid operation') + } + + const operationType = operationKeys[0] as keyof EnumArrComparisonOperator + switch (operationType) { + case 'containsAll': { + const values = operation[operationType]! + const ids = values.map((value) => root.numberToDocumentId.get(value) ?? []) + return intersect(ids) + } + } + + throw new Error('Invalid operation') +} diff --git a/packages/orama/src/types.ts b/packages/orama/src/types.ts index ace3f8326..234140960 100644 --- a/packages/orama/src/types.ts +++ b/packages/orama/src/types.ts @@ -24,6 +24,8 @@ export type SchemaTypes = Value extends 'string' ? number[] : Value extends 'enum' ? string | number + : Value extends 'enum[]' + ? (string | number)[] : // eslint-disable-next-line @typescript-eslint/no-unused-vars Value extends `vector[${number}]` ? number[] @@ -71,7 +73,7 @@ export type Vector = `vector[${number}]` export type VectorType = Float32Array export type ScalarSearchableType = 'string' | 'number' | 'boolean' | 'enum' -export type ArraySearchableType = 'string[]' | 'number[]' | 'boolean[]' | Vector +export type ArraySearchableType = 'string[]' | 'number[]' | 'boolean[]' | 'enum[]' | Vector export type SearchableType = ScalarSearchableType | ArraySearchableType @@ -143,6 +145,10 @@ export type EnumComparisonOperator = { nin?: (string | number | boolean)[] } +export type EnumArrComparisonOperator = { + containsAll ?: (string | number | boolean)[] +} + /** * A custom sorter function item as [id, score, document]. */ @@ -306,7 +312,7 @@ export type SearchParams> * } * }); */ - where?: Partial, boolean | string | string[] | ComparisonOperator | EnumComparisonOperator>> + where?: Partial, boolean | string | string[] | ComparisonOperator | EnumComparisonOperator | EnumArrComparisonOperator>> /** * Threshold to use for refining the search results. @@ -554,7 +560,7 @@ export interface IIndex { searchByWhereClause>( context: SearchContext, index: I, - filters: Partial, boolean | string | string[] | ComparisonOperator | EnumComparisonOperator>>, + filters: Partial, boolean | string | string[] | ComparisonOperator | EnumComparisonOperator | EnumArrComparisonOperator>>, ): SyncOrAsyncValue getSearchableProperties(index: I): SyncOrAsyncValue diff --git a/packages/orama/tests/enum.test.ts b/packages/orama/tests/enum.test.ts index 94a912485..7de30b431 100644 --- a/packages/orama/tests/enum.test.ts +++ b/packages/orama/tests/enum.test.ts @@ -250,3 +250,213 @@ t.test('enum', async t => { t.end() }) + +t.test('enum[]', async t => { + + t.test('filter', async t => { + const db = await create({ + schema: { + tags: 'enum[]', + }, + }) + + const cGreenBlue = await insert(db, { + tags: ['green', 'blue'], + }) + const [cGreen, cBlue, cWhite] = await insertMultiple(db, [ + { tags: ['green'] }, + { tags: ['blue'] }, + { tags: ['white'] }, + ]) + + const testsContainsAll = [ + { values: ['green'], expected: [cGreenBlue, cGreen] }, + { values: ['blue'], expected: [cGreenBlue, cBlue] }, + { values: ['white'], expected: [cWhite] }, + { values: ['unknown'], expected: [] }, + { values: ['green', 'blue'], expected: [cGreenBlue] }, + { values: ['blue', 'green'], expected: [cGreenBlue] }, + { values: ['green', 'blue', 'white'], expected: [] }, + { values: ['white', 'unknown'], expected: [] }, + { values: [], expected: [] }, + ] + t.test('containsAll', async t => { + for (const { values, expected } of testsContainsAll) { + t.test(`"${values}"`, async t => { + const result = await search(db, { + term: '', + where: { + tags: { containsAll: values }, + } + }) + t.equal(result.hits.length, expected.length) + t.strictSame(result.hits.map(h => h.id), expected) + + t.end() + }) + } + }) + + t.test('eq operator shouldn\'t allowed', async t => { + await t.rejects(search(db, { + term: '', + where: { + tags: { eq: 'green' }, + } + }), 'aa') + + t.end() + }) + + t.test('in operator shouldn\'t allowed', async t => { + await t.rejects(search(db, { + term: '', + where: { + tags: { in: ['green'] }, + } + }), 'aa') + + t.end() + }) + + t.test('in operator shouldn\'t allowed', async t => { + await t.rejects(search(db, { + term: '', + where: { + tags: { nin: ['green'] }, + } + }), 'aa') + + t.end() + }) + + t.end() + }) + + t.test(`remove document works fine`, async t => { + const db = await create({ + schema: { + tags: 'enum[]', + }, + }) + const c1 = await insert(db, { tags: ['green', 'blue'] }) + const c11 = await insert(db, { tags: ['blue', 'green'] }) + + const result1 = await search(db, { + term: '', + where: { tags: { containsAll: ['green', 'blue'] }, } + }) + t.equal(result1.hits.length, 2) + t.strictSame(result1.hits.map(h => h.id), [c1, c11]) + + await remove(db, c1) + + const result2 = await search(db, { + term: '', + where: { tags: { containsAll: ['green', 'blue'] }, } + }) + t.equal(result2.hits.length, 1) + t.strictSame(result2.hits.map(h => h.id), [c11]) + + t.end() + }) + + t.test(`still serializable`, async t => { + const db1 = await create({ + schema: { + tags: 'enum[]', + }, + }) + const [c1, c11] = await insertMultiple(db1, [ + { tags: ['green'] }, + { tags: ['green', 'blue'] }, + { tags: ['orange'] }, + { tags: ['purple'] }, + { tags: ['black'] }, + ]) + + const dump = await save(db1) + + const db2 = await create({ + schema: { + tags: 'enum[]', + }, + }) + await load(db2, dump) + + const result1 = await search(db2, { + term: '', + where: { + tags: { containsAll: ['green'] }, + } + }) + t.equal(result1.hits.length, 2) + t.strictSame(result1.hits.map(h => h.id), [c1, c11]) + + const result2 = await search(db2, { + term: '', + where: { + tags: { containsAll: [] }, + } + }) + t.equal(result2.hits.length, 0) + t.strictSame(result2.hits.map(h => h.id), []) + + t.end() + }) + + t.test(`complex example`, async t => { + const filmDb = await create({ + schema: { + title: 'string', + year: 'number', + tags: 'enum[]', + }, + }) + const [, , , c4] = await insertMultiple(filmDb, [ + { title: 'The Shawshank Redemption', year: 1994, tags: ['drama', 'crime'] }, + { title: 'The Godfather', year: 1972, tags: ['drama', 'crime'] }, + { title: 'The Dark Knight', year: 2008, tags: ['action', 'adventure'] }, + { title: 'Schindler\'s List', year: 1993, tags: ['war', 'drama '] }, + { title: 'The Lord of the Rings: The Return of the King', year: 2003, tags: ['fantasy', 'adventure'] }, + ]) + + const result1 = await search(filmDb, { + term: 'l', + }) + t.equal(result1.hits.length, 2) + + const result2 = await search(filmDb, { + term: 'l', + where: { + tags: { containsAll: ['war'] }, + } + }) + t.equal(result2.hits.length, 1) + t.strictSame(result2.hits.map(h => h.id), [c4]) + + const result3 = await search(filmDb, { + term: 'l', + where: { + year: { gt: 2000 }, + tags: { containsAll: ['war'] }, + } + }) + t.equal(result3.hits.length, 0) + t.strictSame(result3.hits.map(h => h.id), []) + + const result4 = await search(filmDb, { + term: 'l', + where: { + year: { lte: 2000 }, + tags: { containsAll: ['war'] }, + } + }) + t.equal(result4.hits.length, 1) + t.strictSame(result4.hits.map(h => h.id), [c4]) + + t.end() + }) + + t.end() +}) diff --git a/packages/orama/tests/facets.test.ts b/packages/orama/tests/facets.test.ts index 541d27621..1ca24174b 100644 --- a/packages/orama/tests/facets.test.ts +++ b/packages/orama/tests/facets.test.ts @@ -1,9 +1,7 @@ import t from 'tap' -import { create, insert, search } from '../src/index.js' +import { create, insert, insertMultiple, search } from '../src/index.js' t.test('facets', t => { - t.plan(2) - t.test('should generate correct facets', async t => { t.plan(6) @@ -161,4 +159,63 @@ t.test('facets', t => { t.same(results.facets?.price.values['4-6'], 2) t.same(results.facets?.price.values['6-8'], 1) }) + + t.test('should work with `enum` and `enum[]`', async t => { + + const db = await create({ + schema: { + category: 'enum', + colors: 'enum[]', + }, + }) + + await insertMultiple(db, [ + { + category: 't-shirt', + colors: ['red', 'green', 'blue'] + }, + { + category: 'sweatshirt', + colors: ['red', 'green', 'orange'] + }, + { + category: 'jeans', + colors: ['white', 'black', 'blue'] + }, + ]) + + const results = await search(db, { + term: '', + facets: { + category: {}, + colors: {}, + }, + }) + + t.strictSame(results.facets, { + category: { + count: 3, + values: { + 't-shirt': 1, + 'sweatshirt': 1, + 'jeans': 1, + } + }, + colors: { + count: 6, + values: { + 'red': 2, + 'green': 2, + 'blue': 2, + 'orange': 1, + 'white': 1, + 'black': 1, + } + } + }) + + t.end() + }) + + t.end() }) diff --git a/packages/plugin-data-persistence/test/index.test.ts b/packages/plugin-data-persistence/test/index.test.ts index e7a6f309d..072b89ac8 100644 --- a/packages/plugin-data-persistence/test/index.test.ts +++ b/packages/plugin-data-persistence/test/index.test.ts @@ -25,38 +25,42 @@ async function generateTestDBInstance() { schema: { quote: 'string', author: 'string', - genre: 'enum' + genre: 'enum', + colors: 'enum[]', } }) await insert(db, { quote: 'I am a great programmer', author: 'Bill Gates', - genre: 'tech' + genre: 'tech', + colors: ['red', 'blue'] }) await insert(db, { quote: 'Be yourself; everyone else is already taken.', author: 'Oscar Wilde', - genre: 'life' + genre: 'life', + colors: ['red', 'green'] }) await insert(db, { quote: "I have not failed. I've just found 10,000 ways that won't work.", author: 'Thomas A. Edison', - genre: 'tech' + genre: 'tech', + colors: ['red', 'blue'] }) await insert(db, { quote: 'The only way to do great work is to love what you do.', - author: 'Steve Jobs' + author: 'Steve Jobs', }) return db } t.test('binary persistence', t => { - t.plan(4) + t.plan(5) t.test('should generate a persistence file on the disk with random name', async t => { t.plan(2) @@ -208,10 +212,34 @@ t.test('binary persistence', t => { await rm(path) }) + + t.test('should continue to work with `enum[]`', async t => { + t.plan(1) + + const db = await generateTestDBInstance() + const q1 = await search(db, { + where: { + colors: { containsAll: ['green'] } + } + }) + + const path = await persistToFile(db, 'binary', 'test.dpack') + const db2 = await restoreFromFile('binary', 'test.dpack') + + const qp1 = await search(db2, { + where: { + colors: { containsAll: ['green'] } + } + }) + + t.same(q1.hits, qp1.hits) + + await rm(path) + }) }) t.test('json persistence', t => { - t.plan(4) + t.plan(5) t.test('should generate a persistence file on the disk with random name and json format', async t => { t.plan(2) @@ -341,10 +369,34 @@ t.test('json persistence', t => { await rm(path) }) + + t.test('should continue to work with `enum[]`', async t => { + t.plan(1) + + const db = await generateTestDBInstance() + const q1 = await search(db, { + where: { + colors: { containsAll: ['green'] } + } + }) + + const path = await persistToFile(db, 'json', 'test.json') + const db2 = await restoreFromFile('json', 'test.json') + + const qp1 = await search(db2, { + where: { + colors: { containsAll: ['green'] } + } + }) + + t.same(q1.hits, qp1.hits) + + await rm(path) + }) }) t.test('dpack persistence', t => { - t.plan(3) + t.plan(4) t.test('should generate a persistence file on the disk with random name and dpack format', async t => { t.plan(2) @@ -437,6 +489,30 @@ t.test('dpack persistence', t => { await rm(path) }) + + t.test('should continue to work with `enum[]`', async t => { + t.plan(1) + + const db = await generateTestDBInstance() + const q1 = await search(db, { + where: { + colors: { containsAll: ['green'] } + } + }) + + const path = await persistToFile(db, 'dpack', 'test.dpack') + const db2 = await restoreFromFile('dpack', 'test.dpack') + + const qp1 = await search(db2, { + where: { + colors: { containsAll: ['green'] } + } + }) + + t.same(q1.hits, qp1.hits) + + await rm(path) + }) }) t.test('should persist data in-memory', async t => {