Skip to content

Commit d3e9f63

Browse files
authored
fix(api-messages-search): ZMSA-80: fix messages search q param parsing for AND fulltext search (#995)
1 parent 17fcf3c commit d3e9f63

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

lib/search-query.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ const getMongoDBQuery = async (db, user, queryStr) => {
143143
const parsed = parseSearchQuery(queryStr);
144144

145145
let hasTextFilter = false;
146+
const isSingleTextQuery = entry => typeof entry?.$text?.$search === 'string' && Object.keys(entry).length === 1;
146147

147148
let walkTree = async node => {
148149
if (Array.isArray(node)) {
@@ -163,6 +164,26 @@ const getMongoDBQuery = async (db, user, queryStr) => {
163164
branch.$and = branch.$and.concat(subBranch || []);
164165
}
165166

167+
// MongoDB allows a single $text expression per query. If this AND branch
168+
// has multiple direct text clauses, merge them into one.
169+
// Used for fulltext AND query (default) (example: `q=term1 term2`)
170+
if (branch.$and.length > 1) {
171+
let textTerms = [];
172+
let nonTextTerms = [];
173+
174+
for (let entry of branch.$and) {
175+
if (isSingleTextQuery(entry)) {
176+
textTerms.push(entry.$text.$search);
177+
} else {
178+
nonTextTerms.push(entry);
179+
}
180+
}
181+
182+
if (textTerms.length > 1) {
183+
branch.$and = [{ $text: { $search: textTerms.join(' ') } }].concat(nonTextTerms);
184+
}
185+
}
186+
166187
return branch;
167188
} else if (node.$or && node.$or.length) {
168189
let branch = {
@@ -178,7 +199,7 @@ const getMongoDBQuery = async (db, user, queryStr) => {
178199
// MongoDB allows a single $text expression per query. If this OR branch
179200
// only contains text queries, merge them into one $text search.
180201
// Used for fulltext OR query search
181-
if (branch.$or.length && branch.$or.every(entry => entry?.$text && typeof entry.$text.$search === 'string' && Object.keys(entry).length === 1)) {
202+
if (branch.$or.length && branch.$or.every(isSingleTextQuery)) {
182203
return {
183204
$text: {
184205
$search: branch.$or.map(entry => entry.$text.$search).join(' ')

test/api/messages-test.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ describe('Messages tests', function () {
2727
subjectAttachment: 'Search Query Attachment Marker',
2828
body: 'searchquerybodytoken',
2929
attachmentBody: 'searchqueryattachmenttoken',
30+
multiTerm1: 'searchquerymultiterm1token',
31+
multiTerm2: 'searchquerymultiterm2token',
3032
toAddress: 'search.query.to@to.com',
3133
ccAddress: 'search.query.cc@to.com',
3234
fromAddress: 'messagestestsuser@web.zone.test'
@@ -89,7 +91,7 @@ describe('Messages tests', function () {
8991
cc: [{ address: queryFixture.ccAddress }],
9092
bcc: [{ address: queryFixture.ccAddress }],
9193
subject: queryFixture.subjectKeyword,
92-
text: `${queryFixture.body} keyword marker`
94+
text: `${queryFixture.body} keyword marker ${queryFixture.multiTerm1} ${queryFixture.multiTerm2}`
9395
})
9496
.expect(200);
9597

@@ -262,6 +264,27 @@ describe('Messages tests', function () {
262264
expect(search.body.results.map(entry => entry.subject)).to.include(queryFixture.subjectExcluded);
263265
});
264266

267+
it('should GET /users/:user/search expect success / q with two terms and searchable=1', async () => {
268+
const q = `${queryFixture.multiTerm1} ${queryFixture.multiTerm2} in:${queryMailbox}`;
269+
270+
const search = await server
271+
.get(`/users/${user}/search?q=${encodeURIComponent(q)}&limit=50`)
272+
.send({})
273+
.expect(200);
274+
275+
const searchWithSearchable = await server
276+
.get(`/users/${user}/search?q=${encodeURIComponent(q)}&searchable=1&limit=50`)
277+
.send({})
278+
.expect(200);
279+
280+
expect(search.body.success).to.be.true;
281+
expect(searchWithSearchable.body.success).to.be.true;
282+
expect(searchWithSearchable.body.query).to.equal(q);
283+
expect(search.body.results.map(entry => entry.subject)).to.include(queryFixture.subjectKeyword);
284+
expect(searchWithSearchable.body.results.map(entry => entry.subject)).to.include(queryFixture.subjectKeyword);
285+
expect(searchWithSearchable.body.results).to.deep.equal(search.body.results);
286+
});
287+
265288
it('should GET /users/:user/search expect success / q supports OR groups', async () => {
266289
const q = `(${queryFixture.body} OR ${queryFixture.attachmentBody}) in:${queryMailbox}`;
267290
const search = await server

0 commit comments

Comments
 (0)