@cachly-dev/mcp-server 0.5.50 → 0.5.70

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.50';
58
+ const CURRENT_VERSION = '0.5.70';
59
59
  /**
60
60
  * ┌──────────────────────────────────────────────────────────────────────┐
61
61
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -434,6 +434,40 @@ const STOPWORDS = new Set([
434
434
  'मैं', 'तुम', 'उन', 'इन', 'ने', 'बहुत', 'सब', 'कुछ', 'फिर',
435
435
  'अब', 'तो', 'ही', 'तरह', 'जैसे', 'करना', 'होना', 'रहा', 'रही',
436
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ç',
454
+ // ── Polish — common prepositions, pronouns, particles ─────────────────────
455
+ 'w', 'na', 'z', 'do', 'się', 'że', 'to', 'jest', 'i', 'a', 'nie',
456
+ 'tak', 'jak', 'czy', 'po', 'o', 'ale', 'go', 'mu', 'jej', 'ich',
457
+ 'tego', 'tej', 'te', 'ten', 'ta', 'są', 'był', 'była', 'było', 'byli',
458
+ 'będzie', 'będą', 'ma', 'mam', 'masz', 'mają', 'ze', 'co', 'już',
459
+ 'przez', 'przy', 'za', 'bez', 'nad', 'pod', 'przed', 'po', 'między',
460
+ 'kiedy', 'gdzie', 'dlaczego', 'który', 'która', 'które', 'tylko', 'też',
461
+ // ── Czech — common words ──────────────────────────────────────────────────
462
+ 'v', 'na', 'z', 'do', 'se', 'že', 'to', 'je', 'a', 'ne', 'pro',
463
+ 'ale', 'jak', 'by', 'byl', 'být', 'jsem', 'jsou', 'má', 'mám',
464
+ 'ten', 'ta', 'ty', 'tato', 'jeho', 'její', 'jejich', 'také', 'jen',
465
+ 'kde', 'kdy', 'proč', 'který', 'která', 'které', 'co', 'při', 'bez',
466
+ // ── Bengali — common particles, pronouns, verbs ───────────────────────────
467
+ 'এই', 'এটি', 'এটা', 'এর', 'তার', 'তারা', 'আমি', 'তুমি', 'সে', 'আমরা',
468
+ 'এবং', 'কিন্তু', 'বা', 'না', 'হ্যাঁ', 'যে', 'কি', 'কে', 'কী', 'থেকে',
469
+ 'দিয়ে', 'জন্য', 'সাথে', 'মধ্যে', 'উপর', 'নিচে', 'আছে', 'ছিল', 'হয়',
470
+ 'করা', 'করে', 'করেছে', 'হবে', 'হয়েছে', 'একটি', 'একটা', 'অনেক', 'সব',
437
471
  ]);
438
472
 
439
473
 
@@ -450,6 +484,12 @@ const ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\u
450
484
  const FARSI_CHAR_RE = /[\u067e\u0686\u0698\u06af\u06cc]/;
451
485
  // Devanagari (Hindi, Sanskrit, Marathi …) — U+0900–U+097F + extended
452
486
  const DEVANAGARI_RE = /[\u0900-\u097f]/;
487
+ // Cyrillic (Russian, Bulgarian, Ukrainian, Serbian …) — U+0400–U+04FF
488
+ const CYRILLIC_RE = /[\u0400-\u04ff]/;
489
+ // Turkish uses Latin alphabet but has unique chars (ğ, ş, ı, ö, ü, ç) — detected by these
490
+ const TURKISH_CHAR_RE = /[\u011f\u015f\u0131\u0130\u00e7]/; // ğ ş ı İ ç
491
+ // Bengali (Bangla) — U+0980–U+09FF
492
+ const BENGALI_RE = /[\u0980-\u09ff]/;
453
493
 
454
494
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
455
495
  // Digraphs must be listed before single chars so they match first.
@@ -611,10 +651,155 @@ function hindiLightStem(word: string): string {
611
651
  if (result.endsWith('\u0915\u0930') && result.length > 4) return result.slice(0, -2); // \u0915\u0930 (kar) conjunctive
612
652
  if (result.endsWith('\u0913\u0902') && result.length > 4) return result.slice(0, -2); // \u0913\u0902 (oṃ) oblique plural
613
653
  if (result.endsWith('\u0907\u092f\u093e\u0902') && result.length > 5) return result.slice(0, -4); // \u0907\u092f\u093e\u0902 (iyāṃ) f plural
614
- if (result.endsWith('\u0940\u092f\u093e\u0902') && result.length > 5) return result.slice(0, -4); // \u0940\u092f\u093e\u0902 (īyāṃ)
654
+ if (result.endsWith('\u0940\u092f\u093e\u0902') && result.length > 5) return result.slice(0, -4); // \u0940\u092f\u093e\u0102 (īyāṃ)
615
655
  return result;
616
656
  }
617
657
 
658
+ /**
659
+ * Russian light stemmer — strips common inflectional suffixes.
660
+ * Handles the most frequent verb/noun/adjective endings to improve recall.
661
+ * Only strips when result is still ≥ 3 chars.
662
+ *
663
+ * Covers:
664
+ * ошибки/ошибка → ошибк (noun plural/genitive)
665
+ * развёртывание → развёртыва (gerund → stem)
666
+ * установить → установ (infinitive)
667
+ * настройки → настройк (genitive plural)
668
+ */
669
+ function russianLightStem(word: string): string {
670
+ let w = word;
671
+ // Longest first to avoid partial stripping
672
+ // Verb infinitives / gerunds
673
+ if (w.endsWith('ывание') && w.length > 7) return w.slice(0, -6);
674
+ if (w.endsWith('ивание') && w.length > 7) return w.slice(0, -6);
675
+ if (w.endsWith('ование') && w.length > 7) return w.slice(0, -6);
676
+ if (w.endsWith('вание') && w.length > 6) return w.slice(0, -5);
677
+ if (w.endsWith('ение') && w.length > 5) return w.slice(0, -4);
678
+ if (w.endsWith('ание') && w.length > 5) return w.slice(0, -4);
679
+ if (w.endsWith('ить') && w.length > 4) return w.slice(0, -3);
680
+ if (w.endsWith('ать') && w.length > 4) return w.slice(0, -3);
681
+ if (w.endsWith('еть') && w.length > 4) return w.slice(0, -3);
682
+ if (w.endsWith('уть') && w.length > 4) return w.slice(0, -3);
683
+ // Noun plural/genitive
684
+ if (w.endsWith('ки') && w.length > 4) return w.slice(0, -2);
685
+ if (w.endsWith('ги') && w.length > 4) return w.slice(0, -2);
686
+ if (w.endsWith('ов') && w.length > 4) return w.slice(0, -2);
687
+ if (w.endsWith('ев') && w.length > 4) return w.slice(0, -2);
688
+ if (w.endsWith('ей') && w.length > 4) return w.slice(0, -2);
689
+ if (w.endsWith('ий') && w.length > 4) return w.slice(0, -2);
690
+ if (w.endsWith('ый') && w.length > 4) return w.slice(0, -2);
691
+ if (w.endsWith('ая') && w.length > 4) return w.slice(0, -2);
692
+ if (w.endsWith('ое') && w.length > 4) return w.slice(0, -2);
693
+ if (w.endsWith('ую') && w.length > 4) return w.slice(0, -2);
694
+ // Verb present tense
695
+ if (w.endsWith('ет') && w.length > 4) return w.slice(0, -2);
696
+ if (w.endsWith('ют') && w.length > 4) return w.slice(0, -2);
697
+ if (w.endsWith('ут') && w.length > 4) return w.slice(0, -2);
698
+ if (w.endsWith('ят') && w.length > 4) return w.slice(0, -2);
699
+ return w;
700
+ }
701
+
702
+ /**
703
+ * Turkish light stemmer — strips common agglutinative suffixes.
704
+ * Turkish is highly agglutinative; this covers the most common inflectional ends.
705
+ *
706
+ * Covers:
707
+ * hatalar → hata (-lar/-ler plural)
708
+ * sunucuda → sunucu (-da/-de locative)
709
+ * dağıtımı → dağıtım (-ı/-i/-u/-ü accusative)
710
+ * yüklemek → yükle (-mek/-mak infinitive)
711
+ */
712
+ function turkishLightStem(word: string): string {
713
+ let w = word;
714
+ // Suffixes longest-first
715
+ if (w.endsWith('lardaki') && w.length > 7) return w.slice(0, -7);
716
+ if (w.endsWith('lerdeki') && w.length > 7) return w.slice(0, -7);
717
+ if (w.endsWith('ların') && w.length > 6) return w.slice(0, -5);
718
+ if (w.endsWith('lerin') && w.length > 6) return w.slice(0, -5);
719
+ if (w.endsWith('larda') && w.length > 6) return w.slice(0, -5);
720
+ if (w.endsWith('lerde') && w.length > 6) return w.slice(0, -5);
721
+ if (w.endsWith('ları') && w.length > 5) return w.slice(0, -4);
722
+ if (w.endsWith('leri') && w.length > 5) return w.slice(0, -4);
723
+ if (w.endsWith('mek') && w.length > 4) return w.slice(0, -3);
724
+ if (w.endsWith('mak') && w.length > 4) return w.slice(0, -3);
725
+ if (w.endsWith('ler') && w.length > 4) return w.slice(0, -3);
726
+ if (w.endsWith('lar') && w.length > 4) return w.slice(0, -3);
727
+ if (w.endsWith('nın') && w.length > 4) return w.slice(0, -3);
728
+ if (w.endsWith('nin') && w.length > 4) return w.slice(0, -3);
729
+ if (w.endsWith('nun') && w.length > 4) return w.slice(0, -3);
730
+ if (w.endsWith('nün') && w.length > 4) return w.slice(0, -3);
731
+ if (w.endsWith('da') && w.length > 4) return w.slice(0, -2);
732
+ if (w.endsWith('de') && w.length > 4) return w.slice(0, -2);
733
+ if (w.endsWith('ta') && w.length > 4) return w.slice(0, -2);
734
+ if (w.endsWith('te') && w.length > 4) return w.slice(0, -2);
735
+ if (w.endsWith('dan') && w.length > 4) return w.slice(0, -3);
736
+ if (w.endsWith('den') && w.length > 4) return w.slice(0, -3);
737
+ if (w.endsWith('tan') && w.length > 4) return w.slice(0, -3);
738
+ if (w.endsWith('ten') && w.length > 4) return w.slice(0, -3);
739
+ if (w.endsWith('ın') && w.length > 4) return w.slice(0, -2);
740
+ if (w.endsWith('in') && w.length > 4) return w.slice(0, -2);
741
+ if (w.endsWith('un') && w.length > 4) return w.slice(0, -2);
742
+ if (w.endsWith('ün') && w.length > 4) return w.slice(0, -2);
743
+ if (w.endsWith('ı') && w.length > 4) return w.slice(0, -1);
744
+ if (w.endsWith('i') && w.length > 4) return w.slice(0, -1);
745
+ if (w.endsWith('u') && w.length > 4) return w.slice(0, -1);
746
+ if (w.endsWith('ü') && w.length > 4) return w.slice(0, -1);
747
+ return w;
748
+ }
749
+
750
+ /**
751
+ * Bengali (Bangla) light stemmer — strips common verbal and nominal suffixes.
752
+ * Bengali is an Indo-Aryan language with moderate morphological complexity.
753
+ * Only strips when result is still ≥ 2 chars.
754
+ *
755
+ * Covers:
756
+ * করছে/করেছে → কর (progressive/perfect aspect)
757
+ * সার্ভারগুলো → সার্ভার (-গুলো plural)
758
+ * সমস্যাটি → সমস্যা (-টি singular marker)
759
+ */
760
+ function bengaliLightStem(word: string): string {
761
+ let w = word;
762
+ // Plural / collective markers (longest first)
763
+ if (w.endsWith('\u0997\u09c1\u09b2\u09cb') && w.length > 5) return w.slice(0, -4); // গুলো (-gulo plural)
764
+ if (w.endsWith('\u0997\u09c1\u09b2\u09bf') && w.length > 5) return w.slice(0, -4); // গুলি (-guli plural)
765
+ if (w.endsWith('\u09a6\u09c7\u09b0') && w.length > 4) return w.slice(0, -3); // দের (genitive plural)
766
+ if (w.endsWith('\u09a6\u09bf\u0997\u09c7') && w.length > 5) return w.slice(0, -4); // দিগে
767
+ // Definiteness / case markers
768
+ if (w.endsWith('\u099f\u09bf') && w.length > 3) return w.slice(0, -2); // টি (singular definite)
769
+ if (w.endsWith('\u099f\u09be') && w.length > 3) return w.slice(0, -2); // টা (singular definite)
770
+ if (w.endsWith('\u0996\u09be\u09a8\u09be') && w.length > 5) return w.slice(0, -4); // খানা
771
+ // Verbal suffixes
772
+ if (w.endsWith('\u099b\u09c7') && w.length > 3) return w.slice(0, -2); // ছে (progressive)
773
+ if (w.endsWith('\u099b\u09bf\u09b2') && w.length > 4) return w.slice(0, -3); // ছিল (past progressive)
774
+ if (w.endsWith('\u09af\u09be\u09ac\u09c7') && w.length > 5) return w.slice(0, -4); // যাবে (future)
775
+ if (w.endsWith('\u0995\u09b0\u09be') && w.length > 4) return w.slice(0, -3); // করা (infinitive)
776
+ if (w.endsWith('\u0995\u09b0\u09c7') && w.length > 4) return w.slice(0, -3); // করে (present)
777
+ if (w.endsWith('\u09b9\u09df') && w.length > 3) return w.slice(0, -2); // হয় (is/becomes)
778
+ if (w.endsWith('\u09b9\u09ac\u09c7') && w.length > 4) return w.slice(0, -3); // হবে (will be)
779
+ return w;
780
+ }
781
+
782
+ /**
783
+ * Pre-process text BEFORE tokenization to split code identifiers.
784
+ * Handles camelCase, PascalCase, snake_case, and kebab-case.
785
+ *
786
+ * Examples:
787
+ * deployServer → deploy Server (then lowercased → deploy server)
788
+ * HTMLParser → HTML Parser
789
+ * my_api_key → my api key
790
+ * get-user-by-id → get user by id (hyphens preserved in regex, spaces also fine)
791
+ * BackendServiceImpl → Backend Service Impl
792
+ */
793
+ function preprocessText(text: string): string {
794
+ return text
795
+ // camelCase boundary: lowercase/digit → uppercase
796
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
797
+ // Consecutive uppercase → uppercase+lowercase boundary: HTMLParser → HTML Parser
798
+ .replace(/([A-Z]{2,})([A-Z][a-z])/g, '$1 $2')
799
+ // snake_case: replace underscores with spaces
800
+ .replace(/_/g, ' ');
801
+ }
802
+
618
803
 
