From 3c0c19c0e3c8c76ca82f8f21196cf92e5f705102 Mon Sep 17 00:00:00 2001 From: Scott Edwards Coll <28233101-scott.ec@users.noreply.gitlab.com> Date: Mon, 17 Nov 2025 22:16:54 +0100 Subject: [PATCH 1/2] Use PostgreSQL's unaccent() to make person-name search accent-insensitive. --- e2e/src/api/specs/search.e2e-spec.ts | 105 +++++++++++++++++++ server/src/repositories/person.repository.ts | 12 ++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 2f6ea75f77..11fac03269 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -700,4 +700,109 @@ describe('/search', () => { expect(status).toBe(200); }); }); + + describe('GET /search/person', () => { + it('matches prefix of first name', async () => { + const person = await utils.createPerson(admin.accessToken, { + name: 'Ford' + }) + + const { status, body } = await request(app) + .get('/search/person?name=For') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: person.id }), + ]), + ); + }); + + it('matches prefix of subsequent name parts', async () => { + const person1 = await utils.createPerson(admin.accessToken, { + name: 'Ford Prefect' + }) + + const person2 = await utils.createPerson(admin.accessToken, { + name: 'Ford Nicholas Prefect' + }) + + const { status, body } = await request(app) + .get('/search/person?name=Pre') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: person1.id }), + expect.objectContaining({ id: person2.id }), + ]), + ); + }); + + it('does not match substring in the middle of a name', async () => { + await utils.createPerson(admin.accessToken, { + name: 'Ford Prefect' + }) + + const { status, body } = await request(app) + .get('/search/person?name=fec') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual([]); + }); + + it('matches accented name without accent in query', async () => { + const person = await utils.createPerson(admin.accessToken, { + name: 'Emília' + }) + + const { status, body } = await request(app) + .get('/search/person?name=Emi') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: person.id }), + ]), + ); + }); + + it('matches unaccented name with accent in query', async () => { + const person = await utils.createPerson(admin.accessToken, { + name: 'Emilia' + }) + + const { status, body } = await request(app) + .get('/search/person?name=Emí') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: person.id }), + ]), + ); + }); + + it('is case insensitive', async () => { + const person = await utils.createPerson(admin.accessToken, { + name: 'Arthur' + }) + + const { status, body } = await request(app) + .get('/search/person?name=arthur') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: person.id }), + ]), + ); + }); + }); }); diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 725304938c..fe4f0aeef0 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -316,8 +316,16 @@ export class PersonRepository { eb.and([ eb('person.ownerId', '=', userId), eb.or([ - eb(eb.fn('lower', ['person.name']), 'like', `${personName.toLowerCase()}%`), - eb(eb.fn('lower', ['person.name']), 'like', `% ${personName.toLowerCase()}%`), + eb( + eb.fn('f_unaccent', [eb.fn('lower', ['person.name'])]), + 'like', + `${eb.fn('f_unaccent', [personName.toLowerCase()])}%`, + ), + eb( + eb.fn('f_unaccent', [eb.fn('lower', ['person.name'])]), + 'like', + `% ${eb.fn('f_unaccent', [personName.toLowerCase()])}%`, + ), ]), ]), ) From 6b456954bf4a31e0115baa2c77ceb6db0da00336 Mon Sep 17 00:00:00 2001 From: Scott Edwards Coll <28233101-scott.ec@users.noreply.gitlab.com> Date: Mon, 24 Nov 2025 21:33:16 +0100 Subject: [PATCH 2/2] Use ilike --- server/src/repositories/person.repository.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index fe4f0aeef0..036d294a9b 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -318,13 +318,13 @@ export class PersonRepository { eb.or([ eb( eb.fn('f_unaccent', [eb.fn('lower', ['person.name'])]), - 'like', - `${eb.fn('f_unaccent', [personName.toLowerCase()])}%`, + 'ilike', + `${eb.fn('f_unaccent', [personName])}%`, ), eb( eb.fn('f_unaccent', [eb.fn('lower', ['person.name'])]), - 'like', - `% ${eb.fn('f_unaccent', [personName.toLowerCase()])}%`, + 'ilike', + `% ${eb.fn('f_unaccent', [personName])}%`, ), ]), ]),