diff --git a/Business/Recommendation/MemoryCF.php b/Business/Recommendation/MemoryCF.php index 7bbb239fd..d8d1578d9 100644 --- a/Business/Recommendation/MemoryCF.php +++ b/Business/Recommendation/MemoryCF.php @@ -150,20 +150,8 @@ final class MemoryCF } /** - * Find potential items/users which are a good match for a user/item. + * Find similar users * - * The algorithm uses the ratings of a a user and tries to find other users who have similar rating behavior and then searches for high rated items that the user doesn't have yet. - * - * This can be used to find items for a specific user (aka might be interested in) or to find users who might be interested in this item - * - * option 1 - find items - * ranking[itemId] = itemRank (how much does specific user like item) - * rankings[userId][itemId] = itemRank - * - * option 2 - find user - * ranking[userId] = itemRank (how much does user like specific item) - * rankings[itemId][userId] = itemRank - * option 1 searches for items, option 2 searches for users * * @param array $ranking Array of item ratings (e.g. products, movies, ...) * @@ -171,7 +159,46 @@ final class MemoryCF * * @since 1.0.0 */ - public function bestMatch(array $ranking, int $size = 10) : array + public function bestMatchUser(array $ranking, int $size = 10) : array + { + $ranking = $this->normalizeRanking([$ranking]); + $ranking = $ranking[0]; + + $euclidean = $this->euclideanDistance($ranking, $this->rankings); + // $cosine = $this->cosineDistance($ranking, $this->rankings); + + \asort($euclidean); + // \asort($cosine); + + $size = \min($size, \count($this->rankings)); + $matches = []; + + $distancePointer = \array_keys($euclidean); + // $anglePointer = \array_keys($cosine); + + // Inspect items of the top n comparable users + for ($i = 0; $i < $size; ++$i) { + // $uId = $i % 2 === 0 ? $distancePointer[$i] : $anglePointer[$i]; + $uId = $distancePointer[$i]; + + if (!\in_array($uId, $matches)) { + $matches[] = $uId; + } + } + + return $matches; + } + + /** + * Find potential users which are a good match for a user. + * + * @param array $ranking Array of item ratings (e.g. products, movies, ...) + * + * @return array + * + * @since 1.0.0 + */ + public function bestMatchItem(array $ranking, int $size = 10) : array { $ranking = $this->normalizeRanking([$ranking]); $ranking = $ranking[0]; @@ -194,13 +221,13 @@ final class MemoryCF $distances = $i % 2 === 0 ? $euclidean : $cosine; foreach ($this->rankings[$uId] as $iId => $_) { - // Item is not already in dataset and not in historic dataset (we are only interested in new) + // Item is already in dataset or in historic dataset (we are only interested in new) if (isset($matches[$iId]) || isset($ranking[$iId])) { continue; } // Calculate the expected rating the user would give based on what the best comparable users did - $matches[$iId] = $this->weightedItemRank($iId, $distances, $this->rankings, $size); + $matches[$iId] = $this->weightedItemRank((string) $iId, $distances, $this->rankings, $size); } } diff --git a/Math/Topology/MetricsND.php b/Math/Topology/MetricsND.php index 2988e911f..772964bdd 100755 --- a/Math/Topology/MetricsND.php +++ b/Math/Topology/MetricsND.php @@ -74,16 +74,10 @@ final class MetricsND * * @return float * - * @throws InvalidDimensionException - * * @since 1.0.0 */ public static function euclidean(array $a, array $b) : float { - if (\count($a) !== \count($b)) { - throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); - } - $dist = 0.0; foreach ($a as $key => $e) { $dist += \abs($e - $b[$key]) ** 2; @@ -100,16 +94,10 @@ final class MetricsND * * @return float * - * @throws InvalidDimensionException - * * @since 1.0.0 */ public static function cosine(array $a, array $b) : float { - if (\count($a) !== \count($b)) { - throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); - } - $dotProduct = 0; foreach ($a as $id => $_) { $dotProduct += $a[$id] * $b[$id]; diff --git a/Router/SocketRouter.php b/Router/SocketRouter.php index 7c070924d..5e364f394 100755 --- a/Router/SocketRouter.php +++ b/Router/SocketRouter.php @@ -129,21 +129,25 @@ final class SocketRouter implements RouterInterface } foreach ($destination as $d) { + if (!($d['active'] ?? true)) { + continue; + } + if ((!isset($d['verb']) || $d['verb'] === RouteVerb::ANY) || $verb === RouteVerb::ANY || ($verb & $d['verb']) === $verb ) { // if csrf is required but not set - if (isset($d['csrf']) && $d['csrf'] && $csrf === null) { + if (($d['csrf'] ?? false) && $csrf === null) { return ['dest' => RouteStatus::INVALID_CSRF]; } // if permission check is invalid - if (isset($d['permission']) && !empty($d['permission']) + if (!empty($d['permission'] ?? null) && ($account === null || $account->id === 0) ) { return ['dest' => RouteStatus::NOT_LOGGED_IN]; - } elseif (isset($d['permission']) && !empty($d['permission']) + } elseif (!empty($d['permission'] ?? null) && !($account?->hasPermission( $d['permission']['type'] ?? 0, $d['permission']['unit'] ?? $unitId, diff --git a/Router/WebRouter.php b/Router/WebRouter.php index aa27213d2..dcdd033de 100755 --- a/Router/WebRouter.php +++ b/Router/WebRouter.php @@ -150,16 +150,16 @@ final class WebRouter implements RouterInterface || ($verb & $d['verb']) === $verb ) { // if csrf is required but not set - if (isset($d['csrf']) && $d['csrf'] && $csrf === null) { + if (($d['csrf'] ?? false) && $csrf === null) { return ['dest' => RouteStatus::INVALID_CSRF]; } // if permission check is invalid - if (isset($d['permission']) && !empty($d['permission']) + if (!empty($d['permission'] ?? null) && ($account === null || $account->id === 0) ) { return ['dest' => RouteStatus::NOT_LOGGED_IN]; - } elseif (isset($d['permission']) && !empty($d['permission']) + } elseif (!empty($d['permission'] ?? null) && !($account?->hasPermission( $d['permission']['type'] ?? 0, $d['permission']['unit'] ?? $unitId, diff --git a/tests/Business/Recommendation/MemoryCFTest.php b/tests/Business/Recommendation/MemoryCFTest.php index e9b5db4e0..2fb3940f9 100644 --- a/tests/Business/Recommendation/MemoryCFTest.php +++ b/tests/Business/Recommendation/MemoryCFTest.php @@ -22,7 +22,7 @@ use phpOMS\Business\Recommendation\MemoryCF; #[\PHPUnit\Framework\Attributes\TestDox('phpOMS\tests\Business\Recommendation\MemoryCFTest: Article affinity/correlation')] final class MemoryCFTest extends \PHPUnit\Framework\TestCase { - public function testBestMatch() : void + public function testBestMatchUser() : void { $memory = new MemoryCF([ 'A' => [1.0, 2.0], @@ -33,7 +33,22 @@ final class MemoryCFTest extends \PHPUnit\Framework\TestCase self::assertEquals( ['B', 'C'], - $memory->bestMatch([2.2, 4.1], 2) + $memory->bestMatchUser([2.2, 4.1], 2) + ); + } + + public function testBestMatchItem() : void + { + $memory = new MemoryCF([ + 'A' => [1.0, 2.0], + 'B' => [2.0, 4.0], + 'C' => [2.5, 4.0], + 'D' => [4.5, 5.0], + ]); + + self::assertEquals( + [1 => 0.0], + $memory->bestMatchItem([2.2], 2) ); } }