@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/src/index.ts CHANGED
@@ -55,7 +55,7 @@ import { Redis } from 'ioredis';
55
55
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
56
56
  const JWT = process.env.CACHLY_JWT ?? '';
57
57
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
58
- const CURRENT_VERSION = '0.5.49';
58
+ const CURRENT_VERSION = '0.5.60';
59
59
  /**
60
60
  * ┌──────────────────────────────────────────────────────────────────────┐
61
61
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -417,8 +417,43 @@ const STOPWORDS = new Set([
417
417
  'רק', 'עוד', 'יש', 'אין', 'מה', 'זה', 'זאת', 'אלה', 'כל', 'כן',
418
418
  'לו', 'לה', 'להם', 'בו', 'בה', 'בהם', 'שם', 'כך', 'כן', 'מי',
419
419
  'אשר', 'אחרי', 'לפני', 'תחת', 'בין', 'מאז', 'עד', 'כמו', 'אז',
420
+ // ── Farsi/Persian — common function words, prepositions, pronouns ──
421
+ 'در', 'از', 'به', 'با', 'که', 'این', 'آن', 'را', 'است',
422
+ 'بود', 'باشد', 'شد', 'شده', 'می', 'نه', 'نیست', 'هم', 'هر',
423
+ 'یک', 'اما', 'و', 'یا', 'تا', 'اگر', 'برای', 'چون', 'چه',
424
+ 'کجا', 'کی', 'چطور', 'وقتی', 'بعد', 'قبل', 'مثل', 'همه', 'بعضی',
425
+ 'هیچ', 'آیا', 'خود', 'چند', 'دیگر', 'هنوز', 'همان', 'آنها', 'اینها',
426
+ 'ما', 'شما', 'آنان', 'من', 'تو', 'او', 'آنان', 'ایشان',
427
+ 'روی', 'زیر', 'بین', 'پیش', 'پس', 'طرف', 'داخل', 'خارج', 'کنار',
428
+ 'همچنین', 'مگر', 'ولی', 'وگرنه', 'چرا', 'چگونه', 'کدام',
429
+ // ── Hindi — common particles, postpositions, pronouns, auxiliary verbs ──
430
+ 'है', 'में', 'से', 'को', 'का', 'की', 'के', 'पर', 'और', 'या',
431
+ 'नहीं', 'यह', 'वह', 'एक', 'इस', 'उस', 'भी', 'हो', 'था', 'थी',
432
+ 'थे', 'हैं', 'हूँ', 'लिए', 'तक', 'साथ', 'बाद', 'पहले', 'जो',
433
+ 'जब', 'कैसे', 'क्यों', 'क्या', 'कहाँ', 'कौन', 'हम', 'आप', 'वे',
434
+ 'मैं', 'तुम', 'उन', 'इन', 'ने', 'बहुत', 'सब', 'कुछ', 'फिर',
435
+ 'अब', 'तो', 'ही', 'तरह', 'जैसे', 'करना', 'होना', 'रहा', 'रही',
436
+ 'रहे', 'गया', 'गई', 'गए', 'किया', 'किए', 'कर', 'हुआ', 'हुई',
437
+ // ── Russian — common prepositions, pronouns, auxiliary verbs ───────────────
438
+ 'в', 'на', 'не', 'с', 'и', 'а', 'но', 'по', 'за', 'из', 'от', 'до',
439
+ 'к', 'у', 'о', 'об', 'во', 'при', 'под', 'над', 'без', 'для',
440
+ 'что', 'как', 'это', 'все', 'так', 'уже', 'или', 'же', 'ли',
441
+ 'если', 'то', 'да', 'нет', 'был', 'была', 'были', 'быть', 'есть',
442
+ 'его', 'её', 'их', 'этот', 'эта', 'эти', 'который', 'которая',
443
+ 'которые', 'который', 'мой', 'моя', 'мои', 'ваш', 'ваша', 'ваши',
444
+ 'он', 'она', 'они', 'мы', 'вы', 'я', 'ты', 'тот', 'та', 'те',
445
+ 'очень', 'тоже', 'также', 'когда', 'где', 'как', 'почему', 'зачем',
446
+ 'сейчас', 'здесь', 'там', 'потому', 'поэтому', 'чтобы', 'тут',
447
+ // ── Turkish — common particles, postpositions, copulas ─────────────────────
448
+ 'bir', 'bu', 'şu', 'o', 've', 'ile', 'de', 'da', 'için', 'gibi',
449
+ 'ama', 'fakat', 'ancak', 'ya', 'ne', 'ki', 'mi', 'mı', 'mu', 'mü',
450
+ 'var', 'yok', 'olan', 'oldu', 'olur', 'olarak', 'ise', 'daha',
451
+ 'çok', 'en', 'her', 'hiç', 'bazı', 'tüm', 'bütün', 'hem', 'veya',
452
+ 'bu', 'şu', 'ben', 'sen', 'biz', 'siz', 'onlar', 'benim', 'senin',
453
+ 'nasıl', 'neden', 'niçin', 'nerede', 'ne', 'hangi', 'kaç',
420
454
  ]);
421
455
 
456
+
422
457
  // CJK Unicode ranges used for bigram extraction
423
458
  const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
424
459
  const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
@@ -427,7 +462,15 @@ const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00
427
462
  const RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufb4f\ufb50-\ufdff\ufe70-\ufeff]/;
428
463
  // Distinguish Arabic from Hebrew within RTL segments
429
464
  const HEBREW_CHAR_RE = /[\u0590-\u05ff\ufb1d-\ufb4f]/;
430
- const ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/;;
465
+ const ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/;
466
+ // Persian/Farsi: exclusive chars not in Arabic (U+067E=پ, U+0686=چ, U+0698=ژ, U+06AF=گ, U+06CC=ی)
467
+ const FARSI_CHAR_RE = /[\u067e\u0686\u0698\u06af\u06cc]/;
468
+ // Devanagari (Hindi, Sanskrit, Marathi …) — U+0900–U+097F + extended
469
+ const DEVANAGARI_RE = /[\u0900-\u097f]/;
470
+ // Cyrillic (Russian, Bulgarian, Ukrainian, Serbian …) — U+0400–U+04FF
471
+ const CYRILLIC_RE = /[\u0400-\u04ff]/;
472
+ // Turkish uses Latin alphabet but has unique chars (ğ, ş, ı, ö, ü, ç) — detected by these
473
+ const TURKISH_CHAR_RE = /[\u011f\u015f\u0131\u0130\u00e7]/; // ğ ş ı İ ç
431
474
 
432
475
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
433
476
  // Digraphs must be listed before single chars so they match first.
@@ -552,7 +595,140 @@ function hebrewLightStem(word: string): string {
552
595
  return result;
553
596
  }
554
597
 
555
- // ── Cross-lingual tech term dictionary ────────────────────────────────────────
598
+ /**
599
+ * Farsi/Persian light stemmer — handles Persian morphology.
600
+ * Strips:
601
+ * \u0647\u0627 / \u0647\u0627\u06cc (ha/haye) — plural suffixes: \u0633\u0631\u0648\u0631\u0647\u0627 \u2192 \u0633\u0631\u0648\u0631
602
+ * \u0645\u06cc\u200c / \u0645\u06cc (mi-) — present-tense prefix: \u0645\u06cc\u200c\u06a9\u0646\u062f \u2192 \u06a9\u0646\u062f
603
+ * \u0646\u0645\u06cc\u200c (nami-) — negated present: \u0646\u0645\u06cc\u200c\u0634\u0648\u062f \u2192 \u0634\u0648\u062f
604
+ * Only strips when the result is still \u2265 3 chars.
605
+ */
606
+ function farsiLightStem(word: string): string {
607
+ let result = word;
608
+ // Suffixes first (longest first)
609
+ if (result.endsWith('\u0647\u0627\u06cc') && result.length > 5) result = result.slice(0, -3);
610
+ else if (result.endsWith('\u0647\u0627') && result.length > 4) result = result.slice(0, -2);
611
+ // Prefixes
612
+ if (result.startsWith('\u0646\u0645\u06cc\u200c') && result.length > 5) result = result.slice(4);
613
+ else if (result.startsWith('\u0645\u06cc\u200c') && result.length > 4) result = result.slice(3);
614
+ else if (result.startsWith('\u0645\u06cc') && result.length > 4) result = result.slice(2);
615
+ return result;
616
+ }
617
+
618
+ /**
619
+ * Hindi light stemmer — strips the most common inflectional suffixes.
620
+ * Handles basic verb forms and oblique plural markers.
621
+ * Avoids over-stemming loanwords (most tech terms in Hindi are English loanwords).
622
+ * Only strips when result is still \u2265 3 chars.
623
+ */
624
+ function hindiLightStem(word: string): string {
625
+ const result = word;
626
+ // Infinitive / verb suffixes (longest first to avoid partial match)
627
+ if (result.endsWith('\u0928\u093e') && result.length > 4) return result.slice(0, -2); // \u0928\u093e (nā) infinitive
628
+ if (result.endsWith('\u0928\u0947') && result.length > 4) return result.slice(0, -2); // \u0928\u0947 (ne) ergative
629
+ if (result.endsWith('\u0928\u0940') && result.length > 4) return result.slice(0, -2); // \u0928\u0940 (nī) fem infinitive
630
+ if (result.endsWith('\u0924\u093e') && result.length > 4) return result.slice(0, -2); // \u0924\u093e (tā) m present participle
631
+ if (result.endsWith('\u0924\u0940') && result.length > 4) return result.slice(0, -2); // \u0924\u0940 (tī) f present participle
632
+ if (result.endsWith('\u0915\u0930') && result.length > 4) return result.slice(0, -2); // \u0915\u0930 (kar) conjunctive
633
+ if (result.endsWith('\u0913\u0902') && result.length > 4) return result.slice(0, -2); // \u0913\u0902 (oṃ) oblique plural
634
+ if (result.endsWith('\u0907\u092f\u093e\u0902') && result.length > 5) return result.slice(0, -4); // \u0907\u092f\u093e\u0902 (iyāṃ) f plural
635
+ if (result.endsWith('\u0940\u092f\u093e\u0902') && result.length > 5) return result.slice(0, -4); // \u0940\u092f\u093e\u0102 (īyāṃ)
636
+ return result;
637
+ }
638
+
639
+ /**
640
+ * Russian light stemmer — strips common inflectional suffixes.
641
+ * Handles the most frequent verb/noun/adjective endings to improve recall.
642
+ * Only strips when result is still ≥ 3 chars.
643
+ *
644
+ * Covers:
645
+ * ошибки/ошибка → ошибк (noun plural/genitive)
646
+ * развёртывание → развёртыва (gerund → stem)
647
+ * установить → установ (infinitive)
648
+ * настройки → настройк (genitive plural)
649
+ */
650
+ function russianLightStem(word: string): string {
651
+ let w = word;
652
+ // Longest first to avoid partial stripping
653
+ // Verb infinitives / gerunds
654
+ if (w.endsWith('ывание') && w.length > 7) return w.slice(0, -6);
655
+ if (w.endsWith('ивание') && w.length > 7) return w.slice(0, -6);
656
+ if (w.endsWith('ование') && w.length > 7) return w.slice(0, -6);
657
+ if (w.endsWith('вание') && w.length > 6) return w.slice(0, -5);
658
+ if (w.endsWith('ение') && w.length > 5) return w.slice(0, -4);
659
+ if (w.endsWith('ание') && w.length > 5) return w.slice(0, -4);
660
+ if (w.endsWith('ить') && w.length > 4) return w.slice(0, -3);
661
+ if (w.endsWith('ать') && w.length > 4) return w.slice(0, -3);
662
+ if (w.endsWith('еть') && w.length > 4) return w.slice(0, -3);
663
+ if (w.endsWith('уть') && w.length > 4) return w.slice(0, -3);
664
+ // Noun plural/genitive
665
+ if (w.endsWith('ки') && w.length > 4) return w.slice(0, -2);
666
+ if (w.endsWith('ги') && w.length > 4) return w.slice(0, -2);
667
+ if (w.endsWith('ов') && w.length > 4) return w.slice(0, -2);
668
+ if (w.endsWith('ев') && w.length > 4) return w.slice(0, -2);
669
+ if (w.endsWith('ей') && w.length > 4) return w.slice(0, -2);
670
+ if (w.endsWith('ий') && w.length > 4) return w.slice(0, -2);
671
+ if (w.endsWith('ый') && w.length > 4) return w.slice(0, -2);
672
+ if (w.endsWith('ая') && w.length > 4) return w.slice(0, -2);
673
+ if (w.endsWith('ое') && w.length > 4) return w.slice(0, -2);
674
+ if (w.endsWith('ую') && w.length > 4) return w.slice(0, -2);
675
+ // Verb present tense
676
+ if (w.endsWith('ет') && w.length > 4) return w.slice(0, -2);
677
+ if (w.endsWith('ют') && w.length > 4) return w.slice(0, -2);
678
+ if (w.endsWith('ут') && w.length > 4) return w.slice(0, -2);
679
+ if (w.endsWith('ят') && w.length > 4) return w.slice(0, -2);
680
+ return w;
681
+ }
682
+
683
+ /**
684
+ * Turkish light stemmer — strips common agglutinative suffixes.
685
+ * Turkish is highly agglutinative; this covers the most common inflectional ends.
686
+ *
687
+ * Covers:
688
+ * hatalar → hata (-lar/-ler plural)
689
+ * sunucuda → sunucu (-da/-de locative)
690
+ * dağıtımı → dağıtım (-ı/-i/-u/-ü accusative)
691
+ * yüklemek → yükle (-mek/-mak infinitive)
692
+ */
693
+ function turkishLightStem(word: string): string {
694
+ let w = word;
695
+ // Suffixes longest-first
696
+ if (w.endsWith('lardaki') && w.length > 7) return w.slice(0, -7);
697
+ if (w.endsWith('lerdeki') && w.length > 7) return w.slice(0, -7);
698
+ if (w.endsWith('ların') && w.length > 6) return w.slice(0, -5);
699
+ if (w.endsWith('lerin') && w.length > 6) return w.slice(0, -5);
700
+ if (w.endsWith('larda') && w.length > 6) return w.slice(0, -5);
701
+ if (w.endsWith('lerde') && w.length > 6) return w.slice(0, -5);
702
+ if (w.endsWith('ları') && w.length > 5) return w.slice(0, -4);
703
+ if (w.endsWith('leri') && w.length > 5) return w.slice(0, -4);
704
+ if (w.endsWith('mek') && w.length > 4) return w.slice(0, -3);
705
+ if (w.endsWith('mak') && w.length > 4) return w.slice(0, -3);
706
+ if (w.endsWith('ler') && w.length > 4) return w.slice(0, -3);
707
+ if (w.endsWith('lar') && w.length > 4) return w.slice(0, -3);
708
+ if (w.endsWith('nın') && w.length > 4) return w.slice(0, -3);
709
+ if (w.endsWith('nin') && w.length > 4) return w.slice(0, -3);
710
+ if (w.endsWith('nun') && w.length > 4) return w.slice(0, -3);
711
+ if (w.endsWith('nün') && w.length > 4) return w.slice(0, -3);
712
+ if (w.endsWith('da') && w.length > 4) return w.slice(0, -2);
713
+ if (w.endsWith('de') && w.length > 4) return w.slice(0, -2);
714
+ if (w.endsWith('ta') && w.length > 4) return w.slice(0, -2);
715
+ if (w.endsWith('te') && w.length > 4) return w.slice(0, -2);
716
+ if (w.endsWith('dan') && w.length > 4) return w.slice(0, -3);
717
+ if (w.endsWith('den') && w.length > 4) return w.slice(0, -3);
718
+ if (w.endsWith('tan') && w.length > 4) return w.slice(0, -3);
719
+ if (w.endsWith('ten') && w.length > 4) return w.slice(0, -3);
720
+ if (w.endsWith('ın') && w.length > 4) return w.slice(0, -2);
721
+ if (w.endsWith('in') && w.length > 4) return w.slice(0, -2);
722
+ if (w.endsWith('un') && w.length > 4) return w.slice(0, -2);
723
+ if (w.endsWith('ün') && w.length > 4) return w.slice(0, -2);
724
+ if (w.endsWith('ı') && w.length > 4) return w.slice(0, -1);
725
+ if (w.endsWith('i') && w.length > 4) return w.slice(0, -1);
726
+ if (w.endsWith('u') && w.length > 4) return w.slice(0, -1);
727
+ if (w.endsWith('ü') && w.length > 4) return w.slice(0, -1);
728
+ return w;
729
+ }
730
+
731
+
556
732
  // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
