diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 4c2441073..d54bae93f 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -339,6 +339,51 @@ describe('#documents.search', async () => { expect(body.data[1].document.id).toEqual(secondResult.id); }); + it('should return partial results in ranked order', async () => { + const { user } = await seed(); + const firstResult = await buildDocument({ + title: 'search term', + text: 'random text', + userId: user.id, + teamId: user.teamId, + }); + const secondResult = await buildDocument({ + title: 'random text', + text: 'search term', + userId: user.id, + teamId: user.teamId, + }); + + const res = await server.post('/api/documents.search', { + body: { token: user.getJwtToken(), query: 'sear &' }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(2); + expect(body.data[0].document.id).toEqual(firstResult.id); + expect(body.data[1].document.id).toEqual(secondResult.id); + }); + + it('should strip junk from search term', async () => { + const { user } = await seed(); + const firstResult = await buildDocument({ + title: 'search term', + text: 'this is some random text of the document body', + userId: user.id, + teamId: user.teamId, + }); + + const res = await server.post('/api/documents.search', { + body: { token: user.getJwtToken(), query: 'rando &\\;:()' }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + expect(body.data[0].document.id).toEqual(firstResult.id); + }); + it('should return draft documents created by user', async () => { const { user } = await seed(); await buildDocument({ diff --git a/server/models/Document.js b/server/models/Document.js index 804570f14..d1f723276 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -212,14 +212,15 @@ Document.searchForUser = async ( ): Promise => { const limit = options.limit || 15; const offset = options.offset || 0; + const wildcardQuery = `${sequelize.escape(query)}:*`; const sql = ` SELECT id, - ts_rank(documents."searchVector", plainto_tsquery('english', :query)) as "searchRanking", - ts_headline('english', "text", plainto_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext" + ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking", + ts_headline('english', "text", to_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext" FROM documents - WHERE "searchVector" @@ plainto_tsquery('english', :query) AND + WHERE "searchVector" @@ to_tsquery('english', :query) AND "collectionId" IN(:collectionIds) AND "deletedAt" IS NULL AND ("publishedAt" IS NOT NULL OR "createdById" = '${user.id}') @@ -234,7 +235,7 @@ Document.searchForUser = async ( const results = await sequelize.query(sql, { type: sequelize.QueryTypes.SELECT, replacements: { - query, + query: wildcardQuery, limit, offset, collectionIds,