diff --git a/apps/client/src/features/search/components/search-spotlight.tsx b/apps/client/src/features/search/components/search-spotlight.tsx
index ee16f08b5..569c85b20 100644
--- a/apps/client/src/features/search/components/search-spotlight.tsx
+++ b/apps/client/src/features/search/components/search-spotlight.tsx
@@ -106,6 +106,8 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
}
}, [aiSearchError, t]);
+ const isLabelBrowse = (filters.labelIds?.length ?? 0) > 0;
+
// Determine result type for rendering
const isAttachmentSearch =
filters.contentType === "attachment" && hasAttachmentIndexing;
@@ -227,17 +229,19 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
>
) : (
<>
- {query.length === 0 && resultItems.length === 0 && (
+ {query.length === 0 && !isLabelBrowse && resultItems.length === 0 && (
{t("Start typing to search...")}
)}
- {query.length > 0 && !isFetching && resultItems.length === 0 && (
- {t("No results found...")}
- )}
+ {(query.length > 0 || isLabelBrowse) &&
+ !isFetching &&
+ resultItems.length === 0 && (
+ {t("No results found...")}
+ )}
{resultItems.length > 0 && <>{resultItems}>}
- {query.length > 0 && isFetching && (
+ {(query.length > 0 || isLabelBrowse) && isFetching && (
{t("Searching...")}
diff --git a/apps/client/src/features/search/hooks/use-unified-search.ts b/apps/client/src/features/search/hooks/use-unified-search.ts
index 156c711d8..500b2fd60 100644
--- a/apps/client/src/features/search/hooks/use-unified-search.ts
+++ b/apps/client/src/features/search/hooks/use-unified-search.ts
@@ -39,11 +39,11 @@ export function useUnifiedSearch(
return await searchPage(backendParams);
}
},
- enabled: !!params.query && enabled,
+ enabled: (!!params.query || (params.labelIds?.length ?? 0) > 0) && enabled,
// keep previous results only within the same search type; page results
// rendered as attachments (or vice versa) crash on missing fields
placeholderData: (previousData, previousQuery) => {
- if (params.query.length < 1) return undefined;
+ if (!params.query && !params.labelIds?.length) return undefined;
if (previousQuery && previousQuery.queryKey[1] !== searchType) {
return undefined;
}
diff --git a/apps/server/src/core/search/dto/search.dto.ts b/apps/server/src/core/search/dto/search.dto.ts
index d66bc9fd8..89fb1b289 100644
--- a/apps/server/src/core/search/dto/search.dto.ts
+++ b/apps/server/src/core/search/dto/search.dto.ts
@@ -9,9 +9,9 @@ import {
} from 'class-validator';
export class SearchDTO {
- @IsNotEmpty()
+ @IsOptional()
@IsString()
- query: string;
+ query?: string;
@IsOptional()
@IsUUID()
diff --git a/apps/server/src/core/search/search.service.ts b/apps/server/src/core/search/search.service.ts
index ceb1468d3..4a0aa2e6e 100644
--- a/apps/server/src/core/search/search.service.ts
+++ b/apps/server/src/core/search/search.service.ts
@@ -29,24 +29,28 @@ export class SearchService {
workspaceId: string;
},
): Promise<{ items: SearchResponseDto[] }> {
- const { query } = searchParams;
+ const query = searchParams.query?.trim() ?? '';
+ const labelIds = [...new Set(searchParams.labelIds ?? [])];
+ // selected labels are browsable without a query
+ const browseByLabels = query.length < 1 && labelIds.length > 0;
- if (query.length < 1) {
+ if (query.length < 1 && !browseByLabels) {
return { items: [] };
}
- const searchQuery = tsquery(query.trim() + '*');
- const labelIds = [...new Set(searchParams.labelIds ?? [])];
+ const searchQuery = tsquery(query + '*');
const titleOnly = searchParams.titleOnly === true;
- const titleQuery = query.trim();
+ const titleQuery = query;
- const rankColumn = titleOnly
- ? sql`word_similarity(lower(f_unaccent(${titleQuery})), lower(f_unaccent(pages.title)))`.as(
- 'rank',
- )
- : sql`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as(
- 'rank',
- );
- const highlightColumn = titleOnly
+ const rankColumn = browseByLabels
+ ? sql`0`.as('rank')
+ : titleOnly
+ ? sql`word_similarity(lower(f_unaccent(${titleQuery})), lower(f_unaccent(pages.title)))`.as(
+ 'rank',
+ )
+ : sql`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as(
+ 'rank',
+ );
+ const highlightColumn = browseByLabels || titleOnly
? sql`''`.as('highlight')
: sql`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
'highlight',
@@ -66,14 +70,14 @@ export class SearchService {
rankColumn,
highlightColumn,
])
- .$if(!titleOnly, (qb) =>
+ .$if(!browseByLabels && !titleOnly, (qb) =>
qb.where(
'tsv',
'@@',
sql`to_tsquery('english', f_unaccent(${searchQuery}))`,
),
)
- .$if(titleOnly, (qb) =>
+ .$if(!browseByLabels && titleOnly, (qb) =>
qb.where((eb) =>
eb(
sql`lower(f_unaccent(pages.title))`,
@@ -102,7 +106,8 @@ export class SearchService {
),
)
.where('deletedAt', 'is', null)
- .orderBy('rank', 'desc')
+ .$if(browseByLabels, (qb) => qb.orderBy('updatedAt', 'desc'))
+ .$if(!browseByLabels, (qb) => qb.orderBy('rank', 'desc'))
.limit(searchParams.limit || 25)
.offset(searchParams.offset || 0);
diff --git a/apps/server/src/ee b/apps/server/src/ee
index 3c198edfa..70976d963 160000
--- a/apps/server/src/ee
+++ b/apps/server/src/ee
@@ -1 +1 @@
-Subproject commit 3c198edfa02569393b954a2a77b9d1e4c1f566db
+Subproject commit 70976d963201cd4e85a8d59dce21edb3e3d287ad