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.

4 min Niveau: Øvet ✓ Kode testet

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:

EndpointCache-Control
/autocomplete og /<ressource>/autocompleteprivate, max-age=300
/adresser og /adgangsadresser (lister)private, max-age=300
/adresser/{id} og /adgangsadresser/{id}private, max-age=3600
/adgangsadresser/reverseprivate, max-age=60
/postnumre, /kommuner, /regioner, /sogne og de andre DAGI-områder, /vejstykker, /vejnavne, /navngivneveje, /jordstykker, /matrikelnumre, /ejerlav, /steder, /stednavneprivate, max-age=86400, stale-while-revalidate=604800
/datavask/… (GET)private, max-age=86400
/api/v1/enrich/…private, max-age=86400
Alle fejlsvarno-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:

Python#etag-304
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

DataForslag til levetidHvorfor
Autocomplete-forslagMinutter, i hukommelsenBrugere sletter og skriver igen. Den samme forespørgsel kommer tit flere gange i samme session
Opslag på id (/adresser/{id})Timer til et døgnEn adresse ændrer sig sjældent. Gem id'et i din database alligevel
Postnumre, kommuner, regioner, DAGIEt døgn eller mereÆndres højst få gange om året
Datavask af samme tekstEt døgnSamme input giver samme svar, indtil registret ændrer sig
Reverse-geokodningKort, eller pr. afrundet koordinatGPS-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:

Python#ttl-cache
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:

Python#redis-cache
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:

JavaScript#browser-cache
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

Relaterede guides

Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)

Prøv det med en gratis nøgle

2 000 kald/måned på gratisplanen. Intet kreditkort.

Opret konto →