557
733
  // to their equivalents in other languages. Used to expand tokens at search time
558
734
  // so "deploy" finds documents containing "デプロイ" / "部署" / "배포", and vice versa.
@@ -560,46 +736,46 @@ function hebrewLightStem(word: string): string {
560
736
  // Keys must be lowercase. CJK values will be bigram-expanded at use time.
561
737
  const CROSS_LINGUAL_MAP = new Map<string, string[]>([
562
738
  // English → all
563
- ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação']],
564
- ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação']],
739
+ ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação', 'развёртывание', 'развертывание', 'dağıtım', 'dağıtmak']],
740
+ ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação', 'развёртывание', 'dağıtım']],
565
741
  ['container', ['コンテナ', '容器', '컨테이너', 'docker']],
566
- ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת']],
567
- ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'base de données', 'base de datos', 'banco de dados']],
568
- ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון']],
569
- ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro']],
570
- ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل']],
571
- ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir']],
572
- ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'bauen', 'construire', 'construir', 'costruire']],
573
- ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'testen', 'tester', 'probar', 'testare', 'testar']],
574
- ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação']],
575
- ['authentication', ['auth', 'مصادقة', 'אימות', '認証', '认证', '인증', 'login', 'oauth']],
576
- ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente']],
577
- ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave']],
578
- ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', 'بعيا', '問題', '問題', '오류', 'fehler', 'problème', 'problema']],
742
+ ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת', 'سرور', 'सर्वर', 'сервер', 'sunucu']],
743
+ ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'پایگاه داده', 'डेटाबेس', 'base de données', 'base de datos', 'banco de dados']],
744
+ ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון', 'کش', 'कैश', 'кэш', 'önbellek']],
745
+ ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'خطا', 'त्रुटि', 'गलती', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro', 'ошибка', 'hata', 'hatalı']],
746
+ ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل', 'اشکال', 'बग']],
747
+ ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'رفع', 'सुधार', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir', 'исправить', 'исправление', 'düzeltmek', 'düzeltme']],
748
+ ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'ساخت', 'निर्माण', 'bauen', 'construire', 'construir', 'costruire', 'сборка', 'собрать', 'derleme', 'derlemek']],
749
+ ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'آزمایش', 'परीक्षण', 'testen', 'tester', 'probar', 'testare', 'testar', 'тест', 'тестирование', 'test', 'testlemek']],
750
+ ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação', 'аутентификация', 'авторизация', 'kimlik doğrulama', 'yetkilendirme']],
751
+ ['authentication', ['auth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', '認証', '认证', '인증', 'login', 'oauth', 'аутентификация', 'авторизация', 'kimlik doğrulama']],
752
+ ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente', 'среда', 'окружение', 'ortam']],
753
+ ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave', 'секрет', 'ключ', 'gizli', 'anahtar']],
754
+ ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', '問題', '오류', 'fehler', 'problème', 'problema', 'проблема', 'ошибка', 'sorun', 'problem']],
579
755
  ['kubernetes', ['クベルネテス', 'k8s', 'kube']],
