ExportPaliSynonyms.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Str;
  5. use App\Models\DhammaTerm;
  6. use App\Models\UserDict;
  7. use App\Services\OpenSearchService;
  8. class ExportPaliSynonyms extends Command
  9. {
  10. /**
  11. * The name and signature of the console command.
  12. * php artisan export:pali.synonyms --output= [--test]
  13. *
  14. * @var string
  15. */
  16. protected $signature = 'export:pali.synonyms {--output=} {--test : 只导出一行,用于快速检查输出格式}';
  17. /**
  18. * The console command description.
  19. *
  20. * @var string
  21. */
  22. protected $description = '导出openSearch用的巴利语变格表';
  23. /**
  24. * Create a new command instance.
  25. *
  26. * @return void
  27. */
  28. public function __construct()
  29. {
  30. parent::__construct();
  31. }
  32. /**
  33. * Execute the console command.
  34. *
  35. * @return int
  36. */
  37. public function handle()
  38. {
  39. if (! $this->option('output')) {
  40. $this->error('please set output file option --output=file');
  41. return 1;
  42. }
  43. /*
  44. //irregular
  45. $dictId = ['4d3a0d92-0adc-4052-80f5-512a2603d0e8'];
  46. //regular
  47. $dictId[] = DictApi::getSysDict('system_regular');
  48. $dictId[] = DictApi::getSysDict('robot_compound');
  49. */
  50. $filename = $this->option('output');
  51. $BASE_PATH = '/srv/opensearch/synonyms/pali/';
  52. $fp = fopen($BASE_PATH . $filename, 'w') or exit('Unable to open file!');
  53. $parents = UserDict::select('parent')
  54. ->whereNotNull('parent')
  55. ->where('parent', '<>', '')
  56. ->groupBy('parent')->cursor();
  57. $droppedTerms = 0;
  58. $droppedLines = 0;
  59. foreach ($parents as $parent) {
  60. if (str_contains($parent->parent, ' ')) {
  61. continue;
  62. }
  63. $words = UserDict::where('parent', $parent->parent)
  64. ->select('word')
  65. ->groupBy('word')->get();
  66. $wordsList = [];
  67. foreach ($words as $word) {
  68. $wordsList[$word->word] = 1;
  69. }
  70. $teams = DhammaTerm::where('word', $parent->parent)
  71. ->select(['meaning'])->get();
  72. foreach ($teams as $term) {
  73. $wordsList[$term->meaning] = 1;
  74. }
  75. $this->info("[{$parent->parent}] " . count($words) . ' team=' . count($teams));
  76. // 合并 $parent->parent, $words->word, $team->meaning 为一个字符串数组
  77. $combinedArray = [];
  78. $combinedArray[] = $parent->parent;
  79. foreach ($wordsList as $word => $value) {
  80. $combinedArray[] = $word;
  81. }
  82. // 过滤掉会被 analyzer 完全消除的 term,否则建索引时会失败
  83. $termCount = count($combinedArray);
  84. $combinedArray = $this->filterSynonymTerms($combinedArray);
  85. $droppedTerms += $termCount - count($combinedArray);
  86. // 同义词行至少要有两个 term 才有意义
  87. if (count($combinedArray) < 2) {
  88. $droppedLines++;
  89. continue;
  90. }
  91. // 将 $combinedArray 写入 CSV 文件
  92. fputcsv($fp, $combinedArray);
  93. if ($this->option('test')) {
  94. $this->warn('--test 已开启,只导出一行。');
  95. break;
  96. }
  97. }
  98. // 关闭文件
  99. fclose($fp);
  100. $this->info("过滤掉非法 term {$droppedTerms} 个,整行丢弃 {$droppedLines} 行。");
  101. //update opensearch
  102. $this->info('update opensearch synonyms');
  103. $result = app(OpenSearchService::class)->updatePaliSynonymsPath($filename);
  104. $this->info(json_encode($result, JSON_PRETTY_PRINT));
  105. $this->info('done');
  106. return 0;
  107. }
  108. /**
  109. * 词典源码标记,这类 term 不是真正的词条
  110. *
  111. * 1. 含 # ‹ › 的:如 "#=cetayati)"、"(#=)(‹paññāpeti)"。
  112. * 其中以 # 开头的 term 若落在行首,整行会被 OpenSearch 当成注释静默忽略。
  113. * 2. 以 ( [ < 开头的:如 "(ku的离格)"、"[ava-hīḷanā<hīḍ]",是词源/变格标注。
  114. * 只匹配开头,保证 "阿拉汉[果]"、"bhesajja[ṃ]" 这类正常释义不受影响。
  115. */
  116. private const DIRTY_TERM_PATTERN = '/[#‹›]|^[(\[<<]/u';
  117. /**
  118. * 过滤掉不能用作同义词的 term,并按原顺序去重
  119. *
  120. * 丢弃两类 term:
  121. * 1. 不含任何字母或数字的(例如 "②"、"——"、"?)")。OpenSearch 的 synonym_graph
  122. * filter 会用 analyzer 分析每一个 term,这类 term 分析后被完全消除,
  123. * 建索引时会抛出 illegal_argument_exception: Failed to build synonyms。
  124. * 2. 带词典源码标记的脏词条,见 self::DIRTY_TERM_PATTERN。
  125. *
  126. * @param array<int, string> $terms
  127. * @return array<int, string>
  128. */
  129. private function filterSynonymTerms(array $terms): array
  130. {
  131. $seen = [];
  132. $filtered = [];
  133. foreach ($terms as $term) {
  134. $term = trim((string) $term);
  135. if ($term === '' || preg_match('/[\p{L}\p{Nd}]/u', $term) !== 1) {
  136. continue;
  137. }
  138. if (preg_match(self::DIRTY_TERM_PATTERN, $term) === 1) {
  139. continue;
  140. }
  141. if (isset($seen[$term])) {
  142. continue;
  143. }
  144. $seen[$term] = true;
  145. $filtered[] = $term;
  146. }
  147. return $filtered;
  148. }
  149. /**
  150. * 将给定文件路径的扩展名替换为 .json
  151. *
  152. * @param string $filePath 完整的文件路径
  153. * @return string 新的文件路径
  154. */
  155. private function changeExtensionToJson(string $filePath): string
  156. {
  157. // 获取路径信息
  158. $pathInfo = pathinfo($filePath);
  159. // 提取目录、文件名(不含扩展名)
  160. $dirname = $pathInfo['dirname'] ?? '';
  161. $filename = $pathInfo['filename'] ?? '';
  162. // 如果目录不是根目录,则添加目录分隔符
  163. $dirname = rtrim($dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
  164. // 构建新路径
  165. return $dirname . $filename . '.json';
  166. }
  167. }