Informatievlaanderen / generieke-hypermedia-api

Bouwen van een specificatie van generieke bouwblokken voor API’s in Vlaanderen
8 stars 4 forks source link

Omgaan met gepagineerde CSV downloads #12

Open koenedaele opened 6 years ago

koenedaele commented 6 years ago

Een regelmatig terugkerend probleem: paginering van grote datasets in bv. csv formaat.

Achterliggend idee is dat een gebruiker een query uitvoert op een dataset. bv. Geef me alle beschermingsbesluiten die voor beschermd varend erfgoed gezorgd hebben: https://besluiten.onroerenderfgoed.be/besluiten?sort=id&beschermingstypes=https%3A%2F%2Fid.erfgoed.net%2Fthesauri%2Faanduidingstypes%2F5

Vaak wil een gebruiker hier nog een nabewerking op doen. Een technisch geschoolde gebruiker zou de bovenstaande query kunnen uitvoeren met een accept: application/json header en de resultaten verwerken tot wat ze nodig hebben. Echter, een minder technisch geschoolde gebruiker wil gewoon een Excell of csv download. Die bieden we aan als: https://besluiten.onroerenderfgoed.be/besluiten.csv?beschermingstypes=https%3A%2F%2Fid.erfgoed.net%2Fthesauri%2Faanduidingstypes%2F5 Dit is een vrij kleine csv en werkt prima.

Echter, een query resultaat kan ook groter dan dat zijn. Bv. alle besluiten over een beschermd monument: https://besluiten.onroerenderfgoed.be/besluiten?sort=id&beschermingstypes=https%3A%2F%2Fid.erfgoed.net%2Fthesauri%2Faanduidingstypes%2F1 Indien we dit omzetten naar een csv bestand, dan bevat dit meer dan 6000 regels en neemt de generatie veel tijd in beslag. Dit geeft problemen naar timeouts en maakt de server kwetsbaarder voor aanvallen. Daarom limiteren we de csv tot 1000 rijen. Bovenaan in de csv nemen we op de eerste regel een stukje uitleg op zoals: Er zijn 6205 resultaten. Dit bestand geeft niet alles weer. Meer gegevens kunnen gevonden worden door de parameter pagina toe te voegen aan de zoekstring. Bv. https://besluiten.onroerenderfgoed.be/besluiten.csv?status=&uitvaardigers=&provincies=&onderwerp=&beschermingstypes=https%3A%2F%2Fid.erfgoed.net%2Fthesauri%2Faanduidingstypes%2F1&jaar_ondertekening=&bestandssoort_id=&sort=id&pagina=2&bestanden.bestandssoort_id=&query=&rechtsgevolgen=&personen=

Dit werkt op zich relatief goed en is niet al te frequent nodig, maar het voelt wat knullig aan en het zorgt er voor dat je de verschillende csv's niet simpelweg aan elkaar kunt plakken. Maar waar kun je anders meegeven aan de gebruiker hoe er gepagineerd moet worden? Een doorsnee gebruiker zal geen HTTP headers lezen, dus dat lost niet meteen iets op. Tenzij je een systeem maakt waarbij er client-side een tool aanwezig is die detecteert dat een csv download in stukjes wordt doorgestuurd, alle stukjes verzameld en deze dan terug aan elkaar plakt? Dat lijkt me dan weer een ideale usecase voor generieke hypermedia controls.

lodagro commented 6 years ago

Dit lijkt een XY probleem te zijn. Als de query en csv generatie voldoende snel is, dan is paginatie niet nodig. Heb even de 1000 rijen csv gedownload en is 266kB. Zelfs maal zes is dat klein naar huidige data normen, toch?

koenedaele commented 6 years ago