580
- ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'netzwerk', 'réseau', 'red', 'rete', 'rede']],
756
+ ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'شبکه', 'नेटवर्क', 'netzwerk', 'réseau', 'red', 'rete', 'rede', 'сеть', 'ağ']],
581
757
  ['timeout', ['タイムアウト', '超时', '타임아웃', 'مهلة']],
582
- ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'speicher', 'mémoire', 'memoria', 'memória']],
583
- ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração']],
584
- ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'installieren', 'installer', 'instalar', 'installare']],
585
- ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar']],
586
- ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'protokoll', 'protokolle', 'journal', 'registro']],
758
+ ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'حافظه', 'मेमोरी', 'speicher', 'mémoire', 'memoria', 'memória', 'память', 'bellek', 'hafıza']],
759
+ ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'پیکربندی', 'कॉन्फ़िग', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração', 'конфигурация', 'настройки', 'yapılandırma', 'ayarlar']],
760
+ ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'نصب', 'इंस्टॉल', 'installieren', 'installer', 'instalar', 'installare', 'установить', 'установка', 'yüklemek', 'kurulum']],
761
+ ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'به‌روزرسانی', 'अपडेट', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar', 'обновление', 'обновить', 'güncelleme']],
762
+ ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'ثبت', 'लॉग', 'protokoll', 'protokolle', 'journal', 'registro', 'журнал', 'лог', 'günlük']],
587
763
  ['port', ['ポート', '端口', '포트', 'منفذ', 'פורט']],
