From a2a55c3864cdcab31610d03170c4c64132df131c Mon Sep 17 00:00:00 2001 From: Purinda Gunasekara Date: Thu, 3 Sep 2015 15:25:42 +1000 Subject: [PATCH] POC for score based recordset filtering --- src/Bravo3/Orm/Drivers/DriverInterface.php | 17 ++++ src/Bravo3/Orm/Drivers/Redis/RedisDriver.php | 46 ++++++++++ src/Bravo3/Orm/Query/ScoreFilterQuery.php | 87 +++++++++++++++++++ src/Bravo3/Orm/Services/EntityManager.php | 17 ++++ src/Bravo3/Orm/Services/QueryManager.php | 53 +++++++++++ .../Tests/Indices/ScoreFilterQueryTest.php | 47 ++++++++++ 6 files changed, 267 insertions(+) create mode 100644 src/Bravo3/Orm/Query/ScoreFilterQuery.php create mode 100644 tests/Bravo3/Orm/Tests/Indices/ScoreFilterQueryTest.php diff --git a/src/Bravo3/Orm/Drivers/DriverInterface.php b/src/Bravo3/Orm/Drivers/DriverInterface.php index 4a8e7de..6e1e0fe 100644 --- a/src/Bravo3/Orm/Drivers/DriverInterface.php +++ b/src/Bravo3/Orm/Drivers/DriverInterface.php @@ -166,6 +166,23 @@ public function removeSortedIndex($key, $value); */ public function getSortedIndex($key, $reverse = false, $start = null, $stop = null); + /** + * Get a range values in a sorted index filtered by member values values + * + * If $start/$stop are null then they are assumed to be the start/end of the entire set + * + * @param string $key + * @param bool $reverse + * @param int $min_score + * @param int $max_score + * @param int $start + * @param int $stop + * + * @return string[] + */ + public function getSortedFilteredIndex($key, $reverse = false, $min_score = '-inf', $max_score = '+inf', + $start = null, $stop = null); + /** * Get the size of a sorted index, without any filters applied * diff --git a/src/Bravo3/Orm/Drivers/Redis/RedisDriver.php b/src/Bravo3/Orm/Drivers/Redis/RedisDriver.php index 5f66ac0..8e5b35a 100644 --- a/src/Bravo3/Orm/Drivers/Redis/RedisDriver.php +++ b/src/Bravo3/Orm/Drivers/Redis/RedisDriver.php @@ -435,6 +435,52 @@ public function getSortedIndex($key, $reverse = false, $start = null, $stop = nu } } + /** + * Get a range values in a sorted index filtered by member values values + * + * If $start/$stop are null then they are assumed to be the start/end of the entire set + * + * @param string $key + * @param bool $reverse + * @param int $min_score + * @param int $max_score + * @param int $start + * @param int $stop + * + * @return string[] + */ + public function getSortedFilteredIndex($key, $reverse = false, $min_score = '-inf', $max_score = '+inf', + $start = null, $stop = null) + { + if ($reverse) { + return $this->clientCmd( + 'zrevrangebyscore', + [ + $key, + $min_score === null ? '-inf' : $min_score, + $max_score === null ? '+inf' : $max_score, + 'limit' => [ + $start === null ? 0 : $start, + $stop === null ? -1 : $stop, + ] + ] + ); + } else { + return $this->clientCmd( + 'zrangebyscore', + [ + $key, + $min_score === null ? '-inf' : $min_score, + $max_score === null ? '+inf' : $max_score, + 'limit' => [ + $start === null ? 0 : $start, + $stop === null ? -1 : $stop, + ] + ] + ); + } + } + /** * Create a debug log * diff --git a/src/Bravo3/Orm/Query/ScoreFilterQuery.php b/src/Bravo3/Orm/Query/ScoreFilterQuery.php new file mode 100644 index 0000000..f2fdb45 --- /dev/null +++ b/src/Bravo3/Orm/Query/ScoreFilterQuery.php @@ -0,0 +1,87 @@ +min_score = $min_score; + $this->max_score = $max_score; + } + + /** + * Get starting member values to filter the sorted data-set + * + * @return int + */ + public function getMinScore() + { + return $this->min_score; + } + + /** + * Set starting member values to filter the sorted data-set + * + * @param int $min_score + * @return $this + */ + public function setMinScore($min_score) + { + $this->min_score = $min_score; + return $this; + } + + /** + * Get maximum value to filter out the members within the sorted data-set + * + * @return int + */ + public function getMaxScore() + { + return $this->max_score; + } + + /** + * Set maximum value to filter out the members within the sorted data-set + * + * @param int $max_score + * @return $this + */ + public function setMaxScore($max_score) + { + $this->max_score = $max_score; + return $this; + } + +} diff --git a/src/Bravo3/Orm/Services/EntityManager.php b/src/Bravo3/Orm/Services/EntityManager.php index bbb52a2..629e468 100644 --- a/src/Bravo3/Orm/Services/EntityManager.php +++ b/src/Bravo3/Orm/Services/EntityManager.php @@ -10,6 +10,7 @@ use Bravo3\Orm\Proxy\OrmProxyInterface; use Bravo3\Orm\Query\IndexedQuery; use Bravo3\Orm\Query\QueryResult; +use Bravo3\Orm\Query\ScoreFilterQuery; use Bravo3\Orm\Query\SortedQuery; use Bravo3\Orm\Serialisers\JsonSerialiser; use Bravo3\Orm\Serialisers\SerialiserMap; @@ -403,6 +404,22 @@ public function sortedQuery(SortedQuery $query, $check_full_set_size = false, $u return $this->getQueryManager()->sortedQuery($query, $check_full_set_size, $use_cache); } + /** + * Get all foreign entities filtered by a given score range + * + * If you have applied a limit to the query but need to know the full size of the unfiltered set, you must set + * $check_full_set_size to true to gather this information at the expense of a second database query. + * + * @param ScoreFilterQuery $query + * @param bool $check_full_set_size + * @param bool $use_cache + * @return QueryResult + */ + public function scoreFilterQuery(ScoreFilterQuery $query, $check_full_set_size = false, $use_cache = true) + { + return $this->getQueryManager()->scoreFilterQuery($query, $check_full_set_size, $use_cache); + } + /** * Will force a database update of an entity * diff --git a/src/Bravo3/Orm/Services/QueryManager.php b/src/Bravo3/Orm/Services/QueryManager.php index e440a1f..c82a45f 100644 --- a/src/Bravo3/Orm/Services/QueryManager.php +++ b/src/Bravo3/Orm/Services/QueryManager.php @@ -6,6 +6,7 @@ use Bravo3\Orm\Mappers\Metadata\Entity; use Bravo3\Orm\Query\IndexedQuery; use Bravo3\Orm\Query\QueryResult; +use Bravo3\Orm\Query\ScoreFilterQuery; use Bravo3\Orm\Query\SortedQuery; use Bravo3\Orm\Services\Io\Reader; @@ -109,6 +110,58 @@ public function sortedQuery(SortedQuery $query, $check_full_set_size = false, $u return new QueryResult($this->entity_manager, $query, $results, $full_size, $use_cache); } + /** + * Get all foreign entities filtered by the range provided + * + * If you have applied a limit to the query but need to know the full size of the unfiltered set, you must set + * $check_full_set_size to true to gather this information at the expense of a second database query. + * + * @param ScoreFilterQuery $query + * @param bool $check_full_set_size + * @param bool $use_cache + * @return QueryResult + */ + public function scoreFilterQuery(ScoreFilterQuery $query, $check_full_set_size = false, $use_cache = true) + { + $metadata = $this->getMapper()->getEntityMetadata($query->getClassName()); + + if ($query->getRelationshipName()) { + // Entity relationship based query + $reader = new Reader($metadata, $query->getEntity()); + $relationship = $metadata->getRelationshipByName($query->getRelationshipName()); + + if (!$relationship) { + throw new InvalidArgumentException('Relationship "'.$query->getRelationshipName().'" does not exist'); + } + + // Important, else the QueryResult class will try to hydrate the wrong entity + $query->setClassName($relationship->getTarget()); + $key = $this->getKeyScheme()->getSortIndexKey($relationship, $query->getSortBy(), $reader->getId()); + } else { + // Table based query + $key = $this->getKeyScheme()->getTableSortKey($metadata->getTableName(), $query->getSortBy()); + } + + $results = $this->getDriver()->getSortedFilteredIndex( + $key, + $query->getDirection() == Direction::DESC(), + $query->getMinScore(), + $query->getMaxScore(), + $query->getStart(), + $query->getEnd() + ); + + if (!$query->getStart() && !$query->getEnd()) { + $full_size = count($results); + } elseif ($check_full_set_size) { + $full_size = $this->getDriver()->getSortedIndexSize($key); + } else { + $full_size = null; + } + + return new QueryResult($this->entity_manager, $query, $results, $full_size, $use_cache); + } + /** * Persist entity indices * diff --git a/tests/Bravo3/Orm/Tests/Indices/ScoreFilterQueryTest.php b/tests/Bravo3/Orm/Tests/Indices/ScoreFilterQueryTest.php new file mode 100644 index 0000000..3076f01 --- /dev/null +++ b/tests/Bravo3/Orm/Tests/Indices/ScoreFilterQueryTest.php @@ -0,0 +1,47 @@ +getEntityManager(); + + $category = new Category(); + $category->setId(600); + $em->persist($category); + + for ($i = 0; $i < 15; $i++) { + $article = new Article(); + $article->setId(601 + $i); + $article->setTitle('Art '.(601 + $i)); + $time = new \DateTime(); + $time->modify('+'.($i + 1).' minutes'); + $article->setSortDate($time); + $article->setCanonicalCategory($category); + $em->persist($article); + } + + $em->flush(); + + /** @var Article $article */ + // Date sorting - + $time = new \DateTime(); + $time->modify('+5 minutes'); + $start_time = $time->getTimestamp(); + + $time = new \DateTime(); + $time->modify('+10 minutes'); + $end_time = $time->getTimestamp(); + + // Check if you only get partial result set filtered by the date range + $results = $em->scoreFilterQuery(new ScoreFilterQuery($category, 'articles', 'sort_date', Direction::ASC(), $start_time, $end_time)); + $this->assertCount(6, $results); + } +}