Skip to content

Commit

Permalink
feat: Return pagination for searching
Browse files Browse the repository at this point in the history
  • Loading branch information
Eltik committed Nov 16, 2023
1 parent 93e9b6a commit b105a7c
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 184 deletions.
27 changes: 20 additions & 7 deletions anify-backend/src/database/impl/search/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { Format, Genres, Season, Sort, SortDirection } from "../../../types/enums";

export const generateSearchWhere = (type: "anime" | "manga", query: string, formats: Format[], sort: Sort) => {
return `WHERE
(
${query.length > 0 ? `$1` : `'%'`} ILIKE ANY("${type}".synonyms)
OR ${query.length > 0 ? `$1` : `'%'`} % ANY("${type}".synonyms)
OR "${type}".title->>'english' ILIKE ${query.length > 0 ? "$1" : "'%'"}
OR "${type}".title->>'romaji' ILIKE ${query.length > 0 ? "$1" : "'%'"}
OR "${type}".title->>'native' ILIKE ${query.length > 0 ? "$1" : "'%'"}
)
${formats.length > 0 ? `AND "${type}"."format" IN (${formats.map((f) => `'${f}'`)})` : ""}
${sort && sort === Sort.YEAR ? `AND "${type}"."year" IS NOT NULL` : ""}`;
};

export const generateAdvancedSearchWhere = (type: "anime" | "manga", query: string, formats: Format[], genres: Genres[] = [], genresExcluded: Genres[] = [], season: Season = Season.UNKNOWN, year = 0, tags: string[] = [], tagsExcluded: string[] = [], sort: Sort = Sort.TITLE) => {
return `WHERE
(
Expand All @@ -19,7 +32,7 @@ export const generateAdvancedSearchWhere = (type: "anime" | "manga", query: stri
${sort && sort === Sort.YEAR ? `AND "${type}"."year" IS NOT NULL` : ""}`;
};

