Browse Source

feat(api-v13): 巴利语同义词导出拆成两步,避开 NFS

- export:pali.synonyms 只导出,文件写到 storage/app/private/{output},
  不再写 /mnt/nfs/pali/、不再顺带切换 OpenSearch
- 新增 export:pali.synonyms.set-path --name=xxx.txt,ops 把文件分发到
  OpenSearch 节点后执行,路径 = OPENSEARCH_SYNONYMS_PATH + '/' + name
- 两个命令都只接受纯文件名;打不开文件时返回 1 而不是 exit()
- 文件里其余的 Pint 格式化一并带上

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visuddhinanda 13 hours ago
parent
commit
d0225d230a

+ 31 - 19
api-v13/app/Console/Commands/ExportPaliSynonyms.php

@@ -2,18 +2,21 @@
 
 
 namespace App\Console\Commands;
 namespace App\Console\Commands;
 
 
-use Illuminate\Console\Command;
-use Illuminate\Support\Str;
-
 use App\Models\DhammaTerm;
 use App\Models\DhammaTerm;
 use App\Models\UserDict;
 use App\Models\UserDict;
-use App\Services\OpenSearchService;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Storage;
 
 
 class ExportPaliSynonyms extends Command
 class ExportPaliSynonyms extends Command
 {
 {
     /**
     /**
      * The name and signature of the console command.
      * The name and signature of the console command.
-     * php artisan export:pali.synonyms --output= [--test]
+     * php artisan export:pali.synonyms --output=pali-synonyms.txt [--test]
+     *
+     * 只负责导出,文件写到 local disk(storage/app/private)。
+     * 由 ops 脚本把文件分发到 OpenSearch 节点后,
+     * 再用 export:pali.synonyms.set-path 切换索引的同义词文件。
      *
      *
      * @var string
      * @var string
      */
      */
@@ -55,9 +58,20 @@ class ExportPaliSynonyms extends Command
         $dictId[] = DictApi::getSysDict('system_regular');
         $dictId[] = DictApi::getSysDict('system_regular');
         $dictId[] = DictApi::getSysDict('robot_compound');
         $dictId[] = DictApi::getSysDict('robot_compound');
 */
 */
-        $filename =  $this->option('output');
-        $BASE_PATH = '/mnt/nfs/pali/';
-        $fp = fopen($BASE_PATH . $filename, 'w') or exit('Unable to open file!');
+        $filename = $this->option('output');
+        if (preg_match(self::FILENAME_PATTERN, $filename) !== 1) {
+            $this->error('--output must be a plain file name matching '.self::FILENAME_PATTERN);
+
+            return 1;
+        }
+        $outputPath = Storage::disk('local')->path($filename);
+        File::ensureDirectoryExists(dirname($outputPath));
+        $fp = fopen($outputPath, 'w');
+        if ($fp === false) {
+            $this->error("Unable to open file [{$outputPath}]");
+
+            return 1;
+        }
 
 
         $parents = UserDict::select('parent')
         $parents = UserDict::select('parent')
             ->whereNotNull('parent')
             ->whereNotNull('parent')
@@ -83,7 +97,7 @@ class ExportPaliSynonyms extends Command
             foreach ($teams as $term) {
             foreach ($teams as $term) {
                 $wordsList[$term->meaning] = 1;
                 $wordsList[$term->meaning] = 1;
             }
             }
-            $this->info("[{$parent->parent}] " . count($words) . ' team=' . count($teams));
+            $this->info("[{$parent->parent}] ".count($words).' team='.count($teams));
             // 合并 $parent->parent, $words->word, $team->meaning 为一个字符串数组
             // 合并 $parent->parent, $words->word, $team->meaning 为一个字符串数组
             $combinedArray = [];
             $combinedArray = [];
             $combinedArray[] = $parent->parent;
             $combinedArray[] = $parent->parent;
@@ -116,18 +130,16 @@ class ExportPaliSynonyms extends Command
         fclose($fp);
         fclose($fp);
 
 
         $this->info("过滤掉非法 term {$droppedTerms} 个,整行丢弃 {$droppedLines} 行。");
         $this->info("过滤掉非法 term {$droppedTerms} 个,整行丢弃 {$droppedLines} 行。");
-
-        //update opensearch
-        $this->info('update opensearch synonyms');
-        $result = app(OpenSearchService::class)->updatePaliSynonymsPath($filename);
-        $this->info(json_encode($result, JSON_PRETTY_PRINT));
-
-        $this->info('done');
-
+        $this->info("done: {$outputPath}");
 
 
         return 0;
         return 0;
     }
     }
 
 