588
764
  ['file', ['ファイル', '文件', '파일', 'ملف', 'קובץ']],
589
765
  ['image', ['イメージ', '镜像', '이미지', 'صورة', 'תמונה', 'abbild', 'imagen', 'imagem']],
590
766
  ['volume', ['ボリューム', '卷', '볼륨']],
591
767
  ['cluster', ['クラスター', '集群', '클러스터']],
592
768
  ['node', ['ノード', '节点', '노드']],
593
- ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות', 'dienst', 'servicio', 'servizio', 'serviço']],
769
+ ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות', 'dienst', 'servicio', 'servizio', 'serviço', 'сервис', 'служба', 'servis', 'hizmet']],
594
770
  ['certificate', ['証明書', '证书', '인증서', 'cert', 'ssl', 'tls', 'شهادة', 'תעודה']],
595
771
  ['password', ['パスワード', '密码', '비밀번호', 'passwd', 'pwd', 'كلمة المرور', 'סיסמה']],
596
772
  ['token', ['トークン', '令牌', '토큰', 'jwt', 'secret', 'رمز', 'אסימון']],
597
773
  ['health', ['ヘルス', '健康', '헬스', 'healthcheck', 'probe', 'صحة']],
598
774
  ['migration', ['マイグレーション', '迁移', '마이그레이션', 'migrate', 'هجرة']],
