DbCreator.js 13 KB

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