From c6e8c48a4cdc14a41931175de87156b09a3a2657 Mon Sep 17 00:00:00 2001 From: Tom Witkowski Date: Thu, 22 Apr 2021 10:37:40 +0200 Subject: [PATCH 1/5] replace Fuse.js with Algolia for home user search --- .env.example | 4 + app/Console/Kernel.php | 9 ++ app/Http/Policies/ContentSecurityPolicy.php | 1 + app/Models/User.php | 22 +++ composer.json | 2 + config/horizon.php | 9 ++ config/scout.php | 126 ++++++++++++++++++ package.json | 3 +- resources/js/alpine/fuse.js | 30 ----- resources/js/app.js | 13 +- .../views/components/layout/html.blade.php | 5 + .../web/home/user-autocomplete.blade.php | 95 ++++++------- 12 files changed, 232 insertions(+), 87 deletions(-) create mode 100644 config/scout.php delete mode 100644 resources/js/alpine/fuse.js diff --git a/.env.example b/.env.example index b1a8157..f78c2fa 100644 --- a/.env.example +++ b/.env.example @@ -46,3 +46,7 @@ BACKUP_DROPBOX_ACCESS_TOKEN= BACKUP_ARCHIVE_PASSWORD= TRUSTED_PROXIES= + +SCOUT_PREFIX= +ALGOLIA_APP_ID= +ALGOLIA_SECRET= diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 35a4123..6c6ebea 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -8,9 +8,12 @@ use App\Console\Commands\GithubRepositoryDetails; use App\Console\Commands\GithubUserDetails; use App\Console\Commands\GithubUserRepositories; +use App\Models\User; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Laravel\Horizon\Console\SnapshotCommand; +use Laravel\Scout\Console\FlushCommand; +use Laravel\Scout\Console\ImportCommand; use Spatie\Backup\Commands\BackupCommand; use Spatie\Backup\Commands\CleanupCommand; use Spatie\Backup\Commands\MonitorCommand; @@ -33,6 +36,12 @@ protected function schedule(Schedule $schedule): void // laravel/horizon $schedule->command(SnapshotCommand::class)->everyFiveMinutes()->onOneServer()->environments('gorgeous-moon'); + // laravel/scout + $schedule->command(FlushCommand::class, [User::class]) + ->dailyAt('02:00') + ->onOneServer() + ->onSuccess(fn () => $this->call(ImportCommand::class, ['model' => User::class])); + // spatie/laravel-schedule-monitor $schedule->command(CleanLogCommand::class)->dailyAt('01:00')->onOneServer(); diff --git a/app/Http/Policies/ContentSecurityPolicy.php b/app/Http/Policies/ContentSecurityPolicy.php index d6842a4..7020563 100644 --- a/app/Http/Policies/ContentSecurityPolicy.php +++ b/app/Http/Policies/ContentSecurityPolicy.php @@ -18,6 +18,7 @@ public function configure(): void ->addDirective(Directive::CHILD, Keyword::NONE) ->addDirective(Directive::CONNECT, Keyword::SELF) ->addDirective(Directive::CONNECT, 'https://plausible.io/api/event') + ->addDirective(Directive::CONNECT, 'https://*.algolia.net') ->addDirective(Directive::FORM_ACTION, Keyword::SELF) ->addDirective(Directive::FRAME, Keyword::NONE) ->addDirective(Directive::FRAME_ANCESTORS, Keyword::NONE) diff --git a/app/Models/User.php b/app/Models/User.php index 4781e11..8777b6f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -18,6 +18,7 @@ use Illuminate\Notifications\RoutesNotifications; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; +use Laravel\Scout\Searchable; use Spatie\Permission\Traits\HasRoles; use Spatie\Sitemap\Contracts\Sitemapable as SitemapableContract; use Spatie\Sitemap\Tags\Url; @@ -72,6 +73,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac use RoutesNotifications; use Blockable; use HasRoles; + use Searchable; protected const SUPERADMIN_IDS = [ 6187884, // Gummibeer @@ -228,4 +230,24 @@ public function toSitemapTag(): Url return Url::create($this->profile_url) ->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY); } + + public function toSearchableArray(): array + { + return [ + 'name' => $this->name, + 'display_name' => $this->display_name, + 'avatar_url' => $this->avatar_url, + 'profile_url' => $this->profile_url, + ]; + } + + public function shouldBeSearchable(): bool + { + return $this->isRegistered(); + } + + protected function makeAllSearchableUsing(Builder $query): Builder + { + return $query->whereIsRegistered(); + } } diff --git a/composer.json b/composer.json index 1b9d859..ce02faa 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "ext-intl": "*", "ext-json": "*", "ext-redis": "*", + "algolia/algoliasearch-client-php": "^3.0", "astrotomic/laravel-cachable-attributes": "^0.3.0", "astrotomic/laravel-imgix": "^0.2.0", "astrotomic/php-open-graph": "^0.5.2", @@ -22,6 +23,7 @@ "laravel/framework": "^8.12", "laravel/horizon": "^5.7", "laravel/nova": "^3.22", + "laravel/scout": "^8.6", "laravel/socialite": "^5.2", "lorisleiva/cron-translator": "^0.2.0", "mazedlx/laravel-feature-policy": "^1.1", diff --git a/config/horizon.php b/config/horizon.php index 9f7cfa0..69ab6bd 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -219,6 +219,15 @@ // 'tries' => 1, // 'nice' => 0, // ], + 'scout' => [ + 'connection' => 'redis', + 'queue' => 'scout', + 'balance' => 'off', + 'maxProcesses' => 1, + 'memory' => 128, + 'tries' => 1, + 'nice' => 0, + ], 'github' => [ 'connection' => 'redis', 'queue' => 'github', diff --git a/config/scout.php b/config/scout.php new file mode 100644 index 0000000..0c750a2 --- /dev/null +++ b/config/scout.php @@ -0,0 +1,126 @@ + 'algolia', + + /* + |-------------------------------------------------------------------------- + | Index Prefix + |-------------------------------------------------------------------------- + | + | Here you may specify a prefix that will be applied to all search index + | names used by Scout. This prefix may be useful if you have multiple + | "tenants" or applications sharing the same search infrastructure. + | + */ + + // ToDo: https://github.com/laravel/framework/pull/37080 + // 'prefix' => (string) Str::of(env('SCOUT_PREFIX', ''))->whenNotEmpty(fn (Stringable $prefix) => $prefix->finish('_')), + 'prefix' => Str::finish(env('SCOUT_PREFIX', ''), '_'), + + /* + |-------------------------------------------------------------------------- + | Queue Data Syncing + |-------------------------------------------------------------------------- + | + | This option allows you to control if the operations that sync your data + | with your search engines are queued. When this is set to "true" then + | all automatic data syncing will get queued for better performance. + | + */ + + 'queue' => [ + 'queue' => 'scout', + ], + + /* + |-------------------------------------------------------------------------- + | Database Transactions + |-------------------------------------------------------------------------- + | + | This configuration option determines if your data will only be synced + | with your search indexes after every open database transaction has + | been committed, thus preventing any discarded data from syncing. + | + */ + + 'after_commit' => true, + + /* + |-------------------------------------------------------------------------- + | Chunk Sizes + |-------------------------------------------------------------------------- + | + | These options allow you to control the maximum chunk size when you are + | mass importing data into the search engine. This allows you to fine + | tune each of these chunk sizes based on the power of the servers. + | + */ + + 'chunk' => [ + 'searchable' => 500, + 'unsearchable' => 500, + ], + + /* + |-------------------------------------------------------------------------- + | Soft Deletes + |-------------------------------------------------------------------------- + | + | This option allows to control whether to keep soft deleted records in + | the search indexes. Maintaining soft deleted records can be useful + | if your application still needs to search for the records later. + | + */ + + 'soft_delete' => false, + + /* + |-------------------------------------------------------------------------- + | Identify User + |-------------------------------------------------------------------------- + | + | This option allows you to control whether to notify the search engine + | of the user performing the search. This is sometimes useful if the + | engine supports any analytics based on this application's users. + | + | Supported engines: "algolia" + | + */ + + 'identify' => false, + + /* + |-------------------------------------------------------------------------- + | Algolia Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Algolia settings. Algolia is a cloud hosted + | search engine which works great with Scout out of the box. Just plug + | in your application ID and admin API key to get started searching. + | + */ + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID'), + 'secret' => env('ALGOLIA_SECRET'), + 'search_key' => env('ALGOLIA_SEARCH_KEY'), + ], + +]; diff --git a/package.json b/package.json index d28c7ae..a7cb407 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ }, "dependencies": { "@ryangjchandler/alpine-clipboard": "^1.0.0", + "algoliasearch": "^4.9.0", "alpinejs": "^2.8.2", - "fuse.js": "^6.4.6", + "instantsearch.js": "^4.21.0", "tailwind-programming-language-colors": "Astrotomic/tailwind-programming-language-colors#main", "tailwindcss": "^2.0.4", "typeface-raleway": "^1.1.13", diff --git a/resources/js/alpine/fuse.js b/resources/js/alpine/fuse.js deleted file mode 100644 index 0d0a67d..0000000 --- a/resources/js/alpine/fuse.js +++ /dev/null @@ -1,30 +0,0 @@ -import Fuse from 'fuse.js'; - -const AlpineFuse = { - start() { - if (!window.Alpine) { - throw new Error('Alpine is required for `alpine-fuse` to work.') - } - - Alpine.addMagicProperty('fuse', () => { - return function (list, options) { - if (typeof list === 'function') { - list = list() - } - - return Promise.resolve(list) - .then(items => new Fuse(items, options)); - } - }) - } -}; - -const deferrer = window.deferLoadingAlpine || ((callback) => callback()); - -window.deferLoadingAlpine = function (callback) { - AlpineFuse.start(); - - deferrer(callback) -}; - -export default AlpineFuse diff --git a/resources/js/app.js b/resources/js/app.js index 543911d..ae9f1a9 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,5 +1,16 @@ import '@ryangjchandler/alpine-clipboard'; -import './alpine/fuse'; import 'alpinejs'; window.components = {}; + +import algoliasearch from 'algoliasearch/lite'; +import instantsearch from 'instantsearch.js'; +import { connectAutocomplete } from 'instantsearch.js/es/connectors' + +window.algolia = { + searchClient: algoliasearch(window.ALGOLIA_ID, window.ALGOLIA_KEY), + instantsearch, + connectors: { + connectAutocomplete, + } +}; diff --git a/resources/views/components/layout/html.blade.php b/resources/views/components/layout/html.blade.php index 8f05927..2affe17 100644 --- a/resources/views/components/layout/html.blade.php +++ b/resources/views/components/layout/html.blade.php @@ -26,6 +26,11 @@ + + @stack('head') merge(['class' => 'antialiased']) }}> diff --git a/resources/views/components/web/home/user-autocomplete.blade.php b/resources/views/components/web/home/user-autocomplete.blade.php index 5ec00c4..99c9c4f 100644 --- a/resources/views/components/web/home/user-autocomplete.blade.php +++ b/resources/views/components/web/home/user-autocomplete.blade.php @@ -1,8 +1,7 @@ -
@@ -19,76 +18,62 @@ class="block py-2 py-3 px-3 pl-10 w-full text-base placeholder-gray-500 rounded-md border border-gray-300 shadow-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500 sm:flex-1 focus:outline-none" placeholder="Enter an username" autocomplete="off" - x-ref="name" - @keyup.debounce="search" - @search="search" @focus="isFocused = true" /> -
-
    -