<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: saman gh</title>
    <description>The latest articles on DEV Community by saman gh (@qatrehai).</description>
    <link>https://dev.to/qatrehai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4113689%2F50b7252d-3181-4208-a711-76efdaaecc55.jpg</url>
      <title>DEV Community: saman gh</title>
      <link>https://dev.to/qatrehai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qatrehai"/>
    <language>en</language>
    <item>
      <title>Why Your Persian Chatbot Answers the Wrong Question</title>
      <dc:creator>saman gh</dc:creator>
      <pubDate>Mon, 07 Sep 2026 11:18:24 +0000</pubDate>
      <link>https://dev.to/qatrehai/why-your-persian-chatbot-answers-the-wrong-question-469d</link>
      <guid>https://dev.to/qatrehai/why-your-persian-chatbot-answers-the-wrong-question-469d</guid>
      <description>&lt;p&gt;When a Persian-language assistant answers half its questions with "I don't have information about that," the first assumption is always that its knowledge base is too small. Usually it isn't. The program is seeing one word as several unrelated strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;In Persian — and in Arabic script generally — a single word can be written several ways that look identical to a reader and are completely different to a computer. &lt;code&gt;چت‌بات&lt;/code&gt; written with a zero-width non-joiner and &lt;code&gt;چت بات&lt;/code&gt; written with an ordinary space share no byte sequence. Until text is folded into one comparable form before any comparison, a large share of real questions will miss, and nothing in the logs will indicate a fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  One word, several spellings
&lt;/h2&gt;

&lt;p&gt;None of these variants is a spelling mistake. All of them appear in formal published Persian, and all are encoded distinctly in Unicode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The zero-width non-joiner&lt;/strong&gt; (U+200C) is a real character with no width. It separates parts of a compound without a space, so &lt;code&gt;می‌رود&lt;/code&gt;, &lt;code&gt;می رود&lt;/code&gt; and &lt;code&gt;میرود&lt;/code&gt; are three distinct strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persian yeh (U+06CC) versus Arabic yeh (U+064A)&lt;/strong&gt; render nearly identically in most fonts, and different keyboard layouts produce different ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persian keheh (U+06A9) versus Arabic kaf (U+0643)&lt;/strong&gt; — the same situation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heh versus teh marbuta&lt;/strong&gt; — a writer using an Arabic layout types &lt;code&gt;شرکة&lt;/code&gt; rather than &lt;code&gt;شرکه&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three digit sets.&lt;/strong&gt; Persian digits (U+06F0–U+06F9), Arabic-Indic digits (U+0660–U+0669) and ASCII digits are three separate encodings of the same numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diacritics and tatweel.&lt;/strong&gt; Optional vowel marks and the decorative elongation character survive in pasted text and break matching silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a term as ordinary as "chatbot", that yields at least four common written forms, all correct. A system that recognises one of them fails three users in four.&lt;/p&gt;

&lt;p&gt;Here is the problem in three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;چت‌بات&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// with U+200C&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;چت بات&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// with a space&lt;/span&gt;

&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;                  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;    &lt;span class="c1"&gt;// true  &amp;lt;-- the trap&lt;/span&gt;

&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;// [ '686', '62a', '200c', '628', '627', '62a' ]&lt;/span&gt;
&lt;span class="c1"&gt;//                  ^^^^ the character you cannot see&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why nothing appears in the logs
&lt;/h2&gt;

&lt;p&gt;This is the part that makes the problem persist. When matching fails the program does not crash — it falls through to a default reply. The web server records a 200, uptime monitoring stays green, and no dashboard turns red.&lt;/p&gt;

&lt;p&gt;From outside, the only symptom is that a user asks once, receives a generic answer, and closes the window. That pattern was present in our own system and stayed invisible until a fixed set of real questions was run against it. The method is in &lt;a href="https://qatrehai.ir/blog/persian-chatbot-testing-method-en" rel="noopener noreferrer"&gt;how to actually test a Persian chatbot&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second trap: substring matching
&lt;/h2&gt;

&lt;p&gt;Suppose the system detects intent by searching for keywords inside the incoming question. It is the simplest approach and it carries a serious flaw, because Persian attaches prefixes and suffixes freely.&lt;/p&gt;

&lt;p&gt;A real example from our own system: the keyword &lt;code&gt;زمان&lt;/code&gt; (time) was registered to detect questions about project schedules. A user asked &lt;em&gt;do you offer enterprise training?&lt;/em&gt; — which in Persian contains the word &lt;code&gt;سازمانی&lt;/code&gt; (organisational). The four-letter keyword was found as a substring inside it, and the system answered with a project timeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;آموزش سازمانی دارید؟&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;زمان&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// true — and wrong&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to compare at whole-token level rather than by substring, and to allow a prefix match only for terms long enough that the shared opening cannot be coincidental — so &lt;code&gt;آموزش&lt;/code&gt; still matches &lt;code&gt;آموزشی&lt;/code&gt;, while &lt;code&gt;زمان&lt;/code&gt; no longer matches &lt;code&gt;سازمانی&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order the folding has to happen in
&lt;/h2&gt;

