Skip to content

Commit 5d8ac94

Browse files
committed
Score elements on a per-site basis
Resolves #13801 Resolves #13978
1 parent 567747b commit 5d8ac94

File tree

4 files changed

+43
-25
lines changed

4 files changed

+43
-25
lines changed

CHANGELOG-WIP.md

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
- The `assets/move-asset` and `assets/move-folder` actions no longer include `success` keys in responses. ([#12159](https://github.com/craftcms/cms/pull/12159))
8585
- The `assets/upload` controller action now includes `errors` object in failure responses. ([#12159](https://github.com/craftcms/cms/pull/12159))
8686
- Element action triggers’ `validateSelection()` and `activate()` methods are now passed an `elementIndex` argument, with a reference to the trigger’s corresponding element index.
87+
- Element search scores set on `craft\events\SearchEvent::$scores` by `craft\services\Search::EVENT_AFTER_SEARCH` or `EVENT_BEFORE_SCORE_RESULTS` now must be indexed by element ID and site ID (e.g. `'100-1'`).
8788
- Added `craft\auth\methods\AuthMethodInterface`.
8889
- Added `craft\auth\methods\BaseAuthMethod`.
8990
- Added `craft\auth\methods\RecoveryCodes`.
@@ -475,3 +476,4 @@
475476
- Improved the initial page load performance for element edit pages that contain Matrix fields.
476477
- Improved the performance of autosaves for elements with newly-created Matrix entries.
477478
- Slugs are no longer required for elements that don’t have a URI format that contains `slug`.
479+
- Fixed a bug where multi-site element queries weren’t scoring elements on a per-site basis. ([#13801](https://github.com/craftcms/cms/discussions/13801))

src/elements/db/ElementQuery.php

+28-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use yii\base\ArrayableTrait;
4444
use yii\base\Exception;
4545
use yii\base\InvalidArgumentException;
46+
use yii\base\InvalidValueException;
4647
use yii\base\NotSupportedException;
4748
use yii\db\Connection as YiiConnection;
4849
use yii\db\Expression;
@@ -524,7 +525,7 @@ class ElementQuery extends Query implements ElementQueryInterface
524525
private ?array $_resultCriteria = null;
525526

526527
/**
527-
* @var int[]|null
528+
* @var array<string,int>|null
528529
* @see _applySearchParam()
529530
* @see _applyOrderByParams()
530531
* @see populate()
@@ -2984,10 +2985,32 @@ private function _applyOrderByParams(YiiConnection $db): void
29842985

29852986
// swap `score` direction value with a fixed order expression
29862987
if (isset($this->_searchResults)) {
2987-
if (!$db instanceof Connection) {
2988-
throw new Exception('The database connection doesn’t support fixed ordering.');
2989-
}
2990-
$orderBy['score'] = new FixedOrderExpression('elements.id', array_keys($this->_searchResults), $db);
2988+
$scoreSql = 'CASE';
2989+
$scoreParams = [];
2990+
$paramSuffix = StringHelper::randomString(10);
2991+
$keys = array_keys($this->_searchResults);
2992+
if ($this->inReverse) {
2993+
$keys = array_reverse($keys);
2994+
}
2995+
$i = -1;
2996+
foreach ($keys as $i => $key) {
2997+
[$elementId, $siteId] = array_pad(explode('-', $key, 2), 2, null);
2998+
if ($siteId === null) {
2999+
throw new InvalidValueException("Invalid element search score key: \"$key\". Search scores should be indexed by element ID and site ID (e.g. \"100-1\").");
3000+
}
3001+
$keyParamSuffix = sprintf('%s_%s', $paramSuffix, $i);
3002+
$scoreSql .= sprintf(
3003+
' WHEN [[elements.id]] = :elementId_%s AND [[elements_sites.siteId]] = :siteId_%s THEN :value_%s',
3004+
$keyParamSuffix, $keyParamSuffix, $keyParamSuffix
3005+
);
3006+
$scoreParams[":elementId_$keyParamSuffix"] = $elementId;
3007+
$scoreParams[":siteId_$keyParamSuffix"] = $siteId;
3008+
$scoreParams[":value_$keyParamSuffix"] = $i;
3009+
}
3010+
$defaultParam = sprintf(':value_%s_%s', $paramSuffix, $i + 1);
3011+
$scoreSql .= sprintf(' ELSE %s END', $defaultParam);
3012+
$scoreParams[$defaultParam] = $i + 1;
3013+
$orderBy['score'] = new Expression($scoreSql, $scoreParams);
29913014
} else {
29923015
unset($orderBy['score']);
29933016
}

src/events/SearchEvent.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class SearchEvent extends BaseEvent
5151
public ?array $results = null;
5252

5353
/**
54-
* @var array|null The result scores, indexed by element ID
54+
* @var array<string,int>|null The element scores indexed by element ID and site ID (e.g. `'100-1'`).
5555
*
5656
* This will only be set ahead of time for [[\craft\services\Search::EVENT_AFTER_SEARCH]].
5757
*

src/services/Search.php

+12-19
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use craft\helpers\ElementHelper;
2323
use craft\helpers\Search as SearchHelper;
2424
use craft\helpers\StringHelper;
25-
use craft\models\Site;
2625
use craft\search\SearchQuery;
2726
use craft\search\SearchQueryTerm;
2827
use craft\search\SearchQueryTermGroup;
@@ -202,8 +201,7 @@ public function indexElementAttributes(ElementInterface $element, ?array $fieldH
202201
* Searches for elements that match the given element query.
203202
*
204203
* @param ElementQuery $elementQuery The element query being executed
205-
* @return array<int,int> Element ID and score mapping, with scores descending
206-
* @phpstan-return array<int,int>
204+
* @return array<string,int> The element scores (descending) indexed by element ID and site ID (e.g. `'100-1'`).
207205
* @since 3.7.14
208206
*/
209207
public function searchElements(ElementQuery $elementQuery): array
@@ -257,8 +255,8 @@ public function searchElements(ElementQuery $elementQuery): array
257255
}
258256
}
259257

260-
// Sort by element ID ascending, then score descending
261-
ksort($scores);
258+
// Sort by element ID/site ID ascending, then score descending
259+
ksort($scores, SORT_NATURAL);
262260
arsort($scores);
263261

264262
return $scores;
@@ -339,14 +337,11 @@ private function _scoreResults(array $results, SearchQuery $searchQuery, Element
339337

340338
// Loop through results and calculate score per element
341339
foreach ($results as $row) {
342-
$elementId = $row['elementId'];
343-
$score = $this->_scoreRow($row, $elementQuery->siteId);
344-
345-
if (!isset($scores[$elementId])) {
346-
$scores[$elementId] = $score;
347-
} else {
348-
$scores[$elementId] += $score;
340+
$key = sprintf('%s-%s', $row['elementId'], $row['siteId']);
341+
if (!isset($scores[$key])) {
342+
$scores[$key] = 0;
349343
}
344+
$scores[$key] += $this->_scoreRow($row);
350345
}
351346

352347
return $scores;
@@ -476,17 +471,16 @@ private function _indexKeywords(ElementInterface $element, string $keywords, ?st
476471
* Calculate score for a result.
477472
*
478473
* @param array $row A single result from the search query.
479-
* @param int|int[]|null $siteId
480474
* @return int The total score for this row.
481475
*/
482-
private function _scoreRow(array $row, array|int|null $siteId = null): int
476+
private function _scoreRow(array $row): int
483477
{
484478
// Starting point
485479
$score = 0;
486480

487481
// Loop through AND-terms and score each one against this row
488482
foreach ($this->_terms as $term) {
489-
$score += $this->_scoreTerm($term, $row, 1, $siteId);
483+
$score += $this->_scoreTerm($term, $row, 1);
490484
}
491485

492486
// Loop through each group of OR-terms
@@ -496,7 +490,7 @@ private function _scoreRow(array $row, array|int|null $siteId = null): int
496490

497491
// Get the score for each term and add it to the total
498492
foreach ($terms as $term) {
499-
$score += $this->_scoreTerm($term, $row, $weight, $siteId);
493+
$score += $this->_scoreTerm($term, $row, $weight);
500494
}
501495
}
502496

@@ -509,14 +503,13 @@ private function _scoreRow(array $row, array|int|null $siteId = null): int
509503
* @param SearchQueryTerm $term The SearchQueryTerm to score.
510504
* @param array $row The result row to score against.
511505
* @param float|int $weight Optional weight for this term.
512-
* @param int|int[]|null $siteId
513506
* @return float The total score for this term/row combination.
514507
*/
515-
private function _scoreTerm(SearchQueryTerm $term, array $row, float|int $weight = 1, array|int|null $siteId = null): float
508+
private function _scoreTerm(SearchQueryTerm $term, array $row, float|int $weight = 1): float
516509
{
517510
// Skip these terms: exact filtering is just that, no weighted search applies since all elements will
518511
// already apply for these filters.
519-
if ($term->exact || !($keywords = $this->_normalizeTerm($term->term, $siteId))) {
512+
if ($term->exact || !($keywords = $this->_normalizeTerm($term->term, $row['siteId']))) {
520513
return 0;
521514
}
522515

0 commit comments

Comments
 (0)