studioespresso / craft-scout

Craft Scout provides a simple solution for adding full-text search to your entries. Scout will automatically keep your search indexes in sync with your entries.
MIT License
81 stars 55 forks source link

[STU-52] TypeError with getSiteById() on multi-site #276

Closed ten1seven closed 11 months ago

ten1seven commented 12 months ago

I recently discovered this TypeError when trying to visit the /scout-indices page:

craft\services\Sites::getSiteById(): Argument #1 ($siteId) must be of type int, array given, called in /var/www/html/vendor/studioespresso/craft-scout/src/utilities/ScoutUtility.php on line 37

PHP: 8.1.22 Craft: 4.4.17 (recently upgraded from Craft 3) Scout: 3.2.1 Running multi-site

I found that if I manually change line 37 in ScoutUtility.php to the ID of the primary site (1) the page loads correctly. What other information would be helpful for debugging?

'site' => $engine->scoutIndex->criteria->siteId === '*' ? 'all' : Craft::$app->getSites()->getSiteById($engine->scoutIndex->criteria->siteId)

to 

'site' => $engine->scoutIndex->criteria->siteId === '*' ? 'all' : Craft::$app->getSites()->getSiteById(1)

(full Yii error report for reference) Screen Shot 2023-10-10 at 07 27 45-fullpage

STU-52

janhenckens commented 12 months ago

@ten1seven Could you share the configuration you have in scout.php? Either way it's something to fix but your specific config would provide a good case to test against. Thanks!

ten1seven commented 12 months ago

Sure!

scout.php

<?php

use modules\hrc\web\services\Search;

return [
    /*
     * Scout listens to numerous Element events to keep them updated in
     * their respective indices. You can disable these and update
     * your indices manually using the commands.
     */
    'sync' => true,

    /*
     *
     * @depcretio
     * By default Scout handles all indexing in a queued job, you can disable
     * this so the indices are updated as soon as the elements are updated
     *
     * Disabling the `queue` option will no longer be supported in the next version of Scout
     *
     */
    'queue' => true,

    /*
     * The connection timeout (in seconds), increase this only if necessary
     */
    'connect_timeout' => 1,

    /*
     * The batch size Scout uses when importing a large amount of elements
     */
    'batch_size' => 1000,

    /*
     * By default Scout will index elements related to the element being save (that are in the same index).
     * Disabling this can improve performance on larger sites that have lots of relations.
     */
    'indexRelations' => true,

    /*
     * The Algolia Application ID, this id can be found in your Algolia Account
     * https://www.algolia.com/api-keys. This id is used to update records.
     */
    'application_id' => '$ALGOLIA_APPLICATION_ID',

    /*
     * The Algolia Admin API key, this key can be found in your Algolia Account
     * https://www.algolia.com/api-keys. This key is used to update records.
     */
    'admin_api_key' => '$ALGOLIA_ADMIN_API_KEY',

    /*
     * The Algolia search API key, this key can be found in your Algolia Account
     * https://www.algolia.com/api-keys. This search key is not used in Scout
     * but can be used through the Scout variable in your template files.
     */
    'search_api_key' => '$ALGOLIA_SEARCH_API_KEY',

    /*
     * A collection of indices that Scout should sync to, these can be configured
     * by using the \rias\scout\ScoutIndex::create('IndexName') command. Each
     * index should define an ElementType, criteria and a transformer.
     */
    'indices' => Search::getIndices(),
];

modules\hrc\web\services\Search.php

<?php

namespace modules\hrc\web\services;

use craft\elements\db\EntryQuery;
use craft\elements\Entry;
use Exception;
use modules\hrc\web\models\SearchTransformer;
use modules\hrc\web\models\SearchTransformerEvents;
use modules\hrc\web\models\SearchTransformerNewsPress;
use rias\scout\IndexSettings;
use rias\scout\ScoutIndex;

class Search
{
    public const SECTION_NEWS = 'news';
    public const SECTION_PRESS_RELEASES = 'pressReleases';
    public const SECTION_RESOURCES = 'resources';
    public const SECTION_EVENTS = 'events';
    public const SECTION_PAGES = 'pages';
    public const SECTION_CAMPAIGNS = 'campaigns';
    public const SECTION_STORIES = 'stories';
    public const SECTION_EQUALITY_INDEXES = 'equalityIndexes';
    public const SECTION_DATA_EXPLORERS = 'dataExplorers';
    public const SECTION_STAFF = 'staff';
    public const SECTION_RESOURCE_TOPICS = 'resourceTopics';
    public const SECTION_MAGAZINE_ARTICLES = 'magazineArticles';

    public const SECTION_FOUNDATION_PAGES = 'foundationPages';
    public const SECTION_FOUNDATION_PROGRAMS = 'programs';
    public const SECTION_FOUNDATION_PROFESSIONAL_RESOURCES = 'professionalResources';

    /**
     * Creates an index name based on the current CRAFT_ENVIRONMENT
     *
     * @param string $name
     * @return string
     */
    public static function getIndexName(string $name): string
    {
        return implode('_', [
            CRAFT_ENVIRONMENT,
            $name,
        ]);
    }

