feat: Update search functionality with FlexSearchService
This commit is contained in:
parent
8c29d760db
commit
19a697e787
@ -50,7 +50,7 @@ export function Hero() {
|
|||||||
Souviens-toi que tu développeras.
|
Souviens-toi que tu développeras.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-2xl tracking-tight text-slate-400">
|
<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>
|
</p>
|
||||||
<div className="mt-8 flex gap-4 md:justify-center lg:justify-start">
|
<div className="mt-8 flex gap-4 md:justify-center lg:justify-start">
|
||||||
<Button href="/">Accédez aux ressources</Button>
|
<Button href="/">Accédez aux ressources</Button>
|
||||||
|
|||||||
@ -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[]> => {
|
export const onSearch = async (query: string): Promise<SearchResult[]> => {
|
||||||
const searchIndex = buildSearchIndex("./app/docs");
|
// const searchIndex = buildSearchIndex("./data/docs");
|
||||||
return search(searchIndex, query);
|
// return search(searchIndex, query);
|
||||||
|
|
||||||
|
const search = buildFlexSearch(await docsService.fetchDocs());
|
||||||
|
const results = search(query);
|
||||||
|
|
||||||
|
return results;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { forwardRef, Fragment, Suspense, useCallback, useEffect, useId, useRef, useState } from "react";
|
||||||
import Highlighter from "react-highlight-words";
|
import Highlighter from "react-highlight-words";
|
||||||
import { usePageContext } from "vike-react/usePageContext";
|
import { usePageContext } from "vike-react/usePageContext";
|
||||||
@ -15,8 +18,6 @@ import clsx from "clsx";
|
|||||||
import { navigation } from "@/lib/navigation";
|
import { navigation } from "@/lib/navigation";
|
||||||
import { onSearch } from "./Search.telefunc";
|
import { onSearch } from "./Search.telefunc";
|
||||||
|
|
||||||
import type { SearchResult } from "@/lib/search";
|
|
||||||
|
|
||||||
type EmptyObject = Record<string, never>;
|
type EmptyObject = Record<string, never>;
|
||||||
|
|
||||||
type Autocomplete = AutocompleteApi<SearchResult, React.SyntheticEvent, React.MouseEvent, React.KeyboardEvent>;
|
type Autocomplete = AutocompleteApi<SearchResult, React.SyntheticEvent, React.MouseEvent, React.KeyboardEvent>;
|
||||||
@ -39,7 +40,7 @@ function useAutocomplete({ close }: { close: (autocomplete: Autocomplete) => voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// router.push(itemUrl);
|
routerNavigate(itemUrl);
|
||||||
|
|
||||||
if (itemUrl === window.location.pathname + window.location.search + window.location.hash) {
|
if (itemUrl === window.location.pathname + window.location.search + window.location.hash) {
|
||||||
close(autocomplete);
|
close(autocomplete);
|
||||||
@ -66,8 +67,6 @@ function useAutocomplete({ close }: { close: (autocomplete: Autocomplete) => voi
|
|||||||
{
|
{
|
||||||
sourceId: "documentation",
|
sourceId: "documentation",
|
||||||
getItems() {
|
getItems() {
|
||||||
console.log({ searchResult });
|
|
||||||
return [];
|
|
||||||
return searchResult;
|
return searchResult;
|
||||||
},
|
},
|
||||||
getItemUrl({ item }) {
|
getItemUrl({ item }) {
|
||||||
@ -118,9 +117,9 @@ function SearchResult({
|
|||||||
collection,
|
collection,
|
||||||
query,
|
query,
|
||||||
}: {
|
}: {
|
||||||
result: Result;
|
result: SearchResult;
|
||||||
autocomplete: Autocomplete;
|
autocomplete: Autocomplete;
|
||||||
collection: AutocompleteCollection<Result>;
|
collection: AutocompleteCollection<SearchResult>;
|
||||||
query: string;
|
query: string;
|
||||||
}) {
|
}) {
|
||||||
let id = useId();
|
let id = useId();
|
||||||
@ -173,7 +172,7 @@ function SearchResults({
|
|||||||
}: {
|
}: {
|
||||||
autocomplete: Autocomplete;
|
autocomplete: Autocomplete;
|
||||||
query: string;
|
query: string;
|
||||||
collection: AutocompleteCollection<Result>;
|
collection: AutocompleteCollection<SearchResult>;
|
||||||
}) {
|
}) {
|
||||||
if (collection.items.length === 0) {
|
if (collection.items.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -204,7 +203,7 @@ const SearchInput = forwardRef<
|
|||||||
React.ComponentRef<"input">,
|
React.ComponentRef<"input">,
|
||||||
{
|
{
|
||||||
autocomplete: Autocomplete;
|
autocomplete: Autocomplete;
|
||||||
autocompleteState: AutocompleteState<Result> | EmptyObject;
|
autocompleteState: AutocompleteState<SearchResult> | EmptyObject;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
|
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
|
||||||
@ -391,7 +390,7 @@ export function Search() {
|
|||||||
{...buttonProps}
|
{...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" />
|
<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 && (
|
{modifierKey && (
|
||||||
<kbd className="ml-auto hidden font-medium text-slate-400 md:block dark:text-slate-500">
|
<kbd className="ml-auto hidden font-medium text-slate-400 md:block dark:text-slate-500">
|
||||||
<kbd className="font-sans">{modifierKey}</kbd>
|
<kbd className="font-sans">{modifierKey}</kbd>
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export interface SearchResult {
|
|||||||
function toString(node: Node): string {
|
function toString(node: Node): string {
|
||||||
let str = node.type === "text" && typeof node.attributes?.content === "string" ? node.attributes.content : "";
|
let str = node.type === "text" && typeof node.attributes?.content === "string" ? node.attributes.content : "";
|
||||||
if ("children" in node) {
|
if ("children" in node) {
|
||||||
for (let child of node.children) {
|
for (let child of node.children!) {
|
||||||
str += toString(child);
|
str += toString(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,14 +46,14 @@ function extractSections(node: Node, sections: Section[], isRoot: boolean = true
|
|||||||
}
|
}
|
||||||
if (node.type === "heading" || node.type === "paragraph") {
|
if (node.type === "heading" || node.type === "paragraph") {
|
||||||
let content = toString(node).trim();
|
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);
|
let hash = node.attributes?.id ?? slugify(content);
|
||||||
sections.push({ content, hash, subsections: [] });
|
sections.push({ content, hash, subsections: [] });
|
||||||
} else {
|
} else {
|
||||||
sections[sections.length - 1].subsections.push(content);
|
sections[sections.length - 1].subsections.push(content);
|
||||||
}
|
}
|
||||||
} else if ("children" in node) {
|
} else if ("children" in node) {
|
||||||
for (let child of node.children) {
|
for (let child of node.children!) {
|
||||||
extractSections(child, sections, false);
|
extractSections(child, sections, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,6 +85,7 @@ export function buildSearchIndex(pagesDir: string): FlexSearch.Document<SearchRe
|
|||||||
sections = cache.get(file)![1];
|
sections = cache.get(file)![1];
|
||||||
} else {
|
} else {
|
||||||
const ast = Markdoc.parse(md);
|
const ast = Markdoc.parse(md);
|
||||||
|
console.log(ast.attributes);
|
||||||
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
|
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
|
||||||
sections = [{ content: title ?? "", subsections: [] }];
|
sections = [{ content: title ?? "", subsections: [] }];
|
||||||
extractSections(ast, sections);
|
extractSections(ast, sections);
|
||||||
@ -113,14 +114,18 @@ export function search(
|
|||||||
query: string,
|
query: string,
|
||||||
options: Record<string, any> = {},
|
options: Record<string, any> = {},
|
||||||
): SearchResult[] {
|
): SearchResult[] {
|
||||||
const result = sectionIndex.search(query, {
|
const results = sectionIndex.search(query, {
|
||||||
...options,
|
...options,
|
||||||
enrich: true,
|
enrich: true,
|
||||||
});
|
});
|
||||||
if (result.length === 0) {
|
|
||||||
|
// console.log({ sectionIndex, query, options, results });
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return result[0].result.map((item: any) => ({
|
|
||||||
|
return results[0].result.map((item: any) => ({
|
||||||
url: item.id,
|
url: item.id,
|
||||||
title: item.doc.title,
|
title: item.doc.title,
|
||||||
pageTitle: item.doc.pageTitle,
|
pageTitle: item.doc.pageTitle,
|
||||||
|
|||||||
@ -53,6 +53,7 @@
|
|||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.26.0",
|
"typescript-eslint": "^8.26.0",
|
||||||
|
"user-agent-data-types": "^0.4.2",
|
||||||
"vite": "^6.2.1"
|
"vite": "^6.2.1"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
|
|||||||
8
app/pnpm-lock.yaml
generated
8
app/pnpm-lock.yaml
generated
@ -144,6 +144,9 @@ importers:
|
|||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.26.0
|
specifier: ^8.26.0
|
||||||
version: 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
|
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:
|
vite:
|
||||||
specifier: ^6.2.1
|
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)
|
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:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
user-agent-data-types@0.4.2:
|
||||||
|
resolution: {integrity: sha512-jXep3kO/dGNmDOkbDa8ccp4QArgxR4I76m3QVcJ1aOF0B9toc+YtSXtX5gLdDTZXyWlpQYQrABr6L1L2GZOghw==}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@ -5464,6 +5470,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
user-agent-data-types@0.4.2: {}
|
||||||
|
|
||||||
util-deprecate@1.0.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))):
|
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))):
|
||||||
|
|||||||
@ -100,6 +100,11 @@ class DocsService {
|
|||||||
return { key, sections };
|
return { key, sections };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async buildSearch() {
|
||||||
|
const data = await this.fetchDocs();
|
||||||
this.search = buildFlexSearch(data);
|
this.search = buildFlexSearch(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { FlexSearchData } from "./DocsService";
|
|||||||
|
|
||||||
import FlexSearch from "flexsearch";
|
import FlexSearch from "flexsearch";
|
||||||
|
|
||||||
interface SearchResult {
|
interface NativeSearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
doc: {
|
doc: {
|
||||||
title: string;
|
title: string;
|
||||||
@ -10,6 +10,12 @@ interface SearchResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SearchResult = {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
pageTitle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function buildFlexSearch(data: FlexSearchData) {
|
export function buildFlexSearch(data: FlexSearchData) {
|
||||||
const sectionIndex = new FlexSearch.Document({
|
const sectionIndex = new FlexSearch.Document({
|
||||||
tokenize: "full",
|
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, {
|
const result = sectionIndex.search<true>(query, 5, {
|
||||||
enrich: true,
|
enrich: true,
|
||||||
});
|
});
|
||||||
@ -44,7 +50,7 @@ export function buildFlexSearch(data: FlexSearchData) {
|
|||||||
if (result.length === 0) return [];
|
if (result.length === 0) return [];
|
||||||
|
|
||||||
return result[0].result.map((rawItem) => {
|
return result[0].result.map((rawItem) => {
|
||||||
const item = rawItem as unknown as SearchResult;
|
const item = rawItem as unknown as NativeSearchResult;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: item.id,
|
url: item.id,
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
"types": ["vite/client", "vike-react"],
|
"types": ["vite/client", "vike-react", "user-agent-data-types"],
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "react",
|
"jsxImportSource": "react",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user