<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.perryizgr8.com//feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.perryizgr8.com//" rel="alternate" type="text/html" /><updated>2026-05-09T10:54:07+00:00</updated><id>https://blog.perryizgr8.com//feed.xml</id><title type="html">Acquire and Release</title><author><name>Parikshit Misra</name></author><entry><title type="html">Building Turnstile, a browser selector for Windows</title><link href="https://blog.perryizgr8.com//projects/2026/05/03/turnstile.html" rel="alternate" type="text/html" title="Building Turnstile, a browser selector for Windows" /><published>2026-05-03T00:00:00+00:00</published><updated>2026-05-03T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//projects/2026/05/03/turnstile</id><content type="html" xml:base="https://blog.perryizgr8.com//projects/2026/05/03/turnstile.html"><![CDATA[<p>I use multiple browsers on my Windows machine. Edge for work stuff, Firefox for personal browsing, and Chrome — well, Chrome is there just to run Playwright tests. This is not an unusual setup. Many people I know keep at least two browsers around.</p>

<p>The problem starts when you click a link somewhere and it opens in the wrong one. Slack link? Should probably go to Edge since I’m signed into work accounts there. A YouTube link from WhatsApp? Firefox, because that’s where I’m logged into my personal Google account. Windows makes you pick one default browser and that’s that. Every single link opens in that one browser, regardless of context.</p>

<p>I got tired of copying links, switching browsers, and pasting them. So I built <a href="https://turnstile.perryizgr8.com">Turnstile</a>.</p>

<h2 id="how-it-works">How it works</h2>

<p>Turnstile registers itself as your default browser. When you click a link anywhere on your system, instead of opening it directly, a small popup appears near your cursor showing all your installed browsers. Each one has a number next to it. Press that number, and the link opens in that browser. Press Escape and nothing happens. The whole thing takes less than a second.</p>

<p>It also detects browser profiles. If you have multiple Edge profiles or Firefox profiles set up, they show up as separate entries in the picker. If you have a work profile and a personal profile in Edge, you can pick which one to use right from the popup.</p>

<h2 id="automation-rules">Automation rules</h2>

<p>After using it for a while, I noticed that I always open Slack links in Edge, and YouTube links in Firefox. So I added automation rules. You can define a rule that says “always open links matching <code class="language-plaintext highlighter-rouge">*.slack.com</code> in Edge”, or “always open links from the Slack app in Firefox”. Turnstile matches the URL against your rules and skips the picker entirely if it finds a match.</p>

<p>Rules can match on domain patterns or regular expressions, and you can also filter by the application that opened the link. So you can say “links from Discord should always open in Firefox” and that works too.</p>

<h2 id="rule-suggestions">Rule suggestions</h2>

<p>This is my favourite feature. Turnstile watches which browser you pick for each domain and source app. After a few consistent choices, it suggests a rule. It tells you something like “You’ve opened links from slack.com in Edge 12 out of 13 times. Create a rule?” If the suggestion makes sense, you accept it and never have to pick again for that domain.</p>

<p>It won’t badger you with suggestions. It needs at least 5 events and 85% consistency before it even brings it up. And if you dismiss a suggestion, it won’t ask again.</p>

<h2 id="source-app-detection">Source app detection</h2>

<p>This was a fun one to implement. When you click a link in an app, Windows passes the URL to the default browser, but doesn’t tell the browser which app sent it. Turnstile works around this by using P/Invoke to call into ntdll.dll and figure out which process actually invoked it. If that fails, it falls back to checking which window was in the foreground. This is how it can let you create rules based on which app the link came from, not just the URL.</p>

<h2 id="how-i-built-it">How I built it</h2>

<p>Here’s the part that surprised even me — I didn’t write a single line of code for this project. The entire app was built using Opencode GLM 5.1. I described what I wanted, iterated on the output, and ended up with a fully functional Windows app without writing any code myself.</p>

<p>Turnstile is a WPF app written in C# on .NET 9. WPF turned out to be the simplest way to get a lightweight, native-looking popup on Windows. The picker window positions itself near your cursor, animates in, and auto-closes if you click away from it.</p>

<p>All the configuration — rules, browser order, usage data — lives in JSON files under <code class="language-plaintext highlighter-rouge">%LOCALAPPDATA%\Turnstile\</code>. No database, no background service, nothing running when you’re not picking a browser.</p>

<h2 id="where-to-get-it">Where to get it</h2>

<p>You can find it on the Microsoft Store by searching for “Turnstile”. More details at <a href="https://turnstile.perryizgr8.com">turnstile.perryizgr8.com</a>.</p>]]></content><author><name>Parikshit Misra</name></author><category term="projects" /><summary type="html"><![CDATA[I use multiple browsers on my Windows machine. Edge for work stuff, Firefox for personal browsing, and Chrome — well, Chrome is there just to run Playwright tests. This is not an unusual setup. Many people I know keep at least two browsers around.]]></summary></entry><entry><title type="html">Degradation of YouTube’s Watch Later feature</title><link href="https://blog.perryizgr8.com//ui/2023/07/29/youtube-watch-later.html" rel="alternate" type="text/html" title="Degradation of YouTube’s Watch Later feature" /><published>2023-07-29T00:00:00+00:00</published><updated>2023-07-29T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//ui/2023/07/29/youtube-watch-later</id><content type="html" xml:base="https://blog.perryizgr8.com//ui/2023/07/29/youtube-watch-later.html"><![CDATA[<p>Enshittification - when an online platform becomes more monetized and less user-oriented the longer it lasts.</p>

