https://github.com/angular/angular
Tip revision: 7ce89a2e94c8bc72ba5bb145cf9f16a752c61a9a authored by Jessica Janiuk on 06 September 2023, 17:55:52 UTC
release: cut the v16.2.4 release
release: cut the v16.2.4 release
Tip revision: 7ce89a2
search.worker.ts
/// <reference lib="webworker" />
import * as lunr from 'lunr';
import {WebWorkerMessage} from '../shared/web-worker-message';
const SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
let index: lunr.Index;
const pageMap: SearchInfo = {};
interface SearchInfo {
[key: string]: PageInfo;
}
interface PageInfo {
path: string;
type: string;
title: string;
headings: string;
keywords: string;
members: string;
topics: string;
}
interface EncodedPages {
dictionary: string;
pages: EncodedPage[];
}
interface EncodedPage {
path: string;
type: string;
title: string;
headings: number[];
keywords: number[];
members: number[];
topics: string;
}
addEventListener('message', handleMessage);
// Create the lunr index - the docs should be an array of objects, each object containing
// the path and search terms for a page
function createIndex(loadIndexFn: IndexLoader): lunr.Index {
// The lunr typings are missing QueryLexer so we have to add them here manually.
const queryLexer = (lunr as any as {QueryLexer: {termSeparator: RegExp}}).QueryLexer;
queryLexer.termSeparator = lunr.tokenizer.separator = /\s+/;
return lunr(function() {
this.pipeline.remove(lunr.stemmer);
this.ref('path');
this.field('topics', {boost: 15});
this.field('title', {boost: 10});
this.field('headings', {boost: 5});
this.field('members', {boost: 4});
this.field('keywords', {boost: 2});
loadIndexFn(this);
});
}
// The worker receives a message to load the index and to query the index
function handleMessage(message: {data: WebWorkerMessage}): void {
const type = message.data.type;
const id = message.data.id;
const payload = message.data.payload;
switch (type) {
case 'load-index':
makeRequest(SEARCH_TERMS_URL, (encodedPages: EncodedPages) => {
index = createIndex(loadIndex(encodedPages));
postMessage({type, id, payload: true});
});
break;
case 'query-index':
postMessage({type, id, payload: {query: payload, results: queryIndex(payload)}});
break;
default:
postMessage({type, id, payload: {error: 'invalid message type'}});
}
}
// Use XHR to make a request to the server
function makeRequest(url: string, callback: (response: any) => void): void {
// The JSON file that is loaded should be an array of PageInfo:
const searchDataRequest = new XMLHttpRequest();
searchDataRequest.onload = function() {
callback(JSON.parse(this.responseText));
};
searchDataRequest.open('GET', url);
searchDataRequest.send();
}
// Create the search index from the searchInfo which contains the information about each page to be
// indexed
function loadIndex({dictionary, pages}: EncodedPages): IndexLoader {
const dictionaryArray = dictionary.split(' ');
return (indexBuilder: lunr.Builder) => {
// Store the pages data to be used in mapping query results back to pages
// Add search terms from each page to the search index
pages.forEach(encodedPage => {
const page = decodePage(encodedPage, dictionaryArray);
indexBuilder.add(page);
pageMap[page.path] = page;
});
};
}
function decodePage(encodedPage: EncodedPage, dictionary: string[]): PageInfo {
return {
...encodedPage,
headings: encodedPage.headings?.map(i => dictionary[i]).join(' ') ?? '',
keywords: encodedPage.keywords?.map(i => dictionary[i]).join(' ') ?? '',
members: encodedPage.members?.map(i => dictionary[i]).join(' ') ?? '',
};
}
// Query the index and return the processed results
function queryIndex(query: string): PageInfo[] {
// Strip off quotes
query = query.replace(/^["']|['"]$/g, '');
try {
if (query.length) {
// First try a query where every term must be present
// (see https://lunrjs.com/guides/searching.html#term-presence)
const queryAll = query.replace(/\S+/g, '+$&');
let results = index.search(queryAll);
// If that was too restrictive just query for any term to be present
if (results.length === 0) {
results = index.search(query);
}
// If that is still too restrictive then search in the title for the first word in the query
if (results.length === 0) {
// E.g. if the search is "ngCont guide" then we search for "ngCont guide title:*ngCont*"
const titleQuery = 'title:*' + query.split(' ', 1)[0] + '*';
results = index.search(query + ' ' + titleQuery);
}
// Map the hits into info about each page to be returned as results
return results.map(hit => pageMap[hit.ref]);
}
} catch (e) {
// If the search query cannot be parsed the index throws an error
// Log it and recover
console.error(e);
}
return [];
}
type IndexLoader = (indexBuilder: lunr.Builder) => void;