Added meilisearch #89

Merged
DrMint merged 8 commits from meilisearch into main 2023-01-07 00:59:55 +00:00
8 changed files with 172 additions and 94 deletions
Showing only changes of commit cd7c163d6e - Show all commits

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { MaterialSymbol } from "material-symbols";
import { Popup } from "components/Containers/Popup";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
@ -46,16 +47,25 @@ export const SearchPopup = (): JSX.Element => {
const fetchLibraryItems = async () => {
const searchResult = await meiliSearch(MeiliIndices.LIBRARY_ITEM, query, {
limit: SEARCH_LIMIT,
attributesToRetrieve: [
"title",
"subtitle",
"descriptions",
"id",
"slug",
"thumbnail",
"release_date",
"price",
"categories",
"metadata",
],
attributesToHighlight: ["title", "subtitle", "descriptions"],
attributesToCrop: ["descriptions"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (
isDefined(item._formatted) &&
item._matchesPosition.descriptions &&
item._matchesPosition.descriptions.length > 0
) {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("descriptions"))) {
item._formatted.descriptions = filterDefined(item._formatted.descriptions).filter(
(description) => description.includes("</mark>")
(description) => containsHighlight(JSON.stringify(description))
);
}
return item;
@ -65,16 +75,15 @@ export const SearchPopup = (): JSX.Element => {
const fetchContents = async () => {
const searchResult = await meiliSearch(MeiliIndices.CONTENT, query, {
attributesToCrop: ["translations.description"],
limit: SEARCH_LIMIT,
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
attributesToHighlight: ["translations"],
attributesToCrop: ["translations.displayable_description"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (
Object.keys(item._matchesPosition).filter((match) => match.startsWith("translations"))
.length > 0
) {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
(translation) => containsHighlight(JSON.stringify(translation))
);
}
return item;
@ -94,8 +103,8 @@ export const SearchPopup = (): JSX.Element => {
"duration",
"description",
],
attributesToHighlight: ["title", "channel", "description"],
attributesToCrop: ["description"],
sort: ["sortable_published_date:desc"],
});
setVideos(searchResult);
};
@ -106,9 +115,16 @@ export const SearchPopup = (): JSX.Element => {
attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"],
attributesToHighlight: ["translations.title", "translations.excerpt", "translations.body"],
attributesToCrop: ["translations.body"],
sort: ["sortable_date:desc"],
filter: ["hidden = false"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
setPosts(searchResult);
};
@ -123,6 +139,17 @@ export const SearchPopup = (): JSX.Element => {
],
attributesToCrop: ["translations.displayable_description"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (
Object.keys(item._matchesPosition).filter((match) => match.startsWith("translations"))
.length > 0
) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
setWikiPages(searchResult);
};
@ -159,22 +186,27 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(libraryItems) && (
<SearchResultSection
title={langui.library}
href={`/library?page=1&query=${query}&sort=0&primary=true&secondary=true&subitems=true&status=all`}
icon="auto_stories"
href={`/library?page=1&query=${query}\
&sort=0&primary=true&secondary=true&subitems=true&status=all`}
totalHits={libraryItems.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{libraryItems.hits.map((item) => (
<PreviewCard
<TranslatedPreviewCard
key={item.id}
className="w-52"
className="w-56"
href={`/library/${item.slug}`}
title={item._formatted.title}
subtitle={item._formatted.subtitle}
description={
item._matchesPosition.descriptions &&
item._matchesPosition.descriptions.length > 0
? item._formatted.descriptions?.[0]
: undefined
}
translations={filterHasAttributes(item._formatted.descriptions, [
"language.data.attributes.code",
] as const).map((translation) => ({
language: translation.language.data.attributes.code,
title: item.title,
subtitle: item.subtitle,
description: containsHighlight(translation.description)
? translation.description
: undefined,
}))}
fallback={{ title: item._formatted.title, subtitle: item._formatted.subtitle }}
thumbnail={item.thumbnail?.data?.attributes}
thumbnailAspectRatio="21/29.7"
thumbnailRounded={false}
@ -201,18 +233,25 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(contents) && (
<SearchResultSection
title={langui.contents}
icon="workspaces"
href={`/contents/all?page=1&query=${query}&sort=0`}
totalHits={contents.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{contents.hits.map((item) => (
<PreviewCard
<TranslatedPreviewCard
key={item.id}
className="w-56"
href={`/contents/${item.slug}`}
pre_title={item._formatted.translations?.[0]?.pre_title}
title={item._formatted.translations?.[0]?.title}
subtitle={item._formatted.translations?.[0]?.subtitle}
description={item._formatted.translations?.[0]?.description}
translations={filterHasAttributes(item._formatted.translations, [
"language.data.attributes.code",
] as const).map(({ displayable_description, language, ...otherAttributes }) => ({
...otherAttributes,
description: containsHighlight(displayable_description)
? displayable_description
: undefined,
language: language.data.attributes.code,
}))}
fallback={{ title: prettySlug(item.slug) }}
thumbnail={item.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2"
thumbnailForceAspectRatio
@ -238,7 +277,8 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(wikiPages) && (
<SearchResultSection
title={langui.wiki}
href={"/wiki"}
icon="travel_explore"
href={`/wiki?page=1&query=${query}`}
totalHits={wikiPages.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{wikiPages.hits.map((item) => (
@ -288,6 +328,7 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(posts) && (
<SearchResultSection
title={langui.news}
icon="newspaper"
href={`/news?page=1&query=${query}`}
totalHits={posts.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
@ -329,6 +370,7 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(videos) && (
<SearchResultSection
title={langui.videos}
icon="movie"
href={`/archives/videos?page=1&query=${query}&sort=1&gone=`}
totalHits={videos.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
@ -370,21 +412,34 @@ export const SearchPopup = (): JSX.Element => {
interface SearchResultSectionProps {
title?: string | null;
icon: MaterialSymbol;
href: string;
totalHits?: number;
children: React.ReactNode;
}
const SearchResultSection = ({ title, href, totalHits, children }: SearchResultSectionProps) => (
const SearchResultSection = ({
title,
icon,
href,
totalHits,
children,
}: SearchResultSectionProps) => (
<>
{isDefined(totalHits) && totalHits > 0 && (
<div>
<div className="mb-6 grid place-content-start">
<UpPressable className="px-6 py-4" href={href}>
<p className="font-headers text-lg">{title}</p>
{isDefined(totalHits) && totalHits > SEARCH_LIMIT && (
<p className="text-sm">{`(showing ${SEARCH_LIMIT} out of ${totalHits} results)`}</p>
)}
<UpPressable
className="grid grid-cols-[auto_1fr] place-items-center gap-6 px-6 py-4"
href={href}>
<Ico icon={icon} className="!text-3xl" isFilled />
<div>
<p className="font-headers text-lg">{title}</p>
{isDefined(totalHits) && totalHits > SEARCH_LIMIT && (
/* TODO: Langui */
<p className="text-sm">{`(showing ${SEARCH_LIMIT} out of ${totalHits} results)`}</p>
)}
</div>
</UpPressable>
</div>
{children}

View File

@ -123,8 +123,8 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
"duration",
"description",
],
attributesToHighlight: ["title", "channel", "description"],
attributesToCrop: ["description"],
attributesToHighlight: ["*"],
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
filter: [onlyShowGone ? "gone = true" : "", `channel_uid = ${channel.uid}`],
});
@ -147,7 +147,6 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
useEffect(() => {
if (router.isReady) {
console.log(router.query);
if (isDefined(router.query.page)) setPage(router.query.page);
if (isDefined(router.query.query)) setQuery(router.query.query);
if (isDefined(router.query.sort)) setSortingMethod(router.query.sort);

View File

@ -117,8 +117,8 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
"duration",
"description",
],
attributesToHighlight: ["title", "channel", "description"],
attributesToCrop: ["description"],
attributesToHighlight: ["*"],
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
filter: onlyShowGone ? ["gone = true"] : undefined,
});
@ -141,7 +141,6 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
useEffect(() => {
if (router.isReady) {
console.log(router.query);
if (isDefined(router.query.page)) setPage(router.query.page);
if (isDefined(router.query.query)) setQuery(router.query.query);
if (isDefined(router.query.sort)) setSortingMethod(router.query.sort);

View File

@ -109,7 +109,8 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
<h2 className="text-2xl">{langui.channel}</h2>
<div>
<Button
href={`/archives/videos/c/${video.channel.data.attributes.uid}?page=1&query=&sort=1&gone=`}
href={`/archives/videos/c/${video.channel.data.attributes.uid}\
?page=1&query=&sort=1&gone=`}
text={video.channel.data.attributes.title}
/>
<p>

View File

@ -88,16 +88,15 @@ const Contents = (props: Props): JSX.Element => {
const fetchPosts = async () => {
const currentSortingMethod = sortingMethods[sortingMethod];
const searchResult = await meiliSearch(MeiliIndices.CONTENT, query, {
attributesToCrop: ["translations.description"],
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
attributesToHighlight: ["translations"],
attributesToCrop: ["translations.displayable_description"],
hitsPerPage: 25,
page,
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
});
searchResult.hits = searchResult.hits.map((item) => {
if (
Object.keys(item._matchesPosition).filter((match) => match.startsWith("translations"))
.length > 0
) {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => containsHighlight(JSON.stringify(translation))
);
@ -212,9 +211,11 @@ const Contents = (props: Props): JSX.Element => {
href={`/contents/${item.slug}`}
translations={filterHasAttributes(item._formatted.translations, [
"language.data.attributes.code",
] as const).map(({ description, language, ...otherAttributes }) => ({
] as const).map(({ displayable_description, language, ...otherAttributes }) => ({
...otherAttributes,
description: containsHighlight(description) ? description : undefined,
description: containsHighlight(displayable_description)
? displayable_description
: undefined,
language: language.data.attributes.code,
}))}
fallback={{ title: prettySlug(item.slug) }}

View File

@ -14,17 +14,23 @@ import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { filterDefined, isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
import {
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
isUndefined,
} from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { CustomSearchResponse, meiliSearch } from "helpers/search";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search";
import { MeiliIndices, MeiliLibraryItem } from "shared/meilisearch-graphql-typings/meiliTypes";
import { useTypedRouter } from "hooks/useTypedRouter";
import { PreviewCard } from "components/PreviewCard";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { prettyItemSubType } from "helpers/formatters";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
@ -156,18 +162,27 @@ const Library = (props: Props): JSX.Element => {
const searchResult = await meiliSearch(MeiliIndices.LIBRARY_ITEM, query, {
hitsPerPage: 25,
page,
attributesToRetrieve: [
"title",
"subtitle",
"descriptions",
"id",
"slug",
"thumbnail",
"release_date",
"price",
"categories",
"metadata",
],
attributesToHighlight: ["title", "subtitle", "descriptions"],
attributesToCrop: ["descriptions"],
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
filter,
});
searchResult.hits = searchResult.hits.map((item) => {
if (
isDefined(item._formatted) &&
item._matchesPosition.descriptions &&
item._matchesPosition.descriptions.length > 0
) {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("descriptions"))) {
item._formatted.descriptions = filterDefined(item._formatted.descriptions).filter(
(description) => description.includes("</mark>")
(description) => containsHighlight(JSON.stringify(description))
);
}
return item;
@ -188,16 +203,17 @@ const Library = (props: Props): JSX.Element => {
]);
useEffect(() => {
if (router.isReady) console.log(router.query, filterUserStatus);
router.updateQuery({
page,
query,
sort: sortingMethod,
primary: showPrimaryItems,
secondary: showSecondaryItems,
subitems: showSubitems,
status: fromLibraryItemUserStatusToString(filterUserStatus),
});
if (router.isReady) {
router.updateQuery({
page,
query,
sort: sortingMethod,
primary: showPrimaryItems,
secondary: showSecondaryItems,
subitems: showSubitems,
status: fromLibraryItemUserStatusToString(filterUserStatus),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
page,
@ -212,7 +228,6 @@ const Library = (props: Props): JSX.Element => {
useEffect(() => {
if (router.isReady) {
console.log(router.query);
if (isDefined(router.query.page)) setPage(router.query.page);
if (isDefined(router.query.query)) setQuery(router.query.query);
if (isDefined(router.query.sort)) setSortingMethod(router.query.sort);
@ -387,16 +402,20 @@ const Library = (props: Props): JSX.Element => {
className="grid grid-cols-[repeat(auto-fill,_minmax(12rem,1fr))] items-end
gap-x-6 gap-y-8">
{libraryItems?.hits.map((item) => (
<PreviewCard
<TranslatedPreviewCard
key={item.id}
href={`/library/${item.slug}`}
title={item._formatted.title}
subtitle={item._formatted.subtitle}
description={
item._matchesPosition.descriptions && item._matchesPosition.descriptions.length > 0
? item._formatted.descriptions?.[0]
: undefined
}
translations={filterHasAttributes(item._formatted.descriptions, [
"language.data.attributes.code",
] as const).map((translation) => ({
language: translation.language.data.attributes.code,
title: item.title,
subtitle: item.subtitle,
description: containsHighlight(translation.description)
? translation.description
: undefined,
}))}
fallback={{ title: item._formatted.title, subtitle: item._formatted.subtitle }}
thumbnail={item.thumbnail?.data?.attributes}
thumbnailAspectRatio="21/29.7"
thumbnailRounded={false}

View File

@ -11,7 +11,12 @@ import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import {
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine";
@ -78,6 +83,14 @@ const News = ({ ...otherProps }: Props): JSX.Element => {
sort: ["sortable_date:desc"],
filter: ["hidden = false"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
setPosts(searchResult);
};
fetchPosts();

View File

@ -11,9 +11,8 @@ import {
WikiPageAttributesFragment,
} from "./generated";
export interface MeiliLibraryItem extends Omit<LibraryItemAttributesFragment, "descriptions"> {
export interface MeiliLibraryItem extends LibraryItemAttributesFragment {
id: string;
descriptions: string[];
sortable_name: string;
sortable_price: number | undefined;
sortable_date: number | undefined;
@ -23,20 +22,12 @@ export interface MeiliLibraryItem extends Omit<LibraryItemAttributesFragment, "d
export interface MeiliContent
extends Omit<ContentAttributesFragment, "translations" | "updatedAt"> {
id: string;
translations?: Array<{
__typename?: "ComponentTranslationsTitle";
pre_title?: string | null;
title: string;
subtitle?: string | null;
description?: string | null;
language?: {
__typename?: "LanguageEntityResponse";
data?: {
__typename?: "LanguageEntity";
attributes?: { __typename?: "Language"; code: string } | null;
} | null;
} | null;
} | null> | null;
translations: (Omit<
NonNullable<NonNullable<ContentAttributesFragment["translations"]>[number]>,
"text_set" | "description"
> & {
displayable_description?: string | null;
})[];
sortable_updated_date: number;
}