Conversation
MongoDB resolves $text through the compound `fulltext` index, which is prefixed with `user`, so a $text clause nested in an $or needs its own equality on `user`, and every sibling branch of that $or has to be index backed. Neither held, so any search mixing a header keyword with a fulltext term under OR returned a 500. applyTextIndexPrefix stamps the user id onto every branch of every $or that wraps a $text clause. The filter already requires `user` at the top level, so the repeated equality does not change what it matches. Both filter builders call it, so the or.* API params are covered as well, not only the q syntax. MongoDB also accepts a single $text expression per query, and the builder emitted two whenever text terms landed in different OR branches. Terms that cannot claim the one slot now fall back to the regex matching quoted phrases already use, keeping AND semantics for a merged AND term and keeping negated terms excluding rather than widening the match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179oqSvLfdTeNBTsSPVmp4m
andris9
approved these changes
Aug 25, 2026
andris9
left a comment
Member
There was a problem hiding this comment.
Approved. Verified the reported query against MongoDB 7 with the real index set: it failed on the original commit and plans as TEXT_MATCH now.
The user prefix alone was necessary but not sufficient, so I pushed a follow-up commit:
- the prefix only made the query plannable when
searchable=1also put a top levelmailboxpredicate in place. Without it, and fromlib/tasks/search-apply.jswhich never sets it, the same query still returned 500. Stampinguseronto every branch of the wrapping$orfixes it for every shape, since each branch then has an index to scan. - the same defect existed untouched in
lib/prepare-search-filter.js(theor.*API params). Both builders now share one helper instead of hand rolling the rule. q=from:a alpha OR to:b betaemitted two$textexpressions and returned 500 regardless ofsearchable. Only one survives now, the rest fall back to the regex path used for quoted phrases.
Tests: the new API test now seeds messages and asserts on content rather than just a 200, and covers the path without searchable=1. Both new API tests fail on the original commit.
Not merging, leaving that to you.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
GET /users/:user/searchreturned HTTP 500 for any query that puts a fulltext term inside an OR branch, for example the reportedMongoDB puts two conditions on a
$textexpression nested inside an$or:fulltextindex is compound ({user: 1, 'headers.value': text, text: text}), so the branch holding$textneeds its own equality match onuser. A top leveluseris not propagated into the branch.$orhas to be index backed, otherwise the planner gives up withNo query solutions.On top of that MongoDB accepts only one
$textexpression per query, and the builder emitted two whenever text terms landed in different OR branches (Too many text expressions).Fix
applyTextIndexPrefix(filter, user)walks the finished filter and stampsuseronto every branch of every$orthat wraps a$textclause. The filter always requiresuserat the top level, so the repeated equality never changes what the filter matches, but it gives the text branch its index prefix and gives the sibling branches an index to scan. Both filter builders call it, so theor.*API params (lib/prepare-search-filter.js) are covered as well, not onlyq=(lib/search-query.js).A
textQueryUsedguard keeps a single$textper query. Terms that cannot claim it fall back to the regex matching the builder already uses for quoted phrases. That fallback now mirrors$textsemantics: a merged AND term stays ANDed, and a negated term keeps excluding instead of widening the result set.Verified
Against MongoDB 7 with the full
messagesindex set fromindexes.yaml:failed to use text index,No query solutionsorToo many text expressionsnow all plan, every one of them asTEXT_MATCHover thefulltextindex, no collection scansTest suites:
test/api/messages-test.js100 passing, full API suite 681 passing, protocol and unit suites 385 passing.Note, out of scope
lib/handlers/on-search.js:71is the third$textproducer in the repo. It hoists an IMAPTEXT/BODYterm to the root of the query on the strength of a comment saying fulltext cannot live inside$or. That path was checked against MongoDB and is safe as written, but the comment is now only half true and the hoisting changes what anOR TEXT ...search matches. Worth a separate look.