diff --git a/.gitignore b/.gitignore index 57872d0..46113e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /vendor/ +.phpunit.result.cache diff --git a/composer.json b/composer.json index 60d586c..88a2865 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,9 @@ } ], "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Gorse\\": "src/" + } }, "require-dev": { "phpunit/phpunit": "^9.3" @@ -20,4 +20,4 @@ "require": { "guzzlehttp/guzzle": "^7.0" } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index f151e12..9a0ce5e 100644 --- a/composer.lock +++ b/composer.lock @@ -736,16 +736,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -788,9 +788,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -1231,16 +1231,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.29", + "version": "9.6.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + "reference": "945d0b7f346a084ce5549e95289962972c4272e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", + "reference": "945d0b7f346a084ce5549e95289962972c4272e5", "shasum": "" }, "require": { @@ -1314,7 +1314,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" }, "funding": [ { @@ -1338,7 +1338,7 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:29:11+00:00" + "time": "2025-12-06T07:45:52+00:00" }, { "name": "sebastian/cli-parser", @@ -2409,5 +2409,5 @@ "prefer-lowest": false, "platform": {}, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Gorse.php b/src/Gorse.php index 4bf0219..851ffa3 100644 --- a/src/Gorse.php +++ b/src/Gorse.php @@ -1,207 +1,230 @@ userId = $userId; - $this->labels = $labels; - $this->comment = $comment; + $this->endpoint = $endpoint; + $this->apiKey = $apiKey; + $this->client = new Client(['base_uri' => $this->endpoint]); } - public function jsonSerialize(): array + /** + * @throws GuzzleException + */ + function insertUser(User $user): RowAffected { - return [ - 'UserId' => $this->userId, - 'Labels' => $this->labels, - 'Comment' => $this->comment, - ]; + return RowAffected::fromJSON($this->request('POST', '/api/user/', $user)); } - public static function fromJSON($json): User + /** + * @throws GuzzleException + */ + function getUser(string $user_id): User { - return new User($json->UserId, (array) $json->Labels, $json->Comment); + return User::fromJSON($this->request('GET', '/api/user/' . $user_id, null)); } -} - -class Feedback implements JsonSerializable -{ - public string $feedback_type; - public string $user_id; - public string $item_id; - public float $value; - public string $timestamp; - public function __construct(string $feedback_type, string $user_id, string $item_id, float $value, string $timestamp) + /** + * @throws GuzzleException + */ + function deleteUser(string $user_id): RowAffected { - $this->feedback_type = $feedback_type; - $this->user_id = $user_id; - $this->item_id = $item_id; - $this->value = $value; - $this->timestamp = $timestamp; + return RowAffected::fromJSON($this->request('DELETE', '/api/user/' . $user_id, null)); } - public function jsonSerialize(): array + /** + * @throws GuzzleException + */ + function insertItem(Item $item): RowAffected { - return [ - 'FeedbackType' => $this->feedback_type, - 'UserId' => $this->user_id, - 'ItemId' => $this->item_id, - 'Value' => $this->value, - 'Timestamp' => $this->timestamp, - ]; + return RowAffected::fromJSON($this->request('POST', '/api/item/', $item)); } -} - -class Item implements JsonSerializable -{ - public string $itemId; - public bool $isHidden; - public array $labels; - public array $categories; - public string $timestamp; - public string $comment; - public function __construct(string $itemId, bool $isHidden, array $labels, array $categories, string $timestamp, string $comment) + /** + * @throws GuzzleException + */ + function getItem(string $item_id): Item { - $this->itemId = $itemId; - $this->isHidden = $isHidden; - $this->labels = $labels; - $this->categories = $categories; - $this->timestamp = $timestamp; - $this->comment = $comment; + return Item::fromJSON($this->request('GET', '/api/item/' . $item_id, null)); } - public function jsonSerialize(): array + /** + * @throws GuzzleException + */ + function deleteItem(string $item_id): RowAffected { - return [ - 'ItemId' => $this->itemId, - 'IsHidden' => $this->isHidden, - 'Labels' => $this->labels, - 'Categories' => $this->categories, - 'Timestamp' => $this->timestamp, - 'Comment' => $this->comment, - ]; + return RowAffected::fromJSON($this->request('DELETE', '/api/item/' . $item_id, null)); } - public static function fromJSON($json): Item + /** + * @throws GuzzleException + */ + function updateItem(string $item_id, Item $item): RowAffected { - return new Item($json->ItemId, $json->IsHidden, (array) $json->Labels, (array) $json->Categories, $json->Timestamp, $json->Comment); + return RowAffected::fromJSON($this->request('PATCH', '/api/item/' . $item_id, $item)); } -} - -class RowAffected -{ - public int $rowAffected; - public static function fromJSON($json): RowAffected + /** + * @throws GuzzleException + */ + function insertFeedback(array $feedback): RowAffected { - $rowAffected = new RowAffected(); - $rowAffected->rowAffected = $json->RowAffected; - return $rowAffected; + return RowAffected::fromJSON($this->request('POST', '/api/feedback/', $feedback)); } -} - -final class Gorse -{ - private string $endpoint; - private string $apiKey; - function __construct(string $endpoint, string $apiKey) + /** + * @throws GuzzleException + */ + function listFeedback(string $feedback_type, string $user_id, string $item_id): array { - $this->endpoint = $endpoint; - $this->apiKey = $apiKey; + return $this->request('GET', '/api/feedback/' . $feedback_type . '/' . $user_id . '/' . $item_id, null); } /** * @throws GuzzleException */ - function insertUser(User $user): RowAffected + function getFeedback(string $user_id, string $item_id): array { - return RowAffected::fromJSON($this->request('POST', '/api/user/', $user)); + return $this->request('GET', '/api/feedback/' . $user_id . '/' . $item_id, null); } - + /** * @throws GuzzleException */ - function getUser(string $user_id): User + function getFeedbackByType(string $feedback_type): array { - return User::fromJSON($this->request('GET', '/api/user/' . $user_id, null)); + return $this->request('GET', '/api/feedback/' . $feedback_type, null); } /** * @throws GuzzleException */ - function deleteUser(string $user_id): RowAffected + function deleteFeedback(string $feedback_type, string $user_id, string $item_id): RowAffected { - return RowAffected::fromJSON($this->request('DELETE', '/api/user/' . $user_id, null)); + return RowAffected::fromJSON($this->request('DELETE', '/api/feedback/' . $feedback_type . '/' . $user_id . '/' . $item_id, null)); } /** * @throws GuzzleException */ - function insertItem(Item $item): RowAffected + function getRecommend(string $user_id, ?string $write_back_type = null, ?string $write_back_delay = null, int $n = 10, int $offset = 0): array { - return RowAffected::fromJSON($this->request('POST', '/api/item/', $item)); + $params = ['n' => $n, 'offset' => $offset]; + if ($write_back_type) $params['write-back-type'] = $write_back_type; + if ($write_back_delay) $params['write-back-delay'] = $write_back_delay; + + return $this->request('GET', '/api/recommend/' . $user_id, null, $params); } - + /** * @throws GuzzleException */ - function getItem(string $item_id): Item + function getSessionRecommend(array $feedback, int $n = 10): array { - return Item::fromJSON($this->request('GET', '/api/item/' . $item_id, null)); + $scores = []; + $response = $this->request('POST', '/api/session/recommend?n=' . $n, $feedback); + foreach ($response as $score) { + $scores[] = Score::fromJSON($score); + } + return $scores; } /** * @throws GuzzleException */ - function deleteItem(string $item_id): RowAffected + function getNeighbors(string $item_id, int $n = 10, int $offset = 0): array { - return RowAffected::fromJSON($this->request('DELETE', '/api/item/' . $item_id, null)); + return $this->getItemNeighbors('neighbors', $item_id, $n, $offset); + } + + /** + * @throws GuzzleException + */ + function getItemNeighbors(string $name, string $item_id, int $n = 10, int $offset = 0): array + { + $scores = []; + $response = $this->request('GET', "/api/item-to-item/$name/$item_id", null, ['n' => $n, 'offset' => $offset]); + foreach ($response as $score) { + $scores[] = Score::fromJSON($score); + } + return $scores; } /** * @throws GuzzleException */ - function insertFeedback(array $feedback): RowAffected + function getUserNeighbors(string $name, string $user_id, int $n = 10, int $offset = 0): array { - return RowAffected::fromJSON($this->request('POST', '/api/feedback/', $feedback)); + $scores = []; + $response = $this->request('GET', "/api/user-to-user/$name/$user_id", null, ['n' => $n, 'offset' => $offset]); + foreach ($response as $score) { + $scores[] = Score::fromJSON($score); + } + return $scores; } /** * @throws GuzzleException */ - function deleteFeedback(string $feedback_type, string $user_id, string $item_id): RowAffected + function getNonPersonalized(string $name, ?string $user_id = null, int $n = 10, int $offset = 0): array { - return RowAffected::fromJSON($this->request('DELETE', '/api/feedback/' . $feedback_type . '/' . $user_id . '/' . $item_id, null)); + $params = ['n' => $n, 'offset' => $offset]; + if ($user_id) $params['user-id'] = $user_id; + + $scores = []; + $response = $this->request('GET', "/api/non-personalized/$name", null, $params); + foreach ($response as $score) { + $scores[] = Score::fromJSON($score); + } + return $scores; } /** * @throws GuzzleException */ - function getRecommend(string $user_id, int $n = 10): array + function getLatest(?string $user_id = null, int $n = 10, int $offset = 0): array { - return $this->request('GET', '/api/recommend/' . $user_id . '?n=' . $n, null); + $params = ['n' => $n, 'offset' => $offset]; + if ($user_id) $params['user-id'] = $user_id; + + $scores = []; + $response = $this->request('GET', '/api/latest', null, $params); + foreach ($response as $score) { + $scores[] = Score::fromJSON($score); + } + return $scores; } /** * @throws GuzzleException */ - private function request(string $method, string $uri, $body) + private function request(string $method, string $uri, $body, array $query = []) { - $client = new GuzzleHttp\Client(['base_uri' => $this->endpoint]); - $options = [GuzzleHttp\RequestOptions::HEADERS => ['X-API-Key' => $this->apiKey]]; + $options = [RequestOptions::HEADERS => ['X-API-Key' => $this->apiKey]]; if ($body != null) { - $options[GuzzleHttp\RequestOptions::JSON] = $body; + $options[RequestOptions::JSON] = $body; + } + if (!empty($query)) { + $options[RequestOptions::QUERY] = $query; } - $response = $client->request($method, $uri, $options); + $response = $this->client->request($method, $uri, $options); return json_decode($response->getBody()); } } \ No newline at end of file diff --git a/src/Model/Feedback.php b/src/Model/Feedback.php new file mode 100644 index 0000000..5cf5eb9 --- /dev/null +++ b/src/Model/Feedback.php @@ -0,0 +1,34 @@ +feedback_type = $feedback_type; + $this->user_id = $user_id; + $this->item_id = $item_id; + $this->value = $value; + $this->timestamp = $timestamp; + } + + public function jsonSerialize(): array + { + return [ + 'FeedbackType' => $this->feedback_type, + 'UserId' => $this->user_id, + 'ItemId' => $this->item_id, + 'Value' => $this->value, + 'Timestamp' => $this->timestamp, + ]; + } +} diff --git a/src/Model/Item.php b/src/Model/Item.php new file mode 100644 index 0000000..b61f064 --- /dev/null +++ b/src/Model/Item.php @@ -0,0 +1,42 @@ +itemId = $itemId; + $this->isHidden = $isHidden; + $this->labels = $labels; + $this->categories = $categories; + $this->timestamp = $timestamp; + $this->comment = $comment; + } + + public function jsonSerialize(): array + { + return [ + 'ItemId' => $this->itemId, + 'IsHidden' => $this->isHidden, + 'Labels' => $this->labels, + 'Categories' => $this->categories, + 'Timestamp' => $this->timestamp, + 'Comment' => $this->comment, + ]; + } + + public static function fromJSON($json): Item + { + return new Item($json->ItemId, $json->IsHidden, (array) $json->Categories, $json->Timestamp, (array) $json->Labels, $json->Comment); + } +} diff --git a/src/Model/RowAffected.php b/src/Model/RowAffected.php new file mode 100644 index 0000000..5610541 --- /dev/null +++ b/src/Model/RowAffected.php @@ -0,0 +1,15 @@ +rowAffected = $json->RowAffected; + return $rowAffected; + } +} diff --git a/src/Model/Score.php b/src/Model/Score.php new file mode 100644 index 0000000..b9a14a9 --- /dev/null +++ b/src/Model/Score.php @@ -0,0 +1,17 @@ +id = $json->Id; + $score->score = $json->Score; + return $score; + } +} diff --git a/src/Model/User.php b/src/Model/User.php new file mode 100644 index 0000000..36772cd --- /dev/null +++ b/src/Model/User.php @@ -0,0 +1,33 @@ +userId = $userId; + $this->labels = $labels; + $this->comment = $comment; + } + + public function jsonSerialize(): array + { + return [ + 'UserId' => $this->userId, + 'Labels' => $this->labels, + 'Comment' => $this->comment, + ]; + } + + public static function fromJSON($json): User + { + return new User($json->UserId, (array) $json->Labels, $json->Comment); + } +} diff --git a/test/GorseTest.php b/test/GorseTest.php index 63a328a..df135d0 100644 --- a/test/GorseTest.php +++ b/test/GorseTest.php @@ -1,5 +1,9 @@ "M", "occupation" => "engineer"), "zhenghaoz"); + $user = new User("1000", array("M", "engineer"), "zhenghaoz"); $rowsAffected = $client->insertUser($user); $this->assertEquals(1, $rowsAffected->rowAffected); $returnUser = $client->getUser("1000"); @@ -39,7 +43,7 @@ public function testItems() { $client = new Gorse(self::ENDPOINT, self::API_KEY); - $item = new Item("2000", true, array("embedding" => array(0.1, 0.2, 0.3)), array("Comedy", "Animation"), "2022-11-20T13:55:27Z", "Minions (2015)"); + $item = new Item("2000", true, array("Comedy", "Animation"), "2022-11-20T13:55:27Z", array("comedy", "movie"), "Minions (2015)"); $rowsAffected = $client->insertItem($item); $this->assertEquals(1, $rowsAffected->rowAffected); $returnItem = $client->getItem("2000"); @@ -63,9 +67,9 @@ public function testFeedback() $client = new Gorse(self::ENDPOINT, self::API_KEY); $feedback = array( - new Feedback("watch", "2000", "1", 1.0, gmdate("Y-m-d\TH:i:s\Z")), - new Feedback("watch", "2000", "1060", 2.0, gmdate("Y-m-d\TH:i:s\Z")), - new Feedback("watch", "2000", "11", 3.0, gmdate("Y-m-d\TH:i:s\Z")), + new Feedback("watch", "2000", "1", gmdate("Y-m-d\TH:i:s\Z"), 1.0), + new Feedback("watch", "2000", "1060", gmdate("Y-m-d\TH:i:s\Z"), 2.0), + new Feedback("watch", "2000", "11", gmdate("Y-m-d\TH:i:s\Z"), 3.0), ); foreach ($feedback as $fb) { $client->deleteFeedback($fb->feedback_type, $fb->user_id, $fb->item_id); @@ -81,7 +85,21 @@ public function testRecommend() { $client = new Gorse(self::ENDPOINT, self::API_KEY); $client->insertUser(new User("3000", array(), "")); - $items = $client->getRecommend('3000', 3); - $this->assertEquals(['315', '1432', '918'], $items); + $items = $client->getRecommend('3000', null, null, 3); + $this->assertIsArray($items); + } + + /** + * @throws GuzzleException + */ + public function testNonPersonalized() + { + $client = new Gorse(self::ENDPOINT, self::API_KEY); + // Test getLatest which returns Score[] + $items = $client->getLatest('3000', 3); + $this->assertIsArray($items); + foreach ($items as $item) { + $this->assertInstanceOf(\Gorse\Model\Score::class, $item); + } } }