query_classifier.py
Rama main · réplica de lectura del repositorio original.
"""
Query Classifier for Smart Scout Search.
Classifies user search queries into intent types (artist, genre, subject, freetext)
to enable per-API optimized search parameters.
Hybrid approach: local dictionary lookup first, Gemini Flash fallback for ambiguous queries.
"""
import logging
from dataclasses import dataclass, field
from typing import Optional
import ai_client
logger = logging.getLogger("artwork-display-api.classifier")
@dataclass
class SearchIntent:
"""Structured search intent produced by the classifier."""
query_type: str # "artist" | "genre" | "subject" | "freetext"
original_query: str # What the user typed
canonical_name: str # Normalized form (e.g., "Vincent van Gogh")
related_terms: list = field(default_factory=list)
era_hint: str = "" # Optional era context (e.g., "Post-Impressionism")
confidence: float = 1.0 # 0.0–1.0 confidence in classification
# ---------------------------------------------------------------------------
# Known Artists Dictionary (~200 entries)
# Keys are lowercase normalized forms; values are (canonical_name, era_hint)
# ---------------------------------------------------------------------------
KNOWN_ARTISTS = {
# Dutch & Flemish Masters
"van gogh": ("Vincent van Gogh", "Post-Impressionism"),
"vincent van gogh": ("Vincent van Gogh", "Post-Impressionism"),
"rembrandt": ("Rembrandt van Rijn", "Dutch Golden Age"),
"rembrandt van rijn": ("Rembrandt van Rijn", "Dutch Golden Age"),
"vermeer": ("Johannes Vermeer", "Dutch Golden Age"),
"johannes vermeer": ("Johannes Vermeer", "Dutch Golden Age"),
"jan vermeer": ("Johannes Vermeer", "Dutch Golden Age"),
"rubens": ("Peter Paul Rubens", "Baroque"),
"peter paul rubens": ("Peter Paul Rubens", "Baroque"),
"hieronymus bosch": ("Hieronymus Bosch", "Northern Renaissance"),
"bosch": ("Hieronymus Bosch", "Northern Renaissance"),
"jan van eyck": ("Jan van Eyck", "Northern Renaissance"),
"van eyck": ("Jan van Eyck", "Northern Renaissance"),
"bruegel": ("Pieter Bruegel the Elder", "Northern Renaissance"),
"pieter bruegel": ("Pieter Bruegel the Elder", "Northern Renaissance"),
"frans hals": ("Frans Hals", "Dutch Golden Age"),
# Italian Masters
"leonardo da vinci": ("Leonardo da Vinci", "Renaissance"),
"da vinci": ("Leonardo da Vinci", "Renaissance"),
"leonardo": ("Leonardo da Vinci", "Renaissance"),
"michelangelo": ("Michelangelo Buonarroti", "Renaissance"),
"raphael": ("Raphael", "Renaissance"),
"raffaello": ("Raphael", "Renaissance"),
"caravaggio": ("Caravaggio", "Baroque"),
"botticelli": ("Sandro Botticelli", "Renaissance"),
"sandro botticelli": ("Sandro Botticelli", "Renaissance"),
"titian": ("Titian", "Renaissance"),
"tintoretto": ("Tintoretto", "Renaissance"),
"giotto": ("Giotto di Bondone", "Proto-Renaissance"),
"bernini": ("Gian Lorenzo Bernini", "Baroque"),
"canaletto": ("Canaletto", "Rococo"),
"tiepolo": ("Giovanni Battista Tiepolo", "Rococo"),
"modigliani": ("Amedeo Modigliani", "Modernism"),
"amedeo modigliani": ("Amedeo Modigliani", "Modernism"),
# French Masters
"monet": ("Claude Monet", "Impressionism"),
"claude monet": ("Claude Monet", "Impressionism"),
"renoir": ("Pierre-Auguste Renoir", "Impressionism"),
"pierre-auguste renoir": ("Pierre-Auguste Renoir", "Impressionism"),
"degas": ("Edgar Degas", "Impressionism"),
"edgar degas": ("Edgar Degas", "Impressionism"),
"cezanne": ("Paul Cézanne", "Post-Impressionism"),
"paul cezanne": ("Paul Cézanne", "Post-Impressionism"),
"cézanne": ("Paul Cézanne", "Post-Impressionism"),
"paul cézanne": ("Paul Cézanne", "Post-Impressionism"),
"manet": ("Édouard Manet", "Impressionism"),
"edouard manet": ("Édouard Manet", "Impressionism"),
"toulouse-lautrec": ("Henri de Toulouse-Lautrec", "Post-Impressionism"),
"henri de toulouse-lautrec": ("Henri de Toulouse-Lautrec", "Post-Impressionism"),
"gauguin": ("Paul Gauguin", "Post-Impressionism"),
"paul gauguin": ("Paul Gauguin", "Post-Impressionism"),
"seurat": ("Georges Seurat", "Post-Impressionism"),
"georges seurat": ("Georges Seurat", "Post-Impressionism"),
"delacroix": ("Eugène Delacroix", "Romanticism"),
"eugene delacroix": ("Eugène Delacroix", "Romanticism"),
"david": ("Jacques-Louis David", "Neoclassicism"),
"jacques-louis david": ("Jacques-Louis David", "Neoclassicism"),
"ingres": ("Jean-Auguste-Dominique Ingres", "Neoclassicism"),
"courbet": ("Gustave Courbet", "Realism"),
"gustave courbet": ("Gustave Courbet", "Realism"),
"poussin": ("Nicolas Poussin", "Baroque"),
"nicolas poussin": ("Nicolas Poussin", "Baroque"),
"watteau": ("Antoine Watteau", "Rococo"),
"fragonard": ("Jean-Honoré Fragonard", "Rococo"),
"boucher": ("François Boucher", "Rococo"),
"rodin": ("Auguste Rodin", "Impressionism"),
"duchamp": ("Marcel Duchamp", "Dadaism"),
"matisse": ("Henri Matisse", "Fauvism"),
"henri matisse": ("Henri Matisse", "Fauvism"),
"rousseau": ("Henri Rousseau", "Post-Impressionism"),
"henri rousseau": ("Henri Rousseau", "Post-Impressionism"),
# Spanish Masters
"picasso": ("Pablo Picasso", "Cubism"),
"pablo picasso": ("Pablo Picasso", "Cubism"),
"dali": ("Salvador Dalí", "Surrealism"),
"salvador dali": ("Salvador Dalí", "Surrealism"),
"dalí": ("Salvador Dalí", "Surrealism"),
"salvador dalí": ("Salvador Dalí", "Surrealism"),
"goya": ("Francisco Goya", "Romanticism"),
"francisco goya": ("Francisco Goya", "Romanticism"),
"velazquez": ("Diego Velázquez", "Baroque"),
"velázquez": ("Diego Velázquez", "Baroque"),
"diego velazquez": ("Diego Velázquez", "Baroque"),
"diego velázquez": ("Diego Velázquez", "Baroque"),
"el greco": ("El Greco", "Mannerism"),
"miro": ("Joan Miró", "Surrealism"),
"joan miro": ("Joan Miró", "Surrealism"),
"miró": ("Joan Miró", "Surrealism"),
# German / Austrian / Swiss
"durer": ("Albrecht Dürer", "Northern Renaissance"),
"dürer": ("Albrecht Dürer", "Northern Renaissance"),
"albrecht durer": ("Albrecht Dürer", "Northern Renaissance"),
"albrecht dürer": ("Albrecht Dürer", "Northern Renaissance"),
"holbein": ("Hans Holbein the Younger", "Northern Renaissance"),
"klimt": ("Gustav Klimt", "Art Nouveau"),
"gustav klimt": ("Gustav Klimt", "Art Nouveau"),
"schiele": ("Egon Schiele", "Expressionism"),
"egon schiele": ("Egon Schiele", "Expressionism"),
"klee": ("Paul Klee", "Expressionism"),
"paul klee": ("Paul Klee", "Expressionism"),
"kandinsky": ("Wassily Kandinsky", "Abstract Art"),
"wassily kandinsky": ("Wassily Kandinsky", "Abstract Art"),
"caspar david friedrich": ("Caspar David Friedrich", "Romanticism"),
"friedrich": ("Caspar David Friedrich", "Romanticism"),
"max ernst": ("Max Ernst", "Surrealism"),
"kirchner": ("Ernst Ludwig Kirchner", "Expressionism"),
# British
"turner": ("J.M.W. Turner", "Romanticism"),
"j.m.w. turner": ("J.M.W. Turner", "Romanticism"),
"william turner": ("J.M.W. Turner", "Romanticism"),
"constable": ("John Constable", "Romanticism"),
"john constable": ("John Constable", "Romanticism"),
"gainsborough": ("Thomas Gainsborough", "Rococo"),
"william blake": ("William Blake", "Romanticism"),
"francis bacon": ("Francis Bacon", "Expressionism"),
"lucian freud": ("Lucian Freud", "Realism"),
"hockney": ("David Hockney", "Pop Art"),
"david hockney": ("David Hockney", "Pop Art"),
"banksy": ("Banksy", "Street Art"),
# American
"warhol": ("Andy Warhol", "Pop Art"),
"andy warhol": ("Andy Warhol", "Pop Art"),
"pollock": ("Jackson Pollock", "Abstract Expressionism"),
"jackson pollock": ("Jackson Pollock", "Abstract Expressionism"),
"rothko": ("Mark Rothko", "Abstract Expressionism"),
"mark rothko": ("Mark Rothko", "Abstract Expressionism"),
"de kooning": ("Willem de Kooning", "Abstract Expressionism"),
"willem de kooning": ("Willem de Kooning", "Abstract Expressionism"),
"basquiat": ("Jean-Michel Basquiat", "Neo-Expressionism"),
"jean-michel basquiat": ("Jean-Michel Basquiat", "Neo-Expressionism"),
"edward hopper": ("Edward Hopper", "Realism"),
"hopper": ("Edward Hopper", "Realism"),
"georgia o'keeffe": ("Georgia O'Keeffe", "Modernism"),
"o'keeffe": ("Georgia O'Keeffe", "Modernism"),
"homer": ("Winslow Homer", "Realism"),
"winslow homer": ("Winslow Homer", "Realism"),
"whistler": ("James McNeill Whistler", "Tonalism"),
"james mcneill whistler": ("James McNeill Whistler", "Tonalism"),
"norman rockwell": ("Norman Rockwell", "Illustration"),
"rockwell": ("Norman Rockwell", "Illustration"),
"sargent": ("John Singer Sargent", "Realism"),
"john singer sargent": ("John Singer Sargent", "Realism"),
"mary cassatt": ("Mary Cassatt", "Impressionism"),
"cassatt": ("Mary Cassatt", "Impressionism"),
"lichtenstein": ("Roy Lichtenstein", "Pop Art"),
"roy lichtenstein": ("Roy Lichtenstein", "Pop Art"),
"jasper johns": ("Jasper Johns", "Pop Art"),
"robert rauschenberg": ("Robert Rauschenberg", "Neo-Dada"),
"alexander calder": ("Alexander Calder", "Modernism"),
"ansel adams": ("Ansel Adams", "Photography"),
"stuart davis": ("Stuart Davis", "Modernism"),
"grant wood": ("Grant Wood", "Regionalism"),
"thomas cole": ("Thomas Cole", "Hudson River School"),
"frederic church": ("Frederic Edwin Church", "Hudson River School"),
"albert bierstadt": ("Albert Bierstadt", "Hudson River School"),
# Scandinavian
"munch": ("Edvard Munch", "Expressionism"),
"edvard munch": ("Edvard Munch", "Expressionism"),
"hammershoi": ("Vilhelm Hammershøi", "Symbolism"),
"hammershøi": ("Vilhelm Hammershøi", "Symbolism"),
# Russian
"malevich": ("Kazimir Malevich", "Suprematism"),
"kazimir malevich": ("Kazimir Malevich", "Suprematism"),
"chagall": ("Marc Chagall", "Modernism"),
"marc chagall": ("Marc Chagall", "Modernism"),
"repin": ("Ilya Repin", "Realism"),
# Japanese
"hokusai": ("Katsushika Hokusai", "Ukiyo-e"),
"katsushika hokusai": ("Katsushika Hokusai", "Ukiyo-e"),
"hiroshige": ("Utagawa Hiroshige", "Ukiyo-e"),
"utagawa hiroshige": ("Utagawa Hiroshige", "Ukiyo-e"),
"utamaro": ("Kitagawa Utamaro", "Ukiyo-e"),
# Mexican
"frida kahlo": ("Frida Kahlo", "Surrealism"),
"kahlo": ("Frida Kahlo", "Surrealism"),
"diego rivera": ("Diego Rivera", "Muralism"),
"rivera": ("Diego Rivera", "Muralism"),
"orozco": ("José Clemente Orozco", "Muralism"),
# Chinese
"ai weiwei": ("Ai Weiwei", "Contemporary"),
# Other notable
"mondrian": ("Piet Mondrian", "De Stijl"),
"piet mondrian": ("Piet Mondrian", "De Stijl"),
"escher": ("M.C. Escher", "Op Art"),
"m.c. escher": ("M.C. Escher", "Op Art"),
"magritte": ("René Magritte", "Surrealism"),
"rene magritte": ("René Magritte", "Surrealism"),
"rené magritte": ("René Magritte", "Surrealism"),
"mucha": ("Alphonse Mucha", "Art Nouveau"),
"alphonse mucha": ("Alphonse Mucha", "Art Nouveau"),
}
# ---------------------------------------------------------------------------
# Known Genres / Movements Dictionary (~50 entries)
# Keys are lowercase; values are the canonical name
# ---------------------------------------------------------------------------
KNOWN_GENRES = {
# Major movements
"impressionism": "Impressionism",
"impressionist": "Impressionism",
"post-impressionism": "Post-Impressionism",
"post impressionism": "Post-Impressionism",
"expressionism": "Expressionism",
"expressionist": "Expressionism",
"abstract expressionism": "Abstract Expressionism",
"cubism": "Cubism",
"cubist": "Cubism",
"surrealism": "Surrealism",
"surrealist": "Surrealism",
"realism": "Realism",
"realist": "Realism",
"romanticism": "Romanticism",
"romantic": "Romanticism",
"baroque": "Baroque",
"rococo": "Rococo",
"renaissance": "Renaissance",
"neoclassicism": "Neoclassicism",
"neoclassical": "Neoclassicism",
"modernism": "Modernism",
"modern art": "Modernism",
"contemporary": "Contemporary Art",
"contemporary art": "Contemporary Art",
"pop art": "Pop Art",
"minimalism": "Minimalism",
"minimalist": "Minimalism",
"abstract": "Abstract Art",
"abstract art": "Abstract Art",
"fauvism": "Fauvism",
"fauvist": "Fauvism",
"dadaism": "Dadaism",
"dada": "Dadaism",
"art nouveau": "Art Nouveau",
"art deco": "Art Deco",
"symbolism": "Symbolism",
"symbolist": "Symbolism",
"constructivism": "Constructivism",
"futurism": "Futurism",
"futurist": "Futurism",
"pointillism": "Pointillism",
"mannerism": "Mannerism",
"gothic": "Gothic Art",
"gothic art": "Gothic Art",
"northern renaissance": "Northern Renaissance",
"dutch golden age": "Dutch Golden Age",
"hudson river school": "Hudson River School",
"pre-raphaelite": "Pre-Raphaelite",
"pre raphaelite": "Pre-Raphaelite",
"ukiyo-e": "Ukiyo-e",
"ukiyo e": "Ukiyo-e",
"japonisme": "Japonisme",
"suprematism": "Suprematism",
"de stijl": "De Stijl",
"neo-expressionism": "Neo-Expressionism",
"photorealism": "Photorealism",
"hyperrealism": "Hyperrealism",
"tonalism": "Tonalism",
"luminism": "Luminism",
"street art": "Street Art",
"op art": "Op Art",
"conceptual art": "Conceptual Art",
# Media-based (useful for filtering)
"watercolor": "Watercolor",
"watercolour": "Watercolor",
"oil painting": "Oil Painting",
"sculpture": "Sculpture",
"photography": "Photography",
"printmaking": "Printmaking",
"etching": "Etching",
"lithograph": "Lithograph",
"woodcut": "Woodcut",
"fresco": "Fresco",
"pastel": "Pastel",
}
class QueryClassifier:
"""
Classifies user search queries into structured SearchIntent objects.
Uses a hybrid approach: local dictionary for known artists/genres,
Gemini Flash fallback for ambiguous queries.
"""
def __init__(self):
pass
def classify(self, query: str) -> SearchIntent:
"""
Classify a search query into a structured SearchIntent.
Priority:
1. Check KNOWN_ARTISTS dictionary
2. Check KNOWN_GENRES dictionary
3. Fall back to Gemini Flash for ambiguous queries
4. Default to 'freetext' if all else fails
"""
if not query or not query.strip():
return SearchIntent(
query_type="freetext",
original_query="",
canonical_name="",
confidence=1.0
)
original = query.strip()
normalized = original.lower().strip()
# 1. Check known artists
if normalized in KNOWN_ARTISTS:
canonical, era = KNOWN_ARTISTS[normalized]
logger.info(f"[Classifier] Dictionary match: '{original}' → artist '{canonical}' ({era})")
return SearchIntent(
query_type="artist",
original_query=original,
canonical_name=canonical,
era_hint=era,
confidence=1.0
)
# 2. Check known genres
if normalized in KNOWN_GENRES:
canonical = KNOWN_GENRES[normalized]
logger.info(f"[Classifier] Dictionary match: '{original}' → genre '{canonical}'")
return SearchIntent(
query_type="genre",
original_query=original,
canonical_name=canonical,
confidence=1.0
)
# 3. Gemini Flash fallback for ambiguous queries
logger.info(f"[Classifier] No dictionary match for '{original}'. Trying AI classification...")
ai_intent = self._classify_with_ai(original)
if ai_intent:
return ai_intent
# 4. Default to freetext
logger.info(f"[Classifier] Defaulting to freetext for '{original}'")
return SearchIntent(
query_type="freetext",
original_query=original,
canonical_name=original,
confidence=0.3
)
def _classify_with_ai(self, query: str) -> Optional[SearchIntent]:
"""Use the configured fast model to classify an ambiguous query."""
if not ai_client.get_ai_config().get("configured"):
logger.warning("[Classifier] No AI model configured. AI fallback disabled.")
return None
prompt = f"""Classify this art search query for a museum collection search system.
Query: "{query}"
Return ONLY valid JSON (no markdown, no code fences) with these fields:
- "type": one of "artist", "genre", "era", "subject", "medium", "freetext"
- "canonical_name": the standard art-historical name if artist, or the standard term for the genre/era/medium. For subjects, use the query as-is.
- "related_terms": list of 2-3 related search terms that would help find relevant artworks
- "era_hint": the primary art-historical era/movement associated (empty string if not applicable)
Examples:
Query: "starry night" → {{"type": "subject", "canonical_name": "The Starry Night", "related_terms": ["Van Gogh", "night sky", "Post-Impressionism"], "era_hint": "Post-Impressionism"}}
Query: "oil on canvas landscapes" → {{"type": "freetext", "canonical_name": "oil on canvas landscapes", "related_terms": ["landscape painting", "plein air"], "era_hint": ""}}
Query: "art nouveau" → {{"type": "genre", "canonical_name": "Art Nouveau", "related_terms": ["Mucha", "decorative arts", "Jugendstil"], "era_hint": "Art Nouveau"}}
"""
try:
response_text = ai_client.chat(
"fast",
[{"role": "user", "content": prompt}],
json_mode=True,
)
data = ai_client.parse_json(response_text)
intent_type = data.get("type", "freetext")
# Normalize type to our expected values
if intent_type in ("era", "medium"):
intent_type = "genre" # Treat era and medium like genre for API param purposes
intent = SearchIntent(
query_type=intent_type,
original_query=query,
canonical_name=data.get("canonical_name", query),
related_terms=data.get("related_terms", []),
era_hint=data.get("era_hint", ""),
confidence=0.8 # AI classification is reliable but not as certain as dictionary
)
logger.info(f"[Classifier] AI classified '{query}' → {intent.query_type}: '{intent.canonical_name}'")
return intent
except Exception as e:
logger.warning(f"[Classifier] AI classification failed for '{query}': {e}")
return None