DbSearcher.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  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)}, 'i');
  62. @@indexIter('value', (v) => re.test(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. //тип файла
  230. if (query.ext) {
  231. const key = `book-ids-ext-${query.ext}`;
  232. let ids = await this.getCached(key);
  233. if (ids === null) {
  234. const extRows = await db.select({
  235. table: 'ext',
  236. rawResult: true,
  237. where: `
  238. const exts = ${db.esc(query.ext.split(','))};
  239. const ids = new Set();
  240. for (const l of exts) {
  241. for (const id of @indexLR('value', l, l))
  242. ids.add(id);
  243. }
  244. const result = new Set();
  245. for (const id of ids) {
  246. const row = @unsafeRow(id);
  247. for (const bookId of row.bookIds)
  248. result.add(bookId);
  249. }
  250. return new Uint32Array(result);
  251. `
  252. });
  253. ids = extRows[0].rawResult;
  254. await this.putCached(key, ids);
  255. }
  256. idsArr.push(ids);
  257. }
  258. if (idsArr.length > 1) {
  259. //ищем пересечение множеств
  260. let proc = 0;
  261. let nextProc = 0;
  262. let inter = new Set(idsArr[0]);
  263. for (let i = 1; i < idsArr.length; i++) {
  264. const newInter = new Set();
  265. for (const id of idsArr[i]) {
  266. if (inter.has(id))
  267. newInter.add(id);
  268. //прерываемся иногда, чтобы не блокировать Event Loop
  269. proc++;
  270. if (proc >= nextProc) {
  271. nextProc += 10000;
  272. await utils.processLoop();
  273. }
  274. }
  275. inter = newInter;
  276. }
  277. return new Uint32Array(inter);
  278. } else if (idsArr.length == 1) {
  279. return idsArr[0];
  280. } else {
  281. return false;
  282. }
  283. }
  284. async fillDbConfig() {
  285. if (!this.dbConfig) {
  286. const rows = await this.db.select({table: 'config'});
  287. const config = {};
  288. for (const row of rows) {
  289. config[row.id] = row.value;
  290. }
  291. this.dbConfig = config;
  292. this.recStruct = config.inpxInfo.recStruct;
  293. }
  294. }
  295. async fillBookIdMap(from) {
  296. const data = await fs.readFile(`${this.config.dataDir}/db/${from}_id.map`, 'utf-8');
  297. const idMap = JSON.parse(data);
  298. idMap.arr = new Uint32Array(idMap.arr);
  299. idMap.map = new Map(idMap.map);
  300. this.bookIdMap[from] = idMap;
  301. }
  302. async tableIdsFilter(from, query) {
  303. //т.к. авторы у книги идут списком (т.е. одна книга относиться сразу к нескольким авторам),
  304. //то в выборку по bookId могут попасть авторы, которые отсутствуют в критерии query.author,
  305. //поэтому дополнительно фильтруем
  306. let result = null;
  307. if (from == 'author' && query.author) {
  308. const key = `filter-ids-author-${query.author}`;
  309. let authorIds = await this.getCached(key);
  310. if (authorIds === null) {
  311. const rows = await this.db.select({
  312. table: 'author',
  313. rawResult: true,
  314. where: `return new Uint32Array(${this.getWhere(query.author)})`
  315. });
  316. authorIds = rows[0].rawResult;
  317. await this.putCached(key, authorIds);
  318. }
  319. result = new Set(authorIds);
  320. }
  321. return result;
  322. }
  323. async selectTableIds(from, query) {
  324. const db = this.db;
  325. const queryKey = this.queryKey(query);
  326. const tableKey = `${from}-table-ids-${queryKey}`;
  327. let tableIds = await this.getCached(tableKey);
  328. if (tableIds === null) {
  329. const bookKey = `book-ids-${queryKey}`;
  330. let bookIds = await this.getCached(bookKey);
  331. if (bookIds === null) {
  332. bookIds = await this.selectBookIds(query);
  333. await this.putCached(bookKey, bookIds);
  334. }
  335. //id книг (bookIds) нашли, теперь надо их смаппировать в id таблицы from (авторов, серий, названий)
  336. if (bookIds) {
  337. //т.к. авторы у книги идут списком, то дополнительно фильтруем
  338. const filter = await this.tableIdsFilter(from, query);
  339. const tableIdsSet = new Set();
  340. const idMap = this.bookIdMap[from];
  341. let proc = 0;
  342. let nextProc = 0;
  343. for (const bookId of bookIds) {
  344. const tableId = idMap.arr[bookId];
  345. if (tableId) {
  346. if (!filter || filter.has(tableId))
  347. tableIdsSet.add(tableId);
  348. proc++;
  349. } else {
  350. const tableIdArr = idMap.map.get(bookId);
  351. if (tableIdArr) {
  352. for (const tableId of tableIdArr) {
  353. if (!filter || filter.has(tableId))
  354. tableIdsSet.add(tableId);
  355. proc++;
  356. }
  357. }
  358. }
  359. //прерываемся иногда, чтобы не блокировать Event Loop
  360. if (proc >= nextProc) {
  361. nextProc += 10000;
  362. await utils.processLoop();
  363. }
  364. }
  365. tableIds = new Uint32Array(tableIdsSet);
  366. } else {//bookIds пустой - критерии не заданы, значит берем все id из from
  367. const rows = await db.select({
  368. table: from,
  369. rawResult: true,
  370. where: `return new Uint32Array(@all())`
  371. });
  372. tableIds = rows[0].rawResult;
  373. }
  374. //сортируем по id
  375. //порядок id соответствует ASC-сортировке по строковому значению из from (имя автора, назание серии, название книги)
  376. tableIds.sort((a, b) => a - b);
  377. await this.putCached(tableKey, tableIds);
  378. }
  379. return tableIds;
  380. }
  381. async restoreBooks(from, ids) {
  382. const db = this.db;
  383. const bookTable = `${from}_book`;
  384. const rows = await db.select({
  385. table: bookTable,
  386. where: `@@id(${db.esc(ids)})`
  387. });
  388. if (rows.length == ids.length)
  389. return rows;
  390. //далее восстановим книги из book в <from>_book
  391. const idsSet = new Set(rows.map(r => r.id));
  392. //недостающие
  393. const tableIds = [];
  394. for (const id of ids) {
  395. if (!idsSet.has(id))
  396. tableIds.push(id);
  397. }
  398. const tableRows = await db.select({
  399. table: from,
  400. where: `@@id(${db.esc(tableIds)})`
  401. });
  402. //список недостающих bookId
  403. const bookIds = [];
  404. for (const row of tableRows) {
  405. for (const bookId of row.bookIds)
  406. bookIds.push(bookId);
  407. }
  408. //выбираем книги
  409. const books = await db.select({
  410. table: 'book',
  411. where: `@@id(${db.esc(bookIds)})`
  412. });
  413. const booksMap = new Map();
  414. for (const book of books)
  415. booksMap.set(book.id, book);
  416. //распределяем
  417. for (const row of tableRows) {
  418. const books = [];
  419. for (const bookId of row.bookIds) {
  420. const book = booksMap.get(bookId);
  421. if (book)
  422. books.push(book);
  423. }
  424. rows.push({id: row.id, name: row.name, books});
  425. }
  426. await db.insert({table: bookTable, ignore: true, rows});
  427. return rows;
  428. }
  429. async search(from, query) {
  430. if (this.closed)
  431. throw new Error('DbSearcher closed');
  432. if (!['author', 'series', 'title'].includes(from))
  433. throw new Error(`Unknown value for param 'from'`);
  434. this.searchFlag++;
  435. try {
  436. const db = this.db;
  437. const ids = await this.selectTableIds(from, query);
  438. const totalFound = ids.length;
  439. let limit = (query.limit ? query.limit : 100);
  440. limit = (limit > maxLimit ? maxLimit : limit);
  441. const offset = (query.offset ? query.offset : 0);
  442. const slice = ids.slice(offset, offset + limit);
  443. //выборка найденных значений
  444. const found = await db.select({
  445. table: from,
  446. map: `(r) => ({id: r.id, ${from}: r.name, bookCount: r.bookCount, bookDelCount: r.bookDelCount})`,
  447. where: `@@id(${db.esc(Array.from(slice))})`
  448. });
  449. //для title восстановим books
  450. if (from == 'title') {
  451. const bookIds = found.map(r => r.id);
  452. const rows = await this.restoreBooks(from, bookIds);
  453. const rowsMap = new Map();
  454. for (const row of rows)
  455. rowsMap.set(row.id, row);
  456. for (const f of found) {
  457. const b = rowsMap.get(f.id);
  458. if (b)
  459. f.books = b.books;
  460. }
  461. }
  462. return {found, totalFound};
  463. } finally {
  464. this.searchFlag--;
  465. }
  466. }
  467. async bookSearchIds(query) {
  468. const queryKey = this.queryKey(query);
  469. const bookKey = `book-search-ids-${queryKey}`;
  470. let bookIds = await this.getCached(bookKey);
  471. if (bookIds === null) {
  472. const db = this.db;
  473. const filterBySearch = (bookField, searchValue) => {
  474. searchValue = searchValue.toLowerCase();
  475. //особая обработка префиксов
  476. if (searchValue == emptyFieldValue) {
  477. return `(row.${bookField} === '' || row.${bookField}.indexOf(${db.esc(emptyFieldValue)}) === 0)`;
  478. } else if (searchValue[0] == '=') {
  479. searchValue = searchValue.substring(1);
  480. return `(row.${bookField}.toLowerCase().localeCompare(${db.esc(searchValue)}) === 0)`;
  481. } else if (searchValue[0] == '*') {
  482. searchValue = searchValue.substring(1);
  483. return `(row.${bookField} && row.${bookField}.toLowerCase().indexOf(${db.esc(searchValue)}) >= 0)`;
  484. } else if (searchValue[0] == '#') {
  485. searchValue = searchValue.substring(1);
  486. return `(row.${bookField} === '' || (!enru.has(row.${bookField}.toLowerCase()[0]) && row.${bookField}.toLowerCase().indexOf(${db.esc(searchValue)}) >= 0))`;
  487. } else if (searchValue[0] == '~') {//RegExp
  488. searchValue = searchValue.substring(1);
  489. return `
  490. (() => {
  491. const re = new RegExp(${db.esc(searchValue)}, 'i');
  492. return re.test(row.${bookField});
  493. })()
  494. `;
  495. } else {
  496. return `(row.${bookField}.toLowerCase().localeCompare(${db.esc(searchValue)}) >= 0 ` +
  497. `&& row.${bookField}.toLowerCase().localeCompare(${db.esc(searchValue)} + ${db.esc(maxUtf8Char)}) <= 0)`;
  498. }
  499. };
  500. const checks = ['true'];
  501. for (const f of this.recStruct) {
  502. if (query[f.field]) {
  503. let searchValue = query[f.field];
  504. if (f.type === 'S') {
  505. checks.push(filterBySearch(f.field, searchValue));
  506. } if (f.type === 'N') {
  507. const v = searchValue.split('..');
  508. if (v.length == 1) {
  509. searchValue = parseInt(searchValue, 10);
  510. if (isNaN(searchValue))
  511. throw new Error(`Wrong query param, ${f.field}=${searchValue}`);
  512. checks.push(`row.${f.field} === ${searchValue}`);
  513. } else {
  514. const from = parseInt(v[0] || '0', 10);
  515. const to = parseInt(v[1] || '0', 10);
  516. if (isNaN(from) || isNaN(to))
  517. throw new Error(`Wrong query param, ${f.field}=${searchValue}`);
  518. checks.push(`row.${f.field} >= ${from} && row.${f.field} <= ${to}`);
  519. }
  520. }
  521. }
  522. }
  523. const rows = await db.select({
  524. table: 'book',
  525. rawResult: true,
  526. where: `
  527. const enru = new Set(${db.esc(enruArr)});
  528. const checkBook = (row) => {
  529. return ${checks.join(' && ')};
  530. };
  531. const result = [];
  532. for (const id of @all()) {
  533. const row = @unsafeRow(id);
  534. if (checkBook(row))
  535. result.push(row);
  536. }
  537. result.sort((a, b) => {
  538. let cmp = a.author.localeCompare(b.author);
  539. if (cmp === 0 && (a.series || b.series)) {
  540. cmp = (a.series && b.series ? a.series.localeCompare(b.series) : (a.series ? -1 : 1));
  541. }
  542. if (cmp === 0)
  543. cmp = a.serno - b.serno;
  544. if (cmp === 0)
  545. cmp = a.title.localeCompare(b.title);
  546. return cmp;
  547. });
  548. return new Uint32Array(result.map(row => row.id));
  549. `
  550. });
  551. bookIds = rows[0].rawResult;
  552. await this.putCached(bookKey, bookIds);
  553. }
  554. return bookIds;
  555. }
  556. //неоптимизированный поиск по всем книгам, по всем полям
  557. async bookSearch(query) {
  558. if (this.closed)
  559. throw new Error('DbSearcher closed');
  560. this.searchFlag++;
  561. try {
  562. const db = this.db;
  563. const ids = await this.bookSearchIds(query);
  564. const totalFound = ids.length;
  565. let limit = (query.limit ? query.limit : 100);
  566. limit = (limit > maxLimit ? maxLimit : limit);
  567. const offset = (query.offset ? query.offset : 0);
  568. const slice = ids.slice(offset, offset + limit);
  569. //выборка найденных значений
  570. const found = await db.select({
  571. table: 'book',
  572. where: `@@id(${db.esc(Array.from(slice))})`
  573. });
  574. return {found, totalFound};
  575. } finally {
  576. this.searchFlag--;
  577. }
  578. }
  579. async opdsQuery(from, query) {
  580. if (this.closed)
  581. throw new Error('DbSearcher closed');
  582. if (!['author', 'series', 'title'].includes(from))
  583. throw new Error(`Unknown value for param 'from'`);
  584. this.searchFlag++;
  585. try {
  586. const db = this.db;
  587. const depth = query.depth || 1;
  588. const queryKey = this.queryKey(query);
  589. const opdsKey = `${from}-opds-d${depth}-${queryKey}`;
  590. let result = await this.getCached(opdsKey);
  591. if (result === null) {
  592. const ids = await this.selectTableIds(from, query);
  593. const totalFound = ids.length;
  594. //группировка по name длиной depth
  595. const found = await db.select({
  596. table: from,
  597. rawResult: true,
  598. where: `
  599. const depth = ${db.esc(depth)};
  600. const group = new Map();
  601. const ids = ${db.esc(Array.from(ids))};
  602. for (const id of ids) {
  603. const row = @unsafeRow(id);
  604. const s = row.value.substring(0, depth);
  605. let g = group.get(s);
  606. if (!g) {
  607. g = {id: row.id, name: row.name, value: s, count: 0, bookCount: 0};
  608. group.set(s, g);
  609. }
  610. g.count++;
  611. g.bookCount += row.bookCount;
  612. }
  613. const result = Array.from(group.values());
  614. result.sort((a, b) => a.value.localeCompare(b.value));
  615. return result;
  616. `
  617. });
  618. result = {found: found[0].rawResult, totalFound};
  619. await this.putCached(opdsKey, result);
  620. }
  621. return result;
  622. } finally {
  623. this.searchFlag--;
  624. }
  625. }
  626. async getAuthorBookList(authorId, author) {
  627. if (this.closed)
  628. throw new Error('DbSearcher closed');
  629. if (!authorId && !author)
  630. return {author: '', books: []};
  631. this.searchFlag++;
  632. try {
  633. const db = this.db;
  634. if (!authorId) {
  635. //восстановим authorId
  636. authorId = 0;
  637. author = author.toLowerCase();
  638. const rows = await db.select({
  639. table: 'author',
  640. rawResult: true,
  641. where: `return Array.from(@dirtyIndexLR('value', ${db.esc(author)}, ${db.esc(author)}))`
  642. });
  643. if (rows.length && rows[0].rawResult.length)
  644. authorId = rows[0].rawResult[0];
  645. }
  646. //выборка книг автора по authorId
  647. const rows = await this.restoreBooks('author', [authorId]);
  648. let authorName = '';
  649. let books = [];
  650. if (rows.length) {
  651. authorName = rows[0].name;
  652. books = rows[0].books;
  653. }
  654. return {author: authorName, books};
  655. } finally {
  656. this.searchFlag--;
  657. }
  658. }
  659. async getAuthorSeriesList(authorId) {
  660. if (this.closed)
  661. throw new Error('DbSearcher closed');
  662. if (!authorId)
  663. return {author: '', series: []};
  664. this.searchFlag++;
  665. try {
  666. const db = this.db;
  667. //выборка книг автора по authorId
  668. const bookList = await this.getAuthorBookList(authorId);
  669. const books = bookList.books;
  670. const seriesSet = new Set();
  671. for (const book of books) {
  672. if (book.series)
  673. seriesSet.add(book.series.toLowerCase());
  674. }
  675. let series = [];
  676. if (seriesSet.size) {
  677. //выборка серий по названиям
  678. series = await db.select({
  679. table: 'series',
  680. map: `(r) => ({id: r.id, series: r.name, bookCount: r.bookCount, bookDelCount: r.bookDelCount})`,
  681. where: `
  682. const seriesArr = ${db.esc(Array.from(seriesSet))};
  683. const ids = new Set();
  684. for (const value of seriesArr) {
  685. for (const id of @dirtyIndexLR('value', value, value))
  686. ids.add(id);
  687. }
  688. return ids;
  689. `
  690. });
  691. }
  692. return {author: bookList.author, series};
  693. } finally {
  694. this.searchFlag--;
  695. }
  696. }
  697. async getSeriesBookList(series) {
  698. if (this.closed)
  699. throw new Error('DbSearcher closed');
  700. if (!series)
  701. return {books: []};
  702. this.searchFlag++;
  703. try {
  704. const db = this.db;
  705. series = series.toLowerCase();
  706. //выборка серии по названию серии
  707. let rows = await db.select({
  708. table: 'series',
  709. rawResult: true,
  710. where: `return Array.from(@dirtyIndexLR('value', ${db.esc(series)}, ${db.esc(series)}))`
  711. });
  712. let books = [];
  713. if (rows.length && rows[0].rawResult.length) {
  714. //выборка книг серии
  715. const bookRows = await this.restoreBooks('series', [rows[0].rawResult[0]])
  716. if (bookRows.length)
  717. books = bookRows[0].books;
  718. }
  719. return {books};
  720. } finally {
  721. this.searchFlag--;
  722. }
  723. }
  724. async getCached(key) {
  725. if (!this.queryCacheEnabled)
  726. return null;
  727. let result = null;
  728. const db = this.db;
  729. const memCache = this.memCache;
  730. if (this.queryCacheMemSize > 0 && memCache.has(key)) {//есть в недавних
  731. result = memCache.get(key);
  732. //изменим порядок ключей, для последующей правильной чистки старых
  733. memCache.delete(key);
  734. memCache.set(key, result);
  735. } else if (this.queryCacheDiskSize > 0) {//смотрим в таблице
  736. const rows = await db.select({table: 'query_cache', where: `@@id(${db.esc(key)})`});
  737. if (rows.length) {//нашли в кеше
  738. await db.insert({
  739. table: 'query_time',
  740. replace: true,
  741. rows: [{id: key, time: Date.now()}],
  742. });
  743. result = rows[0].value;
  744. //заполняем кеш в памяти
  745. if (this.queryCacheMemSize > 0) {
  746. memCache.set(key, result);
  747. if (memCache.size > this.queryCacheMemSize) {
  748. //удаляем самый старый ключ-значение
  749. for (const k of memCache.keys()) {
  750. memCache.delete(k);
  751. break;
  752. }
  753. }
  754. }
  755. }
  756. }
  757. return result;
  758. }
  759. async putCached(key, value) {
  760. if (!this.queryCacheEnabled)
  761. return;
  762. const db = this.db;
  763. if (this.queryCacheMemSize > 0) {
  764. const memCache = this.memCache;
  765. memCache.set(key, value);
  766. if (memCache.size > this.queryCacheMemSize) {
  767. //удаляем самый старый ключ-значение
  768. for (const k of memCache.keys()) {
  769. memCache.delete(k);
  770. break;
  771. }
  772. }
  773. }
  774. if (this.queryCacheDiskSize > 0) {
  775. //кладем в таблицу асинхронно
  776. (async() => {
  777. try {
  778. await db.insert({
  779. table: 'query_cache',
  780. replace: true,
  781. rows: [{id: key, value}],
  782. });
  783. await db.insert({
  784. table: 'query_time',
  785. replace: true,
  786. rows: [{id: key, time: Date.now()}],
  787. });
  788. } catch(e) {
  789. console.error(`putCached: ${e.message}`);
  790. }
  791. })();
  792. }
  793. }
  794. async periodicCleanCache() {
  795. this.timer = null;
  796. const cleanInterval = this.config.cacheCleanInterval*60*1000;
  797. if (!cleanInterval)
  798. return;
  799. try {
  800. if (!this.queryCacheEnabled || this.queryCacheDiskSize <= 0)
  801. return;
  802. const db = this.db;
  803. let rows = await db.select({table: 'query_time', count: true});
  804. const delCount = rows[0].count - this.queryCacheDiskSize;
  805. if (delCount < 1)
  806. return;
  807. //выберем delCount кандидатов на удаление
  808. rows = await db.select({
  809. table: 'query_time',
  810. rawResult: true,
  811. where: `
  812. const delCount = ${delCount};
  813. const rows = [];
  814. @unsafeIter(@all(), (r) => {
  815. rows.push(r);
  816. return false;
  817. });
  818. rows.sort((a, b) => a.time - b.time);
  819. return rows.slice(0, delCount).map(r => r.id);
  820. `
  821. });
  822. const ids = rows[0].rawResult;
  823. //удаляем
  824. await db.delete({table: 'query_cache', where: `@@id(${db.esc(ids)})`});
  825. await db.delete({table: 'query_time', where: `@@id(${db.esc(ids)})`});
  826. //console.log('Cache clean', ids);
  827. } catch(e) {
  828. console.error(e.message);
  829. } finally {
  830. if (!this.closed) {
  831. this.timer = setTimeout(() => { this.periodicCleanCache(); }, cleanInterval);
  832. }
  833. }
  834. }
  835. async close() {
  836. while (this.searchFlag > 0) {
  837. await utils.sleep(50);
  838. }
  839. this.searchCache = null;
  840. if (this.timer) {
  841. clearTimeout(this.timer);
  842. this.timer = null;
  843. }
  844. this.closed = true;
  845. }
  846. }
  847. module.exports = DbSearcher;