Sorting aggregated actors by scene count back-end, showing disclaimer when limit is reached.

This commit is contained in:
DebaucheryLibrarian 2024-02-29 01:40:11 +01:00
parent 2125a91524
commit 92c2b1866b
10 changed files with 172 additions and 130 deletions

View File

@ -1,56 +1,61 @@
<template> <template>
<div class="filter actors-container"> <div class="filter actors-container">
<div
v-if="isAggActorsLimited"
class="filter-disclaimer"
>Some actors may not be listed, apply a filter or search to narrow down results.</div>
<div class="filters-sort">
<input
v-model="search"
type="search"
:placeholder="`Filter ${actors.length} actors`"
class="input input-inline filters-search"
>
<div
class="filter-sort noselect"
@click="selectGender"
>
<div
v-if="!selectedGender"
class="gender-unselected"
><Icon icon="genders" /></div>
<Gender
v-else
:gender="selectedGender"
class="gender"
/>
</div>
<div
v-show="order === 'name'"
class="filter-sort order noselect"
@click="order = 'count'"
>
<Icon
icon="sort-alpha-asc"
/>
</div>
<div
v-show="order === 'count'"
class="filter-sort order noselect"
@click="order = 'name'"
>
<Icon
icon="sort-numeric-desc"
/>
</div>
</div>
<div <div
v-if="availableActors.length === 0" v-if="availableActors.length === 0"
class="filter-empty" class="filter-empty"
>No actors</div> >No actors</div>
<template v-else> <template v-else>
<div class="filters-sort">
<input
v-model="search"
type="search"
:placeholder="`Filter ${actors.length} actors`"
class="input input-inline filters-search"
>
<div
class="filter-sort noselect"
@click="selectGender"
>
<div
v-if="!selectedGender"
class="gender-unselected"
><Icon icon="genders" /></div>
<Gender
v-else
:gender="selectedGender"
class="gender"
/>
</div>
<div
v-show="order === 'name'"
class="filter-sort order noselect"
@click="order = 'count'"
>
<Icon
icon="sort-alpha-asc"
/>
</div>
<div
v-show="order === 'count'"
class="filter-sort order noselect"
@click="order = 'name'"
>
<Icon
icon="sort-numeric-desc"
/>
</div>
</div>
<ul <ul
v-for="(actor, index) in selectedActors" v-for="(actor, index) in selectedActors"
:key="`actor-${actor.id}`" :key="`actor-${actor.id}`"
@ -110,12 +115,14 @@ const emit = defineEmits(['update']);
const search = ref(''); const search = ref('');
const searchRegexp = computed(() => new RegExp(search.value, 'i')); const searchRegexp = computed(() => new RegExp(search.value, 'i'));
const selectedGender = ref(null); const selectedGender = ref(null);
const order = ref('name'); const order = ref('count');
const { pageProps } = inject('pageContext'); const pageContext = inject('pageContext');
const pageProps = pageContext.pageProps;
const { actor: pageActor } = pageProps; const { actor: pageActor } = pageProps;
const selectedActors = computed(() => props.filters.actors.map((filterActor) => props.actors.find((actor) => actor.id === filterActor.id)).filter(Boolean)); const selectedActors = computed(() => props.filters.actors.map((filterActor) => props.actors.find((actor) => actor.id === filterActor.id)).filter(Boolean));
const availableActors = computed(() => props.actors const availableActors = computed(() => props.actors
.filter((actor) => !props.filters.actors.some((filterActor) => filterActor.id === actor.id) .filter((actor) => !props.filters.actors.some((filterActor) => filterActor.id === actor.id)
&& actor.id !== pageActor?.id && actor.id !== pageActor?.id
@ -130,6 +137,7 @@ const availableActors = computed(() => props.actors
})); }));
const genders = computed(() => [null, ...['female', 'male', 'transsexual', 'other'].filter((gender) => props.actors.some((actor) => actor.gender === gender))]); const genders = computed(() => [null, ...['female', 'male', 'transsexual', 'other'].filter((gender) => props.actors.some((actor) => actor.gender === gender))]);
const isAggActorsLimited = computed(() => props.actors.length >= pageContext.env.maxAggregateSize);
function toggleActor(actor, combine) { function toggleActor(actor, combine) {
if (props.filters.actors.some((filterActor) => filterActor.id === actor.id)) { if (props.filters.actors.some((filterActor) => filterActor.id === actor.id)) {

View File

@ -1,36 +1,36 @@
<template> <template>
<div class="filter channels-container"> <div class="filter channels-container">
<div class="filters-sort">
<input
v-model="search"
type="search"
:placeholder="`Filter ${channels.length} channels`"
class="input input-inline filters-search"
>
<div
v-show="order === 'name'"
class="filter-sort order noselect"
@click="order = 'count'"
>
<Icon icon="sort-alpha-asc" />
</div>
<div
v-show="order === 'count'"
class="filter-sort order noselect"
@click="order = 'name'"
>
<Icon icon="sort-numeric-desc" />
</div>
</div>
<div <div
v-if="entities.length === 0" v-if="entities.length === 0"
class="filter-empty" class="filter-empty"
>No channels</div> >No channels</div>
<template v-else> <template v-else>
<div class="filters-sort">
<input
v-model="search"
type="search"
:placeholder="`Filter ${channels.length} channels`"
class="input input-inline filters-search"
>
<div
v-show="order === 'name'"
class="filter-sort order noselect"
@click="order = 'count'"
>
<Icon icon="sort-alpha-asc" />
</div>
<div
v-show="order === 'count'"
class="filter-sort order noselect"
@click="order = 'name'"
>
<Icon icon="sort-numeric-desc" />
</div>
</div>
<ul <ul
class="filter-items nolist" class="filter-items nolist"
> >
@ -128,14 +128,14 @@ const entities = computed(() => {
return acc; return acc;
} }
if (!acc[channel.parent.id] && channel.type === 'channel') { if (channel.parent && !acc[channel.parent.id] && channel.type === 'channel') {
acc[channel.parent.id] = { acc[channel.parent.id] = {
...channel.parent, ...channel.parent,
children: [], children: [],
}; };
} }
if (channel.type === 'channel') { if (channel.parent && channel.type === 'channel') {
acc[channel.parent.id].children.push(channel); acc[channel.parent.id].children.push(channel);
} }

View File

@ -290,6 +290,15 @@ function toggleFilters(state) {
color: var(--shadow); color: var(--shadow);
font-style: italic; font-style: italic;
} }
.filter-disclaimer {
background: var(--notice);
color: var(--highlight-strong-30);
padding: .25rem .5rem;
box-shadow: inset 0 0 3px var(--shadow-weak-30);
line-height: 1.25;
font-size: .9rem;
}
</style> </style>
<style scoped> <style scoped>

View File

@ -1,50 +1,50 @@
<template> <template>
<div class="filter tags-container"> <div class="filter tags-container">
<div class="filters-sort">
<input
v-model="search"
type="search"
:placeholder="`Filter ${tags.length} tags`"
class="input input-inline filters-search"
>
<div
v-show="order === 'priority'"
class="filter-sort order noselect"
@click="order = 'name'"
>
<Icon
icon="star"
/>
</div>
<div
v-show="order === 'name'"
class="filter-sort order noselect"
@click="order = 'count'"
>
<Icon
icon="sort-alpha-asc"
/>
</div>
<div
v-show="order === 'count'"
class="filter-sort order noselect"
@click="order = 'priority'"
>
<Icon
icon="sort-numeric-desc"
/>
</div>
</div>
<div <div
v-if="tags.length === 0" v-if="groupedTags.available.length === 0"
class="filter-empty" class="filter-empty"
>No tags</div> >No tags</div>
<template v-else> <template v-else>
<div class="filters-sort">
<input
v-model="search"
type="search"
:placeholder="`Filter ${tags.length} tags`"
class="input input-inline filters-search"
>
<div
v-show="order === 'priority'"
class="filter-sort order noselect"
@click="order = 'name'"
>
<Icon
icon="star"
/>
</div>
<div
v-show="order === 'name'"
class="filter-sort order noselect"
@click="order = 'count'"
>
<Icon
icon="sort-alpha-asc"
/>
</div>
<div
v-show="order === 'count'"
class="filter-sort order noselect"
@click="order = 'priority'"
>
<Icon
icon="sort-numeric-desc"
/>
</div>
</div>
<ul <ul
v-for="(group, groupKey) in groupedTags" v-for="(group, groupKey) in groupedTags"
:key="groupKey" :key="groupKey"

View File

@ -17,6 +17,9 @@ module.exports = {
host: '127.0.0.1', host: '127.0.0.1',
sqlPort: 9306, sqlPort: 9306,
httpPort: 9308, httpPort: 9308,
maxMatches: 2000, // high match count needed primarily for actor aggregations
maxAggregateSize: 2000, // must be lower or equal to maxMatches
maxQueryTime: 10000,
}, },
timeout: 5000, timeout: 5000,
graphiql: false, graphiql: false,

View File

@ -1,3 +1,3 @@
export default { export default {
passToClient: ['pageProps', 'urlPathname', 'routeParams', 'urlParsed'], passToClient: ['pageProps', 'urlPathname', 'routeParams', 'urlParsed', 'env'],
}; };

View File

@ -75,9 +75,6 @@ async function onRenderHtml(pageContext) {
return { return {
documentHtml, documentHtml,
pageContext: {
// We can add some `pageContext` here, which is useful if we want to do page redirection https://vike.dev/page-redirection
},
}; };
} }

View File

@ -1,3 +1,5 @@
import config from 'config';
import knex from './knex.js'; import knex from './knex.js';
import { searchApi } from './manticore.js'; import { searchApi } from './manticore.js';
import { HttpError } from './errors.js'; import { HttpError } from './errors.js';
@ -275,9 +277,9 @@ function buildAggregates(options) {
aggregates.actorIds = { aggregates.actorIds = {
terms: { terms: {
field: 'actor_ids', field: 'actor_ids',
size: 5000, size: config.database.manticore.maxAggregateSize,
}, },
// sort: [{ doc_count: { order: 'asc' } }], // sort: [{ 'count(*)': { order: 'desc' } }],
}; };
} }
@ -285,7 +287,7 @@ function buildAggregates(options) {
aggregates.tagIds = { aggregates.tagIds = {
terms: { terms: {
field: 'tag_ids', field: 'tag_ids',
size: 1000, size: config.database.manticore.maxAggregateSize,
}, },
}; };
} }
@ -294,7 +296,7 @@ function buildAggregates(options) {
aggregates.channelIds = { aggregates.channelIds = {
terms: { terms: {
field: 'channel_id', field: 'channel_id',
size: 1000, size: config.database.manticore.maxAggregateSize,
}, },
}; };
} }
@ -318,6 +320,8 @@ export async function fetchMovies(filters, rawOptions) {
console.log('options', options); console.log('options', options);
console.log('query', query.bool.must); console.log('query', query.bool.must);
console.time('manticore');
const result = await searchApi.search({ const result = await searchApi.search({
index: 'movies', index: 'movies',
query, query,
@ -326,8 +330,8 @@ export async function fetchMovies(filters, rawOptions) {
sort, sort,
aggs: buildAggregates(options), aggs: buildAggregates(options),
options: { options: {
max_matches: 1000, max_matches: config.database.manticore.maxMatches,
max_query_time: 10000, max_query_time: config.database.manticore.maxQueryTime,
field_weights: { field_weights: {
title_filtered: 7, title_filtered: 7,
actors: 10, actors: 10,
@ -341,16 +345,22 @@ export async function fetchMovies(filters, rawOptions) {
}, },
}); });
console.timeEnd('manticore');
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds?.buckets); const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds?.buckets);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds?.buckets); const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds?.buckets);
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds?.buckets); const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds?.buckets);
console.time('fetch aggregations');
const [aggActors, aggTags, aggChannels] = await Promise.all([ const [aggActors, aggTags, aggChannels] = await Promise.all([
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [], options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: tagCounts }) : [], options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: tagCounts }) : [],
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: channelCounts }) : [], options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: channelCounts }) : [],
]); ]);
console.timeEnd('fetch aggregations');
const movieIds = result.hits.hits.map((hit) => Number(hit._id)); const movieIds = result.hits.hits.map((hit) => Number(hit._id));
const movies = await fetchMoviesById(movieIds); const movies = await fetchMoviesById(movieIds);