619
804
  // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
620
805
  // to their equivalents in other languages. Used to expand tokens at search time
@@ -622,46 +807,104 @@ function hindiLightStem(word: string): string {
622
807
  //
623
808
  // Keys must be lowercase. CJK values will be bigram-expanded at use time.
624
809
  const CROSS_LINGUAL_MAP = new Map<string, string[]>([
810
+
811
+ // ── Tech / Ecosystem synonyms (language-agnostic) ─────────────────────────
812
+ ['javascript', ['js', 'node', 'nodejs', 'ecmascript']],
813
+ ['js', ['javascript', 'node', 'nodejs']],
814
+ ['typescript', ['ts', 'javascript', 'js', 'tsc']],
815
+ ['ts', ['typescript', 'javascript', 'js']],
816
+ ['python', ['py', 'pip', 'django', 'flask', 'fastapi']],
817
+ ['py', ['python', 'pip']],
818
+ ['golang', ['go', 'gopher', 'goroutine']],
819
+ ['go', ['golang', 'goroutine']],
820
+ ['rust', ['cargo', 'rustlang', 'crates']],
821
+ ['cargo', ['rust', 'rustlang']],
822
+ ['java', ['jvm', 'maven', 'gradle', 'spring']],
823
+ ['kotlin', ['jvm', 'android', 'coroutine']],
824
+ ['dotnet', ['csharp', 'asp', 'nuget', 'aspnet']],
825
+ ['postgresql', ['postgres', 'pg', 'psql']],
826
+ ['postgres', ['postgresql', 'pg', 'psql']],
827
+ ['mysql', ['mariadb', 'sql']],
828
+ ['mongodb', ['mongo', 'nosql', 'bson', 'mongoose']],
829
+ ['mongo', ['mongodb', 'nosql', 'bson']],
830
+ ['elasticsearch', ['elastic', 'opensearch', 'lucene', 'kibana']],
831
+ ['elastic', ['elasticsearch', 'opensearch']],
832
+ ['kubernetes', ['k8s', 'kube', 'kubectl', 'helm', 'k3s']],
833
+ ['k8s', ['kubernetes', 'kube', 'kubectl', 'helm']],
834
+ ['helm', ['kubernetes', 'k8s', 'chart']],
835
+ ['terraform', ['tf', 'hcl', 'iac', 'opentofu']],
836
+ ['ansible', ['playbook', 'automation']],
837
+ ['grafana', ['dashboard', 'visualization', 'metrics']],
838
+ ['prometheus', ['metrics', 'alert', 'scrape', 'grafana']],
839
+ ['nginx', ['proxy', 'webserver', 'ingress']],
840
+ ['traefik', ['proxy', 'ingress', 'router']],
841
+ ['rabbitmq', ['amqp', 'queue', 'broker', 'messaging']],
842
+ ['kafka', ['messaging', 'stream', 'broker']],
843
+ ['grpc', ['protobuf', 'proto', 'rpc']],
844
+ ['graphql', ['gql', 'resolver', 'schema']],
845
+ ['react', ['jsx', 'hooks', 'component', 'redux']],
846
+ ['nextjs', ['next', 'react', 'ssr', 'vercel']],
847
+ ['vue', ['vuejs', 'vite', 'nuxt']],
848
+ ['angular', ['ng', 'rxjs', 'typescript']],
849
+ ['aws', ['amazon', 'ec2', 's3', 'lambda', 'cloudwatch', 'ecs', 'eks']],
850
+ ['gcp', ['google cloud', 'gke', 'bigquery']],
851
+ ['azure', ['microsoft cloud', 'aks', 'devops']],
852
+ ['s3', ['bucket', 'object storage', 'aws']],
853
+ ['lambda', ['serverless', 'function', 'faas', 'aws']],
854
+ ['jwt', ['token', 'bearer', 'oauth', 'auth']],
855
+ ['oauth', ['oidc', 'auth', 'token', 'sso']],
856
+ ['webhook', ['callback', 'event', 'trigger']],
857
+ ['cron', ['schedule', 'job', 'timer']],
858
+ ['yaml', ['yml', 'config', 'manifest']],
859
+ ['dotenv', ['env', 'environment', 'envfile']],
860
+ ['github', ['git', 'actions', 'repo', 'ci']],
861
+ ['gitlab', ['git', 'pipeline', 'runner']],
862
+ ['wireguard', ['vpn', 'tunnel', 'wg']],
863
+ ['prisma', ['orm', 'database', 'schema', 'migration']],
864
+ ['npm', ['node', 'package', 'registry', 'yarn', 'pnpm']],
865
+ ['yarn', ['npm', 'package', 'node']],
866
+ ['pip', ['python', 'package', 'pypi']],
867
+ ['brew', ['homebrew', 'macos', 'package']],
625
868
  // English → all
626
- ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação']],
627
- ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação']],
869
+ ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação', 'развёртывание', 'развертывание', 'dağıtım', 'dağıtmak']],
870
+ ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação', 'развёртывание', 'dağıtım']],
628
871
  ['container', ['コンテナ', '容器', '컨테이너', 'docker']],
629
- ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת', 'سرور', 'सर्वर']],
872
+ ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת', 'سرور', 'सर्वर', 'сервер', 'sunucu']],
630
873
  ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'پایگاه داده', 'डेटाबेس', 'base de données', 'base de datos', 'banco de dados']],
631
- ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון', 'کش', 'कैश']],
632
- ['error', ['エラー', '错误', '오류', 'خطأ', 'شגיאה', 'خطا', 'त्रुटि', 'गलती', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro']],
874
+ ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון', 'کش', 'कैश', 'кэш', 'önbellek']],
875
+ ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'خطا', 'त्रुटि', 'गलती', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro', 'ошибка', 'hata', 'hatalı']],
633
876
  ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل', 'اشکال', 'बग']],
634
- ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'رفع', 'सुधार', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir']],
635
- ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'ساخت', 'निर्माण', 'bauen', 'construire', 'construir', 'costruire']],
636
- ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'آزمایش', 'परीक्षण', 'testen', 'tester', 'probar', 'testare', 'testar']],
637
- ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação']],
638
- ['authentication', ['auth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', '認証', '认证', '인증', 'login', 'oauth']],
639
- ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente']],
640
- ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave']],
641
- ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', 'بعيا', '問題', '問題', '오류', 'fehler', 'problème', 'problema']],
877
+ ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'رفع', 'सुधार', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir', 'исправить', 'исправление', 'düzeltmek', 'düzeltme']],
878
+ ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'ساخت', 'निर्माण', 'bauen', 'construire', 'construir', 'costruire', 'сборка', 'собрать', 'derleme', 'derlemek']],
879
+ ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'آزمایش', 'परीक्षण', 'testen', 'tester', 'probar', 'testare', 'testar', 'тест', 'тестирование', 'test', 'testlemek']],
880
+ ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação', 'аутентификация', 'авторизация', 'kimlik doğrulama', 'yetkilendirme']],
881
+ ['authentication', ['auth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', '認証', '认证', '인증', 'login', 'oauth', 'аутентификация', 'авторизация', 'kimlik doğrulama']],
882
+ ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente', 'среда', 'окружение', 'ortam']],
883
+ ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave', 'секрет', 'ключ', 'gizli', 'anahtar']],
884
+ ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', '問題', '오류', 'fehler', 'problème', 'problema', 'проблема', 'ошибка', 'sorun', 'problem']],
642
885
  ['kubernetes', ['クベルネテス', 'k8s', 'kube']],
