Работа над расширенным поиском
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
//const _ = require('lodash');
|
//const _ = require('lodash');
|
||||||
const LockQueue = require('./LockQueue');
|
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
|
||||||
const maxLimit = 1000;
|
const maxLimit = 1000;
|
||||||
@@ -21,7 +20,6 @@ class DbSearcher {
|
|||||||
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
|
||||||
this.lock = new LockQueue();
|
|
||||||
this.searchFlag = 0;
|
this.searchFlag = 0;
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
@@ -30,11 +28,22 @@ class DbSearcher {
|
|||||||
this.bookIdMap = {};
|
this.bookIdMap = {};
|
||||||
|
|
||||||
this.periodicCleanCache();//no await
|
this.periodicCleanCache();//no await
|
||||||
this.fillBookIdMapAll();//no await
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.fillBookIdMap('author');
|
||||||
|
await this.fillBookIdMap('series');
|
||||||
|
await this.fillBookIdMap('title');
|
||||||
|
await this.fillDbConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
queryKey(q) {
|
queryKey(q) {
|
||||||
return JSON.stringify([q.author, q.series, q.title, q.genre, q.lang, q.del, q.date, q.librate]);
|
const result = [];
|
||||||
|
for (const f of this.recStruct) {
|
||||||
|
result.push(q[f.field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWhere(a) {
|
getWhere(a) {
|
||||||
@@ -298,34 +307,28 @@ class DbSearcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fillBookIdMap(from) {
|
async fillDbConfig() {
|
||||||
if (this.bookIdMap[from])
|
if (!this.dbConfig) {
|
||||||
return this.bookIdMap[from];
|
const rows = await this.db.select({table: 'config'});
|
||||||
|
const config = {};
|
||||||
|
|
||||||
await this.lock.get();
|
for (const row of rows) {
|
||||||
try {
|
config[row.id] = row.value;
|
||||||
const data = await fs.readFile(`${this.config.dataDir}/db/${from}_id.map`, 'utf-8');
|
}
|
||||||
|
|
||||||
const idMap = JSON.parse(data);
|
this.dbConfig = config;
|
||||||
idMap.arr = new Uint32Array(idMap.arr);
|
this.recStruct = config.inpxInfo.recStruct;
|
||||||
idMap.map = new Map(idMap.map);
|
|
||||||
|
|
||||||
this.bookIdMap[from] = idMap;
|
|
||||||
|
|
||||||
return this.bookIdMap[from];
|
|
||||||
} finally {
|
|
||||||
this.lock.ret();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fillBookIdMapAll() {
|
async fillBookIdMap(from) {
|
||||||
try {
|
const data = await fs.readFile(`${this.config.dataDir}/db/${from}_id.map`, 'utf-8');
|
||||||
await this.fillBookIdMap('author');
|
|
||||||
await this.fillBookIdMap('series');
|
const idMap = JSON.parse(data);
|
||||||
await this.fillBookIdMap('title');
|
idMap.arr = new Uint32Array(idMap.arr);
|
||||||
} catch (e) {
|
idMap.map = new Map(idMap.map);
|
||||||
throw new Error(`DbSearcher.fillBookIdMapAll error: ${e.message}`)
|
|
||||||
}
|
this.bookIdMap[from] = idMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
async tableIdsFilter(from, query) {
|
async tableIdsFilter(from, query) {
|
||||||
@@ -376,7 +379,7 @@ class DbSearcher {
|
|||||||
const filter = await this.tableIdsFilter(from, query);
|
const filter = await this.tableIdsFilter(from, query);
|
||||||
|
|
||||||
const tableIdsSet = new Set();
|
const tableIdsSet = new Set();
|
||||||
const idMap = await this.fillBookIdMap(from);
|
const idMap = this.bookIdMap[from];
|
||||||
let proc = 0;
|
let proc = 0;
|
||||||
let nextProc = 0;
|
let nextProc = 0;
|
||||||
for (const bookId of bookIds) {
|
for (const bookId of bookIds) {
|
||||||
@@ -534,6 +537,105 @@ class DbSearcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bookSearchIds(query) {
|
||||||
|
const ids = await this.selectBookIds(query);
|
||||||
|
const queryKey = this.queryKey(query);
|
||||||
|
const bookKey = `book-search-ids-${queryKey}`;
|
||||||
|
let bookIds = await this.getCached(bookKey);
|
||||||
|
|
||||||
|
if (bookIds === null) {
|
||||||
|
const db = this.db;
|
||||||
|
const filterBySearch = (bookField, searchValue) => {
|
||||||
|
//особая обработка префиксов
|
||||||
|
if (searchValue[0] == '=') {
|
||||||
|
searchValue = searchValue.substring(1);
|
||||||
|
return `(row.${bookField}.localeCompare(${db.esc(searchValue)}) === 0)`;
|
||||||
|
} else if (searchValue[0] == '*') {
|
||||||
|
searchValue = searchValue.substring(1);
|
||||||
|
return `(row.${bookField} && row.${bookField}.indexOf(${db.esc(searchValue)}) >= 0)`;
|
||||||
|
} else if (searchValue[0] == '#') {
|
||||||
|
|
||||||
|
//searchValue = searchValue.substring(1);
|
||||||
|
//return !bookValue || (bookValue !== emptyFieldValue && !enru.has(bookValue[0]) && bookValue.indexOf(searchValue) >= 0);
|
||||||
|
return 'true';
|
||||||
|
} else {
|
||||||
|
return `(row.${bookField}.localeCompare(${db.esc(searchValue)}) >= 0 && row.${bookField}.localeCompare(${db.esc(searchValue)} + maxUtf8Char) <= 0)`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checks = ['true'];
|
||||||
|
for (const f of this.recStruct) {
|
||||||
|
if (query[f.field]) {
|
||||||
|
let searchValue = query[f.field];
|
||||||
|
if (f.type === 'S') {
|
||||||
|
checks.push(filterBySearch(f.field, searchValue));
|
||||||
|
} if (f.type === 'N') {
|
||||||
|
searchValue = parseInt(searchValue, 10);
|
||||||
|
checks.push(`row.${f.field} === ${searchValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db.select({
|
||||||
|
table: 'book',
|
||||||
|
rawResult: true,
|
||||||
|
where: `
|
||||||
|
const ids = ${(ids ? db.esc(Array.from(ids)) : '@all()')};
|
||||||
|
|
||||||
|
const checkBook = (row) => {
|
||||||
|
return ${checks.join(' && ')};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = new Set();
|
||||||
|
for (const id of ids) {
|
||||||
|
const row = @unsafeRow(id);
|
||||||
|
if (checkBook(row))
|
||||||
|
result.add(row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint32Array(result);
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
bookIds = rows[0].rawResult;
|
||||||
|
|
||||||
|
await this.putCached(bookKey, bookIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
//неоптимизированный поиск по всем книгам, по всем полям
|
||||||
|
async bookSearch(query) {
|
||||||
|
if (this.closed)
|
||||||
|
throw new Error('DbSearcher closed');
|
||||||
|
|
||||||
|
this.searchFlag++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
|
const ids = await this.bookSearchIds(query);
|
||||||
|
|
||||||
|
const totalFound = ids.length;
|
||||||
|
let limit = (query.limit ? query.limit : 100);
|
||||||
|
limit = (limit > maxLimit ? maxLimit : limit);
|
||||||
|
const offset = (query.offset ? query.offset : 0);
|
||||||
|
|
||||||
|
const slice = ids.slice(offset, offset + limit);
|
||||||
|
|
||||||
|
//выборка найденных значений
|
||||||
|
const found = await db.select({
|
||||||
|
table: 'book',
|
||||||
|
where: `@@id(${db.esc(Array.from(slice))})`
|
||||||
|
});
|
||||||
|
|
||||||
|
return {found, totalFound};
|
||||||
|
} finally {
|
||||||
|
this.searchFlag--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async opdsQuery(from, query) {
|
async opdsQuery(from, query) {
|
||||||
if (this.closed)
|
if (this.closed)
|
||||||
throw new Error('DbSearcher closed');
|
throw new Error('DbSearcher closed');
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ const versionInfo = 'version.info';
|
|||||||
|
|
||||||
const defaultStructure = 'AUTHOR;GENRE;TITLE;SERIES;SERNO;FILE;SIZE;LIBID;DEL;EXT;DATE;LANG;LIBRATE;KEYWORDS';
|
const defaultStructure = 'AUTHOR;GENRE;TITLE;SERIES;SERNO;FILE;SIZE;LIBID;DEL;EXT;DATE;LANG;LIBRATE;KEYWORDS';
|
||||||
//'AUTHOR;GENRE;TITLE;SERIES;SERNO;FILE;SIZE;LIBID;DEL;EXT;DATE;INSNO;FOLDER;LANG;LIBRATE;KEYWORDS;'
|
//'AUTHOR;GENRE;TITLE;SERIES;SERNO;FILE;SIZE;LIBID;DEL;EXT;DATE;INSNO;FOLDER;LANG;LIBRATE;KEYWORDS;'
|
||||||
const defaultRecStruct = {
|
const recStructType = {
|
||||||
author: 'S',
|
author: 'S',
|
||||||
genre: 'S',
|
genre: 'S',
|
||||||
title: 'S',
|
title: 'S',
|
||||||
series: 'S',
|
series: 'S',
|
||||||
serno: 'N',
|
serno: 'N',
|
||||||
file: 'S',
|
file: 'S',
|
||||||
size: 'N',
|
size: 'N',
|
||||||
libid: 'S',
|
libid: 'S',
|
||||||
del: 'N',
|
del: 'N',
|
||||||
ext: 'S',
|
ext: 'S',
|
||||||
date: 'S',
|
date: 'S',
|
||||||
insno: 'N',
|
insno: 'N',
|
||||||
folder: 'S',
|
folder: 'S',
|
||||||
lang: 'S',
|
lang: 'S',
|
||||||
librate: 'N',
|
librate: 'N',
|
||||||
keywords: 'S',
|
keywords: 'S',
|
||||||
}
|
}
|
||||||
|
|
||||||
class InpxParser {
|
class InpxParser {
|
||||||
@@ -45,13 +45,16 @@ class InpxParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRecStruct(structure) {
|
getRecStruct(structure) {
|
||||||
const result = {};
|
const result = [];
|
||||||
for (const field of structure)
|
let struct = structure;
|
||||||
if (utils.hasProp(defaultRecStruct, field))
|
|
||||||
result[field] = defaultRecStruct[field];
|
|
||||||
|
|
||||||
//folder есть всегда
|
//folder есть всегда
|
||||||
result['folder'] = defaultRecStruct['folder'];
|
if (!struct.includes('folder'))
|
||||||
|
struct = struct.concat(['folder']);
|
||||||
|
|
||||||
|
for (const field of struct) {
|
||||||
|
if (utils.hasProp(recStructType, field))
|
||||||
|
result.push({field, type: recStructType[field]});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ class WebWorker {
|
|||||||
|
|
||||||
//поисковый движок
|
//поисковый движок
|
||||||
this.dbSearcher = new DbSearcher(config, db);
|
this.dbSearcher = new DbSearcher(config, db);
|
||||||
|
await this.dbSearcher.init();
|
||||||
|
|
||||||
//stuff
|
//stuff
|
||||||
db.wwCache = {};
|
db.wwCache = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user