feat: add title-only search mode

Adds a titleOnly flag to page search: the Postgres driver matches
titles via trigram-indexed unaccented LIKE ranked by word_similarity,
and a Match filter in the search spotlight switches between everything
and title-only matching.
This commit is contained in:
Philipinho
2026-08-24 21:37:28 +01:00
parent 99288ff8ad
commit 5f270d0502
8 changed files with 111 additions and 12 deletions
@@ -16,6 +16,7 @@ import {
IconCheck,
IconUser,
IconTag,
IconLetterCase,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
@@ -53,6 +54,7 @@ export function SearchSpotlightFilters({
null
);
const [selectedLabelIds, setSelectedLabelIds] = useState<string[]>([]);
const [titleOnly, setTitleOnly] = useState(false);
const [openedFilter, setOpenedFilter] = useState<string | null>(null);
const [visibleFilters, setVisibleFilters] = useState<string[]>([]);
const [workspace] = useAtom(workspaceAtom);
@@ -77,12 +79,14 @@ export function SearchSpotlightFilters({
contentType,
creatorId: selectedCreatorId,
labelIds: selectedLabelIds,
titleOnly,
});
}, [
selectedSpaceId,
contentType,
selectedCreatorId,
selectedLabelIds,
titleOnly,
onFiltersChange,
]);
@@ -250,6 +254,55 @@ export function SearchSpotlightFilters({
</Menu.Dropdown>
</Menu>
{contentType !== "attachment" && !isAiMode && (
<Menu
shadow="md"
width={200}
position="bottom-start"
zIndex={getDefaultZIndex("max")}
>
<Menu.Target>
<Button
variant="subtle"
color="gray"
size="sm"
rightSection={<IconChevronDown size={14} />}
leftSection={<IconLetterCase size={16} />}
className={classes.filterButton}
fw={500}
>
{`${t("Match")}: ${titleOnly ? t("Title only") : t("Everything")}`}
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
component={RadioMenuItem}
aria-checked={!titleOnly}
onClick={() => setTitleOnly(false)}
>
<Group flex="1" gap="xs">
<Text size="sm" style={{ flex: 1 }}>
{t("Everything")}
</Text>
{!titleOnly && <IconCheck size={20} aria-hidden />}
</Group>
</Menu.Item>
<Menu.Item
component={RadioMenuItem}
aria-checked={titleOnly}
onClick={() => setTitleOnly(true)}
>
<Group flex="1" gap="xs">
<Text size="sm" style={{ flex: 1 }}>
{t("Title only")}
</Text>
{titleOnly && <IconCheck size={20} aria-hidden />}
</Group>
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
{orderedVisibleFilters.map((filterKey) => {
if (filterKey === "creator") {
return (
@@ -33,6 +33,7 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
contentType?: string;
creatorId?: string | null;
labelIds?: string[];
titleOnly?: boolean;
}>({
contentType: "page",
});
@@ -58,6 +59,10 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
params.labelIds = filters.labelIds;
}
if (filters.titleOnly) {
params.titleOnly = true;
}
return params;
}, [debouncedSearchQuery, filters]);
@@ -38,6 +38,7 @@ export interface IPageSearchParams {
shareId?: string;
creatorId?: string;
labelIds?: string[];
titleOnly?: boolean;
}
export interface IAttachmentSearch {