643
- ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'شبکه', 'नेटवर्क', 'netzwerk', 'réseau', 'red', 'rete', 'rede']],
886
+ ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'شبکه', 'नेटवर्क', 'netzwerk', 'réseau', 'red', 'rete', 'rede', 'сеть', 'ağ']],
644
887
  ['timeout', ['タイムアウト', '超时', '타임아웃', 'مهلة']],
645
- ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'حافظه', 'मेमोरी', 'speicher', 'mémoire', 'memoria', 'memória']],
646
- ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'پیکربندی', 'कॉन्फ़िग', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração']],
647
- ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'نصب', 'इंस्टॉल', 'installieren', 'installer', 'instalar', 'installare']],
648
- ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'به‌روزرسانی', 'अपडेट', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar']],
649
- ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'ثبت', 'लॉग', 'protokoll', 'protokolle', 'journal', 'registro']],
888
+ ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'حافظه', 'मेमोरी', 'speicher', 'mémoire', 'memoria', 'memória', 'память', 'bellek', 'hafıza']],
889
+ ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'پیکربندی', 'कॉन्फ़िग', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração', 'конфигурация', 'настройки', 'yapılandırma', 'ayarlar']],
890
+ ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'نصب', 'इंस्टॉल', 'installieren', 'installer', 'instalar', 'installare', 'установить', 'установка', 'yüklemek', 'kurulum']],
891
+ ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'به‌روزرسانی', 'अपडेट', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar', 'обновление', 'обновить', 'güncelleme']],
892
+ ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'ثبت', 'लॉग', 'protokoll', 'protokolle', 'journal', 'registro', 'журнал', 'лог', 'günlük']],
650
893
  ['port', ['ポート', '端口', '포트', 'منفذ', 'פורט']],
