From 45e64106f20d164a9b2553463a88506f745331d2 Mon Sep 17 00:00:00 2001 From: Wasin Phungwigrai Date: Fri, 20 Feb 2026 15:30:15 +0700 Subject: [PATCH 1/2] fix(api): url encode ids in api requests Apply URL encoding to all ID parameters in API endpoint paths to prevent issues with special characters in user IDs, item IDs, and feedback types. This ensures proper handling of strings containing characters that have special meaning in URLs. --- src/Gorse.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Gorse.php b/src/Gorse.php index 851ffa3..e90a3d3 100644 --- a/src/Gorse.php +++ b/src/Gorse.php @@ -38,7 +38,7 @@ function insertUser(User $user): RowAffected */ function getUser(string $user_id): User { - return User::fromJSON($this->request('GET', '/api/user/' . $user_id, null)); + return User::fromJSON($this->request('GET', '/api/user/' . urlencode($user_id), null)); } /** @@ -46,7 +46,7 @@ function getUser(string $user_id): User */ function deleteUser(string $user_id): RowAffected { - return RowAffected::fromJSON($this->request('DELETE', '/api/user/' . $user_id, null)); + return RowAffected::fromJSON($this->request('DELETE', '/api/user/' . urlencode($user_id), null)); } /** @@ -62,7 +62,7 @@ function insertItem(Item $item): RowAffected */ function getItem(string $item_id): Item { - return Item::fromJSON($this->request('GET', '/api/item/' . $item_id, null)); + return Item::fromJSON($this->request('GET', '/api/item/' . urlencode($item_id), null)); } /** @@ -70,7 +70,7 @@ function getItem(string $item_id): Item */ function deleteItem(string $item_id): RowAffected { - return RowAffected::fromJSON($this->request('DELETE', '/api/item/' . $item_id, null)); + return RowAffected::fromJSON($this->request('DELETE', '/api/item/' . urlencode($item_id), null)); } /** @@ -78,7 +78,7 @@ function deleteItem(string $item_id): RowAffected */ function updateItem(string $item_id, Item $item): RowAffected { - return RowAffected::fromJSON($this->request('PATCH', '/api/item/' . $item_id, $item)); + return RowAffected::fromJSON($this->request('PATCH', '/api/item/' . urlencode($item_id), $item)); } /** @@ -94,7 +94,7 @@ function insertFeedback(array $feedback): RowAffected */ function listFeedback(string $feedback_type, string $user_id, string $item_id): array { - return $this->request('GET', '/api/feedback/' . $feedback_type . '/' . $user_id . '/' . $item_id, null); + return $this->request('GET', '/api/feedback/' . urlencode($feedback_type) . '/' . urlencode($user_id) . '/' . urlencode($item_id), null); } /** @@ -102,7 +102,7 @@ function listFeedback(string $feedback_type, string $user_id, string $item_id): */ function getFeedback(string $user_id, string $item_id): array { - return $this->request('GET', '/api/feedback/' . $user_id . '/' . $item_id, null); + return $this->request('GET', '/api/feedback/' . urlencode($user_id) . '/' . urlencode($item_id), null); } /** @@ -110,7 +110,7 @@ function getFeedback(string $user_id, string $item_id): array */ function getFeedbackByType(string $feedback_type): array { - return $this->request('GET', '/api/feedback/' . $feedback_type, null); + return $this->request('GET', '/api/feedback/' . urlencode($feedback_type), null); } /** @@ -118,7 +118,7 @@ function getFeedbackByType(string $feedback_type): array */ function deleteFeedback(string $feedback_type, string $user_id, string $item_id): RowAffected { - return RowAffected::fromJSON($this->request('DELETE', '/api/feedback/' . $feedback_type . '/' . $user_id . '/' . $item_id, null)); + return RowAffected::fromJSON($this->request('DELETE', '/api/feedback/' . urlencode($feedback_type) . '/' . urlencode($user_id) . '/' . urlencode($item_id), null)); } /** @@ -130,7 +130,7 @@ function getRecommend(string $user_id, ?string $write_back_type = null, ?string 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); + return $this->request('GET', '/api/recommend/' . urlencode($user_id), null, $params); } /** @@ -160,7 +160,7 @@ function getNeighbors(string $item_id, int $n = 10, int $offset = 0): array 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]); + $response = $this->request('GET', "/api/item-to-item/" . urlencode($name) . "/" . urlencode($item_id), null, ['n' => $n, 'offset' => $offset]); foreach ($response as $score) { $scores[] = Score::fromJSON($score); } @@ -173,7 +173,7 @@ function getItemNeighbors(string $name, string $item_id, int $n = 10, int $offse function getUserNeighbors(string $name, string $user_id, int $n = 10, int $offset = 0): array { $scores = []; - $response = $this->request('GET', "/api/user-to-user/$name/$user_id", null, ['n' => $n, 'offset' => $offset]); + $response = $this->request('GET', "/api/user-to-user/" . urlencode($name) . "/" . urlencode($user_id), null, ['n' => $n, 'offset' => $offset]); foreach ($response as $score) { $scores[] = Score::fromJSON($score); } @@ -189,7 +189,7 @@ function getNonPersonalized(string $name, ?string $user_id = null, int $n = 10, if ($user_id) $params['user-id'] = $user_id; $scores = []; - $response = $this->request('GET', "/api/non-personalized/$name", null, $params); + $response = $this->request('GET', "/api/non-personalized/" . urlencode($name), null, $params); foreach ($response as $score) { $scores[] = Score::fromJSON($score); } From f80bc8653bda327dc3f8e89f3542b093ac6b4969 Mon Sep 17 00:00:00 2001 From: Wasin Phungwigrai Date: Tue, 17 Mar 2026 07:48:00 +0000 Subject: [PATCH 2/2] feat(api): add batch insert methods for users and items Add insertUsers() and insertItems() methods to allow batch insertion of multiple users and items in a single API request. This improves efficiency when inserting multiple records at once. --- README.md | 37 ++++++++++++++++++++++++++++++++++- src/Gorse.php | 16 ++++++++++++++++ test/GorseTest.php | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bf5e871..c35d633 100644 --- a/README.md +++ b/README.md @@ -29,20 +29,55 @@ $user = new User("100", ["gender" => "M", "age" => "25"], "my_comment"); $rowsAffected = $client->insertUser($user); ``` +Insert multiple users at once: + +```php +$users = [ + new User("100", ["gender" => "M", "age" => "25"], "user1"), + new User("101", ["gender" => "F", "age" => "30"], "user2"), + new User("102", ["gender" => "M", "age" => "28"], "user3"), +]; +$rowsAffected = $client->insertUsers($users); +``` + Insert items: ```php $item = new Item( "2000", true, - ["embedding" => [0.1, 0.2, 0.3]], ["Comedy", "Animation"], "2022-11-20T13:55:27Z", + ["embedding" => [0.1, 0.2, 0.3]], "Minions (2015)" ); $rowsAffected = $client->insertItem($item); ``` +Insert multiple items at once: + +```php +$items = [ + new Item( + "2000", + false, + ["Comedy", "Animation"], + "2022-11-20T13:55:27Z", + ["comedy", "movie"], + "Minions (2015)" + ), + new Item( + "2001", + false, + ["Action", "Adventure"], + "2022-11-21T13:55:27Z", + ["action", "movie"], + "The Matrix (1999)" + ), +]; +$rowsAffected = $client->insertItems($items); +``` + Insert feedback: ```php diff --git a/src/Gorse.php b/src/Gorse.php index e90a3d3..c433365 100644 --- a/src/Gorse.php +++ b/src/Gorse.php @@ -33,6 +33,14 @@ function insertUser(User $user): RowAffected return RowAffected::fromJSON($this->request('POST', '/api/user/', $user)); } + /** + * @throws GuzzleException + */ + function insertUsers(array $users): RowAffected + { + return RowAffected::fromJSON($this->request('POST', '/api/users', $users)); + } + /** * @throws GuzzleException */ @@ -57,6 +65,14 @@ function insertItem(Item $item): RowAffected return RowAffected::fromJSON($this->request('POST', '/api/item/', $item)); } + /** + * @throws GuzzleException + */ + function insertItems(array $items): RowAffected + { + return RowAffected::fromJSON($this->request('POST', '/api/items', $items)); + } + /** * @throws GuzzleException */ diff --git a/test/GorseTest.php b/test/GorseTest.php index df135d0..09b8cd7 100644 --- a/test/GorseTest.php +++ b/test/GorseTest.php @@ -36,6 +36,30 @@ public function testUsers(): void } } + /** + * @throws GuzzleException + */ + public function testInsertMultipleUsers(): void + { + $client = new Gorse(self::ENDPOINT, self::API_KEY); + + $users = array( + new User("1001", array("M", "engineer"), "user1"), + new User("1002", array("F", "designer"), "user2"), + new User("1003", array("M", "developer"), "user3"), + ); + + // Insert multiple users + $rowsAffected = $client->insertUsers($users); + $this->assertEquals(3, $rowsAffected->rowAffected); + + // Verify users were inserted + foreach ($users as $user) { + $returnUser = $client->getUser($user->userId); + $this->assertEquals($user, $returnUser); + } + } + /** * @throws GuzzleException */ @@ -59,6 +83,30 @@ public function testItems() } } + /** + * @throws GuzzleException + */ + public function testInsertMultipleItems(): void + { + $client = new Gorse(self::ENDPOINT, self::API_KEY); + + $items = array( + new Item("2001", false, array("Comedy", "Animation"), "2022-11-20T13:55:27Z", array("comedy", "movie"), "Minions (2015)"), + new Item("2002", false, array("Action", "Adventure"), "2022-11-21T13:55:27Z", array("action", "movie"), "The Matrix (1999)"), + new Item("2003", false, array("Drama", "Thriller"), "2022-11-22T13:55:27Z", array("drama", "movie"), "The Silence of the Lambs (1991)"), + ); + + // Insert multiple items + $rowsAffected = $client->insertItems($items); + $this->assertEquals(3, $rowsAffected->rowAffected); + + // Verify items were inserted + foreach ($items as $item) { + $returnItem = $client->getItem($item->itemId); + $this->assertEquals($item, $returnItem); + } + } + /** * @throws GuzzleException */