<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Blueprints & Bytes]]></title><description><![CDATA[Blueprints & Bytes]]></description><link>https://blog.vishnusujeesh.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Apr 2026 15:44:04 GMT</lastBuildDate><atom:link href="https://blog.vishnusujeesh.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Setting up your Thinkpad for Linux]]></title><description><![CDATA[Setting up your Thinkpad for Linux
A guide for beginners to get started
If Windows is already installed, do this first
Log into Windows with admin credentials.

Shrink the existing partition that Windows is on as much as possible using the disk manag...]]></description><link>https://blog.vishnusujeesh.com/setting-up-your-thinkpad-for-linux</link><guid isPermaLink="true">https://blog.vishnusujeesh.com/setting-up-your-thinkpad-for-linux</guid><dc:creator><![CDATA[Vishnu S]]></dc:creator><pubDate>Sun, 12 Oct 2025 06:23:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/43P5FiTFcXo/upload/0d5d3ac034a28b928336c02aac7ee401.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-setting-up-your-thinkpad-for-linux"><strong>Setting up your Thinkpad for Linux</strong></h3>
<p>A guide for beginners to get started</p>
<h3 id="heading-if-windows-is-already-installed-do-this-first">If Windows is already installed, do this first</h3>
<p>Log into Windows with admin credentials.</p>
<ul>
<li><p><strong>Shrink the existing partition</strong> that Windows is on as much as possible using the disk management tool (or leave space for Windows if you think you will use it regularly). Do not create a new drive from the unallocated space, that will be done when you install your Linux distribution.</p>
</li>
<li><p><strong>Disable fast startup</strong> from power settings.</p>
</li>
<li><p><strong>Disable any Bitlocker encryption</strong>. To save space, you may also want to disable <strong>hibernation</strong> by running <code>powercfg.exe /hibernate off</code> from an <strong>Administrator Command Prompt</strong>.</p>
</li>
</ul>
<hr />
<h3 id="heading-update-the-settings-in-your-biosuefi">Update the settings in your BIOS/UEFI</h3>
<p>Boot into the BIOS/UEFI interface by pressing <strong>F1</strong> when the laptop is starting up (on some ThinkPad models, you may need to press <strong>Enter</strong> first, then F1).</p>
<ul>
<li><p>Ensure that the <strong>Secure Boot</strong> and encryption options are disabled (usually found under the <strong>Security</strong> tab).</p>
</li>
<li><p>For optimal compatibility, you may also want to check the <strong>Config</strong> or <strong>Restart</strong> menu and ensure <strong>OS Optimized Defaults</strong> is <strong>Disabled</strong>.</p>
</li>
<li><p>Some newer ThinkPads may require setting the <strong>Suspend Mode</strong> (often under the <strong>Power</strong> tab) to <strong>Linux</strong> or <strong>"Linux S3"</strong> for proper suspend-to-RAM functionality.</p>
</li>
<li><p>Save your changes with <strong>F10</strong> and exit.</p>
</li>
</ul>
<hr />
<h3 id="heading-create-a-bootable-usb">Create a bootable USB</h3>
<p>Download the <strong>ISO image</strong> for the distribution of your choice (e.g., Ubuntu LTS).</p>
<ul>
<li>Burn it onto a USB using <strong>Rufus</strong> or <strong>BalenaEtcher</strong>.</li>
</ul>
<hr />
<h3 id="heading-install-ubuntu-lts">Install Ubuntu LTS</h3>
<ul>
<li><p><strong>Boot from the USB:</strong> Plug the USB drive into your ThinkPad. When starting the laptop, press <strong>F12</strong> immediately to bring up the <strong>Boot Menu</strong>. Select the USB drive from the list and press Enter.</p>
</li>
<li><p><strong>Start the Installation:</strong> Once the USB boots, you will see a screen. Select <strong>Try or Install Ubuntu</strong>. It's generally recommended to select "Try Ubuntu" first to ensure everything works before committing to the installation, or simply select "Install Ubuntu" to proceed directly.</p>
</li>
<li><p><strong>Follow the Installer Prompts:</strong></p>
<ul>
<li><p>Choose your <strong>language</strong> and <strong>keyboard layout</strong>.</p>
</li>
<li><p>Select your installation type: choose <strong>Normal Installation</strong> and check the box to <strong>Install third-party software for graphics and Wi-Fi hardware</strong> (this is important for proprietary drivers like Nvidia graphics or some Wi-Fi cards).</p>
</li>
<li><p>When prompted for the <strong>Installation type</strong>, choose <strong>"Install Ubuntu alongside Windows Boot Manager"</strong> if you want to dual-boot, or <strong>"Something else"</strong> for a custom partition setup. If you created unallocated space earlier, select that space and create a new partition for Ubuntu (at minimum, one partition for the root directory <code>/</code> is needed; a separate partition for <code>/home</code> and a <strong>swap</strong> partition are optional but recommended).</p>
</li>
<li><p>Complete the remaining steps by setting your <strong>time zone</strong> and creating your <strong>user account</strong> and password.</p>
</li>
<li><p>Click <strong>Install Now</strong> and confirm the changes to the disk.</p>
</li>
</ul>
</li>
<li><p><strong>Restart:</strong> Once the installation is complete, select <strong>Restart Now</strong> and remove the USB drive when prompted. The laptop should now boot into the <strong>GRUB</strong> bootloader, allowing you to choose between Ubuntu and Windows.</p>
</li>
</ul>
<hr />
<h3 id="heading-post-installation-steps">Post-Installation Steps</h3>
<p>After successfully logging into your new Ubuntu desktop, you should perform a few initial setup tasks:</p>
<ul>
<li><p><strong>Update Your System:</strong> Open a terminal (Ctrl+Alt+T) and run the following commands to ensure all your packages are up-to-date:</p>
<p>  Bash</p>
<pre><code>  sudo apt update
  sudo apt full-upgrade -y