<p>Most online apps and services seem to be falling into this trap. At launch, they solve an important problem or provide great value to users. People start using it en mass. As the userbase grows, the platform starts making changes that are at odds with what users want.</p>

<p>Some examples (including one which is a physical product):</p>

<ul>
  <li>Apple (and subsequently every other phone manufacturer) removed the headphone jack from its phones.</li>
  <li>Reddit practically killed its API which allowed it to grow in the first place.</li>
  <li>Google search, once adorned with tasteful discreet text ads, is now often filled to the brim with ads indistinguishable from real search results.</li>
  <li>Facebook and Instagram have gradually increased the amount of ads to such an extent that it is common to see an entire page full of ads on your feed.</li>
</ul>

<p>Today I want to talk about a recent change in YouTube that I haven’t seen anybody else mention. I believe this one also falls squarely in the enshittification bucket.</p>

<h2 id="what-is-watch-later">What is “Watch Later”?</h2>
<p>Watch Later is a special playlist on YouTube that you can add videos to…watch later. I find it very useful because I don’t always have time to watch a 15 minute video right now, but I still want to watch it at some point. When I have time, instead of hunting for videos I missed, I just go to my Watch Later playlist and select what I want to watch.</p>

<h2 id="how-was-it-wasy-to-use">How was it wasy to use?</h2>
<p>The playlist was special because it was very easy (usually a single click or tap) to add a video to it.</p>

<p>On phone, in the feed, you could tap the three dot menu on any video, and the first option in the menu was watch later. Two easy taps, and you’re done.</p>

<p>On web, if you hover over the thumbnail of any video, there was a dedicated watch later button on the bottom right. One click, and you’re done.</p>

<p>On TV, you could hold the select button on any video and again, the first item in the menu that would open would add the video to your watch later list. One long click, and a short one.</p>

<h2 id="how-has-it-changed">How has it changed?</h2>
<p>On phone, the watch later option is still there in the three dot menu, but it’s the second one. The first one is now an ad for their premium subscription. The change in positioning was very clever. It always makes me pause for a moment now, before I can tap the watch later option.</p>

<p>On web, the hover shortcut is gone, replaced by two useless hover icons which I will never use. Now you need to locate the three dot menu, click it, and choose the second option in the list.</p>

<p>On TV, they got rid of the long select menu entirely. There is no way to add a video to watch later from the feed.</p>

<h2 id="why-did-they-make-it-worse">Why did they make it worse?</h2>
<p>It really feels like someone at YouTube had a grudge against the feature for some reason. It has been made harder to locate and use on every platform more or less at the same time. I have thought a lot about it but cannot figure out why they would do this. It doesn’t make any sense at all. I am sure the justification involves something something advertisers, watch time, something…</p>

<p>/rant</p>]]></content><author><name>Parikshit Misra</name></author><category term="UI" /><summary type="html"><![CDATA[Enshittification - when an online platform becomes more monetized and less user-oriented the longer it lasts.]]></summary></entry><entry><title type="html">Nested code considered harmful</title><link href="https://blog.perryizgr8.com//programming/2023/05/25/never-nester.html" rel="alternate" type="text/html" title="Nested code considered harmful" /><published>2023-05-25T00:00:00+00:00</published><updated>2023-05-25T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//programming/2023/05/25/never-nester</id><content type="html" xml:base="https://blog.perryizgr8.com//programming/2023/05/25/never-nester.html"><![CDATA[<p>I recently watched <a href="https://youtu.be/CFRhGnuXG-4">a YouTube video</a> that presented the benefits of never nesting your code more than a few levels. I’ve felt that it expressed in words what I’d always known within. So let me explain.</p>

