DbCreator.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. const fs = require('fs-extra');
  2. const InpxParser = require('./InpxParser');
  3. const InpxHashCreator = require('./InpxHashCreator');
  4. const utils = require('./utils');
  5. const emptyFieldValue = '?';
  6. class DbCreator {
  7. constructor(config) {
  8. this.config = config;
  9. }
  10. async loadInpxFilter() {
  11. const inpxFilterFile = this.config.inpxFilterFile;
  12. if (await fs.pathExists(inpxFilterFile)) {
  13. let filter = await fs.readFile(inpxFilterFile, 'utf8');
  14. filter = JSON.parse(filter);
  15. if (filter.includeAuthors) {
  16. filter.includeAuthors = filter.includeAuthors.map(a => a.toLowerCase());
  17. filter.includeSet = new Set(filter.includeAuthors);
  18. }
  19. if (filter.excludeAuthors) {
  20. filter.excludeAuthors = filter.excludeAuthors.map(a => a.toLowerCase());
  21. filter.excludeSet = new Set(filter.excludeAuthors);
  22. }
  23. return filter;
  24. } else {
  25. return false;
  26. }
  27. }
  28. //процедура формировани БД несколько усложнена, в целях экономии памяти
  29. async run(db, callback) {
  30. const config = this.config;
  31. callback({jobStepCount: 5});
  32. callback({job: 'load inpx', jobMessage: 'Загрузка INPX', jobStep: 1, progress: 0});
  33. //временная таблица
  34. await db.create({
  35. table: 'book',
  36. cacheSize: (config.lowMemoryMode ? 5 : 500),
  37. });
  38. //поисковые таблицы, позже сохраним в БД
  39. let authorMap = new Map();//авторы
  40. let authorArr = [];
  41. let seriesMap = new Map();//серии
  42. let seriesArr = [];
  43. let titleMap = new Map();//названия
  44. let titleArr = [];
  45. let genreMap = new Map();//жанры
  46. let genreArr = [];
  47. let langMap = new Map();//языки
  48. let langArr = [];
  49. //stats
  50. let authorCount = 0;
  51. let bookCount = 0;
  52. let noAuthorBookCount = 0;
  53. let bookDelCount = 0;
  54. //stuff
  55. let recsLoaded = 0;
  56. callback({recsLoaded});
  57. let chunkNum = 0;
  58. //фильтр
  59. const inpxFilter = await this.loadInpxFilter();
  60. let filter = () => true;
  61. if (inpxFilter) {
  62. let recFilter = () => true;
  63. if (inpxFilter.filter) {
  64. if (config.allowUnsafeFilter)
  65. recFilter = new Function(`'use strict'; return ${inpxFilter.filter}`)();
  66. else
  67. throw new Error(`Unsafe property 'filter' detected in ${this.config.inpxFilterFile}. Please specify '--unsafe-filter' param if you know what you're doing.`);
  68. }
  69. filter = (rec) => {
  70. let author = rec.author;
  71. if (!author)
  72. author = emptyFieldValue;
  73. author = author.toLowerCase();
  74. let excluded = false;
  75. if (inpxFilter.excludeSet) {
  76. const authors = author.split(',');
  77. for (const a of authors) {
  78. if (inpxFilter.excludeSet.has(a)) {
  79. excluded = true;
  80. break;
  81. }
  82. }
  83. }
  84. return recFilter(rec)
  85. && (!inpxFilter.includeSet || inpxFilter.includeSet.has(author))
  86. && !excluded
  87. ;
  88. };
  89. }
  90. //вспомогательные функции
  91. const splitAuthor = (author) => {
  92. if (!author)
  93. author = emptyFieldValue;
  94. const result = author.split(',');
  95. if (result.length > 1)
  96. result.push(author);
  97. return result;
  98. }
  99. let totalFiles = 0;
  100. const readFileCallback = async(readState) => {
  101. callback(readState);
  102. if (readState.totalFiles)
  103. totalFiles = readState.totalFiles;
  104. if (totalFiles)
  105. callback({progress: (readState.current || 0)/totalFiles});
  106. };
  107. let id = 0;
  108. const parsedCallback = async(chunk) => {
  109. let filtered = false;
  110. for (const rec of chunk) {
  111. //сначала фильтр
  112. if (!filter(rec)) {
  113. rec.id = 0;
  114. filtered = true;
  115. continue;
  116. }
  117. rec.id = ++id;
  118. if (!rec.del) {
  119. bookCount++;
  120. if (!rec.author)
  121. noAuthorBookCount++;
  122. } else {
  123. bookDelCount++;
  124. }
  125. //авторы
  126. const author = splitAuthor(rec.author);
  127. for (let i = 0; i < author.length; i++) {
  128. const a = author[i];
  129. const value = a.toLowerCase();
  130. let authorRec;
  131. if (authorMap.has(value)) {
  132. const authorTmpId = authorMap.get(value);
  133. authorRec = authorArr[authorTmpId];
  134. } else {
  135. authorRec = {tmpId: authorArr.length, author: a, value, bookCount: 0, bookDelCount: 0, bookId: []};
  136. authorArr.push(authorRec);
  137. authorMap.set(value, authorRec.tmpId);
  138. if (author.length == 1 || i < author.length - 1) //без соавторов
  139. authorCount++;
  140. }
  141. //это нужно для того, чтобы имя автора начиналось с заглавной
  142. if (a[0].toUpperCase() === a[0])
  143. authorRec.author = a;
  144. //счетчики
  145. if (!rec.del) {
  146. authorRec.bookCount++;
  147. } else {
  148. authorRec.bookDelCount++;
  149. }
  150. //ссылки на книги
  151. authorRec.bookId.push(id);
  152. }
  153. }
  154. let saveChunk = [];
  155. if (filtered) {
  156. saveChunk = chunk.filter(r => r.id);
  157. } else {
  158. saveChunk = chunk;
  159. }
  160. await db.insert({table: 'book', rows: saveChunk});
  161. recsLoaded += chunk.length;
  162. callback({recsLoaded});
  163. if (chunkNum++ % 10 == 0 && config.lowMemoryMode)
  164. utils.freeMemory();
  165. };
  166. //парсинг 1
  167. const parser = new InpxParser();
  168. await parser.parse(config.inpxFile, readFileCallback, parsedCallback);
  169. utils.freeMemory();
  170. //отсортируем авторов и выдадим им правильные id
  171. //порядок id соответствует ASC-сортировке по author.toLowerCase
  172. callback({job: 'author sort', jobMessage: 'Сортировка авторов', jobStep: 2, progress: 0});
  173. await utils.sleep(100);
  174. authorArr.sort((a, b) => a.value.localeCompare(b.value));
  175. id = 0;
  176. authorMap = new Map();
  177. for (const authorRec of authorArr) {
  178. authorRec.id = ++id;
  179. authorMap.set(authorRec.author, id);
  180. delete authorRec.tmpId;
  181. }
  182. utils.freeMemory();
  183. //подготовка к сохранению author_book
  184. const saveBookChunk = async(authorChunk, callback) => {
  185. callback(0);
  186. const ids = [];
  187. for (const a of authorChunk) {
  188. for (const id of a.bookId) {
  189. ids.push(id);
  190. }
  191. }
  192. ids.sort();// обязательно, иначе будет тормозить - особенности JembaDb
  193. callback(0.1);
  194. const rows = await db.select({table: 'book', where: `@@id(${db.esc(ids)})`});
  195. callback(0.6);
  196. await utils.sleep(100);
  197. const bookArr = new Map();
  198. for (const row of rows)
  199. bookArr.set(row.id, row);
  200. const abRows = [];
  201. for (const a of authorChunk) {
  202. const aBooks = [];
  203. for (const id of a.bookId) {
  204. const rec = bookArr.get(id);
  205. aBooks.push(rec);
  206. }
  207. abRows.push({id: a.id, author: a.author, books: JSON.stringify(aBooks)});
  208. delete a.bookId;//в дальнейшем не понадобится, authorArr сохраняем без него
  209. }
  210. callback(0.7);
  211. await db.insert({
  212. table: 'author_book',
  213. rows: abRows,
  214. });
  215. callback(1);
  216. };
  217. callback({job: 'book sort', jobMessage: 'Сортировка книг', jobStep: 3, progress: 0});
  218. //сохранение author_book
  219. await db.create({
  220. table: 'author_book',
  221. });
  222. let idsLen = 0;
  223. let aChunk = [];
  224. let prevI = 0;
  225. for (let i = 0; i < authorArr.length; i++) {// eslint-disable-line
  226. const author = authorArr[i];
  227. aChunk.push(author);
  228. idsLen += author.bookId.length;
  229. if (idsLen > 50000) {//константа выяснена эмпирическим путем "память/скорость"
  230. await saveBookChunk(aChunk, (p) => {
  231. callback({progress: (prevI + (i - prevI)*p)/authorArr.length});
  232. });
  233. prevI = i;
  234. idsLen = 0;
  235. aChunk = [];
  236. await utils.sleep(100);
  237. utils.freeMemory();
  238. await db.freeMemory();
  239. }
  240. }
  241. if (aChunk.length) {
  242. await saveBookChunk(aChunk, () => {});
  243. aChunk = null;
  244. }
  245. callback({progress: 1});
  246. //чистка памяти, ибо жрет как не в себя
  247. await db.close({table: 'book'});
  248. await db.freeMemory();
  249. utils.freeMemory();
  250. //парсинг 2, подготовка
  251. const parseField = (fieldValue, fieldMap, fieldArr, authorIds, bookId) => {
  252. let addBookId = bookId;
  253. if (!fieldValue) {
  254. fieldValue = emptyFieldValue;
  255. addBookId = 0;//!!!
  256. }
  257. const value = fieldValue.toLowerCase();
  258. let fieldRec;
  259. if (fieldMap.has(value)) {
  260. const fieldId = fieldMap.get(value);
  261. fieldRec = fieldArr[fieldId];
  262. } else {
  263. fieldRec = {id: fieldArr.length, value, authorId: new Set()};
  264. if (bookId)
  265. fieldRec.bookId = new Set();
  266. fieldArr.push(fieldRec);
  267. fieldMap.set(value, fieldRec.id);
  268. }
  269. for (const id of authorIds) {
  270. fieldRec.authorId.add(id);
  271. }
  272. if (addBookId)
  273. fieldRec.bookId.add(addBookId);
  274. };
  275. const parseBookRec = (rec) => {
  276. //авторы
  277. const author = splitAuthor(rec.author);
  278. const authorIds = [];
  279. for (const a of author) {
  280. const authorId = authorMap.get(a);
  281. if (!authorId) //подстраховка
  282. continue;
  283. authorIds.push(authorId);
  284. }
  285. //серии
  286. parseField(rec.series, seriesMap, seriesArr, authorIds, rec.id);
  287. //названия
  288. parseField(rec.title, titleMap, titleArr, authorIds);
  289. //жанры
  290. let genre = rec.genre || emptyFieldValue;
  291. genre = rec.genre.split(',');
  292. for (let g of genre) {
  293. if (!g)
  294. g = emptyFieldValue;
  295. let genreRec;
  296. if (genreMap.has(g)) {
  297. const genreId = genreMap.get(g);
  298. genreRec = genreArr[genreId];
  299. } else {
  300. genreRec = {id: genreArr.length, value: g, authorId: new Set()};
  301. genreArr.push(genreRec);
  302. genreMap.set(g, genreRec.id);
  303. }
  304. for (const id of authorIds) {
  305. genreRec.authorId.add(id);
  306. }
  307. }
  308. //языки
  309. parseField(rec.lang, langMap, langArr, authorIds);
  310. };
  311. callback({job: 'search tables create', jobMessage: 'Создание поисковых таблиц', jobStep: 4, progress: 0});
  312. //парсинг 2, теперь можно создавать остальные поисковые таблицы
  313. let proc = 0;
  314. while (1) {// eslint-disable-line
  315. const rows = await db.select({
  316. table: 'author_book',
  317. where: `
  318. let iter = @getItem('parse_book');
  319. if (!iter) {
  320. iter = @all();
  321. @setItem('parse_book', iter);
  322. }
  323. const ids = new Set();
  324. let id = iter.next();
  325. while (!id.done) {
  326. ids.add(id.value);
  327. if (ids.size >= 10000)
  328. break;
  329. id = iter.next();
  330. }
  331. return ids;
  332. `
  333. });
  334. if (rows.length) {
  335. for (const row of rows) {
  336. const books = JSON.parse(row.books);
  337. for (const rec of books)
  338. parseBookRec(rec);
  339. }
  340. proc += rows.length;
  341. callback({progress: proc/authorArr.length});
  342. } else
  343. break;
  344. await utils.sleep(100);
  345. if (config.lowMemoryMode) {
  346. utils.freeMemory();
  347. await db.freeMemory();
  348. }
  349. }
  350. //чистка памяти, ибо жрет как не в себя
  351. authorMap = null;
  352. seriesMap = null;
  353. titleMap = null;
  354. genreMap = null;
  355. utils.freeMemory();
  356. //сортировка серий
  357. callback({job: 'series sort', jobMessage: 'Сортировка серий', jobStep: 5, progress: 0});
  358. await utils.sleep(100);
  359. seriesArr.sort((a, b) => a.value.localeCompare(b.value));
  360. await utils.sleep(100);
  361. callback({progress: 0.3});
  362. id = 0;
  363. for (const seriesRec of seriesArr) {
  364. seriesRec.id = ++id;
  365. }
  366. await utils.sleep(100);
  367. callback({progress: 0.5});
  368. //заодно и названия
  369. titleArr.sort((a, b) => a.value.localeCompare(b.value));
  370. await utils.sleep(100);
  371. callback({progress: 0.7});
  372. id = 0;
  373. for (const titleRec of titleArr) {
  374. titleRec.id = ++id;
  375. }
  376. //stats
  377. const stats = {
  378. filesCount: 0,//вычислим позднее
  379. filesCountAll: 0,//вычислим позднее
  380. filesDelCount: 0,//вычислим позднее
  381. recsLoaded,
  382. authorCount,
  383. authorCountAll: authorArr.length,
  384. bookCount,
  385. bookCountAll: bookCount + bookDelCount,
  386. bookDelCount,
  387. noAuthorBookCount,
  388. titleCount: titleArr.length,
  389. seriesCount: seriesArr.length,
  390. genreCount: genreArr.length,
  391. langCount: langArr.length,
  392. };
  393. //console.log(stats);
  394. //сохраним поисковые таблицы
  395. const chunkSize = 10000;
  396. const saveTable = async(table, arr, nullArr, authorIdToArray = false, bookIdToArray = false) => {
  397. arr.sort((a, b) => a.value.localeCompare(b.value));
  398. await db.create({
  399. table,
  400. index: {field: 'value', unique: true, depth: 1000000},
  401. });
  402. //вставка в БД по кусочкам, экономим память
  403. for (let i = 0; i < arr.length; i += chunkSize) {
  404. const chunk = arr.slice(i, i + chunkSize);
  405. if (authorIdToArray) {
  406. for (const rec of chunk)
  407. rec.authorId = Array.from(rec.authorId);
  408. }
  409. if (bookIdToArray) {
  410. for (const rec of chunk)
  411. rec.bookId = Array.from(rec.bookId);
  412. }
  413. await db.insert({table, rows: chunk});
  414. if (i % 5 == 0) {
  415. await db.freeMemory();
  416. await utils.sleep(100);
  417. }
  418. callback({progress: i/arr.length});
  419. }
  420. nullArr();
  421. await db.close({table});
  422. utils.freeMemory();
  423. await db.freeMemory();
  424. };
  425. //author
  426. callback({job: 'author save', jobMessage: 'Сохранение индекса авторов', jobStep: 6, progress: 0});
  427. await saveTable('author', authorArr, () => {authorArr = null});
  428. //series
  429. callback({job: 'series save', jobMessage: 'Сохранение индекса серий', jobStep: 7, progress: 0});
  430. await saveTable('series', seriesArr, () => {seriesArr = null}, true, true);
  431. //title
  432. callback({job: 'title save', jobMessage: 'Сохранение индекса названий', jobStep: 8, progress: 0});
  433. await saveTable('title', titleArr, () => {titleArr = null}, true);
  434. //genre
  435. callback({job: 'genre save', jobMessage: 'Сохранение индекса жанров', jobStep: 9, progress: 0});
  436. await saveTable('genre', genreArr, () => {genreArr = null}, true);
  437. //lang
  438. callback({job: 'lang save', jobMessage: 'Сохранение индекса языков', jobStep: 10, progress: 0});
  439. await saveTable('lang', langArr, () => {langArr = null}, true);
  440. //кэш-таблицы запросов
  441. await db.create({table: 'query_cache'});
  442. await db.create({table: 'query_time'});
  443. //кэш-таблица имен файлов и их хешей
  444. await db.create({table: 'file_hash'});
  445. //-- завершающие шаги --------------------------------
  446. await db.open({
  447. table: 'book',
  448. cacheSize: (config.lowMemoryMode ? 5 : 500),
  449. });
  450. callback({job: 'optimization', jobMessage: 'Оптимизация', jobStep: 11, progress: 0});
  451. await this.optimizeSeries(db, callback);
  452. callback({job: 'stats count', jobMessage: 'Подсчет статистики', jobStep: 12, progress: 0});
  453. await this.countStats(db, callback, stats);
  454. //чистка памяти, ибо жрет как не в себя
  455. await db.drop({table: 'book'});//больше не понадобится
  456. await db.freeMemory();
  457. utils.freeMemory();
  458. //config сохраняем в самом конце, нет конфига - с базой что-то не так
  459. const inpxHashCreator = new InpxHashCreator(config);
  460. await db.create({
  461. table: 'config'
  462. });
  463. await db.insert({table: 'config', rows: [
  464. {id: 'inpxInfo', value: (inpxFilter && inpxFilter.info ? inpxFilter.info : parser.info)},
  465. {id: 'stats', value: stats},
  466. {id: 'inpxHash', value: await inpxHashCreator.getHash()},
  467. ]});
  468. callback({job: 'done', jobMessage: ''});
  469. }
  470. async optimizeSeries(db, callback) {
  471. //оптимизация series, превращаем массив bookId в books, кладем все в series_book
  472. await db.open({table: 'series'});
  473. await db.create({
  474. table: 'series_book',
  475. flag: {name: 'toDel', check: 'r => r.toDel'},
  476. });
  477. const saveSeriesChunk = async(seriesChunk) => {
  478. const ids = [];
  479. for (const s of seriesChunk) {
  480. for (const id of s.bookId) {
  481. ids.push(id);
  482. }
  483. }
  484. ids.sort();// обязательно, иначе будет тормозить - особенности JembaDb
  485. const rows = await db.select({table: 'book', where: `@@id(${db.esc(ids)})`});
  486. const bookArr = new Map();
  487. for (const row of rows)
  488. bookArr.set(row.id, row);
  489. for (const s of seriesChunk) {
  490. s.books = [];
  491. for (const id of s.bookId) {
  492. const rec = bookArr.get(id);
  493. s.books.push(rec);
  494. }
  495. if (s.books.length) {
  496. s.series = s.books[0].value;
  497. } else {
  498. s.toDel = 1;
  499. }
  500. delete s.bookId;
  501. }
  502. await db.insert({
  503. table: 'series_book',
  504. rows: seriesChunk,
  505. });
  506. };
  507. const rows = await db.select({table: 'series'});
  508. let idsLen = 0;
  509. let chunk = [];
  510. let processed = 0;
  511. for (const row of rows) {// eslint-disable-line
  512. chunk.push(row);
  513. idsLen += row.bookId.length;
  514. processed++;
  515. if (idsLen > 20000) {//константа выяснена эмпирическим путем "память/скорость"
  516. await saveSeriesChunk(chunk);
  517. idsLen = 0;
  518. chunk = [];
  519. callback({progress: processed/rows.length});
  520. await utils.sleep(100);
  521. utils.freeMemory();
  522. await db.freeMemory();
  523. }
  524. }
  525. if (chunk.length) {
  526. await saveSeriesChunk(chunk);
  527. chunk = null;
  528. }
  529. await db.delete({table: 'series_book', where: `@@flag('toDel')`});
  530. await db.close({table: 'series_book'});
  531. await db.close({table: 'series'});
  532. }
  533. async countStats(db, callback, stats) {
  534. //статистика по количеству файлов
  535. //эмуляция прогресса
  536. let countDone = false;
  537. (async() => {
  538. let i = 0;
  539. while (!countDone) {
  540. callback({progress: i/100});
  541. i = (i < 100 ? i + 5 : 100);
  542. await utils.sleep(1000);
  543. }
  544. })();
  545. //подчсет
  546. const countRes = await db.select({table: 'book', rawResult: true, where: `
  547. const files = new Set();
  548. const filesDel = new Set();
  549. for (const id of @all()) {
  550. const r = @row(id);
  551. const file = ${"`${r.folder}/${r.file}.${r.ext}`"};
  552. if (!r.del) {
  553. files.add(file);
  554. } else {
  555. filesDel.add(file);
  556. }
  557. }
  558. for (const file of filesDel)
  559. if (files.has(file))
  560. filesDel.delete(file);
  561. return {filesCount: files.size, filesDelCount: filesDel.size};
  562. `});
  563. if (countRes.length) {
  564. const res = countRes[0].rawResult;
  565. stats.filesCount = res.filesCount;
  566. stats.filesCountAll = res.filesCount + res.filesDelCount;
  567. stats.filesDelCount = res.filesDelCount;
  568. }
  569. countDone = true;
  570. }
  571. }
  572. module.exports = DbCreator;