Benoit Tellier created JAMES-4235:
-------------------------------------

             Summary: mailbox/lucene hardening and adoption for PG app
                 Key: JAMES-4235
                 URL: https://issues.apache.org/jira/browse/JAMES-4235
             Project: James Server
          Issue Type: Bug
          Components: lucene
            Reporter: Benoit Tellier


h3. Context

As part of a hackaton a colleague gave a try to our pg and memory app in a low 
memory setup.

Here is a sum up of the issues:

 - A No viable alternatives to OpenSearch for search snippets on top of the PG 
app
 - B SearchSnippet/get fails  with NPE for non matching text attachment
 - C Email/query subject / text / body never match strings of more than 4 words 
or words after punctuation
 - D Email/query hasKeyword / notKeyword without inMailbox matches messages by 
UID across mailboxes
 - E Expunging two or more consecutive UIDs removes every message of the 
mailbox from the search index

2026 being 2026 there's a lengthy AI description for each, I include links to 
the original reports:

 - A https://github.com/linagora/james-project/issues/5616 
 - B https://github.com/linagora/james-project/issues/5614 
 - C https://github.com/linagora/james-project/issues/5613
 - D https://github.com/linagora/james-project/issues/5612
 - E https://github.com/linagora/james-project/issues/5607

h3. Proposal

Fix B, C, D, E then for A adopt Lucene as an option for pg-app - convenient for 
self hosting. I verified it yields no classpath conflits with OpenSearch driver 
which now is Rest - only.

If this is consensual we can get some linagorians on the topic.

h3. Details

h4. B SearchSnippet/get fails  with NPE for non matching text attachment

{code:Markdown}
### Reproduction
```json
[["Email/query", {"accountId": "…", "filter": {"subject": "Quarterly"}}, "q"],
 ["SearchSnippet/get", {"accountId": "…", "filter": {"subject": "Quarterly"},
   "#emailIds": {"resultOf": "q", "name": "Email/query", "path": "/ids"}}, "s"]]
```
1. Observed: `serverFail` (NPE). Expected: snippet with highlighted subject, 
`preview: null`.

### 
Cause`mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneSearchHighlighter.java`:
1. `getHighlightAttachmentTextBody` (:188-194): `getBestFragment` returns 
`null` when nothing matches, and `Stream.findFirst()` throws NPE on a null 
element. JAMES-4077 fixed subject/body only.
2. `buildQuery` (:162-165): raw value given to the classic `QueryParser`.
3. `highlightSearch` (:118-124): asks for `limit = messageIds.size()` 
documents, but a message has one document per mailbox, so duplicates push 
others out.

### Fix
1. `.filter(Objects::nonNull)` before `findFirst()`.
2. `QueryParser.escape(value)` (or build the query from analyzer tokens); at 
worst catch `ParseException` and return a snippet without highlight.
3. Search with the usual `maxQueryResults` and keep one snippet per message id 
(`distinct`).
{code}

h4. C Email/query subject / text / body never match strings of more than 4 
words or words after punctuation

{code:Markdown}
### Reproduction
emails created in INBOX with subjects `Quarterly budget review meeting notes`, 
`Invitation <Team sync> tomorrow`:
```json
["Email/query", {"accountId": "…", "filter": {"inMailbox": "<INBOX>", 
"subject": "Quarterly budget review meeting notes"}}, "0"]
```
- Observed: no result. Expected: the email.
- `subject: "Quarterly budget review meeting"` (4 words) → found.
- `subject: "Team"` / `"Team sync"` → nothing; `text: "brown fox jumps over 
the"` on a body `The quick brown fox jumps over the lazy dog` → nothing (4 
words → found).

