Health Dashboard, 5. deo: problem EnergyBank-a
Peti deo mog build log-a za Health Dashboard: kako se dnevni energetski score koji je radio na papiru slomio na drugom korisniku, zašto se redizajn oslanja na 14-dnevni contraction mapping i šta znači graduated trust kada sat nije na zglobu.
U 4. delu audit je oborio readiness formulu i izvukao paralelni nalaz: EnergyBank v1 je imao pogrešan znak korelacije sa HRV1 sledećeg dana (r=−0.149) i RHR2 sledećeg dana (r=+0.212) na 387 uparenih dana.
Pogrešan znak korelacije je jaka tvrdnja. To znači da je sama ideja živa, ali je target bio pogrešno postavljen: ono što je v1 merio verovatno je bilo productive strain, a ne recovery deficit. Ta dva pokazatelja koreliraju mnogih dana i razilaze se upravo tamo gde je najvažnije: tokom tihog, ali anksioznog dana, tokom bolesti ili kada sat nije na zglobu.
Ovaj deo je o redizajnu koji je izrastao iz tog nalaza. Tehničko jezgro je malo: state machine, contraction mapping i skup trust states. Zanimljivije je drugo: od kojih konkretnih grešaka svaki od tih elemenata štiti sistem.
Trenutak koji je pokrenuo redizajn
Prvi konkretan artefakt bio je screenshot mog sopstvenog dashboard-a. Ujutru 2026-05-12 glavni blok je prikazivao traku na 59%, a odmah ispod nje tekst: „Ostalo je samo 25% kapaciteta posle današnjeg opterećenja, držite intenzitet laganim“. Ta dva broja se nisu poklapala, jer su dolazila iz dve različite formule. Traka se već računala po v2, a tekst se i dalje generisao po v1.
To je bilo prelazno half-cutover stanje koje sam svesno pustio. v2 brojevi i v2 sparkline već su se prikazivali u glavnom bloku, ali su v1 verdict pragovi (rest / active_recovery / moderate / push_hard) i tekst koji ih objašnjava ostali aktivni: v1 pragovi su imali stvarnu production kalibraciju, a v2 pragovi još nisu. Plan je bio da v2 sakupi sedam dana distribucije i da se zatim ponovo kalibriše.
Pet dana kasnije proverio sam podatke ispod toga:
| datum | jutarnji kapacitet v2 | EOD bank v2 | kapacitet v1 | verdict v1 |
|---|---|---|---|---|
| 05-08 | 37 (bootstrap) | 27 | 83 | rest |
| 05-09 | 77 | 36 | 92 | rest |
| 05-10 | 80 | 48 | 94 | rest |
| 05-11 | 79 | 31 | 85 | rest |
| 05-12 | 80 | 59 | 83 | rest |
Verdict v1 bio je rest svakog dana, jer je formula current = readiness − allostatic_drain u v1 davala ≤25 praktično svakog dana za osobu sa umereno aktivnim navikama. Preporuka formule ostajala je ista bez obzira na ulazne podatke. Istovremeno, jutarnji kapacitet v2 kretao se od 37 do 80, a EOD bank v2 od 27 do 59. U v2 koloni postojao je stvaran signal. v1 kolona je ponavljala „rest“ kao pokvaren sat koji je ipak tačan dva puta dnevno.
Half-cutover plan je implicitno pretpostavljao da je v1 validan baseline u odnosu na koji mogu da kalibrišem v2. Podaci su pokazali da je v1 degenerisan. Plan „sačekati sedam dana“ pretvorio se u plan backfill-ovati v2 kroz istoriju i pustiti istoriju da kalibriše pragove. Oko 630 EOD tačaka za period 2024-07 → 2026-05 bilo je dovoljno da se opsezi postave pošteno.
Taj backfill je postao cmd/energy_backfill. Ujedno je postao trenutak kada je počela cross-tenant priča: čim je v2 morao da radi na retrospektivnim podacima, „radi na mom laptopu“ prestalo je da bude argument.
Šta je radio v1
internal/health/energy.go — v1 fajl koji i dalje stoji u repozitorijumu — računa daily snapshot:
drain = strain · (1 + 0.5 · stress / 100)
restore = sleep_quality_units
bank = clamp(restore − drain, 0, 100)
Stres je ovde bio množilac opterećenja. Dan sa malo active calories ostaje mali, kojim god stress factor-om ga pomnožite. Tih, sedeći, ali autonomno opterećujući dan — recimo dan kada sam, kao i obično, šetao psa, ali je on imao alergijsku reakciju i ja sam ceo dan brinuo za njega — skoro da nije pravio drain u formuli, jer je množilac množio skoro nultu bazu. Bank se nije pomerao. Grafikon je govorio „fresh“, telo se nije slagalo.
Drugi problem v1 bio je strukturni: bank se resetovao svakog dana. Jučerašnji ostatak nije prelazio u danas. Tri loše noći zaredom nisu vukle jutarnju početnu tačku sve niže; svaki dan bio je nezavisan snapshot. Za metriku koja treba da opisuje sustained recovery, to je pogrešan oblik.
Audit je metodološki problem učinio konkretnim. r=−0.149 u odnosu na HRV sledećeg jutra nije „slab signal u očekivanom smeru“. To je signal koji pokazuje u pogrešnom smeru. Formula je hvatala nešto stvarno, ali ono što je hvatala — aktivni produktivni dani jače troše bank — nije predviđalo autonomic recovery, koji zavisi od drugog skupa faktora.
Redizajn u jednoj formuli
EnergyBank v2 je state machine:
bank[t] = bank[t−1] + restore(t−1 → t) − drain(t−1 → t)
Ista ideja kao Garmin Body Battery, ali sa dve ključne korekcije:
- Nema dnevnih resetovanja. Bank se prenosi između dana. Loša noć povlači jutarnju početnu tačku naniže. Dobra noć dopunjava bank ka plafonu, ali ga ne zakucava za njega.
- Drain je aditivan, ne multiplikativan.
drain(Δt) = α · active_energy_kcal + β · sustained_hr_load. Same kalorije pokrivaju većinu dana. Članβ · sustained_hr_loadhvata stresne sedeće dane koje kalorije propuštaju. U release ide saβ = 0i postaje nenult samo per-user posle validation rubric — to je teritorija 6. dela.
Tri svojstva asymptotic restore-a bila su toliko važna da je dizajn građen oko njih.
Deficit sna kroz više noći se akumulira. Restore se računa kao capacity_today = bank_yesterday + (100 − bank_yesterday) · sleep_quality. Što je bank bliže maksimumu, to je prirast manji. Jedna loša noć — 5.4h, 9. april u validation dataset-u — drži bank na najviše 73/100 sledećeg jutra, čak i posle normalnih 7.1h sna sledeće noći.
Capacity se nikada ne zakucava za plafon. Tokom 31 simuliranog dana aditivna varijanta se zalepila za 100 u 28 slučajeva. Asimptotska varijanta nije dotakla plafon nijednom. Donji rep bank-a takođe dobija stvaran raspon: p10 = 17 naspram 35 u aditivnom obliku. To znači da grafikon konačno nosi informaciju.
structure_factor samo kažnjava, nikada ne nagrađuje. Asimptota već daje gornju granicu. Bonus težine bi samo jurile plafon koji već postoji. Zato factor ostaje u opsegu [0.5, 1.0].
Cold start i contraction mapping
Stateful formula ima pitanje cold start-a: odakle početi bank ako nema jučerašnjeg stanja?
Dosadan odgovor je čuvati seed. Takvi dosadni odgovori u ovakvim sistemima brzo postanu dugoročni dug. Svaki bump formula_version, svaka migracija, svako „promenili smo računanje sleep_quality“ pretvara se u razgovor o tome koji seed i kako treba preneti.
Asimptotski restore se ispostavlja kao contraction mapping3. Bilo koji seed se eksponencijalno zaboravlja kroz faktor (100 − bank) · sleep_quality. Na stvarnim podacima tri seed-a {10, 50, 90} konvergiraju na razliku manju od 0.005 do 7. dana i postaju praktično identični do 9. dana.
To znači da stanje seed-a uopšte ne treba čuvati. Svaki recompute prolazi unapred kroz poslednjih 14 dana od hardkodovanog bank = 50:
function compute_today_bank():
bank = 50.0
for day in metric_points.last_14_days:
sq = sleep_quality(day.sleep_metrics)
bank = (bank + (100 - bank) * sq) - drain(day.activity_metrics)
return bank
Do trenutka kada iteracija stigne do „danas“, seed je matematički nestao. 14 dana je otprilike dvostruko više od prozora konvergencije.
Prikaži computeBankFromDays (Go, ~20 linija)
// Pure iteration: 21 days of daily inputs in, today's bank out.
// Extracted so unit tests exercise imputation and trust-state
// logic without a database.
func computeBankFromDays(days []dailyInputs, cfg EnergyConfig) BankResult {
if len(days) != energyWindowDays {
return BankResult{State: "stale", FormulaVersion: cfg.FormulaVersion}
}
bank := 50.0 // contraction-mapping seed; forgotten by day 9
for i := 0; i < energyWindowDays; i++ {
sq, sqImputed := sleepQualityOrTrailingAvg(days, i)
drain, drainImputed := drainOrTrailingAvg(days, i, cfg)
// ... imputation flag bookkeeping omitted ...
bank = (bank + (100.0-bank)*sq) - drain
bank = clamp(bank, -50.0, 100.0)
}
return BankResult{
Bank: int(math.Round(bank)),
FormulaVersion: cfg.FormulaVersion,
// ... flags, state derived from imputation count ...
}
}
Prava verzija — sa nil-checks za svaki dan, two-pass imputation i izvođenjem trust state-a — živi u internal/storage/energy_compute.go. Ovde je njen čitljiv oblik.
Jedan mehanizam pokriva sve edge cases koji bi inače tražili odvojene code paths:
| Scenario | Ponašanje |
| Novi korisnik posle warmup-a | Forward iteration od seed=50; seed od pre 14 dana umire pre renderovanja današnjeg dana |
| Postojeći korisnik pri pokretanju v2 | Isto, migration path nije potreban |
Bump formula_version | Re-iterate 14 dana po novoj formuli; dashboard glatko prelazi tokom nedelje |
| Data gap kraći od 14 dana | Self-heals: propušteni dani daju sq=0, drain=0, bank se gasi ka 0, prva sveža noć ga asimptotski dopunjava |
| Data gap duži od 14 dana | Pokreće ponovni ulazak u staleness / warmup, bank se ne renderuje dok ne stignu sveži podaci |
Ovo je dizajn koji posle deluje očigledno, ali se pojavi tek posle tri neuspele forme. Rane verzije su držale persisted seed sa „EMA warmup state machine“, kojoj je trebala sopstvena migration logic za svaki bump formule. Svojstvo konvergencije sve je to uklonilo.
Postojala je i primamljiva optimizacija koju na kraju nismo prihvatili. Tokom design review-a pojavila se ideja da se jučerašnji EOD bank kešira u bazi i da se svaka dnevna iteracija počinje od te vrednosti, umesto da se prolazi 14 dana od seed = 50. Na papiru to štedi oko 13 dana računanja po recompute-u. U praksi bi prošlogodišnja floating-point aritmetika i prošlonedeljna verzija formule bile vučene unapred kroz vreme, a svaka tiha greška gomilala bi se zauvek. Convergence-from-seed garantuje da je svaki recompute idempotent over source data: drift ne može da se akumulira jer nema u čemu da se akumulira. Iteracija koja izgleda skupo zapravo je jeftino dugoročno rešenje. Prava performance optimizacija je drugačija: recompute window je ograničen na poslednja 24 sata po ingest event-u, a ne na celu istoriju.
Cross-tenant test koji je slomio v2
Prva verzija v2 radila je na mojim podacima i preživela backfill. Slomila se u roku od 24 sata kada je pokrenuta na podacima drugog tenant-a.
Kontekst je važan. Projekat je self-hostable i koristila ga je stvarna druga osoba: nalog moje žene na istom serveru, njen Apple Watch, njen RingConn i njen obrazac nošenja uređaja. Do tog trenutka sistem je već imao jedan dokaz da će drugi korisnici dolaziti na neočekivane načine. U 2. delu bio je PR #17 od makvitaly: eksterni contributor dodao je endpoint za workouts. Drugi tenant bio je druga vrsta dokaza: ne „neko želi da šalje podatke kao ja“, nego „svakodnevna realnost druge osobe uopšte ne liči na moju“.
To je bio isti kod, ali druga osoba i drugi obrazac nošenja sata. Podaci o snu nedostajali su u 21 od 90 dana — oko 23% gaps. Sat na punjaču, propušten sync, običan život: telefon ostao kod kuće, punjač zaboravljen na putu, nekoliko noći bez prstena. Prva verzija formule stavljala je sq=0 — nema oporavka — na dane bez podataka, ali je ostavljala active drain. Rezultat je bio runaway negative signed state. Interna vrednost je stigla do −54.
Svih pet „najgorih dana“ tog tenant-a na dashboard-u bili su data gaps, ne stvarna iscrpljenost. Sistem je govorio osobi da je exhausted zato što nije nosila sat. Slučajno sam na nekoliko sati imao njen telefon: zamolila me je da pogledam zašto joj dashboard stalno govori da se odmori. Gledati red koji si napisao za jedan dan i objašnjavati ga stvarnoj osobi nije isto što i gledati ga na svom nalogu.
U tom trenutku dizajn je dobio dva detalja kojih prvobitno nije bilo. Oba su nastala iz posmatranja pogrešnog ponašanja production-shaped run-a na stvarnom drugom korisniku, a ne iz teorije.
Trailing-average imputation sa čvrstom zabranom feedback-a. Kada za dan nema sleep metrika, sq[d] se uzima kao prosečan sleep quality za poslednjih 7 dana validnih, ne-imputed podataka. Isto važi za activity. Imputed days dobijaju flagove imputed_sleep, imputed_activity, ali još važnije — oni se isključuju iz trailing windows narednih dana. Ako se imputed values puste u prosek za sledeći dan, gap tiho povuče formulu ka tome kako je izgledao sam gap, a ne ka stvarnom baseline-u korisnika.
Ako u lookback window-u ima manje od 3 validna ne-imputed dana, fallback je all-time median korisnika, a trust state eskalira.
Tri nivoa poverenja. Binarno „imputed ili ne“ krije ono što korisnik mora da zna. Bank degradira kroz tri stanja sa jasnim triggers i jasnim UI:
| Stanje | Trigger | Render | AI usage |
fresh | ≤1 imputed day u poslednjih 7 | Bank normalan, bez flag-a | Koristi se u preporukama |
estimated | 2–4 consecutive imputed days ILI 2–4 imputed u 14-day window | Isprekidana linija + badge „procena · nema sensor data“ | Koristi se sa hedge: „na osnovu 7d pattern-a, ne direktnog merenja“ |
stale | ≥5 consecutive imputed days ILI >7 u 14-day window | Bank se NE renderuje; „Nema svežih podataka · proveri sync sata“ | AI se prebacuje sa activity advice na „sync your watch“ |
![]() | |||
| Hero EnergyBank v2. Sparkline pokazuje kako se bank prenosi između dana; badge ispod broja, ako je vidljiv, prikazuje trust state: fresh / estimated / stale. |
Dva paralelna trigger-a, consecutive i proportion, hvataju dva različita failure modes: čist period loše sinhronizacije i razbacane gap-ove koji se akumuliraju. Cross-user data pokazali su 23% scattered gaps; samo pravilo consecutive bi ih promašilo.
Izlazak iz stale nije trenutno vraćanje u fresh. Kada se podaci vrate, bank ulazi u 3-dnevno stanje recovering, gde se ponovo bootstrap-uje od seed=50 kroz poslednjih 14 dana, ignorišući zamrznute pre-stale snapshots. Pet i više dana bez podataka može značiti bolest, put, burnout ili samo mrtav sat. Primena stanja pre gap-a je tiha pretpostavka koja mali bug pretvara u pogrešnu priču o telu korisnika.
Pojavio se i signed floor na −50. Prvobitni nacrt je clamp-ovao samo odozgo. Cross-tenant run pokazao je da je simetričan floor potreban: ispod −50 signal se zasićuje, a dalje integrisanje je samo šum.
Skladište koje ne laže o verzijama formula
energy_snapshots u šemi izgleda kompaktno:
CREATE TABLE energy_snapshots (
ts_bucket TIMESTAMPTZ NOT NULL,
date TEXT NOT NULL,
bank INTEGER NOT NULL,
drain_delta INTEGER NOT NULL,
restore_delta INTEGER NOT NULL,
formula_version INTEGER NOT NULL,
components JSONB,
flags TEXT[] NOT NULL DEFAULT '{}',
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (ts_bucket)
);
Tri detalja zaslužuju mesto u tabeli. Nijedan nije genijalan; svaki sprečava konkretnu vrstu tihog kvara.
date se računa u Go-u, ne generiše ga Postgres. Izraz za GENERATED4 kolonu mora biti IMMUTABLE5, a to primorava timezone da se upiše direktno u DDL. Za multi-tenant sistem to je pogrešno. TZ korisnika dolazi iz REPORT_TZ; datum se računa pri upisu:
loc, _ := time.LoadLocation(tenantTZ)
dateStr := tsBucket.In(loc).Format("2006-01-02")
Snapshot upisan u Beogradu nosi date = '2026-05-08', čak i ako se korisnik kasnije preseli u Njujork. Budući snapshots koriste novu TZ posle promene podešavanja. Prošli podaci ostaju kakvi su bili — i to je ispravno, jer je tada u korisnikovoj timezone zaista bio taj dan.
components i flags su namerno odvojeni. components je audit trail ulaza formule (JSONB6): HR averages, korišćeni RHR baseline, kcal, sleep stage breakdown. Read-mostly. Odgovor na pitanje: „šta je formula videla?“ flags TEXT[] je state metadata: imputation status, trust level, calibration markers. Ako se pomešaju, promena renderovanja flag-a može da promeni audit trail. GIN index7 na flags čini membership queries (WHERE 'imputed_sleep' = ANY(flags)) jeftinim.
formula_version se čuva per-row i nikada se ne prepisuje across versions. Backfilled rows iz v2.0 ostaju v2.0 i posle izlaska v2.2. Grafikon čita najnoviju verziju per date. Week-over-week comparisons ostaju validna unutar jedne formula version, a UI jasno pokazuje calibration-change boundaries. Ovo pravilo pretvara „rerunning backfill changed last week’s numbers“ iz ponavljajućeg buga u svesnu odluku.
Čemu se formula prilagođava u v2.5
Isti cross-tenant run koji je otkrio collapse na data gaps otvorio je pitanje kalibracije. Statički α za sve je dobar osnovni instrument, ali loš dugoročni odgovor. Athlete sa niskim RHR i visokim active kcal zaista ima drugačiju energy economy nego sedentary user. Nekim korisnicima će trebati scaling α.
Naivan put je podešavati α prema tome koliko dobro bank tracks itself. To je klasična overfitting trap: tuning formule prema sopstvenim predictions. Potreban je external signal koji formula ne vidi.
U v2.5 taj signal postaje next-morning HRV. Računa se Pearson r između današnjeg bank_eod i sutrašnjeg overnight HRV na 30-day rolling window:
if r < 0.2: formula does not explain this user, alert, do NOT auto-tune
elif r < 0.3: narrow tune: alpha_factor ∈ [0.8, 1.2]
elif r < 0.5: full tune: alpha_factor ∈ [0.5, 1.5]
else: strong: alpha_factor ∈ [0.4, 1.7] (rare)
Ovde su važna dva guard rails. Uski tune band je kritičan: na 90-day prototype run-u unconstrained optimum se zalepio za granicu pretrage (factor=0.5) zbog marginalnog poboljšanja r. To je školski edge-solution znak misspecification, ne personalisation. Clamp na [0.8, 1.2] u zoni marginal r ne dozvoljava malom statističkom signalu da izazove veliki pomak formule.
Ako je r < 0 — nizak bank korelira sa visokim HRV, odnosno u suprotnom smeru od onoga što formula tvrdi — auto-tune se potpuno potiskuje, a case ide u calibration review. Juriti wrong-direction correlation odličan je način da sistem obučen na sopstvenim predictions završi još dalje od fiziologije nego na početku.
Prvobitni dizajn v2.5 planirao je da koristi next-day RHR kao drugi signal. Isti 90-day prototype run pokazao je da RHR fiziološki menja znak: vagal rebound posle visokog opterećenja može značiti niži RHR sledećeg jutra, a ne viši. RHR je uklonjen iz auto-tune signal-a i ostavljen za diagnostics.
Postepeni rollout i granice verzije
Rollout plan je podeljen u faze sa čvrstim kriterijumima prelaza:
- v2.0: skeleton. DB schema, event-driven recompute, restore samo iz sna, hourly chart u dashboard hero.
β = 0. Konstante su hardcoded, calibration deferred. Stari v1 radi paralelno dok se v2.0 ne potvrdi. - v2.1: calibration. 7 dana prikupljanja podataka, per-user podešavanje
αpo histogramu distribucije bank-a, recalibrate verdict thresholds na osnovu novih percentiles. - v2.2: autonomous-load drain term postaje nenult per-user posle validation iz 6. dela.
- v2.3: daytime restore + day-tagging UI (
alcohol,caffeine_high,illness_confirmed,travel,menstrual). Tags ne menjaju drain direktno, ali koriguju verdict copy. - v2.4: integrations. Prave actions, na primer preporuke tipa treninga u morning briefing-u ili blokiranje calendar slots.
- v2.5: auto-personalisation
αkroz HRV analysis. - v3.0: user profiles (
athlete/sedentary/recovery-impaired), koji zajedno podešavajuαistructure_factor.
Najteža odluka u v2.5 je transparency over knobs. Dashboard prikazuje sistemsku poruku: „kalibrisano automatski · α_factor=0.85 · poslednje ažuriranje: pon, 5. maj“, a u /admin/energy-calibration ostaje audit log. Korisnik vidi da se sistem prilagodio, ali ne dobija gomilu ručki kojima lako može da slomi formulu.
Out of scope ostaje: ručna korekcija bank-a, ručni α sliders, pokušaj predviđanja subjective energy bez posebnog label-a i bilo kakve LLM-driven promene formule. AI može da objašnjava bank i trust state. AI ne menja samu state machine.
Prava lekcija
EnergyBank v1 bio je dobro rešenje za prvu iteraciju i neprihvatljivo za production. Kod v2 je mali: forward iteration, asymptotic update rule i jednostavna state machine za trust level. Svaka linija postoji zato što je prethodni pristup pričao priču koju podaci nisu podržavali.
Prethodni članak bio je o poverenju u target. Ovaj je o poverenju u ćutanje formule: treba umeti sakriti bank kada senzori ćute, umesto samouvereno prikazivati broj izvučen iz vazduha.
Sledeći članak nastavlja otvoreno pitanje: šta je sustained_hr_load, zašto se jednostavan integral HR − RHR pokazao kao pogrešan izbor i kako validation štiti formulu od auto-tune-a u besmislene vrednosti.
Varijabilnost srčanog ritma (HRV). Fluktuacije na nivou milisekundi u intervalima između uzastopnih otkucaja srca. Veća varijabilnost je bolja: ukazuje na parasimpatički oporavak, a ne na simpatičku dominaciju. ↩︎
Puls u stanju mirovanja (RHR). Broj otkucaja srca izmeren tokom mirovanja, idealno tokom poslednjih sati noćnog sna. Porast tokom dana je klasičan signal bolesti ili preopterećenja. ↩︎
Matematičko svojstvo iteriranih funkcija gde svaka iteracija približava vrednost jedinstvenoj fiksnoj tački, bez obzira odakle ste počeli. Klasičan primer: ako
x_new = (x + 100/x)/2, ponovljena primena od bilo koje pozitivne početne vrednosti konvergira ka √100 = 10. Ovde to znači da, bez obzira s kojom bankovnom vrednošću započinjete iteraciju, odgovor nakon dovoljno dana je isti. Seme se eksponencijalno zaboravlja. ↩︎Kolona u Postgresu čija je vrednost izračunata iz izraza, a ne uneta od strane aplikacije. Uvek odražava najnoviju procenu izraza; može biti
STORED(materijalizovana pri unosu) iliVIRTUAL(izračunata pri čitanju, Postgres 17+). ↩︎Oznaka volatilnosti funkcije u Postgresu koja znači „dajući iste ulaze, uvek vraća isti izlaz i nema sporedne efekte.“ Potrebno za izraze koji se koriste unutar generisanih kolona ili indeksnih izraza, jer Postgres agresivno kešira njihove vrednosti. Funkcije poput
now()ilicurrent_setting()nisu tačnoIMMUTABLEjer čitaju spoljašnje stanje. ↩︎Postgres-ov binarni tip za skladištenje JSON-a. Kao
JSON, ali se parsira u stablo pri unosu (sporije pisanje, mnogo brže čitanje), podržava indeksiranje po pojedinačnim ključevima i članovima niza, i odbacuje neispravan JSON pri unosu. ↩︎Generalizovani inverzni indeks. Tip indeksa u Postgresu koji je optimizovan za upite „ova vrednost je jedna od mnogih unutar ovog reda“. Klasičan slučaj upotrebe je članstvo u nizu (
'foo' = ANY(my_text_array)) i pretraživanje punog teksta; unos indeksa usmerava sa jednog tokena na sve redove koji ga sadrže. ↩︎
