refactor(main): search_tags JOINs fandom; matches on display CONCAT

Post-refactor tag.name is bare. Autocomplete needs to match the
display form (name + ' (' + fandom.name + ')') so users can still
find 'Ruby Rose (RWBY)' by typing 'rwby'. Switched to
contains_eager(Tag.fandom, alias=f) to eliminate the duplicate
join that caused a Postgres GROUP BY error in the no-query path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 15:24:24 -04:00
parent bdd69c0a07
commit 51c13129dd
+36 -26
View File
@@ -3,7 +3,7 @@
from flask import Blueprint, render_template, flash, redirect, url_for, abort, send_from_directory, request, jsonify, current_app from flask import Blueprint, render_template, flash, redirect, url_for, abort, send_from_directory, request, jsonify, current_app
from sqlalchemy import desc, func, text, extract, and_ from sqlalchemy import desc, func, text, extract, and_
from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm import joinedload, aliased, contains_eager
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
@@ -637,28 +637,41 @@ def search_tags():
""" """
Search existing tags for autocomplete. Search existing tags for autocomplete.
Query params: Query params:
- q: search query (matches tag name, case-insensitive) - q: search query (matches against display_name, case-insensitive)
- limit: max results (default 10) - limit: max results (default 10)
- exclude_kind: comma-separated list of tag kinds to exclude (e.g., 'archive') - exclude_kind: comma-separated list of tag kinds to exclude
- kind: filter to only include tags of this kind (e.g., 'series') - kind: filter to only include tags of this kind
""" """
query = request.args.get('q', '').strip().lower() from sqlalchemy import case
limit = request.args.get('limit', 10, type=int)
exclude_kind = request.args.get('exclude_kind', '').strip() query = request.args.get("q", "").strip().lower()
exclude_kinds = [k.strip() for k in exclude_kind.split(',') if k.strip()] limit = request.args.get("limit", 10, type=int)
include_kind = request.args.get('kind', '').strip() exclude_kind = request.args.get("exclude_kind", "").strip()
exclude_kinds = [k.strip() for k in exclude_kind.split(",") if k.strip()]
include_kind = request.args.get("kind", "").strip()
f = aliased(Tag)
display_expr = case(
(
and_(Tag.kind == "character", Tag.fandom_id.isnot(None)),
func.concat(Tag.name, " (", f.name, ")"),
),
else_=Tag.name,
)
base_query = (
db.session.query(Tag)
.outerjoin(f, f.id == Tag.fandom_id)
.options(contains_eager(Tag.fandom, alias=f))
)
if exclude_kinds:
base_query = base_query.filter(~Tag.kind.in_(exclude_kinds))
if include_kind:
base_query = base_query.filter(Tag.kind == include_kind)
if not query: if not query:
# Return most-used tags when no query # No query: most-used tags.
base_query = ( base_query = base_query.join(image_tags, image_tags.c.tag_id == Tag.id).group_by(Tag.id, f.id)
Tag.query
.join(image_tags)
.group_by(Tag.id)
)
if exclude_kinds:
base_query = base_query.filter(~Tag.kind.in_(exclude_kinds))
if include_kind:
base_query = base_query.filter(Tag.kind == include_kind)
tags = ( tags = (
base_query base_query
.order_by(func.count(image_tags.c.image_id).desc()) .order_by(func.count(image_tags.c.image_id).desc())
@@ -666,14 +679,10 @@ def search_tags():
.all() .all()
) )
else: else:
base_query = Tag.query.filter(Tag.name.ilike(f'%{query}%')) base_query = base_query.filter(display_expr.ilike(f"%{query}%"))
if exclude_kinds:
base_query = base_query.filter(~Tag.kind.in_(exclude_kinds))
if include_kind:
base_query = base_query.filter(Tag.kind == include_kind)
tags = ( tags = (
base_query base_query
.order_by(Tag.name) .order_by(display_expr)
.limit(limit) .limit(limit)
.all() .all()
) )
@@ -682,9 +691,10 @@ def search_tags():
{ {
"id": t.id, "id": t.id,
"name": t.name, "name": t.name,
"display_name": t.display_name,
"kind": t.kind, "kind": t.kind,
"fandom_id": t.fandom_id if t.kind == "character" else None, "fandom_id": t.fandom_id if t.kind == "character" else None,
"fandom_name": (t.fandom.name.split(":", 1)[1] if t.fandom else None) if t.kind == "character" else None "fandom_name": (t.fandom.name if t.fandom else None) if t.kind == "character" else None,
} }
for t in tags for t in tags
]) ])