### Cause
At james-project `9aac85900f` (same on apache/james-project `master`), in 
`mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/`:
- `LenientImapSearchAnalyzer.java:35-50` (used at 
`LuceneMessageSearchIndex.java:245`): whitespace tokenizer, upper-case, 
shingles of up to `DEFAULT_MAX_TOKEN_LENGTH = 4` tokens; no indexed term 
exceeds 4 words, and `<TEAM` keeps its punctuation.
- `LuceneMessageSearchIndex.java:487-493` (`createTermQuery`, used by 
`createHeaderQuery` :498-512 and `createTextQuery` :701-715): the value is not 
analyzed, just `new PrefixQuery(new Term(field, value.toUpperCase()))`.

### Fix
- Query side (no reindex): beyond 4 tokens, AND per-token 
`PrefixQuery`/`TermQuery` (or a phrase query over unigrams), e.g. via the index 
analyzer without the shingle filter.
- Index side (reindex): a tokenizer that strips leading/trailing punctuation 
(e.g. `StandardTokenizer`).
- Test: "search a subject of more than 4 words".
{code}

h4. D Email/query hasKeyword / notKeyword without inMailbox matches messages by 
UID across mailboxes

{code:Markdown}
### Reproduction
fresh user, created with `Email/set`: INBOX `a1` (UID 1, keyword `label1`), 
Sent `s1` (UID 1, no keyword).
```json
["Email/query", {"accountId": "…", "filter": {"hasKeyword": "label1"}}, "0"]
```
- Observed: `[a1, s1]`. Expected: `[a1]`.
- `notKeyword: label1` → `[a1, s1]`, expected `[s1]`. With `inMailbox` results 
are correct.
- Not the Message-ID: a Sent message sharing `a2`'s Message-ID but not its UID 
is not returned; one sharing its UID is.

### Cause
- 
`mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneIndexableDocument.java:223-253`:
 flags live in a separate per-message "flags" document.
- 
`mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneMessageSearchIndex.java:576-622`
 (`createFlagQuery`): searches flags documents in all query mailboxes, collects 
only their UIDs into a `Set<MessageUid>` (:593-601), and returns a bare UID 
query (:612-618) with no mailbox; `buildQuery` (:405-417) ANDs it with "any of 
the mailboxes", so any message with a matching UID in any mailbox matches.

### Fix
- In `createFlagQuery`, collect `mailboxId -> uids` and return one `SHOULD` 
clause per mailbox (`MAILBOX_ID_FIELD` term `MUST` + UID query `MUST`); mind 
that message documents store `MAILBOX_ID_FIELD` upper-cased 
(`LuceneIndexableDocument.java:133`).
- Structural alternative: store flags on the message document, removing the 
two-step query.
- Test: "flag search across mailboxes having the same uids".
{code}

h4. E Expunging two or more consecutive UIDs removes every message of the 
mailbox from the search index


{code:Markdown}
### Reproduction
create drafts d1…d5 (one `Email/set` each, so UIDs 1…5), then:
```json
["Email/set", {"accountId": "…", "destroy": ["<d2>", "<d3>"]}, "0"]
["Email/query", {"accountId": "…", "filter": {"inMailbox": "<Drafts>"}}, "1"]
```
- Expected: d1, d4, d5.
- Observed: empty. (`destroy [d2, d4]` correctly returns d1, d3, d5; postgresql 
image returns d1, d4, d5.)

### Cause
- 
`mailbox/api/src/main/java/org/apache/james/mailbox/model/MessageRange.java:153-188`:
 `toRanges` turns consecutive UIDs into one `RANGE` (`{2, 3}` → `2:3`).
- 
`mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneMessageSearchIndex.java:831-841`:
 `createQuery(MessageRange)` has no `RANGE` case; it falls into `default`, the 
`ALL` query `[MIN, MAX]`, so `delete(mailboxId, range)` (855-861) deletes every 
document of the mailbox.

### Fix
- In `createQuery`, handle `ONE, RANGE` with `[uidFrom, uidTo]` and make the 
switch exhaustive (no `default`); add a test "expunging two consecutive uids 
keeps the other messages searchable".
{code}





--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to