From 2d4c028a64e4189ec3e13ce559c8fd87fe333a3b Mon Sep 17 00:00:00 2001 From: Lawrence <34475808+acodeninja@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:59:45 +0100 Subject: [PATCH] feat: support caching the search index (#16) * feat: support caching the search index * ci: do not publish deepsource config file to npm --- .npmignore | 1 + src/engine/Engine.js | 23 +++++++++++----- src/engine/Engine.test.js | 57 +++++++++++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/.npmignore b/.npmignore index 5c550a5..e9c8e28 100644 --- a/.npmignore +++ b/.npmignore @@ -10,3 +10,4 @@ release.config.js eslint.config.js Makefile .tool-versions +.deepsource.toml diff --git a/src/engine/Engine.js b/src/engine/Engine.js index 9ccda18..d1c0201 100644 --- a/src/engine/Engine.js +++ b/src/engine/Engine.js @@ -10,6 +10,7 @@ import lunr from 'lunr'; */ class Engine { static configuration = undefined; + static _searchCache = undefined; /** * Retrieves a model by its ID. This method must be implemented by subclasses. @@ -113,16 +114,24 @@ class Engine { * Performs a search query on a model's index and returns the matching models. * * @param {Model.constructor} model - The model class. - * @param {object} query - The search query string. - * @returns {Array} An array of models matching the search query. + * @param {string} query - The search query string. + * @returns {Promise>} An array of models matching the search query. * @throws {EngineError} Throws if the search index is not available for the model. */ static async search(model, query) { this.checkConfiguration(); - const index = await this.getSearchIndexCompiled(model).catch(() => { - throw new EngineError(`The model ${model.toString()} does not have a search index available.`); - }); + const index = + (this._searchCache && this.configuration?.cache?.search && Date.now() - this._searchCache[0] < this.configuration.cache.search) ? + this._searchCache[1] : + await this.getSearchIndexCompiled(model) + .then(i => { + this._searchCache = [Date.now(), i]; + return i; + }) + .catch(error => { + throw new EngineError(`The model ${model.toString()} does not have a search index available.`, error); + }); const searchIndex = lunr.Index.load(index); @@ -317,7 +326,7 @@ class Engine { static configuration = configuration; } - Object.defineProperty(ConfiguredStore, 'name', { value: `${this.toString()}` }); + Object.defineProperty(ConfiguredStore, 'name', {value: `${this.toString()}`}); return ConfiguredStore; } @@ -329,7 +338,7 @@ class Engine { * @abstract */ static checkConfiguration() { - // Implemented in extending Engine class + // Implemented in extending Engine class } /** diff --git a/src/engine/Engine.test.js b/src/engine/Engine.test.js index 832fe53..75bcb24 100644 --- a/src/engine/Engine.test.js +++ b/src/engine/Engine.test.js @@ -1,6 +1,8 @@ import Engine, {NotFoundEngineError, NotImplementedError} from './Engine.js'; import {MainModel} from '../../test/fixtures/Models.js'; +import {Models} from '../../test/fixtures/ModelCollection.js'; import Type from '../type/index.js'; +import sinon from 'sinon'; import test from 'ava'; class UnimplementedEngine extends Engine { @@ -79,13 +81,13 @@ test('UnimplementedEngine.putSearchIndexRaw(Model, {param: value}) raises a putS t.is(error.message, 'UnimplementedEngine does not implement .putSearchIndexRaw()'); }); -class ImplementedEngine extends Engine { - static getById(_id) { - return null; +test('ImplementedEngine.get(MainModel, id) when id does not exist', async t => { + class ImplementedEngine extends Engine { + static getById(_id) { + return null; + } } -} -test('ImplementedEngine.get(MainModel, id) when id does not exist', async t => { await t.throwsAsync( () => ImplementedEngine.get(MainModel, 'MainModel/000000000000'), { @@ -94,3 +96,48 @@ test('ImplementedEngine.get(MainModel, id) when id does not exist', async t => { }, ); }); + +test('ImplementedEngine.search(MainModel, "test") when caching is off calls ImplementedEngine.getSearchIndexCompiled every time', async t => { + class ImplementedEngine extends Engine { + static getById(id) { + const models = new Models(); + models.createFullTestModel(); + return models.models[id] || null; + } + + static getSearchIndexCompiled = sinon.stub().callsFake((model) => { + const models = new Models(); + models.createFullTestModel(); + return Promise.resolve(JSON.parse(JSON.stringify(models.getSearchIndex(model)))); + }); + } + + const engine = ImplementedEngine.configure({}); + + await engine.search(MainModel, 'test'); + await engine.search(MainModel, 'test'); + + t.is(engine.getSearchIndexCompiled.getCalls().length, 2); +}); + +test('ImplementedEngine.search(MainModel, "test") when caching is on calls ImplementedEngine.getSearchIndexCompiled once', async t => { + const models = new Models(); + models.createFullTestModel(); + + class ImplementedEngine extends Engine { + static getById(id) { + return models.models[id]; + } + + static getSearchIndexCompiled = sinon.stub().callsFake((model) => + Promise.resolve(JSON.parse(JSON.stringify(models.getSearchIndex(model)))), + ); + } + + const engine = ImplementedEngine.configure({cache: {search: 5000}}); + + await engine.search(MainModel, 'test'); + await engine.search(MainModel, 'test'); + + t.is(engine.getSearchIndexCompiled.getCalls().length, 1); +});