DbSearcher.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  1. const fs = require('fs-extra');
  2. //const _ = require('lodash');
  3. const utils = require('./utils');
  4. const maxLimit = 1000;
  5. const emptyFieldValue = '?';
  6. const maxUtf8Char = String.fromCodePoint(0xFFFFF);
  7. const ruAlphabet = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя';
  8. const enAlphabet = 'abcdefghijklmnopqrstuvwxyz';
  9. const enruArr = (ruAlphabet + enAlphabet).split('');
  10. class DbSearcher {
  11. constructor(config, db) {
  12. this.config = config;
  13. this.queryCacheMemSize = this.config.queryCacheMemSize;
  14. this.queryCacheDiskSize = this.config.queryCacheDiskSize;
  15. this.queryCacheEnabled = this.config.queryCacheEnabled
  16. && (this.queryCacheMemSize > 0 || this.queryCacheDiskSize > 0);
  17. this.db = db;
  18. this.searchFlag = 0;
  19. this.timer = null;
  20. this.closed = false;
  21. this.memCache = new Map();
  22. this.bookIdMap = {};
  23. this.periodicCleanCache();//no await
  24. }
  25. async init() {
  26. await this.fillBookIdMap('author');
  27. await this.fillBookIdMap('series');
  28. await this.fillBookIdMap('title');
  29. await this.fillDbConfig();
  30. }
  31. queryKey(q) {
  32. const result = [];
  33. for (const f of this.recStruct) {
  34. result.push(q[f.field]);
  35. }
  36. return JSON.stringify(result);
  37. }
  38. getWhere(a) {
  39. const db = this.db;
  40. if (a[0] !== '~')
  41. a = a.toLowerCase();
  42. let where;
  43. //особая обработка префиксов
  44. if (a[0] === '=') {
  45. a = a.substring(1);
  46. where = `@dirtyIndexLR('value', ${db.esc(a)}, ${db.esc(a)})`;
  47. } else if (a[0] === '%') {
  48. a = a.substring(1);
  49. const ands = [];
  50. const words = a.split(' ').filter(a => a);
  51. if (!words.length)
  52. words.push('');
  53. for (const w of words)
  54. ands.push(`v.indexOf(${db.esc(w)}) >= 0`);
  55. where = `@indexIter('value', (v) => (v !== ${db.esc(emptyFieldValue)} && (${ands.join('&&')})) )`;
  56. } else if (a[0] === '*') {
  57. a = a.substring(1);
  58. where = `@indexIter('value', (v) => (v !== ${db.esc(emptyFieldValue)} && v.indexOf(${db.esc(a)}) >= 0) )`;
  59. } else if (a[0] === '#') {
  60. a = a.substring(1);
  61. where = `@indexIter('value', (v) => {
  62. const enru = new Set(${db.esc(enruArr)});
  63. if (!v)
  64. return false;
  65. return v !== ${db.esc(emptyFieldValue)} && !enru.has(v[0]) && v.indexOf(${db.esc(a)}) >= 0;
  66. })`;
  67. } else if (a[0] === '~') {//RegExp
  68. a = a.substring(1);
  69. where = `
  70. await (async() => {
  71. const re = new RegExp(${db.esc(a)}, 'i');
  72. @@indexIter('value', (v) => re.test(v) );
  73. })()
  74. `;
  75. } else {
  76. where = `@dirtyIndexLR('value', ${db.esc(a)}, ${db.esc(a + maxUtf8Char)})`;
  77. }
  78. return where;
  79. }
  80. async selectBookIds(query) {
  81. const db = this.db;
  82. const idsArr = [];
  83. const tableBookIds = async(table, where) => {
  84. const rows = await db.select({
  85. table,
  86. rawResult: true,
  87. where: `
  88. const ids = ${where};
  89. const result = new Set();
  90. for (const id of ids) {
  91. const row = @unsafeRow(id);
  92. for (const bookId of row.bookIds)
  93. result.add(bookId);
  94. }
  95. return new Uint32Array(result);
  96. `
  97. });
  98. return rows[0].rawResult;
  99. };
  100. //авторы
  101. if (query.author) {
  102. const key = `book-ids-author-${query.author}`;
  103. let ids = await this.getCached(key);
  104. if (ids === null) {
  105. ids = await tableBookIds('author', this.getWhere(query.author));
  106. await this.putCached(key, ids);
  107. }
  108. idsArr.push(ids);
  109. }
  110. //серии
  111. if (query.series) {
  112. const key = `book-ids-series-${query.series}`;
  113. let ids = await this.getCached(key);
  114. if (ids === null) {
  115. ids = await tableBookIds('series', this.getWhere(query.series));
  116. await this.putCached(key, ids);
  117. }
  118. idsArr.push(ids);
  119. }
  120. //названия
  121. if (query.title) {
  122. const key = `book-ids-title-${query.title}`;
  123. let ids = await this.getCached(key);
  124. if (ids === null) {
  125. ids = await tableBookIds('title', this.getWhere(query.title));
  126. await this.putCached(key, ids);
  127. }
  128. idsArr.push(ids);
  129. }
  130. //жанры
  131. if (query.genre) {
  132. const key = `book-ids-genre-${query.genre}`;
  133. let ids = await this.getCached(key);
  134. if (ids === null) {
  135. const genreRows = await db.select({
  136. table: 'genre',
  137. rawResult: true,
  138. where: `
  139. const genres = ${db.esc(query.genre.split(','))};
  140. const ids = new Set();
  141. for (const g of genres) {
  142. for (const id of @indexLR('value', g, g))
  143. ids.add(id);
  144. }
  145. const result = new Set();
  146. for (const id of ids) {
  147. const row = @unsafeRow(id);
  148. for (const bookId of row.bookIds)
  149. result.add(bookId);
  150. }
  151. return new Uint32Array(result);
  152. `
  153. });
  154. ids = genreRows[0].rawResult;
  155. await this.putCached(key, ids);
  156. }
  157. idsArr.push(ids);
  158. }
  159. //языки
  160. if (query.lang) {
  161. const key = `book-ids-lang-${query.lang}`;
  162. let ids = await this.getCached(key);
  163. if (ids === null) {
  164. const langRows = await db.select({
  165. table: 'lang',
  166. rawResult: true,
  167. where: `
  168. const langs = ${db.esc(query.lang.split(','))};
  169. const ids = new Set();
  170. for (const l of langs) {
  171. for (const id of @indexLR('value', l, l))
  172. ids.add(id);
  173. }
  174. const result = new Set();
  175. for (const id of ids) {
  176. const row = @unsafeRow(id);
  177. for (const bookId of row.bookIds)
  178. result.add(bookId);
  179. }
  180. return new Uint32Array(result);
  181. `
  182. });
  183. ids = langRows[0].rawResult;
  184. await this.putCached(key, ids);
  185. }
  186. idsArr.push(ids);
  187. }
  188. //удаленные
  189. if (query.del) {
  190. const del = parseInt(query.del, 10) || 0;
  191. const key = `book-ids-del-${del}`;
  192. let ids = await this.getCached(key);
  193. if (ids === null) {
  194. ids = await tableBookIds('del', `@indexLR('value', ${db.esc(del)}, ${db.esc(del)})`);
  195. await this.putCached(key, ids);
  196. }
  197. idsArr.push(ids);
  198. }
  199. //дата поступления
  200. if (query.date) {
  201. const key = `book-ids-date-${query.date}`;
  202. let ids = await this.getCached(key);
  203. if (ids === null) {
  204. let [from = '', to = ''] = query.date.split(',');
  205. ids = await tableBookIds('date', `@indexLR('value', ${db.esc(from)} || undefined, ${db.esc(to)} || undefined)`);
  206. await this.putCached(key, ids);
  207. }
  208. idsArr.push(ids);
  209. }
  210. //оценка
  211. if (query.librate) {
  212. const key = `book-ids-librate-${query.librate}`;
  213. let ids = await this.getCached(key);
  214. if (ids === null) {
  215. const dateRows = await db.select({
  216. table: 'librate',
  217. rawResult: true,
  218. where: `
  219. const rates = ${db.esc(query.librate.split(',').map(n => parseInt(n, 10)).filter(n => !isNaN(n)))};
  220. const ids = new Set();
  221. for (const rate of rates) {
  222. for (const id of @indexLR('value', rate, rate))
  223. ids.add(id);
  224. }
  225. const result = new Set();
  226. for (const id of ids) {
  227. const row = @unsafeRow(id);
  228. for (const bookId of row.bookIds)
  229. result.add(bookId);
  230. }
  231. return new Uint32Array(result);
  232. `
  233. });
  234. ids = dateRows[0].rawResult;
  235. await this.putCached(key, ids);
  236. }
  237. idsArr.push(ids);
  238. }
  239. //тип файла
  240. if (query.ext) {
  241. const key = `book-ids-ext-${query.ext}`;
  242. let ids = await this.getCached(key);
  243. if (ids === null) {
  244. const extRows = await db.select({
  245. table: 'ext',
  246. rawResult: true,
  247. where: `
  248. const exts = ${db.esc(query.ext.split('|'))};
  249. const ids = new Set();
  250. for (const l of exts) {
  251. for (const id of @indexLR('value', l, l))
  252. ids.add(id);
  253. }
  254. const result = new Set();
  255. for (const id of ids) {
  256. const row = @unsafeRow(id);
  257. for (const bookId of row.bookIds)
  258. result.add(bookId);
  259. }
  260. return new Uint32Array(result);
  261. `
  262. });
  263. ids = extRows[0].rawResult;
  264. await this.putCached(key, ids);
  265. }
  266. idsArr.push(ids);
  267. }
  268. if (idsArr.length > 1) {
  269. //ищем пересечение множеств
  270. let proc = 0;
  271. let nextProc = 0;
  272. let inter = new Set(idsArr[0]);
  273. for (let i = 1; i < idsArr.length; i++) {
  274. const newInter = new Set();
  275. for (const id of idsArr[i]) {
  276. if (inter.has(id))
  277. newInter.add(id);
  278. //прерываемся иногда, чтобы не блокировать Event Loop
  279. proc++;
  280. if (proc >= nextProc) {
  281. nextProc += 10000;
  282. await utils.processLoop();
  283. }
  284. }
  285. inter = newInter;
  286. }
  287. return new Uint32Array(inter);
  288. } else if (idsArr.length == 1) {
  289. return idsArr[0];
  290. } else {
  291. return false;
  292. }
  293. }
  294. async fillDbConfig() {
  295. if (!this.dbConfig) {
  296. const rows = await this.db.select({table: 'config'});
  297. const config = {};
  298. for (const row of rows) {
  299. config[row.id] = row.value;
  300. }
  301. this.dbConfig = config;
  302. this.recStruct = config.inpxInfo.recStruct;
  303. }
  304. }
  305. async fillBookIdMap(from) {
  306. const data = await fs.readFile(`${this.config.dataDir}/db/${from}_id.map`, 'utf-8');
  307. const idMap = JSON.parse(data);
  308. idMap.arr = new Uint32Array(idMap.arr);
  309. idMap.map = new Map(idMap.map);
  310. this.bookIdMap[from] = idMap;
  311. }
  312. async tableIdsFilter(from, query) {
  313. //т.к. авторы у книги идут списком (т.е. одна книга относиться сразу к нескольким авторам),
  314. //то в выборку по bookId могут попасть авторы, которые отсутствуют в критерии query.author,
  315. //поэтому дополнительно фильтруем
  316. let result = null;
  317. if (from == 'author' && query.author) {
  318. const key = `filter-ids-author-${query.author}`;
  319. let authorIds = await this.getCached(key);
  320. if (authorIds === null) {
  321. const rows = await this.db.select({
  322. table: 'author',
  323. rawResult: true,
  324. where: `return new Uint32Array(${this.getWhere(query.author)})`
  325. });
  326. authorIds = rows[0].rawResult;
  327. await this.putCached(key, authorIds);
  328. }
  329. result = new Set(authorIds);
  330. }
  331. return result;
  332. }
  333. async selectTableIds(from, query) {
  334. const db = this.db;
  335. const queryKey = this.queryKey(query);
  336. const tableKey = `${from}-table-ids-${queryKey}`;
  337. let tableIds = await this.getCached(tableKey);
  338. if (tableIds === null) {
  339. const bookKey = `book-ids-${queryKey}`;
  340. let bookIds = await this.getCached(bookKey);
  341. if (bookIds === null) {
  342. bookIds = await this.selectBookIds(query);
  343. await this.putCached(bookKey, bookIds);
  344. }
  345. //id книг (bookIds) нашли, теперь надо их смаппировать в id таблицы from (авторов, серий, названий)
  346. if (bookIds) {
  347. //т.к. авторы у книги идут списком, то дополнительно фильтруем
  348. const filter = await this.tableIdsFilter(from, query);
  349. const tableIdsSet = new Set();
  350. const idMap = this.bookIdMap[from];
  351. let proc = 0;
  352. let nextProc = 0;
  353. for (const bookId of bookIds) {
  354. const tableId = idMap.arr[bookId];
  355. if (tableId) {
  356. if (!filter || filter.has(tableId))
  357. tableIdsSet.add(tableId);
  358. proc++;
  359. } else {
  360. const tableIdArr = idMap.map.get(bookId);
  361. if (tableIdArr) {
  362. for (const tableId of tableIdArr) {
  363. if (!filter || filter.has(tableId))
  364. tableIdsSet.add(tableId);
  365. proc++;
  366. }
  367. }
  368. }
  369. //прерываемся иногда, чтобы не блокировать Event Loop
  370. if (proc >= nextProc) {
  371. nextProc += 10000;
  372. await utils.processLoop();
  373. }
  374. }
  375. tableIds = new Uint32Array(tableIdsSet);
  376. } else {//bookIds пустой - критерии не заданы, значит берем все id из from
  377. const rows = await db.select({
  378. table: from,
  379. rawResult: true,
  380. where: `return new Uint32Array(@all())`
  381. });
  382. tableIds = rows[0].rawResult;
  383. }
  384. //сортируем по id
  385. //порядок id соответствует ASC-сортировке по строковому значению из from (имя автора, назание серии, название книги)
  386. tableIds.sort((a, b) => a - b);
  387. await this.putCached(tableKey, tableIds);
  388. }
  389. return tableIds;
  390. }
  391. async restoreBooks(from, ids) {
  392. const db = this.db;
  393. const bookTable = `${from}_book`;
  394. const rows = await db.select({
  395. table: bookTable,
  396. where: `@@id(${db.esc(ids)})`
  397. });
  398. if (rows.length == ids.length)
  399. return rows;
  400. //далее восстановим книги из book в <from>_book
  401. const idsSet = new Set(rows.map(r => r.id));
  402. //недостающие
  403. const tableIds = [];
  404. for (const id of ids) {
  405. if (!idsSet.has(id))
  406. tableIds.push(id);
  407. }
  408. const tableRows = await db.select({
  409. table: from,
  410. where: `@@id(${db.esc(tableIds)})`
  411. });
  412. //список недостающих bookId
  413. const bookIds = [];
  414. for (const row of tableRows) {
  415. for (const bookId of row.bookIds)
  416. bookIds.push(bookId);
  417. }
  418. //выбираем книги
  419. const books = await db.select({
  420. table: 'book',
  421. where: `@@id(${db.esc(bookIds)})`
  422. });
  423. const booksMap = new Map();
  424. for (const book of books)
  425. booksMap.set(book.id, book);
  426. //распределяем
  427. for (const row of tableRows) {
  428. const books = [];
  429. for (const bookId of row.bookIds) {
  430. const book = booksMap.get(bookId);
  431. if (book)
  432. books.push(book);
  433. }
  434. rows.push({id: row.id, name: row.name, books});
  435. }
  436. await db.insert({table: bookTable, ignore: true, rows});
  437. return rows;
  438. }
  439. async search(from, query) {
  440. if (this.closed)
  441. throw new Error('DbSearcher closed');
  442. if (!['author', 'series', 'title'].includes(from))
  443. throw new Error(`Unknown value for param 'from'`);
  444. this.searchFlag++;
  445. try {
  446. const db = this.db;
  447. const ids = await this.selectTableIds(from, query);
  448. const totalFound = ids.length;
  449. let limit = (query.limit ? query.limit : 100);
  450. limit = (limit > maxLimit ? maxLimit : limit);
  451. const offset = (query.offset ? query.offset : 0);
  452. const slice = ids.slice(offset, offset + limit);
  453. //выборка найденных значений
  454. const found = await db.select({
  455. table: from,
  456. map: `(r) => ({id: r.id, ${from}: r.name, bookCount: r.bookCount, bookDelCount: r.bookDelCount})`,
  457. where: `@@id(${db.esc(Array.from(slice))})`
  458. });
  459. //для title восстановим books
  460. if (from == 'title') {
  461. const bookIds = found.map(r => r.id);
  462. const rows = await this.restoreBooks(from, bookIds);
  463. const rowsMap = new Map();
  464. for (const row of rows)
  465. rowsMap.set(row.id, row);
  466. for (const f of found) {
  467. const b = rowsMap.get(f.id);
  468. if (b)
  469. f.books = b.books;
  470. }
  471. }
  472. return {found, totalFound};
  473. } finally {
  474. this.searchFlag--;
  475. }
  476. }
  477. async bookSearchIds(query) {
  478. const queryKey = this.queryKey(query);
  479. const bookKey = `book-search-ids-${queryKey}`;
  480. let bookIds = await this.getCached(bookKey);
  481. if (bookIds === null) {
  482. const db = this.db;
  483. const filterBySearch = (bookField, searchValue) => {
  484. searchValue = searchValue.toLowerCase();
  485. //особая обработка префиксов
  486. if (searchValue === emptyFieldValue) {
  487. return `(row.${bookField} === '' || row.${bookField}.indexOf(${db.esc(emptyFieldValue)}) === 0)`;
  488. } else if (searchValue[0] === '=') {
  489. searchValue = searchValue.substring(1);
  490. return `(row.${bookField}.toLowerCase().localeCompare(${db.esc(searchValue)}) === 0)`;
  491. } else if (searchValue[0] === '%') {
  492. searchValue = searchValue.substring(1);
  493. const ands = [];
  494. const words = searchValue.split(' ').filter(a => a);
  495. if (!words.length)
  496. words.push('');
  497. for (const w of words)
  498. ands.push(`row.${bookField}.toLowerCase().indexOf(${db.esc(w)}) >= 0`);
  499. return `(row.${bookField} && (${ands.join('&&')}))`;
  500. } else if (searchValue[0] === '*') {
  501. searchValue = searchValue.substring(1);
  502. return `(row.${bookField} && row.${bookField}.toLowerCase().indexOf(${db.esc(searchValue)}) >= 0)`;
  503. } else if (searchValue[0] === '#') {
  504. searchValue = searchValue.substring(1);
  505. return `(row.${bookField} === '' || (!enru.has(row.${bookField}.toLowerCase()[0]) && row.${bookField}.toLowerCase().indexOf(${db.esc(searchValue)}) >= 0))`;
  506. } else if (searchValue[0] === '~') {//RegExp
  507. searchValue = searchValue.substring(1);
  508. return `
  509. (() => {
  510. const re = new RegExp(${db.esc(searchValue)}, 'i');
  511. return re.test(row.${bookField});
  512. })()
  513. `;
  514. } else {
  515. return `(row.${bookField}.toLowerCase().localeCompare(${db.esc(searchValue)}) >= 0 ` +
  516. `&& row.${bookField}.toLowerCase().localeCompare(${db.esc(searchValue)} + ${db.esc(maxUtf8Char)}) <= 0)`;
  517. }
  518. };
  519. const checks = ['true'];
  520. for (const f of this.recStruct) {
  521. if (query[f.field]) {
  522. let searchValue = query[f.field];
  523. if (f.type === 'S') {
  524. checks.push(filterBySearch(f.field, searchValue));
  525. } if (f.type === 'N') {
  526. const v = searchValue.split('..');
  527. if (v.length == 1) {
  528. searchValue = parseInt(searchValue, 10);
  529. if (isNaN(searchValue))
  530. throw new Error(`Wrong query param, ${f.field}=${searchValue}`);
  531. checks.push(`row.${f.field} === ${searchValue}`);
  532. } else {
  533. const from = parseInt(v[0] || '0', 10);
  534. const to = parseInt(v[1] || '0', 10);
  535. if (isNaN(from) || isNaN(to))
  536. throw new Error(`Wrong query param, ${f.field}=${searchValue}`);
  537. checks.push(`row.${f.field} >= ${from} && row.${f.field} <= ${to}`);
  538. }
  539. }
  540. }
  541. }
  542. const rows = await db.select({
  543. table: 'book',
  544. rawResult: true,
  545. where: `
  546. const enru = new Set(${db.esc(enruArr)});
  547. const checkBook = (row) => {
  548. return ${checks.join(' && ')};
  549. };
  550. const result = [];
  551. for (const id of @all()) {
  552. const row = @unsafeRow(id);
  553. if (checkBook(row))
  554. result.push(row);
  555. }
  556. result.sort((a, b) => {
  557. let cmp = a.author.localeCompare(b.author);
  558. if (cmp === 0 && (a.series || b.series)) {
  559. cmp = (a.series && b.series ? a.series.localeCompare(b.series) : (a.series ? -1 : 1));
  560. }
  561. if (cmp === 0)
  562. cmp = a.serno - b.serno;
  563. if (cmp === 0)
  564. cmp = a.title.localeCompare(b.title);
  565. return cmp;
  566. });
  567. return new Uint32Array(result.map(row => row.id));
  568. `
  569. });
  570. bookIds = rows[0].rawResult;
  571. await this.putCached(bookKey, bookIds);
  572. }
  573. return bookIds;
  574. }
  575. //неоптимизированный поиск по всем книгам, по всем полям
  576. async bookSearch(query) {
  577. if (this.closed)
  578. throw new Error('DbSearcher closed');
  579. this.searchFlag++;
  580. try {
  581. const db = this.db;
  582. const ids = await this.bookSearchIds(query);
  583. const totalFound = ids.length;
  584. let limit = (query.limit ? query.limit : 100);
  585. limit = (limit > maxLimit ? maxLimit : limit);
  586. const offset = (query.offset ? query.offset : 0);
  587. const slice = ids.slice(offset, offset + limit);
  588. //выборка найденных значений
  589. const found = await db.select({
  590. table: 'book',
  591. where: `@@id(${db.esc(Array.from(slice))})`
  592. });
  593. return {found, totalFound};
  594. } finally {
  595. this.searchFlag--;
  596. }
  597. }
  598. async opdsQuery(from, query) {
  599. if (this.closed)
  600. throw new Error('DbSearcher closed');
  601. if (!['author', 'series', 'title'].includes(from))
  602. throw new Error(`Unknown value for param 'from'`);
  603. this.searchFlag++;
  604. try {
  605. const db = this.db;
  606. const depth = query.depth || 1;
  607. const queryKey = this.queryKey(query);
  608. const opdsKey = `${from}-opds-d${depth}-${queryKey}`;
  609. let result = await this.getCached(opdsKey);
  610. if (result === null) {
  611. const ids = await this.selectTableIds(from, query);
  612. const totalFound = ids.length;
  613. //группировка по name длиной depth
  614. const found = await db.select({
  615. table: from,
  616. rawResult: true,
  617. where: `
  618. const depth = ${db.esc(depth)};
  619. const group = new Map();
  620. const ids = ${db.esc(Array.from(ids))};
  621. for (const id of ids) {
  622. const row = @unsafeRow(id);
  623. const s = row.value.substring(0, depth);
  624. let g = group.get(s);
  625. if (!g) {
  626. g = {id: row.id, name: row.name, value: s, count: 0, bookCount: 0};
  627. group.set(s, g);
  628. }
  629. g.count++;
  630. g.bookCount += row.bookCount;
  631. }
  632. const result = Array.from(group.values());
  633. result.sort((a, b) => a.value.localeCompare(b.value));
  634. return result;
  635. `
  636. });
  637. result = {found: found[0].rawResult, totalFound};
  638. await this.putCached(opdsKey, result);
  639. }
  640. return result;
  641. } finally {
  642. this.searchFlag--;
  643. }
  644. }
  645. async getAuthorBookList(authorId, author) {
  646. if (this.closed)
  647. throw new Error('DbSearcher closed');
  648. if (!authorId && !author)
  649. return {author: '', books: []};
  650. this.searchFlag++;
  651. try {
  652. const db = this.db;
  653. if (!authorId) {
  654. //восстановим authorId
  655. authorId = 0;
  656. author = author.toLowerCase();
  657. const rows = await db.select({
  658. table: 'author',
  659. rawResult: true,
  660. where: `return Array.from(@dirtyIndexLR('value', ${db.esc(author)}, ${db.esc(author)}))`
  661. });
  662. if (rows.length && rows[0].rawResult.length)
  663. authorId = rows[0].rawResult[0];
  664. }
  665. //выборка книг автора по authorId
  666. const rows = await this.restoreBooks('author', [authorId]);
  667. let authorName = '';
  668. let books = [];
  669. if (rows.length) {
  670. authorName = rows[0].name;
  671. books = rows[0].books;
  672. }
  673. return {author: authorName, books};
  674. } finally {
  675. this.searchFlag--;
  676. }
  677. }
  678. async getAuthorSeriesList(authorId) {
  679. if (this.closed)
  680. throw new Error('DbSearcher closed');
  681. if (!authorId)
  682. return {author: '', series: []};
  683. this.searchFlag++;
  684. try {
  685. const db = this.db;
  686. //выборка книг автора по authorId
  687. const bookList = await this.getAuthorBookList(authorId);
  688. const books = bookList.books;
  689. const seriesSet = new Set();
  690. for (const book of books) {
  691. if (book.series)
  692. seriesSet.add(book.series.toLowerCase());
  693. }
  694. let series = [];
  695. if (seriesSet.size) {
  696. //выборка серий по названиям
  697. series = await db.select({
  698. table: 'series',
  699. map: `(r) => ({id: r.id, series: r.name, bookCount: r.bookCount, bookDelCount: r.bookDelCount})`,
  700. where: `
  701. const seriesArr = ${db.esc(Array.from(seriesSet))};
  702. const ids = new Set();
  703. for (const value of seriesArr) {
  704. for (const id of @dirtyIndexLR('value', value, value))
  705. ids.add(id);
  706. }
  707. return ids;
  708. `
  709. });
  710. }
  711. return {author: bookList.author, series};
  712. } finally {
  713. this.searchFlag--;
  714. }
  715. }
  716. async getSeriesBookList(series) {
  717. if (this.closed)
  718. throw new Error('DbSearcher closed');
  719. if (!series)
  720. return {books: []};
  721. this.searchFlag++;
  722. try {
  723. const db = this.db;
  724. series = series.toLowerCase();
  725. //выборка серии по названию серии
  726. let rows = await db.select({
  727. table: 'series',
  728. rawResult: true,
  729. where: `return Array.from(@dirtyIndexLR('value', ${db.esc(series)}, ${db.esc(series)}))`
  730. });
  731. let books = [];
  732. if (rows.length && rows[0].rawResult.length) {
  733. //выборка книг серии
  734. const bookRows = await this.restoreBooks('series', [rows[0].rawResult[0]])
  735. if (bookRows.length)
  736. books = bookRows[0].books;
  737. }
  738. return {books};
  739. } finally {
  740. this.searchFlag--;
  741. }
  742. }
  743. async getCached(key) {
  744. if (!this.queryCacheEnabled)
  745. return null;
  746. let result = null;
  747. const db = this.db;
  748. const memCache = this.memCache;
  749. if (this.queryCacheMemSize > 0 && memCache.has(key)) {//есть в недавних
  750. result = memCache.get(key);
  751. //изменим порядок ключей, для последующей правильной чистки старых
  752. memCache.delete(key);
  753. memCache.set(key, result);
  754. } else if (this.queryCacheDiskSize > 0) {//смотрим в таблице
  755. const rows = await db.select({table: 'query_cache', where: `@@id(${db.esc(key)})`});
  756. if (rows.length) {//нашли в кеше
  757. await db.insert({
  758. table: 'query_time',
  759. replace: true,
  760. rows: [{id: key, time: Date.now()}],
  761. });
  762. result = rows[0].value;
  763. //заполняем кеш в памяти
  764. if (this.queryCacheMemSize > 0) {
  765. memCache.set(key, result);
  766. if (memCache.size > this.queryCacheMemSize) {
  767. //удаляем самый старый ключ-значение
  768. for (const k of memCache.keys()) {
  769. memCache.delete(k);
  770. break;
  771. }
  772. }
  773. }
  774. }
  775. }
  776. return result;
  777. }
  778. async putCached(key, value) {
  779. if (!this.queryCacheEnabled)
  780. return;
  781. const db = this.db;
  782. if (this.queryCacheMemSize > 0) {
  783. const memCache = this.memCache;
  784. memCache.set(key, value);
  785. if (memCache.size > this.queryCacheMemSize) {
  786. //удаляем самый старый ключ-значение
  787. for (const k of memCache.keys()) {
  788. memCache.delete(k);
  789. break;
  790. }
  791. }
  792. }
  793. if (this.queryCacheDiskSize > 0) {
  794. //кладем в таблицу асинхронно
  795. (async() => {
  796. try {
  797. await db.insert({
  798. table: 'query_cache',
  799. replace: true,
  800. rows: [{id: key, value}],
  801. });
  802. await db.insert({
  803. table: 'query_time',
  804. replace: true,
  805. rows: [{id: key, time: Date.now()}],
  806. });
  807. } catch(e) {
  808. console.error(`putCached: ${e.message}`);
  809. }
  810. })();
  811. }
  812. }
  813. async periodicCleanCache() {
  814. this.timer = null;
  815. const cleanInterval = this.config.cacheCleanInterval*60*1000;
  816. if (!cleanInterval)
  817. return;
  818. try {
  819. if (!this.queryCacheEnabled || this.queryCacheDiskSize <= 0)
  820. return;
  821. const db = this.db;
  822. let rows = await db.select({table: 'query_time', count: true});
  823. const delCount = rows[0].count - this.queryCacheDiskSize;
  824. if (delCount < 1)
  825. return;
  826. //выберем delCount кандидатов на удаление
  827. rows = await db.select({
  828. table: 'query_time',
  829. rawResult: true,
  830. where: `
  831. const delCount = ${delCount};
  832. const rows = [];
  833. @unsafeIter(@all(), (r) => {
  834. rows.push(r);
  835. return false;
  836. });
  837. rows.sort((a, b) => a.time - b.time);
  838. return rows.slice(0, delCount).map(r => r.id);
  839. `
  840. });
  841. const ids = rows[0].rawResult;
  842. //удаляем
  843. await db.delete({table: 'query_cache', where: `@@id(${db.esc(ids)})`});
  844. await db.delete({table: 'query_time', where: `@@id(${db.esc(ids)})`});
  845. //console.log('Cache clean', ids);
  846. } catch(e) {
  847. console.error(e.message);
  848. } finally {
  849. if (!this.closed) {
  850. this.timer = setTimeout(() => { this.periodicCleanCache(); }, cleanInterval);
  851. }
  852. }
  853. }
  854. async close() {
  855. while (this.searchFlag > 0) {
  856. await utils.sleep(50);
  857. }
  858. this.searchCache = null;
  859. if (this.timer) {
  860. clearTimeout(this.timer);
  861. this.timer = null;
  862. }
  863. this.closed = true;
  864. }
  865. }
  866. module.exports = DbSearcher;