DbCreator.js 12 KB

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