651
894
  ['file', ['ファイル', '文件', '파일', 'ملف', 'קובץ']],
652
895
  ['image', ['イメージ', '镜像', '이미지', 'صورة', 'תמונה', 'abbild', 'imagen', 'imagem']],
653
896
  ['volume', ['ボリューム', '卷', '볼륨']],
654
897
  ['cluster', ['クラスター', '集群', '클러스터']],
655
898
  ['node', ['ノード', '节点', '노드']],
656
- ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות', 'dienst', 'servicio', 'servizio', 'serviço']],
899
+ ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות', 'dienst', 'servicio', 'servizio', 'serviço', 'сервис', 'служба', 'servis', 'hizmet']],
657
900
  ['certificate', ['証明書', '证书', '인증서', 'cert', 'ssl', 'tls', 'شهادة', 'תעודה']],
658
901
  ['password', ['パスワード', '密码', '비밀번호', 'passwd', 'pwd', 'كلمة المرور', 'סיסמה']],
659
902
  ['token', ['トークン', '令牌', '토큰', 'jwt', 'secret', 'رمز', 'אסימון']],
660
903
  ['health', ['ヘルス', '健康', '헬스', 'healthcheck', 'probe', 'صحة']],
661
904
  ['migration', ['マイグレーション', '迁移', '마이그레이션', 'migrate', 'هجرة']],
