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');
|
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;
|
|
@ -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 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')) {
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue