ExportPaliSynonyms.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Http\Api\DictApi;
  4. use App\Models\DhammaTerm;
  5. use App\Models\UserDict;
  6. use App\Services\OpenSearchService;
  7. use Illuminate\Console\Command;
  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. $fp = fopen($filename, 'w') or exit('Unable to open file!');
  52. $parents = UserDict::select('parent')
  53. ->whereNotNull('parent')
  54. ->where('parent', '<>', '')
  55. ->groupBy('parent')->cursor();
  56. $droppedTerms = 0;
  57. $droppedLines = 0;
  58. foreach ($parents as $parent) {
  59. if (str_contains($parent->parent, ' ')) {
  60. continue;
  61. }
  62. $words = UserDict::where('parent', $parent->parent)
  63. ->select('word')
  64. ->groupBy('word')->get();
  65. $wordsList = [];
  66. foreach ($words as $word) {
  67. $wordsList[$word->word] = 1;
  68. }
  69. $teams = DhammaTerm::where('word', $parent->parent)
  70. ->select(['meaning'])->get();
  71. foreach ($teams as $term) {
  72. $wordsList[$term->meaning] = 1;
  73. }
  74. $this->info("[{$parent->parent}] ".count($words).' team='.count($teams));
  75. // 合并 $parent->parent, $words->word, $team->meaning 为一个字符串数组
  76. $combinedArray = [];
  77. $combinedArray[] = $parent->parent;
  78. foreach ($wordsList as $word => $value) {
  79. $combinedArray[] = $word;
  80. }
  81. // 过滤掉会被 analyzer 完全消除的 term,否则建索引时会失败
  82. $termCount = count($combinedArray);
  83. $combinedArray = $this->filterSynonymTerms($combinedArray);
  84. $droppedTerms += $termCount - count($combinedArray);
  85. // 同义词行至少要有两个 term 才有意义
  86. if (count($combinedArray) < 2) {
  87. $droppedLines++;
  88. continue;
  89. }
  90. // 将 $combinedArray 写入 CSV 文件
  91. fputcsv($fp, $combinedArray);
  92. if ($this->option('test')) {
  93. $this->warn('--test 已开启,只导出一行。');
  94. break;
  95. }
  96. }
  97. // 关闭文件
  98. fclose($fp);
  99. // 写index.json
  100. $info = [
  101. 'index' => config('mint.opensearch.index'),
  102. 'pali_synonyms' => app(OpenSearchService::class)->getPaliSynonymsSetting(),
  103. ];
  104. file_put_contents(
  105. $this->changeExtensionToJson($filename),
  106. json_encode($info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
  107. );
  108. $this->info("过滤掉非法 term {$droppedTerms} 个,整行丢弃 {$droppedLines} 行。");
  109. $this->info('done');
  110. return 0;
  111. }
  112. /**
  113. * 词典源码标记,这类 term 不是真正的词条
  114. *
  115. * 1. 含 # ‹ › 的:如 "#=cetayati)"、"(#=)(‹paññāpeti)"。
  116. * 其中以 # 开头的 term 若落在行首,整行会被 OpenSearch 当成注释静默忽略。
  117. * 2. 以 ( [ < 开头的:如 "(ku的离格)"、"[ava-hīḷanā<hīḍ]",是词源/变格标注。
  118. * 只匹配开头,保证 "阿拉汉[果]"、"bhesajja[ṃ]" 这类正常释义不受影响。
  119. */
  120. private const DIRTY_TERM_PATTERN = '/[#‹›]|^[(\[<<]/u';
  121. /**
  122. * 过滤掉不能用作同义词的 term,并按原顺序去重
  123. *
  124. * 丢弃两类 term:
  125. * 1. 不含任何字母或数字的(例如 "②"、"——"、"?)")。OpenSearch 的 synonym_graph
  126. * filter 会用 analyzer 分析每一个 term,这类 term 分析后被完全消除,
  127. * 建索引时会抛出 illegal_argument_exception: Failed to build synonyms。
  128. * 2. 带词典源码标记的脏词条,见 self::DIRTY_TERM_PATTERN。
  129. *
  130. * @param array<int, string> $terms
  131. * @return array<int, string>
  132. */
  133. private function filterSynonymTerms(array $terms): array
  134. {
  135. $seen = [];
  136. $filtered = [];
  137. foreach ($terms as $term) {
  138. $term = trim((string) $term);
  139. if ($term === '' || preg_match('/[\p{L}\p{Nd}]/u', $term) !== 1) {
  140. continue;
  141. }
  142. if (preg_match(self::DIRTY_TERM_PATTERN, $term) === 1) {
  143. continue;
  144. }
  145. if (isset($seen[$term])) {
  146. continue;
  147. }
  148. $seen[$term] = true;
  149. $filtered[] = $term;
  150. }
  151. return $filtered;
  152. }
  153. /**
  154. * 将给定文件路径的扩展名替换为 .json
  155. *
  156. * @param string $filePath 完整的文件路径
  157. * @return string 新的文件路径
  158. */
  159. private function changeExtensionToJson(string $filePath): string
  160. {
  161. // 获取路径信息
  162. $pathInfo = pathinfo($filePath);
  163. // 提取目录、文件名(不含扩展名)
  164. $dirname = $pathInfo['dirname'] ?? '';
  165. $filename = $pathInfo['filename'] ?? '';
  166. // 如果目录不是根目录,则添加目录分隔符
  167. $dirname = rtrim($dirname, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
  168. // 构建新路径
  169. return $dirname.$filename.'.json';
  170. }
  171. }