From 0b0a51e5d44f66173e3739f8a98189b6f9fa478f Mon Sep 17 00:00:00 2001 From: Book Pauk Date: Fri, 19 Aug 2022 17:16:39 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B4=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/config/base.js | 1 + server/config/index.js | 3 +- server/core/DbCreator.js | 227 ++++++++++++++------------------------ server/core/DbSearcher.js | 109 ++++++++++++++---- server/core/WebWorker.js | 23 ++-- server/index.js | 3 - 6 files changed, 185 insertions(+), 181 deletions(-) diff --git a/server/config/base.js b/server/config/base.js index aeb507d..8cab576 100644 --- a/server/config/base.js +++ b/server/config/base.js @@ -18,6 +18,7 @@ module.exports = { loggingEnabled: true, maxFilesDirSize: 1024*1024*1024,//1Gb + cacheCleanInterval: 60,//minutes webConfigParams: ['name', 'version', 'branch'], diff --git a/server/config/index.js b/server/config/index.js index ec662f6..3f34be2 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -6,7 +6,8 @@ const branchFilename = __dirname + '/application_env'; const propsToSave = [ 'loggingEnabled', 'maxFilesDirSize', - 'server', + 'cacheCleanInterval', + 'server', ]; let instance = null; diff --git a/server/core/DbCreator.js b/server/core/DbCreator.js index 9aace2b..4e925b0 100644 --- a/server/core/DbCreator.js +++ b/server/core/DbCreator.js @@ -41,37 +41,46 @@ class DbCreator { let recsLoaded = 0; let id = 0; let chunkNum = 0; + + const splitAuthor = (author) => { + if (!author) { + author = 'Автор не указан'; + } + + const result = author.split(','); + if (result.length > 1) + result.push(author); + + return result; + } + const parsedCallback = async(chunk) => { for (const rec of chunk) { rec.id = ++id; - if (!rec.del) + if (!rec.del) { bookCount++; - else - bookDelCount++; - - if (!rec.author) { - if (!rec.del) + if (!rec.author) noAuthorBookCount++; - rec.author = 'Автор не указан'; + } else { + bookDelCount++; } //авторы - const author = rec.author.split(','); - if (author.length > 1) - author.push(rec.author); + const author = splitAuthor(rec.author); for (let i = 0; i < author.length; i++) { const a = author[i]; + const value = a.toLowerCase(); let authorRec; - if (authorMap.has(a)) { - const authorTmpId = authorMap.get(a); + if (authorMap.has(value)) { + const authorTmpId = authorMap.get(value); authorRec = authorArr[authorTmpId]; } else { - authorRec = {tmpId: authorArr.length, author: a, value: a.toLowerCase(), bookId: []}; + authorRec = {tmpId: authorArr.length, author: a, value, bookId: []}; authorArr.push(authorRec); - authorMap.set(a, authorRec.tmpId); + authorMap.set(value, authorRec.tmpId); if (author.length == 1 || i < author.length - 1) //без соавторов authorCount++; @@ -112,17 +121,29 @@ class DbCreator { utils.freeMemory(); //теперь можно создавать остальные поисковые таблицы + const parseField = (fieldValue, fieldMap, fieldArr, authorIds) => { + if (fieldValue) { + const value = fieldValue.toLowerCase(); + + let fieldRec; + if (fieldMap.has(value)) { + const fieldId = fieldMap.get(value); + fieldRec = fieldArr[fieldId]; + } else { + fieldRec = {id: fieldArr.length, value, authorId: new Set()}; + fieldArr.push(fieldRec); + fieldMap.set(value, fieldRec.id); + } + + for (const id of authorIds) { + fieldRec.authorId.add(id); + } + } + }; + const parseBookRec = (rec) => { //авторы - if (!rec.author) { - if (!rec.del) - noAuthorBookCount++; - rec.author = 'Автор не указан'; - } - - const author = rec.author.split(','); - if (author.length > 1) - author.push(rec.author); + const author = splitAuthor(rec.author); const authorIds = []; for (const a of author) { @@ -133,42 +154,10 @@ class DbCreator { } //серии - if (rec.series) { - const series = rec.series; - - let seriesRec; - if (seriesMap.has(series)) { - const seriesId = seriesMap.get(series); - seriesRec = seriesArr[seriesId]; - } else { - seriesRec = {id: seriesArr.length, value: series.toLowerCase(), authorId: new Set()}; - seriesArr.push(seriesRec); - seriesMap.set(series, seriesRec.id); - } - - for (const id of authorIds) { - seriesRec.authorId.add(id); - } - } + parseField(rec.series, seriesMap, seriesArr, authorIds); //названия - if (rec.title) { - const title = rec.title; - - let titleRec; - if (titleMap.has(title)) { - const titleId = titleMap.get(title); - titleRec = titleArr[titleId]; - } else { - titleRec = {id: titleArr.length, value: title.toLowerCase(), authorId: new Set()}; - titleArr.push(titleRec); - titleMap.set(title, titleRec.id); - } - - for (const id of authorIds) { - titleRec.authorId.add(id); - } - } + parseField(rec.title, titleMap, titleArr, authorIds); //жанры if (rec.genre) { @@ -192,23 +181,7 @@ class DbCreator { } //языки - if (rec.lang) { - const lang = rec.lang; - - let langRec; - if (langMap.has(lang)) { - const langId = langMap.get(lang); - langRec = langArr[langId]; - } else { - langRec = {id: langArr.length, value: lang, authorId: new Set()}; - langArr.push(langRec); - langMap.set(lang, langRec.id); - } - - for (const id of authorIds) { - langRec.authorId.add(id); - } - } + parseField(rec.lang, langMap, langArr, authorIds); } callback({job: 'search tables create', jobMessage: 'Создание поисковых таблиц'}); @@ -287,93 +260,53 @@ class DbCreator { //сохраним поисковые таблицы const chunkSize = 10000; + const saveTable = async(table, arr, nullArr, authorIdToArray = true) => { + + arr.sort((a, b) => a.value.localeCompare(b.value)); + + await db.create({ + table, + index: {field: 'value', unique: true, depth: 1000000}, + }); + + //вставка в БД по кусочкам, экономим память + for (let i = 0; i < arr.length; i += chunkSize) { + const chunk = arr.slice(i, i + chunkSize); + + if (authorIdToArray) { + for (const rec of chunk) + rec.authorId = Array.from(rec.authorId); + } + + await db.insert({table, rows: chunk}); + + await utils.sleep(100); + } + + nullArr(); + await db.close({table}); + utils.freeMemory(); + }; + //author callback({job: 'author save', jobMessage: 'Сохранение авторов книг'}); - await db.create({ - table: 'author', - index: {field: 'value', depth: config.indexDepth}, - }); - - //вставка в БД по кусочкам, экономим память - for (let i = 0; i < authorArr.length; i += chunkSize) { - const chunk = authorArr.slice(i, i + chunkSize); - - await db.insert({table: 'author', rows: chunk}); - } - - authorArr = null; - await db.close({table: 'author'}); - utils.freeMemory(); + await saveTable('author', authorArr, () => {authorArr = null}, false); //series callback({job: 'series save', jobMessage: 'Сохранение серий книг'}); - await db.create({ - table: 'series', - index: {field: 'value', depth: config.indexDepth}, - }); - - //вставка в БД по кусочкам, экономим память - for (let i = 0; i < seriesArr.length; i += chunkSize) { - const chunk = seriesArr.slice(i, i + chunkSize); - for (const rec of chunk) - rec.authorId = Array.from(rec.authorId); - - await db.insert({table: 'series', rows: chunk}); - } - - seriesArr = null; - await db.close({table: 'series'}); - utils.freeMemory(); + await saveTable('series', seriesArr, () => {seriesArr = null}); //title callback({job: 'title save', jobMessage: 'Сохранение названий книг'}); - await db.create({ - table: 'title', - index: {field: 'value', depth: config.indexDepth}, - }); - - //вставка в БД по кусочкам, экономим память - let j = 0; - for (let i = 0; i < titleArr.length; i += chunkSize) { - const chunk = titleArr.slice(i, i + chunkSize); - for (const rec of chunk) - rec.authorId = Array.from(rec.authorId); - - await db.insert({table: 'title', rows: chunk}); - if (j++ % 10 == 0) - utils.freeMemory(); - await utils.sleep(100); - } - - titleArr = null; - await db.close({table: 'title'}); - utils.freeMemory(); + await saveTable('title', titleArr, () => {titleArr = null}); //genre callback({job: 'genre save', jobMessage: 'Сохранение жанров'}); - await db.create({ - table: 'genre', - index: {field: 'value', depth: config.indexDepth}, - }); - - await db.insert({table: 'genre', rows: genreArr}); - - genreArr = null; - await db.close({table: 'genre'}); - utils.freeMemory(); + await saveTable('genre', genreArr, () => {genreArr = null}); //lang callback({job: 'lang save', jobMessage: 'Сохранение языков'}); - await db.create({ - table: 'lang', - index: {field: 'value', depth: config.indexDepth}, - }); - - await db.insert({table: 'lang', rows: langArr}); - - langArr = null; - await db.close({table: 'lang'}); - utils.freeMemory(); + await saveTable('lang', langArr, () => {langArr = null}); //кэш-таблицы запросов await db.create({table: 'query_cache'}); diff --git a/server/core/DbSearcher.js b/server/core/DbSearcher.js index e243a04..a3675bf 100644 --- a/server/core/DbSearcher.js +++ b/server/core/DbSearcher.js @@ -3,8 +3,15 @@ const utils = require('./utils'); class DbSearcher { - constructor(db) { + constructor(config, db) { + this.config = config; this.db = db; + + this.searchFlag = 0; + this.timer = null; + this.closed = false; + + this.periodicCleanCache();//no await } async selectAuthorIds(query) { @@ -83,6 +90,7 @@ class DbSearcher { const key = JSON.stringify(keyArr); const rows = await db.select({table: 'query_cache', where: `@@id(${db.esc(key)})`}); + if (rows.length) {//нашли в кеше await db.insert({ table: 'query_time', @@ -111,31 +119,88 @@ class DbSearcher { } async search(query) { - const db = this.db; + if (this.closed) + throw new Error('DbSearcher closed'); - const authorIds = await this.getAuthorIds(query); + this.searchFlag++; - const totalFound = authorIds.length; - const limit = (query.limit ? query.limit : 1000); + try { + const db = this.db; - //выборка найденных авторов - let result = await db.select({ - table: 'author', - map: `(r) => ({id: r.id, author: r.author})`, - where: ` - const all = @all(); - const ids = new Set(); - let n = 0; - for (const id of all) { - if (++n > ${db.esc(limit)}) - break; - ids.add(id); - } - return ids; - ` - }); + const authorIds = await this.getAuthorIds(query); - return {result, totalFound}; + const totalFound = authorIds.length; + const limit = (query.limit ? query.limit : 1000); + + //выборка найденных авторов + let result = await db.select({ + table: 'author', + map: `(r) => ({id: r.id, author: r.author})`, + where: ` + const all = @all(); + const ids = new Set(); + let n = 0; + for (const id of all) { + if (++n > ${db.esc(limit)}) + break; + ids.add(id); + } + return ids; + ` + }); + + return {result, totalFound}; + } finally { + this.searchFlag--; + } + } + + async periodicCleanCache() { + this.timer = null; + const cleanInterval = 5*1000;//this.config.cacheCleanInterval*60*1000; + + try { + const db = this.db; + + const oldThres = Date.now() - cleanInterval; + + //выберем всех кандидатов удаление + const rows = await db.select({ + table: 'query_time', + where: ` + @@iter(@all(), (r) => (r.time < ${db.esc(oldThres)})); + ` + }); + + const ids = []; + for (const row of rows) + ids.push(row.id); + + //удаляем + await db.delete({table: 'query_cache', where: `@@id(${db.esc(ids)})`}); + await db.delete({table: 'query_time', where: `@@id(${db.esc(ids)})`}); + + console.log('Cache clean', ids); + } catch(e) { + console.error(e.message); + } finally { + if (!this.closed) { + this.timer = setTimeout(() => { this.periodicCleanCache(); }, cleanInterval); + } + } + } + + async close() { + while (this.searchFlag > 0) { + await utils.sleep(50); + } + + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + + this.closed = true; } } diff --git a/server/core/WebWorker.js b/server/core/WebWorker.js index bc950cd..2ce6b14 100644 --- a/server/core/WebWorker.js +++ b/server/core/WebWorker.js @@ -87,7 +87,6 @@ class WebWorker { }); try { - log(' start INPX import'); const dbCreator = new DbCreator(config); await dbCreator.run(db, (state) => { @@ -101,10 +100,9 @@ class WebWorker { log(` ${state.job}`); }); - log(' finish INPX import'); + log('Searcher DB successfully created'); } finally { await db.unlock(); - log('Searcher DB successfully created'); } } @@ -141,11 +139,7 @@ class WebWorker { //открываем все таблицы await db.openAll(); - //закроем title для экономии памяти, откроем при необходимости - await db.close({table: 'title'}); - this.titleOpen = false; - - this.dbSearcher = new DbSearcher(db); + this.dbSearcher = new DbSearcher(config, db); db.wwCache = {}; this.db = db; @@ -159,6 +153,19 @@ class WebWorker { } } + async recreateDb() { + this.setMyState(ssDbCreating); + + if (this.dbSearcher) { + await this.dbSearcher.close(); + this.dbSearcher = null; + } + + await this.closeDb(); + + await this.loadOrCreateDb(true); + } + async dbConfig() { this.checkMyState(); diff --git a/server/index.js b/server/index.js index 2b9dac6..cfb890e 100644 --- a/server/index.js +++ b/server/index.js @@ -96,9 +96,6 @@ async function init() { config.recreateDb = argv.recreate || false; - //TODO as cli param? - config.indexDepth = 1000; - //app const appDir = `${config.publicDir}/app`; const appNewDir = `${config.publicDir}/app_new`;