DbSearcher.js 34 KB

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