From 488d825c032d2c33440977a7e87217963ea44cb6 Mon Sep 17 00:00:00 2001 From: Taly Date: Fri, 9 Sep 2022 19:12:27 +0300 Subject: [PATCH] add comments --- src/backend/controllers/search.ts | 100 ++++++++++++++++++++---------- src/backend/routes/api/pages.ts | 11 ++++ src/backend/routes/api/search.ts | 40 +++++++----- src/frontend/js/modules/search.js | 20 +++++- yarn.lock | 6 +- 5 files changed, 125 insertions(+), 52 deletions(-) diff --git a/src/backend/controllers/search.ts b/src/backend/controllers/search.ts index 807bb52..12f50dc 100644 --- a/src/backend/controllers/search.ts +++ b/src/backend/controllers/search.ts @@ -1,22 +1,29 @@ -import NodeCache from 'node-cache'; import PageData from '../models/page.js'; import Pages from '../controllers/pages.js'; import urlify from '../utils/urlify.js'; import Page from '../models/page.js'; -const globalWords: { [key: string]: {[key: string]: number} } = Object.create(null); +let globalWords: { [key: string]: {[key: string]: number} } = Object.create(null); let globalPages: PageData[] = []; class Search { - // private words: { [key: string]: {[key: string]: number} } = Object.create(null); - // private pages: PageData[] = []; - // private cache: NodeCache = new NodeCache(); - + /** + * Initialize search + */ public async init() { if (globalWords && Object.keys(globalWords).length) { return Promise.resolve(); } + await this.syncDB(); + } + + /** + * Load all pages from DB and update globalWords + * Use this method when any page was updated + */ + public async syncDB() { + globalWords = Object.create(null); globalPages = await this.getPages(); /** @@ -56,6 +63,10 @@ class Search { console.log('Done'); } + /** + * Search for pages by given query + * @param searchString + */ public async query(searchString: string) { await this.init(); @@ -107,41 +118,40 @@ class Search { }); return { - suggestions: [], + suggestions: ['description', 'about', 'contact'], pages: returnPages .sort((a, b) => b.ratio - a.ratio) .slice(0, 15) } } + /** + * + * @private + */ private async getPages(): Promise { - const pages = await Pages.getAll(); - - return pages; - - // let pages: Page[] | undefined = this.cache.get("SEARCH:PAGES"); - // - // if ( pages === undefined ) { - // console.log('cache for SEARCH:PAGES is missing') - // - // pages = await Pages.getAll(); - // - // this.cache.set("SEARCH:PAGES", pages); - // } else { - // console.log('wow SEARCH:PAGES is cached') - // } - // - // return pages; + return await Pages.getAll(); } + /** + * Return list of pages with a given words + * @param words + * @private + */ private async getPagesByWords(words: string[]) { const pagesList: {[key: string]: number} = {}; + /** + * Get list of words starting with a words from the search query + */ const validWords = Object.keys(globalWords) .filter(word => { return !!words.filter(searchWord => word.indexOf(searchWord) !== -1).length }); + /** + * For each word get list of pages with this word + */ validWords.forEach(word => { Object.keys(globalWords[word]) .forEach(pageId => { @@ -153,6 +163,9 @@ class Search { }) }) + /** + * Sort pages by frequency of given words + */ const sortedPagesList = Object.keys(pagesList) .map(pageId => { return { @@ -165,10 +178,11 @@ class Search { return sortedPagesList; } - private getUnique(elements: string[]) { - return [...new Set(elements)].sort(); - } - + /** + * Get block's ratio. It is used to calculate the weight of the words in the block + * @param block + * @private + */ private getBlockRatio(block: any) { switch (block.type) { case 'header': @@ -189,6 +203,11 @@ class Search { } } + /** + * Return clear text content from block without HTML tags and special characters + * @param block + * @private + */ private getCleanTextFromBlock(block: any): string { let blockContent = ''; @@ -215,14 +234,29 @@ class Search { return blockContent; } + /** + * Remove HTML tags from string. Only content inside tags will be left + * @param text + * @private + */ private removeHTMLTags(text: string) { return text.replace(/<[^>]*>?/gm, ''); } + /** + * Remove special characters from text. For example:   & " < > + * @param text + * @private + */ private removeHTMLSpecialCharacters(text: string) { return text.replace(/&[^;]*;?/gm, ''); } + /** + * Split text to words + * @param text + * @private + */ private splitTextToWords(text: string): string[] { return text // lowercase all words @@ -237,6 +271,7 @@ class Search { // remove multiple spaces .replace(/\s+/g, ' ') + // remove spaces at the beginning and at the end .trim() // split to words by spaces @@ -261,8 +296,7 @@ class Search { } } -const search = new Search(); - -export default search; - -// export default Search; +/** + * Export initialized instance + */ +export default new Search(); diff --git a/src/backend/routes/api/pages.ts b/src/backend/routes/api/pages.ts index f4b1673..0b8b944 100644 --- a/src/backend/routes/api/pages.ts +++ b/src/backend/routes/api/pages.ts @@ -2,6 +2,7 @@ import express, { Request, Response } from 'express'; import multerFunc from 'multer'; import Pages from '../../controllers/pages.js'; import PagesOrder from '../../controllers/pagesOrder.js'; +import Search from '../../controllers/search.js'; const router = express.Router(); const multer = multerFunc(); @@ -70,6 +71,9 @@ router.put('/page', multer.none(), async (req: Request, res: Response) => { /** push to the orders array */ await PagesOrder.push(parent, page._id); + /** Update search index */ + await Search.syncDB(); + res.json({ success: true, result: page, @@ -127,6 +131,10 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => { parent, uri, }); + + /** Update search index */ + await Search.syncDB(); + res.json({ success: true, result: page, @@ -206,6 +214,9 @@ router.delete('/page/:id', async (req: Request, res: Response) => { parentPageOrder.remove(req.params.id); await parentPageOrder.save(); + /** Update search index */ + await Search.syncDB(); + res.json({ success: true, result: pageToRedirect, diff --git a/src/backend/routes/api/search.ts b/src/backend/routes/api/search.ts index 30b4dba..85700b2 100644 --- a/src/backend/routes/api/search.ts +++ b/src/backend/routes/api/search.ts @@ -10,31 +10,37 @@ const router = express.Router(); */ router.get('/search', async (req: Request, res: Response) => { try { + /** + * Get search string + */ const searchString = req.query.text as string; - /** Start measuring search time */ - // const startTime = performance.now(); - - // const search = new Search(); - // - // const searchResponse = await search.query(searchString); - + /** + * Get search results + */ const searchResponse = await Search.query(searchString); - /** End measuring search time */ - // const endTime = performance.now(); - - /** Show search time */ - // const searchItem = (endTime - startTime).toFixed(6); - // console.log(`🔎 "${searchString}" ⏱ ${searchItem} ms`); - + /** + * Compose response + */ const compactedPages = searchResponse.pages.map(page => { return { + /** Page id */ _id: page._id, + + /** Page title */ title: page.title, + + /** Page uri */ uri: page.uri, + + /** Section heading name for the found fragment */ section: page.section, + + /** Section's anchor */ anchor: page.anchor, + + /** Page fragment with searched items */ shortBody: page.shortBody, }; }); @@ -42,9 +48,11 @@ router.get('/search', async (req: Request, res: Response) => { res.json({ success: true, result: { - suggestions: searchResponse.suggestions, + /** Found pages */ pages: compactedPages, - // time: searchItem, + + /** Typing suggestions */ + suggestions: searchResponse.suggestions, }, }); } catch (err) { diff --git a/src/frontend/js/modules/search.js b/src/frontend/js/modules/search.js index c1657f5..d692967 100644 --- a/src/frontend/js/modules/search.js +++ b/src/frontend/js/modules/search.js @@ -57,7 +57,7 @@ export default class Search { * - type a search string * - fire search */ - // const testString = 'codex description'; + // const testString = 'codex descri'; // this.toggleSearchOverlay(true); // this.nodes.searchInput.value = testString; // this.debouncedSearch(testString); @@ -153,6 +153,10 @@ export default class Search { showSearchResult({ data }) { this.clearSearchResults(); + // if (data.result.suggestions.length) { + // this.showSuggestedWordCompletion(data.result.suggestions[0]); + // } + // const suggestionsWrapper = this.generateSearchSuggestions(data.result.suggestions); // // this.nodes.searchResultWrapper.appendChild(suggestionsWrapper); @@ -186,6 +190,20 @@ export default class Search { }); } + // showSuggestedWordCompletion(word) { + // const typedString = this.nodes.searchInput.value; + // const words = typedString.split(' '); + // + // words.pop(); + // words.push(word); + // + // this.nodes.searchInput.value = words.join(' '); + // + // this.nodes.searchInput.select(); + // this.nodes.searchInput.selectionStart = typedString.length; + // this.nodes.searchInput.selectionEnd = this.nodes.searchInput.value.length; + // } + // generateSearchSuggestions(suggestions = []) { // const suggestionsWrapper = document.createElement('div'); // diff --git a/yarn.lock b/yarn.lock index 1c2dd99..596e66b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2068,7 +2068,8 @@ clone-response@^1.0.2: clone@2.x: version "2.1.2" - resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== code-point-at@^1.0.0: version "1.1.0" @@ -4262,7 +4263,8 @@ nise@^5.1.1: node-cache@^5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== dependencies: clone "2.x"