what i have

This commit is contained in:
Daniel Klabbers 2021-10-13 20:06:27 +02:00
parent 2bf4ec01d0
commit 217e4496be
12 changed files with 227 additions and 141 deletions

View File

@ -11,5 +11,8 @@ return [
->js(__DIR__ . '/js/dist/forum.js'),
(new Flarum\Routes('api'))
->get('blomstra/search', 'blomstra.search', Api\Controllers\SearchController::class)
->get('blomstra/search', 'blomstra.search', Api\Controllers\SearchController::class),
(new Flarum\Console)
->command(Commands\RebuildDocumentsCommand::class)
];

View File

@ -0,0 +1,38 @@
<?php
namespace Blomstra\Search\Commands;
use Blomstra\Search\Observe\SavingJob;
use Blomstra\Search\Schemas\Schema;
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
{
protected $signature = 'blomstra:search:documents:rebuild';
protected $description = 'Rebuilds the complete search server with its documents.';
public function handle(Container $container)
{
/** @var array $schemas */
$schemas = $container->tagged('blomstra.search.schemas');
/** @var Queue $queue */
$queue = $container->make(Queue::class);
/** @var Schema $schema */
foreach ($schemas as $schema) {
/** @var Model $model */
$model = $schema::model();
$model::query()->chunk(50, function (Collection $collection) use ($model, $queue) {
$queue->push(new SavingJob($model, $collection));
$this->info("Pushed {$collection->count()} into the index");
});
}
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace Blomstra\Search;
use Illuminate\Support\Collection;
class Mapping extends Collection
{
}

View File

@ -0,0 +1,28 @@
<?php
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 (! $schema) return;
$keys = $this->models->map(function (Model $model) {
return $model->getKey();
});
$meili->index($schema::index())->deleteDocuments(
$keys
);
}
}

View File

