Make search api search BookReferences first (Untested)
This commit is contained in:
parent
0189a5cab0
commit
8f0edf7e01
|
@ -1,13 +1,16 @@
|
|||
const fetch = require('node-fetch');
|
||||
|
||||
class Inventaire {
|
||||
constructor(bookURI, language) {
|
||||
constructor(language = 'en') {
|
||||
this.url = 'https://inventaire.io';
|
||||
this.uri = bookURI;
|
||||
this.lang = language;
|
||||
}
|
||||
|
||||
handleQuickEntity(entityObject) {
|
||||
static getURL() { // Use a method instead of `get` to avoid collisions with `this.url`
|
||||
return this.url;
|
||||
}
|
||||
|
||||
static handleQuickEntity(entityObject) {
|
||||
return {
|
||||
name: (
|
||||
typeof entityObject.label !== 'undefined'
|
||||
|
@ -42,14 +45,14 @@ class Inventaire {
|
|||
};
|
||||
}
|
||||
|
||||
handleEntity(entityObject) {
|
||||
static handleEntity(entityObject, language) {
|
||||
const hasLabels = typeof entityObject.labels !== 'undefined';
|
||||
const hasDescriptions = typeof entityObject.descriptions !== 'undefined';
|
||||
|
||||
return {
|
||||
name: (
|
||||
hasLabels && typeof entityObject.labels[this.lang] !== 'undefined'
|
||||
? entityObject.labels[this.lang]
|
||||
hasLabels && typeof entityObject.labels[language] !== 'undefined'
|
||||
? entityObject.labels[language]
|
||||
: (
|
||||
hasLabels && Object.keys(entityObject.labels).length > 0
|
||||
? entityObject.labels[Object.keys(entityObject.labels)[0]]
|
||||
|
@ -57,8 +60,8 @@ class Inventaire {
|
|||
)
|
||||
),
|
||||
description: (
|
||||
hasDescriptions && typeof entityObject.descriptions[this.lang] !== 'undefined'
|
||||
? entityObject.descriptions[this.lang]
|
||||
hasDescriptions && typeof entityObject.descriptions[language] !== 'undefined'
|
||||
? entityObject.descriptions[language]
|
||||
: (
|
||||
hasDescriptions && Object.keys(entityObject.descriptions).length > 0
|
||||
? entityObject.descriptions[Object.keys(entityObject.descriptions)[0]]
|
||||
|
@ -100,7 +103,7 @@ class Inventaire {
|
|||
const bookData = await json;
|
||||
|
||||
if (typeof bookData.entities !== 'undefined' && typeof bookData.entities[uri] !== 'undefined') {
|
||||
const bookData = this.handleEntity(bookData.entities[uri], this.lang);
|
||||
const bookData = Inventaire.handleEntity(bookData.entities[uri], this.lang);
|
||||
bookData['covers'] = await this.getCovers();
|
||||
|
||||
return bookData;
|
|
@ -0,0 +1,53 @@
|
|||
const fetch = require('node-fetch');
|
||||
|
||||
const Inventaire = require('../bookData/Inventaire');
|
||||
|
||||
function quickSearchInventaire(searchTerm, language) {
|
||||
const request = fetch(`${Inventaire.getURL()}/api/search?types=works&search=${encodeURIComponent(searchTerm)}&lang=${encodeURIComponent(language)}&limit=10`)
|
||||
request.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying to reach the Inventaire API.',
|
||||
}
|
||||
});
|
||||
|
||||
const json = request.then(response => response.json());
|
||||
json.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
||||
}
|
||||
});
|
||||
|
||||
// Map the results to the correct format.
|
||||
return json.then(responseJSON => responseJSON.results.map(work => Inventaire.handleQuickEntity(work)));
|
||||
}
|
||||
|
||||
function searchInventaire(searchTerm, language) {
|
||||
if (this.hasQuery) {
|
||||
const request = fetch(`${Inventaire.getURL()}/api/entities?action=search&search=${encodeURIComponent(searchTerm)}&lang=${encodeURIComponent(language)}`)
|
||||
request.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying to reach the Inventaire API.',
|
||||
}
|
||||
});
|
||||
const json = request.then(response => response.json());
|
||||
json.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
||||
}
|
||||
});
|
||||
return json.then(responseJSON => responseJSON.results.map(work => Inventaire.handleEntity(work, language)));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
quickSearchInventaire,
|
||||
searchInventaire,
|
||||
};
|
|
@ -1,83 +1,158 @@
|
|||
const fetch = require('node-fetch');
|
||||
const { Op, fn } = require('sequelize');
|
||||
|
||||
const BooksController = require('./books');
|
||||
const BooksController = require('../bookData');
|
||||
const { quickSearchInventaire } = require('./Inventaire');
|
||||
|
||||
const defaultSearchOptions = {
|
||||
searchBy: 'name', // A column name in the BookReference model, mainly 'name' or 'description'
|
||||
source: 'inventaire',
|
||||
language: 'en',
|
||||
}
|
||||
|
||||
class SearchController {
|
||||
constructor(searchTerm, language = 'en') {
|
||||
this.term = searchTerm;
|
||||
this.lang = language;
|
||||
constructor(sequelizeModels, searchTerm, options = defaultSearchOptions) {
|
||||
this.models = sequelizeModels;
|
||||
this.searchTerm = searchTerm;
|
||||
this.searchBy = options.searchBy;
|
||||
this.source = options.source;
|
||||
this.lang = options.language;
|
||||
}
|
||||
|
||||
get hasQuery() {
|
||||
return typeof this.term !== 'undefined' && this.term !== '';
|
||||
return typeof this.searchTerm !== 'undefined' && this.searchTerm !== '';
|
||||
}
|
||||
|
||||
quickSearchInventaire() {
|
||||
if (this.hasQuery) {
|
||||
const request = fetch(`https://inventaire.io/api/search?types=works&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}&limit=10`)
|
||||
request.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying to reach the Inventaire API.',
|
||||
}
|
||||
});
|
||||
const json = request.then(response => response.json());
|
||||
json.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
||||
}
|
||||
});
|
||||
return json.then(responseJSON => {
|
||||
const booksController = new BooksController('inventaire', undefined, this.lang);
|
||||
|
||||
return responseJSON.results.map(work => {
|
||||
const bookData = booksController.Inventaire.handleQuickEntity(work);
|
||||
booksController.uri = bookData.uri; // Update booksController.uri for each book when fetching community data.
|
||||
const communityData = booksController.getCommunityData(5);
|
||||
|
||||
return {
|
||||
...bookData,
|
||||
...communityData,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
get includeQuery() {
|
||||
return [
|
||||
{
|
||||
model: this.models.Review,
|
||||
where: { text: { [Op.not]: null } },
|
||||
attributes: [[fn('COUNT', 'id'), 'total']], // Get the total number of text reviews
|
||||
as: 'reviews',
|
||||
},
|
||||
{
|
||||
model: this.models.Review,
|
||||
where: { rating: { [Op.not]: null } },
|
||||
attributes: [[fn('AVG', 'rating'), 'average']], // Get the average star rating
|
||||
as: 'rating',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
searchInventaire(searchBy = 'title') {
|
||||
if (this.hasQuery) {
|
||||
const request = fetch(`https://inventaire.io/api/entities?action=search&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}`)
|
||||
request.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying to reach the Inventaire API.',
|
||||
}
|
||||
});
|
||||
const json = request.then(response => response.json());
|
||||
json.catch(exception => {
|
||||
console.error(exception);
|
||||
return {
|
||||
error: exception,
|
||||
message: 'An error occurred when trying read the response from Inventaire as JSON.',
|
||||
}
|
||||
});
|
||||
return json.then(responseJSON => {
|
||||
const booksController = new BooksController('inventaire', undefined, this.lang);
|
||||
return responseJSON.works.map(work => {
|
||||
const bookData = booksController.Inventaire.handleEntity(work);
|
||||
const communityData = booksController.getCommunityData(5);
|
||||
|
||||
return {
|
||||
...bookData,
|
||||
...communityData,
|
||||
}
|
||||
});
|
||||
});
|
||||
get orderQuery() {
|
||||
return [{
|
||||
model: this.models.Review,
|
||||
attributes: [[fn('COUNT', 'id'), 'total']], // Get the total number of text reviews
|
||||
}, 'total', 'DESC']; // Order references from most to least interaction
|
||||
}
|
||||
|
||||
async search() {
|
||||
const bookReferences = await this.searchReferences();
|
||||
let searchResults;
|
||||
switch (this.source) {
|
||||
case 'openlibrary': {
|
||||
searchResults = await this.searchOpenLibrary(this.searchBy);
|
||||
break;
|
||||
}
|
||||
case 'inventaire':
|
||||
default: {
|
||||
searchResults = await quickSearchInventaire(this.searchTerm, this.lang);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add any search results that match refs with the same URI and delete from results array
|
||||
searchResults.forEach((result, i) => {
|
||||
// If the result is not already in bookReferences
|
||||
if (!bookReferences.some(ref => result.uri === ref.sources[this.source])) {
|
||||
// Check if the URI is already in references table
|
||||
const reference = await this.searchReferencesBySourceCode(this.source, result.uri);
|
||||
if (reference) {
|
||||
bookReferences.push(reference);
|
||||
searchResults[i] = null;
|
||||
}
|
||||
} else { // If the result is already in references, null it out.
|
||||
searchResults[i] = null;
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
...bookReferences,
|
||||
...searchResults.filter(result => result !== null),
|
||||
];
|
||||
}
|
||||
|
||||
async searchReferences() {
|
||||
const { BookReference, Review } = this.models;
|
||||
|
||||
// const includeQuery = [{
|
||||
// model: Review,
|
||||
// include: [
|
||||
// {
|
||||
// model: Reaction.scope('Review'),
|
||||
// group: ['reactionType'],
|
||||
// attributes: ['reactionType', [fn('COUNT', 'reactionType'), 'count']]
|
||||
// },
|
||||
// ],
|
||||
// order: [{
|
||||
// model: Reaction.scope('Review'),
|
||||
// attributes: [[fn('COUNT', 'id'), 'total']],
|
||||
// limit: 1,
|
||||
// }, 'total', 'DESC'],
|
||||
// limit: 5,
|
||||
// }];
|
||||
|
||||
const exact = await BookReference.findAll({
|
||||
where: {
|
||||
[Op.and]: [ // All of the contained cases are true
|
||||
{
|
||||
[this.searchBy]: this.searchTerm, // searchBy is exactly searchTerm
|
||||
},
|
||||
{
|
||||
locale: this.lang,
|
||||
},
|
||||
]
|
||||
},
|
||||
include: includeQuery(Review),
|
||||
order: orderQuery(Review),
|
||||
});
|
||||
|
||||
if (exact.length > 0) {
|
||||
return exact;
|
||||
}
|
||||
|
||||
// If no exact matches are found, return any approximate ones.
|
||||
return await BookReference.findAll({
|
||||
where: {
|
||||
[Op.and]: [ // All of the contained cases are true
|
||||
{
|
||||
[this.searchBy]: { // `name` or `description`
|
||||
[Op.substring]: this.searchTerm, // LIKE '%searchTerm%'
|
||||
},
|
||||
},
|
||||
{
|
||||
locale: this.lang,
|
||||
},
|
||||
]
|
||||
},
|
||||
include: this.includeQuery,
|
||||
order: this.orderQuery,
|
||||
});
|
||||
}
|
||||
|
||||
async searchReferencesBySourceCode(source, sourceId) {
|
||||
const sourceJSONKey = `"${source}"`; // Enable searching withing JSON column.
|
||||
return await this.models.BookReference.findOne({
|
||||
where: {
|
||||
source: {
|
||||
[sourceJSONKey]: { // Where the object key is the source
|
||||
[Op.eq]: sourceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: this.includeQuery,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,7 +211,7 @@ class SearchController {
|
|||
const query = this.mediaWikiQuery('https://en.wikibooks.org/w/api.php', {
|
||||
action: 'query',
|
||||
list: 'search',
|
||||
srsearch: this.term,
|
||||
srsearch: this.searchTerm,
|
||||
srprop: '',
|
||||
});
|
||||
query(response => {
|
||||
|
@ -173,7 +248,7 @@ class SearchController {
|
|||
return [];
|
||||
}
|
||||
|
||||
return fetch(`https://openlibrary.org/search.json?${searchBy}=${encodeURIComponent(this.term)}`)
|
||||
return fetch(`https://openlibrary.org/search.json?${searchBy}=${encodeURIComponent(this.searchTerm)}`)
|
||||
.then(res => res.json())
|
||||
.then(response => {
|
||||
if (!response.hasOwnProperty('docs')) {
|
|
@ -5,31 +5,16 @@ async function routes(fastify, options) {
|
|||
const searchTerm = typeof request.query.for !== 'undefined' ? request.query.for.trim() : '';
|
||||
const searchBy = typeof request.query.by !== 'undefined' ? request.query.by.trim() : 'title';
|
||||
const language = typeof request.query.lang !== 'undefined' ? request.query.lang.trim().split('-')[0] : undefined; // Get base language in cases like 'en-US'
|
||||
const searchSource = typeof request.query.source !== 'undefined' ? request.query.source.trim() : undefined; // Get base language in cases like 'en-US'
|
||||
const search = new SearchController(searchTerm, language);
|
||||
const source = typeof request.query.source !== 'undefined' ? request.query.source.trim() : undefined; // Get base language in cases like 'en-US'
|
||||
const controller = new SearchController(fastify.models, searchTerm, { searchBy, source, language });
|
||||
|
||||
switch (searchSource) {
|
||||
case 'openLibrary': {
|
||||
return await search.searchOpenLibrary(searchBy);
|
||||
}
|
||||
case 'bookBrainz': {
|
||||
return await search.searchOpenLibrary(searchBy);
|
||||
}
|
||||
case 'inventaire':
|
||||
default: {
|
||||
if (searchBy === 'title') {
|
||||
return await search.quickSearchInventaire();
|
||||
} else {
|
||||
return await search.searchInventaire(searchBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
return await controller.search();
|
||||
});
|
||||
|
||||
fastify.get('/api/search/cover', async (request, reply) => {
|
||||
const inventaireURI = typeof request.query.uri !== 'undefined' ? request.query.uri.trim() : false;
|
||||
const language = typeof request.query.lang !== 'undefined' ? request.query.lang.trim().split('-')[0] : undefined; // Get base language in cases like 'en-US'
|
||||
const search = new SearchController(fastify.siteConfig.inventaireDomain, null, language);
|
||||
const search = new SearchController(fastify.models, null, language);
|
||||
|
||||
return await search.getInventaireCovers(inventaireURI);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue