diff --git a/.env.example b/.env.example index b1a8157..90369a5 100644 --- a/.env.example +++ b/.env.example @@ -46,3 +46,8 @@ BACKUP_DROPBOX_ACCESS_TOKEN= BACKUP_ARCHIVE_PASSWORD= TRUSTED_PROXIES= + +SCOUT_PREFIX= +ALGOLIA_APP_ID= +ALGOLIA_SEARCH_KEY= +ALGOLIA_SECRET= diff --git a/algolia/scout-local-users.php b/algolia/scout-local-users.php new file mode 100644 index 0000000..fa6fc1e --- /dev/null +++ b/algolia/scout-local-users.php @@ -0,0 +1,170 @@ + ['name', 'display_name'], + + /* + |-------------------------------------------------------------------------- + | Custom Ranking + |-------------------------------------------------------------------------- + | + | Custom Ranking is about leveraging business metrics to effectively rank search + | results - it's crucial for any successful search experience. Make sure that + | only "numeric" attributes are used, such as the number of sales or views. + | + | Supported: Null, Array + | Examples: ['desc(comments_count)', 'desc(views_count)'] + | + */ + + 'customRanking' => null, + + /* + |-------------------------------------------------------------------------- + | Remove Stop Words + |-------------------------------------------------------------------------- + | + | Stop word removal is useful when you have a query in natural language, e.g. + | “what is a record?”. In that case, the engine will remove “what”, “is”, + | before executing the query, and therefore just search for “record”. + | + | Supported: Null, Boolean, Array + | + */ + + 'removeStopWords' => null, + + /* + |-------------------------------------------------------------------------- + | Disable Typo Tolerance + |-------------------------------------------------------------------------- + | + | Algolia provides robust "typo-tolerance" out-of-the-box. This parameter accepts an + | array of attributes for which typo-tolerance should be disabled. This is useful, + | for example, products that might require SKU search without "typo-tolerance". + | + | Supported: Null, Array + | Example: ['id', 'sku', 'reference', 'code'] + | + */ + + 'disableTypoToleranceOnAttributes' => null, + + /* + |-------------------------------------------------------------------------- + | Attributes For Faceting + |-------------------------------------------------------------------------- + | + | Your index comes with no categories. By designating an attribute as a facet, this enables + | Algolia to compute a set of possible values that can later be used to create categories + | or filters. You can also get a count of records that match those values. + | + | Supported: Null, Array + | Example: ['type', 'filterOnly(country)', 'searchable(city)',] + | + */ + + 'attributesForFaceting' => null, + + /* + |-------------------------------------------------------------------------- + | Unretrievable Attributes + |-------------------------------------------------------------------------- + | + | This is particularly important for security or business reasons, where some attributes are + | used only for ranking or other technical purposes, but should never be seen by your end + | users, such as: total_sales, permissions, stock_count, and other private information. + | + | Supported: Null, Array + | Example: ['total_sales', 'permissions', 'stock_count',] + | + */ + + 'unretrievableAttributes' => null, + + /* + |-------------------------------------------------------------------------- + | Ignore Plurals + |-------------------------------------------------------------------------- + | + | Treats singular, plurals, and other forms of declensions as matching terms. When + | enabled, will make the engine consider “car” and “cars”, or “foot” and “feet”, + | equivalent. This is used in conjunction with the "queryLanguages" setting. + | + | Supported: Null, Boolean, Array + | + */ + + 'ignorePlurals' => null, + + /* + |-------------------------------------------------------------------------- + | Query Languages + |-------------------------------------------------------------------------- + | + | Sets the languages to be used by language-specific settings such as + | "removeStopWords" or "ignorePlurals". For optimum relevance, it is + | recommended to only enable languages that are used in your data. + | + | Supported: Null, Array + | Example: ['en', 'fr',] + | + */ + + 'queryLanguages' => ['en'], + + /* + |-------------------------------------------------------------------------- + | Distinct + |-------------------------------------------------------------------------- + | + | Using this attribute, you can limit the number of returned records that contain the same + | value in that attribute. For example, if the distinct attribute is the series_name and + | several hits (Episodes) have the same value for series_name (Laravel From Scratch). + | + | Supported(distinct): Boolean + | Supported(attributeForDistinct): Null, String + | Example(attributeForDistinct): 'slug' + */ + + 'distinct' => null, + 'attributeForDistinct' => null, + + /* + |-------------------------------------------------------------------------- + | Other Settings + |-------------------------------------------------------------------------- + | + | The easiest way to manage your settings is usually to go to your Algolia dashboard because + | it has a nice UI and you can test the relevancy directly there. Once you fine-tuned your + | configuration, just use the command `scout:sync` to get remote settings in this file. + | + */ + 'allowTyposOnNumericTokens' => false, + 'exactOnSingleWordQuery' => 'word', + 'ranking' => [ + 'exact', + 'typo', + 'geo', + 'words', + 'filters', + 'proximity', + 'attribute', + 'custom', + ], +]; diff --git a/algolia/scout-prod-users.php b/algolia/scout-prod-users.php new file mode 100644 index 0000000..fa6fc1e --- /dev/null +++ b/algolia/scout-prod-users.php @@ -0,0 +1,170 @@ + ['name', 'display_name'], + + /* + |-------------------------------------------------------------------------- + | Custom Ranking + |-------------------------------------------------------------------------- + | + | Custom Ranking is about leveraging business metrics to effectively rank search + | results - it's crucial for any successful search experience. Make sure that + | only "numeric" attributes are used, such as the number of sales or views. + | + | Supported: Null, Array + | Examples: ['desc(comments_count)', 'desc(views_count)'] + | + */ + + 'customRanking' => null, + + /* + |-------------------------------------------------------------------------- + | Remove Stop Words + |-------------------------------------------------------------------------- + | + | Stop word removal is useful when you have a query in natural language, e.g. + | “what is a record?”. In that case, the engine will remove “what”, “is”, + | before executing the query, and therefore just search for “record”. + | + | Supported: Null, Boolean, Array + | + */ + + 'removeStopWords' => null, + + /* + |-------------------------------------------------------------------------- + | Disable Typo Tolerance + |-------------------------------------------------------------------------- + | + | Algolia provides robust "typo-tolerance" out-of-the-box. This parameter accepts an + | array of attributes for which typo-tolerance should be disabled. This is useful, + | for example, products that might require SKU search without "typo-tolerance". + | + | Supported: Null, Array + | Example: ['id', 'sku', 'reference', 'code'] + | + */ + + 'disableTypoToleranceOnAttributes' => null, + + /* + |-------------------------------------------------------------------------- + | Attributes For Faceting + |-------------------------------------------------------------------------- + | + | Your index comes with no categories. By designating an attribute as a facet, this enables + | Algolia to compute a set of possible values that can later be used to create categories + | or filters. You can also get a count of records that match those values. + | + | Supported: Null, Array + | Example: ['type', 'filterOnly(country)', 'searchable(city)',] + | + */ + + 'attributesForFaceting' => null, + + /* + |-------------------------------------------------------------------------- + | Unretrievable Attributes + |-------------------------------------------------------------------------- + | + | This is particularly important for security or business reasons, where some attributes are + | used only for ranking or other technical purposes, but should never be seen by your end + | users, such as: total_sales, permissions, stock_count, and other private information. + | + | Supported: Null, Array + | Example: ['total_sales', 'permissions', 'stock_count',] + | + */ + + 'unretrievableAttributes' => null, + + /* + |-------------------------------------------------------------------------- + | Ignore Plurals + |-------------------------------------------------------------------------- + | + | Treats singular, plurals, and other forms of declensions as matching terms. When + | enabled, will make the engine consider “car” and “cars”, or “foot” and “feet”, + | equivalent. This is used in conjunction with the "queryLanguages" setting. + | + | Supported: Null, Boolean, Array + | + */ + + 'ignorePlurals' => null, + + /* + |-------------------------------------------------------------------------- + | Query Languages + |-------------------------------------------------------------------------- + | + | Sets the languages to be used by language-specific settings such as + | "removeStopWords" or "ignorePlurals". For optimum relevance, it is + | recommended to only enable languages that are used in your data. + | + | Supported: Null, Array + | Example: ['en', 'fr',] + | + */ + + 'queryLanguages' => ['en'], + + /* + |-------------------------------------------------------------------------- + | Distinct + |-------------------------------------------------------------------------- + | + | Using this attribute, you can limit the number of returned records that contain the same + | value in that attribute. For example, if the distinct attribute is the series_name and + | several hits (Episodes) have the same value for series_name (Laravel From Scratch). + | + | Supported(distinct): Boolean + | Supported(attributeForDistinct): Null, String + | Example(attributeForDistinct): 'slug' + */ + + 'distinct' => null, + 'attributeForDistinct' => null, + + /* + |-------------------------------------------------------------------------- + | Other Settings + |-------------------------------------------------------------------------- + | + | The easiest way to manage your settings is usually to go to your Algolia dashboard because + | it has a nice UI and you can test the relevancy directly there. Once you fine-tuned your + | configuration, just use the command `scout:sync` to get remote settings in this file. + | + */ + 'allowTyposOnNumericTokens' => false, + 'exactOnSingleWordQuery' => 'word', + 'ranking' => [ + 'exact', + 'typo', + 'geo', + 'words', + 'filters', + 'proximity', + 'attribute', + 'custom', + ], +]; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 35a4123..78922c1 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,6 +2,7 @@ namespace App\Console; +use Algolia\ScoutExtended\Console\Commands\ReImportCommand; use App\Console\Commands\GithubOrganizationDetails; use App\Console\Commands\GithubOrganizationRepositories; use App\Console\Commands\GithubRepositoryContributors; @@ -33,6 +34,9 @@ protected function schedule(Schedule $schedule): void // laravel/horizon $schedule->command(SnapshotCommand::class)->everyFiveMinutes()->onOneServer()->environments('gorgeous-moon'); + // laravel/scout + $schedule->command(ReImportCommand::class)->dailyAt('02:00')->onOneServer(); + // 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..ca91000 100644 --- a/app/Http/Policies/ContentSecurityPolicy.php +++ b/app/Http/Policies/ContentSecurityPolicy.php @@ -18,6 +18,8 @@ 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::CONNECT, 'https://*.algolianet.com') ->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..f672c99 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "ext-intl": "*", "ext-json": "*", "ext-redis": "*", + "algolia/scout-extended": "^1.17", "astrotomic/laravel-cachable-attributes": "^0.3.0", "astrotomic/laravel-imgix": "^0.2.0", "astrotomic/php-open-graph": "^0.5.2", 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..7f96b71 --- /dev/null +++ b/config/scout.php @@ -0,0 +1,127 @@ + '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'), + 'settings_path' => realpath(__DIR__.'/../algolia'), + ], + +]; 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" /> -
-