@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 +7 -0
- package/dist/client/client/theme/SearchBar/SearchBar.jsx +8 -6
- package/dist/client/client/theme/SearchPage/SearchPage.jsx +17 -19
- package/dist/client/client/theme/searchByWorker.js +24 -0
- package/dist/client/client/theme/worker.js +95 -0
- package/package.json +2 -1
- package/dist/client/client/theme/SearchBar/fetchIndexes.js +0 -45
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 {
|
|
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 {
|
|
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 [
|
|
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:
|
|
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 {
|
|
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 (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
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
|
-
|
|
70
|
+
if (!Array.isArray(searchContextByPaths) ||
|
|
73
71
|
searchContext ||
|
|
74
|
-
useAllContextsWithNoSearchContext
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
{!
|
|
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.
|
|
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
|
-
}
|