<h2 id="nesting">Nesting</h2>
<p>Consider the following function that performs the common spline reticulation procedure.</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><span class="kt">int</span> <span class="nf">reticulate_the_splines</span><span class="p">(</span><span class="n">network</span><span class="p">,</span> <span class="n">curves</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span><span class="p">(</span><span class="n">curves</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="n">network</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="n">ONLINE</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">for</span><span class="p">(</span><span class="k">auto</span> <span class="n">c</span><span class="o">:</span> <span class="n">curves</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">network</span><span class="p">.</span><span class="n">add_curve</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"The network is not ONLINE."</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"No curves to add to the network."</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Inside the function, there are up to 3 levels of nesting. The deepest nesting is on the line:</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">                <span class="n">network</span><span class="p">.</span><span class="n">add_curve</span><span class="p">(</span><span class="n">c</span><span class="p">)</span></code></pre></figure>

<p>In more complicated functions, it is common to encounter even deeper levels of nesting, due to <code class="language-plaintext highlighter-rouge">if...else</code> conditions and loops.</p>

<h2 id="refactoring">Refactoring</h2>
<p>Let’s try to decrese the level of nesting in the above function.</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><span class="kt">int</span> <span class="nf">reticulate_the_splines</span><span class="p">(</span><span class="n">network</span><span class="p">,</span> <span class="n">curves</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span><span class="p">(</span><span class="n">curves</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"No curves to add to the network."</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">if</span><span class="p">(</span><span class="n">network</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">!=</span> <span class="n">ONLINE</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"The network is not ONLINE."</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">for</span><span class="p">(</span><span class="k">auto</span> <span class="n">c</span><span class="o">:</span> <span class="n">curves</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">network</span><span class="p">.</span><span class="n">add_curve</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>This function only has at most one level of nesting. I find it to be much more readable. The control flow seems to be more clearly visible. At a glance you can understand that having no curves or a network that’s not online are error states that prevent the function from carrying out its operation.</p>

<p>Once the error checking is out of the way, it is very clear that the function loops over <code class="language-plaintext highlighter-rouge">curves</code> and adds each to the <code class="language-plaintext highlighter-rouge">network</code>. In the nested version, this main logic was buried deep beneath condition checks, and the error handling was dispersed across the entire length of the function.</p>

<p>The video makes a very detailed and visual point. If you aren’t convinced by my justification here, be sure to give it a watch!</p>]]></content><author><name>Parikshit Misra</name></author><category term="programming" /><summary type="html"><![CDATA[I recently watched a YouTube video that presented the benefits of never nesting your code more than a few levels. I’ve felt that it expressed in words what I’d always known within. So let me explain.]]></summary></entry><entry><title type="html">How to store ranges in Elasticsearch Part 2</title><link href="https://blog.perryizgr8.com//elasticsearch/2023/05/08/range-fields-elastic-search-2.html" rel="alternate" type="text/html" title="How to store ranges in Elasticsearch Part 2" /><published>2023-05-08T00:00:00+00:00</published><updated>2023-05-08T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//elasticsearch/2023/05/08/range-fields-elastic-search-2</id><content type="html" xml:base="https://blog.perryizgr8.com//elasticsearch/2023/05/08/range-fields-elastic-search-2.html"><![CDATA[<p><em>Read part 1 of this series <a href="/elasticsearch/2023/05/01/range-fields-elastic-search.html">here</a>.</em></p>

<p>Last time we saw how to use range fields to store range values in Elasticsearch. You could store two discontinuous brightness ranges supported by a TV operating in two modes (SDR and HDR). However, we did not associate the ranges with their respective modes.</p>

<p>To do that properly, we need to make use of nested types in Elasticsearch.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">PUT</span><span class="w"> </span><span class="err">televisions_nested_index</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"text"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"nested"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"text"</span><span class="w">
                    </span><span class="p">},</span><span class="w">
                    </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer_range"</span><span class="w">
                    </span><span class="p">}</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>Let’s put in the same TVs as last time, but with the mode information attached.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">PUT</span><span class="w"> </span><span class="err">televisions_nested_index/_doc/sony_bravia</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sony Bravia"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SDR"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
                </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">300</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">PUT</span><span class="w"> </span><span class="err">televisions_nested_index/_doc/lg_oled</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LG OLED"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SDR"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
                </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HDR"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">500</span><span class="p">,</span><span class="w">
                </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>Now we can query for a specific brightness range in a particular mode. So let’s search for a TV that supports 600 to 700 nits in the HDR mode.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">GET</span><span class="w"> </span><span class="err">televisions_nested_index/_search</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"bool"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"must"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="s2">"nested"</span><span class="err">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"inner_hits"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
                    </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"brightness"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                        </span><span class="nl">"bool"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                            </span><span class="nl">"must"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                                </span><span class="p">{</span><span class="w">
                                    </span><span class="nl">"term"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                        </span><span class="nl">"brightness.mode"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                            </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HDR"</span><span class="w">
                                        </span><span class="p">}</span><span class="w">
                                    </span><span class="p">}</span><span class="w">
                                </span><span class="p">},</span><span class="w">
                                </span><span class="p">{</span><span class="w">
                                    </span><span class="nl">"range"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                        </span><span class="nl">"brightness.value"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                            </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">600</span><span class="p">,</span><span class="w">
                                            </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">700</span><span class="w">
                                        </span><span class="p">}</span><span class="w">
                                    </span><span class="p">}</span><span class="w">
                                </span><span class="p">}</span><span class="w">
                            </span><span class="p">]</span><span class="w">
                        </span><span class="p">}</span><span class="w">
                    </span><span class="p">}</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">]</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>This will return the LG TV since it is the only one that supports our required brightness in the HDR mode.</p>

<p><em>Thanks to the person who answered <a href="https://stackoverflow.com/questions/76167224/storing-and-searching-units-and-value-ranges-in-elasticsearch">my question on Stack Overflow</a>. It helped me make progress when I was completely stuck.</em></p>]]></content><author><name>Parikshit Misra</name></author><category term="elasticsearch" /><summary type="html"><![CDATA[Read part 1 of this series here.]]></summary></entry><entry><title type="html">How to store ranges in Elasticsearch</title><link href="https://blog.perryizgr8.com//elasticsearch/2023/05/01/range-fields-elastic-search.html" rel="alternate" type="text/html" title="How to store ranges in Elasticsearch" /><published>2023-05-01T00:00:00+00:00</published><updated>2023-05-01T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//elasticsearch/2023/05/01/range-fields-elastic-search</id><content type="html" xml:base="https://blog.perryizgr8.com//elasticsearch/2023/05/01/range-fields-elastic-search.html"><![CDATA[<p><em>This is part 1, read part 2 of this series <a href="/elasticsearch/2023/05/08/range-fields-elastic-search-2.html">here</a>.</em></p>