</code></pre></li>
<li><p><strong>Install Drivers (if needed):</strong> If you checked the "third-party software" box, most drivers should be installed. However, if you have an <strong>Nvidia GPU</strong> or encounter issues with Wi-Fi, go to <strong>Software &amp; Updates</strong> → <strong>Additional Drivers</strong> tab and check if any proprietary drivers are available and enabled.</p>
</li>
<li><p>Configure TLP Battery Charge Thresholds:</p>
<p>  TLP is essential for optimizing battery life and health on ThinkPads. To protect your battery, you can set a custom charge threshold to stop charging before it reaches 100%.</p>
<ol>
<li><p><strong>Install TLP and TLP-RDW:</strong></p>
<p> Bash</p>
<pre><code class="lang-plaintext"> sudo apt install tlp tlp-rdw
</code></pre>
</li>
<li><p>Edit the TLP Configuration File:</p>
<p> Open the configuration file /etc/tlp.conf using a text editor like vim:</p>
<p> Bash</p>
<pre><code class="lang-plaintext"> sudo vim /etc/tlp.conf
</code></pre>
</li>
<li><p>Set the Thresholds:</p>
<p> Scroll down to the Battery Care section and find the lines for START_CHARGE_THRESH_BAT0 and STOP_CHARGE_THRESH_BAT0. Uncomment these lines (remove the # symbol) and set them to your desired values:</p>
<ul>
<li><p><strong>Start Charging Below 60%:</strong> Set <code>START_CHARGE_THRESH_BAT0=60</code></p>
</li>
<li><p><strong>Stop Charging at 75%:</strong> Set <code>STOP_CHARGE_THRESH_BAT0=75</code></p>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<p>        Your edited lines should look like this (for a single battery):</p>
<pre><code class="lang-plaintext">        # Main / Internal battery (values in %)
        START_CHARGE_THRESH_BAT0=60
        STOP_CHARGE_THRESH_BAT0=75
</code></pre>
<p>        <em>Save and exit Vim with</em> <code>:wq</code></p>
<ol start="4">
<li><p>Apply and Verify the Settings:</p>
<p> Apply the new configuration and start TLP:</p>
<p> Bash</p>
<pre><code class="lang-plaintext"> sudo tlp start
</code></pre>
<p> You can verify the settings were applied successfully by checking the battery status:</p>
<p> Bash</p>
<pre><code class="lang-plaintext"> sudo tlp-stat -b
</code></pre>
<p> Look for the output showing the configured <code>charge_start_threshold</code> and <code>charge_stop_threshold</code> values.</p>
</li>
</ol>
<ul>
<li><strong>Final Check:</strong> Test closing the laptop lid to ensure the system correctly enters and resumes from a low-power state (<strong>suspend-to-RAM</strong> or <strong>S3 state</strong>).</li>
</ul>
<h3 id="heading-configuring-fingerprint-authentication">Configuring Fingerprint Authentication</h3>
<p>Configuring fingerprint authentication using <strong>fprintd</strong> and <strong>pam-auth-update</strong> on a Linux system (like Ubuntu) typically involves three main steps: installing the necessary packages, enrolling your fingerprint, and enabling the authentication module.</p>
<hr />
<h3 id="heading-1-install-required-packages">1. Install Required Packages</h3>
<p>First, ensure your system recognizes your fingerprint reader. You can often check this with the command <code>lsusb</code>. If your device is supported by <code>libfprint</code>, install the required packages:</p>
<p>Bash</p>
<pre><code class="lang-plaintext">sudo apt update
sudo apt install fprintd libpam-fprintd
</code></pre>
<ul>
<li><p><code>fprintd</code>: This is the fingerprint matching daemon that communicates with your reader and manages fingerprints.</p>
</li>
<li><p><code>libpam-fprintd</code>: This is the Pluggable Authentication Module (PAM) component that allows system services to use <code>fprintd</code> for authentication.</p>
</li>
</ul>
<hr />
<h3 id="heading-2-enroll-your-fingerprint">2. Enroll Your Fingerprint</h3>
<p>Once the packages are installed, you need to enroll your fingerprint so the system can recognize it.</p>
<p>Bash</p>
<pre><code class="lang-plaintext">fprintd-enroll
</code></pre>
<p>Follow the on-screen prompts, which will typically ask you to swipe or tap your finger on the sensor multiple times until the enrollment is complete. It will usually specify which finger it is enrolling (e.g., "right-index-finger").</p>
<p>You can also use your desktop environment's <strong>User Settings</strong> (e.g., in GNOME or KDE) to enroll your fingerprint, which often provides a graphical interface.</p>
<hr />
<h3 id="heading-3-enable-fingerprint-authentication-via-pam">3. Enable Fingerprint Authentication via PAM</h3>
<p>The <code>pam-auth-update</code> tool configures the system's authentication stack (PAM) to use the new fingerprint module.</p>
<p>Bash</p>
<pre><code class="lang-plaintext">sudo pam-auth-update
</code></pre>
<p>This command will open a text-based configuration menu.</p>
<ol>
<li><p>Use the <strong>Up/Down arrow keys</strong> to navigate to <strong>"Fingerprint authentication"</strong>.</p>
</li>
<li><p>Press the <strong>Spacebar</strong> to place an asterisk (<code>*</code>) next to it, indicating it is selected/enabled.</p>
</li>
<li><p>Use the <strong>Tab</strong> key to highlight <code>&lt;Ok&gt;</code>.</p>
</li>
<li><p>Press <strong>Enter</strong> to save the configuration and exit.</p>
</li>
</ol>
<p>This step integrates the <code>pam_</code><a target="_blank" href="http://fprintd.so"><code>fprintd.so</code></a> module into the common authentication stack (usually by modifying files like <code>/etc/pam.d/common-auth</code>), allowing it to be used for things like graphical login, screen unlock, and <code>sudo</code> elevation.</p>
<hr />
<h3 id="heading-4-test-the-configuration">4. Test the Configuration</h3>
<p>After completing these steps, you should be able to test the fingerprint authentication:</p>
<ul>
<li><p><strong>Sudo:</strong> Open a terminal and run a <code>sudo</code> command. You should be prompted to either scan your finger or enter your password.</p>
</li>
<li><p><strong>Login/Screen Unlock:</strong> Lock your screen or log out. The login prompt should offer a fingerprint option.</p>
</li>
</ul>
<p>Now you can proceed to install all the other packages and utilities you might need, like <code>homebrew</code> for package management.</p>
]]></content:encoded></item><item><title><![CDATA[Digitalizing checklists with Google Apps Script]]></title><description><![CDATA[The main issue with manual checklists is that they are a pain to use. The friction of having to print out a checklist every time that you need to use it is bound to eventually impact the frequency of its use as it provides an unsatisfactory user expe...]]></description><link>https://blog.vishnusujeesh.com/digitalizing-checklists-with-google-apps-script</link><guid isPermaLink="true">https://blog.vishnusujeesh.com/digitalizing-checklists-with-google-apps-script</guid><category><![CDATA[google apps script]]></category><category><![CDATA[rpa]]></category><category><![CDATA[gemini]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Vishnu S]]></dc:creator><pubDate>Thu, 21 Aug 2025 15:34:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/8gr6bObQLOI/upload/ccac44bbc1e06f89100adaab14fc4919.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755785899438/182c7e2f-00a6-4825-b7ef-544dda33693a.png" alt="Flow of the automation" class="image--center mx-auto" /></p>
<p>The main issue with manual checklists is that they are a pain to use. The friction of having to print out a checklist every time that you need to use it is bound to eventually impact the frequency of its use as it provides an unsatisfactory user experience.</p>
<p>If enough users feel that way, then the whole point of the checklist is moot. That is why we should automate these kinds of workflows as early as possible, which will improve compliance with the new workflow by reducing the friction involved in using checklists.</p>
<p>We will have 4 major stages in converting these checklists into forms:</p>
<ol>
<li><p>Convert the PDFs into a structured representation like JSON or CSV that we can directly use in code</p>
</li>
<li><p>Write some Apps Script (Google’s flavor of Javascript) using the Apps Script platform to programmatically create the form.</p>
</li>
<li><p>Write some Apps Script to manage some maintenance tasks, like keeping the Google Sheets backend a manageable size.</p>
</li>
<li><p>Write an ingest pipeline that uses Apps Script to scan a folder in Google Drive for new checklists and incorporate them to create a new version of the form (using LLMs for processing the PDFs into JSON).</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Memory mapped areas for Lucene]]></title><description><![CDATA[When deploying search infrastructure at scale, especially with technologies like Apache Lucene or Elasticsearch, performance tuning often goes beyond application-level optimizations. One of the most critical yet frequently overlooked system parameter...]]></description><link>https://blog.vishnusujeesh.com/memory-mapped-areas-for-lucene</link><guid isPermaLink="true">https://blog.vishnusujeesh.com/memory-mapped-areas-for-lucene</guid><category><![CDATA[search]]></category><category><![CDATA[sharding]]></category><category><![CDATA[lucene]]></category><dc:creator><![CDATA[Vishnu S]]></dc:creator><pubDate>Thu, 21 Aug 2025 02:15:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/RI7l-rspNpY/upload/a917c01108f817997c9921ff1abb91a8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When deploying search infrastructure at scale, especially with technologies like Apache Lucene or Elasticsearch, performance tuning often goes beyond application-level optimizations. One of the most critical yet frequently overlooked system parameters is <code>vm.max_map_count</code>. This Linux kernel setting governs how many virtual memory areas (VMAs) a single process can allocate, and it plays a pivotal role in how Lucene interacts with the operating system to manage large search indexes efficiently.</p>
<p>Lucene is a high-performance, full-text search library that underpins many modern search platforms. One of its key architectural choices is the use of memory-mapped files. Instead of reading index files into memory using traditional I/O operations, Lucene maps these files directly into the process’s virtual address space. This is done using the <code>mmap</code> system call, which allows Lucene to access file contents as if they were part of memory. The operating system handles the actual loading of data into RAM, doing so lazily—only when the application accesses specific portions of the file.</p>
<p>This design is elegant and efficient. It avoids the overhead of manual file reads and buffering, and it allows Lucene to work with very large indexes without consuming excessive heap memory. However, it introduces a dependency on the operating system’s ability to manage a large number of memory mappings. Each memory-mapped file segment creates a virtual memory area in the kernel, tracked by a data structure called <code>vm_area_struct</code>. These structures reside in RAM and consume kernel memory. The <code>vm.max_map_count</code> parameter sets a hard limit on how many of these mappings a single process can have.</p>
<p>By default, most Linux distributions set this value to 65,530. While this is sufficient for many applications, it quickly becomes a bottleneck for Lucene-based systems, especially when dealing with large indexes or numerous shards. To understand why, consider how Lucene structures its indexes. Each index is composed of multiple segments, and each segment consists of several files—such as term dictionaries, postings lists, and stored fields. Lucene typically maps these files in chunks of up to 1 GiB. A single index with dozens of segments can easily require hundreds or thousands of mappings. In Elasticsearch, where a single node might host thousands of shards, the number of required mappings can grow exponentially. If the process exceeds the <code>vm.max_map_count</code> limit, it will fail to create new mappings, leading to errors like <a target="_blank" href="http://java.io"><code>java.io</code></a><code>.IOException: Map failed</code>.</p>
<p>Increasing <code>vm.max_map_count</code> allows Lucene and Elasticsearch to scale more effectively by enabling more memory-mapped files per process. However, this change has implications for system memory usage. While increasing the mapping count does not directly increase the amount of RAM allocated to the kernel, it does allow more VMAs to be created, each of which consumes kernel memory. On average, each VMA uses about 128 bytes of RAM, plus additional overhead from the kernel’s memory allocator. For example, increasing the limit to 262,144 mappings—a common recommendation for Elasticsearch—would consume roughly 33.5 MB of kernel memory. On a system with 8 GB of RAM, this is a negligible amount, but it’s important to understand that this memory is taken from the same physical RAM pool used by user-space applications.</p>
<p>This leads to an important distinction: memory-mapped files do impact the total memory an application can consume, but they behave differently from traditional heap or stack allocations. When a file is memory-mapped, it is not immediately loaded into RAM. Instead, the operating system loads pages into memory on demand, as the application accesses them. This lazy loading mechanism means that the mapped file’s size does not directly translate to RAM usage. Only the accessed portions occupy physical memory, and these pages are managed by the OS’s page cache. This allows Lucene to work with very large indexes without exhausting heap memory or requiring manual memory management.</p>
<p>From the application’s perspective, memory-mapped files expand the process’s virtual memory footprint. On 64-bit systems, the address space is vast, so this is rarely a constraint. However, each mapping still requires kernel bookkeeping, and if many mappings are created, the kernel memory usage can grow. This indirectly reduces the amount of RAM available for other applications, especially if multiple memory-intensive processes are running concurrently.</p>
<p>In essence, memory-mapped files in Lucene act like a giant pointer table into the index files stored on disk. Instead of reading data into buffers, Lucene uses these mappings to navigate the index structure efficiently. The operating system handles the actual data loading, caching, and eviction, allowing Lucene to focus on search logic. This design is elegant and powerful, but it depends heavily on the system’s ability to support a large number of mappings.</p>
<p>For systems with 8 GB of RAM, setting <code>vm.max_map_count</code> to 262,144 is generally safe and recommended. This value provides a generous buffer for Lucene’s mapping needs without significantly impacting other applications. To apply this setting temporarily, one can use:</p>
<pre><code class="lang-bash">sudo sysctl -w vm.max_map_count=262144
</code></pre>
<p>To make it persistent across reboots:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"vm.max_map_count=262144"</span> | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
</code></pre>
<p>Ultimately, tuning <code>vm.max_map_count</code> is about enabling Lucene to scale efficiently while maintaining system stability. It is a small change with a big impact, especially in environments where search performance and reliability are paramount. By understanding how memory-mapped files work and how they interact with kernel memory, developers and system administrators can make informed decisions that optimize both application behavior and system resource usage.</p>
]]></content:encoded></item><item><title><![CDATA[Generative AI for learning by quizzing]]></title><description><![CDATA[Research has shown that being quizzed and actively engaging with the material is the best way to learn and retain the content that was learnt. But that doesn’t mean that all quizzes are created equal. There is a “Goldilocks” zone of sorts for the dif...]]></description><link>https://blog.vishnusujeesh.com/generative-ai-for-learning-by-quizzing</link><guid isPermaLink="true">https://blog.vishnusujeesh.com/generative-ai-for-learning-by-quizzing</guid><category><![CDATA[RAG ]]></category><category><![CDATA[education]]></category><category><![CDATA[Quiz]]></category><dc:creator><![CDATA[Vishnu S]]></dc:creator><pubDate>Wed, 20 Aug 2025 17:49:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755707321450/717d184d-197d-4c1c-9ae2-35d787871970.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Research has shown that being quizzed and actively engaging with the material is the best way to learn and retain the content that was learnt. But that doesn’t mean that all quizzes are created equal. There is a “Goldilocks” zone of sorts for the difficulty of the questions, where the questions are neither so easy that you can answer them without thinking, nor so hard that you get demotivated to even try answering them.</p>
<p>That sweet spot of difficulty keeps learners on their toes, coming back for more questions, which will ultimately help their understanding of the material.</p>
<h2 id="heading-generating-questions-of-different-difficulty-levels">Generating questions of different difficulty levels</h2>
<h3 id="heading-rag-for-getting-relevant-subject-matter">RAG for getting relevant subject matter</h3>
<h3 id="heading-using-graphs-to-track-subject-matter-coverage-and-concept-hierarchies">Using graphs to track subject matter coverage and concept hierarchies</h3>
<p>Generating questions from a text corpus involves a rich interplay of natural language processing (NLP), large language models (LLMs), and graph-based methods. This process can be tailored to produce various types of questions—factual, inferential, multiple-choice, and open-ended—depending on the intended use case, such as educational assessment, conversational AI, or content enrichment.</p>
<p>The process begins with <strong>preprocessing the corpus</strong>. Text is segmented into sentences and paragraphs, cleaned to remove noise, and analyzed to identify key entities and concepts using techniques like Named Entity Recognition (NER), coreference resolution, and keyword extraction. These steps help isolate the most informative parts of the text.</p>
<p>Next, <strong>candidate sentences</strong> are selected based on their semantic richness. Sentences containing definitions, causal relationships, or important facts are prioritized using scoring methods such as TF-IDF or contextual embeddings from models like BERT. These sentences serve as the foundation for question generation.</p>
<p>For <strong>factual and multiple-choice questions</strong>, several approaches can be used. Rule-based systems apply syntactic parsing to extract subject-verb-object structures and transform them into questions using predefined templates. For example, from “The Eiffel Tower is in Paris,” a rule might generate “Where is the Eiffel Tower located?” Neural models like T5 and BART, fine-tuned for question generation, can produce more flexible and abstractive questions from input passages. Additionally, models trained on datasets like SQuAD can extract question-answer pairs directly from text. Multiple-choice questions require an additional step: generating distractors. These can be selected using semantic similarity measures (e.g., WordNet or embedding distances) or entity type matching to ensure plausible alternatives.</p>
<p><strong>Open-ended question generation</strong>, however, demands a deeper semantic understanding. These questions aim to elicit reasoning, interpretation, or synthesis rather than recall. To generate them, one must first identify <strong>conceptual anchors</strong>—themes, causal links, or contrasting ideas—using topic modeling (e.g., LDA or BERTopic), semantic clustering, or graph-based representations. Concept graphs, where nodes represent ideas and edges represent relationships, help pinpoint areas suitable for deeper inquiry.</p>
<p>LLMs play a central role in crafting open-ended questions. By prompting models like GPT-4 or T5 with context-rich passages and directives such as “Generate a question that encourages critical thinking,” one can produce questions that invite analysis or reflection. Few-shot prompting, where examples of open-ended questions are provided, can further guide the model’s output. For instance, from a passage on climate change, the model might generate “How might local communities adapt to the long-term effects of rising sea levels?”—a question that encourages exploration of implications and strategies.</p>
<p>Graph-based methods can also be used to <strong>refine and diversify</strong> the question set. By constructing graphs of generated questions and analyzing their semantic similarity, one can cluster and prune redundant questions. Centrality measures help identify questions that touch on key ideas or bridge different concepts, ensuring broad and meaningful coverage.</p>
<p>Finally, generated questions are <strong>evaluated and ranked</strong> based on clarity, relevance, and depth. Readability metrics, semantic alignment with the source text, and classification based on Bloom’s taxonomy can be used to assess the quality of each question.</p>
<p>In practice, this integrated approach allows for scalable and context-aware question generation. For example, given a corpus of science articles, one could extract factual sentences, use T5 to generate “What” or “Why” questions, rank them by relevance, and optionally generate distractors for multiple-choice formats. For open-ended questions, concept graphs and LLMs can be used to produce prompts that encourage critical thinking and discussion.</p>
<h2 id="heading-identifying-the-difficulty-level-of-questions">Identifying the difficulty level of questions</h2>
<p>The original idea was to generate pairs for each question in the set with another question from the set, and then compare if question A was tougher than question B. If it was, then the tougher question would be assigned a point. Ultimately, the questions could be ranked by the number of points they would have accumulated. However, even for a small data set of 100 questions, that amounts to a comparison of</p>
<h3 id="heading-the-bradley-terry-model">The Bradley-Terry Model</h3>
<p>The Bradley-Terry model is one way to rank questions based on pairwise comparisons of their difficulty. Suppose you have a set of items (e.g., questions) \(Q_1, Q_2, Q_3,\dots,Q_n\)The Bradley-Terry model assigns each item a <strong>difficulty score</strong> \(\theta_i\) The probability that item \(i\) is judged more difficult than item \(j\) is:</p>
<p>$$P(i \text{ beats } j) = \frac{\theta_i}{\theta_i + \theta_j}$$</p><p>Where:</p>
<ul>
<li><p>\(\theta_i &gt; 0\) is the latent difficulty of item \(i\)</p>
</li>
<li><p>The model assumes that comparisons are <strong>independent</strong> and based only on the relative scores.</p>
</li>
</ul>
<h3 id="heading-the-thurstone-model">The Thurstone Model</h3>
<p>The <strong>Thurstone model</strong>, specifically the <strong>Thurstone Case V model</strong>, is another approach for ranking items based on <strong>pairwise comparisons</strong>, similar in spirit to the Bradley-Terry model but based on <strong>psychometric theory</strong> and <strong>Gaussian assumptions</strong>.</p>
<p>Thurstone's model assumes that each item (e.g., question) has a <strong>latent difficulty value</strong> drawn from a <strong>normal distribution</strong>. When comparing two items, the probability that one is judged more difficult than the other depends on the <strong>difference in their latent values</strong>.</p>
<p>Let:</p>
<ul>
<li><p>\(\delta_1\) be the latent difficulty of item \(i\)</p>
</li>
<li><p>\(\delta_j\) be the latent difficulty of item \(j\)</p>
</li>
</ul>
<p>Then the probability that item \(i\) is judged more difficult than item \(j\) is:</p>
<p>$$P(i&gt;j) = \frac{\Phi(\delta_i-\delta_j)}{\sqrt{2} \sigma}$$</p><p>Where:</p>
<ul>
<li><p>\(\Phi\) is the cumulative distribution function (CDF) of the standard normal distribution</p>
</li>
<li><p>\(\sigma\) is the standard deviation of the latent values (often assumed equal across items)</p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>Thurstone Model</strong></td><td><strong>Bradley-Terry Model</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Assumption</td><td>Normal distribution of latent traits</td><td>Logistic distribution of latent traits</td></tr>
<tr>
<td>Probability function</td><td>Uses normal CDF</td><td>Uses logistic function</td></tr>
<tr>
<td>Interpretation</td><td>Psychometric, used in scaling attitudes</td><td>Probabilistic, used in sports, ranking</td></tr>
<tr>
<td>Data requirement</td><td>Pairwise comparisons</td><td>Pairwise comparisons</td></tr>
<tr>
<td>Extensions</td><td>Can model variance across items</td><td>Easier to extend with covariates</td></tr>
</tbody>
</table>
</div><p>Given the above comparison, we will use the Bradley-Terry model, with a subset of manually scored questions to get the difficulty ratings for our entire set of generated questions.</p>
<h3 id="heading-ensuring-the-quality-of-scores">Ensuring the quality of scores</h3>
<p>Using a graph to track the distance and connectivity between questions is useful to ensure that the pairs that are manually scored are the right choices for the model to generate unbiased scores.</p>
<p>Each question is treated as a node in a graph, and every pairwise comparison between questions forms an edge connecting two nodes. This graph structure serves multiple purposes.</p>
<p>First, it helps track coverage by ensuring that each question is involved in a sufficient number of comparisons. Questions with fewer comparisons—those with a low degree—should be prioritized when selecting new pairs to compare.</p>
<p>Second, the graph must remain connected to avoid isolated questions or disconnected clusters. Algorithms such as Breadth-First Search (BFS) or Union-Find can be used to verify and maintain this connectivity.</p>
<p>Third, the selection of pairs should aim to be informative. Comparisons between questions that span distant regions of the graph—such as those differing significantly in difficulty—are more valuable than repeated comparisons between similar or adjacent questions.</p>
<p>Finally, the graph should be updated dynamically as more comparisons are collected. This allows for adaptive sampling, where the next pair to compare is chosen based on criteria such as uncertainty in estimated scores. Techniques like entropy or variance can guide this process to focus on the most informative comparisons.</p>
<h2 id="heading-understanding-the-competence-of-the-student">Understanding the competence of the student</h2>
<p>Once you have a list of questions sorted by difficulty, you can apply Item Response Theory (IRT) to estimate a student's competence level, commonly referred to as ability or proficiency in IRT terminology.</p>
<p>IRT is a family of statistical models that describe how the probability of a correct response to a question depends on both the student's latent ability and the characteristics of the question itself. In its simplest form—the one-parameter logistic model (1PL), also known as the Rasch model—the probability that a student with ability level θ answers a question correctly is given by:</p>
<p>$$P(\text{correct}) = \frac{1}{1 + e^{-(\theta - b_i)}}$$</p><p>Here, θ represents the student's ability, and bi<em>bi</em>​ is the difficulty of question i<em>i</em>. More complex models extend this by adding parameters: the two-parameter logistic model (2PL) introduces a discrimination factor ai<em>ai</em>​, which reflects how well a question distinguishes between students of different abilities, and the three-parameter logistic model (3PL) adds a guessing factor ci<em>ci</em>​, accounting for the chance of answering correctly by guessing.</p>
<p>To use IRT in practice, you begin by assigning difficulty scores to your questions, which can be derived from models like Bradley-Terry or empirical performance data. You then administer a subset of these questions to a student and record their responses. By fitting an IRT model to this data, you can estimate the student's ability level θ.</p>
<p>This estimated ability can be used to place the student on a proficiency scale, recommend the next most suitable questions, and make comparisons across students. IRT is particularly powerful because it simultaneously accounts for both question difficulty and student ability, enabling adaptive testing and yielding more accurate and individualized assessments than raw scores alone.</p>
]]></content:encoded></item><item><title><![CDATA[A RAG-powered article publishing pipeline]]></title><description><![CDATA[Motivation
For some time now, I have been maintaining a Zettelkasten to capture notes on topics I explore, whether through my work or personal learning. Over the years, this system has grown into a substantial network of interconnected notes. The str...]]></description><link>https://blog.vishnusujeesh.com/a-rag-powered-article-publishing-pipeline</link><guid isPermaLink="true">https://blog.vishnusujeesh.com/a-rag-powered-article-publishing-pipeline</guid><category><![CDATA[RAG ]]></category><category><![CDATA[writing]]></category><category><![CDATA[zettelkasten]]></category><dc:creator><![CDATA[Vishnu S]]></dc:creator><pubDate>Fri, 08 Aug 2025 15:07:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/aId-xYRTlEc/upload/c0dc5458b13f26f39b205eb45cedead6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-motivation">Motivation</h1>
<p>For some time now, I have been maintaining a Zettelkasten to capture notes on topics I explore, whether through my work or personal learning. Over the years, this system has grown into a substantial network of interconnected notes. The strength of the Zettelkasten lies in its ability to reveal relationships between ideas that I might never have discovered otherwise. This led me to wonder: what if I could distill these connections into cohesive articles and share the insights they reveal?</p>
<p>Because a Zettelkasten naturally organizes information as a graph, there is an opportunity to use a large language model (LLM) to enhance the writing process. The idea is to have the LLM analyze the graph structure, surface related content already in the system, and use that context to generate a structured article outline that highlights the key points to cover. From there, I can develop the full piece in my own writing style. This could significantly accelerate my publishing workflow, especially when the foundational research and projects are already in place.</p>
<p>Microsoft’s GraphRAG offers a similar concept. It applies graph-based retrieval to deliver more complete and contextually grounded answers from a text corpus by leveraging relationships between nodes and the entities they refer to. While my implementation will be far simpler than GraphRAG, my goal is to achieve a similar benefit: articles that are well-structured, comprehensive, and deeply connected across diverse topics.</p>
<h1 id="heading-solution-design">Solution Design</h1>
<h2 id="heading-requirements">Requirements</h2>
<ul>
<li><p><strong>Core content generation capabilities</strong></p>
<ul>
<li><p>Generate structured article skeletons from the Zettelkasten graph, identifying key sections and points based on the strongest links between notes</p>
</li>
<li><p>Continuously reassess and update suggested outlines as new content is ingested, ensuring the article ideas evolve with the knowledge base</p>
</li>
<li><p>Identify gaps in content completeness and flag areas that require additional research before articles can be developed</p>
</li>
</ul>
</li>
<li><p><strong>Content management and tracking</strong></p>
<ul>
<li><p>Maintain a comprehensive catalogue of all suggested articles, with clear indicators of their progress stages (idea, research in progress, writing, complete)</p>
</li>
<li><p>Track published articles and prevent duplicate or redundant suggestions</p>
</li>
<li><p>Preserve historical versions of article outlines to monitor the evolution of ideas over time</p>
</li>
</ul>
</li>
<li><p><strong>Graph-based analysis and prioritization</strong></p>
<ul>
<li><p>Model the Zettelkasten as a graph with nodes (notes) and edges (links) to uncover clusters of related information suitable for article development</p>
</li>
<li><p>Rank article suggestions by relevance, completeness, novelty, and expected value to readers</p>
</li>
<li><p>Detect emerging thematic trends and highlight cross-domain connections that present unique insights</p>
</li>
<li><p>Recommend priorities for article creation based on timeliness, novelty, and alignment with ongoing projects</p>
</li>
</ul>
</li>
<li><p><strong>System integration</strong></p>
<ul>
<li><p>Support ingestion from existing Zettelkasten formats such as Markdown files or Obsidian vaults, with bidirectional synchronization to keep the graph representation current</p>
</li>
<li><p>Integrate with a large language model to generate article outlines and summaries, allowing customization of prompts to maintain consistent writing style and tone</p>
</li>
</ul>
</li>
<li><p><strong>User interface and workflow management</strong></p>
<ul>
<li><p>Provide an intuitive UI dashboard where users can:</p>
<ul>
<li><p>View, search, and filter suggested article ideas by topic, status, or required research effort</p>
</li>
<li><p>Track progress of articles through various stages from ideation to completion</p>
</li>
<li><p>Review, approve, modify, or reject suggested article outlines before moving to writing</p>
</li>
<li><p>Access historical versions of outlines and compare their evolution</p>
</li>
</ul>
</li>
<li><p>Enable configurable outline detail levels within the UI, allowing users to control the granularity of article skeletons</p>
</li>
<li><p>Offer export options for article outlines in Markdown or other preferred formats, including links back to the original notes in the Zettelkasten</p>
</li>
</ul>
</li>
</ul>
<h1 id="heading-implementation">Implementation</h1>
<h2 id="heading-ingestion-pipeline">Ingestion Pipeline</h2>
<h2 id="heading-content-completeness-evaluation-pipeline">Content Completeness Evaluation Pipeline</h2>
<h2 id="heading-article-draft-generation-pipeline">Article Draft Generation Pipeline</h2>
<h2 id="heading-deployment">Deployment</h2>
<h1 id="heading-evaluation">Evaluation</h1>
]]></content:encoded></item><item><title><![CDATA[How Does Search Work?]]></title><description><![CDATA[Crawling/Ingestion
Indexing
The fundamental data structure involved in search is the inverted index.
Retrieval
Keyword Search
Vector Search
There are 2 common approaches for performing vector search in a vector store. Hierarchical Navigable Small Wor...]]></description><link>https://blog.vishnusujeesh.com/how-does-search-work</link><guid isPermaLink="true">https://blog.vishnusujeesh.com/how-does-search-work</guid><category><![CDATA[search]]></category><category><![CDATA[Search Engines]]></category><category><![CDATA[Retrieval]]></category><dc:creator><![CDATA[Vishnu S]]></dc:creator><pubDate>Fri, 08 Aug 2025 15:04:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/afW1hht0NSs/upload/27f5c0c6de6581c6e0a45b795195dc1c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-crawlingingestion">Crawling/Ingestion</h1>
<h1 id="heading-indexing">Indexing</h1>
<p>The fundamental data structure involved in search is the inverted index.</p>
<h1 id="heading-retrieval">Retrieval</h1>
<h2 id="heading-keyword-search">Keyword Search</h2>
<h2 id="heading-vector-search">Vector Search</h2>
<p>There are 2 common approaches for performing vector search in a vector store. Hierarchical Navigable Small World (HNSW) and Inverted File Index (IVF). These approaches allow for quick nearest neighbour search of vectors to identify those that are similar based on cosine similarity or</p>
<h2 id="heading-re-ranking-and-rank-fusion">Re-ranking and Rank Fusion</h2>
<h1 id="heading-personalisation">Personalisation</h1>
<h2 id="heading-collaborative-filtering">Collaborative Filtering</h2>
<p>Collaborative filtering works on the principle of finding what users like the target user prefer, and recommending those content</p>
<h2 id="heading-content-based-filtering">Content-based Filtering</h2>
<p>Content-based filtering</p>
<h1 id="heading-evaluation">Evaluation</h1>
<p>Search is generally ranked along recall, which is whether the right content is returned in the top K results, and relevance, which has to do with the position of the right content in the top K results.</p>
<p>Recall can be measured via a simple ratio:</p>
<p>$$\frac{\sum \text{Results returned}}{\sum \text{Queries made}} \times 100\%$$</p><p>Relevance can be measured with Mean Average Rank (MAR), which looks at the position of relevant results in the returned top K results and calculates a weighted average based on position. This penalises irrelevant results</p>
<p>A better metric is Normalized Discounted Cumulative Gain (NDCG), which accounts for both recall and relevance, and penalises relevant results that are not ranked the top more heavily.</p>
<h1 id="heading-references">References</h1>
<ol>
<li>How search works by Google</li>
</ol>
]]></content:encoded></item></channel></rss>