what i have
This commit is contained in:
parent
2bf4ec01d0
commit
217e4496be
|
|
@ -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)
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Mapping extends Collection
|
||||
{
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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])));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue