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 c36dddd74..76ea032b5 100644 --- a/apps/server/src/services/search/expressions/note_content_fulltext.ts +++ b/apps/server/src/services/search/expressions/note_content_fulltext.ts @@ -113,7 +113,19 @@ class NoteContentFulltextExp extends Expression { const normalizedFlatText = normalizeSearchText(flatText); // Check if =phrase appears in flatText (indicates attribute value match) - matches = normalizedFlatText.includes(`=${normalizedPhrase}`); + // For single words, use word-boundary matching to avoid substring matches + if (!normalizedPhrase.includes(' ')) { + // Single word: look for =word with word boundaries + // Split by = to get attribute values, then check each value for exact word match + const parts = normalizedFlatText.split('='); + matches = parts.slice(1).some(part => { + const words = part.split(/\s+/); + return words.some(word => word === normalizedPhrase); + }); + } else { + // Multi-word phrase: check for substring match + matches = normalizedFlatText.includes(`=${normalizedPhrase}`); + } if ((this.operator === "=" && matches) || (this.operator === "!=" && !matches)) { resultNoteSet.add(noteFromBecca); @@ -155,7 +167,15 @@ class NoteContentFulltextExp extends Expression { // Join tokens with single space to form the phrase const phrase = normalizedTokens.join(" "); - // Check if the phrase appears as a substring (consecutive words) + // For single-word phrases, use word-boundary matching to avoid substring matches + // e.g., "asd" should not match "asdfasdf" + if (!phrase.includes(' ')) { + // Single word: split into words and check for exact match + const words = normalizedContent.split(/\s+/); + return words.some(word => word === phrase); + } + + // For multi-word phrases, check if the phrase appears as consecutive words if (normalizedContent.includes(phrase)) { return true; }