diff --git a/src/Commands/RebuildDocumentsCommand.php b/src/Commands/RebuildDocumentsCommand.php index 0cc7c96..87939ac 100644 --- a/src/Commands/RebuildDocumentsCommand.php +++ b/src/Commands/RebuildDocumentsCommand.php @@ -3,13 +3,12 @@ namespace Blomstra\Search\Commands; use Blomstra\Search\Observe\SavingJob; -use Blomstra\Search\Schemas\Schema; +use Blomstra\Search\Seeders\Seeder; use Elasticsearch\Client; use Illuminate\Console\Command; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Queue\Queue; use Illuminate\Database\Eloquent\Collection; -use Illuminate\Database\Eloquent\Model; class RebuildDocumentsCommand extends Command { @@ -18,8 +17,8 @@ class RebuildDocumentsCommand extends Command public function handle(Container $container) { - /** @var array $schemas */ - $schemas = $container->tagged('blomstra.search.schemas'); + /** @var array $seeders */ + $seeders = $container->tagged('blomstra.search.seeders'); /** @var Queue $queue */ $queue = $container->make(Queue::class); @@ -29,19 +28,14 @@ class RebuildDocumentsCommand extends Command // Flush the index. if ($this->option('flush')) $client->indices()->delete([ - 'index' => resolve('blomstra.search.elastic_index') + 'index' => $container->make('blomstra.search.elastic_index') ]); - /** @var Schema $schema */ - foreach ($schemas as $schema) { - /** @var Model $model */ - $model = $schema::model(); + /** @var Seeder $seeder */ + foreach ($seeders as $seeder) { + $seeder->query()->chunk(50, function (Collection $collection) use ($queue, &$total) { - $total = 0; - - $schema::query()->chunk(50, function (Collection $collection) use ($model, $queue, &$total) { - - $queue->push(new SavingJob($model, $collection)); + $queue->push(new SavingJob($collection)); $this->info("Pushed {$collection->count()} into the index."); diff --git a/src/Documents/CommentDocument.php b/src/Documents/CommentDocument.php new file mode 100644 index 0000000..30b4dfc --- /dev/null +++ b/src/Documents/CommentDocument.php @@ -0,0 +1,49 @@ + $this->model->content, + ]; + } + + public function attributes(): array + { + $attributes = [ + 'author' => $this->model->user_id, + 'createdAt' => $this->model->created_at->toAtomString(), + 'is_private' => $this->model->is_private, + 'discussion_id' => $this->model->discussion?->id + ]; + + if ($this->extensionEnabled('flarum-flags')) { + $attributes['flags_count'] = $this->model->flags->count(); + } + + if ($this->extensionEnabled('flarum-approval')) { + $attributes['approved'] = $this->model->is_approved; + } + + return $attributes; + } + + public function serializer(): string + { + return PostSerializer::class; + } + + public function model(): string + { + return CommentPost::class; + } +} diff --git a/src/Documents/DiscussionDocument.php b/src/Documents/DiscussionDocument.php new file mode 100644 index 0000000..1c1943e --- /dev/null +++ b/src/Documents/DiscussionDocument.php @@ -0,0 +1,54 @@ + $this->model->title + ]; + } + + public function attributes(): array + { + $attributes = [ + 'author' => $this->model->user_id, + 'createdAt' => $this->model->created_at->toAtomString(), + 'lastPostedAt' => $this->model->last_posted_at?->toAtomString(), + 'is_private' => $this->model->is_private, + 'first_post_id' => $this->model->first_post_id, + 'last_post_id' => $this->model->last_post_id, + 'commentCount' => $this->model?->comment_count, + 'groups' => $this->groupsForDiscussion($this->model), + ]; + + if ($this->extensionEnabled('fof-byobu')) { + $attributes['recipient-users'] = $this->model->recipientUsers->pluck('id')->toArray(); + $attributes['recipient-groups'] = $this->model->recipientGroups->pluck('id')->toArray(); + } + + if ($this->extensionEnabled('flarum-sticky')) { + $attributes['is_sticky'] = $this->model->is_sticky; + } + + return $attributes; + } + + public function serializer(): string + { + return DiscussionSerializer::class; + } + + public function model(): string + { + return Discussion::class; + } +} diff --git a/src/Documents/Document.php b/src/Documents/Document.php new file mode 100644 index 0000000..e05df4a --- /dev/null +++ b/src/Documents/Document.php @@ -0,0 +1,82 @@ +type() . ':' . $this->model->getKey(); + } + + public function index(): string + { + return resolve('blomstra.search.elastic_index'); + } + + public function type(): string + { + return resolve($this->serializer())->getType($this->model); + } + + abstract public function serializer(): string; + + abstract public function model(): string; + + protected function groupsForDiscussion(Discussion $discussion): array + { + $permissions = collect(); + + $globalPermission = Permission::query() + ->where('permission', 'viewForum') + ->pluck('group_id'); + + if ($this->extensionEnabled('flarum-tags')) { + /** @var Collection $tags */ + $tags = $discussion->tags; + + $filters['tags'] = $tags->pluck('id')->toArray(); + $tagPermissions = Permission::query() + ->whereIn( + 'permission', + $tags->pluck('id')->map(function (int $id) { + return "tag$id.viewForum"; + }) + )->get(); + + $permissions = $tags->map(function (Tag $tag) use ($tagPermissions) { + $permissions = $tagPermissions->where('permission', "tag$tag->id.viewForum"); + + if ($tag->is_restricted) { + $permissions = $permissions->add(['group_id' => Group::ADMINISTRATOR_ID]); + } + + return $permissions->pluck('group_id'); + })->flatten(); + } + + if (! $discussion->is_private && $permissions->isEmpty()) { + $permissions = $globalPermission; + } + + return $permissions->toArray(); + } + + protected function extensionEnabled(string $extension): bool + { + /** @var ExtensionManager $manager */ + $manager = resolve(ExtensionManager::class); + + return $manager->isEnabled($extension); + } +} diff --git a/src/Manager.php b/src/Manager.php new file mode 100644 index 0000000..e421ad0 --- /dev/null +++ b/src/Manager.php @@ -0,0 +1,30 @@ +fromTagged('blomstra.search.documents', $type); + } + + protected function fromTagged(string $binding, $search): ?string + { + $entities = $this->container->tagged($binding); + + foreach ($entities as $entity) { + $instance = $this->container->make($entity); + + if ($instance->type() === $search) return $entity; + } + + return null; + } +} diff --git a/src/Observe/DeletingJob.php b/src/Observe/DeletingJob.php index cdd1e2b..9a341b2 100644 --- a/src/Observe/DeletingJob.php +++ b/src/Observe/DeletingJob.php @@ -3,26 +3,26 @@ namespace Blomstra\Search\Observe; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; use MeiliSearch\Client; class DeletingJob extends Job { - public function __construct(protected string $class, protected Collection $models) - {} - public function handle(Client $meili) { - $schema = $this->getSchema(); + if ($this->models->isEmpty()) return; - if (! $schema) return; + $document = $this->getDocument(); - $keys = $this->models->map(function (Model $model) { - return $model->getKey(); - }); + if (! $document) return; - $meili->index($schema::index())->deleteDocuments( - $keys - ); + $keys = $this->models->map(function (Model $model) use ($document) { + return (new $document)($model)->id(); + })->toArray(); + + $meili + ->index(resolve('blomstra.search.elastic_index')) + ->deleteDocuments( + $keys + ); } } diff --git a/src/Observe/Job.php b/src/Observe/Job.php index 4f3db44..bd02f3d 100644 --- a/src/Observe/Job.php +++ b/src/Observe/Job.php @@ -2,18 +2,26 @@ namespace Blomstra\Search\Observe; -use Blomstra\Search\Schemas\Schema; +use Blomstra\Search\Documents\Document; use Flarum\Queue\AbstractJob; use Illuminate\Contracts\Container\Container; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; abstract class Job extends AbstractJob { - protected function getSchema(): ?Schema - { - $mapping = resolve(Container::class)->tagged('blomstra.search.schemas'); + public function __construct(protected Collection $models) + {} - return collect($mapping)->first(function (Schema $schema) { - return $schema::model() === $this->class; + protected function getDocument(): ?Document + { + $documents = resolve(Container::class)->tagged('blomstra.search.documents'); + + /** @var Model $model */ + $model = $this->models->first(); + + return collect($documents)->first(function (Document $document) use ($model) { + return $document->model() === get_class($model); }); } } diff --git a/src/Observe/SavingJob.php b/src/Observe/SavingJob.php index 6683249..6df3c1c 100644 --- a/src/Observe/SavingJob.php +++ b/src/Observe/SavingJob.php @@ -2,71 +2,76 @@ namespace Blomstra\Search\Observe; +use Blomstra\Search\Documents\Document; use Elasticsearch\Client; use Illuminate\Contracts\Container\Container; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; class SavingJob extends Job { - public function __construct(protected string $class, protected Collection $models) - {} - public function handle(Container $container) { + if ($this->models->isEmpty()) return; + /** @var Client $client */ $client = $container->make('blomstra.search.elastic'); - $schema = $this->getSchema(); + $document = $this->getDocument(); - if (! $schema) return; + if (! $document) return; if($first = $this->models->first()) { - $properties = []; - - foreach (array_keys($schema->fulltext($first)) as $key) { - $properties[$key] = ['type' => 'text']; - } +// $properties = []; +// +// foreach (array_keys($schema->fulltext($first)) as $key) { +// $properties[$key] = ['type' => 'text']; +// } // Set up the index - if (! $client->indices()->exists([ - 'index' => resolve('blomstra.search.elastic_index'), - 'expand_wildcards' => 'none' - ])) { - $client->indices()->create([ - 'index' => $schema::index(), - 'body' => [ - 'mappings' => [ - 'properties' => $properties - ], - 'settings' => [ - 'index' => [ - 'query' => [ - 'default_field' => array_keys($schema->fulltext($first)) - ] - ] - ] - ] - ]); - } +// if (! $client->indices()->exists([ +// 'index' => $schema::index(), +// 'expand_wildcards' => 'none' +// ])) { +// $client->indices()->create([ +// 'index' => $schema::index(), +// 'body' => [ +// 'mappings' => [ +// 'properties' => $properties +// ], +// 'settings' => [ +// 'index' => [ +// 'query' => [ +// 'default_field' => array_keys($schema->fulltext($first)) +// ] +// ] +// ] +// ] +// ]); +// } } // Preparing body for storing. - $body = $this->models->map(function (Model $model) use ($schema) { + $body = $this->models->map(function (Model $model) use ($document) { + /** @var Document $markup */ + $markup = new $document($model); + return [ - ['index' => [ - '_id' => $model->getKey(), '_index' => resolve('blomstra.search.elastic_index')] + [ + 'index' => [ + '_id' => $markup->id(), + '_index' => resolve('blomstra.search.elastic_index') + ] ], array_merge( - ['type' => $schema::type()], - $schema->fulltext($model), - $schema->filters($model)) - + ['type' => $markup->type()], + $markup->fulltext(), + $markup->attributes() + ) ]; })->flatten(1); $response = $client->bulk([ - 'index' => $schema::index(), + 'index' => resolve('blomstra.search.elastic_index'), 'body' => $body->toArray(), 'refresh' => true ]); diff --git a/src/Provider.php b/src/Provider.php index 1cf5e38..b1899e8 100644 --- a/src/Provider.php +++ b/src/Provider.php @@ -2,11 +2,11 @@ namespace Blomstra\Search; +use Blomstra\Search\Documents; use Blomstra\Search\Observe\DeletingJob; use Blomstra\Search\Observe\SavingJob; -use Blomstra\Search\Schemas\CommentPostSchema; -use Blomstra\Search\Schemas\DiscussionSchema; use Blomstra\Search\Schemas\Schema; +use Blomstra\Search\Seeders; use Elasticsearch\ClientBuilder; use Flarum\Foundation\AbstractServiceProvider; use Illuminate\Contracts\Container\Container; @@ -20,7 +20,15 @@ class Provider extends AbstractServiceProvider { public function register() { - $this->container->tag([CommentPostSchema::class], 'blomstra.search.schemas'); + $this->container->tag([ + Documents\CommentDocument::class, + Documents\DiscussionDocument::class + ], 'blomstra.search.documents'); + + $this->container->tag([ + Seeders\CommentSeeder::class, + Seeders\DiscussionSeeder::class + ], 'blomstra.search.seeders'); $config = $this->container->make('flarum.config') ?? []; $elastic = Arr::get($config, 'elastic', []); @@ -45,8 +53,8 @@ class Provider extends AbstractServiceProvider public function boot() { - /** @var array $schemas */ - $schemas = $this->container->tagged('blomstra.search.schemas'); + /** @var array|string[] $seeders */ + $seeders = $this->container->tagged('blomstra.search.seeders'); /** @var Dispatcher $events */ $events = resolve(Dispatcher::class); @@ -54,14 +62,14 @@ class Provider extends AbstractServiceProvider /** @var Queue $queue */ $queue = resolve(Queue::class); - /** @var Schema $schema */ - foreach ($schemas as $schema) { - $schema::savingOn($events, function ($model) use ($schema, $queue) { - $queue->push(new SavingJob($schema::model(), Collection::make([$model]))); + /** @var string|Seeders\Seeder $seeder */ + foreach ($seeders as $seeder) { + $seeder::savingOn($events, function ($model) use ($queue) { + $queue->push(new SavingJob(Collection::make([$model]))); }); - $schema::deletingOn($events, function ($model) use ($schema, $queue) { - $queue->push(new DeletingJob($schema::model(), Collection::make([$model]))); + $seeder::deletingOn($events, function ($model) use ($queue) { + $queue->push(new DeletingJob(Collection::make([$model]))); }); } } diff --git a/src/Schemas/CommentPostSchema.php b/src/Schemas/CommentPostSchema.php index ae7b220..500d170 100644 --- a/src/Schemas/CommentPostSchema.php +++ b/src/Schemas/CommentPostSchema.php @@ -3,7 +3,6 @@ namespace Blomstra\Search\Schemas; use Flarum\Api\Serializer\DiscussionSerializer; -use Flarum\Api\Serializer\PostSerializer; use Flarum\Discussion\Discussion; use Flarum\Post\CommentPost; use Flarum\Post\Event\Deleted; diff --git a/src/Seeders/CommentSeeder.php b/src/Seeders/CommentSeeder.php new file mode 100644 index 0000000..5d1ed02 --- /dev/null +++ b/src/Seeders/CommentSeeder.php @@ -0,0 +1,39 @@ +type; + } + + public function query(): Builder + { + return CommentPost::query() + ->where('type', CommentPost::$type) + ->with('discussion'); + } + + public static function savingOn(Dispatcher $events, callable $callable) + { + $events->listen(Posted::class, function (Posted $event) use ($callable) { + $callable($event->post); + }); + } + + public static function deletingOn(Dispatcher $events, callable $callable) + { + $events->listen(Deleted::class, function (Deleted $event) use ($callable) { + $callable($event->post); + }); + } +} diff --git a/src/Seeders/DiscussionSeeder.php b/src/Seeders/DiscussionSeeder.php new file mode 100644 index 0000000..bdedbb1 --- /dev/null +++ b/src/Seeders/DiscussionSeeder.php @@ -0,0 +1,39 @@ +type; + } + + public function query(): Builder + { + return Discussion::query(); + } + + public static function savingOn(Dispatcher $events, callable $callable) + { + $events->listen([Started::class, Restored::class], function ($event) use ($callable) { + return $callable($event->discussion); + }); + } + + public static function deletingOn(Dispatcher $events, callable $callable) + { + $events->listen([Deleted::class, Hidden::class], function ($event) use ($callable) { + return $callable($event->discussion); + }); + } +} diff --git a/src/Seeders/Seeder.php b/src/Seeders/Seeder.php new file mode 100644 index 0000000..f790ddf --- /dev/null +++ b/src/Seeders/Seeder.php @@ -0,0 +1,16 @@ +