From 991c9fec750ab1e5ab79bbf474e47f2a614439ae Mon Sep 17 00:00:00 2001 From: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:26:30 -0500 Subject: [PATCH] Update elasticsearch translator to handle phrase searches. (#27720) * Update elasticsearch translator to handle phrase searches. Signed-off-by: Sydney Achinger * Add helpful regex comment Co-authored-by: John Philip Signed-off-by: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> --------- Signed-off-by: Sydney Achinger Signed-off-by: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> Co-authored-by: John Philip --- .changeset/empty-ways-push.md | 5 ++ .../engines/ElasticSearchSearchEngine.test.ts | 72 +++++++++++++++---- .../src/engines/ElasticSearchSearchEngine.ts | 36 ++++++++-- 3 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 .changeset/empty-ways-push.md diff --git a/.changeset/empty-ways-push.md b/.changeset/empty-ways-push.md new file mode 100644 index 0000000000..11a67985d8 --- /dev/null +++ b/.changeset/empty-ways-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Update the ElasticSearchSearchEngine translator to handle phrase searches. diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 3f64e0b768..379b712469 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -185,12 +185,11 @@ describe('ElasticSearchSearchEngine', () => { expect(queryBody).toEqual({ query: { bool: { - must: { + should: { multi_match: { query: 'testTerm', fields: ['*'], fuzziness: 'auto', - minimum_should_match: 1, }, }, filter: { @@ -205,6 +204,56 @@ describe('ElasticSearchSearchEngine', () => { }); }); + it('should return translated query with phrase terms', async () => { + const translatorUnderTest = inspectableSearchEngine.getTranslator(); + + const actualTranslatedQuery = translatorUnderTest({ + types: ['indexName'], + term: '"test phrase" anotherTerm "another phrase"', + filters: {}, + }) as ElasticSearchConcreteQuery; + + expect(actualTranslatedQuery).toMatchObject({ + documentTypes: ['indexName'], + elasticSearchQuery: expect.any(Object), + }); + + const queryBody = actualTranslatedQuery.elasticSearchQuery; + + expect(queryBody).toEqual({ + query: { + bool: { + should: [ + { + multi_match: { + query: 'test phrase', + fields: ['*'], + type: 'phrase', + }, + }, + { + multi_match: { + query: 'another phrase', + fields: ['*'], + type: 'phrase', + }, + }, + { + multi_match: { + query: 'anotherTerm', + fields: ['*'], + fuzziness: 'auto', + }, + }, + ], + filter: [], + }, + }, + from: 0, + size: 25, + }); + }); + it('should pass page cursor', async () => { const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -225,12 +274,11 @@ describe('ElasticSearchSearchEngine', () => { query: { bool: { filter: [], - must: { + should: { multi_match: { query: 'testTerm', fields: ['*'], fuzziness: 'auto', - minimum_should_match: 1, }, }, }, @@ -264,12 +312,11 @@ describe('ElasticSearchSearchEngine', () => { expect(queryBody).toEqual({ query: { bool: { - must: { + should: { multi_match: { query: 'testTerm', fields: ['*'], fuzziness: 'auto', - minimum_should_match: 1, }, }, filter: [ @@ -320,12 +367,11 @@ describe('ElasticSearchSearchEngine', () => { expect(queryBody).toEqual({ query: { bool: { - must: { + should: { multi_match: { query: 'testTerm', fields: ['*'], fuzziness: 'auto', - minimum_should_match: 1, }, }, filter: { @@ -382,12 +428,11 @@ describe('ElasticSearchSearchEngine', () => { query: { bool: { filter: [], - must: { + should: { multi_match: { query: 'testTerm', fields: ['*'], fuzziness: 'auto', - minimum_should_match: 1, }, }, }, @@ -708,12 +753,11 @@ describe('ElasticSearchSearchEngine', () => { body: { query: { bool: { - must: { + should: { multi_match: { query: 'testTerm', fields: ['*'], fuzziness: 'auto', - minimum_should_match: 1, }, }, filter: [], @@ -747,7 +791,7 @@ describe('ElasticSearchSearchEngine', () => { body: { query: { bool: { - must: { + should: { match_all: {}, }, filter: [], @@ -782,7 +826,7 @@ describe('ElasticSearchSearchEngine', () => { body: { query: { bool: { - must: { + should: { match_all: {}, }, filter: [], diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index e2934da404..16233a5e78 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -246,18 +246,40 @@ export class ElasticSearchSearchEngine implements SearchEngine { 'Failed to add filters to query. Unrecognized filter type', ); }); - const esbQuery = isBlank(term) - ? esb.matchAllQuery() - : esb - .multiMatchQuery(['*'], term) - .fuzziness('auto') - .minimumShouldMatch(1); + + const esbQueries = []; + // https://regex101.com/r/Lr0MqS/1 + const phraseTerms = term.match(/"[^"]*"/g); + + if (isBlank(term)) { + const esbQuery = esb.matchAllQuery(); + esbQueries.push(esbQuery); + } else if (phraseTerms && phraseTerms.length > 0) { + let restTerm = term; + for (const phraseTerm of phraseTerms) { + restTerm = restTerm.replace(phraseTerm, ''); + const esbPhraseQuery = esb + .multiMatchQuery(['*'], phraseTerm.replace(/"/g, '')) + .type('phrase'); + esbQueries.push(esbPhraseQuery); + } + if (restTerm?.length > 0) { + const esbRestQuery = esb + .multiMatchQuery(['*'], restTerm.trim()) + .fuzziness('auto'); + esbQueries.push(esbRestQuery); + } + } else { + const esbQuery = esb.multiMatchQuery(['*'], term).fuzziness('auto'); + esbQueries.push(esbQuery); + } + const pageSize = query.pageLimit || 25; const { page } = decodePageCursor(pageCursor); let esbRequestBodySearch = esb .requestBodySearch() - .query(esb.boolQuery().filter(filter).must([esbQuery])) + .query(esb.boolQuery().filter(filter).should(esbQueries)) .from(page * pageSize) .size(pageSize);