271 mağazada tek dosyayla çalışan arama ekranı

Farklı mağazalara uyum sağlayan arama ekranı: stil çakışmaları, geç gelen yanıtlar, Türkçe harfler, yazım hataları ve filtreler.

Bir mağaza sayfası üzerinde ürün grid'i, kategori çipleri ve yazılmış bir sorguyla tam ekran arama overlay'i

Bir müşteri bize dokuz perakende sitesinin arama panelinden ve iki ticari arama ürününden ekran görüntüleri gönderdi. Tek bir sorusu vardı: biz bunlardan farklı ne yapabiliriz? Hepsine tek tek baktım. Bulduğum her fark yerleşimdi; alttaki motor hepsini zaten yapabiliyordu.

Bu cevap SearchKit'e dönüştü: Haziran'dan beri geliştirdiğim tam ekran arama overlay'i Search V2'nin üstünde çalışan bir preset (önceden hazırlanmış ayar veya görünüm şablonu) motoru. Bugün Revotas'ta tek bir script (belirli bir işi yapan kod dosyası) onu 271 mağazada çalıştırıyor. O sayfaların hiçbiri benim kontrolümde değil.

Katman mağazanın kendi sayfasının üstünde açılıyor. Onu tek bir script ekliyor; alttaki sayfa değişmiyor.

Neyle uğraşıyordum

V2'den önce After Search üzerinde çalışmıştım: her markanın kendi config (uygulamanın davranışını belirleyen ayarlar) dosyası ve özel CSS'i (sayfanın renklerini, boyutlarını ve yerleşimini belirleyen stil kuralları) olan bir sonuç sayfası widget'ı (sayfaya eklenen küçük işlevsel bileşen). Marka başına CSS'in ne kadar hızlı bir destek kuyruğuna dönüştüğünü orada öğrendim.

Overlay'in kuralları daha sertti:

  • Tek dosya. Vanilla JS, esbuild ile tek bir IIFE'ye (tanımlanır tanımlanmaz çalışan JavaScript fonksiyonu) paketleniyor. Kendi DOM'unu (tarayıcının sayfadaki öğeleri temsil ettiği yapı) kuruyor, kendi CSS'ini enjekte ediyor. Markalar onu zaten sahip oldukları bir widget alanından yüklüyor.
  • Başkasının sayfası. Her mağazanın kendi reset CSS'i, kendi header yapısı var. Bazısında başka bir arama eklentisi, bazısında loader'ımızı bekleten bir çerez onayı çıkıyor.
  • Benim yazmadığım bir backend (uygulamanın sunucuda çalışan bölümü). Arama, backend ekibinin sahibi olduğu mevcut Lucene tabanlı bir endpoint'te (bir serviste istek gönderilen belirli adres) çalışıyor. Ben onu tüketen taraftım. Yazım toleransı yok, çalışan facet (sonuçları marka, fiyat veya özelliklere göre daraltan filtre) yok, trend arama yok.
  • Türkçe. Katalogların da sorguların da çoğu.

V2 yaklaşık 3 bin satırda kaldı. SearchKit bunun üstüne yaklaşık 6,2 bin satır; V2'nin veri katmanını fork'lamak (mevcut çalışmadan bağımsız ilerleyen bir kopya oluşturma) yerine import ediyor.

Kaskad savaşı ve geri alınan karar

CSS izolasyonu için ilk refleksim @layer oldu: kit stilleri bir katmanda, marka override'ları onun üstünde.

İlk gerçek mağazalarda dağıldı. Katmansız stiller, specificity'ye (hangi CSS kuralının öncelikli olduğunu belirleyen seçici ağırlığı) hiç bakmadan katmanlı olanları yener. Markanın sıradan reset'leri, *{margin:0}, button{border:0}, input{padding:0}, bütün base CSS'imizi ezdi. Padding sıfırlandı, çipler düz metne döndü, grid boşlukları kayboldu.

@layer'ı geri aldım. Ayakta kalan kurulum şu:

  • Kit base'i ve preset token’ları (renk, boşluk ve yazı boyutu gibi ortak tasarım değerleri) katmansız kalıyor, .sk prefix'i sayesinde specificity'de kazanıyor.
  • Panel CSS'i (bir ekip arkadaşının belirli bir marka için yazdığı CSS) sonra gelen ayrı bir <style> içinde yükleniyor ve kit ona otomatik !important ekliyor. Bu dönüştürücü url('x;y.png') ya da @media içindeki kurallar gibi 15 kenar durumda test edildi.
  • Preset token'ları inline stil olarak değil, bir CSS kuralı olarak yazılıyor.

