@cachly-dev/mcp-server 0.5.49 → 0.5.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
47
47
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
48
48
  const JWT = process.env.CACHLY_JWT ?? '';
49
49
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
50
- const CURRENT_VERSION = '0.5.49';
50
+ const CURRENT_VERSION = '0.5.60';
51
51
  /**
52
52
  * ┌──────────────────────────────────────────────────────────────────────┐
53
53
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -403,6 +403,40 @@ const STOPWORDS = new Set([
403
403
  'רק', 'עוד', 'יש', 'אין', 'מה', 'זה', 'זאת', 'אלה', 'כל', 'כן',
404
404
  'לו', 'לה', 'להם', 'בו', 'בה', 'בהם', 'שם', 'כך', 'כן', 'מי',
405
405
  'אשר', 'אחרי', 'לפני', 'תחת', 'בין', 'מאז', 'עד', 'כמו', 'אז',
406
+ // ── Farsi/Persian — common function words, prepositions, pronouns ──
407
+ 'در', 'از', 'به', 'با', 'که', 'این', 'آن', 'را', 'است',
408
+ 'بود', 'باشد', 'شد', 'شده', 'می', 'نه', 'نیست', 'هم', 'هر',
409
+ 'یک', 'اما', 'و', 'یا', 'تا', 'اگر', 'برای', 'چون', 'چه',
410
+ 'کجا', 'کی', 'چطور', 'وقتی', 'بعد', 'قبل', 'مثل', 'همه', 'بعضی',
411
+ 'هیچ', 'آیا', 'خود', 'چند', 'دیگر', 'هنوز', 'همان', 'آنها', 'اینها',
412
+ 'ما', 'شما', 'آنان', 'من', 'تو', 'او', 'آنان', 'ایشان',
413
+ 'روی', 'زیر', 'بین', 'پیش', 'پس', 'طرف', 'داخل', 'خارج', 'کنار',
414
+ 'همچنین', 'مگر', 'ولی', 'وگرنه', 'چرا', 'چگونه', 'کدام',
415
+ // ── Hindi — common particles, postpositions, pronouns, auxiliary verbs ──
416
+ 'है', 'में', 'से', 'को', 'का', 'की', 'के', 'पर', 'और', 'या',
417
+ 'नहीं', 'यह', 'वह', 'एक', 'इस', 'उस', 'भी', 'हो', 'था', 'थी',
418
+ 'थे', 'हैं', 'हूँ', 'लिए', 'तक', 'साथ', 'बाद', 'पहले', 'जो',
419
+ 'जब', 'कैसे', 'क्यों', 'क्या', 'कहाँ', 'कौन', 'हम', 'आप', 'वे',
420
+ 'मैं', 'तुम', 'उन', 'इन', 'ने', 'बहुत', 'सब', 'कुछ', 'फिर',
421
+ 'अब', 'तो', 'ही', 'तरह', 'जैसे', 'करना', 'होना', 'रहा', 'रही',
422
+ 'रहे', 'गया', 'गई', 'गए', 'किया', 'किए', 'कर', 'हुआ', 'हुई',
423
+ // ── Russian — common prepositions, pronouns, auxiliary verbs ───────────────
424
+ 'в', 'на', 'не', 'с', 'и', 'а', 'но', 'по', 'за', 'из', 'от', 'до',
425
+ 'к', 'у', 'о', 'об', 'во', 'при', 'под', 'над', 'без', 'для',
426
+ 'что', 'как', 'это', 'все', 'так', 'уже', 'или', 'же', 'ли',
427
+ 'если', 'то', 'да', 'нет', 'был', 'была', 'были', 'быть', 'есть',
428
+ 'его', 'её', 'их', 'этот', 'эта', 'эти', 'который', 'которая',
429
+ 'которые', 'который', 'мой', 'моя', 'мои', 'ваш', 'ваша', 'ваши',
430
+ 'он', 'она', 'они', 'мы', 'вы', 'я', 'ты', 'тот', 'та', 'те',
431
+ 'очень', 'тоже', 'также', 'когда', 'где', 'как', 'почему', 'зачем',
432
+ 'сейчас', 'здесь', 'там', 'потому', 'поэтому', 'чтобы', 'тут',
433
+ // ── Turkish — common particles, postpositions, copulas ─────────────────────
434
+ 'bir', 'bu', 'şu', 'o', 've', 'ile', 'de', 'da', 'için', 'gibi',
435
+ 'ama', 'fakat', 'ancak', 'ya', 'ne', 'ki', 'mi', 'mı', 'mu', 'mü',
436
+ 'var', 'yok', 'olan', 'oldu', 'olur', 'olarak', 'ise', 'daha',
437
+ 'çok', 'en', 'her', 'hiç', 'bazı', 'tüm', 'bütün', 'hem', 'veya',
438
+ 'bu', 'şu', 'ben', 'sen', 'biz', 'siz', 'onlar', 'benim', 'senin',
439
+ 'nasıl', 'neden', 'niçin', 'nerede', 'ne', 'hangi', 'kaç',
406
440
  ]);
407
441
  // CJK Unicode ranges used for bigram extraction
408
442
  const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
@@ -412,7 +446,14 @@ const RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufb
412
446
  // Distinguish Arabic from Hebrew within RTL segments
413
447
  const HEBREW_CHAR_RE = /[\u0590-\u05ff\ufb1d-\ufb4f]/;
414
448
  const ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/;
415
- ;
449
+ // Persian/Farsi: exclusive chars not in Arabic (U+067E=پ, U+0686=چ, U+0698=ژ, U+06AF=گ, U+06CC=ی)
450
+ const FARSI_CHAR_RE = /[\u067e\u0686\u0698\u06af\u06cc]/;
451
+ // Devanagari (Hindi, Sanskrit, Marathi …) — U+0900–U+097F + extended
452
+ const DEVANAGARI_RE = /[\u0900-\u097f]/;
453
+ // Cyrillic (Russian, Bulgarian, Ukrainian, Serbian …) — U+0400–U+04FF
454
+ const CYRILLIC_RE = /[\u0400-\u04ff]/;
455
+ // Turkish uses Latin alphabet but has unique chars (ğ, ş, ı, ö, ü, ç) — detected by these
456
+ const TURKISH_CHAR_RE = /[\u011f\u015f\u0131\u0130\u00e7]/; // ğ ş ı İ ç
416
457
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
417
458
  // Digraphs must be listed before single chars so they match first.
418
459
  const KATA_ROMAJI_TABLE = [
@@ -540,7 +581,205 @@ function hebrewLightStem(word) {
540
581
  }
541
582
  return result;
542
583
  }
543
- // ── Cross-lingual tech term dictionary ────────────────────────────────────────
584
+ /**
585
+ * Farsi/Persian light stemmer — handles Persian morphology.
586
+ * Strips:
587
+ * \u0647\u0627 / \u0647\u0627\u06cc (ha/haye) — plural suffixes: \u0633\u0631\u0648\u0631\u0647\u0627 \u2192 \u0633\u0631\u0648\u0631
588
+ * \u0645\u06cc\u200c / \u0645\u06cc (mi-) — present-tense prefix: \u0645\u06cc\u200c\u06a9\u0646\u062f \u2192 \u06a9\u0646\u062f
589
+ * \u0646\u0645\u06cc\u200c (nami-) — negated present: \u0646\u0645\u06cc\u200c\u0634\u0648\u062f \u2192 \u0634\u0648\u062f
590
+ * Only strips when the result is still \u2265 3 chars.
591
+ */
592
+ function farsiLightStem(word) {
593
+ let result = word;
594
+ // Suffixes first (longest first)
595
+ if (result.endsWith('\u0647\u0627\u06cc') && result.length > 5)
596
+ result = result.slice(0, -3);
597
+ else if (result.endsWith('\u0647\u0627') && result.length > 4)
598
+ result = result.slice(0, -2);
599
+ // Prefixes
600
+ if (result.startsWith('\u0646\u0645\u06cc\u200c') && result.length > 5)
601
+ result = result.slice(4);
602
+ else if (result.startsWith('\u0645\u06cc\u200c') && result.length > 4)
603
+ result = result.slice(3);
604
+ else if (result.startsWith('\u0645\u06cc') && result.length > 4)
605
+ result = result.slice(2);
606
+ return result;
607
+ }
608
+ /**
609
+ * Hindi light stemmer — strips the most common inflectional suffixes.
610
+ * Handles basic verb forms and oblique plural markers.
611
+ * Avoids over-stemming loanwords (most tech terms in Hindi are English loanwords).
612
+ * Only strips when result is still \u2265 3 chars.
613
+ */
614
+ function hindiLightStem(word) {
615
+ const result = word;
616
+ // Infinitive / verb suffixes (longest first to avoid partial match)
617
+ if (result.endsWith('\u0928\u093e') && result.length > 4)
618
+ return result.slice(0, -2); // \u0928\u093e (nā) infinitive
619
+ if (result.endsWith('\u0928\u0947') && result.length > 4)
620
+ return result.slice(0, -2); // \u0928\u0947 (ne) ergative
621
+ if (result.endsWith('\u0928\u0940') && result.length > 4)
622
+ return result.slice(0, -2); // \u0928\u0940 (nī) fem infinitive
623
+ if (result.endsWith('\u0924\u093e') && result.length > 4)
624
+ return result.slice(0, -2); // \u0924\u093e (tā) m present participle
625
+ if (result.endsWith('\u0924\u0940') && result.length > 4)
626
+ return result.slice(0, -2); // \u0924\u0940 (tī) f present participle
627
+ if (result.endsWith('\u0915\u0930') && result.length > 4)
628
+ return result.slice(0, -2); // \u0915\u0930 (kar) conjunctive
629
+ if (result.endsWith('\u0913\u0902') && result.length > 4)
630
+ return result.slice(0, -2); // \u0913\u0902 (oṃ) oblique plural
631
+ if (result.endsWith('\u0907\u092f\u093e\u0902') && result.length > 5)
632
+ return result.slice(0, -4); // \u0907\u092f\u093e\u0902 (iyāṃ) f plural
633
+ if (result.endsWith('\u0940\u092f\u093e\u0902') && result.length > 5)
634
+ return result.slice(0, -4); // \u0940\u092f\u093e\u0102 (īyāṃ)
635
+ return result;
636
+ }
637
+ /**
638
+ * Russian light stemmer — strips common inflectional suffixes.
639
+ * Handles the most frequent verb/noun/adjective endings to improve recall.
640
+ * Only strips when result is still ≥ 3 chars.
641
+ *
642
+ * Covers:
643
+ * ошибки/ошибка → ошибк (noun plural/genitive)
644
+ * развёртывание → развёртыва (gerund → stem)
645
+ * установить → установ (infinitive)
646
+ * настройки → настройк (genitive plural)
647
+ */
648
+ function russianLightStem(word) {
649
+ let w = word;
650
+ // Longest first to avoid partial stripping
651
+ // Verb infinitives / gerunds
652
+ if (w.endsWith('ывание') && w.length > 7)
653
+ return w.slice(0, -6);
654
+ if (w.endsWith('ивание') && w.length > 7)
655
+ return w.slice(0, -6);
656
+ if (w.endsWith('ование') && w.length > 7)
657
+ return w.slice(0, -6);
658
+ if (w.endsWith('вание') && w.length > 6)
659
+ return w.slice(0, -5);
660
+ if (w.endsWith('ение') && w.length > 5)
661
+ return w.slice(0, -4);
662
+ if (w.endsWith('ание') && w.length > 5)
663
+ return w.slice(0, -4);
664
+ if (w.endsWith('ить') && w.length > 4)
665
+ return w.slice(0, -3);
666
+ if (w.endsWith('ать') && w.length > 4)
667
+ return w.slice(0, -3);
668
+ if (w.endsWith('еть') && w.length > 4)
669
+ return w.slice(0, -3);
670
+ if (w.endsWith('уть') && w.length > 4)
671
+ return w.slice(0, -3);
672
+ // Noun plural/genitive
673
+ if (w.endsWith('ки') && w.length > 4)
674
+ return w.slice(0, -2);
675
+ if (w.endsWith('ги') && w.length > 4)
676
+ return w.slice(0, -2);
677
+ if (w.endsWith('ов') && w.length > 4)
678
+ return w.slice(0, -2);
679
+ if (w.endsWith('ев') && w.length > 4)
680
+ return w.slice(0, -2);
681
+ if (w.endsWith('ей') && w.length > 4)
682
+ return w.slice(0, -2);
683
+ if (w.endsWith('ий') && w.length > 4)
684
+ return w.slice(0, -2);
685
+ if (w.endsWith('ый') && w.length > 4)
686
+ return w.slice(0, -2);
687
+ if (w.endsWith('ая') && w.length > 4)
688
+ return w.slice(0, -2);
689
+ if (w.endsWith('ое') && w.length > 4)
690
+ return w.slice(0, -2);
691
+ if (w.endsWith('ую') && w.length > 4)
692
+ return w.slice(0, -2);
693
+ // Verb present tense
694
+ if (w.endsWith('ет') && w.length > 4)
695
+ return w.slice(0, -2);
696
+ if (w.endsWith('ют') && w.length > 4)
697
+ return w.slice(0, -2);
698
+ if (w.endsWith('ут') && w.length > 4)
699
+ return w.slice(0, -2);
700
+ if (w.endsWith('ят') && w.length > 4)
701
+ return w.slice(0, -2);
702
+ return w;
703
+ }
704
+ /**
705
+ * Turkish light stemmer — strips common agglutinative suffixes.
706
+ * Turkish is highly agglutinative; this covers the most common inflectional ends.
707
+ *
708
+ * Covers:
709
+ * hatalar → hata (-lar/-ler plural)
710
+ * sunucuda → sunucu (-da/-de locative)
711
+ * dağıtımı → dağıtım (-ı/-i/-u/-ü accusative)
712
+ * yüklemek → yükle (-mek/-mak infinitive)
713
+ */
714
+ function turkishLightStem(word) {
715
+ let w = word;
716
+ // Suffixes longest-first
717
+ if (w.endsWith('lardaki') && w.length > 7)
718
+ return w.slice(0, -7);
719
+ if (w.endsWith('lerdeki') && w.length > 7)
720
+ return w.slice(0, -7);
721
+ if (w.endsWith('ların') && w.length > 6)
722
+ return w.slice(0, -5);
723
+ if (w.endsWith('lerin') && w.length > 6)
724
+ return w.slice(0, -5);
725
+ if (w.endsWith('larda') && w.length > 6)
726
+ return w.slice(0, -5);
727
+ if (w.endsWith('lerde') && w.length > 6)
728
+ return w.slice(0, -5);
729
+ if (w.endsWith('ları') && w.length > 5)
730
+ return w.slice(0, -4);
731
+ if (w.endsWith('leri') && w.length > 5)
732
+ return w.slice(0, -4);
733
+ if (w.endsWith('mek') && w.length > 4)
734
+ return w.slice(0, -3);
735
+ if (w.endsWith('mak') && w.length > 4)
736
+ return w.slice(0, -3);
737
+ if (w.endsWith('ler') && w.length > 4)
738
+ return w.slice(0, -3);
739
+ if (w.endsWith('lar') && w.length > 4)
740
+ return w.slice(0, -3);
741
+ if (w.endsWith('nın') && w.length > 4)
742
+ return w.slice(0, -3);
743
+ if (w.endsWith('nin') && w.length > 4)
744
+ return w.slice(0, -3);
745
+ if (w.endsWith('nun') && w.length > 4)
746
+ return w.slice(0, -3);
747
+ if (w.endsWith('nün') && w.length > 4)
748
+ return w.slice(0, -3);
749
+ if (w.endsWith('da') && w.length > 4)
750
+ return w.slice(0, -2);
751
+ if (w.endsWith('de') && w.length > 4)
752
+ return w.slice(0, -2);
753
+ if (w.endsWith('ta') && w.length > 4)
754
+ return w.slice(0, -2);
755
+ if (w.endsWith('te') && w.length > 4)
756
+ return w.slice(0, -2);
757
+ if (w.endsWith('dan') && w.length > 4)
758
+ return w.slice(0, -3);
759
+ if (w.endsWith('den') && w.length > 4)
760
+ return w.slice(0, -3);
761
+ if (w.endsWith('tan') && w.length > 4)
762
+ return w.slice(0, -3);
763
+ if (w.endsWith('ten') && w.length > 4)
764
+ return w.slice(0, -3);
765
+ if (w.endsWith('ın') && w.length > 4)
766
+ return w.slice(0, -2);
767
+ if (w.endsWith('in') && w.length > 4)
768
+ return w.slice(0, -2);
769
+ if (w.endsWith('un') && w.length > 4)
770
+ return w.slice(0, -2);
771
+ if (w.endsWith('ün') && w.length > 4)
772
+ return w.slice(0, -2);
773
+ if (w.endsWith('ı') && w.length > 4)
774
+ return w.slice(0, -1);
775
+ if (w.endsWith('i') && w.length > 4)
776
+ return w.slice(0, -1);
777
+ if (w.endsWith('u') && w.length > 4)
778
+ return w.slice(0, -1);
779
+ if (w.endsWith('ü') && w.length > 4)
780
+ return w.slice(0, -1);
781
+ return w;
782
+ }
544
783
  // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