    /**
     * Configure the Scout indices and data for Algolia
     *
     * @return array
     * @throws Exception
     */
    public static function getIndices(): array
    {
        $indices = [];

        $siteSearchConfig = self::_getSiteSearchConfig();

        $hrcIndex = self::getIndexName('hrcSearch');
        $indices[] = ScoutIndex::create($hrcIndex)
            ->elementType(Entry::class)
            ->criteria(function (EntryQuery $query) use ($siteSearchConfig) {
                return $query->section([
                    array_keys($siteSearchConfig)
                ])
                    ->site([
                        'hrc',
                        'hrcES'
                    ]);
            })
            ->transformer(function (Entry $entry) use ($siteSearchConfig) {
                return self::_getTransformer($siteSearchConfig, $entry);
            })
            ->splitElementsOn([
                'content',
            ])
            ->indexSettings(IndexSettings::create()
                ->hitsPerPage(10)
                ->attributeForDistinct('distinctID')
                ->distinct(true)
                ->attributesForFaceting([
                    'distinctID',
                    'type',
                    'topics',
                    'locations',
                    'year',
                ])
                ->replicas([
                    $hrcIndex . '_dateAsc',
                    $hrcIndex . '_dateDesc',
                    $hrcIndex . '_popularity',
                ])
            );

        $foundationSearchConfig = self::_getFoundationSearchConfig();

        $hrcFoundationIndex = self::getIndexName('hrcFoundationSearch');
        $indices[] = ScoutIndex::create($hrcFoundationIndex)
            ->elementType(Entry::class)
            ->criteria(function (EntryQuery $query) use ($foundationSearchConfig) {
                return $query->section(
                    array_keys($foundationSearchConfig)
                )
                    ->site([
                        'hrcFoundation',
                    ]);
            })
            ->transformer(function (Entry $entry) use ($foundationSearchConfig) {
                return self::_getTransformer($foundationSearchConfig, $entry);
            })
            ->splitElementsOn([
                'content',
            ])
            ->indexSettings(IndexSettings::create()
                ->hitsPerPage(10)
                ->attributeForDistinct('distinctID')
                ->distinct(true)
                ->attributesForFaceting([
                    'distinctID',
                    'type',
                    'topics',
                    'locations',
                    'year',
                ])
                ->replicas([
                    $hrcFoundationIndex . '_dateAsc',
                    $hrcFoundationIndex . '_dateDesc',
                ])
            );

        return $indices;
    }

    /**
     * @param array $config
     * @param Entry $entry
     * @return array
     * @throws Exception
     */
    private static function _getTransformer(array $config, Entry $entry): array
    {
        $transformer = $config[$entry->section->handle];

        if ($transformer instanceof SearchTransformer === false) {
            throw new Exception('Transformer config must be of type SearchTransformer');
        }

        return $transformer->getEntryData($entry);
    }

    private static function _getSiteSearchConfig(): array
    {
        return [
            self::SECTION_NEWS => new SearchTransformerNewsPress([
                'type' => 'News',
                'topicFieldHandle' => 'newsTopics',
                'useTopicFacet' => true,
                'useLocationFacet' => true,
                'useYearFacet' => true,
            ]),
            self::SECTION_PRESS_RELEASES => new SearchTransformerNewsPress([
                'type' => 'Press Releases',
                'topicFieldHandle' => 'newsTopics',
                'useTopicFacet' => true,
                'useLocationFacet' => true,
                'useYearFacet' => true,
            ]),
            self::SECTION_RESOURCES => new SearchTransformer([
                'type' => 'Resources',
                'topicFieldHandle' => 'resourceTopics',
                'useTopicFacet' => true,
            ]),
            self::SECTION_EVENTS => new SearchTransformerEvents([
                'type' => 'Events',
                'useLocationFacet' => true,
                'useYearFacet' => true,
            ]),
            self::SECTION_CAMPAIGNS => new SearchTransformer([
                'type' => 'Campaigns',
                'useYearFacet' => true,
            ]),
            self::SECTION_STORIES => new SearchTransformer([
                'type' => 'Campaigns',
                'useYearFacet' => true,
            ]),
            self::SECTION_MAGAZINE_ARTICLES => new SearchTransformer([
                'type' => 'Magazine',
                'useYearFacet' => true,
            ]),
            self::SECTION_EQUALITY_INDEXES => new SearchTransformer([
                'eyebrow' => 'Equality Index'
            ]),
            self::SECTION_DATA_EXPLORERS => new SearchTransformer(),
            self::SECTION_STAFF => new SearchTransformer(),
            self::SECTION_RESOURCE_TOPICS => new SearchTransformer(),
            self::SECTION_PAGES => new SearchTransformer(),
        ];
    }

    private static function _getFoundationSearchConfig(): array
    {
        return [
            self::SECTION_FOUNDATION_PAGES => new SearchTransformer([
                'type' => 'Pages',
            ]),
            self::SECTION_FOUNDATION_PROGRAMS => new SearchTransformer([
                'type' => 'Programs',
            ]),
            self::SECTION_FOUNDATION_PROFESSIONAL_RESOURCES => new SearchTransformer([
                'topicFieldHandle' => 'professionalAudiences',
                'type' => 'Professional Resources',
            ]),
        ];
    }
}