Ανάλυση feedback · Πλάνο ενεργειών
Απάντηση στο feedback του Θοδωρή Γκίτσου (Digital Challenge) για το staging.youbath.gr. Η αναζήτηση η ίδια είναι γρήγορη· ο χρόνος χάνεται γύρω της. Το report εξηγεί πού, τι διορθώνουμε εμείς, τι όχι, με ποιον κίνδυνο, και ξεκινά από αυτά που λύνονται σήμερα χωρίς κώδικα.
Ο Θοδωρής έχει δίκιο ότι όπως είναι τώρα δεν περνάει από πελάτη. Δεν φταίει όμως το πλάνο, ο αριθμός προϊόντων ή η μηχανή αναζήτησης. Μετρήσαμε το ίδιο query, στο ίδιο index, από δύο δρόμους: μέσα από το WordPress του youbath (όπως το βλέπει ο επισκέπτης) και απευθείας από το playground μας. Η διαφορά είναι 10× και πάνω.
Με λίγα λόγια: κάθε γράμμα που πληκτρολογεί ο χρήστης κάνει ένα ταξίδι με τέσσερις στάσεις. Η μηχανή αναζήτησης είναι η πιο γρήγορη στάση. Οι δύο ενδιάμεσες, το WordPress τους και ο δικός μας API server, προσθέτουν σταθερό κόστος σε κάθε request, ανεξάρτητα από το τι ψάχνει κανείς. Τα υπόλοιπα σημεία του feedback (broad matching, σειρά αποτελεσμάτων, autocomplete, scroll) είναι πραγματικά, μικρότερα, και τα περισσότερα διορθώνονται με ρυθμίσεις ή λίγες γραμμές.
Η σειρά που προτείνουμε: πρώτα ό,τι γίνεται σήμερα από το admin panel χωρίς deploy (φάση 0), μετά τα backend quick wins που είχαμε ήδη εντοπίσει τον Μάιο και δεν έγιναν (φάση 1), μετά το plugin και το SDK (φάσεις 2–3), στο τέλος τα δευτερεύοντα και το feature των custom facets.
Έτσι ταξιδεύει μια αναζήτηση στο youbath σήμερα. Οι χρόνοι είναι από τις μετρήσεις της 16/9 (desktop, Chrome, από Ελλάδα).
Το πιο πειστικό στοιχείο: το ίδιο ακριβώς query, «πλακάκι πέτρας», τρεις φορές μέσα σε ένα λεπτό μέσω WordPress έκανε 684 ms, 2.670 ms και 2.755 ms, ενώ η μηχανή απάντησε και τις τρεις φορές σε 22–29 ms. Η διακύμανση δεν έχει σχέση με το τι ψάχνεις. Είναι φόρτος server.
| Query | Μηχανή | Μέσω WordPress | Απευθείας (playground) | Αποτελέσματα |
|---|---|---|---|---|
| πλακάκι πέτρας | 22 ms | 684 / 2.670 / 2.755 ms | 287 ms | 1.201 |
| νιπτήρας | 37 ms | 1.697 ms | — | 2.258 |
| καθρέπτης led | 23 ms | 2.804 ms | — | 1.164 |
| karen | 36 ms | 763 ms | 324 ms | 463 |
| μπαταρία νιπτήρα | 23 ms | 3.729 ms | — | 3.417 |
| έπιπλο μπάνιου 80 | 25 ms | 5.623 ms | — | 7.421 |
Και στην εμφάνιση του search bar (σημείο 1Β) το ίδιο μοτίβο: τα scripts ξεκινούν στα 2,3 s (φορτώνουν στο footer, μετά από όλη τη σελίδα), το SDK πριν εμφανίσει οτιδήποτε περιμένει τις ρυθμίσεις (784 ms) και τις μεταφράσεις (443 ms), πάλι μέσω WordPress, εμφανίζεται στα ≈3,5 s, και το custom CSS φορτώνει μετά (3,9 s), οπότε το bar φαίνεται πρώτα «γυμνό».
Σταθερό κόστος ανά request από δύο ενδιάμεσους σταθμούς, όχι από την αναζήτηση. Ο μεγαλύτερος είναι το WordPress proxy του plugin, ο δεύτερος ο δικός μας API server.
Plugin: κάθε request περνάει από /wp-json/searchx/v1/proxy/… → πλήρες WP bootstrap, 3 rate-limit counters σε transients (εγγραφές στη βάση όταν δεν υπάρχει object cache), νέα TLS σύνδεση προς εμάς σε κάθε κλήση (wp_remote_request, χωρίς keep-alive), και json_decode + re-encode ενός body 300–400 KB.
Backend: Key::where(token) + organization + application + applicationForIndexUid() τρεις φορές + queryBelongsToKeyApplication(), όλα χωρίς cache· δύο Log::info με ολόκληρο το request· attributesToHighlight=['*'] που διπλασιάζει το payload (κάθε πεδίο έρχεται δύο φορές, raw και highlighted).
Το ticket TICKET-instant-search-latency.md (Μάιος) τα είχε εντοπίσει. Έγινε μόνο το SDK debounce. Τα backend items δεν έγιναν.
Να αφαιρέσουμε τα logs, να κάνουμε cache το key/app resolution, να επιστρέφουμε μόνο τα πεδία της κάρτας με highlighting μόνο στον τίτλο, να κρατάμε ανοιχτή τη σύνδεση από το plugin και να μη μετατρέπουμε δύο φορές το JSON. Και να δώσουμε στο plugin ένα «direct mode»: ο browser να μιλάει απευθείας στο API μας, όπως ήδη κάνει το OpenCart plugin και το playground.
Να κάνουμε γρήγορο το WordPress τους. Το hosting, τα plugins και ο φόρτος του staging είναι δικά τους. Μπορούμε μόνο να περνάμε λιγότερα από μέσα του, ή καθόλου.
Logs: μηδενικός. Cache: πραγματικός αλλά οριοθετημένος· ένα revoked key ή suspended app θα συνεχίσει να δουλεύει έως 30 s, εκτός αν καθαρίζουμε το cache στο revoke/suspend (θα το κάνουμε) και εξαιρούμε τα playground keys. Λιγότερα πεδία: μόνο αν κάποιο theme περιμένει πεδίο που κόβουμε· γι' αυτό το ζητάει ρητά το SDK, δεν το κόβει το backend γενικά. Direct mode: νέο, προαιρετικό, δεν αγγίζει όσους ήδη χρησιμοποιούν proxy.
Το bar εμφανίζεται ≈3,5 s μετά την έναρξη φόρτωσης· ≈1,2 s από αυτά είναι αναμονή σε δύο proxy round-trips πριν το mount, και το CSS φορτώνει αφού εμφανιστεί.
Στο init() του SDK περιμένουμε Promise.all(i18n, authenticate, search-settings) πριν κάνουμε render. Το custom CSS URL έρχεται μέσα στα search-settings, άρα φορτώνει τελευταίο. Τα scripts είναι enqueued στο footer χωρίς defer/preload.
Να κάνουμε render αμέσως με τις ρυθμίσεις του init script και να εφαρμόζουμε τις backend ρυθμίσεις όταν φτάσουν· να φορτώνουμε το CSS παράλληλα (preload) αντί μετά· στο plugin, να βάζουμε <link rel="preconnect"> προς το CDN μας.
Να ελέγξουμε πόσα άλλα scripts φορτώνει το theme τους πριν τα δικά μας (217 resources στη σελίδα).
Μεσαίος: αν οι backend ρυθμίσεις διαφέρουν από το init script (π.χ. γλώσσα), θα υπάρχει ένα μικρό «πετάρισμα». Το init flow είναι κοινό για OpenCart, Woo, Shopify, οπότε θέλει testing και στα τρία.
«karen» φέρνει 463 αποτελέσματα, «έπιπλο fuji» 7.421, και το popup τα φορτώνει όλα με infinite scroll, οπότε στο κινητό ο χρήστης δεν φτάνει ποτέ στα suggested links.
Τρία πράγματα προστίθενται. (α) Το app ψάχνει σε όλα τα πεδία (25/25 searchable, δηλαδή wildcard): περιγραφές, URL, εικόνες. Με αναζήτηση μόνο σε τίτλο/brand/κωδικό το «karen» δίνει 18. (β) Δεν στέλνουμε matchingStrategy, οπότε το Meilisearch με default last πετάει την τελευταία λέξη όταν δεν βρίσκει και τις δύο· με all το «έπιπλο fuji» δίνει 3. (γ) Typo tolerance από 5 γράμματα: «karen» = «kare». (δ) Το popup χρησιμοποιεί useInfiniteHits χωρίς όριο.
Τα (α) και (γ) είναι ρυθμίσεις του app από το admin, σήμερα. Το (β) είναι μία γραμμή στο SDK, ως ρύθμιση ανά app. Το (δ) είναι μικρή αλλαγή στο SDK: όριο 20 στο popup και κουμπί «Εμφάνιση όλων».
Οι ρυθμίσεις αφορούν μόνο το youbath. Το matchingStrategy: all δεν πρέπει να γίνει global default: σε άλλα eshop το «κρέμα προσώπου ενυδατική» χωρίς προϊόν με και τις τρεις λέξεις θα έδινε 0 αντί για «κρέμες». Ρύθμιση ανά app.
Στο «πλακάκι πέτρας» τα «Keros Redstone … γρανίτη» (η λέξη πέτρα μόνο στην περιγραφή) βγαίνουν πάνω από τα «Keros Adobe … Πλακάκι πέτρα επένδυσης τοίχου» (στον τίτλο).
Δύο πράγματα μαζί, και το επαληθεύσαμε. Πρώτον, με wildcard searchable attributes το Meilisearch δεν ξέρει ότι ο τίτλος μετράει περισσότερο από την περιγραφή. Δεύτερον, ο τίτλος γράφει «πέτρα» ενώ ο χρήστης έγραψε «πέτρας»: για τη μηχανή αυτό είναι typo, και ο κανόνας «Typo Tolerance» τρέχει πριν τον «Attribute Priority». Έτσι η ακριβής λέξη «πέτρας» μέσα στην περιγραφή κερδίζει τη λέξη «πέτρα» στον τίτλο. Απόδειξη: «πλακάκι πέτρα» (χωρίς ς) φέρνει πρώτα τα Adobe.
Τρεις ρυθμίσεις από το admin, χωρίς κώδικα: ρητή λίστα searchable attributes (ο τίτλος μπαίνει αυτόματα πρώτος), συνώνυμο πέτρα ↔ πέτρας, και προαιρετικά μετακίνηση του «Attribute Priority» πάνω από το «Typo Tolerance» στα Ranking Rules.
Χαμηλός και αναστρέψιμος (Reset to defaults). Η αλλαγή στα Ranking Rules σημαίνει ότι ένα προϊόν με τη λέξη στον τίτλο (έστω με ένα «λάθος» γράμμα) θα κερδίζει ένα προϊόν με την ακριβή λέξη στην περιγραφή. Για e-shop αυτό είναι συνήθως το ζητούμενο, αλλά το ελέγχουμε σε 5–6 queries πριν το κρατήσουμε. Δεν το αλλάζουμε ως default σε υπάρχοντες πελάτες.
Το inline suggestion είναι ένα αόρατο αντίγραφο του κειμένου + το υπόλοιπο της λέξης, τοποθετημένα απόλυτα πάνω από το input (TypingPlaceholderInput.tsx, .searchx__inline-suggestion). Λειτουργεί μόνο αν font και padding ταυτίζονται με το input. Το custom CSS του youbath άλλαξε padding/font στο input, όχι στο overlay.
CSS fix στο custom.css του youbath (ίδιο padding/font στο .searchx__inline-suggestion), και ένα toggle στο SDK Config για να απενεργοποιείται όπου δεν ταιριάζει.
Μηδενικός· αφορά ένα CSS αρχείο ενός app.
SearchPage.tsx: τα κουμπιά pagination καλούν refinePagination(page) χωρίς scroll. Μόνο το «Apply» των mobile φίλτρων κάνει scrollTo.
Λίγες γραμμές: scroll στην κορυφή του results container μετά από αλλαγή σελίδας.
Μηδενικός.
Υπάρχει σχεδιασμένο ως Epic SS-547 (feed attributes → merchant-named dynamic facets), με spike SS-548 που αποφασίζει αν προχωράει. Δεν έχει ξεκινήσει. Είναι feature, όχι διόρθωση, και μπαίνει τελευταίο.
Σημείωση: η «διαθεσιμότητα» υπάρχει ήδη ως πεδίο in_stock (filterable) και υπάρχουν έτοιμα facets για κατηγορία, χρώμα, μέγεθος, τιμή, προσφορά που στο youbath είναι κλειστά. Μπορούμε να τα ανοίξουμε σήμερα (βλ. ρυθμίσεις).
Κάθε φάση έχει έναν μετρήσιμο στόχο. Δεν προχωράμε στην επόμενη πριν μετρήσουμε την προηγούμενη στο staging του youbath, με τα ίδια 6 queries του πίνακα παραπάνω.
Στόχος: «karen» < 50 αποτελέσματα, «πλακάκι πέτρας» με τα Adobe πρώτα, payload κάτω από 150 KB. Χρόνος: 30 λεπτά + reindex.
Λεπτομερώς στην ενότητα Ρυθμίσεις admin βήμα-βήμα. Αυτό λύνει τα 2 και 3 και κόβει ένα μέρος του 1Α (μικρότερο payload). Δεν λύνει το κύριο πρόβλημα ταχύτητας.
Στόχος: API floor από ≈300 ms σε ≈100–150 ms (μετράται στο playground, χωρίς WordPress).
Log::info από το InstantSearchController::search(). Παραμένουν τα warning/error.applicationForIndexUid() ανά request αντί για τρία loops (ίδιο αποτέλεσμα, λιγότερα queries).attributesToHighlight = μόνο title όταν το SDK δεν ζητάει άλλο (σήμερα ['*']).Στόχος: μέσω WordPress κάτω από 600 ms σταθερά στο staging τους. Με direct mode, ίσο με το playground.
wp_remote_request και httpversion 1.1.json_decode/re-encode) όταν το upstream επιστρέφει έγκυρο JSON.preconnect προς sdk/css/admin.searchxengine.ai και defer στα scripts.Στόχος: search bar ορατό σε < 1,5 s από την έναρξη στο staging τους· popup με max 20 αποτελέσματα.
hitsPerPage 20, χωρίς infinite scroll, κουμπί «Εμφάνιση όλων (N)» που ήδη υπάρχει ως link.matchingStrategy στο SDK Config (Default / all / frequency), ανά app.attributesToRetrieve (τα πεδία της κάρτας) και attributesToHighlight: ['title'].SearchPage.tsx).Ξεκινά με το spike SS-548 (3 μέρες, σκανάρισμα 10 πραγματικών feeds). Αν περάσει το go/no-go, ακολουθούν SS-549 έως SS-556. Μέχρι τότε, στο youbath ανοίγουμε τα υπάρχοντα facets.
Όλα στο Applications → Youbath POC → Settings. Η σειρά έχει σημασία: τα βήματα 1–3 προκαλούν ένα reindex (≈17.700 προϊόντα, λίγα λεπτά)· κάνε τα με τη σειρά και περίμενε να τελειώσει το background task πριν το επόμενο Save, αλλιώς το admin απαντά «A background task is already running». Τα βήματα 4–5 εφαρμόζονται ζωντανά, χωρίς reindex.
Advanced → Searchable Attributes (σήμερα 25/25). Ξετσέκαρε: URL, Image, Additional Images, Price, Condition, Availability, Unique ID, In Stock, On Sale, Effective Price. Άφησε τσεκαρισμένα: Title, Description, Brand, MPN, Color, και όλα τα πεδία κατηγορίας. Πάτα Save Changes.
Τι πετυχαίνει: από wildcard (όλα τα πεδία, χωρίς προτεραιότητα) περνάμε σε ρητή λίστα με τον Title πρώτο· η μηχανή σταματά να ψάχνει σε URLs και εικόνες. Η σειρά στη λίστα είναι αυτή που φαίνεται στην οθόνη (Title, Description, Brand…) και δεν αλλάζει από το UI, γι' αυτό χρειάζονται και τα βήματα 4–5 για την περίπτωση «πέτρας».
Advanced → Displayed Attributes (σήμερα 25/25). Ξετσέκαρε: Description, Condition, Availability. Save Changes.
Τι πετυχαίνει: η περιγραφή είναι το μεγαλύτερο πεδίο· χωρίς αυτήν η απάντηση μικραίνει περίπου στο μισό. Το SDK δεν την εμφανίζει (Product Cards → Product description = Off), οπότε δεν χάνεται τίποτα. Κράτησε τα Additional Images (χρησιμοποιούνται στο hover της κάρτας).
Αν αργότερα ανοίξει το «Product description» στις κάρτες, πρέπει να ξανατσεκαριστεί εδώ το Description, αλλιώς οι κάρτες θα βγαίνουν χωρίς περιγραφή.
Advanced → Ranking Rules (προαιρετικό, δοκίμασέ το αφού μετρήσεις τα 1–2 και 4–5). Σύρε το Attribute Priority πάνω από το Typo Tolerance, ώστε η σειρά να γίνει: Sort → Words → Attribute Priority → Typo Tolerance → Word Proximity → Exactness. Save Changes.
Τι πετυχαίνει: ό,τι ταιριάζει στον τίτλο (ακόμα κι αν διαφέρει κατά ένα γράμμα, π.χ. πέτρα/πέτρας) κερδίζει ό,τι ταιριάζει ακριβώς μόνο στην περιγραφή. Αναστρέψιμο με «Reset to defaults». Τσέκαρε μετά 5–6 queries («πλακάκι πέτρας», «νιπτήρας», «καθρέπτης led», «karen», «μπαταρία νιπτήρα», «έπιπλο fuji»).
Advanced → Typo Tolerance. One-typo threshold 5 → 6, Two-typo threshold 9 → 10. Εφαρμόζεται σε δευτερόλεπτα, χωρίς reindex.
Τι πετυχαίνει: λέξεις 5 γραμμάτων («karen») πρέπει πλέον να ταιριάξουν ακριβώς· το «kare» φεύγει. Οι ελληνικές λέξεις 6+ γραμμάτων κρατούν την ανοχή στον τόνο/λάθος γράμμα.
Content → Synonyms. Πρόσθεσε: πέτρα ↔ πέτρας, πλακάκι ↔ πλακάκια, νιπτήρας ↔ νιπτήρα. Save.
Τι πετυχαίνει: οι πτώσεις/πληθυντικοί που το Meilisearch δεν καταλαβαίνει στα ελληνικά παύουν να μετράνε ως typo. Εφαρμόζεται ζωντανά. Είναι η πιο στοχευμένη και ασφαλής λύση για το παράδειγμα του Θοδωρή, ανεξάρτητα από το βήμα 3. Από τα analytics (Top search terms: επιπλο fuji, industrial, πλακάκι πέτρα, πλακάκια, πλακάκια μπάνιου) προσθέτουμε ό,τι άλλο εμφανιστεί.
SDK Config → Facets & Filters (προαιρετικό, απαντά μερικώς στο 5). Category facet On, Color filter On αν το feed έχει χρώματα, On sale filter On. Εφαρμόζεται αμέσως.
SDK Config, τι δεν αλλάζουμε: το Search Page URL και το Platform έρχονται από το plugin (ένδειξη «Init script» / «Default»)· αν τα πατήσουμε εδώ θα υπερισχύσουν του plugin. Το AI search μένει Off (δεν υπάρχει embedder). Δεν υπάρχει ακόμα ρύθμιση για όριο αποτελεσμάτων στο popup ούτε για matchingStrategy· αυτά έρχονται στη φάση 3.
Πώς επιβεβαιώνουμε ότι δούλεψε: μετά το reindex, στο staging.youbath.gr: «karen» → λιγότερα από 50 αποτελέσματα και χωρίς «Tema Kare»· «πλακάκι πέτρας» → τα «Keros Adobe … Πλακάκι πέτρα» πρώτα· στο Network tab, το response του instant-search κάτω από 150 KB (decoded).
| Ενέργεια | Πού | Λύνει | Προσπάθεια | Κίνδυνος | Αναστρέψιμο |
|---|---|---|---|---|---|
| Searchable / Displayed attributes, typo, synonyms | Admin (youbath) | 2, 3, μέρος 1Α | 30′ + reindex | χαμηλός | Ναι (Reset) |
| Ranking rules: Attribute πάνω από Typo | Admin (youbath) | 3 | 5′ + reindex | μεσαίος αλλάζει σειρά σε όλα τα queries του app | Ναι (Reset) |
| Αφαίρεση debug logs | Backend | 1Α | 1 ώρα | μηδενικός | Ναι |
| Cache key/app resolution (30 s) | Backend | 1Α | 1 μέρα | μεσαίος revoked key ενεργό έως 30 s χωρίς invalidation | Ναι (feature flag) |
| Highlight μόνο title by default | Backend | 1Α | 2 ώρες | μεσαίος αν κάποιο theme κάνει highlight σε άλλο πεδίο | Ναι |
| Keep-alive, body pass-through, preconnect | WP plugin | 1Α, 1Β | 1 μέρα | χαμηλός | Ναι |
| Direct mode (restricted key) | WP plugin + backend | 1Α, 1Β | 2 μέρες | μεσαίος νέο key type, origin lock· προαιρετικό | Ναι (opt-in) |
| Popup cap 20 + «Εμφάνιση όλων» | SDK | 2 | ½ μέρα | χαμηλός | Ναι |
| matchingStrategy ως ρύθμιση | SDK + admin | 2 | ½ μέρα | χαμηλός (ανά app, default αμετάβλητο) | Ναι |
| attributesToRetrieve / Highlight από SDK | SDK | 1Α | ½ μέρα | μεσαίος λίστα πεδίων πρέπει να καλύπτει όλα τα themes | Ναι |
| Render πριν τα settings, CSS preload | SDK | 1Β | 1 μέρα | μεσαίος flicker αν backend ≠ init script· 3 πλατφόρμες | Ναι |
| Scroll-to-top, autocomplete toggle + CSS | SDK / CSS | 3β, 4 | ½ μέρα | μηδενικός | Ναι |
| Custom facets | Epic SS-547 | 5 | spike 3 μέρες, μετά ~3 εβδ. | feature | — |
Θοδωρή, το κοιτάξαμε αναλυτικά και στο staging σας και στον κώδικα. Η ταχύτητα δεν έχει σχέση με πλάνο ή αριθμό προϊόντων: η μηχανή αναζήτησης απαντά σε 10–30 ms σε όλα τα queries που δοκιμάσαμε, και το ίδιο index απευθείας (χωρίς το WordPress ενδιάμεσα) απαντά σε ~0,3 s. Ο χρόνος χάνεται στο ότι κάθε αναζήτηση περνάει μέσα από το WordPress REST API (το plugin το κάνει για να μη φαίνεται το API key) και σε δικά μας overheads στο API που έχουμε ήδη εντοπίσει. Έχουμε συγκεκριμένο πλάνο σε τρία βήματα: ρυθμίσεις που κάνουμε σήμερα, βελτιώσεις στο API μέσα στην εβδομάδα, και μια επιλογή στο plugin ώστε η αναζήτηση να μην περνάει καθόλου από το WordPress. Για τα υπόλοιπα (broad matching, σειρά, autocomplete, scroll) τα περισσότερα είναι ρυθμίσεις ή μικρές αλλαγές και θα τα δεις να αλλάζουν σταδιακά στο staging. Θα σου στείλουμε μετρήσεις πριν/μετά.
| Θέμα | Αρχείο |
|---|---|
| Debug logs, uncached lookups | ai-search-service/app/Http/Controllers/API/v1/InstantSearchController.php (Log::info ×2, applicationForIndexUid σε 3 loops) |
| Key lookup χωρίς cache | app/Http/Middleware/AuthenticateV1WithBearerToken.php L121, L186, L204 |
| Highlight '*' και σειριακά search calls | app/Services/Meilisearch.php multiSearch() |
| Default searchable = wildcard, ranking rules, typo | app/Constants/SearchSettings.php · εφαρμογή στο app/Jobs/IndexProductsJob.php configureAdvancedSettings() |
| WP proxy | searchx-for-woocommerce.php 2.5.1: searchx_proxy_request() (wp_remote_request timeout 15, json_decode/encode), searchx_proxy_rate_limit() (3 counters), searchx_enqueue_assets() |
| SDK init & mount | searchx-components/src/lib/index.tsx init() Promise.all πριν το render |
| Popup infinite hits, debounce 120 ms | src/components/SearchBar.tsx L112, L239–257, L348 |
| Inline autocomplete overlay | src/components/TypingPlaceholderInput.tsx, src/App.css .searchx__inline-suggestion |
| Pagination χωρίς scroll | src/components/SearchPage.tsx refinePagination |
| Query χωρίς matchingStrategy / attributesToRetrieve | src/utils/meilisearchClient.ts toMeiliQuery() |
| Προηγούμενο ticket | Documents/Claude/Projects/SearchX/TICKET-instant-search-latency.md (30/05/2026) |
Σημείωση για το review: το τοπικό clone του searchx-components είναι στο 2.2.0 ενώ στο CDN τρέχει 2.5.1· οι αναφορές γραμμών είναι από το 2.2.0 και θέλουν επιβεβαίωση στο τρέχον main πριν ανοίξουν PRs.
Feedback analysis · Action plan
Response to Thodoris Gkitsos's (Digital Challenge) feedback on staging.youbath.gr. The search engine itself is fast; the time is lost around it. This report shows where, what we can fix, what we cannot, at what risk, and starts with what can be fixed today without code.
Thodoris is right that, as it stands, this will not pass a client. But the cause is not the plan, the catalogue size, or the search engine. We measured the same query, on the same index, through two routes: via youbath's WordPress (what a visitor sees) and directly from our playground. The difference is 10× and more.
In short: every character the shopper types makes a trip with four stops. The search engine is the fastest stop. The two in the middle, their WordPress and our API server, add a fixed cost to every request regardless of what is searched. The other feedback items (broad matching, ranking, autocomplete, scroll) are real, smaller, and mostly fixable with settings or a few lines.
Proposed order: first everything that can be done today from the admin panel with no deploy (phase 0), then the backend quick wins we identified in May and never shipped (phase 1), then the plugin and the SDK (phases 2–3), and finally the secondary items and the custom-facets feature.
This is how a search travels on youbath today. Timings are from the 16 Sep measurements (desktop Chrome, from Greece).
The most telling data point: the exact same query, "πλακάκι πέτρας", three times within a minute through WordPress took 684 ms, 2,670 ms and 2,755 ms, while the engine answered all three in 22–29 ms. The variance has nothing to do with the query. It is server load.
| Query | Engine | Via WordPress | Direct (playground) | Hits |
|---|---|---|---|---|
| πλακάκι πέτρας | 22 ms | 684 / 2,670 / 2,755 ms | 287 ms | 1,201 |
| νιπτήρας | 37 ms | 1,697 ms | — | 2,258 |
| καθρέπτης led | 23 ms | 2,804 ms | — | 1,164 |
| karen | 36 ms | 763 ms | 324 ms | 463 |
| μπαταρία νιπτήρα | 23 ms | 3,729 ms | — | 3,417 |
| έπιπλο μπάνιου 80 | 25 ms | 5,623 ms | — | 7,421 |
The search bar appearing late (item 1B) follows the same pattern: our scripts start at 2.3 s (enqueued in the footer, after the whole page), the SDK waits for settings (784 ms) and translations (443 ms), again through WordPress, before rendering anything, the bar mounts at ≈3.5 s, and the custom CSS loads after that (3.9 s), so the bar first appears unstyled.
Fixed per-request cost from two middle stops, not from the search. The biggest is the plugin's WordPress proxy; the second is our own API server.
Plugin: every request goes through /wp-json/searchx/v1/proxy/… → full WP bootstrap, 3 rate-limit counters in transients (DB writes when there is no object cache), a new TLS connection to us on every call (wp_remote_request, no keep-alive), and json_decode + re-encode of a 300–400 KB body.
Backend: Key::where(token) + organization + application + applicationForIndexUid() three times + queryBelongsToKeyApplication(), all uncached; two Log::info calls with the full request; attributesToHighlight=['*'], which doubles the payload (every field is returned twice, raw and highlighted).
The May ticket TICKET-instant-search-latency.md had already identified this. Only the SDK debounce shipped. The backend items did not.
Remove the logs, cache key/app resolution, return only card fields with highlighting on the title, keep the plugin's connection to us open and stop re-encoding JSON. And give the plugin a "direct mode": the browser talks to our API directly, as the OpenCart plugin and the playground already do.
Make their WordPress fast. Hosting, plugins and the load on their staging are theirs. We can only pass less through it, or nothing.
Logs: none. Cache: real but bounded; a revoked key or suspended app keeps working for up to 30 s unless we invalidate on revoke/suspend (we will) and exclude playground keys. Fewer fields: only if a theme expects a field we drop, so the SDK requests them explicitly rather than the backend trimming globally. Direct mode: new, opt-in, does not touch anyone already on the proxy.
The bar mounts ≈3.5 s after navigation starts; ≈1.2 s of that is waiting on two proxy round-trips before mount, and the CSS loads after it appears.
The SDK's init() awaits Promise.all(i18n, authenticate, search-settings) before rendering. The custom CSS URL arrives inside search-settings, so it loads last. Scripts are enqueued in the footer with no defer/preload.
Render immediately with the init-script settings and apply backend settings when they arrive; preload the CSS in parallel instead of after; add <link rel="preconnect"> to our CDN from the plugin.
Control how many other scripts their theme loads before ours (217 resources on the page).
Medium: if backend settings differ from the init script (e.g. language) there will be a brief flicker. The init flow is shared by OpenCart, Woo and Shopify, so it needs testing on all three.
"karen" returns 463 hits, "έπιπλο fuji" 7,421, and the popup loads them all with infinite scroll, so on mobile the shopper never reaches the suggested links.
Several things stack. (a) The app searches every field (25/25 searchable, i.e. wildcard): descriptions, URLs, images. Searching title/brand/MPN only, "karen" returns 18. (b) We do not send matchingStrategy, so Meilisearch's default last drops the last word when both do not match; with all, "έπιπλο fuji" returns 3. (c) Typo tolerance from 5 letters: "karen" = "kare". (d) The popup uses useInfiniteHits with no cap.
(a) and (c) are per-app admin settings, today. (b) is one line in the SDK, exposed as a per-app setting. (d) is a small SDK change: cap the popup at 20 and use the existing "Show all" link.
The settings affect youbath only. matchingStrategy: all must not become the global default: on another shop, "κρέμα προσώπου ενυδατική" with no product carrying all three words would return 0 instead of "creams". Per-app setting.
For "πλακάκι πέτρας", "Keros Redstone … γρανίτη" (stone only in the description) ranks above "Keros Adobe … Πλακάκι πέτρα επένδυσης τοίχου" (in the title).
Two things together, verified. First, with wildcard searchable attributes Meilisearch has no way to know the title matters more than the description. Second, the title says "πέτρα" while the shopper typed "πέτρας": to the engine that is a typo, and the "Typo Tolerance" rule runs before "Attribute Priority", so the exact word "πέτρας" in a description beats "πέτρα" in a title. Proof: "πλακάκι πέτρα" (no ς) returns the Adobe tiles first.
Three admin settings, no code: an explicit searchable-attributes list (title automatically first), a synonym πέτρα ↔ πέτρας, and optionally moving "Attribute Priority" above "Typo Tolerance" in Ranking Rules.
Low and reversible (Reset to defaults). The ranking-rule change means a product with the word in its title (even one letter off) beats a product with the exact word only in its description. For an e-shop that is usually what you want, but we verify on 5–6 queries before keeping it. Not changed as a default for existing customers.
The inline suggestion is an invisible copy of the typed text plus the completion, absolutely positioned over the input (TypingPlaceholderInput.tsx, .searchx__inline-suggestion). It only works if font and padding match the input exactly. Youbath's custom CSS changed the input's padding/font but not the overlay's.
CSS fix in youbath's custom.css (same padding/font on .searchx__inline-suggestion), plus an SDK Config toggle to disable it where it does not fit.
None; one CSS file of one app.
SearchPage.tsx: the pagination buttons call refinePagination(page) with no scroll. Only the mobile filters' "Apply" calls scrollTo.
A few lines: scroll to the top of the results container after a page change.
None.
Designed as Epic SS-547 (feed attributes → merchant-named dynamic facets), gated by spike SS-548. Not started. It is a feature, not a fix, and goes last.
Note: "availability" already exists as the filterable field in_stock, and ready-made facets for category, colour, size, price and on-sale exist but are switched off on youbath. We can enable them today (see settings).
Each phase has a measurable goal. We do not move to the next phase before measuring the previous one on youbath's staging with the same 6 queries from the table above.
Goal: "karen" < 50 hits, "πλακάκι πέτρας" with the Adobe tiles first, payload under 150 KB. Time: 30 minutes + reindex.
Detailed in Admin settings step by step. This fixes items 2 and 3 and trims part of 1A (smaller payload). It does not fix the main speed problem.
Goal: API floor from ≈300 ms to ≈100–150 ms (measured on the playground, no WordPress).
Log::info calls from InstantSearchController::search(). Keep warning/error logging.applicationForIndexUid() per request instead of three loops (same result, fewer queries).attributesToHighlight = title only when the SDK does not ask otherwise (today ['*']).Goal: under 600 ms consistently via WordPress on their staging. With direct mode, equal to the playground.
wp_remote_request, httpversion 1.1.json_decode/re-encode) when upstream returns valid JSON.preconnect to sdk/css/admin.searchxengine.ai and defer on the scripts.Goal: search bar visible in < 1.5 s from navigation start on their staging; popup capped at 20 hits.
hitsPerPage 20, no infinite scroll, "Show all (N)" button (the link already exists).matchingStrategy setting in SDK Config (Default / all / frequency), per app.attributesToRetrieve (card fields) and attributesToHighlight: ['title'].SearchPage.tsx).Starts with spike SS-548 (3 days, scan of 10 real feeds). If it passes go/no-go, SS-549 to SS-556 follow. Until then, we enable the existing facets on youbath.
Everything under Applications → Youbath POC → Settings. Order matters: steps 1–3 each trigger one reindex (≈17,700 products, a few minutes); do them in order and wait for the background task to finish before the next Save, otherwise the admin replies "A background task is already running". Steps 4–5 apply live, no reindex.
Advanced → Searchable Attributes (currently 25/25). Untick: URL, Image, Additional Images, Price, Condition, Availability, Unique ID, In Stock, On Sale, Effective Price. Keep ticked: Title, Description, Brand, MPN, Color, and all category fields. Click Save Changes.
What it does: switches from wildcard (all fields, no priority) to an explicit list with Title first; the engine stops searching URLs and image paths. The list order is the one shown on screen (Title, Description, Brand…) and cannot be changed from the UI, which is why steps 4–5 are also needed for the "πέτρας" case.
Advanced → Displayed Attributes (currently 25/25). Untick: Description, Condition, Availability. Save Changes.
What it does: the description is the largest field; without it the response roughly halves. The SDK does not show it (Product Cards → Product description = Off), so nothing is lost. Keep Additional Images (used for the card hover image).
If "Product description" is later switched on for cards, Description must be re-ticked here, otherwise cards will render without it.
Advanced → Ranking Rules (optional; try after measuring 1–2 and 4–5). Drag Attribute Priority above Typo Tolerance, so the order becomes: Sort → Words → Attribute Priority → Typo Tolerance → Word Proximity → Exactness. Save Changes.
What it does: anything matching in the title (even one letter off, e.g. πέτρα/πέτρας) beats anything matching exactly only in the description. Reversible with "Reset to defaults". Check 5–6 queries afterwards ("πλακάκι πέτρας", "νιπτήρας", "καθρέπτης led", "karen", "μπαταρία νιπτήρα", "έπιπλο fuji").
Advanced → Typo Tolerance. One-typo threshold 5 → 6, Two-typo threshold 9 → 10. Applies within seconds, no reindex.
What it does: 5-letter words ("karen") must now match exactly; "kare" goes away. Greek words of 6+ letters keep tolerance for an accent or a wrong letter.
Content → Synonyms. Add: πέτρα ↔ πέτρας, πλακάκι ↔ πλακάκια, νιπτήρας ↔ νιπτήρα. Save.
What it does: Greek cases/plurals that Meilisearch does not understand stop counting as typos. Applies live. This is the most targeted and safest fix for Thodoris's example, independent of step 3. From Analytics (top terms: επιπλο fuji, industrial, πλακάκι πέτρα, πλακάκια, πλακάκια μπάνιου) we add whatever else shows up.
SDK Config → Facets & Filters (optional, partly answers item 5). Category facet On, Color filter On if the feed has colours, On sale filter On. Applies immediately.
SDK Config, what we do not touch: Search Page URL and Platform come from the plugin ("Init script" / "Default" markers); setting them here would override the plugin. AI search stays Off (no embedder). There is no setting yet for a popup result cap or matchingStrategy; those arrive in phase 3.
How we confirm it worked: after the reindex, on staging.youbath.gr: "karen" → fewer than 50 hits and no "Tema Kare"; "πλακάκι πέτρας" → the "Keros Adobe … Πλακάκι πέτρα" tiles first; in the Network tab, the instant-search response under 150 KB (decoded).
| Action | Where | Fixes | Effort | Risk | Reversible |
|---|---|---|---|---|---|
| Searchable / Displayed attributes, typo, synonyms | Admin (youbath) | 2, 3, part of 1A | 30′ + reindex | low | Yes (Reset) |
| Ranking rules: Attribute above Typo | Admin (youbath) | 3 | 5′ + reindex | medium changes ordering on every query of the app | Yes (Reset) |
| Remove debug logs | Backend | 1A | 1 hour | none | Yes |
| Cache key/app resolution (30 s) | Backend | 1A | 1 day | medium revoked key live up to 30 s without invalidation | Yes (feature flag) |
| Highlight title only by default | Backend | 1A | 2 hours | medium if a theme highlights another field | Yes |
| Keep-alive, body pass-through, preconnect | WP plugin | 1A, 1B | 1 day | low | Yes |
| Direct mode (restricted key) | WP plugin + backend | 1A, 1B | 2 days | medium new key type, origin lock; opt-in | Yes (opt-in) |
| Popup cap 20 + "Show all" | SDK | 2 | ½ day | low | Yes |
| matchingStrategy as a setting | SDK + admin | 2 | ½ day | low (per app, default unchanged) | Yes |
| attributesToRetrieve / Highlight from SDK | SDK | 1A | ½ day | medium field list must cover every theme | Yes |
| Render before settings, CSS preload | SDK | 1B | 1 day | medium flicker if backend ≠ init script; 3 platforms | Yes |
| Scroll-to-top, autocomplete toggle + CSS | SDK / CSS | 3b, 4 | ½ day | none | Yes |
| Custom facets | Epic SS-547 | 5 | 3-day spike, then ~3 weeks | feature | — |
Thodoris, we went through it in detail, both on your staging and in the code. Speed has nothing to do with the plan or the number of products: the search engine answers in 10–30 ms on every query we tried, and the same index called directly (without WordPress in between) answers in ~0.3 s. The time is lost because every search goes through the WordPress REST API (the plugin does that to keep the API key off the browser) plus overheads on our API side that we have already pinned down. We have a concrete three-step plan: settings we apply today, API improvements within the week, and a plugin option so search does not go through WordPress at all. For the rest (broad matching, ordering, autocomplete, scroll) most are settings or small changes and you will see them land on staging progressively. We will send before/after measurements.
| Topic | File |
|---|---|
| Debug logs, uncached lookups | ai-search-service/app/Http/Controllers/API/v1/InstantSearchController.php (Log::info ×2, applicationForIndexUid in 3 loops) |
| Uncached key lookup | app/Http/Middleware/AuthenticateV1WithBearerToken.php L121, L186, L204 |
| Highlight '*' and sequential search calls | app/Services/Meilisearch.php multiSearch() |
| Default searchable = wildcard, ranking rules, typo | app/Constants/SearchSettings.php · applied in app/Jobs/IndexProductsJob.php configureAdvancedSettings() |
| WP proxy | searchx-for-woocommerce.php 2.5.1: searchx_proxy_request() (wp_remote_request timeout 15, json_decode/encode), searchx_proxy_rate_limit() (3 counters), searchx_enqueue_assets() |
| SDK init & mount | searchx-components/src/lib/index.tsx init() Promise.all before render |
| Popup infinite hits, 120 ms debounce | src/components/SearchBar.tsx L112, L239–257, L348 |
| Inline autocomplete overlay | src/components/TypingPlaceholderInput.tsx, src/App.css .searchx__inline-suggestion |
| Pagination without scroll | src/components/SearchPage.tsx refinePagination |
| Query without matchingStrategy / attributesToRetrieve | src/utils/meilisearchClient.ts toMeiliQuery() |
| Earlier ticket | Documents/Claude/Projects/SearchX/TICKET-instant-search-latency.md (30 May 2026) |
Review note: the local searchx-components clone is at 2.2.0 while the CDN serves 2.5.1; line references are from 2.2.0 and need confirming against current main before opening PRs.