From 51c13129dd6372693deb29548d67d1fbea437a9d Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 22 Apr 2026 15:24:24 -0400 Subject: [PATCH] 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 --- app/main.py | 62 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/app/main.py b/app/main.py index f1ac15d..7488cfe 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ 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.orm import joinedload, aliased +from sqlalchemy.orm import joinedload, aliased, contains_eager from collections import OrderedDict from datetime import datetime @@ -637,28 +637,41 @@ def search_tags(): """ Search existing tags for autocomplete. Query params: - - q: search query (matches tag name, case-insensitive) + - q: search query (matches against display_name, case-insensitive) - limit: max results (default 10) - - exclude_kind: comma-separated list of tag kinds to exclude (e.g., 'archive') - - kind: filter to only include tags of this kind (e.g., 'series') + - exclude_kind: comma-separated list of tag kinds to exclude + - kind: filter to only include tags of this kind """ - query = request.args.get('q', '').strip().lower() - limit = request.args.get('limit', 10, type=int) - 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() + from sqlalchemy import case + + query = request.args.get("q", "").strip().lower() + limit = request.args.get("limit", 10, type=int) + 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: - # Return most-used tags when no query - base_query = ( - 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) + # No query: most-used tags. + base_query = base_query.join(image_tags, image_tags.c.tag_id == Tag.id).group_by(Tag.id, f.id) tags = ( base_query .order_by(func.count(image_tags.c.image_id).desc()) @@ -666,14 +679,10 @@ def search_tags(): .all() ) else: - base_query = Tag.query.filter(Tag.name.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) + base_query = base_query.filter(display_expr.ilike(f"%{query}%")) tags = ( base_query - .order_by(Tag.name) + .order_by(display_expr) .limit(limit) .all() ) @@ -682,9 +691,10 @@ def search_tags(): { "id": t.id, "name": t.name, + "display_name": t.display_name, "kind": t.kind, "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 ])