{"id":28620,"date":"2026-02-28T15:58:35","date_gmt":"2026-02-28T14:58:35","guid":{"rendered":"https:\/\/blog.mi.hdm-stuttgart.de\/?p=28620"},"modified":"2026-03-01T22:25:32","modified_gmt":"2026-03-01T21:25:32","slug":"ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart","status":"publish","type":"post","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/","title":{"rendered":"AI meets Kebab: Konzeption und Umsetzung des D\u00f6nerguide Stuttgart"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">I. Was ist der D\u00f6nerguide?<\/h2>\n\n\n\n<p>Kurz gesagt: Ein studentisches Webprojekt zwischen Hunger, Daten, KI und Architekturentscheidungen.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"569\" data-attachment-id=\"28748\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/screenshot-2026-02-26-1634151\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151.png\" data-orig-size=\"1563,869\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot 2026-02-26 163415(1)\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151-1024x569.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151-1024x569.png\" alt=\"\" class=\"wp-image-28748\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151-1024x569.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151-300x167.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151-768x427.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151-1536x854.png 1536w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-26-1634151.png 1563w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Problem<\/h3>\n\n\n\n<p>Die Suche nach dem besten D\u00f6nerladen, der den eigenen Anforderungen entspricht ist oft gar nicht so einfach. Besonders in einer Stadt wie Stuttgart existieren hunderte D\u00f6nerl\u00e4den, mit stark unterschiedlicher Qualit\u00e4t, Preisen, \u00d6ffnungszeiten und Angeboten. Klassische Plattformen wie Google Maps liefern zwar viele Ergebnisse, beantworten aber selten die eigentlichen Fragen der Nutzer:innen:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Wo finde ich jetzt einen guten D\u00f6ner in meiner N\u00e4he?<\/li>\n\n\n\n<li>Welcher Laden bietet vegetarische oder vegane Optionen?<\/li>\n\n\n\n<li>Wo bekomme ich einen g\u00fcnstigen D\u00f6ner unter einem bestimmten Preis?<\/li>\n<\/ul>\n\n\n\n<p>Bewertungen sind h\u00e4ufig subjektiv, inkonsistent oder beziehen sich auf v\u00f6llig unterschiedliche Kriterien. Gleichzeitig fehlen strukturierte Filterm\u00f6glichkeiten f\u00fcr genau die Eigenschaften, die f\u00fcr D\u00f6ner-Fans wirklich relevant sind, wie etwa Fleischanteil, Wartezeit oder Preis-Leistungs-Verh\u00e4ltnis.<br>In unserer Projektidee entstand deshalb ein klares Szenario: <\/p>\n\n\n\n<p>\u201eIch m\u00f6chte einen veganen D\u00f6ner f\u00fcr maximal 6 \u20ac, der gerade ge\u00f6ffnet ist und Kartenzahlung akzeptiert.\u201c <\/p>\n\n\n\n<p>Mit bestehenden Plattformen l\u00e4sst sich eine solche Anfrage kaum effizient beantworten. Genau hier setzt unser D\u00f6nerguide Stuttgart an.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Die L\u00f6sung<\/h3>\n\n\n\n<p>Der D\u00f6nerguide Stuttgart ist eine spezialisierte Web-Anwendung, die sich vollst\u00e4ndig auf die Suche und Bewertung von D\u00f6nerl\u00e4den konzentriert. Genau richtig f\u00fcr Menschen, die genaue Vorstellungen von ihrem perfekten D\u00f6ner haben oder einfach nur den n\u00e4chsten D\u00f6nerladen mit der besten Bewertung finden m\u00f6chten.<\/p>\n\n\n\n<p>Die Anwendung kombiniert dabei mehrere Ans\u00e4tze:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>strukturierte Daten aus der Google Maps Places API<\/li>\n\n\n\n<li>eine interaktive Kartenansicht<\/li>\n\n\n\n<li>umfangreiche Filterm\u00f6glichkeiten<\/li>\n\n\n\n<li>sowie eine KI-gest\u00fctzte Analyse von D\u00f6nerbildern<\/li>\n<\/ul>\n\n\n\n<p>Dadurch entsteht eine Suche, die nicht nur Orte auflistet, sondern echte Entscheidungsunterst\u00fctzung bietet. Nutzer:innen k\u00f6nnen D\u00f6nerl\u00e4den anhand konkreter Kriterien entdecken, statt sich durch unstrukturierte Bewertungen zu arbeiten.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Features &#8211; Was den D\u00f6nerguide besonders macht<\/h3>\n\n\n\n<p><strong>Filterbare D\u00f6ner-Suche<\/strong><br>Im Zentrum der Anwendung steht eine dynamische \u00dcbersicht aller D\u00f6nerl\u00e4den in Stuttgart. Diese kann nach verschiedenen Kriterien gefiltert werden, wie Preis, Bewertung, vegetarische Optionen, Zahlungsmethoden, Wartezeit oder dem Stadtbezirk. So lassen sich sehr spezifische Anforderungen direkt abbilden.<\/p>\n\n\n\n<p><strong>Kartenintegration<\/strong><br>Alle Ergebnisse werden zus\u00e4tzlich auf einer interaktiven Karte visualisiert, wodurch Nutzer:innen neben der Liste eine \u00dcbersicht der verschiedenen Standorte der D\u00f6nerl\u00e4den haben.<\/p>\n\n\n\n<p><strong>KI-gest\u00fctzte D\u00f6nerbewertung<\/strong><br>Ein besonderes Alleinstellungsmerkmal ist die automatisierte Analyse von D\u00f6nerbildern. Eine KI-Pipeline bewertet Bilder anhand mehrerer Kriterien, darunter der Geschmackseindruck, Belag, Verh\u00e4ltnis der Zutaten und die Gesamtqualit\u00e4t. Aus diesen Faktoren entsteht ein strukturierter Score, der eine vergleichbare Bewertung zwischen verschiedenen L\u00e4den erm\u00f6glicht.<br>Damit erg\u00e4nzt die KI klassische Nutzerbewertungen um eine objektivierbare Perspektive.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-scaled.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"513\" data-attachment-id=\"28749\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/group-41-2\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-scaled.jpg\" data-orig-size=\"2560,1281\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Group 41\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-1024x513.jpg\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-1024x513.jpg\" alt=\"\" class=\"wp-image-28749\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-1024x513.jpg 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-300x150.jpg 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-768x384.jpg 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-1536x769.jpg 1536w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Group-41-2048x1025.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">II. Architektur<\/h2>\n\n\n\n<p>Der D\u00f6nerguide besteht aus zwei klar getrennten Verantwortlichkeiten: einer KI-Datenpipeline, die automatisch D\u00f6nerl\u00e4den findet, Bilder analysiert, Bewertungen und Bilder f\u00fcr die D\u00f6nerl\u00e4den generiert, und einer API, die diese Daten f\u00fcr das Frontend bereitstellt.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"512\" data-attachment-id=\"28690\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/gruppe-73\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73.png\" data-orig-size=\"1759,880\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Gruppe 73\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73-1024x512.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73-1024x512.png\" alt=\"\" class=\"wp-image-28690\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73-1024x512.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73-300x150.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73-768x384.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73-1536x768.png 1536w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-73.png 1759w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p>Als Alternative haben wir einen Monolithen in Betracht gezogen, z.B. eine Express-Anwendung mit eingebautem Scheduler, die Aggregation und API in einem Docker-Container vereint. Aus zwei Hauptgr\u00fcnden haben wir uns gegen diese Variante entschieden: Die Pipeline h\u00e4tte die API-Performance beeinflussen k\u00f6nnen, und der Container h\u00e4tte durchgehend laufen m\u00fcssen, obwohl die Pipeline nur alle 15 Minuten ausgef\u00fchrt wird.<br>Stattdessen entschieden wir uns f\u00fcr einen lose gekoppelten Ansatz mit Azure Functions und Azure Service Bus. Jede Function \u00fcbernimmt eine einzige Aufgabe und kommuniziert ausschlie\u00dflich \u00fcber Queues, ohne direkte Abh\u00e4ngigkeiten zwischen den Functions. Das erm\u00f6glicht Zero Scaling (Kosten entstehen nur bei Ausf\u00fchrung), automatisches Circuit-Breaker-Verhalten \u00fcber die Dead-Letter-Queue sowie unabh\u00e4ngige Entwicklung und Deployment pro Function.<\/p>\n\n\n\n<p>Diese Architektur bringt auch Nachteile mit: Die API-Function hat messbare Cold Starts beim ersten Request, das verteilte System ist aufw\u00e4ndiger zu debuggen als ein einzelner Prozess. Dazu mehr im Abschnitt Learnings.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Frontend<\/h3>\n\n\n\n<p>Wenn man ein Projekt wie den D\u00f6nerguide Stuttgart startet, stellt man sich am Anfang immer die gleiche Frage: \u201eWelches Werkzeug nehmen wir eigentlich?\u201c Wir wollten eine L\u00f6sung, die modern ist, Spa\u00df macht und vor allem performant l\u00e4uft. Hier ist der Deep Dive in unsere Frontend-Entscheidungen.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Das NextJS Frontend<\/h4>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"245\" data-attachment-id=\"28709\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/screenshot-2026-02-25-at-19-19-53\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53.png\" data-orig-size=\"1992,476\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot 2026-02-25 at 19.19.53\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53-1024x245.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53-1024x245.png\" alt=\"\" class=\"wp-image-28709\" style=\"width:758px;height:auto\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53-1024x245.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53-300x72.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53-768x184.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53-1536x367.png 1536w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-25-at-19.19.53.png 1992w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\"><em>Unser Frontend Techstack: NextJS Typescript, DaisyUI und Mapbox<\/em><\/figcaption><\/figure>\n\n\n\n<p>F\u00fcr das \u201eGesicht\u201c unserer Anwendung haben wir uns auf das bew\u00e4hrte Trio aus <strong>NextJS<\/strong>, <strong>TypeScript<\/strong> und <strong>DaisyUI<\/strong> verlassen.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Warum NextJS?<\/strong> Ganz ehrlich: Wir hatten bereits Experten f\u00fcr dieses Framework im Team. Das hat uns den Start enorm erleichtert. Andere Frameworks wie <strong>Angular<\/strong> f\u00fchlten sich f\u00fcr unser Vorhaben einfach zu \u00fcberdimensioniert an. Und bei <strong>Svelte<\/strong>? Da hatten wir Bauchschmerzen wegen des (noch) fehlenden direkten Supports f\u00fcr Azure. NextJS passte also am besten.<\/li>\n\n\n\n<li><strong>Statisch, aber oho:<\/strong> Unser Frontend ist zwar statisch gebaut, f\u00fcttert sich aber dynamisch mit Daten \u00fcber einen REST-API-Endpoint von unserem D\u00f6ner-Backend. So bleibt die Seite blitzschnell, zeigt aber immer die aktuellsten D\u00f6ner-Rankings an.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Karten-Chaos: Von Google Maps zu Mapbox<\/h4>\n\n\n\n<p>Ein Highlight unserer App ist die Karte, auf der ihr alle D\u00f6nerl\u00e4den in Stuttgart findet. Urspr\u00fcnglich wollten wir <strong>Google Maps<\/strong> nutzen. Aber als wir das Preisschild von 100 Dollar pro Monat f\u00fcr den passenden API-Key sahen, sind wir fast vom Stuhl gefallen.<\/p>\n\n\n\n<p>Die Rettung? <strong>Mapbox<\/strong>! Es basiert auf OpenStreetMaps, ist deutlich budgetfreundlicher und hat einen riesigen Vorteil: Wir konnten unser eigenes Design direkt in die Karte integrieren. Damit sieht die Map genauso schick aus wie der Rest der App.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Hosting mit Azure Static Web Apps<\/h4>\n\n\n\n<p>Beim Hosting haben wir keine Experimente gemacht. Da unser komplettes Backend sowieso in der Azure-Cloud lebt, war es nur logisch, auch das Frontend \u00fcber <strong>Azure Static Web Apps<\/strong> laufen zu lassen. Alles aus einer Hand macht die Pipeline einfach viel entspannter.<\/p>\n\n\n\n<p>Die Struktur der App haben wir dabei bewusst schlank gehalten:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Main Page:<\/strong> Filtern, Suchen, Liste anschauen oder direkt auf der Karte den n\u00e4chsten Snack finden.<\/li>\n\n\n\n<li><strong>Store Pages:<\/strong> Die Detailansichten f\u00fcr jeden einzelnen D\u00f6nerladen.<\/li>\n\n\n\n<li><strong>Der Rest:<\/strong> Kontakt, Datenschutz und Impressum.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1230\" height=\"692\" data-attachment-id=\"28708\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/do%cc%88nerguide-sitemap-3\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap-edited-1.png\" data-orig-size=\"1230,692\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Do\u0308nerguide-Sitemap\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap-edited-1-1024x576.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap-edited-1.png\" alt=\"\" class=\"wp-image-28708\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap-edited-1.png 1230w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap-edited-1-300x169.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap-edited-1-1024x576.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Do\u0308nerguide-Sitemap-edited-1-768x432.png 768w\" sizes=\"auto, (max-width: 1230px) 100vw, 1230px\" \/><\/a><figcaption class=\"wp-element-caption\">Die <em>Sitemap des D\u00f6nerguides<\/em><\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Backend &amp; Infrastruktur<\/h3>\n\n\n\n<p>Der D\u00f6nerguide l\u00e4uft vollst\u00e4ndig auf Azure. F\u00fcnf Functions bilden das R\u00fcckgrat der Anwendung: Der Suchdienst l\u00e4uft mit einem Timer (Cron)-Trigger und findet D\u00f6nerl\u00e4den \u00fcber die Google Maps API. Die Bildklassifizierung verarbeitet die Fotos der L\u00e4den mit Azure Computer Vision, klassifiziert diese in Essen und Ambiente und speichert sie in einem Blob Storage Container. Der LLM-Bewertungsgenerator erstellt auf Basis der klassifizierten Bilder KI-Bewertungen \u00fcber Azure OpenAI mit GPT-5 mini. Der Bildgenerator erstellt mit Nano Banana synthetische Bilder zu den L\u00e4den \u00fcber die Google Gemini API. Die API ist die einzige Function mit einem HTTP-Trigger und liefert die aufbereiteten Daten an das Frontend.<\/p>\n\n\n\n<p>Die Functions kommunizieren ausschlie\u00dflich \u00fcber Azure Service Bus. Drei Queues verbinden die Pipeline: Die erste \u00fcbergibt neue L\u00e4den an die Bildklassifizierung, die zweite klassifizierte Bilder an den Bewertungsgenerator, die dritte Bild-Prompts an den Bildgenerator. Durch diese Entkopplung hat jede Function nur Kenntnis von ihrer eigenen Queue, Daten werden \u00fcber Messages mit minimalen Informationen weitergegeben und k\u00f6nnen nicht bei der \u00fcbertragung verloren gehen<\/p>\n\n\n\n<p>Als Datenbank haben wir uns f\u00fcr die Azure CosmosDB entschienden, unter anderem wegen des Pay-as-you-go-Modells, der automatischen Skalierung und des flexiblen Dokumentenmodells, das schnelle Iteration ohne striktes Schema-Management erm\u00f6glicht.<\/p>\n\n\n\n<p>Alle Azure-internen Dienste (CosmosDB, Service Bus, Blob Storage) werden \u00fcber Managed Identity angebunden, ohne Credentials im Code. Externe API-Keys (Google Maps, Google Gemini) werden im Key Vault verwaltet.<\/p>\n\n\n\n<p>F\u00fcr Monitoring hat jede Function eine eigene Application-Insights-Instanz; ein zentraler Log Analytics Workspace erm\u00f6glicht \u00fcbergreifende Auswertung. Das Next.js-Frontend l\u00e4uft als Azure Static Web App mit Custom Domain \u00fcber Cloudflare DNS.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Place Search Algorithmus<\/h3>\n\n\n\n<p>Die <strong>Google Places API<\/strong> liefert pro Suchanfrage maximal <strong>60 Ergebnisse<\/strong>. Stuttgart hat aber deutlich mehr als 60 D\u00f6nerl\u00e4den. Eine einzelne Suche \u00fcber das gesamte Stadtgebiet liefert dadurch unvollst\u00e4ndige Ergebnisse. Deswegen wird ein Algorithmus ben\u00f6tigt, der das Stadtgebiet systematisch in kleinere Bereiche zerlegt, um m\u00f6glichst alle L\u00e4den zu finden.<\/p>\n\n\n\n<p>Die Place Search ist eine <strong>Timer-getriggerte Azure Function<\/strong>, die alle 16 Stunden ausgef\u00fchrt wird. Pro Durchlauf wird genau eine Zelle des Grids verarbeitet. Gefundene D\u00f6nerl\u00e4den werden in CosmosDB gespeichert, neue Fotos per Azure Service Bus an die KI-Pipeline (Image Classifier) weitergeleitet. Die Zellen des Grids werden ebenfalls in CosmosDB abgespeichert, sodass der Fortschritt zwischen den Funktionsaufrufen erhalten bleibt. Die Place Search bildet damit den ersten Schritt in unserer Pipeline: <strong>Place Search \u2192 Datenbank \u2192 KI-Analyse \u2192 Frontend<\/strong>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Unsere L\u00f6sung<\/h4>\n\n\n\n<h5 class=\"wp-block-heading\">Google Places API<\/h5>\n\n\n\n<p>Wir nutzen die Google Places API (New) mit dem <strong>Text-Search<\/strong>-Endpoint, um D\u00f6nerl\u00e4den zu finden und alle relevanten Daten abzufragen, darunter Name, Standort, \u00d6ffnungszeiten und Fotos. Als Suchanfrage wird &#8220;D\u00f6ner&#8221; \u00fcbergeben.<\/p>\n\n\n\n<p>Die API liefert maximal ~20 Ergebnisse pro Seite, \u00fcber ein <code class=\"\" data-line=\"\">nextPageToken<\/code> k\u00f6nnen bis zu 3 Seiten abgerufen werden. Daraus ergibt sich das harte Limit von <strong>~60 Ergebnissen pro Suchbereich<\/strong>.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Grid-Erstellung<\/h5>\n\n\n\n<p>Das Stuttgarter Stadtgebiet wird anhand einer <strong>GeoJSON-Datei<\/strong> der Stadtgrenze in eine <strong>Bounding Box<\/strong> gesetzt und dann in ein <strong>kilometerbasiertes Grid<\/strong> aufgeteilt (~7 km Zellseitenl\u00e4nge). Zellen, die au\u00dferhalb der Stuttgarter Stadtgrenze liegen, werden dabei verworfen, um unn\u00f6tige API-Anfragen zu sparen.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Adaptive Verarbeitung (Splitting)<\/h5>\n\n\n\n<p>Pro Durchlauf fragt die Place Search Function die Google Places API f\u00fcr genau eine Zelle ab. Enth\u00e4lt eine Zelle <strong>55 oder mehr Ergebnisse<\/strong>, wird sie gesplittet.<\/p>\n\n\n\n<p>Das Splitting erfolgt als <strong>Binary Split<\/strong>. Dabei wird die Zelle entlang ihrer l\u00e4ngeren Seite in zwei H\u00e4lften geteilt. Die neu entstandenen Zellen werden ebenfalls gegen die Stadtgrenze gepr\u00fcft. Eine maximale Splitting-Tiefe von 10 und eine minimale Zellgr\u00f6\u00dfe von 50 m verhindern endloses Splitten.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"291\" data-attachment-id=\"28721\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/place-search-architecture\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture.png\" data-orig-size=\"1678,477\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Place-Search Architecture\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture-1024x291.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture-1024x291.png\" alt=\"\" class=\"wp-image-28721\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture-1024x291.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture-300x85.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture-768x218.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture-1536x437.png 1536w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Place-Search-Architecture.png 1678w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<h5 class=\"wp-block-heading\">Zellen-Merging (Post-Scan-Optimierung)<\/h5>\n\n\n\n<p>Nach einem vollst\u00e4ndigen Durchlauf aller Zellen werden diese daraufhin untersucht, welche benachbarten Zellen zusammengef\u00fchrt werden k\u00f6nnen. Zwei Zellen m\u00fcssen daf\u00fcr eine vollst\u00e4ndige gemeinsame Kante teilen und ihre kombinierte Ergebnisanzahl darf 40 nicht \u00fcberschreiten. Das Merge wird nur durchgef\u00fchrt, wenn es tats\u00e4chlich API-Requests einspart.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Architekturentscheidungen<\/h4>\n\n\n\n<h5 class=\"wp-block-heading\">Warum Text Search statt Nearby Search?<\/h5>\n\n\n\n<p>Die Google Places API bietet zwei Suchendpoints: <strong>Text Search<\/strong> und <strong>Nearby Search<\/strong>. Text Search erlaubt eine semantische Suchanfrage wie &#8220;D\u00f6ner&#8221;, wodurch gezielt D\u00f6nerl\u00e4den gefunden werden. Nearby Search kann dagegen nur mit vordefinierten Typen filtern, was f\u00fcr unseren Anwendungsfall zu unflexibel ist.<\/p>\n\n\n\n<p>Dar\u00fcber hinaus l\u00e4sst sich bei Text Search der Suchbereich als Rechteck definieren, w\u00e4hrend Nearby Search ausschlie\u00dflich kreisf\u00f6rmige Bereiche (Mittelpunkt + Radius) unterst\u00fctzt.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Text Search erm\u00f6glicht die semantische Suche nach &#8220;D\u00f6ner&#8221; und erlaubt den Suchbereich als Rechteck zu definieren.<\/p>\n<\/blockquote>\n\n\n\n<h5 class=\"wp-block-heading\">Warum ein rechteckiges Grid als Suchstrategie?<\/h5>\n\n\n\n<p>F\u00fcr die r\u00e4umliche Zerlegung gibt es zwei Optionen: <strong>kreisf\u00f6rmige Suchbereiche<\/strong> oder ein <strong>rechteckiges Grid<\/strong>. Radiusbasierte Suche klingt intuitiv, aber entweder \u00fcberlappen sich die Kreise oder es entstehen L\u00fccken.<\/p>\n\n\n\n<p>Ein rechteckiges Grid hat dieses Problem nicht. Jede Koordinate geh\u00f6rt zu genau einer Zelle, wodurch eine <strong>l\u00fcckenlose Abdeckung<\/strong> entsteht. Zus\u00e4tzlich l\u00e4sst sich ein rechteckiges Grid entlang der l\u00e4ngeren Achse halbieren, wobei die entstehenden Kinderzellen direkt wieder als Suchbereiche verwendet werden k\u00f6nnen.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Ein rechteckiges Grid deckt das Stadtgebiet l\u00fcckenlos ab und die Zellen lassen sich einfach teilen.<\/p>\n<\/blockquote>\n\n\n\n<h5 class=\"wp-block-heading\">Warum Binary Split statt Quadtree?<\/h5>\n\n\n\n<p>Ein <strong>Quadtree<\/strong> teilt jede Zelle gleichm\u00e4\u00dfig in vier Rechtecke. Bei jedem Split entstehen dadurch vier neue Zellen und vier zus\u00e4tzliche API-Requests. Das Problem dabei: In der Praxis hat h\u00e4ufig nur ein Teil der Zelle eine hohe Ergebnisdichte, die restlichen Rechtecke w\u00e4ren unn\u00f6tig.<\/p>\n\n\n\n<p>Mit einem <strong>Binary Split<\/strong> teilen wir dagegen entlang der l\u00e4ngeren Achse und erzeugen nur zwei neue Zellen. Dadurch passt sich das Grid besser an die Form des Gebiets an, da jede Zelle wiederholt entlang ihrer l\u00e4ngsten Seite geteilt wird.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Der Binary Split ist sparsamer als ein Quadtree und passt sich durch die Aufteilung besser an die tats\u00e4chliche Ergebnisdichte des Gebiets an.<\/p>\n<\/blockquote>\n\n\n\n<h5 class=\"wp-block-heading\">Warum Split-Threshold bei 55 statt 60<\/h5>\n\n\n\n<p>Das API-Maximum liegt bei <strong>60 Ergebnissen<\/strong>. Die API f\u00fchrt eine <strong>relevanzbasierte Suche<\/strong> durch und liefert nicht bei jeder Anfrage exakt dieselben Ergebnisse. Eine Zelle mit 58 Ergebnissen k\u00f6nnte in Wirklichkeit mehr L\u00e4den enthalten, abh\u00e4ngig vom Ranking der API.<\/p>\n\n\n\n<p>Durch das Splitten bei <strong>55<\/strong> teilen wir solche Zellen auf. Die beiden kleineren Anfragen liegen jeweils weit unter dem Limit und liefern zusammen zuverl\u00e4ssiger alle Ergebnisse.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Fr\u00fcher splitten f\u00fcr vollst\u00e4ndige Daten.<\/p>\n<\/blockquote>\n\n\n\n<h5 class=\"wp-block-heading\">Warum werden Grid-Zellen in der Datenbank persistiert?<\/h5>\n\n\n\n<p>Die Persistenz der Zellen erm\u00f6glicht, dass sich das Grid \u00fcber mehrere Durchl\u00e4ufe hinweg optimiert: <strong>Splitting<\/strong> und <strong>Merging<\/strong> passen die Zellen schrittweise an die tats\u00e4chliche Ergebnisdichte an und reduzieren so die Anzahl der API-Aufrufe. Ohne Persistenz w\u00fcrde man jedes Mal von vorne anfangen und dieser Fortschritt ginge verloren.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Die Datenbank speichert die Optimierungen des Grids.<\/p>\n<\/blockquote>\n\n\n\n<h5 class=\"wp-block-heading\">Warum Timer-Trigger mit 16-Stunden-Intervall statt Batch-Verarbeitung?<\/h5>\n\n\n\n<p><strong>Azure Functions<\/strong> sind f\u00fcr kurzlebige Ausf\u00fchrungen ausgelegt. Ein vollst\u00e4ndiger Batch-Durchlauf \u00fcber alle Zellen w\u00fcrde das <strong>Execution-Timeout<\/strong> \u00fcberschreiten und ist damit in einer Serverless-Umgebung nicht praktikabel. Der <strong>Timer-Trigger<\/strong> mit einer Zelle pro Ausf\u00fchrung l\u00f6st dieses Problem.<\/p>\n\n\n\n<p>Zus\u00e4tzlich verteilt der <strong>Timer-Trigger<\/strong> die API-Requests gleichm\u00e4\u00dfig \u00fcber die Zeit und vermeidet so <strong>Rate Limiting<\/strong>. Schl\u00e4gt ein Durchlauf fehl, wird nur diese eine Zelle beim n\u00e4chsten Mal erneut verarbeitet.<\/p>\n\n\n\n<p>Das 16-Stunden-Intervall ergibt sich direkt aus unserer Anforderung von 14 Tagen pro vollst\u00e4ndigem Durchlauf, was f\u00fcr einen D\u00f6ner-Guide mit geringer \u00c4nderungsrate ausreicht. Bei ~20 Zellen entspricht das einer Ausf\u00fchrung alle 16 Stunden.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Der Timer-Trigger macht die Place Search serverless-kompatibel.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">III. Challenges<\/h2>\n\n\n\n<h3 class=\"wp-block-heading has-large-font-size\">Challenge: State Management im Frontend<\/h3>\n\n\n\n<p>Die Explore Seite ist das zentrale Interface unseres D\u00f6nerguides: Benutzer k\u00f6nnen nach Preis, Stadtteil, Score, So\u00dfenmenge, Fleischanteil oder Eigenschaften wie halal bzw. vegetarisch filtern und die Ergebnisse zus\u00e4tzlich sortieren. Was zuerst wie eine einfache Filterliste aussah, stellte sich im Frontend schnell als State Management Challenge heraus.<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Ausgangssituation: UI, URL und Backend m\u00fcssen synchron bleiben<\/strong><\/h4>\n\n\n\n<p>F\u00fcr eine gute User Experience wollten wir drei Anforderungen gleichzeitig erf\u00fcllen:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Direktes UI Feedback:<\/strong> Filter (z. B. Slider oder Checkboxen) sollen sich sofort aktualisieren.<\/li>\n\n\n\n<li><strong>Persistenter Zustand \u00fcber die URL:<\/strong> Filter und Sortierung sollen bei Reload, Navigation oder Bookmark erhalten bleiben.<\/li>\n\n\n\n<li><strong>Kontrollierte Backend Requests:<\/strong> Interaktionen wie das Bewegen eines Sliders d\u00fcrfen nicht dazu f\u00fchren, dass st\u00e4ndig neue API Calls an das Backend geschickt werden.<\/li>\n<\/ul>\n\n\n\n<p>Diese drei Ziele stehen in einem gewissen Spannungsverh\u00e4ltnis: Bindet man die UI direkt an die URL, wirkt sie tr\u00e4ge. H\u00e4lt man alles nur lokal, gehen Navigation und Persistenz verloren.<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>L\u00f6sung: Trennung von UI State und URL State<\/strong><\/h4>\n\n\n\n<p>Unser Ansatz war eine klare Aufgabentrennung zwischen zwei States:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Der <strong>URL State<\/strong> (<code class=\"\" data-line=\"\">urlParams<\/code> \u00fcber <code class=\"\" data-line=\"\">nuqs<\/code>) wird debounced aktualisiert und repr\u00e4sentiert den persistenten Zustand der Seite.<\/li>\n\n\n\n<li>Der <strong>UI State<\/strong> (<code class=\"\" data-line=\"\">uiFilters<\/code>, <code class=\"\" data-line=\"\">uiSort<\/code>) wird direkt durch Nutzerinteraktionen aktualisiert und steuert die sichtbare Oberfl\u00e4che. <\/li>\n<\/ul>\n\n\n\n<p>Im Hook sieht diese Trennung so aus:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\" data-line=\"\">\/\/ URL state\nconst [urlParams, setUrlParams] = useQueryStates(exploreParsers)\n\n\/\/ UI state (instant feedback)\nconst [uiFilters, setUiFilters] = useState(urlFilters)\nconst [uiSort, setUiSort] = useState(urlParams.sort)<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Beispiel: Interaktion mit dem Preis Slider<\/strong><\/h4>\n\n\n\n<p>Bewegt ein Nutzer den Preis Slider, wird zun\u00e4chst nur der UI State aktualisiert:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\" data-line=\"\">const handleFiltersChange = () =&gt; {\n  setUiFilters(filters)  \/\/ sofortiges UI Update\n  updateFiltersDebounced(filters, uiSort) \/\/ verz\u00f6gertes URL Update\n}<\/code><\/pre>\n\n\n\n<p>Der Slider reagiert dadurch direkt. Die URL hingegen wird erst nach kurzer Inaktivit\u00e4t synchronisiert:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\" data-line=\"\">const DEBOUNCE_MS = 300\n\nconst updateFiltersDebounced = async (newFilters, sort) =&gt; {\n  window.setTimeout(async () =&gt; {\n    await setUrlParams(newFilters, sort)\n  }, DEBOUNCE_MS )\n}<\/code><\/pre>\n\n\n\n<p>Zieht man den Slider beispielsweise von 6 \u20ac auf 10 \u20ac, passieren mehrere UI Updates, aber nur <strong>ein<\/strong> URL Update nach Ende der Interaktion. Dadurch bleibt die Oberfl\u00e4che fl\u00fcssig, w\u00e4hrend der persistente Zustand konsistent gespeichert wird.<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Fetch-Trigger \u00fcber den URL-State<\/strong><\/h4>\n\n\n\n<p>Der eigentliche Daten Fetch h\u00e4ngt bewusst am URL State, nicht an der UI Interaktion. Sobald die URL aktualisiert wird, synchronisieren wir UI und Daten:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\" data-line=\"\">useEffect(() =&gt; {\n  setUiFilters(urlFilters)\n  setUiSort(urlParams.sort)\n  void fetchData(urlFilters, urlParams.sort)\n}, [urlParams, urlFilters, fetchData])<\/code><\/pre>\n\n\n\n<p>Die Update Kette lautet somit:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"100\" data-attachment-id=\"28642\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/gruppe-72-2\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1.png\" data-orig-size=\"1480,145\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Gruppe 72\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1-1024x100.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1-1024x100.png\" alt=\"\" class=\"wp-image-28642\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1-1024x100.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1-300x29.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1-768x75.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Gruppe-72-1.png 1480w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Sortierung behandeln wir bewusst anders<\/strong><\/h4>\n\n\n\n<p>Im Gegensatz zu Slidern wird die Sortierung nicht schrittweise ver\u00e4ndert, sondern einmalig ausgew\u00e4hlt (Dropdown). Deshalb wird die URL hier <strong>ohne Verz\u00f6gerung<\/strong> aktualisiert.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\" data-line=\"\">const handleSortChange = async (newSort) =&gt; {\n  setUiSort(newSort)\n  setUiFilters(filters)\n  updateFiltersImmediate(filters, newSort)\n}<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Stale Requests verhindern (Race Conditions)<\/strong><\/h4>\n\n\n\n<p>Wenn Filter schnell wechseln oder das Backend langsam antwortet, k\u00f6nnen Responses in falscher Reihenfolge zur\u00fcckkommen. Damit die alten Daten nicht die neuen \u00fcberschreiben, vergeben wir f\u00fcr jeden Request eine ID und stellen sicher, dass nur die <strong>letzte Anfrage<\/strong> \u00fcbernommen wird.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\" data-line=\"\">const requestIdRef = useRef(0)\n\nconst fetchData = async (filters, sort) =&gt; {\n  const currentRequestId = ++requestIdRef.current\n  const payload = await fetchPlaces(filters, sort)\n\n  if (currentRequestId !== requestIdRef.current) return \/\/ veraltete requests ignorieren\n\n  setStores(payload.items)\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge: Page Indexing von Slug-URLs f\u00fcr Azure<\/h3>\n\n\n\n<p>Ein Fehler ist bei uns furchtbar lange unter dem Radar geblieben: Wir hatten das Problem, dass einige unserer Stores nicht korrekt angezeigt wurden. Stattdessen bekamen die Nutzer einen unsch\u00f6nen Azure 404 Error zu sehen.<br><br>Das sah dann etwa so aus:<br><em><code class=\"\" data-line=\"\">https:\/\/doenerguide-stuttgart.de\/ChIJryPzk07bmUcRsC6cdRmB7Jk<\/code><\/em> -&gt; <strong>Azure 404<\/strong><br><br>Wir konnten es erst gar nicht glauben. Die API funktionierte einwandfrei und wir hatten sogar eine eigene D\u00f6nerguide-Errorpage entworfen, auf die bei fehlenden Daten verlinkt werden sollte. Lokal trat dieser Fehler nat\u00fcrlich nicht auf.<br><br>Nach viel Recherche fiel uns auf: Im Deployment-Log tauchten nur die Links auf, die auch tats\u00e4chlich funktionierten. Dort wurden lediglich 20 Routen geloggt, obwohl wir eigentlich hunderte Stores in unserer Datenbank haben und anzeigen wollen.<br><\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-24-at-23.14.41.png\"><img loading=\"lazy\" decoding=\"async\" width=\"986\" height=\"592\" data-attachment-id=\"28679\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/screenshot-2026-02-24-at-23-14-41\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-24-at-23.14.41.png\" data-orig-size=\"986,592\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot 2026-02-24 at 23.14.41\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-24-at-23.14.41.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-24-at-23.14.41.png\" alt=\"\" class=\"wp-image-28679\" style=\"width:888px;height:auto\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-24-at-23.14.41.png 986w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-24-at-23.14.41-300x180.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Screenshot-2026-02-24-at-23.14.41-768x461.png 768w\" sizes=\"auto, (max-width: 986px) 100vw, 986px\" \/><\/a><figcaption class=\"wp-element-caption\"><em>Bild der Deployment-Pipeline mit 20 definierten Store-Routen<\/em>.<\/figcaption><\/figure>\n\n\n\n<p><br>Es stellte sich heraus: Eine Azure Static Web App muss beim Deployment quasi &#8220;wissen&#8221;, welche statischen Links sie bereitstellen soll. Ansonsten zeigt Azure seinen eigenen 404-Fehler an.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Die L\u00f6sung: generateStaticParams<\/h4>\n\n\n\n<p>In NextJS gibt es eine feine Funktion f\u00fcr statische Webseiten, die genau dieses Problem l\u00f6st. Wir mussten NextJS dazu bringen, bereits beim Build-Prozess alle IDs unserer Stores abzufragen und als Parameter zu definieren.<br><br>Im Code nennen wir diese IDs slug und die L\u00e4den places. <br>Die L\u00f6sung sah dann so aus:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\" data-line=\"\">export async function generateStaticParams() {\n    \/\/ Alle verf\u00fcgbaren Pl\u00e4tze von der API abrufen\n    const data = await fetchPlaces(&#039;limit=2000&#039;) \n    const places = data.items || []\n    \n    \/\/ Jede Slug-ID an NextJS (und damit an Azure) \u00fcbergeben\n    return places.map((place: StoreBase) =&gt; ({\n        slug: place.slug,\n    }))\n}<\/code><\/pre>\n\n\n\n<p>Diese Definition sorgt daf\u00fcr, dass Azure beim Deployment die kompletten Metadaten vom NextJS-Frontend erh\u00e4lt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge: Deployment<\/h3>\n\n\n\n<p>Beim Deployment der Infrastrukturkomponenten hatten wir zwei unerwartete Probleme.<\/p>\n\n\n\n<p>Das erste betraf das Zusammenspiel von Terraform und dem Deployment-Prozess, der Functions. Bei jeder Infrastruktur\u00e4nderung hat Terraform das Function-Deployment \u00fcberschrieben, da deployment-relevante Felder nicht als <em>ignore_changes<\/em> markiert waren. Betroffen waren die Felder <em>WEBSITE_RUN_FROM_PACKAGE<\/em>, <em>WEBSITE_ENABLE_SYNC_UPDATE_SITE<\/em> und <em>site_config[0].application_stack<\/em>. Als Workaround haben wir zun\u00e4chst ein automatisches Redeployment aller Functions in die Infrastruktur-Pipeline eingebaut. Die saubere L\u00f6sung war, diese Felder in Terraform als `ignore_changes` zu markieren, sodass Infrastruktur und Deployment klar getrennt sind.<\/p>\n\n\n\n<p>Das zweite Problem betrifft das Azure-Tooling generell. Die <em>Azure\/functions-action<\/em> f\u00fcr GitHub Actions erf\u00fcllt ihren Zweck, bietet aber weniger Flexibilit\u00e4t als das Deployment anderer Anbieter, bei denen sich Functions direkt \u00fcber Terraform deployen lassen. Beim Frontend zeigte sich dasselbe Muster: Die <em>Azure\/static-web-apps-deploy<\/em>-Action lie\u00df sich nicht ausreichend an unsere Build-Anforderungen anpassen. Wir bauen das Frontend deshalb separat mit npm und nutzen die Action nur noch f\u00fcr das reine Deployment.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge: Docker Setup unter ARM<\/h3>\n\n\n\n<p>F\u00fcr die lokale Entwicklung emulieren wir die Azure-Infrastruktur vollst\u00e4ndig per Docker. Auf Apple Silicon M-Chips sind wir dabei auf zwei Kompatibilit\u00e4tsprobleme gesto\u00dfen.<\/p>\n\n\n\n<p>Das erste war schnell gel\u00f6st: Der Service Bus Emulator ben\u00f6tigt Microsoft SQL Edge als Datenbank, welche keinen ARM-Support hat. Als Ersatz konnten wir <em>mssql\/server:2022-latest<\/em> verwenden.<\/p>\n\n\n\n<p>Das zweite Problem war aufw\u00e4ndiger in der Fehlersuche. Die Azure Functions liefen in Node.js-Containern, die nat\u00fcrlich auch f\u00fcr ARM verf\u00fcgbar sind. Was wir nicht wussten: Die Azure Functions Dependency enth\u00e4lt ein Installationsskript, das bei der Installation die Functions Runtime aus dem Internet nachl\u00e4dt. Auf ARM schl\u00e4gt dieses Skript fehl, weil keine ARM-Runtime verf\u00fcgbar ist. Der entscheidende Punkt dabei: kein Fehler, kein Log. Die Functions starteten einfach nicht.<\/p>\n\n\n\n<p>Um die Ursache zu finden, haben wir den <em>node_modules<\/em>-Ordner zwischen einer Linux-x86-Installation und der M-Mac-Installation verglichen. Die Functions Runtime hat auf ARM gefehlt. Beim genaueren Untersuchen der Dependency, fanden wir das Installationsskript und sahen, wie es den Download-Link f\u00fcr die Runtime dynamisch zusammengebaut hat. Der Link existierte f\u00fcr ARM schlicht nicht.<\/p>\n\n\n\n<p>Die L\u00f6sung: <em>platform: linux\/amd64<\/em> in <em>compose.yaml<\/em> erzwingt x86-Emulation, Docker l\u00e4dt die korrekte Runtime und alles l\u00e4uft. Tradeoff ist eine sp\u00fcrbar schlechtere Performance durch die Emulation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge: Place Search Algorithmus Optimierung<\/h3>\n\n\n\n<p>Der Place Search Algorithmus wurde \u00fcber mehrere Iterationen hinweg optimiert. Jede \u00c4nderung war durch eine beobachtete Ineffizienz motiviert. Das Ziel war immer dasselbe: die Anzahl der API-Requests reduzieren, ohne die Abdeckung des Stadtgebiets zu verlieren. Die folgenden Abschnitte beschreiben die drei zentralen Herausforderungen und wie wir sie gel\u00f6st haben.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Ausgangslage: Das erste Grid<\/h4>\n\n\n\n<p>Die erste Version nutzte vier fest kodierte Koordinaten als Bounding Box um Stuttgart und teilte diese in ein fixes 4\u00d74-Grid mit 16 gleichm\u00e4\u00dfigen Zellen auf. Die Zellgr\u00f6\u00dfe war dadurch eine Konsequenz der 4\u00d74-Aufteilung. Au\u00dferdem waren die Zellen in Ost-West-Richtung geometrisch verzerrt, weil ein Grad L\u00e4ngengrad bei 48\u00b0N nur rund 73 km entspricht, aber ein Grad Breitengrad immer ~111 km. Gab es in einer Zelle mehr als 55 Ergebnisse, wurde sie in zwei Kinderzellen gesplittet. F\u00fcr alle Zellen innerhalb der Bounding Box wurde die API angefragt.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1.png\"><img loading=\"lazy\" decoding=\"async\" width=\"2185\" height=\"493\" data-attachment-id=\"28737\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/originale-implementierung-place-search-algorithm-2\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1.png\" data-orig-size=\"2185,493\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Originale Implementierung &amp;#8211; Place Search Algorithm\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1-1024x231.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1.png\" alt=\"\" class=\"wp-image-28737\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1.png 2185w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1-300x68.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1-1024x231.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1-768x173.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1-1536x347.png 1536w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/Originale-Implementierung-Place-Search-Algorithm-1-2048x462.png 2048w\" sizes=\"auto, (max-width: 2185px) 100vw, 2185px\" \/><\/a><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Herausforderung 1: Unn\u00f6tige Anfragen au\u00dferhalb der Stadtgrenze<\/h4>\n\n\n\n<p><strong>Problem:<\/strong> Die Eckzellen der Bounding Box lagen teilweise vollst\u00e4ndig au\u00dferhalb des Stuttgarter Stadtgebiets. Diese Zellen lieferten keine Ergebnisse, kosteten aber trotzdem jeweils einen API-Request.<\/p>\n\n\n\n<p><strong>Ursache:<\/strong> Die Bounding Box ist ein Rechteck, Stuttgart ist ein unregelm\u00e4\u00dfiges Polygon. Die Ecken eines Rechtecks um ein unregelm\u00e4\u00dfiges Gebiet enthalten Fl\u00e4chen ohne Relevanz.<\/p>\n\n\n\n<p><strong>L\u00f6sung<\/strong>: Wir haben die GeoJSON-Stadtgrenze Stuttgarts als Multipolygon hinterlegt. Bei der Grid-Erstellung wird jede Zelle mit <code class=\"\" data-line=\"\">booleanIntersects<\/code> aus Turf.js gegen dieses Polygon gepr\u00fcft. Nur Zellen, die tats\u00e4chlich mit dem Stadtgebiet \u00fcberlappen, werden ins Grid aufgenommen. Derselbe Filter wird auch beim Splitting angewendet: Wenn eine Kindzelle nach dem Split au\u00dferhalb der Stadtgrenze liegt, wird sie verworfen.<\/p>\n\n\n\n<p><strong>Ergebnis:<\/strong> Zwei Eckzellen des 4&#215;4-Grids wurden entfernt. Dadurch sank die durchschnittliche Anzahl der API-Requests von 26 auf 24, was einer Ersparnis von 8 % entspricht.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1.png\"><img loading=\"lazy\" decoding=\"async\" width=\"2196\" height=\"501\" data-attachment-id=\"28739\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/1-optimierung-stadtgrenze-place-search-algorithm-2\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1.png\" data-orig-size=\"2196,501\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"1. Optimierung- Stadtgrenze &amp;#8211; Place Search Algorithm\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1-1024x234.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1.png\" alt=\"\" class=\"wp-image-28739\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1.png 2196w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1-300x68.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1-1024x234.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1-768x175.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1-1536x350.png 1536w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/1.-Optimierung-Stadtgrenze-Place-Search-Algorithm-1-2048x467.png 2048w\" sizes=\"auto, (max-width: 2196px) 100vw, 2196px\" \/><\/a><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Herausforderung 2: Starres Grid ohne konfigurierbare Zellgr\u00f6\u00dfe<\/h4>\n\n\n\n<p><strong>Problem<\/strong>: Die Zellgr\u00f6\u00dfe war nicht auf das Stuttgarter Stadtgebiet abgestimmt. Das 4\u00d74-Grid erzeugte 16 Startzellen, ohne R\u00fccksicht darauf, ob diese Gr\u00f6\u00dfe f\u00fcr die tats\u00e4chliche Ergebnisdichte sinnvoll ist. Eine zu kleine Zellgr\u00f6\u00dfe bedeutet mehr Startzellen und damit mehr API-Requests von Anfang an, eine zu gro\u00dfe f\u00fchrt zu mehr Splits. Deswegen musste die richtige Gr\u00f6\u00dfe gefunden werden. Zus\u00e4tzlich war das Grid geometrisch verzerrt: Die Zellen waren in Ost-West-Richtung schmaler als in Nord-S\u00fcd-Richtung.<\/p>\n\n\n\n<p><strong>Ursache:<\/strong> Die Grid-Erstellung war hardcoded auf 4\u00d74. Dadurch war die Zellgr\u00f6\u00dfe eine Konsequenz der Aufteilung. Die geometrische Verzerrung entstand, weil die Zellgrenzen in Grad-Koordinaten berechnet wurden: Bei 48\u00b0N entspricht ein Grad L\u00e4ngengrad nur rund 73 km, ein Grad Breitengrad dagegen ~111 km.<\/p>\n\n\n\n<p><strong>L\u00f6sung:<\/strong> Wir haben auf ein <strong>kilometerbasiertes Grid<\/strong> umgestellt. Die Zellgr\u00f6\u00dfe ist jetzt ein konfigurierbarer Parameter. Die Anzahl der Zeilen und Spalten ergibt sich automatisch aus der Bounding Box und der gew\u00e4hlten Zielzellgr\u00f6\u00dfe. Wir rechnen nun Kilometer in Grad-Koordinaten um und korrigieren dabei die Verzerrung, die durch die Erdkr\u00fcmmung entsteht. Auch der Split-Algorithmus vergleicht Seitenl\u00e4ngen jetzt in Kilometer.<\/p>\n\n\n\n<p><strong>Ergebnis:<\/strong> Die Zellgr\u00f6\u00dfe wurde zum steuerbaren Parameter. Verschiedene Werte konnten per Benchmark verglichen werden. Eine Zielzellgr\u00f6\u00dfe von 7 km ergab ein 3&#215;3-Grid mit 9 Zellen statt 16. Das sind 7 Startzellen weniger. Gleichzeitig sind die Zellen geometrisch korrekt und der Split erfolgt entlang der tats\u00e4chlich l\u00e4ngeren Seite. Dadurch sank die durchschnittliche Anzahl der API-Requests von<strong> 24 auf 20<\/strong>, was einer Ersparnis von 23 % gegen\u00fcber dem Original entspricht.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1184\" height=\"462\" data-attachment-id=\"28741\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/2-optimierung-kilometerbasiertes-grid-place-search-algorithm-2\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1.png\" data-orig-size=\"1184,462\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"2. Optimierung- Kilometerbasiertes Grid &amp;#8211; Place Search Algorithm\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1-1024x400.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1.png\" alt=\"\" class=\"wp-image-28741\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1.png 1184w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1-300x117.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1-1024x400.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/2.-Optimierung-Kilometerbasiertes-Grid-Place-Search-Algorithm-1-768x300.png 768w\" sizes=\"auto, (max-width: 1184px) 100vw, 1184px\" \/><\/a><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Herausforderung 3: Sparse Zellen nach dem Splitting<\/h4>\n\n\n\n<p><strong>Problem:<\/strong> Nach einem vollst\u00e4ndigen Scan-Durchlauf hatten manche benachbarte Zellen nur wenige Ergebnisse. Beispielsweise k\u00f6nnte eine Zelle 8 und ihre Nachbarzelle 10 Ergebnisse enthalten. Jede dieser Zellen ben\u00f6tigt mindestens einen API-Request, obwohl sie zusammen nur 18 Ergebnisse h\u00e4tten und damit unter dem Limit von 20 pro Anfrage liegen. Also werden zwei Requests verbraucht, obwohl einer gen\u00fcgt h\u00e4tte.<\/p>\n\n\n\n<p><strong>L\u00f6sung:<\/strong> Nach dem Scan werden benachbarte Zellen durch einen Merging-Algorithmus zusammengef\u00fchrt. Ein Merge muss vier Bedingungen erf\u00fcllen:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Die Zellen m\u00fcssen eine vollst\u00e4ndige gemeinsame Kante teilen.<\/li>\n\n\n\n<li>Die kombinierten Ergebnisse d\u00fcrfen 40 nicht \u00fcberschreiten.<\/li>\n\n\n\n<li>Der Merge muss tats\u00e4chlich API-Requests einsparen.<\/li>\n\n\n\n<li>Die resultierende Zelle darf nicht gr\u00f6\u00dfer als 15<strong> <\/strong>km Seitenl\u00e4nge sein.<\/li>\n<\/ol>\n\n\n\n<p><strong>Die Kernidee<\/strong>: Ein Merge wird nur durchgef\u00fchrt, wenn er tats\u00e4chlich API-Requests einspart. Da die Google Places API maximal 20 Ergebnisse pro Request liefert, braucht eine Zelle mit 8 Ergebnissen einen Request und eine mit 10 Ergebnissen ebenfalls einen, zusammen also zwei. Merged man beide, hat die neue Zelle 18 Ergebnisse, die in einem einzigen Request abgedeckt werden. Der Merge spart also einen Request. Anders bei 18 und 7 Ergebnissen: Zusammen sind das 25, was immer noch zwei Requests erfordert. In diesem Fall findet kein Merge statt. Merges werden iterativ durchgef\u00fchrt, wobei zusammengef\u00fchrte Zellen erneut als Kandidaten f\u00fcr weitere Merges gelten.<\/p>\n\n\n\n<p><strong>Ergebnis:<\/strong> Durch das Merging sank die durchschnittliche Anzahl der API-Requests von <strong>20 auf 19,5<\/strong>, was einer Gesamtersparnis von <strong>25 %<\/strong> gegen\u00fcber dem Original entspricht.<\/p>\n\n\n\n<p><strong>Einschr\u00e4nkung:<\/strong> Bei einer Basiszellgr\u00f6\u00dfe von 7 km ist der Effekt des Mergings begrenzt. Sparse Zellen entstehen vor allem am Stadtrand, wo Zellen nur teilweise mit dem Stadtgebiet \u00fcberlappen und deshalb wenige Ergebnisse liefern. Bei 7 km gro\u00dfen Basiszellen gibt es jedoch nur wenige solcher Randzellen, sodass das Merge-Potenzial gering bleibt. Mit kleineren Basiszellen w\u00fcrden mehr Randzellen entstehen und damit auch mehr Merge-Kandidaten, allerdings steigt dann auch die Gesamtzahl der Startzellen, was den Vorteil wieder aufheben kann.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1183\" height=\"466\" data-attachment-id=\"28742\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/ai-meets-kebab-konzeption-und-umsetzung-des-donerguide-stuttgart\/3-optimierung-zellen-zusammenfu%cc%88hren-place-search-algorithm\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm.png\" data-orig-size=\"1183,466\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"3. Optimierung- Zellen zusammenfu\u0308hren &amp;#8211; Place Search Algorithm\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm-1024x403.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm.png\" alt=\"\" class=\"wp-image-28742\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm.png 1183w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm-300x118.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm-1024x403.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/3.-Optimierung-Zellen-zusammenfu\u0308hren-Place-Search-Algorithm-768x303.png 768w\" sizes=\"auto, (max-width: 1183px) 100vw, 1183px\" \/><\/a><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Ergebnisse<\/h4>\n\n\n\n<p>Die drei Optimierungen wurden mit einem eigenen Benchmark-Skript gegen die echte Google Places API gemessen. Der Benchmark f\u00fchrt alle Strategien unter identischen Bedingungen aus und z\u00e4hlt die tats\u00e4chlichen API-Requests.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Strategie<\/th><th>\u00d8 API-Requests<\/th><th>Ersparnis<\/th><\/tr><\/thead><tbody><tr><td>Original (4&#215;4-Grid)<\/td><td>26<\/td><td><\/td><\/tr><tr><td>Stadtgrenze nutzen<\/td><td>24<\/td><td>2 (8 %)<\/td><\/tr><tr><td>Konfigurierbare Zellgr\u00f6\u00dfe (7 km)<\/td><td>20<\/td><td>6 (23 %)<\/td><\/tr><tr><td>Zellen zusammenf\u00fchren<\/td><td>19,5<\/td><td>6,5 (25 %)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>\u00dcber 10 Benchmark-L\u00e4ufe ergibt sich eine <strong>Reduktion von 26 auf 19,5 Requests, also etwa 25 %<\/strong>. Die Einsparungen setzen sich aus drei Quellen zusammen: weniger Startzellen durch die <strong>Stadtgrenze-Filterung<\/strong>, eine optimierte Zellgr\u00f6\u00dfe durch das konfigurierbare kilometerbasierte Grid und gelegentliche Merges von sparse Zellen nach dem Scan.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge: Performance-Optimierung von Functions<\/h3>\n\n\n\n<p>Unsere Architektur nutzt Azure Functions, dessen Lebenszyklus in mehreren aufeinanderfolgenden Phasen abl\u00e4uft. Wenn die Funktion nach l\u00e4ngerer Inaktivit\u00e4t erstmals aufgerufen wird, kommt es zu einem sogenannten Cold Start. In dieser Phase wird der Code zun\u00e4chst geladen und anschlie\u00dfend ausgef\u00fchrt. Nach dieser initialen Ausf\u00fchrung wird die Funktion jedoch nicht sofort wieder abgeschaltet, sondern bleibt f\u00fcr etwa 10 bis 20 Minuten &#8220;warm&#8221; im Arbeitsspeicher (RAM) erhalten, um auf nachfolgende Anfragen verz\u00f6gerungsfrei reagieren zu k\u00f6nnen. Erfolgen in diesem Zeitfenster keine weiteren Aufrufe, wird die Instanz beendet. Ein wesentliches Merkmal dieses Modells ist die effiziente Ressourcennutzung, da stets nur die reine Ausf\u00fchrungszeit vom eingehenden Request bis zur finalen Response gemessen und abgerechnet wird.<\/p>\n\n\n\n<p><strong>Das Problem<\/strong>: Unser Frontend ben\u00f6tigte bei jedem Seitenwechsel oder beim Sortieren 5 bis 7 Sekunden Ladezeit. Der Grund war nicht der Cold Start, sondern ein ineffizientes Datenmanagement in der &#8220;Warm&#8221;-Phase. Jeder Klick l\u00f6ste eine komplett neue Abfrage an die CosmosDB aus. Dadurch verschlechterte sich die User Experience und es wurde bei jedem Request unn\u00f6tig Rechenkapazit\u00e4t verschwendet.<\/p>\n\n\n\n<p><strong>Die L\u00f6sung<\/strong> Wir haben den RAM der Azure Function, der nach dem ersten Aufruf ohnehin bestehen bleibt, strategisch genutzt, um dieses Problem zu beheben:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>In-Memory Caching:<\/strong> Wir speichern die Datenbank-Rohdaten in einer globalen Variablen. Solange die Funktion &#8220;warm&#8221; ist, werden nachfolgende Requests, wie Pagination oder Sortierung, direkt aus dem Arbeitsspeicher bedient. Das verhindert einen komplett neuen Aufruf an die Datenbank.<\/li>\n\n\n\n<li><strong>HTTP Cache-Control:<\/strong> Zus\u00e4tzlich senden wir einen Cache-Control-Header mit dem Wert \u201emax-age=60\u201d an das Frontend. Wenn ein Nutzer auf eine zuvor besuchte Seite navigiert, l\u00e4dt der Browser die Daten aus seinem lokalen Cache, was die Ladezeit deutlich reduziert.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge: Prompt-Engineering <\/h3>\n\n\n\n<p>Am Anfang unserer Prompt-Engineering Reise beschr\u00e4nkten sich unsere Bewertungstexte auf das Aussehen des zu bewertenden D\u00f6ner. Es fehlte das Menschliche, die Pers\u00f6nlichkeit, die echte Google Maps-Reviews ausmacht.<\/p>\n\n\n\n<p>Das Problem war offensichtlich: Unsere generierten Bewertungen waren viel zu generisch, um als authentische Texte von echten Menschen durchzugehen. Doch wie schafft man es, dass KI-generierte Bewertungen unterschiedlich klingen, so als w\u00fcrden verschiedene Menschen schreiben?<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Die L\u00f6sung: Personas im Prompt Engineering<\/strong><\/h4>\n\n\n\n<p>Die Antwort fanden wir in einem Konzept namens &#8220;Persona Prompting&#8221;. Statt der KI einfach zu sagen &#8220;Bewerte diesen D\u00f6ner&#8221;, erschaffen wir detaillierte Charaktere mit eigenen Pers\u00f6nlichkeiten, Pr\u00e4ferenzen und Schreibstilen. Diese Personas geben der KI einen Kontext und eine &#8220;Rolle&#8221;, in die sie schl\u00fcpfen kann.<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Warum funktioniert das? Die Wissenschaft dahinter<\/strong><\/h4>\n\n\n\n<p>Forschungsergebnisse zeigen, dass Persona Prompting besonders effektiv f\u00fcr offene, subjektive Aufgaben wie kreatives Schreiben ist [1]. D\u00f6ner-Bewertungen sind genau das: subjektive, offene Aufgaben, bei denen es um Meinungen zu Geschmack, Atmosph\u00e4re und Service geht.<\/p>\n\n\n\n<p>Ein weiterer Vorteil: Bei Restaurant-Reviews werden sowohl explizite Faktoren (Geschmack, Portionsgr\u00f6\u00dfe, Preis, Servicequalit\u00e4t) als auch implizite Pers\u00f6nlichkeitsmerkmale aus dem Schreibstil extrahiert. Personas helfen der KI, diese verschiedenen Ebenen nat\u00fcrlich miteinander zu verweben.<\/p>\n\n\n\n<h3 class=\"wp-block-heading has-large-font-size\">Unsere drei Personas: Vielfalt durch unterschiedliche Perspektiven<\/h3>\n\n\n\n<p>Wir haben uns f\u00fcr drei grundlegend verschiedene Reviewer-Typen entschieden, die unterschiedliche Zielgruppen und Bewertungsperspektiven repr\u00e4sentieren:<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Sasha (45) \u2013 Der traditionsbewusste Experte<\/strong><\/h4>\n\n\n\n<p>Sasha ist unser Qualit\u00e4tspr\u00fcfer mit t\u00fcrkischen Wurzeln und jahrelanger Gastronomie-Erfahrung. Er kennt die traditionelle Zubereitung in- und auswendig und bewertet mit kritischem, aber fairem Blick.<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Sarah (28) \u2013 Die Food-Bloggerin mit Auge f\u00fcrs Detail<\/strong><\/h4>\n\n\n\n<p>Sarah ist unsere enthusiastische Food-Bloggerin, die D\u00f6ner mit viel Liebe zum Detail und einem Auge f\u00fcr \u00c4sthetik bewertet. Sie denkt in Instagram-Perspektiven: &#8220;W\u00fcrde ich das fotografieren?&#8221;<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Marco (35) \u2013 Der pragmatische Stammkunde<\/strong><\/h4>\n\n\n\n<p>Marco ist unser Alltags-Experte, der 2-3 Mal pro Woche D\u00f6ner isst. Er bewertet pragmatisch mit Fokus auf Preis-Leistung und vergleicht st\u00e4ndig mit seinem Stammladen.<\/p>\n\n\n\n<p>Unsere Personas werden in dem Bewertungsteil unserer KI-Pipeline zuf\u00e4llig ausgew\u00e4hlt. Sie werden anschlie\u00dfend mit einem Basisprompt kombiniert, um diesselbe Grundstruktur im Ergebnis bei allen Personas zu gew\u00e4hrleisten.<\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Vor der Verwendung von Personas<\/strong> <\/h4>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>&#8220;bewertungstext&#8221;: &#8220;Der D\u00f6ner wirkt insgesamt sehr appetitlich und gut gef\u00fcllt;<br>die Fleischscheiben zeigen sichtbare R\u00f6staromen und wirken saftig.<br>Die Zutaten sehen frisch aus \u2014 knackiger Salat, Rucola und eine gebratene Paprikascheibe<br>sorgen f\u00fcr sch\u00f6ne Farbakzente, dazu ist die So\u00dfe cremig sichtbar. Schw\u00e4chen sind eine<br>leicht ungleichm\u00e4\u00dfige So\u00dfenverteilung und eine ungew\u00f6hnlich gro\u00dfe, runde Kartoffel in der<br>Mitte, die den Biss lokal ungleich macht. Das Fladenbrot ist stabil und h\u00e4lt die F\u00fcllung gut,<br>die saubere Pr\u00e4sentation und das helle Ambiente unterst\u00fctzen den positiven Gesamteindruck.&#8221;,<br>\u2028 &#8220;score_geschmack&#8221;: 8,\u2028 &#8220;score_belag&#8221;: 8,\u2028 &#8220;score_verhaeltnis&#8221;: 7,\u2028 &#8220;score_gesamt&#8221;: 86<\/p>\n<\/blockquote>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\"><strong>Nach der Verwendung von Personas<\/strong><\/h4>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>&#8220;bewertungstext&#8221;: &#8220;F\u00fcr den Preis passt das hier ganz gut.<br>Viel Fleisch und sch\u00f6n saftig, Gem\u00fcse ist frisch und die Portion macht satt \u2014 bei meinem Stammladen krieg ich<br>zwar manchmal noch ne Spur mehr Raffinesse, aber hier stimmt das Preis-Leistungs-Verh\u00e4ltnis.<br>Schw\u00e4che ist die So\u00dfe: eher sparsam und ungleichm\u00e4\u00dfig verteilt, das macht den D\u00f6ner teils etwas trocken.<br>Insgesamt kann man das locker als Alltagsessen empfehlen, hab schon bessere gesehen, aber auch deutlich schlechtere.&#8221;,<br>&#8220;score_geschmack&#8221;: 7,&#8221;score_belag&#8221;: 7,&#8221;score_fleischanteil&#8221;: 8,&#8221;score_so\u00dfenanteil&#8221;: 6,&#8221;score_gesamt&#8221;: 71<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">IV. Learnings<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Frontend<\/h3>\n\n\n\n<p>Wir haben w\u00e4hrend der Entwicklung gemerkt, wo unsere Entscheidungen richtig waren und wo wir uns beim n\u00e4chsten Mal vielleicht ein bisschen Arbeit gespart h\u00e4tten.<\/p>\n\n\n\n<p><strong>NextJS: Der Gewinner<\/strong><br>NextJS hat sich als ph\u00e4nomenal praktisches Framework herausgestellt. Die Routing-Struktur ist so intuitiv, dass wir uns oft gefragt haben: \u201eDas war\u2019s schon?\u201c. Ohne zu tief in die Nerd-Details zu versinken: NextJS hat uns w\u00e4hrend der Programmierung immer wieder mit echten \u201eDas ging aber schnell!\u201c-Momenten \u00fcberrascht.<br>Innerhalb des Teams hatten wir eine coole Mischung: Einige von uns brachten echtes Expertenwissen zum Framework mit, w\u00e4hrend der Rest des Teams hei\u00df darauf war, ein modernes Tool zu lernen. Diese Synergie hat perfekt funktioniert. Die Lernkurve war steil, aber dank der guten Dokumentation und Struktur von NextJS extrem belohnend.<\/p>\n\n\n\n<p><strong>DaisyUI: H\u00e4tten wir auch weglassen k\u00f6nnen<\/strong><br>Manchmal greift man im Eifer des Gefechts zu Tools, die man am Ende gar nicht so dringend braucht. <strong>DaisyUI<\/strong> war bei uns so ein Fall. R\u00fcckblickend h\u00e4tten wir es auch weglassen k\u00f6nnen.<br>Versteht uns nicht falsch: Es ist ein tolles Plugin, aber uns hat es im Endeffekt kaum mehr als eine vorgefertigte Farbpalette geliefert. Sp\u00e4testens als wir f\u00fcr unsere Filter komplexere UI-Elemente brauchten, wie zum Beispiel doppelseitige Slider f\u00fcr die Preis- oder Bewertungsauswahl, stie\u00df DaisyUI an seine Grenzen. Diese Komponenten mussten wir dann selbst bauen.<\/p>\n\n\n\n<p><strong>Fazit hier:<\/strong> Pures <strong>Tailwind CSS<\/strong> h\u00e4tte f\u00fcr unsere Anforderungen v\u00f6llig ausgereicht und das Projekt noch ein St\u00fcck schlanker gehalten.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Backend &amp; Infrastruktur<\/h3>\n\n\n\n<p>R\u00fcckblickend war unsere Architektur f\u00fcr den tats\u00e4chlichen Use Case \u00fcberdimensioniert. Die Anwendung muss nicht massiv skalieren, und unsere KI-Pipeline h\u00e4tte auch als einzelner Container zuverl\u00e4ssig funktioniert.<\/p>\n\n\n\n<p>Ein einfacherer Alternativstack h\u00e4tte ausgereicht: ein Docker-Container mit Cron-Trigger f\u00fcr die Pipeline und ein Always-on-Container f\u00fcr die API. Damit w\u00e4re das Deployment auf ein versioniertes Container-Image reduziert worden und mehrere der sp\u00e4teren Pain Points w\u00e4ren gar nicht erst entstanden: weniger Abh\u00e4ngigkeit von Azure-spezifischem Tooling, keine Cold Starts der API, und keine ARM-Inkompatibilit\u00e4ten rund um den Service-Bus-Emulator bzw. Workarounds wie ignore_changes in Terraform.<\/p>\n\n\n\n<p>Das Kernlearning ist weniger Azure-spezifisch: Komplexit\u00e4t sollte man dann hinzuf\u00fcgen, wenn man sie wirklich braucht, nicht auf Vorrat. Lieber simpel starten und bei Bedarf gezielt skalieren.<\/p>\n\n\n\n<p>Der entscheidende Punkt: Die KI-API-Kosten (OpenAI, Gemini) waren deutlich h\u00f6her als der Rest der Infrastrukturkosten. Die geringf\u00fcgig h\u00f6heren Infrastrukturkosten eines Always-on-Containers w\u00e4ren also irrelevant gewesen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Place Search Algorithmus<\/h3>\n\n\n\n<p><strong>Externe Constraints f\u00fcr die Architektur beachten:<\/strong> Die 60-Ergebnisse-Grenze der Google Places API hat uns gezwungen, die Architektur entsprechend auszurichten. Ohne dieses Limit w\u00e4re kein Grid, kein Splitting und kein Merging n\u00f6tig gewesen. Auch auf Detail-Ebene war das API-Verst\u00e4ndnis wichtig. Wir hatten zun\u00e4chst <code class=\"\" data-line=\"\">locationBias<\/code> verwendet, bis uns auffiel, dass nur <code class=\"\" data-line=\"\">locationRestriction<\/code> eine harte Grenze definiert.<\/p>\n\n\n\n<p><strong>Einfaches Grunddesign mit gezielter Verfeinerung:<\/strong> Da in der Innenstadt mehr Ergebnisse zu erwarten sind, wollten wir dort kleinere Zellen verwenden und au\u00dfen gr\u00f6\u00dfere. Das Problem dabei war, dass sich unterschiedlich gro\u00dfe Zellen nicht nahtlos aneinanderf\u00fcgen lassen, sodass man mit Overlap arbeiten muss. Der Overlap f\u00fchrt aber zu doppelter Abdeckung und macht es schwieriger, die richtige Gr\u00f6\u00dfe zu finden. Ein einheitliches Grid mit adaptivem Splitting war robuster.<\/p>\n\n\n\n<p><strong>Optimieren braucht Messbarkeit:<\/strong> Jede \u00c4nderung war durch eine konkret beobachtete Ineffizienz motiviert. Ob eine \u00c4nderung tats\u00e4chlich eine Verbesserung war, zeigte erst der Benchmark gegen die echte API. Ohne die wiederholbare Messung unter identischen Bedingungen w\u00e4ren die Einsparungen nur Vermutungen geblieben.<\/p>\n\n\n\n<p><strong>Serverless erzwingt inkrementelle Verarbeitung:<\/strong> Azure Functions haben ein Execution-Timeout, wodurch eine einzelne Ausf\u00fchrung nicht unbegrenzt laufen kann. Das hat uns dazu gezwungen, die Verarbeitung inkrementell zu denken. Jede Ausf\u00fchrung \u00fcbernimmt genau eine Zelle, der aktuelle Zustand wird in der Datenbank gespeichert und beim n\u00e4chsten Aufruf weitergef\u00fchrt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">KI-Pipeline und Images<\/h3>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\">Durch Probleme in der Google API-Schnittstelle sind fehlerhafte Eintr\u00e4ge in unserer Datenbank gelandet. Neben unseren D\u00f6ner des Vertrauens finden sich nun auch verschiedene andere Restaurants auf unserer Webseite. Diese besitzen keinen erkennbaren Score-Unterschied durch unsere Bewertung.Die KI bewertet also Nicht-D\u00f6nerl\u00e4den genauso wie echte D\u00f6nerl\u00e4den, ohne dies zu erkennen.<\/h4>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\">Bevor die Bewertungstexte durch die KI generiert werden, gibt es keine weitere \u00dcberpr\u00fcfung, ob das Restaurant tats\u00e4chlich D\u00f6ner anbietet oder die mitgelieferten Bilder von D\u00f6nern sind. Die KI hat somit keine Plausibilit\u00e4tspr\u00fcfung vor der Bewertung und geht immer davon aus, dass es sich um D\u00f6nerbilder handelt. Dadurch sucht sie selbst bei anderen Speisen nach D\u00f6nermerkmalen und interpretiert diese entsprechend. Hinzu kommt, dass wir aus Kostengr\u00fcnden keine Bilderbeispiele als Referenz mitliefern, wodurch alles, was entfernt nach Essen aussieht, als D\u00f6ner interpretiert wird.<\/h4>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\">Daraus lernen wir, dass unsere Pipeline vor der Bewertung eine strikte Input-Validierung braucht. Dabei m\u00fcssen die Bilder erstmals einen Precheck bestehen um bewertet zu werden. Anschlie\u00dfend k\u00f6nnte man einen Confidence-Threshold in der Ausgabe hinzuf\u00fcgen. Die KI kann damit ausdr\u00fccken mit welcher Sicherheit es sich um einen D\u00f6ner handelt.<\/h4>\n\n\n\n<h2 class=\"wp-block-heading\">V. Fazit<\/h2>\n\n\n\n<p>Der D\u00f6nerguide diente uns als praktisches Szenario, um das Zusammenspiel von moderner Cloud-Infrastruktur und k\u00fcnstlicher Intelligenz zu testen. Auch wenn die gew\u00e4hlte Serverless-Architektur f\u00fcr den produktiven Einsatz eher gro\u00dfz\u00fcgig dimensioniert war, bot sie die M\u00f6glichkeit, technologische Grenzen auszutesten und Praxiserfahrung mit dem Azure-Stack zu sammeln. Dabei demonstrierte der Einsatz verschiedener KI-Modelle und Prompting-Strategien, wie unstrukturierte Daten effizient in Formate wie KI-Reviews \u00fcbersetzt werden k\u00f6nnen. Letztlich vereinte das Projekt theoretische Konzepte wie State-Management und automatisierte Daten-Pipelines erfolgreich in einer funktionalen Anwendung.<br><\/p>\n\n\n\n<p>Quellen<strong>:<\/strong><br>[1]Bui, N., Nguyen, H.T., Kumar, S., Theodore, J., Qiu, W., Nguyen, V.A., &amp; Ying, R. (2025). <em>Mixture-of-Personas Language Models for Population Simulation.<\/em> In Findings of the Association for Computational Linguistics: ACL 2025 (pp. 24761-24778). <a href=\"https:\/\/arxiv.org\/abs\/2504.05019\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/arxiv.org\/abs\/2504.05019<\/a><\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I. Was ist der D\u00f6nerguide? Kurz gesagt: Ein studentisches Webprojekt zwischen Hunger, Daten, KI und Architekturentscheidungen. Problem Die Suche nach dem besten D\u00f6nerladen, der den eigenen Anforderungen entspricht ist oft gar nicht so einfach. Besonders in einer Stadt wie Stuttgart existieren hunderte D\u00f6nerl\u00e4den, mit stark unterschiedlicher Qualit\u00e4t, Preisen, \u00d6ffnungszeiten und Angeboten. Klassische Plattformen wie Google [&hellip;]<\/p>\n","protected":false},"author":1166,"featured_media":28647,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1,22,651,2],"tags":[355,227,986,7,1219,406,1213,50,463,1212],"ppma_author":[984,1204,1198,1201,1038,1217,1218],"class_list":["post-28620","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-allgemein","category-student-projects","category-system-designs","category-system-engineering","tag-ai","tag-azure","tag-backend","tag-cloud","tag-doner","tag-frontend","tag-functions","tag-infrastructure","tag-ki","tag-system-engineering-und-management"],"aioseo_notices":[],"jetpack_featured_media_url":"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/prasi-1-\u2013-49-1.png","jetpack-related-posts":[{"id":28240,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/01\/29\/open-source-ki-modelle-chancen-und-herausforderungen-fur-unternehmen-2\/","url_meta":{"origin":28620,"position":0},"title":"Open-Source-KI-Modelle \u2013 Chancen und Herausforderungen f\u00fcr Unternehmen","author":"Erzan Gashi","date":"29. January 2026","format":false,"excerpt":"Anmerkung:\u00a0Dieser Blogpost wurde f\u00fcr das Modul Enterprise IT (113601a) verfasst 1. Einleitung In heutiger Unternehmens-IT gewinnt Open-Source-K\u00fcnstliche Intelligenz (KI) immer mehr an Bedeutung. In einem Beitrag der Linux Foundation wird beschrieben, dass sich das Open-Source-Modell seit den 1980er-Jahren von einer Bewegung zu einem wichtigen Treiber technologischer Innovation entwickelt hat. Diese\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/01\/grafik.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/01\/grafik.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/01\/grafik.png?resize=525%2C300&ssl=1 1.5x"},"classes":[]},{"id":28402,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/20\/ai-aided-requirements-engineering-methodische-integration-von-llms-und-agenten-frameworks-in-den-anforderungsprozess\/","url_meta":{"origin":28620,"position":1},"title":"AI-Aided Requirements Engineering: Methodische Integration von LLMs und Agenten-Frameworks in den Anforderungsprozess","author":"Cedric Gottschalk","date":"20. February 2026","format":false,"excerpt":"Abstract Unklare und unvollst\u00e4ndige Anforderungen gelten als einer der Hauptgr\u00fcnde f\u00fcr das Scheitern von Softwareprojekten. Das vorliegende Paper untersucht den \u00dcbergang von klassischem Requirements Engineering (RE) hin zu einem KI-gest\u00fctzten Anforderungsprozess (AI-Aided RE). Zun\u00e4chst wird der Status Quo der Human-AI Collaboration (HAIC) im RE analysiert, wobei die Dominanz explorativer Proof-of-Concept-Ans\u00e4tze\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/CLEAR-1.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/CLEAR-1.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/CLEAR-1.png?resize=525%2C300&ssl=1 1.5x"},"classes":[]},{"id":25076,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2023\/07\/27\/sicherheit-geht-vor-die-soziale-verantwortung-der-unternehmen-in-bezug-auf-die-it-security\/","url_meta":{"origin":28620,"position":2},"title":"Sicherheit geht vor: Die soziale Verantwortung der Unternehmen in Bezug auf die IT-Security","author":"Michael Maximilian Bader","date":"27. July 2023","format":false,"excerpt":"In einer zunehmend vernetzten und digitalisierten Welt sehen sich Unternehmen einer Vielzahl von Cyberbedrohungen gegen\u00fcber. Laut einer Studie der Bitkom wird praktisch jedes deutsche Unternehmen irgendwann mit einem Cyberangriff konfrontiert sein, wodurch im letzten Jahr ein Schaden von 203 Milliarden Euro durch Diebstahl, Spionage und Sabotage entstanden ist [1]. Die\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/07\/it-sicherheit-AdobeStock_493619792-1980x700-1.jpeg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/07\/it-sicherheit-AdobeStock_493619792-1980x700-1.jpeg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/07\/it-sicherheit-AdobeStock_493619792-1980x700-1.jpeg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/07\/it-sicherheit-AdobeStock_493619792-1980x700-1.jpeg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/07\/it-sicherheit-AdobeStock_493619792-1980x700-1.jpeg?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/07\/it-sicherheit-AdobeStock_493619792-1980x700-1.jpeg?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":28304,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/08\/ai-security-leveraging-ai-for-security-automation\/","url_meta":{"origin":28620,"position":3},"title":"AI Security &#8211; Leveraging AI for Security automation","author":"Nico Giaquinta","date":"8. February 2026","format":false,"excerpt":"Dieser Blogpost wurde f\u00fcr das Modul Enterprise IT (113601a) verfasst. Lesezeit: 5 Minuten In der heutigen IT-Landschaft ist Cybersicherheit unausweichlich. Tausende Alerts, unz\u00e4hlige Logs und verd\u00e4chtige Aktivit\u00e4ten m\u00fcssen in Echtzeit analysiert werden. Traditionelle Sicherheitssysteme mit definierten Regeln sto\u00dfen an ihre Grenzen.Die Herausforderung: sich selbst verbreitende Ransomware, Phishing-Kampagnen mit Deepfakes und\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":28288,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/08\/open-source-ai-how-open-are-they-really\/","url_meta":{"origin":28620,"position":4},"title":"Open-Source AI: How open are they really?","author":"Vadim Dolgopolov","date":"8. February 2026","format":false,"excerpt":"Dieser Blogpost wurde f\u00fcr das Modul Enterprise IT (113601a) verfasst Abstract Die Demokratisierung der K\u00fcnstlichen Intelligenz ist so aktuell wie noch nie. Um sich von \"Black Box\"-Anbietern wie OpenAI unabh\u00e4ngig zu machen, setzen immer mehr Unternehmen auf vermeintlich offene Modelle wie LLaMA oder Mistral. Doch Vorsicht: Wo \"Open Source\" draufsteht,\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/open-diagram_3_small.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/open-diagram_3_small.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/open-diagram_3_small.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/open-diagram_3_small.png?resize=700%2C400&ssl=1 2x"},"classes":[]},{"id":27197,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2025\/02\/24\/open-source-ki-modelle-chancen-und-herausforderungen-fur-unternehmen\/","url_meta":{"origin":28620,"position":5},"title":"Open-Source-KI-Modelle: Chancen und Herausforderungen f\u00fcr Unternehmen","author":"David Vogt","date":"24. February 2025","format":false,"excerpt":"Einleitung KI gewinnt rasant an Bedeutung und ist in vielen Branchen bereits nicht mehr wegzudenken. Dabei spielt es keine Rolle, ob man auf den Aktienmarkt schaut, wo Unternehmen wie NVIDIA aufgrund ihres neuen Fokus auf KI durch die Decke gehen, oder auf Firmen, die KI t\u00e4glich zur Unterst\u00fctzung nutzen [6].\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"jetpack_sharing_enabled":true,"authors":[{"term_id":984,"user_id":1166,"is_guest":0,"slug":"jasmin-joy_springer","display_name":"js409","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/1f6b0be7c4d382e4436df225487fc66cd74c3f9b03189343fc7a76e2be638862?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""},{"term_id":1204,"user_id":1318,"is_guest":0,"slug":"kathrin_keubler","display_name":"Kathrin Keubler","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/293ef0e94d061d48a042ac672c71218d034be5140bf78ab582f79f5745c0c966?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""},{"term_id":1198,"user_id":1313,"is_guest":0,"slug":"ai_dinh","display_name":"Ai Nhu Dinh","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/16cd6fd6eb02d73de4bf4c7cbaeb54b9f8d499b32ecc776a03c3f73b5ff24984?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""},{"term_id":1201,"user_id":1316,"is_guest":0,"slug":"anna_rinck","display_name":"Anna Rinck","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/431ec91c0419debf115bd6b3e6ac967b03b2f719fc475e9debf0db2ff9318f65?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""},{"term_id":1038,"user_id":1215,"is_guest":0,"slug":"kay_knpfle","display_name":"Kay Kn\u00f6pfle","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/2d1443758d2a9573ae4184248346519b07734a699bfbec76e2f3bc599651a542?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""},{"term_id":1217,"user_id":1322,"is_guest":0,"slug":"jakob_gnster","display_name":"Jakob G\u00fcnster","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/d2a966dc0b952f35e52eba3fbc9d2cfe5ac45d511f85d792c531f85cefc49ca6?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""},{"term_id":1218,"user_id":1321,"is_guest":0,"slug":"jens_schlegel-2","display_name":"Jens Schlegel","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/cbd889b629b7f044c3c72a52c36ff39c90a4c933de89189e8b1b9f23790129c2?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""}],"_links":{"self":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/28620","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/users\/1166"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/comments?post=28620"}],"version-history":[{"count":49,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/28620\/revisions"}],"predecessor-version":[{"id":28898,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/28620\/revisions\/28898"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/media\/28647"}],"wp:attachment":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/media?parent=28620"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/categories?post=28620"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/tags?post=28620"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/ppma_author?post=28620"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}