CINXE.COM

Kay Kremerskothen | code.flickr.com

<!DOCTYPE html> <!--[if IE 6]> <html id="ie6" lang="en-US"> <![endif]--> <!--[if IE 7]> <html id="ie7" lang="en-US"> <![endif]--> <!--[if IE 8]> <html id="ie8" lang="en-US"> <![endif]--> <!--[if !(IE 6) & !(IE 7) & !(IE 8)]><!--> <html lang="en-US"> <!--<![endif]--> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <title> Kay Kremerskothen | code.flickr.com </title> <link rel="profile" href="https://gmpg.org/xfn/11" /> <link rel="stylesheet" type="text/css" media="all" href="https://code.flickr.net/wp-content/themes/flickr-code/style.css?ver=20190507" /> <link rel="pingback" href="https://code.flickr.net/xmlrpc.php"> <!--[if lt IE 9]> <script src="https://code.flickr.net/wp-content/themes/twentyeleven/js/html5.js?ver=3.7.0" type="text/javascript"></script> <![endif]--> <meta name='robots' content='max-image-preview:large' /> <style>img:is([sizes="auto" i], [sizes^="auto," i]) { contain-intrinsic-size: 3000px 1500px }</style> <link rel='dns-prefetch' href='//stats.wp.com' /> <link rel="alternate" type="application/rss+xml" title="code.flickr.com &raquo; Feed" href="https://code.flickr.net/feed/" /> <link rel="alternate" type="application/rss+xml" title="code.flickr.com &raquo; Comments Feed" href="https://code.flickr.net/comments/feed/" /> <link rel="alternate" type="application/rss+xml" title="code.flickr.com &raquo; Posts by Kay Kremerskothen Feed" href="https://code.flickr.net/author/flickrphotography/feed/" /> <script type="text/javascript"> /* <![CDATA[ */ window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/svg\/","svgExt":".svg","source":{"concatemoji":"https:\/\/code.flickr.net\/wp-includes\/js\/wp-emoji-release.min.js?ver=6.7.1"}}; /*! This file is auto-generated */ !function(i,n){var o,s,e;function c(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function p(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data),r=(e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0),new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data));return t.every(function(e,t){return e===r[t]})}function u(e,t,n){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\uddfa\ud83c\uddf3","\ud83c\uddfa\u200b\ud83c\uddf3")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!n(e,"\ud83d\udc26\u200d\u2b1b","\ud83d\udc26\u200b\u2b1b")}return!1}function f(e,t,n){var r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):i.createElement("canvas"),a=r.getContext("2d",{willReadFrequently:!0}),o=(a.textBaseline="top",a.font="600 32px Arial",{});return e.forEach(function(e){o[e]=t(a,e,n)}),o}function t(e){var t=i.createElement("script");t.src=e,t.defer=!0,i.head.appendChild(t)}"undefined"!=typeof Promise&&(o="wpEmojiSettingsSupports",s=["flag","emoji"],n.supports={everything:!0,everythingExceptFlag:!0},e=new Promise(function(e){i.addEventListener("DOMContentLoaded",e,{once:!0})}),new Promise(function(t){var n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),p.toString()].join(",")+"));",r=new Blob([e],{type:"text/javascript"}),a=new Worker(URL.createObjectURL(r),{name:"wpTestEmojiSupports"});return void(a.onmessage=function(e){c(n=e.data),a.terminate(),t(n)})}catch(e){}c(n=f(s,u,p))}t(n)}).then(function(e){for(var t in e)n.supports[t]=e[t],n.supports.everything=n.supports.everything&&n.supports[t],"flag"!==t&&(n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&n.supports[t]);n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&!n.supports.flag,n.DOMReady=!1,n.readyCallback=function(){n.DOMReady=!0}}).then(function(){return e}).then(function(){var e;n.supports.everything||(n.readyCallback(),(e=n.source||{}).concatemoji?t(e.concatemoji):e.wpemoji&&e.twemoji&&(t(e.twemoji),t(e.wpemoji)))}))}((window,document),window._wpemojiSettings); /* ]]> */ </script> <style id='wp-emoji-styles-inline-css'> img.wp-smiley, img.emoji { display: inline !important; border: none !important; box-shadow: none !important; height: 1em !important; width: 1em !important; margin: 0 0.07em !important; vertical-align: -0.1em !important; background: none !important; padding: 0 !important; } </style> <link rel='stylesheet' id='all-css-2' href='https://code.flickr.net/wp-includes/css/dist/block-library/style.min.css?m=1732206022g' type='text/css' media='all' /> <style id='wp-block-library-theme-inline-css'> .wp-block-audio :where(figcaption){color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-audio :where(figcaption){color:#ffffffa6}.wp-block-audio{margin:0 0 1em}.wp-block-code{border:1px solid #ccc;border-radius:4px;font-family:Menlo,Consolas,monaco,monospace;padding:.8em 1em}.wp-block-embed :where(figcaption){color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-embed :where(figcaption){color:#ffffffa6}.wp-block-embed{margin:0 0 1em}.blocks-gallery-caption{color:#555;font-size:13px;text-align:center}.is-dark-theme .blocks-gallery-caption{color:#ffffffa6}:root :where(.wp-block-image figcaption){color:#555;font-size:13px;text-align:center}.is-dark-theme :root :where(.wp-block-image figcaption){color:#ffffffa6}.wp-block-image{margin:0 0 1em}.wp-block-pullquote{border-bottom:4px solid;border-top:4px solid;color:currentColor;margin-bottom:1.75em}.wp-block-pullquote cite,.wp-block-pullquote footer,.wp-block-pullquote__citation{color:currentColor;font-size:.8125em;font-style:normal;text-transform:uppercase}.wp-block-quote{border-left:.25em solid;margin:0 0 1.75em;padding-left:1em}.wp-block-quote cite,.wp-block-quote footer{color:currentColor;font-size:.8125em;font-style:normal;position:relative}.wp-block-quote:where(.has-text-align-right){border-left:none;border-right:.25em solid;padding-left:0;padding-right:1em}.wp-block-quote:where(.has-text-align-center){border:none;padding-left:0}.wp-block-quote.is-large,.wp-block-quote.is-style-large,.wp-block-quote:where(.is-style-plain){border:none}.wp-block-search .wp-block-search__label{font-weight:700}.wp-block-search__button{border:1px solid #ccc;padding:.375em .625em}:where(.wp-block-group.has-background){padding:1.25em 2.375em}.wp-block-separator.has-css-opacity{opacity:.4}.wp-block-separator{border:none;border-bottom:2px solid;margin-left:auto;margin-right:auto}.wp-block-separator.has-alpha-channel-opacity{opacity:1}.wp-block-separator:not(.is-style-wide):not(.is-style-dots){width:100px}.wp-block-separator.has-background:not(.is-style-dots){border-bottom:none;height:1px}.wp-block-separator.has-background:not(.is-style-wide):not(.is-style-dots){height:2px}.wp-block-table{margin:0 0 1em}.wp-block-table td,.wp-block-table th{word-break:normal}.wp-block-table :where(figcaption){color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-table :where(figcaption){color:#ffffffa6}.wp-block-video :where(figcaption){color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-video :where(figcaption){color:#ffffffa6}.wp-block-video{margin:0 0 1em}:root :where(.wp-block-template-part.has-background){margin-bottom:0;margin-top:0;padding:1.25em 2.375em} </style> <link rel='stylesheet' id='all-css-6' href='https://code.flickr.net/_static/??-eJzTLy/QzcxLzilNSS3WzyrWz01NyUxMzUnNTc0rQeEU5CRWphbp5qSmJyZX6uVm5uklFxfr6OPTDpRD5sM02efaGpobGxkZmBkYGQMARIMu1Q==' type='text/css' media='all' /> <style id='jetpack-sharing-buttons-style-inline-css'> .jetpack-sharing-buttons__services-list{display:flex;flex-direction:row;flex-wrap:wrap;gap:0;list-style-type:none;margin:5px;padding:0}.jetpack-sharing-buttons__services-list.has-small-icon-size{font-size:12px}.jetpack-sharing-buttons__services-list.has-normal-icon-size{font-size:16px}.jetpack-sharing-buttons__services-list.has-large-icon-size{font-size:24px}.jetpack-sharing-buttons__services-list.has-huge-icon-size{font-size:36px}@media print{.jetpack-sharing-buttons__services-list{display:none!important}}.editor-styles-wrapper .wp-block-jetpack-sharing-buttons{gap:0;padding-inline-start:0}ul.jetpack-sharing-buttons__services-list.has-background{padding:1.25em 2.375em} </style> <style id='classic-theme-styles-inline-css'> /*! This file is auto-generated */ .wp-block-button__link{color:#fff;background-color:#32373c;border-radius:9999px;box-shadow:none;text-decoration:none;padding:calc(.667em + 2px) calc(1.333em + 2px);font-size:1.125em}.wp-block-file__button{background:#32373c;color:#fff;text-decoration:none} </style> <style id='global-styles-inline-css'> :root{--wp--preset--aspect-ratio--square: 1;--wp--preset--aspect-ratio--4-3: 4/3;--wp--preset--aspect-ratio--3-4: 3/4;--wp--preset--aspect-ratio--3-2: 3/2;--wp--preset--aspect-ratio--2-3: 2/3;--wp--preset--aspect-ratio--16-9: 16/9;--wp--preset--aspect-ratio--9-16: 9/16;--wp--preset--color--black: #000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #fff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--color--blue: #1982d1;--wp--preset--color--dark-gray: #373737;--wp--preset--color--medium-gray: #666;--wp--preset--color--light-gray: #e2e2e2;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);}:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;} :where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;} :where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;} :root :where(.wp-block-pullquote){font-size: 1.5em;line-height: 1.6;} </style> <link rel='stylesheet' id='all-css-12' href='https://code.flickr.net/_static/??-eJzTLy/QTc7PK0nNK9EvyUjNTS3WLykHcipTc1LLUvP0i0sqc1L1kouLdfQxVablZCZnFwFFU1LxK0QxMiknPzm7GKTUPtfW0NzI0NDQxMDUEABaNTMI' type='text/css' media='all' /> <link rel="https://api.w.org/" href="https://code.flickr.net/wp-json/" /><link rel="alternate" title="JSON" type="application/json" href="https://code.flickr.net/wp-json/wp/v2/users/1828088" /><link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://code.flickr.net/xmlrpc.php?rsd" /> <meta name="generator" content="WordPress 6.7.1" /> <style>img#wpstats{display:none}</style> <style type="text/css" id="twentyeleven-header-css"> #site-title, #site-description { position: absolute; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px, 1px, 1px, 1px); } </style> <style type="text/css" id="wp-custom-css"> body { background: #fff; } body,input,textarea { font-family: Arial, sans-serif; color: #404040; } #page { margin: 1em auto; max-width: 1000px; } a:link { text-decoration: none; color: #0063dc; } a:visited { text-decoration: none; color: #0063dc; } a:hover { text-decoration: none; background: #0063dc; color: #fff; } a:active { text-decoration: none; background-color: #0259c4; color: #fff; } #branding { border: none; } #branding .only-search #s,#branding .only-search #s:focus { width: 280px; background-color: transparent; border-color: #ddd; } #branding #searchform { right: 0; } nav#access { background: none; box-shadow: none; -webkit-box-shadow: none; -moz-box-shadow: none; margin: 0; } nav#access a { font-weight: bold; margin: 11px 28px 0 0; padding: 0; line-height: 21px; } nav#access a:link,nav#access a:visited { text-decoration: none; color: #888; } nav#access a:hover,nav#access li:hover > a,nav#access a:focus,nav#access a:active { text-decoration: none; color: #0063dc; background: none; } #branding .only-search + nav#access div { padding-right: 300px; } nav#access div,nav#access ul { margin: 0; } @media screen and (max-width: 768px) { #branding .only-search + nav#access div { padding-right: 0; } #branding #searchform { display: none; position: static; text-align: center; } #branding .with-image #searchform { max-width: 100%; } #branding .only-search #s,#branding .only-search #s:focus { width: 85%; float: none; } nav#access { margin: 0 0 0 5%; } } @media screen and (max-width: 480px) { nav#access { display: none; } } #content { margin: 0 20% 0 0; width: 80%; } #content .comments-link { display: none; } @media screen and (max-width: 800px) { #main { padding-top: 0; overflow: hidden; } #branding { padding-bottom: 0; } #main #content { margin: 0; } .entry-title,.entry-header .entry-meta { padding-right: 0; } } .singular #content,.left-sidebar.singular #content { margin: 0 10%; width: 80%; } .singular .entry-header,.singular .entry-content,.singular footer.entry-meta,.singular #comments-title { width: 100%; } .hentry,.no-results { border-bottom: 1px dotted #dadada; } .singular .hentry { padding-top: 1em; } .entry-title { padding-bottom: 0; } .entry-title,.entry-title a { font-size: 28px; color: #000; } .entry-title a:hover,.entry-title a:focus,.entry-title a:active { color: #0063dc; background-color: transparent; } @media screen and (max-width: 650px) { .singular .entry-title { padding-top: 0; line-height: 42px; } } .singular .entry-content { margin-top: 0; } #jp-post-flair { margin-top: 3em; } footer.entry-meta .cat-links,footer.entry-meta .sep,footer.entry-meta .tag-links { display: none; } .singular .entry-meta .edit-link a { position: static; } .singular footer.entry-meta { margin-top: 2em; } #comments { display: none; } #site-generator { background: transparent; border-top: none; padding: 0 .5em; } img#wpstats { position: absolute !important; clip: rect(1px 1px 1px 1px); } #secondary { margin-right: 0; width: 16%; text-align: right; } #secondary #s { display: none; } #secondary aside ul { list-style: none; } #secondary .widget-title { color: #999; font-size: 12px; letter-spacing: auto; text-transform: none; } .widget a { font-weight: normal; } .widget a:hover,.widget a:focus,.widget a:active { text-decoration: none; } #everything-after-this-line-is-a-post-level-style---yay-for-wordpress-stripping-comments-from-css { color: #000; } .entry-content p { margin-bottom: 1.3em; } .entry-content p.note { padding: 11px; background-color: #fffdeb; border-bottom: 1px solid #fff9c2; } .entry-content p.warning { padding: 11px; background-color: #fdf8f8; border-bottom: 1px solid #f7dedd; } .entry-content .aside { margin-bottom: 1.3em; padding: 11px; background-color: #f8fdf8; border-bottom: 1px solid #def6df; } .entry-content .aside p:last-child { margin-bottom: 0; } .entry-content h2,.entry-content h3,.entry-content h4 { font-weight: bold; color: #000; text-transform: none; } .entry-content h2 { font-size: 22px; } .entry-content h3 { font-size: 16px; letter-spacing: 0; line-height: 1.3em; margin-bottom: 1em; } .entry-content h4 { font-size: 13px; line-height: 1.4em; margin-bottom: 0; } .entry-content p.flickr-photo,.entry-content p.flickr-photo a,.entry-content p.figure { color: #999; font-size: 14px; } .entry-content p.flickr-photo a:hover,.entry-content p.flickr-photo a:active { color: #0063dc; background-color: transparent; } .entry-content p.flickr-photo .caption { display: block; padding-left: 22px; background: url('http://farm4.staticflickr.com/3329/favicons/72157601614001242_7730.png') no-repeat 0 3px; } .entry-content img { max-width: 100%; height: auto; } .undersized-image-container { text-align: center; } .entry-content code { color: #000; font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; font-size: 14px; background-color: #F6FcFF; border-bottom: 1px solid #ebf5ff; } .entry-content ul { list-style: disc; } .entry-content ol ol { list-style: lower-alpha; } .entry-content table caption { font-size: 13px; font-weight: bold; color: #000; } .entry-content table tr.odd td,.entry-content table tr.odd th { background-color: #f8f8f8; } .entry-content table td,.entry-content table th { padding: 3px 10px; line-height: 1.4em; } .entry-content table tr th { color: #000; font-weight: bold; text-transform: none; font-size: 13px; letter-spacing: 0; border-top: 1px solid #ddd; } .entry-content table tr td { font-size: 14px; } .entry-content table.data tr td,.entry-content table.data thead tr th { text-align: right; } .entry-content table.tight td,.entry-content table.tight th { padding: 3px 6px; } .entry-content table.tight th { font-size: 12px; } .entry-content table.tight td { font-size: 13px; } @media screen and (max-width: 650px) { .entry-content table td,.entry-content table th { padding: 3px 5px; } .entry-content table tr th { font-size: 12px; } .entry-content table tr td { font-size: 13px; } .entry-content table.tight td,.entry-content table.tight th { padding: 3px 4px; } .entry-content table.tight th { font-size: 11px; } .entry-content table.tight td { font-size: 12px; } } .entry-content blockquote { font-family: inherit; font-style: normal; font-weight: normal; margin: 0 2em 1.3em; padding: 1em; background: #F9F9F9; border-bottom: 1px solid #ececec; } .entry-content blockquote p.source { margin-bottom: 0; } .entry-content .hiring-banner { position: relative; background: #ffebf5; padding: 11px 11px 11px 88px; border-bottom: 1px solid #ffc5e2; margin: 2em 0; } .entry-content .hiring-banner p { margin-bottom: 0; } .entry-content .hiring-banner .group-photo { position: absolute; left: -57px; top: -5px; width: 120px; padding: 6px; background: #fff; -webkit-box-shadow: 1px 1px 8px rgba(50,50,50,0.8); -moz-box-shadow: 1px 1px 8px rgba(50,50,50,0.8); box-shadow: 1px 1px 8px rgba(50,50,50,0.8); -webkit-transform: rotate(-11deg); -moz-transform: rotate(-11deg); -o-transform: rotate(-11deg); -ms-transform: rotate(-11deg); } .entry-content .hiring-banner .group-photo img { display: block; margin: 0; max-width: 100%; } @media screen and (max-width: 800px) { .entry-content .hiring-banner { padding-left: 11px; } .entry-content .hiring-banner .group-photo { display: none; } } #everything-after-this-line-is-for-the-syntaxhighlighter---all-rules-require-important { color: #000; } #main .syntaxhighlighter .lines { border-bottom: 1px solid #ebf5ff !important; } #main .syntaxhighlighter,#main .syntaxhighlighter div,#main .syntaxhighlighter code,#main .syntaxhighlighter table,#main .syntaxhighlighter table td,#main .syntaxhighlighter table tr,#main .syntaxhighlighter table tbody { font-size: 14px !important; } #main .syntaxhighlighter .line.alt1,#main .syntaxhighlighter .line.alt2 { background-color: #fafdff !important; } #main .syntaxhighlighter .line.highlighted { background-color: #fffbd6 !important; } #main .syntaxhighlighter .plain,#main .syntaxhighlighter .plain a { color: #000 !important; } #main .syntaxhighlighter .comments,#main .syntaxhighlighter .comments a { color: #999 !important; } #main .syntaxhighlighter .string,#main .syntaxhighlighter .string a { color: #ff52a9 !important; } #main .syntaxhighlighter .keyword { color: #0034fe !important; font-weight: normal !important; } #main .syntaxhighlighter .preprocessor { color: #417ba9 !important; } #main .syntaxhighlighter .variable { color: #b130c0 !important; } #main .syntaxhighlighter .value { color: #6b77f7 !important; } #main .syntaxhighlighter .functions { color: #002ad5 !important; } #main .syntaxhighlighter .constants { color: #d11e08 !important; } #main .syntaxhighlighter .script { background-color: yellow !important; } #main .syntaxhighlighter .color1,#main .syntaxhighlighter .color1 a { color: #808080 !important; } #main .syntaxhighlighter .color2,#main .syntaxhighlighter .color2 a { color: #ff1493 !important; } #main .syntaxhighlighter .color3,#main .syntaxhighlighter .color3 a { color: red !important; } a[data-flickr-embed] img { width: 800px; } </style> </head> <body class="archive author author-flickrphotography author-1828088 custom-background wp-embed-responsive jps-theme-flickr-code two-column right-sidebar"> <div class="skip-link"><a class="assistive-text" href="#content">Skip to primary content</a></div><div class="skip-link"><a class="assistive-text" href="#secondary">Skip to secondary content</a></div><div id="page" class="hfeed"> <header id="branding"> <hgroup> <h1 id="site-title"><span><a href="https://code.flickr.net/" rel="home">code.flickr.com</a></span></h1> <h2 id="site-description"></h2> </hgroup> <a href="https://code.flickr.net/"> <img src="https://wp.flickr.net/wp-content/uploads/sites/3/2012/09/code-flickr-com-drawn-header-grey-large.png" width="1000" height="157" alt="code.flickr.com" /> </a> <div class="only-search with-image"> <form method="get" id="searchform" action="https://code.flickr.net/"> <label for="s" class="assistive-text">Search</label> <input type="text" class="field" name="s" id="s" placeholder="Search" /> <input type="submit" class="submit" name="submit" id="searchsubmit" value="Search" /> </form> </div> <nav id="access"> <h3 class="assistive-text">Main menu</h3> <div class="menu-menu-container"><ul id="menu-menu" class="menu"><li id="menu-item-2084" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2084"><a href="http://www.flickr.com/">Flickr</a></li> <li id="menu-item-2085" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2085"><a href="http://blog.flickr.net/">Flickr Blog</a></li> <li id="menu-item-2250" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2250"><a href="http://twitter.com/flickr">@flickr</a></li> <li id="menu-item-2086" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2086"><a href="http://twitter.com/flickrapi">@flickrapi</a></li> <li id="menu-item-2087" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2087"><a href="https://www.flickr.com/services/developer/">Developer Guidelines</a></li> <li id="menu-item-2088" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2088"><a href="http://www.flickr.com/services/api/">API</a></li> <li id="menu-item-2089" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2089"><a href="http://www.flickr.com/jobs/">Jobs</a></li> </ul></div> </nav><!-- #access --> </header><!-- #branding --> <div id="main"> <section id="primary"> <div id="content" role="main"> <header class="page-header"> <h1 class="page-title author"> Author Archives: <span class="vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="Kay Kremerskothen" rel="me">Kay Kremerskothen</a></span> </h1> </header> <nav id="nav-above"> <h3 class="assistive-text">Post navigation</h3> <div class="nav-previous"><a href="https://code.flickr.net/author/flickrphotography/page/2/" ><span class="meta-nav">&larr;</span> Older posts</a></div> <div class="nav-next"></div> </nav><!-- #nav-above --> <div id="author-info"> <div id="author-avatar"> <img alt='' src='https://secure.gravatar.com/avatar/06e1b01eceade0a86c05b2a6ba1bb59e?s=60&#038;d=identicon&#038;r=g' srcset='https://secure.gravatar.com/avatar/06e1b01eceade0a86c05b2a6ba1bb59e?s=120&#038;d=identicon&#038;r=g 2x' class='avatar avatar-60 photo' height='60' width='60' decoding='async'/> </div><!-- #author-avatar --> <div id="author-description"> <h2> About Kay Kremerskothen </h2> Kay is a Community Manager for Flickr and passionate about extraordinary photography. As an editor on Flickr Blog he loves to showcase the beauty and diversity of Flickr in his posts. When he's not blogging or making Flickr more awesome (in front of and behind the scenes), you can find him taking pictures with his beloved Nikon and iPhone, listening to Hans Zimmer's music or playing board games. | On Flickr you can find him at https://flic.kr/quicksilver </div><!-- #author-description --> </div><!-- #author-info --> <article id="post-1716" class="post-1716 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2011/10/11/talk-real-time-updates-on-the-cheap-for-fun-and-profit/" rel="bookmark">Talk: Real-time Updates on the Cheap for Fun and Profit</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2011/10/11/talk-real-time-updates-on-the-cheap-for-fun-and-profit/" title="5:42 pm" rel="bookmark"><time class="entry-date" datetime="2011-10-11T17:42:51-07:00">October 11, 2011</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>Superheros Neil and Nolan (N&amp;N) just gave their talk on our real-time PuSH system at Web 2.0 in New York. You can <a href="http://l.yimg.com/g/assets/talks/w20nyc.pdf">download the slides</a>, or view the contents I&#8217;ve oh-so-roughly transcribed here:</p> <p><strong>Oh Hai My name is Nils.</strong></p> <p>My name is Neil Walker, I’m an Engineer at Flickr.</p> <p>I mostly work on the back-end infrastructure portion of things. I’m going to be telling you about a system Nolan and I built to send real-time updates about things that happen on Flickr out to the things and people that want to know about them.</p> <p>My portion of the talk is mostly going to be concerned with the back-end. How did we build the guts of the system and what we think are the key components.</p> <p><strong>What I’m not talking about.</strong></p> <p>This talk isn’t about building Twitter, or a full-scale pubsub system built from the ground-up to handle millions of updates per second. Instead it’s geared around what you can do with less. What if you’ve got an existing site with lots of functionality already built and you want to add some real-time notification capabilities to it? What can you do if you don’t have a lot of resources to throw at the problem?</p> <p>It turns out that you can get pretty far with some bits and pieces that many sites will already have, and how to fit those bits together is what we’re going to cover.</p> <p><strong>This is Nolan</strong></p> <p>Hi, I’m Nolan Caudill, a backend engineer at Flickr. I work on most of our backend systems, but focus mainly on geo, the API, internationalization and localization, and general site performance.</p> <p><strong>The Fun Part</strong></p> <p>I’m going to focus mainly on why you would want to use the new push api, what problems it solves, and why it’s better in some cases than our traditional pull-based APIs. Also, I’m going to talk about how you get up and running with the new APIs. We know it’s a bit of mindflip from the traditional APIs and we want to help you with the transition on getting up and running with it.</p> <p>So this was the Wired cover for March 1997. From the article: “Media that merrily slip across channels, guiding human attention as it skips from desktop screen to phonetop screen to a car windshield. These new interfaces work on the emerging universe of networked media that are spreading across the telecosm.” Whatever that means. So that was almost 15 years ago. “Kiss your browser goodbye”! didn’t exactly pan out.</p> <p><strong>Flickr PuSH</strong></p> <p>But jumping forward to now, we DO have all those screens. Most of us have 3, 4 or more devices with high-resolution displays on them, almost all of them with web browsers, and varying levels of human interfaces. Some of them even have cameras. Lots of us have 2nd or even 3rd monitors at work. For Flickr, having apps that can be used to explore photos on all those different screens regardless of user interface seems only natural, and that’s what we hoped to inspire with a real-time API. Before we get into the details I’d like to show a simple little application that Aaron Cope, who used to work at Flickr, built on top of our PUSH API, just to see what would happen.</p> <p><a title="05072011051 by straup, on Flickr" href="http://www.flickr.com/photos/straup/5695041843/"><img fetchpriority="high" decoding="async" src="http://farm4.static.flickr.com/3080/5695041843_8a67420414.jpg" alt="05072011051" width="500" height="375" /></a></p> <p>Basically it’s a simple web page that can be run full-screen on almost any device that has a browser. It lets you subscribe to various streams on Flickr like photos from your friends, photos that your friends favorite, photos with particular tags, and photos from a location. It then receives these photos in more or less real-time and displays them full-screen with the title overlaid on top of the photo.</p> <p>That’s it. No controls, no user interface after the initial point of telling it what you’re interested in.</p> <p><a title="05052011035 by straup, on Flickr" href="http://www.flickr.com/photos/straup/5691236091/"><img decoding="async" src="http://farm6.static.flickr.com/5268/5691236091_692642fb54.jpg" alt="05052011035" width="500" height="375" /></a></p> <p>So Flickr being a social website, you start to get photos of your friends, and the things that happen to them. You also get photos of things that your friends are interested in. As they fave them, they show up in your live stream. Then you might fave the same image, and more of your friends see it, fave it, and so on. It’s a very natural way for a popular object to percolate around your network, and often you don’t have to worry about missing something good because if it’s popular it’ll bubble back up as another one of your contacts faves it later on.</p> <p><a title="screens / beget screens by straup, on Flickr" href="http://www.flickr.com/photos/straup/5762394807/"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3272/5762394807_62639be9cc.jpg" alt="screens / beget screens" width="500" height="375" /></a></p> <p>Interesting things start happening when you have devices with screens AND cameras &#8211; a photo makes it into someone’s stream, they take a photo of it and upload that, which makes it back into the original uploader’s stream, who faves it, etc. and you get a sort of live conversation taking place with photos. Knowing that when you upload a photo it’s going to appear on someone’s monitor or widescreen TV adds a new dimension to photosharing.</p> <p>Eventually it can get a bit ridiculous.</p> <p><a title="metapua by : Nils, on Flickr" href="http://www.flickr.com/photos/slopetrash/5762989098/"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3163/5762989098_726b811249.jpg" alt="metapua" width="500" height="374" /></a></p> <p><strong>A bit of history.</strong></p> <p>Anyway, we’ll go back to how all this got started at Flickr.</p> <p>&#8211; A while ago we had the need to get new public uploads/updates to search partners in a timely manner. Why? &#8211; Being crawled is fine but is expensive for the crawler and crawlee and isn’t always accurate. &#8211; Making a specific API for that purpose could do it but is probably going to be clunky, require too much work by both parties, and means that whatever it is that’s hitting your API can potentially affect your site performance, or other API users. Pushing out updates where you control the flow is really the way to go. &#8211; What did we build?</p> <p>&#8216;headers&#8217; =&gt; array( &#8216;Content-type&#8217; =&gt; &#8216;application/atom+xml&#8217;, &#8216;User-Agent&#8217; =&gt; &#8216;Flickr Kitten Hose&#8217; )</p> <p>Basically a firehose of public uploads/updates (incl. deletions/privacy changes) that we can aim somewhere and turn it on. Similar to the Twitter firehose, but for photos uploaded to Flickr. So essentially stuff happens on Flickr, we transform the events into some easily parseable format, bundle everything up into reasonable-sized blobs and POST it to a web server somewhere that consumes the data.</p> <p><strong>PubSubHubbub: Thanks, Google</strong></p> <p>Rather than invent a specific format/protocol we decided to pick something familiar. Something that already has a spec, is well-documented and is well-understood. So we picked a common Publish-Subscribe protocol. Google’s PubSubHubbub has definitions for how to publish a feed to a hub, how to subscribe and unsubscribe to topics and verify subscriptions and all the other good things that people who write specs like to specify. Of course we didn’t actually need a lot of the spec to accomplish our goals for just the firehose bit, but we did have in the back of our minds the thought of expanding on what we built.</p> <p>So choosing at the start to meet an accepted spec was probably a good idea, and allowed us to get going without having to think too hard about what might come later &#8211; because someone else already figured out how that stuff should work.</p> <p><strong>What?</strong></p> <p>A hub with no subscription control and a single hard- coded endpoint. If you want to fit the firehose idea into the PubSub metaphor then essentially what we built at first was a hub (i.e all of Flickr) that has one hard-coded “topic” that can be subscribed to &#8211; namely all public uploads &amp; updates. Except that the subscribing and unsubscribing part involves humans agreeing to things and then turning the firehose on, instead of machines POSTing to web servers and receiving callbacks, etc. But the pubsub metaphor still fits.</p> <p><strong>How?</strong></p> <p><a title="Phil exploded by : Nils, on Flickr" href="http://www.flickr.com/photos/slopetrash/4276535489/"><img loading="lazy" decoding="async" src="http://farm3.static.flickr.com/2719/4276535489_3de723d30b.jpg" alt="Phil exploded" width="500" height="375" /></a></p> <p>So how did we build it? What I’m going to get into now are what I think are the key pieces of the back-end. I’ll be glossing over some parts of it that are pretty basic and not terribly interesting in the interests of focusing on the good stuff.</p> <p><strong>Async task system Gearman, etc</strong></p> <p>The first (and probably the most critical) part of what we built is an asynchronous job queue, or what we often call our Offline Task System. Essentially it’s a way of de-coupling expensive work from the web servers. Sooner or later you end up wanting to perform an operation that takes longer than you want to make a user at the other end wait (or places more load on your web servers than you care to suffer) and so you want to off-load that work somewhere else.</p> <p>We use this concept EVERYWHERE. Examples: Modifying/deleting large batches of photos. Computing recommendations for who you might want to add as a contact. We have several hundred different tasks, and there are often thousands of them running in parallel. We built our own that fits our specific needs, but a great open-source example is Gearman. Mature, easy to set up and use and scales quite well.</p> <p><strong>Stuff happens on flickr.</strong></p> <p>So the very start of the flow of our system begins when stuff happens on Flickr. The obvious example is a photo upload, often involving cats. Other things we’re interested in are updates: Changing the title, the description or other meta-data of a photo. Also we want to provide updates when photos are deleted (or have their visibility switched to private, which in terms of updates should look the same as a deletion)</p> <p>Insert a task. olt::insert(‘push_new_photo’, $photo_id);</p> <p>When any of these things happen, we simply insert a task into our offline task system. It’s very low cost, doesn’t block and once that’s complete everything else that happens is completely de-coupled for the web servers.</p> <p><strong>Task runs.</strong></p> <p>Insert the event into a queue When the task runs, all it does is take the event it represents, transforms it into a little blob of JSON and sticks it in a queue.</p> <p><strong>Redis Lists</strong></p> <p>RPUSH kitten_hose blob_of_json</p> <p>For the queues we chose to use Redis Lists. For those of you not familiar with Redis yet, it’s an open-source, memory-only, key-value store that supports structured data. Think memcached with commonly-used data structures like hashes, sets, and lists and efficient operations on them. At this point Redis is fairly stable and reliable, the performance is fantastic, and it’s dead-simple to use.</p> <p>We accept a little bit of a HA compromise for the fact that it’s RAM-only and not clustered (yet), so if it’s down you’ll drop some updates on the floor. But our goal here is just building a firehose for updates &#8211; not a 100%-reliable archive of user activity. We can happily accept the trade-offs that using Redis implies.</p> <p><strong>Uhhh</strong></p> <p>Why didn’t we just insert into the queue in the first place? At this point it might occur to you that instead of inserting a task (into a queue) that when it runs just inserts something else into another queue, why not just insert the thing into the queue in the first place? We’ll get to that in a bit.</p> <p>However, two useful properties of our task system are that 1) you can insert tasks with a delay before they run, and 2) task inserts with the same arguments as an existing task fail silently &#8211; i.e. you only get the one task.</p> <p>The upshot is that you can set a delay on things like updates so that a number of updates that happen over a short period (example) only generate a single task &#8211; and when it runs it finds all of them.</p> <p><strong>Then what happens?</strong></p> <p>Summary:</p> <p>So at this point you’ve got events happening all over Flickr, uploads and updates (around 100/s depending on the time of day), all of them inserting tasks, and the task system grabbing each event and sticking it in a queue, in the form of a Redis list.</p> <p>The next step is obviously to consume the queue.</p> <p><strong>Cron</strong></p> <ul> <li> Insert more tasks!</li> <li> Consume the queue</li> <li>LPOP kitten_hose</li> </ul> <p>You could have a daemon that sits there and consumes the queue and makes posts to the endpoint and you’re done. Basically what we do is have a cron job that periodically looks at the queue and inserts one or more tasks (depending on the size of the queue) whose job is to drain a particular number of updates from the queue and send them to the endpoint.</p> <p>At first this is going to seem needlessly complicated but you’ll see why when we get to generalize the system for an arbitrary number of subscribers. One thing it does however is provide a convenient way to throttle and buffer the output &#8211; depending on what your endpoint can handle you can twiddle the knobs on your task system to run more jobs in parallel, or turn it down to choke off the firehose (and maybe drop updates on the floor if the queue gets too big&#8230;). It gives you flexibility by decoupling the delivery mechanism.</p> <p><strong>Draw me a picture</strong></p> <p>It’s probably time for a simple diagram: Things happening on Flickr trigger the creation of tasks, that run asynchronously, and stuff things into a queue in Redis. Periodically more tasks get triggered to drain the queue and push whatever they find back out to the eventual destination.</p> <p><strong>It worked pretty well.</strong></p> <ul> <li>Didn’t take the site down</li> <li>Backfill 3B photos</li> </ul> <p>So it turns out that it all actually worked pretty well.</p> <p>The decoupling of everything using the asynchronous task approach meant that it had pretty much zero impact on the rest of the site. We could develop it live, experiment with capacity etc without having any fear of impacting site performance for our users because of how loosely-coupled all the pieces were &#8211; if anything goes wrong the damage is limited to that little piece.</p> <p>Eventually we decided to run a backfill to a 3rd-party search index of all of our public photos back to the beginning of Flickr (somewhere around 3 billion) and we were able to complete it in 2 weeks, using our existing task system and the only new hardware being one redis box for the queues. So, it worked really well!</p> <p><strong>Photos from your friends</strong></p> <p>So we started to ask what if we wanted to make it into something that might be useful on an individual level? It seemed to be pretty reliable and low-impact to the site and showed promise of scaling fairly well. The obvious thing that would be of interest on a user level would be photos from your contacts; Here’s my endpoint, POST stuff to me when my friends upload new photos (or change existing ones).</p> <p>Other things that could be of interest are when your contacts favorite a photo, or when you or your contacts are tagged in a photo, or when someone anywhere on Flickr tags a photo with the tag “kitten”. But let’s start with photos from your contacts.</p> <p><strong>What?</strong></p> <ul> <li>A list of users (we have those!)</li> <li>An endpoint (URL)</li> </ul> <p>Looking at what we’d need to add to the system to support user-specific subscriptions there’s the obvious: a record of the subscription somewhere in a database, the callback mechanism as per the pubhub spec etc. I’m going to gloss over that because it’s pretty straightforward and not really interesting. What we need to look at to manage the updates and figure out who gets what though is basically a mapping of users (the contacts who upload photos) to endpoints (i.e. subscribers).</p> <p><strong>Moar Redis</strong></p> <p>SADD user_1234 endpoint_5678</p> <p>So we turned to redis again. Redis offers a set datastructure that we can use to maintain this relationship pretty easily. When someone wants to subscribe to photos from their contacts, we create a redis set for each of their contacts and add to it a pointer to the endpoint.</p> <p><strong>The task again.</strong></p> <p>SMEMBERS user_1234</p> <p>foreach $endpoint RPUSH $endpoint json_blob</p> <p>So now we go back to the Task that gets inserted in response to something happening on Flickr. Where it used to just insert an event into “The” single queue, now it looks at the set of endpoints that are interested in uploads from that user and inserts the event into the queue for each of them. Hopefully now you can see why we have a task do this rather than do it on the web server while the user waits &#8211; it’s still going to be quick because the queue inserts in redis are constant-time, but if you have a lot of contacts they could add up. Best to just de-couple it all and not worry about running into that problem.</p> <p><strong>Cron again.</strong></p> <p>Insert tasks for each queue (endpoint)</p> <p>Now we turn to draining the queues, and again you can see where the task system comes in. With a large number of endpoints all the cron job has to do is run through each of them, look at how many events need to be consumed and insert an appropriate number of tasks. So scaling the output part of the system (and actually most of the input too) then becomes a matter of scaling your task system &#8211; and that’s a problem that’s already been solved.</p> <p><strong>Cache is your friend.</strong></p> <p>One thing that plays a key role in the system that I haven’t talked about is a caching layer. Almost any site of a reasonable size these days has a memcached layer (or something similar) in between the front-ends and the databases. One of the great things this for a push system is that because you’re dealing with things as they happen, all the objects that you typically need to access to build your update stream are almost always in cache &#8211; because they were just accessed.</p> <p>So it turns out that not only does a system like this have almost no impact on your normal page serving time (because of all the de-coupling), it also ends up having very little impact on your databases, due most things being in cache.</p> <p><strong>Redis Numbers</strong></p> <ul> <li>1000 subscriptions</li> <li>50K keys (queues) / 300 MB</li> <li>100 qps &#8211; 8-core / 8GB 5% cpu</li> </ul> <p>And a few numbers showing how Redis is performing as our little DIY queueing and subscription system. 1000 subscriptions for various different things takes around 50K keys, consuming 300 MB of ram, and at about 100 qps on an 8-core Linux box it’s barely ticking over.</p> <p><strong>3 Things: Cache, Tasks, &amp; Queues</strong></p> <p>So putting all the pieces together it’s actually pretty simple, and relies upon things that you probably already have: a caching layer, some kind of asynchronous task system and rudimentary queueing system. Even if you don’t have all of these they’re pretty well-understood pieces and there are lots of open-source options to choose from: Just grab memcached, gearman, and redis and off you go.</p> <p>We think you can go a long way to building the back-end of a decent push update system with just these simple pieces. So that’s the end of my portion of the talk, and now I’ll turn it over to Nolan to talk more about the front-end, and how to make it easier for a client to consume updates.</p> <p><strong>Why Push? And what&#8217;s in it for me.</strong></p> <p>So now that Neil&#8217;s explained the original reasoning and how we built a system for real-time push, the question is why, as an API consumer, would you want to use it which leads into how to get started with it.</p> <p><strong>Flickr API = Great Success</strong></p> <p>Tens of thousands of keys making hundreds of millions of calls per day. By any measure, Flickr&#8217;s tradtional pull-based API has been incredibly successful. Developers that have used our API range from Fortune 10 businesses to PhD students to hundreds of thousands of other developers that just want to do something fun with photos and the data around them.</p> <p>I checked the numbers this wekend and and on average, we’ve got around 10,000 different API keys making hundreds of millions of calls per day. It&#8217;s easy. Just use your browser.</p> <p>One major reason for its popularity is due to how easy it is to use. When you combine data that people want with an API that’s easy to use, you thousands and thousands of apps built with it.</p> <p>There’s effectively no barrier to entry to get started with using Flickr’s API, if you have a browser, you can access every single API method. And if you want to do this programatically server-side , say a nightly-cronjob that fetches your own uploads, you can can just do a simple &#8216;curl&#8217;. Either way, our pull-based API is a one-liner: whether that line is a URL in your browser, or a one-line shell script.</p> <p><strong>We’ll curl it for you.</strong></p> <p>It’s even simpler than that, if you need it to be. Even if you don&#8217;t have a server, or don&#8217;t want to read our documentation, just use the API Explorer on Flickr. Every API method we publicly support is represented here, not just in documentation, but on an actual real-life form for you to build queries with. Just fill out the form, press enter, and we&#8217;ll curl it for you and spit out the results in pretty JSON or XML format.</p> <p>One side benefit of this pull-based API is that it also makes debugging easy. You can run tons of API queries as fast as you can debug. This is huge when building a new system. If you formed the call correctly, you get data. If you messed up, modify, rinse, and repeat. Flickr is a visual news feed.</p> <p><strong>So back to “why push”&#8230;</strong></p> <p><a title="haight &amp; fillmore fire by sethoscope, on Flickr" href="http://www.flickr.com/photos/sethoscope/6190891920/"><img loading="lazy" decoding="async" src="http://farm7.static.flickr.com/6168/6190891920_411411447d.jpg" alt="haight &amp; fillmore fire" width="500" height="333" /></a></p> <p>I’m the kind of programmer that needs to have a fairly-focused work environment, but I also like to know what’s going on in the world at the same time. Twitter is a really great news-before-its-News service, but for me it’s a bit too distracting to have running on a screen next to my code.</p> <p>I’ve recently hooked up my second monitor to show me two Flickr real-time feeds: the first of which is the Commons, our group of accounts belonging to museums and various historical archives, which is motivational for me, reminding me that Flickr is really something special with real history being stored on it.</p> <p>The second feed is what I consider my glimpse into what’s going on around me, the stuff that immediately affects me. So I’ve hooked up a push feed of photos geotagged a half kilometer around my house and a half-kilometer around the office.</p> <p>Around my house, it’s usually people taking pictures of the Painted Ladies in Alamo Square but then I saw this picture come across. When your entire neighborhood is made up of densely-packed, 50+ year old homes made of questionable building materials, fires are scary things. I then jumped on Twitter and found out there was a multiple-alarm fire just a few blocks from my house. Also, around the office, just this past week, I saw pictures from San Francisco’s chapter of the OccupyWallStreet protests. The news wasn’t on the ground yet but here I was seeing live and extremely relevant things to me. Like they say, a picture can often say more than a thousand words.</p> <p>Subscribing to a real-time stream of photos from a specific location is about as hyperlocal as you can be. Maybe I wouldn’t follow the right people on Twitter, or I wouldn’t check the local news site until later that evening, but people do take pictures of important things and events and post them to the site, and it doesn’t get much more timely than real-time.</p> <p><strong>Push is the opposite of Pull. And other obvious facts.</strong></p> <p>Ok, so back to the technology. Push is a completely different animal than our pull-based APIs. Consuming a real-time push feed flips most things about our API on its head. For starters, you&#8217;ll need a full-fledged, always-on web server that is exposed to the public Internet. And running on this web server will be a software that is based on a protocol described in a spec-with-a-capital-S. This demands a lot more from the developer than just a one-line curl.</p> <p><strong>Pull: Hit this URL and read Push: Read this spec and wait</strong></p> <p>With our regular API, you could basically treat it as a function call, that is &#8220;ask for something, get something&#8221;. With Push, now you&#8217;re implementing full protocol where the data will come *eventually*. Now you&#8217;ll need to wade through pages of dense and sometimes ambiguous instructions, so you can wait for us to tell you that your friend uploa a picture. Another big but obvious difference between the push and pull API is that with Push, your application only gets what we send to you. There&#8217;s no looking back in time. And due to this read-spec, write-code, and wait and wait some more debug cycle, it takes awhile to get the endpoint working correctly.</p> <p>Personally, I have access to the Flickr codebase and I still had questions about how to make my endpoint do the right thing and it took me a few good hours one morning and more than one cup of coffee get working right.</p> <p><strong>So, seriously, why Push?</strong></p> <p>Because it&#8217;s fantastic. Well now that I&#8217;ve made Push seem a little scary, seriously, why Push?</p> <p>The main thing, I think, is that you we give you things as they happen. If you are building a site that uses Flickr data, would you rather us send y data when we get it, or set up a bunch of cronjobs that fetch possibly non-changing data? Even if you like your cronjobs, what happens when yo have 100 users? 1000 users? 1000000? I do want to make one note about what we mean by “real-time.” As you saw from Neil&#8217;s presentation, there are levels of queues and consumers, each having a non-zero delay. We get you the photo data as soon as we can, but this might be a few seconds after we receive up to a minute or two. We understood we were building a real-time feed for photos uploaded to a website, and not sending direction data to a nuclear warhead. It was okay if things were off by a few seconds.</p> <p><strong>Flickr Globe</strong></p> <p>Also, receiving real-time photo updates are great for certain types of problems. So, we had the press in the office recently and we wanted to build something cool to run on our big screen behind the developers as we worked. Using the real-time push feed, one of our front-ends, Phil Dokas, was able to build an interactive globe, that charted where on Earth our photos were being uploaded from in real-time.</p> <p>The great thing about this is, not only was it really pretty looking visualization, but it impressed upon me the magnitude of Flickr being used. The site felt really organic: you could see it being used and growing. For me, it was one of those ‘pale blue dot’ moments that Carl Sagan talked about, where you get a sense of the bigger picture of what you work on. When you work deep on the backend of Flickr, it’s easy to forget that these are real people from literally everywhere on Earth, uploading things they want to remember.</p> <p>And using the pushed data made this really easy to build. And for reference, this about 3 minutes worth of publically geotagged photos.</p> <p><strong>I want to use it, BUT&#8230; &#8230;it sounds hard.</strong></p> <p>So, we think this stuff is awesome, and we want you to use it but we do understand it&#8217;s fiddly. The spec is dense and debugging is painful. So we&#8217;re going to give you a headstart on the whole thing. We&#8217;ve written a tiny web server that handles all the subscription handshake stuff and the parsing of photo data and we put it on GitHub, and open sourced it.</p> <p><strong>flickr-conduit</strong></p> <p><a href="https://github.com/mncaudill/flickr-conduit" rel="nofollow">https://github.com/mncaudill/flickr-conduit</a></p> <p>So we’re introducing, flickr-conduit which is a simple server written in node.js that handles the subscription stuff, keeps tabs on when to unsubsc users, and then finally receives the Flickr posts.</p> <p>There&#8217;s a lot of moving parts to setting up a push endpoint: handling everything from getting users authenticating your API key, telling you what topics they&#8217;re interested in, setting up the subscription between your callback and Flickr, and then figuring out how to get those events to the use when Flickr sends them to you.</p> <p>In the conduit repository, I’ve included a server that implements the push protocol and then just fires off events in JavaScript when Flickr sends something. I’ve also included a PHP application that handles authenticating the user, letting them pick their topics, and then finally showing them the photos they come in as just a demo of what it can do.</p> <p>I didn’t have time to get a slide up for it, but Tom Carden from Bloom, took the PHP application I made and got it ready so that you can easily de it to heroku. It’s great and if you want to run your own conduit-server demo, I’d advise checking it out.</p> <p>To start with, Conduit is the piece that represents your callback endpoint. You tell Flickr that this is your endpoint and it handles the rest of the subscription handshake stuff and when Flickr posts data to you server, it goes to your conduit server.</p> <p><strong>Pub/Sub all the way down</strong></p> <p><a title="Fractal Fun! The Christmas Tree Vegetable by French Tart, on Flickr" href="http://www.flickr.com/photos/frenchtart/5461147989/"><img loading="lazy" decoding="async" src="http://farm6.static.flickr.com/5051/5461147989_eaebd4f7ed.jpg" alt="Fractal Fun! The Christmas Tree Vegetable" width="500" height="469" /></a></p> <p>After I finished building this, I realized that unintentionally I took the idea of pub/sub, that is having a many subscribers expressing interesting in many published streams, and kept that spirit up all the way through to the end of my little node.js server and explaining this model sheds some light on how the conduit server works.</p> <p>First, as Neil described, Flickr runs a pub/sub service. With conduit, you can run your own pub/sub server that talks to our published streams. And inside of conduit, we let your application code subscribe to certain events that conduit itself receives Flickr post events, and publishes them to your app code to handle how you wish. So there are technically 3 different pub/sub hubs before your app code, which sounds a little complicated, but if you grasp the mental model of how pub/sub works, you understand the full system. Also, keeping the pub/sub levels decoupled from each other provides a lot of simplicity and flexibility.</p> <p>So, this is a small digression about architecture that I stumbled across while building, that when you’re building systems in the small, it sometimes makes a lot of sense to model the smaller system after the larger system. This may not be groundbreaking for some, but I just found it really neat.</p> <p><strong>I know &#8216;curl&#8217;. Now, tell me about Push.</strong></p> <p>With Conduit, we&#8217;ve removed a fair amount of the fiddly bits so you can get to doing the fun parts later. I’m going to step you through each of the steps of the whole pubsub flow to show you what each part does.</p> <p>First, subscribe the user to a topic.</p> <p>/callback?sub=user1234-contacts-faves</p> <p>First, you&#8217;ll want to subscribe the user to a topic. Topics can be anything from their own uploads, to their contacts uploads, to their contacts faves and even photos being geotagged at a specific place.</p> <p>During this subscription, you&#8217;ll specify a callback. Once you have conduit running, the callback URL you give Flickr in your subscription should p to the conduit server. One of the tricky parts of handling a multi-user pub/sub server is that you can have many users attached that are waiting for many different strea When Flickr posts a piece of data to your server, you only know the URL that it came in on, so the callback URL needs to be significant so you ro this piece of data to right consumer. One way to handle this is just create a globally unique identifier for a particular subscription, and tuck that in a database and then use that to ma URL to a subscribed user.</p> <p>Personally, I’m a fan of reducing the number of moving parts as much as possible so I just use something simple, like the example, and make th URL itself identifiable. So for example if user 1234 wants to subscribe to her contact’s faves, the URL could simply be /user1234-contacts-faves. As long as this callback URL creation algorithm is repeatable, it’s easy to have a common dictionary throughout your app of how to handle a particular subscription.</p> <p><strong>The part of the play where Flickr asks a question.</strong></p> <p>Almost immediately, after making the API call to subscribe the user to a topic, Flickr will respond with a &#8216;subscribe&#8217; request. Here Flickr is basically asking, &#8220;Hi. We just received a subscription request for this callback. Did you send this? If so, repeat the magic password back to me.” All this is described in detail in the spec but you don’t need to know any of that. Conduit handles all of this for you.</p> <p><a title="1286128288489 by Kevin, on Flickr" href="http://www.flickr.com/photos/kevincollins/5048199686/"><img loading="lazy" decoding="async" src="http://farm5.static.flickr.com/4103/5048199686_a8f662d820.jpg" alt="1286128288489" width="500" height="302" /></a></p> <p><strong>Debugging: the waiting game.</strong></p> <p>As I mentioned earlier, debugging the server was the most boring and thus most frustrating part of building an endpoint. I’d write some code, and to see if did the right thing, I had to wait for on my subscribed events happen and get sent to me. One shortcut I found is that I could subscribe to my own faves, and then I go through a test account and fave photos, therefore forcing events. This sped things up quite a bit.</p> <p><strong>Someone&#8217;s at the door!</strong></p> <p>/callback?sub=user1234-contacts-faves maps to user1234-contacts-faves</p> <p>When an event does occur, Flickr will post it to your callback URL. Conduit then takes this callback URL and through whatever method you decide, creates an event name for it and then passes into a internal structure, In my running example, the callback you see up there gets the event name of “user1234-contacts-faves”. Like I said, you could do this mapping of URL to event name however you like, but this is simple and works for me.</p> <p><strong>The emitter emits.</strong></p> <p>user1234-contacts-faves</p> <p>Conduit exposes something that node.js calls an &#8220;emitter&#8217;. It&#8217;s basically a pub/sub structure itself. So when a post comes in and you decode the callback URL into an event name, you tell Conduit about it. Effectively you’re saying, &#8220;Hey I just received some data and it has this event name. Give this data to anyone that is interested in user1234- contacts-faves.&#8221;</p> <p><strong>Finally, fun photo data.</strong></p> <p>Your application code registers with this emitter what events it&#8217;s interested in and when they come in, they do something fun with them. Now I&#8217;m going to explain a bit about the mini-app that I bundled with the conduit server to shows how this works.</p> <p><strong>Real-time stream</strong></p> <p>Flickr + conduit + node.js + socket.io. I wanted the final product to be a simple webpage that when my subscriptions were posted to, I&#8217;d see them simply shown up on the page. I glued a few fun tools together and got something really interesting.</p> <p><strong>PHP is my engine.</strong></p> <p>Handles the PuSH subscribing and printing out my JS. This entire sample app could easily have been written in JS, but PHP is what I what do all day at work and my JavaScript is a little rusty and I wanted to get something up and running quickly.</p> <p>I was able to grab former-Flickr engineer, now Etsy CTO, Kellan Elliot-McCrae’s flickr.simple.php’s library to handle the authentication of the use well as posting of subscription requests.</p> <p>So the PHP app I built, first authenticates the user with Flickr and then presents the list of real-time topics he or she can subscribe to. The user checks the topics they&#8217;re interested in and then gets redirected to a screen where the images will show up as they come in. I create the unique callback URLs and at the same time dump these event names into the Javascript on the page so that my socket.io code can my node server, what events it&#8217;d like to receive when they come in.</p> <p><strong>Meanwhile, back on the server&#8230;</strong></p> <p>As mentioned earlier, Flickr will post the photo data to the conduit server to my callback URLs. This callback URL directly becomes the event name, mapping exactly to the what the browser told socket.io that it was interested in. When this event comes in, conduit hands it to the emitter, which then broadcasts the event back out. The server-end of the socket then receives this and pumps down into the browser&#8217;s open arms.</p> <p><strong>And back to the browser.</strong></p> <p>The browser then receives the photo data and inserts it into the page. That was a whole lot of engineering just to get an image up on the screen and we know it, but with flickr-conduit, you can skip most of the details and just write fun code. This actually turns out to be a great way to explore Flickr.</p> <p>On my extra monitor at work, I&#8217;ve subscribed to my contacts&#8217; faves and all the updates from our Commons area (which includes museums and various archives) to get a weird blend of 100 year old pictures and things my friends find interesting.</p> <p><a title="An American soldier with a joey, 1942 by Australian War Memorial collection, on Flickr" href="http://www.flickr.com/photos/australian-war-memorial/3527155504/"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3623/3527155504_6a47fb4988.jpg" alt="An American soldier with a joey, 1942" width="500" height="376" /></a></p> <p>So, in summary, this is the why and the how of Flickr added a real-time push feed on the cheap. Since we know the spec is dense, and there are several moving parts, we also hope flickr-conduit helps out new developers that will hopefully le to love our new real-time feeds as much as they love our existing pull feeds.</p> <p>Thanks.</p> <p>Related links:</p> <p>conduit: <a href="https://github.com/mncaudill/flickr-conduit">https://github.com/mncaudill/flickr-conduit</a><br /> tom&#8217;s conduit links: <a href="https://github.com/RandomEtc/flickr-conduit-front">https://github.com/RandomEtc/flickr-conduit-front</a>, <a href="https://github.com/RandomEtc/flickr-conduit-back">https://github.com/RandomEtc/flickr-conduit-back</a><br /> pubsubhubbub spec: <a href="http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html">http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html</a><br /> pua: <a href="http://pua.spum.org">http://pua.spum.org</a> (<a href="http://www.aaronland.info/weblog/2011/05/07/fancy/">http://www.aaronland.info/weblog/2011/05/07/fancy/</a>)<br /> nolan&#8217;s conduit: <a href="http://nolancaudill.com/projects/conduit/">http://nolancaudill.com/projects/conduit/</a><br /> flickr globe: <a href="http://nolancaudill.com/~pdokas/flobe/">http://nolancaudill.com/~pdokas/flobe/</a><br /> kellan&#8217;s post: <a href="http://laughingmeme.org/2011/07/24/getting-started-with-flickr-real-time-apis-in-php/">http://laughingmeme.org/2011/07/24/getting-started-with-flickr-real-time-apis-in-php/</a></p> <p>neil&#8217;s flickr post: <a href="http://code.flickr.com/blog/2011/06/30/dont-be-so-pushy/">http://code.flickr.com/blog/2011/06/30/dont-be-so-pushy/</a></p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1716 --> <article id="post-1657" class="post-1657 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2011/09/01/the-code-behind-geofences/" rel="bookmark">The Code Behind Geofences</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2011/09/01/the-code-behind-geofences/" title="6:08 pm" rel="bookmark"><time class="entry-date" datetime="2011-09-01T18:08:55-07:00">September 1, 2011</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>We launched geofences earlier this week. I want to give you a glimpse into some of the code that runs the feature.</p> <p>Geofences are defined by three things: a coordinate pair, a radius, and a privacy level. We store the coordinates (ie, decimal latitude/longitude) in our database as integers, the radius as an integer representing meters, and the privacy level as an integer that maps to a constant in our code.</p> <p>When an image is geotagged, we have to determine which geofences the point falls within. We do some complex privacy calculations that Trevor talks about in his <a href="http://code.flickr.com/blog/2011/08/30/in-the-privacy-of-our-homes/">introductory post</a>.</p> <p>First, we fetch all of your geofences from the database and then loop through each one, determining if the photo falls in any geofences. We limit the number of geofences you can create to ten, so running a point through 10 calculations is not that expensive.</p> <p>We use the <a href="http://en.wikipedia.org/wiki/Great-circle_distance">great-circle distance formula</a> to figure out how far the image is from the center of a geofence. If this distance is less than the radius of the geofence, the geofence applies.</p> <p><a title="European detail map of Flickr and Twitter locations by Eric Fischer, on Flickr" href="http://www.flickr.com/photos/walkingsf/5912946760/"><img loading="lazy" decoding="async" src="http://farm6.static.flickr.com/5277/5912946760_becca2264e.jpg" alt="European detail map of Flickr and Twitter locations" width="500" height="359" /></a></p> <h2>MySQL is a Great Hammer</h2> <p>When you create a geofence and want to apply it to existing photos, the backfill is a more involved process. In order to grab just the photos we care about for a geofence, we have to limit the number of photos that we select due to performance.</p> <p>As mentioned, we store the latitude and longitude as integers in MySQL. Since radial queries are next to impossible with this setup, we do a first pass with a <a href="http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates">bounding box formula</a> that encompasses the geofence and then use our great-circle distance formula to cull the photos that don’t fall in the geofence.</p> <p>The bounding box formula we use takes into consideration geographic gotchas, like the poles and the 180 degree discontinuity (ie, the International Date Line). Since a geofence could overlap the International Date Line, we have to modify the DB query in doing the longitude part of our query since doing a simple BETWEEN won’t work. When the bounding box doesn’t cross the IDL, we can do “SELECT id FROM Table WHERE longitude BETWEEN $lon1 and $lon2” and when it does we do a “SELECT id FROM Table WHERE longitude &lt; $west_lon AND longitude &gt; $east_lon”.</p> <p>In short, we use a query that MySQL is a good at it to limit our initial dataset and then use a little post-processesing of the data to get the points we actually care about for the geofence.</p> <p>After we have this bundle of photos that the geofence backfill applies to, we then run our normal geoprivacy calculations, lowering the privacy if the combination of fences calls for it.</p> <h2>Concurrency, or the world keeps turning</h2> <p>The introduction of the geofences backfill pane also introduced some concurrency issues, which were fun to deal with.</p> <p>The contract that we wanted to create for the user was that the calculated privacy that they saw in the preview pane for the backfill would exactly match the result of running the backfill. This seemed intuitive but the implementation was tricky.</p> <p>There are two things that can affect a photo’s geoprivacy: the user’s default privacy and the geofences that exist. These things can change through time, that is between the time that a user hits ‘apply’ on the preview pane and when the backfill finishes running.</p> <p>Whenever you deal with mutable state through the lifetime of a process, you have to change how you treat this state. Who can modify it? What can they modify? And what processes see this updated object?</p> <p>The easiest way to deal with mutable state is to make it immutable. We do this by storing the state of the user’s geoprivacy world (that is, the default geoprivacy and the geofences) alongside the backfill task, so that when the task runs it uses this state instead of querying the DB, which is mutable, possibly having changed since the user hit the ‘apply’ button.</p> <p>Storing this state ensures that regardless of how the user modifies their geoprivacy settings, the result of the backfill will match exactly what they saw in the preview pane, providing a consistent view of the world.</p> <p>We also added the restriction of only allowing one backfill task per user to be running at a time, which simplified our bookeeping, our mental model of the problem, and the user’s expecation of what happens.</p> <h2>Lessons Learned</h2> <p>Geo, by itself, is not easy. The math is complex (especially for someone like me a few years out of school). Privacy is even harder. Since the project evolved quite a bit while we were building it, having a large amount of automated testing around the privacy rules gave us the confidence that we could go forward with new privacy demands without introducing bad stuff into code that was “done”. We learned to give ourselves plenty of time to get all this stuff right, since geo has so many edge cases and violating privacy is an absolute no-no.</p> <p>During the project, we had to keep balance between our users’ privacy and their expectations, all while keeping a complex and new feature understandable and even fun to play with. The only way we could do this was being open and honest between engineering, design and the product team about what we were building. This was a total team effort from Flickr, and we’re very proud of the end result and the control that it gives our users over their presence on the Internet.</p> <p>Also, if you&#8217;re interested in working on fun projects like this one, <a href="http://flickr.jobs">we&#8217;re hiring</a>!</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1657 --> <article id="post-1621" class="post-1621 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2011/08/19/engage-kitten-hose/" rel="bookmark">Engage Kitten Hose</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2011/08/19/engage-kitten-hose/" title="4:56 pm" rel="bookmark"><time class="entry-date" datetime="2011-08-19T16:56:45-07:00">August 19, 2011</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><a title="REAL-TIME KITTENS!!!! by straup, on Flickr" href="http://www.flickr.com/photos/straup/6048205855/"><img loading="lazy" decoding="async" src="http://farm7.static.flickr.com/6067/6048205855_3b76ae93ef.jpg" alt="REAL-TIME KITTENS!!!!" width="500" height="379" /></a></p> <p class="p1">A little while ago we released <a href="http://code.flickr.com/blog/2011/06/30/dont-be-so-pushy/">some new API methods</a> that enabled real-time updates for new photo uploads to be pushed out via a PubSub-like subscription system. Initially you could only subscribe to photos from your contacts and favorites from your contacts. Which was pretty neat, but that barely scratches the surface of stuff that happens on Flickr that people might be interested in. So we added some more stuff to subscribe to. Calling <code>flickr.push.getTopics</code> now gets you:</p> <pre>&lt;rsp stat="ok"&gt; &lt;topics&gt; &lt;topic name="contacts_photos" display_name="photos from your contacts" /&gt; &lt;topic name="contacts_faves" display_name="favorites from your contacts" /&gt; &lt;topic name="photos_of_contacts" display_name="photos of your contacts" /&gt; &lt;topic name="photos_of_me" display_name="photos of you" /&gt; &lt;topic name="my_photos" display_name="your photos" /&gt; &lt;topic name="my_faves" display_name="your favorites" /&gt; &lt;topic name="geo" display_name="photos from an area (geo)" /&gt; &lt;topic name="commons" display_name="photos from the Flickr Commons" /&gt; &lt;topic name="tags" display_name="photos with a tag (or tags)" /&gt; &lt;/topics&gt; &lt;/rsp&gt;</pre> <p class="p2"> <p class="p2">The details for the extra arguments required by the new topic types are part of the <code><a href="http://www.flickr.com/services/api/flickr.push.subscribe.htm">flickr.push.subscribe</a></code> API method documentation.</p> <p class="p2">The <code>my_photos</code> and <code>my_faves</code> topic types are exactly like <code>contacts_photos</code> and <code>contacts_faves</code>, just scoped to your account. The <code>photos_of_me</code> and <code>photos_of_contacts</code> topic types create subscriptions that receive events when you or your contacts are tagged in a photo. The really interesting ones though are the next 3: <code>commons</code>, <code>geo</code>, and <code>tags</code>.</p> <h2>The Commons</h2> <p><a href="http://www.flickr.com/services/api/flickr.push.subscribe.htm"></a></p> <p><a href="http://www.flickr.com/services/api/flickr.push.subscribe.htm"></a></p> <p><a href="http://www.flickr.com/services/api/flickr.push.subscribe.htm"></a><a title="Motorcyclist in Leakey, Texas, near San Antonio, 05/1973 by The U.S. National Archives, on Flickr" href="http://www.flickr.com/photos/usnationalarchives/3704383594/"><img loading="lazy" decoding="async" src="http://farm3.static.flickr.com/2199/3704383594_ed6d66509b.jpg" alt="Motorcyclist in Leakey, Texas, near San Antonio, 05/1973" width="500" height="335" /></a></p> <p>One of the great new subscription types is for photos from the <a href="http://www.flickr.com/commons">Flickr Commons</a>. Set the topic type to commons, and set the <code>nsids</code> argument to a comma-separated list of NSIDs of Commons institutions you&#8217;re interested in (get them by calling <code><a href="http://www.flickr.com/services/api/flickr.commons.getInstitutions.htm">flickr.push.getInstitutions</a>)</code> or just leave <code>nsids</code> empty to get all uploads and updates from the Commons.</p> <h2>Geo Subscriptions</h2> <p><a title="37° 36' 42&quot; N, -122° 23' 25&quot; E by straup, on Flickr" href="http://www.flickr.com/photos/straup/2691179647/"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3105/2691179647_242a51448d.jpg" border="0" alt="37° 36' 42&quot; N, -122° 23' 25&quot; E" width="500" height="500" /></a></p> <p class="p2">With the <code>geo</code> topic type you can subscribe to photos from a particular area, specified as either a set of <a href="http://developer.yahoo.com/geo/geoplanet/">WOE ID</a>s (also, <a href="http://www.flickr.com/places/info/1">here</a>), a set of Flickr Place IDs, or by a point and a radius. The radial query is the obvious choice for creating subscriptions that aren&#8217;t well-known areas, or say, user-specified by dragging a circle on a map. WOE IDs on the other hand are incredibly useful since they represent many well-known geographic features. For example, London, England: <a href="http://www.flickr.com/places/info/44418">WOE ID 44418</a>.</p> <p class="p2">Or, if you&#8217;re like me and completely mad about aviation and anything that flies, there are convenient WOE IDs for airports. Go somewhere like <a href="http://www.airportcodes.org/">this</a>, grab a bunch of airport codes for interesting places, use the Y! GeoPlanet APIs or the <code><a href="http://www.flickr.com/services/api/flickr.places.find.htm">flickr.places.find</a></code> method) to resolve the airport codes into WOE IDs and put them into a comma-separated list in the woe_ids argument of flickr.push.subscribe and watch the aviation photos roll in. For example:</p> <ul> <li>Hong Kong International, China (HKG): <a href="http://www.flickr.com/places/info/24875607">24875607</a></li> <li>Innsbruck Airport, Austria (INN): <a href="http://www.flickr.com/places/info/12510823">12510823</a></li> <li>Keflavik International Airport, Iceland (KEF): <a href="http://www.flickr.com/places/info/12513445">12513445</a></li> <li>Le Bourget Airport, Paris (LBG): <a href="http://www.flickr.com/places/info/22137770">22137770</a></li> <li>Kuala Lumpur International Airport, Malaysia (KUL): <a href="http://www.flickr.com/places/info/28752278">28752278</a></li> <li>etc.</li> </ul> <h2>Tags</h2> <p><a title="Bump by : Nils, on Flickr" href="http://www.flickr.com/photos/slopetrash/3790165652/"><img loading="lazy" decoding="async" src="http://farm3.static.flickr.com/2545/3790165652_529c5cffa2.jpg" alt="Bump" width="500" height="375" /></a></p> <p>We&#8217;ve also added a very basic tag subscription type. With the topic type set to <code>tags</code>, you can provide a comma-separated list of tag names in the <code>tags</code> argument of <code>flickr.push.subscribe</code> and receive uploads and updates from photos containing <em>any</em> (i.e. OR mode) of those tags. Try something like <code>kitten,cat</code> (We&#8217;re not responsible for your bandwidth bill). The tags you specify should be all lower-case and not contain any spaces, so if you want to match something like &#8220;Justin Bieber&#8221; you&#8217;ll need to specify &#8220;justinbieber&#8221;. Coming soon: machine tags! <code>robots:zomg=yes</code>&#8230;</p> <h2>Warning: Code Alert!</h2> <p>In case you missed it, last month Kellan devoted a little Sunday-morning hack time to whipping up a little <a href="http://laughingmeme.org/2011/07/24/getting-started-with-flickr-real-time-apis-in-php/">example</a> of how to get started with the Flickr real-time APIs. It&#8217;s got a little bit about authentication, how to subscribe, processing a feed, and even some PHP. Thanks Kellan!</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1621 --> <article id="post-1532" class="post-1532 post type-post status-publish format-standard hentry category-uncategorized tag-api tag-feed tag-push"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2011/06/30/dont-be-so-pushy/" rel="bookmark">Don&#8217;t be so PuSHy</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2011/06/30/dont-be-so-pushy/" title="4:00 pm" rel="bookmark"><time class="entry-date" datetime="2011-06-30T16:00:49-07:00">June 30, 2011</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p class="p1">You know three things that would be cool?</p> <ul class="ul1"> <li class="li2">the ability to subscribe to the output of a Flickr API call in a feed aggregator</li> <li class="li2">the ability to get the results of Flickr API calls as&#8230;</li> </ul> <p class="p2"> <p class="p2">Oh wait. <a href="http://code.flickr.com/blog/2008/08/25/api-responses-as-feeds/">That</a> was a while ago. Wouldn&#8217;t it be great if you didn&#8217;t have to poll our API over and over just to see if photos were there, only to find out you waited too long since last time and now because of the results size limits you can&#8217;t get them all and you have to figure out how many you missed and then you have to make another call with the right offset to get those results but of course in between then and now the result set changed a bit so you aren&#8217;t sure if you really got them all and&#8230;</p> <p class="p2">Wouldn&#8217;t it be great if Flickr had something that could just PuSH photos to you as they appeared, kind of like <a href="http://www.aaronland.info/weblog/2011/05/07/fancy/#likeadog">this</a>?</p> <p class="p2"><span class="s1">Introducing the new (and experimental) flickr.push API methods. These allow you to subscribe to new uploads and updates from your contacts and favorites from your contacts. Let&#8217;s dive right in and see exactly how it all works:</span></p> <p><a title="&quot;sky captain&quot; by straup, on Flickr" href="http://www.flickr.com/photos/straup/5883650870/"><img loading="lazy" decoding="async" src="http://farm6.static.flickr.com/5276/5883650870_4003695c96.jpg" border="0" alt="&quot;there is a virtuous circle in this ecosystem&quot;" width="500" height="375" /></a></p> <p>The 20,000 ft overview is basically this:</p> <ol> <li>You make an API call to Flickr asking to subscribe to one of several different photo feeds, providing a callback URL in the arguments.</li> <li>A little verification dance ensues during which we make a request to your callback URL. If you respond appropriately we&#8217;re all good and from then on&#8230;</li> <li>Live(-ish) updates are POSTed from Flickr to your callback URL in Atom 1.0 format.</li> </ol> <p></p> <h2>Subscribing</h2> <p>The subscription system is based as closely as possible on Google&#8217;s <a href="http://code.google.com/p/pubsubhubbub/" target="_blank">Pubsubhubbub protocol</a>, with a few wrinkles. One is that Flickr acts as the hub and the publisher all rolled in to one. We&#8217;re obviously not really &#8220;publishing&#8221; separate feeds of every single user&#8217;s contacts&#8217; photos and faves to a central hub somewhere, we only create the feeds on demand when someone subscribes to them. So we couldn&#8217;t, for example, publish them all to a 3rd-party hub like <a href="http://superfeedr.com/" target="_blank">Superfeedr</a>. But the whole pubsubhubbub metaphor still works pretty well.</p> <p class="p2">Another difference is that the subscription happens via an authenticated API call and not an <code>HTTP POST</code>; hopefully the reasons for this are obvious. We&#8217;ll get into them in detail a little bit later. But even though the mechanism for the subscription request is different we&#8217;ve tried to follow the protocol as closely as possible and keep the parameters the same. The Google PubSubHubbub Core 0.3 section on <a href="http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html#subscribing" target="_blank">how the subscription flow works</a> is a good place to start, and the rest of this post assumes you&#8217;ve read that and more or less understand what the interactions should be between hub and subscriber. Done? OK, here are the methods:</p> <p><code>flickr.push.getTopics</code></p> <p class="p2">This method just tells you what you can subscribe to. It returns something like this:</p> <pre>&lt;<span class="s1">rsp</span> stat="<span class="s2">ok</span>"&gt; <span class="s3"> &lt;</span>topics<span class="s3">&gt; </span> &lt;<span class="s1">topic</span> name="<span class="s2">contacts_photos</span>" /&gt; &lt;<span class="s1">topic</span> name="<span class="s2">contacts_faves</span>" /&gt; <span class="s3"> &lt;</span>/topics<span class="s3">&gt; </span><span class="s3">&lt;</span>/rsp<span class="s3">&gt;</span></pre> <p class="p2"> <p class="p2">yeah, yeah, you already get that part. You can currently subscribe to contacts&#8217; photos (new uploads and updates) or contacts&#8217; faves. So subscribe already!</p> <p><code>flickr.push.subscribe</code></p> <p class="p2">This method (which requires an authentication token with read permissions) takes almost exactly the same arguments as a &#8220;proper&#8221; PubSubHubbub subscribe HTTP request would. Wee differences:</p> <p style="padding-left: 30px;"><code>topic</code> &#8211; unlike the topic argument in the HTTP version (which is a URL), this is just one of the topic types returned by <code>flickr.push.getTopics</code>.</p> <p style="padding-left: 30px;"><code>secret</code> &#8211; currently not supported, so this parameter is omitted.</p> <p style="padding-left: 30px;"><code>callback</code> &#8211; this must be unique, i.e. you can&#8217;t use the same URL for more than one subscription.</p> <p class="p2">Everything else works as you would expect &#8211; verification (either synchronous or asynchronous), the hub challenge string, subscription expiration/refreshing, unsubscribing (with <code>flickr.push.unsubscribe</code>) etc. Which brings us to:</p> <p><code>flickr.push.getSubscriptions</code></p> <p class="p2">This method also requires an authentication token with read permissions, and returns a list of subscriptions for the authenticated user, like so:</p> <pre>&lt;<span class="s1">rsp</span> stat="<span class="s2">ok</span>"&gt; <span class="s3"> &lt;</span>subscriptions<span class="s3">&gt; </span> &lt;<span class="s1">subscription</span> topic="<span class="s2">contacts_photos</span>" callback="<span class="s2">http://example.com/contacts_photos_endpoint?user=12345</span>" pending="<span class="s2">0</span>" date_create="<span class="s2">1309293755</span>" lease_seconds="<span class="s2">0</span>" expiry="<span class="s2">1309380155</span>" verify_attempts="<span class="s2">0</span>" /&gt; &lt;<span class="s1">subscription</span> topic="<span class="s2">contacts_faves</span>" callback="<span class="s2">http://example.com/contacts_faves_endpoint?user=12345</span>" pending="<span class="s2">0</span>" date_create="<span class="s2">1309293785</span>" lease_seconds="<span class="s2">0</span>" expiry="<span class="s2">1309380185</span>" verify_attempts="<span class="s2">0</span>" /&gt; <span class="s3"> &lt;</span>/subscriptions<span class="s3">&gt; </span><span class="s3">&lt;</span>/rsp<span class="s3">&gt;</span></pre> <p class="p2"> <p class="p2">Oh yeah, the docs:</p> <p><a href="http://www.flickr.com/services/api/flickr.push.subscribe.html">flickr.push.subscribe</a><br /> <a href="http://www.flickr.com/services/api/flickr.push.unsubscribe.html">flickr.push.unsubscribe</a><br /> <a href="http://www.flickr.com/services/api/flickr.push.getTopics.html">flickr.push.getTopics</a><br /> <a href="http://www.flickr.com/services/api/flickr.push.getSubscriptions.html">flickr.push.getSubscriptions</a></p> <h2>Feeds</h2> <p class="p2">The format of the feed that gets posted to your endpoint is currently limited to Atom 1.0, i.e. exactly what you&#8217;d get from something like</p> <p style="padding-left: 30px;"><code><a href="http://api.flickr.com/services/feeds/photos_public.gne?id=_YOUR_NSID_HERE_&#038;format=atom" rel="nofollow">http://api.flickr.com/services/feeds/photos_public.gne?id=_YOUR_NSID_HERE_&#038;format=atom</a></code></p> <p>For the contacts_faves topic type it&#8217;s the same thing but with the addition of the <a href="http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.contributor">atom:contributor</a> element to indicate the user who faved the photo.</p> <p class="p2"><span class="s3">And that&#8217;s about it. Questions?</span></p> <h2><span class="s3">Privacy and Restrictions</span></h2> <p class="p2"><span class="s3">The astute observer may notice that not all photos are being sent in the PuSH feeds. Since this is a new (and experimental) feature for Flickr, we&#8217;ve basically turned all of the privacy/safety restrictions on it up to 11, at least to start with. PuSH feeds currently only contain images that have public visibility and safe adultness level. In addition, users with their &#8220;Who can access your original image files&#8221; option set to anything other than &#8220;anyone&#8221; and users who are opted out of the API will not have their photos included in PuSH feeds.</span></p> <p class="p2"><span class="s3">While this may be a bit restrictive (for example since the API call is authenticated and the photos are coming from your contacts technically you should be allowed to see contacts only or friends/family photos for contacts that allow it),  we feel that since this is a new thing it&#8217;s better to start conservative and see how the feature is being used. It&#8217;s possible that we may relax some of these restrictions in the future, but for now a PuSH feed is essentially what a signed-out user could get just by grabbing the RSS feeds from various people&#8217;s photostreams.</span></p> <p class="p2"><span class="s3">You will also notice that for now we&#8217;ve limited the feature to pro account holders only.</span></p> <h2>So&#8230; What?</h2> <p><a title="screens beget BACON!!! by straup, on Flickr" href="http://www.flickr.com/photos/straup/5843200417/"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3045/5843200417_ef63ec2347.jpg" alt="screens beget BACON!!!" width="500" height="374" /></a></p> <p>So what can you do with it? There&#8217;s the obvious: any web application which currently does some kind of polling of the Flickr API to get photos for its users can potentially be altered to receive the push feeds instead. More timely updates, cheaper/simpler for the application and as it turns out cheaper for Flickr, too &#8211; it&#8217;s often easier on our servers to push out events shortly after they happen and we&#8217;ve got them (often fresh in our cache) than it is to go and dig them up when they&#8217;re asked for some time later.</p> <p><a href="http://www.flickr.com/photos/straup/5891080433/" title="Surprise! by straup, on Flickr"><img loading="lazy" decoding="async" src="http://farm7.static.flickr.com/6024/5891080433_c2628ce5af.jpg" width="500" height="374" alt="Surprise!"></a></p> <p>Some of the more interesting things that we hope these API methods will enable revolve around the more real-time nature of the events they expose. As an example of what&#8217;s possible in this space, Aaron Cope has created a little application he calls &#8220;Pua&#8221;. It&#8217;s a wonderfully simple way to surf Flickr without having to do much of anything; Pua takes you on a ride through your contacts&#8217; photos and favorites, as they happen. <a href="http://pua.spum.org/about">Have a read</a> about exactly what it is, why it&#8217;s called Pua and why he made it. If you ask nicely maybe Pua will give you an invite code.</p> <h2><span class="s3">Later</span></h2> <p class="p2"><span class="s3">Hopefully there will be much more to come. Finer-grained controls on the subscriptions (safety levels, visibility levels, restricting to just new uploads or only certain types of updates, lightweight JSON feeds, etc.), new types of subscriptions (photos of your friends/family, photos from a particular location, photos having a particular tag, something to do with galleries&#8230;), and maybe some other stuff we haven&#8217;t thought of yet. Hey, </span>wouldn&#8217;t it be cool if you didn&#8217;t need to run a web server on the other end to be the endpoint of the feeds?</p> <p class="p2"><span class="s3">Let us know what you&#8217;d like to see! What works, what doesn&#8217;t, what we got wrong and how to make it more useful to the people who want to Build Stuff (that&#8217;s you).</span></p> <h2><span class="s3">Fine Print</span></h2> <p><a title="&quot;there is a virtuous circle in this ecosystem&quot; by straup, on Flickr" href="http://www.flickr.com/photos/straup/2228947655/"><img loading="lazy" decoding="async" src="http://farm3.static.flickr.com/2151/2228947655_f954471d29.jpg" border="0" alt="&quot;there is a virtuous circle in this ecosystem&quot;" width="500" height="375" /></a></p> <p class="p2"><span class="s3">The Flickr PuSH feeds are part of the Flickr API, and thus fall under the <a href="http://www.flickr.com/services/api/tos/" target="_blank">API Terms of Service Agreement</a>. This means all the usual things about respecting photo owners&#8217; copyrights and all also the other good bits about API abuse. In other words, don&#8217;t try to subscribe to all of Flickr. Trust me, we&#8217;ll notice.</span></p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://code.flickr.net/tag/api/" rel="tag">api</a>, <a href="https://code.flickr.net/tag/feed/" rel="tag">feed</a>, <a href="https://code.flickr.net/tag/push/" rel="tag">push</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1532 --> <article id="post-1518" class="post-1518 post type-post status-publish format-standard hentry category-uncategorized tag-api tag-api-explorer tag-kittens"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2011/06/01/refreshing-the-api-explorer/" rel="bookmark">Refreshing The API Explorer</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2011/06/01/refreshing-the-api-explorer/" title="9:42 pm" rel="bookmark"><time class="entry-date" datetime="2011-06-01T21:42:03-07:00">June 1, 2011</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>Most people know that Flickr has an API. As it wouldn&#8217;t be much use without <a href="http://www.flickr.com/services/api/">documentation</a>, we have that, too. (There&#8217;s even a <a href="http://www.flickr.com/services/api/flickr.reflection.getMethods.html">list of methods</a> and <a href="http://www.flickr.com/services/api/flickr.reflection.getMethodInfo.html">information about each</a> available via the API itself.) What if I told you there was <em>also</em> a way to experiment with it from the comfort of your browser, no coding required?</p> <div style="margin-top:30px;margin-bottom:30px"> <a href="http://www.flickr.com/photos/zabowski/2911616552/" title="Sink Explorer by Zabowski, on Flickr"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3191/2911616552_002c0d337c.jpg" width="500" height="333" alt="Sink Explorer" style="border:1px dotted #ccc;padding:10px"></a></p> <div style="font-size:small;text-align:right">Sink Explorer by <a href="http://www.flickr.com/photos/zabowski/">Zabowski</a></div> </div> <p>Well, that&#8217;s what Flickr&#8217;s API Explorer offers. It&#8217;s an easy way to customise requests by filling in simple form fields, whether the method requires authentication or not, and to see the responses that are returned. It&#8217;s great for one-off prototype scripts where you quickly want to find some data, for seeing whether a method does what you think it does, or to sanity-check some code that&#8217;s not doing the right thing.</p> <p>It&#8217;s been around for years, but nobody ever seems to have made much of a fuss about it. (The only mention I can find on this blog is an interview with a certain <a href="http://code.flickr.com/blog/2008/11/06/5-questions-for-paul-mison/">API developer</a> singing its praises.) However, it&#8217;s needed a little attention to bring it up to date, and so I made some time to teach it a few new tricks.</p> <p>Firstly, it now offers a choice of output response. While it doesn&#8217;t offer every format that the API does, the three (and a bit) available &#8211; the default <a href="http://www.flickr.com/services/api/response.rest.html">XML</a>, <a href="http://www.flickr.com/services/api/response.json.html">JSONP</a> (or raw JSON), and <a href="http://www.flickr.com/services/api/response.php.html">PHP</a> serialized data &#8211; should cover a lot of ground. Secondly, the Explorer pages now have proper URLs, so it&#8217;s possible to link to the API method for fetching the <a href="http://www.flickr.com/services/api/explore/flickr.panda.getList">list of pandas</a>, for example. Finally, for the most popular of those response types &#8211; XML and JSON(P) &#8211; responses are now pretty-printed and syntax highlighted, as are the examples in the API documentation pages for each method. That is to say, the returned values are indented and have line breaks, while the name, attributes and quoted values of the elements are coloured appropriately.</p> <p>Now that you know that it exists, and that it&#8217;s all freshened up with spiffy features, why not go and <a href="http://www.flickr.com/services/api/explore/flickr.photos.search">play around</a>? Have fun!</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://code.flickr.net/tag/api/" rel="tag">api</a>, <a href="https://code.flickr.net/tag/api-explorer/" rel="tag">api explorer</a>, <a href="https://code.flickr.net/tag/kittens/" rel="tag">kittens</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1518 --> <article id="post-1394" class="post-1394 post type-post status-publish format-standard hentry category-geo tag-clustr tag-geo tag-shapefile"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2011/01/08/flickr-shapefiles-public-dataset-2-0/" rel="bookmark">Flickr Shapefiles Public Dataset 2.0</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2011/01/08/flickr-shapefiles-public-dataset-2-0/" title="12:47 am" rel="bookmark"><time class="entry-date" datetime="2011-01-08T00:47:11-08:00">January 8, 2011</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><a href="http://www.flickr.com/photos/straup/5049664321/"><img loading="lazy" decoding="async" class="alignnone" src="http://farm5.static.flickr.com/4131/5049664321_3282398ebd.jpg" alt="" width="500" height="399" /></a></p> <p>An embarrassingly long time ago we released the first public version of <a href="http://code.flickr.com/blog/2009/05/21/flickr-shapefiles-public-dataset-10/">the Flickr Shapefiles</a>. What does this have to do with Captain America and a cat? Nothing, really.</p> <p>Anyway, we haven&#8217;t completely forgotten about shapefiles and have finally gotten around to generating a new batch (read about <a href="http://code.flickr.com/blog/2008/10/30/the-shape-of-alpha/">Alpha Shapes</a> to find out how it&#8217;s done). When Aaron did the first run we had somewhere around ninety million (90M) geotagged photos. Today we have over one hundred and ninety million (190M) and that number is growing rapidly. Of course lots of those will fall within the boundaries of the existing shapes and won&#8217;t give us any new information, but some of them will improve the boundaries of old shapes, and others will help create new shapes where there weren&#8217;t any before. Version 1 of the dataset had shapes for around one hundred and eighty thousand (180K) WOE IDs, and now we have shapes for roughly two hundred and seventy thousand (270K) WOE IDs. Woo.</p> <p>The dataset is available for download today, available for use under the <a href="http://creativecommons.org/publicdomain/zero/1.0/">Creative Commons Zero Waiver</a>:</p> <p style="text-align: center; padding-top: 20px; padding-bottom: 20px; border: 1px dotted #666; margin-top: 30px; margin-bottom: 30px;"><strong style="font-family: sans-serif; font-size: large;"><a href="http://www.flickr.com/services/shapefiles/2.0/">http://www.flickr.com/services/shapefiles/2.0/</a></strong></p> <h2>Little Johnny JSON</h2> <p><a title="Today I'm a Cop. by jspaw, on Flickr" href="http://www.flickr.com/photos/allspaw/456597470/"><img loading="lazy" decoding="async" src="http://farm1.static.flickr.com/243/456597470_f85795df09.jpg" alt="Today I'm a Cop." width="500" height="333" /></a></p> <p>Originally we provided the full dataset in our own home-grown XML format because, well, it seemed like a good idea. For version two we&#8217;re releasing the shapes in GeoJSON format. We think this is a Good Thing because unlike our  old XML format, <a href="http://wiki.geojson.org/Users">at least one other person in the world</a> already knows how to read and write GeoJSON. For example, Our friends over at <a href="http://stamen.com/">Stamen Design</a> and <a href="http://simplegeo.com/">SimpleGeo</a> have created a ridiculously easy-to-use JavaScript library called <a href="http://polymaps.org/">Polymaps</a> which of course reads GeoJSON out of the box. With a few lines of JavaScript you can render the Flickr shapefiles and start using them without all that pesky XML parsing stuff:</p> <p><a title="Statez by : Nils, on Flickr" href="http://www.flickr.com/photos/slopetrash/5327825882/"><img loading="lazy" decoding="async" src="http://farm6.static.flickr.com/5086/5327825882_df9ec897ed.jpg" alt="Statez" width="500" height="479" /></a></p> <p>Or if GeoJSON doesn&#8217;t suit you you can use a free tool like <a href="http://www.gdal.org/ogr2ogr.html">ogr2ogr</a> to convert it to something that does.</p> <h2>Layers</h2> <p><a title="1000 layers by doug88888, on Flickr" href="http://www.flickr.com/photos/doug88888/3139395660/"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3292/3139395660_38594071e7.jpg" alt="1000 layers" width="333" height="500" /></a></p> <h6>(photo by <a href="http://www.flickr.com/photos/doug88888/">doug88888</a>)</h6> <p>The GeoJSON format allows grouping of features (and their related geometries) into <a href="http://geojson.org/geojson-spec.html#feature-collection-objects">FeatureCollection</a> objects. A FeatureCollection seems to be roughly equivalent to a layer in a typical GIS, or a placetype in <a href="http://developer.yahoo.com/geo/geoplanet/guide/concepts.html">WhereOnEarth-speak</a>. To make the dataset a little easier to manage we decided to break the shapes up into FeatureCollections based on placetype; one each for continents, countries, regions (states), counties, localities (cities), and neighbourhoods. Each of these is its own GeoJSON file in the dataset.</p> <p>Here&#8217;s an example of what one of our GeoJSON objects looks like:</p> <pre>{ "type": "FeatureCollection", "name": "Flickr Shapes Public Dataset 2.0 - Regions", "features": [ { "type": "Feature", "id": 2344541, "properties": { "woe_id": 2344541, "place_id": "Cxf0SmObApi9R9T8", "place_type": "region", "place_type_id": 8, "label": "Barbuda, AG, Antigua and Barbuda", }, "geometry": { "type": "MultiPolygon", "created": 1292444482, "alpha": 0.03, "points": 118, "edges": 17, "is_donuthole": 0, "link": { "href": "http://farm6.static.flickr.com/5209/shapefiles/2344541_20101215_724c4cae47.tar.gz", }, "bbox": [-62.314453125,17.086019515991,-61.690979003906,17.93692779541], "coordinates": [ [ [[-61.739044,17.587740], [-61.735268,17.546171], [-61.690979,17.426649], [-61.765137,17.413546] ... etc</pre> <p>A file is a single FeatureCollection object which holds an array of Features, which each hold a Geometry which is a MultiPolygon, which holds an array of Polygons which in turn each consist of an array of LinearRings. Got it?</p> <h2>You&#8217;ve Been Superseded</h2> <p>We&#8217;ve also included a separate file/layer called flickr_shapes_superseded.geojson. This is a FeatureCollection that consists of all the WOE IDs that have been &#8220;superseded&#8221;. Occasionally (too often?) a WOE ID needs to be <a href="http://developer.yahoo.com/geo/geoplanet/guide/concepts.html">retired and replaced with a new one</a>. We keep up to date with these and are always reverse-geocoding against the latest WOE IDs. However, there are plenty of old photos (and Flickr shapes) that have been assigned to one of these old IDs, and we have shapes for them (currently a little more than nine thousand). A simple solution might be to just re-assign these photos to the new WOE IDs (when a WOE ID is retired its replacement is specified), or even just re-run the reverse-geocoding process. This may not be what the owner of the photo wants; it might come out with a different result than they had at first (which they may have been perfectly happy with), if the size and location of the WOE rectangle changed (which it probably did). So it&#8217;s a problem without a clear solution. And since we have data for these old WOE IDs we&#8217;ve included their associated shapes in the dataset. In most cases there will be shapes corresponding to the new WOE IDs that will over time become more and more accurate as more photos get uploaded and end up being assigned to the new WOE IDs. But you can have the old ones too.</p> <h2>WTF</h2> <p>Sometimes, Clustr just gets it wrong. As mentioned in previous posts many of the Flickr shapes are just plain weird. It may be due to a lack of data (i.e. source photos), a weakness of the algorithm, an inappropriate choice of the alpha parameter or (shame!) a plain old bug. One of the things that surprised us was that Clustr was not supposed to output <em>inner rings</em>, or polygons with holes. It turns out that it does. In the GeoJSON output this can cause some weirdness (depending on what you use to render the shapes) since the GeoJSON is formatted with the assumption that each ring is a distinct polygon, instead of possibly one part of a single polygon with holes in it. This and other weirdness are known issues and something we shall strive to fix in the future, however we felt it best to release the existing dataset now rather than spend forever trying to get it perfect, and end up not releasing anything at all.</p> <p>You may also notice that unlike version 1 of the dataset, there is only a single shape per WOE ID. All of the previous versions of the shapes are still available via <a href="http://www.flickr.com/services/api/flickr.places.getShapeHistory.html">the Flickr API</a>, but in the interests of keeping the file size down we&#8217;ve limited this download to just the latest versions of each shape.</p> <p>And as always, there&#8217;s lots more to do.</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/geo/" rel="category tag">geo</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://code.flickr.net/tag/clustr/" rel="tag">clustr</a>, <a href="https://code.flickr.net/tag/geo/" rel="tag">geo</a>, <a href="https://code.flickr.net/tag/shapefile/" rel="tag">shapefile</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1394 --> <article id="post-1367" class="post-1367 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2010/08/20/creating-a-dashboard-for-the-help-team/" rel="bookmark">Creating a dashboard for the help team</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2010/08/20/creating-a-dashboard-for-the-help-team/" title="8:38 pm" rel="bookmark"><time class="entry-date" datetime="2010-08-20T20:38:53-07:00">August 20, 2010</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>When creating a tool for the help team, one of the main things we wanted to do was find a good way to give them updates on new features and site issues. For any of you that have ever been on a help team you know that no matter how much your boss tells you it&#8217;s <em>very important</em> that you check your email or look at X web page for updates before each case, that&#8217;s probably not going to happen. When you are trying to get through help cases every click and keystroke counts.</p> <p>So if you are supposed to check some page that only changes 1 out of 100 times you check it, it naturally falls into the list of things that you probably don&#8217;t 100% have to check. You don&#8217;t have time for that, you&#8217;ve got people to help and the queue keeps growing!</p> <p>So how do you get people to look at those updates? <strong>Make it useful!</strong></p> <p>To make the page useful we tried to solve one of the other frustrations common to most help teams, the tools you need are all on different pages (maybe even managed by different teams). Go here to search for accounts, over there to search for a photo, another place to look up an ID, etc. Any search that might be needed to research a question we put all on one page. The actual tools may still reside somewhere else, but a search box is also included here so you can get to any tool you need, even the flickr.com searches for pictures and people.</p> <p>Here is an example of what it looks like with a few parts and dates changed for security reasons.</p> <p><img loading="lazy" decoding="async" src="http://farm5.static.flickr.com/4079/4910617189_1c8f6da8b7.jpg" alt="T1Screen" width="482" height="363" /></p> <p>Directly below the searches is &#8220;Current Issues&#8221; and new FAQs. Now that this is the page you will start at for every case, you&#8217;ll always see these updates.</p> <p>But is that enough? At each stage we tried to think of our audience. If you are trying to get through cases, when you go to a page over and over you start to tune out what you don&#8217;t use. To combat that tunnel vision we rotate the <span style="color: #ff0000;">c</span><span style="color: #00ff00;">o</span><span style="color: #3366ff;">l</span><span style="color: #cc99ff;">o</span><span style="color: #ffcc00;">r</span> of the issue title and FAQs so it&#8217;s easier to notice that something changed. (I actually usually think of that T-Rex in Jurassic park that only sees you if you move. But don&#8217;t tell the help team that&#8217;s what I was thinking. They&#8217;re actually very nice.)</p> <p>When we released it to the help team, everyone made it their homepage without the boss man having to go around and make them. Success!</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1367 --> <article id="post-1363" class="post-1363 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2010/08/16/now-in-belorussian/" rel="bookmark">Now in Belorussian&#8230;</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2010/08/16/now-in-belorussian/" title="9:40 pm" rel="bookmark"><time class="entry-date" datetime="2010-08-16T21:40:00-07:00">August 16, 2010</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><a href="http://www.flickr.com/photos/jorge-11/2432664559/" title="Minsk Central Train Station by Jorge-11, on Flickr"><img loading="lazy" decoding="async" src="http://farm4.static.flickr.com/3278/2432664559_fb382f5933.jpg" width="500" height="267" alt="Minsk Central Train Station" /></a></p> <p>As compliments to writers go, having your work translated into another language comes pretty high on the list. That said, I&#8217;m not sure I ever expected to see one of my code.flickr blog posts re-interpreted in Belorussian until this weekend when I was contacted by a translator by the name of Patricia Clausnitzer.</p> <p>Patricia has provided <a href="http://pc.de/pages/people-in-photos-the-api-methods-be">a Belorussian rendering of my post</a> (complete with pictures of paint tins and me in a stretcher) on a site called pc.de. So if you read Belorussian, you can now get the skinny on our &#8220;People in Photos&#8221; API methods in your native tongue.</p> <p>And if you don&#8217;t speak Belorussian but want to code up an app that takes advantage of our people-annotating features, you can revisit the original post about the API methods <a href="http://code.flickr.com/blog/2010/01/21/people-in-photos-the-api-methods/">here</a>.</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1363 --> <article id="post-1343" class="post-1343 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2010/05/13/stats-api-redux/" rel="bookmark">Stats API Redux</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2010/05/13/stats-api-redux/" title="4:40 pm" rel="bookmark"><time class="entry-date" datetime="2010-05-13T16:40:42-07:00">May 13, 2010</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>We&#8217;ve seen great uptake by library developers on supporting the <a href="http://code.flickr.com/blog/2010/03/03/flickr-stats-api/">new Stats APIs</a>. Now we&#8217;d love to see what you&#8217;re building with those APIs.</p> <p><a href="http://phpflickr.com/">PHPFlickr</a>, <a href="http://flickrnet.codeplex.com/">Flickr.Net</a>, <a href="http://stuvel.eu/projects/flickrapi">flickrapi.py</a>, <a href="http://cflickr.riaforge.org/">CFlickr</a>, and <a href="http://librdf.org/flickcurl/">flickcurl</a> all have let us know they support the <a href="http://code.flickr.com/blog/2010/03/03/flickr-stats-api/">new APIs</a>.</p> <p>Poking around the <a href="http://flickr.com/services">App Garden</a> I found <a href="http://www.flickr.com/services/apps/72157623630152423/">flickrstats</a> which provides a very interactive, and lovely timeline based visualization. (in the style of Google Finance). Are there other great examples? Let us know by adding them to the <a href="http://flickr.com/services">App Garden</a> or telling us about them in the <a href="http://flickr.com/groups/api">API Group</a>.</p> <h3 id="june_1st">June 1st.</h3> <p>And a reminder. </p> <p>Historical stats data is <a href="http://www.flickr.com/help/stats/#1369409">only available until June 1st</a>. After June 1st only the last 28 days of data will be available. If you want to archive that data we have the <a href="www.flickr.com/photos/me/stats/downloads/">stats download page</a>, and a new utility method <a href="http://www.flickr.com/services/api/flickr.stats.getCSVFiles.html">flickr.stats.getCSVFiles</a> (because I was too lazy to manually download those files, see <a href="http://c2.com/cgi/wiki?LazinessImpatienceHubris">programmer virtues</a>).</p> <p><a href="http://randomfoo.net/sandbox/flickr/statsdumper/">statsdumper</a> provides a couple of different methods to facilitate backing up all your stats data, but I&#8217;d love to hear about others.</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1343 --> <article id="post-1333" class="post-1333 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://code.flickr.net/2010/04/13/ishmael-a-ui-for-mk-query-digest/" rel="bookmark">Ishmael &#8211; a UI for mk-query-digest</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://code.flickr.net/2010/04/13/ishmael-a-ui-for-mk-query-digest/" title="8:10 pm" rel="bookmark"><time class="entry-date" datetime="2010-04-13T20:10:47-07:00">April 13, 2010</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://code.flickr.net/author/flickrphotography/" title="View all posts by Kay Kremerskothen" rel="author">Kay Kremerskothen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>Ever since Peter Z from <a href="http://www.percona.com/">Percona</a> showed us the awesome that is the <strong><a href="http://www.maatkit.org/doc/mk-query-digest.html">mk-query-digest</a></strong> tool, we&#8217;ve been using it quite a bit to help identify costly queries. It analyzes the MySQL slow query log and compiles a detailed report which includes tons of useful data &#8211; how many times was a particular query called? how much time did MySQL spend executing a particular query during the given timeframe? how long did the query take to execute on average? what about the worst case?</p> <p>The report is output as a long long text file that you can page through and look at all the queries. All the data you need is there, but it&#8217;s a bit difficult to get to, especially if you want to compare multiple reports or if you want to aggregate over a period of time longer than the interval at which you run the reports (we&#8217;ve been running it every 15 minutes on one of our shards and are in the process of adding it to all servers).</p> <h3>Enter ishmael</h3> <p style="text-align: center"><img decoding="async" src="http://farm3.static.flickr.com/2401/2052477420_fe7fb452b0.jpg" alt="" /><br /> Photo from <a href="http://www.flickr.com/photos/raphie/2052477420/">raphie</a>.</p> <p>Luckily, the tool can be configured to write most of the data in the report to a database table. Where there&#8217;s a database, there&#8217;s a former tools developer eager to write a UI on top of it (me). After Timmy, our DBA, showed me a quick prototype of a page he put together for displaying the data, I took it and ran with it. The result is <strong>ishmael</strong> &#8211; a UI on top of mk-query-digest. The name comes from the tools purpose &#8211; to help hunt down &#8220;whale&#8221; queries.</p> <p>For now, ishmael lets you sort the queries by 3 characteristics &#8211; the total amount of time MySQL spent executing the query, the number of times the query was actually executed, and the ratio between the two. It also displays the queries with some highlighting (done using a brutal regex) and lets you click through to see historical data (assuming you&#8217;ve run the report more than once) as well as a page that shows the <code>EXPLAIN</code> output for the query and the <code>SHOW CREATE TABLE</code> output for the tables involved (once again, traced back from their aliases using brutal regex hacks).</p> <p><a href="http://farm5.static.flickr.com/4008/4518331253_99b205631d_o.jpg"><img loading="lazy" decoding="async" class="aligncenter" src="http://farm5.static.flickr.com/4008/4518331253_99b205631d_o.jpg" alt="" width="509" height="316" /></a></p> <p>There are already a bunch of additional features in the pipeline &#8211; being able to configure ishmael to switch between different databases, better handling of historical data, etc. We hope other mk-query-digest users find the tool useful and can let us know how the tool can be made better. Patches are welcome.</p> <p>The docs for mk-query-digest are on the maatkit toolkit website: <a href="http://www.maatkit.org/doc/mk-query-digest.html">http://www.maatkit.org/doc/mk-query-digest.html</a>.</p> <p>The source for ishmael, is on github: <a href="http://github.com/mihasya/ishmael">http://github.com/mihasya/ishmael</a><br /> (click &#8220;Issues&#8221; to view the list of open tickets)</p> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://code.flickr.net/category/uncategorized/" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1333 --> <nav id="nav-below"> <h3 class="assistive-text">Post navigation</h3> <div class="nav-previous"><a href="https://code.flickr.net/author/flickrphotography/page/2/" ><span class="meta-nav">&larr;</span> Older posts</a></div> <div class="nav-next"></div> </nav><!-- #nav-above --> </div><!-- #content --> </section><!-- #primary --> <div id="secondary" class="widget-area" role="complementary"> <aside id="jetpack-search-filters-3" class="widget jetpack-filters widget_search"> <div id="jetpack-search-filters-3-wrapper" class="jetpack-instant-search-wrapper"> <div class="jetpack-search-form"> <form method="get" id="searchform" action="https://code.flickr.net/"> <label for="s" class="assistive-text">Search</label> <input type="text" class="field" name="s" id="s" placeholder="Search" /> <input type="submit" class="submit" name="submit" id="searchsubmit" value="Search" /> <input type="hidden" name="orderby" value="" /><input type="hidden" name="order" value="" /></form> </div> <h4 class="jetpack-search-filters-widget__sub-heading"> Categories </h4> <ul class="jetpack-search-filters-widget__filter-list"> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="category" data-val="uncategorized"> Uncategorized&nbsp;(136) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="category" data-val="geo"> geo&nbsp;(12) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="category" data-val="kittens"> kittens&nbsp;(10) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="category" data-val="change-log"> changelog&nbsp;(7) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="category" data-val="uploadr"> uploadr&nbsp;(6) </a> </li> </ul> <h4 class="jetpack-search-filters-widget__sub-heading"> Tags </h4> <ul class="jetpack-search-filters-widget__filter-list"> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="post_tag" data-val="api"> api&nbsp;(24) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="post_tag" data-val="geo"> geo&nbsp;(13) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="post_tag" data-val="machine-tags"> machine tags&nbsp;(10) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="post_tag" data-val="javascript"> javascript&nbsp;(9) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="taxonomy" data-taxonomy="post_tag" data-val="kittentuesday"> kittentuesday&nbsp;(8) </a> </li> </ul> <h4 class="jetpack-search-filters-widget__sub-heading"> Year </h4> <ul class="jetpack-search-filters-widget__filter-list"> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="year_post_date" data-val="2022-01-01 00:00:00" > 2022&nbsp;(2) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="year_post_date" data-val="2021-01-01 00:00:00" > 2021&nbsp;(1) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="year_post_date" data-val="2018-01-01 00:00:00" > 2018&nbsp;(1) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="year_post_date" data-val="2017-01-01 00:00:00" > 2017&nbsp;(2) </a> </li> <li> <a href="#" class="jetpack-search-filter__link" data-filter-type="year_post_date" data-val="2016-01-01 00:00:00" > 2016&nbsp;(5) </a> </li> </ul> </div></aside> <aside id="recent-posts-2" class="widget widget_recent_entries"> <h3 class="widget-title">Recent Posts</h3> <ul> <li> <a href="https://code.flickr.net/2022/02/14/safer-internet-day-and-open-source-codes-of-conduct/">Safer Internet Day and Open Source Codes of Conduct</a> </li> <li> <a href="https://code.flickr.net/2022/01/04/a-pluggable-solution-for-api-observability-on-our-php-system/">A Pluggable Solution for API Observability on our PHP System</a> </li> <li> <a href="https://code.flickr.net/2021/11/22/flickr-engineering-team-vision-guiding-principles/">Flickr Engineering Team Vision &#038; Guiding Principles</a> </li> <li> <a href="https://code.flickr.net/2018/04/20/together/">Together</a> </li> <li> <a href="https://code.flickr.net/2017/03/07/introducing-similarity-search-at-flickr/">Introducing Similarity Search at Flickr</a> </li> </ul> </aside><aside id="archives-2" class="widget widget_archive"><h3 class="widget-title">Archives</h3> <ul> <li><a href='https://code.flickr.net/2022/02/'>February 2022</a></li> <li><a href='https://code.flickr.net/2022/01/'>January 2022</a></li> <li><a href='https://code.flickr.net/2021/11/'>November 2021</a></li> <li><a href='https://code.flickr.net/2018/04/'>April 2018</a></li> <li><a href='https://code.flickr.net/2017/03/'>March 2017</a></li> <li><a href='https://code.flickr.net/2017/01/'>January 2017</a></li> <li><a href='https://code.flickr.net/2016/09/'>September 2016</a></li> <li><a href='https://code.flickr.net/2016/05/'>May 2016</a></li> <li><a href='https://code.flickr.net/2016/04/'>April 2016</a></li> <li><a href='https://code.flickr.net/2016/03/'>March 2016</a></li> <li><a href='https://code.flickr.net/2015/12/'>December 2015</a></li> <li><a href='https://code.flickr.net/2015/11/'>November 2015</a></li> <li><a href='https://code.flickr.net/2015/09/'>September 2015</a></li> <li><a href='https://code.flickr.net/2015/07/'>July 2015</a></li> <li><a href='https://code.flickr.net/2015/06/'>June 2015</a></li> <li><a href='https://code.flickr.net/2015/03/'>March 2015</a></li> <li><a href='https://code.flickr.net/2014/10/'>October 2014</a></li> <li><a href='https://code.flickr.net/2014/08/'>August 2014</a></li> <li><a href='https://code.flickr.net/2014/07/'>July 2014</a></li> <li><a href='https://code.flickr.net/2014/05/'>May 2014</a></li> <li><a href='https://code.flickr.net/2014/04/'>April 2014</a></li> <li><a href='https://code.flickr.net/2014/02/'>February 2014</a></li> <li><a href='https://code.flickr.net/2013/09/'>September 2013</a></li> <li><a href='https://code.flickr.net/2013/06/'>June 2013</a></li> <li><a href='https://code.flickr.net/2013/03/'>March 2013</a></li> <li><a href='https://code.flickr.net/2012/12/'>December 2012</a></li> <li><a href='https://code.flickr.net/2012/10/'>October 2012</a></li> <li><a href='https://code.flickr.net/2012/07/'>July 2012</a></li> <li><a href='https://code.flickr.net/2012/06/'>June 2012</a></li> <li><a href='https://code.flickr.net/2012/05/'>May 2012</a></li> <li><a href='https://code.flickr.net/2012/04/'>April 2012</a></li> <li><a href='https://code.flickr.net/2012/02/'>February 2012</a></li> <li><a href='https://code.flickr.net/2012/01/'>January 2012</a></li> <li><a href='https://code.flickr.net/2011/12/'>December 2011</a></li> <li><a href='https://code.flickr.net/2011/10/'>October 2011</a></li> <li><a href='https://code.flickr.net/2011/09/'>September 2011</a></li> <li><a href='https://code.flickr.net/2011/08/'>August 2011</a></li> <li><a href='https://code.flickr.net/2011/07/'>July 2011</a></li> <li><a href='https://code.flickr.net/2011/06/'>June 2011</a></li> <li><a href='https://code.flickr.net/2011/03/'>March 2011</a></li> <li><a href='https://code.flickr.net/2011/02/'>February 2011</a></li> <li><a href='https://code.flickr.net/2011/01/'>January 2011</a></li> <li><a href='https://code.flickr.net/2010/11/'>November 2010</a></li> <li><a href='https://code.flickr.net/2010/10/'>October 2010</a></li> <li><a href='https://code.flickr.net/2010/09/'>September 2010</a></li> <li><a href='https://code.flickr.net/2010/08/'>August 2010</a></li> <li><a href='https://code.flickr.net/2010/07/'>July 2010</a></li> <li><a href='https://code.flickr.net/2010/05/'>May 2010</a></li> <li><a href='https://code.flickr.net/2010/04/'>April 2010</a></li> <li><a href='https://code.flickr.net/2010/03/'>March 2010</a></li> <li><a href='https://code.flickr.net/2010/02/'>February 2010</a></li> <li><a href='https://code.flickr.net/2010/01/'>January 2010</a></li> <li><a href='https://code.flickr.net/2009/12/'>December 2009</a></li> <li><a href='https://code.flickr.net/2009/11/'>November 2009</a></li> <li><a href='https://code.flickr.net/2009/10/'>October 2009</a></li> <li><a href='https://code.flickr.net/2009/09/'>September 2009</a></li> <li><a href='https://code.flickr.net/2009/07/'>July 2009</a></li> <li><a href='https://code.flickr.net/2009/06/'>June 2009</a></li> <li><a href='https://code.flickr.net/2009/05/'>May 2009</a></li> <li><a href='https://code.flickr.net/2009/04/'>April 2009</a></li> <li><a href='https://code.flickr.net/2009/03/'>March 2009</a></li> <li><a href='https://code.flickr.net/2009/02/'>February 2009</a></li> <li><a href='https://code.flickr.net/2009/01/'>January 2009</a></li> <li><a href='https://code.flickr.net/2008/12/'>December 2008</a></li> <li><a href='https://code.flickr.net/2008/11/'>November 2008</a></li> <li><a href='https://code.flickr.net/2008/10/'>October 2008</a></li> <li><a href='https://code.flickr.net/2008/09/'>September 2008</a></li> <li><a href='https://code.flickr.net/2008/08/'>August 2008</a></li> <li><a href='https://code.flickr.net/2008/07/'>July 2008</a></li> <li><a href='https://code.flickr.net/2008/06/'>June 2008</a></li> <li><a href='https://code.flickr.net/2008/05/'>May 2008</a></li> <li><a href='https://code.flickr.net/2008/04/'>April 2008</a></li> </ul> </aside><aside id="categories-2" class="widget widget_categories"><h3 class="widget-title">Categories</h3> <ul> <li class="cat-item cat-item-11749740"><a href="https://code.flickr.net/category/api-2/">API</a> </li> <li class="cat-item cat-item-564792"><a href="https://code.flickr.net/category/change-log/">changelog</a> </li> <li class="cat-item cat-item-5784"><a href="https://code.flickr.net/category/event/">event</a> </li> <li class="cat-item cat-item-29160"><a href="https://code.flickr.net/category/geo/">geo</a> </li> <li class="cat-item cat-item-139037766"><a href="https://code.flickr.net/category/hadoop/">hadoop</a> </li> <li class="cat-item cat-item-32"><a href="https://code.flickr.net/category/infrastructure/">infrastructure</a> </li> <li class="cat-item cat-item-139037765"><a href="https://code.flickr.net/category/kittens/">kittens</a> </li> <li class="cat-item cat-item-20156"><a href="https://code.flickr.net/category/labs/">labs</a> </li> <li class="cat-item cat-item-171"><a href="https://code.flickr.net/category/meta/">meta</a> </li> <li class="cat-item cat-item-7092"><a href="https://code.flickr.net/category/metrics/">metrics</a> </li> <li class="cat-item cat-item-139037764"><a href="https://code.flickr.net/category/open-source/">open source</a> </li> <li class="cat-item cat-item-1930"><a href="https://code.flickr.net/category/performance/">performance</a> </li> <li class="cat-item cat-item-304"><a href="https://code.flickr.net/category/photos/">photos</a> </li> <li class="cat-item cat-item-2373"><a href="https://code.flickr.net/category/search/">search</a> </li> <li class="cat-item cat-item-1"><a href="https://code.flickr.net/category/uncategorized/">Uncategorized</a> </li> <li class="cat-item cat-item-249276"><a href="https://code.flickr.net/category/uploadr/">uploadr</a> </li> <li class="cat-item cat-item-412"><a href="https://code.flickr.net/category/video/">video</a> </li> <li class="cat-item cat-item-830560"><a href="https://code.flickr.net/category/xulrunner/">xulrunner</a> </li> </ul> </aside><aside id="meta-2" class="widget widget_meta"><h3 class="widget-title">Meta</h3> <ul> <li><a href="https://code.flickr.net/wp-login.php">Log in</a></li> <li><a href="https://code.flickr.net/feed/">Entries feed</a></li> <li><a href="https://code.flickr.net/comments/feed/">Comments feed</a></li> <li><a href="https://wordpress.org/">WordPress.org</a></li> </ul> </aside> </div><!-- #secondary .widget-area --> </div><!-- #main --> <footer id="colophon" role="contentinfo"> <div id="site-generator"> &copy; 2024 Flickr, Inc. All rights reserved. | Powered by <a href="https://wpvip.com/?utm_source=vip_powered_wpcom&#038;utm_medium=web&#038;utm_campaign=VIP%20Footer%20Credit&#038;utm_term=code.flickr.net" rel="generator nofollow" class="powered-by-wpcom">WordPress VIP</a> </div> </footer><!-- #colophon --> </div><!-- #page --> <div class="jetpack-instant-search__widget-area" style="display: none"> <div id="jetpack-search-filters-2" class="widget jetpack-filters widget_search"> <div id="jetpack-search-filters-2-wrapper" class="jetpack-instant-search-wrapper"> </div></div> </div> <script type="text/javascript" src="https://code.flickr.net/wp-includes/js/dist/hooks.min.js?m=1732206023g" ></script><script type="text/javascript" src="https://code.flickr.net/wp-includes/js/dist/i18n.min.js?ver=5e580eb46a90c2b997e6" id="wp-i18n-js"></script> <script type="text/javascript" id="wp-i18n-js-after"> /* <![CDATA[ */ wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ 'ltr' ] } ); /* ]]> */ </script> <script type="text/javascript" src="https://code.flickr.net/wp-content/mu-plugins/jetpack-14.0/jetpack_vendor/automattic/jetpack-assets/build/i18n-loader.js?minify=true&amp;ver=becd7d9884bc1b331e45" id="wp-jp-i18n-loader-js"></script> <script type="text/javascript" id="wp-jp-i18n-loader-js-after"> /* <![CDATA[ */ wp.jpI18nLoader.state = {"baseUrl":"https://code.flickr.net/wp-content/languages/","locale":"en_US","domainMap":{"jetpack-admin-ui":"plugins/jetpack","jetpack-assets":"plugins/jetpack","jetpack-backup-pkg":"plugins/jetpack","jetpack-blaze":"plugins/jetpack","jetpack-boost-core":"plugins/jetpack","jetpack-boost-speed-score":"plugins/jetpack","jetpack-classic-theme-helper":"plugins/jetpack","jetpack-compat":"plugins/jetpack","jetpack-config":"plugins/jetpack","jetpack-connection":"plugins/jetpack","jetpack-explat":"plugins/jetpack","jetpack-forms":"plugins/jetpack","jetpack-image-cdn":"plugins/jetpack","jetpack-import":"plugins/jetpack","jetpack-ip":"plugins/jetpack","jetpack-jitm":"plugins/jetpack","jetpack-licensing":"plugins/jetpack","jetpack-masterbar":"plugins/jetpack","jetpack-my-jetpack":"plugins/jetpack","jetpack-password-checker":"plugins/jetpack","jetpack-plugins-installer":"plugins/jetpack","jetpack-post-list":"plugins/jetpack","jetpack-protect-models":"plugins/jetpack","jetpack-protect-status":"plugins/jetpack","jetpack-publicize-pkg":"plugins/jetpack","jetpack-search-pkg":"plugins/jetpack","jetpack-stats":"plugins/jetpack","jetpack-stats-admin":"plugins/jetpack","jetpack-sync":"plugins/jetpack","jetpack-videopress-pkg":"plugins/jetpack","jetpack-waf":"plugins/jetpack","jetpack-wordads":"plugins/jetpack","woocommerce-analytics":"plugins/jetpack"},"domainPaths":{"jetpack-admin-ui":"jetpack_vendor/automattic/jetpack-admin-ui/","jetpack-assets":"jetpack_vendor/automattic/jetpack-assets/","jetpack-backup-pkg":"jetpack_vendor/automattic/jetpack-backup/","jetpack-blaze":"jetpack_vendor/automattic/jetpack-blaze/","jetpack-boost-core":"jetpack_vendor/automattic/jetpack-boost-core/","jetpack-boost-speed-score":"jetpack_vendor/automattic/jetpack-boost-speed-score/","jetpack-classic-theme-helper":"jetpack_vendor/automattic/jetpack-classic-theme-helper/","jetpack-compat":"jetpack_vendor/automattic/jetpack-compat/","jetpack-config":"jetpack_vendor/automattic/jetpack-config/","jetpack-connection":"jetpack_vendor/automattic/jetpack-connection/","jetpack-explat":"jetpack_vendor/automattic/jetpack-explat/","jetpack-forms":"jetpack_vendor/automattic/jetpack-forms/","jetpack-image-cdn":"jetpack_vendor/automattic/jetpack-image-cdn/","jetpack-import":"jetpack_vendor/automattic/jetpack-import/","jetpack-ip":"jetpack_vendor/automattic/jetpack-ip/","jetpack-jitm":"jetpack_vendor/automattic/jetpack-jitm/","jetpack-licensing":"jetpack_vendor/automattic/jetpack-licensing/","jetpack-masterbar":"jetpack_vendor/automattic/jetpack-masterbar/","jetpack-my-jetpack":"jetpack_vendor/automattic/jetpack-my-jetpack/","jetpack-password-checker":"jetpack_vendor/automattic/jetpack-password-checker/","jetpack-plugins-installer":"jetpack_vendor/automattic/jetpack-plugins-installer/","jetpack-post-list":"jetpack_vendor/automattic/jetpack-post-list/","jetpack-protect-models":"jetpack_vendor/automattic/jetpack-protect-models/","jetpack-protect-status":"jetpack_vendor/automattic/jetpack-protect-status/","jetpack-publicize-pkg":"jetpack_vendor/automattic/jetpack-publicize/","jetpack-search-pkg":"jetpack_vendor/automattic/jetpack-search/","jetpack-stats":"jetpack_vendor/automattic/jetpack-stats/","jetpack-stats-admin":"jetpack_vendor/automattic/jetpack-stats-admin/","jetpack-sync":"jetpack_vendor/automattic/jetpack-sync/","jetpack-videopress-pkg":"jetpack_vendor/automattic/jetpack-videopress/","jetpack-waf":"jetpack_vendor/automattic/jetpack-waf/","jetpack-wordads":"jetpack_vendor/automattic/jetpack-wordads/","woocommerce-analytics":"jetpack_vendor/automattic/woocommerce-analytics/"}}; /* ]]> */ </script> <script type="text/javascript" src="https://code.flickr.net/_static/??/wp-includes/js/dist/vendor/wp-polyfill.min.js,/wp-includes/js/dist/url.min.js?m=1732206023j" ></script><script type="text/javascript" id="jetpack-instant-search-js-before"> /* <![CDATA[ */ var JetpackInstantSearchOptions=JSON.parse(decodeURIComponent("%7B%22overlayOptions%22%3A%7B%22colorTheme%22%3A%22light%22%2C%22enableInfScroll%22%3Atrue%2C%22enableFilteringOpensOverlay%22%3Atrue%2C%22enablePostDate%22%3Atrue%2C%22enableSort%22%3Atrue%2C%22highlightColor%22%3A%22%23FFC%22%2C%22overlayTrigger%22%3A%22submit%22%2C%22resultFormat%22%3A%22expanded%22%2C%22showPoweredBy%22%3Atrue%2C%22defaultSort%22%3A%22relevance%22%2C%22excludedPostTypes%22%3A%5B%5D%7D%2C%22homeUrl%22%3A%22https%3A%5C%2F%5C%2Fcode.flickr.net%22%2C%22locale%22%3A%22en-US%22%2C%22postsPerPage%22%3A10%2C%22siteId%22%3A185426273%2C%22postTypes%22%3A%7B%22post%22%3A%7B%22singular_name%22%3A%22Post%22%2C%22name%22%3A%22Posts%22%7D%2C%22page%22%3A%7B%22singular_name%22%3A%22Page%22%2C%22name%22%3A%22Pages%22%7D%2C%22attachment%22%3A%7B%22singular_name%22%3A%22Media%22%2C%22name%22%3A%22Media%22%7D%7D%2C%22webpackPublicPath%22%3A%22https%3A%5C%2F%5C%2Fcode.flickr.net%5C%2Fwp-content%5C%2Fmu-plugins%5C%2Fjetpack-14.0%5C%2Fjetpack_vendor%5C%2Fautomattic%5C%2Fjetpack-search%5C%2Fbuild%5C%2Finstant-search%5C%2F%22%2C%22isPhotonEnabled%22%3Afalse%2C%22isFreePlan%22%3Afalse%2C%22apiRoot%22%3A%22https%3A%5C%2F%5C%2Fcode.flickr.net%5C%2Fwp-json%5C%2F%22%2C%22apiNonce%22%3A%2212c3391b55%22%2C%22isPrivateSite%22%3Afalse%2C%22isWpcom%22%3Afalse%2C%22hasOverlayWidgets%22%3Atrue%2C%22widgets%22%3A%5B%7B%22filters%22%3A%5B%7B%22name%22%3A%22Bylines%22%2C%22type%22%3A%22taxonomy%22%2C%22taxonomy%22%3A%22byline%22%2C%22count%22%3A5%2C%22widget_id%22%3A%22jetpack-search-filters-2%22%2C%22filter_id%22%3A%22taxonomy_0%22%7D%2C%7B%22name%22%3A%22Categories%22%2C%22type%22%3A%22taxonomy%22%2C%22taxonomy%22%3A%22category%22%2C%22count%22%3A5%2C%22widget_id%22%3A%22jetpack-search-filters-2%22%2C%22filter_id%22%3A%22taxonomy_1%22%7D%2C%7B%22name%22%3A%22Tags%22%2C%22type%22%3A%22taxonomy%22%2C%22taxonomy%22%3A%22post_tag%22%2C%22count%22%3A5%2C%22widget_id%22%3A%22jetpack-search-filters-2%22%2C%22filter_id%22%3A%22taxonomy_2%22%7D%2C%7B%22name%22%3A%22Year%22%2C%22type%22%3A%22date_histogram%22%2C%22count%22%3A5%2C%22field%22%3A%22post_date%22%2C%22interval%22%3A%22year%22%2C%22widget_id%22%3A%22jetpack-search-filters-2%22%2C%22filter_id%22%3A%22date_histogram_3%22%7D%5D%2C%22widget_id%22%3A%22jetpack-search-filters-2%22%7D%5D%2C%22widgetsOutsideOverlay%22%3A%5B%7B%22filters%22%3A%5B%7B%22name%22%3A%22Bylines%22%2C%22type%22%3A%22taxonomy%22%2C%22taxonomy%22%3A%22byline%22%2C%22count%22%3A5%2C%22widget_id%22%3A%22jetpack-search-filters-3%22%2C%22filter_id%22%3A%22taxonomy_4%22%7D%2C%7B%22name%22%3A%22Categories%22%2C%22type%22%3A%22taxonomy%22%2C%22taxonomy%22%3A%22category%22%2C%22count%22%3A5%2C%22widget_id%22%3A%22jetpack-search-filters-3%22%2C%22filter_id%22%3A%22taxonomy_5%22%7D%2C%7B%22name%22%3A%22Tags%22%2C%22type%22%3A%22taxonomy%22%2C%22taxonomy%22%3A%22post_tag%22%2C%22count%22%3A5%2C%22widget_id%22%3A%22jetpack-search-filters-3%22%2C%22filter_id%22%3A%22taxonomy_6%22%7D%2C%7B%22name%22%3A%22Year%22%2C%22type%22%3A%22date_histogram%22%2C%22count%22%3A5%2C%22field%22%3A%22post_date%22%2C%22interval%22%3A%22year%22%2C%22widget_id%22%3A%22jetpack-search-filters-3%22%2C%22filter_id%22%3A%22date_histogram_7%22%7D%5D%2C%22widget_id%22%3A%22jetpack-search-filters-3%22%7D%5D%2C%22hasNonSearchWidgets%22%3Afalse%2C%22preventTrackingCookiesReset%22%3Afalse%7D")); /* ]]> */ </script> <script type="text/javascript" src="https://code.flickr.net/wp-content/mu-plugins/jetpack-14.0/jetpack_vendor/automattic/jetpack-search/build/instant-search/jp-search.js?minify=false&amp;ver=2b91c6a8150537fa6728" id="jetpack-instant-search-js"></script> <script type="text/javascript" src="//stats.wp.com/w.js?ver=202448" id="jp-tracks-js"></script> <script type="text/javascript" src="https://stats.wp.com/e-202448.js" id="jetpack-stats-js" data-wp-strategy="defer"></script> <script type="text/javascript" id="jetpack-stats-js-after"> /* <![CDATA[ */ _stq = window._stq || []; _stq.push([ "view", JSON.parse("{\"v\":\"ext\",\"blog\":\"185426273\",\"post\":\"0\",\"tz\":\"-8\",\"srv\":\"code.flickr.net\",\"hp\":\"vip\",\"j\":\"1:14.0\"}") ]); _stq.push([ "clickTrackerInit", "185426273", "0" ]); /* ]]> */ </script> <script async src="https://embedr.flickr.com/assets/client-code.js" charset="utf-8"></script> </body> </html>

Pages: 1 2 3 4 5 6 7 8 9 10