<p>Elasticsearch is a great index and document based search system. It’s amazing for storing a bunch of data and retrieving the right bit very quickly. It has a rich querying system that is highly flexible and overall it’s a drop-in, batteries-included search engine that you can host.</p>

<p>I recently found out that Elasticsearch supports range field types. Instead of assigning a single value to a field, you can instead put in a range. These are called <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/range.html#range">range field types</a>. You can then make <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html">ranged queries</a> to look up these kind of fields.</p>

<p>Suppose you want to store the attributes of TVs in your index. One of those is the brightness of the TV which is commonly measured in nits. Let’s create an index with a range field that can store this info.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">PUT</span><span class="w"> </span><span class="err">televisions_index</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"text"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer_range"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>Now we have an index ready to accept documents about each TV we want to store. Let’s store our first TV. This is a Sony Bravia TV whose screen brightness can vary from 100 nits to 300 nits.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">PUT</span><span class="w"> </span><span class="err">televisions_index/_doc/sony_bravia</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sony Bravia"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
            </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">300</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>Here <code class="language-plaintext highlighter-rouge">gte</code> and <code class="language-plaintext highlighter-rouge">lte</code> stand for “greater than or equal to” and “less than or equal to”. This signifies a range of 100 to 300 inclusive.</p>

<p>Now let’s store another TV. This LG OLED has two modes. In the standard mode, its screen can go from 100 to 400 nits. But in the HDR mode, the screen can go from 500 to 1000 nits.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">PUT</span><span class="w"> </span><span class="err">televisions_index/_doc/lg_oled</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LG OLED"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
            </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">500</span><span class="p">,</span><span class="w">
            </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>Now let’s make a simple query to our index. We want to get a list of TVs that support a brightness value of 700 nits.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">GET</span><span class="w"> </span><span class="err">televisions_index/_search</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"term"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="mi">700</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>This will return only the LG OLED TV in the result, since only that one supports a brightness value of 700 nits.</p>

<p>We can also query for a range of brightness values.</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="err">GET</span><span class="w"> </span><span class="err">televisions_index/_search</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"range"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"brightness"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="mi">150</span><span class="p">,</span><span class="w">
                </span><span class="nl">"lte"</span><span class="p">:</span><span class="w"> </span><span class="mi">250</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>This will return both TVs in the result, since both of them support 150 to 250 nits of screen brightness.</p>]]></content><author><name>Parikshit Misra</name></author><category term="elasticsearch" /><summary type="html"><![CDATA[This is part 1, read part 2 of this series here.]]></summary></entry><entry><title type="html">Self-hosting a Google Photos alternative</title><link href="https://blog.perryizgr8.com//photos/2023/03/20/google-photos-alt.html" rel="alternate" type="text/html" title="Self-hosting a Google Photos alternative" /><published>2023-03-20T00:00:00+00:00</published><updated>2023-03-20T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//photos/2023/03/20/google-photos-alt</id><content type="html" xml:base="https://blog.perryizgr8.com//photos/2023/03/20/google-photos-alt.html"><![CDATA[<p>A story about my quest to avoid paying Google in perpetuity.</p>

<h2 id="googles-bait-and-switch">Google’s bait and switch</h2>
<p>Google offered their excellent Google Photos app and backup service for many years, with unlimited photo/video backup. The only catch being that your media is compressed and uploaded. But the resulting quality was good enough that I did not mind. My wife and I used it for backing up all our photos, and sharing them with each other.</p>

<p>They ended the unlimited free tier in June 2021. Now we must pay a monthly rent to Google to be able to keep using it as our backup.</p>

<h2 id="googles-nonsensical-pricing">Google’s nonsensical pricing</h2>

<table>
  <thead>
    <tr>
      <th>Plan</th>
      <th>Storage</th>
      <th>Monthly Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Basic</td>
      <td>100 GB</td>
      <td>₹130</td>
    </tr>
    <tr>
      <td>Standard</td>
      <td>200 GB</td>
      <td>₹210</td>
    </tr>
    <tr>
      <td>Premium</td>
      <td>2 TB</td>
      <td>₹650</td>
    </tr>
  </tbody>
</table>

<p>I know that my photo library is over 200 GB, maybe around the 1 TB mark. But it is continuously growing. I am sure it will cross 2 TB in the next 5 years, given the ever-increasing quality of our cameras.</p>

<p>So paying every month for 2 TB now, and then inevitably being forced to pay more and more indefinitely is not a very enticing thought. In my opinion, they should charge a low fixed amount (say ₹50) per month, regardless of the amount you’re storing, plus a very low amount per gigabyte (say ₹0.0002). That way, everyone pays for the base service equally, and everyone also pays for the storage they are actually consuming.</p>

<h2 id="photoprism">PhotoPrism</h2>
<p>The first alternative I found was <a href="https://www.photoprism.app/">PhotoPrism</a>, a self-hosted app that you can run on your server, and access from your phone or web. Point it to a folder on your drive and it imports all photos and videos.</p>

<p>Pros:</p>
<ul>
  <li>Easy to set up.</li>
  <li>Runs on docker, so platform-independent.</li>
  <li>Open source and free.</li>
  <li>Feature-rich, includes face recognition and a powerful search.</li>
  <li>Actively developed, few bugs.</li>
