Update search endpoints to search db directly

This commit is contained in:
advplyr 2023-08-19 13:59:22 -05:00
parent b334d40998
commit c77cead9ae
8 changed files with 343 additions and 77 deletions

View File

@ -36,7 +36,7 @@ export default {
return this.narrator?.name || ''
},
numBooks() {
return this.narrator?.books?.length || 0
return this.narrator?.numBooks || this.narrator?.books?.length || 0
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']

View File

@ -103,7 +103,7 @@ export default {
return this.$store.state.libraries.currentLibraryId
},
totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length
}
},
methods: {

View File

@ -44,6 +44,21 @@ class Database {
return this.models.series
}
/** @type {typeof import('./models/Book')} */
get bookModel() {
return this.models.book
}
/** @type {typeof import('./models/Podcast')} */
get podcastModel() {
return this.models.podcast
}
/** @type {typeof import('./models/LibraryItem')} */
get libraryItemModel() {
return this.models.libraryItem
}
async checkHasDb() {
if (!await fs.pathExists(this.dbPath)) {
Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`)

View File

@ -790,80 +790,22 @@ class LibraryController {
})
}
// GET: Global library search
search(req, res) {
/**
* GET: /api/libraries/:id/search
* Search library items with query
* ?q=search
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async search(req, res) {
if (!req.query.q) {
return res.status(400).send('No query string')
}
const libraryItems = req.libraryItems
const maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
const query = req.query.q.trim().toLowerCase()
const itemMatches = []
const authorMatches = {}
const narratorMatches = {}
const seriesMatches = {}
const tagMatches = {}
libraryItems.forEach((li) => {
const queryResult = li.searchQuery(req.query.q)
if (queryResult.matchKey) {
itemMatches.push({
libraryItem: li.toJSONExpanded(),
matchKey: queryResult.matchKey,
matchText: queryResult.matchText
})
}
if (queryResult.series?.length) {
queryResult.series.forEach((se) => {
if (!seriesMatches[se.id]) {
const _series = Database.series.find(_se => _se.id === se.id)
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
} else {
seriesMatches[se.id].books.push(li.toJSON())
}
})
}
if (queryResult.authors?.length) {
queryResult.authors.forEach((au) => {
if (!authorMatches[au.id]) {
const _author = Database.authors.find(_au => _au.id === au.id)
if (_author) {
authorMatches[au.id] = _author.toJSON()
authorMatches[au.id].numBooks = 1
}
} else {
authorMatches[au.id].numBooks++
}
})
}
if (queryResult.tags?.length) {
queryResult.tags.forEach((tag) => {
if (!tagMatches[tag]) {
tagMatches[tag] = { name: tag, books: [li.toJSON()] }
} else {
tagMatches[tag].books.push(li.toJSON())
}
})
}
if (queryResult.narrators?.length) {
queryResult.narrators.forEach((narrator) => {
if (!narratorMatches[narrator]) {
narratorMatches[narrator] = { name: narrator, books: [li.toJSON()] }
} else {
narratorMatches[narrator].books.push(li.toJSON())
}
})
}
})
const itemKey = req.library.mediaType
const results = {
[itemKey]: itemMatches.slice(0, maxResults),
tags: Object.values(tagMatches).slice(0, maxResults),
authors: Object.values(authorMatches).slice(0, maxResults),
series: Object.values(seriesMatches).slice(0, maxResults),
narrators: Object.values(narratorMatches).slice(0, maxResults)
}
res.json(results)
const matches = await libraryItemFilters.search(req.library, query, limit)
res.json(matches)
}
async stats(req, res) {

View File

@ -86,7 +86,7 @@ class ApiRouter {
this.router.get('/libraries/:id/personalized2', LibraryController.middlewareNew.bind(this), LibraryController.getUserPersonalizedShelves.bind(this))
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this))
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
this.router.get('/libraries/:id/search', LibraryController.middlewareNew.bind(this), LibraryController.search.bind(this))
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
this.router.get('/libraries/:id/authors', LibraryController.middlewareNew.bind(this), LibraryController.getAuthors.bind(this))
this.router.get('/libraries/:id/narrators', LibraryController.middlewareNew.bind(this), LibraryController.getNarrators.bind(this))

View File

@ -1,5 +1,7 @@
const Sequelize = require('sequelize')
const Database = require('../../Database')
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
module.exports = {
/**
@ -127,7 +129,7 @@ module.exports = {
/**
* Get all library items that have narrators
* @param {string[]} narrators
* @returns {Promise<LibraryItem[]>}
* @returns {Promise<import('../../models/LibraryItem')[]>}
*/
async getAllLibraryItemsWithNarrators(narrators) {
const libraryItems = []
@ -162,5 +164,20 @@ module.exports = {
libraryItems.push(libraryItem)
}
return libraryItems
},
/**
* Search library items
* @param {import('../../objects/Library')} oldLibrary
* @param {string} query
* @param {number} limit
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[], podcast:object[]}}
*/
search(oldLibrary, query, limit) {
if (oldLibrary.isBook) {
return libraryItemsBookFilters.search(oldLibrary, query, limit, 0)
} else {
return libraryItemsPodcastFilters.search(oldLibrary, query, limit, 0)
}
}
}

View File

@ -918,12 +918,205 @@ module.exports = {
/**
* Get library items for series
* @param {oldSeries} oldSeries
* @param {[oldUser]} oldUser
* @returns {Promise<oldLibraryItem[]>}
* @param {import('../../objects/entities/Series')} oldSeries
* @param {import('../../objects/user/User')} [oldUser]
* @returns {Promise<import('../../objects/LibraryItem')[]>}
*/
async getLibraryItemsForSeries(oldSeries, oldUser) {
const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null)
return libraryItems.map(li => Database.models.libraryItem.getOldLibraryItem(li))
},
/**
* Search books, authors, series
* @param {import('../../objects/Library')} oldLibrary
* @param {string} query
* @param {number} limit
* @param {number} offset
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}}
*/
async search(oldLibrary, query, limit, offset) {
// Search title, subtitle, asin, isbn
const books = await Database.bookModel.findAll({
where: {
[Sequelize.Op.or]: [
{
title: {
[Sequelize.Op.substring]: query
}
},
{
subtitle: {
[Sequelize.Op.substring]: query
}
},
{
asin: {
[Sequelize.Op.substring]: query
}
},
{
isbn: {
[Sequelize.Op.substring]: query
}
}
]
},
include: [
{
model: Database.libraryItemModel,
where: {
libraryId: oldLibrary.id
}
},
{
model: Database.models.bookSeries,
include: {
model: Database.seriesModel
},
separate: true
},
{
model: Database.models.bookAuthor,
include: {
model: Database.authorModel
},
separate: true
}
],
subQuery: false,
distinct: true,
limit,
offset
})
const itemMatches = []
for (const book of books) {
const libraryItem = book.libraryItem
delete book.libraryItem
libraryItem.media = book
let matchText = null
let matchKey = null
for (const key of ['title', 'subtitle', 'asin', 'isbn']) {
if (book[key]?.toLowerCase().includes(query)) {
matchText = book[key]
matchKey = key
break
}
}
if (matchKey) {
itemMatches.push({
matchText,
matchKey,
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
})
}
}
// Search narrators
const narratorMatches = []
const [narratorResults] = await Database.sequelize.query(`SELECT value, count(*) AS numBooks FROM books b, libraryItems li, json_each(b.narrators) WHERE json_valid(b.narrators) AND json_each.value LIKE :query AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
replacements: {
query: `%${query}%`,
libraryId: oldLibrary.id,
limit,
offset
},
raw: true
})
for (const row of narratorResults) {
narratorMatches.push({
name: row.value,
numBooks: row.numBooks
})
}
// Search tags
const tagMatches = []
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.tags) WHERE json_valid(b.tags) AND json_each.value LIKE :query AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
replacements: {
query: `%${query}%`,
libraryId: oldLibrary.id,
limit,
offset
},
raw: true
})
for (const row of tagResults) {
tagMatches.push({
name: row.value,
numItems: row.numItems
})
}
// Search series
const allSeries = await Database.seriesModel.findAll({
where: {
name: {
[Sequelize.Op.substring]: query
},
libraryId: oldLibrary.id
},
include: {
separate: true,
model: Database.models.bookSeries,
include: {
model: Database.bookModel,
include: {
model: Database.libraryItemModel
}
}
},
subQuery: false,
distinct: true,
limit,
offset
})
const seriesMatches = []
for (const series of allSeries) {
const books = series.bookSeries.map((bs) => {
const libraryItem = bs.book.libraryItem
libraryItem.media = bs.book
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
})
seriesMatches.push({
series: series.getOldSeries().toJSON(),
books
})
}
// Search authors
const authors = await Database.authorModel.findAll({
where: {
name: {
[Sequelize.Op.substring]: query
},
libraryId: oldLibrary.id
},
attributes: {
include: [
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
]
},
limit,
offset
})
const authorMatches = []
for (const author of authors) {
const oldAuthor = author.getOldAuthor().toJSON()
oldAuthor.numBooks = author.dataValues.numBooks
authorMatches.push(oldAuthor)
}
return {
book: itemMatches,
narrators: narratorMatches,
tags: tagMatches,
series: seriesMatches,
authors: authorMatches
}
}
}

View File

@ -291,5 +291,104 @@ module.exports = {
libraryItems,
count
}
},
/**
* Search podcasts
* @param {import('../../objects/Library')} oldLibrary
* @param {string} query
* @param {number} limit
* @param {number} offset
* @returns {{podcast:object[], tags:object[]}}
*/
async search(oldLibrary, query, limit, offset) {
// Search title, author, itunesId, itunesArtistId
const podcasts = await Database.podcastModel.findAll({
where: {
[Sequelize.Op.or]: [
{
title: {
[Sequelize.Op.substring]: query
}
},
{
author: {
[Sequelize.Op.substring]: query
}
},
{
itunesId: {
[Sequelize.Op.substring]: query
}
},
{
itunesArtistId: {
[Sequelize.Op.substring]: query
}
}
]
},
include: [
{
model: Database.libraryItemModel,
where: {
libraryId: oldLibrary.id
}
}
],
subQuery: false,
distinct: true,
limit,
offset
})
const itemMatches = []
for (const podcast of podcasts) {
const libraryItem = podcast.libraryItem
delete podcast.libraryItem
libraryItem.media = podcast
let matchText = null
let matchKey = null
for (const key of ['title', 'author', 'itunesId', 'itunesArtistId']) {
if (podcast[key]?.toLowerCase().includes(query)) {
matchText = podcast[key]
matchKey = key
break
}
}
if (matchKey) {
itemMatches.push({
matchText,
matchKey,
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
})
}
}
// Search tags
const tagMatches = []
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.tags) WHERE json_valid(p.tags) AND json_each.value LIKE :query AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
replacements: {
query: `%${query}%`,
libraryId: oldLibrary.id,
limit,
offset
},
raw: true
})
for (const row of tagResults) {
tagMatches.push({
name: row.value,
numItems: row.numItems
})
}
return {
podcast: itemMatches,
tags: tagMatches
}
}
}