DbSearcher.js 7.7 KB

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