feat: index and sync view_count for fof/discussion-views sort support
- Rename SavingJob to UpdateSearchJob - Add ViewsSearchJob for lightweight partial view_count updates - Add probabilistic sync throttle in DiscussionSeeder::viewingOn() to avoid an ES update on every page view - Add view_count to mapping, DiscussionSeeder::toDocument(), and translateSort - Log a warning and skip instead of 500ing on unknown sort fields
This commit is contained in:
parent
ff2a252e99
commit
80d66c98c4
|
|
@ -48,6 +48,7 @@ class SearchController extends ListDiscussionsController
|
||||||
'lastPostedAt' => 'updated_at',
|
'lastPostedAt' => 'updated_at',
|
||||||
'createdAt' => 'created_at',
|
'createdAt' => 'created_at',
|
||||||
'commentCount' => 'comment_count',
|
'commentCount' => 'comment_count',
|
||||||
|
'view_count' => 'view_count',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected Collection $searchers;
|
protected Collection $searchers;
|
||||||
|
|
@ -107,9 +108,19 @@ class SearchController extends ListDiscussionsController
|
||||||
$this->addFilters($filterQuery, $actor, $filters)
|
$this->addFilters($filterQuery, $actor, $filters)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$knownSortFields = array_merge(array_values($this->translateSort), ['rawId']);
|
||||||
|
|
||||||
foreach ($this->extractSort($request) as $field => $direction) {
|
foreach ($this->extractSort($request) as $field => $direction) {
|
||||||
$field = $this->translateSort[$field] ?? $field;
|
$translated = $this->translateSort[$field] ?? $field;
|
||||||
$builder->addSort(new Sort($field, $direction));
|
|
||||||
|
if (!in_array($translated, $knownSortFields)) {
|
||||||
|
resolve(\Psr\Log\LoggerInterface::class)->warning(
|
||||||
|
"blomstra/search: unknown sort field \"{$field}\", ignoring."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->addSort(new Sort($translated, $direction));
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = $builder->search();
|
$response = $builder->search();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
namespace Blomstra\Search\Commands;
|
namespace Blomstra\Search\Commands;
|
||||||
|
|
||||||
use Blomstra\Search\Jobs\Job;
|
use Blomstra\Search\Jobs\Job;
|
||||||
use Blomstra\Search\Jobs\SavingJob;
|
use Blomstra\Search\Jobs\UpdateSearchJob;
|
||||||
use Blomstra\Search\Seeders\Seeder;
|
use Blomstra\Search\Seeders\Seeder;
|
||||||
use Elasticsearch\Client;
|
use Elasticsearch\Client;
|
||||||
use Flarum\Settings\SettingsRepositoryInterface;
|
use Flarum\Settings\SettingsRepositoryInterface;
|
||||||
|
|
@ -71,6 +71,7 @@ class BuildCommand extends Command
|
||||||
'recipient_groups' => ['type' => 'integer'],
|
'recipient_groups' => ['type' => 'integer'],
|
||||||
'recipient_users' => ['type' => 'integer'],
|
'recipient_users' => ['type' => 'integer'],
|
||||||
'comment_count' => ['type' => 'integer'],
|
'comment_count' => ['type' => 'integer'],
|
||||||
|
'view_count' => ['type' => 'integer'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -181,7 +182,7 @@ class BuildCommand extends Command
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($collection->isNotEmpty()) {
|
if ($collection->isNotEmpty()) {
|
||||||
$queue->pushOn(Job::$onQueue, new SavingJob($collection, $seeder));
|
$queue->pushOn(Job::$onQueue, new UpdateSearchJob($collection, $seeder));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info("IDs {$rangeFrom}–{$rangeTo} | type: {$seeder->type()} | queued: {$collection->count()}.");
|
$this->info("IDs {$rangeFrom}–{$rangeTo} | type: {$seeder->type()} | queued: {$collection->count()}.");
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use Elasticsearch\Client;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class SavingJob extends Job
|
class UpdateSearchJob extends Job
|
||||||
{
|
{
|
||||||
public function handle(Client $client)
|
public function handle(Client $client)
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of blomstra/search.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022 Blomstra Ltd.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE.md
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Blomstra\Search\Jobs;
|
||||||
|
|
||||||
|
use Elasticsearch\Client;
|
||||||
|
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||||
|
use Flarum\Discussion\Discussion;
|
||||||
|
use Flarum\Queue\AbstractJob;
|
||||||
|
|
||||||
|
class ViewsSearchJob extends AbstractJob
|
||||||
|
{
|
||||||
|
protected string $index;
|
||||||
|
|
||||||
|
public function __construct(protected int $discussionId)
|
||||||
|
{
|
||||||
|
$this->index = resolve('blomstra.search.elastic_index');
|
||||||
|
|
||||||
|
if (Job::$onQueue) {
|
||||||
|
$this->onQueue(Job::$onQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(Client $client): void
|
||||||
|
{
|
||||||
|
$discussion = Discussion::find($this->discussionId);
|
||||||
|
|
||||||
|
if (!$discussion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = resolve(DiscussionSerializer::class)->getType(new Discussion());
|
||||||
|
|
||||||
|
$client->update([
|
||||||
|
'index' => $this->index,
|
||||||
|
'id' => "$type:{$this->discussionId}",
|
||||||
|
'retry_on_conflict' => 3,
|
||||||
|
'ignore' => [404],
|
||||||
|
'body' => [
|
||||||
|
'doc' => ['view_count' => (int) $discussion->view_count],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ namespace Blomstra\Search;
|
||||||
|
|
||||||
use Blomstra\Search\Jobs\DeletingJob;
|
use Blomstra\Search\Jobs\DeletingJob;
|
||||||
use Blomstra\Search\Jobs\Job;
|
use Blomstra\Search\Jobs\Job;
|
||||||
use Blomstra\Search\Jobs\SavingJob;
|
use Blomstra\Search\Jobs\UpdateSearchJob;
|
||||||
|
use Blomstra\Search\Jobs\ViewsSearchJob;
|
||||||
use Elasticsearch\Client as Elastic;
|
use Elasticsearch\Client as Elastic;
|
||||||
use Elasticsearch\ClientBuilder;
|
use Elasticsearch\ClientBuilder;
|
||||||
use Flarum\Api\Client;
|
use Flarum\Api\Client;
|
||||||
|
|
@ -108,12 +109,18 @@ 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) {
|
$seeder::savingOn($events, function ($model) use ($queue, $seeder) {
|
||||||
$queue->pushOn(Job::$onQueue, new SavingJob(Collection::make([$model]), $seeder));
|
$queue->pushOn(Job::$onQueue, new UpdateSearchJob(Collection::make([$model]), $seeder));
|
||||||
});
|
});
|
||||||
|
|
||||||
$seeder::deletingOn($events, function ($model) use ($queue, $seeder) {
|
$seeder::deletingOn($events, function ($model) use ($queue, $seeder) {
|
||||||
$queue->pushOn(Job::$onQueue, new DeletingJob(Collection::make([$model]), $seeder));
|
$queue->pushOn(Job::$onQueue, new DeletingJob(Collection::make([$model]), $seeder));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (method_exists($seeder, 'viewingOn')) {
|
||||||
|
$seeder::viewingOn($events, function (int $discussionId) use ($queue) {
|
||||||
|
$queue->pushOn(Job::$onQueue, new ViewsSearchJob($discussionId));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ 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 as Core;
|
use Flarum\Discussion\Event as Core;
|
||||||
|
use Flarum\Extension\ExtensionManager;
|
||||||
use FoF\Byobu\Events as Byobu;
|
use FoF\Byobu\Events as Byobu;
|
||||||
|
use FoF\DiscussionViews\Events\DiscussionWasViewed;
|
||||||
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;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -58,6 +60,27 @@ class DiscussionSeeder extends Seeder
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function viewingOn(Dispatcher $events, callable $callable): void
|
||||||
|
{
|
||||||
|
if (!resolve(ExtensionManager::class)->isEnabled('fof-discussion-views')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$events->listen(DiscussionWasViewed::class, function (DiscussionWasViewed $event) use ($callable) {
|
||||||
|
$viewCount = $event->discussion->view_count;
|
||||||
|
|
||||||
|
$shouldSync = match (true) {
|
||||||
|
$viewCount < 15 => true,
|
||||||
|
$viewCount < 100 => rand(1, 3) === 1,
|
||||||
|
default => rand(1, 19) === 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($shouldSync) {
|
||||||
|
$callable($event->discussion->id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static function deletingOn(Dispatcher $events, callable $callable)
|
public static function deletingOn(Dispatcher $events, callable $callable)
|
||||||
{
|
{
|
||||||
$events->listen([
|
$events->listen([
|
||||||
|
|
@ -107,6 +130,10 @@ class DiscussionSeeder extends Seeder
|
||||||
$document['is_sticky'] = (bool) $model->is_sticky;
|
$document['is_sticky'] = (bool) $model->is_sticky;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->extensionEnabled('fof-discussion-views')) {
|
||||||
|
$document['view_count'] = (int) ($model->view_count ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
return $document;
|
return $document;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue