<?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://mienxiu.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mienxiu.com/" rel="alternate" type="text/html" /><updated>2026-03-21T09:44:54+00:00</updated><id>https://mienxiu.com/feed.xml</id><title type="html">Mienxiu</title><subtitle>An amazing website.</subtitle><author><name>Mienxiu</name></author><entry><title type="html">Virtualization</title><link href="https://mienxiu.com/virtualization/" rel="alternate" type="text/html" title="Virtualization" /><published>2026-03-21T00:00:00+00:00</published><updated>2026-03-21T00:00:00+00:00</updated><id>https://mienxiu.com/virtualization</id><content type="html" xml:base="https://mienxiu.com/virtualization/"><![CDATA[<p>In the 1960s, early computers like IBM mainframes typically ran only one operating system at a time.
This design had several drawbacks:</p>
<ul>
  <li>Different workloads could not easily share the same hardware.</li>
  <li>Hardware was often idle.</li>
  <li>Testing a new OS or switching OSes required another machine or a reboot.</li>
</ul>

<p>Virtualization was created to solve these problems.
With virtualization, multiple operating systems and applications can run at the same time on the same hardware.
Each one acts like it has its own machine, without forcing everyone to use the same operating system.</p>

<h2 id="virtual-machines-and-the-hypervisor">Virtual Machines and the Hypervisor</h2>

<p><img src="/assets/posts/41/virtual_machine.png" alt="Virtual Machine" /></p>

<h3 id="virtual-machines">Virtual Machines</h3>

<p>An early definition of a virtual machine is:</p>

<blockquote>
  <p>A virtual machine is taken to be an efficient, isolated duplicate of the real machine.
–<cite><a href="https://www.cs.cornell.edu/courses/cs6411/2018sp/papers/popek-goldberg.pdf">Popek and Goldber, 1974</a></cite></p>
</blockquote>

<p>A virtual machine (VM) is a software-defined machine that shares the physical CPU, memory, and devices.
A VM is also called a guest or a domain.</p>

<p>The operating system running inside a VM is called the guest operating system (guest OS).
From the guest operating system’s point of view, it is running on a real machine.
Therefore, the guest OS does not know (and ideally does not care) that it is sharing physical resources with other VMs.</p>

<h3 id="hypervisors">Hypervisors</h3>

<p>To run multiple VMs safely, you need a control layer that manages hardware and keeps VMs apart.
This layer is called virtual machine monitor (VMM), or hypervisor.</p>

<p class="notice--info">In most virtualization literature, VMM means virtual machine monitor (the hypervisor).
But, some products also use VMM to mean virtual machine manager (a management tool).</p>

<p>A hypervisor is software that sits between the physical hardware and the virtual machines.
It is responsible for mainly three goals:</p>
<ol>
  <li>Fidelity: The VM should look like a real machine. The guest OS should not need special changes just to boot and run.</li>
  <li>Performance: The hypervisor should not slow down normal execution.</li>
  <li>Safety: The hypervisor must control shared resources.</li>
</ol>

<p>In general, the hypervisor stays out of the way during ordinary execution.
It mainly steps in when a guest tries to do something that must be controlled, such as:</p>
<ul>
  <li>Accessing privileged CPU registers</li>
  <li>Changing page tables</li>
  <li>Performing certain I/O operations</li>
</ul>

<h2 id="benefits-and-drawbacks">Benefits and Drawbacks</h2>

<p>Virtualization is widely used because it has multiple benefits:</p>
<ul>
  <li>Consolidation: Run many servers on one machine to increase utilization.</li>
  <li>Cost reduction: Fewer machines means less power, cooling, and hardware cost.</li>
  <li>Portability: A VM is easy to copy, move, or back up.</li>
  <li>Fast provisioning: You can create new VMs quickly from templates.</li>
  <li>Maintenance and migration: You can move VMs during hardware maintenance or load changes.</li>
  <li>Fault isolation: A crash in one VM does not have to crash others.</li>
  <li>Testing and debugging: OS experiments are easier when you can snapshot and roll back.</li>
  <li>Legacy support: Old OSes can run without dedicating old hardware.</li>
</ul>

<p>However, virtualization also has drawbacks:</p>
<ul>
  <li>Performance overhead compared to running directly on hardware.</li>
  <li>Increased system complexity and management overhead.</li>
  <li>Limited direct resource sharing due to isolation.</li>
  <li>Host failure can impact many VMs unless redundancy is used.</li>
  <li>Licensing and virtualization platform costs may be significant.</li>
</ul>

<h2 id="types-of-vms">Types of VMs</h2>

<p>VMM implementations differ widely.
Common approaches are:</p>
<ul>
  <li>Type 1</li>
  <li>Type 2</li>
  <li>Paravirtualization</li>
  <li>Containerization</li>
</ul>

<p class="notice--info">In some systems like IBM LPARs and Oracle LDOMs, virtualization support is built directly into the hardware or firmware, rather than implemented as a traditional software hypervisor.
These systems are often called Type 0 hypervisors.</p>

<h3 id="type-1-bare-metal-hypervisor">Type 1: Bare-metal hypervisor</h3>

<p>In type 1, the hypervisor runs directly on the hardware, without a host operating system underneath.
It has full control over the machine and usually provides better performance and stronger isolation.
The VMs run on top of hypervisor.</p>

<p><img src="/assets/posts/41/type1_virtualization.png" alt="Type 1 virtualization" /></p>

<p>Type 1 is common in data centers and cloud platforms.</p>

<p>Examples of type 1 virtualization are:</p>
<ul>
  <li>VMware ESXi</li>
  <li>XenServer</li>
  <li>Linux KVM</li>
  <li>Microsoft Hyper-V</li>
</ul>

<h3 id="type-2-hosted-virtualization">Type 2: Hosted virtualization</h3>

<p>In type 2, a normal host OS runs on the hardware first.
The virtualization software runs on top of that OS.
The host OS then supplies device drivers and many I/O services.
And the hypervisor is hosted by the OS.</p>

<p><img src="/assets/posts/41/type2_virtualization.png" alt="Type 2 virtualization" /></p>

<p>Type 2 is common on personal computers and for development or testing.</p>

<p>Examples of type 2 virtualization are:</p>
<ul>
  <li>VMware Fusion and Workstation</li>
  <li>Oracle VirtualBox</li>
  <li>Parallels Desktop</li>
</ul>

<h3 id="paravirtualization">Paravirtualization</h3>

<p>Paravirtualization does not try to fully imitate real hardware.
Instead, the guest OS is modified to work in cooperation with the VMM to optimize performance.</p>

<p>In a paravirtualized system, the guest kernel replaces some privileged operations with hypercalls.
A hypercall is like a system call, but instead of entering the OS kernel, it enters the hypervisor.
Examples of privileged operations that are commonly done via hypercalls include:</p>
<ul>
  <li>Updating page tables</li>
  <li>Configuring interrupts</li>
  <li>Performing device I/O operations</li>
</ul>

<p>Paravirtualization can be faster because hypercalls avoid replying on traps or instruction rewriting.
The downside is that the guest OS must be modified and maintained.
It can also be less portable, because it depends on a particular hypervisor interface.</p>

<p>XenServer popularized this approach.</p>

<h3 id="containerization">Containerization</h3>

<p>Sometimes the goal is not to run a full guest OS, but to isolate and manage just applications.
If all applications run on the same OS, full virtualization may be unnecessary.
Instead, the system can use containerization (or application containment).</p>

<p><img src="/assets/posts/41/containerization.png" alt="Containerization" /></p>

<p>A container is an isolated environment for running applications while sharing the host OS kernel.
Unlike a virtual machine, a container does not include a guest OS and does not virtualize hardware.
Instead, it bundles only what the application needs to run, such as the application binaries, libraries, and configuration files.</p>

<p>Compared to other virtualization approaches, its main advantage is efficiency.
Containers are much lighter than VMs.
They use fewer resources and can start and stop quickly—more like processes than full virtual machines.</p>

<p>Containerization is popular in software development and deployment, particularly in cloud systems.
It is commonly managed with tools like Docker and Kubernetes.</p>

<h2 id="building-blocks">Building Blocks</h2>

<p>Whether virtualization is possible basically depends a lot on the CPU’s features.</p>

<p>To virtualize a system, the hypervisor must provide an exact duplicate of the underlying machine.
This is especially difficult on older systems that support only two CPU modes: user mode and kernel mode.</p>

<p>For example, classic x86 defines four privilege levels, called rings:</p>
<ul>
  <li>Ring 0: most privileged, used by the OS kernel</li>
  <li>(Rings 1 and 2 exist, but they are rarely used.)</li>
  <li>Ring 3: least privileged, used by user programs</li>
</ul>

<p>On CPUs with only these privilege rings, it’s hard to fit a hypervisor in the right place.
The hypervisor needs to run with the highest privileges so it can keep control.
But a normal OS is written assuming it also runs with the highest privileges (ring 0).
If you push the guest OS down to a less-privileged ring (like ring 3), some operations won’t behave the same way, and the hypervisor may not be able to catch and handle them correctly.</p>

<!-- hardware assistance -->
<p>To fix this limitation, modern x86 CPUs add hardware support for virtualization (Intel VT-x and AMD-V).
They introduce two new modes:</p>
<ul>
  <li>Root mode: used by the hypervisor</li>
  <li>Non-root mode: used by guest operating systems</li>
</ul>

<p>With this setup, the guest kernel can still run in ring 0, but it runs in non-root mode.
In other words, it looks like ring 0 to the guest, but the CPU still keeps it on a leash.</p>

<p>When the guest executes certain sensitive instructions, the CPU automatically traps into the hypervisor.
Control switches from non-root to root mode, the hypervisor handles the request, and then the CPU returns to the guest.</p>

<p>On top of this hardware foundation, VMMs use techniques such as trap-and-emulate and binary translation to handle privileged behavior efficiently.</p>

<p>Another important concept is the virtual CPU (vCPU).
A vCPU does not execute instructions by itself.
Instead, it stores what the guest thinks the CPU state is, including things like:</p>
<ul>
  <li>Register values</li>
  <li>Program counter</li>
  <li>CPU flags</li>
</ul>

<p>For each VM, the VMM keeps one or more vCPU data structures.
When the VMM schedules a guest to run on a real CPU core, it does a simple save/restore cycle:</p>
<ol>
  <li>Load: copy the saved CPU state from the vCPU into the real CPU (registers, instruction pointer, flags, etc.).</li>
  <li>Run: let the guest execute for a time slice, until it traps, blocks, or gets preempted.</li>
  <li>Save: copy the updated CPU state back into the vCPU so the guest can resume later from the same point.</li>
</ol>

<h3 id="trap-and-emulate">Trap-and-Emulate</h3>

<p>On a typical CPU with only two modes (user and kernel) without extra hardware support, the guest kernel can execute only in user mode.
Because the VM must imitate the physical machine, the VMM provides virtual modes:</p>
<ul>
  <li>Virtual user mode</li>
  <li>Virtual kernel mode</li>
</ul>

<p>Both virtual modes still execute in physical user mode.</p>

<p>For example, the following actions cause a transfer from user mode to kernel mode on a real machine:</p>
<ul>
  <li>a system call</li>
  <li>an interrupt</li>
  <li>a privileged instruction</li>
</ul>

<p>When the same actions occur inside the guest kernel, the VM must also transfer from virtual user mode to virtual kernel mode.
This is done using trap-and-emulate:</p>
<ol>
  <li>The guest kernel executes a privileged instruction.</li>
  <li>Because the guest kernel is in physical user mode, it causes a trap to the VMM (in the real machine).</li>
  <li>The VMM executes (or emulates) the privileged instruction of the guest kernel.</li>
  <li>It then returns control to the virtual machine.</li>
</ol>

<p><img src="/assets/posts/41/trap_and_emulate.png" alt="Trap-and-emulate" /></p>

<p>The downside is overhead.
Nonprivileged instructions run at near native speed, but privileged instructions trap to the VMM and must be emulated.
Sharing the CPU across multiple VMs can also make performance unpredictable.</p>

<p>However, as hardware has improved, trap-and-emulate has gotten faster, and the system needs it less often.
For example, modern CPUs add extra modes for virtualization beyond the typical user and kernel modes.
This lets the CPU handle more of the work directly at the hardware level, so the VMM has less to do.</p>

<h3 id="binary-translation">Binary Translation</h3>

<p>Trap-and-emulate works only on CPUs where all sensitive instructions trap when executed in unprivileged mode.
If some sensitive instructions can run without trapping, trap-and-emulate alone is not sufficient.</p>

<p>Binary translation fixes this by rewriting guest kernel code on the fly.
Assume there are some sensitive instructions that do not automatically trap.
Call them special instructions. Then:</p>
<ul>
  <li>If the guest vCPU is in virtual user mode, the guest can run its instructions natively on a physical CPU.</li>
  <li>If the guest vCPU is in virtual kernel mode, the VMM inspects every instruction by the guest:
    <ul>
      <li>Non-special instructions run natively.</li>
      <li>Special instructions are translated (rewrited) into safe sequences that produce the same effect</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/posts/41/binary_translation.png" alt="Binary translation" /></p>

<p>Even though most instructions run natively, special instructions still add overhead because they must be translated.
To reduce this cost, the VMM saves translated code in a translation cache.
When the same code runs again, the VMM can reuse the cached translation instead of translating it again.</p>

<h2 id="core-operating-system-components-in-virtualization">Core Operating-System Components in Virtualization</h2>

<h3 id="cpu-scheduling">CPU Scheduling</h3>

<p>The effect of virtualization on scheduling varies a lot.
This is because virtualization can make even a single-CPU system look like a multiprocessor sytem.
For example, even if a host has six physical CPUs, the VMM can present twelve vCPUs across its guests.
This implies:</p>
<ul>
  <li>If there are enough physical CPUs to satisfy each guest’s requested vCPUs, the VMM can treat the allocation as effectively dedicated. It can schedule only a given guest’s threads on that guest’s assigned CPUs. In this situation, the guests behave much like native operating systems running on native hardware.</li>
  <li>If the total number of requested vCPUs exceeds the available physical CPUs (overcommitment), the VMM relies on a standard scheduling algorithm so that guests continue to perceive adequate CPU availability. For instance, with six physical CPUs and twelve vCPUs total, the VMM can distribute CPU time proportionally, so each guest effectively receives about half of the host’s CPU capacity.</li>
</ul>

<p>Under CPU overcommitment, some performance degradation is expected because vCPUs must time-share physical CPUs and may spend time waiting to be scheduled.</p>

<h3 id="memory-management">Memory Management</h3>

<p>VMMs typically overcommit memory, so the total memory allocated to guests can exceed the amount of physical memory available.
This increases memory pressure because there are more guest operating systems and their applications, plus the VMM itself.
Therefore, the VMM must preserve the illusion that the guest has that amount of memory, even when physical memory is smaller.
To do that:</p>
<ol>
  <li>The VMM first evaluates each guest’s maximum memory size so that the guest OSes continue to believe they have what they asked for.</li>
  <li>The VMM then computes how much memory to actually give it right now based on current load and overcommitment.</li>
  <li>The VMM then reclaims memory from the guests when the host system is running low on memory.</li>
</ol>

<p>For reclaiming memory, the most commonly used technique is <a href="https://www.vmware.com/docs/perf-vsphere-memory_management">memory ballooning, developed by VMware</a>.
Here, a balloon driver is loaded into the guest as a pseudo-device driver.</p>

<p class="notice--info">A pseudo–device driver uses device-driver interfaces, appearing to the kernel to be a device driver, but does not actually control a device. Rather, it is an easy way to add kernel-mode code without directly modifying the kernel.</p>

<p>The role of a balloon driver is to make the guests aware of the low memory status of the host.
It does this by polling the hypervisor and getting a target balloon size.
Without this driver, the VM cannot detect host memory pressure because the VM is isolated from the host.
When the host is low on memory:</p>
<ol>
  <li>The hypervisor sets a target balloon size based on how much memory it wants to reclaim.</li>
  <li>The balloon driver “inflates” the balloon inside the guest. It allocates and pins that amount of guest physical memory to the balloon. This makes the guest OS treat those pages as in use.</li>
  <li>The balloon driver reports the pinned page frame numbers to the hypervisor. The hypervisor can then reclaim the host physical pages backing those frames.</li>
</ol>

<p>When the host has enough memory again, the hypervisor “deflates” the balloon, and the balloon driver releases pages back to the guest.</p>

<p><img src="/assets/posts/41/memory_ballooning.png" alt="Memory ballooning" /></p>

<p>The key benefit of ballooning is that it lets the guest operating system decide which pages to page out.
The hypervisor does not need to be involved in that decision.</p>

<h3 id="storage-management">Storage Management</h3>

<p>In a virtualized system, storage must be handled differently from a native operating system.</p>

<p>The exact approach depends on the type of hypervisor:</p>
<ul>
  <li>Type 1 hypervisors store each guest’s root disk and configuration data as one or more files in file systems managed by the VMM.</li>
  <li>Type 2 hypervisors store the same information as files in the host operating system’s file systems.</li>
</ul>

<p>In practice, a guest’s entire root disk is packaged into a single disk image file managed by the VMM. Although this design can introduce performance overhead, it is highly practical. It makes copying, backing up, and moving virtual machines straightforward.
For example, if an administrator wants to create a duplicate guest for testing, they can simply copy the disk image file and register the new copy with the VMM.
Similarly, moving a virtual machine between systems running the same VMM is easy: shut down the guest, copy the disk image to the new system, and start the guest there.</p>

<p>Many VMMs also support converting systems between physical and virtual systems:</p>
<ul>
  <li>Physical-to-virtual (P-to-V) conversion: converts a running physical machine into a virtual machine. This is done by copying its disk contents into virtual disk image files.</li>
  <li>Virtual-to-physical (V-to-P) conversion: converts a virtual machine into a physical system. This is useful for debugging a problem where the virtualization layer itself may be contributing to the issue.</li>
</ul>

<h3 id="io-management">I/O Management</h3>

<p>I/O virtualization is easier than CPU or memory virtualization because operating systems already handle many kinds of devices through device drivers.
A hypervisor uses this by providing specific virtualized devices to guests.</p>

<p>Common ways a VMM handles I/O:</p>
<ul>
  <li>Pass-through (dedicated device): one real device is directly dedicated to one guest. This can be close to native speed, but other guests cannot use that device.</li>
  <li>Shared device: many guests share one real device. The VMM must sit in the middle for every I/O to keep guests isolated and send data to the right place.</li>
  <li>Split-device: the guest uses a simple driver that talks to the VMM, and the VMM talks to the real hardware. This is often faster and easier to manage than fully emulating real devices.</li>
</ul>

<p>Direct access can be fast in type 0 hypervisor, but type 1 and type 2 hypervisors usually need hardware support.
Without it, I/O slows down because the hypervisor must handle many extra events, especially interrupts.</p>

<p>For networking, each guest must have at least one IP address to communicate with other systems.
The VMM then acts like a virtual switch to route the packets to the right guest.
Guests can connect using:</p>
<ul>
  <li>Bridging: the guest appears directly on the external network with its own IP.</li>
  <li>NAT: the guest uses an internal IP, and the VMM translates and forwards traffic.</li>
</ul>

<p>VMMs often also provide basic firewall rules to control traffic between guests and to the outside network.</p>]]></content><author><name>Mienxiu</name></author><category term="docker" /><category term="os" /><summary type="html"><![CDATA[In the 1960s, early computers like IBM mainframes typically ran only one operating system at a time. This design had several drawbacks: Different workloads could not easily share the same hardware. Hardware was often idle. Testing a new OS or switching OSes required another machine or a reboot.]]></summary></entry><entry><title type="html">I/O Management</title><link href="https://mienxiu.com/io-management/" rel="alternate" type="text/html" title="I/O Management" /><published>2026-03-07T00:00:00+00:00</published><updated>2026-03-07T00:00:00+00:00</updated><id>https://mienxiu.com/io-management</id><content type="html" xml:base="https://mienxiu.com/io-management/"><![CDATA[<p>A core responsibility of an operating system is managing the computer’s hardware.
This responsibility covers not only the CPU and memory, but also input/output (I/O) devices.</p>

<h2 id="io-devices">I/O Devices</h2>

<p><img src="/assets/posts/40/io_devices.png" alt="I/O devices" /></p>

<p>I/O devices cover a wide range of hardware.
Common examples are storage devices such as SSDs, network interface cards (NICs), and devices used for human interaction, including displays, keyboards, speakers, and microphones.</p>

<p>Since I/O devices behave very differently from one another, the operating system needs to hide these differences from applications.
To make this separation possible, operating systems rely on two key ideas:</p>
<ul>
  <li>Standard interfaces</li>
  <li>Device drivers</li>
</ul>

<h3 id="standard-interfaces">Standard Interfaces</h3>

<p>Because I/O devices are very different, applications cannot communicate with them directly.
Instead, the operating system defines standard interfaces for broad classes of devices, such as storage devices, network devices, or display devices.
These interfaces define what operations a device supports (for example, read, write, send, receive) and how those operations are requested.
Applications interact only with these interfaces, not with the hardware itself.
This allows the same application code to work with many different devices of the same type.</p>

<p>At a lower level, devices must physically connect to the CPU and memory.
In modern systems, this is done using device interconnects, most commonly PCI Express (PCIe).</p>

<p><img src="/assets/posts/40/pcie_bus.png" alt="PCIe bus" /></p>

<p>PCIe provides a high-speed, point-to-point connection between devices and the CPU.
Unlike the older shared PCI bus, PCIe uses dedicated links (lanes) for each device.
Through PCIe, devices can exchange data with the CPU, memory, and the rest of the system in a standardized way.</p>

<p>Other types of buses are also used even in a PCIe system, depending on the device.
For example:</p>

<ul>
  <li>SATA controllers for certain storage devices</li>
  <li>Simpler peripheral buses (such as USB) for devices like keyboards and mice</li>
</ul>

<p>The choice of bus affects performance, how devices are discovered at boot time, and how the operating system manages and schedules access to hardware.</p>

<h3 id="device-drivers">Device Drivers</h3>

<p>A device driver is software that knows how to control a specific hardware device.
Each driver contains all the device-specific code needed to configure the device, send commands to it, move data in and out, and respond to events such as interrupts or errors.</p>

<p><img src="/assets/posts/40/device_drivers.png" alt="Device drivers" /></p>

<p>When an application makes an I/O request through a standard interface, the operating system forwards that request to the appropriate driver.
This design allows the operating system and applications to remain unchanged even when hardware is replaced, as long as the correct driver is available.
From the operating system’s perspective, supporting a new device usually means adding a new driver, not rewriting the rest of the system.</p>

<p>Typically, the operating system defines a standard device driver framework, and hardware manufacturers provide drivers that fit into this framework.
This is why users often need to install drivers when adding new hardware, such as printers.</p>

<p>At the hardware level, a device controller contains built-in logic and firmware that directly run the physical device.
It receives commands from the device driver, performs the requested actions, transfers data between the device and memory, and signals completion or errors via interrupts.</p>

<h2 id="devices-as-files">Devices as Files</h2>

<p>Many operating systems represent devices as files.
This means applications can interact with devices using familiar operations such as read and write.</p>

<p>On Unix-like systems, devices appear as special files under the <code class="language-plaintext highlighter-rouge">/dev</code> directory.
For example, running <code class="language-plaintext highlighter-rouge">echo "hello, world" &gt; /dev/lp0</code> sends the text <code class="language-plaintext highlighter-rouge">hello, world</code> to the first line printer (<code class="language-plaintext highlighter-rouge">lp0</code>).
Although this looks like a normal file write, the kernel intercepts the operation and translates it into the appropriate device-specific commands handled by the printer driver.</p>

<p class="notice--info">Unix-like operating systems provide a virtual device known as the null device, exposed as <code class="language-plaintext highlighter-rouge">/dev/null</code>.
Any data written to this device is discarded by the kernel.</p>

<h2 id="typical-device-access-flow">Typical Device Access Flow</h2>

<p>Typically, user programs cannot access hardware devices directly.
Instead, device access goes through the operating system:</p>
<ol>
  <li>When a process needs to use a device—such as sending a network packet or reading a disk file—it makes a system call. This transfers control from the process to the kernel.</li>
  <li>Inside the kernel, the operating system runs the relevant internal code:
    <ul>
      <li>Networking requests go through the TCP/IP stack.</li>
      <li>File requests go through the file system to locate disk blocks.</li>
      <li>The kernel converts the high-level request into a low-level device operation.</li>
    </ul>
  </li>
  <li>The kernel then calls the device driver, which understands the hardware details. The driver:
    <ul>
      <li>Translates requests into device-specific commands</li>
      <li>Configures the device by writing to its registers</li>
      <li>Manages pending requests safely</li>
    </ul>
  </li>
  <li>The driver uses mechanisms like programmed I/O or DMA to transfer data. Once configured, the device performs the operation, such as transmitting data or reading from disk.</li>
  <li>When the operation finishes, the result travels back through the driver and kernel to the user process.</li>
</ol>

<h3 id="os-bypass">OS Bypass</h3>

<p>Some devices support a different model in which applications can access hardware without going through the kernel on every operation.
This is called operating system bypass (OS bypass).</p>

<p>In this model, the operating system is involved only during setup.
It maps device memory and registers into the process’s address space and establishes permissions.
After setup, the data path goes directly between the user process and the device.</p>

<p>Because the kernel is no longer in the data path, applications use a user-level driver, typically a library provided by the device vendor, to issue device commands.</p>

<h2 id="blocking-nonblocking-and-asynchronous-io">Blocking, Nonblocking, and Asynchronous I/O</h2>

<p>When a process issues an I/O request, the device will eventually return some response.
What happens to the calling thread depends on whether the I/O is synchronous or asynchronous.</p>

<p><img src="/assets/posts/40/sync_and_async_io.png" alt="Synchronous and asynchronous I/O" /></p>

<p>An important design choice in the system-call interface is how I/O operations interact with a running program.
The key question is whether the calling thread should wait for I/O to finish or continue running while the I/O happens in the background.</p>

<h3 id="blocking-synchronous-io">Blocking (synchronous) I/O</h3>

<p>In blocking I/O, a system call pauses the calling thread until the I/O operation completes.</p>

<p>Here is what happens step by step:</p>
<ol>
  <li>The application issues a blocking system call (for example, <code class="language-plaintext highlighter-rouge">read()</code>).</li>
  <li>The operating system suspends the calling thread.</li>
  <li>The thread is moved from the run queue to a wait queue.</li>
  <li>When the I/O finishes, the thread is moved back to the run queue.</li>
  <li>When the thread runs again, it receives the return values from the system call.</li>
</ol>

<p>Although I/O devices work asynchronously at the hardware level and may take an unpredictable amount of time, operating systems still offer blocking calls.
The reason is simple: blocking code is easier to write and reason about.</p>

<h3 id="nonblocking-io">Nonblocking I/O</h3>

<p>Some operating systems provide nonblocking I/O system calls.
These calls do not suspend the thread for a long time.</p>

<p>A nonblocking call:</p>
<ul>
  <li>Returns immediately.</li>
  <li>Reports how many bytes were transferred.</li>
  <li>May return fewer bytes than requested, or even zero, if no data is available yet.</li>
</ul>

<p>The thread can then decide whether to try again later or do something else.
This avoids wasting CPU time and allows computation to overlap with I/O.</p>

<p>One way to handle this problem is multithreading.
An application can create multiple threads:</p>
<ul>
  <li>Some threads perform blocking I/O.</li>
  <li>Other threads continue running useful work.</li>
</ul>

<p>This approach works well but increases complexity due to synchronization and shared data management.</p>

<h3 id="asynchronous-io">Asynchronous I/O</h3>

<p>Another option is asynchronous I/O, which goes one step further.</p>

<p>With an asynchronous system call:</p>
<ul>
  <li>The call returns immediately.</li>
  <li>The operating system guarantees that the entire I/O request will be completed later.</li>
  <li>The thread continues executing without waiting.</li>
</ul>

<p>When the I/O finishes, the operating system notifies the application using one of several methods:</p>
<ul>
  <li>Updating a variable in the process’s address space</li>
  <li>Sending a signal or software interrupt</li>
  <li>Invoking a callback function</li>
</ul>

<p>Asynchronous operations are common inside modern operating systems, even when applications are not directly aware of them.</p>

<h2 id="block-device-stack">Block Device Stack</h2>

<p>The block device stack describes how an I/O request follows from an application down to the physical storage device.</p>

<p><img src="/assets/posts/40/block_device_stack.png" alt="Block device stack" /></p>

<p>At the top of the stack are applications.=
As explained eariler, applications do not work with disks directly.
Instead, they operate on files, which are logical objects provided by the operating system.
Programs use system calls such as <code class="language-plaintext highlighter-rouge">open</code>, <code class="language-plaintext highlighter-rouge">read</code>, and <code class="language-plaintext highlighter-rouge">write</code> to access files without knowing where or how the data is stored.</p>

<p>These system calls are handled by the file system.
The file system manages files and directories, checks permissions, and translates file-level operations into accesses to disk blocks.
Before accessing the disk, the operating system usually checks the page cache.
If the requested data is already in memory, it can be returned immediately.
If not, the request continues down the stack.</p>

<p>Below the file system and page cache is the generic block layer.
This layer provides a common interface for different block devices such as HDDs, SSDs, and USB drives.
Even though these devices work differently internally, the block layer hides those differences and schedules read and write requests.</p>

<p>At the bottom of the stack are the device drivers.
Drivers communicate directly with the hardware and handle device-specific commands, errors, and interrupts.</p>

<p>A typical storage request flows down the stack as follows:</p>

<ol>
  <li>Application calls read or write</li>
  <li>Operating system enters the file system. If data is present in page cache, return immediately.</li>
  <li>File system maps the request to disk blocks</li>
  <li>Block layer schedules the request</li>
  <li>Device driver accesses the hardware</li>
</ol>

<h2 id="virtual-file-system">Virtual File System</h2>

<p>Modern operating systems support many different kinds of storage.
Files may live on different disks, use different file system formats, or even be stored on remote drives and accessed over a network.
Despite this complexity, applications should not need to know where a file is stored or how it is managed internally.</p>

<p>To solve this problem, operating systems such as Linux use a Virtual File System (VFS) layer.
The VFS sits between applications and the actual file systems.
Applications interact with files using a standard interface, such as the POSIX API, and the VFS takes care of translating these requests to the appropriate underlying file system.</p>

<p><img src="/assets/posts/40/virtual_file_system.png" alt="Virtual file system" /></p>

<p>This separation makes it easy to add new file systems or switch storage devices without modifying application code.</p>

<p>To support many file systems in a uniform way, the VFS defines a small set of common abstractions.</p>

<h3 id="files-and-file-descriptors">Files and File Descriptors</h3>

<p>A file is the basic object that the VFS operates on.
When a process opens a file, the operating system creates a file descriptor, which is a small integer used to identify that open file.</p>

<p>All file operations—such as reading, writing, locking, or closing—are performed using the file descriptor.
The file descriptor exists only while the file is open and is specific to the process that opened it.</p>

<h3 id="inodes">Inodes</h3>

<p><img src="/assets/posts/40/inode.png" alt="Inode" /></p>

<p>An inode (short for index node) is the core data structure a file system uses to manage files.
Internally, each file is identified by an inode number, not by its file name.
The file name is just a human-friendly label used to locate the corresponding inode.</p>

<p>An inode contains a list of disk block numbers that store the file’s actual data.
In this sense, the inode acts as an index: it does not store the file contents itself, but instead stores pointers to the blocks that hold the contents.
Reading those blocks in the correct order reconstructs the file.</p>

<p>When more data is written to a file and extra space is needed, the file system:</p>
<ol>
  <li>Allocates a free disk block</li>
  <li>Adds its block number to the inode</li>
  <li>Updates the inode on disk</li>
</ol>

<p>The inode always reflects the current layout of the file.</p>

<p>Besides block pointers, an inode also stores metadata such as:</p>
<ul>
  <li>Access permissions</li>
  <li>Ownership</li>
  <li>Locking or status information</li>
</ul>

<p>This metadata is used to enforce access rules and manage concurrent use.</p>

<p>This inode-based design supports efficient access patterns.
Sequential access simply follows the list of blocks in order, while random access directly computes the required block from the file offset.</p>

<p>The main drawback of this simple design is that it limits how large a file can be.
Consider this example:</p>
<ul>
  <li>Inode size: 128 bytes</li>
  <li>Each block pointer: 4 bytes</li>
  <li>Maximum number of block pointers: 128 / 4 = 32</li>
  <li>Block size: 1 KB</li>
</ul>

<p>With only direct block pointers and no space for metadata, the largest file that can be addressed is 32 blocks * 1 KB = 32 KB.</p>

<p>A common way to overcome the file-size limitation of simple inodes is to use indirect pointers.</p>

<p><img src="/assets/posts/40/inodes_indirect_pointers.png" alt="Inodes with indirect pointers" /></p>

<p>An inode still begins with metadata, followed by pointers to data blocks.
The first pointers are direct pointers, which point straight to blocks containing file data.</p>

<p>An indirect pointer does not point to data directly.
Instead, it points to a block that contains only block addresses.
A 1 KB block can store 256 such addresses, allowing a single indirect pointer to reference 256 KB of file data.
For larger files, double or even triple indirect pointers are used.
A double indirect pointer, for example, points to a block of pointers to other pointer blocks, which then point to data blocks.
With 256 pointers per block, double indirect addressing supports up to 64 MB of data.</p>

<p>The trade-off of using indirect pointers is that it increases file size but also increases access cost.</p>

<h3 id="directories-and-dentires">Directories and Dentires</h3>

<p>Files are organized into directories.
From the VFS perspective, a directory is simply a special kind of file.
The difference is that the contents of a directory describe file names and the inodes they refer to.</p>

<p>Dentires are cached in memory in what is known as the dentry cache.
This cache avoids repeated disk accesses when the same directories are accessed multiple times.
Dentires are not stored on disk.
They are temporary objects maintained only in memory.</p>

<h3 id="superblock">Superblock</h3>

<p>Every file system has a superblock, which describes how the file system is laid out on disk.
The superblock contains information such as:</p>
<ul>
  <li>Where inodes are stored</li>
  <li>Where data blocks are located</li>
  <li>How free space is managed</li>
</ul>

<p>The superblock acts as a map that allows the operating system to interpret the on-disk data structures correctly.</p>

<p>Many VFS structures, such as file descriptors and dentries, exist only in memory.
Others, including file data, inodes, and superblocks, must be stored persistently on disk.</p>

<p>A widely used modern file system in Linux is ext4 (4th extended file system), which is the successor to ext2 and ext3.</p>

<h3 id="disk-access-optimizations">Disk Access Optimizations</h3>

<p>Disk operations are much slower than memory operations, so file systems use several techniques to reduce disk access and improve I/O performance.
These techniques focus on keeping data in memory, reducing disk head movement, and avoiding unnecessary random accesses.</p>

<p>Key techniques include:</p>
<ul>
  <li>buffer caching: Keep recently accessed file data in memory so most reads and writes avoid disk access; changes are flushed to disk periodically.</li>
  <li>I/O scheduling: Reorder disk requests to minimize disk head movement and favor sequential access over random access.</li>
  <li>prefetching: Load nearby file blocks into memory in advance, increasing cache hits and reducing future access latency.</li>
  <li>journaling: Record updates in a sequential log before writing them to their final disk locations, improving reliability while limiting random disk writes.</li>
</ul>]]></content><author><name>Mienxiu</name></author><category term="os" /><summary type="html"><![CDATA[A core responsibility of an operating system is managing the computer’s hardware. This responsibility covers not only the CPU and memory, but also input/output (I/O) devices.]]></summary></entry><entry><title type="html">Deploying Self-managed Elasticsearch 8 Cluster with Docker on AWS</title><link href="https://mienxiu.com/deploying-self-managed-elasticsearch-8-cluster-with-docker-on-aws/" rel="alternate" type="text/html" title="Deploying Self-managed Elasticsearch 8 Cluster with Docker on AWS" /><published>2026-01-03T00:00:00+00:00</published><updated>2026-01-03T00:00:00+00:00</updated><id>https://mienxiu.com/deploying-self-managed-elasticsearch-8-cluster-with-docker-on-aws</id><content type="html" xml:base="https://mienxiu.com/deploying-self-managed-elasticsearch-8-cluster-with-docker-on-aws/"><![CDATA[<p>Deploying a self-managed Elasticsearch cluster for production is a bit of pain.
It’s not just about understanding the core components.
You also have to manually configure availability and security.
To do that, you end up jumping between many pages in the official documentation to piece everything together.</p>

<p>This post is a comprehensive walkthrough of deploying a self-managed Elasticsearch cluster using one of <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/install-elasticsearch.html#elasticsearch-deployment-options">the supported installation methods</a>: Docker.
It also aims to follow production best practices and recommendations.</p>

<h2 id="planning-elasticsearch-cluster">Planning Elasticsearch Cluster</h2>

<p>By the end of this tutorial, you’ll have the following setup in place:</p>

<p><img src="/assets/posts/39/production_cluster_and_monitoring_node.png" alt="Production cluster and monitoring node" /></p>

<p>We will deploy:</p>

<ul>
  <li>A production cluster with:
    <ul>
      <li>Three master nodes, as <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/modules-discovery-voting.html#_even_numbers_of_master_eligible_nodes">there should normally be an odd number of master-eligible nodes in a cluster</a>.</li>
      <li>Three data nodes</li>
    </ul>
  </li>
  <li>A dedicated monitoring cluster with Kibana and Elastic Agent</li>
  <li>An optional load balancer in front of the production cluster.</li>
</ul>

<h3 id="about-node-roles">About Node Roles</h3>

<p>Here’s a quick note on node roles in this setup:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">master</code>: Responsible for lightweight cluster-wide actions such as creating or deleting an index, tracking which nodes are part of the cluster, and deciding which shards to allocate to which nodes.</li>
  <li><code class="language-plaintext highlighter-rouge">data</code>: Responsible for holding data and performing data related operations such as CRUD, search, and aggregations.</li>
</ul>

<p>We won’t use <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/node-roles-overview.html#coordinating-only-node-role">coordinating only nodes</a> in this setup, since data nodes can happily serve the same purpose.
For more details on node roles, refer to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/node-roles-overview.html">Node roles</a>.</p>

<h3 id="why-use-a-dedicated-monitoring-cluster">Why Use a Dedicated Monitoring Cluster?</h3>

<p>Using a dedicated monitoring cluster is the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/monitoring-overview.html">recommended configuration</a> for two key reasons:</p>
<ul>
  <li>Production outages do not impact access to monitoring data.</li>
  <li>Monitoring workloads are isolated and cannot degrade production cluster performance.</li>
</ul>

<p>Elastic also recommends using <a href="https://www.elastic.co/guide/en/kibana/8.19/monitoring-data.html">a separate Kibana instance</a> to view a separate monitoring cluster.</p>

<p>For the sake of operational simplicity, this setup uses a single-node monitoring cluster.
When operating at scale, however, you can also configure a more resilient multi-node monitoring cluster by following this tutorial.</p>

<h2 id="infrastructure-setup">Infrastructure Setup</h2>

<p>You can skip this part if you already have servers ready to host the Elasticsearch cluster.</p>

<p>This tutorial has been demonstrated using the following setup:</p>

<ul>
  <li>Elastic Stack version: 8.19.7.</li>
  <li>VPN access: A VPN for secure configuration and access to the Elasticsearch cluster, using the CIDR block <code class="language-plaintext highlighter-rouge">10.0.0.0/8</code></li>
</ul>

<h3 id="create-a-security-group">Create a Security Group</h3>

<p>Log in to the AWS Management Console and navigate to EC2 -&gt; Security Groups -&gt; Create security group.</p>

<p>Create a security group named <code class="language-plaintext highlighter-rouge">my-elasticsearch-node</code> for the Elasticsearch nodes, and configure the following inbound rules:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Type</th>
      <th style="text-align: left">Protocol</th>
      <th style="text-align: left">Port range</th>
      <th style="text-align: left">Source</th>
      <th style="text-align: left">Description - optional</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">SSH</td>
      <td style="text-align: left">TCP</td>
      <td style="text-align: left">22</td>
      <td style="text-align: left">10.0.0.0/8</td>
      <td style="text-align: left"> </td>
    </tr>
    <tr>
      <td style="text-align: left">Custom TCP</td>
      <td style="text-align: left">TCP</td>
      <td style="text-align: left">5601</td>
      <td style="text-align: left">10.0.0.0/8</td>
      <td style="text-align: left">Kibana</td>
    </tr>
    <tr>
      <td style="text-align: left">Custom TCP</td>
      <td style="text-align: left">TCP</td>
      <td style="text-align: left">9200</td>
      <td style="text-align: left">10.0.0.0/8</td>
      <td style="text-align: left">HTTP port for REST clients to communicate with Elasticsearch</td>
    </tr>
    <tr>
      <td style="text-align: left">Custom TCP</td>
      <td style="text-align: left">TCP</td>
      <td style="text-align: left">9300</td>
      <td style="text-align: left">10.0.0.0/8</td>
      <td style="text-align: left">Transport port for nodes to communicate with one another</td>
    </tr>
  </tbody>
</table>

<h3 id="create-a-launch-template">Create a Launch Template</h3>

<p>Create a Launch Template to make it easier to add multiple nodes to the cluster.</p>

<p>Go to EC2 → Launch Templates → Create launch template, then configure the following:</p>

<ul>
  <li>Launch template name: <code class="language-plaintext highlighter-rouge">my-elasticsearch-node</code></li>
  <li>Launch template contents:
    <ul>
      <li>Amazon Machine Image (AMI): <code class="language-plaintext highlighter-rouge">Ubuntu Server 24.04 LTS (HVM), SSD Volume Type</code></li>
      <li>Architecture: <code class="language-plaintext highlighter-rouge">64-bit (Arm)</code></li>
    </ul>
  </li>
  <li>Network settings:
    <ul>
      <li>Security groups: <code class="language-plaintext highlighter-rouge">my-elasticsearch-node</code></li>
    </ul>
  </li>
  <li>Storage:
    <ul>
      <li>Size: 100 GiB</li>
    </ul>
  </li>
</ul>

<p>This configuration is just an example.
You can adjust the values to match your own requirements.
Just make sure to select the security group created in the previous step, that is <code class="language-plaintext highlighter-rouge">my-elasticsearch-node</code>.</p>

<h3 id="launch-instances">Launch Instances</h3>

<ol>
  <li>Go to EC2 -&gt; Instances -&gt; Launch instance from template and configure the following:
    <ul>
      <li>Source template: <code class="language-plaintext highlighter-rouge">my-elasticsearch-node</code></li>
      <li>Instance type: (Your choice. For this tutorial, the monitoring node requires at least 8 GiB of RAM.)</li>
      <li>Key pair (login): Select your key pair.</li>
      <li>Resource tags:
        <ul>
          <li>Key: <code class="language-plaintext highlighter-rouge">Name</code> (for identifying instances)</li>
          <li>Value: <code class="language-plaintext highlighter-rouge">my-elasticsearch-node</code></li>
        </ul>
      </li>
      <li>Number of instances: <code class="language-plaintext highlighter-rouge">7</code> (6 for the production cluster and 1 for the monitoring node)</li>
    </ul>
  </li>
  <li>Click <code class="language-plaintext highlighter-rouge">Launch instance</code></li>
  <li>Go to EC2 -&gt; Instances -&gt; Search <code class="language-plaintext highlighter-rouge">my-elasticsearch-node</code> and update the <code class="language-plaintext highlighter-rouge">Name</code> tags as follows:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-master-1</code></li>
      <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-master-2</code></li>
      <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-master-3</code></li>
      <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-1</code></li>
      <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-2</code></li>
      <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-3</code></li>
      <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-monitoring</code></li>
    </ul>
  </li>
  <li>Retrieve the private IP addresses of all EC2 instances using the AWS CLI:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> aws ec2 describe-instances <span class="se">\</span>
     <span class="nt">--filters</span> <span class="s2">"Name=tag:Name,Values=my-elasticsearch-node-*"</span> <span class="se">\</span>
     <span class="nt">--query</span> <span class="s2">"Reservations[].Instances[].[Tags[?Key=='Name'].Value | [0], PrivateIpAddress]"</span> <span class="se">\</span>
     <span class="nt">--output</span> text <span class="se">\</span>
     <span class="nt">--no-cli-pager</span>
</code></pre></div>    </div>
    <p>Output:</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> my-elasticsearch-node-master-1   10.0.0.1
 my-elasticsearch-node-master-2   10.0.0.2
 my-elasticsearch-node-master-3   10.0.0.3
 my-elasticsearch-node-data-1     10.0.1.1
 my-elasticsearch-node-data-2     10.0.1.2
 my-elasticsearch-node-data-3     10.0.1.3
 my-elasticsearch-node-monitoring        10.0.2.1
</code></pre></div>    </div>
  </li>
  <li>Update SSH config file (usually <code class="language-plaintext highlighter-rouge">~/.ssh/config</code>) to connect to the instances from your local environment:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Host my-elasticsearch-node-*
     User ubuntu
     IdentityFile your_key.pem
 Host my-elasticsearch-node-master-1
     HostName 10.0.0.1
 Host my-elasticsearch-node-master-2
     HostName 10.0.0.2
 Host my-elasticsearch-node-master-3
     HostName 10.0.0.3
 Host my-elasticsearch-node-data-1
     HostName 10.0.1.1
 Host my-elasticsearch-node-data-2
     HostName 10.0.1.2
 Host my-elasticsearch-node-data-3
     HostName 10.0.1.3
 Host my-elasticsearch-node-monitoring
     HostName 10.0.2.1
</code></pre></div>    </div>
  </li>
</ol>

<p>The IP addresses used in this tutorial are simplified for clarity.
Your IP addresses will differ depending on your VPC and subnet configuration.</p>

<h2 id="deploying-an-elasticsearch-cluster">Deploying an Elasticsearch cluster</h2>

<h3 id="initializing-the-cluster-with-master-nodes">Initializing the Cluster with Master Nodes</h3>

<ol>
  <li>Connect to <code class="language-plaintext highlighter-rouge">my-elasticsearch-node-master-1</code>:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ssh my-elasticsearch-node-master-1
</code></pre></div>    </div>
  </li>
  <li>Install Docker <a href="https://docs.docker.com/engine/install/ubuntu/">7</a>:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> ca-certificates curl
 <span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings
 <span class="nb">sudo </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg <span class="nt">-o</span> /etc/apt/keyrings/docker.asc
 <span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.asc
 <span class="nb">echo</span> <span class="se">\</span>
     <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu </span><span class="se">\</span><span class="s2">
     </span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">UBUNTU_CODENAME</span><span class="k">:-</span><span class="nv">$VERSION_CODENAME</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span><span class="s2"> stable"</span> | <span class="se">\</span>
     <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null
 <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
 <span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker <span class="k">${</span><span class="nv">USER</span><span class="k">}</span>
</code></pre></div>    </div>
  </li>
  <li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/vm-max-map-count.html">Set <code class="language-plaintext highlighter-rouge">vm.max_map_count</code> to at least <code class="language-plaintext highlighter-rouge">262144</code></a>:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">sudo </span>sysctl <span class="nt">-w</span> vm.max_map_count<span class="o">=</span>262144
 <span class="c"># Add `vm.max_map_count` to `sysctl.conf`</span>
 <span class="nb">echo</span> <span class="s2">"vm.max_map_count=262144"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf
</code></pre></div>    </div>
    <ul>
      <li>This is to increase the limits on <code class="language-plaintext highlighter-rouge">mmap</code> counts to avoid out of memory exceptions:</li>
    </ul>
  </li>
  <li><a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-prod#_configuration_files_must_be_readable_by_the_elasticsearch_user">Prepare local directories for storing data and logs through a bind-mount</a>:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">mkdir </span>esdatadir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx esdatadir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 esdatadir
 <span class="nb">mkdir </span>eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 eslogsdir
</code></pre></div>    </div>
  </li>
  <li>Prepare a certificate directory:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">mkdir </span>certs <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx certs <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 certs
</code></pre></div>    </div>
  </li>
  <li>Generate CA (only if it doesn’t exist - first node should create and share with others):
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> unzip
 <span class="nb">sudo </span>docker run <span class="nt">--rm</span> <span class="se">\</span>
     <span class="nt">-v</span> <span class="nv">$HOME</span>/certs:/usr/share/elasticsearch/config/certs <span class="se">\</span>
     docker.elastic.co/elasticsearch/elasticsearch:8.19.7 <span class="se">\</span>
     bin/elasticsearch-certutil ca <span class="nt">--silent</span> <span class="nt">--pem</span> <span class="nt">--out</span> /usr/share/elasticsearch/config/certs/ca.zip
 <span class="nb">cd</span> <span class="nv">$HOME</span>/certs <span class="o">&amp;&amp;</span> unzip <span class="nt">-o</span> ca.zip <span class="o">&amp;&amp;</span> <span class="nb">mv </span>ca/<span class="k">*</span> <span class="nb">.</span> <span class="o">&amp;&amp;</span> <span class="nb">rmdir </span>ca <span class="o">&amp;&amp;</span> <span class="nb">rm </span>ca.zip
</code></pre></div>    </div>
  </li>
  <li>Generate node certificate signed by CA:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">ES_NETWORK_HOST</span><span class="o">=</span><span class="si">$(</span><span class="nb">hostname</span> <span class="nt">-I</span> | <span class="nb">awk</span> <span class="s1">'{print $1}'</span><span class="si">)</span>
 <span class="nb">sudo </span>docker run <span class="nt">--rm</span> <span class="se">\</span>
     <span class="nt">-v</span> <span class="nv">$HOME</span>/certs:/usr/share/elasticsearch/config/certs <span class="se">\</span>
     docker.elastic.co/elasticsearch/elasticsearch:8.19.7 <span class="se">\</span>
     bin/elasticsearch-certutil cert <span class="nt">--silent</span> <span class="nt">--pem</span> <span class="se">\</span>
     <span class="nt">--ca-cert</span> /usr/share/elasticsearch/config/certs/ca.crt <span class="se">\</span>
     <span class="nt">--ca-key</span> /usr/share/elasticsearch/config/certs/ca.key <span class="se">\</span>
     <span class="nt">--dns</span> master-1,localhost <span class="se">\</span>
     <span class="nt">--ip</span> <span class="k">${</span><span class="nv">ES_NETWORK_HOST</span><span class="k">}</span>,127.0.0.1 <span class="se">\</span>
     <span class="nt">--out</span> /usr/share/elasticsearch/config/certs/node.zip
 <span class="nb">cd</span> <span class="nv">$HOME</span>/certs <span class="o">&amp;&amp;</span> unzip <span class="nt">-o</span> node.zip <span class="o">&amp;&amp;</span> <span class="nb">mv </span>instance/<span class="k">*</span> <span class="nb">.</span> <span class="o">&amp;&amp;</span> <span class="nb">rmdir </span>instance <span class="o">&amp;&amp;</span> <span class="nb">rm </span>node.zip
</code></pre></div>    </div>
  </li>
  <li>Create a custom <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> in your home directory with <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/important-settings.html">important settings</a>:
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">cluster.name</span><span class="pi">:</span> <span class="s">my-elasticsearch-cluster</span>
 <span class="na">node.roles</span><span class="pi">:</span> <span class="s">master</span>
 <span class="na">node.name</span><span class="pi">:</span> <span class="s">master-1</span>
 <span class="na">network.host</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
 <span class="na">discovery.seed_hosts</span><span class="pi">:</span> <span class="s">10.0.0.1,10.0.0.2,10.0.0.3</span>
 <span class="na">cluster.initial_master_nodes</span><span class="pi">:</span> <span class="s">master-1,master-2,master-3</span>
 <span class="na">xpack.security.enabled</span><span class="pi">:</span> <span class="no">true</span>
 <span class="na">xpack.security.transport.ssl.enabled</span><span class="pi">:</span> <span class="no">true</span>
 <span class="na">xpack.security.transport.ssl.verification_mode</span><span class="pi">:</span> <span class="s">certificate</span>
 <span class="na">xpack.security.transport.ssl.key</span><span class="pi">:</span> <span class="s">certs/instance.key</span>
 <span class="na">xpack.security.transport.ssl.certificate</span><span class="pi">:</span> <span class="s">certs/instance.crt</span>
 <span class="na">xpack.security.transport.ssl.certificate_authorities</span><span class="pi">:</span> <span class="s">certs/ca.crt</span>
</code></pre></div>    </div>
    <ul>
      <li>The configuration files should contain settings which are node-specific (such as <code class="language-plaintext highlighter-rouge">node.name</code> and paths), or settings which a node requires in order to be able to join a cluster (such as <code class="language-plaintext highlighter-rouge">cluster.name</code> and <code class="language-plaintext highlighter-rouge">network.host</code>), as <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/settings.html">most settings can be changed on a running cluster using the Cluster update settings API</a>.</li>
      <li><code class="language-plaintext highlighter-rouge">cluster.name</code>: A node can only join a cluster when it shares its <code class="language-plaintext highlighter-rouge">cluster.name</code> with all the other nodes in the cluster. The default name is <code class="language-plaintext highlighter-rouge">elasticsearch</code>, but you should change it to an appropriate name that describes the purpose of the cluster.</li>
      <li><code class="language-plaintext highlighter-rouge">node.roles</code>: We set the role to <code class="language-plaintext highlighter-rouge">master</code> to create a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/node-roles-overview.html#dedicated-master-node">dedicated master-eligible node</a>. This is the most reliable way to avoid overloading the master with other tasks.</li>
      <li><code class="language-plaintext highlighter-rouge">node.name</code>: Elasticsearch uses <code class="language-plaintext highlighter-rouge">node.name</code> as a human-readable identifier for a particular instance of Elasticsearch. The node name defaults to the hostname of the machine when Elasticsearch starts.</li>
      <li><code class="language-plaintext highlighter-rouge">network.host</code>: Sets the address of this node for both HTTP and transport traffic.</li>
      <li><code class="language-plaintext highlighter-rouge">discovery.seed_hosts</code>: When you want to form a cluster with nodes on other hosts, this setting provides a list of other nodes in the cluster that are master-eligible and likely to be live and contactable to seed the discovery process.</li>
      <li><code class="language-plaintext highlighter-rouge">cluster.initial_master_nodes</code>: A list of node names of master-eligible nodes whose votes are counted in the first election. Do not configure this setting on master-ineligible nodes or nodes joining an existing cluster.</li>
      <li><code class="language-plaintext highlighter-rouge">xpack.security.enabled</code>: Set <code class="language-plaintext highlighter-rouge">true</code> to install Elastic Agent for stack monitoring.</li>
      <li><code class="language-plaintext highlighter-rouge">xpack.security.transport.ssl.enabled</code>: Set <code class="language-plaintext highlighter-rouge">true</code> as <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/bootstrap-checks-xpack.html#bootstrap-checks-tls">transport SSL must be enabled if security is enabled</a>.</li>
      <li>For a full list of configuration options, refer to <a href="https://www.elastic.co/docs/reference/elasticsearch/configuration-reference">Elasticsearch configuration reference</a>.</li>
    </ul>
  </li>
  <li>Run Elasticsearch container:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker run <span class="nt">-d</span> <span class="se">\</span>
    <span class="nt">--restart</span> always <span class="se">\</span>
    <span class="nt">--group-add</span> 0 <span class="se">\</span>
    <span class="nt">--volume</span> <span class="nv">$HOME</span>/esdatadir:/usr/share/elasticsearch/data <span class="se">\</span>
    <span class="nt">--volume</span> <span class="nv">$HOME</span>/eslogsdir:/usr/share/elasticsearch/logs <span class="se">\</span>
    <span class="nt">--volume</span> <span class="nv">$HOME</span>/certs:/usr/share/elasticsearch/config/certs:ro <span class="se">\</span>
    <span class="nt">--ulimit</span> <span class="nv">nofile</span><span class="o">=</span>65535:65535 <span class="se">\</span>
    <span class="nt">--env</span> <span class="s2">"bootstrap.memory_lock=true"</span> <span class="nt">--ulimit</span> <span class="nv">memlock</span><span class="o">=</span><span class="nt">-1</span>:-1 <span class="se">\</span>
    <span class="nt">--publish-all</span> <span class="se">\</span>
    <span class="nt">--network</span> host <span class="se">\</span>
    <span class="nt">--volume</span> <span class="nv">$HOME</span>/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml <span class="se">\</span>
    <span class="nt">--env</span> <span class="nv">TZ</span><span class="o">=</span>Asia/Seoul <span class="se">\</span>
    <span class="nt">--env</span> <span class="nv">ELASTIC_PASSWORD</span><span class="o">=</span>elasticpassword <span class="se">\</span>
    <span class="nt">--name</span> elasticsearch <span class="se">\</span>
    docker.elastic.co/elasticsearch/elasticsearch:8.19.7
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">--restart always</code>: to automatically restarts the container if it stops.</li>
      <li><a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-prod#_configuration_files_must_be_readable_by_the_elasticsearch_user"><code class="language-plaintext highlighter-rouge">--group-add 0</code></a>: to ensure that the user under which Elasticsearch is running has file permissions to <code class="language-plaintext highlighter-rouge">esdatadir</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">--volume $HOME/...</code>: to persist data, logs, and certificates across container restarts.</li>
      <li><a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-prod#_increase_ulimits_for_nofile_and_nproc"><code class="language-plaintext highlighter-rouge">--ulimit nofile=65535:65535</code></a>: to increase the file descriptor limit required for production workloads.</li>
      <li><code class="language-plaintext highlighter-rouge">--publish-all</code>: to randomize published ports, which is <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-prod#_randomize_published_ports">recommended for production clusters</a>.</li>
      <li><code class="language-plaintext highlighter-rouge">--network host</code>: to <a href="https://docs.docker.com/engine/network/drivers/host/">optimize performance</a>. The <code class="language-plaintext highlighter-rouge">host</code> network only works on Linux hosts and <code class="language-plaintext highlighter-rouge">--publish-all</code> option is ignored when it is enabled.</li>
      <li><code class="language-plaintext highlighter-rouge">--volume $HOME/...</code>: to bind-mount custom <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code>, <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-configure">the preferred approach for production setups</a>.</li>
      <li><a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-prod#_disable_swapping"><code class="language-plaintext highlighter-rouge">--env "bootstrap.memory_lock=true" --ulimit memlock=-1:-1</code></a>: to disable swapping for performance and node stability.</li>
      <li><code class="language-plaintext highlighter-rouge">--env TZ=Asia/Seoul</code>: Sets the container timezone (adjust as needed).</li>
      <li><code class="language-plaintext highlighter-rouge">ELASTIC_PASSWORD</code>: Replace it with a password for your production Elasticsearch cluster.</li>
      <li>Some setups use <code class="language-plaintext highlighter-rouge">ES_JAVA_OPTS</code>,  but this is <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/advanced-configuration.html">not recommended for production</a>. <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-prod#docker-set-heap-size">Use the default Elasticsearch memory settings instead</a>.</li>
    </ul>
  </li>
</ol>

<p>At this stage, the cluster isn’t fully formed yet, so you’ll see a warning in the container logs like:</p>
<pre><code class="language-log">master not discovered yet, this node has not previously joined a bootstrapped cluster, and this node must discover master-eligible nodes [master-1, master-2, master-3] to bootstrap a cluster: ...
</code></pre>

<p class="notice--info">You won’t see this error if you’re using only a single master-eligible node.</p>

<p>This happens because we configured three initial master nodes—<code class="language-plaintext highlighter-rouge">master-1</code>, <code class="language-plaintext highlighter-rouge">master-2</code>, and <code class="language-plaintext highlighter-rouge">master-3</code>—but only <code class="language-plaintext highlighter-rouge">master-1</code> has joined the cluster so far. Elasticsearch is still waiting for the remaining master-eligible nodes before it can complete the bootstrap process.
To finish forming the cluster, you need to bring up and join the other master-eligible nodes (<code class="language-plaintext highlighter-rouge">master-2</code> and <code class="language-plaintext highlighter-rouge">master-3</code>) as well.</p>

<h4 id="joining-master-nodes-to-the-cluster">Joining Master Nodes to the Cluster</h4>

<ol>
  <li>Copy <code class="language-plaintext highlighter-rouge">certs/ca.crt</code> and <code class="language-plaintext highlighter-rouge">certs/ca.key</code> files to your local machine or another secure location. These will be reused when setting up the remaining nodes.</li>
  <li>Connect to <code class="language-plaintext highlighter-rouge">my-elasticsearch-node-master-2</code>:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ssh my-elasticsearch-node-master-2
</code></pre></div>    </div>
  </li>
  <li>Copy <code class="language-plaintext highlighter-rouge">ca.crt</code> and <code class="language-plaintext highlighter-rouge">ca.key</code> into the home directory on this node.</li>
  <li>Install docker, set <code class="language-plaintext highlighter-rouge">vm.max_map_count</code>, prepare local directories, and generate node certificate:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># install docker</span>
 <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> ca-certificates curl unzip
 <span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings
 <span class="nb">sudo </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg <span class="nt">-o</span> /etc/apt/keyrings/docker.asc
 <span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.asc
 <span class="nb">echo</span> <span class="se">\</span>
     <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu </span><span class="se">\</span><span class="s2">
     </span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">UBUNTU_CODENAME</span><span class="k">:-</span><span class="nv">$VERSION_CODENAME</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span><span class="s2"> stable"</span> | <span class="se">\</span>
     <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null
 <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
 <span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker <span class="k">${</span><span class="nv">USER</span><span class="k">}</span>

 <span class="c"># Set `vm.max_map_count` to at least 262144</span>
 <span class="nb">sudo </span>sysctl <span class="nt">-w</span> vm.max_map_count<span class="o">=</span>262144
 <span class="nb">echo</span> <span class="s2">"vm.max_map_count=262144"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf

 <span class="c"># Prepare a local directory for storing data and logs through a bind-mount</span>
 <span class="nb">mkdir </span>esdatadir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx esdatadir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 esdatadir
 <span class="nb">mkdir </span>eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 eslogsdir

 <span class="c"># Prepare certificate directory</span>
 <span class="nb">mkdir </span>certs <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx certs <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 certs

 <span class="nb">mv </span>ca.crt certs/
 <span class="nb">mv </span>ca.key certs/

 <span class="c"># Generate node certificate signed by CA</span>
 <span class="nv">ES_NETWORK_HOST</span><span class="o">=</span><span class="si">$(</span><span class="nb">hostname</span> <span class="nt">-I</span> | <span class="nb">awk</span> <span class="s1">'{print $1}'</span><span class="si">)</span>
 <span class="nb">sudo </span>docker run <span class="nt">--rm</span> <span class="se">\</span>
     <span class="nt">-v</span> <span class="nv">$HOME</span>/certs:/usr/share/elasticsearch/config/certs <span class="se">\</span>
     docker.elastic.co/elasticsearch/elasticsearch:8.19.7 <span class="se">\</span>
     bin/elasticsearch-certutil cert <span class="nt">--silent</span> <span class="nt">--pem</span> <span class="se">\</span>
     <span class="nt">--ca-cert</span> /usr/share/elasticsearch/config/certs/ca.crt <span class="se">\</span>
     <span class="nt">--ca-key</span> /usr/share/elasticsearch/config/certs/ca.key <span class="se">\</span>
     <span class="nt">--dns</span> master-2,localhost <span class="se">\</span>
     <span class="nt">--ip</span> <span class="k">${</span><span class="nv">ES_NETWORK_HOST</span><span class="k">}</span>,127.0.0.1 <span class="se">\</span>
     <span class="nt">--out</span> /usr/share/elasticsearch/config/certs/node.zip

 <span class="nb">cd</span> <span class="nv">$HOME</span>/certs <span class="o">&amp;&amp;</span> unzip <span class="nt">-o</span> node.zip <span class="o">&amp;&amp;</span> <span class="nb">mv </span>instance/<span class="k">*</span> <span class="nb">.</span> <span class="o">&amp;&amp;</span> <span class="nb">rmdir </span>instance <span class="o">&amp;&amp;</span> <span class="nb">rm </span>node.zip
</code></pre></div>    </div>
  </li>
  <li>Create a custom <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> in your home directory:
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">cluster.name</span><span class="pi">:</span> <span class="s">my-elasticsearch-cluster</span>
 <span class="na">node.roles</span><span class="pi">:</span> <span class="s">master</span>
 <span class="na">node.name</span><span class="pi">:</span> <span class="s">master-2</span>
 <span class="na">network.host</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
 <span class="na">discovery.seed_hosts</span><span class="pi">:</span> <span class="s">10.0.0.1,10.0.0.2,10.0.0.3</span>
 <span class="na">xpack.security.enabled</span><span class="pi">:</span> <span class="no">true</span>
 <span class="na">xpack.security.transport.ssl.enabled</span><span class="pi">:</span> <span class="no">true</span>
 <span class="na">xpack.security.transport.ssl.verification_mode</span><span class="pi">:</span> <span class="s">certificate</span>
 <span class="na">xpack.security.transport.ssl.key</span><span class="pi">:</span> <span class="s">certs/instance.key</span>
 <span class="na">xpack.security.transport.ssl.certificate</span><span class="pi">:</span> <span class="s">certs/instance.crt</span>
 <span class="na">xpack.security.transport.ssl.certificate_authorities</span><span class="pi">:</span> <span class="s">certs/ca.crt</span>
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">node.name</code>: now set to <code class="language-plaintext highlighter-rouge">master-2</code></li>
    </ul>
  </li>
  <li>Run Elasticsearch container:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">sudo </span>docker run <span class="nt">-d</span> <span class="se">\</span>
     <span class="nt">--restart</span> always <span class="se">\</span>
     <span class="nt">--group-add</span> 0 <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/esdatadir:/usr/share/elasticsearch/data <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/eslogsdir:/usr/share/elasticsearch/logs <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/certs:/usr/share/elasticsearch/config/certs:ro <span class="se">\</span>
     <span class="nt">--ulimit</span> <span class="nv">nofile</span><span class="o">=</span>65535:65535 <span class="se">\</span>
     <span class="nt">--env</span> <span class="s2">"bootstrap.memory_lock=true"</span> <span class="nt">--ulimit</span> <span class="nv">memlock</span><span class="o">=</span><span class="nt">-1</span>:-1 <span class="se">\</span>
     <span class="nt">--publish-all</span> <span class="se">\</span>
     <span class="nt">--network</span> host <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml <span class="se">\</span>
     <span class="nt">--env</span> <span class="nv">TZ</span><span class="o">=</span>Asia/Seoul <span class="se">\</span>
     <span class="nt">--env</span> <span class="nv">ELASTIC_PASSWORD</span><span class="o">=</span>elasticpassword <span class="se">\</span>
     <span class="nt">--name</span> elasticsearch <span class="se">\</span>
     docker.elastic.co/elasticsearch/elasticsearch:8.19.7
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">--env TZ=Asia/Seoul</code>: Sets the container timezone (adjust as needed).</li>
      <li><code class="language-plaintext highlighter-rouge">ELASTIC_PASSWORD</code>: Replace it with a password for your production Elasticsearch cluster.</li>
    </ul>
  </li>
  <li>Connect to <code class="language-plaintext highlighter-rouge">my-elasticsearch-node-master-3</code> and repeat steps 13-16, making sure to set <code class="language-plaintext highlighter-rouge">node.name</code> to <code class="language-plaintext highlighter-rouge">master-3</code> in the <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> file.</li>
  <li>Verify that the master nodes have formed a cluster:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> curl <span class="nt">-u</span> elastic:elasticpassword <span class="s2">"http://localhost:9200/_cat/nodes?v&amp;s=name"</span>
</code></pre></div>    </div>
    <p>Output:</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ip             heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
 10.0.0.1                 40          98   0    0.00    0.05     0.07 m         *      master-1
 10.0.0.2                 26          98   0    0.07    0.17     0.10 m         -      master-2
 10.0.0.3                 24          98   0    0.39    0.31     0.13 m         -      master-3
</code></pre></div>    </div>
  </li>
</ol>

<p>Since there are no data nodes to host shards yet, the cluster health will show up as <code class="language-plaintext highlighter-rouge">red</code>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://elastic:elasticpassword@localhost:9200/_cluster/health
</code></pre></div></div>
<p>Output:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"cluster_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-elasticsearch-cluster"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"red"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"timed_out"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"number_of_nodes"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
    </span><span class="nl">"number_of_data_nodes"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"active_primary_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"active_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"relocating_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"initializing_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"unassigned_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
    </span><span class="nl">"unassigned_primary_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
    </span><span class="nl">"delayed_unassigned_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"number_of_pending_tasks"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"number_of_in_flight_fetch"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"task_max_waiting_in_queue_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"active_shards_percent_as_number"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.0</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This is expected.
Once the data nodes join the cluster, Elasticsearch will be able to allocate shards and the status should move to <code class="language-plaintext highlighter-rouge">yellow</code> or <code class="language-plaintext highlighter-rouge">green</code> depending on replica allocation.</p>

<h3 id="joining-data-nodes-to-the-cluster">Joining Data Nodes to the Cluster</h3>

<p>The process for adding a data node is very similar to adding a master node, with a few small changes in the <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> configuration.</p>

<ol>
  <li>Connect to <code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-1</code>:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ssh my-elasticsearch-node-data-1
</code></pre></div>    </div>
  </li>
  <li>Copy <code class="language-plaintext highlighter-rouge">ca.crt</code> and <code class="language-plaintext highlighter-rouge">ca.key</code> into the home directory on this node.</li>
  <li>Install docker, set <code class="language-plaintext highlighter-rouge">vm.max_map_count</code>, prepare local directories, and generate node certificate:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># install docker</span>
 <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> ca-certificates curl unzip
 <span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings
 <span class="nb">sudo </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg <span class="nt">-o</span> /etc/apt/keyrings/docker.asc
 <span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.asc
 <span class="nb">echo</span> <span class="se">\</span>
     <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu </span><span class="se">\</span><span class="s2">
     </span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">UBUNTU_CODENAME</span><span class="k">:-</span><span class="nv">$VERSION_CODENAME</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span><span class="s2"> stable"</span> | <span class="se">\</span>
     <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null
 <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
 <span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker <span class="k">${</span><span class="nv">USER</span><span class="k">}</span>

 <span class="c"># Set `vm.max_map_count` to at least 262144</span>
 <span class="nb">sudo </span>sysctl <span class="nt">-w</span> vm.max_map_count<span class="o">=</span>262144
 <span class="nb">echo</span> <span class="s2">"vm.max_map_count=262144"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf

 <span class="c"># Prepare a local directory for storing data and logs through a bind-mount</span>
 <span class="nb">mkdir </span>esdatadir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx esdatadir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 esdatadir
 <span class="nb">mkdir </span>eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 eslogsdir

 <span class="c"># Prepare certificate directory</span>
 <span class="nb">mkdir </span>certs <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx certs <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 certs

 <span class="nb">mv </span>ca.crt certs/
 <span class="nb">mv </span>ca.key certs/

 <span class="c"># Generate node certificate signed by CA</span>
 <span class="nv">ES_NETWORK_HOST</span><span class="o">=</span><span class="si">$(</span><span class="nb">hostname</span> <span class="nt">-I</span> | <span class="nb">awk</span> <span class="s1">'{print $1}'</span><span class="si">)</span>
 <span class="nb">sudo </span>docker run <span class="nt">--rm</span> <span class="se">\</span>
     <span class="nt">-v</span> <span class="nv">$HOME</span>/certs:/usr/share/elasticsearch/config/certs <span class="se">\</span>
     docker.elastic.co/elasticsearch/elasticsearch:8.19.7 <span class="se">\</span>
     bin/elasticsearch-certutil cert <span class="nt">--silent</span> <span class="nt">--pem</span> <span class="se">\</span>
     <span class="nt">--ca-cert</span> /usr/share/elasticsearch/config/certs/ca.crt <span class="se">\</span>
     <span class="nt">--ca-key</span> /usr/share/elasticsearch/config/certs/ca.key <span class="se">\</span>
     <span class="nt">--dns</span> data-1,localhost <span class="se">\</span>
     <span class="nt">--ip</span> <span class="k">${</span><span class="nv">ES_NETWORK_HOST</span><span class="k">}</span>,127.0.0.1 <span class="se">\</span>
     <span class="nt">--out</span> /usr/share/elasticsearch/config/certs/node.zip

 <span class="nb">cd</span> <span class="nv">$HOME</span>/certs <span class="o">&amp;&amp;</span> unzip <span class="nt">-o</span> node.zip <span class="o">&amp;&amp;</span> <span class="nb">mv </span>instance/<span class="k">*</span> <span class="nb">.</span> <span class="o">&amp;&amp;</span> <span class="nb">rmdir </span>instance <span class="o">&amp;&amp;</span> <span class="nb">rm </span>node.zip
</code></pre></div>    </div>
  </li>
  <li>Create a custom <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> in your home directory:
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">cluster.name</span><span class="pi">:</span> <span class="s">my-elasticsearch-cluster</span>
 <span class="na">node.roles</span><span class="pi">:</span> <span class="s">data</span>
 <span class="na">node.name</span><span class="pi">:</span> <span class="s">data-1</span>
 <span class="na">network.host</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
 <span class="na">discovery.seed_hosts</span><span class="pi">:</span> <span class="s">10.0.0.1,10.0.0.2,10.0.0.3</span>
 <span class="na">xpack.security.enabled</span><span class="pi">:</span> <span class="no">true</span>
 <span class="na">xpack.security.transport.ssl.enabled</span><span class="pi">:</span> <span class="no">true</span>
 <span class="na">xpack.security.transport.ssl.verification_mode</span><span class="pi">:</span> <span class="s">certificate</span>
 <span class="na">xpack.security.transport.ssl.key</span><span class="pi">:</span> <span class="s">certs/instance.key</span>
 <span class="na">xpack.security.transport.ssl.certificate</span><span class="pi">:</span> <span class="s">certs/instance.crt</span>
 <span class="na">xpack.security.transport.ssl.certificate_authorities</span><span class="pi">:</span> <span class="s">certs/ca.crt</span>
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">node.roles</code>: now set to <code class="language-plaintext highlighter-rouge">data</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">node.name</code>: now set to <code class="language-plaintext highlighter-rouge">data-1</code></li>
    </ul>
  </li>
  <li>Run Elasticsearch container:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">sudo </span>docker run <span class="nt">-d</span> <span class="se">\</span>
     <span class="nt">--restart</span> always <span class="se">\</span>
     <span class="nt">--group-add</span> 0 <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/esdatadir:/usr/share/elasticsearch/data <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/eslogsdir:/usr/share/elasticsearch/logs <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/certs:/usr/share/elasticsearch/config/certs:ro <span class="se">\</span>
     <span class="nt">--ulimit</span> <span class="nv">nofile</span><span class="o">=</span>65535:65535 <span class="se">\</span>
     <span class="nt">--env</span> <span class="s2">"bootstrap.memory_lock=true"</span> <span class="nt">--ulimit</span> <span class="nv">memlock</span><span class="o">=</span><span class="nt">-1</span>:-1 <span class="se">\</span>
     <span class="nt">--publish-all</span> <span class="se">\</span>
     <span class="nt">--network</span> host <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml <span class="se">\</span>
     <span class="nt">--env</span> <span class="nv">TZ</span><span class="o">=</span>Asia/Seoul <span class="se">\</span>
     <span class="nt">--env</span> <span class="nv">ELASTIC_PASSWORD</span><span class="o">=</span>elasticpassword <span class="se">\</span>
     <span class="nt">--name</span> elasticsearch <span class="se">\</span>
     docker.elastic.co/elasticsearch/elasticsearch:8.19.7
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">--env TZ=Asia/Seoul</code>: Sets the container timezone (adjust as needed).</li>
      <li><code class="language-plaintext highlighter-rouge">ELASTIC_PASSWORD</code>: Replace it with a password for your production Elasticsearch cluster.</li>
      <li>Joining the cluster can take a little time. Once a data node joins the cluster, the health status transitions from <code class="language-plaintext highlighter-rouge">red</code> to <code class="language-plaintext highlighter-rouge">green</code>:
        <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="p">{</span><span class="w">
      </span><span class="nl">"cluster_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-elasticsearch-cluster"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"green"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"timed_out"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"number_of_nodes"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w">
      </span><span class="nl">"number_of_data_nodes"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"active_primary_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
      </span><span class="nl">"active_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
      </span><span class="nl">"relocating_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"initializing_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"unassigned_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"unassigned_primary_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"delayed_unassigned_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"number_of_pending_tasks"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"number_of_in_flight_fetch"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"task_max_waiting_in_queue_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"active_shards_percent_as_number"</span><span class="p">:</span><span class="w"> </span><span class="mf">100.0</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span></code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>Repeat steps 1-5 for <code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-2</code> and <code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-3</code>, updating <code class="language-plaintext highlighter-rouge">node.name</code> to match each node.</li>
  <li>From any node, verify that all nodes have successfully joined the cluster:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> curl <span class="nt">-u</span> elastic:elasticpassword <span class="s2">"http://localhost:9200/_cat/nodes?v&amp;s=name"</span>
</code></pre></div>    </div>
    <p>Output:</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ip             heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
 10.0.1.1                 41          97   0    0.00    0.06     0.08 d         -      data-1
 10.0.1.2                 36          98   0    0.22    0.33     0.16 d         -      data-2
 10.0.1.3                 33          98   0    0.14    0.24     0.11 d         -      data-3
 10.0.0.1                  8          98   0    0.00    0.00     0.00 m         *      master-1
 10.0.0.2                 44          97   0    0.00    0.00     0.00 m         -      master-2
 10.0.0.3                 42          97   0    0.00    0.00     0.00 m         -      master-3
</code></pre></div>    </div>
  </li>
</ol>

<p>At this point, all master and data nodes should be present, and the cluster should be fully formed and healthy.</p>

<h3 id="updating-custom-elasticsearchyml">Updating custom <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code></h3>

<p>Updating a custom <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> in a Docker-based setup is straightforward:</p>
<ol>
  <li>Make your changes in <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> in your home directory.</li>
  <li>Restart the container:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker restart elasticsearch.yml
</code></pre></div>    </div>
  </li>
</ol>

<p>Be aware that restarting the container will cause downtime.
For production clusters, plan configuration changes carefully and apply them during a maintenance window, especially if the setting you’re changing affects cluster behavior or node roles.</p>

<h2 id="stack-monitoring">Stack Monitoring</h2>

<p>Stack Monitoring lets you collect logs and metrics from Elastic products, most importantly your production Elasticsearch nodes, and also things like Kibana.
At a minimum, you need monitoring data for the production Elasticsearch cluster.
<a href="https://www.elastic.co/guide/en/kibana/8.19/monitoring-data.html">Once that’s in place, Kibana can show monitoring data for other products in the Stack Monitoring page</a>.</p>

<p class="notice--info">The legacy Elasticsearch Monitoring plugin approach (enabled via <code class="language-plaintext highlighter-rouge">xpack.monitoring.collection.enabled</code>) was deprecated in 7.16.
These days, Elastic recommends using Elastic Agent or Metricbeat to collect and ship monitoring data to a monitoring cluster.</p>

<p>Next, we’ll deploy a single-node monitoring cluster and use Elastic Agent to monitor our production cluster.</p>

<h3 id="deploying-elasticsearch">Deploying Elasticsearch</h3>

<ol>
  <li>Connect to <code class="language-plaintext highlighter-rouge">my-elasticsearch-monitoring</code>
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ssh my-elasticsearch-node-monitoring
</code></pre></div>    </div>
  </li>
  <li>Install docker, set <code class="language-plaintext highlighter-rouge">vm.max_map_count</code>, and prepare local directories for data persistency:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># install docker</span>
 <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> ca-certificates curl
 <span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings
 <span class="nb">sudo </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg <span class="nt">-o</span> /etc/apt/keyrings/docker.asc
 <span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.asc
 <span class="nb">echo</span> <span class="se">\</span>
     <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu </span><span class="se">\</span><span class="s2">
     </span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">UBUNTU_CODENAME</span><span class="k">:-</span><span class="nv">$VERSION_CODENAME</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span><span class="s2"> stable"</span> | <span class="se">\</span>
     <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null
 <span class="nb">sudo </span>apt-get update
 <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
 <span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker <span class="k">${</span><span class="nv">USER</span><span class="k">}</span>

 <span class="c"># Set `vm.max_map_count` to at least 262144</span>
 <span class="nb">sudo </span>sysctl <span class="nt">-w</span> vm.max_map_count<span class="o">=</span>262144
 <span class="nb">echo</span> <span class="s2">"vm.max_map_count=262144"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf

 <span class="c"># Prepare a local directory for storing data and logs through a bind-mount</span>
 <span class="nb">mkdir </span>esdatadir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx esdatadir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 esdatadir
 <span class="nb">mkdir </span>eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+rwx eslogsdir <span class="o">&amp;&amp;</span> <span class="nb">sudo chgrp </span>0 eslogsdir
</code></pre></div>    </div>
  </li>
  <li>Create a custom <code class="language-plaintext highlighter-rouge">elasticsearch.yml</code> in your home directory:
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">discovery.type</span><span class="pi">:</span> <span class="s">single-node</span>
 <span class="na">xpack.security.enabled</span><span class="pi">:</span> <span class="no">true</span>
 <span class="na">network.host</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
</code></pre></div>    </div>
  </li>
  <li>Run Elasticsearch container:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">sudo </span>docker run <span class="nt">-d</span> <span class="se">\</span>
     <span class="nt">--restart</span> always <span class="se">\</span>
     <span class="nt">--group-add</span> 0 <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/esdatadir:/usr/share/elasticsearch/data <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/eslogsdir:/usr/share/elasticsearch/logs <span class="se">\</span>
     <span class="nt">--ulimit</span> <span class="nv">nofile</span><span class="o">=</span>65535:65535 <span class="se">\</span>
     <span class="nt">--env</span> <span class="s2">"bootstrap.memory_lock=true"</span> <span class="nt">--ulimit</span> <span class="nv">memlock</span><span class="o">=</span><span class="nt">-1</span>:-1 <span class="se">\</span>
     <span class="nt">--publish-all</span> <span class="se">\</span>
     <span class="nt">--network</span> host <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml <span class="se">\</span>
     <span class="nt">--env</span> <span class="nv">TZ</span><span class="o">=</span>Asia/Seoul <span class="se">\</span>
     <span class="nt">--env</span> <span class="nv">ELASTIC_PASSWORD</span><span class="o">=</span>elasticpassword <span class="se">\</span>
     <span class="nt">--name</span> elasticsearch <span class="se">\</span>
     docker.elastic.co/elasticsearch/elasticsearch:8.19.7
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">--env TZ=Asia/Seoul</code>: Sets the container timezone (adjust as needed).</li>
      <li><code class="language-plaintext highlighter-rouge">ELASTIC_PASSWORD</code>: Replace it with a password for your monitoring Elasticsearch node.</li>
    </ul>
  </li>
  <li>Verify:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> curl <span class="nt">-u</span> elastic:elasticpassword <span class="s2">"http://localhost:9200/_cat/nodes?v&amp;s=name"</span>
</code></pre></div>    </div>
    <p>Output</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ip             heap.percent ram.percent cpu load_1m load_5m load_15m node.role   master name
 10.0.2.1                 22          97  44    0.92    0.31     0.11 cdfhilmrstw *      ip-10-0-2-1
</code></pre></div>    </div>
    <ul>
      <li>Note that the node is assigned <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/high-availability-cluster-small-clusters.html#high-availability-cluster-design-one-node">every role as it must do everything</a>.</li>
    </ul>
  </li>
</ol>

<h3 id="deploying-kibana">Deploying Kibana</h3>

<ol>
  <li>Set a password for <code class="language-plaintext highlighter-rouge">kibana_system</code> user:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> curl <span class="nt">-s</span> <span class="nt">-X</span> POST <span class="nt">-u</span> <span class="s2">"elastic:elasticpassword"</span> <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> http://localhost:9200/_security/user/kibana_system/_password <span class="nt">-d</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">password</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">kibanapassword</span><span class="se">\"</span><span class="s2">}"</span>
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">kibanapassword</code>: Replace it with your password.</li>
    </ul>
  </li>
  <li>Create custom <code class="language-plaintext highlighter-rouge">kibana.yml</code>:
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">server.host</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0.0.0.0"</span>
 <span class="na">server.shutdownTimeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">5s"</span>
 <span class="na">elasticsearch.hosts</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">http://localhost:9200"</span><span class="pi">]</span>
 <span class="na">elasticsearch.username</span><span class="pi">:</span> <span class="s2">"</span><span class="s">kibana_system"</span>
 <span class="na">elasticsearch.password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">kibanapassword"</span>
 <span class="na">xpack.encryptedSavedObjects.encryptionKey</span><span class="pi">:</span> <span class="s2">"</span><span class="s">d7x9s2k5v8y4b3m6n1q0w7e2r5t8y9u1"</span> <span class="c1"># Replace this with your own key. You can generate one using `./kibana-encryption-keys generate`.</span>
 <span class="na">server.name</span><span class="pi">:</span> <span class="s">kibana</span>
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">elasticsearch.hosts</code>: Use <code class="language-plaintext highlighter-rouge">http://localhost:9200</code> since Elasticsearch is running on the same machine.</li>
    </ul>
  </li>
  <li>Run Kibana container:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">sudo </span>docker run <span class="nt">-d</span> <span class="se">\</span>
     <span class="nt">--restart</span> always <span class="se">\</span>
     <span class="nt">--network</span> host <span class="se">\</span>
     <span class="nt">--env</span> <span class="nv">TZ</span><span class="o">=</span>Asia/Seoul <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/kibana.yml:/usr/share/kibana/config/kibana.yml <span class="se">\</span>
     <span class="nt">--name</span> kibana <span class="se">\</span>
     docker.elastic.co/kibana/kibana:8.19.7
</code></pre></div>    </div>
  </li>
  <li>
    <p>Verify the deployment by opening Kibana (<code class="language-plaintext highlighter-rouge">http://10.0.2.1:5601</code> in this case) in your browser:</p>

    <p><img src="/assets/posts/39/kibana_login_page.png" alt="Kibana login page" /></p>
  </li>
</ol>

<h3 id="installing-elasticsearch-integration">Installing Elasticsearch Integration</h3>

<ol>
  <li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/configuring-elastic-agent.html#_prerequisites_11">Create a user on the production cluster that has the <code class="language-plaintext highlighter-rouge">remote_monitoring_collector</code> built-in role</a>.
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> curl <span class="nt">-X</span> POST <span class="s2">"http://localhost:9200/_security/user/elastic_agent?pretty"</span> <span class="se">\</span>
     <span class="nt">-u</span> elastic:elasticpassword <span class="se">\</span>
     <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
     <span class="nt">-d</span> <span class="s1">'{
         "password" : "changeme",
         "roles" : [ "remote_monitoring_collector"]
     }'</span>
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">elastic_agent</code>: This is just a username for Elastic Agent. Feel free to use a different one.</li>
      <li><code class="language-plaintext highlighter-rouge">password</code>: Choose a secure password for this user.</li>
    </ul>
  </li>
  <li>Go to Kibana in your browser.</li>
  <li>Login with:
    <ul>
      <li>Username: <code class="language-plaintext highlighter-rouge">elastic</code></li>
      <li>Password: Your <code class="language-plaintext highlighter-rouge">ELASTIC_PASSWORD</code></li>
    </ul>
  </li>
  <li>Click <code class="language-plaintext highlighter-rouge">Add integrations</code>.</li>
  <li>
    <p>In the query bar, search for and select the <code class="language-plaintext highlighter-rouge">Elasticsearch</code> integration for Elastic Agent:</p>

    <p><img src="/assets/posts/39/integrations.png" alt="Integrations" /></p>
  </li>
  <li>Click <code class="language-plaintext highlighter-rouge">Add Elasticsearch</code>.</li>
  <li>Configure the integration name and optionally add a description. Make sure you configure all required settings:
    <ul>
      <li>Go to Metrics (Stack monitoring) -&gt; Change defaults:
        <ul>
          <li>Hosts: Specify data nodes, or <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/configuring-elastic-agent.html#_add_elasticsearch_monitoring_data">a load balancer that routes to master-ineligible nodes</a>:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">http://10.0.1.1:9200</code></li>
              <li><code class="language-plaintext highlighter-rouge">http://10.0.1.2:9200</code></li>
              <li><code class="language-plaintext highlighter-rouge">http://10.0.1.3:9200</code></li>
            </ul>
          </li>
          <li>Advanced options:
            <ul>
              <li>Username: <code class="language-plaintext highlighter-rouge">elastic_agent</code></li>
              <li>Password: <code class="language-plaintext highlighter-rouge">changeme</code></li>
              <li>Scope: <code class="language-plaintext highlighter-rouge">cluster</code> (If set to <code class="language-plaintext highlighter-rouge">node</code>, Elastic Agent only collects metrics from the specified hosts.)</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Click <code class="language-plaintext highlighter-rouge">Save and continue</code>. This may take a minute or two. When it finishes, an agent policy with the Elasticsearch monitoring integration will be created.</li>
  <li>Click <code class="language-plaintext highlighter-rouge">Add Elastic Agent later</code>.</li>
</ol>

<h3 id="deploying-elastic-agent">Deploying Elastic Agent</h3>

<p>There are two ways to run Elastic Agent:</p>

<ul>
  <li>Fleet-managed: Elastic Agent is centrally managed through Fleet in Kibana.</li>
  <li>Standalone: Elastic Agent is configured locally using YAML files.</li>
</ul>

<p>In this tutorial, we’ll use the standalone approach to avoid the extra dependency on Fleet.</p>

<ol>
  <li>Go to Kibana for the monitoring cluster in your browser.</li>
  <li>Go to <code class="language-plaintext highlighter-rouge">Fleet</code> -&gt; <code class="language-plaintext highlighter-rouge">Agents</code> -&gt; <code class="language-plaintext highlighter-rouge">Add Agent</code> -&gt; <code class="language-plaintext highlighter-rouge">Run standalone</code></li>
  <li>Select <code class="language-plaintext highlighter-rouge">Agent Policy 1</code>.</li>
  <li>Click <code class="language-plaintext highlighter-rouge">Create API key</code> to generate an API key for Elastic Agent.
    <ul>
      <li>By default, the output host is set to <code class="language-plaintext highlighter-rouge">http://localhost:9200</code>, which is where Elastic Agent will send monitoring data.</li>
    </ul>
  </li>
  <li>Click <code class="language-plaintext highlighter-rouge">Copy to clipboard</code> or <code class="language-plaintext highlighter-rouge">Download policy</code></li>
  <li>Connect to monitoring node:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ssh my-elasticsearch-node-monitoring
</code></pre></div>    </div>
  </li>
  <li>Create an <code class="language-plaintext highlighter-rouge">elastic-agent.yml</code> file and paste the configuration from the clipboard, or copy the downloaded policy from step 5.</li>
  <li>Run Elastic Agent container:
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker run <span class="se">\</span>
     <span class="nt">-d</span> <span class="se">\</span>
     <span class="nt">--restart</span> always <span class="se">\</span>
     <span class="nt">--name</span> elastic-agent <span class="se">\</span>
     <span class="nt">--network</span> host <span class="se">\</span>
     <span class="nt">--user</span> root <span class="se">\</span>
     <span class="nt">--volume</span> <span class="nv">$HOME</span>/elastic-agent.yml:/usr/share/elastic-agent/elastic-agent.yml <span class="se">\</span>
     docker.elastic.co/beats/elastic-agent:8.19.7
</code></pre></div>    </div>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">--network host</code>: Allows the agent to connect to Elasticsearch via <code class="language-plaintext highlighter-rouge">http://localhost:9200</code>.</li>
      <li>Startup usually takes a few seconds.</li>
    </ul>
  </li>
  <li>Go to Kibana for the monitoring cluster in your browser.</li>
  <li>Click <code class="language-plaintext highlighter-rouge">Stack Monitoring</code> in the left menu, then select <code class="language-plaintext highlighter-rouge">Remind me later</code> when prompted.</li>
</ol>

<p>Once everything is set up successfully, Elastic Agent will start collecting and shipping monitoring data from the production Elasticsearch cluster.
With this data in place, Kibana can display monitoring information for Elasticsearch and other Elastic Stack components:</p>

<p><img src="/assets/posts/39/stack_monitoring.png" alt="Stack Monitoring" /></p>

<p>Click <code class="language-plaintext highlighter-rouge">Nodes</code> for node monitoring:</p>

<p><img src="/assets/posts/39/nodes_monitoring.png" alt="Nodes monitoring" /></p>

<p>Click specific node for detailed metrics:</p>

<p><img src="/assets/posts/39/node_monitoring.png" alt="Node monitoring" /></p>

<p>So far, Elastic Agent runs on the same node as the monitoring Elasticsearch and Kibana, but it can be deployed on any other node as well.
To do that, update the <code class="language-plaintext highlighter-rouge">Outputs</code> in the agent policy:</p>

<ol>
  <li>Go to <code class="language-plaintext highlighter-rouge">Fleet</code> -&gt; <code class="language-plaintext highlighter-rouge">Settings</code> -&gt; <code class="language-plaintext highlighter-rouge">Outputs</code> -&gt; <code class="language-plaintext highlighter-rouge">Add output</code></li>
  <li>Set the monitoring Elasticsearch host to <code class="language-plaintext highlighter-rouge">http://10.0.2.1:9200</code>.</li>
</ol>

<p>Updating the output will result in an <code class="language-plaintext highlighter-rouge">elastic-agent.yml</code> similar to the following:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">...</span>
<span class="na">outputs</span><span class="pi">:</span>
  <span class="na">default</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">elasticsearch</span>
    <span class="na">hosts</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">http://10.0.2.1:9200</span>
<span class="nn">...</span>
</code></pre></div></div>

<p>When new nodes are added to the production cluster, Elastic Agent will automatically start collecting their metrics without requiring any output changes.</p>

<p>In practice, however, it’s advisable to place <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/configuring-elastic-agent.html#_add_elasticsearch_monitoring_data">a load-balancing proxy in front of master-ineligible nodes</a>.
This distributes monitoring traffic evenly and prevents overloading master nodes, which <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/node-roles-overview.html#master-node-role">must remain stable</a>.</p>

<h2 id="creating-a-load-balancer">Creating a Load Balancer</h2>

<p>Below is an example of setting up a load balancer with a TLS listener that forwards traffic only to master-ineligible data nodes in your Elasticsearch cluster:</p>
<ol>
  <li>Create a target group:
    <ul>
      <li>Target type: <code class="language-plaintext highlighter-rouge">Instances</code></li>
      <li>Target group name: <code class="language-plaintext highlighter-rouge">my-elasticsearch</code></li>
      <li>Protocol: <code class="language-plaintext highlighter-rouge">TCP</code></li>
      <li>Port: <code class="language-plaintext highlighter-rouge">9200</code></li>
      <li>Health check protocol: <code class="language-plaintext highlighter-rouge">TCP</code></li>
      <li>Health check port: <code class="language-plaintext highlighter-rouge">Trafic port</code> (9200 in this case)</li>
      <li>Targets:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-1</code></li>
          <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-2</code></li>
          <li><code class="language-plaintext highlighter-rouge">my-elasticsearch-node-data-3</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Create a networkd load balancer. This is a good choice for Elasticsearch because it has low latency and works well with TCP/TLS passthrough:
    <ul>
      <li>Load balancer name: <code class="language-plaintext highlighter-rouge">my-elasticsearch</code></li>
      <li>Scheme: <code class="language-plaintext highlighter-rouge">Internal</code> (recommended for private networks)</li>
      <li>VPC: (Use the same VPC as your Elasticsearch nodes)</li>
      <li>Security group: (Allow inbound traffic on TCP 443 from trusted sources)</li>
      <li>Listeners:
        <ul>
          <li>Protocol: <code class="language-plaintext highlighter-rouge">TLS</code></li>
          <li>Port: <code class="language-plaintext highlighter-rouge">443</code></li>
          <li>Target group: <code class="language-plaintext highlighter-rouge">my-elasticsearch</code></li>
          <li>Default SSL/TLS server certificate: (Your ACM or imported certificate)</li>
        </ul>
      </li>
      <li>With this setup, clients connect over HTTPS on port 443, and the NLB forwards traffic directly to port 9200 on the data nodes.</li>
    </ul>
  </li>
  <li>Create a DNS record (for example, in Route 53):
    <ul>
      <li>Use the hostname covered by your TLS certificate</li>
      <li>Route to the network load balancer</li>
    </ul>
  </li>
</ol>

<p>After this, clients can reach Elasticsearch using a simple HTTPS URL.
You can use this URL as the Elasticsearch hosts for Elastic Agent instead of listing the IP addresses of individual data nodes.
When you add or remove data nodes, the only thing you need to update is the target group.
The client configuration stays the same, and the load balancer automatically handles routing traffic to the available data nodes.</p>

<h2 id="shell-scripts-for-easier-deployment">Shell Scripts for Easier Deployment</h2>

<p>Often, we need to scale out the cluster to handle more traffic.
Manually repeating every step in this guide is daunting.</p>

<p>To make life easier, here are complete scripts you can use:</p>

<ul>
  <li>For initializing a fresh Elasticsearch cluster: <a href="/assets/posts/39/init_cluster.sh"><code class="language-plaintext highlighter-rouge">init_cluster.sh</code></a></li>
  <li>For joining nodes to an exisiting Elasticsearch cluster: <a href="/assets/posts/39/join_cluster.sh"><code class="language-plaintext highlighter-rouge">join_cluster.sh</code></a></li>
  <li>For initializing a single-node monitoring cluster: <a href="/assets/posts/39/init_monitoring_stack.sh"><code class="language-plaintext highlighter-rouge">init_monitoring_stack.sh</code></a></li>
</ul>

<p>Before running them, make sure you update these values to match your environment:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">VERSION</code></li>
  <li><code class="language-plaintext highlighter-rouge">Timezone</code></li>
  <li><code class="language-plaintext highlighter-rouge">ELASTIC_PASSWORD</code></li>
  <li><code class="language-plaintext highlighter-rouge">ca.crt</code> and <code class="language-plaintext highlighter-rouge">ca.key</code> files (refer to <code class="language-plaintext highlighter-rouge">REPLACE IT</code> in the <code class="language-plaintext highlighter-rouge">join_cluster.sh</code> file)</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Running a self-managed cluster means you own everything.
You operate it, you watch it, and you fix it when it breaks.
Scaling, monitoring, and compliance are all on you.
That work costs time and money, and there’s no way around that.</p>

<p>The upside is what you don’t have.
No extra control planes, no operators, no Kubernetes glue code deciding things on your behalf.
Systems like ECK (Elastic Cloud on Kubernetes) add layers, and layers add complexity.
Complexity creates edge cases, and edge cases could fail in unexpected ways.</p>

<p>If your environment can handle the operational load and you care about clarity, predictability, and knowing exactly how your system behaves, a self-managed cluster can be a good choice.</p>]]></content><author><name>Mienxiu</name></author><category term="aws" /><category term="docker" /><category term="elasticsearch" /><summary type="html"><![CDATA[Deploying a self-managed Elasticsearch cluster for production is a bit of pain. It’s not just about understanding the core components. You also have to manually configure availability and security. To do that, you end up jumping between many pages in the official documentation to piece everything together.]]></summary></entry><entry><title type="html">Systematic Evaluation for Information Retrieval: MAP and nDCG</title><link href="https://mienxiu.com/systematic-evaluation-for-ir-map-and-ndcg/" rel="alternate" type="text/html" title="Systematic Evaluation for Information Retrieval: MAP and nDCG" /><published>2025-09-10T00:00:00+00:00</published><updated>2025-09-10T00:00:00+00:00</updated><id>https://mienxiu.com/systematic-evaluation-for-ir-map-and-ndcg</id><content type="html" xml:base="https://mienxiu.com/systematic-evaluation-for-ir-map-and-ndcg/"><![CDATA[<p>The effectiveness (or accuracy) of an information retrieval system refers to how well it ranks relevant documents higher than non-relevant ones.
Evaluating this effectiveness is important because it allows us to compare different algorithms, helping researchers and developers figure out which ideas and methods actually work better.</p>

<p>So, how do we measure it?
Since information retrieval is fundamentally an empirical problem, its effectiveness has to be judged by users in some way.
One obvious way is to look at what real users do.
Metrics like click-through rate (CTR) and conversion rate fall into this category.
These are often called online metrics.
A/B testing is a popular way to compare different systems using this kind of data.</p>

<p>However, online experiments require deploying systems to actual users, which isn’t always practical or even possible in research settings.
This is where systematic approaches based on offline metrics come in.</p>

<script>
  MathJax = {
    output: {
      displayOverflow: 'scroll'
    },
    tex: {
      inlineMath: {'[+]': [['$', '$']]}
    }
  };
</script>

<script id="MathJax-script" async="" src="https://cdn.jsdelivr.net/npm/mathjax@4/tex-mml-chtml.js"></script>

<h2 id="cranfield-evaluation-methodology">Cranfield Evaluation Methodology</h2>

<p>Cranfield evaluation methodology is a framework for systematically evaluating the effectiveness of information retrieval systems, developed in the 1960s by Cyril Cleverdon at Cranfield University.</p>

<p>This methodology is built upon three key components:</p>
<ul>
  <li>A collection of documents: a standardized set of documents to search against.</li>
  <li>A set of sample queries: representative queries that simulate user needs.</li>
  <li>Human-judged relevance judgments: a definitive list of which documents are considered relevant for each query.</li>
</ul>

<p>Note that relevance judgments is by far the most important and the most labor-intensive part of the evaluation since they rely on human assessors.
A common strategy to manage this is pooling, where assessors only judge a subset of results pulled together from several existing systems.
However, this approach can introduce selection bias.
If a new system retrieves relevant documents that never made it into the pool, those documents remain unjudged, and the system ends up being unfairly penalized.</p>

<p>Once we have all three components, the question becomes: how do we quantify the effectiveness?</p>

<h3 id="precision-and-recall">Precision and Recall</h3>

<p>Precision and recall are core metrics to quantify the effectiveness:</p>
<ul>
  <li>Precision: Of all the documents the system retrieved, how many were actually relevant?</li>
  <li>Recall: Of all the relevant documents, how many did the system retrieve?</li>
</ul>

<p>These concepts can be illustrated using a confusion matrix:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: center">Retrieved</th>
      <th style="text-align: center">Not Retrieved</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Relevant</td>
      <td style="text-align: center">True Positive</td>
      <td style="text-align: center">False Negative</td>
    </tr>
    <tr>
      <td>Not Relevant</td>
      <td style="text-align: center">False Positive</td>
      <td style="text-align: center">True Negative</td>
    </tr>
  </tbody>
</table>

<p>From this matrix, precision and recall are defined as follows:</p>
<ul>
  <li>$Precision = \frac{TP}{TP + FP} = \frac{\text{Number of relevant retrieved}}{\text{Total number of retrieved}}$</li>
  <li>$Recall = \frac{TP}{TP + FN} = \frac{\text{Number of relevant retrieved}}{\text{Total number of relevant}}$</li>
</ul>

<p>Here’s an example.</p>

<p>Suppose we have a small test collection of 10 documents about software:</p>

<table>
  <thead>
    <tr>
      <th>Document ID</th>
      <th>Title</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>Mastering Design Patterns in Python</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>Writing Unit Tests with pytest</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>Introduction to TypeScript</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>Refactoring Legacy Codebases</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>The Practical Test Pyramid</td>
    </tr>
    <tr>
      <td>d6</td>
      <td>Code Review Best Practices</td>
    </tr>
    <tr>
      <td>d7</td>
      <td>SQL Optimization Techniques</td>
    </tr>
    <tr>
      <td>d8</td>
      <td>Static Analysis with ruff</td>
    </tr>
    <tr>
      <td>d9</td>
      <td>Principles of Agile Development</td>
    </tr>
    <tr>
      <td>d10</td>
      <td>Test-Driven Development Explained</td>
    </tr>
  </tbody>
</table>

<p>Our query is “software testing practices”</p>

<p>A human evaluator goes through the entire collection and determines which documents are relevant to the query:</p>

<table>
  <thead>
    <tr>
      <th>Document ID</th>
      <th>Title</th>
      <th>Relevant</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>Mastering Design Patterns in Python</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>Writing Unit Tests with pytest</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>Introduction to TypeScript</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>Refactoring Legacy Codebases</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>The Practical Test Pyramid</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d6</td>
      <td>Code Review Best Practices</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d7</td>
      <td>SQL Optimization Techniques</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d8</td>
      <td>Static Analysis with ruff</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d9</td>
      <td>Principles of Agile Development</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d10</td>
      <td>Test-Driven Development Explained</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>So, there are a total of 5 relevant documents in the collection.</p>

<p>Now, let’s say our retrieval system returns these 4 documents for the query:</p>

<ul>
  <li>d2: Writing Unit Tests with pytest</li>
  <li>d5: The Practical Test Pyramid</li>
  <li>d9: Principles of Agile Development</li>
  <li>d10: Test-Driven Development Explained</li>
</ul>

<p>Out of these 4, three are actually relevant (d2, d5, d10), while one (d9) is a false alarm.
So:</p>
<ul>
  <li>Precision = 3 / 4 = 0.75</li>
  <li>Recall = 3 / 5 = 0.6</li>
</ul>

<p>In Python:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">retrieved_docs</span> <span class="o">=</span> <span class="p">[</span><span class="s">"d2"</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">,</span> <span class="s">"d9"</span><span class="p">,</span> <span class="s">"d10"</span><span class="p">]</span>
<span class="n">relevant_docs</span> <span class="o">=</span> <span class="p">[</span><span class="s">"d2"</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">,</span> <span class="s">"d6"</span><span class="p">,</span> <span class="s">"d8"</span><span class="p">,</span> <span class="s">"d10"</span><span class="p">]</span>


<span class="k">def</span> <span class="nf">get_precision</span><span class="p">(</span><span class="n">retrieved</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">relevant</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="n">true_positives</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">retrieved</span><span class="p">)</span> <span class="o">&amp;</span> <span class="nb">set</span><span class="p">(</span><span class="n">relevant</span><span class="p">))</span>
    <span class="n">false_positives</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">retrieved</span><span class="p">)</span> <span class="o">-</span> <span class="nb">set</span><span class="p">(</span><span class="n">relevant</span><span class="p">))</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">true_positives</span> <span class="o">+</span> <span class="n">false_positives</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">true_positives</span> <span class="o">/</span> <span class="p">(</span><span class="n">true_positives</span> <span class="o">+</span> <span class="n">false_positives</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span>


<span class="k">def</span> <span class="nf">get_recall</span><span class="p">(</span><span class="n">retrieved</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">relevant</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="n">true_positives</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">retrieved</span><span class="p">)</span> <span class="o">&amp;</span> <span class="nb">set</span><span class="p">(</span><span class="n">relevant</span><span class="p">))</span>
    <span class="n">false_negatives</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">relevant</span><span class="p">)</span> <span class="o">-</span> <span class="nb">set</span><span class="p">(</span><span class="n">retrieved</span><span class="p">))</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">true_positives</span> <span class="o">+</span> <span class="n">false_negatives</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">true_positives</span> <span class="o">/</span> <span class="p">(</span><span class="n">true_positives</span> <span class="o">+</span> <span class="n">false_negatives</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span>

<span class="n">precision</span> <span class="o">=</span> <span class="n">get_precision</span><span class="p">(</span><span class="n">retrieved_docs</span><span class="p">,</span> <span class="n">relevant_docs</span><span class="p">)</span>
<span class="n">recall</span> <span class="o">=</span> <span class="n">get_recall</span><span class="p">(</span><span class="n">retrieved_docs</span><span class="p">,</span> <span class="n">relevant_docs</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Precision: </span><span class="si">{</span><span class="n">precision</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Recall: </span><span class="si">{</span><span class="n">recall</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Precision: 0.75
Recall: 0.60
</code></pre></div></div>

<p>In practice, we often use a <em>cutoff</em> at a fixed number of top-ranked results.
For example:</p>
<ul>
  <li>Precision@5 (P@5): The precision when considering only the top 5 retrieved documents.</li>
  <li>Recall@10 (R@10): The recall when considering only the top 10 results.</li>
</ul>

<p>The ideal system would achieve a precision and recall of 1, meaning it retrieved all relevant documents and no non-relevant ones.
However, these two metrics has a classic trade-off: increasing recall by retrieving more documents often leads to a decrease in precision, and vice-versa.
To balance the two, we use the F-measure.</p>

<h2 id="f-measure">F-Measure</h2>

<p>F-measure (or F-score) is a way to combine precision and recall into a single score.
Instead of just simply averaging them like (P+R)/2, it ensures that both precision and recall matter.
The formula is:</p>

\[F_{\beta} = \frac{(\beta^2 + 1) \times \text{P} \times \text{R}}{(\beta^2 \times \text{P}) + \text{R}}\]

<p>Here, the parameter $\beta$ lets you put more weight on recall (if $\beta&gt;1$) or on precision (if $\beta&lt;1$).</p>

<p>The most common variant is the F1-measure, which calculates the harmonic mean of the two:</p>

\[F_{1} = \frac{2 \times \text{P} \times \text{R}}{\text{P} + \text{R}}\]

<p>In Python:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_f_score</span><span class="p">(</span><span class="n">precision</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">recall</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">beta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">precision</span> <span class="o">+</span> <span class="n">recall</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">(</span><span class="n">beta</span><span class="o">**</span><span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">precision</span> <span class="o">*</span> <span class="n">recall</span> <span class="o">/</span> <span class="p">(</span><span class="n">beta</span><span class="o">**</span><span class="mi">2</span> <span class="o">*</span> <span class="n">precision</span> <span class="o">+</span> <span class="n">recall</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span>
</code></pre></div></div>

<p>With F1-measure, a system cannot achieve a high F1-score without having both reasonably high precision and high recall.
For example,</p>

<table>
  <thead>
    <tr>
      <th>Precision</th>
      <th>Recall</th>
      <th>$F_{1}$-score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0.5</td>
      <td>0.5</td>
      <td>0.5</td>
    </tr>
    <tr>
      <td>0.9</td>
      <td>0.1</td>
      <td>0.18</td>
    </tr>
  </tbody>
</table>

<p>Notice how the last row shows the penalty in action: even though precision is high (0.9), the low recall (0.1) drags the F1-score way down.</p>

<h3 id="limitation">Limitation</h3>

<p>The main drawback of F-measure is that it’s set-based so it ignores the order of results.
A system that ranks the most relevant document at the top gets the same score as one that hides it at the last, even though the user experience is completely different.</p>

<p>To evaluate a ranked list, we need to consider the order of the results.</p>

<h2 id="precision-recall-pr-curve">Precision-Recall (PR) Curve</h2>

<p>The Precision–Recall (PR) curve shows how well a retrieval system balances precision and recall as you move down a ranked list of results.
In simple terms, it helps you see whether the relevant documents are concentrated near the top (good) or scattered deeper in the list (not good).</p>

<p>We create the curve by plotting precision against recall at every rank position $k$.</p>

<p>Let’s walk through an example.</p>

<p>Suppose there are 8 relevant documents in total for a query, and our system manages to retrieve 6 of them within the top 10 results ($k$ = 10):</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Relevant</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d6</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d7</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d8</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d9</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d10</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<p>Now, to build the PR curve, we calculate precision and recall at each step:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Relevant</th>
      <th>Cumulative Relevant</th>
      <th>Precision</th>
      <th>Recall</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>1</td>
      <td>1</td>
      <td>1/1</td>
      <td>1/8</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>1</td>
      <td>2</td>
      <td>2/2</td>
      <td>2/8</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>1</td>
      <td>3</td>
      <td>3/3</td>
      <td>3/8</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>0</td>
      <td>3</td>
      <td>3/4</td>
      <td>3/8</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>1</td>
      <td>4</td>
      <td>4/5</td>
      <td>4/8</td>
    </tr>
    <tr>
      <td>d6</td>
      <td>1</td>
      <td>5</td>
      <td>5/6</td>
      <td>5/8</td>
    </tr>
    <tr>
      <td>d7</td>
      <td>0</td>
      <td>5</td>
      <td>5/7</td>
      <td>5/8</td>
    </tr>
    <tr>
      <td>d8</td>
      <td>1</td>
      <td>6</td>
      <td>6/8</td>
      <td>6/8</td>
    </tr>
    <tr>
      <td>d9</td>
      <td>0</td>
      <td>6</td>
      <td>6/9</td>
      <td>6/8</td>
    </tr>
    <tr>
      <td>d10</td>
      <td>0</td>
      <td>6</td>
      <td>6/10</td>
      <td>6/8</td>
    </tr>
  </tbody>
</table>

<p>When plotting the curve, we usually only take the points where a new relevant document appears so that the curve look smoother.
That gives us the following (recall on the x-axis, precision on the y-axis):</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Precision</th>
      <th>Recall</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>1/1</td>
      <td>1/8</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2/2</td>
      <td>2/8</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>3/3</td>
      <td>3/8</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>4/5</td>
      <td>4/8</td>
    </tr>
    <tr>
      <td>d6</td>
      <td>5/6</td>
      <td>5/8</td>
    </tr>
    <tr>
      <td>d8</td>
      <td>6/8</td>
      <td>6/8</td>
    </tr>
  </tbody>
</table>

<p>And visually:</p>

<p><img src="/assets/posts/38/pr_curve.png" alt="PR curve" /></p>

<p>The curve makes it clear how front-loaded the relevant results are.
If the curve stays high, the system surfaces relevant docs early.
If it drops quickly, it means users would have to dig deeper before finding what they’re looking for.</p>

<p>A system whose curve is higher and more to the right is better.
Therefore, an ideal system would have a constant precision of 1.0 for all recall levels.</p>

<p><img src="/assets/posts/38/ideal_pr_curve.png" alt="Ideal PR curve" /></p>

<p>PR curves are especially useful when comparing multiple retrieval systems.
Take the example below:</p>

<p><img src="/assets/posts/38/comparing_pr_curves.png" alt="Comparing PR curves" /></p>

<p>As System A’s curve is consistently above System B’s, A is clearly superior—it delivers better precision at every level of recall.</p>

<p>However, things get trickier when the curves cross.
Take the example below:</p>

<p><img src="/assets/posts/38/intersecting_pr_curves.png" alt="Intersecting PR curves" /></p>

<p>In this scenario, system A may have higher precision at low recall levels, while system B has higher precision at high recall levels.
The better system depends on the user’s task.
For example, a user doing a quick search might prefer System A’s high initial precision, whereas a researcher needing comprehensive results would prefer System B’s high recall.</p>

<h2 id="mean-average-precision-map">Mean Average Precision (MAP)</h2>

<h3 id="average-precision-ap">Average Precision (AP)</h3>

<p>When PR curves intersect, it’s useful to summarize performance with a single number.
Average Precision (AP) does this by averaging the precision values at the ranks where relevant documents appear:</p>

\[\text{AP}=\frac{1}{R}\sum_{k=i}^{N}P(k)Rel(k)\]

<p>Where:</p>
<ul>
  <li>$R$ is the total number of relevant documents for the query.</li>
  <li>$N$ is the total number of retrieved documents.</li>
  <li>$P(k)$ is the precision at the rank $k$.</li>
  <li>$Rel(k)$ is whether the document at rank $k$ is relevant (1) or not (0).</li>
</ul>

<p class="notice--warning">$R$ is NOT the number of retrieved relevant documents.
If you were to divide by the number of retrieved relevant documents instead, you’d give an unfair advantage to a system that retrieves only a few documents.
This would make the score look artificially high even if the system hardly retrieves anything useful.</p>

<p>AP is rank-sensitive: moving a relevant document higher in the list increases the score, while pushing it down lowers it.
You can also think of it as a practical approximation of the area under the PR curve.</p>

<p>Let’s compute it for our example.
We’ll take the precision at every rank where a relevant document shows up (d1, d2, d3, d5, d6, d8) and average them:</p>

<ul>
  <li>Precision@d1 = 1/1</li>
  <li>Precision@d2 = 2/2</li>
  <li>Precision@d3 = 3/3</li>
  <li>Precision@d5 = 4/5</li>
  <li>Precision@d6 = 5/6</li>
  <li>Precision@d8 = 6/8</li>
</ul>

<p>Now average these values:</p>

\[AP = \frac{\frac{1}{1} + \frac{2}{2} + \frac{3}{3} + \frac{4}{5} + \frac{5}{6} + \frac{6}{8}}{8} \;\approx\; 0.67\]

<p>In Python:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_average_precision</span><span class="p">(</span><span class="n">relevances</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span> <span class="n">num_relevant</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Compute Average Precision (AP) for a single query.
    """</span>
    <span class="k">if</span> <span class="n">num_relevant</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="mf">0.0</span>  <span class="c1"># edge case: no relevant docs for this query
</span>
    <span class="n">precisions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">relevant_found</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">rel</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">relevances</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span>  <span class="c1"># rank positions start at 1
</span>        <span class="k">if</span> <span class="n">rel</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">relevant_found</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="n">precision_at_k</span> <span class="o">=</span> <span class="n">relevant_found</span> <span class="o">/</span> <span class="n">k</span>
            <span class="n">precisions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">precision_at_k</span><span class="p">)</span>

    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">precisions</span><span class="p">)</span> <span class="o">/</span> <span class="n">num_relevant</span>


<span class="c1"># Example usage
</span><span class="n">relevances</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>  <span class="c1"># system output (10 results)
</span><span class="n">num_relevant</span> <span class="o">=</span> <span class="mi">8</span>  <span class="c1"># total relevant docs
</span>
<span class="n">AP</span> <span class="o">=</span> <span class="n">get_average_precision</span><span class="p">(</span><span class="n">docs</span><span class="p">,</span> <span class="n">num_relevant</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Average Precision:"</span><span class="p">,</span> <span class="n">AP</span><span class="p">)</span>
</code></pre></div></div>

<p>Average Precision is great for judging performance on a single query, but it only tells part of the story.
To really understand how well a system works, we need to evaluate it across many queries and then aggregate the results.
That way, the score reflects overall performance rather than just one example.</p>

<h3 id="mean-average-precision-map-1">Mean Average Precision (MAP)</h3>

<p>Mean Average Precision (MAP) is simply the mean of the Average Precision (AP) scores across all queries in the test set:</p>

\[\text{MAP}=\frac{1}{Q}\sum_{q=1}^{Q}\text{AP}(q)\]

<p>Where $Q$ is the total number of queries.</p>

<p>It provides a single, comprehensive number that summarizes the system’s ranking quality.</p>

<p>An alternative is the Geometric Mean Average Precision (gMAP), which uses a geometric mean instead of an arithmetic one.
The choice between MAP and gMAP depends on the evaluation goal:</p>
<ul>
  <li>MAP (Arithmetic Mean): The final score is more heavily influenced by high AP scores. This means MAP gives more weight to performance on “easy” queries where the system does well. It is useful if the goal is to measure overall performance, particularly on popular queries that are often easier.</li>
  <li>gMAP (Geometric Mean): This average is more sensitive to low AP scores. Therefore, gMAP emphasizes performance on “difficult” queries where the system struggles. It is useful if the goal is to improve the system’s performance on its weakest queries.</li>
</ul>

<p>Here’s an example.
Suppose we evaluate three queries and get the following AP scores:</p>

<ul>
  <li>Query 1: 1.0</li>
  <li>Query 2: 0.5</li>
  <li>Query 3: 0.1</li>
</ul>

<p>Then:</p>
<ul>
  <li>MAP = $\frac{1.0 + 0.5 + 0.1}{3} \approx 0.53$</li>
  <li>gMAP = $(1.0 \times 0.5 \times 0.1)^{1/3} \approx 0.37$</li>
</ul>

<p>Here, MAP emphasizes the strong performance on Query 1, while gMAP penalizes the weak score on Query 3 more heavily.</p>

<h3 id="mean-reciprocal-rank-mrr">Mean Reciprocal Rank (MRR)</h3>

<p>For certain search tasks, there is only one correct or relevant document.
This occurs in “known-item searches,” where a user wants to find a single, specific page (e.g., the Youtube homepage), or in some question-answering tasks where there is only one right answer.</p>

<p>In this scenario, the Average Precision calculation simplifies to the Reciprocal Rank:</p>

\[\text{Reciprocal Rank} = \frac{1}{r}\]

<p>where $r$ is the rank position of the single relevant documen.
For example, if the document is at rank 1, the score is 1.
If it’s at rank 2, the score is 0.5, and so on.</p>

<p>The Mean Reciprocal Rank (MRR) is the average of these reciprocal ranks across all queries.</p>

<p>Here’s an example.
Suppose we evaluate three queries and the relevant document appears at:</p>

<ul>
  <li>Query 1: Rank 1 → Reciprocal Rank = 1/1</li>
  <li>Query 2: Rank 3 → Reciprocal Rank = 1/3</li>
  <li>Query 3: Rank 2 → Reciprocal Rank = 1/2</li>
</ul>

<p>Then</p>
<ul>
  <li>MRR = $\frac{1/1 + 1/3 + 1/2}{3} \approx 0.61$</li>
</ul>

<h3 id="limitation-1">Limitation</h3>

<p>MAP and MRR are built on the assumption of binary relevance judgments, either a document is relevant (1) or not (0).
But in reality, some documents are highly relevant, while others are only marginally relevant.</p>

<p>Let’s revisit our earlier example with the query “software testing practices”:</p>

<table>
  <thead>
    <tr>
      <th>Document ID</th>
      <th>Title</th>
      <th>Relevant</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>Mastering Design Patterns in Python</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>Writing Unit Tests with pytest</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>Introduction to TypeScript</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>Refactoring Legacy Codebases</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>The Practical Test Pyramid</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d6</td>
      <td>Code Review Best Practices</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d7</td>
      <td>SQL Optimization Techniques</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d8</td>
      <td>Static Analysis with ruff</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d9</td>
      <td>Principles of Agile Development</td>
      <td>0</td>
    </tr>
    <tr>
      <td>d10</td>
      <td>Test-Driven Development Explained</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>Here, documents like d2, d5, and d10 are highly relevant — they directly address testing practices.
On the other hand, d6 and d8 are only somewhat relevant - they touch on quality practices but don’t focus on testing itself.</p>

<p>To handle this, we need a measure that accounts for graded relevance.</p>

<h2 id="normalized-discounted-cumulative-gain-ndcg">Normalized Discounted Cumulative Gain (nDCG)</h2>

<p>Normalized Discounted Cumulative Gain (nDCG) is a metric for evaluating retrieval systems when relevance judgments are multi-level.
It measures how good a ranking is by considering both the relevance and position.</p>

<p>In nDCG, relevance is usually graded on a scale from 1 to $r$ (where $r$ &gt; 2).
For example, if $r$ = 3, you might define:</p>
<ul>
  <li>1 = not relevant</li>
  <li>2 = somewhat relevant</li>
  <li>3 = highly relevant</li>
</ul>

<p>The formula is:</p>

\[\text{nDCG@}k=\frac{\text{DCG@}k}{\text{IDCG@}k}\]

<p>Let’s walk through an example to see how this works.</p>

<h3 id="gain">Gain</h3>

<p>The first step is assigning a gain value to each document, which is simply its relevance score:</p>

\[G_i=Rel_i\]

<p>Where $G_i$ is the gain of the result at position $i$.</p>

<p>Let’s take a simple example using a 3-level relevance scale ($r$ = 3).
Suppose our system returns 5 results for a query <code class="language-plaintext highlighter-rouge">q1</code>, ranked from to to bottom:</p>

<table>
  <thead>
    <tr>
      <th>Rank</th>
      <th>Document</th>
      <th>Relevance</th>
      <th>Gain</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>d1</td>
      <td>3 (highly relevant)</td>
      <td>3</td>
    </tr>
    <tr>
      <td>2</td>
      <td>d2</td>
      <td>2 (somewhat relevant)</td>
      <td>2</td>
    </tr>
    <tr>
      <td>3</td>
      <td>d3</td>
      <td>1 (not relevant)</td>
      <td>1</td>
    </tr>
    <tr>
      <td>4</td>
      <td>d4</td>
      <td>2 (somewhat relevant)</td>
      <td>2</td>
    </tr>
    <tr>
      <td>5</td>
      <td>d5</td>
      <td>3 (highly relevant)</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

<p>So far, gain is just the raw relevance score.</p>

<h3 id="cumulative-gain-cg">Cumulative Gain (CG)</h3>

<p>Cumulative Gain (CG) is the sum of gains up to rank $k$:</p>

\[\text{CG@k}=\sum_{i=1}^{k}{G_i}\]

<p>For our example:</p>

<table>
  <thead>
    <tr>
      <th>Document</th>
      <th>Gain</th>
      <th>Cumulative Gain</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>3</td>
      <td>3</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
      <td>3 + 2</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>1</td>
      <td>3 + 2 + 1</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>2</td>
      <td>3 + 2 + 1 + 2</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>3</td>
      <td>3 + 2 + 1 + 2 + 3</td>
    </tr>
  </tbody>
</table>

<p>So,</p>

\[\text{CG@5}=3+2+1+2+3=11\]

<p>The problem with CG is that it ignores the rank order.
If you change the top and bottom documents, the CG@5 is still 11.
Clearly, that doesn’t reflect user experience because users care much more about the top results.</p>

<h3 id="discounted-cumulative-gain-dcg">Discounted Cumulative Gain (DCG)</h3>

<p>To make the metric rank-sensitive, the gain at each position is discounted by a logarithmic reduction factor (discount factor).
The formula is:</p>

\[\text{DCG@k}=\sum_{i=1}^{k}\frac{G_i}{\log_2(i+1)}={G_1}+\sum_{i=2}^{k}\frac{G_i}{\log_2(i+1)}\]

<p>Here, the denominator, $\log_2(i+1)$, reduces the contribution of documents as their rank gets lower.
This means a highly relevant document contributes more to the score if it is ranked higher.</p>

<p>For our example:</p>

<table>
  <thead>
    <tr>
      <th>Document</th>
      <th>Gain</th>
      <th>Discount Factor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>3</td>
      <td>1</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
      <td>log3</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>1</td>
      <td>log4</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>2</td>
      <td>log5</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>3</td>
      <td>log6</td>
    </tr>
  </tbody>
</table>

<p>So:</p>

\[\text{DCG@}5=3+\frac{2}{\log3}+\frac{1}{\log4}+\frac{2}{\log5}+\frac{3}{\log6} \approx 6.78\]

<p>To compute this with Python, we first define two dictionaries:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">qrels_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mi">3</span><span class="p">}}</span>
<span class="n">run_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mf">0.7</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mf">0.6</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">}}</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">qrels_dict</code>: the relevance judgments for each query and document.</li>
  <li><code class="language-plaintext highlighter-rouge">run_dict</code>: the system’s predicted ranking scores. The exact values don’t matter for DCG as long as they preserve the order since what we ultimately care about is the ranking of documents.</li>
</ul>

<p>A function to compute DCG:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">math</span>


<span class="k">def</span> <span class="nf">get_dcg</span><span class="p">(</span><span class="n">qrels_dict</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">run_dict</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Compute DCG@k for a specific query.
    """</span>
    <span class="c1"># Extract documents for the specific query
</span>    <span class="n">query_qrels</span> <span class="o">=</span> <span class="n">qrels_dict</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="p">{})</span>
    <span class="n">query_run</span> <span class="o">=</span> <span class="n">run_dict</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="p">{})</span>

    <span class="c1"># Sort documents by their scores in run_dict (descending order)
</span>    <span class="n">sorted_docs</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">query_run</span><span class="p">.</span><span class="n">items</span><span class="p">(),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">dcg</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">sorted_docs</span><span class="p">))):</span>
        <span class="n">doc_id</span> <span class="o">=</span> <span class="n">sorted_docs</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
        <span class="n">relevance</span> <span class="o">=</span> <span class="n">query_qrels</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">doc_id</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>  <span class="c1"># default to 0 if doc not in qrels
</span>        <span class="n">rank</span> <span class="o">=</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">rank</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">dcg</span> <span class="o">+=</span> <span class="n">relevance</span>  <span class="c1"># no log discount for rank 1
</span>        <span class="k">else</span><span class="p">:</span>
            <span class="n">dcg</span> <span class="o">+=</span> <span class="n">relevance</span> <span class="o">/</span> <span class="p">(</span><span class="n">math</span><span class="p">.</span><span class="n">log2</span><span class="p">(</span><span class="n">rank</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>  <span class="c1"># log base 2
</span>    <span class="k">return</span> <span class="n">dcg</span>


<span class="n">qrels_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mi">3</span><span class="p">}}</span>
<span class="n">run_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mf">0.7</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mf">0.6</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">}}</span>

<span class="n">dcg</span> <span class="o">=</span> <span class="n">get_dcg</span><span class="p">(</span><span class="n">qrels_dict</span><span class="p">,</span> <span class="n">run_dict</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="s">"q1"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">dcg</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>6.78
</code></pre></div></div>

<p>Now the ranking matters, but there’s still an issue.
DCG scores aren’t directly comparable across queries.
Imagine one query has only one relevant document and another has five.
Even if both systems ranked perfectly, the DCG for the “easy” query (with lots of relevant docs) will naturally be higher.
Without normalization, comparing those scores is unfair.</p>

<h3 id="normalized-discounted-cumulative-gain-ndcg-1">Normalized Discounted Cumulative Gain (nDCG)</h3>

<p>To solve this, we normalize DCG by dividing it by the ideal DCG (IDCG):</p>

\[\text{nDCG@}k=\frac{\text{DCG@}k}{\text{IDCG@}k}\]

<p>Where IDCG is the DCG of an ideal ranking where all the most relevant documents are at the top.</p>

<p>This scales the score between 0 and 1:
A system with nDCG = 1 means it produced the ideal ranking.
And values below 1 tell you how close (or far) the system is from that ideal.</p>

<p>Suppose the ideal ordering is: [3, 3, 2, 2, 1] for our example:</p>

<table>
  <thead>
    <tr>
      <th>Rank (k)</th>
      <th>Gain</th>
      <th>Discount Factor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>3</td>
      <td>1</td>
    </tr>
    <tr>
      <td>2</td>
      <td>3</td>
      <td>log3</td>
    </tr>
    <tr>
      <td>3</td>
      <td>2</td>
      <td>log4</td>
    </tr>
    <tr>
      <td>4</td>
      <td>2</td>
      <td>log5</td>
    </tr>
    <tr>
      <td>5</td>
      <td>1</td>
      <td>log6</td>
    </tr>
  </tbody>
</table>

<p>(Note that the ideal ordering could also be [3, 3, 3, 3, 2] or even [3, 3, 3, 3, 3], depending on how many highly relevant documents exist in the collection.)</p>

<p>So:</p>

\[\text{IDCG@}5=3+\frac{3}{\log3}+\frac{2}{\log4}+\frac{2}{\log5}+\frac{1}{\log6} \approx 7.14\]

<p>Finally:</p>

\[\text{nDCG@}5=\frac{\text{DCG}@5}{\text{IDCG}@5}=\frac{6.78}{7.14} \approx 0.95\]

<p>In Python:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">math</span>


<span class="k">def</span> <span class="nf">get_dcg</span><span class="p">(</span><span class="n">qrels_dict</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">run_dict</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Compute DCG@k for a specific query.
    """</span>
    <span class="c1"># Extract documents for the specific query
</span>    <span class="n">query_qrels</span> <span class="o">=</span> <span class="n">qrels_dict</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="p">{})</span>
    <span class="n">query_run</span> <span class="o">=</span> <span class="n">run_dict</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="p">{})</span>

    <span class="c1"># Sort documents by their scores in run_dict (descending order)
</span>    <span class="n">sorted_docs</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">query_run</span><span class="p">.</span><span class="n">items</span><span class="p">(),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">dcg</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">sorted_docs</span><span class="p">))):</span>
        <span class="n">doc_id</span> <span class="o">=</span> <span class="n">sorted_docs</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
        <span class="n">relevance</span> <span class="o">=</span> <span class="n">query_qrels</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">doc_id</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>  <span class="c1"># default to 0 if doc not in qrels
</span>        <span class="n">rank</span> <span class="o">=</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">rank</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">dcg</span> <span class="o">+=</span> <span class="n">relevance</span>  <span class="c1"># no log discount for rank 1
</span>        <span class="k">else</span><span class="p">:</span>
            <span class="n">dcg</span> <span class="o">+=</span> <span class="n">relevance</span> <span class="o">/</span> <span class="p">(</span><span class="n">math</span><span class="p">.</span><span class="n">log2</span><span class="p">(</span><span class="n">rank</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>  <span class="c1"># log base 2
</span>
    <span class="k">return</span> <span class="n">dcg</span>


<span class="k">def</span> <span class="nf">get_ndcg</span><span class="p">(</span><span class="n">qrels_dict</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">run_dict</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Compute nDCG@k for a specific query.
    """</span>
    <span class="n">dcg</span> <span class="o">=</span> <span class="n">get_dcg</span><span class="p">(</span><span class="n">qrels_dict</span><span class="p">,</span> <span class="n">run_dict</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">query</span><span class="p">)</span>

    <span class="c1"># Compute ideal DCG (iDCG)
</span>    <span class="n">query_qrels</span> <span class="o">=</span> <span class="n">qrels_dict</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="p">{})</span>
    <span class="n">ideal_relevances</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">query_qrels</span><span class="p">.</span><span class="n">values</span><span class="p">(),</span> <span class="n">reverse</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">idcg</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">ideal_relevances</span><span class="p">))):</span>
        <span class="n">relevance</span> <span class="o">=</span> <span class="n">ideal_relevances</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="n">rank</span> <span class="o">=</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">rank</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">idcg</span> <span class="o">+=</span> <span class="n">relevance</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">idcg</span> <span class="o">+=</span> <span class="n">relevance</span> <span class="o">/</span> <span class="p">(</span><span class="n">math</span><span class="p">.</span><span class="n">log2</span><span class="p">(</span><span class="n">rank</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>

    <span class="k">if</span> <span class="n">idcg</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="mf">0.0</span>  <span class="c1"># Avoid division by zero; if no relevant documents, nDCG is defined as 0
</span>
    <span class="n">ndcg</span> <span class="o">=</span> <span class="n">dcg</span> <span class="o">/</span> <span class="n">idcg</span>

    <span class="k">return</span> <span class="n">ndcg</span>


<span class="n">qrels_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mi">3</span><span class="p">}}</span>
<span class="n">run_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mf">0.7</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mf">0.6</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">}}</span>

<span class="n">ndcg</span> <span class="o">=</span> <span class="n">get_ndcg</span><span class="p">(</span><span class="n">qrels_dict</span><span class="p">,</span> <span class="n">run_dict</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="s">"q1"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">ndcg</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0.95
</code></pre></div></div>

<p>In fact, there’s a Python library <a href="https://github.com/AmenRa/ranx">ranx</a> that handles evaluation metrics like nDCG out of the box.</p>

<p>Here’s how you can compute nDCG@5 using <code class="language-plaintext highlighter-rouge">ranx</code> in just a few lines:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">ranx</span> <span class="kn">import</span> <span class="n">Qrels</span><span class="p">,</span> <span class="n">Run</span><span class="p">,</span> <span class="n">evaluate</span>

<span class="n">qrels_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mi">3</span><span class="p">}}</span>
<span class="n">run_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">"q1"</span><span class="p">:</span> <span class="p">{</span><span class="s">"d1"</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span> <span class="s">"d2"</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">,</span> <span class="s">"d3"</span><span class="p">:</span> <span class="mf">0.7</span><span class="p">,</span> <span class="s">"d4"</span><span class="p">:</span> <span class="mf">0.6</span><span class="p">,</span> <span class="s">"d5"</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">}}</span>

<span class="n">qrels</span> <span class="o">=</span> <span class="n">Qrels</span><span class="p">(</span><span class="n">qrels_dict</span><span class="p">)</span>
<span class="n">run</span> <span class="o">=</span> <span class="n">Run</span><span class="p">(</span><span class="n">run_dict</span><span class="p">)</span>

<span class="n">ndcg</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">(</span><span class="n">qrels</span><span class="p">,</span> <span class="n">run</span><span class="p">,</span> <span class="n">metrics</span><span class="o">=</span><span class="s">"ndcg@5"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">ndcg</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0.95
</code></pre></div></div>

<h3 id="limitation-2">Limitation</h3>

<p>When comparing two systems, looking only at the average nDCG score (or any single metric) isn’t enough.
The question is: “How confident are we that the observed difference isn’t just because of the particular queries we happened to test?”</p>

<p>Imagine two systems:</p>
<ul>
  <li>System A has an average score of 0.20</li>
  <li>System B has an average score of 0.40</li>
</ul>

<p>At first glance, System B looks twice as good.
But let’s dig deeper:</p>
<ul>
  <li>Experiment 1 (low variance): System B beats System A on every single query. Here, it’s safe to say B is better.</li>
  <li>Experiment 2 (high variance): Some queries favor A, others favor B, and results swing wildly. The conclusion is much shakier.</li>
</ul>

<p>This is where statistical significance tests come in.</p>

<h2 id="statistical-significance-testing">Statistical Significance Testing</h2>

<p>Statistical significance testing answers the question: “Is the observed difference real, or could it have happened by chance?”</p>

<h3 id="sign-test">Sign Test</h3>

<p>One of the simplest approaches is the sign test.
Instead of looking at averages, it counts how many times each system wins across queries.</p>

<p>Here’s an example with five queries and scores for two IR systems:</p>

<table>
  <thead>
    <tr>
      <th>Query</th>
      <th>System A</th>
      <th>System B</th>
      <th>Winner</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>q1</td>
      <td>0.28</td>
      <td>0.35</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q2</td>
      <td>0.30</td>
      <td>0.20</td>
      <td>A</td>
    </tr>
    <tr>
      <td>q3</td>
      <td>0.38</td>
      <td>0.40</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q4</td>
      <td>0.29</td>
      <td>0.33</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q5</td>
      <td>0.23</td>
      <td>0.24</td>
      <td>B</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>Wins for System A: 1</li>
  <li>Wins for System B: 4</li>
</ul>

<p>Here, System B clearly dominates by beating System A in 4 out of 5 queries.
That’s a strong sign that B is better than A, at least on this test set.</p>

<h3 id="limitation-3">Limitation</h3>

<p>But here’s where things get tricky.
Let’s extend the test set with four more queries:</p>

<table>
  <thead>
    <tr>
      <th>Query</th>
      <th>System A</th>
      <th>System B</th>
      <th>Winner</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>q1</td>
      <td>0.28</td>
      <td>0.35</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q2</td>
      <td>0.30</td>
      <td>0.20</td>
      <td>A</td>
    </tr>
    <tr>
      <td>q3</td>
      <td>0.38</td>
      <td>0.40</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q4</td>
      <td>0.29</td>
      <td>0.33</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q5</td>
      <td>0.23</td>
      <td>0.24</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q6</td>
      <td>0.30</td>
      <td>0.18</td>
      <td>A</td>
    </tr>
    <tr>
      <td>q7</td>
      <td>0.21</td>
      <td>0.24</td>
      <td>B</td>
    </tr>
    <tr>
      <td>q8</td>
      <td>0.30</td>
      <td>0.18</td>
      <td>A</td>
    </tr>
    <tr>
      <td>q9</td>
      <td>0.34</td>
      <td>0.18</td>
      <td>A</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>Wins for System A: 4</li>
  <li>Wins for System B: 5</li>
</ul>

<p>Now, things don’t look so clear anymore.
Even though System B has more wins overall, the margin is just a single query.
With such small differences, it’s hard to say whether B is truly the better system or if the result is just due to chance.
The sign test doesn’t capture how big the improvements are, only who won more often.
To deal with this limitation, researchers often use more advanced significance tests, like the Wilcoxon signed-rank test.</p>

<h2 id="conclusion">Conclusion</h2>

<p>For comparing different IR systems, MAP and nDCG are useful tools.
They capture both relevance and ranking quality in a way that’s more informative than simple set-based measures.</p>

<p>But again, information retrieval is fundamentally an empirical problem.
What a human assessor marks as “relevant” might not line up with what real users actually care about in the real-world context of search.</p>

<p>The takeaway is that to make reliable decisions, we need to evaluate across many queries.
That’s how we avoid shaky averages and get confidence that one system is truly better than another.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#">Evaluation measures (information retrieval) - Wikipedia</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Discounted_cumulative_gain">Discounted cumulative gain - Wikipedia</a></li>
  <li><a href="https://www.coursera.org/learn/text-retrieval">Text Retrieval and Search Engines by University of Illinois Urbana-Champaign</a></li>
</ul>]]></content><author><name>Mienxiu</name></author><category term="ir" /><category term="python" /><summary type="html"><![CDATA[The effectiveness (or accuracy) of an information retrieval system refers to how well it ranks relevant documents higher than non-relevant ones. Evaluating this effectiveness is important because it allows us to compare different algorithms, helping researchers and developers figure out which ideas and methods actually work better.]]></summary></entry><entry><title type="html">Understanding BM25 in Text Retrieval</title><link href="https://mienxiu.com/understanding-bm25/" rel="alternate" type="text/html" title="Understanding BM25 in Text Retrieval" /><published>2025-08-27T00:00:00+00:00</published><updated>2025-08-27T00:00:00+00:00</updated><id>https://mienxiu.com/understanding-bm25</id><content type="html" xml:base="https://mienxiu.com/understanding-bm25/"><![CDATA[<p>When you type a query into a search engine, it finds documents that contain your keywords.
The problem is: <strong>how do you order those documents from most to least relevant</strong>?</p>

<script>
  MathJax = {
    output: {
      displayOverflow: 'scroll'
    },
    tex: {
      inlineMath: {'[+]': [['$', '$']]}
    }
  };
</script>

<script id="MathJax-script" async="" src="https://cdn.jsdelivr.net/npm/mathjax@4/tex-mml-chtml.js"></script>

<p>Search engines use a ranking function to score document relevance.
We write it as $f(q,d)$, where $q$ is the query and $d$ is a document.
The higher the $f(q,d)$, the higher the document appears in the search results.</p>

<p>Although various methods exist to estimate document relevance, BM25 (Best Matching 25) is widely regarded as the most practical and effective ranking function for this task.
It’s been around since the 90s, and despite newer ranking models, BM25 is still a strong baseline and often tough to beat in real-world applications.</p>

<p>The BM25 formula looks like this:</p>

\[f(q,d) = \sum_{i=1}^{n}\frac{c(t_i,d)(k+1)}{c(t_i,d)+k(1-b+b\frac{|d|}{avdl})}(\log\frac{N+1}{df(t)+1}+1)\]

<p>At its heart, BM25 is just combining three heuristics:</p>
<ul>
  <li>Term Frequency (TF)</li>
  <li>Inverse Document Frequencey (IDF)</li>
  <li>Document Length Normalization</li>
</ul>

<p>In this post, I will explain each heuristic and show how they come together to form the BM25 formula.</p>

<h2 id="term-frequency-tf">Term Frequency (TF)</h2>

<p>The simplest idea: if a query term appears more often in a document, that document is probably more relevant.</p>

<p>A TF-only ranking function’s formula can be represented as this:</p>

\[f(q,d) = \sum_{i=1}^{n}TF(t_i,d)\]

<p>Where:</p>
<ul>
  <li>$f(q,d)$ is the relevance score of document $d$ for query $q$.</li>
  <li>$\sum_{i=1}^{n}$ is the summation over all terms ($t_1$, $t_2$, …, $t_n$) in the query $q$.</li>
  <li>$TF(t_i,d)$ is the frequency score of query term $t_i$ in $d$.</li>
</ul>

<p>Then how do we define $TF()$?</p>

<p>A naive TF is just to apply a linear count; $y=x$.
Therefore:</p>

\[TF(t,d) = c(t,d)\]

<p>Where $c(t,d)$ is the raw count of term <code class="language-plaintext highlighter-rouge">t</code> in document <code class="language-plaintext highlighter-rouge">d</code>.</p>

<p><img src="/assets/posts/37/linear_tf.png" alt="Linear TF" /></p>

<p>Let’s see an example.
Suppose we have two documents in our collection:</p>
<ul>
  <li>d1: The Bank of Korea is expected to lower its benchmark interest rate next month.</li>
  <li>d2: A lower interest rate will be welcomed by indebted households.</li>
</ul>

<p>Query: “korea interest rate”</p>

<p>Note that this query contains three terms: {$t_1$, $t_2$, $t_3$} = {“Korea”, “interest”, “rate”}</p>

<p>Under this linear application of the raw count, the scores are:</p>

<table>
  <thead>
    <tr>
      <th>documents</th>
      <th>$f(q,d)$</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>3</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<p>Here, <code class="language-plaintext highlighter-rouge">d2</code> contains two of the query terms—”interest” and “rate”—but misses “Korea”, and <code class="language-plaintext highlighter-rouge">d1</code> matches all three.
This ordering makes intuitive sense: <code class="language-plaintext highlighter-rouge">d1</code> fully matches the query, so it gets a higher score.</p>

<p>But there’s a problem: just repeating a term many times can inflate the score too much.
This opens the door to what’s called <em>term spamming</em>.
Term spamming is when a document unnaturally repeats certain keywords over and over to trick the ranking function into thinking it’s more relevant.</p>

<p>Suppose we add one more document to the collection:</p>
<ul>
  <li>d3: The interest rate charged on loans is often higher than the interest rate paid on deposits.</li>
</ul>

<p>With this new collection, the scores are:</p>

<table>
  <thead>
    <tr>
      <th>documents</th>
      <th>$f(q,d)$</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>3</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>4</td>
    </tr>
  </tbody>
</table>

<p>Obviously, this is not what we want.
Instead, we’d prefer a system where repetition still helps, but each extra occurrence of the same word contributes less and less to the score.
In other words, diminishing returns from repetition.</p>

<h3 id="tf-transformation">TF Transformation</h3>

<p>One simple fix is to apply a sublinear transform like logarithmic transformation; $y=\log(x+1)$.
Therefore:</p>

\[TF(t,d) = \log(c(t,d)+1)\]

<p>(1 is added to avoid $\log(0)$.)</p>

<p>This way, the impact of each extra occurrence shrinks.</p>

<p><img src="/assets/posts/37/logarithmic_tf.png" alt="Logarithmic TF" /></p>

<p>Still, it’s not perfect - TF is unbounded, meaning there’s no upper limit.
A spammy document can still rack up arbitrarily high scores if it repeats a term enough times.</p>

<p>To improve it more, here comes a bounded transformation; $y=\frac{x(k+1)}{x+k}$.
Therefore:</p>

\[TF(t,d) = \frac{c(t,d)(k+1)}{c(t,d)+k}\]

<p>This function saturates at $(k+1)$, no matter how large $x$ gets.
In other words, the function is upper-bounded by $k+1$.</p>

<p><img src="/assets/posts/37/bounded_tf.png" alt="Bounded TF" /></p>

<p>Not only is this transformation capped at $(k+1)$—which stops a single term’s frequency from completely dominating the score—it’s also flexible.
The parameter $k$ controls how quickly the curve saturates:</p>
<ul>
  <li>If $k=0$, it reduces to a simple binary scheme: any occurrence of the term contributes a score of 1, no matter how many times it appears.</li>
  <li>If $k$ is very large, the function behaves almost linearly, getting closer to the linear term frequency.</li>
</ul>

<p>With this, our TF-based ranking function becomes:</p>

\[f(q,d) = \sum_{i=1}^{n}\frac{c(t_i,d)(k+1)}{c(t_i,d)+k}\]

<p>This TF function can be implemented in Python like below:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">tf</span><span class="p">(</span><span class="n">term</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.2</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate TF score using bounded transformation.
    """</span>
    <span class="c1"># Apply some basic text normalization: 1. punctuation handling 2. lowercasing
</span>    <span class="n">doc_terms</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"."</span><span class="p">,</span> <span class="s">" "</span><span class="p">).</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">()</span>
    <span class="n">term_count</span> <span class="o">=</span> <span class="n">doc_terms</span><span class="p">.</span><span class="n">count</span><span class="p">(</span><span class="n">term</span><span class="p">.</span><span class="n">lower</span><span class="p">())</span>
    <span class="n">score</span> <span class="o">=</span> <span class="n">term_count</span> <span class="o">*</span> <span class="p">(</span><span class="n">k</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">term_count</span> <span class="o">+</span> <span class="n">k</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">score</span>


<span class="k">def</span> <span class="nf">tf_ranking_function</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.2</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate TF score of a document with respect to a query.
    """</span>
    <span class="n">score</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">tf</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">document</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span> <span class="k">for</span> <span class="n">term</span> <span class="ow">in</span> <span class="n">query</span><span class="p">.</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">())</span>
    <span class="k">return</span> <span class="n">score</span>
</code></pre></div></div>

<p>To keep things simple, this implementation only applies a couple of basic text normalization steps—removing punctuation and lowercasing.
In a real search engine, though, you’d usually go further with more steps such as stemming, lemmatization, and stopword removal to make scoring more robust and accurate.</p>

<p>We can use this code to get the relevance score of a document for a given query.
For instance:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">score</span> <span class="o">=</span> <span class="n">tf_ranking_function</span><span class="p">(</span>
    <span class="n">query</span><span class="o">=</span><span class="s">"korea interest rate"</span><span class="p">,</span>
    <span class="n">document</span><span class="o">=</span><span class="s">"The Bank of Korea is expected to lower its benchmark interest rate next month."</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">score</span><span class="p">)</span>
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3.0
</code></pre></div></div>

<p>The scores for different <code class="language-plaintext highlighter-rouge">k</code> values are shown below:</p>

<table>
  <thead>
    <tr>
      <th>documents</th>
      <th>$f(q,d)$ ($k$=1.2)</th>
      <th>$f(q,d)$ ($k$=1.6)</th>
      <th>$f(q,d)$ ($k$=2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>3</td>
      <td>3</td>
      <td>3</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
      <td>2</td>
      <td>2</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>2.75</td>
      <td>2.89</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

<p>This illustrates the role of $k$: a smaller $k$ dampens the impact of raw term frequency, while a larger $k$ makes the function behave closer to linear counting.</p>

<p>The key takeaway here is that with this transformation, <code class="language-plaintext highlighter-rouge">d3</code> no longer unfairly outranks <code class="language-plaintext highlighter-rouge">d1</code> just because it happens to repeat the query terms.</p>

<h3 id="limitation">Limitation</h3>

<p>TF alone isn’t enough to reliably measure relevance because not all words are equally informative.</p>

<p>As an example, consider one more document that contains three “interest rate”s:</p>
<ul>
  <li>d4: The interest rate remains unchanged, but many fear this interest rate keeps loans costly while others welcome a stable interest rate.</li>
</ul>

<p>Using our TF-only ranking function for the same query “korea interest rate”, repetition wins out:</p>

<table>
  <thead>
    <tr>
      <th>documents</th>
      <th>$f(q,d)$ ($k$=1.2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>3</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>2.75</td>
    </tr>
    <tr>
      <td>d4</td>
      <td><strong>3.14</strong></td>
    </tr>
  </tbody>
</table>

<p>As you can see, <code class="language-plaintext highlighter-rouge">d4</code> comes out on top even though it never mentions “Korea”, which is likely to be a crucial term.
That’s not what we want.</p>

<p>That’s where Inverse Document Frequency (IDF) comes in.</p>

<h2 id="inverse-document-frequencey-idf">Inverse Document Frequencey (IDF)</h2>

<p>Inverse Document Frequency (IDF) solves the problem of treating all words equally.
The intuition is that rare words are more informative than common ones.
For example, a rarer word like “Korea” can be a strong signal that a document is actually about the topic we care about.
On the other hand, common words like “the” or “of” show up in almost every document, so they don’t really help us figure out which document is more relevant.</p>

<p>At its core, IDF penalizes overly common terms that appear everywhere and rewards rarer, more descriptive terms that help distinguish one document from another.</p>

<p>When we combine TF with IDF, we get the TF–IDF ranking function:</p>

\[f(q,d) = \sum_{i=1}^{n}TF(t_i,d)IDF(t_i)\]

<p>Then how do we define $IDF()$?</p>

<p>The standard notation of IDF is this:</p>

\[IDF(t) = \log\frac{N}{df(t)}\]

<p>Where:</p>
<ul>
  <li>$N$ is the total number of documents in the collection. (cardinality of documents)</li>
  <li>$df(t)$ is the number of documents containing the term $t$. ($df$ stands for document frequency.)</li>
</ul>

<p>In words, a word with a low document frequency (a rare word) will have a high IDF score, while a very common word will have a low IDF score.</p>

<p>In practice, however, smoothed IDF variants are often used.</p>

<h3 id="smoothing">Smoothing</h3>

<p>In statistics and NLP, smoothing generally means adjusting counts slightly to avoid extreme cases like zeros or infinities.</p>

<p>One of the most common IDF variants with smoothing is this:</p>

\[IDF(t) = \log\frac{N+1}{df(t)+1} + 1\]

<p class="notice--info">This is also what <a href="https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html">scikit-learn</a> uses when calculating IDF.</p>

<p>Here’s why the formula has all those extra <code class="language-plaintext highlighter-rouge">+1</code>s:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">+1</code> in the denominator: Without it, if a term never appears in the collection ($df(t) = 0$), you’d end up dividing by zero. The <code class="language-plaintext highlighter-rouge">+1</code> smooths this out so the math always works. You can think of it as “Even if the term is unseen, let’s pretend it appeared at least once.”</li>
  <li><code class="language-plaintext highlighter-rouge">+1</code> in the numerator: If a term shows up in every document ($df(t) = N$), then $\log\frac{N}{N} = \log(1) = 0$. That would make the term’s weight vanish completely. By adding 1 to the numerator, those super-common terms still get a tiny but non-zero weight.</li>
  <li><code class="language-plaintext highlighter-rouge">+1</code> outside the log: Even with smoothing inside the fraction, the log value can hover around zero or dip negative when terms are extremely common. Negative IDF values don’t make sense—TF-IDF weights should never imply “anti-importance.” Adding 1 outside guarantees that IDF stays positive, avoiding zeros or negatives.</li>
</ul>

<p>Graphically:</p>

<p><img src="/assets/posts/37/idf.png" alt="idf" /></p>

<p>With this smoothed IDF formula, our TF-IDF ranking function becomes:</p>

\[f(q,d) = \sum_{i=1}^{n}\frac{c(t_i,d)(k+1)}{c(t_i,d)+k}(\log\frac{N+1}{df(t)+1}+1)\]

<p>In Python:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">math</span>


<span class="k">def</span> <span class="nf">tf</span><span class="p">(</span><span class="n">term</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.2</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate TF score using bounded transformation.
    """</span>
    <span class="c1"># Apply some basic text normalization: 1. punctuation handling 2. lowercasing
</span>    <span class="n">doc_terms</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"."</span><span class="p">,</span> <span class="s">" "</span><span class="p">).</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">()</span>
    <span class="n">term_count</span> <span class="o">=</span> <span class="n">doc_terms</span><span class="p">.</span><span class="n">count</span><span class="p">(</span><span class="n">term</span><span class="p">.</span><span class="n">lower</span><span class="p">())</span>
    <span class="n">score</span> <span class="o">=</span> <span class="n">term_count</span> <span class="o">*</span> <span class="p">(</span><span class="n">k</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">term_count</span> <span class="o">+</span> <span class="n">k</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">score</span>


<span class="k">def</span> <span class="nf">idf</span><span class="p">(</span><span class="n">term</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">documents</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate Inverse Document Frequency (IDF) for a term.
    """</span>
    <span class="n">N</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">documents</span><span class="p">)</span>  <span class="c1"># Total number of documents
</span>    <span class="n">df_t</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">documents</span><span class="p">:</span>
        <span class="c1"># Apply some basic text normalization: 1. punctuation handling 2. lowercasing
</span>        <span class="k">if</span> <span class="n">term</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">doc</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"."</span><span class="p">,</span> <span class="s">" "</span><span class="p">).</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">():</span>
            <span class="n">df_t</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="n">score</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="n">log</span><span class="p">((</span><span class="n">N</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">df_t</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span> <span class="o">+</span> <span class="mi">1</span>

    <span class="k">return</span> <span class="n">score</span>


<span class="k">def</span> <span class="nf">tf_idf_ranking_function</span><span class="p">(</span>
    <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">documents</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">k</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.2</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate TF-IDF score
    """</span>
    <span class="n">query_terms</span> <span class="o">=</span> <span class="n">query</span><span class="p">.</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">()</span>
    <span class="n">score</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">term</span> <span class="ow">in</span> <span class="n">query_terms</span><span class="p">:</span>
        <span class="n">tf_score</span> <span class="o">=</span> <span class="n">tf</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">document</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
        <span class="n">idf_score</span> <span class="o">=</span> <span class="n">idf</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span>
        <span class="n">score</span> <span class="o">+=</span> <span class="n">tf_score</span> <span class="o">*</span> <span class="n">idf_score</span>

    <span class="k">return</span> <span class="n">score</span>
</code></pre></div></div>

<p>Let’s revisit the query “korea interest rate” and four documents:</p>
<ul>
  <li>d1: The Bank of Korea is expected to lower its benchmark interest rate next month.</li>
  <li>d2: A lower interest rate will be welcomed by indebted households.</li>
  <li>d3: The interest rate charged on loans is often higher than the interest rate paid on deposits.</li>
  <li>d4: The interest rate remains unchanged, but many fear this interest rate keeps loans costly while others welcome a stable interest rate.</li>
</ul>

<p>Using TF-IDF ranking function, the scores are:</p>

<table>
  <thead>
    <tr>
      <th>documents</th>
      <th>$f(q,d)$ ($k$=1.2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td><strong>3.92</strong></td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>2.75</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>3.14</td>
    </tr>
  </tbody>
</table>

<p>The ranking now matches user intent so that <code class="language-plaintext highlighter-rouge">d1</code> rises above <code class="language-plaintext highlighter-rouge">d4</code> because it contains the rarer term “Korea”.</p>

<h3 id="limitation-1">Limitation</h3>

<p>The ranking models discussed so far are inherently biased toward long documents.
A longer document has a statistically higher chance of matching query terms, even if the matches are coincidental and scattered throughout the text.</p>

<p>Let’s look at one more document:</p>
<ul>
  <li>d5: In South Korea, the central bank’s decision on the interest rate is closely watched by both businesses and households. Rising interest rate levels have slowed consumer spending, while exporters in Korea argue that a stable interest rate is necessary to remain competitive. Many in Korea believe that future growth depends on how carefully the government manages the interest rate policy.</li>
</ul>

<p>Notice how <code class="language-plaintext highlighter-rouge">d5</code> is stuffed with the words Korea and interest rate.
TF-IDF would likely push this document to the very top, not because it’s the most relevant, but simply because it’s long and keeps repeating the keywords.
The scores are:</p>

<table>
  <thead>
    <tr>
      <th>documents</th>
      <th>$f(q,d)$ ($k$=1.2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>3.69</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>2.75</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>3.14</td>
    </tr>
    <tr>
      <td>d5</td>
      <td><strong>5.71</strong></td>
    </tr>
  </tbody>
</table>

<p>That’s where Document Length Normalization comes in.</p>

<h2 id="document-length-normalization">Document Length Normalization</h2>

<p>The last heuristic is that longer documents naturally contain more terms, and therefore match queries more often.
To counter this, their scores are adjusted relative to the average document length.
The goal is simple: balance the bias toward long documents.
In short, don’t reward a document just for being long.</p>

<p>Penalizing long documents must be done carefully to avoid over-penalization since a document can be long either because:</p>
<ul>
  <li>It’s verbose: Some texts are just wordy. They take a simple idea and wrap it in way too many words. In this case, penalization makes sense.</li>
  <li>It’s comprehensive: Other texts are long because they actually cover more ground (like a news archive that bundles many different stories). These shouldn’t be punished too harshly, because their length comes from offering more useful, distinct content.</li>
</ul>

<h3 id="pivoted-length-normalization">Pivoted Length Normalization</h3>

<p>A robust solution to this problem is Pivoted Length Normalization.
This method uses the average document length (avdl) of the entire collection as a “pivot” or reference point.</p>

<p>Here’s how it works in practice:</p>
<ul>
  <li>Documents with a length equal to the <code class="language-plaintext highlighter-rouge">avdl</code> receive no penalty or reward (normalizer = 1).</li>
  <li>Documents longer than the <code class="language-plaintext highlighter-rouge">avdl</code> are penalized.</li>
  <li>Documents shorter than the <code class="language-plaintext highlighter-rouge">avdl</code> are rewarded.</li>
</ul>

<p>The formula for the pivoted normalizer is:</p>

\[normalizer = 1 - b + b \frac{|d|}{avdl}\]

<p>Where:</p>
<ul>
  <li>$|d|$ is the length of the document (number of terms).</li>
  <li>$avdl$ is the average document length across the whole collection.</li>
  <li>$b$ is a tuning parameter between 0 and 1.
    <ul>
      <li>If $b$ = 0, the normalizer is always 1, and no length normalization occurs.</li>
      <li>As $b$ increases towards 1, the penalty for long documents and the reward for short documents become more aggressive.</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/posts/37/pivoted_length_normalization.png" alt="Pivoted Length Normalization" /></p>

<p>So where does this normalizer actually get applied?</p>

<p>In BM25, it slides into the denominator of the TF term.
Here’s the BM25 TF function:</p>

\[TF'(t,d) = \frac{c(t,d)(k+1)}{c(t,d)+k(1-b+b\frac{|d|}{avdl})}\]

<p>Finally, we arrive at the BM25 (Okapi) ranking function by integrating all three heuristics—TF Transformation, IDF Weighting, and Pivoted Length Normalization:</p>

\[f(q,d) = \sum_{i=1}^{n}\frac{c(t_i,d)(k+1)}{c(t_i,d)+k(1-b+b\frac{|d|}{avdl})}(\log\frac{N+1}{df(t)+1}+1)\]

<p>Where:</p>
<ul>
  <li>$k$ is the term frequency saturation parameter</li>
  <li>$b$ is the document-length normalization weight (0–1)</li>
</ul>

<p>In Python:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">math</span>


<span class="k">def</span> <span class="nf">tf_with_normalizer</span><span class="p">(</span><span class="n">term</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.2</span><span class="p">,</span> <span class="n">normalizer</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate TF score for a term using BM25 with document length normalization.
    """</span>
    <span class="c1"># Apply some basic text normalization: 1. punctuation handling 2. lowercasing
</span>    <span class="n">doc_terms</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"."</span><span class="p">,</span> <span class="s">" "</span><span class="p">).</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">()</span>
    <span class="n">term_count</span> <span class="o">=</span> <span class="n">doc_terms</span><span class="p">.</span><span class="n">count</span><span class="p">(</span><span class="n">term</span><span class="p">.</span><span class="n">lower</span><span class="p">())</span>
    <span class="n">score</span> <span class="o">=</span> <span class="n">term_count</span> <span class="o">*</span> <span class="p">(</span><span class="n">k</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">term_count</span> <span class="o">+</span> <span class="n">k</span> <span class="o">*</span> <span class="n">normalizer</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">score</span>


<span class="k">def</span> <span class="nf">idf</span><span class="p">(</span><span class="n">term</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">documents</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate Inverse Document Frequency (IDF) for a term.
    """</span>
    <span class="n">N</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">documents</span><span class="p">)</span>  <span class="c1"># Total number of documents
</span>    <span class="n">df_t</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">documents</span><span class="p">:</span>
        <span class="c1"># Apply some basic text normalization: 1. punctuation handling 2. lowercasing
</span>        <span class="k">if</span> <span class="n">term</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">doc</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"."</span><span class="p">,</span> <span class="s">" "</span><span class="p">).</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">():</span>
            <span class="n">df_t</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="n">score</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="n">log</span><span class="p">((</span><span class="n">N</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">df_t</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span> <span class="o">+</span> <span class="mi">1</span>

    <span class="k">return</span> <span class="n">score</span>


<span class="k">def</span> <span class="nf">document_length_normalization</span><span class="p">(</span><span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">documents</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">b</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.75</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate document length normalization factor.
    """</span>
    <span class="c1"># Calculate average document length
</span>    <span class="n">total_length</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">doc</span><span class="p">.</span><span class="n">split</span><span class="p">())</span> <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">documents</span><span class="p">)</span>
    <span class="n">avdl</span> <span class="o">=</span> <span class="n">total_length</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">documents</span><span class="p">)</span>
    <span class="n">doc_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">document</span><span class="p">.</span><span class="n">split</span><span class="p">())</span>
    <span class="n">normalizer</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">b</span> <span class="o">+</span> <span class="n">b</span> <span class="o">*</span> <span class="p">(</span><span class="n">doc_length</span> <span class="o">/</span> <span class="n">avdl</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">normalizer</span>


<span class="k">def</span> <span class="nf">bm25_ranking_function</span><span class="p">(</span>
    <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">documents</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">k</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.2</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.75</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculate BM25 score for a document given a query.

    Args:
        query: The query string.
        document: The document string.
        documents: The list of all documents. (collection)
        k: The term frequency saturation parameter.
        b: The length normalization parameter.
    """</span>
    <span class="n">terms</span> <span class="o">=</span> <span class="n">query</span><span class="p">.</span><span class="n">lower</span><span class="p">().</span><span class="n">split</span><span class="p">()</span>
    <span class="n">normalizer</span> <span class="o">=</span> <span class="n">document_length_normalization</span><span class="p">(</span><span class="n">document</span><span class="p">,</span> <span class="n">documents</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
    <span class="n">score</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">term</span> <span class="ow">in</span> <span class="n">terms</span><span class="p">:</span>
        <span class="n">tf_score</span> <span class="o">=</span> <span class="n">tf_with_normalizer</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">document</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">normalizer</span><span class="p">)</span>
        <span class="n">idf_score</span> <span class="o">=</span> <span class="n">idf</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span>
        <span class="n">score</span> <span class="o">+=</span> <span class="n">tf_score</span> <span class="o">*</span> <span class="n">idf_score</span>

    <span class="k">return</span> <span class="n">score</span>
</code></pre></div></div>

<p>Using the BM25 ranking function, the scores for our example become more acceptable:</p>

<table>
  <thead>
    <tr>
      <th>documents</th>
      <th>$f(q,d)$ ($k$=1.2, $b$=0.75)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>d1</td>
      <td>4.46</td>
    </tr>
    <tr>
      <td>d2</td>
      <td>2.63</td>
    </tr>
    <tr>
      <td>d3</td>
      <td>3.04</td>
    </tr>
    <tr>
      <td>d4</td>
      <td>3.23</td>
    </tr>
    <tr>
      <td>d5</td>
      <td>4.34</td>
    </tr>
  </tbody>
</table>

<p>What happens here is that even though <code class="language-plaintext highlighter-rouge">d5</code> is long and loaded with repeated mentions of Korea and interest rate, it doesn’t unfairly dominate the ranking anymore.</p>

<h2 id="conclusion">Conclusion</h2>

<p>BM25 is the backbone for many retrieval systems because it’s simple, fast, and surprisingly effective.
But here’s one clear limitation: BM25 doesn’t actually understand context.
It sees words, counts them, and scores documents based on math, not semantics.</p>

<p>Take these two documents:</p>
<ul>
  <li>d1: The Bank of Korea is expected to lower its benchmark interest rate next month.</li>
  <li>d6: The interest in street dance across Korea has grown at such a fast rate.</li>
</ul>

<p>And the query: “korea interest rate”</p>

<p>From BM25’s perspective, both documents look almost identical.
The same words appear, with the same frequency, so the scores end up nearly the same.
But to a human, the difference is obvious: <code class="language-plaintext highlighter-rouge">d1</code> is clearly more relevant than <code class="language-plaintext highlighter-rouge">d6</code>.</p>

<p>That’s why modern search engines rarely rely on BM25 alone.
Here are some techniques often used to address BM25’s limitations:</p>
<ul>
  <li>Relevance feedback: learning from user clicks or pseudo-relevance feedback.</li>
  <li>Semantic embeddings: capturing meaning rather than just word overlap.</li>
  <li>Learning to Rank (LTR): training a machine learning model that combines multiple signals (BM25 scores, embeddings, click data, etc.) to optimize search result ordering.</li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Okapi_BM25">Okapi BM25 - Wikipedia</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf">tf–idf - Wikipedia</a></li>
  <li><a href="https://www.elastic.co/blog/found-similarity-in-elasticsearch">Similarity in Elasticsearch by Konrad Beiske</a></li>
  <li><a href="https://www.coursera.org/learn/text-retrieval">Text Retrieval and Search Engines by University of Illinois Urbana-Champaign</a></li>
</ul>]]></content><author><name>Mienxiu</name></author><category term="ir" /><category term="python" /><summary type="html"><![CDATA[When you type a query into a search engine, it finds documents that contain your keywords. The problem is: how do you order those documents from most to least relevant?]]></summary></entry><entry><title type="html">Building Korean Query Auto-completion using Elasticsearch</title><link href="https://mienxiu.com/building-korean-query-autocompletion/" rel="alternate" type="text/html" title="Building Korean Query Auto-completion using Elasticsearch" /><published>2025-06-25T00:00:00+00:00</published><updated>2025-06-25T00:00:00+00:00</updated><id>https://mienxiu.com/building-korean-query-autocompletion</id><content type="html" xml:base="https://mienxiu.com/building-korean-query-autocompletion/"><![CDATA[<p>Query auto-completion (QAC) is a feature that suggests completions for a user’s input query.
It tries to guess what you’re about to type and throws out suggestions, commonly based on past searches.
Pretty much every site or app with a search box has it—Google, YouTube, Amazon, Naver, you name it.
It’s just one of those things we use every day.</p>

<p><img src="/assets/posts/36/youtube_query_autocompletion.png" alt="YouTube's query auto-completion" /></p>

<p>(YoutTube’s query auto-completion)</p>

<p>This post is a walk-through of building Korean query auto-completion feature based on past searches using Elasticsearch.</p>

<h2 id="what-problems-does-query-auto-completion-solve">What problems does query auto-completion solve?</h2>

<ul>
  <li>Typing can be slow and error-prone. This gets even trickier depending on the user’s environment like mobile. For car navigation, query auto-completion isn’t just nice to have, it’s a must.</li>
  <li>Users sometimes don’t know the exact keywords to search. Query auto-completion helps them find what they want faster, even if they’re not sure what to type.</li>
</ul>

<h2 id="how-is-korean-query-auto-completion-different-from-english">How is Korean query auto-completion different from English?</h2>

<p>The simplest way to build query auto-completion is using just simple prefix matching.</p>

<p>For English, that could be enough with some lowercasing thrown in.
For example, if your dataset includes <code class="language-plaintext highlighter-rouge">apple</code>, <code class="language-plaintext highlighter-rouge">autocomplete</code>, and <code class="language-plaintext highlighter-rouge">artificial intelligence</code>, typing just <code class="language-plaintext highlighter-rouge">a</code> or <code class="language-plaintext highlighter-rouge">A</code> will match all of them.</p>

<p>For Korean, that approach doesn’t quite cut it.
Say your dataset includes <code class="language-plaintext highlighter-rouge">가구</code> (furniture), <code class="language-plaintext highlighter-rouge">간식</code> (snack), and <code class="language-plaintext highlighter-rouge">겨울 코트</code> (winter coat).
They all start with the consonant <code class="language-plaintext highlighter-rouge">ㄱ</code>, but if a user types just <code class="language-plaintext highlighter-rouge">ㄱ</code>, none of those will match.
That’s because <code class="language-plaintext highlighter-rouge">ㄱ</code>, <code class="language-plaintext highlighter-rouge">가</code>, <code class="language-plaintext highlighter-rouge">간</code>, and <code class="language-plaintext highlighter-rouge">겨</code> are completely different characters.
If you want <code class="language-plaintext highlighter-rouge">가구</code> to show up in the suggestions, you have to type at least <code class="language-plaintext highlighter-rouge">가</code>.</p>

<p>To give users a better experience, you need to break Hangul syllables apart into their building blocks—a process called <em>syllable decomposition</em>.
That’s the key to making Korean auto-completion actually work better.</p>

<h2 id="hangul-syllables-and-jamo">Hangul syllables and Jamo</h2>

<p>Hangul (한글) is the Korean alphabet, made up of Hangul Jamos (자모).
These Jamos are the Korean consonants and vowels, which are basically the building blocks of Korean syllables.
There are 19 consonants and 21 vowels in Jamo.</p>

<p>19 consonants:</p>

<table>
  <thead>
    <tr>
      <th>Hangul</th>
      <th>ㄱ</th>
      <th>ㄲ</th>
      <th>ㄴ</th>
      <th>ㄷ</th>
      <th>ㄸ</th>
      <th>ㄹ</th>
      <th>ㅁ</th>
      <th>ㅂ</th>
      <th>ㅃ</th>
      <th>ㅅ</th>
      <th>ㅆ</th>
      <th>ㅇ</th>
      <th>ㅈ</th>
      <th>ㅉ</th>
      <th>ㅊ</th>
      <th>ㅋ</th>
      <th>ㅌ</th>
      <th>ㅍ</th>
      <th>ㅎ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Initial RR</td>
      <td>g</td>
      <td>kk</td>
      <td>n</td>
      <td>d</td>
      <td>tt</td>
      <td>r</td>
      <td>m</td>
      <td>b</td>
      <td>pp</td>
      <td>s</td>
      <td>ss</td>
      <td>–</td>
      <td>j</td>
      <td>jj</td>
      <td>ch</td>
      <td>k</td>
      <td>t</td>
      <td>p</td>
      <td>h</td>
    </tr>
    <tr>
      <td>Final RR</td>
      <td>k</td>
      <td>k</td>
      <td>n</td>
      <td>t</td>
      <td>–</td>
      <td>l</td>
      <td>m</td>
      <td>p</td>
      <td>–</td>
      <td>t</td>
      <td>t</td>
      <td>ng</td>
      <td>t</td>
      <td>–</td>
      <td>t</td>
      <td>k</td>
      <td>t</td>
      <td>p</td>
      <td>t</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>Initial RR: Romanization of consonants at the start of a syllable. These are the sounds you hear starting a Korean syllable.</li>
  <li>Final RR: Romanization of consonants at the end of a syllable. “-“ means these letters don’t appear at the end of syllables.</li>
</ul>

<p>21 vowels:</p>

<table>
  <thead>
    <tr>
      <th>Hangul</th>
      <th>ㅏ</th>
      <th>ㅐ</th>
      <th>ㅑ</th>
      <th>ㅒ</th>
      <th>ㅓ</th>
      <th>ㅔ</th>
      <th>ㅕ</th>
      <th>ㅖ</th>
      <th>ㅗ</th>
      <th>ㅘ</th>
      <th>ㅙ</th>
      <th>ㅚ</th>
      <th>ㅛ</th>
      <th>ㅜ</th>
      <th>ㅝ</th>
      <th>ㅞ</th>
      <th>ㅟ</th>
      <th>ㅠ</th>
      <th>ㅡ</th>
      <th>ㅢ</th>
      <th>ㅣ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>RR</td>
      <td>a</td>
      <td>ae</td>
      <td>ya</td>
      <td>yae</td>
      <td>eo</td>
      <td>e</td>
      <td>yeo</td>
      <td>ye</td>
      <td>o</td>
      <td>wa</td>
      <td>wae</td>
      <td>oe</td>
      <td>yo</td>
      <td>u</td>
      <td>wo</td>
      <td>we</td>
      <td>wi</td>
      <td>yu</td>
      <td>eu</td>
      <td>ui/yi</td>
      <td>i</td>
    </tr>
  </tbody>
</table>

<p>Here are some examples of Korean syllables made by combining jamos:</p>

<table>
  <thead>
    <tr>
      <th>Syllable</th>
      <th>Romanization</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>가</td>
      <td>ga</td>
      <td>(depends on context)</td>
    </tr>
    <tr>
      <td>옷</td>
      <td>ot</td>
      <td>clothes</td>
    </tr>
    <tr>
      <td>사과</td>
      <td>sagwa</td>
      <td>apple</td>
    </tr>
    <tr>
      <td>크림</td>
      <td>keurim</td>
      <td>cream</td>
    </tr>
  </tbody>
</table>

<p>That’s pretty much it.
Once you’ve got this down, you can read and write almost any Korean.</p>

<p class="notice--info">Hangul system is remarkably unique in that its letters are made to look like the mouth and tongue shapes used to say the sounds.
It is designed so that anyone can easily learn to read and write.</p>

<hr />

<p>The example in this post uses the following software versions:</p>
<ul>
  <li>Python 3.13.4 (with <code class="language-plaintext highlighter-rouge">pip install elasticsearch==8.13.2 "fastapi[standard]" aiohttp</code>)</li>
  <li>Elasticsearch 8.13.4</li>
  <li>Kibana 8.13.4</li>
</ul>

<p>Elasticsearch and Kibana are running locally at http://localhost:9200 and http://localhost:5601, respectively.</p>

<h2 id="context">Context</h2>

<p>You’re building an e-commerce application with a search bar. Every time a user types a search query, you’re logging that into an Elasticsearch index called <code class="language-plaintext highlighter-rouge">search-logs</code> with the following mappings:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">/search-logs</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">"@timestamp"</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">"date"</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">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"keyword"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"user_id"</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">"keyword"</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></code></pre></div></div>

<p>For demo purposes, let’s insert some sample data:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/search-logs/_bulk</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:00Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"가구"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user1"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:01Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"가방"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user2"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:02Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"가방"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user3"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:03Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"간식"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user4"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:04Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"겨울 코트"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user5"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:05Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"사과"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user6"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:06Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"1234567890"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user7"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"index"</span><span class="p">:{}}</span><span class="w">
</span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="s2">"2025-06-21T09:00:07Z"</span><span class="p">,</span><span class="nl">"query"</span><span class="p">:</span><span class="s2">"airpods 4"</span><span class="p">,</span><span class="nl">"user_id"</span><span class="p">:</span><span class="s2">"user8"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="solution">Solution</h2>

<p>There are plugins like nori for Korean, but I’m skipping those to build the logic ourselves for full control and to better understand how Korean QAC actually works under the hood.</p>

<p>Another possible solution is using <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.13/analysis-edgengram-tokenizer.html">Edge n-gram tokenizer</a>.
With this approach, you can set up a custom tokenizer in your index and tweak options like <code class="language-plaintext highlighter-rouge">min_gram</code>, <code class="language-plaintext highlighter-rouge">max_gram</code>, and <code class="language-plaintext highlighter-rouge">token_chars</code> to fine-tune how autocomplete behaves.
That said, I won’t be covering this method here.</p>

<p>Instead, I’ll use Elasticsearch’s <code class="language-plaintext highlighter-rouge">completion</code> suggester which is a more purpose-built option.
I’ll also take a look at its limitations and walk through ways to gradually improve the experience.</p>

<h3 id="version0-simple-prefix-matching">Version0: Simple prefix matching</h3>

<p>Start simple.
No syllable decomposition, just raw keyword prefix matching using Elasticsearch’s <code class="language-plaintext highlighter-rouge">completion</code> suggester.</p>

<p>First, create a keyword suggestion index named <code class="language-plaintext highlighter-rouge">search-keywords</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">/search-keywords</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">"suggest"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">can</span><span class="w"> </span><span class="err">use</span><span class="w"> </span><span class="err">any</span><span class="w"> </span><span class="err">name</span><span class="w"> </span><span class="err">here</span><span class="w"> </span><span class="err">instead</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="s2">"suggest"</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completion"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"analyzer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"standard"</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></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">completion</code> suggester is designed to be as fast as possible because auto-completion should keep pace with your typing, giving you instant, relevant suggestions as you go.
This is why auto-completion is also often called as “search-as-you-type” functionality.</p>

<p>We also use the <code class="language-plaintext highlighter-rouge">standard</code> analyzer instead of the default <code class="language-plaintext highlighter-rouge">simple</code> analyzer to make sure numbers are handled properly.
The <code class="language-plaintext highlighter-rouge">simple</code> analyzer strips out digits completely, so a query like “1” won’t match “1234567890” in our dataset.
The <code class="language-plaintext highlighter-rouge">standard</code> analyzer keeps numbers intact—so numeric queries work just like you’d expect.</p>

<p>Now that we have two indices, <code class="language-plaintext highlighter-rouge">search-logs</code> and <code class="language-plaintext highlighter-rouge">search-keywords</code>, we can read from <code class="language-plaintext highlighter-rouge">search-logs</code> and populate <code class="language-plaintext highlighter-rouge">search-keywords</code> using this Python script:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># update_keywords0.py
</span><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>

<span class="kn">from</span> <span class="nn">elasticsearch</span> <span class="kn">import</span> <span class="n">Elasticsearch</span><span class="p">,</span> <span class="n">helpers</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">hosts</span> <span class="o">=</span> <span class="p">[</span><span class="s">"http://localhost:9200"</span><span class="p">]</span>
    <span class="n">username</span> <span class="o">=</span> <span class="s">"elastic"</span>
    <span class="n">password</span> <span class="o">=</span> <span class="s">"elasticpassword"</span>
    <span class="k">with</span> <span class="n">Elasticsearch</span><span class="p">(</span><span class="n">hosts</span><span class="o">=</span><span class="n">hosts</span><span class="p">,</span> <span class="n">basic_auth</span><span class="o">=</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">))</span> <span class="k">as</span> <span class="n">es_client</span><span class="p">:</span>
        <span class="n">top_queries</span> <span class="o">=</span> <span class="n">_get_top_queries_from_search_logs</span><span class="p">(</span><span class="n">es_client</span><span class="o">=</span><span class="n">es_client</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="mi">10000</span><span class="p">)</span>
        <span class="n">success_count</span> <span class="o">=</span> <span class="n">_update_search_keywords</span><span class="p">(</span><span class="n">es_client</span><span class="p">,</span> <span class="n">keywords</span><span class="o">=</span><span class="n">top_queries</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Success count:"</span><span class="p">,</span> <span class="n">success_count</span><span class="p">)</span>


<span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">Keyword</span><span class="p">:</span>
    <span class="n">key</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">count</span><span class="p">:</span> <span class="nb">int</span>


<span class="k">def</span> <span class="nf">_get_top_queries_from_search_logs</span><span class="p">(</span><span class="n">es_client</span><span class="p">:</span> <span class="n">Elasticsearch</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">Keyword</span><span class="p">]:</span>
    <span class="s">"""
    Get the top search queries from the search log index.
    We use composite aggregation to handle large datasets efficiently.
    """</span>
    <span class="n">index</span> <span class="o">=</span> <span class="s">"search-logs"</span>

    <span class="n">all_buckets</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">after_key</span> <span class="o">=</span> <span class="bp">None</span>  <span class="c1"># for pagination
</span>    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="n">comp_query</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"size"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
            <span class="s">"aggs"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"top_queries"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"composite"</span><span class="p">:</span> <span class="p">{</span>
                        <span class="s">"size"</span><span class="p">:</span> <span class="n">size</span><span class="p">,</span>
                        <span class="s">"sources"</span><span class="p">:</span> <span class="p">[{</span><span class="s">"term"</span><span class="p">:</span> <span class="p">{</span><span class="s">"terms"</span><span class="p">:</span> <span class="p">{</span><span class="s">"field"</span><span class="p">:</span> <span class="s">"query"</span><span class="p">}}}],</span>
                        <span class="o">**</span><span class="p">({</span><span class="s">"after"</span><span class="p">:</span> <span class="n">after_key</span><span class="p">}</span> <span class="k">if</span> <span class="n">after_key</span> <span class="k">else</span> <span class="p">{}),</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">},</span>
        <span class="p">}</span>
        <span class="n">res</span> <span class="o">=</span> <span class="n">es_client</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="n">index</span><span class="p">,</span> <span class="n">body</span><span class="o">=</span><span class="n">comp_query</span><span class="p">)</span>
        <span class="n">buckets</span> <span class="o">=</span> <span class="n">res</span><span class="p">[</span><span class="s">"aggregations"</span><span class="p">][</span><span class="s">"top_queries"</span><span class="p">][</span><span class="s">"buckets"</span><span class="p">]</span>
        <span class="n">all_buckets</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">buckets</span><span class="p">)</span>
        <span class="n">after_key</span> <span class="o">=</span> <span class="n">res</span><span class="p">[</span><span class="s">"aggregations"</span><span class="p">][</span><span class="s">"top_queries"</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">"after_key"</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">after_key</span><span class="p">:</span>
            <span class="k">break</span>

    <span class="c1"># Convert the buckets to a list of Keyword dataclass instances
</span>    <span class="n">keywords</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Keyword</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">all_buckets</span><span class="p">:</span>
        <span class="n">keywords</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">Keyword</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="n">item</span><span class="p">[</span><span class="s">"key"</span><span class="p">][</span><span class="s">"term"</span><span class="p">].</span><span class="n">strip</span><span class="p">(),</span> <span class="n">count</span><span class="o">=</span><span class="n">item</span><span class="p">[</span><span class="s">"doc_count"</span><span class="p">]))</span>

    <span class="k">return</span> <span class="n">keywords</span>


<span class="k">def</span> <span class="nf">_update_search_keywords</span><span class="p">(</span><span class="n">es_client</span><span class="p">:</span> <span class="n">Elasticsearch</span><span class="p">,</span> <span class="n">keywords</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Keyword</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="s">"""
    Update the autocomplete index with new data.
    Return the number of successfully executed actions (including overrides).
    """</span>
    <span class="n">index</span> <span class="o">=</span> <span class="s">"search-keywords"</span>

    <span class="n">actions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
        <span class="n">action</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"_op_type"</span><span class="p">:</span> <span class="s">"index"</span><span class="p">,</span>
            <span class="s">"_index"</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span>
            <span class="s">"_id"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="s">"_source"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"input"</span><span class="p">:</span> <span class="p">[</span>
                        <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>  <span class="c1"># Use the keyword itself as input
</span>                    <span class="p">],</span>
                    <span class="s">"weight"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">count</span><span class="p">,</span>
                <span class="p">}</span>
            <span class="p">},</span>
        <span class="p">}</span>
        <span class="n">actions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>

    <span class="n">res</span> <span class="o">=</span> <span class="n">helpers</span><span class="p">.</span><span class="n">bulk</span><span class="p">(</span><span class="n">es_client</span><span class="p">,</span> <span class="n">actions</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">res</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>What it does:</p>
<ol>
  <li>Aggregates the top queries from <code class="language-plaintext highlighter-rouge">search-logs</code>, using a composite aggregation which efficiently handles large datasets by paginating results. (Without composite aggregation, there is a size limit to the number of buckets.)</li>
  <li>Bulk-indexes them into <code class="language-plaintext highlighter-rouge">search-keywords</code> with:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">input</code>: the actual query</li>
      <li><code class="language-plaintext highlighter-rouge">weight</code>: how often it was searched. This way, more frequently searched keywords show up higher in suggestions.</li>
    </ul>
  </li>
</ol>

<p>In practice, if you’re dealing with a large volume of logs, you’ll want to add a range filter to speed things up and avoid processing stale data.
For example, here’s how to retrieve search logs from the past week:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"size"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</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="nl">"bool"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"filter"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="nl">"range"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="s2">"now-7d"</span><span class="p">}}}]}},</span><span class="w">
    </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Running the code will output:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Success</span> <span class="n">count</span><span class="p">:</span> <span class="mi">7</span>
</code></pre></div></div>

<p>In practice, again, you can run this code periodically to keep your dataset fresh and up to date.</p>

<p>Verify the documents by using <code class="language-plaintext highlighter-rouge">_search</code> API in Kibana Dev Tools:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET</span><span class="w"> </span><span class="err">/search-keywords/_search</span><span class="w">
</span></code></pre></div></div>

<p>Next up, use FastAPI to expose an endpoint <code class="language-plaintext highlighter-rouge">/keyword-suggestions</code>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app.py
</span><span class="kn">from</span> <span class="nn">contextlib</span> <span class="kn">import</span> <span class="n">asynccontextmanager</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Annotated</span>

<span class="kn">from</span> <span class="nn">elasticsearch</span> <span class="kn">import</span> <span class="n">AsyncElasticsearch</span>
<span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">Depends</span><span class="p">,</span> <span class="n">FastAPI</span><span class="p">,</span> <span class="n">Query</span>
<span class="kn">from</span> <span class="nn">fastapi.middleware.cors</span> <span class="kn">import</span> <span class="n">CORSMiddleware</span>

<span class="c1"># Create an Elasticsearch client
</span><span class="n">hosts</span> <span class="o">=</span> <span class="p">[</span><span class="s">"http://localhost:9200"</span><span class="p">]</span>
<span class="n">username</span> <span class="o">=</span> <span class="s">"elastic"</span>
<span class="n">password</span> <span class="o">=</span> <span class="s">"elasticpassword"</span>
<span class="n">es_client</span> <span class="o">=</span> <span class="n">AsyncElasticsearch</span><span class="p">(</span><span class="n">hosts</span><span class="o">=</span><span class="n">hosts</span><span class="p">,</span> <span class="n">basic_auth</span><span class="o">=</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">))</span>


<span class="o">@</span><span class="n">asynccontextmanager</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">lifespan</span><span class="p">(</span><span class="n">app</span><span class="p">:</span> <span class="n">FastAPI</span><span class="p">):</span>
    <span class="k">yield</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Closing Elasticsearch client..."</span><span class="p">)</span>
    <span class="k">await</span> <span class="n">es_client</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>


<span class="c1"># Create FastAPI app with CORS middleware
</span><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Keyword Suggestion API"</span><span class="p">,</span> <span class="n">lifespan</span><span class="o">=</span><span class="n">lifespan</span><span class="p">)</span>
<span class="n">app</span><span class="p">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">CORSMiddleware</span><span class="p">,</span> <span class="n">allow_origins</span><span class="o">=</span><span class="p">[</span><span class="s">"*"</span><span class="p">])</span>


<span class="k">def</span> <span class="nf">get_es_client</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">AsyncElasticsearch</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">es_client</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span>
    <span class="s">"/keyword-suggestions"</span><span class="p">,</span>
    <span class="n">response_model</span><span class="o">=</span><span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
    <span class="n">summary</span><span class="o">=</span><span class="s">"Suggest keywords for a given query"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"Returns a list of keyword suggestions based on the input query."</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">suggest_keywords</span><span class="p">(</span>
    <span class="n">es_client</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">AsyncElasticsearch</span><span class="p">,</span> <span class="n">Depends</span><span class="p">(</span><span class="n">get_es_client</span><span class="p">)],</span>
    <span class="n">query</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Query</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">"User input query for keyword suggestions"</span><span class="p">)],</span>
    <span class="n">limit</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Query</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">"Maximum number of keywords to return"</span><span class="p">,</span> <span class="n">gt</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span> <span class="o">=</span> <span class="mi">10</span><span class="p">,</span>
<span class="p">):</span>
    <span class="n">body</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"keyword-suggest"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"prefix"</span><span class="p">:</span> <span class="n">query</span><span class="p">,</span>
                <span class="s">"completion"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"field"</span><span class="p">:</span> <span class="s">"suggest"</span><span class="p">,</span>
                    <span class="s">"size"</span><span class="p">:</span> <span class="n">limit</span><span class="p">,</span>
                <span class="p">},</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">res</span> <span class="o">=</span> <span class="k">await</span> <span class="n">es_client</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s">"search-keywords"</span><span class="p">,</span> <span class="n">body</span><span class="o">=</span><span class="n">body</span><span class="p">)</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">option</span> <span class="ow">in</span> <span class="n">res</span><span class="p">[</span><span class="s">"suggest"</span><span class="p">][</span><span class="s">"keyword-suggest"</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s">"options"</span><span class="p">]:</span>
        <span class="n">results</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">option</span><span class="p">[</span><span class="s">"_source"</span><span class="p">][</span><span class="s">"suggest"</span><span class="p">][</span><span class="s">"input"</span><span class="p">][</span><span class="mi">0</span><span class="p">])</span>

    <span class="k">return</span> <span class="n">results</span>
</code></pre></div></div>

<p>This FastAPI app handles Korean keyword autocomplete using Elasticsearch. Here’s how it works:</p>
<ol>
  <li>Set up and manages an Elasticsearch client.</li>
  <li>Expose a <code class="language-plaintext highlighter-rouge">/keyword-suggestions</code> endpoint that takes a query and an optional limit.</li>
  <li>Query the <code class="language-plaintext highlighter-rouge">search-keywords</code> index using Elasticsearch’s <code class="language-plaintext highlighter-rouge">completion</code> suggester.</li>
  <li>Return a list of matching keyword suggestions.</li>
</ol>

<p>The core of the logic lives in this Elasticsearch query:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">body</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
        <span class="s">"keyword-suggest"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"prefix"</span><span class="p">:</span> <span class="n">query</span><span class="p">,</span>
            <span class="s">"completion"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"field"</span><span class="p">:</span> <span class="s">"suggest"</span><span class="p">,</span>
                <span class="s">"size"</span><span class="p">:</span> <span class="n">limit</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here’s a quick breakdown:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">prefix</code>: the user’s input—we’re trying to autocomplete this.</li>
  <li><code class="language-plaintext highlighter-rouge">completion</code>: tells Elasticsearch to use the completion suggester.</li>
  <li><code class="language-plaintext highlighter-rouge">field</code>: the field in the index we defined to support autocomplete (in this case, <code class="language-plaintext highlighter-rouge">suggest</code>).</li>
</ul>

<p>You can run the app with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fastapi run
</code></pre></div></div>

<p>Then head to http://localhost:8000/docs to try it out.</p>

<p>If you hit the endpoint with <code class="language-plaintext highlighter-rouge">query=가</code>, you’ll get a response like:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="s2">"가방"</span><span class="p">,</span><span class="w">
  </span><span class="s2">"가구"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>Note that <code class="language-plaintext highlighter-rouge">가방</code> comes first because it has a higher weight (i.e., it’s been searched more often).
It’s a simple heuristic: the more people search for something, the higher it shows up.</p>

<p>Here are a couple of limitations:</p>
<ul>
  <li>Typing a single jamo like <code class="language-plaintext highlighter-rouge">ㄱ</code> won’t return anything even if some keywords start with that consonant.</li>
  <li>Typing <code class="language-plaintext highlighter-rouge">갑</code> or <code class="language-plaintext highlighter-rouge">가바</code> (a typo or maybe just middle of typing <code class="language-plaintext highlighter-rouge">가방</code>) won’t return anything either.</li>
</ul>

<h3 id="version1-syllable-decomposition">Version1: Syllable decomposition</h3>

<p>To support keyword suggestions even when the user types only jamo (like <code class="language-plaintext highlighter-rouge">ㄱ</code>, <code class="language-plaintext highlighter-rouge">ㄱㄱ</code>, etc.), we need to make a few changes to how we index and search.</p>

<p>We’ll re-create a <code class="language-plaintext highlighter-rouge">search-keywords</code> index:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">/search-keywords</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">"suggest"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">can</span><span class="w"> </span><span class="err">use</span><span class="w"> </span><span class="err">any</span><span class="w"> </span><span class="err">name</span><span class="w"> </span><span class="err">here</span><span class="w"> </span><span class="err">instead</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="s2">"suggest"</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completion"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"analyzer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"standard"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"keyword"</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">"keyword"</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></code></pre></div></div>

<p>Now, we’ve separated responsibilities by defining two fields:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">suggest</code>: used for querying suggestions.</li>
  <li><code class="language-plaintext highlighter-rouge">keyword</code>: stores the original keyword to display to users.</li>
</ul>

<p>Next, we need to break each Korean syllable into its jamos (choseong, jungseong, and jongseong) so the <code class="language-plaintext highlighter-rouge">completion</code> suggester can match partial inputs.</p>

<p>Here’s a utility function to do that:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Hangul.py
</span><span class="s">"""
Hangul (Korean alphabet) processing utilities.

This module provides functions for working with Korean text, including:
- Extracting initial consonants (choseong)
- Splitting Korean syllables into individual jamo (consonants and vowels)
- Converting Korean text to romanized form based on keyboard layout

The module uses Unicode code points to manipulate Korean characters.
"""</span>

<span class="c1"># For long lists, use `extend` in favor of readability
</span>
<span class="c1"># 19 initial consonants
</span><span class="n">CHOSEONGS</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">CHOSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㄱ"</span><span class="p">,</span> <span class="s">"ㄲ"</span><span class="p">,</span> <span class="s">"ㄴ"</span><span class="p">,</span> <span class="s">"ㄷ"</span><span class="p">])</span>
<span class="n">CHOSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㄸ"</span><span class="p">,</span> <span class="s">"ㄹ"</span><span class="p">,</span> <span class="s">"ㅁ"</span><span class="p">,</span> <span class="s">"ㅂ"</span><span class="p">])</span>
<span class="n">CHOSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅃ"</span><span class="p">,</span> <span class="s">"ㅅ"</span><span class="p">,</span> <span class="s">"ㅆ"</span><span class="p">,</span> <span class="s">"ㅇ"</span><span class="p">])</span>
<span class="n">CHOSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅈ"</span><span class="p">,</span> <span class="s">"ㅉ"</span><span class="p">,</span> <span class="s">"ㅊ"</span><span class="p">,</span> <span class="s">"ㅋ"</span><span class="p">])</span>
<span class="n">CHOSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅌ"</span><span class="p">,</span> <span class="s">"ㅍ"</span><span class="p">,</span> <span class="s">"ㅎ"</span><span class="p">])</span>

<span class="c1"># 21 vowels
</span><span class="n">JUNGSEONGS</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">JUNGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅏ"</span><span class="p">,</span> <span class="s">"ㅐ"</span><span class="p">,</span> <span class="s">"ㅑ"</span><span class="p">,</span> <span class="s">"ㅒ"</span><span class="p">])</span>
<span class="n">JUNGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅓ"</span><span class="p">,</span> <span class="s">"ㅔ"</span><span class="p">,</span> <span class="s">"ㅕ"</span><span class="p">,</span> <span class="s">"ㅖ"</span><span class="p">])</span>
<span class="n">JUNGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅗ"</span><span class="p">,</span> <span class="s">"ㅘ"</span><span class="p">,</span> <span class="s">"ㅙ"</span><span class="p">,</span> <span class="s">"ㅚ"</span><span class="p">])</span>
<span class="n">JUNGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅛ"</span><span class="p">,</span> <span class="s">"ㅜ"</span><span class="p">,</span> <span class="s">"ㅝ"</span><span class="p">,</span> <span class="s">"ㅞ"</span><span class="p">])</span>
<span class="n">JUNGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅟ"</span><span class="p">,</span> <span class="s">"ㅠ"</span><span class="p">,</span> <span class="s">"ㅡ"</span><span class="p">,</span> <span class="s">"ㅢ"</span><span class="p">])</span>
<span class="n">JUNGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅣ"</span><span class="p">])</span>

<span class="c1"># 28 final consonants (including no-final)
</span><span class="n">JONGSEONGS</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">JONGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">" "</span><span class="p">,</span> <span class="s">"ㄱ"</span><span class="p">,</span> <span class="s">"ㄲ"</span><span class="p">,</span> <span class="s">"ㄳ"</span><span class="p">])</span>
<span class="n">JONGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㄴ"</span><span class="p">,</span> <span class="s">"ㄵ"</span><span class="p">,</span> <span class="s">"ㄶ"</span><span class="p">,</span> <span class="s">"ㄷ"</span><span class="p">])</span>
<span class="n">JONGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㄹ"</span><span class="p">,</span> <span class="s">"ㄺ"</span><span class="p">,</span> <span class="s">"ㄻ"</span><span class="p">,</span> <span class="s">"ㄼ"</span><span class="p">])</span>
<span class="n">JONGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㄽ"</span><span class="p">,</span> <span class="s">"ㄾ"</span><span class="p">,</span> <span class="s">"ㄿ"</span><span class="p">,</span> <span class="s">"ㅀ"</span><span class="p">])</span>
<span class="n">JONGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅁ"</span><span class="p">,</span> <span class="s">"ㅂ"</span><span class="p">,</span> <span class="s">"ㅄ"</span><span class="p">,</span> <span class="s">"ㅅ"</span><span class="p">])</span>
<span class="n">JONGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅆ"</span><span class="p">,</span> <span class="s">"ㅇ"</span><span class="p">,</span> <span class="s">"ㅈ"</span><span class="p">,</span> <span class="s">"ㅊ"</span><span class="p">])</span>
<span class="n">JONGSEONGS</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="s">"ㅋ"</span><span class="p">,</span> <span class="s">"ㅌ"</span><span class="p">,</span> <span class="s">"ㅍ"</span><span class="p">,</span> <span class="s">"ㅎ"</span><span class="p">])</span>


<span class="k">def</span> <span class="nf">decompose_syllables</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="s">"""
    Decompose Hangul syllables into individual jamo (consonants and vowels).

    Example:
        "사과" -&gt; ["ㅅ", "ㅏ", "ㄱ", "ㅘ"]
    """</span>
    <span class="n">array</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">character</span> <span class="ow">in</span> <span class="nb">list</span><span class="p">(</span><span class="n">text</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">" "</span><span class="p">,</span> <span class="s">""</span><span class="p">).</span><span class="n">strip</span><span class="p">()):</span>
        <span class="k">if</span> <span class="s">"가"</span> <span class="o">&lt;=</span> <span class="n">character</span> <span class="o">&lt;=</span> <span class="s">"힣"</span><span class="p">:</span>  <span class="c1"># For Hangul syllables
</span>            <span class="n">offset</span> <span class="o">=</span> <span class="nb">ord</span><span class="p">(</span><span class="n">character</span><span class="p">)</span> <span class="o">-</span> <span class="nb">ord</span><span class="p">(</span><span class="s">"가"</span><span class="p">)</span>
            <span class="n">choseong</span> <span class="o">=</span> <span class="n">offset</span> <span class="o">//</span> <span class="mi">588</span>
            <span class="n">jungseong</span> <span class="o">=</span> <span class="p">(</span><span class="n">offset</span> <span class="o">-</span> <span class="p">(</span><span class="mi">588</span> <span class="o">*</span> <span class="n">choseong</span><span class="p">))</span> <span class="o">//</span> <span class="mi">28</span>
            <span class="n">jongseong</span> <span class="o">=</span> <span class="n">offset</span> <span class="o">-</span> <span class="p">(</span><span class="mi">588</span> <span class="o">*</span> <span class="n">choseong</span><span class="p">)</span> <span class="o">-</span> <span class="p">(</span><span class="mi">28</span> <span class="o">*</span> <span class="n">jungseong</span><span class="p">)</span>
            <span class="n">array</span><span class="p">.</span><span class="n">append</span><span class="p">([</span><span class="n">CHOSEONGS</span><span class="p">[</span><span class="n">choseong</span><span class="p">],</span> <span class="n">JUNGSEONGS</span><span class="p">[</span><span class="n">jungseong</span><span class="p">],</span> <span class="n">JONGSEONGS</span><span class="p">[</span><span class="n">jongseong</span><span class="p">]])</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">array</span><span class="p">.</span><span class="n">append</span><span class="p">([</span><span class="n">character</span><span class="p">])</span>

    <span class="k">return</span> <span class="p">[</span><span class="n">char</span> <span class="k">for</span> <span class="n">sublist</span> <span class="ow">in</span> <span class="n">array</span> <span class="k">for</span> <span class="n">char</span> <span class="ow">in</span> <span class="n">sublist</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">char</span> <span class="o">==</span> <span class="s">" "</span><span class="p">]</span>
</code></pre></div></div>

<p>To explain in detail, there are 19 initial consonants * 21 vowels * 28 final consonants (including no-final) = 11,172 possible combinations for modern Hangul syllables.
And they occupy consecutive Unicode code points staring at <code class="language-plaintext highlighter-rouge">U+AC00</code> (“가”, 44032) and ending at <code class="language-plaintext highlighter-rouge">U+D7A3</code> (“힣”, 55203).
So, the check <code class="language-plaintext highlighter-rouge">if "가" &lt;= character &lt;= "힣":</code> ensures we only apply the decomposition math to Hangul syllables.</p>

<p>The Unicode code points for Hangul syllables advance first by final consonant, then by medial vowel, and last by initial consonant.
For example:</p>
<ul>
  <li>by 28 final consonant:
    <ul>
      <li>가: 44032</li>
      <li>각: 44033 (advanced by 1)</li>
      <li>갂: 44034</li>
      <li>…</li>
    </ul>
  </li>
  <li>by 21 medial vowel:
    <ul>
      <li>가: 44032</li>
      <li>개: 44060 (advanced by 28)</li>
      <li>갸: 44088</li>
      <li>…</li>
    </ul>
  </li>
  <li>by 19 initial consonant:
    <ul>
      <li>가: 44032</li>
      <li>까: 44620 (advanced by 21 * 28)</li>
      <li>나: 45208</li>
      <li>…</li>
      <li>힣: 55203</li>
    </ul>
  </li>
</ul>

<p>Thus, every Hangul syllable code point can be thought of as:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code_point = 0xAC00 + (initial_index * 21 * 28) + (medial_index * 28) + final_index
</code></pre></div></div>
<p>where <code class="language-plaintext highlighter-rouge">initial_index</code>, <code class="language-plaintext highlighter-rouge">medial_index</code>, and <code class="language-plaintext highlighter-rouge">final_index</code> are zero-based positions in the respective jamo lists.</p>

<p>Therefore:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">offset</span> <span class="o">=</span> <span class="nb">ord</span><span class="p">(</span><span class="n">character</span><span class="p">)</span> <span class="o">-</span> <span class="nb">ord</span><span class="p">(</span><span class="s">"가"</span><span class="p">)</span>  <span class="c1"># ord("가") == 0xAC00 == 44032
</span><span class="n">choseong</span> <span class="o">=</span> <span class="n">offset</span> <span class="o">//</span> <span class="mi">588</span>  <span class="c1"># initial_index (0–18)
</span><span class="n">jungseong</span> <span class="o">=</span> <span class="p">(</span><span class="n">offset</span> <span class="o">-</span> <span class="p">(</span><span class="mi">588</span> <span class="o">*</span> <span class="n">choseong</span><span class="p">))</span> <span class="o">//</span> <span class="mi">28</span>  <span class="c1"># medial_index (0–20)
</span><span class="n">jongseong</span> <span class="o">=</span> <span class="n">offset</span> <span class="o">-</span> <span class="p">(</span><span class="mi">588</span> <span class="o">*</span> <span class="n">choseong</span><span class="p">)</span> <span class="o">-</span> <span class="p">(</span><span class="mi">28</span> <span class="o">*</span> <span class="n">jungseong</span><span class="p">)</span>  <span class="c1"># final_index (0–27)
</span></code></pre></div></div>
<p>(588 here is just the precomputed value for 21 * 28.)</p>

<p>For example:</p>

<table>
  <thead>
    <tr>
      <th>character</th>
      <th>code point</th>
      <th>offset</th>
      <th>choseong</th>
      <th>jungseong</th>
      <th>jongseong</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>가</td>
      <td>44032</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <td>각</td>
      <td>44033</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
    </tr>
    <tr>
      <td>힣</td>
      <td>55203</td>
      <td>11171</td>
      <td>18</td>
      <td>20</td>
      <td>27</td>
    </tr>
  </tbody>
</table>

<p>Once you have those three indices, you simply look up the corresponding jamo in the three lists:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">CHOSEONGS</span><span class="p">[</span><span class="n">choseong</span><span class="p">],</span> <span class="n">JUNGSEONGS</span><span class="p">[</span><span class="n">jungseong</span><span class="p">],</span> <span class="n">JONGSEONGS</span><span class="p">[</span><span class="n">jongseong</span><span class="p">]</span>
</code></pre></div></div>

<p>As a result, the <code class="language-plaintext highlighter-rouge">decompose_syllables</code> takes a string like <code class="language-plaintext highlighter-rouge">사과</code> and turns it into <code class="language-plaintext highlighter-rouge">["ㅅ", "ㅏ", "ㄱ", "ㅘ"]</code>.</p>

<p>This utility function is used to decompose the syllables and feed that into the <code class="language-plaintext highlighter-rouge">suggest</code> field while the original keyword goes into <code class="language-plaintext highlighter-rouge">keyword</code> field:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># update_keywords1.py
</span><span class="kn">from</span> <span class="nn">Hangul</span> <span class="kn">import</span> <span class="n">decompose_syllables</span>

<span class="p">...</span>

<span class="k">def</span> <span class="nf">_update_search_keywords</span><span class="p">(</span><span class="n">es_client</span><span class="p">:</span> <span class="n">Elasticsearch</span><span class="p">,</span> <span class="n">keywords</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Keyword</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="s">"""
    Update the autocomplete index with new data.
    Return the number of successfully executed actions (including overrides).
    """</span>
    <span class="n">index</span> <span class="o">=</span> <span class="s">"search-keywords"</span>

    <span class="n">actions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
        <span class="n">action</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"_op_type"</span><span class="p">:</span> <span class="s">"index"</span><span class="p">,</span>
            <span class="s">"_index"</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span>
            <span class="s">"_id"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="s">"_source"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"input"</span><span class="p">:</span> <span class="p">[</span>
                        <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">decompose_syllables</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>  <span class="c1"># changed
</span>                    <span class="p">],</span>
                    <span class="s">"weight"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">count</span><span class="p">,</span>
                <span class="p">},</span>
                <span class="s">"keyword"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">}</span>
        <span class="n">actions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>

    <span class="n">res</span> <span class="o">=</span> <span class="n">helpers</span><span class="p">.</span><span class="n">bulk</span><span class="p">(</span><span class="n">es_client</span><span class="p">,</span> <span class="n">actions</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">res</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>Running this code will output the same as the previous one:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Success</span> <span class="n">count</span><span class="p">:</span> <span class="mi">7</span>
</code></pre></div></div>

<p>Next, modify the FastAPI endpoint to:</p>
<ul>
  <li>Decompose the user query.</li>
  <li>Use the decomposed form for prefix search.</li>
  <li>Return the original keywords (from <code class="language-plaintext highlighter-rouge">keyword</code> field) as the suggestions.</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app.py
</span><span class="kn">from</span> <span class="nn">Hangul</span> <span class="kn">import</span> <span class="n">decompose_syllables</span>

<span class="p">...</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span>
    <span class="s">"/keyword-suggestions"</span><span class="p">,</span>
    <span class="n">response_model</span><span class="o">=</span><span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
    <span class="n">summary</span><span class="o">=</span><span class="s">"Suggest keywords for a given query"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"Returns a list of keyword suggestions based on the input query."</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">suggest_keywords</span><span class="p">(</span>
    <span class="n">es_client</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">AsyncElasticsearch</span><span class="p">,</span> <span class="n">Depends</span><span class="p">(</span><span class="n">get_es_client</span><span class="p">)],</span>
    <span class="n">query</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Query</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">"User input query for keyword suggestions"</span><span class="p">)],</span>
    <span class="n">limit</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Query</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">"Maximum number of keywords to return"</span><span class="p">,</span> <span class="n">gt</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span> <span class="o">=</span> <span class="mi">10</span><span class="p">,</span>
<span class="p">):</span>
    <span class="n">body</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"keyword-suggest"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"prefix"</span><span class="p">:</span> <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">decompose_syllables</span><span class="p">(</span><span class="n">query</span><span class="p">)),</span>  <span class="c1"># changed
</span>                <span class="s">"completion"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"field"</span><span class="p">:</span> <span class="s">"suggest"</span><span class="p">,</span>
                    <span class="s">"size"</span><span class="p">:</span> <span class="n">limit</span><span class="p">,</span>
                <span class="p">},</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">res</span> <span class="o">=</span> <span class="k">await</span> <span class="n">es_client</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s">"search-keywords"</span><span class="p">,</span> <span class="n">body</span><span class="o">=</span><span class="n">body</span><span class="p">)</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">option</span> <span class="ow">in</span> <span class="n">res</span><span class="p">[</span><span class="s">"suggest"</span><span class="p">][</span><span class="s">"keyword-suggest"</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s">"options"</span><span class="p">]:</span>
        <span class="n">results</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">option</span><span class="p">[</span><span class="s">"_source"</span><span class="p">][</span><span class="s">"keyword"</span><span class="p">])</span>  <span class="c1"># changed
</span>
    <span class="k">return</span> <span class="n">results</span>
</code></pre></div></div>

<p>With these changes, autocomplet becomes more responsive, like this:</p>

<table>
  <thead>
    <tr>
      <th>query</th>
      <th>results</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ㄱ</td>
      <td>가방, 가구, 간식, 겨울 코트</td>
    </tr>
    <tr>
      <td>가</td>
      <td>가방, 가구, 간식</td>
    </tr>
    <tr>
      <td>갑</td>
      <td>가방</td>
    </tr>
  </tbody>
</table>

<p>One limitation of this improvement is that people sometimes search with first consonants, like <code class="language-plaintext highlighter-rouge">ㄱㄱ</code> for <code class="language-plaintext highlighter-rouge">가구</code> and <code class="language-plaintext highlighter-rouge">ㅅㄱ</code> for <code class="language-plaintext highlighter-rouge">사과</code>.</p>

<h3 id="version2-initial-consonant-search">Version2: Initial consonant search</h3>

<p>Initial consonant (초성) search is one of the unique traits of the Korean language.
Instead of typing full syllables, people often just type the initial sounds.
For example, to search for “맥도날드” (McDonald’s), you might just type <code class="language-plaintext highlighter-rouge">ㅁㄷㄴㄷ</code>.</p>

<p>To make this work, add this function to <code class="language-plaintext highlighter-rouge">Hangul.py</code>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Hangul.py
</span>
<span class="p">...</span>

<span class="k">def</span> <span class="nf">extract_choseongs</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="s">"""
    Extracts the initial consonants (choseong) from Korean text.

    Example:
        "사과" -&gt; ["ㅅ", "ㄱ"]
    """</span>
    <span class="n">result</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">character</span> <span class="ow">in</span> <span class="n">text</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">" "</span><span class="p">,</span> <span class="s">""</span><span class="p">).</span><span class="n">strip</span><span class="p">():</span>
        <span class="k">if</span> <span class="s">"가"</span> <span class="o">&lt;=</span> <span class="n">character</span> <span class="o">&lt;=</span> <span class="s">"힣"</span><span class="p">:</span>  <span class="c1"># For Hangul syllables
</span>            <span class="n">code</span> <span class="o">=</span> <span class="nb">ord</span><span class="p">(</span><span class="n">character</span><span class="p">)</span> <span class="o">-</span> <span class="nb">ord</span><span class="p">(</span><span class="s">"가"</span><span class="p">)</span>
            <span class="n">choseong</span> <span class="o">=</span> <span class="n">code</span> <span class="o">//</span> <span class="p">(</span><span class="mi">21</span> <span class="o">*</span> <span class="mi">28</span><span class="p">)</span>
            <span class="n">result</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">CHOSEONGS</span><span class="p">[</span><span class="n">choseong</span><span class="p">])</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">result</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">character</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">result</span>
</code></pre></div></div>

<p>This function simply extracts first consonant (choseong) from each Hangul syllable and returns as a list of them.
For example:</p>

<table>
  <thead>
    <tr>
      <th>Input</th>
      <th>Output</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>“가구”</td>
      <td>[“ㄱ”, “ㄱ”]</td>
    </tr>
    <tr>
      <td>“가방”</td>
      <td>[“ㄱ”, “ㅂ”]</td>
    </tr>
    <tr>
      <td>“사과”</td>
      <td>[“ㅅ”, “ㄱ”]</td>
    </tr>
  </tbody>
</table>

<p>Now we update our keyword indexing logic to include initial consonants in the <code class="language-plaintext highlighter-rouge">suggest.input</code> array:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># update_keywords2.py
</span><span class="kn">from</span> <span class="nn">Hangul</span> <span class="kn">import</span> <span class="n">decompose_syllables</span><span class="p">,</span> <span class="n">extract_choseongs</span>

<span class="p">...</span>

<span class="k">def</span> <span class="nf">_update_search_keywords</span><span class="p">(</span><span class="n">es_client</span><span class="p">:</span> <span class="n">Elasticsearch</span><span class="p">,</span> <span class="n">keywords</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Keyword</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="s">"""
    Update the autocomplete index with new data.
    Return the number of successfully executed actions (including overrides).
    """</span>
    <span class="n">index</span> <span class="o">=</span> <span class="s">"search-keywords"</span>

    <span class="n">actions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
        <span class="n">action</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"_op_type"</span><span class="p">:</span> <span class="s">"index"</span><span class="p">,</span>
            <span class="s">"_index"</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span>
            <span class="s">"_id"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="s">"_source"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"input"</span><span class="p">:</span> <span class="nb">list</span><span class="p">(</span>
                        <span class="nb">set</span><span class="p">(</span>  <span class="c1"># Remove duplicate items if keyword is not Korean
</span>                            <span class="p">[</span>
                                <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">decompose_syllables</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>
                                <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">extract_choseongs</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>  <span class="c1"># new line
</span>                            <span class="p">]</span>
                        <span class="p">)</span>
                    <span class="p">),</span>
                    <span class="s">"weight"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">count</span><span class="p">,</span>
                <span class="p">},</span>
                <span class="s">"keyword"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">}</span>
        <span class="n">actions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>

    <span class="n">res</span> <span class="o">=</span> <span class="n">helpers</span><span class="p">.</span><span class="n">bulk</span><span class="p">(</span><span class="n">es_client</span><span class="p">,</span> <span class="n">actions</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">res</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>This way, our autocomplete can handle initial consonant input.
For example:</p>

<table>
  <thead>
    <tr>
      <th>query</th>
      <th>results</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ㄱㄱ</td>
      <td>가구</td>
    </tr>
    <tr>
      <td>ㄱㅇㅋㅌ</td>
      <td>겨울 코트</td>
    </tr>
  </tbody>
</table>

<p>Our query auto-completion feature now feels a lot better now, but there’s still room to improve.
What’s the common problem we haven’t catched yet?
It’s that users sometimes type Korean words on an English keyboard — like <code class="language-plaintext highlighter-rouge">tkrhk</code> when they meant <code class="language-plaintext highlighter-rouge">사과</code>.</p>

<h3 id="version3-latin-to-jamo-mapping">Version3: Latin-to-Jamo mapping</h3>

<p>To type in Korean, users need to switch their keyboard to the Korean layout.
But it’s super common for people to forget and just start typing in English mode whether they’re on a phone or a PC.</p>

<p>For example, someone trying to search for <code class="language-plaintext highlighter-rouge">가구</code> might accidentally type <code class="language-plaintext highlighter-rouge">rkrn</code>—because that’s what shows up when you type <code class="language-plaintext highlighter-rouge">가구</code> on a standard Korean keyboard while in English mode.</p>

<p>To automatically correct such typos, we map Korean syllables to their corresponding Latin keyboard inputs so the autocomplete system can recognize and suggest the intended Korean keywords even when users accidentally type using the English keyboard layout.</p>

<p>Here’s a utility function that maps Hangul jamo to their Latin keyboard equivalents.
Add this to <code class="language-plaintext highlighter-rouge">Hangul.py</code>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Hangul.py
</span>
<span class="p">...</span>

<span class="n">ENG_TO_KOR</span> <span class="o">=</span> <span class="p">{}</span>
<span class="c1"># Consonants
</span><span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"r"</span><span class="p">:</span> <span class="s">"ㄱ"</span><span class="p">,</span> <span class="s">"R"</span><span class="p">:</span> <span class="s">"ㄲ"</span><span class="p">,</span> <span class="s">"rt"</span><span class="p">:</span> <span class="s">"ㄳ"</span><span class="p">,</span> <span class="s">"s"</span><span class="p">:</span> <span class="s">"ㄴ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"sw"</span><span class="p">:</span> <span class="s">"ㄵ"</span><span class="p">,</span> <span class="s">"sg"</span><span class="p">:</span> <span class="s">"ㄶ"</span><span class="p">,</span> <span class="s">"e"</span><span class="p">:</span> <span class="s">"ㄷ"</span><span class="p">,</span> <span class="s">"E"</span><span class="p">:</span> <span class="s">"ㄸ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"f"</span><span class="p">:</span> <span class="s">"ㄹ"</span><span class="p">,</span> <span class="s">"fr"</span><span class="p">:</span> <span class="s">"ㄺ"</span><span class="p">,</span> <span class="s">"fa"</span><span class="p">:</span> <span class="s">"ㄻ"</span><span class="p">,</span> <span class="s">"fq"</span><span class="p">:</span> <span class="s">"ㄼ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"ft"</span><span class="p">:</span> <span class="s">"ㄽ"</span><span class="p">,</span> <span class="s">"fx"</span><span class="p">:</span> <span class="s">"ㄾ"</span><span class="p">,</span> <span class="s">"fv"</span><span class="p">:</span> <span class="s">"ㄿ"</span><span class="p">,</span> <span class="s">"fg"</span><span class="p">:</span> <span class="s">"ㅀ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"a"</span><span class="p">:</span> <span class="s">"ㅁ"</span><span class="p">,</span> <span class="s">"q"</span><span class="p">:</span> <span class="s">"ㅂ"</span><span class="p">,</span> <span class="s">"Q"</span><span class="p">:</span> <span class="s">"ㅃ"</span><span class="p">,</span> <span class="s">"qt"</span><span class="p">:</span> <span class="s">"ㅄ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"t"</span><span class="p">:</span> <span class="s">"ㅅ"</span><span class="p">,</span> <span class="s">"T"</span><span class="p">:</span> <span class="s">"ㅆ"</span><span class="p">,</span> <span class="s">"d"</span><span class="p">:</span> <span class="s">"ㅇ"</span><span class="p">,</span> <span class="s">"w"</span><span class="p">:</span> <span class="s">"ㅈ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"W"</span><span class="p">:</span> <span class="s">"ㅉ"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">:</span> <span class="s">"ㅊ"</span><span class="p">,</span> <span class="s">"z"</span><span class="p">:</span> <span class="s">"ㅋ"</span><span class="p">,</span> <span class="s">"x"</span><span class="p">:</span> <span class="s">"ㅌ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"v"</span><span class="p">:</span> <span class="s">"ㅍ"</span><span class="p">,</span> <span class="s">"g"</span><span class="p">:</span> <span class="s">"ㅎ"</span><span class="p">})</span>
<span class="c1"># Vowels
</span><span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"k"</span><span class="p">:</span> <span class="s">"ㅏ"</span><span class="p">,</span> <span class="s">"o"</span><span class="p">:</span> <span class="s">"ㅐ"</span><span class="p">,</span> <span class="s">"i"</span><span class="p">:</span> <span class="s">"ㅑ"</span><span class="p">,</span> <span class="s">"O"</span><span class="p">:</span> <span class="s">"ㅒ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"j"</span><span class="p">:</span> <span class="s">"ㅓ"</span><span class="p">,</span> <span class="s">"p"</span><span class="p">:</span> <span class="s">"ㅔ"</span><span class="p">,</span> <span class="s">"u"</span><span class="p">:</span> <span class="s">"ㅕ"</span><span class="p">,</span> <span class="s">"P"</span><span class="p">:</span> <span class="s">"ㅖ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"h"</span><span class="p">:</span> <span class="s">"ㅗ"</span><span class="p">,</span> <span class="s">"hk"</span><span class="p">:</span> <span class="s">"ㅘ"</span><span class="p">,</span> <span class="s">"ho"</span><span class="p">:</span> <span class="s">"ㅙ"</span><span class="p">,</span> <span class="s">"hl"</span><span class="p">:</span> <span class="s">"ㅚ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"y"</span><span class="p">:</span> <span class="s">"ㅛ"</span><span class="p">,</span> <span class="s">"n"</span><span class="p">:</span> <span class="s">"ㅜ"</span><span class="p">,</span> <span class="s">"nj"</span><span class="p">:</span> <span class="s">"ㅝ"</span><span class="p">,</span> <span class="s">"np"</span><span class="p">:</span> <span class="s">"ㅞ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"nl"</span><span class="p">:</span> <span class="s">"ㅟ"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">:</span> <span class="s">"ㅠ"</span><span class="p">,</span> <span class="s">"m"</span><span class="p">:</span> <span class="s">"ㅡ"</span><span class="p">,</span> <span class="s">"ml"</span><span class="p">:</span> <span class="s">"ㅢ"</span><span class="p">})</span>
<span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"l"</span><span class="p">:</span> <span class="s">"ㅣ"</span><span class="p">})</span>

<span class="c1"># Create a reverse mapping from Korean to English
</span><span class="n">KOR_TO_ENG</span> <span class="o">=</span> <span class="p">{</span><span class="n">v</span><span class="p">:</span> <span class="n">k</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">ENG_TO_KOR</span><span class="p">.</span><span class="n">items</span><span class="p">()}</span>

<span class="k">def</span> <span class="nf">convert_jamo_to_latin</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"""
    Convert Hangul jamo (consonants and vowels) to a romanized form based on the standard Korean keyboard layout.

    Example:
        "사과" -&gt; "tkrhk"
    """</span>
    <span class="n">jamos</span> <span class="o">=</span> <span class="n">decompose_syllables</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">text</span><span class="p">)</span>
    <span class="n">result</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">jamo</span> <span class="ow">in</span> <span class="n">jamos</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">jamo</span> <span class="ow">in</span> <span class="n">KOR_TO_ENG</span><span class="p">:</span>
            <span class="n">result</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">KOR_TO_ENG</span><span class="p">[</span><span class="n">jamo</span><span class="p">])</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">result</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">jamo</span><span class="p">)</span>
    <span class="k">return</span> <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</code></pre></div></div>

<p>What this does:</p>
<ul>
  <li>Decompose Korean syllables into jamos.</li>
  <li>Convert each jamo to its QWERTY key equivalent.</li>
  <li>Join the result back into a string.</li>
</ul>

<p>For example:</p>

<table>
  <thead>
    <tr>
      <th>Input</th>
      <th>Output</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>가구</td>
      <td>rkrn</td>
    </tr>
    <tr>
      <td>가방</td>
      <td>rkqkd</td>
    </tr>
    <tr>
      <td>사과</td>
      <td>tkrhk</td>
    </tr>
  </tbody>
</table>

<p>After that, update <code class="language-plaintext highlighter-rouge">_update_search_keywords</code> function to include these typo-prone Latin inputs in the <code class="language-plaintext highlighter-rouge">suggest.input</code> array:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># update_keywords3.py
</span><span class="kn">from</span> <span class="nn">Hangul</span> <span class="kn">import</span> <span class="n">convert_jamo_to_latin</span><span class="p">,</span> <span class="n">decompose_syllables</span><span class="p">,</span> <span class="n">extract_choseongs</span>

<span class="p">...</span>

<span class="k">def</span> <span class="nf">_update_search_keywords</span><span class="p">(</span><span class="n">es_client</span><span class="p">:</span> <span class="n">Elasticsearch</span><span class="p">,</span> <span class="n">keywords</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Keyword</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="s">"""
    Update the autocomplete index with new data.
    Return the number of successfully executed actions (including overrides).
    """</span>
    <span class="n">index</span> <span class="o">=</span> <span class="s">"search-keywords"</span>

    <span class="n">actions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
        <span class="n">action</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"_op_type"</span><span class="p">:</span> <span class="s">"index"</span><span class="p">,</span>
            <span class="s">"_index"</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span>
            <span class="s">"_id"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="s">"_source"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"input"</span><span class="p">:</span> <span class="nb">list</span><span class="p">(</span>
                        <span class="nb">set</span><span class="p">(</span>  <span class="c1"># Remove duplicate items if keyword is not Korean
</span>                            <span class="p">[</span>
                                <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">decompose_syllables</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>
                                <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">extract_choseongs</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>
                                <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">convert_jamo_to_latin</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>  <span class="c1"># new line
</span>                            <span class="p">]</span>
                        <span class="p">)</span>
                    <span class="p">),</span>
                    <span class="s">"weight"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">count</span><span class="p">,</span>
                <span class="p">},</span>
                <span class="s">"keyword"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">}</span>
        <span class="n">actions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>

    <span class="n">res</span> <span class="o">=</span> <span class="n">helpers</span><span class="p">.</span><span class="n">bulk</span><span class="p">(</span><span class="n">es_client</span><span class="p">,</span> <span class="n">actions</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">res</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>After running the keyword update script, here’s what the document for “사과” might look like:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"_index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"search-keywords"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"사과"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_score"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_source"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"suggest"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"input"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"ㅅㅏㄱㅘ"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"ㅅㄱ"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"tkrhk"</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"weight"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"keyword"</span><span class="p">:</span><span class="w"> </span><span class="s2">"사과"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Note that each input value is the result of the following utility functions:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">"".join(decompose_syllables("사과"))</code> -&gt; <code class="language-plaintext highlighter-rouge">ㅅㅏㄱㅘ</code></li>
  <li><code class="language-plaintext highlighter-rouge">"".join(extract_choseongs("사과"))</code> -&gt; <code class="language-plaintext highlighter-rouge">ㅅㄱ</code></li>
  <li><code class="language-plaintext highlighter-rouge">"".join(convert_jamo_to_latin("사과"))</code> -&gt; <code class="language-plaintext highlighter-rouge">tkrhk</code></li>
</ul>

<p>Even if the user mistakenly types in English mode, your autocomplete will still just work.</p>

<p>Meanwhile, the English keyword <code class="language-plaintext highlighter-rouge">airpods 4</code> stays mostly intact because we removed duplicate items when indexing <code class="language-plaintext highlighter-rouge">input</code> values:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"_index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"search-keywords"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"airpods 4"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_score"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_source"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"suggest"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"input"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"airpods4"</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"weight"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"keyword"</span><span class="p">:</span><span class="w"> </span><span class="s2">"airpods 4"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="demonstration-with-search-box">Demonstration with search box</h2>

<p>Up until now, we’ve been testing things without a proper UI with actual search box.</p>

<p>Here’s a simple HTML + JavaScript setup you can use to test the example in action.
It talks to a backend endpoint that returns suggestions based on user input.</p>

<p>Just save the code below as an <code class="language-plaintext highlighter-rouge">.html</code> file and open it in your browser:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>

<span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;title&gt;</span>Search Suggestions<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/head&gt;</span>

<span class="nt">&lt;body&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container mt-5"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-md-6 mx-auto"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"search"</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">placeholder=</span><span class="s">"Search"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;ul</span> <span class="na">id=</span><span class="s">"results"</span> <span class="na">class=</span><span class="s">"list-group mt-2"</span><span class="nt">&gt;&lt;/ul&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;script&gt;</span>
        <span class="kd">const</span> <span class="nx">API_URL</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://localhost:8000/keyword-suggestions</span><span class="dl">'</span><span class="p">;</span>

        <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">search</span><span class="dl">'</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">input</span><span class="dl">'</span><span class="p">,</span> <span class="nx">e</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">API_URL</span><span class="p">}</span><span class="s2">?limit=10&amp;query=</span><span class="p">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
                <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">())</span>
                <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">results</span><span class="dl">'</span><span class="p">);</span>
                    <span class="nx">results</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="dl">''</span><span class="p">;</span>
                    <span class="nx">data</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">text</span> <span class="o">=&gt;</span> <span class="p">{</span>
                        <span class="kd">const</span> <span class="nx">li</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">li</span><span class="dl">'</span><span class="p">);</span>
                        <span class="nx">li</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">list-group-item</span><span class="dl">'</span><span class="p">;</span>
                        <span class="nx">li</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">cursor</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">pointer</span><span class="dl">'</span><span class="p">;</span>
                        <span class="nx">li</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">text</span><span class="p">;</span>
                        <span class="nx">li</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
                            <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">search</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">text</span><span class="p">;</span>
                            <span class="nx">results</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="dl">''</span><span class="p">;</span>
                        <span class="p">});</span>
                        <span class="nx">results</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">li</span><span class="p">);</span>
                    <span class="p">});</span>
                <span class="p">})</span>
                <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">));</span>
        <span class="p">});</span>
    <span class="nt">&lt;/script&gt;</span>
<span class="nt">&lt;/body&gt;</span>

<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>This is just a simple example page, but it gives you a working UI to see your suggestions in real time.
Just make sure your backend is running and serving suggestions at the URL specified (http://localhost:8000/keyword-suggestions).</p>

<p><img src="/assets/posts/36/search_box_demo.png" alt="Search box" /></p>

<h2 id="context-suggester">Context suggester</h2>

<p>Autocomplete suggestions can be fine-tuned with filters and boostings.
One useful example is to personalize suggestions—so instead of everyone seeing the same results, each user gets suggestions based on their own search history or behavior.</p>

<p>To make that happen, you can add context mappings to the <code class="language-plaintext highlighter-rouge">search-keywords</code> index.
Here’s a simple example where we define a context using <code class="language-plaintext highlighter-rouge">user_id</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">/search-keywords</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">"suggest"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">can</span><span class="w"> </span><span class="err">use</span><span class="w"> </span><span class="err">any</span><span class="w"> </span><span class="err">name</span><span class="w"> </span><span class="err">here</span><span class="w"> </span><span class="err">instead</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="s2">"suggest"</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completion"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"analyzer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"standard"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"contexts"</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">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user_id"</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">"category"</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">"keyword"</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">"keyword"</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></code></pre></div></div>

<p>Here, we’re using the <code class="language-plaintext highlighter-rouge">category</code> context type, which lets us tag suggestions with one or more user IDs when we index them.
When a user types something, we can filter or boost suggestions based on their context.</p>

<p>Let’s look at the indexing part.
Here’s how you’d update the <code class="language-plaintext highlighter-rouge">search-keywords</code> index with user-personalized data:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_update_search_keywords</span><span class="p">(</span>
    <span class="n">es_client</span><span class="p">:</span> <span class="n">Elasticsearch</span><span class="p">,</span> <span class="n">keywords</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Keyword</span><span class="p">],</span> <span class="n">user_ids</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="s">"""
    Update the autocomplete index with new data.
    Return the number of successfully executed actions (including overrides).
    """</span>
    <span class="n">index</span> <span class="o">=</span> <span class="s">"search-keywords"</span>

    <span class="n">actions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
        <span class="n">action</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"_op_type"</span><span class="p">:</span> <span class="s">"index"</span><span class="p">,</span>
            <span class="s">"_index"</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span>
            <span class="s">"_id"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="s">"_source"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"input"</span><span class="p">:</span> <span class="p">[</span>
                        <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">decompose_syllables</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>
                        <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">extract_choseongs</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>
                        <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">convert_jamo_to_latin</span><span class="p">(</span><span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">)),</span>
                    <span class="p">],</span>
                    <span class="s">"weight"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">count</span><span class="p">,</span>
                    <span class="s">"contexts"</span><span class="p">:</span> <span class="p">{</span>  <span class="c1"># new line
</span>                        <span class="s">"user_id"</span><span class="p">:</span> <span class="n">user_ids</span><span class="p">,</span>
                    <span class="p">},</span>
                <span class="p">},</span>
                <span class="s">"keyword"</span><span class="p">:</span> <span class="n">keyword</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">}</span>
        <span class="n">actions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>

    <span class="n">res</span> <span class="o">=</span> <span class="n">helpers</span><span class="p">.</span><span class="n">bulk</span><span class="p">(</span><span class="n">es_client</span><span class="p">,</span> <span class="n">actions</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">res</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>In the querying part, just plug the <code class="language-plaintext highlighter-rouge">user_id</code> context into your search body like so:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>

<span class="n">body</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"suggest"</span><span class="p">:</span> <span class="p">{</span>
        <span class="s">"keyword-suggest"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"prefix"</span><span class="p">:</span> <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">decompose_syllables</span><span class="p">(</span><span class="n">query</span><span class="p">)),</span>
            <span class="s">"completion"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"field"</span><span class="p">:</span> <span class="s">"suggest"</span><span class="p">,</span>
                <span class="s">"size"</span><span class="p">:</span> <span class="n">limit</span><span class="p">,</span>
                <span class="s">"contexts"</span><span class="p">:</span> <span class="p">{</span><span class="s">"user_id"</span><span class="p">:</span> <span class="p">[</span><span class="s">"user1"</span><span class="p">]},</span>  <span class="c1"># new line
</span>            <span class="p">},</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This way, you’ve added personalized query suggestions to your search experience.</p>

<p class="notice--info">As of Elasticsearch 8.13.4, you can define up to 10 context mappings per completion field.</p>

<p>To tear down the indices after the demonstration:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">DELETE</span><span class="w"> </span><span class="err">/search-logs</span><span class="w">
</span><span class="err">DELETE</span><span class="w"> </span><span class="err">/search-keywords</span><span class="w">
</span></code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>While you can adapt the example in this post for your own application, there’s more you can (and probably should) do—like handling special characters, supporting partial matches, and filtering out weird or meaningless suggestions.
However, once you’ve got the basics down, adding those extra layers gets a lot easier.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Hangul">Hangul - Wikipedia</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Hangul_Syllables">Hangul Syllables - Wikipedia</a></li>
  <li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.13/search-suggesters.html">Suggesters - Elasticsearch</a></li>
</ul>]]></content><author><name>Mienxiu</name></author><category term="elasticsearch" /><category term="ir" /><category term="python" /><summary type="html"><![CDATA[Query auto-completion (QAC) is a feature that suggests completions for a user’s input query. It tries to guess what you’re about to type and throws out suggestions, commonly based on past searches. Pretty much every site or app with a search box has it—Google, YouTube, Amazon, Naver, you name it. It’s just one of those things we use every day.]]></summary></entry><entry><title type="html">Solving Latin Squares as Constraint Satisfaction Problems using Backtracking Search with Heuristics</title><link href="https://mienxiu.com/solving-latin-squares-as-csps/" rel="alternate" type="text/html" title="Solving Latin Squares as Constraint Satisfaction Problems using Backtracking Search with Heuristics" /><published>2025-04-30T00:00:00+00:00</published><updated>2025-04-30T00:00:00+00:00</updated><id>https://mienxiu.com/solving-latin-squares-as-csps</id><content type="html" xml:base="https://mienxiu.com/solving-latin-squares-as-csps/"><![CDATA[<p>A <strong>Latin square</strong> is an <code class="language-plaintext highlighter-rouge">n x n</code> grid filled with <code class="language-plaintext highlighter-rouge">n</code> different symbols (commonly numbers 1 to <code class="language-plaintext highlighter-rouge">n</code>), each appearing exactly once in every row and every column.</p>

<p>An example of a 3x3 Latin square would look like the following table:</p>

<table>
  <thead>
    <tr>
      <th>1</th>
      <th>2</th>
      <th>3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>2</td>
    </tr>
    <tr>
      <td>2</td>
      <td>3</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>In this post, I will illustrate the key concepts of constraint satisfaction problems (CSPs) and backtracking search, two fundamental techniques in AI, by applying them to solve Latin squares.</p>

<h2 id="constraint-satisfaction">Constraint Satisfaction</h2>

<p>Constraint satisfaction is a subfield of optimization that focuses on selecting the best option from a set of possible options.
It ensures that a solution meets all given constraints, such as resource limitations or time restrictions.
Once these constraints are met, the next goal is to optimize a given function, either by maximizing or minimizing it.</p>

<p>In a constraint satisfaction problem (often abbreviated as CSP), variables must be assigned values while meeting specific conditions.</p>

<p>For a computer to solve this problem, the problem must be clearly structured.
The key properties of a CSP are:</p>
<ul>
  <li>Variables: The set of possible values that each variable can take.</li>
  <li>Domains: The possible values (options) for a variable.</li>
  <li>Constraints: The specific conditions that must be satisfied when assigning values to the variables.</li>
</ul>

<p>The constraints in particular can be categorized based on how many variables they involve:</p>
<ul>
  <li>Unary Constraint: A constraint that involves only one variable, restricting the values it can take from its domain.</li>
  <li>Binary Constraint: A constraint that involves two variables, specifying the allowable pairs of values between them.</li>
</ul>

<p>One of the most well-known examples of a CSP is Sudoku.
In the context of Sudoku, the properties can be described as follows:</p>
<ul>
  <li>Variables: The empty squares on the Sudoku grid.</li>
  <li>Domains: The set <code class="language-plaintext highlighter-rouge">{1, 2, 3, 4, 5, 6, 7, 8, 9}</code>, representing the possible digits for each empty square.</li>
  <li>Constraints: No digit can repeat in any row, column, or 3x3 subgrid.</li>
</ul>

<p>In the context of a 3x3 Latin square, each property of CSP can be described as follows:</p>
<ul>
  <li>
    <p>Variables: <code class="language-plaintext highlighter-rouge">{(0, 0), (0, 1), (1, 1), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)}</code> (the positions of the grid)</p>

    <table>
      <thead>
        <tr>
          <th>position</th>
          <th>0</th>
          <th>1</th>
          <th>2</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>0</td>
          <td>(0, 0)</td>
          <td>(0, 1)</td>
          <td>(0, 2)</td>
        </tr>
        <tr>
          <td>1</td>
          <td>(1, 0)</td>
          <td>(1, 1)</td>
          <td>(1, 2)</td>
        </tr>
        <tr>
          <td>2</td>
          <td>(2, 0)</td>
          <td>(2, 1)</td>
          <td>(2, 2)</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>Domains: <code class="language-plaintext highlighter-rouge">{1, 2, 3}</code> (the possible values for each cell)</li>
  <li>Constraints:
    <ul>
      <li>Row ronstraints: No two cells in the same row can have the same value (e.g., <code class="language-plaintext highlighter-rouge">{(0, 0) != (0, 1), (0, 0) != (0, 2), ...}</code>).</li>
      <li>Column constraints: No two cells in the same column can have the same value (e.g., <code class="language-plaintext highlighter-rouge">{(0, 0) != (1, 0), (0, 0) != (2, 0), ...}</code>).</li>
    </ul>
  </li>
</ul>

<p>Note that the constraints defining a Latin square are all binary constraints unless there are any pre-filled cells, in which case they would be unary constraints.</p>

<p>To model the Latin square as a CSP in Python, we first define a general-purpose <code class="language-plaintext highlighter-rouge">CSP</code> class:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># csp.py
</span>

<span class="k">class</span> <span class="nc">CSP</span><span class="p">:</span>
    <span class="s">"""
    Represents a Constraint Satisfaction Problem (CSP).
    """</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">variables</span><span class="p">:</span> <span class="nb">list</span><span class="p">,</span> <span class="n">domains</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">constraints</span><span class="p">:</span> <span class="nb">list</span><span class="p">):</span>
        <span class="s">"""
        Initialize the CSP with variables, domains, and constraints.
        Args:
            variables (list): A list of variables in the CSP.
            domains (dict): A dictionary mapping each variable to a set of possible values.
            constraints (list): A list of constraints that must be satisfied.
        """</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">variables</span> <span class="o">=</span> <span class="n">variables</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">domains</span> <span class="o">=</span> <span class="n">domains</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">constraints</span> <span class="o">=</span> <span class="n">constraints</span>
</code></pre></div></div>

<p>Next, we create a <code class="language-plaintext highlighter-rouge">CSP</code> specifically for a Latin square:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># latin_square.py
</span><span class="kn">from</span> <span class="nn">csp</span> <span class="kn">import</span> <span class="n">CSP</span>


<span class="k">def</span> <span class="nf">create_latin_square</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">CSP</span><span class="p">:</span>
    <span class="s">"""
    Create a CSP for Latin square of given size.
    """</span>
    <span class="n">variables</span> <span class="o">=</span> <span class="p">[(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)]</span>  <span class="c1"># variable == cell
</span>    <span class="n">_domains</span> <span class="o">=</span> <span class="p">{</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)}</span>  <span class="c1"># Possible values for each cell
</span>    <span class="n">domains</span> <span class="o">=</span> <span class="p">{</span><span class="n">variable</span><span class="p">:</span> <span class="nb">set</span><span class="p">(</span><span class="n">_domains</span><span class="p">)</span> <span class="k">for</span> <span class="n">variable</span> <span class="ow">in</span> <span class="n">variables</span><span class="p">}</span>

    <span class="c1"># Generate binary constraints for rows and columns
</span>    <span class="n">constraints</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span><span class="p">):</span>
                <span class="n">constraints</span><span class="p">.</span><span class="n">append</span><span class="p">(((</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">k</span><span class="p">)))</span>  <span class="c1"># Row constraints
</span>                <span class="n">constraints</span><span class="p">.</span><span class="n">append</span><span class="p">(((</span><span class="n">j</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">i</span><span class="p">)))</span>  <span class="c1"># Column constraints
</span>
    <span class="k">return</span> <span class="n">CSP</span><span class="p">(</span><span class="n">variables</span><span class="p">,</span> <span class="n">domains</span><span class="p">,</span> <span class="n">constraints</span><span class="p">)</span>
</code></pre></div></div>

<p>Once a solution is found, we can neatly print the Latin square with the function below:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># latin_square.py
</span><span class="p">...</span>


<span class="k">def</span> <span class="nf">print_latin_square</span><span class="p">(</span><span class="n">solution</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">]):</span>
    <span class="s">"""Print the Latin square solution as a formatted table."""</span>
    <span class="c1"># Determine the size automatically from the solution
</span>    <span class="n">coordinates</span> <span class="o">=</span> <span class="n">solution</span><span class="p">.</span><span class="n">keys</span><span class="p">()</span>
    <span class="n">size</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">max</span><span class="p">(</span><span class="nb">max</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">j</span> <span class="ow">in</span> <span class="n">coordinates</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>

    <span class="c1"># Print the Latin square
</span>    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Latin square Solution for </span><span class="si">{</span><span class="n">size</span><span class="si">}</span><span class="s">x</span><span class="si">{</span><span class="n">size</span><span class="si">}</span><span class="s">:"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
        <span class="n">row</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
            <span class="n">row</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">solution</span><span class="p">[(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)]))</span>
        <span class="k">print</span><span class="p">(</span><span class="s">" "</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">row</span><span class="p">))</span>
</code></pre></div></div>

<h3 id="arc-consistency">Arc Consistency</h3>

<p>While unary constraints reduce the domain of one variable, binary constraints define relationships between pairs of variables.
This relationship is called an <em>arc</em>.</p>

<p>An arc is typically denoted as <code class="language-plaintext highlighter-rouge">(X -&gt; Y)</code>, meaning:</p>
<ul>
  <li>Variable <code class="language-plaintext highlighter-rouge">X</code> is constrained in relation to variable <code class="language-plaintext highlighter-rouge">Y</code>.</li>
  <li>The constraint defines the allowable values of <code class="language-plaintext highlighter-rouge">X</code>, depending on the values of <code class="language-plaintext highlighter-rouge">Y</code>.</li>
</ul>

<p>In the context of a 3x3 Latin square, there are arcs between any two variables that are constrained by being in the same row or column.
These arcs can be represented as follows:</p>
<ul>
  <li>Same row: ((0, 0) -&gt; (0, 1)), ((0, 0) -&gt; (0, 2)), …</li>
  <li>Same column: ((0, 0) -&gt; (1, 0)), ((0, 0) -&gt; (2, 0)), …</li>
</ul>

<p>The following figure is a node graph of a 3x3 Latin square.
Each node represents a variable, and each line represents a binary constraint (an arc).</p>

<p><img src="/assets/posts/35/latin_square_node_graph.png" alt="Node graph of a 3 x 3 Latin square" /></p>

<p>Arc consistency is a property of binary constraints between two variables in a CSP.</p>

<p>In technical terms:</p>
<blockquote>
  <p>An arc <code class="language-plaintext highlighter-rouge">(X -&gt; Y)</code> is consistent if for every value in the domain of <code class="language-plaintext highlighter-rouge">X</code>, there is at least one compatible value in the domain of <code class="language-plaintext highlighter-rouge">Y</code> that satisfies the constraint between <code class="language-plaintext highlighter-rouge">X</code> and <code class="language-plaintext highlighter-rouge">Y</code>.</p>
</blockquote>

<p>Arc consistency is an important concept because it helps prune the search space by eliminating values from the variable domains that can’t possibly lead to a solution.
This significantly speeds up the solving process.
The most common algorithm used to enforce arc consistency is <strong>AC-3</strong>, which we’ll discuss later.</p>

<h2 id="backtracking">Backtracking</h2>

<p>The idea of backtracking is to build a solution incrementally, abandoning a path as soon as it becomes clear that there is no solution found.
It’s not just used in constraint satisfaction problems.
You’ll also see it in problems like puzzles and combinatorial search, where efficiently exploring all possible options really matters.</p>

<p>Here’s how it works, step by step:</p>
<ol>
  <li>Start from an empty or initial state.</li>
  <li>Make a choice to move forward (pick a possible next step).</li>
  <li>If the current path is valid, continue exploring deeper.</li>
  <li>If you hit a dead-end (no valid moves or constraints are violated), undo the last choice (backtrack) and try another option.</li>
  <li>Repeat this process until you find a complete solution or exhaust all possibilities.</li>
</ol>

<p>Basically, it’s a recursive algorithm.</p>

<p>Let’s encode this idea in Python.</p>

<p>Here’s a naive implementaion to solve a 5x5 Latin square:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backtrack.py
</span><span class="s">"""
Naive backtracking search for Latin square CSP.
"""</span>

<span class="kn">from</span> <span class="nn">csp</span> <span class="kn">import</span> <span class="n">CSP</span>
<span class="kn">from</span> <span class="nn">latin_square</span> <span class="kn">import</span> <span class="n">create_latin_square</span><span class="p">,</span> <span class="n">print_latin_square</span>


<span class="k">def</span> <span class="nf">backtrack</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">assignment</span><span class="p">)</span> <span class="o">==</span> <span class="nb">len</span><span class="p">(</span><span class="n">csp</span><span class="p">.</span><span class="n">variables</span><span class="p">):</span>  <span class="c1"># Return assignment if complete
</span>        <span class="k">return</span> <span class="n">assignment</span>

    <span class="n">variable</span> <span class="o">=</span> <span class="n">select_unassigned_variable</span><span class="p">(</span><span class="n">assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">get_domain_values</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">):</span>
        <span class="n">new_assignment</span> <span class="o">=</span> <span class="n">assignment</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
        <span class="n">new_assignment</span><span class="p">[</span><span class="n">variable</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
        <span class="k">if</span> <span class="n">is_consistent</span><span class="p">(</span><span class="n">new_assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">):</span>
            <span class="n">result</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">(</span><span class="n">new_assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">)</span>  <span class="c1"># Recur with the new assignment
</span>            <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">result</span>  <span class="c1"># Solution found
</span>    <span class="k">return</span> <span class="bp">None</span>  <span class="c1"># No solution found
</span>

<span class="k">def</span> <span class="nf">select_unassigned_variable</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">variable</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">variables</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">variable</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">variable</span>
    <span class="k">return</span> <span class="bp">None</span>


<span class="k">def</span> <span class="nf">get_domain_values</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""
    Get the domain values for a variable without specific ordering.
    """</span>
    <span class="k">return</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">variable</span><span class="p">]</span>


<span class="k">def</span> <span class="nf">is_consistent</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">constraints</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""
    Check if the assignment is consistent with the constraints for Latin square.
    """</span>
    <span class="c1"># Check if any constraints are violated
</span>    <span class="k">for</span> <span class="n">var1</span><span class="p">,</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">constraints</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var1</span> <span class="ow">in</span> <span class="n">assignment</span> <span class="ow">and</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">assignment</span><span class="p">[</span><span class="n">var1</span><span class="p">]</span> <span class="o">==</span> <span class="n">assignment</span><span class="p">[</span><span class="n">var2</span><span class="p">]:</span>
                <span class="k">return</span> <span class="bp">False</span>  <span class="c1"># assignment is not consistent
</span>    <span class="k">return</span> <span class="bp">True</span>  <span class="c1"># assignment is consistent
</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">SIZE</span> <span class="o">=</span> <span class="mi">5</span>
    <span class="n">latin_square_csp</span> <span class="o">=</span> <span class="n">create_latin_square</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="n">SIZE</span><span class="p">)</span>

    <span class="c1"># assignment is a dictionary that maps variable to value
</span>    <span class="n">assignment</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">({},</span> <span class="n">latin_square_csp</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">assignment</span><span class="p">:</span>
        <span class="n">print_latin_square</span><span class="p">(</span><span class="n">assignment</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"No solution found."</span><span class="p">)</span>
</code></pre></div></div>

<p>Here’s how it flows.
The <code class="language-plaintext highlighter-rouge">backtrack</code> function tries to assign values to variables one by one:</p>
<ul>
  <li>Base case: If every variable has a value (assignment complete), return the solution.</li>
  <li><code class="language-plaintext highlighter-rouge">select_unassigned_variable</code>: Pick the next unassigned variable.</li>
  <li><code class="language-plaintext highlighter-rouge">get_domain_values</code>: Try every possible value from the variable’s domain.</li>
  <li><code class="language-plaintext highlighter-rouge">is_consistent</code>: After each assignment, check if constraints (no conflicts) are satisfied.</li>
  <li>Recursion: Continue with this new assignment; if it leads to a dead end, backtrack.</li>
</ul>

<p>Running this code will output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Latin square Solution for 5x5:
1 2 3 4 5
2 1 4 5 3
3 4 5 1 2
4 5 2 3 1
5 3 1 2 4
</code></pre></div></div>

<p>Below is a visual breakdown of the step-by-step process for solving a 5x5 Latin square:</p>

<p><img src="/assets/posts/35/solving_latin_square.webp" alt="Step-by-step process for solving 5x5 Latin square" /></p>

<h3 id="testing">Testing</h3>

<p>To verify the solution:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test_latin_square.py
</span><span class="kn">from</span> <span class="nn">backtrack</span> <span class="kn">import</span> <span class="n">backtrack</span>
<span class="kn">from</span> <span class="nn">latin_square</span> <span class="kn">import</span> <span class="n">create_latin_square</span>


<span class="k">def</span> <span class="nf">check_rows</span><span class="p">(</span><span class="n">solution</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">],</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""Check if each row contains unique numbers from 1 to size."""</span>
    <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
        <span class="n">row_values</span> <span class="o">=</span> <span class="p">{</span><span class="n">solution</span><span class="p">[(</span><span class="n">r</span><span class="p">,</span> <span class="n">c</span><span class="p">)]</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)}</span>
        <span class="k">if</span> <span class="n">row_values</span> <span class="o">!=</span> <span class="nb">set</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)):</span>
            <span class="k">return</span> <span class="bp">False</span>
    <span class="k">return</span> <span class="bp">True</span>


<span class="k">def</span> <span class="nf">check_cols</span><span class="p">(</span><span class="n">solution</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">],</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""Check if each column contains unique numbers from 1 to size."""</span>
    <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
        <span class="n">col_values</span> <span class="o">=</span> <span class="p">{</span><span class="n">solution</span><span class="p">[(</span><span class="n">r</span><span class="p">,</span> <span class="n">c</span><span class="p">)]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)}</span>
        <span class="k">if</span> <span class="n">col_values</span> <span class="o">!=</span> <span class="nb">set</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)):</span>
            <span class="k">return</span> <span class="bp">False</span>
    <span class="k">return</span> <span class="bp">True</span>


<span class="k">def</span> <span class="nf">test_latin_square</span><span class="p">():</span>
    <span class="s">"""Test the Latin square CSP with different sizes."""</span>
    <span class="k">for</span> <span class="n">size</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">10</span><span class="p">):</span>
        <span class="n">latin_square</span> <span class="o">=</span> <span class="n">create_latin_square</span><span class="p">(</span><span class="n">size</span><span class="p">)</span>
        <span class="n">solution</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">({},</span> <span class="n">latin_square</span><span class="p">)</span>

        <span class="k">assert</span> <span class="n">solution</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">,</span> <span class="s">"Backtrack function should find a solution"</span>
        <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">solution</span><span class="p">)</span> <span class="o">==</span> <span class="n">size</span> <span class="o">*</span> <span class="n">size</span><span class="p">,</span> <span class="s">"Solution should have size * size assignments"</span>

        <span class="c1"># Verify constraints
</span>        <span class="k">assert</span> <span class="n">check_rows</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="s">"Row constraint violated"</span>
        <span class="k">assert</span> <span class="n">check_cols</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="s">"Column constraint violated"</span>
</code></pre></div></div>

<p>You can run it using <a href="https://docs.pytest.org/en/stable/">pytest</a>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pytest test_latin_square.py
</code></pre></div></div>

<h3 id="limitation">Limitation</h3>

<p>The problem with this naive implementation is that as the size of the square increases, the number of <code class="language-plaintext highlighter-rouge">backtrack</code> calls grows dramatically, leading to a huge increase in computation.</p>

<p>To illustrate this, we can get a sense of the computational effort by counting how many times <code class="language-plaintext highlighter-rouge">backtrack</code> is called.</p>

<p>Here’s the result for Latin squares ranging from size 1 to 10:</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Backtrack Calls</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
    </tr>
    <tr>
      <td>2</td>
      <td>5</td>
    </tr>
    <tr>
      <td>3</td>
      <td>11</td>
    </tr>
    <tr>
      <td>4</td>
      <td>17</td>
    </tr>
    <tr>
      <td>5</td>
      <td>38</td>
    </tr>
    <tr>
      <td>6</td>
      <td>63</td>
    </tr>
    <tr>
      <td>7</td>
      <td>132</td>
    </tr>
    <tr>
      <td>8</td>
      <td>65</td>
    </tr>
    <tr>
      <td>9</td>
      <td>303</td>
    </tr>
    <tr>
      <td>10</td>
      <td>1465</td>
    </tr>
  </tbody>
</table>

<p>Line graph:</p>

<p><img src="/assets/posts/35/backtrack_calls_for_latin_squares.png" alt="Backtrack calls for latin squares of different sizes" /></p>

<p>Meanwhile, one interesting observation is that a larger problem doesn’t always mean more backtracking.
For example, solving the 8x8 grid actually required fewer backtrack calls than the 7x7.
This suggests that problem difficulty isn’t just about size.
Some instances are just naturally friendlier to the search process.</p>

<p>Back to the main point, as you can see, the computational cost increases rapidly, even for moderately larger squares.</p>

<p>Moreover, the problem becomes even harder when additional constraints are added.
For example, consider a Diagonal Latin square, where elements along both the main diagonal and the anti-diagonal must also be unique.</p>

<p>We can model this diagonal Latin square by adding more constraints as follows:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># latin_square.py
</span><span class="p">...</span>


<span class="k">def</span> <span class="nf">create_diagonal_latin_square</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">CSP</span><span class="p">:</span>
    <span class="s">"""
    Create a CSP for diagonal Latin square (DLS) of given size.
    """</span>
    <span class="n">csp</span> <span class="o">=</span> <span class="n">create_latin_square</span><span class="p">(</span><span class="n">size</span><span class="p">)</span>

    <span class="c1"># Add constraints for diagonal elements
</span>    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span><span class="p">):</span>
            <span class="c1"># Main diagonal constraints
</span>            <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">.</span><span class="n">append</span><span class="p">(((</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">k</span><span class="p">)))</span>
            <span class="c1"># Anti-diagonal constraints
</span>            <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">.</span><span class="n">append</span><span class="p">(((</span><span class="n">i</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">),</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">k</span><span class="p">)))</span>
    <span class="k">return</span> <span class="n">csp</span>
</code></pre></div></div>

<p>The following figure is a node graph of a 3x3 diagonal Latin square.</p>

<p><img src="/assets/posts/35/diagonal_latin_square_node_graph.png" alt="Node graph of a 3x3 Diagonal Latin square" /></p>

<p class="notice--info">In fact, diagonal Latin squares are impossible for certain sizes, such as 2 and 3.</p>

<p>As as result, adding these diagonal constraints causes the number of backtracking calls to skyrocket:</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Backtrack Calls</th>
      <th>Solution Found</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>2</td>
      <td>5</td>
      <td>No</td>
    </tr>
    <tr>
      <td>3</td>
      <td>28</td>
      <td>No</td>
    </tr>
    <tr>
      <td>4</td>
      <td>30</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>5</td>
      <td>191</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>6</td>
      <td>1658</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>7</td>
      <td>87944</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>Below is a visual explanation of how many steps are added to solve a 5x5 Latin square compared to the previous example:</p>

<p><img src="/assets/posts/35/solving_diagonal_latin_square.webp" alt="Step-by-step process of solving diagonal Latin square" /></p>

<p>This happens because the search space becomes much more restricted, making it harder for the algorithm to find valid placements without conflict.
With more constraints, the algorithm has to backtrack more frequently.</p>

<p>Adding unary constraints can also make the problem significantly harder as well.</p>

<p>For instance, let’s force the main diagonal elements to follow a specific sequence:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># latin_square.py
</span><span class="p">...</span>


<span class="k">def</span> <span class="nf">create_diagonal_latin_square_with_unary_constraints</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">CSP</span><span class="p">:</span>
    <span class="s">"""
    Create a CSP for diagonal Latin square (DLS) of given size with additional unary constraints.
    """</span>
    <span class="n">csp</span> <span class="o">=</span> <span class="n">create_diagonal_latin_square</span><span class="p">(</span><span class="n">size</span><span class="p">)</span>
    <span class="c1"># Add unary constraints to make problem harder
</span>    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
        <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[(</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span><span class="p">)]</span> <span class="o">=</span> <span class="p">{</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">}</span>
    <span class="k">return</span> <span class="n">csp</span>
</code></pre></div></div>

<p>These unary constraints further reduce the available choices for key variables right from the beginning, which leads to more failures during search and thus more backtracking.</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Backtrack Calls</th>
      <th>Solution Found</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>2</td>
      <td>3</td>
      <td>No</td>
    </tr>
    <tr>
      <td>3</td>
      <td>10</td>
      <td>No</td>
    </tr>
    <tr>
      <td>4</td>
      <td>56</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>5</td>
      <td>400</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>6</td>
      <td>44534</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>Once you throw in those diagonal and unary constraints, even a 7x7 Latin square becomes brutally slow for naive backtracking.
On most modern PCs, you’ll be stuck waiting who-knows-how-long for a solution.</p>

<p>To address these challenges and improve efficiency, we will enforce arc consistency by using AC-3 algorithm.</p>

<p>Update the test to verify the diagonal Latin square with unary constraints:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test_latin_square.py
</span><span class="p">...</span>


<span class="k">def</span> <span class="nf">check_main_diagonal</span><span class="p">(</span><span class="n">solution</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">],</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""Check if the main diagonal contains unique numbers from 1 to size."""</span>
    <span class="n">diag_values</span> <span class="o">=</span> <span class="p">{</span><span class="n">solution</span><span class="p">[(</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span><span class="p">)]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)}</span>
    <span class="c1"># For the specific DLS generated by create_dls, the diagonal is fixed
</span>    <span class="n">expected_diag_values</span> <span class="o">=</span> <span class="p">{</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)}</span>
    <span class="k">return</span> <span class="n">diag_values</span> <span class="o">==</span> <span class="n">expected_diag_values</span>


<span class="k">def</span> <span class="nf">check_anti_diagonal</span><span class="p">(</span><span class="n">solution</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">],</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""Check if the anti-diagonal contains unique numbers from 1 to size."""</span>
    <span class="n">anti_diag_values</span> <span class="o">=</span> <span class="p">{</span><span class="n">solution</span><span class="p">[(</span><span class="n">i</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">)]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)}</span>
    <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">anti_diag_values</span><span class="p">)</span> <span class="o">==</span> <span class="n">size</span> <span class="ow">and</span> <span class="nb">all</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;=</span> <span class="n">v</span> <span class="o">&lt;=</span> <span class="n">size</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">anti_diag_values</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">check_unary_constraints</span><span class="p">(</span><span class="n">solution</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">],</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""Check if the unary constraints are satisfied."""</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span> <span class="ow">in</span> <span class="n">solution</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">solution</span><span class="p">[(</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span><span class="p">)]</span> <span class="o">!=</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">:</span>
                <span class="k">return</span> <span class="bp">False</span>
    <span class="k">return</span> <span class="bp">True</span>


<span class="k">def</span> <span class="nf">test_latin_square_diagonal_with_unary_constraints</span><span class="p">():</span>
    <span class="s">"""Test the Latin square CSP with diagonal constraints and unary constraints."""</span>
    <span class="k">for</span> <span class="n">size</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">7</span><span class="p">):</span>
        <span class="n">latin_square</span> <span class="o">=</span> <span class="n">create_diagonal_latin_square_with_unary_constraints</span><span class="p">(</span><span class="n">size</span><span class="p">)</span>
        <span class="n">solution</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">({},</span> <span class="n">latin_square</span><span class="p">)</span>

        <span class="k">assert</span> <span class="n">solution</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">,</span> <span class="s">"Backtrack function should find a solution"</span>
        <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">solution</span><span class="p">)</span> <span class="o">==</span> <span class="n">size</span> <span class="o">*</span> <span class="n">size</span><span class="p">,</span> <span class="s">"Solution should have size * size assignments"</span>

        <span class="c1"># Verify constraints
</span>        <span class="k">assert</span> <span class="n">check_rows</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="s">"Row constraint violated"</span>
        <span class="k">assert</span> <span class="n">check_cols</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="s">"Column constraint violated"</span>
        <span class="k">assert</span> <span class="n">check_main_diagonal</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="s">"Main diagonal constraint violated"</span>
        <span class="k">assert</span> <span class="n">check_anti_diagonal</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="s">"Anti-diagonal constraint violated"</span>
        <span class="k">assert</span> <span class="n">check_unary_constraints</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="s">"Unary constraint violated"</span>
</code></pre></div></div>

<h2 id="ac-3-algorithm">AC-3 Algorithm</h2>

<p>The AC-3 is an algorithm used in constraint satisfaction problems to enforce arc consistency.
It processes all arcs in the problem, removing inconsistent values from variable domains until all arcs are consistent — or until it detects that no solution is possible.
By reducing the domains early, AC-3 helps prune the search space, leading to fewer backtracks and faster solutions during search.</p>

<p>In Python, it can be implemented as follows:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ac3.py
</span><span class="kn">from</span> <span class="nn">csp</span> <span class="kn">import</span> <span class="n">CSP</span>


<span class="k">def</span> <span class="nf">ac3</span><span class="p">(</span><span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""
    Enforce arc consistency using AC-3 algorithm.
    """</span>
    <span class="n">queue</span> <span class="o">=</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>  <span class="c1"># Initialize the queue with all constraints
</span>    <span class="k">while</span> <span class="n">queue</span><span class="p">:</span>
        <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="o">=</span> <span class="n">queue</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">arc_reduce</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">csp</span><span class="p">):</span>
            <span class="k">if</span> <span class="ow">not</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">x</span><span class="p">]:</span>
                <span class="k">return</span> <span class="bp">False</span>  <span class="c1"># No solution found
</span>            <span class="k">for</span> <span class="n">z</span> <span class="ow">in</span> <span class="n">get_neighbors</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">):</span>
                <span class="k">if</span> <span class="n">z</span> <span class="o">!=</span> <span class="n">y</span><span class="p">:</span>
                    <span class="n">queue</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">z</span><span class="p">,</span> <span class="n">x</span><span class="p">))</span>
    <span class="k">return</span> <span class="bp">True</span>


<span class="k">def</span> <span class="nf">arc_reduce</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""
    Make variable x arc-consistent with variable y.
    Return True if domain of x is reduced, False otherwise.
    """</span>
    <span class="n">changed</span> <span class="o">=</span> <span class="bp">False</span>
    <span class="k">for</span> <span class="n">vx</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">x</span><span class="p">].</span><span class="n">copy</span><span class="p">():</span>
        <span class="n">satisfied</span> <span class="o">=</span> <span class="bp">False</span>
        <span class="c1"># Find a value in the domain of y that satisfies the constraint
</span>        <span class="k">for</span> <span class="n">vy</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">y</span><span class="p">]:</span>
            <span class="k">if</span> <span class="n">vx</span> <span class="o">!=</span> <span class="n">vy</span><span class="p">:</span>
                <span class="n">satisfied</span> <span class="o">=</span> <span class="bp">True</span>
                <span class="k">break</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">satisfied</span><span class="p">:</span>
            <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">x</span><span class="p">].</span><span class="n">remove</span><span class="p">(</span><span class="n">vx</span><span class="p">)</span>
            <span class="n">changed</span> <span class="o">=</span> <span class="bp">True</span>
    <span class="k">return</span> <span class="n">changed</span>


<span class="k">def</span> <span class="nf">get_neighbors</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">constraints</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""
    Get all neighbors of a variable based on the constraints.
    """</span>
    <span class="n">neighbors</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">var1</span><span class="p">,</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">constraints</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var1</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var2</span><span class="p">)</span>
        <span class="k">elif</span> <span class="n">var2</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">neighbors</span>

</code></pre></div></div>

<p>In the code above:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">ac3(csp)</code> initializes a queue with all binary constraints and processes them one by one.
For each arc <code class="language-plaintext highlighter-rouge">(X -&gt; Y)</code>, it checks whether the domain of <code class="language-plaintext highlighter-rouge">X</code> can be reduced based on <code class="language-plaintext highlighter-rouge">Y</code> using <code class="language-plaintext highlighter-rouge">arc_reduce(x, y, csp)</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">arc_reduce(x, y, csp)</code> attempts to prune inconsistent values from <code class="language-plaintext highlighter-rouge">X</code>’s domain. If a value of <code class="language-plaintext highlighter-rouge">X</code> cannot be matched with any value of <code class="language-plaintext highlighter-rouge">Y</code>, that value is removed.</li>
  <li><code class="language-plaintext highlighter-rouge">get_neighbors(variable, constraints)</code> returns all variables that share a constraint with the given variable, ensuring that after reducing <code class="language-plaintext highlighter-rouge">X</code>’s domain, related arcs are re-added to the queue for rechecking.</li>
</ul>

<p>If any domain becomes empty during this process, the algorithm returns <code class="language-plaintext highlighter-rouge">False</code>, indicating that no solution is possible under the current assignments.
Otherwise, it returns <code class="language-plaintext highlighter-rouge">True</code> once all arcs are consistent.</p>

<p>Here are two main ways to apply the AC-3 algorithm:</p>
<ul>
  <li>Initial pruning: at the start of the problem before starting the search.</li>
  <li>Maintaining Arc Consistency (MAC): during the search itself.</li>
</ul>

<h3 id="initial-pruning">Initial Pruning</h3>

<p>We can apply AC-3 to the CSP to prune the search space before staring the search.
This preprocessing step can help reduce the number of backtracks needed during the actual search in advance:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">latin_square_csp</span> <span class="o">=</span> <span class="n">create_diagonal_latin_square_with_unary_constraints</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>

<span class="c1"># assignment is a dictionary that maps variable to value
</span><span class="n">ac3</span><span class="p">(</span><span class="n">latin_square_csp</span><span class="p">)</span>  <span class="c1"># Enforce arc consistency before backtracking (initial pruning)
</span><span class="n">assignment</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">({},</span> <span class="n">latin_square_csp</span><span class="p">)</span>
<span class="k">if</span> <span class="n">assignment</span><span class="p">:</span>
    <span class="n">print_latin_square</span><span class="p">(</span><span class="n">assignment</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"No solution found."</span><span class="p">)</span>
</code></pre></div></div>

<p>Pros:</p>
<ul>
  <li>Easy to implement.</li>
  <li>Can eliminate many early inconsistencies.</li>
  <li>Reduces search space before starting.</li>
</ul>

<p>Cons:</p>
<ul>
  <li>Only detects inconsistencies before search.</li>
  <li>Cannot prevent failures that arise during search.</li>
</ul>

<p>As an example, the table below shows how the domains of each variable in a 3×3 Latin square are refined before and after applying the AC-3 algorithm at the beginning.</p>

<table>
  <thead>
    <tr>
      <th>Variable</th>
      <th>Before AC-3</th>
      <th>After AC-3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>(0, 0)</td>
      <td>{1}</td>
      <td>∅</td>
    </tr>
    <tr>
      <td>(0, 1)</td>
      <td>{1, 2, 3}</td>
      <td>{1, 3}</td>
    </tr>
    <tr>
      <td>(0, 2)</td>
      <td>{1, 2, 3}</td>
      <td>{1}</td>
    </tr>
    <tr>
      <td>(1, 0)</td>
      <td>{1, 2, 3}</td>
      <td>{1, 3}</td>
    </tr>
    <tr>
      <td>(1, 1)</td>
      <td>{2}</td>
      <td>{2}</td>
    </tr>
    <tr>
      <td>(1, 2)</td>
      <td>{1, 2, 3}</td>
      <td>{1, 2}</td>
    </tr>
    <tr>
      <td>(2, 0)</td>
      <td>{1, 2, 3}</td>
      <td>{1, 2}</td>
    </tr>
    <tr>
      <td>(2, 1)</td>
      <td>{1, 2, 3}</td>
      <td>{1, 2}</td>
    </tr>
    <tr>
      <td>(2, 2)</td>
      <td>{3}</td>
      <td>{3}</td>
    </tr>
  </tbody>
</table>

<p>As a result, we can immediately detect that the problem has no solution because one of the variables (0, 0) has an empty domain after <em>constraint propagation</em>, meaning no valid value can be assigned without violating the Latin square constraints.</p>

<p>The number of backtracking calls when solving a diagonal Latin square with unary constraints, with AC-3 applied before backtracking:</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Backtrack Calls</th>
      <th>Solution Found</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2</td>
      <td>No</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>No</td>
    </tr>
    <tr>
      <td>4</td>
      <td>21</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>5</td>
      <td>48</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>6</td>
      <td>864</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>7</td>
      <td>155466</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>8</td>
      <td>111898</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>When compared to the result without AC-3, the difference in backtracking calls becomes increasingly noticeable as the grid size grows.</p>

<h3 id="maintaining-arc-consistency-mac">Maintaining Arc Consistency (MAC)</h3>

<p>We can also maintain arc consistency dynamically during the backtracking process.
After each variable assignment, we enforce arc consistency again, ensuring that every partial assignment remains as consistent as possible before proceeding further.
This approach can help reduce the need for deep backtracking by detecting dead-ends earlier:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">backtrack</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">assignment</span><span class="p">)</span> <span class="o">==</span> <span class="nb">len</span><span class="p">(</span><span class="n">csp</span><span class="p">.</span><span class="n">variables</span><span class="p">):</span>  <span class="c1"># Return assignment if complete
</span>        <span class="k">return</span> <span class="n">assignment</span>

    <span class="n">variable</span> <span class="o">=</span> <span class="n">select_unassigned_variable</span><span class="p">(</span><span class="n">assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="nb">list</span><span class="p">(</span><span class="n">get_domain_values</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">)):</span>
        <span class="n">new_assignment</span> <span class="o">=</span> <span class="n">assignment</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
        <span class="n">new_assignment</span><span class="p">[</span><span class="n">variable</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
        <span class="k">if</span> <span class="n">is_consistent</span><span class="p">(</span><span class="n">new_assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">):</span>
            <span class="n">domains</span> <span class="o">=</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>  <span class="c1"># Save the original domains
</span>            <span class="k">if</span> <span class="n">ac3</span><span class="p">(</span><span class="n">csp</span><span class="p">):</span>  <span class="c1"># Enforce arc consistency
</span>                <span class="n">result</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">(</span><span class="n">new_assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">)</span>  <span class="c1"># Recur with the new assignment
</span>                <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
                    <span class="k">return</span> <span class="n">result</span>  <span class="c1"># Solution found
</span>            <span class="n">csp</span><span class="p">.</span><span class="n">domains</span> <span class="o">=</span> <span class="n">domains</span>  <span class="c1"># Restore the original domains
</span>    <span class="k">return</span> <span class="bp">None</span>  <span class="c1"># No solution found
</span></code></pre></div></div>

<p>Pros:</p>
<ul>
  <li>Continuously prunes domains after each assignment.</li>
  <li>Detects dead-ends early.</li>
  <li>Leads to fewer backtracks and deeper pruning.</li>
</ul>

<p>Cons:</p>
<ul>
  <li>More computational overhead at each step.</li>
  <li>Slightly more complex to implement and manage.</li>
</ul>

<p>The number of backtrack calls when using MAC isn’t much different from that of initial pruning.
In fact, using MAC can actually be slower than simply doing that initial pruning.
You can see this for yourself by comparing the processing time of both versions of the code.
So, running AC-3 just once at the start is more than enough to significantly improve performance for our Latin square example.</p>

<p>The takeaway here is that while it’s possible to apply AC-3 at the beginning and also during the backtracking process, it’s not free.
It comes with a computational cost.
And sometimes, that extra overhead outweighs any benefit it might bring.</p>

<p>We will use initial pruning for the rest of the examples.</p>

<hr />

<p>To further improve search efficiency, we can use <em>heuristics</em>.</p>

<h2 id="heuristics">Heuristics</h2>

<p>Heuristics are strategies used to make better decisions during search — like which variable to pick next or which value to try first.
Instead of going through every possibility blindly, heuristics aim to cut down the search space and avoid dead ends early.</p>

<p>Three heuristics we’ll look at:</p>
<ul>
  <li>Least Constraining Values (LCV)</li>
  <li>Minimum Remaining Values (MRV)</li>
  <li>Degree</li>
</ul>

<h3 id="least-constraining-values-lcv">Least Constraining Values (LCV)</h3>

<p>The least constraining value (LCV) heuristic suggests trying the value that rules out the fewest choices for the neighboring (constrained) unassigned variables.
The goal is to keep options open for future assignments, increasing the chance of finding a solution without backtracking.</p>

<p>This requires calculating, for each potential value of the current variable, how many choices would be eliminated for its unassigned neighbors.
(Previosuly, the <code class="language-plaintext highlighter-rouge">get_domain_values</code> just returns all domain values for a variable.)</p>

<p>For this, we will update the <code class="language-plaintext highlighter-rouge">get_domain_values</code> function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>

<span class="k">def</span> <span class="nf">get_domain_values</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""
    Order domain values using the Least Constraining Values (LCV) heuristic.
    Returns values sorted by the number of choices they rule out for neighboring variables.
    """</span>
    <span class="n">neighbors</span> <span class="o">=</span> <span class="n">get_neighbors</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">)</span>
    <span class="n">unassigned_neighbors</span> <span class="o">=</span> <span class="p">[</span><span class="n">n</span> <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">neighbors</span> <span class="k">if</span> <span class="n">n</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">]</span>

    <span class="n">value_counts</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">variable</span><span class="p">]:</span>
        <span class="n">constraint_count</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="k">for</span> <span class="n">unassigned_neighbor</span> <span class="ow">in</span> <span class="n">unassigned_neighbors</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">unassigned_neighbor</span><span class="p">]:</span>
                <span class="n">constraint_count</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="n">value_counts</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">value</span><span class="p">,</span> <span class="n">constraint_count</span><span class="p">))</span>

    <span class="c1"># Sort values by their constraint count (ascending)
</span>    <span class="n">value_counts</span><span class="p">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>

    <span class="c1"># NOTE The first item is the least constraining value (that rules out the least number of choices for neighboring variables)
</span>    <span class="k">return</span> <span class="p">[</span><span class="n">value</span> <span class="k">for</span> <span class="n">value</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">value_counts</span><span class="p">]</span>

<span class="k">def</span> <span class="nf">get_neighbors</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">constraints</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""
    Get all neighbors of a variable based on the constraints.
    """</span>
    <span class="n">neighbors</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">var1</span><span class="p">,</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">constraints</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var1</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var2</span><span class="p">)</span>
        <span class="k">elif</span> <span class="n">var2</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">neighbors</span>
</code></pre></div></div>

<p>With LCV heuristic used (alongside AC-3), the number of backtrack calls significantly improves:</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Backtrack Calls</th>
      <th>Solution Found</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2</td>
      <td>No</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>No</td>
    </tr>
    <tr>
      <td>4</td>
      <td>20</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>5</td>
      <td>36</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>6</td>
      <td>905</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>7</td>
      <td>854</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>8</td>
      <td>5588</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>Note the drastic drop in backtrack calls for size 7 compared to the previous 155466 without LCV.
This demonstrates how combining constraint propagation and value ordering (like LCV) can make a huge difference in search efficiency.</p>

<h3 id="minimum-remaining-values-mrv">Minimum Remaining Values (MRV)</h3>

<p>The minimum remaining values (MRV) heuristic (a.k.a. “most constrained variable” heuristic) suggests selecting the variable with the fewest legal values remaining in its domain.
The intuition is that if a variable has only one or very few valid choices left, it’s better off to assign it now.
This might lead to an immediate failure if no values are legal, allowing for quick backtracking.</p>

<p>For this, we will update the <code class="language-plaintext highlighter-rouge">select_unassigned_variable</code> function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>

<span class="k">def</span> <span class="nf">select_unassigned_variable</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="s">"""Select the unassigned variable with the fewest remaining legal values (MRV)."""</span>
    <span class="n">variable</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">minimum_remaining_values</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="s">"inf"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">candidate_variable</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">variables</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">candidate_variable</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">:</span>
            <span class="n">num_remaining_values</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">candidate_variable</span><span class="p">])</span>
            <span class="k">if</span> <span class="n">num_remaining_values</span> <span class="o">&lt;</span> <span class="n">minimum_remaining_values</span><span class="p">:</span>
                <span class="n">minimum_remaining_values</span> <span class="o">=</span> <span class="n">num_remaining_values</span>
                <span class="n">variable</span> <span class="o">=</span> <span class="n">candidate_variable</span>  <span class="c1"># Update the best variable found so far
</span>    <span class="k">return</span> <span class="n">variable</span>
</code></pre></div></div>

<p>With MRV heuristic used (alongside AC-3):</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Backtrack Calls</th>
      <th>Solution Found</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>2</td>
      <td>1</td>
      <td>No</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>No</td>
    </tr>
    <tr>
      <td>4</td>
      <td>20</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>5</td>
      <td>40</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>6</td>
      <td>711</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>7</td>
      <td>22164</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>8</td>
      <td>91113</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>While MRV consistently improves performance, it results in more backtracking calls than LCV when solving larger Latin squares.</p>

<h3 id="degree">Degree</h3>

<p>The degree heuristic suggests selecting the variable involved in the largest number of constraints with other unassigned variables.</p>

<p>In a Latin square, each cell (r, c) is constrained by the other N-1 cells in its row and the other N-1 cells in its column.
The degree heuristic tries to pick the variable that constrains the most remaining choices.</p>

<p>Here’s the degree heuristic implemented version:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>

<span class="k">def</span> <span class="nf">select_unassigned_variable</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="s">"""
    Select the unassigned variable using degree heuristic.
    This heuristic selects the variable that is involved in the largest number of constraints.
    """</span>
    <span class="n">max_degree</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
    <span class="n">selected_variable</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">for</span> <span class="n">var</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">variables</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">:</span>
            <span class="n">num_neighbors</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">get_neighbors</span><span class="p">(</span><span class="n">var</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">))</span>
            <span class="k">if</span> <span class="n">num_neighbors</span> <span class="o">&gt;</span> <span class="n">max_degree</span><span class="p">:</span>
                <span class="n">max_degree</span> <span class="o">=</span> <span class="n">num_neighbors</span>
                <span class="n">selected_variable</span> <span class="o">=</span> <span class="n">var</span>  <span class="c1"># Update the selected variable
</span>    <span class="k">return</span> <span class="n">selected_variable</span>

<span class="k">def</span> <span class="nf">get_neighbors</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">constraints</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""
    Get all neighbors of a variable based on the constraints.
    """</span>
    <span class="n">neighbors</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">var1</span><span class="p">,</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">constraints</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var1</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var2</span><span class="p">)</span>
        <span class="k">elif</span> <span class="n">var2</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">neighbors</span>
</code></pre></div></div>

<p>With degree heuristic used (alongside AC-3):</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Backtrack Calls</th>
      <th>Solution Found</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2</td>
      <td>No</td>
    </tr>
    <tr>
      <td>3</td>
      <td>2</td>
      <td>No</td>
    </tr>
    <tr>
      <td>4</td>
      <td>17</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>5</td>
      <td>31</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>6</td>
      <td>39</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>7</td>
      <td>651</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>8</td>
      <td>568577</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>The degree heuristic performs really well for Latin squares up to size 7, but after that, the computation cost shoots up significantly.</p>

<p>The key insight here is that there’s no one-size-fits-all solution—even for a single problem type like Latin squares.
Different heuristics behave differently depending on the size and complexity of the instance.
That’s why it’s important to experiment with various strategies rather than sticking to just one.
What works well for a 7x7 grid might fall apart on an 8x8.</p>

<h3 id="lcv--mrv--degree">LCV + MRV + Degree</h3>

<p>Finally, let’s put all the heuristics together—LCV, MRV, and the Degree heuristic to see how well they perform when combined in a backtracking search.</p>

<p>Here’s the code that integrates all three:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">ac3</span> <span class="kn">import</span> <span class="n">ac3</span>
<span class="kn">from</span> <span class="nn">csp</span> <span class="kn">import</span> <span class="n">CSP</span>
<span class="kn">from</span> <span class="nn">latin_square</span> <span class="kn">import</span> <span class="n">create_diagonal_latin_square_with_unary_constraints</span><span class="p">,</span> <span class="n">print_latin_square</span>


<span class="k">def</span> <span class="nf">backtrack</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">assignment</span><span class="p">)</span> <span class="o">==</span> <span class="nb">len</span><span class="p">(</span><span class="n">csp</span><span class="p">.</span><span class="n">variables</span><span class="p">):</span>  <span class="c1"># Return assignment if complete
</span>        <span class="k">return</span> <span class="n">assignment</span>

    <span class="n">variable</span> <span class="o">=</span> <span class="n">select_unassigned_variable</span><span class="p">(</span><span class="n">assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">get_domain_values</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">):</span>
        <span class="n">new_assignment</span> <span class="o">=</span> <span class="n">assignment</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
        <span class="n">new_assignment</span><span class="p">[</span><span class="n">variable</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
        <span class="k">if</span> <span class="n">is_consistent</span><span class="p">(</span><span class="n">new_assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">):</span>
            <span class="n">result</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">(</span><span class="n">new_assignment</span><span class="p">,</span> <span class="n">csp</span><span class="p">)</span>  <span class="c1"># Recur with the new assignment
</span>            <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">result</span>  <span class="c1"># Solution found
</span>    <span class="k">return</span> <span class="bp">None</span>  <span class="c1"># No solution found
</span>

<span class="k">def</span> <span class="nf">select_unassigned_variable</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="s">"""
    Select unassigned variable using MRV (Minimum Remaining Values) heuristic
    with Degree heuristic as a tie-breaker.
    """</span>
    <span class="n">unassigned_vars</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">minimum_remaining_values</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="s">"inf"</span><span class="p">)</span>

    <span class="c1"># First pass: find all variables with minimum remaining values
</span>    <span class="k">for</span> <span class="n">var</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">variables</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">:</span>
            <span class="n">num_remaining_values</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">var</span><span class="p">])</span>
            <span class="k">if</span> <span class="n">num_remaining_values</span> <span class="o">&lt;</span> <span class="n">minimum_remaining_values</span><span class="p">:</span>
                <span class="n">minimum_remaining_values</span> <span class="o">=</span> <span class="n">num_remaining_values</span>
                <span class="n">unassigned_vars</span> <span class="o">=</span> <span class="p">[</span><span class="n">var</span><span class="p">]</span>
            <span class="k">elif</span> <span class="n">num_remaining_values</span> <span class="o">==</span> <span class="n">minimum_remaining_values</span><span class="p">:</span>
                <span class="n">unassigned_vars</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var</span><span class="p">)</span>

    <span class="c1"># If only one variable with minimum remaining values, return it
</span>    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">unassigned_vars</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">unassigned_vars</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>

    <span class="c1"># Use degree heuristic as tie-breaker
</span>    <span class="n">max_degree</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
    <span class="n">selected_variable</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">for</span> <span class="n">var</span> <span class="ow">in</span> <span class="n">unassigned_vars</span><span class="p">:</span>
        <span class="n">num_neighbors</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">get_neighbors</span><span class="p">(</span><span class="n">var</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">))</span>
        <span class="k">if</span> <span class="n">num_neighbors</span> <span class="o">&gt;</span> <span class="n">max_degree</span><span class="p">:</span>
            <span class="n">max_degree</span> <span class="o">=</span> <span class="n">num_neighbors</span>
            <span class="n">selected_variable</span> <span class="o">=</span> <span class="n">var</span>

    <span class="k">return</span> <span class="n">selected_variable</span>


<span class="k">def</span> <span class="nf">get_domain_values</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">csp</span><span class="p">:</span> <span class="n">CSP</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""
    Order domain values using the Least Constraining Values (LCV) heuristic.
    Returns values sorted by the number of choices they rule out for neighboring variables.
    """</span>
    <span class="n">neighbors</span> <span class="o">=</span> <span class="n">get_neighbors</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">csp</span><span class="p">.</span><span class="n">constraints</span><span class="p">)</span>
    <span class="n">unassigned_neighbors</span> <span class="o">=</span> <span class="p">[</span><span class="n">n</span> <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">neighbors</span> <span class="k">if</span> <span class="n">n</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">]</span>

    <span class="n">value_counts</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">variable</span><span class="p">]:</span>
        <span class="n">constraint_count</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="k">for</span> <span class="n">unassigned_neighbor</span> <span class="ow">in</span> <span class="n">unassigned_neighbors</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">csp</span><span class="p">.</span><span class="n">domains</span><span class="p">[</span><span class="n">unassigned_neighbor</span><span class="p">]:</span>
                <span class="n">constraint_count</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="n">value_counts</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">value</span><span class="p">,</span> <span class="n">constraint_count</span><span class="p">))</span>

    <span class="c1"># Sort values by their constraint count (ascending)
</span>    <span class="n">value_counts</span><span class="p">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>

    <span class="c1"># Return values ordered from least constraining to most constraining
</span>    <span class="k">return</span> <span class="p">[</span><span class="n">value</span> <span class="k">for</span> <span class="n">value</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">value_counts</span><span class="p">]</span>


<span class="k">def</span> <span class="nf">is_consistent</span><span class="p">(</span><span class="n">assignment</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">constraints</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""
    Check if the assignment is consistent with the constraints for Latin square.
    """</span>
    <span class="c1"># Check if any constraints are violated
</span>    <span class="k">for</span> <span class="n">var1</span><span class="p">,</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">constraints</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var1</span> <span class="ow">in</span> <span class="n">assignment</span> <span class="ow">and</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">assignment</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">assignment</span><span class="p">[</span><span class="n">var1</span><span class="p">]</span> <span class="o">==</span> <span class="n">assignment</span><span class="p">[</span><span class="n">var2</span><span class="p">]:</span>
                <span class="k">return</span> <span class="bp">False</span>  <span class="c1"># assignment is not consistent
</span>    <span class="k">return</span> <span class="bp">True</span>  <span class="c1"># assignment is consistent
</span>

<span class="k">def</span> <span class="nf">get_neighbors</span><span class="p">(</span><span class="n">variable</span><span class="p">,</span> <span class="n">constraints</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""
    Get all neighbors of a variable based on the constraints.
    """</span>
    <span class="n">neighbors</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">var1</span><span class="p">,</span> <span class="n">var2</span> <span class="ow">in</span> <span class="n">constraints</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">var1</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var2</span><span class="p">)</span>
        <span class="k">elif</span> <span class="n">var2</span> <span class="o">==</span> <span class="n">variable</span><span class="p">:</span>
            <span class="n">neighbors</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">var1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">neighbors</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">latin_square_csp</span> <span class="o">=</span> <span class="n">create_diagonal_latin_square_with_unary_constraints</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
    <span class="n">ac3</span><span class="p">(</span><span class="n">latin_square_csp</span><span class="p">)</span>
    <span class="n">assignment</span> <span class="o">=</span> <span class="n">backtrack</span><span class="p">({},</span> <span class="n">latin_square_csp</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">assignment</span><span class="p">:</span>
        <span class="n">print_latin_square</span><span class="p">(</span><span class="n">assignment</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"No solution found."</span><span class="p">)</span>
</code></pre></div></div>

<p>The number of backtrack calls of all heuristics combined:</p>

<table>
  <thead>
    <tr>
      <th>size</th>
      <th>Backtrack Calls</th>
      <th>Solution Found</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>2</td>
      <td>1</td>
      <td>No</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>No</td>
    </tr>
    <tr>
      <td>4</td>
      <td>17</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>5</td>
      <td>26</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>6</td>
      <td>49</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>7</td>
      <td>91</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>8</td>
      <td>475</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>You’ll notice a big drop in the number of backtrack calls and not just that, it also cuts down the actual processing time if you run the code.</p>

<h3 id="comparisons">Comparisons</h3>

<p>The graph below compares six different approaches to solve diagonal Latin square with unary constraints.</p>

<p><img src="/assets/posts/35/csp_comparison.png" alt="Comparison of CSP solutions" /></p>

<p>The key insight is that the logarithmic scale really highlights the performance differences.
It shows that by combining advanced heuristics, you can solve larger Latin squares with a huge drop in computational effort.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Keep in mind that the number of backtrack calls is just one way to measure how efficient the search is.
Heuristics can definitely help guide the search more intelligently, but figuring out the best move at every step can take extra computational overhead.
So even if your heuristic seems to be reducing backtracking, if it’s working too hard behind the scenes, it might actually be slowing things down overall.</p>

<h2 id="references">References</h2>
<ul>
  <li><a href="https://en.wikipedia.org/wiki/Latin_square">Latin square</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Constraint_satisfaction_problem">Constraint satisfaction problem</a></li>
  <li><a href="https://en.wikipedia.org/wiki/AC-3_algorithm">AC-3 algorithm</a></li>
</ul>]]></content><author><name>Mienxiu</name></author><category term="algorithm" /><category term="python" /><category term="testing" /><summary type="html"><![CDATA[A Latin square is an n x n grid filled with n different symbols (commonly numbers 1 to n), each appearing exactly once in every row and every column.]]></summary></entry><entry><title type="html">Interprocess Communication</title><link href="https://mienxiu.com/interprocess-communication/" rel="alternate" type="text/html" title="Interprocess Communication" /><published>2025-03-24T00:00:00+00:00</published><updated>2025-03-24T00:00:00+00:00</updated><id>https://mienxiu.com/interprocess-communication</id><content type="html" xml:base="https://mienxiu.com/interprocess-communication/"><![CDATA[<p>A process is either independent or cooperating:</p>
<ul>
  <li>independent: if it cannot affect or be affected by the other processes, and does not share data with any other processes</li>
  <li>cooperating: if it can affect or be affected by the other processes, and shares data with other processes</li>
</ul>

<p>There are several benefits to provide a system where processes can work together:</p>
<ul>
  <li>Information sharing. Since several users may be interested in the same piece of information (for instance, a shared file), we must provide an environment to allow concurrent access to such information.</li>
  <li>Computation speedup. If we want a particular task to run faster, we must break it into subtasks, each of which will be executing in parallel with the others. Notice that such a speedup can be achieved only if the computer has multiple processing cores.</li>
  <li>Modularity. We may want to construct the system in a modular fashion, dividing the system functions into separate processes or threads, as we discussed in Chapter 2.</li>
  <li>Convenience. Even an individual user may work on many tasks at the same time. For instance, a user may be editing, listening to music, and compiling in parallel.</li>
</ul>

<h2 id="interprocess-communication">Interprocess Communication</h2>
<p>Interprocess Communication (IPC) is a set of methods that the operating system provides to allow multiple processes to communicate and work together.</p>

<p><img src="/assets/posts/34/two_models_of_ipc.png" alt="Two models of IPC" /></p>

<p>There are two fundamental models that enable interprocess communication:</p>
<ul>
  <li>message passing model: communication takes place by means of messages exchanged between the cooperating processes via sockets, pipes, message queues.</li>
  <li>shared memory model: a region of memory that is shared by cooperating processes is established. Processes can then exchange information by reading and writing data to the shared region</li>
</ul>

<p>Message passing is useful for exchanging small amounts of data because it avoids conflicts and is easier to implement in distributed systems compared to shared memory.
However, shared memory can be faster since message passing relies on system calls, which require more time due to kernel involvement.</p>

<p>While shared memory can be faster because it bypasses system calls and kernel intervention, recent research on multi-core systems suggests that message passing often outperforms shared memory in such environments.</p>

<h2 id="message-passing-systems">Message-Passing Systems</h2>
<p>In message-passing systems, processes create messages and then send or receive these messages through a communication channel.
This communication channel may be implemented as a buffer or a FIFO queue.</p>

<p>For message-based ICP, the operating system kernel is responsible for:</p>
<ul>
  <li>setting up and managing the communication channel</li>
  <li>handling the actual message transfers by providing an interface that enables processes to send and receive messages</li>
  <li>ensuring synchronization</li>
</ul>

<p><img src="/assets/posts/34/message_passing.png" alt="Message-passing" /></p>

<p>The basic mechanism is as follows:</p>
<ol>
  <li>Establishing a Communication Channel: The OS creates a communication channel between processes.</li>
  <li>Sending a Message: The sending process sends data into a port via a system call (<code class="language-plaintext highlighter-rouge">send()</code>).</li>
  <li>Transmitting the Message: The OS transfers the message through the channel to the destination.</li>
  <li>Receiving a Message: The receiving process receives the message by reading from a port via system call (<code class="language-plaintext highlighter-rouge">recv()</code>).</li>
</ol>

<p>Advantages:</p>
<ul>
  <li>simplicity: The OS kernel abstracts the complexities of direct process communication and synchronization.</li>
  <li>safety: The OS kernel enforces access control and isolation, preventing unauthorized access and ensuring data integrity.</li>
</ul>

<p>Disadvantages:</p>
<ul>
  <li>overheads: Crossing the kernel boundary and copying data in and out of the kernel introduce performance overheads, especially in high-frequency messaging scenarios.</li>
</ul>

<p>There are three primary methods of message-based IPC: pipes, message queues, and sockets.
Below is a table that outlines the key differences and characteristics of each method:</p>

<table>
  <thead>
    <tr>
      <th>IPC Method</th>
      <th>Data Structure</th>
      <th>Communication Scope</th>
      <th>API/Standards</th>
      <th>Key Characteristics</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pipes</td>
      <td>stream of bytes</td>
      <td>two endpoints (one-to-one)</td>
      <td>POSIX</td>
      <td>simple, unidirectional</td>
    </tr>
    <tr>
      <td>Message queues</td>
      <td>discrete messages</td>
      <td>multiple processes supported</td>
      <td>POSIX, System V</td>
      <td>supports message formatting, prioritization, and scheduling</td>
    </tr>
    <tr>
      <td>Sockets</td>
      <td>message-based / stream</td>
      <td>local and networked processes</td>
      <td>socket API (TCP/IP)</td>
      <td>uses socket abstraction as ports (<code class="language-plaintext highlighter-rouge">socket()</code>, <code class="language-plaintext highlighter-rouge">send()</code>, <code class="language-plaintext highlighter-rouge">recv()</code>)</td>
    </tr>
  </tbody>
</table>

<h2 id="shared-memory-systems">Shared-Memory Systems</h2>
<p>In shared-memory systems, multiple processes can read from and write to a common memory region.
Typically, the process that creates the shared memory segment allocates it within its own address space, and any other process that needs to communicate through this segment must attach it to its address space.</p>

<p>The operating system maps specific physical pages into the virtual address spaces of all cooperating processes.
As a result, even though each process’s virtual address space may place the shared memory at different locations, they all reference the same physical memory.
This arrangement ensures that while the underlying physical memory is shared, each process maintains an independent virtual address layout.</p>

<p>Advantages:</p>
<ul>
  <li>Reduced OS overheads: Once the physical memory is mapped into the processes’ address spaces, the operating system plays no further role in data transfers, as system calls are only required during the initial setup.</li>
  <li>Minimized data copying: Processes can directly access only the necessary information in the shared memory without the overhead of copying entire datasets.</li>
</ul>

<p>Disadvantages:</p>
<ul>
  <li>Explicit synchronization: Because multiple processes can concurrently access the shared memory area, explicit synchronization mechanisms are required to prevent race conditions.</li>
  <li>Communication protocol design: Programmers are responsible to establish the communication protocol, determining how messages are formatted and exchanged.</li>
  <li>Shared buffer management: The allocation and management of the shared memory buffer are also the responsibility of the programmer, including decisions on when and which process can access the buffer.</li>
</ul>

<p>In Unix-based systems, including Linux, the System V API and POSIX API are the most common interfaces for managing shared memory.</p>

<h2 id="message-based-vs-shared-memory-based">Message-based vs Shared memory-based</h2>
<p>Below is a table comparing message-based IPC and shared memory-based IPC:</p>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>Message-based IPC</th>
      <th>Shared memory-based IPC</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Data Transfer</td>
      <td>Data is transferred via a communication channel (port).</td>
      <td>Both processes share a region of memory for direct access.</td>
    </tr>
    <tr>
      <td>Data Copying</td>
      <td>The CPU copies data into the port, then into the target process.</td>
      <td>The CPU maps physical memory into the address spaces of both processes.</td>
    </tr>
    <tr>
      <td>Efficiency for Large Data</td>
      <td>Less efficient, as multiple copies are required.</td>
      <td>More efficient, as fewer copies are needed.</td>
    </tr>
  </tbody>
</table>

<p>In summary, message-based IPC consumes more CPU cycles, particularly when dealing with large data.
On the other hand, while shared memory-based IPC can be initially costly due to memory mapping, it proves to be more efficient for transferring large amounts of data.
Therefore, the best IPC method depends on the size of the data and performance requirements, with shared memory being more efficient for larger data transfers.</p>

<p>In practice, systems like Windows uses a hybrid approach, also called as Local Procedure Calls (LPC), choosing the best method based on the data size:</p>
<ul>
  <li>For small data, message passing is used.</li>
  <li>For large data, shared memory mapping is employed for efficiency.</li>
</ul>

<h2 id="shared-memory-apis">Shared Memory APIs</h2>
<p>Below are the key high-level operations for managing shared memory in IPC:</p>
<ol>
  <li>Create: Allocates physical memory for a shared memory segment (or a communication channel), and returns an unique identifier (key) for any other processes to refer to the segment.</li>
  <li>Attach: Allows a process to connect to the shared memory segment by using the key, establishing mappings between virtual addresses and physical memory addresses of the segment.</li>
  <li>Detach: Disconnects a process from the shared memory segment by invalidating the address mappings.</li>
  <li>Destroy: Deallocates the shared memory segment when no longer needed, preventing memory leaks and freeing system resources.</li>
</ol>

<p>Note that a shared memory segment can be repeatedly attached and detached by multiple processes until it is explicitly destroyed.
This is in contrast to regular, non-shared memory, which is automatically freed when the process that allocated it exits.</p>

<p>There are two primary standards for implementing IPC on Unix-like systems: System V IPC and POSIX IPC.
While both standards offer similar functionality, their system calls differ in naming and implementation.</p>

<p>The following table summarizes the system calls for shared memory operations in each standard:</p>

<table>
  <thead>
    <tr>
      <th>Operation</th>
      <th>System V</th>
      <th>POSIX</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Create</td>
      <td><code class="language-plaintext highlighter-rouge">shmget()</code></td>
      <td><code class="language-plaintext highlighter-rouge">shm_open()</code></td>
    </tr>
    <tr>
      <td>Attach</td>
      <td><code class="language-plaintext highlighter-rouge">shmat()</code></td>
      <td><code class="language-plaintext highlighter-rouge">mmap()</code></td>
    </tr>
    <tr>
      <td>Detach</td>
      <td><code class="language-plaintext highlighter-rouge">shmdt()</code></td>
      <td><code class="language-plaintext highlighter-rouge">munmap()</code></td>
    </tr>
    <tr>
      <td>Destroy</td>
      <td><code class="language-plaintext highlighter-rouge">shmctl()</code></td>
      <td><code class="language-plaintext highlighter-rouge">shm_unlink()</code></td>
    </tr>
  </tbody>
</table>

<h3 id="system-v-ipc">System V IPC</h3>
<!-- Ref: https://tldp.org/LDP/lpg/node21.html -->
<p>System V IPC is a set of communication mechanisms that were originally introduced in the UNIX System V operating system.
While System V IPC is not widely used today, it laid the foundation for later IPC methods and is still supported in many UNIX-like systems for backward compatibility.</p>

<p>The following examples demonstrate the basic usage of System V shared memory.</p>

<p>For creating and writing to shared memory:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// create_and_write.c</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="c1">   // for printf()</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/ipc.h&gt;</span><span class="c1"> // for ftok()</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/shm.h&gt;</span><span class="c1"> // for shmget(), shmat(), shmdt()</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="c1">  // for strcpy()</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">key_t</span> <span class="n">key</span> <span class="o">=</span> <span class="n">ftok</span><span class="p">(</span><span class="s">"shmfile"</span><span class="p">,</span> <span class="mi">65</span><span class="p">);</span>                 <span class="c1">// generate unique key for System V IPC</span>
    <span class="kt">int</span> <span class="n">shmid</span> <span class="o">=</span> <span class="n">shmget</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="mi">1024</span><span class="p">,</span> <span class="mo">0666</span> <span class="o">|</span> <span class="n">IPC_CREAT</span><span class="p">);</span> <span class="c1">// create shared memory segment</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">str</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">shmat</span><span class="p">(</span><span class="n">shmid</span><span class="p">,</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>  <span class="c1">// attach shared memory segment to the process's address space</span>

    <span class="n">strcpy</span><span class="p">(</span><span class="n">str</span><span class="p">,</span> <span class="s">"Hello from System V shared memory!"</span><span class="p">);</span> <span class="c1">// write data to shared memory</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Data written: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">str</span><span class="p">);</span>

    <span class="n">shmdt</span><span class="p">(</span><span class="n">str</span><span class="p">);</span> <span class="c1">// detach shared memory segment from the process's address space</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Detailed explanations:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">key_t key = ftok("shmfile", 65);</code>: generates a key of type <code class="language-plaintext highlighter-rouge">key_t</code> based on the file’s inode (<code class="language-plaintext highlighter-rouge">"shmfile"</code>) and the provided integer (<code class="language-plaintext highlighter-rouge">65</code>, which is just an arbitrary number). This key uniquely identifies the shared memory segment.</li>
  <li><code class="language-plaintext highlighter-rouge">int shmid = shmget(key, 1024, 0666 | IPC_CREAT);</code>: uses the key generated earlier. The second parameter, <code class="language-plaintext highlighter-rouge">1024</code>, specifies the size of the memory segment in bytes. The third parameter, <code class="language-plaintext highlighter-rouge">0666|IPC_CREAT</code>, sets the permissions (read and write for everyone) and tells the system to create the segment if it doesn’t already exist.</li>
  <li><code class="language-plaintext highlighter-rouge">char *str = (char *)shmat(shmid, (void *)0, 0);</code>: returns a pointer to the shared memory. The first parameter is the shared memory ID obtained from <code class="language-plaintext highlighter-rouge">shmget()</code>. The second parameter is set to <code class="language-plaintext highlighter-rouge">(void*)0</code>, which lets the operating system choose the attach address. The third parameter is <code class="language-plaintext highlighter-rouge">0</code>, which means no special flags are used during attachment.</li>
  <li><code class="language-plaintext highlighter-rouge">shmdt(str);</code>: releases the memory region previously attached with <code class="language-plaintext highlighter-rouge">shmat()</code>, ensuring that the process no longer accesses the shared memory.</li>
</ul>

<p>Complie and run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcc create_and_write.c <span class="nt">-o</span> create_and_write
./create_and_write
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Data written: Hello from System V shared memory!
</code></pre></div></div>

<p>For reading and destroying the shared memory:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// read_and_destroy.c</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/ipc.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/shm.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">key_t</span> <span class="n">key</span> <span class="o">=</span> <span class="n">ftok</span><span class="p">(</span><span class="s">"shmfile"</span><span class="p">,</span> <span class="mi">65</span><span class="p">);</span>
    <span class="kt">int</span> <span class="n">shmid</span> <span class="o">=</span> <span class="n">shmget</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="mi">1024</span><span class="p">,</span> <span class="mo">0666</span> <span class="o">|</span> <span class="n">IPC_CREAT</span><span class="p">);</span> <span class="c1">// access shared memory segment</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">str</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">shmat</span><span class="p">(</span><span class="n">shmid</span><span class="p">,</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>  <span class="c1">// attach shared memory segment to the process's address space</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"Data read: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">str</span><span class="p">);</span>

    <span class="n">shmdt</span><span class="p">(</span><span class="n">str</span><span class="p">);</span> <span class="c1">// detach shared memory segment from the process's address space</span>

    <span class="n">shmctl</span><span class="p">(</span><span class="n">shmid</span><span class="p">,</span> <span class="n">IPC_RMID</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span> <span class="c1">// destroy shared memory segment</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Shared memory destroyed.</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Detailed explanations:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">int shmid = shmget(key, 1024, 0666 | IPC_CREAT);</code>: accesses the exsiting shared memory segment. Note that it does not create the shared memory as it already exists.</li>
  <li><code class="language-plaintext highlighter-rouge">shmctl(shmid, IPC_RMID, NULL);</code>: destroys (or marks for deletion) the shared memory segment. The command <code class="language-plaintext highlighter-rouge">IPC_RMID</code> tells the system to remove the shared memory segment. The third parameter is <code class="language-plaintext highlighter-rouge">NULL</code> because no additional options or data structures are needed for this operation.</li>
</ul>

<p>Complie and run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcc read_and_destroy.c <span class="nt">-o</span> read_and_destroy
./read_and_destroy
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Data read: Hello from System V shared memory!
Shared memory destroyed.
</code></pre></div></div>

<p>As demonstrated here, even after a process exits, the shared memory remains allocated until it is explicitly removed, allowing other processes to access the data if needed.</p>

<h3 id="posix-ipc">POSIX IPC</h3>
<!-- Ref: https://man7.org/linux/man-pages/man7/shm_overview.7.html -->
<p>POSIX shared memory is a modern and widely-used inter-process communication (IPC) mechanism standardized in POSIX-compliant systems.</p>

<p>The following examples demonstrate the basic usage of POSIX shared memory.</p>

<p>For creating and writing to shared memory:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// create_and_write.c</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="c1">    // for printf()</span><span class="cp">
#include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="c1">    // for O_CREAT, O_RDWR</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/mman.h&gt;</span><span class="c1"> // for shm_open(), mmap()</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="c1">   // for ftruncate()</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="c1">   // for strcpy()</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">int</span> <span class="n">shm_fd</span> <span class="o">=</span> <span class="n">shm_open</span><span class="p">(</span><span class="s">"/posix_shm"</span><span class="p">,</span> <span class="n">O_CREAT</span> <span class="o">|</span> <span class="n">O_RDWR</span><span class="p">,</span> <span class="mo">0666</span><span class="p">);</span> <span class="c1">// create POSIX shared memory object</span>
    <span class="n">ftruncate</span><span class="p">(</span><span class="n">shm_fd</span><span class="p">,</span> <span class="mi">1024</span><span class="p">);</span>                                     <span class="c1">// set size of shared memory segment</span>

    <span class="kt">char</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="n">mmap</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1024</span><span class="p">,</span> <span class="n">PROT_WRITE</span><span class="p">,</span> <span class="n">MAP_SHARED</span><span class="p">,</span> <span class="n">shm_fd</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="c1">// map shared memory segment to process address space</span>

    <span class="n">strcpy</span><span class="p">(</span><span class="n">ptr</span><span class="p">,</span> <span class="s">"Hello from POSIX shared memory!"</span><span class="p">);</span> <span class="c1">// write data to shared memory</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Data written: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ptr</span><span class="p">);</span>

    <span class="n">munmap</span><span class="p">(</span><span class="n">ptr</span><span class="p">,</span> <span class="mi">1024</span><span class="p">);</span> <span class="c1">// unmap shared memory segment</span>
    <span class="n">close</span><span class="p">(</span><span class="n">shm_fd</span><span class="p">);</span>     <span class="c1">// close file descriptor</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Detailed explanations:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">shm_open("/posix_shm", O_CREAT | O_RDWR, 0666);</code>: creates a POSIX shared memory object named <code class="language-plaintext highlighter-rouge">/posix_shm</code>. <code class="language-plaintext highlighter-rouge">O_CREAT</code> flag instructs <code class="language-plaintext highlighter-rouge">shm_open()</code> to create a new shared memory object if one does not already exist. If the object exists, it is opened. <code class="language-plaintext highlighter-rouge">O_RDWR</code> flag grants read and write access to the shared memory object. The permissions <code class="language-plaintext highlighter-rouge">0666</code> allow read/write access to everyone.</li>
  <li><code class="language-plaintext highlighter-rouge">ftruncate(shm_fd, 1024);</code>: resizes the shared memory segment to 1024 bytes. This is necessary as shared memory objects don’t automatically have a defined size when created.</li>
  <li><code class="language-plaintext highlighter-rouge">mmap(0, 1024, PROT_WRITE, MAP_SHARED, shm_fd, 0);</code>: maps the shared memory into the calling process’s address space with write permissions. Passing <code class="language-plaintext highlighter-rouge">0</code> lets the system choose the address. <code class="language-plaintext highlighter-rouge">1024</code> is the length of the memory to map, which is 1024 bytes in this case. <code class="language-plaintext highlighter-rouge">PROT_WRITE</code> flag allows the process to write to the mapped memory. <code class="language-plaintext highlighter-rouge">PROT_READ</code> can be added if reading is also needed. <code class="language-plaintext highlighter-rouge">shm_fd</code> is the file descriptor returned by <code class="language-plaintext highlighter-rouge">shm_open()</code>, which identifies the shared memory object to be mapped. The last <code class="language-plaintext highlighter-rouge">0</code> is the offset within the shared memory object at which the mapping should start.</li>
  <li><code class="language-plaintext highlighter-rouge">munmap(ptr, 1024);</code>: unmaps the previously mapped shared memory region from the process’s address space.</li>
  <li><code class="language-plaintext highlighter-rouge">close(shm_fd)</code>: closes the file descriptor <code class="language-plaintext highlighter-rouge">shm_fd</code> to ensure that the file system resources are properly freed and prevents memory leaks.</li>
</ul>

<p>Compile and run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcc create_and_write.c <span class="nt">-o</span> create_and_write
./create_and_write
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Data written: Hello from POSIX shared memory!
</code></pre></div></div>

<p>For reading and destroying the shared memory:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// read_and_destroy.c</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/mman.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/stat.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">int</span> <span class="n">shm_fd</span> <span class="o">=</span> <span class="n">shm_open</span><span class="p">(</span><span class="s">"/posix_shm"</span><span class="p">,</span> <span class="n">O_RDONLY</span><span class="p">,</span> <span class="mo">0666</span><span class="p">);</span> <span class="c1">// open existing shared memory object</span>

    <span class="k">struct</span> <span class="n">stat</span> <span class="n">shm_stat</span><span class="p">;</span>
    <span class="c1">// prevent segmentation fault if shared memory object does not exist</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">fstat</span><span class="p">(</span><span class="n">shm_fd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">shm_stat</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
    <span class="p">{</span> <span class="c1">// get size of shared memory</span>
        <span class="n">perror</span><span class="p">(</span><span class="s">"fstat failed"</span><span class="p">);</span>
        <span class="n">close</span><span class="p">(</span><span class="n">shm_fd</span><span class="p">);</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kt">char</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="n">mmap</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">shm_stat</span><span class="p">.</span><span class="n">st_size</span><span class="p">,</span> <span class="n">PROT_READ</span><span class="p">,</span> <span class="n">MAP_SHARED</span><span class="p">,</span> <span class="n">shm_fd</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="c1">// map shared memory segment for reading</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"Data read: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ptr</span><span class="p">);</span>

    <span class="n">munmap</span><span class="p">(</span><span class="n">ptr</span><span class="p">,</span> <span class="n">shm_stat</span><span class="p">.</span><span class="n">st_size</span><span class="p">);</span> <span class="c1">// unmap shared memory segment</span>
    <span class="n">close</span><span class="p">(</span><span class="n">shm_fd</span><span class="p">);</span>                 <span class="c1">// close file descriptor</span>

    <span class="n">shm_unlink</span><span class="p">(</span><span class="s">"/posix_shm"</span><span class="p">);</span> <span class="c1">// destroy shared memory segment</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Shared memory destroyed.</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Detailed explanations:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">shm_open("/posix_shm", O_RDONLY, 0666);</code>: opens an existing shared memory object named <code class="language-plaintext highlighter-rouge">/posix_shm</code> in read-only mode (<code class="language-plaintext highlighter-rouge">O_RDONLY</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">fstat(shm_fd, &amp;shm_stat);</code>: obtains the size of the shared memory object. The <code class="language-plaintext highlighter-rouge">&amp;shm_stat</code> is a pointer to a <code class="language-plaintext highlighter-rouge">struct stat</code> where the metadata (like the size of the shared memory) will be stored. The relevant field here is <code class="language-plaintext highlighter-rouge">st_size</code>, which holds the size of the shared memory object.</li>
  <li><code class="language-plaintext highlighter-rouge">shm_unlink("/posix_shm");</code>: destroys the shared memory object <code class="language-plaintext highlighter-rouge">/posix_shm</code> from the system.</li>
</ul>

<p>Compile and run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcc read_and_destroy.c <span class="nt">-o</span> read_and_destroy
./read_and_destroy
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Data read: Hello from POSIX shared memory!
Shared memory destroyed.
</code></pre></div></div>

<p>Just like System V shared memory, POSIX shared memory also remains available to other processes until explicitly removed using <code class="language-plaintext highlighter-rouge">shm_unlink()</code>.</p>

<h2 id="design-considerations">Design Considerations</h2>
<p>When using shared memory, the operating system provides the shared memory area but imposes no restrictions on how the memory is used.
This means that it is the programmer’s responsibility to manage data passing and synchronization effectively.</p>

<p><img src="/assets/posts/34/segment_consideration.png" alt="segment consideration" /></p>

<p>A key design consideration is determining how many memory segments for communication.
There are two primary options:</p>
<ul>
  <li>One large segment</li>
  <li>Multiple smaller segments</li>
</ul>

<p>Using a single large segment simplifies memory management since there is only one shared area to allocate and free.
This reduces the complexity associated with managing multiple segments.
However, it still requires careful memory management to allocate and free memory for threads from different processes.</p>

<p>On the other hand, using multiple smaller segments often involves pre-allocating a pool of segments to minimize the overhead of segment creation.
In addition, the programmer must implement a mechanism, such as a queue, to allow threads to determine which available segment to use for communication.
After all, it adds some additional complexity to the design.</p>

<p>Another consideration is determining how large a shared memory segment should be.
Fixed-size segments work well if the data size is known and static, however, it limits flexibility since data sizes might not be static.
For dynamic data, transferring in rounds is an option, where data is split across multiple rounds.
The programmer needs to implement some protocol to track progress, using a data structure that includes the buffer, synchronization, and flags.</p>]]></content><author><name>Mienxiu</name></author><category term="c" /><category term="os" /><summary type="html"><![CDATA[A process is either independent or cooperating: independent: if it cannot affect or be affected by the other processes, and does not share data with any other processes cooperating: if it can affect or be affected by the other processes, and shares data with other processes]]></summary></entry><entry><title type="html">Memory Management</title><link href="https://mienxiu.com/memory-management/" rel="alternate" type="text/html" title="Memory Management" /><published>2025-03-09T00:00:00+00:00</published><updated>2025-03-09T00:00:00+00:00</updated><id>https://mienxiu.com/memory-management</id><content type="html" xml:base="https://mienxiu.com/memory-management/"><![CDATA[<p>Managing the physical memory (DRAM) is a role of the operating system.</p>

<h2 id="virtual-memory-vs-physical-memory">Virtual Memory vs Physical Memory</h2>
<p>In order to provide flexibility in address space management—without being constrained by the amount of physical memory available or how it is shared among multiple processes—the operating system introduces the concept of virtual memory (also called logical memory).<br />
Virtual memory allows each process to have a large, continuous virtual address space, independent of the actual physical memory installed in the system.</p>

<p><img src="/assets/posts/33/virtual_and_physical_memory.png" alt="virtual and physical memory" /></p>

<p>Here are several key benefits of using virtual memory:</p>
<ul>
  <li>Efficient Multitasking: Multiple processes run concurrently in separate virtual spaces.</li>
  <li>Memory Isolation: Processes cannot access each other’s memory, enhancing security.</li>
  <li>Execution of Large Programs: Allows running programs larger than physical memory via paging or segmentation.</li>
  <li>Simplified Memory Management: Developers don’t need to worry about manual memory allocation as the OS handles it.</li>
</ul>

<p>There are two approaches to implement virtual memory:</p>
<ul>
  <li>Page-based memory management (Paging): The virtual address space is divided into fixed-size blocks called <em>pages</em>, which are mapped to corresponding <em>frames</em> in physical memory.</li>
  <li>Segment-based memory management (Segmentation): The address space is divided into variable-sized <em>segments</em>.</li>
</ul>

<p>Although paging and segmentation can be used together, modern 64-bit systems primarily rely on paging, with segmentation supported mainly for backward compatibility.
So, we’ll focus on page-based memory management.</p>

<h2 id="hardware-support">Hardware Support</h2>
<p>Memory management isn’t purely handled by the operating system alone; it also relies heavily on hardware support.
The primary hardware support for memory management is provided by the Memory Management Unit (MMU), which is integrated into modern CPUs.</p>

<p><img src="/assets/posts/33/mmu.png" alt="MMU" /></p>

<p>The main responsibilities of the MMU include:</p>
<ul>
  <li>Address Translation: The MMU translates virtual addresses (used by programs) into physical addresses (actual locations in the system’s DRAM).</li>
  <li>Page Fault Handling: The MMU detects and triggers page <em>faults</em> when a program attempts to access memory that is not currently mapped into physical memory. The MMU also checks for illegal access and permission violations.</li>
</ul>

<p>In addition to the MMU, there are other hardware components that assist in memory management:</p>
<ul>
  <li>Registers: Special registers in the MMU store information about the current state of memory management. For example, they can store the Page Table Base Register (PTBR) (which points to the page table), and Page Table Entry (PTE) registers.</li>
  <li>Cache - Translation Lookaside Buffer (TLB): To improve the performance of address translation, the MMU uses a small, high-speed cache called the Translation Lookaside Buffer (TLB), which stores recent virtual-to-physical address translations.</li>
</ul>

<p>The key takeaway is that the actual translation from a virtual address to a physical address is performed by hardware, specifically by the Memory Management Unit (MMU).
While the operating system sets up and manages the page tables, permissions, and overall memory policies, the MMU handles the translation process.</p>

<h2 id="page-tables">Page Tables</h2>
<p>To translate virtual memory addresses into physical memory addresses, the Memory Management Unit (MMU) relies on page tables.</p>

<p><img src="/assets/posts/33/paging_model.png" alt="Paging model" /></p>

<p>A page table acts as a “map” that allows the system to look up the corresponding physical address for a given virtual address.</p>

<p>Every process running on the system has its own page table.
And during a context switch—when the operating system changes from running one process to another—it must update the reference to the current process’s page table.
This update is managed by a hardware register that stores the address of the new page table, ensuring that the MMU always consults the correct mapping.</p>

<p>Note that virtual memory is divided into pages, and physical memory is divided into frames, both of equal size.
This design means that instead of mapping every single virtual address (which would be highly inefficient), the page table maps whole pages to frames.</p>

<h3 id="page-table-entry">Page Table Entry</h3>
<p>Each virtual address generated by the CPU is split into two parts: the Virtual Page Number (VPN) and the offset.
The VPN identifies the particular virtual page, while the offset specifies the exact location within that page.
When the CPU generates a virtual address, the MMU uses the VPN to index into the page table, retrieves the corresponding Physical Frame Number (PFN), and then appends the offset to get the complete physical address.</p>

<p>Another important aspect to consider is the concept of “allocation on first touch”.
This means that the physical memory corresponding to an array (or any other data structure) is not allocated until the process actually accesses it for the first time.
This strategy helps conserve memory resources because it avoids allocating memory for data structures that a program may never use.</p>

<h3 id="flags">Flags</h3>
<p>Every page table entry contains more than just a frame number; it also includes a bit known as the valid bit (or valid-invalid bit).
When the valid bit is set to 1, the page is legal and accessible; when it is 0, the page is illegal and any access attempt typically results in a page fault.
This mechanism ensures that processes only use the memory allocated to them.</p>

<p>Besides the valid bit, page table entries often include several other bits that help manage memory efficiently. For example:</p>
<ul>
  <li>dirty bit: indicates if a page has been modified, meaning it must be written back to disk before replacement.</li>
  <li>reference (access) bit: tracks if a page has been used recently, which aids in choosing pages for replacement.</li>
  <li>protection bit: specifying what types of operations—such as read, write, or execute—are allowed on that page.</li>
</ul>

<p>Some systems include additional bits for caching control or special functions like copy-on-write, all of which help the operating system maintain security and performance.</p>

<h2 id="page-fault">Page Fault</h2>
<p>A page fault happens when a process tries to access a page that isn’t in main memory.
When this occurs, the operating system’s page fault handler jumps into action.
It determines why the page fault happened—whether the page simply needs to be loaded from disk or if the access is invalid.
If the page is valid but not in memory, the handler locates it on disk, loads it into RAM, updates the page table, and then resumes the process’s execution.</p>

<h2 id="page-table-size">Page Table Size</h2>
<p>In Linux x86, the default page size is 4 KB, which is commonly used in memory management.
However, the operating system also supports larger page sizes, such as 2 MB (large pages) and 1 GB (huge pages)</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Regular</th>
      <th>Large</th>
      <th>Huge</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Page Size</td>
      <td>4 KB</td>
      <td>2 MB</td>
      <td>1 GB</td>
    </tr>
    <tr>
      <td>Offset bits</td>
      <td>12</td>
      <td>21</td>
      <td>30</td>
    </tr>
  </tbody>
</table>

<p>Using larger pages have several advantages:</p>
<ul>
  <li>Smaller page tables: Larger pages mean fewer page table entries (PTEs), reducing memory overhead and improving efficiency.</li>
  <li>Higher TLB hit rate: Since fewer pages are needed, the Translation Lookaside Buffer (TLB) can store more relevant page mappings, improving performance.</li>
</ul>

<p>Larger pages also have drawbacks:</p>
<ul>
  <li>Internal fragmentation: If a large page is not fully utilized, significant portions of it may remain unused, leading to wasted memory.</li>
</ul>

<p>The default 4 KB page size remains the most commonly used because it strikes a balance between memory efficiency and performance.</p>

<h2 id="page-table-size-in-a-single-level-page-model">Page Table Size in a Single-level Page Model</h2>
<p>Consider a single-level page table model with pages that are typically 4 KB in size, which is a common configuration on systems like x86-64 and ARM.</p>

<p>A 32-bit system supports 2^32 virtual address space.
If the page size is 4 KB (2^12 bytes), then a single page table can have up to 1 million page table entries (2^32)/(2^12).
Since each page table entry (PTE) is 4 bytes, including PFN and flags, a process may then need a page table of up to 4 MB (1 million * 4 bytes).</p>

<p>Now, a 64-bit system vastly expands the address space to 2^64 bytes.
Using the same 4 KB pages, a page table can have up to 4.5*10^15 pages (2^(64-12)).
With the PTE now being 8 bytes, if you do the math, a process may need up to roughly 32 petabytes.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>32-bit Architecture</th>
      <th>64-bit Architecture</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Page Size</td>
      <td>4 KB</td>
      <td>4 KB</td>
    </tr>
    <tr>
      <td>Number of Pages</td>
      <td>2^32 / 2^12 = 2^20 ~= 1,048,576 pages</td>
      <td>2^64 / 2^12 = 2^52 ~= 4.5*10^15 pages</td>
    </tr>
    <tr>
      <td>PTE Size</td>
      <td>4 bytes</td>
      <td>8 bytes</td>
    </tr>
    <tr>
      <td><strong>Page Table Size</strong></td>
      <td>~= <strong>4 MB</strong> (1,048,576 pages * 4 bytes)</td>
      <td>~= <strong>32 PB</strong> (4.5*10^15 pages * 8 bytes)</td>
    </tr>
  </tbody>
</table>

<p>Obviously, allocating the page table contiguously in main memory has its limitations.</p>

<h2 id="multi-level-page-tables">Multi-level Page Tables</h2>
<p>Multi-level page tables are one solution to reduce the memory requirement for page tables.
Instead of allocating a single, large page table for the entire address space, the page table is divided into multiple layers.
This hierarchical structure allocates memory only for the portions of the address space that are actually used.</p>

<p>In a two-level page table, the system first consults an outer page table.
This outer table doesn’t store the complete mapping details; instead, it contains pointers to inner page tables.
Each inner page table is responsible for a specific region of the virtual address space.
Importantly, an inner table is allocated only if that region is actively used.
This lazy allocation saves memory by avoiding the creation of page tables for unused regions.</p>

<p>The following figure illustrates the address translation process for a two-level 32-bit paging architecture:</p>

<p><img src="/assets/posts/33/address_translation_for_two_level_paging_architecture.png" alt="Address translation for a two-level paging architecture" /></p>

<p>Adding more layers to the page table hierarchy can further reduce the size of the page tables.
For example, a three-level page table divides the virtual address into three parts, where each part indexes into a different level of the page tables before reaching the final physical frame:</p>

<p><img src="/assets/posts/33/virtual_address_for_three_level_page_table.png" alt="A virtual address for a three-level page table" /></p>

<p>However, there’s a trade-off: increased translation latency.
With each additional level in the page table hierarchy, the CPU must perform extra memory accesses to resolve a virtual address to its corresponding physical location.</p>

<p class="notice--info">In 64-bit systems with vast address spaces, even three or four levels might not be sufficient.
In general, hierarchical page tables are not ideal for 64-bit architectures.</p>

<h3 id="optimizing-address-translation-with-tlb">Optimizing Address Translation with TLB</h3>
<p>As mentioned earlier, the more layers present in the page table hierarchy, the longer the address translation process takes.
To speed up this process, most modern architectures incorporate a hardware cache called the Translation Lookaside Buffer (TLB).
The TLB is a small, fast cache that stores a limited number of recent virtual-to-physical address mappings.</p>

<p><img src="/assets/posts/33/paging_with_tlb.png" alt="Paging with TLB" /></p>

<p>When a virtual address is accessed, the TLB is checked first.
If the mapping is found in the TLB (a TLB hit), the translation is done quickly.
If not (a TLB miss), the operating system has to perform a page table lookup, which takes more time.</p>

<p>To further speed up TLB lookups, replacement algorithms like Least Recently Used (LRU) or random replacement are often used.
In fact, even a relatively small TLB can significantly improve performance due to the following properties of memory references:</p>
<ul>
  <li>temporal locality: recently accessed addresses are likely to be accessed again soon</li>
  <li>spatial locality: addresses near those recently accessed are likely to be accessed next.</li>
</ul>

<h2 id="inverted-page-tables">Inverted Page Tables</h2>
<p>Inverted page tables can solve the traditional paging model’s drawback of having millions of entries per page table, which consume significant physical memory just for tracking usage.
In an inverted page table, there is one entry for every physical frame rather than one entry for every virtual page of each process.
This means that instead of each process having its own page table, there is a single, global page table that records information for every physical frame in memory.</p>

<p><img src="/assets/posts/33/inverted_page_table.png" alt="Inverted page table" /></p>

<p>Each entry in the inverted page table consists of a pair: a process identifier (<code class="language-plaintext highlighter-rouge">pid</code>) and a virtual page number (<code class="language-plaintext highlighter-rouge">p</code>).
When the system needs to translate a virtual address into a physical address, it searches the inverted table for the entry that matches both the process ID and the virtual page number.
Once it finds the correct entry, the index of that entry corresponds to the physical frame number.
By combining this frame number with the offset from the virtual address, the system can produce the complete physical address.</p>

<p>A challenge with this approach is that the inverted page table is organized by physical frames, but the lookup is performed using virtual addresses.
This mismatch means that the system might have to search through many entries to find the correct one.
In practice, however, the TLB helps alleviate this problem quite well.</p>

<p>Another approach to speed up the process of the linear search is to use a hash table.</p>

<h2 id="hashed-page-tables">Hashed Page Tables</h2>
<p>Hashed page tables are a common approach for handling expansive address spaces of 64-bit systems.
In a hash table, each entry contains a linked list of elements that hash to the same location to handle hash collisions. (This technique is also known as chaining.)
And each element consists of three fields:</p>
<ol>
  <li>the virtual page number</li>
  <li>the page frame number</li>
  <li>a pointer to the next element in the linked list</li>
</ol>

<p><img src="/assets/posts/33/hashed_page_table.png" alt="Hashed page table" /></p>

<p>The lookup algorithm works as follows:</p>
<ol>
  <li>A hash function is used to hash the virtual page number to determine the corresponding entry in the hash table.</li>
  <li>The virtual page number is compared with the VPN in the first element in the linked list. If there is a match, the corresponding page frame number is used to form the complete physical address.</li>
  <li>If there is no match, the remaining entries in the linked list are sequentially searched until a matching VPN is found.</li>
</ol>

<p>This design significantly reduces the search time, often limiting the lookup to just one or a few entries.</p>

<h2 id="memory-allocation">Memory Allocation</h2>
<p>Deciding how much memory is allocated to a process and which specific portion of memory is assigned is primarily handled by the memory management subsystem (a part of the OS kernel) of the operating system.</p>

<p>Memory allocation occurs at both the kernel level and the user level, each with distinct responsibilities.</p>

<p>Kernel-level memory allocators are responsible for:</p>
<ul>
  <li>Allocating memory for the kernel itself – various components of the kernel require memory for their operation.</li>
  <li>Providing static memory for processes – this includes sections like code and stack when a process is created.</li>
  <li>Managing free memory – keeping track of available pages in the system.</li>
</ul>

<p>User-level allocators handle is responsible for:</p>
<ul>
  <li>Dynamic memory allocation during process execution - this includes managing the heap through functions such as <code class="language-plaintext highlighter-rouge">malloc()</code> and <code class="language-plaintext highlighter-rouge">free()</code>.</li>
</ul>

<p>The Linux kernel, for example, uses two strategies for managing free memory: the buddy allocation and slap allocation.</p>

<h3 id="buddy-allocation">Buddy Allocation</h3>
<p>The buddy allocator manages memory by dividing it into blocks of different sizes, always in powers of two.
When a block of memory is requested, the allocator finds the smallest “buddy” block that fits the request.
If no block of the required size is available, it splits a larger block into two “buddies” until the desired size is achieved.</p>

<p><img src="/assets/posts/33/buddy_allocation.png" alt="Buddy allocation" /></p>

<p>An advantage of this strategy is its ability to quickly merge adjacent buddies into larger segments, a process known as coalescing.
However, the obvious drawback is that it can still cause internal fragmentation.</p>

<h3 id="slab-allocation">Slab Allocation</h3>
<p>A slab consists of one or more physically contiguous pages, and a cache consists of one or more slabs.
The slab allocator pre-creates caches for the different kernel data structures.
For example:</p>
<ul>
  <li>a cache for the process descriptors</li>
  <li>a cache for file objects</li>
  <li>a cache for semaphores</li>
</ul>

<p>When a cache is first created, all objects are marked as <code class="language-plaintext highlighter-rouge">free</code>.
When a new object for a particular kernel data structure is requested, the allocator assigns an available <code class="language-plaintext highlighter-rouge">free</code> object from the corresponding cache.
Once the object is assigned, it is marked as <code class="language-plaintext highlighter-rouge">used</code>.</p>

<p><img src="/assets/posts/33/slab_allocation.png" alt="Slab allocation" /></p>

<p>The slab allocator offers two main advantages:</p>
<ul>
  <li>No fragmentation: Each kernel data structure has a dedicated cache, divided into slabs that match the object size.</li>
  <li>Quick memory allocation: Memory requests are quickly satisfied because objects are pre-allocated in the cache.</li>
</ul>

<h2 id="demand-paging">Demand Paging</h2>
<p>Loading an entire program from disk into physical memory at execution time can be inefficient, as not all parts of the program may be immediately required.
Demand paging is a commonly used technique in which pages are only loaded into physical memory when they are actually needed.
With demand paging, pages are swapped between physical memory and a secondary storage device, such as a disk containing the swap partition.</p>

<p>Basically, the hardware uses a valid-invalid bit within each page table entry to distinguish between pages currently in memory and those stored on disk.
The meaning of this bit is as follows:</p>
<ul>
  <li>valid: The page is legal and currently resides in physical memory.</li>
  <li>invalid: The page is either:
    <ul>
      <li>Not valid (not within the logical address space of the process), or</li>
      <li>Valid but currently stored on disk (not loaded into memory).</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/posts/33/valid_invalid_bit_in_page_table.png" alt="valid-invalid bit in page table" /></p>

<p>Procedure for handling a page fault:</p>
<ol>
  <li>The MMU detects an invalid or missing page reference and triggers a trap to the OS kernel.</li>
  <li>The OS checks the internal tables (associated with the process control block):
    <ul>
      <li>If the reference is invalid, terminate the process.</li>
      <li>If valid but the page isn’t in memory, continue to step 3.</li>
    </ul>
  </li>
  <li>The OS selects a free memory frame for the required page.</li>
  <li>The OS schedules an I/O operation to load the page from disk into the allocated frame.</li>
  <li>After loading completes, update page tables and process information to mark the page as present.</li>
  <li>Return control to the interrupted process, the program counter will be restarted with the same intstructions.</li>
</ol>

<p><img src="/assets/posts/33/page_fault_handling.png" alt="page fault handling" /></p>

<p>When a page is “pinned”, it is locked into physical memory and cannot be swapped out.
Pinning is useful when pages contain critical kernel operations, I/O buffers, or data that must remain in memory to ensure system stability and performance.</p>

<h2 id="page-replacement">Page Replacement</h2>
<p>Since memory is finite, the system must decide which existing page to replace when a page fault (a page is not currently in physical memory) occurs and memory is full.
Page replacement is the process used to decide which pages to swap out of memory when a new page needs to be loaded.</p>

<p><img src="/assets/posts/33/page_replacement.png" alt="Page replacement" /></p>

<p>Basic page replacement process:</p>
<ol>
  <li>Locate the desired page on the disk.</li>
  <li>Select a victim page to be replaced using a page-replacement algorithm and write it back to disk. (page-out)</li>
  <li>Update the page table by setting the valid-invalid bit to invalid.</li>
  <li>Load the desired page into the freed frame. (page-in)</li>
  <li>Reset the page table to reflect the change.</li>
</ol>

<p>For “when” to swap out pages, the operating system will run some page out daemon when the amount of occupied memory reaches a particular threshold.
Two kinds of thresholds are:</p>
<ul>
  <li>high watermark: when memory usage is above threshold</li>
  <li>low watermark: when CPU usage is below</li>
</ul>

<p>For “which” to swap out pages, the operating system should predict based on algorithms such as:</p>
<ul>
  <li>LRU (Least Recently Used): The page that has not been used for the longest period of time is replaced.</li>
  <li>FIFO (First-In-First-Out): The oldest page is replaced.</li>
  <li>Optimal: The page that will not be used for the longest time in the future is replaced (though this is difficult to implement in practice).</li>
</ul>

<p>While each algorithm has its trade-offs, the goal is to minimize the number of page faults and keep the system running efficiently by managing memory effectively.</p>

<h2 id="copy-on-write">Copy-on-Write</h2>
<p>Copy-on-Write (COW) is an optimization technique used to efficiently handle process creation, particularly in the <code class="language-plaintext highlighter-rouge">fork()</code> system call.
Instead of immediately duplicating the entire address space of a process, the parent and child initially share the same memory pages as read-only.
This is possible because many pages are static and don’t change.</p>

<p>When either process attempts to write to a shared page, the MMU detects the write operation on a protected page and triggers a page fault.
The operating system then creates the actual copy of that page for the modifying process.
This lazy copying reduces memory usage and improves performance by copying pages only when necessary.</p>

<p>Note that COW relies on MMU support to manage page protection and fault handling efficiently.</p>]]></content><author><name>Mienxiu</name></author><category term="os" /><summary type="html"><![CDATA[Managing the physical memory (DRAM) is a role of the operating system.]]></summary></entry><entry><title type="html">Centralized Logging for Kubernetes Applications with EFbK Stack (Demonstration with FastAPI)</title><link href="https://mienxiu.com/efbk_stack/" rel="alternate" type="text/html" title="Centralized Logging for Kubernetes Applications with EFbK Stack (Demonstration with FastAPI)" /><published>2025-02-24T00:00:00+00:00</published><updated>2025-02-24T00:00:00+00:00</updated><id>https://mienxiu.com/efbk_stack</id><content type="html" xml:base="https://mienxiu.com/efbk_stack/"><![CDATA[<p>In software development, logging is the act of recording events and messages generated by an application.
It plays a crucial role in <strong>observability</strong>, enabling developers and system administrators to monitor application behavior, debug issues, and optimize performance effectively.</p>

<p class="notice--info">About events and messages, events represent a fact that something happened at a specific time.
Messages serve a broader role, encompassing both events and documents which can contain any verbose debugging information.</p>

<h2 id="logging-challenges-in-clustered-environments">Logging Challenges in Clustered Environments</h2>
<p>In a clustered environment like Kubernetes, where multiple applications run across distributed nodes, log management becomes significantly more complex due to several challenges:</p>
<ul>
  <li>Ephemeral nature of containers: When a pod is terminated or evicted, its logs disappear unless explicitly stored elsewhere.</li>
  <li>Logs are distributed across multiple nodes: As each node maintains its own local logs, searching through them manually from different nodes is inefficient and impractical.</li>
  <li>Scalability challenges: A large-scale Kubernetes cluster generates an enormous volume of logs. Handling such volume can lead to performance bottlenecks and storage issues.</li>
</ul>

<p>To address these challenges, Kubernetes applications require a <strong>centralized logging</strong> solution that collects logs from all nodes, aggregates them in a structured format (like JSON for example), and makes them searchable and analyzable in real-time.</p>

<p><img src="/assets/posts/32/centralized_logging.png" alt="Centralized logging" /></p>

<p>The EFbK stack is a choice for achieving this.</p>

<h2 id="efbk-stack">EFbK Stack</h2>
<p><img src="/assets/posts/32/efbk.png" alt="EFbK" /></p>

<p>The EFbK stack is a centralized logging solution for aggregating, forwarding, and analyzing logs from multiple applications running in a Kubernetes environment.</p>

<p>EFbK stands for:</p>
<ul>
  <li>E = Elasticsearch: A scalable, distributed search and analytics engine used for storing and indexing logs.</li>
  <li>Fb = Fluent Bit: A lightweight log processor and forwarder that collects logs from Kubernetes containers and routes them to destinations like Elasticsearch.</li>
  <li>K = Kibana: A visualization tool that provides a UI for querying, analyzing, and monitoring logs stored in Elasticsearch.</li>
</ul>

<h3 id="why-fluent-bit-a-comparison-with-other-log-processors">Why Fluent Bit? (A Comparison with Other Log Processors)</h3>
<p>For centralized logging in Kubernetes, several alternatives exist, with the most common being the ELK and EFK stacks.
The key difference between these stacks lies in the log processor—Logstash or Fluentd.
While both are powerful, I chose Fluent Bit over these alternatives for the following reasons:</p>
<ol>
  <li>Simplicity: Fluent Bit is purpose-built for log collection and forwarding, making it ideal for straightforward use cases. Its lightweight design minimizes deployment and configuration complexity.</li>
  <li>Efficient resource utilization: Fluent Bit consumes significantly fewer CPU and memory resources compared to Logstash and Fluentd, making it a great choice for resource-constrained environments or even large-scale clusters.</li>
  <li>Scalability without overhead: Its low resource footprint allows it to scale efficiently without negatively impacting overall cluster performance.</li>
</ol>

<p>Below is a comparison of the three log processors:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Logstash</th>
      <th>Fluentd</th>
      <th>Fluent Bit</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Language</td>
      <td>JRuby</td>
      <td>C &amp; Ruby</td>
      <td>C</td>
    </tr>
    <tr>
      <td>Memory</td>
      <td><a href="https://logz.io/blog/fluentd-logstash/">Greater than 120MB</a></td>
      <td>Greater than 60MB</td>
      <td>Approximately 1MB</td>
    </tr>
    <tr>
      <td>Performance</td>
      <td>Low to medium</td>
      <td>Medium</td>
      <td>High</td>
    </tr>
    <tr>
      <td>Plugins</td>
      <td>Many plugins, strong with Elasticsearch</td>
      <td>Over 1,000 external plugins</td>
      <td>Over 100 built-in plugins</td>
    </tr>
    <tr>
      <td>Best For</td>
      <td>Heavy log processing &amp; transformation</td>
      <td>Flexible log processing &amp; forwarding</td>
      <td>Lightweight log forwarding</td>
    </tr>
  </tbody>
</table>

<p>Filebeat is another lightweight log shipper.
While Filebeat is an excellent choice for simple file-based log collection, Fluent Bit is CNCF-maintained, making it a better fit for Kubernetes environments.
Fluent Bit also offers more flexibility with built-in filtering, parsing, and buffering mechanisms that Filebeat lacks.</p>

<h2 id="prerequisites">Prerequisites</h2>
<p>This tutorial assumes that you have an operational Kubernetes cluster, along with Elasticsearch and Kibana already set up.
We will install and configure Fluent Bit later in this guide.</p>

<p>If you don’t have Elasticsearch and Kibana installed, there are several deployment options available:</p>
<ul>
  <li>Elastic Cloud – A managed solution provided by Elastic (requires a subscription).</li>
  <li>Self-hosted deployment – Install Elasticsearch and Kibana on your own dedicated servers.</li>
  <li>Kubernetes deployment – Use Elastic Cloud on Kubernetes (ECK) to deploy them within your cluster.</li>
</ul>

<p>For those who need a quick test setup, I have provided minimal Kubernetes manifests to deploy Elasticsearch and Kibana in your cluster.
This setup includes:</p>
<ul>
  <li>A single-node Elasticsearch instance running within Kubernetes.</li>
  <li>A Kibana dashboard exposed externally via a <code class="language-plaintext highlighter-rouge">LoadBalancer</code> service.</li>
  <li>An internal <code class="language-plaintext highlighter-rouge">ClusterIP</code> service for Elasticsearch (not directly exposed).</li>
  <li>Security features enabled, with credentials configured at deployment time.</li>
</ul>

<p>To deploy, download the <a href="/assets/posts/32/elastic.yaml">elastic.yaml</a> file and run the following command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> elastic.yaml
</code></pre></div></div>

<p>Once applied, the deployment may take a few minutes to complete.</p>

<p class="notice--warning">For production use, I strongly recommend setting up a multi-node Elasticsearch cluster for better availability and provisioning persistent storage to safely retain log data.</p>

<p>The versions used in this guide are:</p>
<ul>
  <li>Kubernetes 1.30</li>
  <li>Elasticsearch 8.13.4</li>
  <li>Fluent Bit 3.2.2</li>
  <li>Kibana 8.13.4</li>
</ul>

<p>Below is an overview of the workflow for storing container logs in Elasticsearch and analyzing them using Kibana:</p>

<p><img src="/assets/posts/32/efbk_workflow.png" alt="EFbK workflow" /></p>

<h2 id="application-for-demonstration-fastapi">Application for Demonstration (FastAPI)</h2>
<p>For this demonstration, we will deploy a simple web application using FastAPI, a popular web framework in Python.
The application simulates an e-commerce service and provides two API endpoints:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/search</code> – Allows users to search for items.</li>
  <li><code class="language-plaintext highlighter-rouge">/items/{item_id}</code> – Retrieves details of a specific item.</li>
</ul>

<p>To make logging more informative, the application utilizes a <a href="https://fastapi.tiangolo.com/tutorial/middleware/">middleware</a> that logs incoming HTTP requests along with relevant metadata, such as:</p>
<ul>
  <li>HTTP method (e.g., GET, POST)</li>
  <li>URL of the request</li>
  <li>Response status code</li>
  <li>Elapsed time (request duration)</li>
  <li>Timestamp</li>
</ul>

<p>Additionally, the application introduces variable processing times to simulate real-world execution delays, ensuring the logged elapsed times reflect different request durations.</p>

<p>Here’s the application code:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app.py
</span>
<span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">import</span> <span class="nn">random</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>

<span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span><span class="p">,</span> <span class="n">Request</span><span class="p">,</span> <span class="n">Response</span>

<span class="c1"># Configure logging to print only the message for more flexible log structure
</span><span class="n">logging</span><span class="p">.</span><span class="n">basicConfig</span><span class="p">(</span><span class="n">level</span><span class="o">=</span><span class="n">logging</span><span class="p">.</span><span class="n">INFO</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"%(message)s"</span><span class="p">)</span>

<span class="c1"># Disable uvicorn access logs to delegate HTTP logs to the logging middleware
</span><span class="n">logging</span><span class="p">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s">"uvicorn.access"</span><span class="p">).</span><span class="n">setLevel</span><span class="p">(</span><span class="n">logging</span><span class="p">.</span><span class="n">WARNING</span><span class="p">)</span>

<span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="p">.</span><span class="n">getLogger</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">middleware</span><span class="p">(</span><span class="s">"http"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">log_http_request_and_elapsed_time</span><span class="p">(</span><span class="n">request</span><span class="p">:</span> <span class="n">Request</span><span class="p">,</span> <span class="n">call_next</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Response</span><span class="p">:</span>
    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">perf_counter</span><span class="p">()</span>
    <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">call_next</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
    <span class="n">elapsed_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">perf_counter</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span>
    <span class="n">log</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"method"</span><span class="p">:</span> <span class="n">request</span><span class="p">.</span><span class="n">method</span><span class="p">,</span>
        <span class="s">"url"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">url</span><span class="p">),</span>
        <span class="s">"status"</span><span class="p">:</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span>
        <span class="s">"elapsed"</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">elapsed_time</span><span class="p">,</span> <span class="mi">3</span><span class="p">),</span>
        <span class="s">"time"</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">().</span><span class="n">isoformat</span><span class="p">(),</span>
    <span class="p">}</span>
    <span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">log</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">response</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/search"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.3</span><span class="p">))</span>  <span class="c1"># Simulate a slow search process
</span>    <span class="k">return</span> <span class="p">{</span><span class="s">"query"</span><span class="p">:</span> <span class="n">query</span><span class="p">}</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/items/{item_id}"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">read_item</span><span class="p">(</span><span class="n">item_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="mf">0.2</span><span class="p">,</span> <span class="mf">0.4</span><span class="p">))</span>  <span class="c1"># Simulate a slow read process
</span>    <span class="k">return</span> <span class="p">{</span><span class="s">"id"</span><span class="p">:</span> <span class="n">item_id</span><span class="p">}</span>
</code></pre></div></div>

<p>When requests are processed, logs are generated in structured JSON format, making them easily parsable by logging systems like Elasticsearch.
JSON-based logs are beneficial because they:</p>
<ul>
  <li>are machine-readable and can be easily indexed by Elasticsearch.</li>
  <li>provide consistent formatting for efficient log analysis.</li>
  <li>allow powerful querying and filtering in Kibana.</li>
</ul>

<p>To generate traffic for our application, we use a script that simulates requests to the API endpoints:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># generate_requests.py
</span>
<span class="kn">import</span> <span class="nn">random</span>
<span class="kn">import</span> <span class="nn">string</span>

<span class="kn">import</span> <span class="nn">requests</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""
    Simulate HTTP requests to the web app endpoints.
    """</span>
    <span class="n">BASE_URL</span> <span class="o">=</span> <span class="s">"http://localhost:8000"</span>
    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="c1"># Choose an endpoint based on weighted probabilities.
</span>        <span class="n">endpoint</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">choices</span><span class="p">([</span><span class="s">"search"</span><span class="p">,</span> <span class="s">"items"</span><span class="p">],</span> <span class="n">weights</span><span class="o">=</span><span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">7</span><span class="p">],</span> <span class="n">k</span><span class="o">=</span><span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
        <span class="k">if</span> <span class="n">endpoint</span> <span class="o">==</span> <span class="s">"search"</span><span class="p">:</span>
            <span class="c1"># Generate a random gibberish string just to simulate different search queries.
</span>            <span class="n">query</span> <span class="o">=</span> <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">random</span><span class="p">.</span><span class="n">choices</span><span class="p">(</span><span class="n">string</span><span class="p">.</span><span class="n">ascii_lowercase</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">4</span><span class="p">))</span>
            <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">BASE_URL</span><span class="si">}</span><span class="s">/search?query=</span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="s">"</span>
            <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[SEARCH] GET </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> -&gt; </span><span class="si">{</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="k">elif</span> <span class="n">endpoint</span> <span class="o">==</span> <span class="s">"items"</span><span class="p">:</span>
            <span class="c1"># Choose an item_id between 1 and 5 based on weighted probabilities.
</span>            <span class="n">item_id</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">choices</span><span class="p">(</span><span class="n">population</span><span class="o">=</span><span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span> <span class="n">weights</span><span class="o">=</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">],</span> <span class="n">k</span><span class="o">=</span><span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
            <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">BASE_URL</span><span class="si">}</span><span class="s">/items/</span><span class="si">{</span><span class="n">item_id</span><span class="si">}</span><span class="s">"</span>
            <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[ITEMS] GET </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> -&gt; </span><span class="si">{</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>Build the image using the Dockerfile below, tag it as <code class="language-plaintext highlighter-rouge">demo-image</code>, and push it to your container registry:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> python:3.11.4-slim</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">RUN </span>pip <span class="nb">install </span>fastapi uvicorn[standard] requests
<span class="k">COPY</span><span class="s"> . .</span>
</code></pre></div></div>

<p>Assuming the image name is <code class="language-plaintext highlighter-rouge">demo-image</code>, below is a Kubernetes manifest to deploy the application:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># demo-app.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Namespace</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">demo-namespace</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">demo-namespace</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">demo-app</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">demo-image</span>
      <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">uvicorn"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">app:app"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">generate-requests</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">demo-image</span>
      <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">python"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">generate_requests.py"</span><span class="pi">]</span>
</code></pre></div></div>

<p>Apply the manifest to deploy the application in your cluster:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> demo-app.yaml
</code></pre></div></div>

<p>Once the application is successfully deployed, you can stream the logs using:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k <span class="nt">-n</span> demo-namespace logs <span class="nt">-f</span> demo-app <span class="nt">-c</span> app
</code></pre></div></div>

<p>With the application running inside the Kubernetes cluster, logs will be forwarded to Elasticsearch once Fluent Bit is installed and configured.</p>

<h2 id="fluent-bit">Fluent Bit</h2>
<p>To understand Fluent Bit better, I recommend reading <a href="https://docs.fluentbit.io/manual/concepts/key-concepts">the Fluent Bit Key Concepts</a>.
This page covers essential concepts such as events (records), tags, matches, and more.</p>

<p><img src="/assets/posts/32/fluentbit_pipeline.png" alt="EFbK stack" /></p>

<p>Fluent Bit collects and processes telemetry data through a structured data pipeline consisting of the following components:</p>
<ul>
  <li>Input: gathers data from various sources (e.g. text files, system logs).</li>
  <li>Parser: converts unstructured messages into structured messages (e.g. JSON).</li>
  <li>Filter: modifies, enriches or drops records (e.g. adding Kubernetes metadata).</li>
  <li>Buffer: temporarily stores records in the system memory (heap) to handle backpressure and delivery failures.</li>
  <li>Router: routes logs through filters and forwards them to one or multiple destinations.</li>
  <li>Output: defines the final destinations for your logs. (e.g. Elasticsearch)</li>
</ul>

<p>Each of these components is installed as a <em>plugin</em>.
And each plugin is then created as an instance with its own independent configuration.</p>

<p>Plugin configurations are defined under the <code class="language-plaintext highlighter-rouge">config</code> mapping in a YAML-formatted file, as shown below:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">config</span><span class="pi">:</span>
    <span class="na">inputs</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">[INPUT]</span>
            <span class="s">...</span>

    <span class="na">customParsers</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">[PARSER]</span>
            <span class="s">...</span>

    <span class="na">filters</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">[FILTER]</span>
            <span class="s">...</span>

    <span class="na">outputs</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">[OUTPUT]</span>
            <span class="s">...</span>
</code></pre></div></div>

<p>You can create multiple instances of each component.
For example, multiple input plugins:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">inputs</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">[INPUT]</span>
        <span class="s">...</span>

    <span class="s">[INPUT]</span>
        <span class="s">...</span>
</code></pre></div></div>

<p>If you install Fluent Bit using Helm, it comes with default configurations for reading container and systemd logs and forwarding them to an Elasticsearch cluster.
However, to tailor it to our environment and gain better flexibility, we will download <a href="https://github.com/fluent/helm-charts/blob/main/charts/fluent-bit/values.yaml">the default values</a> and modify them accordingly.</p>

<p>For a full list of available plugins for each phase, refer to the documentation linked above each section.</p>

<h3 id="input">Input</h3>
<p>We use the <a href="https://docs.fluentbit.io/manual/pipeline/inputs/tail"><code class="language-plaintext highlighter-rouge">tail</code></a> input plugin to read container logs from the Kubernetes nodes.
This plugin monitors log files and forwards log entries to the Fluent Bit pipeline.</p>

<p>Configuration:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">## https://docs.fluentbit.io/manual/pipeline/inputs</span>
<span class="na">inputs</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">[INPUT]</span>
        <span class="s">Name tail</span>
        <span class="s">Path /var/log/containers/*.log</span>
        <span class="s">multiline.parser docker, cri</span>
        <span class="s">Tag kube.*</span>
        <span class="s">Mem_Buf_Limit 5MB</span>
        <span class="s">Skip_Long_Lines On</span>
</code></pre></div></div>

<p>Properties:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Name</code>: Specifies the input plugin to use. <code class="language-plaintext highlighter-rouge">tail</code> is used in this case.</li>
  <li><code class="language-plaintext highlighter-rouge">Path</code>: Defines the location of log files. The wildcard (<code class="language-plaintext highlighter-rouge">*.log</code>) ensures that Fluent Bit reads all log files in <code class="language-plaintext highlighter-rouge">/var/log/containers</code> directory.</li>
  <li><code class="language-plaintext highlighter-rouge">multiline.parser</code>: Specifies one or multiple parsers to apply to the content. For example, it will first try <code class="language-plaintext highlighter-rouge">docker</code>, and then try <code class="language-plaintext highlighter-rouge">cri</code> if <code class="language-plaintext highlighter-rouge">docker</code> does not match.</li>
  <li><code class="language-plaintext highlighter-rouge">Tag</code>: Assigns a tag (<code class="language-plaintext highlighter-rouge">kube.*</code>) to logs for filtering and routing in later phases in the pipeline.</li>
  <li><code class="language-plaintext highlighter-rouge">Mem_Buf_Limit</code>: Sets the limit of the buffer size per monitored file.</li>
  <li><code class="language-plaintext highlighter-rouge">Skip_Long_Lines</code>: When it’s <code class="language-plaintext highlighter-rouge">On</code>, Fluent Bit skips long lines that exceed the buffer limit (<code class="language-plaintext highlighter-rouge">Buffer_Max_Size</code>) instead of stopping monitoring.</li>
</ul>

<p>What’s important here is <em>Tag</em>:</p>
<blockquote>
  <p>Every event ingested by Fluent Bit is assigned a Tag. This tag is an internal string used in a later stage by the Router to decide which <code class="language-plaintext highlighter-rouge">Filter</code> or <code class="language-plaintext highlighter-rouge">Output</code> phase it must go through.</p>
</blockquote>

<p>About <code class="language-plaintext highlighter-rouge">Path</code>, <code class="language-plaintext highlighter-rouge">/var/log/containers</code> contains logs of all running containers on the node.
Specifically, each container’s logs can be found in a file named:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/var/log/containers/&lt;pod_name&gt;_&lt;namespace&gt;_&lt;container_id&gt;.log
</code></pre></div></div>

<p>And by specifying <code class="language-plaintext highlighter-rouge">kube.*</code> for <code class="language-plaintext highlighter-rouge">Tag</code>, any event read from it is assigned a tag of:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kube.var.log.containers.&lt;pod_name&gt;_&lt;namespace&gt;_&lt;container_id&gt;.log
</code></pre></div></div>

<p>(Note that FLuent Bit replaces slashes (<code class="language-plaintext highlighter-rouge">/</code>) in the path with dots (<code class="language-plaintext highlighter-rouge">.</code>) through what’s called “tag expansion”.)</p>

<h3 id="parser">Parser</h3>
<p>The parser converts unstructured data to structured data.</p>

<p>Configuration:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">## https://docs.fluentbit.io/manual/pipeline/parsers</span>
<span class="na">customParsers</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">[PARSER]</span>
        <span class="s">Name docker</span>
        <span class="s">Format json</span>
        <span class="s">Time_Keep On</span>
        <span class="s">Time_Key time</span>
        <span class="s">Time_Format %Y-%m-%dT%H:%M:%S.%L</span>
</code></pre></div></div>

<p>Properties:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Name</code>: Sets a unique name for the parser.</li>
  <li><code class="language-plaintext highlighter-rouge">Format</code>: Specifies the format of the parser. <code class="language-plaintext highlighter-rouge">json</code> is used in this case, meaning logs are parsed as JSON objects.</li>
  <li><code class="language-plaintext highlighter-rouge">Time_Key</code>: Specifies the name of a timestamp field.</li>
  <li><code class="language-plaintext highlighter-rouge">Time_Format</code>: Specifies the format of the time field.</li>
</ul>

<p>Since every container log already includes a timestamp field, we preserve this original timestamp by setting <code class="language-plaintext highlighter-rouge">Time_Keep On</code>.
The extracted timestamp will be used as the <code class="language-plaintext highlighter-rouge">@timestamp</code> field in Elasticsearch indices.</p>

<p class="notice--info">For log messages that are not formatted as JSON, regular expressions can be used to transform them into a structured format.</p>

<h3 id="filter">Filter</h3>
<p>We use the <a href="https://docs.fluentbit.io/manual/pipeline/filters/kubernetes"><code class="language-plaintext highlighter-rouge">kubernetes</code></a> filter plugin to enrich log files with Kubernetes metadata, such as pod name, namespace, container name, and more.</p>

<p>Configuration:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">## https://docs.fluentbit.io/manual/pipeline/filters</span>
<span class="na">filters</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">[FILTER]</span>
        <span class="s">Name kubernetes</span>
        <span class="s">Match kube.*</span>
        <span class="s">Merge_Log On</span>
        <span class="s">Keep_Log Off</span>
        <span class="s">K8S-Logging.Parser On</span>
        <span class="s">K8S-Logging.Exclude On</span>
</code></pre></div></div>

<p>Properties:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Name</code>: Specifies the filter plugin to use. <code class="language-plaintext highlighter-rouge">kubernetes</code> is used in this case.</li>
  <li><code class="language-plaintext highlighter-rouge">Match</code>: Defines which logs this filter should apply to. The value <code class="language-plaintext highlighter-rouge">kube.*</code> ensures that the filter processes logs tagged with <code class="language-plaintext highlighter-rouge">kube.*</code>, which corresponds to logs collected from Kubernetes containers.</li>
  <li><code class="language-plaintext highlighter-rouge">Merge_Log</code>: When set to <code class="language-plaintext highlighter-rouge">On</code>, it checks if the <code class="language-plaintext highlighter-rouge">log</code> field content is a JSON string map, if so, it append the map fields as part of the log structure.</li>
  <li><code class="language-plaintext highlighter-rouge">Keep_Log</code>: When set to <code class="language-plaintext highlighter-rouge">Off</code>, the <code class="language-plaintext highlighter-rouge">log</code> field is removed from the incoming message once it has been successfully merged (<code class="language-plaintext highlighter-rouge">Merge_Log</code> must be enabled as well).</li>
  <li><code class="language-plaintext highlighter-rouge">K8S-Logging.Parser</code>: When set to <code class="language-plaintext highlighter-rouge">On</code>, Fluent Bit allows Kubernetes Pods to suggest a pre-defined Parser.</li>
  <li><code class="language-plaintext highlighter-rouge">K8S-Logging.Exclude</code>: When set to <code class="language-plaintext highlighter-rouge">On</code>, Fluent Bit allows Kubernetes Pods to exclude their logs from the log processor.</li>
</ul>

<p>In addition to the original log message, this filter enriches log records with Kubernetes metadata.
Below is an example of an enriched log entry (some values are masked for simplicity):</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"kubernetes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"pod_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-app"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"namespace_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-namespace"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"pod_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
        </span><span class="nl">"labels"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"app.kubernetes.io/name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-app"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"annotations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="err">...</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"host"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-host"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"pod_ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"10.42.209.165"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"container_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"api"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"docker_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
        </span><span class="nl">"container_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
        </span><span class="nl">"container_image"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-app:latest"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This additional metadata helps developers track logs back to specific Kubernetes resources, making it easier to find issues, and search logs more effectively in Kibana.</p>

<h3 id="output">Output</h3>
<p>We use the <a href="https://docs.fluentbit.io/manual/pipeline/outputs/elasticsearch"><code class="language-plaintext highlighter-rouge">es</code></a> output plugin to ingest logs into an Elasticsearch database.</p>

<p>Configuration:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">## https://docs.fluentbit.io/manual/pipeline/outputs</span>
<span class="na">outputs</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">[OUTPUT]</span>
        <span class="s">Name es</span>
        <span class="s">Match kube.var.log.containers.demo-app_demo-namespace_api*.log</span>
        <span class="s">Host elasticsearch.elastic.svc.cluster.local</span>
        <span class="s">HTTP_User elastic</span>
        <span class="s">HTTP_Passwd elasticpassword</span>
        <span class="s">Logstash_Format On</span>
        <span class="s">Logstash_Prefix demo-app</span>
        <span class="s">Suppress_Type_Name On</span>
</code></pre></div></div>

<p>Properties:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Name</code>: Specifies the output plugin to use. <code class="language-plaintext highlighter-rouge">es</code> is used in this case.</li>
  <li><code class="language-plaintext highlighter-rouge">Match</code>: Defines the tag pattern to filter logs for output. The pattern <code class="language-plaintext highlighter-rouge">kube.var.log.containers.demo-app_demo-namespace_api*.log</code> ensures that only logs from the <code class="language-plaintext highlighter-rouge">api*</code> container of <code class="language-plaintext highlighter-rouge">demo-app</code> pods within the <code class="language-plaintext highlighter-rouge">demo-namespace</code> namespace are routed to Elasticsearch. The pattern <code class="language-plaintext highlighter-rouge">kube.var.log.containers.demo-app_demo-namespace_api*.log</code> ensures that only logs from <code class="language-plaintext highlighter-rouge">api*</code> containers running within the <code class="language-plaintext highlighter-rouge">demo-app</code> pods in the <code class="language-plaintext highlighter-rouge">demo-namespace</code> namespace are sent to Elasticsearch.</li>
  <li><code class="language-plaintext highlighter-rouge">Host</code>: Specifies the host of the target Elasticsearch. Here, <code class="language-plaintext highlighter-rouge">elasticsearch.elastic.svc.cluster.local</code> just refers to the internal cluster DNS name of the Elasticsearch service.</li>
  <li><code class="language-plaintext highlighter-rouge">HTTP_User</code>: Username for Elasticsearch.</li>
  <li><code class="language-plaintext highlighter-rouge">HTTP_Passwd</code> `Password for Elasticsearch user.</li>
  <li><code class="language-plaintext highlighter-rouge">Logstash_Format</code>: When set to <code class="language-plaintext highlighter-rouge">On</code>, Fluent Bit enables Logstash format compatibility.</li>
  <li><code class="language-plaintext highlighter-rouge">Logstash_Prefix</code>: Specifies the prefix used for index names. Setting this to <code class="language-plaintext highlighter-rouge">demo-app</code> results in indices being named in the format <code class="language-plaintext highlighter-rouge">demo-app-YYYY.MM.DD</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">Suppress_Type_Name</code>: Must be set to <code class="language-plaintext highlighter-rouge">On</code>, as Elasticsearch 8.0.0 and later no longer support mapping types.</li>
</ul>

<p>In order to filter logs from pods of a deployment, you have to set <code class="language-plaintext highlighter-rouge">Match</code> to something like <code class="language-plaintext highlighter-rouge">kube.var.log.containers.demo-api-*_demo-namespace_api*.log</code>.
This is because pod names contain a dynamic suffix that is automatically generated by Kubernetes when creating pod replicas for a deployment.</p>

<p>When <code class="language-plaintext highlighter-rouge">Logstash_Format</code> is <code class="language-plaintext highlighter-rouge">On</code>, Fluent Bit appends a date suffix (<code class="language-plaintext highlighter-rouge">-YYYY.MM.DD</code>) to index names.
This time-based indices facilitate automated retention policies, ensuring efficient storage and deletion of old logs (which will be covered later in this tutorial).</p>

<p>When <code class="language-plaintext highlighter-rouge">Logstash_Format</code> is <code class="language-plaintext highlighter-rouge">Off</code>, Fluent Bit instead stores logs in an index specified by the <code class="language-plaintext highlighter-rouge">Index</code> property, which defaults to <code class="language-plaintext highlighter-rouge">fluent-bit</code>.</p>

<h3 id="installation">Installation</h3>
<p>To deploy Fluent Bit in your Kubernetes cluster, we use Helm.
For a complete <code class="language-plaintext highlighter-rouge">values.yaml</code> configuration file, refer to this link: <a href="/assets/posts/32/values.yaml">values.yaml</a>.</p>

<p>With the configurations we’ve set up, install Fluent Bit using the following Helm command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add fluent https://fluent.github.io/helm-charts
helm upgrade <span class="nt">--install</span> fluent-bit fluent/fluent-bit <span class="nt">--values</span> values.yaml
</code></pre></div></div>

<p>Expected output on successful installation:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Release "fluent-bit" does not exist. Installing it now.
NAME: fluent-bit
LAST DEPLOYED: Tue Feb 18 01:02:47 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
Get Fluent Bit build information by running these commands:

export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=fluent-bit,app.kubernetes.io/instance=fluent-bit" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace default port-forward $POD_NAME 2020:2020
curl http://127.0.0.1:2020
</code></pre></div></div>

<p>Once Fluent Bit is successfully installed and running, it will start collecting logs in your Kubernetes environment and forwarding them to Elasticsearch as configured.</p>

<h2 id="configuring-ilm">Configuring ILM</h2>
<p>With finite storage, it’s important to periodically delete old log documents to prevent storage exhaustion.
You can use Index Lifecycle Management (ILM) to automate this process.</p>

<p>In this example, we will configure ILM to delete any index whose name starts with <code class="language-plaintext highlighter-rouge">demo-app-</code> once it is older than 7 days. The steps involved are:</p>
<ol>
  <li>Create am ILM policy.</li>
  <li>Create an index template that applies this policy to matching indices.</li>
</ol>

<p>The ILM policy defines the lifecycle phases for your indices.
Here, we set up a policy to delete indices that are older than 7 days.
Execute the following command in the Kibana Console (Go to the <code class="language-plaintext highlighter-rouge">Management</code> tab and choose <code class="language-plaintext highlighter-rouge">Dev Tools</code>):</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">create</span><span class="w"> </span><span class="err">policy</span><span class="w">
</span><span class="err">PUT</span><span class="w"> </span><span class="err">_ilm/policy/demo-policy</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"policy"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"phases"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"delete"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"min_age"</span><span class="p">:</span><span class="w"> </span><span class="s2">"7d"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"actions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"delete"</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></code></pre></div></div>
<p>This command creates a policy named <code class="language-plaintext highlighter-rouge">demo-policy</code> that automatically deletes any index older than 7 days.</p>

<p>Next, create an index template that applies the <code class="language-plaintext highlighter-rouge">demo-policy</code> ILM policy to any index whose name begins with <code class="language-plaintext highlighter-rouge">demo-app-</code>.
Run the following command in the Kibana Console:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">create</span><span class="w"> </span><span class="err">template</span><span class="w">
</span><span class="err">PUT</span><span class="w"> </span><span class="err">_index_template/demo-template</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"index_patterns"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"demo-app-*"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"template"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"settings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"index.lifecycle.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-policy"</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></code></pre></div></div>
<p>With this template, any newly created index matching <code class="language-plaintext highlighter-rouge">demo-app-*</code> will automatically have the ILM policy applied.
Note that this template only affects indices created after the template is set up.</p>

<p>If you have pre-existing indices that match the pattern (for example, <code class="language-plaintext highlighter-rouge">demo-app-2025.02.23</code>), they will not automatically have the ILM policy applied.
To verify whether an index is managed by ILM, use the following command:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET</span><span class="w"> </span><span class="err">demo-app-*/_ilm/explain</span><span class="w">
</span></code></pre></div></div>

<p>Output:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"indices"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"demo-app-2025.02.23"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-app-2025.02.23"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"managed"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</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></code></pre></div></div>

<p>The managed <code class="language-plaintext highlighter-rouge">field</code> indicates whether ILM is managing the index.
In this example, <code class="language-plaintext highlighter-rouge">demo-app-2025.02.23</code> is not managed because it was created before the template was applied.</p>

<p>To manually apply the ILM policy to an existing index, run:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">demo-app</span><span class="mf">-2025.02</span><span class="err">.</span><span class="mi">23</span><span class="err">/_settings</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"index"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"lifecycle"</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="s2">"demo-policy"</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></code></pre></div></div>

<p>After applying the policy, you can confirm the change by running the explain command again:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET</span><span class="w"> </span><span class="err">demo-app-*/_ilm/explain</span><span class="w">
</span></code></pre></div></div>

<p>Output:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"indices"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"demo-app-2025.02.23"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-app-2025.02.23"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"managed"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"policy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-policy"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"index_creation_date_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">1739841343775</span><span class="p">,</span><span class="w">
      </span><span class="nl">"time_since_index_creation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4.54h"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"lifecycle_date_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">1739841343775</span><span class="p">,</span><span class="w">
      </span><span class="nl">"age"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4.54h"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"phase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"new"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"phase_time_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">1739857711731</span><span class="p">,</span><span class="w">
      </span><span class="nl">"action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"complete"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"action_time_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">1739857711731</span><span class="p">,</span><span class="w">
      </span><span class="nl">"step"</span><span class="p">:</span><span class="w"> </span><span class="s2">"complete"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"step_time_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">1739857711731</span><span class="p">,</span><span class="w">
      </span><span class="nl">"phase_execution"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"policy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo-policy"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
        </span><span class="nl">"modified_date_in_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">1739857599286</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></code></pre></div></div>

<p>Now that the <code class="language-plaintext highlighter-rouge">managed</code> field is true, the index <code class="language-plaintext highlighter-rouge">demo-app-2025.02.23</code> is under ILM control and will be automatically deleted 7 days after its creation, as specified by the <code class="language-plaintext highlighter-rouge">demo-policy</code>.</p>

<h3 id="ilm-poll-interval">ILM poll interval</h3>
<p>By default, ILM checks for indices that meet policy criteria every 10 minutes.
To permanently update the poll interval to 1 minute for example, run the following command in Kibana Console:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">/_cluster/settings</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"persistent"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"indices.lifecycle.poll_interval"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1m"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>This change will persist across cluster restarts.</p>

<p>If you prefer to change the setting only until the next restart, you can update the poll interval temporarily using:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">/_cluster/settings</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"transient"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"indices.lifecycle.poll_interval"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1m"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>After making these changes, verify that the setting has been applied by running:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET</span><span class="w"> </span><span class="err">/_cluster/settings</span><span class="w">
</span></code></pre></div></div>

<p>Output:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"persistent"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
  </span><span class="nl">"transient"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"indices"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"lifecycle"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"poll_interval"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1m"</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></code></pre></div></div>

<h2 id="log-analytics">Log Analytics</h2>
<p>Once everything is set up and logs are flowing into Elasticsearch, we can analyze them using Kibana.</p>

<p>To start, open Kibana in your web browser, navigate to the <code class="language-plaintext highlighter-rouge">Analytics</code> menu, and create a data view for <code class="language-plaintext highlighter-rouge">demo-app-*</code>.</p>

<p><img src="/assets/posts/32/create_data_view1.png" alt="Create data view 1" /></p>

<p>For the index pattern, enter <code class="language-plaintext highlighter-rouge">demo-app-*</code> to include all indices that match this prefix.
This allows Kibana to aggregate and visualize logs from all instances of the <code class="language-plaintext highlighter-rouge">demo-app</code>.</p>

<p><img src="/assets/posts/32/create_data_view2.png" alt="Create data view 2" /></p>

<p>For basic log exploration, navigate to the <code class="language-plaintext highlighter-rouge">Discover</code> tab.
Here, you can view all collected logs in real time or filter them based on specific time ranges.</p>

<p>For example, to analyze logs between <code class="language-plaintext highlighter-rouge">2025-02-23T14:00:00</code> and <code class="language-plaintext highlighter-rouge">2025-02-23T15:00:00</code>, select the appropriate time range using the time filter in Kibana:
<img src="/assets/posts/32/discover.png" alt="Discover" /></p>

<p>On this page, you can search, filter, and apply queries to analyze logs efficiently.
Kibana supports Lucene Query Syntax, KQL (Kibana Query Language), and full-text search to help you quickly find relevant logs.
You can also customize displayed fields, save searches, and export data for further analysis.</p>

<p>Below are some possible log analysis use cases for our scenario.</p>

<h3 id="monitoring-response-time">Monitoring Response Time</h3>
<p>Understanding response times is crucial for tracking application performance.
Kibana allows you to visualize response times over a given period to detect potential slowdowns.</p>

<p>To analyze response times for a specific API endpoint, <code class="language-plaintext highlighter-rouge">/seach</code> for example:</p>

<ol>
  <li>
    <p>Add a filter for the <code class="language-plaintext highlighter-rouge">url</code> field to match the <code class="language-plaintext highlighter-rouge">search</code> endpoint:
<img src="/assets/posts/32/add_url_filter_for_search.png" alt="Add a url filter for search" /></p>
  </li>
  <li>
    <p>Select the <code class="language-plaintext highlighter-rouge">elapsed</code> field, which represents the request processing time:
<img src="/assets/posts/32/top_elapsed_values.png" alt="Top values of elapsed" /></p>
  </li>
  <li>
    <p>Choose <code class="language-plaintext highlighter-rouge">Visualize</code> to generate a chart:
<img src="/assets/posts/32/visualize_average_elapsed.png" alt="Visualize average elapsed" /></p>
  </li>
</ol>

<p>Kibana automatically plots the median elapsed time per minute.
With this visualization, you can assess how long the application takes to process requests on average within a given time range.
If response times spike unexpectedly, it may indicate performance bottlenecks or increased server load.</p>

<h3 id="analyzing-the-most-viewed-items">Analyzing The Most Viewed Items</h3>
<p>To determine which items are most frequently accessed, you can analyze log data from API requests to the <code class="language-plaintext highlighter-rouge">/items</code> endpoint.</p>

<ol>
  <li>
    <p>Add a filter for the <code class="language-plaintext highlighter-rouge">url</code> field to match the <code class="language-plaintext highlighter-rouge">items</code> endpoint:
<img src="/assets/posts/32/add_url_filter_for_items.png" alt="Add a url filter for items" /></p>
  </li>
  <li>
    <p>Select the <code class="language-plaintext highlighter-rouge">url</code> field to group requests by different item paths:
<img src="/assets/posts/32/top_url_values.png" alt="Top values of url" /></p>
  </li>
  <li>
    <p>Choose <code class="language-plaintext highlighter-rouge">Visualize</code> to generate chart:
<img src="/assets/posts/32/visualize_top_url_values.png" alt="Visualize" /></p>
  </li>
</ol>

<p>Kibana will display a count of records for each unique item URL.
This allows you to identify the most frequently accessed items and gain insights into user behavior.
For example, if certain products receive significantly more views, they might be candidates for promotions or featured recommendations.</p>

<p>These are just a few basic examples of how you can leverage Kibana for log analytics.
Kibana offers many more powerful visualization and querying options.
For example, you can create custom visualizations in the <code class="language-plaintext highlighter-rouge">Visualize</code> tab and then assemble them into a dashboard in the <code class="language-plaintext highlighter-rouge">Dashboard</code> tab:</p>

<p><img src="/assets/posts/32/kibana_dashboard.png" alt="Kibana Dashboard" /></p>

<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="fluent-bit-is-running-but-new-logs-are-not-being-indexed-to-elasticsearch">FLuent Bit is Running but New Logs are Not Being Indexed to Elasticsearch</h3>
<p>If Fluent Bit is running but logs are not appearing in Elasticsearch, you can debug the issue by enabling tracing in the <code class="language-plaintext highlighter-rouge">es</code> output configuration.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">[</span><span class="nv">OUTPUT</span><span class="pi">]</span>
    <span class="s">Name es</span>
    <span class="s">...</span>
    <span class="s">Trace_Output Osn</span>
    <span class="s">Trace_Error On</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Trace_Output</code>: When it’s <code class="language-plaintext highlighter-rouge">On</code>, Fluent Bit prints the all ElasticSearch API request payloads for diagnostics.</li>
  <li><code class="language-plaintext highlighter-rouge">Trace_Error</code>: When it’s <code class="language-plaintext highlighter-rouge">On</code>, Fluent Bit prints the ElasticSearch API request and response for diagnostics.</li>
</ul>

<p>Run this command to apply the updates:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm upgrade <span class="nt">--install</span> fluent-bit fluent/fluent-bit <span class="nt">--values</span> values.yaml
</code></pre></div></div>

<p>Once these settings are enabled, inspect Fluent Bit’s logs by checking the container logs for detailed diagnostic messages.</p>

<p>One possible cause is a mismatch between <code class="language-plaintext highlighter-rouge">Tag</code> and <code class="language-plaintext highlighter-rouge">Match</code>.
Ensure that the <code class="language-plaintext highlighter-rouge">Tag</code> assigned to incoming logs in Fluent Bit matches the <code class="language-plaintext highlighter-rouge">Match</code> property in the output configuration.</p>

<h3 id="index-health-is-yellow">Index Health is <code class="language-plaintext highlighter-rouge">yellow</code></h3>
<p>If you have set up Elasticsearch using the manifest provided in this guide, it deploys a single-node Elasticsearch cluster.
Because of this, the index health status appears as <code class="language-plaintext highlighter-rouge">yellow</code>.
This happens because the default <code class="language-plaintext highlighter-rouge">number_of_replicas</code> is set to 1, requiring at least two nodes to ensure redundancy.
Since only one node is available, the replica shards cannot be assigned, leading to a <code class="language-plaintext highlighter-rouge">yellow</code> status.</p>

<p>To resolve this and turn the index health status to <code class="language-plaintext highlighter-rouge">green</code>, you need to manually update the index settings to remove replicas by running the following command:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PUT</span><span class="w"> </span><span class="err">/demo-app-*/_settings</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"index"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"number_of_replicas"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>To verify the current replica settings, use:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET</span><span class="w"> </span><span class="err">demo-app-*/_settings</span><span class="w">
</span></code></pre></div></div>

<h3 id="the-value-of-stream-is-stderr-is-this-a-problem">The Value of <code class="language-plaintext highlighter-rouge">stream</code> is <code class="language-plaintext highlighter-rouge">stderr</code>. Is this a Problem?</h3>
<p>If you’re following the demonstration with the FastAPI application I provided, you might notice that logs indexed in Elasticsearch have the <code class="language-plaintext highlighter-rouge">stream</code> value set to <code class="language-plaintext highlighter-rouge">stderr</code> (standard error).
This behavior occurs because Python’s <code class="language-plaintext highlighter-rouge">logging.basicConfig</code> function, by default, creates a <code class="language-plaintext highlighter-rouge">StreamHandler</code> that writes logs to <code class="language-plaintext highlighter-rouge">sys.stderr</code>.
Although you can configure it to use <code class="language-plaintext highlighter-rouge">stdout</code> instead of <code class="language-plaintext highlighter-rouge">stderr</code>, this is not necessarily an issue-it depends on how your logging and monitoring setup is designed.</p>

<h2 id="unintallation">Unintallation</h2>
<p>To remove Fluent Bit from your Kubernetes cluster, run the following command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm uninstall fluent-bit
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>
<p>With the ability to manage logs from multiple applications and sources, you can eliminate the need for individual applications to explicitly send logs to a centralized storage service like Elasticsearch.
Instead, Fluent Bit efficiently handles log collection and forwarding, allowing applications to remain focused on their core functionality.</p>

<p>This <strong>separation of concerns</strong> provides several benefits:</p>
<ol>
  <li>Applications can generate logs without worrying about storage or processing bottlenecks.</li>
  <li>Fluent Bit can route logs to multiple destinations, making it easier to adapt to different observability needs.</li>
  <li>Developers can focus on building features while the logging infrastructure ensures reliable data collection.</li>
</ol>

<p>Additionally, Kibana provides powerful visualization and analysis tools, enabling developers to explore logs, identify patterns, and troubleshoot issues efficiently.
Kibana can also help teams gain insights from their log data.</p>

<p>If in-depth application monitoring is required, integrating a dedicated APM (Application Performance Monitoring) tool can be a great option.
However, APM solutions tend to consume significant storage due to the additional application context they collect.
To optimize storage usage, it’s beneficial to limit retention periods or configure sampling rates to reduce excessive data collection.</p>

<p>A common strategy would be as follows:</p>
<ol>
  <li>Use the EFbK stack for long-term storage of essential logs.</li>
  <li>Use an APM tool for short-term, high-resolution application performance monitoring.</li>
</ol>

<h2 id="references">References</h2>
<ul>
  <li><a href="https://www.elastic.co/docs">Elastic Documentation</a></li>
  <li><a href="https://docs.fluentbit.io/manual">Fluent Bit: Official Manual</a></li>
</ul>]]></content><author><name>Mienxiu</name></author><category term="docker" /><category term="elasticsearch" /><category term="kubernetes" /><category term="python" /><summary type="html"><![CDATA[In software development, logging is the act of recording events and messages generated by an application. It plays a crucial role in observability, enabling developers and system administrators to monitor application behavior, debug issues, and optimize performance effectively.]]></summary></entry></feed>