View File

@ -1,3 +1,5 @@
import config from 'config';
import knex from './knex.js'; import knex from './knex.js';
import { searchApi } from './manticore.js'; import { searchApi } from './manticore.js';
import { HttpError } from './errors.js'; import { HttpError } from './errors.js';
@ -257,9 +259,9 @@ function buildAggregates(options) {
aggregates.actorIds = { aggregates.actorIds = {
terms: { terms: {
field: 'actor_ids', field: 'actor_ids',
size: 5000, size: config.database.manticore.maxAggregateSize,
}, },
// sort: [{ doc_count: { order: 'asc' } }], sort: [{ 'count(*)': { order: 'desc' } }],
}; };
} }
@ -267,7 +269,7 @@ function buildAggregates(options) {
aggregates.tagIds = { aggregates.tagIds = {
terms: { terms: {
field: 'tag_ids', field: 'tag_ids',
size: 1000, size: config.database.manticore.maxAggregateSize,
}, },
}; };
} }
@ -276,7 +278,7 @@ function buildAggregates(options) {
aggregates.channelIds = { aggregates.channelIds = {
terms: { terms: {
field: 'channel_id', field: 'channel_id',
size: 1000, size: config.database.manticore.maxAggregateSize,
}, },
}; };
} }
@ -300,6 +302,8 @@ export async function fetchScenes(filters, rawOptions) {
console.log('options', options); console.log('options', options);
console.log('query', query.bool.must); console.log('query', query.bool.must);
console.time('manticore');
const result = await searchApi.search({ const result = await searchApi.search({
index: 'scenes', index: 'scenes',
query, query,
@ -308,8 +312,8 @@ export async function fetchScenes(filters, rawOptions) {
sort, sort,
aggs: buildAggregates(options), aggs: buildAggregates(options),
options: { options: {
max_matches: 1000, max_matches: config.database.manticore.maxMatches,
max_query_time: 10000, max_query_time: config.database.manticore.maxQueryTime,
field_weights: { field_weights: {
title_filtered: 7, title_filtered: 7,
actors: 10, actors: 10,
@ -323,16 +327,24 @@ export async function fetchScenes(filters, rawOptions) {
}, },
}); });
console.timeEnd('manticore');
console.log('hits', result.hits.hits.length);
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds?.buckets); const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds?.buckets);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds?.buckets); const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds?.buckets);
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds?.buckets); const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds?.buckets);
console.time('fetch aggregations');
const [aggActors, aggTags, aggChannels] = await Promise.all([ const [aggActors, aggTags, aggChannels] = await Promise.all([
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [], options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: tagCounts }) : [], options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: tagCounts }) : [],
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: channelCounts }) : [], options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: channelCounts }) : [],
]); ]);
console.timeEnd('fetch aggregations');
const sceneIds = result.hits.hits.map((hit) => Number(hit._id)); const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
const scenes = await fetchScenesById(sceneIds); const scenes = await fetchScenesById(sceneIds);

View File

@ -79,6 +79,9 @@ export default async function initServer() {
const pageContextInit = { const pageContextInit = {
urlOriginal: req.originalUrl, urlOriginal: req.originalUrl,
urlQuery: req.query, // vike's own query does not apply boolean parser urlQuery: req.query, // vike's own query does not apply boolean parser
env: {
maxAggregateSize: config.database.manticore.maxAggregateSize,
},
}; };
const pageContext = await renderPage(pageContextInit); const pageContext = await renderPage(pageContextInit);