</ul>

<p>Cons:</p>
<ul>
  <li>No good way to backup from phone to server.</li>
  <li>No native apps on mobile, have to use horrible and clunky PWA.</li>
</ul>

<p>The last one is extremely annoying. PWAs are simply not ready. It literally does not feel like a proper app. It feels like a web page. Each button press is laggy, every photo takes a while to load, all around a pain to use. Infuriatingly, it seems like there are no plans for a native app at all. Apparently, the PWA <a href="https://www.photoprism.app/kb/compliance-faq#:~:text=At%20the%20moment%2C%20PhotoPrism%20does%20not%20have%20a,of%20all%20major%20operating%20systems%20and%20mobile%20devices">offers an almost native app-like experience</a>. No, PhotoPrism, it doesn’t.</p>

<p>There is an <a href="https://github.com/thielepaul/photoprism-mobile">unofficial Flutter app</a>, but it is currently incompatible with the latest version of PhotoPrism. As far as I can tell, it has been incompatible for over 2 years. So I don’t have high hopes from this project.</p>

<h2 id="immich">Immich</h2>
<p>Recently, I stumbled upon <a href="https://immich.app/">Immich</a>, a self-hosted app that has the explicit aim of replacing Google Photos. To that end, it has native mobile apps that work very similar to Google Photos. The apps seem highly polished, and the server is easy to set up using docker compose.</p>

<p>Pros:</p>
<ul>
  <li>Easy to set up.</li>
  <li>Runs on docker, so platform-independent.</li>
  <li>Open source and free.</li>
  <li>Slick native Android and iOS apps.</li>
</ul>

<p>Cons:</p>
<ul>
  <li>Not very feature-rich yet.</li>
  <li>Very active development means things are likely to break.</li>
  <li>Can’t import existing photo library.</li>
</ul>

<p>A strange quirk with Immich is that you can’t point it to your existing photo library and have it scan it (as far as I can tell). You must upload photos from your phone using the backup feature, or upload pictures via the web app. In my opinion they went too far in replicating Google Photos here.</p>

<h2 id="problem-with-immich">Problem with Immich</h2>
<p>I set up the server on my PC, downloaded the app on my phone and started the backup process. The backup always gets stuck on a particular video file. It’s not overly huge, about 150 MB. No matter what I did, it always gets stuck on this file and the backup never completes.</p>

<h2 id="how-my-server-connects-to-the-internet">How my server connects to the internet</h2>
<p>My “server” is just my PC in my bedroom. It runs Windows 11. I have set up a Cloudflare tunnel to expose a port to a particular DNS record. I input that DNS name in the Immich app’s server setting. I need to do this because I don’t have a public IP from my ISP, and all ports are effectively blocked due to a double NAT.</p>

<h2 id="debugging">Debugging</h2>
<p>I had a suspicion that the issue had something to do with the tunnel. Since the upload gets stuck on a slightly large video, and not on a regular-sized photo, I wondered if the tunnel was restricting uploads above a certain limit. After all, Cloudflare’s architecture is built to support small files, and that too in the other direction.</p>

<p>I started a python server using the <a href="https://pypi.org/project/uploadserver/">uploadserver</a> package. It lets you download and upload files via a rudimentary web interface. I tried to upload a video file of about the same size:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~&gt; curl -v 'https://mysite/upload' \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary1dMEK7Ts8bf8AZTg' \
  -F "files=@/Users/parikshit/Downloads/clip.mp4"
*   Trying xxx.xxx.xxx.xxx:443...
* Connected to mysite (xxx.xxx.xxx.xxx) port 443 (#0)

...skipped...

&lt; HTTP/2 413
&lt; date: Mon, 20 Mar 2023 14:29:51 GMT
&lt; content-type: text/html
&lt; report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=******"}],"group":"cf-nel","max_age":604800}
&lt; nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
&lt; server: cloudflare
&lt; cf-ray: *****-BLR
&lt; alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
* HTTP error before end of send, stop sending
&lt;
&lt;html&gt;
&lt;head&gt;&lt;title&gt;413 Request Entity Too Large&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
&lt;center&gt;&lt;h1&gt;413 Request Entity Too Large&lt;/h1&gt;&lt;/center&gt;
&lt;hr&gt;&lt;center&gt;cloudflare&lt;/center&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre></div></div>

<p>As suspected, Cloudflare doesn’t like it if you try sending large files in the opposite direction. Possibly their network is simply not optimised for that.</p>

<h2 id="solution">Solution</h2>
<p>There are two possible solutions I can think of.</p>

<ol>
  <li>Immich is open-source. I could contribute a patch that chunks larger files when uploading.</li>
  <li>I could use a Wireguard or Tailscale tunnel between my PC and some cloud server, and use that to route my traffic.</li>
</ol>

<p>I’ll post an update when I manage to solve the issue.</p>

<h2 id="update">Update</h2>
<p>I submitted a <a href="https://github.com/immich-app/immich/pull/2101">PR</a> to Immich’s GitHub repo that simply adds the header <code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code> to the request when uploading a file from the Flutter mobile app to the server. This makes the <code class="language-plaintext highlighter-rouge">HttpClient</code> omit the <code class="language-plaintext highlighter-rouge">Content-Length</code> header, which bypasses CloudFlare’s checks, and our file upload completes successfully.</p>

<p>The PR was merged and my changes are live in version <a href="https://github.com/immich-app/immich/releases/tag/v1.52.0">1.52.0</a> of the app.</p>]]></content><author><name>Parikshit Misra</name></author><category term="photos" /><summary type="html"><![CDATA[A story about my quest to avoid paying Google in perpetuity.]]></summary></entry><entry><title type="html">Airpods considered harmful</title><link href="https://blog.perryizgr8.com//apple/2023/01/27/airpods-considered-harmful.html" rel="alternate" type="text/html" title="Airpods considered harmful" /><published>2023-01-27T00:00:00+00:00</published><updated>2023-01-27T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//apple/2023/01/27/airpods-considered-harmful</id><content type="html" xml:base="https://blog.perryizgr8.com//apple/2023/01/27/airpods-considered-harmful.html"><![CDATA[<p>This is a story of how Apple’s wireless airpods have made my life worse even though I am not even a customer.</p>