545
784
  // to their equivalents in other languages. Used to expand tokens at search time
546
785
  // so "deploy" finds documents containing "デプロイ" / "部署" / "배포", and vice versa.
@@ -548,46 +787,46 @@ function hebrewLightStem(word) {
548
787
  // Keys must be lowercase. CJK values will be bigram-expanded at use time.
549
788
  const CROSS_LINGUAL_MAP = new Map([
550
789
  // English → all
551
- ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação']],
552
- ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação']],
790
+ ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação', 'развёртывание', 'развертывание', 'dağıtım', 'dağıtmak']],
791
+ ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação', 'развёртывание', 'dağıtım']],
553
792
  ['container', ['コンテナ', '容器', '컨테이너', 'docker']],
554
- ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת']],
555
- ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'base de données', 'base de datos', 'banco de dados']],
556
- ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון']],
557
- ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro']],
558
- ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل']],
559
- ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir']],
560
- ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'bauen', 'construire', 'construir', 'costruire']],
561
- ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'testen', 'tester', 'probar', 'testare', 'testar']],
562
- ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação']],
563
- ['authentication', ['auth', 'مصادقة', 'אימות', '認証', '认证', '인증', 'login', 'oauth']],
564
- ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente']],
565
- ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave']],
566
- ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', 'بعيا', '問題', '問題', '오류', 'fehler', 'problème', 'problema']],
793
+ ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת', 'سرور', 'सर्वर', 'сервер', 'sunucu']],
794
+ ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'پایگاه داده', 'डेटाबेس', 'base de données', 'base de datos', 'banco de dados']],
795
+ ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון', 'کش', 'कैश', 'кэш', 'önbellek']],
796
+ ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'خطا', 'त्रुटि', 'गलती', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro', 'ошибка', 'hata', 'hatalı']],
797
+ ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل', 'اشکال', 'बग']],
798
+ ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'رفع', 'सुधार', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir', 'исправить', 'исправление', 'düzeltmek', 'düzeltme']],
799
+ ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'ساخت', 'निर्माण', 'bauen', 'construire', 'construir', 'costruire', 'сборка', 'собрать', 'derleme', 'derlemek']],
800
+ ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'آزمایش', 'परीक्षण', 'testen', 'tester', 'probar', 'testare', 'testar', 'тест', 'тестирование', 'test', 'testlemek']],
801
+ ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação', 'аутентификация', 'авторизация', 'kimlik doğrulama', 'yetkilendirme']],
802
+ ['authentication', ['auth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', '認証', '认证', '인증', 'login', 'oauth', 'аутентификация', 'авторизация', 'kimlik doğrulama']],
803
+ ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente', 'среда', 'окружение', 'ortam']],
804
+ ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave', 'секрет', 'ключ', 'gizli', 'anahtar']],
805
+ ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', '問題', '오류', 'fehler', 'problème', 'problema', 'проблема', 'ошибка', 'sorun', 'problem']],
567
806
  ['kubernetes', ['クベルネテス', 'k8s', 'kube']],