export const generateAdvancedSearchQueries = (type: "anime" | "manga", where: string, query: string, sort: Sort = Sort.TITLE, sortDirection: SortDirection = SortDirection.DESC, perPage: number, skip: number) => {
export const generateSearchQueries = (type: "anime" | "manga", where: string, query: string, sort: Sort = Sort.TITLE, sortDirection: SortDirection = SortDirection.DESC, perPage: number, skip: number) => {
const countQuery = `
SELECT COUNT(*) FROM "${type}"
${where}
Expand Down Expand Up @@ -48,10 +61,10 @@ export const generateAdvancedSearchQueries = (type: "anime" | "manga", where: st
? `CAST("${type}"."totalEpisodes" AS NUMERIC)`
: sort === Sort.YEAR
? `CAST("${type}"."year" AS NUMERIC)`
: sort === Sort.TOTAL_CHAPTERS ?
`CAST("${type}"."totalChapters" AS NUMERIC)`
: sort === Sort.TOTAL_VOLUMES ?
`CAST("${type}"."totalVolumes" AS NUMERIC)`
: sort === Sort.TOTAL_CHAPTERS
? `CAST("${type}"."totalChapters" AS NUMERIC)`
: sort === Sort.TOTAL_VOLUMES
? `CAST("${type}"."totalVolumes" AS NUMERIC)`
: `
(CASE WHEN "${type}".title->>'english' IS NOT NULL THEN similarity(LOWER("${type}".title->>'english'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "${type}".title->>'romaji' IS NOT NULL THEN similarity(LOWER("${type}".title->>'romaji'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
Expand All @@ -75,5 +88,5 @@ export const generateAdvancedSearchQueries = (type: "anime" | "manga", where: st
return {
countQuery,
sqlQuery,
}
};
};
};
192 changes: 41 additions & 151 deletions anify-backend/src/database/impl/search/search.ts
Original file line number Diff line number Diff line change
@@ -1,168 +1,54 @@
import { sqlite, dbType, postgres } from "../..";
import { Format, Sort, SortDirection, Type } from "../../../types/enums";
import { Anime, Db, Manga } from "../../../types/types";
import { generateSearchQueries, generateSearchWhere } from "./helper";

type ReturnType<T> = T extends Type.ANIME ? Anime[] : Manga[];

export const search = async <T extends Type.ANIME | Type.MANGA>(query: string, type: T, formats: Format[], page: number, perPage: number, sort: Sort, sortDirection: SortDirection): Promise<ReturnType<T>> => {
export const search = async <T extends Type.ANIME | Type.MANGA>(
query: string,
type: T,
formats: Format[],
page: number,
perPage: number,
sort: Sort,
sortDirection: SortDirection,
): Promise<{
results: ReturnType<T>;
total: number;
lastPage: number;
}> => {
if (dbType === "postgresql") {
const skip = page > 0 ? perPage * (page - 1) : 0;
let where;
const where = generateSearchWhere(type === Type.ANIME ? "anime" : "manga", query, formats, sort);

if (type === Type.ANIME) {
where = `
WHERE
(
${query.length > 0 ? `$1` : `'%'`} ILIKE ANY("anime".synonyms)
OR ${query.length > 0 ? `$1` : `'%'`} % ANY("anime".synonyms)
OR "anime".title->>'english' ILIKE ${query.length > 0 ? "$1" : "'%'"}
OR "anime".title->>'romaji' ILIKE ${query.length > 0 ? "$1" : "'%'"}
OR "anime".title->>'native' ILIKE ${query.length > 0 ? "$1" : "'%'"}
)
${formats.length > 0 ? `AND "anime"."format" IN (${formats.map((f) => `'${f}'`)})` : ""}
${sort && sort === Sort.YEAR ? `AND "anime"."year" IS NOT NULL` : ""}
`;
} else {
where = `
WHERE
(
${query.length > 0 ? `$1` : `'%'`} ILIKE ANY("manga".synonyms)
OR ${query.length > 0 ? `$1` : `'%'`} % ANY("manga".synonyms)
OR "manga".title->>'english' ILIKE ${query.length > 0 ? "$1" : "'%'"}
OR "manga".title->>'romaji' ILIKE ${query.length > 0 ? "$1" : "'%'"}
OR "manga".title->>'native' ILIKE ${query.length > 0 ? "$1" : "'%'"}
)
${formats.length > 0 ? `AND "manga"."format" IN (${formats.map((f) => `'${f}'`)})` : ""}
${sort && sort === Sort.YEAR ? `AND "manga"."year" IS NOT NULL` : ""}
`;
}

let [count, results] = [0, []];
if (type === Type.ANIME) {
const countQuery = `
SELECT COUNT(*) FROM anime
${where}
`;
const sqlQuery = `
SELECT * FROM "anime"
${where}
${
query.length > 0
? `
ORDER BY
(CASE WHEN "anime".title->>'english' IS NOT NULL THEN similarity(LOWER("anime".title->>'english'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "anime".title->>'romaji' IS NOT NULL THEN similarity(LOWER("anime".title->>'romaji'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "anime".title->>'native' IS NOT NULL THEN similarity(LOWER("anime".title->>'native'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN synonyms IS NOT NULL THEN most_similar(LOWER(${query.length > 0 ? `$1` : "'%'"}), synonyms) ELSE 0 END)
DESC
`
: `
ORDER BY
${
sort === Sort.SCORE
? `CAST("anime"."averageRating" AS NUMERIC)`
: sort === Sort.POPULARITY
? `CAST("anime"."averagePopularity" AS NUMERIC)`
: sort === Sort.TOTAL_EPISODES
? `CAST("anime"."totalEpisodes" AS NUMERIC)`
: sort === Sort.YEAR
? `CAST("anime"."year" AS NUMERIC)`
: `
(CASE WHEN "anime".title->>'english' IS NOT NULL THEN similarity(LOWER("anime".title->>'english'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "anime".title->>'romaji' IS NOT NULL THEN similarity(LOWER("anime".title->>'romaji'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "anime".title->>'native' IS NOT NULL THEN similarity(LOWER("anime".title->>'native'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN synonyms IS NOT NULL THEN most_similar(LOWER(${query.length > 0 ? `$1` : "'%'"}), synonyms) ELSE 0 END)
`
}
${sortDirection === SortDirection.ASC ? "ASC" : "DESC"}
`
}
LIMIT ${perPage}
OFFSET ${skip}
`;

[count, results] = (await Promise.all([(await postgres.query(countQuery, query.length > 0 ? [`%${query}`] : [])).rows, (await postgres.query(sqlQuery, query.length > 0 ? [`%${query}`] : [])).rows])) as [any, any];

if (sort === Sort.SCORE) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.averageRating) - Number(b.averageRating)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.averageRating) - Number(a.averageRating));
}
if (sort === Sort.POPULARITY) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.averagePopularity) - Number(b.averagePopularity)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.averagePopularity) - Number(a.averagePopularity));
}
if (sort === Sort.TOTAL_EPISODES) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime, b: Anime) => Number(a.totalEpisodes) - Number(b.totalEpisodes)) : results.sort((a: Anime, b: Anime) => Number(b.totalEpisodes) - Number(a.totalEpisodes));
}
if (sort === Sort.YEAR) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.year) - Number(b.year)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.year) - Number(a.year));
}
} else {
const countQuery = `
SELECT COUNT(*) FROM "manga"
${where}
`;
const sqlQuery = `
SELECT * FROM "manga"
${where}
${
query.length > 0
? `
ORDER BY
(CASE WHEN "manga".title->>'english' IS NOT NULL THEN similarity(LOWER("manga".title->>'english'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "manga".title->>'romaji' IS NOT NULL THEN similarity(LOWER("manga".title->>'romaji'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "manga".title->>'native' IS NOT NULL THEN similarity(LOWER("manga".title->>'native'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN synonyms IS NOT NULL THEN most_similar(LOWER(${query.length > 0 ? `$1` : "'%'"}), synonyms) ELSE 0 END)
DESC
`
: `
ORDER BY
${
sort === Sort.SCORE
? `CAST("manga"."averageRating" AS NUMERIC)`
: sort === Sort.POPULARITY
? `CAST("manga"."averagePopularity" AS NUMERIC)`
: sort === Sort.TOTAL_CHAPTERS
? `CAST("manga"."totalChapters" AS NUMERIC)`
: sort === Sort.TOTAL_VOLUMES
? `CAST("manga"."totalVolumes" AS NUMERIC)`
: sort === Sort.YEAR
? `CAST("manga"."year" AS NUMERIC)`
: `
(CASE WHEN "manga".title->>'english' IS NOT NULL THEN similarity(LOWER("manga".title->>'english'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "manga".title->>'romaji' IS NOT NULL THEN similarity(LOWER("manga".title->>'romaji'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN "manga".title->>'native' IS NOT NULL THEN similarity(LOWER("manga".title->>'native'), LOWER(${query.length > 0 ? `$1` : "'%'"})) ELSE 0 END,
+ CASE WHEN synonyms IS NOT NULL THEN most_similar(LOWER(${query.length > 0 ? `$1` : "'%'"}), synonyms) ELSE 0 END)
`
}
${sortDirection === SortDirection.ASC ? "ASC" : "DESC"}
`
}
LIMIT ${perPage}
OFFSET ${skip}
`;

[count, results] = (await Promise.all([(await postgres.query(countQuery, query.length > 0 ? [`%${query}`] : [])).rows, (await postgres.query(sqlQuery, query.length > 0 ? [`%${query}`] : [])).rows])) as [any, any];
const queries = generateSearchQueries(type === Type.ANIME ? "anime" : "manga", where, query, sort, sortDirection, perPage, skip);
let [count, results] = (await Promise.all([(await postgres.query(queries.countQuery, query.length > 0 ? [`%${query}`] : [])).rows, (await postgres.query(queries.sqlQuery, query.length > 0 ? [`%${query}`] : [])).rows])) as [any, any];

if (sort === Sort.SCORE) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.averageRating) - Number(b.averageRating)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.averageRating) - Number(a.averageRating));
}
if (sort === Sort.POPULARITY) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.averagePopularity) - Number(b.averagePopularity)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.averagePopularity) - Number(a.averagePopularity));
}
if (sort === Sort.TOTAL_CHAPTERS) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Manga, b: Manga) => Number(a.totalChapters) - Number(b.totalChapters)) : results.sort((a: Manga, b: Manga) => Number(b.totalChapters) - Number(a.totalChapters));
}
if (sort === Sort.TOTAL_VOLUMES) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Manga, b: Manga) => Number(a.totalVolumes) - Number(b.totalVolumes)) : results.sort((a: Manga, b: Manga) => Number(b.totalVolumes) - Number(a.totalVolumes));
}
if (sort === Sort.YEAR) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.year) - Number(b.year)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.year) - Number(a.year));
}
if (sort === Sort.SCORE) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.averageRating) - Number(b.averageRating)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.averageRating) - Number(a.averageRating));
}
if (sort === Sort.POPULARITY) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.averagePopularity) - Number(b.averagePopularity)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.averagePopularity) - Number(a.averagePopularity));
}
if (sort === Sort.TOTAL_EPISODES) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime, b: Anime) => Number(a.totalEpisodes) - Number(b.totalEpisodes)) : results.sort((a: Anime, b: Anime) => Number(b.totalEpisodes) - Number(a.totalEpisodes));
}
if (sort === Sort.YEAR) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Anime | Manga, b: Anime | Manga) => Number(a.year) - Number(b.year)) : results.sort((a: Anime | Manga, b: Anime | Manga) => Number(b.year) - Number(a.year));
}
if (sort === Sort.TOTAL_CHAPTERS) {
results = sortDirection === SortDirection.ASC ? results.sort((a: Manga, b: Manga) => Number(a.totalChapters) - Number(b.totalChapters)) : results.sort((a: Manga, b: Manga) => Number(b.totalChapters) - Number(a.totalChapters));
}