+    /**
+     * 输出文件名,只允许纯文件名,不能带路径
+     */
+    public const FILENAME_PATTERN = '/^[A-Za-z0-9][A-Za-z0-9._-]*$/';
+
     /**
     /**
      * 词典源码标记,这类 term 不是真正的词条
      * 词典源码标记,这类 term 不是真正的词条
      *
      *
@@ -188,9 +200,9 @@ class ExportPaliSynonyms extends Command
         $filename = $pathInfo['filename'] ?? '';
         $filename = $pathInfo['filename'] ?? '';
 
 
         // 如果目录不是根目录,则添加目录分隔符
         // 如果目录不是根目录,则添加目录分隔符
-        $dirname = rtrim($dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+        $dirname = rtrim($dirname, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
 
 
         // 构建新路径
         // 构建新路径
-        return $dirname . $filename . '.json';
+        return $dirname.$filename.'.json';
     }
     }
 }
 }

+ 53 - 0
api-v13/app/Console/Commands/ExportPaliSynonymsSetPath.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Services\OpenSearchService;
+use Exception;
+use Illuminate\Console\Attributes\Description;
+use Illuminate\Console\Attributes\Signature;
+use Illuminate\Console\Command;
+
+/**
+ * 把索引的 pali_synonyms filter 指向新的同义词文件(两步流程的第二步)
+ *
+ * 第一步 export:pali.synonyms 把文件导出到 storage/app/private,
+ * ops 脚本把文件分发到 OpenSearch 各节点后再执行本命令。
+ * 实际路径 = OPENSEARCH_SYNONYMS_PATH + '/' + name,
+ * 文件必须已存在于各节点,否则索引关闭后无法重新打开。
+ *
+ * Usage:
+ *   php artisan export:pali.synonyms.set-path --name=pali-synonyms-20261021.txt
+ */
+#[Signature('export:pali.synonyms.set-path {--name= : 同义词文件名,不能带路径}')]
+#[Description('切换 OpenSearch 索引使用的巴利语同义词文件')]
+class ExportPaliSynonymsSetPath extends Command
+{
+    /**
+     * Execute the console command.
+     *
+     * @return int Command::SUCCESS | Command::FAILURE
+     */
+    public function handle(OpenSearchService $service): int
+    {
+        $name = (string) $this->option('name');
+
+        if (preg_match(ExportPaliSynonyms::FILENAME_PATTERN, $name) !== 1) {
+            $this->error('--name must be a plain file name matching '.ExportPaliSynonyms::FILENAME_PATTERN);
+
+            return self::FAILURE;
+        }
+
+        try {
+            $result = $service->updatePaliSynonymsPath($name);
+        } catch (Exception $e) {
+            $this->error('Update failed: '.$e->getMessage());
+
+            return self::FAILURE;
+        }
+
+        $this->info("synonyms_path => {$result['path']}");
+
+        return self::SUCCESS;
+    }
+}

+ 32 - 0
api-v13/tests/Feature/ExportPaliSynonymsSetPathTest.php

@@ -0,0 +1,32 @@
+<?php
+
+use App\Services\OpenSearchService;
+
+it('points the index at the given synonyms file', function () {
+    $this->mock(OpenSearchService::class)
+        ->shouldReceive('updatePaliSynonymsPath')
+        ->once()
+        ->with('aaa.txt')
+        ->andReturn(['path' => '/srv/opensearch/synonyms/aaa.txt', 'settings' => ['acknowledged' => true]]);
+
+    $this->artisan('export:pali.synonyms.set-path', ['--name' => 'aaa.txt'])
+        ->expectsOutputToContain('/srv/opensearch/synonyms/aaa.txt')
+        ->assertSuccessful();
+});
+
+it('fails when OpenSearch rejects the update', function () {
+    $this->mock(OpenSearchService::class)
+        ->shouldReceive('updatePaliSynonymsPath')
+        ->once()
+        ->andThrow(new Exception('Index [wikipali] does not exist.'));
+
+    $this->artisan('export:pali.synonyms.set-path', ['--name' => 'aaa.txt'])
+        ->assertFailed();
+});
+
+it('rejects a name that is missing or contains a path', function (?string $name) {
+    $this->mock(OpenSearchService::class)->shouldNotReceive('updatePaliSynonymsPath');
+
+    $this->artisan('export:pali.synonyms.set-path', $name === null ? [] : ['--name' => $name])
+        ->assertFailed();
+})->with([null, '', '../etc/passwd', 'dir/aaa.txt', '.hidden']);