<p>As is true for many software teams today, ours is a mixture of people from multiple cultures and backgrounds. Along with all the bonuses this brings, there is also the small inconvenience of varying accents.</p>

<p>I consider my accent-understanding skills to be decent. I can make sense of all but the most extreme ones. And I like to think mine is at least intelligible to most others.</p>

<p>Face-to-face conversations in person are rare, since our team is highly distributed across geographies and timezones. We use Google Meet and Slack huddles to converse with each other. Both of these tools’ audio quality is excellent (although Google Meet’s screen sharing quality leaves a lot to be desired).</p>

<p>Everybody’s voice comes out crystal clear, even when network connections are not the best.</p>

<p>In recent times, I’ve started noticing something. Some people’s voice seems to be extremely low fidelity. It sounds like they are on the other side of a kiddie walkie-talkie, operating at the limit of its range. Coupled with my inability to comprehend all accents equally, it gets really hard to follow what’s being said.</p>

<p>I was puzzled, because the same meeting would have everybody loud and clear, and a couple of people muffled and quiet and generally low-quality. Over the last few months I’ve figured it out.</p>

<p>Everybody who sounds muffled is using an Apple airpod. That’s it. The mic on this fancy device is worse than on children’s toys from the 90s.</p>

<p>So whoever is reading this, if you’re using an airpod, please don’t use it in meetings. It will help you get your point across much more clearly if you ditch the low quality mics that are built into the contraption.</p>

<p>Thanks for tuning into my Apple rant of the week.</p>]]></content><author><name>Parikshit Misra</name></author><category term="Apple" /><summary type="html"><![CDATA[This is a story of how Apple’s wireless airpods have made my life worse even though I am not even a customer.]]></summary></entry><entry><title type="html">What’s wrong with Shark Tank India</title><link href="https://blog.perryizgr8.com//tv/2023/01/27/shark-tank-india.html" rel="alternate" type="text/html" title="What’s wrong with Shark Tank India" /><published>2023-01-27T00:00:00+00:00</published><updated>2023-01-27T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//tv/2023/01/27/shark-tank-india</id><content type="html" xml:base="https://blog.perryizgr8.com//tv/2023/01/27/shark-tank-india.html"><![CDATA[<p><em>Shark Tank India</em> is airing its second season, and it’s the only TV show I am actively watching these days.</p>

<p>I saw <a href="https://twitter.com/hvgoenka/status/1617095313902415872?s=20">this tweet</a> making the rounds last week. If you don’t want to visit Twitter I’ll describe what it says.</p>

<p>Harsh Goenka says:</p>
<blockquote>
  <p>I enjoy #SharkTankIndia as a program and I think it is a great platform for our budding entrepreneurs.</p>

  <p>But whenever I think of sharks, I think of the movie ‘Jaws’ and bleeding 🩸!</p>
</blockquote>

<p>There’s an image of a table like so:</p>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Company</th>
      <th>FY22 profit (₹ crores)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Aman Gupta</td>
      <td>boAt</td>
      <td>79</td>
    </tr>
    <tr>
      <td>Amit Jain</td>
      <td>CarDekho</td>
      <td>-246</td>
    </tr>
    <tr>
      <td>Anupam Mittal</td>
      <td>Shaadi.com</td>
      <td>-27 (FY20)</td>
    </tr>
    <tr>
      <td>Peyush Bansal</td>
      <td>Lenskart</td>
      <td>-102</td>
    </tr>
    <tr>
      <td>Namita Thapar</td>
      <td>Emcure Pharma</td>
      <td>Not a startup</td>
    </tr>
    <tr>
      <td>Vineeta Singh</td>
      <td>SUGAR cosmetics</td>
      <td>-75</td>
    </tr>
  </tbody>
</table>

<p>The point is that all the sharks (with the exception of Aman Gupta’s boAt) are presiding over failing, unprofitable companies.</p>

<h2 id="thoughts-on-the-first-season">Thoughts on the first season</h2>
<p>I quite enjoyed the first season last year. I had previously watched the UK show <em>Dragon’s Den</em> which seems to have inspired the string of <em>Shark Tank</em> productions around the world. I liked the Indian take on the subject matter and was honestly surprised at the variety of ideas and determination with which people are trying to realise them across the country.</p>

<h2 id="whats-changed-with-season-two">What’s changed with season two</h2>
<p>The show lost its most controversial shark, Ashneer Grover. Though they haven’t come out and said it, his disgraceful exit from his own company must have played a role in the decision to replace him.</p>

