Skip to content

Commit

Permalink
update: Cleanup advanced searching
Browse files Browse the repository at this point in the history
  • Loading branch information
Eltik committed Nov 16, 2023
1 parent ec42104 commit c4410f0
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 159 deletions.
73 changes: 73 additions & 0 deletions anify-backend/src/database/impl/search/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Format, Genres, Season, Sort, SortDirection } from "../../../types/enums";

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
(
${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}'`)})` : ""}
${genres && genres.length > 0 ? `AND ARRAY[${genres.map((g) => `'${g}'`)}] <@ "${type}"."genres"` : ""}
${genresExcluded.length > 0 ? `AND NOT ARRAY[${genresExcluded.map((g) => `'${g}'`)}] && "${type}"."genres"` : ""}
${tags && tags.length > 0 ? `AND ARRAY[${tags.map((g) => `'${g}'`)}] <@ "${type}"."tags"` : ""}
${tagsExcluded.length > 0 ? `AND NOT ARRAY[${tagsExcluded.map((g) => `'${g}'`)}] && "${type}"."tags"` : ""}
${season && season !== Season.UNKNOWN ? `AND "${type}"."season" = '${season}'` : ""}
${year > 0 ? `AND "${type}"."year" = ${year}` : ""}
${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) => {
const countQuery = `
SELECT COUNT(*) FROM "${type}"
${where}
`;
const sqlQuery = `
SELECT * FROM "${type}"
${where}
${
query.length > 0
? `
ORDER BY
(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,
+ CASE WHEN "${type}".title->>'native' IS NOT NULL THEN similarity(LOWER("${type}".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("${type}"."averageRating" AS NUMERIC)`
: sort === Sort.POPULARITY
? `CAST("${type}"."averagePopularity" AS NUMERIC)`
: sort === Sort.TOTAL_EPISODES
? `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)`
: `
(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,
+ CASE WHEN "${type}".title->>'native' IS NOT NULL THEN similarity(LOWER("${type}".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}
`;

return {
countQuery,
sqlQuery,
}
};
177 changes: 18 additions & 159 deletions anify-backend/src/database/impl/search/searchAdvanced.ts
Original file line number Diff line number Diff line change
@@ -1,6 +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";

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

Expand All @@ -21,167 +22,25 @@ export const searchAdvanced = async <T extends Type.ANIME | Type.MANGA>(
) => {
if (dbType === "postgresql") {
const skip = page > 0 ? perPage * (page - 1) : 0;
let where;

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);
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 (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}'`)})` : ""}
${genres && genres.length > 0 ? `AND ARRAY[${genres.map((g) => `'${g}'`)}] <@ "anime"."genres"` : ""}
${genresExcluded.length > 0 ? `AND NOT ARRAY[${genresExcluded.map((g) => `'${g}'`)}] && "anime"."genres"` : ""}
${tags && tags.length > 0 ? `AND ARRAY[${tags.map((g) => `'${g}'`)}] <@ "anime"."tags"` : ""}
${tagsExcluded.length > 0 ? `AND NOT ARRAY[${tagsExcluded.map((g) => `'${g}'`)}] && "anime"."tags"` : ""}
${season && season !== Season.UNKNOWN ? `AND "anime"."season" = '${season}'` : ""}
${year > 0 ? `AND "anime"."year" = ${year}` : ""}
${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}'`)})` : ""}
${genres && genres.length > 0 ? `AND ARRAY[${genres.map((g) => `'${g}'`)}] <@ "manga"."genres"` : ""}
${genresExcluded.length > 0 ? `AND NOT ARRAY[${genresExcluded.map((g) => `'${g}'`)}] && "manga"."genres"` : ""}
${tags && tags.length > 0 ? `AND ARRAY[${tags.map((g) => `'${g}'`)}] <@ "manga"."tags"` : ""}
${tagsExcluded.length > 0 ? `AND NOT ARRAY[${tagsExcluded.map((g) => `'${g}'`)}] && "manga"."tags"` : ""}
${year > 0 ? `AND "manga"."year" = ${year}` : ""}
${sort && sort === Sort.YEAR ? `AND "manga"."year" IS NOT NULL` : ""}
`;
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));
}

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];

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.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);
Expand Down
Loading

0 comments on commit c4410f0

Please sign in to comment.