Просмотр исходного кода

feat: 术语新建/修改时重建 OpenSearch 索引,索引逻辑抽到 TermIndexService

将单条词条的索引构建与写入逻辑从 IndexTerm 命令抽到 TermIndexService 复用;术语 store/update 后触发重建索引,失败仅记日志不阻断写入
visuddhinanda 4 дней назад
Родитель
Сommit
9019d03168

+ 8 - 104
api-v13/app/Console/Commands/IndexTerm.php

@@ -4,7 +4,7 @@ namespace App\Console\Commands;
 
 use App\Models\DhammaTerm;
 use App\Services\OpenSearchService;
-use App\Services\TermService;
+use App\Services\TermIndexService;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
@@ -25,7 +25,7 @@ class IndexTerm extends Command
 
     public function __construct(
         protected OpenSearchService $openSearchService,
-        protected TermService $termService,
+        protected TermIndexService $termIndexService,
     ) {
         parent::__construct();
     }
@@ -82,7 +82,12 @@ class IndexTerm extends Command
                     Cache::put(self::CACHE_KEY, $term->id, now()->addHours(48));
                 }
 
-                $this->indexTerm($term->guid);
+                if ($this->isTest) {
+                    $document = $this->termIndexService->buildDocument($term->guid);
+                    $this->info($document['title']['text']['pali']);
+                } else {
+                    $this->termIndexService->index($term->guid);
+                }
             }
 
             // 全部完成,清除断点缓存
@@ -97,105 +102,4 @@ class IndexTerm extends Command
             return 1;
         }
     }
-
-    /**
-     * 构建单条词条文档并写入 OpenSearch
-     *
-     * 文档结构遵循新版 mapping:
-     *   title.text.pali / title.text.zh  → 全文检索
-     *   title.suggest.pali / title.suggest.zh → 自动建议
-     *   content.text.pali / content.text.zh   → 正文内容
-     *
-     * @param  string  $id  DhammaTerm 的 guid
-     */
-    protected function indexTerm(string $id): void
-    {
-        $termData = $this->termService->find($id, 'text');
-        $channelName = $termData['channel']['name'] ?? '';
-        $isCommunity = $this->termService->isCommunity($termData['channel_id']);
-        $content = $termData['html'] ?? $termData['meaning'];
-
-        $categories = $this->extractCategories($termData['note'] ?? '');
-        $quality = $this->extractFirstQuality($termData['note'] ?? '');
-        $tags = [];
-        foreach ($categories as $key => $category) {
-            $tags[] = "category:{$category}";
-        }
-        if (! empty($quality)) {
-            $tags[] = "quality:{$quality}";
-        }
-        $document = [
-            'id' => "term_{$id}",
-            'resource_id' => $id,
-            'resource_type' => 'term',
-            'title' => [
-                'text' => [
-                    'pali' => $termData['word'],
-                    'zh' => $termData['meaning'],
-                ],
-                'suggest' => [
-                    'pali' => [$termData['word']],
-                    'zh' => [$termData['meaning']],
-                ],
-            ],
-            'summary' => [
-                'text' => $termData['summary'] ?? '',
-            ],
-            'content' => [],
-            'bold_single' => [$termData['meaning'], $termData['word']],
-            'related_id' => $termData['word'],
-            'category' => null,
-            'tags' => $tags,
-            'language' => $termData['language'],
-            'updated_at' => now()->toIso8601String(),
-            'path' => $termData['studio']['realName']."/{$channelName}",
-            'metadata' => ['channel' => $termData['channel_id']],
-        ];
-
-        // TODO: 补充语言判断,将内容放入对应的 text.pali 或 text.zh 字段
-        $plainText = strip_tags($content);
-        if (str_contains($termData['language'], 'zh')) {
-            $document['content']['text']['zh'] = $plainText;
-        } else {
-            $document['content']['text']['zh'] = $plainText;
-        }
-        $document['content']['display'] = $content;             // 展示
-
-        if ($this->isTest) {
-            $this->info($document['title']['text']['pali']);
-            // $this->info($document['summary']['text']);
-        } else {
-            $this->openSearchService->create($document['id'], $document);
-        }
-    }
-
-    /**
-     * 提取 Markdown 中的 {{category|...}} 分类标签
-     */
-    private function extractCategories(string $content): array
-    {
-        if (empty($content)) {
-            return [];
-        }
-        preg_match_all('/\{\{category\|([^}]+)\}\}/u', $content, $matches);
-
-        return array_values(array_filter(array_map(
-            fn ($item) => trim($item),
-            $matches[1] ?? []
-        )));
-    }
-
-    /**
-     * 提取 Markdown 中第一个 {{quality|...}} 标签内的内容
-     */
-    private function extractFirstQuality(string $content): string
-    {
-        if (empty($content)) {
-            return '';
-        }
-
-        preg_match('/\{\{quality\|([^}]+)\}\}/u', $content, $matches);
-
-        return isset($matches[1]) ? trim($matches[1]) : '';
-    }
 }

+ 28 - 0
api-v13/app/Http/Controllers/DhammaTermController.php

@@ -11,17 +11,23 @@ use App\Models\AiModel;
 use App\Models\Channel;
 use App\Models\DhammaTerm;
 use App\Services\AuthService;