<p>Apart from this, I feel the show has changed its tone significantly. Sharks seem to be much more reluctant to partake in risky bets. They fail to ask pertinent questions, and seem to be eager only when presented with a perfect company which has almost no risk at all.</p>

<h2 id="the-core-issue---lack-of-focus-on-whats-important">The core issue - lack of focus on what’s important</h2>
<p>In the British <em>Dragon’s Den</em>, a very important question that every entrepreneur got asked was “What are you going to do with the money, if you were to get it today?” When someone is looking for an outside investment, they must have some plan in mind that they think will take the business to the next level, but they just don’t have enough cash to put the plan into action. Hence, the appeal to investors.</p>

<p>So if you can’t explain what you plan to do with the money infusion, it used to be a sure-shot failure to secure funding in <em>Dragon’s Den</em>.</p>

<p>This question is totally missing from the second season of <em>Shark Tank</em>. Not one shark bothered to make this all-important query of any pitcher.</p>

<blockquote>
  <p>Oh you want 50 lakhs? What is your sales last year? What is your profit margin? What is your projection for the next year? What are your qualifications?</p>

  <p>Ahh, that’s a nicely running profitable growing business! I’d like me a piece of it. Here’s your money!</p>
</blockquote>

<p>If someone has a business that’s already doing well, and they don’t have any plans to make it explode using a capital infusion, why would they ever come to you? They’re already running a profitable venture. They can continue doing whatever they are and collect their profits.</p>

<p>Another glaring factor that’s missing in the second season is the broke inventor who has a brilliant idea, has done basic market research and needs seed capital to get his company off the ground. Sharks in season two will laugh such a guy out of the show, telling him to come back when he has a business. That’s why he came to you for help! If he is able to prove his idea in the market and start making good money off it, why would he come back to you?</p>

<h2 id="not-supposed-to-be-a-show-about-investment">Not supposed to be a show about investment</h2>
<p>What the sharks don’t seem to understand is that the point of this show was never to be an investment show. It was supposed to promote the spirit of entrepreneurship. It wanted to ensure that no good idea goes un-funded. If you have an idea and you can convince these industry stalwarts of its worth, you don’t need to worry about your first capital investment to get your venture started.</p>

<p>Intriguingly, the first season was much better in this regard. There were multiple lone inventors who appeared on the show and a few of them were also able to secure funds.</p>

<h2 id="is-it-because-of-the-sharks-not-being-profitable">Is it because of the sharks not being profitable?</h2>
<p>Now, we come back to the tweet. Are these problems in the season because the sharks are simply not good enough businessmen? Are they just incompetent as entrepreneurs? After all, their companies are bleeding money by the hundreds of crores. What do they know about running a profitable business?</p>

<p>I don’t know if that’s what the issue is, but it’s definitely something to think about.</p>]]></content><author><name>Parikshit Misra</name></author><category term="TV" /><summary type="html"><![CDATA[Shark Tank India is airing its second season, and it’s the only TV show I am actively watching these days.]]></summary></entry><entry><title type="html">Cookiecutter: making repetition easy</title><link href="https://blog.perryizgr8.com//dev/2023/01/03/cookiecutter.html" rel="alternate" type="text/html" title="Cookiecutter: making repetition easy" /><published>2023-01-03T00:00:00+00:00</published><updated>2023-01-03T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//dev/2023/01/03/cookiecutter</id><content type="html" xml:base="https://blog.perryizgr8.com//dev/2023/01/03/cookiecutter.html"><![CDATA[<p>Developing a web app is an exercise in balance. One critical aspect is to decide between the opposing tendencies of monolithisation and micro-services. Like most decisions, this is always a matter of degree. You can absolutely go too far in one direction and suffer for it.</p>

<p>As the app grows, especially if it is written in a “slow” language like Python, you will find yourself trying to jump for a micro-service for every new feature that comes your way. This is because it becomes much harder to maintain quality and performance in a Python/Django monolith as the codebase grows.</p>

<p>The general tendency in this case is to copy the existing Django project, make your new addition, remove or comment the existing unneeded stuff and deploy it somewhere else. But cruft accumulates, every bad decision made in the app carries forward.</p>

<p>The ideal thing to do in such situations is to create a new service from scratch, preferably in a faster language like Go or Rust. However it’s hard to do, and takes a lot of time and effort. This additional delay is hard to justify for a small to medium feature.</p>

<p>In times like these, it is helpful if you have a ready-to-go template to start you off with basic scaffolding already in place. You can start from “Hello, world!” within a minute. Then add your required functionality.</p>

<p>Enter <a href="https://github.com/cookiecutter/cookiecutter">Cookiecutter</a>.</p>

<p>It’s easy to create a template using Cookiecutter for a basic Go app that serves a REST API using <a href="https://echo.labstack.com/">Echo</a>. It’s set up with common amenities and has a nice starting point for any service.</p>]]></content><author><name>Parikshit Misra</name></author><category term="dev" /><summary type="html"><![CDATA[Developing a web app is an exercise in balance. One critical aspect is to decide between the opposing tendencies of monolithisation and micro-services. Like most decisions, this is always a matter of degree. You can absolutely go too far in one direction and suffer for it.]]></summary></entry><entry><title type="html">PDF deduplication</title><link href="https://blog.perryizgr8.com//python/2022/11/29/pdf-dedup.html" rel="alternate" type="text/html" title="PDF deduplication" /><published>2022-11-29T00:00:00+00:00</published><updated>2022-11-29T00:00:00+00:00</updated><id>https://blog.perryizgr8.com//python/2022/11/29/pdf-dedup</id><content type="html" xml:base="https://blog.perryizgr8.com//python/2022/11/29/pdf-dedup.html"><![CDATA[<p>Suppose you have a large number of PDF documents stored in your computer, and every so often keep adding new ones to your collection. A problem that rears its head soon is that you end up with a lot of duplicate documents on your disk. They eat up your storage and are annoying when you want to search for a particular document.</p>

<h2 id="naive-solution---file-checksum">Naive solution - file checksum</h2>
<p>A super easy solution is to store a list of checksums for your files in a spreadsheet, like so:</p>

<table>
  <thead>
    <tr>
      <th>File</th>
      <th>MD5</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>cyberdyne_systems.pdf</td>
      <td>81f65b305684dfc5168b23472ddaef88</td>
    </tr>
    <tr>
      <td>umbrella_corp.pdf</td>
      <td>a19dad769173792e21ae3aa33ec704d1</td>
    </tr>
    <tr>
      <td>weyland_yutani_corp.pdf</td>
      <td>eff5ee486550143f563360cbdf80ab10</td>
    </tr>
  </tbody>
</table>

<p>Whenever you are about to add a file to your collection, calculate its MD5 checksum (<code class="language-plaintext highlighter-rouge">$&gt; md5sum evil_corp.pdf</code>) and search for the checksum in your spreadsheet. If you don’t find it, add it as a new row, confident in the fact that you’re not duplicating anything.</p>

<h3 id="why-this-fails">Why this fails</h3>
<p>Any small change to the document will cause the checksum to change completely (<a href="https://en.wikipedia.org/wiki/Avalanche_effect">Avalanche effect</a>), even if the change is insignificant or even invisible to the reader. PDF files have metadata in them, stuff like <code class="language-plaintext highlighter-rouge">/Producer</code>, <code class="language-plaintext highlighter-rouge">/CreationDate</code>, <code class="language-plaintext highlighter-rouge">/ModDate</code>, etc. For my purpose, I don’t care about these things. I want to know if the content of the PDF is same or not.</p>

<p>Also, some PDFs are generated on the fly when you request for them, and the generator program will put a small <code class="language-plaintext highlighter-rouge">Generated on 29-11-2022</code> somewhere in the document, probably in the footer. Again, even though this is technically part of the PDF content, and is also visible on the document, I don’t care about this.</p>

<h2 id="smarter-solution---image-checksum">Smarter solution - image checksum</h2>
<p>Open the PDF file with a utility like <a href="https://github.com/pymupdf/PyMuPDF">PyMuPDF</a>. This gives you an option to render the document to an image. Then you can checksum the generated image instead. To handle files with multiple pages you can checksum each page’s image, concatenate the checksums, and then checksum this concatenated string.</p>

<h3 id="why-this-fails-1">Why this fails</h3>
<p>This solves the metadata problem, you are now checksumming the visible part of the document, which is what you care about. Metadata can be totally different, but that won’t affect your deduplication.
It still doesn’t do anything about the second problem. You don’t want to strictly consider every pixel in the document. I want to tolerate small differences (a different date in a footnote, an extra box drawn around a word, etc.), and still consider such files identical.</p>

<h2 id="locality-sensitive-hashing">Locality sensitive hashing</h2>
<p>My search for a suitable solution led me to <a href="https://en.wikipedia.org/wiki/Locality-sensitive_hashing">this wikipedia page</a>.</p>

<blockquote>
  <p>locality-sensitive hashing (LSH) is an algorithmic technique that hashes similar input items into the same “buckets” with high probability.
Since similar items end up in the same buckets, this technique can be used for data clustering and nearest neighbor search. It differs from conventional hashing techniques in that hash collisions are maximized, not minimized. Alternatively, the technique can be seen as a way to reduce the dimensionality of high-dimensional data; high-dimensional input items can be reduced to low-dimensional versions while preserving relative distances between items.</p>
</blockquote>

<p>Pay attention to the last part:</p>
<blockquote>
  <p>high-dimensional input items can be reduced to low-dimensional versions while preserving relative distances between items.</p>
</blockquote>

<p>Sounds like exactly what I want. I want to reduce a PDF to a low-dimensional checksum, which remains comparable between different PDFs.</p>

<p>This is quite new to me, so I am going to read all I can about it before I try to apply the technique to my problem. I am listing some links that I plan to examine.</p>

<ul>
  <li><a href="https://pypi.org/project/ImageHash/">ImageHash on PyPi</a></li>
  <li><a href="https://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html">Description of average hash and perceptual hash</a></li>
  <li><a href="https://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html">Description of difference hash</a></li>
</ul>]]></content><author><name>Parikshit Misra</name></author><category term="python" /><summary type="html"><![CDATA[Suppose you have a large number of PDF documents stored in your computer, and every so often keep adding new ones to your collection. A problem that rears its head soon is that you end up with a lot of duplicate documents on your disk. They eat up your storage and are annoying when you want to search for a particular document.]]></summary></entry></feed>