Skip to content

Commit

Permalink
multi-metadata order
Browse files Browse the repository at this point in the history
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
  • Loading branch information
ArtificialOwl committed Nov 2, 2023
1 parent 3685bf8 commit 447618c
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 128 deletions.
1 change: 1 addition & 0 deletions apps/dav/lib/Connector/Sabre/FilesPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class FilesPlugin extends ServerPlugin {
public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
public const FILE_METADATA_SIZE = '{http://nextcloud.org/ns}file-metadata-size';
public const FILE_METADATA_GPS = '{http://nextcloud.org/ns}file-metadata-gps';

Expand Down
101 changes: 59 additions & 42 deletions apps/dav/lib/Files/FileSearchBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\Files\Search\ISearchQuery;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IUser;
use OCP\Share\IManager;
use Sabre\DAV\Exception\NotFound;
Expand All @@ -57,37 +59,14 @@
class FileSearchBackend implements ISearchBackend {
public const OPERATOR_LIMIT = 100;

/** @var CachingTree */
private $tree;

/** @var IUser */
private $user;

/** @var IRootFolder */
private $rootFolder;

/** @var IManager */
private $shareManager;

/** @var View */
private $view;

/**
* FileSearchBackend constructor.
*
* @param CachingTree $tree
* @param IUser $user
* @param IRootFolder $rootFolder
* @param IManager $shareManager
* @param View $view
* @internal param IRootFolder $rootFolder
*/
public function __construct(CachingTree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
$this->tree = $tree;
$this->user = $user;
$this->rootFolder = $rootFolder;
$this->shareManager = $shareManager;
$this->view = $view;
public function __construct(
private CachingTree $tree,
private IUser $user,
private IRootFolder $rootFolder,
private IManager $shareManager,
private View $view,
private IFilesMetadataManager $filesMetadataManager,
) {
}

/**
Expand Down Expand Up @@ -115,7 +94,7 @@ public function getPropertyDefinitionsForScope(string $href, ?string $path): arr
// all valid scopes support the same schema

//todo dynamically load all propfind properties that are supported
return [
$props = [
// queryable properties
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
Expand All @@ -137,6 +116,33 @@ public function getPropertyDefinitionsForScope(string $href, ?string $path): arr
new SearchPropertyDefinition(FilesPlugin::FILE_METADATA_SIZE, true, false, false, SearchPropertyDefinition::DATATYPE_STRING),
new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
];

return array_merge($props, $this->getPropertyDefinitionsForMetadata());
}


private function getPropertyDefinitionsForMetadata(): array {
$metadataProps = [];
$metadata = $this->filesMetadataManager->getAllMetadata();
$indexes = $metadata->getIndexes();
foreach ($metadata->getKeys() as $key) {
$isIndex = in_array($key, $indexes);
$type = match ($metadata->getType($key)) {
IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER,
IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL,
IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN,
default => SearchPropertyDefinition::DATATYPE_STRING
};
$metadataProps[] = new SearchPropertyDefinition(
FilesPlugin::FILE_METADATA_PREFIX . $key,
true,
$isIndex,
$isIndex,
$type
);
}

return $metadataProps;
}

/**
Expand Down Expand Up @@ -300,11 +306,20 @@ private function getHrefForNode(Node $node) {

/**
* @param Query $query
*
* @return ISearchQuery
*/
private function transformQuery(Query $query): ISearchQuery {
$orders = array_map(function (Order $order): ISearchOrder {
$direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), 'metadata');
} else {
return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property));
}
}, $query->orderBy);

$limit = $query->limit;
$orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
$offset = $limit->firstResult;

$limitHome = false;
Expand Down Expand Up @@ -352,14 +367,6 @@ private function countSearchOperators(Operator $operator): int {
}
}

/**
* @param Order $order
* @return ISearchOrder
*/
private function mapSearchOrder(Order $order) {
return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property));
}

/**
* @param Operator $operator
* @return ISearchOperator
Expand Down Expand Up @@ -387,7 +394,17 @@ private function transformSearchOperation(Operator $operator) {
if (!($operator->arguments[1] instanceof Literal)) {
throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
}
return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value));

$property = $operator->arguments[0];
$value = $this->castValue($property, $operator->arguments[1]->value);
if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
// return new SearchComparison($trimmedType, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), $value, 'metadata');
return new SearchComparison($trimmedType, 'photo-taken', $value, 'metadata');
} else {
return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($property), $value);
}

// no break
case Operator::OPERATION_IS_COLLECTION:
return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
default:
Expand Down
4 changes: 3 additions & 1 deletion apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
use OCP\AppFramework\Http\Response;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\ICacheFactory;
use OCP\IRequest;
use OCP\Profiler\IProfiler;
Expand Down Expand Up @@ -316,7 +317,8 @@ public function __construct(IRequest $request, string $baseUri) {
$user,
\OC::$server->getRootFolder(),
\OC::$server->getShareManager(),
$view
$view,
\OCP\Server::get(IFilesMetadataManager::class)
));
$this->server->addPlugin(
new BulkUploadPlugin(
Expand Down
21 changes: 6 additions & 15 deletions lib/private/Files/Cache/QuerySearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,14 @@ protected function equipQueryForMetadata(CacheQueryBuilder $query, ISearchQuery
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($query, 'file', 'fileid');
$metadataQuery->retrieveMetadata();

$order = $searchQuery->getOrder();
if ($order) {
foreach ($order as $orderField) {
$field = $orderField->getField();
if (!str_starts_with($field, 'metakey_')) {
continue;
}
$metadataQuery->joinIndex(substr($field, 8));
$query->orderBy($metadataQuery->getMetadataValueIntField(), $orderField->getDirection());
foreach ($searchQuery->getOrder() as $order) {
if ($order->getExtra() !== 'metadata') {
continue; // only metadata search order are managed here.
}
}

// TODO: add filter on metadatakey / metadatavalue
// is it possible to get information from the webdav request ?
// $expr = $query->expr();
// $query->andWhere($expr->eq($metadataQuery->getMetadataKeyField(), $query->createNamedParameter('my_key')));
// $query->andWhere($expr->eq($metadataQuery->getMetadataValueField(), $query->createNamedParameter('my_value')));
$alias = $metadataQuery->joinIndex($order->getField());
$query->addOrderBy($metadataQuery->getMetadataValueIntField($alias), $order->getDirection());
}

return $metadataQuery;
}
Expand Down
9 changes: 8 additions & 1 deletion lib/private/Files/Cache/SearchBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function extractRequestedFields(ISearchOperator $operator): array {
return array_reduce($operator->getArguments(), function (array $fields, ISearchOperator $operator) {
return array_unique(array_merge($fields, $this->extractRequestedFields($operator)));
}, []);
} elseif ($operator instanceof ISearchComparison) {
} elseif ($operator instanceof ISearchComparison && !$operator->isExtra()) {
return [$operator->getField()];
}
return [];
Expand Down Expand Up @@ -124,6 +124,10 @@ public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $
}

private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
if ($comparison->isExtra()) {
return null;
}

$this->validateComparison($comparison);

[$field, $value, $type] = $this->getOperatorFieldAndValue($comparison);
Expand Down Expand Up @@ -231,6 +235,9 @@ private function getParameterForValue(IQueryBuilder $builder, $value) {
*/
public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) {
foreach ($orders as $order) {
if ($order->isExtra()) {
continue; // extra search orders are managed elsewhere
}
$field = $order->getField();
if ($field === 'fileid') {
$field = 'file.fileid';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function inspectOperator(ISearchOperator $operator): void {
}

public function processOperator(ISearchOperator &$operator) {
if (!$this->useHashEq && $operator instanceof ISearchComparison && $operator->getField() === 'path' && $operator->getType() === ISearchComparison::COMPARE_EQUAL) {
if (!$this->useHashEq && $operator instanceof ISearchComparison && !$operator->isExtra() && $operator->getField() === 'path' && $operator->getType() === ISearchComparison::COMPARE_EQUAL) {
$operator->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, false);
}

Expand All @@ -69,7 +69,7 @@ private function isPathPrefixOperator(ISearchOperator $operator): bool {
private function operatorPairIsPathPrefix(ISearchOperator $like, ISearchOperator $equal): bool {
return (
$like instanceof ISearchComparison && $equal instanceof ISearchComparison &&
$like->getField() === 'path' && $equal->getField() === 'path' &&
!$like->isExtra() && !$equal->isExtra() && $like->getField() === 'path' && $equal->getField() === 'path' &&
$like->getType() === ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE && $equal->getType() === ISearchComparison::COMPARE_EQUAL
&& $like->getValue() === SearchComparison::escapeLikeParameter($equal->getValue()) . '/%'
);
Expand Down
34 changes: 16 additions & 18 deletions lib/private/Files/Search/SearchComparison.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,15 @@
use OCP\Files\Search\ISearchComparison;

class SearchComparison implements ISearchComparison {
/** @var string */
private $type;
/** @var string */
private $field;
/** @var string|integer|\DateTime */
private $value;
private $hints = [];
private array $hints = [];

/**
* SearchComparison constructor.
*
* @param string $type
* @param string $field
* @param \DateTime|int|string $value
*/
public function __construct($type, $field, $value) {
$this->type = $type;
$this->field = $field;
$this->value = $value;

public function __construct(
private string $type,
private string $field,
private \DateTime|int|string $value,
private string $extra = ''
) {
}

/**
Expand All @@ -67,6 +57,14 @@ public function getValue() {
return $this->value;
}

public function getExtra(): string {
return $this->extra;
}

public function isExtra(): bool {
return ($this->extra !== '');
}

public function getQueryHint(string $name, $default) {
return $this->hints[$name] ?? $default;
}
Expand Down
31 changes: 16 additions & 15 deletions lib/private/Files/Search/SearchOrder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
Expand All @@ -26,36 +27,36 @@
use OCP\Files\Search\ISearchOrder;

class SearchOrder implements ISearchOrder {
/** @var string */
private $direction;
/** @var string */
private $field;

/**
* SearchOrder constructor.
*
* @param string $direction
* @param string $field
*/
public function __construct($direction, $field) {
$this->direction = $direction;
$this->field = $field;
public function __construct(
private string $direction,
private string $field,
private string $extra = ''
) {
}

/**
* @return string
*/
public function getDirection() {
public function getDirection(): string {
return $this->direction;
}

/**
* @return string
*/
public function getField() {
public function getField(): string {
return $this->field;
}

public function getExtra(): string {
return $this->extra;
}

public function isExtra(): bool {
return ($this->extra !== '');
}

public function sortFileInfo(FileInfo $a, FileInfo $b): int {
$cmp = $this->sortFileInfoNoDirection($a, $b);
return $cmp * ($this->direction === ISearchOrder::DIRECTION_ASCENDING ? 1 : -1);
Expand Down
Loading

0 comments on commit 447618c

Please sign in to comment.