568
- ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'netzwerk', 'réseau', 'red', 'rete', 'rede']],
807
+ ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'شبکه', 'नेटवर्क', 'netzwerk', 'réseau', 'red', 'rete', 'rede', 'сеть', 'ağ']],
569
808
  ['timeout', ['タイムアウト', '超时', '타임아웃', 'مهلة']],
570
- ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'speicher', 'mémoire', 'memoria', 'memória']],
571
- ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração']],
572
- ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'installieren', 'installer', 'instalar', 'installare']],
573
- ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar']],
574
- ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'protokoll', 'protokolle', 'journal', 'registro']],
809
+ ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'حافظه', 'मेमोरी', 'speicher', 'mémoire', 'memoria', 'memória', 'память', 'bellek', 'hafıza']],
810
+ ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'پیکربندی', 'कॉन्फ़िग', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração', 'конфигурация', 'настройки', 'yapılandırma', 'ayarlar']],
811
+ ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'نصب', 'इंस्टॉल', 'installieren', 'installer', 'instalar', 'installare', 'установить', 'установка', 'yüklemek', 'kurulum']],
812
+ ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'به‌روزرسانی', 'अपडेट', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar', 'обновление', 'обновить', 'güncelleme']],
813
+ ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'ثبت', 'लॉग', 'protokoll', 'protokolle', 'journal', 'registro', 'журнал', 'лог', 'günlük']],
575
814
  ['port', ['ポート', '端口', '포트', 'منفذ', 'פורט']],
