DbSearcher.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. //const _ = require('lodash');
  2. const utils = require('./utils');
  3. const maxUtf8Char = String.fromCodePoint(0xFFFFF);
  4. class DbSearcher {
  5. constructor(config, db) {
  6. this.config = config;
  7. this.db = db;
  8. this.searchFlag = 0;
  9. this.timer = null;
  10. this.closed = false;
  11. this.periodicCleanCache();//no await
  12. }
  13. async selectAuthorIds(query) {
  14. const db = this.db;
  15. let authorRows;
  16. let authorIds = new Set();
  17. //сначала выберем все id авторов по фильтру
  18. //порядок id соответсвует ASC-сортировке по author
  19. if (query.author) {
  20. const a = query.author.toLowerCase(query.author);
  21. authorRows = await db.select({
  22. table: 'author',
  23. map: `(r) => ({id: r.id})`,
  24. where: `
  25. @@dirtyIndexLR('value', ${db.esc(a)}, ${db.esc(a + maxUtf8Char)})
  26. `,
  27. });
  28. for (const row of authorRows)
  29. authorIds.add(row.id);
  30. } else {//все авторы
  31. if (!db.searchCache.authorIdsAll) {
  32. authorRows = await db.select({
  33. table: 'author',
  34. map: `(r) => ({id: r.id})`,
  35. });
  36. db.searchCache.authorIdsAll = [];
  37. for (const row of authorRows) {
  38. authorIds.add(row.id);
  39. db.searchCache.authorIdsAll.push(row.id);
  40. }
  41. } else {//оптимизация
  42. for (const id of db.searchCache.authorIdsAll) {
  43. authorIds.add(id);
  44. }
  45. }
  46. }
  47. const idsArr = [];
  48. idsArr.push(authorIds);
  49. //серии
  50. //названия
  51. //жанры
  52. //языки
  53. if (idsArr.length > 1)
  54. authorIds = utils.intersectSet(idsArr);
  55. //сортировка
  56. authorIds = Array.from(authorIds);
  57. authorIds.sort((a, b) => a > b);
  58. return authorIds;
  59. }
  60. async getAuthorIds(query) {
  61. const db = this.db;
  62. if (!db.searchCache)
  63. db.searchCache = {};
  64. let result;
  65. //сначала попробуем найти в кеше
  66. const q = query;
  67. const keyArr = [q.author, q.series, q.title, q.genre, q.lang];
  68. const keyStr = keyArr.join('');
  69. if (!keyStr) {//пустой запрос
  70. if (db.searchCache.authorIdsAll)
  71. result = db.searchCache.authorIdsAll;
  72. else
  73. result = await this.selectAuthorIds(query);
  74. } else {//непустой запрос
  75. const key = JSON.stringify(keyArr);
  76. const rows = await db.select({table: 'query_cache', where: `@@id(${db.esc(key)})`});
  77. if (rows.length) {//нашли в кеше
  78. await db.insert({
  79. table: 'query_time',
  80. replace: true,
  81. rows: [{id: key, time: Date.now()}],
  82. });
  83. result = rows[0].value;
  84. } else {//не нашли в кеше, ищем в поисковых таблицах
  85. result = await this.selectAuthorIds(query);
  86. await db.insert({
  87. table: 'query_cache',
  88. replace: true,
  89. rows: [{id: key, value: result}],
  90. });
  91. await db.insert({
  92. table: 'query_time',
  93. replace: true,
  94. rows: [{id: key, time: Date.now()}],
  95. });
  96. }
  97. }
  98. return result;
  99. }
  100. async search(query) {
  101. if (this.closed)
  102. throw new Error('DbSearcher closed');
  103. this.searchFlag++;
  104. try {
  105. const db = this.db;
  106. const authorIds = await this.getAuthorIds(query);
  107. const totalFound = authorIds.length;
  108. const limit = (query.limit ? query.limit : 1000);
  109. //выборка найденных авторов
  110. let result = await db.select({
  111. table: 'author',
  112. map: `(r) => ({id: r.id, author: r.author})`,
  113. where: `@@id(${db.esc(authorIds.slice(0, limit))})`
  114. });
  115. return {result, totalFound};
  116. } finally {
  117. this.searchFlag--;
  118. }
  119. }
  120. async periodicCleanCache() {
  121. this.timer = null;
  122. const cleanInterval = 5*1000;//this.config.cacheCleanInterval*60*1000;
  123. try {
  124. const db = this.db;
  125. const oldThres = Date.now() - cleanInterval;
  126. //выберем всех кандидатов удаление
  127. const rows = await db.select({
  128. table: 'query_time',
  129. where: `
  130. @@iter(@all(), (r) => (r.time < ${db.esc(oldThres)}));
  131. `
  132. });
  133. const ids = [];
  134. for (const row of rows)
  135. ids.push(row.id);
  136. //удаляем
  137. await db.delete({table: 'query_cache', where: `@@id(${db.esc(ids)})`});
  138. await db.delete({table: 'query_time', where: `@@id(${db.esc(ids)})`});
  139. console.log('Cache clean', ids);
  140. } catch(e) {
  141. console.error(e.message);
  142. } finally {
  143. if (!this.closed) {
  144. this.timer = setTimeout(() => { this.periodicCleanCache(); }, cleanInterval);
  145. }
  146. }
  147. }
  148. async close() {
  149. while (this.searchFlag > 0) {
  150. await utils.sleep(50);
  151. }
  152. if (this.timer) {
  153. clearTimeout(this.timer);
  154. this.timer = null;
  155. }
  156. this.closed = true;
  157. }
  158. }
  159. module.exports = DbSearcher;