Skip to content

Commit

Permalink
feat: support caching the search index
Browse files Browse the repository at this point in the history
  • Loading branch information
acodeninja committed Sep 25, 2024
1 parent 8e00267 commit 1cdd2df
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 12 deletions.
23 changes: 16 additions & 7 deletions src/engine/Engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Model>} An array of models matching the search query.
* @param {string} query - The search query string.
* @returns {Promise<Array<Model>>} 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);

Expand Down Expand Up @@ -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;
}
Expand All @@ -329,7 +338,7 @@ class Engine {
* @abstract
*/
static checkConfiguration() {
// Implemented in extending Engine class
// Implemented in extending Engine class
}

/**
Expand Down
57 changes: 52 additions & 5 deletions src/engine/Engine.test.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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'),
{
Expand All @@ -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);
});

0 comments on commit 1cdd2df

Please sign in to comment.