DbCreator.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. const InpxParser = require('./InpxParser');
  2. const utils = require('./utils');
  3. const emptyFieldValue = '?';
  4. class DbCreator {
  5. constructor(config) {
  6. this.config = config;
  7. }
  8. //процедура формировани БД несколько усложнена, в целях экономии памяти
  9. async run(db, callback) {
  10. const config = this.config;
  11. callback({jobStepCount: 5});
  12. callback({job: 'load inpx', jobMessage: 'Загрузка INPX', jobStep: 1, progress: 0});
  13. //временная таблица
  14. await db.create({
  15. table: 'book',
  16. cacheSize: (config.lowMemoryMode ? 5 : 500),
  17. });
  18. //поисковые таблицы, позже сохраним в БД
  19. let authorMap = new Map();//авторы
  20. let authorArr = [];
  21. let seriesMap = new Map();//серии
  22. let seriesArr = [];
  23. let titleMap = new Map();//названия
  24. let titleArr = [];
  25. let genreMap = new Map();//жанры
  26. let genreArr = [];
  27. let langMap = new Map();//языки
  28. let langArr = [];
  29. //stats
  30. let authorCount = 0;
  31. let bookCount = 0;
  32. let noAuthorBookCount = 0;
  33. let bookDelCount = 0;
  34. //stuff
  35. let recsLoaded = 0;
  36. callback({recsLoaded});
  37. let chunkNum = 0;
  38. const splitAuthor = (author) => {
  39. if (!author) {
  40. author = emptyFieldValue;
  41. }
  42. const result = author.split(',');
  43. if (result.length > 1)
  44. result.push(author);
  45. return result;
  46. }
  47. let totalFiles = 0;
  48. const readFileCallback = async(readState) => {
  49. callback(readState);
  50. if (readState.totalFiles)
  51. totalFiles = readState.totalFiles;
  52. if (totalFiles)
  53. callback({progress: (readState.current || 0)/totalFiles});
  54. };
  55. let id = 0;
  56. const parsedCallback = async(chunk) => {
  57. for (const rec of chunk) {
  58. rec.id = ++id;
  59. if (!rec.del) {
  60. bookCount++;
  61. if (!rec.author)
  62. noAuthorBookCount++;
  63. } else {
  64. bookDelCount++;
  65. }
  66. //авторы
  67. const author = splitAuthor(rec.author);
  68. for (let i = 0; i < author.length; i++) {
  69. const a = author[i];
  70. const value = a.toLowerCase();
  71. let authorRec;
  72. if (authorMap.has(value)) {
  73. const authorTmpId = authorMap.get(value);
  74. authorRec = authorArr[authorTmpId];
  75. } else {
  76. authorRec = {tmpId: authorArr.length, author: a, value, bookCount: 0, bookDelCount: 0, bookId: []};
  77. authorArr.push(authorRec);
  78. authorMap.set(value, authorRec.tmpId);
  79. if (author.length == 1 || i < author.length - 1) //без соавторов
  80. authorCount++;
  81. }
  82. //это нужно для того, чтобы имя автора начиналось с заглавной
  83. if (a[0].toUpperCase() === a[0])
  84. authorRec.author = a;
  85. //счетчики
  86. if (!rec.del) {
  87. authorRec.bookCount++;
  88. } else {
  89. authorRec.bookDelCount++;
  90. }
  91. //ссылки на книги
  92. authorRec.bookId.push(id);
  93. }
  94. }
  95. await db.insert({table: 'book', rows: chunk});
  96. recsLoaded += chunk.length;
  97. callback({recsLoaded});
  98. if (chunkNum++ % 10 == 0 && config.lowMemoryMode)
  99. utils.freeMemory();
  100. };
  101. //парсинг 1
  102. const parser = new InpxParser();
  103. await parser.parse(config.inpxFile, readFileCallback, parsedCallback);
  104. utils.freeMemory();
  105. //отсортируем авторов и выдадим им правильные id
  106. //порядок id соответствует ASC-сортировке по author.toLowerCase
  107. callback({job: 'author sort', jobMessage: 'Сортировка авторов', jobStep: 2, progress: 0});
  108. await utils.sleep(100);
  109. authorArr.sort((a, b) => a.value.localeCompare(b.value));
  110. id = 0;
  111. authorMap = new Map();
  112. for (const authorRec of authorArr) {
  113. authorRec.id = ++id;
  114. authorMap.set(authorRec.author, id);
  115. delete authorRec.tmpId;
  116. }
  117. utils.freeMemory();
  118. //подготовка к сохранению author_book
  119. const saveBookChunk = async(authorChunk, callback) => {
  120. callback(0);
  121. const ids = [];
  122. for (const a of authorChunk) {
  123. for (const id of a.bookId) {
  124. ids.push(id);
  125. }
  126. }
  127. ids.sort();// обязательно, иначе будет тормозить - особенности JembaDb
  128. callback(0.1);
  129. const rows = await db.select({table: 'book', where: `@@id(${db.esc(ids)})`});
  130. callback(0.6);
  131. await utils.sleep(100);
  132. const bookArr = new Map();
  133. for (const row of rows)
  134. bookArr.set(row.id, row);
  135. const abRows = [];
  136. for (const a of authorChunk) {
  137. const aBooks = [];
  138. for (const id of a.bookId) {
  139. const rec = bookArr.get(id);
  140. aBooks.push(rec);
  141. }
  142. abRows.push({id: a.id, author: a.author, books: JSON.stringify(aBooks)});
  143. delete a.bookId;//в дальнейшем не понадобится, authorArr сохраняем без него
  144. }
  145. callback(0.7);
  146. await db.insert({
  147. table: 'author_book',
  148. rows: abRows,
  149. });
  150. callback(1);
  151. };
  152. callback({job: 'book sort', jobMessage: 'Сортировка книг', jobStep: 3, progress: 0});
  153. //сохранение author_book
  154. await db.create({
  155. table: 'author_book',
  156. });
  157. let idsLen = 0;
  158. let aChunk = [];
  159. let prevI = 0;
  160. for (let i = 0; i < authorArr.length; i++) {// eslint-disable-line
  161. const author = authorArr[i];
  162. aChunk.push(author);
  163. idsLen += author.bookId.length;
  164. if (idsLen > 50000) {//константа выяснена эмпирическим путем "память/скорость"
  165. await saveBookChunk(aChunk, (p) => {
  166. callback({progress: (prevI + (i - prevI)*p)/authorArr.length});
  167. });
  168. prevI = i;
  169. idsLen = 0;
  170. aChunk = [];
  171. await utils.sleep(100);
  172. utils.freeMemory();
  173. await db.freeMemory();
  174. }
  175. }
  176. if (aChunk.length) {
  177. await saveBookChunk(aChunk, () => {});
  178. aChunk = null;
  179. }
  180. callback({progress: 1});
  181. //чистка памяти, ибо жрет как не в себя
  182. await db.drop({table: 'book'});
  183. await db.freeMemory();
  184. utils.freeMemory();
  185. //парсинг 2, подготовка
  186. const parseField = (fieldValue, fieldMap, fieldArr, authorIds) => {
  187. if (!fieldValue)
  188. fieldValue = emptyFieldValue;
  189. const value = fieldValue.toLowerCase();
  190. let fieldRec;
  191. if (fieldMap.has(value)) {
  192. const fieldId = fieldMap.get(value);
  193. fieldRec = fieldArr[fieldId];
  194. } else {
  195. fieldRec = {id: fieldArr.length, value, authorId: new Set()};
  196. fieldArr.push(fieldRec);
  197. fieldMap.set(value, fieldRec.id);
  198. }
  199. for (const id of authorIds) {
  200. fieldRec.authorId.add(id);
  201. }
  202. };
  203. const parseBookRec = (rec) => {
  204. //авторы
  205. const author = splitAuthor(rec.author);
  206. const authorIds = [];
  207. for (const a of author) {
  208. const authorId = authorMap.get(a);
  209. if (!authorId) //подстраховка
  210. continue;
  211. authorIds.push(authorId);
  212. }
  213. //серии
  214. parseField(rec.series, seriesMap, seriesArr, authorIds);
  215. //названия
  216. parseField(rec.title, titleMap, titleArr, authorIds);
  217. //жанры
  218. let genre = rec.genre || emptyFieldValue;
  219. genre = rec.genre.split(',');
  220. for (let g of genre) {
  221. if (!g)
  222. g = emptyFieldValue;
  223. let genreRec;
  224. if (genreMap.has(g)) {
  225. const genreId = genreMap.get(g);
  226. genreRec = genreArr[genreId];
  227. } else {
  228. genreRec = {id: genreArr.length, value: g, authorId: new Set()};
  229. genreArr.push(genreRec);
  230. genreMap.set(g, genreRec.id);
  231. }
  232. for (const id of authorIds) {
  233. genreRec.authorId.add(id);
  234. }
  235. }
  236. //языки
  237. parseField(rec.lang, langMap, langArr, authorIds);
  238. };
  239. callback({job: 'search tables create', jobMessage: 'Создание поисковых таблиц', jobStep: 4, progress: 0});
  240. //парсинг 2, теперь можно создавать остальные поисковые таблицы
  241. let proc = 0;
  242. while (1) {// eslint-disable-line
  243. const rows = await db.select({
  244. table: 'author_book',
  245. where: `
  246. let iter = @getItem('parse_book');
  247. if (!iter) {
  248. iter = @all();
  249. @setItem('parse_book', iter);
  250. }
  251. const ids = new Set();
  252. let id = iter.next();
  253. while (!id.done) {
  254. ids.add(id.value);
  255. if (ids.size >= 10000)
  256. break;
  257. id = iter.next();
  258. }
  259. return ids;
  260. `
  261. });
  262. if (rows.length) {
  263. for (const row of rows) {
  264. const books = JSON.parse(row.books);
  265. for (const rec of books)
  266. parseBookRec(rec);
  267. }
  268. proc += rows.length;
  269. callback({progress: proc/authorArr.length});
  270. } else
  271. break;
  272. await utils.sleep(100);
  273. if (config.lowMemoryMode) {
  274. utils.freeMemory();
  275. await db.freeMemory();
  276. }
  277. }
  278. //чистка памяти, ибо жрет как не в себя
  279. authorMap = null;
  280. seriesMap = null;
  281. titleMap = null;
  282. genreMap = null;
  283. utils.freeMemory();
  284. //config
  285. callback({job: 'config save', jobMessage: 'Сохранение конфигурации', jobStep: 5, progress: 0});
  286. await db.create({
  287. table: 'config'
  288. });
  289. const stats = {
  290. recsLoaded,
  291. authorCount,
  292. authorCountAll: authorArr.length,
  293. bookCount,
  294. bookCountAll: bookCount + bookDelCount,
  295. bookDelCount,
  296. noAuthorBookCount,
  297. titleCount: titleArr.length,
  298. seriesCount: seriesArr.length,
  299. genreCount: genreArr.length,
  300. langCount: langArr.length,
  301. };
  302. //console.log(stats);
  303. const inpxHash = await utils.getFileHash(config.inpxFile, 'sha256', 'hex');
  304. await db.insert({table: 'config', rows: [
  305. {id: 'inpxInfo', value: parser.info},
  306. {id: 'stats', value: stats},
  307. {id: 'inpxHash', value: inpxHash},
  308. ]});
  309. //сохраним поисковые таблицы
  310. const chunkSize = 10000;
  311. const saveTable = async(table, arr, nullArr, authorIdToArray = true) => {
  312. arr.sort((a, b) => a.value.localeCompare(b.value));
  313. await db.create({
  314. table,
  315. index: {field: 'value', unique: true, depth: 1000000},
  316. });
  317. //вставка в БД по кусочкам, экономим память
  318. for (let i = 0; i < arr.length; i += chunkSize) {
  319. const chunk = arr.slice(i, i + chunkSize);
  320. if (authorIdToArray) {
  321. for (const rec of chunk)
  322. rec.authorId = Array.from(rec.authorId);
  323. }
  324. await db.insert({table, rows: chunk});
  325. if (i % 5 == 0) {
  326. await db.freeMemory();
  327. await utils.sleep(100);
  328. }
  329. callback({progress: i/arr.length});
  330. }
  331. nullArr();
  332. await db.close({table});
  333. utils.freeMemory();
  334. await db.freeMemory();
  335. };
  336. //author
  337. callback({job: 'author save', jobMessage: 'Сохранение индекса авторов', jobStep: 6, progress: 0});
  338. await saveTable('author', authorArr, () => {authorArr = null}, false);
  339. //series
  340. callback({job: 'series save', jobMessage: 'Сохранение индекса серий', jobStep: 7, progress: 0});
  341. await saveTable('series', seriesArr, () => {seriesArr = null});
  342. //title
  343. callback({job: 'title save', jobMessage: 'Сохранение индекса названий', jobStep: 8, progress: 0});
  344. await saveTable('title', titleArr, () => {titleArr = null});
  345. //genre
  346. callback({job: 'genre save', jobMessage: 'Сохранение индекса жанров', jobStep: 9, progress: 0});
  347. await saveTable('genre', genreArr, () => {genreArr = null});
  348. //lang
  349. callback({job: 'lang save', jobMessage: 'Сохранение индекса языков', jobStep: 10, progress: 0});
  350. await saveTable('lang', langArr, () => {langArr = null});
  351. //кэш-таблицы запросов
  352. await db.create({table: 'query_cache'});
  353. await db.create({table: 'query_time'});
  354. callback({job: 'done', jobMessage: ''});
  355. }
  356. }
  357. module.exports = DbCreator;