diff --git a/.changeset/search-spicy-foxes-joke.md b/.changeset/search-spicy-foxes-joke.md new file mode 100644 index 0000000000..c1ed318f6d --- /dev/null +++ b/.changeset/search-spicy-foxes-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Handle special case when filter array has single value optimizing Lunr search behaviour. diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index a2cbb4ecc3..cbe48ef5a7 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -192,6 +192,37 @@ describe('LunrSearchEngine', () => { }); }); + it('should handle single-item array filter as scalar value', async () => { + const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + logger: getVoidLogger(), + }); + const translatorUnderTest = inspectableSearchEngine.getTranslator(); + + const actualTranslatedQuery = translatorUnderTest({ + term: 'testTerm', + filters: { kind: ['testKind'] }, + }) as ConcreteLunrQuery; + + expect(actualTranslatedQuery).toMatchObject({ + documentTypes: undefined, + lunrQueryBuilder: expect.any(Function), + }); + + const query: jest.Mocked = { + allFields: ['kind'], + clauses: [], + term: jest.fn(), + clause: jest.fn(), + }; + + actualTranslatedQuery.lunrQueryBuilder.bind(query)(query); + + expect(query.term).toBeCalledWith(lunr.tokenizer('testKind'), { + fields: ['kind'], + presence: lunr.Query.presence.REQUIRED, + }); + }); + it('should return translated query with multiple filters', async () => { const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ logger: getVoidLogger(), diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index f22762cb9f..ea51ffe8e1 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -80,11 +80,17 @@ export class LunrSearchEngine implements SearchEngine { }); if (filters) { - Object.entries(filters).forEach(([field, value]) => { + Object.entries(filters).forEach(([field, fieldValue]) => { if (!q.allFields.includes(field)) { // Throw for unknown field, as this will be a non match throw new Error(`unrecognised field ${field}`); } + // Arrays are poorly supported, but we can make it better for single-item arrays, + // which should be a common case + const value = + Array.isArray(fieldValue) && fieldValue.length === 1 + ? fieldValue[0] + : fieldValue; // Require that the given field has the given value if (['string', 'number', 'boolean'].includes(typeof value)) {