From fb0d971e48480fe7862ffdf3db30d5a644381ed9 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Tue, 21 Oct 2025 10:12:14 -0700 Subject: [PATCH] fix(search): also support exact phrase matching such as `='test phrase'` --- .../search/expressions/note_content_fulltext.ts | 10 ++++++++-- .../src/services/search/services/build_comparator.ts | 12 +++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/server/src/services/search/expressions/note_content_fulltext.ts b/apps/server/src/services/search/expressions/note_content_fulltext.ts index 81250dda5..cf68f6e23 100644 --- a/apps/server/src/services/search/expressions/note_content_fulltext.ts +++ b/apps/server/src/services/search/expressions/note_content_fulltext.ts @@ -118,7 +118,7 @@ class NoteContentFulltextExp extends Expression { } /** - * Checks if content contains the exact word (with word boundaries) + * Checks if content contains the exact word (with word boundaries) or exact phrase * This is case-insensitive since content and token are already normalized */ private containsExactWord(token: string, content: string): boolean { @@ -126,7 +126,13 @@ class NoteContentFulltextExp extends Expression { const normalizedToken = normalizeSearchText(token); const normalizedContent = normalizeSearchText(content); - // Split content into words and check for exact match + // If token contains spaces, it's a multi-word phrase from quotes + // Check for substring match (consecutive phrase) + if (normalizedToken.includes(' ')) { + return normalizedContent.includes(normalizedToken); + } + + // For single words, split content into words and check for exact match const words = normalizedContent.split(/\s+/); return words.some(word => word === normalizedToken); } diff --git a/apps/server/src/services/search/services/build_comparator.ts b/apps/server/src/services/search/services/build_comparator.ts index 0f8020de3..c090b458f 100644 --- a/apps/server/src/services/search/services/build_comparator.ts +++ b/apps/server/src/services/search/services/build_comparator.ts @@ -15,18 +15,19 @@ type Comparator = (comparedValue: T) => (val: string) => boolean; const stringComparators: Record> = { "=": (comparedValue) => (val) => { // For the = operator, check if the value contains the exact word or phrase - // This is case-insensitive since both values are already lowercased + // This is case-insensitive if (!val) return false; const normalizedVal = normalizeSearchText(val); const normalizedCompared = normalizeSearchText(comparedValue); - // If comparedValue has multiple words, check for exact phrase + // If comparedValue has spaces, it's a multi-word phrase + // Check for substring match (consecutive phrase) if (normalizedCompared.includes(" ")) { return normalizedVal.includes(normalizedCompared); } - // For single word, split into words and check for exact match + // For single word, split into words and check for exact word match const words = normalizedVal.split(/\s+/); return words.some(word => word === normalizedCompared); }, @@ -37,12 +38,13 @@ const stringComparators: Record> = { const normalizedVal = normalizeSearchText(val); const normalizedCompared = normalizeSearchText(comparedValue); - // If comparedValue has multiple words, check for exact phrase + // If comparedValue has spaces, it's a multi-word phrase + // Check for substring match (consecutive phrase) and negate if (normalizedCompared.includes(" ")) { return !normalizedVal.includes(normalizedCompared); } - // For single word, split into words and check for exact match + // For single word, split into words and check for exact word match, then negate const words = normalizedVal.split(/\s+/); return !words.some(word => word === normalizedCompared); },