@ -2,33 +2,18 @@
namespace Blomstra\Search\Observe;
use Blomstra\Search\Mapping;
use Blomstra\Search\Searchables\Searchable;
use Blomstra\Search\Schemas\Schema;
use Flarum\Queue\AbstractJob;
use Illuminate\Database\Eloquent\Model;
use MeiliSearch\Client;
use Illuminate\Contracts\Container\Container;
class Job extends AbstractJob
abstract class Job extends AbstractJob
{
public function __construct(protected Model $model)
{}
public function handle(Client $meili, Mapping $mapping)
protected function getSchema(): ?Schema
{
$map = $mapping->get(get_class($this->model));
$mapping = resolve(Container::class)->tagged('blomstra.search.schemas');
/** @var Searchable $searchable */
$searchable = new $map['searchable']($this->model);
$body = array_merge([
$searchable->fulltext()
], [
$this->model->getKeyName() => $this->model->getKey()
]);
$meili->index($map['index'])->addDocuments(
[$body],
$this->model->getKeyName()
);
return collect($mapping)->first(function (Schema $schema) {
return $schema::model() === $this->class;
});
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace Blomstra\Search\Observe;
use Illuminate\Contracts\Queue\Queue;
class Observer
{
public function saved($model)
{
$this->queue()->push(new Job($model));
}
public function deleted($model)
{
}
protected function queue(): Queue
{
return resolve(Queue::class);
}
}

42
src/Observe/SavingJob.php Normal file
View File

@ -0,0 +1,42 @@
<?php
namespace Blomstra\Search\Observe;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use MeiliSearch\Client;
class SavingJob extends Job
{
public function __construct(protected string $class, protected Collection $models)
{}
public function handle(Client $meili)
{
$schema = $this->getSchema();
if (! $schema) return;
if($first = $this->models->first()) {
// Set up the index
$meili->index($schema::index())->updateSettings([
'filterableAttributes' => array_keys($schema->filters($first)),
'searchableAttributes' => array_keys($schema->fulltext($first)),
]);
}
// Preparing body for storing.
$body = $this->models->map(function (Model $model) use ($schema) {
return array_merge(
$schema->fulltext($model),
$schema->filters($model), [
$model->getKeyName() => $model->getKey()
]);
});
$meili->index($schema::index())->addDocuments(
$body->toArray(),
(new $this->class)->getKeyName()
);
}
}

View File

@ -2,29 +2,23 @@
namespace Blomstra\Search;
use Blomstra\Search\Observe\Observer;
use Blomstra\Search\Observe\DeletingJob;
use Blomstra\Search\Observe\SavingJob;
use Blomstra\Search\Schemas\DiscussionSchema;
use Blomstra\Search\Schemas\Schema;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Post\CommentPost;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use MeiliSearch\Client;
class Provider extends AbstractServiceProvider
{
public function register()
{
$this->container->singleton(Mapping::class, function () {
return new Mapping([
\Flarum\Discussion\Discussion::class => [
'index' => 'discussions',
'searchable' => \Blomstra\Search\Searchables\Discussion::class
],
CommentPost::class => [
'index' => 'posts',
'searchable' => null,
]
]);
});
$this->container->tag([DiscussionSchema::class], 'blomstra.search.schemas');
$this->container->singleton(Client::class, function (Container $container) {
$config = $container->make('flarum.config') ?? [];
@ -41,14 +35,24 @@ class Provider extends AbstractServiceProvider
public function boot()
{
/** @var Mapping $searchables */
$searchables = $this->container->get(Mapping::class);
/** @var array $schemas */
$schemas = $this->container->tagged('blomstra.search.schemas');
$searchables->each(function ($mapped, $model) {
forward_static_call(
[$model, 'observe'],
Observer::class
);
});
/** @var Dispatcher $events */
$events = resolve(Dispatcher::class);
/** @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])));
});
$schema::deletingOn($events, function ($model) use ($schema, $queue) {
$queue->push(new DeletingJob($schema::model(), Collection::make([$model])));
});
}
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Blomstra\Search\Schemas;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Event\Deleted;
use Flarum\Discussion\Event\Hidden;
use Flarum\Discussion\Event\Started;
use Illuminate\Contracts\Events\Dispatcher;
class DiscussionSchema extends Schema
{
public function filters(Discussion $discussion): array
{
$filters = [];
if ($this->extensionEnabled('flarum-tags')) {
$filters['tags'] = $discussion->tags->pluck('id')->toArray();
}
return $filters;
}
public function fulltext(Discussion $discussion): array
{
return [
'title' => $discussion->title,
'content' => $discussion->firstPost?->content
];
}
public static function index(): string
{
return 'discussions';
}
public static function model(): string
{
return Discussion::class;
}
public static function savingOn(Dispatcher $events, callable $callable)
{
$events->listen(Started::class, function (Started $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) {
if ($event->discussion->is_private) return;
return $callable($event->discussion);
});
}
}

24
src/Schemas/Schema.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace Blomstra\Search\Schemas;
use Flarum\Extension\ExtensionManager;
use Illuminate\Contracts\Events\Dispatcher;
abstract class Schema
{
protected function extensionEnabled(string $extension): bool
{
/** @var ExtensionManager $manager */
$manager = resolve(ExtensionManager::class);
return $manager->isEnabled($extension);
}
abstract public static function index(): string;
abstract public static function model(): string;
abstract public static function savingOn(Dispatcher $events, callable $callable);
abstract public static function deletingOn(Dispatcher $events, callable $callable);
}

View File

@ -1,32 +0,0 @@
<?php
namespace Blomstra\Search\Searchables;
use Flarum\Discussion\Discussion as Model;
use Flarum\Tags\Tag;
class Discussion extends Searchable
{
public function __construct(
protected Model $discussion
) {}
public function filters(): ?array
{
if ($this->extensionEnabled('flarum-tags')) {
return $this->discussion->tags->map(function (Tag $tag) {
return "tag:$tag->id";
})->toArray();
}
return [];
}
public function fulltext(): ?array
{
return [
'title' => $this->discussion->title,
'content' => $this->discussion->firstPost->content
];
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace Blomstra\Search\Searchables;
use Flarum\Extension\ExtensionManager;
abstract class Searchable
{
/**
* Returns an array of filterable attributes.
*
* @return array|null
*/
abstract public function filters(): ?array;
/**
* An array of strings with which fulltext matching can be executed.
*
* @return array|null
*/
abstract public function fulltext(): ?array;
protected function extensionEnabled(string $extension): bool
{
/** @var ExtensionManager $manager */
$manager = resolve(ExtensionManager::class);
return $manager->isEnabled($extension);
}
}