DbCreator.js 17 KB

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