Bunu koruyan test cascade-test.html: 8 kontrol, 8'i de geçmeli. İlk sürümü hatayı kaçırdı, çünkü kiti temiz bir sayfada test ediyordu. Şimdiki sürümde sahte bir marka reset'i var.

Hâlâ bir artık kaldı. Prefix'li .sk button reset'i .sk-tab gibi tek sınıflı kuralları yeniyor; bazı sekmeler ~12px yerine 16px çıkıyor. Çözüm, reset'i :where() içine alıp specificity'sini sıfırlamak. Bu her mağazanın görünümünü aynı anda değiştirdiği için 1440px'te önce/sonra ölçümüyle çıkıyor.

Yarış kontrolü

Yazdıkça arama çok istek atar ve yavaş olan hep en son geliyormuş gibi gelir. O yüzden iki koruma var.

Veri katmanı tek bir AbortController tutuyor; her yeni arama uçuştaki isteği iptal ediyor. State store bir sıra numarası tutuyor ve yanıt yalnızca numarası hâlâ güncelse uygulanıyor. Bir istek 10 saniyelik zaman aşımına takılınca sıra ilerliyor, böylece geç gelen bir cevap kullanıcının o an baktığı şeyin üstüne yazamıyor.

Yüklemenin de kendi zaman çizelgesi var. İlk 550 ms hiçbir şey gösterilmiyor. Sonra skeleton (veri gelene kadar gerçek içeriğin yerini tutan yükleme iskeleti) geliyor, ama yalnızca önceki sonuç yoksa; varsa eski grid soluk halde ekranda kalıyor. 3 saniyede bir durum satırı, 10 saniyede iptal ve yeniden dene.

Paylaşılan abort controller'ın bir yan etkisi oldu. Her kategori çipi için sonuç sayısını arka planda önceden saymak istiyordum. Her deneme kullanıcının gerçek aramasını iptal ederdi. O yüzden sayılar gerçek tıklama ve hover'lardan öğreniliyor, 24 saat saklanıyor.

Türkçe metin bilinen numaraları bozuyor

Yaygın normalizasyon (farklı yazımları veya veri biçimlerini ortak kurala dönüştürme) tarifi NFD artı aksan temizliği. ı için çalışmıyor. Noktasız i ayrıştırılamayan atomik bir karakter, asla i olmuyor. Tek başına toLocaleLowerCase('tr') ise PIKE'ı pıke yaptı; o da Pike ile eşleşmedi ve tekilleştirmeyi bozdu. Yerine açık bir fold (arama için harflerin ortak bir yazıma dönüştürülmesi) haritası yazdım.

Sonucu somut: ASCII klavyeyle "yemek takimi" yazan kişi sıfır sonuç alıyor. Kit "Yemek Takımı" öneriyor ve o 20 sonuç döndürüyor.

Kategori adları mojibake ile geliyor (’ olması gereken yerde ’), bu yüzden bir onarım tablosu var. URL-decode da edilemiyorlar: "%60'a Varan İndirim" gibi bir adda decode (modelin yanıtı parça parça üretmesi) yüzde işaretini yer.

Ve fiyatlar. Ortak parser (yanıtı beklenen veri yapısına dönüştüren ayrıştırıcı) rakam ve nokta dışındaki her şeyi siliyordu. 31.000,00 TL verince 31.000 kalıyor, parseFloat bunu 31 okuyor. 31 bin liralık ürün 31 liralıkların yanında sıralanıyor.

İstemci tarafında yazım toleransı ve facet'ler

Backend'de yazım toleransı olmadığı için kitte küçük bir tane var. Kategori adları, önerilen sorgular ve son aramalardan kurulan bir sözlüğe karşı iki satırlık bir Levenshtein. En fazla 2 mesafe, dört harf ve altındaki kelimelerde 1'e iniyor ("bot" "not" olmasın). Yalnızca sıfır sonuçta çalışıyor, tek öneri veriyor ve sorguyu sessizce asla değiştirmiyor.

Facet'ler daha kötüydü. Endpoint daraltma parametrelerini yok sayıyor: minimum fiyatı 900.000 verince de aynı liste dönüyor. Eski istemci filtresi API'nin (yazılımların birbirine istek göndermesini sağlayan arayüz) hiç göndermediği alan adlarını arıyordu, yani facet'ler aslında hiç çalışmamıştı. Kit facet değerlerini ve sayılarını dönen ürünlerden türetiyor. Sayılar tam sonuç kümesinden geliyor, böylece bir facet seçmek diğerlerini silmiyor ve sıfır sayılı seçenek hiç oluşmuyor. Daraltma parametreleri istekte yine gidiyor; backend ekibi bir gün açarsa doğrudan çalışır.