662
- ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'پشتیبان‌گیری', 'बैकअप', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança']],
663
- ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'نظارت', 'निगरानी', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar']],
664
- ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'عملکرد', 'प्रदर्शन', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho']],
905
+ ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'پشتیبان‌گیری', 'बैकअप', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança', 'резервная копия', 'резерв', 'yedekleme']],
906
+ ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'نظارت', 'निगरानी', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar', 'мониторинг', 'наблюдение', 'izleme']],
907
+ ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'عملکرد', 'प्रदर्शन', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho', 'производительность', 'быстродействие', 'performans']],
665
908
  ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'اتصال', 'कनेक्शन', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
666
909
  ['queue', ['キュー', '队列', '큐', 'طابور', 'warteschlange', 'file d\'attente', 'cola', 'coda', 'fila']],
667
910
  ['redis', ['レディス', '레디스']],
@@ -676,10 +919,10 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
676
919
  ['git', ['version control', 'repo', 'repository', 'commit', 'push', 'pull']],
677
920
  ['ci', ['pipeline', 'github actions', 'gitlab', 'jenkins', 'build']],
678
921
  ['cd', ['deploy', 'deployment', 'release']],
679
- ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'اشکال‌زدایی', 'डीबग', 'debuggen', 'déboguer', 'depurar']],
680
- ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'خرابی', 'क्रैश', 'absturz', 'panne', 'plantage', 'caída', 'falha']],
922
+ ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'اشکال‌زدایی', 'डीबग', 'debuggen', 'déboguer', 'depurar', 'отладка', 'дебаг', 'hata ayıklama']],
923
+ ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'خرابی', 'क्रैश', 'absturz', 'panne', 'plantage', 'caída', 'falha', 'сбой', 'крэш', 'çökme', 'çöküş']],
681
924
  ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'راه‌اندازی مجدد', 'पुنरारंभ', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
682
- ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'مجوز', 'अनुमति', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão']],
925
+ ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'مجوز', 'अनुमति', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão', 'разрешение', 'доступ', 'izin', 'yetki']],
683
926
  // Japanese katakana → English
684
927
  ['デプロイ', ['deploy', 'deployment']],
685
928
  ['コンテナ', ['container', 'docker']],
@@ -1114,6 +1357,105 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
1114
1357
  ['ambiente', ['environment', 'env']],
1115
1358
  ['fila', ['queue']],
1116
1359
  ['imagem', ['image']],
1360
+
1361
+ // ── Russian (Cyrillic) → English ──────────────────────────────────────────
1362
+ ['ошибка', ['error', 'err']],
1363
+ ['ошибки', ['error', 'err']],
1364
+ ['сбой', ['crash', 'failure', 'error']],
1365
+ ['крэш', ['crash', 'panic']],
1366
+ ['баг', ['bug', 'issue']],
1367
+ ['исправить', ['fix', 'bugfix']],
1368
+ ['исправление', ['fix', 'patch']],
1369
+ ['сборка', ['build']],
1370
+ ['собрать', ['build']],
1371
+ ['тест', ['test']],
1372
+ ['тестирование', ['test', 'testing']],
1373
+ ['аутентификация', ['auth', 'authentication']],
1374
+ ['авторизация', ['auth', 'authorization']],
1375
+ ['конфигурация', ['config', 'configuration']],
1376
+ ['настройки', ['config', 'settings']],
1377
+ ['установить', ['install']],
1378
+ ['установка', ['install']],
1379
+ ['обновление', ['update', 'upgrade']],
1380
+ ['обновить', ['update', 'upgrade']],
1381
+ ['журнал', ['log']],
1382
+ ['лог', ['log', 'logs']],
1383
+ ['сервер', ['server']],
1384
+ ['база данных', ['database', 'db']],
1385
+ ['сеть', ['network']],
1386
+ ['память', ['memory', 'ram']],
1387
+ ['кэш', ['cache']],
1388
+ ['отладка', ['debug', 'debugging']],
1389
+ ['перезапуск', ['restart', 'reboot']],
1390
+ ['перезагрузка', ['restart', 'reboot']],
1391
+ ['разрешение', ['permission', 'access']],
1392
+ ['доступ', ['permission', 'access']],
1393
+ ['резерв', ['backup']],
1394
+ ['производительность', ['performance']],
1395
+ ['проблема', ['problem', 'issue', 'error']],
1396
+ ['сервис', ['service']],
1397
+ ['служба', ['service']],
1398
+ ['подключение', ['connection', 'conn']],
1399
+ ['соединение', ['connection', 'conn']],
1400
+ ['развёртывание', ['deploy', 'deployment']],
1401
+ ['развертывание', ['deploy', 'deployment']],
1402
+ ['контейнер', ['container', 'docker']],
1403
+ ['образ', ['image']],
1404
+ ['кластер', ['cluster']],
1405
+ ['узел', ['node']],
1406
+ ['секрет', ['secret', 'key', 'token']],
1407
+ ['ключ', ['key', 'token', 'secret']],
1408
+ ['пароль', ['password']],
1409
+ ['токен', ['token', 'secret']],
1410
+ ['мониторинг', ['monitor', 'monitoring']],
1411
+ ['наблюдение', ['monitor', 'monitoring']],
1412
+ ['мигрировать', ['migrate', 'migration']],
1413
+ ['миграция', ['migrate', 'migration']],
1414
+
1415
+ // ── Turkish (Latin + special chars ğ/ş/ı) → English ─────────────────────
1416
+ ['hata', ['error', 'err']],
1417
+ ['hatalı', ['error', 'err']],
1418
+ ['çökme', ['crash', 'panic']],
1419
+ ['çöküş', ['crash', 'failure']],
1420
+ ['düzeltmek', ['fix', 'bugfix']],
1421
+ ['düzeltme', ['fix', 'patch']],
1422
+ ['derleme', ['build']],
1423
+ ['derlemek', ['build']],
1424
+ ['test', ['test']],
1425
+ ['testlemek', ['test', 'testing']],
1426
+ ['kimlik doğrulama',['auth', 'authentication']],
1427
+ ['yetkilendirme', ['auth', 'authorization']],
1428
+ ['yapılandırma', ['config', 'configuration']],
1429
+ ['ayarlar', ['config', 'settings']],
1430
+ ['yüklemek', ['install']],
1431
+ ['kurulum', ['install']],
1432
+ ['güncelleme', ['update', 'upgrade']],
1433
+ ['günlük', ['log']],
1434
+ ['sunucu', ['server']],
1435
+ ['veritabanı', ['database', 'db']],
1436
+ ['ağ', ['network']],
1437
+ ['bellek', ['memory', 'ram']],
1438
+ ['önbellek', ['cache']],
1439
+ ['hata ayıklama', ['debug', 'debugging']],
1440
+ ['yeniden başlatma',['restart', 'reboot']],
1441
+ ['izin', ['permission', 'access']],
1442
+ ['yetki', ['permission', 'access', 'acl']],
1443
+ ['yedekleme', ['backup']],
1444
+ ['performans', ['performance']],
1445
+ ['sorun', ['problem', 'issue', 'error']],
1446
+ ['servis', ['service']],
1447
+ ['hizmet', ['service']],
1448
+ ['bağlantı', ['connection', 'conn']],
1449
+ ['dağıtım', ['deploy', 'deployment']],
1450
+ ['dağıtmak', ['deploy']],
1451
+ ['konteyner', ['container', 'docker']],
1452
+ ['küme', ['cluster']],
1453
+ ['düğüm', ['node']],
1454
+ ['gizli', ['secret', 'key']],
1455
+ ['anahtar', ['key', 'token', 'secret']],
1456
+ ['şifre', ['password']],
1457
+ ['izleme', ['monitor', 'monitoring']],
1458
+ ['göç', ['migrate', 'migration']],
1117
1459
  ]);