const total = Number((count as any)[0]?.count ?? 0);
const lastPage = Math.ceil(Number(total) / perPage);

return results;
return {
results: results as unknown as ReturnType<T>,
total: results.length,
lastPage,
};
}
const skip = page > 0 ? perPage * (page - 1) : 0;

Expand Down Expand Up @@ -255,5 +141,9 @@ export const search = async <T extends Type.ANIME | Type.MANGA>(query: string, t
}
});

return parsedResults as unknown as ReturnType<T>;
return {
results: parsedResults as unknown as ReturnType<T>,
total: parsedResults.length,
lastPage: -1,
};
};
30 changes: 23 additions & 7 deletions anify-backend/src/database/impl/search/searchAdvanced.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sqlite, dbType, postgres } from "../..";
import { Format, Genres, Season, Sort, SortDirection, Type } from "../../../types/enums";
import { Anime, Db, Manga } from "../../../types/types";
import { generateAdvancedSearchQueries, generateAdvancedSearchWhere } from "./helper";
import { generateAdvancedSearchWhere, generateSearchQueries } from "./helper";

type ReturnType<T> = T extends Type.ANIME ? Anime[] : Manga[];

Expand All @@ -19,13 +19,17 @@ export const searchAdvanced = async <T extends Type.ANIME | Type.MANGA>(
tagsExcluded: string[] = [],
sort: Sort = Sort.TITLE,
sortDirection: SortDirection = SortDirection.DESC,
) => {
): Promise<{
results: ReturnType<T>;
total: number;
lastPage: number;
}> => {
if (dbType === "postgresql") {
const skip = page > 0 ? perPage * (page - 1) : 0;

const where = generateAdvancedSearchWhere(type === Type.ANIME ? "anime" : "manga", query, formats, genres, genresExcluded, season, year, tags, tagsExcluded, sort);

const queries = generateAdvancedSearchQueries(type === Type.ANIME ? "anime" : "manga", where, query, sort, sortDirection, perPage, skip);
const queries = generateSearchQueries(type === Type.ANIME ? "anime" : "manga", where, query, sort, sortDirection, perPage, skip);
let [count, results] = (await Promise.all([(await postgres.query(queries.countQuery, query.length > 0 ? [`%${query}`] : [])).rows, (await postgres.query(queries.sqlQuery, query.length > 0 ? [`%${query}`] : [])).rows])) as [any, any];

if (sort === Sort.SCORE) {
Expand All @@ -47,7 +51,11 @@ export const searchAdvanced = async <T extends Type.ANIME | Type.MANGA>(
const total = Number((count as any)[0]?.count ?? 0);
const lastPage = Math.ceil(Number(total) / perPage);

return results;
return {
results: results as unknown as ReturnType<T>,
total: results.length,
lastPage,
};
}

const skip = page > 0 ? perPage * (page - 1) : 0;
Expand Down Expand Up @@ -185,9 +193,17 @@ export const searchAdvanced = async <T extends Type.ANIME | Type.MANGA>(
}
});

return parsedResults as unknown as ReturnType<T>;
return {
results: parsedResults as unknown as ReturnType<T>,
total: parsedResults.length,
lastPage: 1,
};
} catch (e) {
console.error(e);
return [];
return {
results: [],
total: 0,
lastPage: -1,
};
}
};
Loading

0 comments on commit b105a7c

Please sign in to comment.