Skip to content

Commit d76ba79

Browse files
committed
Website Search - fuse.js performance
Improve Fuse.js performance by creating a subindex once the term length gets long enough. Fuse.js using the Bitap algorithm for text matching which runs in O(nm) time (no matter the structure of the text). In our case this means that long search terms mixed with large index gets very slow This injects a subIndex that will be used once the terms get long enough Usually making this subindex is cheap since there will typically be a subset of results matching the existing query Fixes #8567
1 parent 57b9d59 commit d76ba79

File tree

1 file changed

+52
-13
lines changed

1 file changed

+52
-13
lines changed

src/resources/projects/website/search/quarto-search.js

+52-13
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,18 @@ function showCopyLink(query, options) {
675675
// create the index
676676
var fuseIndex = undefined;
677677
var shownWarning = false;
678+
679+
// fuse index options
680+
const kFuseIndexOptions = {
681+
keys: [
682+
{ name: "title", weight: 20 },
683+
{ name: "section", weight: 20 },
684+
{ name: "text", weight: 10 },
685+
],
686+
ignoreLocation: true,
687+
threshold: 0.1,
688+
};
689+
678690
async function readSearchData() {
679691
// Initialize the search index on demand
680692
if (fuseIndex === undefined) {
@@ -685,17 +697,7 @@ async function readSearchData() {
685697
shownWarning = true;
686698
return;
687699
}
688-
// create fuse index
689-
const options = {
690-
keys: [
691-
{ name: "title", weight: 20 },
692-
{ name: "section", weight: 20 },
693-
{ name: "text", weight: 10 },
694-
],
695-
ignoreLocation: true,
696-
threshold: 0.1,
697-
};
698-
const fuse = new window.Fuse([], options);
700+
const fuse = new window.Fuse([], kFuseIndexOptions);
699701

700702
// fetch the main search.json
701703
const response = await fetch(offsetURL("search.json"));
@@ -1226,8 +1228,34 @@ function algoliaSearch(query, limit, algoliaOptions) {
12261228
});
12271229
}
12281230

1229-
function fuseSearch(query, fuse, fuseOptions) {
1230-
return fuse.search(query, fuseOptions).map((result) => {
1231+
let subSearchTerm = undefined;
1232+
let subSearchFuse = undefined;
1233+
const kFuseMaxWait = 125;
1234+
1235+
async function fuseSearch(query, fuse, fuseOptions) {
1236+
let index = fuse;
1237+
// Fuse.js using the Bitap algorithm for text matching which runs in
1238+
// O(nm) time (no matter the structure of the text). In our case this
1239+
// means that long search terms mixed with large index gets very slow
1240+
//
1241+
// This injects a subIndex that will be used once the terms get long enough
1242+
// Usually making this subindex is cheap since there will typically be
1243+
// a subset of results matching the existing query
1244+
if (subSearchFuse !== undefined && query.startsWith(subSearchTerm)) {
1245+
// Use the existing subSearchFuse
1246+
index = subSearchFuse;
1247+
} else if (subSearchFuse !== undefined) {
1248+
// The term changed, discard the existing fuse
1249+
subSearchFuse = undefined;
1250+
subSearchTerm = undefined;
1251+
}
1252+
1253+
// Search using the active fuse
1254+
const then = performance.now();
1255+
const resultsRaw = await index.search(query, fuseOptions);
1256+
const now = performance.now();
1257+
1258+
const results = resultsRaw.map((result) => {
12311259
const addParam = (url, name, value) => {
12321260
const anchorParts = url.split("#");
12331261
const baseUrl = anchorParts[0];
@@ -1244,4 +1272,15 @@ function fuseSearch(query, fuse, fuseOptions) {
12441272
crumbs: result.item.crumbs,
12451273
};
12461274
});
1275+
1276+
// If we don't have a subfuse and the query is long enough, go ahead
1277+
// and create a subfuse to use for subsequent queries
1278+
if (now - then > kFuseMaxWait && subSearchFuse === undefined) {
1279+
subSearchTerm = query;
1280+
subSearchFuse = new window.Fuse([], kFuseIndexOptions);
1281+
resultsRaw.forEach((rr) => {
1282+
subSearchFuse.add(rr.item);
1283+
});
1284+
}
1285+
return results;
12471286
}

0 commit comments

Comments
 (0)