diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c490a..a9bf4a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index faa0bb2..af55a14 100644 --- a/package.json +++ b/package.json @@ -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/*": { diff --git a/src/components/layout/search.tsx b/src/components/layout/search.tsx index 240815a..19adff1 100644 --- a/src/components/layout/search.tsx +++ b/src/components/layout/search.tsx @@ -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"; /* ============================================================================================= */ @@ -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">; @@ -61,7 +64,8 @@ export const SearchRoot = ({ DEV, SEARCH_INDEX_FIELDS, SEARCH_INDEX_FILE_NAME, - SEARCH_INDEX_RETURN_FIELDS, + SEARCH_INDEX_QUERY_OPTIONS, + contentList, ...rest }: SearchRootProps): ReactElement => { // @@ -69,7 +73,7 @@ export const SearchRoot = ({ DEV, SEARCH_INDEX_FIELDS, SEARCH_INDEX_FILE_NAME, - SEARCH_INDEX_RETURN_FIELDS, + SEARCH_INDEX_QUERY_OPTIONS, }); const [search, setSearch] = useState(""); @@ -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 - +
{children}
@@ -111,7 +114,7 @@ export const SearchRoot = ({ /* ============================================================================================= */ -export type SearchProps = UseSearchOptions; +export type SearchProps = UseSearchOptions & { contentList: ContentList }; export const Search = (props: SearchProps): ReturnType => { return ( @@ -279,8 +282,12 @@ export const SearchResultContainer = ({ children, className, }: SearchResultContainerProps): ReactElement => { + const { result } = useSearchContext(); return ( -
+
{children}
); @@ -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 -
  • +
  • - {/* oxlint-disable typescript/no-unsafe-assignment */} - - {data.metaTitle ?? data.label} - {data.url} - Confidence: {data.score.toFixed(2)}% + + + {searchData.frontMatter.title ?? searchData.label} + + {searchData.url} + + Confidence: {((data.score / result.maxScore) * 100).toFixed(2)}% +
  • diff --git a/src/config/constants.ts b/src/config/constants.ts index 48b3cda..8037470 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -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, + }, + }, }; diff --git a/src/hooks/use-search.ts b/src/hooks/use-search.ts index 1d65071..0dcac5e 100644 --- a/src/hooks/use-search.ts +++ b/src/hooks/use-search.ts @@ -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"; @@ -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 = { @@ -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(null); @@ -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 => { // setSearchError(null); // abort the previous request if any abortController.current?.abort(); - if (instance.current || ready) { + if (instance.current || ready || searchError) { return; } @@ -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, + }); + } } }; @@ -110,26 +112,13 @@ export const useSearch = ({ // const start = performance.now(); - let data: SearchQueryResult["data"] = []; + let data: Omit["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) { // @@ -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; + }), + ), }; } diff --git a/src/lib/content.ts b/src/lib/content.ts index 444e08a..db6ccdf 100644 --- a/src/lib/content.ts +++ b/src/lib/content.ts @@ -41,6 +41,7 @@ import type { * exposed properties: * * - `instance.paths` + * - `instance.list` * * exposed methods: * @@ -54,7 +55,7 @@ export class Content = {}> { // public paths!: Paths; private slugs!: Slugs; - private list!: List; + public list!: List; private tree!: Tree; private search!: Search & Singleton; private options!: ContentBaseOptions & U; @@ -433,8 +434,7 @@ export class Content = {}> { ========================= */ private buildSearchIndex() { - const documents = [...this.list.values()]; - this.search.ingestAll(documents); + this.search.ingestAll([...this.list.values()]); } /* ========================= diff --git a/src/lib/search.ts b/src/lib/search.ts index 9e22673..348c744 100644 --- a/src/lib/search.ts +++ b/src/lib/search.ts @@ -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" >; /** @@ -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, }); } @@ -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)); } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..341b05e --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,30 @@ +import { isObj } from "@jadeja/ts/lib/types"; + +import type { NestedObject, Primitive } from "@jadeja/ts/types/data"; + +/** + * get nested value of field from the object with dotted key + * + * @param document - nested object + * @param field - dotted key for nested field + */ +export const getNestedValue = ( + document: NestedObject, + field: string, +): Primitive | NestedObject => { + // + if (!field.includes(".")) { + return document[field]; + } + + // oxlint-disable-next-line unicorn/no-array-reduce + return field.split(".").reduce((doc: Primitive | NestedObject, key: string) => { + // + if (isObj(doc) && key in doc) { + return doc[key]; + } else if (!isObj(doc)) { + return doc; + } + return null; + }, document); +}; diff --git a/src/types/config.ts b/src/types/config.ts index 166334b..077b97b 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,5 +1,6 @@ import type { NextMDXOptions } from "@next/mdx"; import type { MDXComponents } from "mdx/types"; +import type { SearchOptions } from "minisearch"; import type { NextConfig } from "next"; import type { ShilpConfig } from "shilpcss/types"; import type { Configuration } from "webpack"; @@ -126,9 +127,9 @@ export interface Constants { SEARCH_INDEX_FIELDS: string[]; /** - * search index result fields + * search index query options */ - SEARCH_INDEX_RETURN_FIELDS: string[]; + SEARCH_INDEX_QUERY_OPTIONS: SearchOptions; } /* ================================================================================================