diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 15c089a0f1147..1e4acc637cc39 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -28,6 +28,7 @@ use OC\Files\Cache\Wrapper\CacheJail; use OC\Files\Search\QueryOptimizer\QueryOptimizer; use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; use OC\SystemConfig; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\ICache; @@ -36,6 +37,8 @@ use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; use OCP\Files\Search\ISearchQuery; use OCP\IDBConnection; use OCP\IGroupManager; @@ -82,11 +85,72 @@ protected function getQueryBuilder() { ); } + private function isCompareEqual(ISearchOperator $operator, string $fieldName): bool { + return + $operator instanceof ISearchComparison && + $operator->getType() === ISearchComparison::COMPARE_EQUAL && + $operator->getField() === $fieldName; + } + + private function checkStorageAndPathFilter(ISearchOperator $operator, array &$storageToPathsMap, array &$storageOtherFilters): void { + if ($operator instanceof ISearchBinaryOperator && $operator->getType() === ISearchBinaryOperator::OPERATOR_AND && count($operator->getArguments()) == 2) { + $a = $operator->getArguments()[0]; + $b = $operator->getArguments()[1]; + if ($this->isCompareEqual($a, "storage") && $this->isCompareEqual($b, "path")) { + /** @psalm-suppress UndefinedInterfaceMethod */ + $storage = $a->getValue(); + /** @psalm-suppress UndefinedInterfaceMethod */ + $path = $b->getValue(); + $storageToPathsMap[$storage][] = $path; + return; + } + } + $storageOtherFilters[] = $operator; + } + + private function optimizeStorageFilters(array $storageFilters): array { + // + // Optimize the storage filters query, when there are many shared files. + // + // Originally for each shared file the following section is added to the SQL WHERE clause: + // + // (`storage` = ) AND (`path` = ) + // + // When many files are shared between the same two users, the storage part of the filter is repeated many times. + // + // Here we want to refactor the query to have a single filter for each storage + // and provide all `path_hash` values for the same storage in the IN clause: + // + // (`storage` = ) AND (`path_hash` IN (, , ...)) + + // Pick up single file shares to prepare more efficient query + $storageToPathsMap = []; + $storageOtherFilters = []; + foreach ($storageFilters as $storageFilter) { + $this->checkStorageAndPathFilter($storageFilter, $storageToPathsMap, $storageOtherFilters); + } + + // Create filters for single file shares + $singleFileFilters = []; + foreach ($storageToPathsMap as $storage => $paths) { + $singleFileFilters[] = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_AND, + [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $storage), + new SearchComparison(ISearchComparison::COMPARE_IN, 'path_hash', array_map(fn ($path) => md5($path), $paths)) + ] + ); + } + + return array_merge($storageOtherFilters, $singleFileFilters); + } + protected function applySearchConstraints(CacheQueryBuilder $query, ISearchQuery $searchQuery, array $caches): void { $storageFilters = array_values(array_map(function (ICache $cache) { return $cache->getQueryFilterForStorage(); }, $caches)); - $storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters); + $optimizedStorageFilters = $this->optimizeStorageFilters($storageFilters); + $storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $optimizedStorageFilters); $filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]); $this->queryOptimizer->processOperator($filter); diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index b9a70bbd39b2c..d2b685d1026cb 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -45,6 +45,7 @@ class SearchBuilder { ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte', ISearchComparison::COMPARE_LESS_THAN => 'lt', ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte', + ISearchComparison::COMPARE_IN => 'in', ]; protected static $searchOperatorNegativeMap = [ @@ -187,6 +188,7 @@ private function validateComparison(ISearchComparison $operator) { 'favorite' => 'boolean', 'fileid' => 'integer', 'storage' => 'integer', + 'path_hash' => 'array', ]; $comparisons = [ 'mimetype' => ['eq', 'like'], @@ -199,6 +201,7 @@ private function validateComparison(ISearchComparison $operator) { 'favorite' => ['eq'], 'fileid' => ['eq'], 'storage' => ['eq'], + 'path_hash' => ['in'], ]; if (!isset($types[$operator->getField()])) { @@ -217,7 +220,9 @@ private function getParameterForValue(IQueryBuilder $builder, $value) { if ($value instanceof \DateTime) { $value = $value->getTimestamp(); } - if (is_numeric($value)) { + if (is_array($value)) { + $type = IQueryBuilder::PARAM_STR_ARRAY; + } elseif (is_numeric($value)) { $type = IQueryBuilder::PARAM_INT; } else { $type = IQueryBuilder::PARAM_STR; diff --git a/lib/private/Files/Search/SearchComparison.php b/lib/private/Files/Search/SearchComparison.php index 122a1f730b403..199235fe64cbb 100644 --- a/lib/private/Files/Search/SearchComparison.php +++ b/lib/private/Files/Search/SearchComparison.php @@ -29,7 +29,7 @@ class SearchComparison implements ISearchComparison { private $type; /** @var string */ private $field; - /** @var string|integer|\DateTime */ + /** @var string|integer|\DateTime|array */ private $value; private $hints = []; @@ -38,7 +38,7 @@ class SearchComparison implements ISearchComparison { * * @param string $type * @param string $field - * @param \DateTime|int|string $value + * @param \DateTime|int|string|array $value */ public function __construct($type, $field, $value) { $this->type = $type; @@ -61,7 +61,7 @@ public function getField() { } /** - * @return \DateTime|int|string + * @return \DateTime|int|string|array */ public function getValue() { return $this->value; diff --git a/lib/public/Files/Search/ISearchComparison.php b/lib/public/Files/Search/ISearchComparison.php index 8ebaeced304a1..af89254e49f7f 100644 --- a/lib/public/Files/Search/ISearchComparison.php +++ b/lib/public/Files/Search/ISearchComparison.php @@ -34,6 +34,7 @@ interface ISearchComparison extends ISearchOperator { public const COMPARE_LESS_THAN_EQUAL = 'lte'; public const COMPARE_LIKE = 'like'; public const COMPARE_LIKE_CASE_SENSITIVE = 'clike'; + public const COMPARE_IN = 'in'; public const HINT_PATH_EQ_HASH = 'path_eq_hash'; // transform `path = "$path"` into `path_hash = md5("$path")`, on by default @@ -58,7 +59,7 @@ public function getField(); /** * Get the value to compare the field with * - * @return string|integer|\DateTime + * @return string|integer|\DateTime|array * @since 12.0.0 */ public function getValue();