<?php
 namespace App\Services\Search; use App\Models\Language; use App\Models\Post; use Illuminate\Support\Collection; use Meilisearch\Client; abstract class PostSearchIndexer { public function __construct(protected readonly Client $client) { } abstract protected function indexName(): string; abstract protected function postType(): string; public function ensureIndexConfigured(): void { $index = $this->client->index($this->indexName()); try { $existingKey = $index->fetchPrimaryKey(); } catch (\Throwable) { $existingKey = null; } if ($existingKey === null) { try { $task = $this->client->createIndex($this->indexName(), ['primaryKey' => 'id']); $this->client->waitForTask($task['taskUid']); } catch (\Throwable) { } try { if ($index->fetchPrimaryKey() === null) { $task = $index->update(['primaryKey' => 'id']); $this->client->waitForTask($task['taskUid']); } } catch (\Throwable) { } } $index->updateSearchableAttributes(['title', 'content', 'excerpt', 'tags', 'subtype']); $index->updateFilterableAttributes(['post_id', 'language', 'status', 'is_featured', 'tags', 'subtype']); } public function reindex(Post $post): void { if ($post->type !== $this->postType()) { return; } $index = $this->client->index($this->indexName()); try { $index->deleteDocuments(['filter' => "post_id = {$post->id}"]); } catch (\Throwable) { foreach (Language::where('status', true)->pluck('a2_iso') as $loc) { try { $index->deleteDocument("{$post->id}_{$loc}"); } catch (\Throwable) { } } } if (! $post->isPublished()) { return; } $tagSlugs = $post->tags->pluck('slug')->all(); $docs = []; foreach (Language::where('status', true)->pluck('a2_iso') as $locale) { $title = $post->getTranslation('title', $locale); $content = $post->getTranslation('content', $locale); $excerpt = $post->getTranslation('excerpt', $locale); if (empty($title) && empty($content)) { continue; } $docs[] = [ 'id' => "{$post->id}_{$locale}", 'post_id' => $post->id, 'language' => $locale, 'title' => (string) $title, 'content' => strip_tags((string) $content), 'excerpt' => (string) $excerpt, 'slug' => $post->getLocaleSlug($locale), 'subtype' => $post->subtype, 'status' => $post->status, 'is_featured' => (bool) $post->is_featured, 'tags' => $tagSlugs, 'published_at' => $post->published_at?->timestamp, ]; } if (! empty($docs)) { $index->addDocuments($docs); } } public function delete(int $postId): void { $index = $this->client->index($this->indexName()); try { $index->deleteDocuments(['filter' => "post_id = {$postId}"]); } catch (\Throwable) { foreach (Language::where('status', true)->pluck('a2_iso') as $loc) { try { $index->deleteDocument("{$postId}_{$loc}"); } catch (\Throwable) { } } } } public function search(string $query, string $locale, int $limit = 20): Collection { $index = $this->client->index($this->indexName()); $result = $index->search($query, [ 'filter' => ["language = {$locale}", "status = published"], 'limit' => $limit, ]); return collect($result->getHits()); } } 