DbCreator.js 21 KB

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