Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
singlestore-labs
GitHub Repository: singlestore-labs/singlestoredb-python
Path: blob/main/docs/_static/searchtools.js
801 views
1
/*
2
* Sphinx JavaScript utilities for the full-text search.
3
*/
4
"use strict";
5
/**
6
* Simple result scoring code.
7
*/
8
if (typeof Scorer === "undefined") {
9
var Scorer = {
10
// Implement the following function to further tweak the score for each result
11
// The function takes a result array [docname, title, anchor, descr, score, filename]
12
// and returns the new score.
13
/*
14
score: result => {
15
const [docname, title, anchor, descr, score, filename, kind] = result
16
return score
17
},
18
*/
19
// query matches the full name of an object
20
objNameMatch: 11,
21
// or matches in the last dotted part of the object name
22
objPartialMatch: 6,
23
// Additive scores depending on the priority of the object
24
objPrio: {
25
0: 15, // used to be importantResults
26
1: 5, // used to be objectResults
27
2: -5, // used to be unimportantResults
28
},
29
// Used when the priority is not in the mapping.
30
objPrioDefault: 0,
31
// query found in title
32
title: 15,
33
partialTitle: 7,
34
// query found in terms
35
term: 5,
36
partialTerm: 2,
37
};
38
}
39
// Global search result kind enum, used by themes to style search results.
40
class SearchResultKind {
41
static get index() { return "index"; }
42
static get object() { return "object"; }
43
static get text() { return "text"; }
44
static get title() { return "title"; }
45
}
46
const _removeChildren = (element) => {
47
while (element && element.lastChild) element.removeChild(element.lastChild);
48
};
49
/**
50
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
51
*/
52
const _escapeRegExp = (string) =>
53
string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
54
const _displayItem = (item, searchTerms, highlightTerms) => {
55
const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
56
const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
57
const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
58
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
59
const contentRoot = document.documentElement.dataset.content_root;
60
const [docName, title, anchor, descr, score, _filename, kind] = item;
61
let listItem = document.createElement("li");
62
// Add a class representing the item's type:
63
// can be used by a theme's CSS selector for styling
64
// See SearchResultKind for the class names.
65
listItem.classList.add(`kind-${kind}`);
66
let requestUrl;
67
let linkUrl;
68
if (docBuilder === "dirhtml") {
69
// dirhtml builder
70
let dirname = docName + "/";
71
if (dirname.match(/\/index\/$/))
72
dirname = dirname.substring(0, dirname.length - 6);
73
else if (dirname === "index/") dirname = "";
74
requestUrl = contentRoot + dirname;
75
linkUrl = requestUrl;
76
} else {
77
// normal html builders
78
requestUrl = contentRoot + docName + docFileSuffix;
79
linkUrl = docName + docLinkSuffix;
80
}
81
let linkEl = listItem.appendChild(document.createElement("a"));
82
linkEl.href = linkUrl + anchor;
83
linkEl.dataset.score = score;
84
linkEl.innerHTML = title;
85
if (descr) {
86
listItem.appendChild(document.createElement("span")).innerHTML =
87
" (" + descr + ")";
88
// highlight search terms in the description
89
if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
90
highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
91
}
92
else if (showSearchSummary)
93
fetch(requestUrl)
94
.then((responseData) => responseData.text())
95
.then((data) => {
96
if (data)
97
listItem.appendChild(
98
Search.makeSearchSummary(data, searchTerms, anchor)
99
);
100
// highlight search terms in the summary
101
if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
102
highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
103
});
104
Search.output.appendChild(listItem);
105
};
106
const _finishSearch = (resultCount) => {
107
Search.stopPulse();
108
Search.title.innerText = _("Search Results");
109
if (!resultCount)
110
Search.status.innerText = Documentation.gettext(
111
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
112
);
113
else
114
Search.status.innerText = Documentation.ngettext(
115
"Search finished, found one page matching the search query.",
116
"Search finished, found ${resultCount} pages matching the search query.",
117
resultCount,
118
).replace('${resultCount}', resultCount);
119
};
120
const _displayNextItem = (
121
results,
122
resultCount,
123
searchTerms,
124
highlightTerms,
125
) => {
126
// results left, load the summary and display it
127
// this is intended to be dynamic (don't sub resultsCount)
128
if (results.length) {
129
_displayItem(results.pop(), searchTerms, highlightTerms);
130
setTimeout(
131
() => _displayNextItem(results, resultCount, searchTerms, highlightTerms),
132
5
133
);
134
}
135
// search finished, update title and status message
136
else _finishSearch(resultCount);
137
};
138
// Helper function used by query() to order search results.
139
// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
140
// Order the results by score (in opposite order of appearance, since the
141
// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
142
const _orderResultsByScoreThenName = (a, b) => {
143
const leftScore = a[4];
144
const rightScore = b[4];
145
if (leftScore === rightScore) {
146
// same score: sort alphabetically
147
const leftTitle = a[1].toLowerCase();
148
const rightTitle = b[1].toLowerCase();
149
if (leftTitle === rightTitle) return 0;
150
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
151
}
152
return leftScore > rightScore ? 1 : -1;
153
};
154
/**
155
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
156
* custom function per language.
157
*
158
* The regular expression works by splitting the string on consecutive characters
159
* that are not Unicode letters, numbers, underscores, or emoji characters.
160
* This is the same as ``\W+`` in Python, preserving the surrogate pair area.
161
*/
162
if (typeof splitQuery === "undefined") {
163
var splitQuery = (query) => query
164
.split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
165
.filter(term => term) // remove remaining empty strings
166
}
167
/**
168
* Search Module
169
*/
170
const Search = {
171
_index: null,
172
_queued_query: null,
173
_pulse_status: -1,
174
htmlToText: (htmlString, anchor) => {
175
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
176
for (const removalQuery of [".headerlink", "script", "style"]) {
177
htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });
178
}
179
if (anchor) {
180
const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`);
181
if (anchorContent) return anchorContent.textContent;
182
console.warn(
183
`Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`
184
);
185
}
186
// if anchor not specified or not found, fall back to main content
187
const docContent = htmlElement.querySelector('[role="main"]');
188
if (docContent) return docContent.textContent;
189
console.warn(
190
"Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template."
191
);
192
return "";
193
},
194
init: () => {
195
const query = new URLSearchParams(window.location.search).get("q");
196
document
197
.querySelectorAll('input[name="q"]')
198
.forEach((el) => (el.value = query));
199
if (query) Search.performSearch(query);
200
},
201
loadIndex: (url) =>
202
(document.body.appendChild(document.createElement("script")).src = url),
203
setIndex: (index) => {
204
Search._index = index;
205
if (Search._queued_query !== null) {
206
const query = Search._queued_query;
207
Search._queued_query = null;
208
Search.query(query);
209
}
210
},
211
hasIndex: () => Search._index !== null,
212
deferQuery: (query) => (Search._queued_query = query),
213
stopPulse: () => (Search._pulse_status = -1),
214
startPulse: () => {
215
if (Search._pulse_status >= 0) return;
216
const pulse = () => {
217
Search._pulse_status = (Search._pulse_status + 1) % 4;
218
Search.dots.innerText = ".".repeat(Search._pulse_status);
219
if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);
220
};
221
pulse();
222
},
223
/**
224
* perform a search for something (or wait until index is loaded)
225
*/
226
performSearch: (query) => {
227
// create the required interface elements
228
const searchText = document.createElement("h2");
229
searchText.textContent = _("Searching");
230
const searchSummary = document.createElement("p");
231
searchSummary.classList.add("search-summary");
232
searchSummary.innerText = "";
233
const searchList = document.createElement("ul");
234
searchList.setAttribute("role", "list");
235
searchList.classList.add("search");
236
const out = document.getElementById("search-results");
237
Search.title = out.appendChild(searchText);
238
Search.dots = Search.title.appendChild(document.createElement("span"));
239
Search.status = out.appendChild(searchSummary);
240
Search.output = out.appendChild(searchList);
241
const searchProgress = document.getElementById("search-progress");
242
// Some themes don't use the search progress node
243
if (searchProgress) {
244
searchProgress.innerText = _("Preparing search...");
245
}
246
Search.startPulse();
247
// index already loaded, the browser was quick!
248
if (Search.hasIndex()) Search.query(query);
249
else Search.deferQuery(query);
250
},
251
_parseQuery: (query) => {
252
// stem the search terms and add them to the correct list
253
const stemmer = new Stemmer();
254
const searchTerms = new Set();
255
const excludedTerms = new Set();
256
const highlightTerms = new Set();
257
const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));
258
splitQuery(query.trim()).forEach((queryTerm) => {
259
const queryTermLower = queryTerm.toLowerCase();
260
// maybe skip this "word"
261
// stopwords array is from language_data.js
262
if (
263
stopwords.indexOf(queryTermLower) !== -1 ||
264
queryTerm.match(/^\d+$/)
265
)
266
return;
267
// stem the word
268
let word = stemmer.stemWord(queryTermLower);
269
// select the correct list
270
if (word[0] === "-") excludedTerms.add(word.substr(1));
271
else {
272
searchTerms.add(word);
273
highlightTerms.add(queryTermLower);
274
}
275
});
276
if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js
277
localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" "))
278
}
279
// console.debug("SEARCH: searching for:");
280
// console.info("required: ", [...searchTerms]);
281
// console.info("excluded: ", [...excludedTerms]);
282
return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];
283
},
284
/**
285
* execute search (requires search index to be loaded)
286
*/
287
_performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => {
288
const filenames = Search._index.filenames;
289
const docNames = Search._index.docnames;
290
const titles = Search._index.titles;
291
const allTitles = Search._index.alltitles;
292
const indexEntries = Search._index.indexentries;
293
// Collect multiple result groups to be sorted separately and then ordered.
294
// Each is an array of [docname, title, anchor, descr, score, filename, kind].
295
const normalResults = [];
296
const nonMainIndexResults = [];
297
_removeChildren(document.getElementById("search-progress"));
298
const queryLower = query.toLowerCase().trim();
299
for (const [title, foundTitles] of Object.entries(allTitles)) {
300
if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
301
for (const [file, id] of foundTitles) {
302
const score = Math.round(Scorer.title * queryLower.length / title.length);
303
const boost = titles[file] === title ? 1 : 0; // add a boost for document titles
304
normalResults.push([
305
docNames[file],
306
titles[file] !== title ? `${titles[file]} > ${title}` : title,
307
id !== null ? "#" + id : "",
308
null,
309
score + boost,
310
filenames[file],
311
SearchResultKind.title,
312
]);
313
}
314
}
315
}
316
// search for explicit entries in index directives
317
for (const [entry, foundEntries] of Object.entries(indexEntries)) {
318
if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
319
for (const [file, id, isMain] of foundEntries) {
320
const score = Math.round(100 * queryLower.length / entry.length);
321
const result = [
322
docNames[file],
323
titles[file],
324
id ? "#" + id : "",
325
null,
326
score,
327
filenames[file],
328
SearchResultKind.index,
329
];
330
if (isMain) {
331
normalResults.push(result);
332
} else {
333
nonMainIndexResults.push(result);
334
}
335
}
336
}
337
}
338
// lookup as object
339
objectTerms.forEach((term) =>
340
normalResults.push(...Search.performObjectSearch(term, objectTerms))
341
);
342
// lookup as search terms in fulltext
343
normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms));
344
// let the scorer override scores with a custom scoring function
345
if (Scorer.score) {
346
normalResults.forEach((item) => (item[4] = Scorer.score(item)));
347
nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
348
}
349
// Sort each group of results by score and then alphabetically by name.
350
normalResults.sort(_orderResultsByScoreThenName);
351
nonMainIndexResults.sort(_orderResultsByScoreThenName);
352
// Combine the result groups in (reverse) order.
353
// Non-main index entries are typically arbitrary cross-references,
354
// so display them after other results.
355
let results = [...nonMainIndexResults, ...normalResults];
356
// remove duplicate search results
357
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
358
let seen = new Set();
359
results = results.reverse().reduce((acc, result) => {
360
let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(',');
361
if (!seen.has(resultStr)) {
362
acc.push(result);
363
seen.add(resultStr);
364
}
365
return acc;
366
}, []);
367
return results.reverse();
368
},
369
query: (query) => {
370
const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query);
371
const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms);
372
// for debugging
373
//Search.lastresults = results.slice(); // a copy
374
// console.info("search results:", Search.lastresults);
375
// print the results
376
_displayNextItem(results, results.length, searchTerms, highlightTerms);
377
},
378
/**
379
* search for object names
380
*/
381
performObjectSearch: (object, objectTerms) => {
382
const filenames = Search._index.filenames;
383
const docNames = Search._index.docnames;
384
const objects = Search._index.objects;
385
const objNames = Search._index.objnames;
386
const titles = Search._index.titles;
387
const results = [];
388
const objectSearchCallback = (prefix, match) => {
389
const name = match[4]
390
const fullname = (prefix ? prefix + "." : "") + name;
391
const fullnameLower = fullname.toLowerCase();
392
if (fullnameLower.indexOf(object) < 0) return;
393
let score = 0;
394
const parts = fullnameLower.split(".");
395
// check for different match types: exact matches of full name or
396
// "last name" (i.e. last dotted part)
397
if (fullnameLower === object || parts.slice(-1)[0] === object)
398
score += Scorer.objNameMatch;
399
else if (parts.slice(-1)[0].indexOf(object) > -1)
400
score += Scorer.objPartialMatch; // matches in last name
401
const objName = objNames[match[1]][2];
402
const title = titles[match[0]];
403
// If more than one term searched for, we require other words to be
404
// found in the name/title/description
405
const otherTerms = new Set(objectTerms);
406
otherTerms.delete(object);
407
if (otherTerms.size > 0) {
408
const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();
409
if (
410
[...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)
411
)
412
return;
413
}
414
let anchor = match[3];
415
if (anchor === "") anchor = fullname;
416
else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;
417
const descr = objName + _(", in ") + title;
418
// add custom score for some objects according to scorer
419
if (Scorer.objPrio.hasOwnProperty(match[2]))
420
score += Scorer.objPrio[match[2]];
421
else score += Scorer.objPrioDefault;
422
results.push([
423
docNames[match[0]],
424
fullname,
425
"#" + anchor,
426
descr,
427
score,
428
filenames[match[0]],
429
SearchResultKind.object,
430
]);
431
};
432
Object.keys(objects).forEach((prefix) =>
433
objects[prefix].forEach((array) =>
434
objectSearchCallback(prefix, array)
435
)
436
);
437
return results;
438
},
439
/**
440
* search for full-text terms in the index
441
*/
442
performTermsSearch: (searchTerms, excludedTerms) => {
443
// prepare search
444
const terms = Search._index.terms;
445
const titleTerms = Search._index.titleterms;
446
const filenames = Search._index.filenames;
447
const docNames = Search._index.docnames;
448
const titles = Search._index.titles;
449
const scoreMap = new Map();
450
const fileMap = new Map();
451
// perform the search on the required terms
452
searchTerms.forEach((word) => {
453
const files = [];
454
// find documents, if any, containing the query word in their text/title term indices
455
// use Object.hasOwnProperty to avoid mismatching against prototype properties
456
const arr = [
457
{ files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term },
458
{ files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title },
459
];
460
// add support for partial matches
461
if (word.length > 2) {
462
const escapedWord = _escapeRegExp(word);
463
if (!terms.hasOwnProperty(word)) {
464
Object.keys(terms).forEach((term) => {
465
if (term.match(escapedWord))
466
arr.push({ files: terms[term], score: Scorer.partialTerm });
467
});
468
}
469
if (!titleTerms.hasOwnProperty(word)) {
470
Object.keys(titleTerms).forEach((term) => {
471
if (term.match(escapedWord))
472
arr.push({ files: titleTerms[term], score: Scorer.partialTitle });
473
});
474
}
475
}
476
// no match but word was a required one
477
if (arr.every((record) => record.files === undefined)) return;
478
// found search word in contents
479
arr.forEach((record) => {
480
if (record.files === undefined) return;
481
let recordFiles = record.files;
482
if (recordFiles.length === undefined) recordFiles = [recordFiles];
483
files.push(...recordFiles);
484
// set score for the word in each file
485
recordFiles.forEach((file) => {
486
if (!scoreMap.has(file)) scoreMap.set(file, new Map());
487
const fileScores = scoreMap.get(file);
488
fileScores.set(word, record.score);
489
});
490
});
491
// create the mapping
492
files.forEach((file) => {
493
if (!fileMap.has(file)) fileMap.set(file, [word]);
494
else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word);
495
});
496
});
497
// now check if the files don't contain excluded terms
498
const results = [];
499
for (const [file, wordList] of fileMap) {
500
// check if all requirements are matched
501
// as search terms with length < 3 are discarded
502
const filteredTermCount = [...searchTerms].filter(
503
(term) => term.length > 2
504
).length;
505
if (
506
wordList.length !== searchTerms.size &&
507
wordList.length !== filteredTermCount
508
)
509
continue;
510
// ensure that none of the excluded terms is in the search result
511
if (
512
[...excludedTerms].some(
513
(term) =>
514
terms[term] === file ||
515
titleTerms[term] === file ||
516
(terms[term] || []).includes(file) ||
517
(titleTerms[term] || []).includes(file)
518
)
519
)
520
break;
521
// select one (max) score for the file.
522
const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w)));
523
// add result to the result list
524
results.push([
525
docNames[file],
526
titles[file],
527
"",
528
null,
529
score,
530
filenames[file],
531
SearchResultKind.text,
532
]);
533
}
534
return results;
535
},
536
/**
537
* helper function to return a node containing the
538
* search summary for a given text. keywords is a list
539
* of stemmed words.
540
*/
541
makeSearchSummary: (htmlText, keywords, anchor) => {
542
const text = Search.htmlToText(htmlText, anchor);
543
if (text === "") return null;
544
const textLower = text.toLowerCase();
545
const actualStartPosition = [...keywords]
546
.map((k) => textLower.indexOf(k.toLowerCase()))
547
.filter((i) => i > -1)
548
.slice(-1)[0];
549
const startWithContext = Math.max(actualStartPosition - 120, 0);
550
const top = startWithContext === 0 ? "" : "...";
551
const tail = startWithContext + 240 < text.length ? "..." : "";
552
let summary = document.createElement("p");
553
summary.classList.add("context");
554
summary.textContent = top + text.substr(startWithContext, 240).trim() + tail;
555
return summary;
556
},
557
};
558
_ready(Search.init);
559
560