Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.0.0-alpha.9

### Patch Changes

- fix: search index fields and size

## 1.0.0-alpha.8

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@jadeja/docs",
"description": "an opinionated documentation framework",
"version": "1.0.0-alpha.8",
"version": "1.0.0-alpha.9",
"type": "module",
"exports": {
"./styles/*": {
Expand Down
51 changes: 34 additions & 17 deletions src/components/layout/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { ComponentProps, ReactElement, ReactNode } from "react";

import type { DialogCloseProps } from "@/components/dialog";
import type { SearchError, SearchQueryResult, UseSearchOptions } from "@/hooks/use-search";
import type { List as ContentList } from "@/types/content";

/* ============================================================================================= */

Expand All @@ -48,10 +49,12 @@ export interface SearchRootChildParams {
handleSearch: (text: string) => void;
error: SearchError;
result: SearchQueryResult | undefined;
contentList: ContentList;
}

export type SearchRootProps = {
children: ReactNode;
contentList: ContentList;
} & UseSearchOptions &
ComponentProps<"div">;

Expand All @@ -61,15 +64,16 @@ export const SearchRoot = ({
DEV,
SEARCH_INDEX_FIELDS,
SEARCH_INDEX_FILE_NAME,
SEARCH_INDEX_RETURN_FIELDS,
SEARCH_INDEX_QUERY_OPTIONS,
contentList,
...rest
}: SearchRootProps): ReactElement<HTMLDivElement> => {
//
const { initialize, ready, error, query } = useSearch({
DEV,
SEARCH_INDEX_FIELDS,
SEARCH_INDEX_FILE_NAME,
SEARCH_INDEX_RETURN_FIELDS,
SEARCH_INDEX_QUERY_OPTIONS,
});

const [search, setSearch] = useState("");
Expand All @@ -89,19 +93,18 @@ export const SearchRoot = ({
startTransition(() => {
//
debounce(() => {
const fn = async () => {
// oxlint-disable-next-line typescript/no-floating-promises
(async () => {
const queryResult = await query(text);
setResult(queryResult);
};
// oxlint-disable-next-line typescript/no-floating-promises
fn();
})();
});
});
};

return (
// oxlint-disable-next-line react/jsx-no-constructed-context-values
<SearchContext.Provider value={{ search, ready, handleSearch, error, result }}>
<SearchContext.Provider value={{ search, ready, handleSearch, error, result, contentList }}>
<div className={cls("search", className)} {...rest}>
{children}
</div>
Expand All @@ -111,7 +114,7 @@ export const SearchRoot = ({

/* ============================================================================================= */

export type SearchProps = UseSearchOptions;
export type SearchProps = UseSearchOptions & { contentList: ContentList };

export const Search = (props: SearchProps): ReturnType<typeof SearchRoot> => {
return (
Expand Down Expand Up @@ -279,8 +282,12 @@ export const SearchResultContainer = ({
children,
className,
}: SearchResultContainerProps): ReactElement<HTMLDivElement> => {
const { result } = useSearchContext();
return (
<div className={cls("search-result__container", { "scroll-fade": scrollFade }, className)}>
<div
className={cls("search-result__container", { "scroll-fade": scrollFade }, className)}
data-search-empty={(result?.count ?? 0) === 0 ? true : undefined}
>
{children}
</div>
);
Expand Down Expand Up @@ -341,21 +348,31 @@ export type SearchResultListProps = ComponentProps<"li">;

export const SearchResultList = (props: SearchResultListProps): ReactElement | null => {
//
const { result } = useSearchContext();
const { result, contentList } = useSearchContext();

return (
// oxlint-disable-next-line react/jsx-no-useless-fragment
<>
{result?.data?.map((data) => {
//
const searchData = contentList.get(Number(data.id));

if (!searchData?.url) {
// oxlint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}

return (
// oxlint-disable-next-line typescript/no-unsafe-assignment
<li key={data.id} {...props}>
<li key={Number(data.id)} {...props}>
<DialogClose isWrapper hideFocus>
{/* oxlint-disable typescript/no-unsafe-assignment */}
<Link href={data.url} title={data.title}>
<span className="link__label">{data.metaTitle ?? data.label}</span>
<span className="link__url">{data.url}</span>
<span className="link__confidence">Confidence: {data.score.toFixed(2)}%</span>
<Link href={searchData.url} title={searchData.title}>
<span className="link__label">
{searchData.frontMatter.title ?? searchData.label}
</span>
<span className="link__url">{searchData.url}</span>
<span className="link__confidence">
Confidence: {((data.score / result.maxScore) * 100).toFixed(2)}%
</span>
</Link>
</DialogClose>
</li>
Expand Down
23 changes: 18 additions & 5 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,27 @@ export const constants: Constants = {
"title",
"label",
"url",
"frontMatter.title",
"frontMatter.description",
"frontMatter.keywords",
"content",
"metaTitle",
"metaDescription",
"metaKeywords",
],

/**
* search index result fields
* search result query options
*/
SEARCH_INDEX_RETURN_FIELDS: ["title", "label", "url", "metaTitle", "metaDescription"],
SEARCH_INDEX_QUERY_OPTIONS: {
prefix: true,
fuzzy: 0.2,
combineWith: "and",
boost: {
"frontMatter.title": 4,
"frontMatter.description": 4,
"frontMatter.keywords": 4,
title: 3,
url: 3,
label: 2,
content: 2,
},
},
};
66 changes: 30 additions & 36 deletions src/hooks/use-search.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { throwError } from "@jadeja/ts/lib/logger";
import { sleep } from "@jadeja/ts/lib/utils";
import MiniSearch from "minisearch";
import { useRef, useEffect, useState } from "react";
Expand All @@ -17,11 +18,12 @@ export interface SearchQueryResult {
count: number;
time: number;
data: SearchResult[];
maxScore: number;
}

export type UseSearchOptions = Pick<
DocsConfig["constants"],
"DEV" | "SEARCH_INDEX_FILE_NAME" | "SEARCH_INDEX_FIELDS" | "SEARCH_INDEX_RETURN_FIELDS"
"DEV" | "SEARCH_INDEX_FILE_NAME" | "SEARCH_INDEX_FIELDS" | "SEARCH_INDEX_QUERY_OPTIONS"
>;

export type SearchError = {
Expand All @@ -36,13 +38,13 @@ export type SearchError = {
* @param options.DEV - is current environment is "development"
* @param options.SEARCH_INDEX_FIELDS - fields for search indexing
* @param options.SEARCH_INDEX_FILE_NAME - file path of indexed search data
* @param options.SEARCH_INDEX_RETURN_FIELDS - result fields from search index data
* @param options.SEARCH_INDEX_QUERY_OPTIONS - search index query options
*/
export const useSearch = ({
DEV,
SEARCH_INDEX_FIELDS,
SEARCH_INDEX_FILE_NAME,
SEARCH_INDEX_RETURN_FIELDS,
SEARCH_INDEX_QUERY_OPTIONS,
}: UseSearchOptions) => {
//
const instance = useRef<MiniSearch>(null);
Expand All @@ -55,14 +57,14 @@ export const useSearch = ({
// for production, it will be force cached
// cache will be auto refreshed when new version is deployed
// api path: `/${SEARCH_INDEX_KEY}-v-${DEV ? "dev" : packageJSON.version}.json`
const initialize = async () => {
const initialize = async (): Promise<void> => {
//
setSearchError(null);

// abort the previous request if any
abortController.current?.abort();

if (instance.current || ready) {
if (instance.current || ready || searchError) {
return;
}

Expand All @@ -75,31 +77,31 @@ export const useSearch = ({
cache: DEV ? "no-store" : "force-cache",
});

if (!res.ok) {
return throwError("Failed to initialize the search!");
}

// oxlint-disable-next-line typescript/no-unsafe-assignment
const data = await res.json();

// oxlint-disable-next-line typescript/no-unsafe-member-access
instance.current = MiniSearch.loadJS(data.index as AsPlainObject, {
fields: SEARCH_INDEX_FIELDS,
storeFields: SEARCH_INDEX_RETURN_FIELDS,
});
instance.current = MiniSearch.loadJS(data as AsPlainObject, { fields: SEARCH_INDEX_FIELDS });

// adding artificial delay
await sleep(1000);

setReady(true);

// oxlint-disable-next-line typescript/no-explicit-any
} catch (error: any) {
// oxlint-disable-next-line typescript/no-unsafe-member-access
if (error.name === "AbortError") {
return;
}
//
setSearchError({
message: "Failed to initialize the search!",
reason: error,
});
} catch (error) {
if (error instanceof Error) {
if (error.name === "AbortError") {
return;
}
//
setSearchError({
message: "Failed to initialize the search!",
reason: error,
});
}
}
};

Expand All @@ -110,26 +112,13 @@ export const useSearch = ({
//
const start = performance.now();

let data: SearchQueryResult["data"] = [];
let data: Omit<SearchQueryResult, "maxScore">["data"] = [];

try {
// adding artificial delay
await sleep(QUERY_ARTIFICIAL_DELAY);

data =
instance.current?.search(searchQuery, {
prefix: true,
fuzzy: 0.2,
boost: {
metaTitle: 4,
metaDescription: 4,
metaKeywords: 4,
title: 3,
url: 3,
label: 2,
content: 2,
},
}) ?? [];
data = instance.current?.search(searchQuery, SEARCH_INDEX_QUERY_OPTIONS) ?? [];
//
} catch (error) {
//
Expand All @@ -145,6 +134,11 @@ export const useSearch = ({
// remove artificial delay
time: performance.now() - start - QUERY_ARTIFICIAL_DELAY,
data,
maxScore: Math.max(
...data.map((x) => {
return x.score;
}),
),
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
* exposed properties:
*
* - `instance.paths`
* - `instance.list`
*
* exposed methods:
*
Expand All @@ -54,7 +55,7 @@ export class Content<U extends Record<string, unknown> = {}> {
//
public paths!: Paths;
private slugs!: Slugs;
private list!: List;
public list!: List;
private tree!: Tree;
private search!: Search & Singleton;
private options!: ContentBaseOptions & U;
Expand Down Expand Up @@ -433,8 +434,7 @@ export class Content<U extends Record<string, unknown> = {}> {
========================= */

private buildSearchIndex() {
const documents = [...this.list.values()];
this.search.ingestAll(documents);
this.search.ingestAll([...this.list.values()]);
}

/* =========================
Expand Down
12 changes: 5 additions & 7 deletions src/lib/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { cwd } from "node:process";
import { Singleton } from "@jadeja/ts/lib/singleton";
import MiniSearch from "minisearch";

import { getNestedValue } from "@/lib/utils";

import type { DocsConfig } from "@/types/config";

/* ============================================================================================= */

export type CreateSearchInstanceOptions = Pick<
DocsConfig["constants"],
"SEARCH_INDEX_FIELDS" | "SEARCH_INDEX_RETURN_FIELDS" | "SEARCH_INDEX_FILE_NAME"
"SEARCH_INDEX_FIELDS" | "SEARCH_INDEX_FILE_NAME" | "SEARCH_INDEX_QUERY_OPTIONS"
>;

/**
Expand Down Expand Up @@ -57,7 +59,7 @@ export class Search {
private createSearchInstance() {
return new MiniSearch({
fields: this.searchOptions.SEARCH_INDEX_FIELDS,
storeFields: this.searchOptions.SEARCH_INDEX_RETURN_FIELDS,
extractField: getNestedValue,
});
}

Expand All @@ -66,11 +68,7 @@ export class Search {
this.miniSearchInstance.addAll(documents);

const searchIndexPath = join(cwd(), "public", this.searchOptions.SEARCH_INDEX_FILE_NAME);
const serachContent = {
index: this.miniSearchInstance.toJSON(),
documents,
};

writeFileSync(searchIndexPath, JSON.stringify(serachContent));
writeFileSync(searchIndexPath, JSON.stringify(this.miniSearchInstance));
}
}
Loading