<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Atlys Engineering - Medium]]></title>
        <description><![CDATA[Building the underlying infrastructure for global mobility - Medium]]></description>
        <link>https://engineering.atlys.com?source=rss----a2f2b8cf50ab---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Atlys Engineering - Medium</title>
            <link>https://engineering.atlys.com?source=rss----a2f2b8cf50ab---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 25 Jun 2026 17:28:08 GMT</lastBuildDate>
        <atom:link href="https://engineering.atlys.com/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Fine-Tuning a 2B Vision-Language Model for Document AI: Four Lessons That Beat Hyperparameter…]]></title>
            <link>https://engineering.atlys.com/fine-tuning-a-2b-vision-language-model-for-document-ai-four-lessons-that-beat-hyperparameter-e6c0a1b3b023?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/e6c0a1b3b023</guid>
            <category><![CDATA[vlm-serving]]></category>
            <category><![CDATA[fine-tuning-vlm]]></category>
            <category><![CDATA[vlm-in-production]]></category>
            <category><![CDATA[vision-lora]]></category>
            <category><![CDATA[ai-for-document-parsing]]></category>
            <dc:creator><![CDATA[Shubham Tiwari]]></dc:creator>
            <pubDate>Sun, 17 May 2026 07:42:17 GMT</pubDate>
            <atom:updated>2026-05-17T07:42:16.130Z</atom:updated>
            <content:encoded><![CDATA[<h3>Fine-Tuning a 2B Vision-Language Model for Document AI: Four Lessons That Beat Hyperparameter Tuning</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y6QrXohGP9uU1NF68ZEejQ.png" /></figure><p>Frontier APIs handle document understanding well out of the box, until you’re processing tens of thousands of multi-page financial documents per day. At that point, API costs run into thousands of dollars a month, you inherit rate limits and latency variance and you can’t fix specific failure modes that matter to your domain.</p><p>A purpose-built 2B vision-language model on a single GPU runs at a fraction of that cost with predictable latency, full data privacy and the ability to address edge cases by adjusting your training data. We spent the last few weeks fine-tuning one to parse bank statements at production scale and most of the actual lessons turned out to be in places the standard fine-tuning playbook skips.</p><p>The scope is <strong>document AI with structured response outputs</strong>. Tasks where you give the model an image and expect a JSON output with specific fields: bank statements, invoices, medical records, lab reports, KYC forms, identity documents. For chat models or open-ended generation, some of what we say below might not apply.</p><h3>Our fine-tuning playbook</h3><p>Most blog posts on fine-tuning fixate on hyperparameters. In practice, fine-tuning a VLM for production is mostly about figuring out what actually moves the needle. We recently fine-tuned a 2B VLM to parse bank statements. Four things consistently helped us hit production-grade accuracy:</p><ol><li><strong>Schema design</strong> as a training lever</li><li><strong>Vision-side LoRA</strong> targeting and serving</li><li><strong>Serving configuration</strong> that interacts with model architecture</li><li><strong>Input-side engineering</strong> that beats more training</li></ol><p>The training script was the simplest part. Each of these had its own surprises.</p><h3>1. Schema Is a Fine-Tuning Lever</h3><p>The single most impactful decision wasn’t a hyperparameter. It was what we asked the model to output.</p><p>We removed one field from our transaction schema and eval loss dropped 60%. Inference throughput roughly doubled. Truncation errors went from a regular annoyance to almost zero. No change to model size, training data, or learning rate.</p><p>The model didn’t get smarter. The task got smaller.</p><h4>The Setup</h4><p>For our first three versions, the transaction schema looked like this:</p><pre>{<br>  &quot;date&quot;: &quot;2024-01-15&quot;,<br>  &quot;narration&quot;: &quot;UPI/P2L329398532229/New Cron/UPI/TM Bank&quot;,<br>  &quot;debit&quot;: 495.0,<br>  &quot;credit&quot;: 0,<br>  &quot;balance&quot;: 288209.95<br>}</pre><p>Five fields per transaction. A page with 50 transactions produces around 1,800 to 2,000 output tokens. The 95th percentile of our training data sat at 2,562 output tokens.</p><p>For version 4, we removed narration:</p><pre>{<br>  &quot;date&quot;: &quot;2024-01-15&quot;,<br>  &quot;debit&quot;: 495.0,<br>  &quot;credit&quot;: 0,<br>  &quot;balance&quot;: 288209.95<br>}</pre><p>Same training data, same model, same hyperparameters. Results across all four versions:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X-G8SgGhA1RxPx1Jj5zdsQ.png" /><figcaption>From: Author</figcaption></figure><h4>Why This Happens</h4><p>When you fine-tune a causal language model, even a vision-language one, the loss is computed token by token over the output sequence. A sample with a 2,000-token output produces roughly 10x more loss signal than a sample with a 200-token output, plus 10x more gradient computation, activation memory, and wall-clock time per training step.</p><p>Most of those output tokens were spent on the wrong thing. Narration strings are essentially uncompressed OCR. The model has to predict each character of strings like UPI/P2K329398532229/New Corn. Any uncertainty compounds across the sequence.</p><p>The structured fields are where the actual document understanding lives. The model has to recognize the table layout, identify column boundaries, parse Indian number formatting. The narration field is just transcription. It’s spending most of its gradient signal memorizing strings that won’t appear again.</p><h4>Output Token Cascade Through the Stack</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5757-f2xBjWSvaPbIWRT2Q.png" /><figcaption>From: Author</figcaption></figure><h4>A Note on Eval Loss</h4><p>There’s a measurement trap here: V3’s 0.064 eval loss is not “almost as good” as V4’s 0.024. They’re solving different tasks and their loss numbers live in different scales.</p><p>Eval loss measures next-token prediction on your training distribution. Change the schema and you change what the loss is measuring. The only metric that means the same thing across schema changes is field-level accuracy on a held-out validation set.</p><h3>2. Vision LoRA Is Where Most Tutorials Stop Short</h3><p>Most LoRA tutorials target q/k/v projections in the language layers. For a vision-language model, that leaves the vision encoder essentially frozen, which means the model can’t learn to look differently at your domain.</p><h4>Module Discovery</h4><p>The vision encoder has its own LoRA-targetable modules.</p><pre>import torch.nn as nn<br><br>vision_modules = []<br>for name, module in model.named_modules():<br>    if &quot;visual&quot; in name and isinstance(module, nn.Linear):<br>        vision_modules.append(name)<br>print(f&quot;Found {len(vision_modules)} vision-side Linear modules&quot;)</pre><p>They fall into two categories:</p><ul><li><strong>Attention projections:</strong> q_proj, k_proj, v_proj, o_proj for each vision block. The obvious ones, and where most tutorials stop.</li><li><strong>MLP layers:</strong> gate_proj, up_proj, down_proj for each vision block. Where most of the capacity lives.</li></ul><p>Our final target_modules list:</p><pre>target_modules = [<br>    &quot;q_proj&quot;, &quot;k_proj&quot;, &quot;v_proj&quot;, &quot;o_proj&quot;,<br>    &quot;gate_proj&quot;, &quot;up_proj&quot;, &quot;down_proj&quot;,<br>]</pre><p>PEFT will match the pattern across both vision and language stacks giving LoRA adapters on every Linear in both sides.</p><h4>Verifying Adapters Are Actually Training</h4><p>A useful sanity check after a few hundred training steps: compare adapter norms on the vision side vs the language side. If vision adapters are essentially zero while language adapters have grown, your gradient flow on the vision side is broken.</p><h4>The Serving Trap</h4><p>vLLM supports dynamic LoRA loading: start the server with the base model, specify which adapter to apply at request time. Convenient for serving multiple adapters from one base model.</p><p>For VLMs, this has a problem we hit hard. When you load a LoRA adapter dynamically in vLLM, the vision-side adapters get silently dropped during loading in many configurations. No error, no warning. The model serves, the language adapter is applied, the vision adapter is just gone.</p><p>The symptom is subtle: your fine-tuned model behaves about as well as the base model on tasks that depend on visual adaptation. We noticed because our version 3 to version 4 improvement vanished when we deployed via dynamic loading.</p><p>The fix is to merge the LoRA adapter into the base model before serving:</p><pre>from peft import PeftModel<br>from transformers import AutoModelForCausalLM<br><br>base = AutoModelForCausalLM.from_pretrained(&quot;base-model-path&quot;)<br>peft_model = PeftModel.from_pretrained(base, &quot;lora-adapter-path&quot;)<br>merged = peft_model.merge_and_unload()<br>merged.save_pretrained(&quot;merged-model-path&quot;)</pre><p>Then serve the merged model directly. You lose hot-swapping, but you get the vision adaptations you actually trained.</p><h3>3. Serving Configuration Is Where Real Throughput Lives</h3><p>Once your model is trained and merged, three vLLM parameters end up determining real production throughput. Defaults are reasonable for text-only models and wrong for VLMs.</p><h4>Dtype Depends on Your GPU, Not Your Model</h4><p>The standard advice is “bf16 for training, quantize for inference.” For consumer GPUs serving small VLMs, the picture is more constrained:</p><ul><li><strong>2B model on 24GB RTX 4090 consumer GPU:</strong> bf16 fits comfortably with vision LoRA enabled.</li><li><strong>4B at the same resolution:</strong> doesn’t fit.</li></ul><p>Dtype choices by hardware</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M-XwV_qMxoiboQcu6t1ceA.png" /><figcaption>From: Author</figcaption></figure><p>There’s also a counterintuitive interaction with image tokens: image tokens are consumed by the vision encoder, not generated. Quantizing the language model doesn’t speed up vision-side computation at all. For our workload, the vision encoder represents 30–40% of single-request latency. Even an aggressive language-model quantization that 2x’d LM speed would only improve total latency by 30–40%.</p><h4>max-num-seqs Is the Real Concurrency Cap</h4><p>This sets the maximum number of requests vLLM processes simultaneously. The right number scales with KV cache headroom, not raw GPU memory.</p><p>For a VLM with around 12K image tokens per request and 3K output tokens, our 24GB GPU comfortably handles 32 concurrent sequences in bf16. Going to 48 caused OOM on busy days. Going to 16 cut throughput nearly in half.</p><p>To find the right number empirically: start at half your initial guess, ramp up, watch for both OOM and the throughput plateau. The plateau matters as much as the OOM ceiling.</p><h4>max-model-len Splits Between Input and Output</h4><p>This is the total context budget, shared between prompt, image tokens, and output tokens. For a 16K context VLM:</p><ul><li>Image: ~12K tokens (depends on image resolution)</li><li>Prompt: ~500 tokens</li><li>Output: ~3,500 tokens remaining</li></ul><p>If your output schema produces more than the remaining budget, you’ll see truncation errors that look like model failures but are actually serving config failures.</p><p>Two ways to fix it:</p><ul><li><strong>Increase max-model-len.</strong> Works if you have memory headroom. KV cache scales with context length, so doubling max-model-len roughly doubles cache size per sequence. You’ll need to reduce max-num-seqs to compensate.</li><li><strong>Cap your image resolution.</strong> Smaller images produce fewer image tokens, leaving more output budget. We cap our images at roughly 1.2 million pixels.</li></ul><h4>KV Cache Is Pre-Allocated at Startup</h4><p>vLLM grabs the gpu-memory-utilization percentage of your GPU at startup. Memory doesn’t grow under load. This means you’ll never OOM mid-request, but you have to size correctly upfront. Set it to 0.92 for dedicated serving, lower for multi-tenant boxes.</p><h4>The Cost Structure of VLM Serving</h4><p>The mental model from text-only serving is “tokens per second.” For VLMs, this metric obscures more than it reveals.</p><ul><li><strong>Image tokens are consumed during prefill.</strong> Heavy per token, but a fixed cost per request.</li><li><strong>Output tokens are generated sequentially during decode.</strong> Cheaper per token, but scales linearly with output length.</li></ul><p>A request with 12K image tokens and 500-token output spends most of its time on prefill. The same image with a 3,000-token output spends most of its time on decode. Adding concurrent requests with short outputs increases throughput substantially. Adding concurrent requests with long outputs increases throughput less.</p><h3>4. Input-Side Engineering Beats Another Training Epoch</h3><p>For our first three training iterations, our default response to “model isn’t quite good enough” was to train longer, train more, or train with more data. Each iteration took 90 minutes to two hours and produced incremental improvements we struggled to even measure correctly.</p><p>Three interventions that weren’t training itself ended up moving the needle more than any individual training run.</p><h4>Measure Field-Level Accuracy, Not Eval Loss</h4><p>Eval loss is approximately useless for document AI tasks. Three problems compound:</p><ul><li><strong>Loss across schema changes isn’t comparable</strong> (covered above).</li><li><strong>Loss is dominated by noisy parts of your output.</strong> Free-form text dominates the gradient signal.</li><li><strong>Loss doesn’t tell you which fields are wrong.</strong> Two models with identical 0.05 loss can have completely different field-level failure patterns.</li></ul><p>Field-level accuracy on real-world documents survives all three problems. For each document, mark each business-critical field as correct or incorrect against the source image. Aggregate across a held-out set.</p><p>Once we finalized this metric, conversations about quality became concrete: opening balance went from 92% to 96%, joint holder detection from 60% to 85%. Schema changes became easy to evaluate even when their eval losses lived in different scales.</p><h4>Oversample the Rare Cases</h4><p>The first time we noticed the model failing on joint accounts, our instinct was to look at the training run. The actual problem was simpler.</p><p>Distribution of rare cases before and after oversampling:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8jlEOduyyRoRR98zT2WzqA.png" /><figcaption>From: Author</figcaption></figure><p>The model dutifully learned that “single holder” was the default and ignored the second name even when clearly visible. The fix was duplicating those samples until they appeared roughly 10% of the dataset:</p><p>For any field or pattern that’s underrepresented in training data, the model will learn it as the exception, not the rule. This is true regardless of how good your model is or how long you train.</p><p>Two things to get right- find rare cases through field-level evaluation rather than guessing, and don’t oversample too aggressively. We targeted ~10% per rare case. Higher (say 20%) starts to overfit on the specific samples being duplicated.</p><h4>Treat Prompts as a Training-Time Variable</h4><p>Most teams treat prompts as a runtime knob. For fine-tuning, the prompt is part of your training data. The model learns to attend to whatever your prompt emphasizes.</p><p>Between version 2 and version 3, our training prompt had a line that read:</p><blockquote><em>Transactions: keys MUST be exactly date, narration, debit, credit, balance.</em></blockquote><p>The capitalization was deliberate emphasis on the part that was failing in V1. It worked: V2 had perfectly consistent transaction schemas. But other things broke. Account holder names disappeared on documents where they’d been correctly extracted in V1. Statement dates regressed.</p><p>The MUST language effectively told the model “transactions are the important part, focus your attention there.” It learned exactly that, at the cost of header extraction.</p><p>V3 used the same training data with a rebalanced prompt: equal weight to header and transaction rules, no CAPS emphasis, full schema spelled out for both sections. Header extraction came back. Transaction consistency stayed. Total work: 30 minutes of prompt rewriting.</p><p>We could have spent another training run trying to fix this with hyperparameters. The actual fix didn’t need GPU time at all.</p><h3>What This Looks Like in Production</h3><p>After multiple training iterations and significant work on the serving stack, we have a system that:</p><ul><li>Processes a 50-page document in under 25 seconds end-to-end</li><li>Achieves over 95% field-level accuracy on the structurally critical fields</li><li>Runs at a fraction of frontier API costs at our volumes</li><li>Operates entirely on our own infrastructure on a single consumer GPU</li></ul><h3>What We’re Building Next</h3><p>Bank statements are one document type. The same approach applies broadly across identity documents and financial documents. Each one has its own schema design problem, its own serving constraints and its own evaluation surface.</p><p>We’re hiring engineers who think about engineering as a systems problem. Not which framework is best, but how the pieces fit together, where the actual bottlenecks are and what the cheapest intervention that moves them looks like.</p><p><a href="https://careers.atlys.com/">Check here for openings</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e6c0a1b3b023" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/fine-tuning-a-2b-vision-language-model-for-document-ai-four-lessons-that-beat-hyperparameter-e6c0a1b3b023">Fine-Tuning a 2B Vision-Language Model for Document AI: Four Lessons That Beat Hyperparameter…</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Migrating 600M Records to ClickHouse Cloud]]></title>
            <link>https://engineering.atlys.com/migrating-600m-records-to-clickhouse-cloud-3548241141ae?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/3548241141ae</guid>
            <category><![CDATA[cloud-migration]]></category>
            <category><![CDATA[clickhouse]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[database-migration]]></category>
            <category><![CDATA[data-analytics]]></category>
            <dc:creator><![CDATA[Abhishek Banerjee]]></dc:creator>
            <pubDate>Wed, 19 Nov 2025 10:56:26 GMT</pubDate>
            <atom:updated>2025-11-19T10:56:24.765Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RYiB7cg6xBgX6Nq2g5XZcA.png" /></figure><h3>The Technical Reality of Self-Hosted ClickHouse at Scale</h3><p>For twelve months, I managed a self-hosted ClickHouse cluster handling 2,259 tables and 600+ million records. The architecture seemed solid on paper: 2 shards with 3 replicas, each ClickHouse node provisioned with 1TB storage, 25GB RAM, and 6 vCPUs. The coordination layer ran on 3 ZooKeeper instances with 40GB storage and 12GB RAM each.</p><p>The Pulse pipeline- our GO-based ingestion system- was writing 30 INSERT queries per second, translating to roughly 2.6 million rows daily. This should have been manageable for a well-architected ClickHouse deployment.</p><p>But scale changes everything.</p><h3>Infrastructure Debt Compounds</h3><p>The problems started subtle and escalated into production incidents:</p><p><strong>Memory Management Issues:</strong> Analytical queries would unpredictably consume massive buffers, causing system-wide slowdowns. A query performing in milliseconds on a subset could take tens of seconds on the full dataset. Without predictable resource isolation, this meant production queries could choke the entire cluster.</p><p><strong>ZooKeeper Corruption:</strong> Early in our deployment, a ZooKeeper outage cascaded into data corruption across all replicas. Recovery required manual intervention- backfilling data, validating consistency across shards, and verifying nothing was permanently lost. This was a multi-day incident that exposed the fragility of managing distributed state at this scale.</p><p><strong>Scaling Requires Downtime:</strong> Adding compute capacity meant planned maintenance windows. During a 10x event traffic spike from a sale event, we had no way to elastically scale. At 6 PM, we scheduled emergency maintenance for midnight scaling while stakeholders waited for business reports. The system that should have handled dynamic load gracefully was instead requiring manual intervention during peak demand.</p><p><strong>Single Point of Failure:</strong> As the sole data engineer responsible for this infrastructure, every incident meant context-switching from feature development to firefighting. The operational burden wasn’t just technical- it was consuming all available bandwidth.</p><h3>Designing a Zero-Downtime Migration</h3><p>A big-bang migration- dump everything to Cloud and cutover- carries massive risk. Database locks, memory spikes during bulk inserts, index building blocking production traffic, and no rollback path if issues appear halfway through.</p><p>Instead, I designed a three-phase approach spanning 30 days, optimized for safety and validation.</p><h3>Phase 1: Idempotent Backfill Architecture</h3><p>After provisioning the ClickHouse Cloud cluster, the critical challenge was backfilling 600M+ records without disrupting production or risking data inconsistency.</p><p>The key insight: make the backfill process idempotent. If it runs once and moves data up to timestamp X, subsequent runs should only move data <em>before</em> X- no duplicates, no overwrites, deterministic results regardless of execution count.</p><p>Here’s the implementation:</p><pre>INSERT INTO FUNCTION remoteSecure(<br>  &#39;{host}&#39;,<br>  &#39;{database}.{table_name}&#39;,<br>  &#39;{user}&#39;,<br>  &#39;{password}&#39;<br>)<br>SELECT *<br>FROM {database}.{table_name}<br>WHERE timestamp &lt; coalesce(<br>  (SELECT min(timestamp)<br>   FROM remoteSecure(<br>     &#39;{host}&#39;,<br>     &#39;{database}.{table_name}&#39;,<br>     &#39;{user}&#39;,<br>     &#39;{password}&#39;<br>   )),<br>  toDateTime(&#39;2100-01-01 00:00:00&#39;)<br>)</pre><p>The coalesce clause is the critical piece. It queries the destination cluster for the minimum timestamp already present. If data exists, it only backfills records older than that. If the destination is empty, the fallback date (2100-01-01) ensures all records are included.</p><p>This architecture meant:</p><ul><li>Restartable backfills after interruptions</li><li>No manual tracking of progress state</li><li>Safe to run multiple times during validation</li><li>No risk of duplicate data</li></ul><p>The complete backfill-600 million records across 2,259 tables -completed in <strong>3 hours</strong>. This included time for ClickHouse to build indexes and merge data parts in the background.</p><h3>Phase 2: Dual-Write Validation Period</h3><p>The most nerve-wracking phase: running both clusters simultaneously in production.</p><p>I modified the Pulse pipeline to write every event to both self-hosted and Cloud clusters. The write logic implemented retry logic for Cloud failures, with self-hosted as the source of truth during this transition period.</p><p><strong>Why three weeks?</strong> Because trust is built through sustained validation, not spot checks.</p><p>I implemented comprehensive validation:</p><p><strong>Data Integrity Checks:</strong></p><ul><li>Row count comparisons across all 2,259 tables</li><li>Aggregation result verification (sums, counts, distinct values)</li><li>Sampling and checksum validation for large tables</li></ul><p><strong>Performance Benchmarking:</strong></p><ul><li>Query latency comparisons on identical queries</li><li>Resource utilisation monitoring (CPU, memory, disk I/O)</li><li>Dashboard load time measurements</li></ul><p><strong>Schema Compatibility Validation:</strong></p><ul><li>Distributed tables automatically converted to MergeTree engine on Cloud</li><li>External table connections (Postgres integrations) functioned without modification</li><li>Materialized views rebuilt and maintained correct aggregations</li></ul><p><strong>Query Behavior Analysis:</strong> Not all queries improved immediately. Some required optimization for Cloud’s query planner. I identified queries that regressed, analyzed their execution plans, and adjusted them before cutover.</p><p>The business KPI monitoring was crucial-payments flowing correctly, conversion funnels matching, revenue reports aligning between clusters. This gave stakeholders confidence that the migration wouldn’t introduce business logic errors.</p><h3>Phase 3: Staged Cutover</h3><p><strong>Day 20:</strong> <strong>Switch Reads</strong> All production read queries pointed to ClickHouse Cloud. This was the highest-risk moment -if Cloud data was inconsistent or queries behaved differently, users would see it immediately. But three weeks of validation meant I was confident.</p><p>Monitoring showed:</p><ul><li>Query latency improved drastically</li><li>Error rates stayed at baseline</li><li>Dashboard load times felt instant to users</li><li>Business metrics continued flowing without anomalies</li></ul><p><strong>Day 30:</strong> Decommission after 10 days of flawless operation on Cloud, I disabled writes to the self-hosted cluster and immediately decommissioned it. The three-week validation period had given me enough confidence to pull the trigger without an extended monitoring window.</p><p>The migration was complete with zero downtime and zero data loss.</p><h3>Performance Improvements</h3><p>The numbers told a clear story:</p><p><strong>Traffic Attribution Query:</strong></p><ul><li>Self-hosted: 30 seconds</li><li>Cloud: 0.6 seconds</li><li><strong>50x improvement</strong></li></ul><p>This query joins multiple large tables to attribute user conversions to traffic sources -a complex analytical workload that benefits from ClickHouse Cloud’s improved hardware and query optimisation.</p><p><strong>Fulfilment Dashboard:</strong></p><ul><li>Self-hosted: 8.1 seconds</li><li>Cloud: 1.154 seconds</li><li><strong>7x improvement</strong></li></ul><p>The most critical dashboard for business operations went from painfully slow to nearly instant, improving decision-making speed for stakeholders.</p><p><strong>General Dashboard Experience:</strong> Users described self-hosted dashboards as “painful” with frequent complaints about load times. Post-migration, dashboards loaded “in the blink of an eye.”</p><p>Why the improvement? ClickHouse Cloud’s managed infrastructure includes:</p><ul><li>Better hardware provisioning</li><li>Improved query planning and optimisation</li><li>Superior resource allocation and isolation</li><li>Automatic performance tuning based on workload patterns</li></ul><h3>Reliability Impact</h3><p><strong>Operational:</strong></p><ul><li>Before: Multiple incidents per quarter (ZooKeeper corruption, scaling-induced downtime, memory explosions, DDL query failures)</li><li>After: Zero incidents, 100% uptime, zero maintenance overhead, zero middle-of-night alerts</li></ul><h3>What Transferred Successfully</h3><p>The migration preserved our entire infrastructure without schema changes:</p><ul><li>All 2,259 tables in a single database</li><li>External table connections to Postgres (read-only analytics sources)</li><li>Distributed table coordination (automatically optimised on Cloud)</li><li>Analytical views and query patterns</li><li>Materialized views maintaining real-time aggregations</li><li>Schema caching optimisations (80% system load reduction)</li></ul><p>The Pulse pipeline required only endpoint configuration change- no logic modifications.</p><h3>Trade-offs and Considerations</h3><p><strong>Reduced Control:</strong> Self-hosted ClickHouse allowed granular tuning of replication factors, buffer sizes, memory limits, and merge tree parameters. ClickHouse Cloud abstracts these controls, managing optimization automatically.</p><p><strong>Worth it?</strong> Absolutely. The trade is comparable to owning versus leasing infrastructure- you lose some control but gain expert management and reliability.</p><p><strong>Initial concerns that didn’t materialize:</strong></p><ul><li>Data transfer costs remained minimal</li><li>Query performance regressions were rare and easily fixed</li><li>Schema compatibility was seamless</li><li>Learning curve for Cloud-specific features was gentle</li></ul><h3>Key Technical Lessons</h3><ol><li><strong>Idempotent operations are non-negotiable for migrations.</strong> Being able to safely re-run backfill scripts without side effects eliminated stress and enabled confident validation.</li><li><strong>Dual-write periods expose edge cases synthetic tests miss.</strong> Timezone handling differences, collation mismatches, and query planner quirks only appeared under real production load.</li><li><strong>Phased migrations aren’t slower- they’re safer and ultimately faster.</strong> Three weeks of validation prevented months of debugging production incidents.</li><li><strong>Performance testing must include business-critical queries.</strong> Don’t just benchmark- validate the queries that matter to your users and stakeholders.</li></ol><h3>Should You Migrate?</h3><p>Consider this migration if:</p><ul><li>You’re managing millions+ rows with limited operational support</li><li>You’ve experienced scaling challenges requiring downtime</li><li>You want to focus engineering resources on features rather than infrastructure maintenance</li><li>You need predictable performance during traffic spikes</li></ul><p><strong>The migration profile:</strong></p><ul><li>Timeline: 30 days</li><li>Downtime: Zero</li><li>Data loss: Zero</li><li>Performance improvement: 7–50x (query-dependent)</li></ul><h3>Closing Perspective</h3><p>This migration wasn’t about admitting defeat- it was about recognizing that infrastructure management isn’t core business value. The best infrastructure decision is one that lets you forget about infrastructure and focus on delivering features.</p><p>I migrated from being an ops engineer maintaining databases to a data engineer building analytics products. That shift is how I spend my time is the real ROI.</p><h3>Key Metrics</h3><ul><li><strong>2,259 tables, 600M+ rows</strong></li><li><strong>50x faster queries (30s → 0.6s)</strong></li><li><strong>7x faster dashboards (8.1s → 1.154s)</strong></li><li><strong>Zero incidents post-migration</strong></li><li><strong>Zero downtime migration</strong></li></ul><p>The best infrastructure is the one you don’t have to think about.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3548241141ae" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/migrating-600m-records-to-clickhouse-cloud-3548241141ae">Migrating 600M Records to ClickHouse Cloud</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Database Connection Pool — DIY Edition ️]]></title>
            <link>https://engineering.atlys.com/building-a-database-connection-pool-diy-edition-%EF%B8%8F-bed75c4e0366?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/bed75c4e0366</guid>
            <category><![CDATA[diy]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[postresql]]></category>
            <category><![CDATA[sql]]></category>
            <dc:creator><![CDATA[Gaurav Sharma]]></dc:creator>
            <pubDate>Mon, 19 May 2025 05:54:49 GMT</pubDate>
            <atom:updated>2025-05-19T05:54:49.097Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building a Database Connection Pool — DIY Edition 🛠️</h3><p>You’ve probably come across this nasty error from your database:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/802/1*zGDBu5AX_OaDuLkTAci1GA.png" /><figcaption>too many clients already error</figcaption></figure><p>Well, now that I have your attention, let’s unpack what this actually means — and more importantly, how to fix it (and even build a fix yourself!).</p><h3>Understanding the Root Cause</h3><p>The error message</p><blockquote><strong>“Too many clients already”</strong></blockquote><p>typically indicates that your application is attempting to open more concurrent connections to the database than it is configured to handle.</p><p>Most relational databases, including PostgreSQL, set a default limit on the number of active connections — often around 100. You can check this limit using:</p><pre>SHOW MAX_CONNECTIONS;</pre><p>In high-throughput systems where each incoming request initiates a new database connection, you can quickly exceed this limit. One quick fix might be increasing the allowed number of connections:</p><pre>ALTER SYSTEM SET max_connections = &lt;desired_number&gt;;</pre><p>However, this approach has limitations. Every active connection consumes memory and system resources on the database server. Simply increasing the connection limit without understanding your workload and hardware capacity can lead to degraded performance or even downtime.</p><p>To handle this in a scalable and reliable way, a <strong>connection pooling</strong> strategy is essential. Before we get into building one, let’s simulate the problem to see it in action.</p><pre>import pg from &quot;pg&quot;;<br>import dotenv from &quot;dotenv&quot;;<br><br>dotenv.config();<br><br>/**<br> * Execute multiple database connections in parallel<br> * @param {number} numConnections - Number of connections to create<br> * @returns {Promise&lt;Array&gt;} Results from all connections<br> */<br>export async function runInParallel(numConnections) {<br>  const dbPromises = [];<br><br>  async function executeQuery(id) {<br>    const client = new pg.Client();<br>    try {<br>      await client.connect();<br>      const res = await client.query(&quot;SELECT pg_sleep(2)&quot;);<br>      await client.end();<br>      return { id, success: true, time: res.rows?.[0]?.now };<br>    } catch (error) {<br>      console.error(`Connection ${id}: Error:`, error.message);<br>      await client.end().catch(() =&gt; {});<br>      return { id, success: false, error: error.message };<br>    }<br>  }<br><br>  for (let i = 0; i &lt; numConnections; i++) {<br>    dbPromises.push(executeQuery(i));<br>  }<br><br>  return Promise.all(dbPromises);<br>}<br><br>// Example usage<br>export const runParallelExample = async () =&gt; {<br>  console.log(&quot;Running parallel connections example...&quot;);<br>  <br>  try {<br>    const results = await runInParallel(100);<br>    console.log(&quot;All database connections completed&quot;);<br>    console.log(`Successful connections: ${results.filter(r =&gt; r.success).length}`);<br>    console.log(`Failed connections: ${results.filter(r =&gt; !r.success).length}`);<br>    return results;<br>  } catch (err) {<br>    console.error(&quot;Error in parallel connections:&quot;, err);<br>    throw err;<br>  }<br>};</pre><p>Output -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/866/1*_dihYM-cDZuyjT-BsjyoFw.png" /></figure><h3>Enter: Connection Pools</h3><p>This is where <strong>connection pooling</strong> becomes critical.</p><p>Creating a new database connection for every request doesn’t scale — it’s slow, resource-intensive, and can overwhelm your database under load.</p><p>Connection pools solve this by maintaining a fixed number of open connections that are reused across requests. Each request borrows a connection, runs its query, and returns it to the pool — reducing overhead and avoiding connection exhaustion.</p><p>While most libraries offer pooling out of the box, building one yourself is a great way to understand how it really works. Let’s see how to implement a simple version in JavaScript.</p><p>You can use your own preferred language. Implementation will not differ much.</p><p>We’ll use the <a href="https://www.npmjs.com/package/pg">pg</a> package to talk to a Postgres DB.</p><h4>Step 1: Create a Single Connection</h4><pre>import pg from &quot;pg&quot;;<br><br>export const createConnection = () =&gt; {<br>  const client = new pg.Client();<br>  return client.connect().then(() =&gt; client);<br>};</pre><h4>Step 2: Build the Pool</h4><pre>export const createConnectionPool = async (poolCount) =&gt; {<br>  const connections = [];<br>  for (let i = 0; i &lt; poolCount; i++) {<br>    connections.push(createConnection());<br>  }<br>  return Promise.all(connections);<br>};</pre><h4>Step 3: Pool Manager Class</h4><pre>import pg from &quot;pg&quot;;<br><br>/**<br> * Creates a single database connection<br> * @returns {Promise&lt;pg.Client&gt;} A connected database client<br> */<br>export const createConnection = () =&gt; {<br>  const client = new pg.Client();<br>  return client.connect().then(() =&gt; client);<br>};<br><br>/**<br> * Creates a pool of database connections<br> * @param {number} poolCount - Number of connections to create<br> * @returns {Promise&lt;pg.Client[]&gt;} Array of connected database clients<br> */<br>export const createConnectionPool = async (poolCount) =&gt; {<br>  const connections = [];<br><br>  for (let i = 0; i &lt; poolCount; i++) {<br>    connections.push(createConnection());<br>  }<br><br>  return Promise.all(connections);<br>};<br><br>/**<br> * Connection pool manager class<br> */<br>export class ConnectionPool {<br>  /**<br>   * Create a new connection pool<br>   * @param {number} poolSize - Number of connections to maintain<br>   */<br>  constructor(poolSize = 10) {<br>    this.poolSize = poolSize;<br>    this.connections = [];<br>    this.availableConnections = [];<br>    this.mutex = new Set(); // Track connections in use<br>    this.waitQueue = []; // Queue for waiting query requests<br>    this.isInitialized = false;<br>  }<br><br>  /**<br>   * Initialize the connection pool<br>   * @returns {Promise&lt;void&gt;}<br>   */<br>  async initialize() {<br>    if (this.isInitialized) return;<br>    <br>    this.connections = await createConnectionPool(this.poolSize);<br>    this.availableConnections = [...this.connections];<br>    this.isInitialized = true;<br>    <br>    console.log(`Created a pool with ${this.connections.length} connections`);<br>  }<br><br>  /**<br>   * Execute a SQL query using a connection from the pool<br>   * @param {string} queryText - SQL query to execute<br>   * @param {Array} params - Query parameters<br>   * @param {any} queryId - Optional identifier for the query<br>   * @returns {Promise&lt;Object&gt;} Query result<br>   */<br>  async executeQuery(queryText, params = [], queryId = null) {<br>    if (!this.isInitialized) {<br>      await this.initialize();<br>    }<br><br>    // Wait for an available connection if none are free<br>    if (this.availableConnections.length === 0) {<br>      return new Promise((resolve) =&gt; {<br>        this.waitQueue.push(() =&gt; {<br>          this.executeQuery(queryText, params, queryId).then(resolve);<br>        });<br>      });<br>    }<br><br>    // Get a connection from the pool<br>    const connection = this.availableConnections.pop();<br>    this.mutex.add(connection); // Lock this connection<br><br>    try {<br>      // Execute query<br>      console.log(`Query ${queryId}: Executing with connection`);<br>      const result = await connection.query(queryText, params);<br>      return { <br>        queryId, <br>        success: true, <br>        rows: result.rows,<br>        rowCount: result.rowCount<br>      };<br>    } catch (error) {<br>      console.error(`Query ${queryId}: Error:`, error.message);<br>      return { queryId, success: false, error: error.message };<br>    } finally {<br>      // Release the connection back to the pool<br>      this.mutex.delete(connection);<br>      this.availableConnections.push(connection);<br><br>      // If there are waiting queries, process the next one<br>      if (this.waitQueue.length &gt; 0) {<br>        const nextQuery = this.waitQueue.shift();<br>        nextQuery();<br>      }<br>    }<br>  }<br><br>  /**<br>   * Close all connections in the pool<br>   * @returns {Promise&lt;void&gt;}<br>   */<br>  async close() {<br>    // Wait for all connections to be available (not in use)<br>    while (this.mutex.size &gt; 0) {<br>      await new Promise(resolve =&gt; setTimeout(resolve, 100));<br>    }<br>    <br>    // Close all connections<br>    await Promise.all(<br>      this.connections.map(client =&gt; client.end())<br>    );<br>    <br>    this.connections = [];<br>    this.availableConnections = [];<br>    this.isInitialized = false;<br>  }<br>}</pre><p>This is a simple pool manager class responsible for managing the connection pool. Overall what it does is —</p><ol><li>initializes and creates the connection pool with given number of connections</li><li>executes the given SQL query. If request for number of queries to execute exceeds the number of available connections, the request is put inside a waitQueue and whenever an execution of query is successful, mutex on the connection is released, connection is put back in the pool and any waiting request in the queue is picked up for execution.</li></ol><h4>Example Usage</h4><pre>import dotenv from &quot;dotenv&quot;;<br>import { ConnectionPool } from &quot;../lib/connectionPool.js&quot;;<br><br>dotenv.config();<br><br><br>export async function runUsingPool(poolCount, queryCount) {<br>  const pool = new ConnectionPool(poolCount);<br>  await pool.initialize();<br><br>  try {<br>    // Execute multiple queries<br>    const queryPromises = [];<br>    for (let i = 0; i &lt; queryCount; i++) {<br>      queryPromises.push(pool.executeQuery(&quot;SELECT pg_sleep(2)&quot;, [], i));<br>    }<br><br>    const results = await Promise.all(queryPromises);<br>    return results;<br>  } finally {<br>    // Ensure pool is closed even if there are errors<br>    await pool.close();<br>  }<br>}<br><br>// Example usage<br>export const runPoolExample = async () =&gt; {<br>  console.log(&quot;Running connection pool example...&quot;);<br>  <br>  try {<br>    // Run 100 queries with a pool of 20 connections<br>    const results = await runUsingPool(20, 100);<br>    console.log(&quot;All queries completed:&quot;, results.length);<br>    console.log(`Successful queries: ${results.filter(r =&gt; r.success).length}`);<br>    console.log(`Failed queries: ${results.filter(r =&gt; !r.success).length}`);<br>    return results;<br>  } catch (err) {<br>    console.error(&quot;Error executing queries:&quot;, err);<br>    throw err;<br>  }<br>};</pre><p>Tada 🎉 —</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/934/1*z7fF6Z_HscqDIyImerwvBA.png" /></figure><p>Cool, Right ? 😎</p><p>We now have a basic, working connection pool that:</p><ul><li>Initializes a fixed number of DB connections</li><li>Reuses them across queries</li><li>Queues requests when all connections are busy</li><li>Gracefully handles overloads</li></ul><h3>Optimizations &amp; Ideas 💡</h3><ul><li><strong>Elastic Pools: </strong>Dynamically scale up or down based on demand.</li><li><strong>Timeouts:</strong> Add timeouts for queries stuck in queue.</li><li><strong>Query Prioritization:</strong> Serve critical requests first.</li><li><strong>Observability:</strong> Track pool usage, saturation, and wait times.</li></ul><p>Complete codebase here — <a href="https://github.com/grvsharma1810/connection-pool-diy">https://github.com/grvsharma1810/connection-pool-diy</a></p><h3>Wrapping Up</h3><p>Hope this helped you understand the “why” behind connection pools — and how to build one yourself.</p><p>If you’re into backend engineering and love going deep into systems stuff, let’s connect:</p><p>📍 <a href="https://x.com/imgaurav_sh">Twitter</a><br> 🔗 <a href="https://www.linkedin.com/in/gaurav-kr-sharma/">LinkedIn</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bed75c4e0366" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/building-a-database-connection-pool-diy-edition-%EF%B8%8F-bed75c4e0366">Building a Database Connection Pool — DIY Edition 🛠️</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Slice-Based Zustand Store for Next.js 14 and TypeScript]]></title>
            <link>https://engineering.atlys.com/a-slice-based-zustand-store-for-next-js-14-and-typescript-6b92385a48f5?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/6b92385a48f5</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[frontend]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[startupş]]></category>
            <category><![CDATA[zustand]]></category>
            <dc:creator><![CDATA[Heinrich Winterbach]]></dc:creator>
            <pubDate>Thu, 30 Jan 2025 05:10:00 GMT</pubDate>
            <atom:updated>2025-01-30T05:10:00.123Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Ever feel like your React state is spiraling out of control? Or that you’re gluing together too many unlinked pieces in a big Next.js app? Zustand might just be the relaxing bubble bath your state logic needs.</em></p><p>In this article, we’ll explore how to implement a <strong>slice-based</strong> state management approach in a <strong>Next.js 14</strong> application written in <strong>TypeScript</strong>, using <strong>Zustand</strong> and its powerful middlewares. You’ll see how to keep your store modular, add advanced features like devtools and persist, and incorporate easy immutability with Immer. The best part? Once you learn the basics of slice-based design, you can mix and match these pieces however you like.</p><p><strong>1. Why Zustand in a Next.js 14 World?</strong></p><p>Next.js 14 gives you a spiffy environment for writing both server and client components, but guess what? The client side still needs to orchestrate user-driven interactions, data, and logic. Redux or large frameworks might be overkill. Zustand brings a refreshing minimalism:</p><p>• <strong>No forced structure</strong>: You just write JavaScript (or TypeScript) objects for your state and methods.</p><p>• <strong>Slices</strong>: Rather than a monolithic store, you break things down by domain. Each slice is an isolated chunk that’s easy to understand and test.</p><p>• <strong>Optional middlewares</strong>: You can add devtools, persist, subscribeWithSelector, and immer based on your needs.</p><p>• <strong>TypeScript-friendly</strong>: If you love strong typing, Zustand’s got your back.</p><p>When building advanced features — like verifying passports, linking child travelers, or managing entire booking processes — this approach is a lifesaver. Rather than burying your head in a single monstrous store, you define small slices that do exactly what they need. Then you glue them together into a single store that the rest of your Next.js 14 app can consume.</p><p><strong>2. Setting Up: Dependencies and Folder Structure</strong></p><p>Let’s say you have a Next.js 14 project called my-app. Here’s roughly where our store code might live:</p><pre>my-app/<br>├─ app/<br>│ ├─ (various client/server components)<br>├─ store/<br>│ ├─ slices/<br>│ │ ├─ application-travelers-slice.ts<br>│ │ ├─ application-slice.ts<br>│ │ ├─ insurance-slice.ts<br>│ │ ├─ activities-slice.ts<br>│ │ ├─ …<br>│ ├─ agency-store.ts<br>│ └─ …<br>├─ package.json<br>└─ …</pre><p>We’ll define slices like application-travelers-slice.ts, application-slice.ts, insurance-slice.ts, etc. Each slice focuses on a distinct domain — passports, user identity, child linkages, or insurance coverage.</p><p><strong>Install Zustand </strong>(the tools you need):</p><pre># Yarn<br>yarn add zustand immer<br># or NPM<br>npm install zustand immer</pre><p>This includes:</p><p>• <strong>Zustand</strong>: The state library itself</p><p>• <strong>zustand/middleware</strong>: Devtools, persist, subscribeWithSelector, etc.</p><p>• <strong>immer</strong>: Easy immutability</p><p>Once installed, you’re all set to code the slices.</p><p><strong>3. What Exactly Is a “Slice” in Zustand?</strong></p><p>A slice is simply a function that returns part of your state plus the actions for that part. For instance, if you have a “traveler” slice, it might look like:</p><pre>// store/slices/application-travelers-slice.ts<br><br>export type ApplicationTravelerSliceState = {<br>  travelers: Record&lt;number, { <br>    passportNumber: string; <br>    name: string; <br>    // ...<br>  }&gt;;<br>  travelerErrors: Record&lt;number, string&gt;;<br>};<br><br>export type ApplicationTravelerSliceActions = {<br>  addTraveler: (travelerData: { name: string; passportNumber: string }) =&gt; void;<br>  removeTraveler: (id: number) =&gt; void;<br>};<br><br>export type ApplicationTravelerSlice =<br>  ApplicationTravelerSliceState &amp; ApplicationTravelerSliceActions;<br><br>// The slice is a function returning that combined shape<br>export const createApplicationTravelerSlice: StateCreator&lt;<br>  /* your store type */,<br>  /* any middlewares used */,<br>  [],<br>  ApplicationTravelerSlice<br>&gt; = (set, get) =&gt; ({<br>  travelers: {},<br>  travelerErrors: {},<br><br>  addTraveler: (travelerData) =&gt; {<br>    const nextId = Object.keys(get().travelers).length + 1;<br>    set((state) =&gt; {<br>      state.travelers[nextId] = travelerData;<br>    });<br>  },<br><br>  removeTraveler: (id) =&gt; {<br>    set((state) =&gt; {<br>      delete state.travelers[id];<br>    });<br>  },<br>});</pre><p>This is the entire gist of a slice: define the shape of data it controls, define how you mutate that data, then unify. Because it’s pure TypeScript, you can scale it up with advanced logic — like validations or watchers — however you like.</p><p><strong>4. Distilling Multiple Slices</strong></p><p>Chances are, you’ll have multiple slices — like an “Application slice” for high-level data, an “Insurance slice” for coverage, a “Bulk Upload slice,” and so on. Each slice:</p><p>1. <strong>Exports</strong> its state type, its actions type, and the combined slice type.</p><p>2. <strong>Includes</strong> an init function that configures the slice.</p><p>This might feel like a lot of ceremony at first, but it’s super clean once you get going. If you only have one slice, you don’t need to slice at all — Zustand works either way. But for a more complicated Next.js 14 application (think multiple user flows or specialized data?), you’ll want that separation.</p><p><strong>5. Merging Slices in a Single Store</strong></p><p><strong>Enter </strong>agency-store.ts (you can name it whatever you like). This is where all the magic middlewares come into play, plus the final aggregator. Typically:</p><pre>// store/agency-store.ts<br><br>import { create } from &quot;zustand&quot;;<br>import { devtools, persist } from &quot;zustand/middleware&quot;;<br>import { immer } from &quot;zustand/middleware/immer&quot;;<br>import { subscribeWithSelector } from &quot;zustand/middleware&quot;;<br><br>import { createApplicationTravelerSlice, ApplicationTravelerSlice } from &quot;./slices/application-travelers-slice&quot;;<br>import { createApplicationSlice, ApplicationSlice } from &quot;./slices/application-slice&quot;;<br>import { createInsuranceSlice, InsuranceSlice } from &quot;./slices/insurance-slice&quot;;<br>// ...<br><br>// 1) Build an overall Store type<br>export type Store = ApplicationTravelerSlice &amp; ApplicationSlice &amp; InsuranceSlice /* ...more slices */;<br><br>// 2) Actually create the store<br>export const useAgencyStore = create&lt;Store&gt;()(<br>  devtools(<br>    persist(<br>      subscribeWithSelector(<br>        immer((...args) =&gt; ({<br>          ...createApplicationTravelerSlice(...args),<br>          ...createApplicationSlice(...args),<br>          ...createInsuranceSlice(...args),<br>          // ...<br>        }))<br>      ),<br>      {<br>        name: &quot;agency-store&quot;,<br>        partialize: (state) =&gt; state, // or pick certain fields<br>      }<br>    ),<br>    { name: &quot;AgencyDevtools&quot; }<br>  )<br>);</pre><p>Let’s break down the main players:</p><p>• create&lt;Store&gt;()(…): Yup, TypeScript nuance. The first parentheses let us pass a generic type for the store, the second parentheses actually call the function with our logic.</p><p>• devtools(…): Wraps your store so that the Redux DevTools extension can track state changes.</p><p>• persist(…): Saves state to localStorage (by default) under “agency-store”. Next time the user visits, it rehydrates their previous session.</p><p>• subscribeWithSelector(…): Allows advanced watchers or finer-grained subscriptions so you don’t re-render components unnecessarily.</p><p>• immer(…): Provides a “mutable” syntax while preserving immutability under the hood.</p><p>Inside immer(…), we unify all slices by spreading them: …createApplicationTravelerSlice(…args). We do the same for any slice we want to incorporate.</p><p><strong>6. Using the Store in Your Next.js 14 Components</strong></p><p>Now, let’s put it to work in a client component:</p><pre>&quot;use client&quot;;<br><br>import React from &quot;react&quot;;<br>import { useAgencyStore } from &quot;@/store/agency-store&quot;;<br><br>export default function TravelerList() {<br>  const { travelers, addTraveler } = useAgencyStore((state) =&gt; ({<br>    travelers: state.travelers,<br>    addTraveler: state.addTraveler,<br>  }));<br><br>  const handleAdd = () =&gt; {<br>    addTraveler({ name: &quot;Alice&quot;, passportNumber: &quot;AB123XYZ&quot; });<br>  };<br><br>  return (<br>    &lt;div&gt;<br>      &lt;button onClick={handleAdd}&gt;Add a Traveler&lt;/button&gt;<br>      &lt;ul&gt;<br>        {Object.entries(travelers).map(([id, data]) =&gt; (<br>          &lt;li key={id}&gt;<br>            {id}: {data.name} - {data.passportNumber}<br>          &lt;/li&gt;<br>        ))}<br>      &lt;/ul&gt;<br>    &lt;/div&gt;<br>  );<br>}</pre><p>Key points:</p><p>• <strong>Mark</strong> your component with “use client” at the top — Zustand stores are strictly client-based (unless you do advanced SSR).</p><p>• useAgencyStore: A single hook that merges all slices. We can destruct the traveler slice’s methods or data easily.</p><p>That’s it. If you also had an insurance slice, you’d do useAgencyStore((s) =&gt; s.insuranceTravelers) or s.setInsuranceTripDetails as needed.</p><p><strong>7. DevTools and Persist: Real-World Observations</strong></p><p>Once your code runs, open your browser’s DevTools with the Redux DevTools extension installed. You’ll see an entry named “AgencyDevtools” (or whatever name you specified). Each time you call an action like addTraveler, it logs a state change. You can time-travel, inspect states, etc.</p><p>Meanwhile, if you check localStorage, you’ll spot a key named “agency-store”. That’s your serialized data. If you refresh the page, the store rehydrates, so your travelers persist. If you only want to store some fields, partialize is your friend:</p><pre>persist(<br>  subscribeWithSelector(immer(...)),<br>  {<br>    name: &quot;agency-store&quot;,<br>    partialize: (state) =&gt; {<br>      return { travelers: state.travelers }; <br>    },<br>  }<br>);</pre><p>Now you persist only travelers, ignoring everything else.</p><p><strong>8. Handling Immense Data or Nested Changes</strong></p><p>By default, Zustand merges states at the top level. That means if you set a nested object, you might need to do a bit of manual merging. This is precisely where <strong>Immer</strong> is so helpful:</p><pre>// Without Immer, you&#39;d do something like:<br>// set((state) =&gt; ({ <br>//   travelers: {<br>//     ...state.travelers,<br>//     [id]: {...state.travelers[id], ...updates}<br>//   }<br>// }));<br><br>// With Immer, you can do:<br>set((state) =&gt; {<br>  Object.assign(state.travelers[id], updates);<br>});</pre><p>Internally, Immer ensures these updates remain immutable. It’s magical for slice logic that’s deeper than one or two levels.</p><p><strong>9. Avoiding Unnecessary Re-renders via useShallow</strong></p><p>When you do:</p><pre>const user = useAgencyStore((store) =&gt; store.user);</pre><p>Your component re-renders if the object store.user changes reference, even if it’s the same data. Sometimes that’s a problem. Zustand offers useShallow:</p><pre>import { useShallow } from &quot;zustand/react/shallow&quot;;<br><br>const userInfo = useAgencyStore(<br>  useShallow((store) =&gt; ({ <br>    name: store.user.name, <br>    age: store.user.age <br>  }))<br>);<br>// Re-renders only if name or age changes</pre><p>You can keep your slices returning objects while skipping re-renders if the actual fields remain the same.</p><p><strong>10. Testing Slices</strong></p><p>Zustand slices are simple to test — each slice is mostly a function. You can test them individually or test the combined store:</p><pre>import { useAgencyStore } from &quot;@/store/agency-store&quot;;<br><br>describe(&quot;Traveler Slice Tests&quot;, () =&gt; {<br>  it(&quot;adds a traveler properly&quot;, () =&gt; {<br>    const before = useAgencyStore.getState().travelers;<br>    expect(Object.keys(before)).toHaveLength(0);<br><br>    useAgencyStore.getState().addTraveler({ name: &quot;Alice&quot;, passportNumber: &quot;XYZ123&quot; });<br><br>    const after = useAgencyStore.getState().travelers;<br>    expect(Object.keys(after)).toHaveLength(1);<br>  });<br>});</pre><p>If you want to reset your store after each test, you can call your slice’s reset methods or re-initialize the store. It’s just JavaScript objects — no exotic mocking required.</p><p><strong>11. Common Pitfalls</strong></p><p>1. <strong>Server vs. Client</strong>:</p><p>• Don’t import your client store inside a server component. That will result in confusion (and possibly runtime errors).</p><p>2. <strong>Middleware Order</strong>:</p><p>• Typically, you want devtools as the outermost wrapper, then persist, then subscribeWithSelector, then immer.</p><p>3. <strong>LocalStorage Limitations</strong>:</p><p>• If you’re persisting large volumes, you could run out of space. Keep that in mind or narrow your data with partialize.</p><p>4. <strong>Performance</strong>:</p><p>• If you keep thousands of items in a single slice, be sure you’re selecting small pieces in your UI. Or rely on useShallow to avoid rerenders if the slice is stable.</p><p><strong>12. Wrapping Up</strong></p><p>That’s the gist of building a slice-based Zustand store in Next.js 14 with TypeScript. <strong>Zustand</strong> remains incredibly straightforward, letting you define slices of logic with near-zero overhead. You can sprinkle in advanced validations (like with Yup, Zod, or your favorite library), nest as deeply as you want with Immer, and watch your changes in real time with devtools. Meanwhile, you only store exactly what you want with persist.</p><p><strong>Key Takeaways</strong>:</p><p>1. <strong>Slices</strong> keep your logic cohesive. Each domain or feature in your Next.js app can be one slice.</p><p>2. <strong>Aggregator</strong> (useAgencyStore) merges them, hooking in devtools, persist, etc.</p><p>3. <strong>TypeScript</strong> ensures safe, typed interactions.</p><p>4. useShallow helps you skip unnecessary re-renders if you’re returning objects from the store.</p><p>5. <strong>Testing</strong> is easy: Just import the store or slices and call the methods.</p><p>Next time your Next.js 14 app calls for a “done-for-you” client state solution, give this approach a spin. It’s a breeze to test, debug, and expand. And if your boss asks for a new slice — like, say, a “Coupon slice” or “Payment Gateway slice” — no problem. Just drop it in, merge it in agency-store.ts, and away you go. That’s the beauty of small, well-defined pieces over a single monstrous state file.</p><p>Now, go forth: build that traveler-management or e-commerce or quiz application. Relax in the knowledge that you’re not stuck wrangling boilerplate — Zustand has you covered. Happy slicing and coding!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6b92385a48f5" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/a-slice-based-zustand-store-for-next-js-14-and-typescript-6b92385a48f5">A Slice-Based Zustand Store for Next.js 14 and TypeScript</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automating passport detection and quality analysis using deep learning]]></title>
            <link>https://engineering.atlys.com/automating-passport-detection-and-quality-analysis-using-deep-learning-cd1d4f4e908b?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/cd1d4f4e908b</guid>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[object-detection]]></category>
            <category><![CDATA[deep-learning]]></category>
            <dc:creator><![CDATA[Shubham Tiwari]]></dc:creator>
            <pubDate>Wed, 27 Nov 2024 05:36:14 GMT</pubDate>
            <atom:updated>2024-11-27T05:36:14.530Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1Tuz-ty8g-SMyJcDUvo_oA.png" /><figcaption>[Image source](<a href="https://www.shutterstock.com/image-vector/vector-blank-open-passport-template-international-1060912469">https://www.shutterstock.com/image-vector/vector-blank-open-passport-template-international-1060912469</a>)</figcaption></figure><p>In the visa application process, accurate and reliable identification of passport images is a critical step. Given that Atlys specializes in providing visa, the ability to detect and validate passport images with high precision is essential for a smooth application process. Here are the key challenges that define this problem:</p><ol><li>Detecting the presence of a valid passport in a given image is fundamental. Ensuring high detection accuracy reduces errors in the application process, minimizes manual intervention, and improves user trust.</li><li>Low-quality images with issues like blur, glare, or obstructing objects (e.g., fingers) are common problems in user-submitted photos. Such imperfections can lead to image rejection by embassies, as they compromise the document’s readability. Thus, the model must be able to assess these quality aspects and flag problematic images.</li><li>High-quality images are vital for the next step of optical character recognition (OCR), where passport details are extracted. Poor image quality can lead to errors in field extraction, impacting the overall application process.</li><li>The faster and more seamless the image verification process, the better the user experience. By providing real-time feedback on image quality, we can guide users to capture a valid image on the first attempt. This reduces the need for resubmissions, enhances user satisfaction, and increases conversion rates.</li></ol><h3>Product description and model selection</h3><p>Our product is designed to handle two primary workflows: <strong>Live Capture</strong> and <strong>Upload</strong>. These workflows cater to different user interactions while ensuring that only high-quality images meeting all requirements proceed to the next stage of processing. The following key objectives were our target for the model training -</p><ol><li>Both workflows requires high latency for a seamless user experience.</li><li>Model should generalize well with passports from all countries, even for countries for which data is not available.</li><li>Since, we are using same model for the detection of passport and other quality check classes like glare, fingers. It should generalize well with all classes.</li></ol><p>We benchmarked various models based on our objectives and finally selected YOLOv8 model given its strength in latency and architectural efficiency. This model designed with an anchor-free structure and optimized layers, which reduces computational overhead compared to older YOLO versions. This enables faster inference while maintaining high detection accuracy.</p><p>This offers multiple size variants (e.g., YOLOv8n, YOLOv8s, YOLOv8m) that can be chosen based on the deployment requirements. For our use-case, we used smaller variant (YOLOv8n and YOLOv8s) to minimize latency without compromising too much on accuracy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Enq5pMMF9bzI3OIj1zkYxA.png" /><figcaption>[Image source](<a href="https://blog.roboflow.com/what-is-yolov8/">https://blog.roboflow.com/what-is-yolov8/</a>)</figcaption></figure><h3><strong>Annotation and model training</strong></h3><p>To avoid data and annotation challenges, we followed the active learning approach. It’s a semi-automated approach to annotation where a model is used to predict labels on new data, and then those predictions are reviewed and corrected by humans before retraining the model. Here’s how it works and why it made your annotation job easier:</p><h4>How the Process Works</h4><ol><li><strong>Initial Model Training</strong>: We start with a small, labeled dataset and train a model.</li><li><strong>Model-Assisted Annotation</strong>: As new, unlabeled data becomes available, we run it through the trained model to generate initial annotations or predictions.</li><li><strong>Human Review</strong>: We manually review and correct the annotations generated by the model, ensuring high-quality labeled data.</li><li><strong>Retraining</strong>: The corrected data is added back to the dataset, and the model is retrained on this expanded, more accurate dataset.</li><li><strong>Iteration</strong>: This process is repeated as more data becomes available, progressively improving both the dataset quality and the model’s accuracy.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ki0OqfyYePtI2Dd_0dWXEQ.png" /><figcaption>Image : by author</figcaption></figure><h4>How the active learning fast paced model training</h4><ol><li><strong>Reduced Annotation Effort</strong>: The model automatically generates initial labels, saving time compared to annotating everything manually from scratch. We only need to correct the errors, which is significantly faster.</li><li><strong>Improved Data Quality</strong>: By iteratively refining annotations, we ensure that each iteration of the model trains on more accurate data, leading to better predictions over time.</li><li><strong>Efficient Use of Resources</strong>: We make the most of limited initial labeled data and human effort, leveraging the model to handle repetitive tasks while focusing human effort where it’s most needed (on corrections).</li><li><strong>Scalability</strong>: This approach scales well with growing datasets. As more data comes in, the model’s predictions become increasingly accurate, further reducing manual work.</li><li><strong>Continuous Model Improvement</strong>: Each iteration improves the model’s understanding of the task, leading to better performance not only for generating annotations but also for the final deployment.</li></ol><p>In addition to streamlining annotation, the active labeling approach made it much easier to add new classes to your model’s training process. Since the model could accurately label existing classes, we only needed to annotate the new class on relevant samples, significantly cutting down the manual workload.</p><h3>Optimization and deployment</h3><p>Deploying deep learning models for both live capture and upload use cases requires careful optimization and integration to meet performance, latency and scalability requirements. TFLite is a lightweight framework that optimizes models for deployment on web and mobile devices. It ensures low latency and efficient use of resources, which is crucial for live capture use cases. Ultralytics’s yolo framework makes it very easy to benchmark and export model to tflite.</p><pre>from ultralytics import YOLO<br><br>model = YOLO(&quot;yolo11n.pt&quot;)<br>model = YOLO(&quot;path/to/best.pt&quot;)<br># Export the model<br>model.export(format=&quot;tflite&quot;, half=True)</pre><p>To understand what all parameters can be tuned while exporting model to tflite, you can go through their documentation in detail -</p><p><a href="https://docs.ultralytics.com/modes/export/">Export</a></p><h3>Post-processing for quality checks</h3><p>In post-processing, we detect if the fingers or glare are on the passport region. Some countries have really hard check that they don’t allow fingers or glare to be anywhere on passport, Where as some countries are flexible if it’s not on texts and photos. Based on this, we defined two different ROI’s to check. Two different guide box gives us fine balance between our visa photo requirements and hassle-free user experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*069wq1Cc4Ad6dgZi6u-ktQ.png" /><figcaption>[Image source](<a href="https://www.shutterstock.com/image-vector/vector-blank-open-passport-template-international-1060912469">https://www.shutterstock.com/image-vector/vector-blank-open-passport-template-international-1060912469</a>)</figcaption></figure><p>This below flow chart details the process of post-processing -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cjSxUI7493kgEHqAfjhdsw.png" /><figcaption>Image — by author</figcaption></figure><p>Also, Check out the below blog on how we have integrated it on our production web:</p><p><a href="https://engineering.atlys.com/how-to-build-a-react-app-to-interact-with-a-ml-model-locally-30ae304b9f54">How to build a React App to interact with a ML model locally</a></p><p>This enabled us to provide a hassle-free, efficient and accurate solution for passport image detection and validation. Just navigate to our <a href="https://www.atlys.com/">website</a> to experience our seamless passport scanning process.</p><p>If you’re excited by projects like this, consider joining our team! <a href="https://careers.atlys.com/">We’re hiring!</a></p><h3><strong>Reference links</strong></h3><ul><li><a href="https://docs.ultralytics.com/models/yolov8/">YOLOv8</a></li><li><a href="https://blog.roboflow.com/what-is-yolov8/">What is YOLOv8? A Complete Guide.</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cd1d4f4e908b" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/automating-passport-detection-and-quality-analysis-using-deep-learning-cd1d4f4e908b">Automating passport detection and quality analysis using deep learning</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Production-Grade Full-Text Search System with PostgreSQL: Lessons from Atlys]]></title>
            <link>https://engineering.atlys.com/building-a-production-grade-full-text-search-system-with-postgresql-lessons-from-atlys-d3179288bf3b?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/d3179288bf3b</guid>
            <category><![CDATA[full-text-search]]></category>
            <category><![CDATA[postgresql]]></category>
            <dc:creator><![CDATA[Hardik Gupta]]></dc:creator>
            <pubDate>Mon, 25 Nov 2024 19:27:13 GMT</pubDate>
            <atom:updated>2024-11-25T19:27:13.624Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*E3j1g7tGgN70oZf8.png" /></figure><p>At Atlys, we recently implemented a full-text search system for our activities platform using PostgreSQL’s text search capabilities. Before diving into the implementation, let’s understand some key PostgreSQL full-text search components.</p><blockquote><strong>Key PostgreSQL Full-Text Search Components</strong></blockquote><p>1. <strong>tsvector</strong>: <br>- A sorted list of normalized lexemes (words stripped to their base form)<br>- PostgreSQL Documentation: <a href="https://www.postgresql.org/docs/current/datatype-textsearch.html#DATATYPE-TSVECTOR">tsvector</a></p><pre>SELECT to_tsvector(&#39;english&#39;, &#39;The quick brown foxes jumped&#39;);<br> - Result: &#39;brown&#39;:3 &#39;fox&#39;:4 &#39;jump&#39;:5 &#39;quick&#39;:2</pre><p>2. <strong>tsquery</strong>:<br>- Query tree for text search that specifies what to search for<br>- PostgreSQL Documentation: <a href="https://www.postgresql.org/docs/current/datatype-textsearch.html#DATATYPE-TSQUERY">tsquery</a></p><pre>SELECT to_tsquery(&#39;english&#39;, &#39;quick &amp; fox&#39;);<br> - Searches for documents containing both &quot;quick&quot; and &quot;fox&quot;</pre><p>3. <strong>unaccent</strong>:<br>- Text search dictionary that removes accents (diacritics) from lexemes<br>- [PostgreSQL Documentation: <a href="https://www.postgresql.org/docs/current/unaccent.html">unaccent</a></p><pre>CREATE EXTENSION IF NOT EXISTS unaccent;</pre><h3><strong>Setting Up the Foundation</strong></h3><p>First, we needed to prepare our database for text search. We added a <strong>tsvector</strong> column and created necessary indexes:</p><pre>ALTER TABLE product_metadata <br>ADD COLUMN search_vector tsvector;<br>- Create GIN index for faster searches<br>CREATE INDEX product_metadata_search_idx <br>ON product_metadata USING GIN (search_vector);</pre><p>Our trigger for automatic vector updates:</p><pre>CREATE OR REPLACE FUNCTION product_metadata_search_vector_update() RETURNS trigger AS $$<br>BEGIN<br> - Combine and weight different fields<br> NEW.search_vector :=<br> setweight(to_tsvector(&#39;english&#39;, unaccent(coalesce(NEW.city_name, &#39;&#39;))), &#39;A&#39;) ||<br> setweight(to_tsvector(&#39;english&#39;, unaccent(coalesce(NEW.product_name, &#39;&#39;))), &#39;B&#39;);<br> RETURN NEW;<br>END;<br>$$ LANGUAGE plpgsql;</pre><p><strong>The Search Implementation</strong></p><p>Our search implementation uses <strong>to_tsquery</strong> with proper query cleaning as <strong>to_tsquery </strong>has a high chance of failure in case your search argument contains some special characters like %, [ etc.</p><p>websearch_to_tsquery handles these edge cases but does not provide the full length text based search you might want and just helps with exact matches.</p><p>We decided to go ahead with <strong>to_tsquery </strong>along with implementing our own railguards around keeping the seach argument clean:</p><pre>async def search_products(<br> self, <br> query: str,<br> limit: int = 10,<br> offset: int = 0,<br>) -&gt; Tuple[List[dict], List[dict]]:<br> def _clean_query(q: str) -&gt; str:<br> import re<br> # Handle special characters<br> q = re.sub(r&#39;[!&amp;|():&lt;&gt;\&#39;&quot;\[\]{}+\-~*?\\%@#$^=;,]+&#39;, &#39; &#39;, q)<br> q = re.sub(r&#39;\s+&#39;, &#39; &#39;, q)<br> q = q.strip()<br> return &#39; &amp; &#39;.join(q.split())<br>cleaned_query = _clean_query(query)</pre><h4>websearch_to_tsquery vs to_tsquery: Our Journey</h4><p>PostgreSQL offers multiple text search functions:<br>- <strong>to_tsquery</strong>: Basic text search parser<br>- <strong>plainto_tsquery</strong>: Converts plain text to tsquery<br>- <strong>websearch_to_tsquery</strong>: Implements web-style search syntax<br>- P<a href="https://www.postgresql.org/docs/current/functions-textsearch.html">ostgreSQL Text Search Functions</a></p><p>Initially, we used <strong>websearch_to_tsquery</strong>, but switched to <strong>to_tsquery</strong> because:<br>1. Better control over search term combinations<br>2. More predictable partial matching behavior<br>3. Consistent handling of special characters</p><p>Our search query:</p><pre>WITH SearchResults AS (<br> SELECT <br> city_name,<br> city_code,<br> product_name,<br> ts_rank_cd(search_vector, to_tsquery(&#39;english&#39;, :query || &#39;:*&#39;)) as rank<br> FROM product_metadata<br> WHERE search_vector @@ to_tsquery(&#39;english&#39;, :query || &#39;:*&#39;)<br> ORDER BY rank DESC<br>)</pre><h4>Handling Special Characters</h4><p>Special characters can break tsquery syntax. Our solution:</p><pre>def _clean_query(q: str) -&gt; str:<br> # Remove special characters that could break tsquery<br> q = re.sub(r&#39;[!&amp;|():&lt;&gt;\&#39;&quot;\[\]{}+\-~*?\\%@#$^=;,]+&#39;, &#39; &#39;, q)<br> return &#39; &amp; &#39;.join(q.split())<br># Input: &quot;[Limited Time: 15% Off] City Sightseeing&quot;<br># Output: &quot;Limited &amp; Time &amp; Off &amp; City &amp; Sightseeing&quot;</pre><h4>Performance Optimization</h4><ol><li><strong>GIN Index</strong>: <br>- GIN (Generalized Inverted Index) is optimized for full-text search<br>- <a href="https://www.postgresql.org/docs/current/gin.html">PostgreSQL GIN Index</a></li></ol><pre>CREATE INDEX product_metadata_search_idx ON product_metadata USING GIN (search_vector);</pre><h4>Ranking and Weighting</h4><p>PostgreSQL provides several ranking functions:<br>- <strong>ts_rank</strong>: Basic text search ranking<br>- <strong>ts_rank_cd</strong> : Ranks based on cover density<br>- [<a href="https://www.postgresql.org/docs/current/functions-textsearch.html#TEXTSEARCH-FUNCTIONS-RANKING">PostgreSQL Ranking Functions</a>]</p><p>We use weights to prioritize matches:</p><pre>setweight(to_tsvector(&#39;english&#39;, city_name), &#39;A&#39;) || - Weight: 1.0<br>setweight(to_tsvector(&#39;english&#39;, product_name), &#39;B&#39;) - Weight: 0.4</pre><h3>Key Learnings</h3><ol><li>Always clean user input before creating a tsquery<br>2. Use GIN indexes for performance<br>3. Consider weighting different fields based on importance<br>4. Handle edge cases explicitly</li></ol><h3>Sample request response we got</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xTr-wtafzm1JMq3PO2pmQA.png" /><figcaption>Handling for special chars help you deal with cases when user enters these kind of inputs</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ek5Ate_o3MAuTowcPrXPLQ.png" /><figcaption>Supporting multi word searches</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4D0e69GR04CPvNRJ5y1ivw.png" /><figcaption>Edge case of empty input</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5tTKzomxaY9tI_dbCBeztw.png" /><figcaption>Default single word cases</figcaption></figure><p>PS ~ This is based on our interal data set of some 30 cities and activities available in them.</p><p>## Further Reading<br>- [PostgreSQL Full Text Search](<a href="https://www.postgresql.org/docs/current/textsearch.html">https://www.postgresql.org/docs/current/textsearch.html</a>)<br>- [Using Full Text Search in PostgreSQL](<a href="https://www.postgresql.org/docs/current/textsearch-intro.html">https://www.postgresql.org/docs/current/textsearch-intro.html</a>)<br>- [PostgreSQL Text Search Configuration](<a href="https://www.postgresql.org/docs/current/textsearch-configuration.html">https://www.postgresql.org/docs/current/textsearch-configuration.html</a>)<br>- [GiST and GIN Index Types](<a href="https://www.postgresql.org/docs/current/textsearch-indexes.html">https://www.postgresql.org/docs/current/textsearch-indexes.html</a>)</p><p>This real-world implementation shows how PostgreSQL’s full-text search capabilities can be effectively used in production systems when properly configured and optimized.</p><p>[Note: This is an engineering blog post from Atlys’ engineering team. The code samples are from our actual implementation, showcasing real solutions to real problems we encountered.]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d3179288bf3b" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/building-a-production-grade-full-text-search-system-with-postgresql-lessons-from-atlys-d3179288bf3b">Building a Production-Grade Full-Text Search System with PostgreSQL: Lessons from Atlys</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Animate SVGs: An Introduction to SMIL]]></title>
            <link>https://engineering.atlys.com/how-to-animate-svgs-an-introduction-to-smil-ad068b6a0aa8?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/ad068b6a0aa8</guid>
            <category><![CDATA[svg]]></category>
            <category><![CDATA[frontend]]></category>
            <category><![CDATA[animation]]></category>
            <dc:creator><![CDATA[Anuj Kapoor]]></dc:creator>
            <pubDate>Mon, 25 Nov 2024 07:12:11 GMT</pubDate>
            <atom:updated>2024-11-25T07:52:00.688Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/542/1*YoukOb6R5LsM4uPqe5lCcA.gif" /></figure><p><strong>France jao, Germany jao, Italy jao… lekin ek baat yaad rakhna — apni maati(soil) ko mat bhoolna!</strong></p><p>Have you noticed the new SVG animation in the Atlys visa application flow? It’s not just a pretty graphic — it’s designed to enhance the user experience by turning data into a visually engaging animated graph.</p><p>Curious about how it works? In this blog, I’ll take you behind the scenes and break down the magic behind the center pentagon’s growth animation. Let’s dive in and bring your ideas to life with SVG animations!</p><h4>Step 1: Creating the SVG</h4><p>The foundation of this animation lies in SVG (Scalable Vector Graphics). Here’s how we start:</p><ul><li>Define the viewBox to make scaling and positioning easier.</li><li>Use SVG elements like polygon and line to create the pentagon and its background.</li><li>Group elements using the &lt;g&gt; tag to share properties such as fill, strokeWidth, and strokeDasharray.</li></ul><pre>{/* Background pentagons and lines*/} <br>&lt;svg viewBox=&quot;0 0 100 100&quot; width=&quot;450&quot;&gt;<br>   &lt;g fill=&quot;none&quot; stroke=&quot;gray&quot; strokeWidth=&quot;0.4&quot; strokeDasharray=&quot;2&quot;&gt;<br>      &lt;line x1=&quot;50&quot; y1=&quot;0&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;2&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;21&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;79&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;98&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;polygon points=&quot;50,0 2,35 21,90 79,90 98,35&quot; /&gt;<br>      &lt;polygon points=&quot;50,12 14,38 28,81 72,81 86,38&quot; /&gt;<br>      &lt;polygon points=&quot;50,24 25,42 35,71 65,71 75,42&quot; /&gt;<br>    &lt;/g&gt;<br> &lt;/svg&gt;</pre><p>Here’s how the SVG will look after this step:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1003/1*cnT7EaYeFAIzD6yiddiR8A.png" /></figure><h4>Step 2: Adding the Animated Polygon</h4><p>Next, we layer the animated pentagon on top of the static background. This polygon will dynamically change its shape based on interactions.</p><ul><li>Use a distinct fill and stroke style for visibility.</li><li>Place this polygon outside the &lt;g&gt; group for cleaner code and easier animation control.</li></ul><pre> &lt;svg viewBox=&quot;0 0 100 100&quot; width=&quot;450&quot;&gt;<br>    {/* Background for our svg animation */}<br>    &lt;g fill=&quot;none&quot; stroke=&quot;gray&quot; strokeWidth=&quot;0.4&quot; strokeDasharray=&quot;2&quot;&gt;<br>      &lt;line x1=&quot;50&quot; y1=&quot;0&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;2&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;21&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;79&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;line x1=&quot;98&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>      &lt;polygon points=&quot;50,0 2,35 21,90 79,90 98,35&quot; /&gt;<br>      &lt;polygon points=&quot;50,12 14,38 28,81 72,81 86,38&quot; /&gt;<br>      &lt;polygon points=&quot;50,24 25,42 35,71 65,71 75,42&quot; /&gt;<br>    &lt;/g&gt;<br><br>    {/* The above polygon that we will animate */}<br>    &lt;polygon points=&quot;50,24 25,42 35,71 65,71 75,42&quot; fill=&quot;yellow&quot; fillOpacity=&quot;0.2&quot; stroke=&quot;red&quot; strokeWidth=&quot;0.4&quot; strokeDasharray=&quot;2&quot;/&gt;<br> &lt;/svg&gt;</pre><p>Here’s what it looks like with the polygon added:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/990/1*aTp055_K6_Ci58c1G7pmhA.png" /></figure><h4>Step 3: Logic for Updating Pentagon Coordinates</h4><p>To make the animation dynamic, we calculate the new pentagon coordinates using custom logic.</p><ol><li>Define three sets of polygon coordinates: largestPolygon, mediumPolygon, and smallerPolygon.</li><li>Use a function to generate random points between the smaller and medium polygons.</li></ol><pre>// ./utils<br>function generateRandomPoints(medium, smaller) {<br>  return medium.map(([x1, y1], index) =&gt; {<br>    const [x2, y2] = smaller[index];<br>    const t = Math.random();<br>    const x = Math.round(x1 + t * (x2 - x1));<br>    const y = Math.round(y1 + t * (y2 - y1));<br>    return [x, y];<br>  });<br>}<br><br>const coordinatesToPoint = (coordinates) =&gt;<br>  coordinates.map(([x, y]) =&gt; `${x},${y}`).join(&quot; &quot;);<br><br>export { generateRandomPoints, coordinatesToPoint };<br><br>// ./contants.js<br>const largestPolygon = [<br>  [50, 0],<br>  [2, 35],<br>  [21, 90],<br>  [79, 90],<br>  [98, 35],<br>];<br>const mediumPolygon = [<br>  [50, 12],<br>  [14, 38],<br>  [28, 81],<br>  [72, 81],<br>  [86, 38],<br>];<br>const smallerPolygon = [<br>  [50, 24],<br>  [25, 42],<br>  [35, 71],<br>  [65, 71],<br>  [75, 42],<br>];<br><br>export { largestPolygon, mediumPolygon, smallerPolygon };</pre><p>This logic ensures that the pentagon updates organically.</p><h4>Step 4: Adding Interactivity</h4><p>Now, let’s add a button to make the animation interactive! Clicking this button will trigger a change in the pentagon’s shape by updating its coordinates.</p><pre>export default function App() {<br>  const [coordinates, setCoordinates] = useState(smallerPolygon);<br><br>  function handleUpdatePolygon() {<br>    const randomPolygonPoints = generateRandomPoints(<br>      mediumPolygon,<br>      smallerPolygon<br>    );<br>    setCoordinates(randomPolygonPoints);<br>  }<br><br>  return (<br>    &lt;div className=&quot;App&quot;&gt;<br>      &lt;div&gt;<br>        &lt;svg viewBox=&quot;0 0 100 100&quot; width=&quot;450&quot;&gt;<br>          {/* Background for our svg animation */}<br>          &lt;g fill=&quot;none&quot; stroke=&quot;gray&quot; strokeWidth=&quot;0.4&quot; strokeDasharray=&quot;2&quot;&gt;<br>            &lt;line x1=&quot;50&quot; y1=&quot;0&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;2&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;21&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;79&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;98&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;polygon points={coordinatesToPoint(largestPolygon)} /&gt;<br>            &lt;polygon points={coordinatesToPoint(mediumPolygon)} /&gt;<br>            &lt;polygon points={coordinatesToPoint(smallerPolygon)} /&gt;<br>          &lt;/g&gt;<br><br>          {/* The above polygon that we will animate */}<br>          &lt;polygon<br>            points={coordinatesToPoint(coordinates)}<br>            fill=&quot;yellow&quot;<br>            fillOpacity=&quot;0.2&quot;<br>            stroke=&quot;red&quot;<br>            strokeWidth=&quot;0.4&quot;<br>            strokeDasharray=&quot;2&quot;<br>          /&gt;<br>        &lt;/svg&gt;<br>      &lt;/div&gt;<br>      &lt;button onClick={handleUpdatePolygon}&gt;Update Polygon&lt;/button&gt;<br>    &lt;/div&gt;<br>  );<br>}</pre><p>Here’s what it looks like when interacting with the button:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*6d4GJiwSpoF-yCPeuqwmEg.gif" /></figure><h4>Step 5: Animating the Pentagon</h4><p>Finally, we bring everything to life with SVG animations using the &lt;animate&gt; tag. This allows the pentagon to transition smoothly between its current and new states.</p><p>Here’s how the animation looks with smooth transitions:</p><p>Key animation attributes include:</p><ul><li><strong>attributeName</strong>: Specifies the attribute to animate (e.g., points).</li><li><strong>from and </strong><strong>to</strong>: Define the animation&#39;s starting and ending coordinates.</li><li><strong>dur</strong>: Specifies the duration of the animation.</li><li><strong>begin</strong>: Triggers the animation (e.g., on button click).</li></ul><pre>import { useRef, useState } from &quot;react&quot;;<br>const { largestPolygon, mediumPolygon, smallerPolygon } from &quot;./contants&quot;;<br>const { generateRandomPoints, coordinatesToPoint } from &quot;./utils&quot;;<br>import &quot;./styles.css&quot;;<br><br><br>export default function App() {<br>  const [coordinates, setCoordinates] = useState(smallerPolygon);<br>  const prevCoordinates = useRef(smallerPolygon);<br>  const animateRef = useRef(null);<br><br>  function handleUpdatePolygon() {<br>    const randomPolygonPoints = generateRandomPoints(<br>      mediumPolygon,<br>      smallerPolygon<br>    );<br>    // stroing previous coordinates in ref<br>    prevCoordinates.current = coordinates;<br>    // storing new coordinates<br>    setCoordinates(randomPolygonPoints);<br>    // for trigging animation<br>    animateRef.current.beginElement();<br>  }<br><br>  return (<br>    &lt;div className=&quot;App&quot;&gt;<br>      &lt;div&gt;<br>        &lt;svg viewBox=&quot;0 0 100 100&quot; width=&quot;450&quot;&gt;<br>          {/* Background for our svg animation */}<br>          &lt;g fill=&quot;none&quot; stroke=&quot;gray&quot; strokeWidth=&quot;0.4&quot; strokeDasharray=&quot;2&quot;&gt;<br>            &lt;line x1=&quot;50&quot; y1=&quot;0&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;2&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;21&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;79&quot; y1=&quot;90&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;line x1=&quot;98&quot; y1=&quot;35&quot; x2=&quot;50&quot; y2=&quot;50&quot; /&gt;<br>            &lt;polygon points={coordinatesToPoint(largestPolygon)} /&gt;<br>            &lt;polygon points={coordinatesToPoint(mediumPolygon)} /&gt;<br>            &lt;polygon points={coordinatesToPoint(smallerPolygon)} /&gt;<br>          &lt;/g&gt;<br><br>          {/* The above polygon that we will animate */}<br>          &lt;polygon<br>            points={coordinatesToPoint(coordinates)}<br>            fill=&quot;yellow&quot;<br>            fillOpacity=&quot;0.2&quot;<br>            stroke=&quot;red&quot;<br>            strokeWidth=&quot;0.4&quot;<br>            strokeDasharray=&quot;2&quot;<br>          &gt;<br>            &lt;animate<br>              attributeName=&quot;points&quot;<br>              from={coordinatesToPoint(prevCoordinates.current)}<br>              to={coordinatesToPoint(coordinates)}<br>              dur=&quot;0.3s&quot;<br>              begin=&quot;indefinite&quot;<br>              ref={animateRef}<br>            /&gt;<br>          &lt;/polygon&gt;<br>        &lt;/svg&gt;<br>      &lt;/div&gt;<br>      &lt;button onClick={handleUpdatePolygon}&gt;Update Polygon&lt;/button&gt;<br>    &lt;/div&gt;<br>  );<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*Li_aVtVyEELhn0FJk3Vm2g.gif" /></figure><h4>What Else Can We Do to Make It Even Smoother?</h4><p>You guessed it — we can tweak the animation for a buttery-smooth transition by adding Bézier curves!</p><p>By using attributes like calcMode, keyTimes, and keySplines, we can fine-tune the motion. Here’s an example:</p><pre>&lt;animate<br>   {/* previous code*/}<br>   calcMode=&quot;spline&quot;<br>   keyTimes=&quot;0; 1&quot;<br>   keySplines=&quot;0.25 0.1 0.25 1&quot;<br>/&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*PAQbFFICtYN_wc87VyyqCA.gif" /></figure><p><em>Magical, right?</em> With just a little more effort, you can customize the feel of your animation to match your product perfectly.</p><h4>Step 6: Check Out the Full Working Code</h4><p>Want to see the entire implementation in action? Check out the live, working code on <strong>CodeSandbox</strong>:</p><p><a href="https://codesandbox.io/p/sandbox/pensive-meadow-ff8xc4"><strong>👉 View the CodeSandbox Here</strong></a></p><p>Feel free to explore the live project, interact with it, and customize the animation to suit your needs. It’s a fun and creative way to bring your ideas to life!</p><h4>The Result: A Dynamic Approval Meter</h4><p>With these steps, you’ve created a dynamic and visually appealing pentagon animation. Each click updates the pentagon’s shape, enhancing user engagement with smooth, interactive visuals.</p><h4>Pro Tip: Customize Your Animation</h4><p>Want to take it a step further?</p><ul><li>Experiment with gradient fills or multi-colored strokes.</li><li>Adjust animation durations and easing curves to match your product’s style.</li><li>Play with opacity and stroke effects for added flair.</li></ul><p>SVG animations are incredibly versatile, so don’t hesitate to make them your own!</p><p>Yours friendly engineer,<br><strong>Anuj Kapoor</strong></p><p>Passionate about solving tricky customer problems👨‍💻? <em>Join us: </em><a href="https://careers.atlys.com/">https://careers.atlys.com/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad068b6a0aa8" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/how-to-animate-svgs-an-introduction-to-smil-ad068b6a0aa8">How to Animate SVGs: An Introduction to SMIL</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making visa documents collection a 10x experience with our AI Chatbot: Nanite]]></title>
            <link>https://engineering.atlys.com/making-visa-documents-collection-a-10x-experience-with-our-ai-chatbot-nanite-0aa9c7734930?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/0aa9c7734930</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[chatbots]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Vaibhav singhal]]></dc:creator>
            <pubDate>Wed, 20 Nov 2024 08:00:37 GMT</pubDate>
            <atom:updated>2024-11-20T08:00:37.678Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/898/0*zLanosqWOKO3Mmfj" /></figure><p>At Atlys, we continually strive to enhance user experience. One of the most time-consuming tasks for users is to identify the right documents customers need for their visa applications based on their specific circumstances like profession, marital status, travel history, sponsorship, etc. Previously our team would manually reach out to customers, ask about these details and determine the necessary documents. The customers would then share these documents on the app or via mail.</p><p>However there were few challenges in this process</p><ol><li><strong>Process was vulnerable to operational failures</strong></li></ol><ul><li>There were knowledge gaps on the side of the representative handling the case</li><li>The embassy and government requirements keep changing</li><li>Sometimes there is a lack of response from the customer on call / emails</li></ul><p><strong>2. Process was non-uniform — </strong>A user’s experience would largely be governed by the representative assigned to the case</p><p><strong>3. Process was slow</strong></p><ul><li>There was constant back and forth communication from initial assessment to gathering docs one by one to submitting docs.</li><li>Often this process would take several days to complete</li></ul><p><strong>4. Security Concerns</strong></p><ul><li>Users felt uncomfortable sharing docs via email / whatsapp because of security concerns.</li><li>Docs like Bank statements and ITRs are highly sensitive docs and often customers would be concerned about the privacy of said docs</li></ul><p>5. <strong>Ease of uploading</strong></p><ul><li>Nanite would allow users to upload documents quickly by uploading from gallery, via email link or taking a photo on the spot to upload an image of the document</li><li>This greatly reduces the number of steps involved for the user to forward the docs to Atlys</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/918/0*SrqXV382d20bMXfd" /></figure><p>To convert this into a 10x experience, we developed an intelligent chatbot called <strong>Nanite</strong> designed to inquire, collect and process the customer information to determine the necessary visa documents. The chatbot is powered by a decision tree and is capable of handling additional queries through integration with large language models (LLMs). Here’s a breakdown of how it all works.</p><h3><strong>The Role of the Decision Tree</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/928/0*2vCqUKDpFtPZ9X8L" /></figure><p>To ensure accuracy of the overall process, we used decision trees as the core of this chatbot. Higher accuracy helps in increasing the visa approval rate. Decision trees provide a logic-based system to determine what question to ask next based on the user’s responses and calculate the documents which the user needs to share. Additionally it also helps to model complicated conditions into the chatbot flow, like salaried people based out of delhi who don’t have a sponsor need to share their bank statement. To make the process more convenient for users, chatbot reuses data provided by customers in the past and stores the new data again for future use.</p><p>The logic varies for each country and the type of visa offered by it. For instance, if the user selects that they are applying for a tourist Singapore visa, the chatbot will ask questions specific to that visa. The decision tree dictates which questions are to be asked next based on previous responses, such as:</p><ul><li><strong>Sponsor related data: </strong>Is someone sponsoring the travelers? His information</li><li><strong>Profession</strong>: Are you a student, employed, or self-employed?</li><li><strong>Marital Status</strong>: Are you married, single, or divorced?</li><li><strong>Travel History</strong>: Have you traveled internationally before?</li><li>and more</li></ul><p>Based on the answers, the decision tree continues to branch out. The aim is to narrow down the exact set of documents needed, such as financial statements, invitation letters, or proof of employment</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/466/0*S0ZUQs6bAIGu8lOl" /></figure><h3>Handling Customer Queries Using LLMs</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/0*jEKwquODLLnLdSXI" /></figure><p>While the decision tree efficiently gathers the necessary data, users often have additional questions, especially when dealing with visa-related terminology or document retrieval. Questions like:</p><ul><li>“How do I obtain a bank statement?”</li><li>“What is a sponsor letter?”</li><li>“I don’t have a GST certificate. Can I submit any other document for business proof?”</li></ul><p>This is where large language models (LLMs) come into play. The chatbot is integrated with LLMs, which can understand natural language and respond intelligently to customer queries. These models are trained on vast amounts of data, making them capable of providing answers even for slightly ambiguous or complex questions.</p><p>For example, if a customer asks what a “No Objection Certificate” means, the chatbot accesses the LLM to provide a concise explanation, thus eliminating the need for customers to seek external help. This seamless interaction ensures that users not only receive instructions but also understand the visa process better, enhancing their experience with Atlys.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/918/0*EAnq8lqjPyYITsS5" /></figure><h3>Handling Unknown Scenarios with Support Tickets</h3><p>Despite the robustness of both the decision tree and LLM integration, there are always scenarios where the chatbot might not have the answer. For instance, a customer might ask about a highly specific or unusual situation that the LLM cannot handle effectively, or they might have a technical issue while uploading documents.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/822/0*cV25cI25mriIU5mn" /></figure><p>In such cases, the chatbot is designed to gracefully acknowledge its limitations and ask the customers if they want to talk to someone else from the team. If they do, it creates a support ticket which is routed to our operational team, who can then follow up directly with the customer to resolve the issue. This system ensures that no query is left unanswered and customers feel supported throughout their application process.</p><p>Stay tuned for part 2 of this article to know about more features we have implemented.</p><p>Passionate about solving tricky customer problems👨‍💻? <em>Join us: </em><a href="https://careers.atlys.com/">https://careers.atlys.com/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0aa9c7734930" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/making-visa-documents-collection-a-10x-experience-with-our-ai-chatbot-nanite-0aa9c7734930">Making visa documents collection a 10x experience with our AI Chatbot: Nanite</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Taming Database Connections: Our Journey to Better Connection Pool Management]]></title>
            <link>https://engineering.atlys.com/taming-database-connections-our-journey-to-better-connection-pool-management-bff2e731932f?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/bff2e731932f</guid>
            <category><![CDATA[postgresql]]></category>
            <dc:creator><![CDATA[Hardik Gupta]]></dc:creator>
            <pubDate>Tue, 19 Nov 2024 07:47:00 GMT</pubDate>
            <atom:updated>2024-11-20T04:58:09.445Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/370/0*TXJm7DWZSzf72QIG.png" /></figure><h3>Taming Database Connections: Our Journey to Better Connection Pool Management</h3><p>At Atlys, we recently tackled an interesting challenge in our insurance marketplace service where we were seeing an unusually high number of idle database connections. Here’s how we diagnosed the issue, fixed it, and what we learned along the way.</p><h3><strong>The Problem: Connection Pool Saturation</strong></h3><p>During a routine infrastructure review, we noticed our PostgreSQL connection pool was frequently reaching its limits, with many connections sitting idle. Upon investigation, we traced this back to our <strong>get_agency_insurance_details</strong> endpoint, which retrieves insurance policies for travel agencies.</p><p>The original implementation was opening a new database session for each policy belonging to an agent:</p><pre># Simplified version of the problematic code<br>async def get_agency_insurance_details(self, b2b_agent_uid: str):<br> policies = []<br> for policy_id in policy_ids:<br> async with AsyncPostgresSessionManager().get_session() as session:<br> policy = await self._get_policy_details(session, policy_id)<br> policies.append(policy)<br> return policies</pre><p>This approach was creating multiple database connections for what could be accomplished with a single connection, leading to connection pool saturation and potential performance bottlenecks.</p><h3>The Solution: Consolidated Database Access</h3><p>We refactored the code to use a single database session for fetching all required data:</p><pre>async def get_agency_insurance_details(self, b2b_agent_uid: str, limit: int = 10, offset: int = 0):<br> async with AsyncPostgresSessionManager().get_session() as session:<br> policies, total_count = await self._policy_repo.get_policies_by_agency(<br> session, b2b_agent_uid, limit, offset<br> )<br> if not policies:<br> return PaginatedPolicyResponse(<br> policies=[],<br> total=0,<br> limit=limit,<br> offset=offset<br> )<br>processed_policies = await asyncio.gather(<br> *[self._process_policy(policy) for policy in policies]<br> )<br> return PaginatedPolicyResponse(<br> policies=processed_policies,<br> total=total_count,<br> limit=limit,<br> offset=offset<br> )</pre><h4>Tackling the N+1 Query Problem</h4><p>While fixing the connection pool issue, we also addressed another common database performance pitfall: the N+1 query problem. This occurs when you fetch a record and then need to make additional queries to fetch related data for each record.</p><p>In our case, for each insurance policy, we needed:<br>- The insurance type details<br>- Associated travelers<br>- Payment information</p><p>Without proper optimization, this would result in multiple queries per policy. Here’s how we solved it using SQLAlchemy’s `selectinload`:</p><pre>async def get_policies_by_agency(self, db_session: AsyncSession, b2b_agent_uid: str, limit: int = 10, offset: int = 0):<br> query = (<br> select(InsurancePolicy)<br> .distinct()<br> .join(InsuranceType)<br> .join(InsuranceTraveler)<br> .join(TravelerDetail)<br> .filter(TravelerDetail.b2b_agent_uid == b2b_agent_uid)<br> .options(<br> selectinload(InsurancePolicy.insurance_type),<br> selectinload(InsurancePolicy.insurance_travelers).selectinload(InsuranceTraveler.traveler_details),<br> selectinload(InsurancePolicy.payments),<br> )<br> .order_by(InsurancePolicy.created_at.desc())<br> .limit(limit)<br> .offset(offset)<br> )</pre><p>The <strong>selectinload</strong> strategy eagerly loads related objects using a separate SELECT statement, which is then matched with the parent objects in Python. This approach:<br>- Reduces the number of database queries<br>- Maintains clean separation of concerns in the SQL statements<br>- Efficiently handles one-to-many relationships</p><h3>Impact and Learnings</h3><p>After deploying these changes, we saw significant improvements:<br>- ~95% reduction in idle database connections<br>- Improved response times for policy retrieval endpoints<br>- More stable connection pool utilization</p><h3>Key Takeaways</h3><p>1. <strong>Connection Management</strong>: Always analyze whether multiple database sessions can be consolidated into a single session.<br>2. <strong>Eager Loading</strong>: Use appropriate eager loading strategies (`selectinload`, `joinedload`, etc.) based on your data access patterns.<br>3. <strong>Monitor and Measure</strong>: Regular monitoring of database metrics helped us identify and fix these issues before they became critical problems.</p><h3>Looking Forward</h3><p>This optimization work has led us to establish new best practices for database access patterns in our codebase:<br>- Prefer single-session operations where possible<br>- Use appropriate eager loading strategies by default<br>- Regular review of database connection patterns in code reviews</p><p>Remember, while connection pooling is a powerful feature, it’s important to use it judiciously and always be mindful of how your application interacts with the database layer.</p><p>If you’re excited by projects like this, consider joining our team! <a href="https://careers.atlys.com/">We’re hiring!</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bff2e731932f" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/taming-database-connections-our-journey-to-better-connection-pool-management-bff2e731932f">Taming Database Connections: Our Journey to Better Connection Pool Management</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Built a Real-Time Feedback-Assisted Auto Face Capture in React]]></title>
            <link>https://engineering.atlys.com/how-we-built-a-real-time-feedback-assisted-auto-face-capture-in-react-2754f02d0862?source=rss----a2f2b8cf50ab---4</link>
            <guid isPermaLink="false">https://medium.com/p/2754f02d0862</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[mediapipe]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Gaurav Sharma]]></dc:creator>
            <pubDate>Mon, 11 Nov 2024 04:53:04 GMT</pubDate>
            <atom:updated>2024-11-11T05:15:40.953Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MOxOw6ByAx5icLxPqEmg4Q.jpeg" /><figcaption>Face landmarks illustration</figcaption></figure><p>Capturing a valid photo that meets certain criteria can be tricky, especially when users need to ensure their faces are aligned correctly, lighting is appropriate, and no obstructions are present. Recently, I had the opportunity to work on an exciting auto face capture feature that assists users in capturing photos by guiding them in real-time.</p><p>This feature automatically captures a photo once all conditions are met, eliminating the need for manual intervention. It was built using a combination of MediaPipe Face Landmarker machine learning model and a secondary model which detects other facial attributes which the MediaPipe Face Landmarker cannot, integrated into a React-based UI. In this blog, we’ll be mainly diving into how MediaPipe Face Landmarker can be used to process frames from video stream and provide near real time results.</p><h3>Final Output</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FlN8XFuqjqyY%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlN8XFuqjqyY&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FlN8XFuqjqyY%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/4235cbbd645097582d0859687d9f6bec/href">https://medium.com/media/4235cbbd645097582d0859687d9f6bec/href</a></iframe><h3>Overview</h3><p>The auto face capture is designed to provide real-time feedback while the user is in front of the camera, ensuring their photo meets all criteria before it’s captured. Here’s a high-level overview of what this feature does:</p><ul><li><strong>Face Detection</strong>: Detects facial landmarks (eyes, nose, mouth, etc.) and facial attributes.</li><li><strong>Validation</strong>: Checks for conditions such as proper lighting, face alignment, distance from the camera, and whether the face is covered.</li><li><strong>Auto Capture</strong>: Once the face meets all the required conditions, the system automatically captures the frame after a short countdown.</li></ul><h3>Tools Used</h3><ul><li><strong>MediaPipe Face Landmarker ML Model</strong>: This model identifies facial landmarks, providing the x, y, z coordinates of key points on the face. Ref — <a href="https://ai.google.dev/edge/mediapipe/solutions/vision/face_landmarker">https://ai.google.dev/edge/mediapipe/solutions/vision/face_landmarker</a></li><li><strong>React</strong>: For rendering UI</li></ul><h3>Step-by-Step Implementation with React</h3><p>Let’s break down how this is implemented</p><h4>1. Creating Face Landmarker instance</h4><p>First we need to install the following package from Google — @mediapipe/task-vision which will help in detecting the landmarks of faces. Once done, we can initialize the face landmarker instance which will also download the binary of model - face_landmarker.task</p><pre>export const createFaceLandmarker = async () =&gt; {<br>  const filesetResolver = await FilesetResolver.forVisionTasks(<br>    &#39;https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm&#39;,<br>  );<br><br>  const faceLandmarker = await FaceLandmarker.createFromOptions(<br>    filesetResolver,<br>    {<br>      baseOptions: {<br>        modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task`,<br>        delegate: &#39;CPU&#39; // or &#39;GPU&#39;, check if GPU is available and set accordingly<br>      },<br>      outputFaceBlendshapes: true,<br>      runningMode: &#39;IMAGE&#39;,<br>      numFaces: 50,<br>    },<br>  );<br><br>  return faceLandmarker;<br>};</pre><p>We will run this model in IMAGE mode.</p><h4>2. Setting up the Video Stream</h4><p>Next is accessing the device camera and streaming the video feed into an HTML video element. We achieve this by using the navigator.mediaDevices.getUserMedia API from browser and a React’s useRef to manage the video element.</p><pre>const videoRef = useRef&lt;HTMLVideoElement | null&gt;(null);<br><br>useEffect(() =&gt; {<br>  if (navigator.mediaDevices.getUserMedia) {<br>    navigator.mediaDevices.getUserMedia({ video: true })<br>      .then((stream) =&gt; {<br>        if (videoRef.current) {<br>          videoRef.current.srcObject = stream;<br>        }<br>      })<br>      .catch((error) =&gt; {<br>        console.error(&quot;Camera access error: &quot;, error);<br>      });<br>  }<br>}, []);</pre><ul><li>videoRef: A reference to the video element where the video stream is displayed. This is essential for accessing and controlling the video feed within the React component.</li><li>useEffect: This hook ensures that the camera access is requested and the stream is applied to the video element as soon as the component mounts.</li></ul><h4>3. <strong>Processing Video Frames Using Canvas and ML Models</strong></h4><p>Once the video stream is active, we need to process each frame to run it through the machine learning models. We use an HTML &lt;canvas&gt; element (controlled via a React useRef) to capture and process the video frames in real-time.</p><pre>const canvasRef = useRef&lt;HTMLCanvasElement | null&gt;(null);<br>const isModelRunningRef = useRef(false);<br>const [captureStatus, setCaptureStatus] = useState(&#39;&#39;);<br><br>const validateFrame = (<br>  faceLandmarkerResult?: FaceLandmarkerResult,<br>  canvas?: HTMLCanvasElement,<br>) =&gt; {<br>  const { isTooBright, isTooDark } = isTooDarkOrTooBright(canvas);<br><br>  if (isTooDark) {<br>    return &#39;TOO_DARK&#39;;<br>  }<br><br>  if (isTooBright) {<br>    return &#39;TOO_BRIGHT&#39;;<br>  }<br><br>  if (isMultipleFaces(faceLandmarkerResult)) {<br>    return &#39;MULTIPLE_FACE&#39;<br>  }<br><br>  //...all other checks can be added here.<br><br>  return &#39;GOOD_PHOTO&#39;;<br>}<br><br>const runModel = (canvas, faceLandmarker) =&gt; {<br>    // This is make sure to run models on new frames only if processing of previous frame is complete.<br>    // Please note this mean some frames are ignored and not processed<br>    if(isModelRunningRef.current === true) return;<br><br>    isModelRunningRef.current = true;<br><br>    // Process the frame using Face Landmarker<br>    const faceLandmarks = faceLandmarker.detect(canvas)<br><br>    // Process frame using internal ML Model<br>    const modelResult = runTFLiteModel(canvas);<br><br>    // Validate the frame<br>    const captureStatus = validateFrame(faceLandmarks, canvas, modelResult);<br><br>    setCaptureStatus(captureStatus); // use this state to show feedback on UI<br><br>    if(captureStatus !== &#39;GOOD_PHOTO&#39;){<br>        stopCapture();<br>        setCaptureStatus(captureStatus);<br>        isModelRunningRef.current = false;<br>        return;<br>    }<br><br>    // captureStatus is POSITIVE, start the capture<br>    startCapture();<br>    setCaptureStatus(&#39;GOOD_PHOTO&#39;);<br>    isModelRunningRef.current = false;<br>}<br><br>const processFrame = (faceLandmarker) =&gt; {<br>  const canvas = canvasRef.current;<br>  const video = videoRef.current;<br><br>  if (canvas &amp;&amp; video) {<br>    const context = canvas.getContext(&#39;2d&#39;)!;<br>    canvas.width = video.videoWidth;<br>    canvas.height = video.videoHeight;<br><br>    // Draw the current video frame onto the canvas<br>    context.drawImage(video, 0, 0, canvas.width, canvas.height);<br>    <br>    runModel(canvas, faceLandmarker);<br><br>    // Continue processing frames recursively<br>    window.requestAnimationFrame(processFrame);<br>  }<br>};<br><br>navigator.mediaDevices<br>    .getUserMedia(constraints)<br>    .then((stream) =&gt; {<br>        streamRef.current = stream;<br><br>        if (videoRef.current == null) return;<br><br>        videoRef.current.srcObject = stream;<br>        videoRef.current.play();<br>        <br>        // Start processing frames on `loadeddata` event on video element.<br>        videoRef.current.addEventListener(&#39;loadeddata&#39;, () =&gt;<br>          processFrame(faceLandmarker),<br>        );<br>    })</pre><ul><li>canvasRef: a reference to the canvas element where each frame from the video is drawn.</li><li>processFrame: this function is recursively called using window.requestAnimationFrame which basically means processFrame is called after each repaint done by browser and has it’s own advantage. For instance, if the tab is not active, then processFrame would not be called.</li><li>startCapture() : just starts the countdown and handle whatever is needed when countdown is started.</li></ul><h4>4. Processing Video Frames Using Canvas and ML Models</h4><p>To ensure the captured photo meets all the necessary criteria, we validate each frame by running various checks. Here are the utility functions used for validation:</p><ol><li><strong>Lighting Validation (</strong>isTooDark<strong>,</strong> isTooBright<strong>)</strong></li></ol><p>These functions check whether the lighting is either too dark or too bright, based on the RGB values of each pixel</p><pre>const TOO_DARK_THRESHOLD = 60;<br>const TOO_BRIGHT_THRESHOLD = 200;<br><br>// This function will convert each color to gray scale and return average of all pixels, so final value will be between 0 (darkest) and 255 (brightest)<br>const getFrameBrightness = (canvas: HTMLCanvasElement) =&gt; {<br>  const ctx = canvas.getContext(&#39;2d&#39;);<br><br>  if (!ctx) return;<br><br>  let colorSum = 0;<br><br>  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);<br>  const data = imageData.data;<br>  let r, g, b, avg;<br><br>  for (let x = 0, len = data.length; x &lt; len; x += 4) {<br>    r = data[x];<br>    g = data[x + 1];<br>    b = data[x + 2];<br><br>    avg = Math.floor((r + g + b) / 3);<br>    colorSum += avg;<br>  }<br><br>  // value between 0 - 255<br>  const brightness = Math.floor(colorSum / (canvas.width * canvas.height));<br><br>  return brightness;<br>};<br><br>const isTooDarkOrTooBright = (canvas: HTMLCanvasElement) =&gt; {<br>  const brightness = getFrameBrightness(canvas);<br><br>  let isTooDark = false;<br>  let isTooBright = false;<br><br>  if (brightness == null) {<br>    return {<br>      isTooBright,<br>      isTooDark,<br>    };<br>  }<br><br>  if (brightness &lt; TOO_DARK_THRESHOLD) {<br>    isTooDark = true;<br>  } else if (brightness &gt; TOO_BRIGHT_THRESHOLD) {<br>    isTooBright = true;<br>  }<br><br>  return {<br>    isTooBright,<br>    isTooDark,<br>  };<br>};</pre><p>2. <strong>Checking for Multiple Faces (</strong>isMultipleFaces<strong>)</strong></p><p>Result returned by face landmarker can be passed to this utility and if there are face landmarks of multiple faces present, this returns true</p><pre>export const isMultipleFaces = (<br>  faceLandmarkerResult,<br>) =&gt; {<br>  if (faceLandmarkerResult &amp;&amp; faceLandmarkerResult.faceLandmarks.length &gt; 1) {<br>    return true;<br>  }<br><br>  return false;<br>};</pre><p>3. <strong>Face Cutoff Detection (</strong>isFaceCutoff<strong>)</strong></p><p>This function checks whether any of the face landmarks are outside the boundaries of the image (canvas). Since x and y co-ordinates in face landmarker result are normalized, we convert to actual pixel co-ordinates and multiplying with <strong>frame width and height</strong> accordingly.</p><pre>import { NormalizedLandmark } from &#39;@mediapipe/tasks-vision&#39;;<br><br>function isFaceCutOffScreen(<br>  faceLandmarks: NormalizedLandmark[],<br>  imgW: number,<br>  imgH: number,<br>): boolean {<br>  for (const landmark of faceLandmarks) {<br>    const x = Math.round(landmark.x * imgW);<br>    const y = Math.round(landmark.y * imgH);<br><br>    if (x &lt;= 0 || x &gt;= imgW || y &lt;= 0 || y &gt;= imgH) {<br>      return true;<br>    }<br>  }<br>  return false;<br>}</pre><p>4. <strong>Face Distance Detection (</strong>isFaceTooClose<strong>,</strong> isFaceTooFar<strong>)</strong></p><p>This function determines if the face is too far from the camera by measuring the distance between the eyes.</p><pre>import { NormalizedLandmark } from &#39;@mediapipe/tasks-vision&#39;;<br><br>// Calculate Euclidean distance between two points<br>const getDistance = (point1: number[], point2: number[]): number =&gt; {<br>  const [x1, y1] = point1;<br>  const [x2, y2] = point2;<br>  return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));<br>};<br><br>const FACE_TOO_CLOSE_THRESHOLD = 370;<br>const FACE_TOO_FAR_THRESHOLD = 300;<br><br>function isFaceTooFar(<br>  landmark: NormalizedLandmark[],<br>  imgW: number,<br>  imgH: number,<br>  threshold: number = FACE_TOO_FAR_THRESHOLD,<br>): boolean {<br>  const leftEye = [landmark[33].x * imgW, landmark[33].y * imgH];<br>  const rightEye = [landmark[263].x * imgW, landmark[263].y * imgH];<br><br>  // Calculate the distance between the eyes<br>  const eyeDistance = getDistance(leftEye, rightEye);<br>  return eyeDistance &lt; threshold;<br>}<br><br>function isFaceTooClose(<br>  landmark: NormalizedLandmark[],<br>  imgW: number,<br>  imgH: number,<br>  threshold: number = FACE_TOO_CLOSE_THRESHOLD,<br>): boolean {<br>  const leftEye = [landmark[33].x * imgW, landmark[33].y * imgH];<br>  const rightEye = [landmark[263].x * imgW, landmark[263].y * imgH];<br><br>  // Calculate the distance between the eyes<br>  const eyeDistance = getDistance(leftEye, rightEye);<br>  return eyeDistance &gt; threshold;<br>}</pre><p>5. <strong>Is the face centered ?</strong></p><p>These functions check whether the face is positioned too far to the left, too far right, too far up, too far down in frame. This is done by checking leftmost, rightmost, topmost and bottommost points from landmarks and adjusting the threshold accordingly.</p><pre>const FACE_TOO_RIGHT_THRESHOLD = 500;<br>const FACE_TOO_LEFT_THRESHOLD = 600;<br>const FACE_TOO_FAR_UP_THRESHOLD = 150;<br>const FACE_TOO_FAR_DOWN_THRESHOLD = 450;<br><br>function isFaceTooFarLeft(<br>  landmark: NormalizedLandmark[],<br>  imgWidth: number,<br>  thresholdRatio: number = FACE_TOO_LEFT_THRESHOLD,<br>): boolean {<br>  const leftmostX = Math.min(<br>    landmark[1].x * imgWidth,<br>    landmark[263].x * imgWidth,<br>  );<br>  return leftmostX &gt; thresholdRatio;<br>}<br><br>function isFaceTooFarRight(<br>  landmark: NormalizedLandmark[],<br>  imgWidth: number,<br>  thresholdRatio: number = FACE_TOO_RIGHT_THRESHOLD,<br>): boolean {<br>  const rightmostX = Math.max(<br>    landmark[1].x * imgWidth,<br>    landmark[263].x * imgWidth,<br>  );<br>  return rightmostX &lt; thresholdRatio;<br>}<br><br>function isFaceTooFarUp(<br>  landmark: NormalizedLandmark[],<br>  imgHeight: number,<br>  thresholdRatio: number = FACE_TOO_FAR_UP_THRESHOLD,<br>): boolean {<br>  const topmostY = landmark[10].y * imgHeight;<br>  return topmostY &lt; thresholdRatio;<br>}<br><br>function isFaceTooFarDown(<br>  landmark: NormalizedLandmark[],<br>  imgHeight: number,<br>  thresholdRatio: number = FACE_TOO_FAR_DOWN_THRESHOLD,<br>): boolean {<br>  const bottommostY = landmark[10].y * imgHeight;<br>  return bottommostY &gt; thresholdRatio;<br>}</pre><p>6. <strong>Are Eyes Closed?</strong></p><p>Fortunately Face Landmarker returns something called as face blendshapes which has different face attributes like are eyes closed, looking left right etc. We can leverage 2 of these attributes to check if eyes are closed or not.</p><p>For more such attributes, please refer to this codepen — <a href="https://codepen.io/mediapipe-preview/pen/OJBVQJm">https://codepen.io/mediapipe-preview/pen/OJBVQJm</a></p><pre>import { FaceLandmarkerResult } from &#39;@mediapipe/tasks-vision&#39;;<br><br>const isEyesClosed = (faceLandmarkResult: FaceLandmarkerResult) =&gt; {<br>  const result = faceLandmarkResult?.faceBlendshapes?.[0]?.categories<br>    ?.filter(<br>      (category: any) =&gt;<br>        category.categoryName === &#39;eyeBlinkLeft&#39; ||<br>        category.categoryName === &#39;eyeBlinkRight&#39;,<br>    )<br>    ?.map((category: any) =&gt; category.score);<br><br>  if (!result) return false;<br><br>  return result[0] &gt; 0.5 || result[1] &gt; 0.5;<br>};</pre><p>7.<strong> Detecting Head Orientation</strong></p><p>To check if user is looking up, down, left right, we can calculate something called as yaw and pitch angles. There are ways to calculate these angles using OpenCV library which includes doing some complex calculations on landmark points to get these angles. You can check it out here —</p><p><a href="https://medium.com/@susanne.thierfelder/head-pose-estimation-with-mediapipe-and-opencv-in-javascript-c87980df3acb">Head Pose Estimation with MediaPipe and OpenCV in Javascript</a></p><p>Though I did not wanted to add OpenCV package as dependency to the project just for this usecase, so I found an alternative to the above method which does a decent job. You can read more about it here —</p><p><a href="https://medium.com/@sshadmand/a-simple-and-efficient-face-direction-detection-in-react-e02cd9d547e5">A Simple and efficient Face direction detection in React</a></p><p>Here’s how I implemented the same -</p><pre>const getAngleBetweenLines = (<br>  midpoint: NormalizedLandmark,<br>  point1: NormalizedLandmark,<br>  point2: NormalizedLandmark,<br>) =&gt; {<br>  const vector1 = { x: point1.x - midpoint.x, y: point1.y - midpoint.y };<br>  const vector2 = { x: point2.x - midpoint.x, y: point2.y - midpoint.y };<br><br>  // Calculate the dot product of the two vectors<br>  const dotProduct = vector1.x * vector2.x + vector1.y * vector2.y;<br><br>  // Calculate the magnitudes of the vectors<br>  const magnitude1 = Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y);<br>  const magnitude2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y);<br><br>  // Calculate the cosine of the angle between the two vectors<br>  const cosineTheta = dotProduct / (magnitude1 * magnitude2);<br><br>  // Use the arccosine function to get the angle in radians<br>  const angleInRadians = Math.acos(cosineTheta);<br><br>  // Convert the angle to degrees<br>  const angleInDegrees = (angleInRadians * 180) / Math.PI;<br><br>  return angleInDegrees;<br>};<br><br>const calculateDirection = (<br>  faceLandmarkerResult: FaceLandmarkerResult,<br>) =&gt; {<br>  const landmarks = faceLandmarkerResult.faceLandmarks[0];<br><br>  // leftmost, center, rightmost points of nose.<br>  if (!landmarks?.[1] || !landmarks?.[279] || !landmarks?.[49])<br>    return {<br>      isLookingDown: false,<br>      isLookingLeft: false,<br>      isLookingRight: false,<br>      isLookingUp: false,<br>    };<br><br>  const noseTip = { ...landmarks[1] };<br>  const leftNose = { ...landmarks[279] };<br>  const rightNose = { ...landmarks[49] };<br><br>  // MIDESCTION OF NOSE IS BACK OF NOSE PERPENDICULAR<br>  const midpoint: NormalizedLandmark = {<br>    x: (leftNose.x + rightNose.x) / 2,<br>    y: (leftNose.y + rightNose.y) / 2,<br>    z: (leftNose.z + rightNose.z) / 2,<br>    visibility: 0,<br>  };<br><br>  const perpendicularUp: NormalizedLandmark = {<br>    x: midpoint.x,<br>    y: midpoint.y - 50,<br>    z: midpoint.z,<br>    visibility: 0,<br>  };<br><br>  // CALC ANGLES<br>  const pitch = getAngleBetweenLines(midpoint, noseTip, perpendicularUp);<br>  const yaw = getAngleBetweenLines(midpoint, rightNose, noseTip);<br><br>  const isLookingUp = pitch &lt; PITCH_UP_THRESHOLD;<br>  const isLookingDown = pitch &gt; PITCH_DOWN_THRESHOLD;<br>  const isLookingLeft = yaw &gt; YAW_LEFT_THRESHOLD;<br>  const isLookingRight = yaw &lt; YAW_RIGHT_THRESHOLD;<br><br>  return { isLookingDown, isLookingLeft, isLookingRight, isLookingUp };<br>};</pre><h4>5. <strong>Face Capture and Final Confirmation</strong></h4><p>Once all validations pass and the frame is deemed valid, a countdown starts, and the frame is captured automatically. useCountdown hook can be implemented from scratch or can be consumed from any external package. I used usehooks-ts package as I did not want to reinvent the wheel and this package handles the nitty gritty details of hook’s implementation.</p><pre>import { useCountdown } from &#39;usehooks-ts&#39;;<br><br>const isCapturingRef = useRef(false);<br>const [photo, setPhoto] = useState&lt;Blob | null&gt;(null);<br>const [count, { startCountdown, stopCountdown, resetCountdown }] =<br>    useCountdown({<br>      countStart: 3,<br>      countStop: 1,<br>      intervalMs: 1000,<br>    });<br><br>const startCapture = () =&gt; {<br>    startCountdown();<br>};<br><br>const stopCapture = () =&gt; {<br>    stopCountdown();<br>    resetCountdown();<br>};<br><br>const onImageCapture = () =&gt; {<br>  if (canvasRef &amp;&amp; canvasRef.current) {<br>    const context = canvasRef.current.getContext(&#39;2d&#39;);<br>    if (context) {<br>      // Convert the canvas to a blob and store photo in state<br>      canvasRef.current.toBlob((b) =&gt; setPhoto(b), &#39;image/jpeg&#39;, 0.9);<br>    }<br>  }<br>};<br><br>useEffect(() =&gt; {<br>  if (count === 1) {<br>    onImageCapture();<br>  }<br>}, [count]);</pre><p>Finally we have our captured photo stored in a React’s state photo, which can be consumed as needed. This can be shown to user for confirmation and then sent to upstream services.</p><p><strong>Useful trick</strong></p><p>To get the image url from a blob, you can simply use URL.createObjectURL(photo) this will return a string which can be passed to src attribute of img tag.</p><h3>Fine-Tuning Thresholds</h3><p>While the conditions mentioned above work well out of the box, it’s highly customizable. You can adjust thresholds for detecting brightness, face distance etc.</p><h3>Performance Optimization</h3><p>Since the models are running continuously processing one frame after another, it can overwhelm the main thread potentially deeming the UI to be frozen degrading user experience, unusable. To solve this, we can run the models asynchronously. Especially for time-consuming operations like face detection, asynchronous execution is preferred to maintain a responsive user interface and provide a better user experience.</p><p>So I wrote a wrapper which converts a sync function to an async function and used this wrapper to run Face Landmarker.</p><pre>function asyncWrapper(syncFunction: () =&gt; void) {<br>  return new Promise((resolve, reject) =&gt; {<br>    setTimeout(() =&gt; {<br>      try {<br>        const result = syncFunction();<br>        resolve(result);<br>      } catch (error) {<br>        reject(error);<br>      }<br>    }, 0);<br>  });<br>}<br><br>const runModel = async () =&gt; {<br>    //...<br>    await asyncWrapper(() =&gt; faceLandmarker.detect(canvas);<br>    //....<br>}</pre><p>I hope this is helpful to you! If you’re excited by projects like this, consider joining our team! <a href="https://careers.atlys.com/">We’re hiring!</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2754f02d0862" width="1" height="1" alt=""><hr><p><a href="https://engineering.atlys.com/how-we-built-a-real-time-feedback-assisted-auto-face-capture-in-react-2754f02d0862">How We Built a Real-Time Feedback-Assisted Auto Face Capture in React</a> was originally published in <a href="https://engineering.atlys.com">Atlys Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>