Sahibi olmadığın sayfada erişilebilirlik

Dropdown kabuğu WAI-ARIA APG combobox kalıbını izliyor. Split panel ise role=region kullanıyor, çünkü bir listbox sekme ya da carousel (kartları sırayla veya kaydırarak gösteren bileşen) içeremez.

Vazgeçmediğim birkaç kural:

  • Hover ile klavye-aktif farklı görünüyor. Hover %60 dolgu, klavye-aktif tam dolgu artı 2px aksan çubuğu. Aynı görünürlerse klavye kullanıcısı imleci kaybeder.
  • Öneride yalnızca tamamlanan kısım kalın.
  • Azaltılmış hareket tercihinde yer değiştirme kapanıyor, solma kalıyor.
  • Kapanış markanın input'unu (girdi) olduğu gibi geri bırakıyor: artık role ya da aria-* kalmıyor.

Son ölçüm, 10 preset, 1440px ve 390px: sıfır taşma, 24px altında sıfır hedef, sıfır etiketsiz kontrol.

Saha hata ayıklama kataloğu

Zamanımın çoğu "bizim sitede açılmıyor" bildirimlerine gitti. Bundle'ı suçlamadan önce neye hangi sırayla baktığımı yazdım:

  1. Config'teki input selector'ı (sayfadaki bir öğeyi bulmak için kullanılan seçici) canlı sayfada say. Her sayfa tipinde tam 1 eleman eşleşmeli. Bir markanın selector'ı 7 input'a bağlanıyordu, Giriş'e tıklayınca arama açılıyordu.
  2. Çerez onayına bak. Bazı siteler ziyaretçi onay vermeden widget loader'ımızı yüklemiyor.
  3. Widget'ın tetikleme koşuluna bak.
  4. Session storage'da eski bir config var mı bak; açık başka bir sekme onu yeniden yayabiliyor.
  5. Arama ikonunu değiştiren rakip bir arama eklentisi var mı bak.

Her madde gerçek bir bildirimden geldi.

Preset'ler veri

SearchKit'te 4 kabuk (fullscreen, dropdown, split, inline) ve 16 blok var. Her blok aynı sözleşmeyi sunuyor: { element, update(state) }. Preset, hangi kabuğun, hangi bölgede hangi blokların ve hangi token'ların kullanılacağını anlatan bir JSON (veriyi alan ve değer çiftleriyle taşıyan metin biçimi). 10 preset dokuz ekran görüntüsünün hepsini ve bir keşif yerleşimini böyle karşılıyor.

Marka bir preset seçiyor, üstüne override'ları derin merge (ayrı geliştirme dalındaki değişikliği ana kodla birleştirme) ediyor. Bilinmeyen blok uyarıyla atılıyor, bilinmeyen kabuk default'a düşüyor; config'teki bir yazım hatası paneli düşüremiyor.

Markalar renk, tipografi ve şekli değiştirebiliyor. Boşluk ve grid global kalıyor. Bu kural var, çünkü V2 marka CSS'inin her şeyi ezmesine izin veriyordu ve her marka kendi CSS düzeltme döngüsüne dönüşmüştü.

Neyi farklı yapardım

Kaskad testini izolasyon stratejisini seçmeden önce, ilk gün, düşmanca bir marka reset'iyle kurardım. @layer'ı bir öğleden sonrada eleyebilirdi.

Gerçek faceted search ve gerçek trend arama sunucu tarafında iş istiyor; bugünkü trend sayacı tek bir tarayıcıda yaşıyor. Bundle henüz minify edilmedi (gzip ile yaklaşık 64 KB, minify ile 49 KB civarına iner). Kitin arayüz metinleri de Türkçe sabit.

Tek script, 271 mağaza, hiçbirinin üzerinde kontrol yok. Püf noktası her sayfayı düşmanca saymak ve testleri de öyle yazmaktı.

Etkileşimli demoyu deneyin. Revotas'taki diğer işlerim Revotas sayfasında.

Bu yazı İngilizce aslından Türkçeye çevrilmiştir.

Çözmeye değer bir problemin mi var?Birlikte çözelim

Konuşmaya başla

Full-stack ve uygulamalı AI rollerine açığım, uzaktan veya hibrit.

Rolü, ekibi ve problemi anlat.