576
815
  ['file', ['ファイル', '文件', '파일', 'ملف', 'קובץ']],
577
816
  ['image', ['イメージ', '镜像', '이미지', 'صورة', 'תמונה', 'abbild', 'imagen', 'imagem']],
578
817
  ['volume', ['ボリューム', '卷', '볼륨']],
579
818
  ['cluster', ['クラスター', '集群', '클러스터']],
580
819
  ['node', ['ノード', '节点', '노드']],
581
- ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות', 'dienst', 'servicio', 'servizio', 'serviço']],
820
+ ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות', 'dienst', 'servicio', 'servizio', 'serviço', 'сервис', 'служба', 'servis', 'hizmet']],
582
821
  ['certificate', ['証明書', '证书', '인증서', 'cert', 'ssl', 'tls', 'شهادة', 'תעודה']],
583
822
  ['password', ['パスワード', '密码', '비밀번호', 'passwd', 'pwd', 'كلمة المرور', 'סיסמה']],
584
823
  ['token', ['トークン', '令牌', '토큰', 'jwt', 'secret', 'رمز', 'אסימון']],
585
824
  ['health', ['ヘルス', '健康', '헬스', 'healthcheck', 'probe', 'صحة']],
586
825
  ['migration', ['マイグレーション', '迁移', '마이그레이션', 'migrate', 'هجرة']],
