diff --git a/apps/server/src/services/search/services/search.ts b/apps/server/src/services/search/services/search.ts
index 3236b6edb..f80b812d7 100644
--- a/apps/server/src/services/search/services/search.ts
+++ b/apps/server/src/services/search/services/search.ts
@@ -468,8 +468,13 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
content = striptags(content);
}
- // Normalize whitespace
- content = content.replace(/\s+/g, " ").trim();
+ // Normalize whitespace while preserving paragraph breaks
+ // First, normalize multiple newlines to double newlines (paragraph breaks)
+ content = content.replace(/\n\s*\n/g, "\n\n");
+ // Then normalize spaces within lines
+ content = content.split('\n').map(line => line.replace(/\s+/g, " ").trim()).join('\n');
+ // Finally trim the whole content
+ content = content.trim();
if (!content) {
return "";
@@ -495,21 +500,36 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
// Extract snippet
let snippet = content.substring(snippetStart, snippetStart + maxLength);
- // Try to start/end at word boundaries
- if (snippetStart > 0) {
- const firstSpace = snippet.indexOf(" ");
- if (firstSpace > 0 && firstSpace < 20) {
- snippet = snippet.substring(firstSpace + 1);
- }
- snippet = "..." + snippet;
- }
-
- if (snippetStart + maxLength < content.length) {
- const lastSpace = snippet.lastIndexOf(" ");
- if (lastSpace > snippet.length - 20) {
- snippet = snippet.substring(0, lastSpace);
- }
+ // If snippet contains linebreaks, limit to max 4 lines and override character limit
+ const lines = snippet.split('\n');
+ if (lines.length > 4) {
+ snippet = lines.slice(0, 4).join('\n');
+ // Add ellipsis if we truncated lines
snippet = snippet + "...";
+ } else if (lines.length > 1) {
+ // For multi-line snippets, just limit to 4 lines (keep existing snippet)
+ snippet = lines.slice(0, 4).join('\n');
+ if (lines.length > 4) {
+ snippet = snippet + "...";
+ }
+ } else {
+ // Single line content - apply original word boundary logic
+ // Try to start/end at word boundaries
+ if (snippetStart > 0) {
+ const firstSpace = snippet.search(/\s/);
+ if (firstSpace > 0 && firstSpace < 20) {
+ snippet = snippet.substring(firstSpace + 1);
+ }
+ snippet = "..." + snippet;
+ }
+
+ if (snippetStart + maxLength < content.length) {
+ const lastSpace = snippet.search(/\s[^\s]*$/);
+ if (lastSpace > snippet.length - 20 && lastSpace > 0) {
+ snippet = snippet.substring(0, lastSpace);
+ }
+ snippet = snippet + "...";
+ }
}
return snippet;
@@ -574,7 +594,10 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
// Initialize highlighted content snippet
if (result.contentSnippet) {
- result.highlightedContentSnippet = escapeHtml(result.contentSnippet).replace(/[<{}]/g, "");
+ // Escape HTML but preserve newlines for later conversion to
+ result.highlightedContentSnippet = escapeHtml(result.contentSnippet);
+ // Remove any stray < { } that might interfere with our highlighting markers
+ result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/[<{}]/g, "");
}
}
@@ -621,7 +644,10 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
}
if (result.highlightedContentSnippet) {
+ // Replace highlighting markers with HTML tags
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/{/g, "").replace(/}/g, "");
+ // Convert newlines to
tags for HTML display
+ result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/\n/g, "
");
}
}
}