diff --git a/packages/twenty-server/scripts/setup-db.ts b/packages/twenty-server/scripts/setup-db.ts index ca2c63e253..82fe1cce85 100644 --- a/packages/twenty-server/scripts/setup-db.ts +++ b/packages/twenty-server/scripts/setup-db.ts @@ -21,6 +21,18 @@ rawDataSource 'create extension "uuid-ossp"', ); + await performQuery( + 'CREATE EXTENSION IF NOT EXISTS "unaccent"', + 'create extension "unaccent"', + ); + + await performQuery( + `CREATE OR REPLACE FUNCTION unaccent_immutable(text) RETURNS text AS $$ + SELECT public.unaccent($1) + $$ LANGUAGE sql IMMUTABLE;`, + 'create immutable unaccent wrapper function', + ); + // We paused the work on FDW if (process.env.IS_FDW_ENABLED !== 'true') { return; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts index 9d0a1c5624..79aa7bd40b 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts @@ -101,8 +101,8 @@ export const computeWhereConditionParts = ({ return { sql: `( - "${objectNameSingular}"."${key}" @@ to_tsquery('simple', :${key}${uuid}Ts) OR - "${objectNameSingular}"."${key}"::text ILIKE :${key}${uuid}Like + "${objectNameSingular}"."${key}" @@ to_tsquery('simple', public.unaccent_immutable(:${key}${uuid}Ts)) OR + public.unaccent_immutable("${objectNameSingular}"."${key}"::text) ILIKE public.unaccent_immutable(:${key}${uuid}Like) )`, params: { [`${key}${uuid}Ts`]: tsQuery, diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index 8a4cac0a3e..91b1e7989a 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -177,9 +177,9 @@ export class SearchService { ...(imageIdentifierField ? [imageIdentifierField] : []), ].map((field) => `"${field}"`); - const tsRankCDExpr = `ts_rank_cd("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`; + const tsRankCDExpr = `ts_rank_cd("${SEARCH_VECTOR_FIELD.name}", to_tsquery('simple', public.unaccent_immutable(:searchTerms)))`; - const tsRankExpr = `ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTermsOr))`; + const tsRankExpr = `ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery('simple', public.unaccent_immutable(:searchTermsOr)))`; const cursorWhereCondition = this.computeCursorWhereCondition({ after, @@ -197,10 +197,10 @@ export class SearchService { queryBuilder.andWhere( new Brackets((qb) => { qb.where( - `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery('simple', :searchTerms)`, + `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery('simple', public.unaccent_immutable(:searchTerms))`, { searchTerms }, ).orWhere( - `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery('simple', :searchTermsOr)`, + `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery('simple', public.unaccent_immutable(:searchTermsOr))`, { searchTermsOr }, ); }), diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts index 0d420f3245..72138eb5dc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts @@ -18,7 +18,9 @@ describe('getTsVectorColumnExpressionFromFields', () => { const fields = [nameTextField] as FieldTypeAndNameMetadata[]; const result = getTsVectorColumnExpressionFromFields(fields); - expect(result).toContain("to_tsvector('simple', COALESCE(\"name\", ''))"); + expect(result).toContain( + "to_tsvector('simple', COALESCE(public.unaccent_immutable(\"name\"), ''))", + ); }); it('should handle multiple fields', () => { @@ -28,13 +30,22 @@ describe('getTsVectorColumnExpressionFromFields', () => { emailsEmailsField, ] as FieldTypeAndNameMetadata[]; const result = getTsVectorColumnExpressionFromFields(fields); - const expected = ` - to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' || - COALESCE("emailsPrimaryEmail", '') || ' ' || - COALESCE(SPLIT_PART("emailsPrimaryEmail", '@', 2), '')) - `.trim(); - expect(result.trim()).toBe(expected); + expect(result).toContain( + 'COALESCE(public.unaccent_immutable("nameFirstName"), \'\')', + ); + expect(result).toContain( + 'COALESCE(public.unaccent_immutable("nameLastName"), \'\')', + ); + expect(result).toContain( + 'COALESCE(public.unaccent_immutable("jobTitle"), \'\')', + ); + expect(result).toContain( + 'COALESCE(public.unaccent_immutable("emailsPrimaryEmail"), \'\')', + ); + expect(result).toContain( + "COALESCE(public.unaccent_immutable(SPLIT_PART(\"emailsPrimaryEmail\", '@', 2)), '')", + ); }); it('should handle rich text fields', () => { @@ -43,7 +54,9 @@ describe('getTsVectorColumnExpressionFromFields', () => { ] as FieldTypeAndNameMetadata[]; const result = getTsVectorColumnExpressionFromFields(fields); - expect(result).toBe("to_tsvector('simple', COALESCE(\"body\", ''))"); + expect(result).toBe( + "to_tsvector('simple', COALESCE(public.unaccent_immutable(\"body\"), ''))", + ); }); it('should handle rich text v2 fields', () => { @@ -53,7 +66,7 @@ describe('getTsVectorColumnExpressionFromFields', () => { const result = getTsVectorColumnExpressionFromFields(fields); expect(result).toBe( - "to_tsvector('simple', COALESCE(\"bodyV2Markdown\", ''))", + "to_tsvector('simple', COALESCE(public.unaccent_immutable(\"bodyV2Markdown\"), ''))", ); }); }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts index 2e85a35d71..509bd42c8d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts @@ -82,10 +82,10 @@ const getColumnExpression = ( switch (fieldType) { case FieldMetadataType.EMAILS: return ` - COALESCE(${quotedColumnName}, '') || ' ' || - COALESCE(SPLIT_PART(${quotedColumnName}, '@', 2), '')`; + COALESCE(public.unaccent_immutable(${quotedColumnName}), '') || ' ' || + COALESCE(public.unaccent_immutable(SPLIT_PART(${quotedColumnName}, '@', 2)), '')`; default: - return `COALESCE(${quotedColumnName}, '')`; + return `COALESCE(public.unaccent_immutable(${quotedColumnName}), '')`; } };