587
- ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança']],
588
- ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar']],
589
- ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho']],
590
- ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
826
+ ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'پشتیبان‌گیری', 'बैकअप', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança', 'резервная копия', 'резерв', 'yedekleme']],
827
+ ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'نظارت', 'निगरानी', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar', 'мониторинг', 'наблюдение', 'izleme']],
828
+ ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'عملکرد', 'प्रदर्शन', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho', 'производительность', 'быстродействие', 'performans']],
829
+ ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'اتصال', 'कनेक्शन', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
591
830
  ['queue', ['キュー', '队列', '큐', 'طابور', 'warteschlange', 'file d\'attente', 'cola', 'coda', 'fila']],
592
831
  ['redis', ['レディス', '레디스']],
593
832
  ['nginx', ['エンジンエックス']],
@@ -601,10 +840,10 @@ const CROSS_LINGUAL_MAP = new Map([
601
840
  ['git', ['version control', 'repo', 'repository', 'commit', 'push', 'pull']],
602
841
  ['ci', ['pipeline', 'github actions', 'gitlab', 'jenkins', 'build']],
603
842
  ['cd', ['deploy', 'deployment', 'release']],
604
- ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'debuggen', 'déboguer', 'depurar']],
605
- ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'absturz', 'panne', 'plantage', 'caída', 'falha']],
606
- ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
607
- ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão']],
843
+ ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'اشکال‌زدایی', 'डीबग', 'debuggen', 'déboguer', 'depurar', 'отладка', 'дебаг', 'hata ayıklama']],
844
+ ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'خرابی', 'क्रैश', 'absturz', 'panne', 'plantage', 'caída', 'falha', 'сбой', 'крэш', 'çökme', 'çöküş']],
845
+ ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'راه‌اندازی مجدد', 'पुنरारंभ', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
846
+ ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'مجوز', 'अनुमति', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão', 'разрешение', 'доступ', 'izin', 'yetki']],
608
847
  // Japanese katakana → English
609
848
  ['デプロイ', ['deploy', 'deployment']],
610
849
  ['コンテナ', ['container', 'docker']],