Hier hebben we nu op dit moment inderdaad maar 6000 items. Bij deze bv. https://inventaris.onroerenderfgoed.be/erfgoedobjecten?provincie=10000&pagina=1 hebben we 21908 resultaten en elk van die resultaten wordt gegenereerd uit een complex object. En deze (https://inventaris.onroerenderfgoed.be/erfgoedobjecten?type=dibe&pagina=1) geeft er 102598. In die toepassing hebben we limieten moeten instellen omdat de servers anders platgetrokken werd (Hier zit dacht ik geen paginering in op de csv downloads, dus gebruikers kunnen nooit meer dan de eerste X records krijgen). Toegegeven, die toepassing is een pak ouder en wordt momenteel herschreven. Maar dan nog is er op een bepaald moment een query die te zwaar is of die het gewoon te makkelijk maakt om een server te belasten. Dus, er zal altijd wel ergens een nood aan paginering zijn. Alternatieven zijn server side generatie van een downloadpakket genereren of zo, maar voor de gemiddelde gebruiker die een query met 25 resultaten heeft gegenereerd is dat ook weer overkill. Of je kunt gewoon een arbitraire limiet instellen, maar dat is ook niet erg bevredigend en zorgt voor frustraties voor mensen die meer dan die limiet aan records nodig hebben.

pietercolpaert commented 6 years ago

Het probleem focust zich hier op minder geschoolde ontwikkelaars die niet kijken naar HTTP-headers. Ik denk dat dit net een opportuniteit is om die ontwikkelaars te tonen hoe dit wel compliant aan #1 kan worden opgelost via documentatie voor mensen.

Deze specificatie focust zich eerder op geautomatiseerd hergebruik, waarbij we met een exhaustieve lijst van opties zeker kunnen zijn dat, als er een volgende pagina bestaat, we die zullen vinden. Ik ben dan ook eerder geneigd om dit out of scope te zien. Wel zouden we een implementatie-voorbeeld kunnen voorzien waar we de standaard toepassen op CSV, en tonen hoe een hybride oplossing voor zoals generieke agents als voor handmatige adoptie er uit kan zien.

Ander leesvoer om interoperabiliteit van CSV-files te verhogen:

koenedaele commented 6 years ago

Akkoord dat dit eerder buiten de scope van dit document is en hier niet verder aan bod hoeft te komen. Of gewoon verder gebruik kan maken van de paginering bouwblok.

Ik denk echter niet dat het een probleem van minder geschoolde ontwikkelaars is. De mensen die bij ons CSV downloads gebruiken zijn totaal ongeschoolde eindgebruikers. Zelf een minder geschoolde ontwikkelaar kan je wel uitleggen hoe Link headers werken en hoe hij een paar CSV's terug aan elkaar kan hangen.

Op zich denk ik inderdaad wel dat je dankzij de standaard een kleine, herbruikbare user agent zou kunnen schrijven die kan detecteren dat er een gepagineerde csv download aanwezig is en een eindgebruiker in zijn browser helpt om de download volledig te doorlopen. Dus, voor deze use case: zolang er een mogelijkheid is om met Link headers of iets anders dat niet in de eigenlijke payload zelf zit kunt werken is dit mogelijk.

En bedankt voor het leesvoer over de CSV files!

CumpsD commented 6 years ago

"Probleem" dat niet technische mensen gewoon een URL aanroepen en een CSV verwachten als download is inderdaad iets dat nog voorkomt.

Heb wel even een ander vraagje voor @koenedaele, als het downloaden van 102598 lijnen de server plat trekt, waarom zou het gepagineerd downloaden van die hoeveelheid de server niet plat trekken?

koenedaele commented 6 years ago

Plat is misschien niet het juiste woord. Geeft eerder timeouts op zo'n request. Je kunt altijd wel de grenzen voor een timeout verhogen, maar dan maak je je server kwetsbaarder voor DOS aanvallen. Dus we stellen een bovenlimiet in. Met paginering is dat allemaal vrij simpel op te lossen.

bertvannuffelen commented 6 years ago

Een suggestie:

misschien kan je dit omvormen tot een serverside generatie oplossing. Stel je hebt een query waarvan je niet alle rijen worden teruggegeven. Dan kan je een service voorzien met als parameter de query en die geeft je een link naar een file terug. Deze file wordt serverside gemaakt en is tijdelijk. Dus na 1uur ruim je die weer op.

Met deze methode kan je ook heel grote opvragingen ondersteunen.

koenedaele commented 6 years ago

@bertvannuffelen Klopt. Zoiets is zeker te overwegen. Maar de technische inspanning daarvan is groter omdat er een queue moet komen die zo'n zaken regelt. Voor het gepagineerd downloaden van resultatensets hebben we eigenlijk alles in huis en is het schrijven van 1 enkele extra rendering component voldoende. Veruit de kleinste technische inspanning voor de server denk ik.