&lt;p&gt;Before any comparison, the incoming text and the reference text must pass through the same pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Strip diacritics and tatweel&lt;/li&gt;
&lt;li&gt;Convert the zero-width non-joiner and directional marks to a plain space&lt;/li&gt;
&lt;li&gt;Convert Persian and Arabic-Indic digits to ASCII&lt;/li&gt;
&lt;li&gt;Unify letters: yeh, alef maqsura and yeh hamza to Persian yeh; Arabic kaf to keheh; teh marbuta to heh; the alef forms to plain alef&lt;/li&gt;
&lt;li&gt;Lowercase any Latin characters&lt;/li&gt;
&lt;li&gt;Replace every non-letter, non-digit character with a space and collapse runs of whitespace&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In full — every character written as an escape, so it survives copy and paste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ARABIC_FORMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ي&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ی&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Arabic yeh    -&amp;gt; Persian yeh&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ى&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ی&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// alef maqsura  -&amp;gt; Persian yeh&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ئ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ی&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// yeh with hamza&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ك&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ک&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Arabic kaf    -&amp;gt; Persian keheh&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ة&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ه&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// teh marbuta   -&amp;gt; heh&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ۀ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ه&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// heh with yeh above&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;أ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ا&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// alef with hamza above&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;إ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ا&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// alef with hamza below&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;آ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ا&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// alef with madda&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ؤ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;و&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// waw with hamza -&amp;gt; waw&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. harakat, dagger alef, tatweel — decorative, never semantic&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;ً-ْٰـ&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. ZWNJ and the bidi control marks become a SPACE, not nothing&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;​-‏‪-‮⁦-⁩﻿&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// 3. Persian digits, then Arabic-Indic digits&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;۰-۹&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x06f0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;٠-٩&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x0660&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;// 4. one letter per sound&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;يىئكةۀأإآؤ&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ARABIC_FORMS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// 5 and 6. fold case, then reduce punctuation to whitespace&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\p&lt;/span&gt;&lt;span class="sr"&gt;{L}&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{N}&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/gu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should pass all three of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;چت‌بات&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;چت بات&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// true — the ZWNJ case&lt;/span&gt;
&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;شرکة&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;شرکه&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// true — teh marbuta&lt;/span&gt;
&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;۱۴۰۳&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1403&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// true — Persian digits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subtlety is in step two. The zero-width non-joiner must become a &lt;strong&gt;space&lt;/strong&gt;, not be deleted. Delete it and &lt;code&gt;چت‌بات&lt;/code&gt; becomes &lt;code&gt;چتبات&lt;/code&gt;, which still does not match &lt;code&gt;چت بات&lt;/code&gt;. Convert it to a space and both spellings arrive at the same two tokens and find each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;چت‌بات&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/‌/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// "چتبات"  — still no match&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;چت‌بات&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/‌/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// "چت بات" — matches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more thing that costs nothing and catches a whole class of bugs: run the &lt;em&gt;reference&lt;/em&gt; strings through &lt;code&gt;normalize&lt;/code&gt; too, at startup. A keyword list typed by a developer on one keyboard layout and a question typed by a user on another will otherwise never meet, no matter how good the folding is on the input side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more for a local deployment
&lt;/h2&gt;

&lt;p&gt;Customers of a shop, a training centre or a factory write from a range of devices and keyboard layouts. Some produce standard Persian characters; others produce the Arabic forms. Both families are therefore present in genuine inbound messages.&lt;/p&gt;

&lt;p&gt;A system validated only against clean, uniform text performs flawlessly in a demonstration and stumbles on contact with real users. We run this evaluation against real message traffic rather than synthetic text, because the gap between those two is the gap between a successful demo and a service that holds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What exactly is the zero-width non-joiner and why does it break things?
&lt;/h3&gt;

&lt;p&gt;It is Unicode character U+200C. It has no visual width but is stored in the string. &lt;code&gt;می‌رود&lt;/code&gt; and &lt;code&gt;می رود&lt;/code&gt; are therefore identical to the eye and different to any string comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this only affect chatbots?
&lt;/h3&gt;

&lt;p&gt;No. Anywhere Persian text is compared is affected: product search, customer-name lookup, address matching, deduplication. In a database, two spellings of one company name remain two separate records.&lt;/p&gt;

&lt;h3&gt;
  
  
  What share of questions fail because of this?
&lt;/h3&gt;

&lt;p&gt;There is no fixed figure; it depends on the vocabulary involved. In our own evaluation, before folding was introduced, three out of ten perfectly ordinary Persian questions fell through to the default reply. Afterwards, all ten reached the correct topic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do large language models avoid the problem?
&lt;/h3&gt;

&lt;p&gt;They are more tolerant of spelling variation, but they do not remove it. Wherever string matching happens before or after the model — document retrieval, filtering, routing to a department — the same trap returns. I compare the two architectures in &lt;a href="https://qatrehai.ir/blog/persian-chatbot-llm-or-retrieval-en" rel="noopener noreferrer"&gt;language model or retrieval from your own knowledge&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How expensive is the folding step?
&lt;/h3&gt;

&lt;p&gt;Negligible. It is six replacements over a short string. In our measurements the entire server-side processing time, including retrieving the answer, was 10 to 14 milliseconds. The cost of not doing it is far higher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't this what &lt;code&gt;String.prototype.normalize()&lt;/code&gt; is for?
&lt;/h3&gt;

&lt;p&gt;No — and this is a common misread. Unicode NFC and NFKC compose and decompose characters, but Persian yeh and Arabic yeh are separate letters with separate meanings, not two encodings of one character. Unicode will not merge them for you, and it should not. Script folding is an application-level decision, and only you know how aggressive it should be.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written while building a Persian-language assistant at &lt;a href="https://qatrehai.ir/chatbot-karaj?lang=en" rel="noopener noreferrer"&gt;Qatreh&lt;/a&gt; in Karaj, Iran. If you have hit a variant this pipeline misses, I would like to hear about it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nlp</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
