Caching af adresseopslag
Se hvilke svar du kan cache og hvor længe, hvad Cache-Control og ETag fra API'et betyder, og byg en cache i Python med og uden Redis.
Adresser ændrer sig sjældent. En given adresse kan være uændret i årevis, og postnumre og kommuner skifter næsten aldrig. Derfor kan en stor del af dine kald besvares fra en cache i stedet for fra API'et. Det gør din side hurtigere og sparer af din kvote.
Guiden viser, hvad vi selv skriver i Cache-Control, hvordan ETag virker hos os, og to små cache-implementeringer.
Hvad API'et sender
Hvert vellykket svar har en Cache-Control-header. Værdien afhænger af endpointet:
| Endpoint | Cache-Control |
|---|---|
/autocomplete og /<ressource>/autocomplete | private, max-age=300 |
/adresser og /adgangsadresser (lister) | private, max-age=300 |
/adresser/{id} og /adgangsadresser/{id} | private, max-age=3600 |
/adgangsadresser/reverse | private, max-age=60 |
/postnumre, /kommuner, /regioner, /sogne og de andre DAGI-områder, /vejstykker, /vejnavne, /navngivneveje, /jordstykker, /matrikelnumre, /ejerlav, /steder, /stednavne | private, max-age=86400, stale-while-revalidate=604800 |
/datavask/… (GET) | private, max-age=86400 |
/api/v1/enrich/… | private, max-age=86400 |
| Alle fejlsvar | no-store |
private betyder, at din egen klient må gemme svaret, men en delt cache undervejs (en CDN eller en fælles proxy) må ikke. Svaret hører til din nøgle. En browser, der henter /postnumre, genbruger altså svaret i et døgn uden at spørge igen.
Fejl må aldrig caches. Gemmer du et 429- eller 503-svar, bliver fejlen ved med at komme, efter at årsagen er væk.
ETag og 304
JSON-svar op til 256 KB har en svag ETag, fx ETag: W/"3f2a9c1b0d4e5f61". Sender du den tilbage i If-None-Match, og har svaret ikke ændret sig, får du 304 Not Modified uden indhold:
import os import requests URL = "https://api.danadresse.dk/postnumre/8000" headers = {"X-Api-Key": os.environ["DANADRESSE_API_KEY"]} forste = requests.get(URL, headers=headers, timeout=10) etag = forste.headers.get("ETag") print("første kald:", forste.status_code, etag) anden = requests.get(URL, headers={**headers, "If-None-Match": etag}, timeout=10) print("andet kald: ", anden.status_code, len(anden.content), "bytes")
Et 304-svar tæller stadig som et kald. Nøglen bliver kontrolleret og forbruget talt, før svaret bliver lavet. ETag sparer båndbredde og parsing, men ikke kvote. Vil du spare kald, skal svaret komme fra din egen cache uden at spørge API'et.
Hvad du kan cache, og hvor længe
| Data | Forslag til levetid | Hvorfor |
|---|---|---|
| Autocomplete-forslag | Minutter, i hukommelsen | Brugere sletter og skriver igen. Den samme forespørgsel kommer tit flere gange i samme session |
Opslag på id (/adresser/{id}) | Timer til et døgn | En adresse ændrer sig sjældent. Gem id'et i din database alligevel |
| Postnumre, kommuner, regioner, DAGI | Et døgn eller mere | Ændres højst få gange om året |
| Datavask af samme tekst | Et døgn | Samme input giver samme svar, indtil registret ændrer sig |
| Reverse-geokodning | Kort, eller pr. afrundet koordinat | GPS-punkter varierer, så samme nøgle rammes sjældent præcist |
Cache-nøglen skal indeholde stien og alle parametre, fx srid, struktur og per_side. Ellers får en kalder med srid=25832 et svar i WGS84. Læg aldrig API-nøglen ind i cache-nøglen eller i logs.
Skal du bruge hele datasættet, så hent det med replikering i stedet for at cache dig frem til det med enkeltkald. Det står også i vilkårene, at hele datasættet skal hentes via replikeringen. Se Første udtræk.
En TTL-cache i hukommelsen
Til en enkelt proces er en ordbog med udløbstid nok. Eksemplet slår det samme postnummer op to gange, men kalder kun API'et én gang:
import os import time import requests API = "https://api.danadresse.dk" session = requests.Session() session.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"] _cache: dict[tuple, tuple[float, object]] = {} api_kald = 0 def hent(sti: str, params: dict | None = None, ttl: float = 3600): global api_kald noegle = (sti, tuple(sorted((params or {}).items()))) hit = _cache.get(noegle) if hit and hit[0] > time.monotonic(): return hit[1] r = session.get(API + sti, params=params, timeout=10) r.raise_for_status() # fejl caches ikke api_kald += 1 data = r.json() _cache[noegle] = (time.monotonic() + ttl, data) return data for _ in range(2): print(hent("/postnumre/8000", ttl=86400)["navn"]) print(f"API-kald: {api_kald}")
Cachen vokser uden grænse. Til autocomplete med mange forskellige forespørgsler bør du bruge en cache med en maksimal størrelse, fx cachetools.TTLCache i Python.
En delt cache i Redis
Kører du flere processer eller servere, så læg cachen i Redis. Alle instanser deler så de samme svar. Levetiden vælges ud fra stien:
import hashlib import json import os import redis import requests API = "https://api.danadresse.dk" r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379/0")) session = requests.Session() session.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"] # Levetid i sekunder pr. sti-præfiks. Den første, der passer, vinder. TTL = [ ("/autocomplete", 300), ("/adgangsadresser/reverse", 60), ("/adresser/", 86400), ("/adgangsadresser/", 86400), ("/postnumre", 7 * 86400), ("/kommuner", 7 * 86400), ("/datavask/", 86400), ] def ttl_for(sti: str) -> int: for praefiks, sekunder in TTL: if sti.startswith(praefiks): return sekunder return 300 def hent(sti: str, params: dict | None = None): raa = json.dumps([sti, sorted((params or {}).items())], ensure_ascii=False) noegle = "danadresse:" + hashlib.sha256(raa.encode()).hexdigest() gemt = r.get(noegle) if gemt is not None: return json.loads(gemt) svar = session.get(API + sti, params=params, timeout=10) svar.raise_for_status() # kun 2xx caches data = svar.json() r.setex(noegle, ttl_for(sti), json.dumps(data, ensure_ascii=False)) return data
Falder Redis ud, bør din kode kalde API'et direkte i stedet for at fejle. Pak r.get og r.setex ind i en try, hvis cachen ikke må kunne vælte forespørgslen.
I browseren
Til et adressefelt er det nok at huske de sidste forespørgsler i et Map. Brugeren, der skriver "Vest", sletter og skriver "Vest" igen, får det andet svar uden et nyt kald:
const cache = new Map(); const MAX = 100; export async function forslag(q, apiKey) { if (cache.has(q)) return cache.get(q); const url = new URL("https://api.danadresse.dk/autocomplete"); url.search = new URLSearchParams({ q, api_key: apiKey }); const res = await fetch(url); if (!res.ok) return []; // fejl gemmes ikke const data = await res.json(); cache.set(q, data); if (cache.size > MAX) cache.delete(cache.keys().next().value); // smid den ældste ud return data; }
Nøglen her skal være en domænelåst browsernøgle. Se Browsernøgler og tilladte domæner.
Næste skridt
- Hvad du gør, når et kald fejler: Retries og backoff.
- Se effekten på forbruget: Overvåg forbruget.
Relaterede guides
Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)