feat: Update search functionality with FlexSearchService

This commit is contained in:
Gauthier Daniels 2025-04-11 11:43:20 +02:00
parent 8c29d760db
commit 19a697e787
9 changed files with 57 additions and 25 deletions

View File

@ -50,7 +50,7 @@ export function Hero() {
Souviens-toi que tu développeras.
</p>
<p className="mt-3 text-2xl tracking-tight text-slate-400">
Découvrez des ressources essentielles pour améliorer vos compétences en développement.
Découvrez des ressources essentielles pour améliorer tes compétences en développement.
</p>
<div className="mt-8 flex gap-4 md:justify-center lg:justify-start">
<Button href="/">Accédez aux ressources</Button>

View File

@ -1,8 +1,16 @@
import type { SearchResult } from "@/lib/search";
// import type { SearchResult } from "@/lib/search";
import type { SearchResult } from "@/services/FlexSearchService";
import { buildSearchIndex, search } from "@/lib/search";
// import { buildSearchIndex, search } from "@/lib/search";
import { buildFlexSearch } from "@/services/FlexSearchService";
import { docsService } from "@/services/DocsService";
export const onSearch = async (query: string): Promise<SearchResult[]> => {
const searchIndex = buildSearchIndex("./app/docs");
return search(searchIndex, query);
// const searchIndex = buildSearchIndex("./data/docs");
// return search(searchIndex, query);
const search = buildFlexSearch(await docsService.fetchDocs());
const results = search(query);
return results;
};

View File

@ -1,3 +1,6 @@
// import type { SearchResult } from "@/lib/search";
import type { SearchResult } from "@/services/FlexSearchService";
import { forwardRef, Fragment, Suspense, useCallback, useEffect, useId, useRef, useState } from "react";
import Highlighter from "react-highlight-words";
import { usePageContext } from "vike-react/usePageContext";
@ -15,8 +18,6 @@ import clsx from "clsx";
import { navigation } from "@/lib/navigation";
import { onSearch } from "./Search.telefunc";
import type { SearchResult } from "@/lib/search";
type EmptyObject = Record<string, never>;
type Autocomplete = AutocompleteApi<SearchResult, React.SyntheticEvent, React.MouseEvent, React.KeyboardEvent>;
@ -39,7 +40,7 @@ function useAutocomplete({ close }: { close: (autocomplete: Autocomplete) => voi
return;
}
// router.push(itemUrl);
routerNavigate(itemUrl);
if (itemUrl === window.location.pathname + window.location.search + window.location.hash) {
close(autocomplete);
@ -66,8 +67,6 @@ function useAutocomplete({ close }: { close: (autocomplete: Autocomplete) => voi
{
sourceId: "documentation",
getItems() {
console.log({ searchResult });
return [];
return searchResult;
},
getItemUrl({ item }) {
@ -118,9 +117,9 @@ function SearchResult({
collection,
query,
}: {
result: Result;
result: SearchResult;
autocomplete: Autocomplete;
collection: AutocompleteCollection<Result>;
collection: AutocompleteCollection<SearchResult>;
query: string;
}) {
let id = useId();
@ -173,7 +172,7 @@ function SearchResults({
}: {
autocomplete: Autocomplete;
query: string;
collection: AutocompleteCollection<Result>;
collection: AutocompleteCollection<SearchResult>;
}) {
if (collection.items.length === 0) {
return (
@ -204,7 +203,7 @@ const SearchInput = forwardRef<
React.ComponentRef<"input">,
{
autocomplete: Autocomplete;
autocompleteState: AutocompleteState<Result> | EmptyObject;
autocompleteState: AutocompleteState<SearchResult> | EmptyObject;
onClose: () => void;
}
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
@ -391,7 +390,7 @@ export function Search() {
{...buttonProps}
>
<SearchIcon className="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 md:group-hover:fill-slate-400 dark:fill-slate-500" />
<span className="sr-only md:not-sr-only md:ml-2 md:text-slate-500 md:dark:text-slate-400">Search docs</span>
<span className="sr-only md:not-sr-only md:ml-2 md:text-slate-500 md:dark:text-slate-400">Rechercher...</span>
{modifierKey && (
<kbd className="ml-auto hidden font-medium text-slate-400 md:block dark:text-slate-500">
<kbd className="font-sans">{modifierKey}</kbd>

View File

@ -33,7 +33,7 @@ export interface SearchResult {
function toString(node: Node): string {
let str = node.type === "text" && typeof node.attributes?.content === "string" ? node.attributes.content : "";
if ("children" in node) {
for (let child of node.children) {
for (let child of node.children!) {
str += toString(child);
}
}
@ -46,14 +46,14 @@ function extractSections(node: Node, sections: Section[], isRoot: boolean = true
}
if (node.type === "heading" || node.type === "paragraph") {
let content = toString(node).trim();
if (node.type === "heading" && node.attributes?.level <= 2) {
if (node.type === "heading" && node.attributes?.level! <= 2) {
let hash = node.attributes?.id ?? slugify(content);
sections.push({ content, hash, subsections: [] });
} else {
sections[sections.length - 1].subsections.push(content);
}
} else if ("children" in node) {
for (let child of node.children) {
for (let child of node.children!) {
extractSections(child, sections, false);
}
}
@ -85,6 +85,7 @@ export function buildSearchIndex(pagesDir: string): FlexSearch.Document<SearchRe
sections = cache.get(file)![1];
} else {
const ast = Markdoc.parse(md);
console.log(ast.attributes);
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
sections = [{ content: title ?? "", subsections: [] }];
extractSections(ast, sections);
@ -113,14 +114,18 @@ export function search(
query: string,
options: Record<string, any> = {},
): SearchResult[] {
const result = sectionIndex.search(query, {
const results = sectionIndex.search(query, {
...options,
enrich: true,
});
if (result.length === 0) {
// console.log({ sectionIndex, query, options, results });
if (results.length === 0) {
return [];
}
return result[0].result.map((item: any) => ({
return results[0].result.map((item: any) => ({
url: item.id,
title: item.doc.title,
pageTitle: item.doc.pageTitle,

View File

@ -53,6 +53,7 @@
"tsx": "^4.19.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.0",
"user-agent-data-types": "^0.4.2",
"vite": "^6.2.1"
},
"type": "module"

8
app/pnpm-lock.yaml generated
View File

@ -144,6 +144,9 @@ importers:
typescript-eslint:
specifier: ^8.26.0
version: 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
user-agent-data-types:
specifier: ^0.4.2
version: 0.4.2
vite:
specifier: ^6.2.1
version: 6.2.1(@types/node@18.19.80)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.3)
@ -2676,6 +2679,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
user-agent-data-types@0.4.2:
resolution: {integrity: sha512-jXep3kO/dGNmDOkbDa8ccp4QArgxR4I76m3QVcJ1aOF0B9toc+YtSXtX5gLdDTZXyWlpQYQrABr6L1L2GZOghw==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -5464,6 +5470,8 @@ snapshots:
dependencies:
punycode: 2.3.1
user-agent-data-types@0.4.2: {}
util-deprecate@1.0.2: {}
vike-react@0.5.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(vike@0.4.225(react-streaming@0.3.50(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.2.1(@types/node@18.19.80)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.3))):

View File

@ -100,6 +100,11 @@ class DocsService {
return { key, sections };
});
return data;
}
public async buildSearch() {
const data = await this.fetchDocs();
this.search = buildFlexSearch(data);
}

View File

@ -2,7 +2,7 @@ import type { FlexSearchData } from "./DocsService";
import FlexSearch from "flexsearch";
interface SearchResult {
interface NativeSearchResult {
id: string;
doc: {
title: string;
@ -10,6 +10,12 @@ interface SearchResult {
};
}
export type SearchResult = {
url: string;
title: string;
pageTitle?: string;
};
export function buildFlexSearch(data: FlexSearchData) {
const sectionIndex = new FlexSearch.Document({
tokenize: "full",
@ -36,7 +42,7 @@ export function buildFlexSearch(data: FlexSearchData) {
}
}
return function search(query: string) {
return function search(query: string): SearchResult[] {
const result = sectionIndex.search<true>(query, 5, {
enrich: true,
});
@ -44,7 +50,7 @@ export function buildFlexSearch(data: FlexSearchData) {
if (result.length === 0) return [];
return result[0].result.map((rawItem) => {
const item = rawItem as unknown as SearchResult;
const item = rawItem as unknown as NativeSearchResult;
return {
url: item.id,

View File

@ -11,7 +11,7 @@
"moduleResolution": "Bundler",
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["vite/client", "vike-react"],
"types": ["vite/client", "vike-react", "user-agent-data-types"],
"jsx": "preserve",
"jsxImportSource": "react",
"baseUrl": "./",