@easyops-cn/docusaurus-search-local 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [0.46.0](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.45.0...v0.46.0) (2024-11-27)
6
+
7
+
8
+ ### Features
9
+
10
+ * move search to Web Worker ([15b7fba](https://github.com/easyops-cn/docusaurus-search-local/commit/15b7fba2537d12eb1674611948e363723dcedae4))
11
+
5
12
  ## [0.45.0](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.44.6...v0.45.0) (2024-10-09)
6
13
 
7
14
 
@@ -6,11 +6,10 @@ import { useHistory, useLocation } from "@docusaurus/router";
6
6
  import { translate } from "@docusaurus/Translate";
7
7
  import { ReactContextError, useDocsPreferredVersion, } from "@docusaurus/theme-common";
8
8
  import { useActivePlugin } from "@docusaurus/plugin-content-docs/client";
9
- import { fetchIndexes } from "./fetchIndexes";
10
- import { SearchSourceFactory } from "../../utils/SearchSourceFactory";
9
+ import { fetchIndexesByWorker, searchByWorker } from "../searchByWorker";
11
10
  import { SuggestionTemplate } from "./SuggestionTemplate";
12
11
  import { EmptyTemplate } from "./EmptyTemplate";
13
- import { searchResultLimits, Mark, searchBarShortcut, searchBarShortcutHint, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, } from "../../utils/proxiedGenerated";
12
+ import { Mark, searchBarShortcut, searchBarShortcutHint, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, } from "../../utils/proxiedGenerated";
14
13
  import LoadingRing from "../LoadingRing/LoadingRing";
15
14
  import styles from "./SearchBar.module.css";
16
15
  import { normalizeContextByPath } from "../../utils/normalizeContextByPath";
@@ -107,9 +106,9 @@ export default function SearchBar({ handleSearchBarToggle, }) {
107
106
  indexStateMap.current.set(searchContext, "loading");
108
107
  search.current?.autocomplete.destroy();
109
108
  setLoading(true);
110
- const [{ wrappedIndexes, zhDictionary }, autoComplete] = await Promise.all([
111
- fetchIndexes(versionUrl, searchContext),
109
+ const [autoComplete] = await Promise.all([
112
110
  fetchAutoCompleteJS(),
111
+ fetchIndexesByWorker(versionUrl, searchContext),
113
112
  ]);
114
113
  const searchFooterLinkElement = ({ query, isEmpty, }) => {
115
114
  const a = document.createElement("a");
@@ -187,7 +186,10 @@ export default function SearchBar({ handleSearchBarToggle, }) {
187
186
  },
188
187
  }, [
189
188
  {
190
- source: SearchSourceFactory(wrappedIndexes, zhDictionary, searchResultLimits),
189
+ source: async (input, callback) => {
190
+ const result = await searchByWorker(versionUrl, searchContext, input);
191
+ callback(result);
192
+ },
191
193
  templates: {
192
194
  suggestion: SuggestionTemplate,
193
195
  empty: EmptyTemplate,
@@ -7,8 +7,7 @@ import { translate } from "@docusaurus/Translate";
7
7
  import { usePluralForm } from "@docusaurus/theme-common";
8
8
  import clsx from "clsx";
9
9
  import useSearchQuery from "../hooks/useSearchQuery";
10
- import { fetchIndexes } from "../SearchBar/fetchIndexes";
11
- import { SearchSourceFactory } from "../../utils/SearchSourceFactory";
10
+ import { fetchIndexesByWorker, searchByWorker } from "../searchByWorker";
12
11
  import { SearchDocumentType, } from "../../../shared/interfaces";
13
12
  import { highlight } from "../../utils/highlight";
14
13
  import { highlightStemmed } from "../../utils/highlightStemmed";
@@ -28,7 +27,6 @@ function SearchPageContent() {
28
27
  const { selectMessage } = usePluralForm();
29
28
  const { searchValue, searchContext, searchVersion, updateSearchPath, updateSearchContext, } = useSearchQuery();
30
29
  const [searchQuery, setSearchQuery] = useState(searchValue);
31
- const [searchSource, setSearchSource] = useState();
32
30
  const [searchResults, setSearchResults] = useState();
33
31
  const versionUrl = `${baseUrl}${searchVersion}`;
34
32
  const pageTitle = useMemo(() => searchQuery
@@ -46,19 +44,18 @@ function SearchPageContent() {
46
44
  }), [searchQuery]);
47
45
  useEffect(() => {
48
46
  updateSearchPath(searchQuery);
49
- if (searchSource) {
50
- if (searchQuery) {
51
- searchSource(searchQuery, (results) => {
52
- setSearchResults(results);
53
- });
54
- }
55
- else {
56
- setSearchResults(undefined);
57
- }
47
+ if (searchQuery) {
48
+ (async () => {
49
+ const results = await searchByWorker(versionUrl, searchContext, searchQuery);
50
+ setSearchResults(results);
51
+ })();
52
+ }
53
+ else {
54
+ setSearchResults(undefined);
58
55
  }
59
56
  // `updateSearchPath` should not be in the deps,
60
57
  // otherwise will cause call stack overflow.
61
- }, [searchQuery, searchSource]);
58
+ }, [searchQuery, versionUrl, searchContext]);
62
59
  const handleSearchInputChange = useCallback((e) => {
63
60
  setSearchQuery(e.target.value);
64
61
  }, []);
@@ -67,14 +64,15 @@ function SearchPageContent() {
67
64
  setSearchQuery(searchValue);
68
65
  }
69
66
  }, [searchValue]);
67
+ const [searchWorkerReady, setSearchWorkerReady] = useState(false);
70
68
  useEffect(() => {
71
69
  async function doFetchIndexes() {
72
- const { wrappedIndexes, zhDictionary } = !Array.isArray(searchContextByPaths) ||
70
+ if (!Array.isArray(searchContextByPaths) ||
73
71
  searchContext ||
74
- useAllContextsWithNoSearchContext
75
- ? await fetchIndexes(versionUrl, searchContext)
76
- : { wrappedIndexes: [], zhDictionary: [] };
77
- setSearchSource(() => SearchSourceFactory(wrappedIndexes, zhDictionary, 100));
72
+ useAllContextsWithNoSearchContext) {
73
+ await fetchIndexesByWorker(versionUrl, searchContext);
74
+ }
75
+ setSearchWorkerReady(true);
78
76
  }
79
77
  doFetchIndexes();
80
78
  }, [searchContext, versionUrl]);
@@ -117,7 +115,7 @@ function SearchPageContent() {
117
115
  </div>) : null}
118
116
  </div>
119
117
 
120
- {!searchSource && searchQuery && (<div>
118
+ {!searchWorkerReady && searchQuery && (<div>
121
119
  <LoadingRing />
122
120
  </div>)}
123
121
 
@@ -0,0 +1,24 @@
1
+ import * as Comlink from "comlink";
2
+ let remoteWorkerPromise;
3
+ function getRemoteWorker() {
4
+ if (!remoteWorkerPromise) {
5
+ remoteWorkerPromise = (async () => {
6
+ const Remote = Comlink.wrap(new Worker(new URL("./worker.js", import.meta.url)));
7
+ return await new Remote();
8
+ })();
9
+ }
10
+ return remoteWorkerPromise;
11
+ }
12
+ export async function fetchIndexesByWorker(baseUrl, searchContext) {
13
+ if (process.env.NODE_ENV === "production") {
14
+ const remoteWorker = await getRemoteWorker();
15
+ await remoteWorker.fetchIndexes(baseUrl, searchContext);
16
+ }
17
+ }
18
+ export async function searchByWorker(baseUrl, searchContext, input) {
19
+ if (process.env.NODE_ENV === "production") {
20
+ const remoteWorker = await getRemoteWorker();
21
+ return remoteWorker.search(baseUrl, searchContext, input);
22
+ }
23
+ return [];
24
+ }
@@ -0,0 +1,95 @@
1
+ import * as Comlink from "comlink";
2
+ import lunr from "lunr";
3
+ import { searchIndexUrl, searchResultLimits } from "../utils/proxiedGenerated";
4
+ import { tokenize } from "../utils/tokenize";
5
+ import { smartQueries } from "../utils/smartQueries";
6
+ import { SearchDocumentType, } from "../../shared/interfaces";
7
+ import { sortSearchResults } from "../utils/sortSearchResults";
8
+ import { processTreeStatusOfSearchResults } from "../utils/processTreeStatusOfSearchResults";
9
+ import { language } from "../utils/proxiedGenerated";
10
+ const cache = new Map();
11
+ export class SearchWorker {
12
+ async fetchIndexes(baseUrl, searchContext) {
13
+ await this.lowLevelFetchIndexes(baseUrl, searchContext);
14
+ }
15
+ async lowLevelFetchIndexes(baseUrl, searchContext) {
16
+ const cacheKey = `${baseUrl}${searchContext}`;
17
+ let promise = cache.get(cacheKey);
18
+ if (!promise) {
19
+ promise = legacyFetchIndexes(baseUrl, searchContext);
20
+ cache.set(cacheKey, promise);
21
+ }
22
+ return promise;
23
+ }
24
+ async search(baseUrl, searchContext, input) {
25
+ const rawTokens = tokenize(input, language);
26
+ if (rawTokens.length === 0) {
27
+ return [];
28
+ }
29
+ const { wrappedIndexes, zhDictionary } = await this.lowLevelFetchIndexes(baseUrl, searchContext);
30
+ const queries = smartQueries(rawTokens, zhDictionary);
31
+ const results = [];
32
+ search: for (const { term, tokens } of queries) {
33
+ for (const { documents, index, type } of wrappedIndexes) {
34
+ results.push(...index
35
+ .query((query) => {
36
+ for (const item of term) {
37
+ query.term(item.value, {
38
+ wildcard: item.wildcard,
39
+ presence: item.presence,
40
+ });
41
+ }
42
+ })
43
+ .slice(0, searchResultLimits)
44
+ // Remove duplicated results.
45
+ .filter((result) => !results.some((item) => item.document.i.toString() === result.ref))
46
+ .slice(0, searchResultLimits - results.length)
47
+ .map((result) => {
48
+ const document = documents.find((doc) => doc.i.toString() === result.ref);
49
+ return {
50
+ document,
51
+ type,
52
+ page: type !== SearchDocumentType.Title &&
53
+ wrappedIndexes[0].documents.find((doc) => doc.i === document.p),
54
+ metadata: result.matchData.metadata,
55
+ tokens,
56
+ score: result.score,
57
+ };
58
+ }));
59
+ if (results.length >= searchResultLimits) {
60
+ break search;
61
+ }
62
+ }
63
+ }
64
+ sortSearchResults(results);
65
+ processTreeStatusOfSearchResults(results);
66
+ return results;
67
+ }
68
+ }
69
+ async function legacyFetchIndexes(baseUrl, searchContext) {
70
+ const url = `${baseUrl}${searchIndexUrl.replace("{dir}", searchContext ? `-${searchContext.replace(/\//g, "-")}` : "")}`;
71
+ // Catch potential attacks.
72
+ const fullUrl = new URL(url, location.origin);
73
+ if (fullUrl.origin !== location.origin) {
74
+ throw new Error("Unexpected version url");
75
+ }
76
+ const json = (await (await fetch(url)).json());
77
+ const wrappedIndexes = json.map(({ documents, index }, type) => ({
78
+ type: type,
79
+ documents,
80
+ index: lunr.Index.load(index),
81
+ }));
82
+ const zhDictionary = json.reduce((acc, item) => {
83
+ for (const tuple of item.index.invertedIndex) {
84
+ if (/\p{Unified_Ideograph}/u.test(tuple[0][0])) {
85
+ acc.add(tuple[0]);
86
+ }
87
+ }
88
+ return acc;
89
+ }, new Set());
90
+ return {
91
+ wrappedIndexes,
92
+ zhDictionary: Array.from(zhDictionary),
93
+ };
94
+ }
95
+ Comlink.expose(SearchWorker);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyops-cn/docusaurus-search-local",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "An offline/local search plugin for Docusaurus v3",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,6 +39,7 @@
39
39
  "@node-rs/jieba": "^1.6.0",
40
40
  "cheerio": "^1.0.0",
41
41
  "clsx": "^1.1.1",
42
+ "comlink": "^4.4.2",
42
43
  "debug": "^4.2.0",
43
44
  "fs-extra": "^10.0.0",
44
45
  "klaw-sync": "^6.0.0",
@@ -1,45 +0,0 @@
1
- import lunr from "lunr";
2
- import { searchIndexUrl } from "../../utils/proxiedGenerated";
3
- const cache = new Map();
4
- export function fetchIndexes(baseUrl, searchContext) {
5
- const cacheKey = `${baseUrl}${searchContext}`;
6
- let promise = cache.get(cacheKey);
7
- if (!promise) {
8
- promise = legacyFetchIndexes(baseUrl, searchContext);
9
- cache.set(cacheKey, promise);
10
- }
11
- return promise;
12
- }
13
- export async function legacyFetchIndexes(baseUrl, searchContext) {
14
- if (process.env.NODE_ENV === "production") {
15
- const url = `${baseUrl}${searchIndexUrl.replace("{dir}", searchContext ? `-${searchContext.replace(/\//g, "-")}` : "")}`;
16
- // Catch potential attacks.
17
- const fullUrl = new URL(url, location.origin);
18
- if (fullUrl.origin !== location.origin) {
19
- throw new Error("Unexpected version url");
20
- }
21
- const json = (await (await fetch(url)).json());
22
- const wrappedIndexes = json.map(({ documents, index }, type) => ({
23
- type: type,
24
- documents,
25
- index: lunr.Index.load(index),
26
- }));
27
- const zhDictionary = json.reduce((acc, item) => {
28
- for (const tuple of item.index.invertedIndex) {
29
- if (/\p{Unified_Ideograph}/u.test(tuple[0][0])) {
30
- acc.add(tuple[0]);
31
- }
32
- }
33
- return acc;
34
- }, new Set());
35
- return {
36
- wrappedIndexes,
37
- zhDictionary: Array.from(zhDictionary),
38
- };
39
- }
40
- // The index does not exist in development, therefore load a dummy index here.
41
- return {
42
- wrappedIndexes: [],
43
- zhDictionary: [],
44
- };
45
- }