@@ -790,6 +1029,67 @@ const CROSS_LINGUAL_MAP = new Map([
790
1029
  ['ייצור', ['production', 'prod']],
791
1030
  ['סודי', ['secret', 'key', 'token']],
792
1031
  ['מפתח סודי', ['secret', 'key']],
1032
+ // ── Farsi/Persian → English ───────────────────────────────────────────────
1033
+ ['استقرار', ['deploy', 'deployment']],
1034
+ ['خطا', ['error', 'err']], // Farsi: خطا vs Arabic: خطأ
1035
+ ['اشکال', ['bug', 'issue', 'error']],
1036
+ ['رفع اشکال', ['fix', 'debug', 'debugging']],
1037
+ ['ساخت', ['build']],
1038
+ ['آزمایش', ['test']],
1039
+ ['احراز هویت', ['auth', 'authentication']],
1040
+ ['پیکربندی', ['config', 'configuration']],
1041
+ ['نصب', ['install']],
1042
+ ['به‌روزرسانی', ['update', 'upgrade']],
1043
+ ['ثبت', ['log']],
1044
+ ['سرور', ['server']],
1045
+ ['پایگاه داده', ['database', 'db']],
1046
+ ['شبکه', ['network']], // Farsi شبکه vs Arabic شبكة
1047
+ ['حافظه', ['memory', 'ram']], // Farsi حافظه vs Arabic ذاكرة
1048
+ ['کش', ['cache']],
1049
+ ['اشکال‌زدایی', ['debug', 'debugging']],
1050
+ ['خرابی', ['crash', 'failure', 'error']],
1051
+ ['راه‌اندازی مجدد', ['restart', 'reboot']],
1052
+ ['مجوز', ['permission', 'access']],
1053
+ ['پشتیبان‌گیری', ['backup']],
1054
+ ['عملکرد', ['performance']],
1055
+ ['مشکل', ['problem', 'issue', 'error']],
1056
+ ['سرویس', ['service']],
1057
+ ['کلید', ['key', 'token', 'secret']],
1058
+ ['محیط', ['environment', 'env']],
1059
+ ['تولید', ['production', 'prod']],
1060
+ ['رمز', ['secret', 'key', 'token']],
1061
+ ['اتصال', ['connection', 'conn']],
1062
+ // ── Hindi (Devanagari) → English ──────────────────────────────────────────
1063
+ ['तैनाती', ['deploy', 'deployment']],
1064
+ ['त्रुटि', ['error', 'err']],
1065
+ ['गलती', ['error', 'bug', 'issue']],
1066
+ ['सुधार', ['fix', 'bugfix']],
1067
+ ['निर्माण', ['build']],
1068
+ ['परीक्षण', ['test']],
1069
+ ['प्रमाणीकरण', ['auth', 'authentication']],
1070
+ ['विन्यास', ['config', 'configuration']],
1071
+ ['कॉन्फ़िगरेशन', ['config', 'configuration']],
1072
+ ['इंस्टॉल', ['install']],
1073
+ ['अपडेट', ['update', 'upgrade']],
1074
+ ['लॉग', ['log']],
1075
+ ['सर्वर', ['server']],
1076
+ ['डेटाबेस', ['database', 'db']],
1077
+ ['नेटवर्क', ['network']],
1078
+ ['मेमोरी', ['memory', 'ram']],
1079
+ ['कैश', ['cache']],
1080
+ ['डीबग', ['debug', 'debugging']],
1081
+ ['क्रैश', ['crash', 'panic']],
1082
+ ['पुनरारंभ', ['restart', 'reboot']],
1083
+ ['अनुमति', ['permission', 'access']],
1084
+ ['बैकअप', ['backup']],
1085
+ ['प्रदर्शन', ['performance']],
1086
+ ['समस्या', ['problem', 'issue', 'error']],
1087
+ ['सेवा', ['service']],
1088
+ ['कनेक्शन', ['connection', 'conn']],
1089
+ ['पासवर्ड', ['password']],
1090
+ ['टोकन', ['token', 'secret']],
1091
+ ['वातावरण', ['environment', 'env']],
1092
+ ['उत्पादन', ['production', 'prod']],
793
1093
  // ── European languages → English ─────────────────────────────────────────
794
1094
  // German (Deutsch) → English
795
1095
  ['fehler', ['error', 'err']],
@@ -971,6 +1271,103 @@ const CROSS_LINGUAL_MAP = new Map([
971
1271
  ['ambiente', ['environment', 'env']],
972
1272
  ['fila', ['queue']],
973
1273
  ['imagem', ['image']],
1274
+ // ── Russian (Cyrillic) → English ──────────────────────────────────────────
1275
+ ['ошибка', ['error', 'err']],
1276
+ ['ошибки', ['error', 'err']],
1277
+ ['сбой', ['crash', 'failure', 'error']],
1278
+ ['крэш', ['crash', 'panic']],
1279
+ ['баг', ['bug', 'issue']],
1280
+ ['исправить', ['fix', 'bugfix']],
1281
+ ['исправление', ['fix', 'patch']],
1282
+ ['сборка', ['build']],
1283
+ ['собрать', ['build']],
1284
+ ['тест', ['test']],
1285
+ ['тестирование', ['test', 'testing']],
1286
+ ['аутентификация', ['auth', 'authentication']],
1287
+ ['авторизация', ['auth', 'authorization']],
1288
+ ['конфигурация', ['config', 'configuration']],
1289
+ ['настройки', ['config', 'settings']],
1290
+ ['установить', ['install']],
1291
+ ['установка', ['install']],
1292
+ ['обновление', ['update', 'upgrade']],
1293
+ ['обновить', ['update', 'upgrade']],
1294
+ ['журнал', ['log']],
1295
+ ['лог', ['log', 'logs']],
1296
+ ['сервер', ['server']],
1297
+ ['база данных', ['database', 'db']],
1298
+ ['сеть', ['network']],
1299
+ ['память', ['memory', 'ram']],
1300
+ ['кэш', ['cache']],
1301
+ ['отладка', ['debug', 'debugging']],
1302
+ ['перезапуск', ['restart', 'reboot']],
1303
+ ['перезагрузка', ['restart', 'reboot']],
1304
+ ['разрешение', ['permission', 'access']],
1305
+ ['доступ', ['permission', 'access']],
1306
+ ['резерв', ['backup']],
1307
+ ['производительность', ['performance']],
1308
+ ['проблема', ['problem', 'issue', 'error']],
1309
+ ['сервис', ['service']],
1310
+ ['служба', ['service']],
1311
+ ['подключение', ['connection', 'conn']],
1312
+ ['соединение', ['connection', 'conn']],
1313
+ ['развёртывание', ['deploy', 'deployment']],
1314
+ ['развертывание', ['deploy', 'deployment']],
1315
+ ['контейнер', ['container', 'docker']],
1316
+ ['образ', ['image']],
1317
+ ['кластер', ['cluster']],
1318
+ ['узел', ['node']],
1319
+ ['секрет', ['secret', 'key', 'token']],
1320
+ ['ключ', ['key', 'token', 'secret']],
1321
+ ['пароль', ['password']],
1322
+ ['токен', ['token', 'secret']],
1323
+ ['мониторинг', ['monitor', 'monitoring']],
1324
+ ['наблюдение', ['monitor', 'monitoring']],
1325
+ ['мигрировать', ['migrate', 'migration']],
1326
+ ['миграция', ['migrate', 'migration']],
1327
+ // ── Turkish (Latin + special chars ğ/ş/ı) → English ─────────────────────
1328
+ ['hata', ['error', 'err']],
1329
+ ['hatalı', ['error', 'err']],
1330
+ ['çökme', ['crash', 'panic']],
1331
+ ['çöküş', ['crash', 'failure']],
1332
+ ['düzeltmek', ['fix', 'bugfix']],
1333
+ ['düzeltme', ['fix', 'patch']],
1334
+ ['derleme', ['build']],
1335
+ ['derlemek', ['build']],
1336
+ ['test', ['test']],
1337
+ ['testlemek', ['test', 'testing']],
1338
+ ['kimlik doğrulama', ['auth', 'authentication']],
1339
+ ['yetkilendirme', ['auth', 'authorization']],
1340
+ ['yapılandırma', ['config', 'configuration']],
1341
+ ['ayarlar', ['config', 'settings']],
1342
+ ['yüklemek', ['install']],
1343
+ ['kurulum', ['install']],
1344
+ ['güncelleme', ['update', 'upgrade']],
1345
+ ['günlük', ['log']],
1346
+ ['sunucu', ['server']],
1347
+ ['veritabanı', ['database', 'db']],
1348
+ ['ağ', ['network']],
1349
+ ['bellek', ['memory', 'ram']],
1350
+ ['önbellek', ['cache']],
1351
+ ['hata ayıklama', ['debug', 'debugging']],
1352
+ ['yeniden başlatma', ['restart', 'reboot']],
1353
+ ['izin', ['permission', 'access']],
1354
+ ['yetki', ['permission', 'access', 'acl']],
1355
+ ['yedekleme', ['backup']],
1356
+ ['performans', ['performance']],
1357
+ ['sorun', ['problem', 'issue', 'error']],
1358
+ ['servis', ['service']],
1359
+ ['hizmet', ['service']],
1360
+ ['bağlantı', ['connection', 'conn']],
1361
+ ['dağıtım', ['deploy', 'deployment']],
1362
+ ['dağıtmak', ['deploy']],
1363
+ ['konteyner', ['container', 'docker']],
1364
+ ['küme', ['cluster']],
1365
+ ['düğüm', ['node']],
1366
+ ['gizli', ['secret', 'key']],
1367
+ ['anahtar', ['key', 'token', 'secret']],
1368
+ ['şifre', ['password']],
1369
+ ['izleme', ['monitor', 'monitoring']],
1370
+ ['göç', ['migrate', 'migration']],
974
1371
  ]);
975
1372
  /**
976
1373
  * Expand a token to its cross-lingual synonyms.
@@ -987,9 +1384,11 @@ function expandCrossLingual(token) {
987
1384
  * Features:
988
1385
  * • CJK → character bigrams
989
1386
  * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
990
- * • Arabic → word tokenization + iterative prefix stemming (al-, wa-, bi-, li-, fa-)
991
- * • Hebrew → word tokenization + iterative prefix stemming (ha-, ve-, be-, le-, mi-, ke-)
992
- * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE↔DE↔FR↔ES↔IT↔PT)
1387
+ * • Arabic → word tokenization + iterative prefix stemming (ال/و/ب/ل/ف/ك)
1388
+ * • Hebrew → word tokenization + iterative prefix stemming (ה/ו/ב/ל/מ/כ)
1389
+ * • Farsi → word tokenization + suffix/prefix stemming (ها/های plural, می prefix)
1390
+ * • Hindi → Devanagari word tokenization + light suffix stemming
1391
+ * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE↔FA↔HI↔DE↔FR↔ES↔IT↔PT)
993
1392
  */
994
1393
  function tokenize(text) {
995
1394
  const tokens = [];
@@ -1028,26 +1427,65 @@ function tokenize(text) {
1028
1427
  }
1029
1428
  }
1030
1429
  else if (RTL_RE.test(seg)) {
1031
- // RTL segment — split on spaces, apply language-specific prefix stemming
1032
- // Arabic: strips ال/و/ب/ل/ف/ك prefixes iteratively
1033
- // Hebrew: strips ה/ו/ב/ל/מ/כ prefixes iteratively
1034
- const isHebrew = HEBREW_CHAR_RE.test(seg);
1430
+ // RTL segment — 3-way branch: Farsi / Hebrew / Arabic
1035
1431
  const words = seg.trim().split(/\s+/);
1432
+ let stemFn;
1433
+ if (FARSI_CHAR_RE.test(seg))
1434
+ stemFn = farsiLightStem;
1435
+ else if (HEBREW_CHAR_RE.test(seg))
1436
+ stemFn = hebrewLightStem;
1437
+ else
1438
+ stemFn = arabicLightStem;
1036
1439
  for (const w of words) {
1037
1440
  if (w.length <= 1 || STOPWORDS.has(w))
1038
1441
  continue;
1039
- const stemmed = isHebrew ? hebrewLightStem(w) : arabicLightStem(w);
1442
+ const stemmed = stemFn(w);
1443
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1444
+ tokens.push(stemmed);
1445
+ }
1446
+ }
1447
+ else if (DEVANAGARI_RE.test(seg)) {
1448
+ // Devanagari segment (Hindi / Marathi) — naturally space-separated
1449
+ // Words don't need bigram expansion; stems reduce inflected forms.
1450
+ const words = seg.trim().split(/[\s\u200c\u200d]+/); // handle zero-width joiners
1451
+ for (const w of words) {
1452
+ // Keep Devanagari chars + digits; discard pure Latin portions (e.g. embedded numbers)
1453
+ const deva = w.replace(/[^\u0900-\u097f0-9]/g, '');
1454
+ if (deva.length <= 1 || STOPWORDS.has(deva))
1455
+ continue;
1456
+ const stemmed = hindiLightStem(deva);
1457
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1458
+ tokens.push(stemmed);
1459
+ }
1460
+ }
1461
+ else if (CYRILLIC_RE.test(seg)) {
1462
+ // Cyrillic segment (Russian, Bulgarian, Ukrainian, Serbian …)
1463
+ const words = seg.trim().split(/\s+/);
1464
+ for (const w of words) {
1465
+ const cyr = w.replace(/[^\u0400-\u04ff]/g, '');
1466
+ if (cyr.length <= 1 || STOPWORDS.has(cyr))
1467
+ continue;
1468
+ const stemmed = russianLightStem(cyr);
1040
1469
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1041
1470
  tokens.push(stemmed);
1042
1471
  }
1043
1472
  }
1044
1473
  else {
1045
- // Latin / other — existing logic preserved exactly
1474
+ // Latin / other (includes Turkish with special chars ğ/ş/ı)
1046
1475
  const words = seg
1047
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
1476
+ .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœğşıİ0-9:_\-./]/g, ' ')
1048
1477
  .split(/\s+/)
1049
1478
  .filter(w => w.length > 1 && !STOPWORDS.has(w));
1050
- tokens.push(...words);
1479
+ // Turkish: apply light stemmer to words with Turkish-specific chars
1480
+ for (const w of words) {
1481
+ if (TURKISH_CHAR_RE.test(w)) {
1482
+ const stemmed = turkishLightStem(w);
1483
+ if (stemmed !== w && stemmed.length > 1 && !STOPWORDS.has(stemmed)) {
1484
+ tokens.push(stemmed);
1485
+ }
1486
+ }
1487
+ tokens.push(w);
1488
+ }
1051
1489
  }
