diff --git a/services/search-service/src/__tests__/unit/psql/query.builder.unit.ts b/services/search-service/src/__tests__/unit/psql/query.builder.unit.ts index aa23b4a887..6e1931a86f 100644 --- a/services/search-service/src/__tests__/unit/psql/query.builder.unit.ts +++ b/services/search-service/src/__tests__/unit/psql/query.builder.unit.ts @@ -6,10 +6,49 @@ import {buildTestsRunner} from '../runner'; import {PsqlQueryBuilder} from '../../../classes'; import {expect} from '@loopback/testlab'; import {testModelList, testModelListWithIdentifier} from '../..'; +import {AnyObject} from '@loopback/repository'; describe('PostgreSQL QueryBuilder', () => { const queryPart = " from public.TestSearchedCustom where to_tsvector(public.f_concat_ws(' ', about, identifier)) @@ to_tsquery($1))"; + + describe('_formatAndSanitize', () => { + it('should preserve dots in search terms', () => { + const builder = new PsqlQueryBuilder({match: 'Deal 10.1'}); + const result = (builder as AnyObject)._formatAndSanitize('Deal 10.1'); + expect(result).to.equal('Deal:*<->10.1:*'); + }); + + it('should handle project names with dots', () => { + const builder = new PsqlQueryBuilder({match: 'test'}); + const result = (builder as AnyObject)._formatAndSanitize('testlift.QA'); + expect(result).to.equal('testlift.QA:*'); + }); + + it('should handle multiple dots in different positions', () => { + const builder = new PsqlQueryBuilder({match: 'test'}); + const result = (builder as AnyObject)._formatAndSanitize( + 'Project.Alpha.Beta', + ); + expect(result).to.equal('Project.Alpha.Beta:*'); + }); + + it('should handle mix of dots and special characters', () => { + const builder = new PsqlQueryBuilder({match: 'test'}); + const result = (builder as AnyObject)._formatAndSanitize( + 'Deal.10 & Test.1.5', + ); + expect(result).to.equal('Deal.10:*<->Test.1.5:*'); + }); + + it('should handle complex search with dots and spaces', () => { + const builder = new PsqlQueryBuilder({match: 'test'}); + const result = (builder as AnyObject)._formatAndSanitize( + 'Version 2.5 (Release)', + ); + expect(result).to.equal('Version:*<->2.5:*<->Release:*'); + }); + }); describe( 'with match parameter', buildTestsRunner( diff --git a/services/search-service/src/classes/psql/query.builder.ts b/services/search-service/src/classes/psql/query.builder.ts index 6312d923d1..c9855de0fb 100644 --- a/services/search-service/src/classes/psql/query.builder.ts +++ b/services/search-service/src/classes/psql/query.builder.ts @@ -78,11 +78,17 @@ export class PsqlQueryBuilder extends SearchQueryBuilder { } _formatAndSanitize(param: string) { - return param - .replace(/[^A-Za-z\s0-9]/g, ' ') - .split(' ') - .filter(p => p) - .map(p => `${p}:*`) - .join('<->'); + let result = param; + while (result.endsWith('.')) { + result = result.slice(0, -1); + } + + return result + .replace(/[!&|<>():'"]/g, ' ') // Remove PostgreSQL tsquery special chars + .replace(/[^A-Za-z0-9.\s]/g, ' ') // Keep dots, remove other symbols + .split(/\s+/) // Split on any whitespace (not just spaces) + .filter(p => p && p !== '.') // Filter empty and standalone dots + .map(p => `${p}:*`) // Add prefix match operator + .join('<->'); // Join with followed-by operator } }