599
- ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança']],
600
- ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar']],
601
- ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho']],
602
- ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
775
+ ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'پشتیبان‌گیری', 'बैकअप', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança', 'резервная копия', 'резерв', 'yedekleme']],
776
+ ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'نظارت', 'निगरानी', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar', 'мониторинг', 'наблюдение', 'izleme']],
777
+ ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'عملکرد', 'प्रदर्शन', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho', 'производительность', 'быстродействие', 'performans']],
778
+ ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'اتصال', 'कनेक्शन', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
603
779
  ['queue', ['キュー', '队列', '큐', 'طابور', 'warteschlange', 'file d\'attente', 'cola', 'coda', 'fila']],
604
780
  ['redis', ['レディス', '레디스']],
605
781
  ['nginx', ['エンジンエックス']],
@@ -613,10 +789,10 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
613
789
  ['git', ['version control', 'repo', 'repository', 'commit', 'push', 'pull']],
614
790
  ['ci', ['pipeline', 'github actions', 'gitlab', 'jenkins', 'build']],
615
791
  ['cd', ['deploy', 'deployment', 'release']],
616
- ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'debuggen', 'déboguer', 'depurar']],
617
- ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'absturz', 'panne', 'plantage', 'caída', 'falha']],
618
- ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
619
- ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão']],
792
+ ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'اشکال‌زدایی', 'डीबग', 'debuggen', 'déboguer', 'depurar', 'отладка', 'дебаг', 'hata ayıklama']],
793
+ ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'خرابی', 'क्रैश', 'absturz', 'panne', 'plantage', 'caída', 'falha', 'сбой', 'крэш', 'çökme', 'çöküş']],
794
+ ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'راه‌اندازی مجدد', 'पुنरारंभ', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
795
+ ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'مجوز', 'अनुमति', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão', 'разрешение', 'доступ', 'izin', 'yetki']],
620
796
  // Japanese katakana → English
621
797
  ['デプロイ', ['deploy', 'deployment']],
622
798
  ['コンテナ', ['container', 'docker']],
@@ -803,6 +979,69 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
803
979
  ['סודי', ['secret', 'key', 'token']],
804
980
  ['מפתח סודי', ['secret', 'key']],
805
981
 
