fix(catalog): cap each facet query at 1000 distinct values
/api/v1/catalog/filters returned an 11.7 MB payload on this server's audiobook library — 88k authors, 92k narrators, 161k series — and that landed in every client filter-sheet open. Above ~1000 entries a dropdown is unusable anyway, so cap each facet helper at catalogFacetMaxValues (1000) with a SQL LIMIT. Applied uniformly to listDistinctArrayColumnWithSource (genres, studios, networks, countries), listDistinctScalarColumnWithSource (content_rating, original_language), listDistinctPeopleByKindWithSource (authors, narrators), listDistinctAudiobookSeriesWithSource, and listDistinctJSONBLanguageWithSource (audio + subtitle languages, both array and JSONB paths). Genres, studios, etc. have fewer than 1000 distinct values today, so the cap is a no-op for the small facets and the visible effect lands on authors / narrators / series. Stopgap, not the final answer. A real fix is server-side typeahead where the request includes a search prefix and the response is the top N matches for that prefix; tracked in a follow-up plan rather than shipped here so existing audiobookshelf clients and the silo UI keep working unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
33cbf6274a
commit
83d23462ba
@@ -523,7 +523,8 @@ func listDistinctArrayColumnWithSource(
|
||||
) vals
|
||||
WHERE val <> ''
|
||||
ORDER BY val ASC
|
||||
`, column, fromClause, whereClause)
|
||||
LIMIT %d
|
||||
`, column, fromClause, whereClause, catalogFacetMaxValues)
|
||||
|
||||
rows, err := pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
@@ -576,7 +577,8 @@ func listDistinctScalarColumnWithSource(
|
||||
%s
|
||||
AND mi.%s <> ''
|
||||
ORDER BY mi.%s ASC
|
||||
`, column, fromClause, whereClause, column, column)
|
||||
LIMIT %d
|
||||
`, column, fromClause, whereClause, column, column, catalogFacetMaxValues)
|
||||
|
||||
// When there are no WHERE conditions, the extra AND is invalid — prepend WHERE instead.
|
||||
if whereClause == "" {
|
||||
@@ -585,7 +587,8 @@ func listDistinctScalarColumnWithSource(
|
||||
FROM %s
|
||||
WHERE mi.%s <> ''
|
||||
ORDER BY mi.%s ASC
|
||||
`, column, fromClause, column, column)
|
||||
LIMIT %d
|
||||
`, column, fromClause, column, column, catalogFacetMaxValues)
|
||||
}
|
||||
|
||||
rows, err := pool.Query(ctx, query, args...)
|
||||
@@ -756,7 +759,8 @@ func listDistinctPeopleByKindWithSource(
|
||||
AND p.name IS NOT NULL
|
||||
AND BTRIM(p.name) <> ''
|
||||
ORDER BY p.name ASC
|
||||
`, fromClause, kindIdx, browseFilterPrefix(whereClause))
|
||||
LIMIT %d
|
||||
`, fromClause, kindIdx, browseFilterPrefix(whereClause), catalogFacetMaxValues)
|
||||
return queryDistinctStrings(ctx, pool, query, args)
|
||||
}
|
||||
|
||||
@@ -791,7 +795,8 @@ func listDistinctAudiobookSeriesWithSource(
|
||||
AND BTRIM(s.series_name) <> ''
|
||||
) names
|
||||
ORDER BY LOWER(name) ASC
|
||||
`, fromClause, browseFilterPrefix(whereClause))
|
||||
LIMIT %d
|
||||
`, fromClause, browseFilterPrefix(whereClause), catalogFacetMaxValues)
|
||||
return queryDistinctStrings(ctx, pool, query, args)
|
||||
}
|
||||
|
||||
@@ -825,7 +830,8 @@ func listDistinctJSONBLanguageWithSource(
|
||||
AND lang IS NOT NULL
|
||||
AND lang <> ''
|
||||
ORDER BY value ASC
|
||||
`, fromClause, mediaFileJoin, arrayColumn, browseFilterPrefix(whereClause))
|
||||
LIMIT %d
|
||||
`, fromClause, mediaFileJoin, arrayColumn, browseFilterPrefix(whereClause), catalogFacetMaxValues)
|
||||
return queryDistinctStrings(ctx, pool, query, args)
|
||||
}
|
||||
|
||||
@@ -838,7 +844,8 @@ func listDistinctJSONBLanguageWithSource(
|
||||
AND mf.missing_since IS NULL
|
||||
AND COALESCE(track->>'language', '') <> ''
|
||||
ORDER BY value ASC
|
||||
`, fromClause, mediaFileJoin, column, browseFilterPrefix(whereClause))
|
||||
LIMIT %d
|
||||
`, fromClause, mediaFileJoin, column, browseFilterPrefix(whereClause), catalogFacetMaxValues)
|
||||
return queryDistinctStrings(ctx, pool, query, args)
|
||||
}
|
||||
|
||||
|
||||
@@ -690,6 +690,16 @@ func (r *CatalogResolver) ListFiltersWithOptions(ctx context.Context, req Catalo
|
||||
// headroom for other concurrent work and avoids saturating the pool.
|
||||
const catalogFacetConcurrency = 6
|
||||
|
||||
// catalogFacetMaxValues caps how many distinct values each facet query
|
||||
// returns. Above ~1000 entries a typeahead UI is the only sensible way to
|
||||
// present a dropdown — and the audiobook library on this server has
|
||||
// >88k distinct authors / >92k narrators / >161k series, which produced
|
||||
// an 11.7 MB /api/v1/catalog/filters response before this cap landed.
|
||||
// The SearchableSelect dropdown stays responsive at 1000 client-side
|
||||
// entries; beyond that, the filter response should switch to a server-
|
||||
// side typeahead surface (out of scope for the cap-only fix).
|
||||
const catalogFacetMaxValues = 1000
|
||||
|
||||
func (r *CatalogResolver) listFiltersForSource(
|
||||
ctx context.Context,
|
||||
filters BrowseFilters,
|
||||
|
||||
Reference in New Issue
Block a user