]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
fts-flatcurve: Add support for phrase searching
authorMichael M Slusarz <michael.slusarz@open-xchange.com>
Mon, 4 May 2026 22:43:09 +0000 (16:43 -0600)
committerMichael M Slusarz <michael.slusarz@open-xchange.com>
Mon, 11 May 2026 20:20:07 +0000 (14:20 -0600)
Fixes issues with false positives on certain phrase searches, as searches
on tokenized pieces are really intended to be used as a limiter on the
full phrase searching as opposed to an indication of full phrase match
on those messages.

Example 1: Exact phrase no-match (the canonical case)

Message body: "abcde fghij"
Search:       BODY "abcd fghi"
Before:       MATCHES (false positive — tokenized "abcd*" AND "fghi*" found)
After:        NO MATCH (correct — exact phrase "abcd fghi" not in body)

Example 2: Exact phrase that should match

Message body: "the quick brown fox"
Search:       BODY "quick brown"
Before:       MATCHES (worked, but for wrong reason — tokenized terms matched)
After:        MATCHES (correct — exact phrase "quick brown" verified by core)

Example 3: Words exist but not as a phrase

Message body: "brown fox quick"
Search:       BODY "quick brown"
Before:       MATCHES (false positive — both words present, just not adjacent)
After:        NO MATCH (correct — "quick brown" doesn't appear as a substring)

Example 4: Single-word split (e.g., dotted word)

Message body: "foo.bar value"
Search:       TEXT "foo.bar"
Before:       NO MATCH (broken — phrase logic incorrectly applied, original
              string "foo.bar" was skipped during token expansion)
After:        MATCHES (correct — single word without spaces is not treated
              as a phrase; "foo.bar" is indexed and matched directly)

Example 5: Multi-word search with one non-phrase word

Message body: "hello world"
Search:       BODY hello "world peace"
Before:       "hello" matched; "world peace" could produce false positives
              from tokenized "world*" AND "peace*"
After:        "hello" matches; "world peace" correctly requires exact phrase

src/plugins/fts-flatcurve/fts-backend-flatcurve-xapian.cc
src/plugins/fts-flatcurve/fts-backend-flatcurve.c

index e34775531f1e043aedcda24ea72c4841042787b4..342e54627d05315a5fc9d8092f19113d70086ff9 100644 (file)
@@ -2114,6 +2114,20 @@ fts_flatcurve_build_query_arg(struct flatcurve_fts_query *query,
        if (arg->no_fts)
                return;
 
+       /* Phrase searching is not supported natively, so we can only do
+        * single token searching (as FTS core provides index terms without
+        * positional context).
+        *
+        * We can do matching for the tokenized input, as these results reduce
+        * the message space iterated by the core search code to do the full
+        * phrase matching (since these results are ANDed with the phrase search
+        * due to FTS_BACKEND_FLAG_SEARCH_ARGS_V2 being set). */
+       if (HAS_ANY_BITS(arg->value.search_flags, MAIL_SEARCH_ARG_FLAG_PHRASE_FULL)) {
+               /* We skip the full phrase and don't set "match_always", so
+                * that the core FTS code will process this search argument. */
+               return;
+       }
+
        switch (arg->type) {
        case SEARCH_TEXT:
        case SEARCH_BODY:
@@ -2148,19 +2162,9 @@ fts_flatcurve_build_query_arg(struct flatcurve_fts_query *query,
                return;
        }
 
-       if (strchr(arg->value.str, ' ') == NULL) {
-               /* Prepare search term.
-                * This includes existence searches where arg is "" */
-               fts_flatcurve_build_query_arg_term(query, arg, arg->value.str);
-       } else {
-               /* Phrase searching is not supported natively, so we can only do
-                * single term searching with Xapian (FTS core provides index
-                * terms without positional context).
-
-                * FTS core will send both the phrase search and individual search
-                * terms separately as part of the same query. Therefore, if we
-                * encounter a multi-term search, just ignore it */
-       }
+       /* Prepare search term.
+        * This includes existence searches where arg is "" */
+       fts_flatcurve_build_query_arg_term(query, arg, arg->value.str);
 }
 
 void
index 4f926a540e43200d116dade47fc6800f1cbcf634..7db6ff5e8a610c207f64cdaa409d9cd93987fc82 100644 (file)
@@ -749,7 +749,8 @@ int fts_backend_flatcurve_delete_dir(const char *path, const char **error_r)
 
 struct fts_backend fts_backend_flatcurve = {
        .name = "flatcurve",
-       .flags = FTS_BACKEND_FLAG_TOKENIZED_INPUT,
+       .flags = FTS_BACKEND_FLAG_TOKENIZED_INPUT |
+                FTS_BACKEND_FLAG_SEARCH_ARGS_V2,
        .v = {
                .alloc = fts_backend_flatcurve_alloc,
                .init = fts_backend_flatcurve_init,