1052
1490
  }
1053
1491
  // ── Cross-lingual expansion ──────────────────────────────────────────────
@@ -1075,15 +1513,44 @@ function tokenize(text) {
1075
1513
  }
1076
1514
  }
1077
1515
  else if (RTL_RE.test(syn)) {
1078
- // RTL synonym
1079
- const stemmed = arabicLightStem(syn);
1516
+ // RTL synonym — apply correct stemmer per language
1517
+ const rtlStem = FARSI_CHAR_RE.test(syn) ? farsiLightStem
1518
+ : HEBREW_CHAR_RE.test(syn) ? hebrewLightStem
1519
+ : arabicLightStem;
1520
+ const stemmed = rtlStem(syn);
1521
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1522
+ expansions.push(stemmed);
1523
+ }
1524
+ else if (DEVANAGARI_RE.test(syn)) {
1525
+ // Devanagari synonym (Hindi)
1526
+ const stemmed = hindiLightStem(syn);
1527
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1528
+ expansions.push(stemmed);
1529
+ }
1530
+ else if (CYRILLIC_RE.test(syn)) {
1531
+ // Cyrillic synonym (Russian)
1532
+ const cyr = syn.replace(/[^\u0400-\u04ff]/g, '');
1533
+ const stemmed = russianLightStem(cyr);
1080
1534
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1081
1535
  expansions.push(stemmed);
1082
1536
  }
