Skip to content

Commit 3d58732

Browse files
abnegateclaude
andcommitted
perf(adapter): skip op_search relevance projection when caller supplied order, fold search detection into find partition
The find pipeline previously walked queries twice: once for the partition (vector / adapter-filter / other) and again via `extractSearchQueries` to project a `_relevance` SELECT and ORDER BY. With FULLTEXT MATCH that means the relevance MATCH is compiled and evaluated a second time. Pick up search queries in the existing single-pass partition loop and skip the relevance projection entirely when the caller already supplied an explicit order — the Documents trait auto-appends `$sequence` as a tiebreaker, so anything richer than a lone `$sequence` is caller-driven and would be silently overridden by relevance order otherwise. `extractSearchQueries` is kept as a protected method for adapter overrides but is no longer called from `find`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 11b444d commit 3d58732

1 file changed

Lines changed: 24 additions & 4 deletions

File tree

src/Database/Adapter/SQL.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,14 +1182,17 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
11821182

11831183
// Single pass partitioning: pull vector queries out for ORDER BY and
11841184
// detect aggregation/join shape in the same walk. Each Method::value
1185-
// is checked once per query rather than three times.
1185+
// is checked once per query rather than three times. Search queries
1186+
// are picked up here too so we don't need a second pass via
1187+
// `extractSearchQueries` later in this method.
11861188
// Defer the defensive `clone` until we know the query path will mutate
11871189
// the Query objects (joins or aggregations-with-joins). The vast
11881190
// majority of finds take neither path and don't need a per-query
11891191
// clone allocation.
11901192
$vectorQueries = [];
11911193
$otherQueries = [];
11921194
$adapterFilterQueries = [];
1195+
$searchQueries = [];
11931196
$hasAggregation = false;
11941197
$hasJoins = false;
11951198

@@ -1210,6 +1213,10 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
12101213

12111214
$otherQueries[] = $query;
12121215

1216+
if ($method === Method::Search) {
1217+
$searchQueries[] = $query;
1218+
}
1219+
12131220
if ($method->isAggregate() || $method === Method::GroupBy) {
12141221
$hasAggregation = true;
12151222
}
@@ -1423,9 +1430,22 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
14231430
}
14241431
}
14251432

1426-
// Full-text search relevance scoring
1427-
$searchQueries = $this->extractSearchQueries($queries);
1428-
if (! empty($searchQueries)) {
1433+
// Full-text search relevance scoring.
1434+
//
1435+
// Skip the second MATCH compilation (and its ORDER BY) when the caller
1436+
// already asked for an explicit order. The Documents trait auto-appends
1437+
// '$sequence' as a tiebreaker, so a `$orderAttributes === ['$sequence']`
1438+
// signal — with no entries before it — means "caller did not specify
1439+
// an order" and is the only case where we should auto-order by
1440+
// relevance. Anything else (multiple entries, or a leading attribute
1441+
// other than $sequence) means the caller has an explicit order and
1442+
// relevance ordering would silently override it.
1443+
$callerSuppliedOrder = ! (
1444+
count($orderAttributes) === 0
1445+
|| (count($orderAttributes) === 1 && $orderAttributes[0] === '$sequence')
1446+
);
1447+
1448+
if (! empty($searchQueries) && ! $callerSuppliedOrder) {
14291449
$builder->select(['*']);
14301450
foreach ($searchQueries as $searchQuery) {
14311451
$relevanceRaw = $this->getSearchRelevanceRaw($searchQuery, $alias);

0 commit comments

Comments
 (0)