visuddhinanda 1 week ago
parent
commit
105fb729da
2 changed files with 228 additions and 0 deletions
  1. 137 0
      api-v12/app/Console/Commands/IndexTerm.php
  2. 91 0
      api-v12/resources/js/search-suggest.js

+ 137 - 0
api-v12/app/Console/Commands/IndexTerm.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\DhammaTerm;
+use Illuminate\Console\Command;
+use App\Services\OpenSearchService;
+use App\Services\TermService;
+use Illuminate\Support\Facades\Log;
+
+
+class IndexTerm extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan opensearch:index-term --word=anomadassī
+     * @var string
+     */
+    protected $signature = 'opensearch:index-term
+    {--test}
+    {--word= : index word. omit to all}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Index Term data into OpenSearch ';
+
+    private $isTest = false;
+    private $summary = false;
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct(
+        protected OpenSearchService $openSearchService,
+        protected TermService $termService,
+    ) {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $word = $this->option('word');
+
+
+        if ($this->option('test')) {
+            $this->isTest = true;
+            $this->info('test mode');
+        }
+
+
+        try {
+            // Test OpenSearch connection
+            [$connected, $message] = $this->openSearchService->testConnection();
+            if (!$connected) {
+                $this->error($message);
+                Log::error($message);
+                return 1;
+            }
+            $overallStatus = 0; // Track overall command status (0 for success, 1 for any failure)
+            $total = DhammaTerm::count();
+            $terms = DhammaTerm::select(['guid', 'word'])->orderBy('updated_at', 'asc');
+            if ($word) {
+                $terms = $terms->where('word', $word);
+            }
+
+            foreach ($terms->cursor() as $key => $term) {
+                $percent = (int)(($key * 100) / $total);
+                $this->info("[{$percent}%]-{$key}  " . $term->word);
+                $this->indexTerm($term->guid);
+            }
+
+            return $overallStatus;
+        } catch (\Exception $e) {
+            $this->error("Failed to index Pali data: " . $e->getMessage());
+            Log::error("Failed to index Term data : ", ['error' => $e]);
+            return 1;
+        }
+    }
+
+    /**
+     *
+     */
+    protected function indexTerm(string $id)
+    {
+        $termData = $this->termService->find($id, 'text');
+        $channelName = $termData["channel"]['name'] ?? '';
+        $isCommunity = $this->termService->isCommunity($termData["channel_id"]);
+        $content = $termData['html'] ?? $termData['meaning'];
+        $document = [
+            'id' => "term_{$id}",
+            'resource_id' => $id, // Use uid from getPaliData for resource_id
+            'resource_type' => 'term',
+            'title' => [
+                'pali' => $termData['word'],
+                'zh' => $termData['meaning'],
+                'suggest_pali' => [$termData['word']],
+                'suggest_zh' => [$termData['meaning']],
+            ],
+            'summary' => [
+                'text' => $termData['summary'] ?? $termData['note'] ?? ''
+            ],
+            'content' => [],
+            'bold_single' => [$termData['meaning'], $termData['word']],
+            'related_id' => $termData['word'],
+            'category' => [],
+            'tags' => $isCommunity ? ['community'] : [],
+            'language' => $termData['language'],
+            'updated_at' => now()->toIso8601String(),
+            'path' => $termData['studio']['realName'] . "/{$channelName}",
+        ];
+
+        if (strpos($termData['language'], 'zh') !== false) {
+            $document['content']['zh'] = $content;
+        } else {
+            //TODO 判断语言 放在合适的字段
+            $document['content']['zh'] = $content;
+        }
+
+        if ($this->isTest) {
+            $this->info($document['title']['pali']);
+            $this->info($document['summary']['text']);
+        } else {
+            $this->openSearchService->create($document['id'], $document);
+        }
+        return;
+    }
+}

+ 91 - 0
api-v12/resources/js/search-suggest.js

@@ -0,0 +1,91 @@
+document.addEventListener("DOMContentLoaded", () => {
+    const inputs = document.querySelectorAll(".search-input");
+
+    inputs.forEach((input) => {
+        const form = input.closest(".search-input-form");
+        const dropdown = form.querySelector(".search-suggest-dropdown");
+
+        let controller = null;
+
+        input.addEventListener("input", async () => {
+            const q = input.value.trim();
+
+            if (q.length < 2) {
+                dropdown.classList.remove("show");
+                return;
+            }
+
+            // 取消上一次请求(防抖 + 避免竞态)
+            if (controller) controller.abort();
+            controller = new AbortController();
+
+            try {
+                const url = new URL(
+                    input.dataset.suggestUrl,
+                    window.location.origin
+                );
+                url.searchParams.set("q", q);
+                url.searchParams.set("limit", 10);
+
+                const res = await fetch(url, {
+                    signal: controller.signal,
+                });
+
+                const json = await res.json();
+
+                renderSuggestions(json.data.suggestions || []);
+            } catch (e) {
+                if (e.name !== "AbortError") {
+                    console.error(e);
+                }
+            }
+        });
+
+        function renderSuggestions(list) {
+            if (!list.length) {
+                dropdown.classList.remove("show");
+                return;
+            }
+
+            dropdown.innerHTML = list
+                .map((item) => {
+                    return `
+                    <button type="button"
+                        class="dropdown-item"
+                        data-text="${item.text}">
+
+                        <div class="d-flex justify-content-between">
+                            <span>${item.text}</span>
+                            <small class="text-muted">${item.resource_type}</small>
+                        </div>
+                    </button>
+                `;
+                })
+                .join("");
+
+            dropdown.classList.add("show");
+        }
+
+        // 点击选择
+        dropdown.addEventListener("click", (e) => {
+            const btn = e.target.closest(".dropdown-item");
+            if (!btn) return;
+
+            input.value = btn.dataset.text;
+            dropdown.classList.remove("show");
+
+            form.submit(); // 或者只填充不提交
+        });
+
+        // 失焦隐藏
+        input.addEventListener("blur", () => {
+            setTimeout(() => dropdown.classList.remove("show"), 150);
+        });
+
+        input.addEventListener("focus", () => {
+            if (dropdown.innerHTML.trim()) {
+                dropdown.classList.add("show");
+            }
+        });
+    });
+});