"""Global search API endpoint.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import ( Machine, Application, KnowledgeBase ) from shopdb.utils.responses import success_response search_bp = Blueprint('search', __name__) @search_bp.route('', methods=['GET']) @jwt_required(optional=True) def global_search(): """ Global search across multiple entity types. Returns combined results from: - Machines (equipment and PCs) - Applications - Knowledge Base articles - Printers (if available) Results are sorted by relevance score. """ query = request.args.get('q', '').strip() if not query or len(query) < 2: return success_response({ 'results': [], 'query': query, 'message': 'Search query must be at least 2 characters' }) if len(query) > 200: return success_response({ 'results': [], 'query': query[:200], 'message': 'Search query too long' }) results = [] search_term = f'%{query}%' # Search Machines (Equipment and PCs) machines = Machine.query.filter( Machine.isactive == True, db.or_( Machine.machinenumber.ilike(search_term), Machine.alias.ilike(search_term), Machine.hostname.ilike(search_term), Machine.serialnumber.ilike(search_term), Machine.notes.ilike(search_term) ) ).limit(10).all() for m in machines: # Determine type: PC, Printer, or Equipment is_pc = m.pctypeid is not None is_printer = m.is_printer # Calculate relevance - exact matches score higher relevance = 15 if m.machinenumber and query.lower() == m.machinenumber.lower(): relevance = 100 elif m.hostname and query.lower() == m.hostname.lower(): relevance = 100 elif m.alias and query.lower() in m.alias.lower(): relevance = 50 display_name = m.hostname if is_pc and m.hostname else m.machinenumber if m.alias and not is_pc: display_name = f"{m.machinenumber} ({m.alias})" # Determine result type and URL if is_printer: result_type = 'printer' url = f"/printers/{m.machineid}" elif is_pc: result_type = 'pc' url = f"/pcs/{m.machineid}" else: result_type = 'machine' url = f"/machines/{m.machineid}" # Get location - prefer machine's own location, fall back to parent machine's location location_name = None if m.location: location_name = m.location.locationname elif m.parent_relationships: # Check parent machines for location for rel in m.parent_relationships: if rel.parent_machine and rel.parent_machine.location: location_name = rel.parent_machine.location.locationname break # Get machinetype from model (single source of truth) mt = m.derived_machinetype results.append({ 'type': result_type, 'id': m.machineid, 'title': display_name, 'subtitle': mt.machinetype if mt else None, 'location': location_name, 'url': url, 'relevance': relevance }) # Search Applications apps = Application.query.filter( Application.isactive == True, db.or_( Application.appname.ilike(search_term), Application.appdescription.ilike(search_term) ) ).limit(10).all() for app in apps: relevance = 20 if query.lower() == app.appname.lower(): relevance = 100 elif query.lower() in app.appname.lower(): relevance = 50 results.append({ 'type': 'application', 'id': app.appid, 'title': app.appname, 'subtitle': app.appdescription[:100] if app.appdescription else None, 'url': f"/applications/{app.appid}", 'relevance': relevance }) # Search Knowledge Base kb_articles = KnowledgeBase.query.filter( KnowledgeBase.isactive == True, db.or_( KnowledgeBase.shortdescription.ilike(search_term), KnowledgeBase.keywords.ilike(search_term) ) ).limit(20).all() for kb in kb_articles: # Weight by clicks and keyword match relevance = 10 + (kb.clicks or 0) * 0.1 if kb.keywords and query.lower() in kb.keywords.lower(): relevance += 15 results.append({ 'type': 'knowledgebase', 'id': kb.linkid, 'title': kb.shortdescription, 'subtitle': kb.application.appname if kb.application else None, 'url': f"/knowledgebase/{kb.linkid}", 'linkurl': kb.linkurl, 'relevance': relevance }) # Search Printers (check if printers model exists) try: from shopdb.plugins.printers.models import Printer printers = Printer.query.filter( Printer.isactive == True, db.or_( Printer.printercsfname.ilike(search_term), Printer.printerwindowsname.ilike(search_term), Printer.serialnumber.ilike(search_term), Printer.fqdn.ilike(search_term) ) ).limit(10).all() for p in printers: relevance = 15 if p.printercsfname and query.lower() == p.printercsfname.lower(): relevance = 100 display_name = p.printercsfname or p.printerwindowsname or f"Printer #{p.printerid}" results.append({ 'type': 'printer', 'id': p.printerid, 'title': display_name, 'subtitle': p.printerwindowsname if p.printercsfname else None, 'url': f"/printers/{p.printerid}", 'relevance': relevance }) except ImportError: pass # Printers plugin not installed # Sort by relevance (highest first) results.sort(key=lambda x: x['relevance'], reverse=True) # Limit total results results = results[:30] return success_response({ 'results': results, 'query': query, 'total': len(results) })