refactored

This commit is contained in:
Daniel Klabbers 2021-10-20 14:24:44 +02:00
parent 4fa4ccce99
commit 0574307439
15 changed files with 274 additions and 566 deletions

View File

@ -2,33 +2,39 @@
namespace Blomstra\Search\Api\Controllers; namespace Blomstra\Search\Api\Controllers;
use Blomstra\Search\Save\Document as ElasticDocument;
use Blomstra\Search\Elasticsearch\TermsQuery; use Blomstra\Search\Elasticsearch\TermsQuery;
use Blomstra\Search\Schemas\Schema;
use Elasticsearch\Client; use Elasticsearch\Client;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Api\Controller\ListDiscussionsController; use Flarum\Api\Controller\ListDiscussionsController;
use Flarum\Discussion\Filter\DiscussionFilterer; use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Discussion\Discussion;
use Flarum\Extension\ExtensionManager; use Flarum\Extension\ExtensionManager;
use Flarum\Group\Group; use Flarum\Group\Group;
use Flarum\Http\RequestUtil; use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\User\User; use Flarum\User\User;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Str;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Spatie\ElasticsearchQueryBuilder\Aggregations\TermsAggregation; use Spatie\ElasticsearchQueryBuilder\Aggregations\TermsAggregation;
use Spatie\ElasticsearchQueryBuilder\Aggregations\TopHitsAggregation; use Spatie\ElasticsearchQueryBuilder\Aggregations\TopHitsAggregation;
use Spatie\ElasticsearchQueryBuilder\Builder; use Spatie\ElasticsearchQueryBuilder\Builder;
use Spatie\ElasticsearchQueryBuilder\Queries\BoolQuery; use Spatie\ElasticsearchQueryBuilder\Queries\BoolQuery;
use Spatie\ElasticsearchQueryBuilder\Queries\MultiMatchQuery; use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery;
use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery; use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery;
use Spatie\ElasticsearchQueryBuilder\Sorts\Sort; use Spatie\ElasticsearchQueryBuilder\Sorts\Sort;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
class SearchController extends ListDiscussionsController class SearchController extends ListDiscussionsController
{ {
public $serializer = DiscussionSerializer::class;
protected array $translateSort = [
'lastPostedAt' => 'updated_at',
'createdAt' => 'created_at'
];
public function __construct() public function __construct()
{ {
} }
@ -44,17 +50,13 @@ class SearchController extends ListDiscussionsController
$filters = $this->extractFilter($request); $filters = $this->extractFilter($request);
$schema = $this->getSchema($type); $include = array_merge($this->extractInclude($request), ['state']);
$this->serializer = $schema::serializer();
$filterQuery = (BoolQuery::create()) $filterQuery = (BoolQuery::create())
->add( ->add(
BoolQuery::create() BoolQuery::create()
->add(MultiMatchQuery::create($filters['q'], array_keys($schema->fulltext(new ($schema::model()))))) ->add(MatchQuery::create('content', $filters['q']))
->add(TermQuery::create('type', $type))
); );
$builder = (new Builder($client)) $builder = (new Builder($client))
->index(resolve('blomstra.search.elastic_index')) ->index(resolve('blomstra.search.elastic_index'))
->size($this->extractLimit($request)) ->size($this->extractLimit($request))
@ -62,26 +64,78 @@ class SearchController extends ListDiscussionsController
->addQuery( ->addQuery(
$this->addFilters($filterQuery, $actor) $this->addFilters($filterQuery, $actor)
) )
->addAggregation( // ->addAggregation(
TermsAggregation::create('discussions', 'discussion_id') // TermsAggregation::create('discussions', 'discussion_id')
->aggregation(TopHitsAggregation::create('hits', 1)) // ->aggregation(TopHitsAggregation::create('hits', 1))
); // )
;
foreach ($this->extractSort($request) as $field => $direction) { foreach ($this->extractSort($request) as $field => $direction) {
$field = $this->translateSort[$field] ?? $field;
$builder->addSort(new Sort($field, $direction)); $builder->addSort(new Sort($field, $direction));
} }
$result = $builder->search(); $result = $builder->search();
return $schema::results(Arr::get($result, 'hits.hits')); Discussion::setStateUser($actor);
// Eager load groups for use in the policies (isAdmin check)
if (in_array('mostRelevantPost.user', $include)) {
$include[] = 'mostRelevantPost.user.groups';
// If the first level of the relationship wasn't explicitly included,
// add it so the code below can look for it
if (! in_array('mostRelevantPost', $include)) {
$include[] = 'mostRelevantPost';
}
}
$results = Collection::make(Arr::get($result, 'hits.hits'))
->map(function ($hit) {
$id = Str::after($hit['_source']['id'], ':');
$type = $hit['_source']['type'];
if ($type === 'posts') {
/** @var Discussion $discussion */
$discussion = Discussion::whereHas('posts', function ($query) use ($id) {
$query->where('id', $id);
})->first();
$discussion->most_relevant_post_id = $id;
}
if ($type === 'discussions') {
/** @var Discussion $discussion */
$discussion = Discussion::find($id);
$discussion->most_relevant_post_id = $discussion->first_post_id;
}
return $discussion;
})
->keyBy('id')
->unique();
$this->loadRelations($results, $include);
if ($relations = array_intersect($include, ['firstPost', 'lastPost', 'mostRelevantPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
$discussion->$relation->discussion = $discussion;
}
}
}
}
return $results;
} }
protected function getSchema(string $type): ?Schema protected function getDocument(string $type): ?ElasticDocument
{ {
$mapping = resolve(Container::class)->tagged('blomstra.search.schemas'); $documents = resolve(Container::class)->tagged('blomstra.search.documents');
return collect($mapping)->first(function (Schema $schema) use ($type) { return collect($documents)->first(function (ElasticDocument $document) use ($type) {
return $schema::type() === $type; return $document->type() === $type;
}); });
} }
@ -103,12 +157,12 @@ class SearchController extends ListDiscussionsController
if ($actor->is_email_confirmed) $groups->add(Group::MEMBER_ID); if ($actor->is_email_confirmed) $groups->add(Group::MEMBER_ID);
$subQuery = BoolQuery::create() $subQuery = BoolQuery::create()
->add(TermQuery::create('private', 'false')) ->add(TermQuery::create('is_private', 'false'))
->add(TermsQuery::create('groups', $groups->toArray())); ->add(TermsQuery::create('groups', $groups->toArray()));
if ($this->extensionEnabled('fof-byobu') && $actor->exists) { if ($this->extensionEnabled('fof-byobu') && $actor->exists) {
$byobuQuery = BoolQuery::create() $byobuQuery = BoolQuery::create()
->add(TermQuery::create('private', 'true'), 'should') ->add(TermQuery::create('is_private', 'true'), 'should')
->add( ->add(
BoolQuery::create() BoolQuery::create()
->add(TermsQuery::create('recipient-groups', $groups->toArray())) ->add(TermsQuery::create('recipient-groups', $groups->toArray()))

View File

@ -12,7 +12,10 @@ use Illuminate\Database\Eloquent\Collection;
class RebuildDocumentsCommand extends Command class RebuildDocumentsCommand extends Command
{ {
protected $signature = 'blomstra:search:documents:rebuild {--flush : Flushes ALL the documents inside the index}'; protected $signature = 'blomstra:search:documents:rebuild
{--flush : Flushes ALL the documents inside the index}
{--mapping : Create property mappings}
{--max-id= : Limits for each object the number of items to seed}';
protected $description = 'Rebuilds the complete search server with its documents.'; protected $description = 'Rebuilds the complete search server with its documents.';
public function handle(Container $container) public function handle(Container $container)
@ -27,20 +30,47 @@ class RebuildDocumentsCommand extends Command
$client = $container->make('blomstra.search.elastic'); $client = $container->make('blomstra.search.elastic');
// Flush the index. // Flush the index.
if ($this->option('flush')) $client->indices()->delete([ if ($this->option('flush')) {
'index' => $container->make('blomstra.search.elastic_index') $client->indices()->delete([
]); 'index' => $container->make('blomstra.search.elastic_index'),
'ignore_unavailable' => true
]);
$client->indices()->create([
'index' => $container->make('blomstra.search.elastic_index')
]);
}
if ($this->option('mapping')) {
$client->indices()->putMapping([
'index' => $container->make('blomstra.search.elastic_index'),
'body' => [
'properties' => [
'content' => ['type' => 'text'],
'created_at' => ['type' => 'date'],
'updated_at' => ['type' => 'date'],
'is_private' => ['type' => 'boolean'],
'is_sticky' => ['type' => 'boolean'],
'groups' => ['type' => 'integer'],
'recipient_groups' => ['type' => 'integer'],
'recipient_users' => ['type' => 'integer'],
]
]
]);
}
/** @var Seeder $seeder */ /** @var Seeder $seeder */
foreach ($seeders as $seeder) { foreach ($seeders as $seeder) {
$seeder->query()->chunk(50, function (Collection $collection) use ($queue, &$total) { $seeder->query()
->when($this->option('max-id'), function ($query, $id) {
$query->where('id', '<=', $id);
})
->chunk(50, function (Collection $collection) use ($queue, &$total, $seeder) {
$queue->push(new SavingJob($collection, $seeder));
$queue->push(new SavingJob($collection)); $this->info("Pushed into the index, type: {$seeder->type()}, amount: {$collection->count()}.");
$this->info("Pushed {$collection->count()} into the index."); $total += $collection->count();
});
$total += $collection->count();
});
$this->info("Pushed a total of $total into the index."); $this->info("Pushed a total of $total into the index.");
} }

View File

@ -1,49 +0,0 @@
<?php
namespace Blomstra\Search\Documents;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Post\CommentPost;
class CommentDocument extends Document
{
public function __construct(protected CommentPost $model)
{}
public function fulltext(): array
{
return [
'content' => $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;
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace Blomstra\Search\Documents;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
class DiscussionDocument extends Document
{
public function __construct(protected Discussion $model)
{}
public function fulltext(): array
{
return [
'content' => $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;
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace Blomstra\Search\Documents;
use Flarum\Discussion\Discussion;
use Flarum\Extension\ExtensionManager;
use Flarum\Group\Group;
use Flarum\Group\Permission;
use Flarum\Tags\Tag;
use Illuminate\Database\Eloquent\Collection;
abstract class Document
{
abstract public function fulltext(): array;
abstract public function attributes(): array;
public function id(): string
{
return $this->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);
}
}

View File

@ -2,26 +2,16 @@
namespace Blomstra\Search\Observe; namespace Blomstra\Search\Observe;
use Blomstra\Search\Documents\Document; use Blomstra\Search\Seeders\Seeder;
use Flarum\Queue\AbstractJob; use Flarum\Queue\AbstractJob;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
abstract class Job extends AbstractJob abstract class Job extends AbstractJob
{ {
public function __construct(protected Collection $models) protected string $index;
{}
protected function getDocument(): ?Document public function __construct(protected Collection $models, protected Seeder $seeder)
{ {
$documents = resolve(Container::class)->tagged('blomstra.search.documents'); $this->index = resolve('blomstra.search.elastic_index');
/** @var Model $model */
$model = $this->models->first();
return collect($documents)->first(function (Document $document) use ($model) {
return $document->model() === get_class($model);
});
} }
} }

View File

@ -2,7 +2,6 @@
namespace Blomstra\Search\Observe; namespace Blomstra\Search\Observe;
use Blomstra\Search\Documents\Document;
use Elasticsearch\Client; use Elasticsearch\Client;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -16,62 +15,19 @@ class SavingJob extends Job
/** @var Client $client */ /** @var Client $client */
$client = $container->make('blomstra.search.elastic'); $client = $container->make('blomstra.search.elastic');
$document = $this->getDocument();
if (! $document) return;
if($first = $this->models->first()) {
// $properties = [];
//
// foreach (array_keys($schema->fulltext($first)) as $key) {
// $properties[$key] = ['type' => 'text'];
// }
// Set up the index
// 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. // Preparing body for storing.
$body = $this->models->map(function (Model $model) use ($document) { $body = $this->models->map(function (Model $model) {
/** @var Document $markup */ $document = $this->seeder->toDocument($model);
$markup = new $document($model);
return [ return [
[ ['index' => ['_index' => $this->index, '_id' => $document->id]],
'index' => [ $document->toArray()
'_id' => $markup->id(),
'_index' => resolve('blomstra.search.elastic_index')
]
],
array_merge(
['type' => $markup->type()],
$markup->fulltext(),
$markup->attributes()
)
]; ];
})->flatten(1); })
->flatten(1);
$response = $client->bulk([ $response = $client->bulk([
'index' => resolve('blomstra.search.elastic_index'), 'index' => $this->index,
'body' => $body->toArray(), 'body' => $body->toArray(),
'refresh' => true 'refresh' => true
]); ]);

View File

@ -2,10 +2,8 @@
namespace Blomstra\Search; namespace Blomstra\Search;
use Blomstra\Search\Documents;
use Blomstra\Search\Observe\DeletingJob; use Blomstra\Search\Observe\DeletingJob;
use Blomstra\Search\Observe\SavingJob; use Blomstra\Search\Observe\SavingJob;
use Blomstra\Search\Schemas\Schema;
use Blomstra\Search\Seeders; use Blomstra\Search\Seeders;
use Elasticsearch\ClientBuilder; use Elasticsearch\ClientBuilder;
use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\AbstractServiceProvider;
@ -21,13 +19,8 @@ class Provider extends AbstractServiceProvider
public function register() public function register()
{ {
$this->container->tag([ $this->container->tag([
Documents\CommentDocument::class, Seeders\DiscussionSeeder::class,
Documents\DiscussionDocument::class
], 'blomstra.search.documents');
$this->container->tag([
Seeders\CommentSeeder::class, Seeders\CommentSeeder::class,
Seeders\DiscussionSeeder::class
], 'blomstra.search.seeders'); ], 'blomstra.search.seeders');
$config = $this->container->make('flarum.config') ?? []; $config = $this->container->make('flarum.config') ?? [];
@ -64,12 +57,12 @@ class Provider extends AbstractServiceProvider
/** @var string|Seeders\Seeder $seeder */ /** @var string|Seeders\Seeder $seeder */
foreach ($seeders as $seeder) { foreach ($seeders as $seeder) {
$seeder::savingOn($events, function ($model) use ($queue) { $seeder::savingOn($events, function ($model) use ($queue, $seeder) {
$queue->push(new SavingJob(Collection::make([$model]))); $queue->push(new SavingJob(Collection::make([$model]), $seeder));
}); });
$seeder::deletingOn($events, function ($model) use ($queue) { $seeder::deletingOn($events, function ($model) use ($queue, $seeder) {
$queue->push(new DeletingJob(Collection::make([$model]))); $queue->push(new DeletingJob(Collection::make([$model]), $seeder));
}); });
} }
} }

23
src/Save/Document.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace Blomstra\Search\Save;
use Carbon\Carbon;
use Illuminate\Support\Fluent;
/**
* @property string $type
* @property string $id
* @property string $content
* @property Carbon $created_at
* @property Carbon $updated_at
* @property int $user_id
* @property bool $is_private
* @property bool $is_sticky
* @property array|int[] $groups
* @property array|int[] $recipient_groups
* @property array|int[] $recipient_users
*/
class Document extends Fluent
{
}

View File

@ -1,91 +0,0 @@
<?php
namespace Blomstra\Search\Schemas;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Post\CommentPost;
use Flarum\Post\Event\Deleted;
use Flarum\Post\Event\Posted;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class CommentPostSchema extends Schema
{
public function filters(CommentPost $post): array
{
$filters = [
'type' => CommentPost::$type,
'author' => $post->user_id,
'createdAt' => $post->created_at->toAtomString(),
'private' => $post->is_private,
'discussion_id' => $post->discussion?->id
];
if ($this->extensionEnabled('flarum-flags')) {
$filters['flags_count'] = $post->flags->count();
}
if ($this->extensionEnabled('flarum-approval')) {
$filters['approved'] = $post->is_approved;
}
return $filters;
}
public function fulltext(CommentPost $post): array
{
return [
'content' => $post->content,
];
}
public static function model(): string
{
return CommentPost::class;
}
public static function query(): Builder
{
return CommentPost::query()
->where('type', CommentPost::$type)
->with('discussion');
}
public static function results(array $hits): \Illuminate\Database\Eloquent\Collection
{
$postIds = Collection::make($hits)->keyBy('_source.discussion_id')->pluck('_id');
$discussionIds = Collection::make($hits)->pluck('_source.discussion_id');
return Discussion::query()->findMany($discussionIds)->map(function (Discussion $discussion) use ($postIds) {
$discussion->most_relevant_post_id = $postIds->get($discussion->id);
return $discussion;
})->load('mostRelevantPost');
}
public static function serializer(): string
{
return DiscussionSerializer::class;
}
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);
});
}
public static function type(): string
{
return CommentPost::$type;
}
}

View File

@ -1,104 +0,0 @@
<?php
namespace Blomstra\Search\Schemas;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Event\Deleted;
use Flarum\Discussion\Event\Hidden;
use Flarum\Discussion\Event\Restored;
use Flarum\Discussion\Event\Started;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
class DiscussionSchema extends Schema
{
public function filters(Discussion $discussion): array
{
$filters = [
'type' => 'discussions',
'author' => $discussion->user_id,
'createdAt' => $discussion->created_at->toAtomString(),
'lastPostedAt' => $discussion->last_posted_at->toAtomString(),
'private' => $discussion->is_private,
'first_post_id' => $discussion->first_post_id,
'last_post_id' => $discussion->last_post_id,
'commentCount' => $discussion?->comment_count,
'groups' => $this->groupsForDiscussion($discussion),
];
if ($this->extensionEnabled('fof-byobu')) {
$filters['recipient-users'] = $discussion->recipientUsers->pluck('id')->toArray();
$filters['recipient-groups'] = $discussion->recipientGroups->pluck('id')->toArray();
}
if ($this->extensionEnabled('flarum-sticky')) {
$filters['is_sticky'] = $discussion->is_sticky;
}
return $filters;
}
public static function relations()
{
return [
'discussion_id' => [
'type' => 'discussion'
]
];
}
public function fulltext(Discussion $discussion): array
{
return [
'title' => $discussion->title
];
}
public static function model(): string
{
return Discussion::class;
}
public static function serializer(): string
{
return DiscussionSerializer::class;
}
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);
});
}
public static function query(): Builder
{
return Discussion::query();
}
public static function results(array $hits): Collection
{
$postIds = \Illuminate\Support\Collection::make($hits)->keyBy('_source.discussion_id')->pluck('_id');
$discussionIds = Collection::make($hits)->pluck('_source.discussion_id');
return Discussion::query()->findMany($discussionIds)->map(function (Discussion $discussion) use ($postIds) {
$discussion->most_relevant_post_id = $postIds->get($discussion->id);
return $discussion;
})->load('mostRelevantPost');
}
public static function type(): string
{
return 'discussion';
}
}

View File

@ -1,73 +0,0 @@
<?php
namespace Blomstra\Search\Schemas;
use Flarum\Discussion\Discussion;
use Flarum\Extension\ExtensionManager;
use Flarum\Group\Group;
use Flarum\Group\Permission;
use Flarum\Tags\Tag;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
abstract class Schema
{
protected function extensionEnabled(string $extension): bool
{
/** @var ExtensionManager $manager */
$manager = resolve(ExtensionManager::class);
return $manager->isEnabled($extension);
}
abstract public static function type(): string;
abstract public static function model(): string;
abstract public static function query(): Builder;
abstract public static function results(array $hits): Collection;
abstract public static function serializer(): string;
abstract public static function savingOn(Dispatcher $events, callable $callable);
abstract public static function deletingOn(Dispatcher $events, callable $callable);
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();
}
}

View File

@ -2,18 +2,20 @@
namespace Blomstra\Search\Seeders; namespace Blomstra\Search\Seeders;
use Blomstra\Search\Save\Document;
use Flarum\Api\Serializer\PostSerializer; use Flarum\Api\Serializer\PostSerializer;
use Flarum\Post\CommentPost; use Flarum\Post\CommentPost;
use Flarum\Post\Event\Deleted; use Flarum\Post\Event\Deleted;
use Flarum\Post\Event\Posted; use Flarum\Post\Event\Posted;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class CommentSeeder extends Seeder class CommentSeeder extends Seeder
{ {
public function type(): string public function type(): string
{ {
return resolve(PostSerializer::class)->type; return resolve(PostSerializer::class)->getType(new CommentPost);
} }
public function query(): Builder public function query(): Builder
@ -36,4 +38,29 @@ class CommentSeeder extends Seeder
$callable($event->post); $callable($event->post);
}); });
} }
/**
* @param CommentPost $model
* @return Document
*/
public function toDocument(Model $model): Document
{
$document = new Document([
'type' => $this->type(),
'id' => $this->type() . ':' . $model->id,
'content' => $model->content,
'created_at' => $model->created_at?->toAtomString(),
'updated_at' => $model->edited_at?->toAtomString(),
'is_private' => $model->is_private,
'user_id' => $model->user_id,
'groups' => $this->groupsForDiscussion($model->discussion)
]);
if ($this->extensionEnabled('fof-byobu')) {
$document['recipient_users'] = $model->discussion->recipientUsers->pluck('id')->toArray();
$document['recipient_groups'] = $model->discussion->recipientGroups->pluck('id')->toArray();
}
return $document;
}
} }