+use App\Services\TermIndexService;
 use App\Tools\Tools;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 
 class DhammaTermController extends Controller
 {
     use ChecksChannelEditPower;
 
+    public function __construct(
+        private TermIndexService $termIndexService,
+    ) {}
+
     /**
      * Display a listing of the resource.
      *
@@ -302,6 +308,8 @@ class DhammaTermController extends Controller
             $term->save();
             // 删除cache
             $this->deleteCache($term);
+            // 重建 OpenSearch 索引
+            $this->reindex($term);
 
             return $this->ok(new TermResource($term));
         } else {
@@ -343,6 +351,24 @@ class DhammaTermController extends Controller
         }
     }
 
+    /**
+     * 术语新建/修改后重建 OpenSearch 索引。
+     *
+     * 索引是检索/维基展示的副作用数据,重建失败不应阻断本次写入,
+     * 因此只记录日志、不向上抛异常。
+     */
+    private function reindex(DhammaTerm $term): void
+    {
+        try {
+            $this->termIndexService->index($term->guid);
+        } catch (\Throwable $e) {
+            Log::error('Failed to index term after write', [
+                'guid' => $term->guid,
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
     /**
      * Display the specified resource.
      *
@@ -417,6 +443,8 @@ class DhammaTermController extends Controller
         $dhammaTerm->save();
         // 删除cache
         $this->deleteCache($dhammaTerm);
+        // 重建 OpenSearch 索引
+        $this->reindex($dhammaTerm);
 
         return $this->ok(new TermResource($dhammaTerm));
     }

+ 118 - 0
api-v13/app/Services/TermIndexService.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace App\Services;
+
+class TermIndexService
+{
+    public function __construct(
+        protected OpenSearchService $openSearchService,
+        protected TermService $termService,
+    ) {}
+
+    /**
+     * 构建单条词条文档并写入 OpenSearch
+     *
+     * @param  string  $guid  DhammaTerm 的 guid
+     */
+    public function index(string $guid): void
+    {
+        $document = $this->buildDocument($guid);
+
+        $this->openSearchService->create($document['id'], $document);
+    }
+
+    /**
+     * 构建单条词条的 OpenSearch 文档(不写入)
+     *
+     * 文档结构遵循新版 mapping:
+     *   title.text.pali / title.text.zh  → 全文检索
+     *   title.suggest.pali / title.suggest.zh → 自动建议
+     *   content.text.pali / content.text.zh   → 正文内容
+     *
+     * @param  string  $guid  DhammaTerm 的 guid
+     * @return array<string, mixed>
+     */
+    public function buildDocument(string $guid): array
+    {
+        $termData = $this->termService->find($guid, 'text');
+        $channelName = $termData['channel']['name'] ?? '';
+        $content = $termData['html'] ?? $termData['meaning'];
+
+        $categories = $this->extractCategories($termData['note'] ?? '');
+        $quality = $this->extractFirstQuality($termData['note'] ?? '');
+        $tags = [];
+        foreach ($categories as $category) {
+            $tags[] = "category:{$category}";
+        }
+        if (! empty($quality)) {
+            $tags[] = "quality:{$quality}";
+        }
+
+        $document = [
+            'id' => "term_{$guid}",
+            'resource_id' => $guid,
+            'resource_type' => 'term',
+            'title' => [
+                'text' => [
+                    'pali' => $termData['word'],
+                    'zh' => $termData['meaning'],
+                ],
+                'suggest' => [
+                    'pali' => [$termData['word']],
+                    'zh' => [$termData['meaning']],
+                ],
+            ],
+            'summary' => [
+                'text' => $termData['summary'] ?? '',
+            ],
+            'content' => [],
+            'bold_single' => [$termData['meaning'], $termData['word']],
+            'related_id' => $termData['word'],
+            'category' => null,
+            'tags' => $tags,
+            'language' => $termData['language'],
+            'updated_at' => now()->toIso8601String(),
+            'path' => $termData['studio']['realName']."/{$channelName}",
+            'metadata' => ['channel' => $termData['channel_id']],
+        ];
+
+        // TODO: 补充语言判断,将内容放入对应的 text.pali 或 text.zh 字段
+        $plainText = strip_tags($content);
+        $document['content']['text']['zh'] = $plainText;
+        $document['content']['display'] = $content;             // 展示
+
+        return $document;
+    }
+
+    /**
+     * 提取 Markdown 中的 {{category|...}} 分类标签
+     *
+     * @return array<int, string>
+     */
+    private function extractCategories(string $content): array
+    {
+        if (empty($content)) {
+            return [];
+        }
+        preg_match_all('/\{\{category\|([^}]+)\}\}/u', $content, $matches);
+
+        return array_values(array_filter(array_map(
+            fn ($item) => trim($item),
+            $matches[1] ?? []
+        )));
+    }
+
+    /**
+     * 提取 Markdown 中第一个 {{quality|...}} 标签内的内容
+     */
+    private function extractFirstQuality(string $content): string
+    {
+        if (empty($content)) {
+            return '';
+        }
+
+        preg_match('/\{\{quality\|([^}]+)\}\}/u', $content, $matches);
+
+        return isset($matches[1]) ? trim($matches[1]) : '';
+    }
+}