1083
1537
  else {
1084
- // Latin synonym — split in case of multi-word (e.g. "load balancer")
1085
- const parts = syn.split(/\s+/).filter(w => w.length > 1 && !STOPWORDS.has(w));
1086
- expansions.push(...parts);
1538
+ // Latin synonym (includes Turkish) — split in case of multi-word
1539
+ const parts = syn.split(/\s+/).filter(w => {
1540
+ if (w.length <= 1 || STOPWORDS.has(w))
1541
+ return false;
1542
+ return true;
1543
+ });
1544
+ for (const p of parts) {
1545
+ expansions.push(p);
1546
+ // Apply Turkish stemmer if it has Turkish-specific characters
1547
+ if (TURKISH_CHAR_RE.test(p)) {
1548
+ const stemmed = turkishLightStem(p);
1549
+ if (stemmed !== p && stemmed.length > 1 && !STOPWORDS.has(stemmed)) {
1550
+ expansions.push(stemmed);
1551
+ }
1552
+ }
1553
+ }
1087
1554
  }
1088
1555
  }
1089
1556
  }
@@ -1109,8 +1576,8 @@ function splitMultiQuery(query) {
1109
1576
  const semiParts = query.split(/[;\n]+/).filter(s => s.trim().length > 2);
1110
1577
  if (semiParts.length >= 2)
1111
1578
  return semiParts.map(s => s.trim());
1112
- // Conjunctions (EN + DE + FR + ES)
1113
- const conjParts = query.split(/\b(?:and also|also noch|außerdem|plus|additionally|furthermore|de plus|además|inoltre|além disso)\b/i).filter(s => s.trim().length > 2);
1579
+ // Conjunctions (EN + DE + FR + ES + IT + PT + RU + TR)
1580
+ const conjParts = query.split(/\b(?:and also|also noch|außerdem|plus|additionally|furthermore|de plus|además|inoltre|além disso|а также|и также|кроме того|плюс|ayrıca|bunun yanı sıra|همچنین|وأيضاً|وكذلك|وهمچنین|और भी|इसके अलावा)\b/i).filter(s => s.trim().length > 2);
1114
1581
  if (conjParts.length >= 2)
1115
1582
  return conjParts.map(s => s.trim());
1116
1583
  // Comma-separated with 3+ parts (likely a list)
@@ -1340,6 +1807,11 @@ async function keywordSearch(redis, patterns, query, topK = 10) {
1340
1807
  score *= 1 + 0.5 * (bigramHits / queryBigrams.size);
1341
1808
  }
1342
1809
  }
1810
+ // Phrase-match boost: if the raw (lowercased) query appears verbatim in content, 2× boost
1811
+ // This rewards docs where the exact phrase exists (vs scattered tokens).
1812
+ if (sq.length >= 4 && doc.content.toLowerCase().includes(sq.toLowerCase())) {
1813
+ score *= 2.0;
1814
+ }
1343
1815
  // Recency boost
1344
1816
  score *= recencyBoost(doc.timestamp);
1345
1817
  if (score > 0) {