982
+ // ── Farsi/Persian → English ───────────────────────────────────────────────
983
+ ['استقرار', ['deploy', 'deployment']],
984
+ ['خطا', ['error', 'err']], // Farsi: خطا vs Arabic: خطأ
985
+ ['اشکال', ['bug', 'issue', 'error']],
986
+ ['رفع اشکال', ['fix', 'debug', 'debugging']],
987
+ ['ساخت', ['build']],
988
+ ['آزمایش', ['test']],
989
+ ['احراز هویت', ['auth', 'authentication']],
990
+ ['پیکربندی', ['config', 'configuration']],
991
+ ['نصب', ['install']],
992
+ ['به‌روزرسانی', ['update', 'upgrade']],
993
+ ['ثبت', ['log']],
994
+ ['سرور', ['server']],
995
+ ['پایگاه داده', ['database', 'db']],
996
+ ['شبکه', ['network']], // Farsi شبکه vs Arabic شبكة
997
+ ['حافظه', ['memory', 'ram']], // Farsi حافظه vs Arabic ذاكرة
998
+ ['کش', ['cache']],
999
+ ['اشکال‌زدایی', ['debug', 'debugging']],
1000
+ ['خرابی', ['crash', 'failure', 'error']],
1001
+ ['راه‌اندازی مجدد', ['restart', 'reboot']],
1002
+ ['مجوز', ['permission', 'access']],
1003
+ ['پشتیبان‌گیری', ['backup']],
1004
+ ['عملکرد', ['performance']],
1005
+ ['مشکل', ['problem', 'issue', 'error']],
1006
+ ['سرویس', ['service']],
1007
+ ['کلید', ['key', 'token', 'secret']],
1008
+ ['محیط', ['environment', 'env']],
1009
+ ['تولید', ['production', 'prod']],
1010
+ ['رمز', ['secret', 'key', 'token']],
1011
+ ['اتصال', ['connection', 'conn']],
1012
+
1013
+ // ── Hindi (Devanagari) → English ──────────────────────────────────────────
1014
+ ['तैनाती', ['deploy', 'deployment']],
1015
+ ['त्रुटि', ['error', 'err']],
1016
+ ['गलती', ['error', 'bug', 'issue']],
1017
+ ['सुधार', ['fix', 'bugfix']],
1018
+ ['निर्माण', ['build']],
1019
+ ['परीक्षण', ['test']],
1020
+ ['प्रमाणीकरण', ['auth', 'authentication']],
1021
+ ['विन्यास', ['config', 'configuration']],
1022
+ ['कॉन्फ़िगरेशन', ['config', 'configuration']],
1023
+ ['इंस्टॉल', ['install']],
1024
+ ['अपडेट', ['update', 'upgrade']],
1025
+ ['लॉग', ['log']],
1026
+ ['सर्वर', ['server']],
1027
+ ['डेटाबेस', ['database', 'db']],
1028
+ ['नेटवर्क', ['network']],
1029
+ ['मेमोरी', ['memory', 'ram']],
1030
+ ['कैश', ['cache']],
1031
+ ['डीबग', ['debug', 'debugging']],
1032
+ ['क्रैश', ['crash', 'panic']],
1033
+ ['पुनरारंभ', ['restart', 'reboot']],
1034
+ ['अनुमति', ['permission', 'access']],
1035
+ ['बैकअप', ['backup']],
1036
+ ['प्रदर्शन', ['performance']],
1037
+ ['समस्या', ['problem', 'issue', 'error']],
1038
+ ['सेवा', ['service']],
1039
+ ['कनेक्शन', ['connection', 'conn']],
1040
+ ['पासवर्ड', ['password']],
1041
+ ['टोकन', ['token', 'secret']],
1042
+ ['वातावरण', ['environment', 'env']],
1043
+ ['उत्पादन', ['production', 'prod']],
1044
+
806
1045
  // ── European languages → English ─────────────────────────────────────────
807
1046
  // German (Deutsch) → English
808
1047
  ['fehler', ['error', 'err']],
@@ -988,6 +1227,105 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
988
1227
  ['ambiente', ['environment', 'env']],
989
1228
  ['fila', ['queue']],
990
1229
  ['imagem', ['image']],
1230
+
1231
+ // ── Russian (Cyrillic) → English ──────────────────────────────────────────
1232
+ ['ошибка', ['error', 'err']],
1233
+ ['ошибки', ['error', 'err']],
1234
+ ['сбой', ['crash', 'failure', 'error']],
1235
+ ['крэш', ['crash', 'panic']],
1236
+ ['баг', ['bug', 'issue']],
1237
+ ['исправить', ['fix', 'bugfix']],
1238
+ ['исправление', ['fix', 'patch']],
1239
+ ['сборка', ['build']],
1240
+ ['собрать', ['build']],
1241
+ ['тест', ['test']],
1242
+ ['тестирование', ['test', 'testing']],
1243
+ ['аутентификация', ['auth', 'authentication']],
1244
+ ['авторизация', ['auth', 'authorization']],
1245
+ ['конфигурация', ['config', 'configuration']],
1246
+ ['настройки', ['config', 'settings']],
1247
+ ['установить', ['install']],
1248
+ ['установка', ['install']],
1249
+ ['обновление', ['update', 'upgrade']],
1250
+ ['обновить', ['update', 'upgrade']],
1251
+ ['журнал', ['log']],
1252
+ ['лог', ['log', 'logs']],
1253
+ ['сервер', ['server']],
1254
+ ['база данных', ['database', 'db']],
1255
+ ['сеть', ['network']],
1256
+ ['память', ['memory', 'ram']],
1257
+ ['кэш', ['cache']],
1258
+ ['отладка', ['debug', 'debugging']],
1259
+ ['перезапуск', ['restart', 'reboot']],
1260
+ ['перезагрузка', ['restart', 'reboot']],
1261
+ ['разрешение', ['permission', 'access']],
1262
+ ['доступ', ['permission', 'access']],
1263
+ ['резерв', ['backup']],
1264
+ ['производительность', ['performance']],
1265
+ ['проблема', ['problem', 'issue', 'error']],
1266
+ ['сервис', ['service']],
1267
+ ['служба', ['service']],
1268
+ ['подключение', ['connection', 'conn']],
1269
+ ['соединение', ['connection', 'conn']],
1270
+ ['развёртывание', ['deploy', 'deployment']],
1271
+ ['развертывание', ['deploy', 'deployment']],
1272
+ ['контейнер', ['container', 'docker']],
1273
+ ['образ', ['image']],
1274
+ ['кластер', ['cluster']],
1275
+ ['узел', ['node']],
1276
+ ['секрет', ['secret', 'key', 'token']],
1277
+ ['ключ', ['key', 'token', 'secret']],
1278
+ ['пароль', ['password']],
1279
+ ['токен', ['token', 'secret']],
1280
+ ['мониторинг', ['monitor', 'monitoring']],
1281
+ ['наблюдение', ['monitor', 'monitoring']],
1282
+ ['мигрировать', ['migrate', 'migration']],
1283
+ ['миграция', ['migrate', 'migration']],
1284
+
1285
+ // ── Turkish (Latin + special chars ğ/ş/ı) → English ─────────────────────
1286
+ ['hata', ['error', 'err']],
1287
+ ['hatalı', ['error', 'err']],
1288
+ ['çökme', ['crash', 'panic']],
1289
+ ['çöküş', ['crash', 'failure']],
1290
+ ['düzeltmek', ['fix', 'bugfix']],
1291
+ ['düzeltme', ['fix', 'patch']],
1292
+ ['derleme', ['build']],
1293
+ ['derlemek', ['build']],
1294
+ ['test', ['test']],
1295
+ ['testlemek', ['test', 'testing']],
1296
+ ['kimlik doğrulama',['auth', 'authentication']],
1297
+ ['yetkilendirme', ['auth', 'authorization']],
1298
+ ['yapılandırma', ['config', 'configuration']],
1299
+ ['ayarlar', ['config', 'settings']],
1300
+ ['yüklemek', ['install']],
1301
+ ['kurulum', ['install']],
1302
+ ['güncelleme', ['update', 'upgrade']],
1303
+ ['günlük', ['log']],
1304
+ ['sunucu', ['server']],
1305
+ ['veritabanı', ['database', 'db']],
1306
+ ['ağ', ['network']],
1307
+ ['bellek', ['memory', 'ram']],
1308
+ ['önbellek', ['cache']],
1309
+ ['hata ayıklama', ['debug', 'debugging']],
1310
+ ['yeniden başlatma',['restart', 'reboot']],
1311
+ ['izin', ['permission', 'access']],
1312
+ ['yetki', ['permission', 'access', 'acl']],
1313
+ ['yedekleme', ['backup']],
1314
+ ['performans', ['performance']],
1315
+ ['sorun', ['problem', 'issue', 'error']],
1316
+ ['servis', ['service']],
1317
+ ['hizmet', ['service']],
1318
+ ['bağlantı', ['connection', 'conn']],
1319
+ ['dağıtım', ['deploy', 'deployment']],
1320
+ ['dağıtmak', ['deploy']],
1321
+ ['konteyner', ['container', 'docker']],
1322
+ ['küme', ['cluster']],
1323
+ ['düğüm', ['node']],
1324
+ ['gizli', ['secret', 'key']],
1325
+ ['anahtar', ['key', 'token', 'secret']],
1326
+ ['şifre', ['password']],
1327
+ ['izleme', ['monitor', 'monitoring']],
1328
+ ['göç', ['migrate', 'migration']],
991
1329
  ]);