View File

@ -2,6 +2,7 @@
namespace Blomstra\Search\Seeders; namespace Blomstra\Search\Seeders;
use Blomstra\Search\Save\Document;
use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion; use Flarum\Discussion\Discussion;
use Flarum\Discussion\Event\Deleted; use Flarum\Discussion\Event\Deleted;
@ -10,12 +11,13 @@ use Flarum\Discussion\Event\Restored;
use Flarum\Discussion\Event\Started; use Flarum\Discussion\Event\Started;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class DiscussionSeeder extends Seeder class DiscussionSeeder extends Seeder
{ {
public function type(): string public function type(): string
{ {
return resolve(DiscussionSerializer::class)->type; return resolve(DiscussionSerializer::class)->getType(new Discussion);
} }
public function query(): Builder public function query(): Builder
@ -36,4 +38,33 @@ class DiscussionSeeder extends Seeder
return $callable($event->discussion); return $callable($event->discussion);
}); });
} }
/**
* @param Discussion $model
* @return Document
*/
public function toDocument(Model $model): Document
{
$document = new Document([
'type' => $this->type(),
'id' => $this->type() . ':' . $model->id,
'content' => $model->title,
'created_at' => $model->created_at?->toAtomString(),
'updated_at' => $model->last_posted_at?->toAtomString(),
'is_private' => $model->is_private,
'user_id' => $model->user_id,
'groups' => $this->groupsForDiscussion($model)
]);
if ($this->extensionEnabled('fof-byobu')) {
$document['recipient_users'] = $model->recipientUsers->pluck('id')->toArray();
$document['recipient_groups'] = $model->recipientGroups->pluck('id')->toArray();
}
if ($this->extensionEnabled('flarum-sticky')) {
$document['is_sticky'] = $model->is_sticky;
}
return $document;
}
} }

View File

@ -2,8 +2,16 @@
namespace Blomstra\Search\Seeders; namespace Blomstra\Search\Seeders;
use Blomstra\Search\Save\Document;
use Flarum\Discussion\Discussion;
use Flarum\Extension\ExtensionManager;
use Flarum\Group\Group;
use Flarum\Group\Permission;
use Flarum\Tags\Tag;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
abstract class Seeder abstract class Seeder
{ {
@ -13,4 +21,53 @@ abstract class Seeder
abstract public static function savingOn(Dispatcher $events, callable $callable); abstract public static function savingOn(Dispatcher $events, callable $callable);
abstract public static function deletingOn(Dispatcher $events, callable $callable); abstract public static function deletingOn(Dispatcher $events, callable $callable);
abstract public function toDocument(Model $model): Document;
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);
}
} }