Make search api search BookReferences first (Untested)

This commit is contained in:
Robbie Antenesse 2020-02-06 13:55:17 -07:00
parent 0189a5cab0
commit 8f0edf7e01
5 changed files with 214 additions and 98 deletions

View File

@ -1,13 +1,16 @@
const fetch = require('node-fetch'); const fetch = require('node-fetch');
class Inventaire { class Inventaire {
constructor(bookURI, language) { constructor(language = 'en') {
this.url = 'https://inventaire.io'; this.url = 'https://inventaire.io';
this.uri = bookURI;
this.lang = language; 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 { return {
name: ( name: (
typeof entityObject.label !== 'undefined' typeof entityObject.label !== 'undefined'
@ -42,14 +45,14 @@ class Inventaire {
}; };
} }
handleEntity(entityObject) { static handleEntity(entityObject, language) {
const hasLabels = typeof entityObject.labels !== 'undefined'; const hasLabels = typeof entityObject.labels !== 'undefined';
const hasDescriptions = typeof entityObject.descriptions !== 'undefined'; const hasDescriptions = typeof entityObject.descriptions !== 'undefined';
return { return {
name: ( name: (
hasLabels && typeof entityObject.labels[this.lang] !== 'undefined' hasLabels && typeof entityObject.labels[language] !== 'undefined'
? entityObject.labels[this.lang] ? entityObject.labels[language]
: ( : (
hasLabels && Object.keys(entityObject.labels).length > 0 hasLabels && Object.keys(entityObject.labels).length > 0
? entityObject.labels[Object.keys(entityObject.labels)[0]] ? entityObject.labels[Object.keys(entityObject.labels)[0]]
@ -57,8 +60,8 @@ class Inventaire {
) )
), ),
description: ( description: (
hasDescriptions && typeof entityObject.descriptions[this.lang] !== 'undefined' hasDescriptions && typeof entityObject.descriptions[language] !== 'undefined'
? entityObject.descriptions[this.lang] ? entityObject.descriptions[language]
: ( : (
hasDescriptions && Object.keys(entityObject.descriptions).length > 0 hasDescriptions && Object.keys(entityObject.descriptions).length > 0
? entityObject.descriptions[Object.keys(entityObject.descriptions)[0]] ? entityObject.descriptions[Object.keys(entityObject.descriptions)[0]]
@ -100,7 +103,7 @@ class Inventaire {
const bookData = await json; const bookData = await json;
if (typeof bookData.entities !== 'undefined' && typeof bookData.entities[uri] !== 'undefined') { 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(); bookData['covers'] = await this.getCovers();
return bookData; return bookData;

View File

@ -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,
};

View File

@ -1,83 +1,158 @@
const fetch = require('node-fetch'); 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 { class SearchController {
constructor(searchTerm, language = 'en') { constructor(sequelizeModels, searchTerm, options = defaultSearchOptions) {
this.term = searchTerm; this.models = sequelizeModels;
this.lang = language; this.searchTerm = searchTerm;
this.searchBy = options.searchBy;
this.source = options.source;
this.lang = options.language;
} }
get hasQuery() { get hasQuery() {
return typeof this.term !== 'undefined' && this.term !== ''; return typeof this.searchTerm !== 'undefined' && this.searchTerm !== '';
} }
quickSearchInventaire() { get includeQuery() {
if (this.hasQuery) { return [
const request = fetch(`https://inventaire.io/api/search?types=works&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}&limit=10`) {
request.catch(exception => { model: this.models.Review,
console.error(exception); where: { text: { [Op.not]: null } },
return { attributes: [[fn('COUNT', 'id'), 'total']], // Get the total number of text reviews
error: exception, as: 'reviews',
message: 'An error occurred when trying to reach the Inventaire API.', },
{
model: this.models.Review,
where: { rating: { [Op.not]: null } },
attributes: [[fn('AVG', 'rating'), 'average']], // Get the average star rating
as: 'rating',
},
]
} }
});
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 => { get orderQuery() {
const bookData = booksController.Inventaire.handleQuickEntity(work); return [{
booksController.uri = bookData.uri; // Update booksController.uri for each book when fetching community data. model: this.models.Review,
const communityData = booksController.getCommunityData(5); attributes: [[fn('COUNT', 'id'), 'total']], // Get the total number of text reviews
}, 'total', 'DESC']; // Order references from most to least interaction
return {
...bookData,
...communityData,
} }
});
}); 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;
} }
} }
searchInventaire(searchBy = 'title') { // Add any search results that match refs with the same URI and delete from results array
if (this.hasQuery) { searchResults.forEach((result, i) => {
const request = fetch(`https://inventaire.io/api/entities?action=search&search=${encodeURIComponent(this.term)}&lang=${encodeURIComponent(this.lang)}`) // If the result is not already in bookReferences
request.catch(exception => { if (!bookReferences.some(ref => result.uri === ref.sources[this.source])) {
console.error(exception); // Check if the URI is already in references table
return { const reference = await this.searchReferencesBySourceCode(this.source, result.uri);
error: exception, if (reference) {
message: 'An error occurred when trying to reach the Inventaire API.', bookReferences.push(reference);
searchResults[i] = null;
}
} else { // If the result is already in references, null it out.
searchResults[i] = null;
} }
}); });
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 { return [
...bookData, ...bookReferences,
...communityData, ...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', { const query = this.mediaWikiQuery('https://en.wikibooks.org/w/api.php', {
action: 'query', action: 'query',
list: 'search', list: 'search',
srsearch: this.term, srsearch: this.searchTerm,
srprop: '', srprop: '',
}); });
query(response => { query(response => {
@ -173,7 +248,7 @@ class SearchController {
return []; 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(res => res.json())
.then(response => { .then(response => {
if (!response.hasOwnProperty('docs')) { if (!response.hasOwnProperty('docs')) {

View File

@ -5,31 +5,16 @@ async function routes(fastify, options) {
const searchTerm = typeof request.query.for !== 'undefined' ? request.query.for.trim() : ''; const searchTerm = typeof request.query.for !== 'undefined' ? request.query.for.trim() : '';
const searchBy = typeof request.query.by !== 'undefined' ? request.query.by.trim() : 'title'; 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 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 source = 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 controller = new SearchController(fastify.models, searchTerm, { searchBy, source, language });
switch (searchSource) { return await controller.search();
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);
}
}
}
}); });
fastify.get('/api/search/cover', async (request, reply) => { fastify.get('/api/search/cover', async (request, reply) => {
const inventaireURI = typeof request.query.uri !== 'undefined' ? request.query.uri.trim() : false; 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 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); return await search.getInventaireCovers(inventaireURI);
}); });