992
1330
 
993
1331
  /**
@@ -1006,9 +1344,11 @@ function expandCrossLingual(token: string): string[] {
1006
1344
  * Features:
1007
1345
  * • CJK → character bigrams
1008
1346
  * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
1009
- * • Arabic → word tokenization + iterative prefix stemming (al-, wa-, bi-, li-, fa-)
1010
- * • Hebrew → word tokenization + iterative prefix stemming (ha-, ve-, be-, le-, mi-, ke-)
1011
- * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE↔DE↔FR↔ES↔IT↔PT)
1347
+ * • Arabic → word tokenization + iterative prefix stemming (ال/و/ب/ل/ف/ك)
1348
+ * • Hebrew → word tokenization + iterative prefix stemming (ה/ו/ב/ל/מ/כ)
1349
+ * • Farsi → word tokenization + suffix/prefix stemming (ها/های plural, می prefix)
1350
+ * • Hindi → Devanagari word tokenization + light suffix stemming
1351
+ * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE↔FA↔HI↔DE↔FR↔ES↔IT↔PT)
1012
1352
  */
1013
1353
  function tokenize(text: string): string[] {
1014
1354
  const tokens: string[] = [];
@@ -1046,23 +1386,53 @@ function tokenize(text: string): string[] {
1046
1386
  }
1047
1387
  }
1048
1388
  } else if (RTL_RE.test(seg)) {
1049
- // RTL segment — split on spaces, apply language-specific prefix stemming
1050
- // Arabic: strips ال/و/ب/ل/ف/ك prefixes iteratively
1051
- // Hebrew: strips ה/ו/ב/ל/מ/כ prefixes iteratively
1052
- const isHebrew = HEBREW_CHAR_RE.test(seg);
1389
+ // RTL segment — 3-way branch: Farsi / Hebrew / Arabic
1053
1390
  const words = seg.trim().split(/\s+/);
1391
+ let stemFn: (w: string) => string;
1392
+ if (FARSI_CHAR_RE.test(seg)) stemFn = farsiLightStem;
1393
+ else if (HEBREW_CHAR_RE.test(seg)) stemFn = hebrewLightStem;
1394
+ else stemFn = arabicLightStem;
1054
1395
  for (const w of words) {
1055
1396
  if (w.length <= 1 || STOPWORDS.has(w)) continue;
1056
- const stemmed = isHebrew ? hebrewLightStem(w) : arabicLightStem(w);
1397
+ const stemmed = stemFn(w);
1398
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1399
+ }
1400
+ } else if (DEVANAGARI_RE.test(seg)) {
1401
+ // Devanagari segment (Hindi / Marathi) — naturally space-separated
1402
+ // Words don't need bigram expansion; stems reduce inflected forms.
1403
+ const words = seg.trim().split(/[\s\u200c\u200d]+/); // handle zero-width joiners
1404
+ for (const w of words) {
1405
+ // Keep Devanagari chars + digits; discard pure Latin portions (e.g. embedded numbers)
1406
+ const deva = w.replace(/[^\u0900-\u097f0-9]/g, '');
1407
+ if (deva.length <= 1 || STOPWORDS.has(deva)) continue;
1408
+ const stemmed = hindiLightStem(deva);
1409
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1410
+ }
1411
+ } else if (CYRILLIC_RE.test(seg)) {
1412
+ // Cyrillic segment (Russian, Bulgarian, Ukrainian, Serbian …)
1413
+ const words = seg.trim().split(/\s+/);
1414
+ for (const w of words) {
1415
+ const cyr = w.replace(/[^\u0400-\u04ff]/g, '');
1416
+ if (cyr.length <= 1 || STOPWORDS.has(cyr)) continue;
1417
+ const stemmed = russianLightStem(cyr);
1057
1418
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1058
1419
  }
1059
1420
  } else {
1060
- // Latin / other — existing logic preserved exactly
1421
+ // Latin / other (includes Turkish with special chars ğ/ş/ı)
1061
1422
  const words = seg
1062
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
1423
+ .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœğşıİ0-9:_\-./]/g, ' ')
1063
1424
  .split(/\s+/)
1064
1425
  .filter(w => w.length > 1 && !STOPWORDS.has(w));
1065
- tokens.push(...words);
1426
+ // Turkish: apply light stemmer to words with Turkish-specific chars
1427
+ for (const w of words) {
1428
+ if (TURKISH_CHAR_RE.test(w)) {
1429
+ const stemmed = turkishLightStem(w);
1430
+ if (stemmed !== w && stemmed.length > 1 && !STOPWORDS.has(stemmed)) {
1431
+ tokens.push(stemmed);
1432
+ }
1433
+ }
1434
+ tokens.push(w);
1435
+ }
1066
1436
  }
1067
1437
  }
1068
1438
 
@@ -1087,13 +1457,37 @@ function tokenize(text: string): string[] {
1087
1457
  if (!STOPWORDS.has(last)) expansions.push(last);
1088
1458
  }
1089
1459
  } else if (RTL_RE.test(syn)) {
1090
- // RTL synonym
1091
- const stemmed = arabicLightStem(syn);
1460
+ // RTL synonym — apply correct stemmer per language
1461
+ const rtlStem = FARSI_CHAR_RE.test(syn) ? farsiLightStem
1462
+ : HEBREW_CHAR_RE.test(syn) ? hebrewLightStem
1463
+ : arabicLightStem;
1464
+ const stemmed = rtlStem(syn);
1465
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1466
+ } else if (DEVANAGARI_RE.test(syn)) {
1467
+ // Devanagari synonym (Hindi)
1468
+ const stemmed = hindiLightStem(syn);
1469
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1470
+ } else if (CYRILLIC_RE.test(syn)) {
1471
+ // Cyrillic synonym (Russian)
1472
+ const cyr = syn.replace(/[^\u0400-\u04ff]/g, '');
1473
+ const stemmed = russianLightStem(cyr);
1092
1474
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1093
1475
  } else {
1094
- // Latin synonym — split in case of multi-word (e.g. "load balancer")
1095
- const parts = syn.split(/\s+/).filter(w => w.length > 1 && !STOPWORDS.has(w));
1096
- expansions.push(...parts);
1476
+ // Latin synonym (includes Turkish) — split in case of multi-word
1477
+ const parts = syn.split(/\s+/).filter(w => {
1478
+ if (w.length <= 1 || STOPWORDS.has(w)) return false;
1479
+ return true;
1480
+ });
1481
+ for (const p of parts) {
1482
+ expansions.push(p);
1483
+ // Apply Turkish stemmer if it has Turkish-specific characters
1484
+ if (TURKISH_CHAR_RE.test(p)) {
1485
+ const stemmed = turkishLightStem(p);
1486
+ if (stemmed !== p && stemmed.length > 1 && !STOPWORDS.has(stemmed)) {
1487
+ expansions.push(stemmed);
1488
+ }
1489
+ }
1490
+ }
1097
1491
  }
1098
1492
  }
1099
1493
  }
@@ -1121,9 +1515,9 @@ function splitMultiQuery(query: string): string[] {
1121
1515
  const semiParts = query.split(/[;\n]+/).filter(s => s.trim().length > 2);
1122
1516
  if (semiParts.length >= 2) return semiParts.map(s => s.trim());
1123
1517
 
1124
- // Conjunctions (EN + DE + FR + ES)
1518
+ // Conjunctions (EN + DE + FR + ES + IT + PT + RU + TR)
1125
1519
  const conjParts = query.split(
1126
- /\b(?:and also|also noch|außerdem|plus|additionally|furthermore|de plus|además|inoltre|além disso)\b/i
1520
+ /\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
1127
1521
  ).filter(s => s.trim().length > 2);
1128
1522
  if (conjParts.length >= 2) return conjParts.map(s => s.trim());
1129
1523
 
@@ -1388,6 +1782,12 @@ async function keywordSearch(
1388
1782
  }
1389
1783
  }
1390
1784
 
1785
+ // Phrase-match boost: if the raw (lowercased) query appears verbatim in content, 2× boost
1786
+ // This rewards docs where the exact phrase exists (vs scattered tokens).
1787
+ if (sq.length >= 4 && doc.content.toLowerCase().includes(sq.toLowerCase())) {
1788
+ score *= 2.0;
1789
+ }
1790
+
1391
1791
  // Recency boost
1392
1792
  score *= recencyBoost(doc.timestamp);
1393
1793