1118
1460
 
1119
1461
  /**
@@ -1140,7 +1482,8 @@ function expandCrossLingual(token: string): string[] {
1140
1482
  */
1141
1483
  function tokenize(text: string): string[] {
1142
1484
  const tokens: string[] = [];
1143
- const lower = text.toLowerCase();
1485
+ // Pre-process: split camelCase/PascalCase identifiers and snake_case before lowercasing
1486
+ const lower = preprocessText(text).toLowerCase();
1144
1487
 
1145
1488
  for (const [seg] of lower.matchAll(SEGMENT_RE)) {
1146
1489
  if (CJK_RE.test(seg)) {
@@ -1187,22 +1530,48 @@ function tokenize(text: string): string[] {
1187
1530
  }
1188
1531
  } else if (DEVANAGARI_RE.test(seg)) {
1189
1532
  // Devanagari segment (Hindi / Marathi) — naturally space-separated
1190
- // Words don't need bigram expansion; stems reduce inflected forms.
1191
1533
  const words = seg.trim().split(/[\s\u200c\u200d]+/); // handle zero-width joiners
1192
1534
  for (const w of words) {
1193
- // Keep Devanagari chars + digits; discard pure Latin portions (e.g. embedded numbers)
1194
1535
  const deva = w.replace(/[^\u0900-\u097f0-9]/g, '');
1195
1536
  if (deva.length <= 1 || STOPWORDS.has(deva)) continue;
1196
1537
  const stemmed = hindiLightStem(deva);
1197
1538
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1198
1539
  }
1540
+ } else if (BENGALI_RE.test(seg)) {
1541
+ // Bengali (Bangla) segment — space-separated words, Bengali script
1542
+ const words = seg.trim().split(/[\s\u200c\u200d]+/);
1543
+ for (const w of words) {
1544
+ const bn = w.replace(/[^\u0980-\u09ff0-9]/g, '');
1545
+ if (bn.length <= 1 || STOPWORDS.has(bn)) continue;
1546
+ const stemmed = bengaliLightStem(bn);
1547
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1548
+ }
1549
+ } else if (CYRILLIC_RE.test(seg)) {
1550
+ // Cyrillic segment (Russian, Bulgarian, Ukrainian, Serbian …)
1551
+ const words = seg.trim().split(/\s+/);
1552
+ for (const w of words) {
1553
+ const cyr = w.replace(/[^\u0400-\u04ff]/g, '');
1554
+ if (cyr.length <= 1 || STOPWORDS.has(cyr)) continue;
1555
+ const stemmed = russianLightStem(cyr);
1556
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1557
+ }
1199
1558
  } else {
1200
- // Latin / other — existing logic preserved exactly
1559
+ // Latin / other — includes Turkish (ğ/ş/ı), Polish (ą/ę/ł/ń/ó/ś/ź/ż/ć),
1560
+ // Czech (č/š/ž/ě/ů/ř), Hungarian (ő/ű), Romanian (ș/ț)
1201
1561
  const words = seg
1202
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
1562
+ .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœğşıİąćęłńśźżčšžěůřőűșț0-9:\-./]/g, ' ')
1203
1563
  .split(/\s+/)
1204
1564
  .filter(w => w.length > 1 && !STOPWORDS.has(w));
1205
- tokens.push(...words);
1565
+ // Turkish: apply light stemmer to words with Turkish-specific chars
1566
+ for (const w of words) {
1567
+ if (TURKISH_CHAR_RE.test(w)) {
1568
+ const stemmed = turkishLightStem(w);
1569
+ if (stemmed !== w && stemmed.length > 1 && !STOPWORDS.has(stemmed)) {
1570
+ tokens.push(stemmed);
1571
+ }
1572
+ }
1573
+ tokens.push(w);
1574
+ }
1206
1575
  }
1207
1576
  }
1208
1577
 
@@ -1237,10 +1606,31 @@ function tokenize(text: string): string[] {
1237
1606
  // Devanagari synonym (Hindi)
1238
1607
  const stemmed = hindiLightStem(syn);
1239
1608
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1609
+ } else if (BENGALI_RE.test(syn)) {
1610
+ // Bengali synonym
1611
+ const stemmed = bengaliLightStem(syn);
1612
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1613
+ } else if (CYRILLIC_RE.test(syn)) {
1614
+ // Cyrillic synonym (Russian)
1615
+ const cyr = syn.replace(/[^\u0400-\u04ff]/g, '');
1616
+ const stemmed = russianLightStem(cyr);
1617
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1240
1618
  } else {
1241
- // Latin synonym — split in case of multi-word (e.g. "load balancer")
1242
- const parts = syn.split(/\s+/).filter(w => w.length > 1 && !STOPWORDS.has(w));
1243
- expansions.push(...parts);
1619
+ // Latin synonym (includes Turkish) — split in case of multi-word
1620
+ const parts = syn.split(/\s+/).filter(w => {
1621
+ if (w.length <= 1 || STOPWORDS.has(w)) return false;
1622
+ return true;
1623
+ });
1624
+ for (const p of parts) {
1625
+ expansions.push(p);
1626
+ // Apply Turkish stemmer if it has Turkish-specific characters
1627
+ if (TURKISH_CHAR_RE.test(p)) {
1628
+ const stemmed = turkishLightStem(p);
1629
+ if (stemmed !== p && stemmed.length > 1 && !STOPWORDS.has(stemmed)) {
1630
+ expansions.push(stemmed);
1631
+ }
1632
+ }
1633
+ }
1244
1634
  }
1245
1635
  }
1246
1636
  }
@@ -1268,9 +1658,9 @@ function splitMultiQuery(query: string): string[] {
1268
1658
  const semiParts = query.split(/[;\n]+/).filter(s => s.trim().length > 2);
1269
1659
  if (semiParts.length >= 2) return semiParts.map(s => s.trim());
1270
1660
 
1271
- // Conjunctions (EN + DE + FR + ES)
1661
+ // Conjunctions (EN + DE + FR + ES + IT + PT + RU + TR)
1272
1662
  const conjParts = query.split(
1273
- /\b(?:and also|also noch|außerdem|plus|additionally|furthermore|de plus|además|inoltre|além disso)\b/i
1663
+ /\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
1274
1664
  ).filter(s => s.trim().length > 2);
1275
1665
  if (conjParts.length >= 2) return conjParts.map(s => s.trim());
1276
1666
 
@@ -1308,6 +1698,7 @@ interface DocEntry {
1308
1698
  tokens: string[];
1309
1699
  tokenFreq: Map<string, number>; // term → count in this doc
1310
1700
  bigrams: Set<string>; // "term1|term2" adjacency pairs
1701
+ keyTokens: Set<string>; // tokens from key/title only (for title boost)
1311
1702
  timestamp?: number; // epoch ms, extracted from content if present
1312
1703
  }
1313
1704
 
@@ -1427,7 +1818,9 @@ async function keywordSearch(
1427
1818
  }
1428
1819
 
1429
1820
  const timestamp = extractTimestamp(content);
1430
- docs.push({ key: allKeys[i], content, tokens, tokenFreq, bigrams, timestamp });
1821
+ // Key tokens for title-boost: terms appearing in the Redis key get extra weight
1822
+ const keyTokens = new Set(tokenize(allKeys[i]));
1823
+ docs.push({ key: allKeys[i], content, tokens, tokenFreq, bigrams, keyTokens, timestamp });
1431
1824
  totalTokens += tokens.length;
1432
1825
  }
1433
1826
 
@@ -1535,6 +1928,24 @@ async function keywordSearch(
1535
1928
  }
1536
1929
  }
1537
1930
 
1931
+ // Phrase-match boost: if the raw (lowercased) query appears verbatim in content, 2× boost
1932
+ // This rewards docs where the exact phrase exists (vs scattered tokens).
1933
+ if (sq.length >= 4 && doc.content.toLowerCase().includes(sq.toLowerCase())) {
1934
+ score *= 2.0;
1935
+ }
1936
+
1937
+ // Key/title boost: if query terms appear in the Redis key (=title), 1.5× boost
1938
+ // Keys often encode the primary topic (e.g. "deploy:api:server"), so key hits are high-precision.
1939
+ if (score > 0) {
1940
+ let keyHits = 0;
1941
+ for (const qt of queryTokens) {
1942
+ if (doc.keyTokens.has(qt)) keyHits++;
1943
+ }
1944
+ if (keyHits > 0) {
1945
+ score *= 1 + 0.5 * (keyHits / queryTokens.length);
1946
+ }
1947
+ }
1948
+
1538
1949
  // Recency boost
1539
1950
  score *= recencyBoost(doc.timestamp);
1540
1951