Skip to content

Commit

Permalink
Implement support for Item Properties in Recommendation Requests
Browse files Browse the repository at this point in the history
  • Loading branch information
foglcz committed Aug 31, 2018
1 parent 58b464d commit a3e5789
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 7 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
<!-- There is always Unreleased section on the top. Subsections (Added, Changed, Fixed, Removed) should be added as needed. -->

## Unreleased
### Changed
- **BC BREAK** | `UserRecommendation` now returns new format of response in `->getData()`, which is a list of `stdClass` instances.

### Fixed
- Exceptions occurring during async request now generate rejected promise (as they should) and are no longer thrown directly.

### Added
- Ability to request item properties in `UserRecommendation` command, which are then returned by Matej alongside with `item_id`.

## 1.6.0 - 2018-06-01
### Added
- `UserForget` command to anonymize or completely delete all data belonging to specific user(s) (`$matej->request()->forget()`).
Expand Down
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ foreach ($response->getCommandResponses() as $commandResponse) {
and [Item Properties](#item-properties-setup-to-setup-you-matej-database) endpoints have syntax sugar shortcuts,
which makes processing responses easier. See below for detailed examples.

### Item properties setup (to setup you Matej database)
### Item properties setup (to setup your Matej's database)

```php
$matej = new Matej('accountId', 'apikey');
Expand Down Expand Up @@ -205,8 +205,64 @@ echo $response->getUserMerge()->getStatus(); // SKIPPED
echo $response->getRecommendation()->getStatus(); // OK

$recommendations = $response->getRecommendation()->getData();

// var_dump($recommendations):
// array(2) {
// [0] => object(stdClass)#1 (2) {
// ["item_id"] => string(9) "item_id_1"
// }
// [1] => object(stdClass)#2 (2) {
// ["item_id"] => string(9) "item_id_2"
// }
// }
```

#### Recommendation response properties

Every item in Matej has its id, and optionally other item properties. These properties can be set up in [item properties setup](#item-properties-setup-to-setup-you-matej-database),
and you can upload item data in the [events](#send-events-data-to-matej) request. This has major benefit because you can request
these properties to be returned as part of your Recommendation Request.

We call them response properties, and they can be specified either as the last parameter of `UserRecommendation::create` function,
by calling `->addResponseProperty()` method, or by calling `->setResponseProperties()` method. Following will request an `item_id`,
`item_url` and `item_title`:

```php
$recommendation = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600, ['item_url'])
->addResponseProperty('item_title');

$response = $matej->request()
->recommendation($recommendation)
->send();

$recommendedItems = $response->getRecommendation()->getData();

// $recommendedItems is an array of stdClass instances:
//
// array(2) {
// [0] => object(stdClass)#1 (2) {
// ["item_id"] => string(9) "item_id_1"
// ["item_url"] => string(5) "url_1"
// ["item_title"] => string(5) "title_1"
// }
// [1] => object(stdClass)#2 (2) {
// ["item_id"] => string(9) "item_id_2"
// ["item_url"] => string(10) "url_2"
// ["item_title"] => string(10) "title_2"
// }
// }
```

If you don't specify any response properties, Matej will return an array of `stdClass` instances, which contain only `item_id` property.
If you do request at least one response property, you don't need to metion `item_id`, as Matej will always return it regardless of the
properties requested.

If you request an unknown property, Matej will return a `BAD REQUEST` with HTTP status code `400`.

This way, when you receive recommendations from Matej, you don't need to loop the `item_id` and retrieve further information
from your local database. It means, however, that you'll have to keep all items up to date within Matej,
which can be done through [events](#send-events-data-to-matej) request.

### Request item sorting for single user

Request item sorting for a single user. You can combine this sorting command with the most recent interaction
Expand Down
49 changes: 44 additions & 5 deletions src/Model/Command/UserRecommendation.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,27 @@ class UserRecommendation extends AbstractCommand implements UserAwareInterface
private $hardRotation = false;
/** @var string */
private $minimalRelevance = self::MINIMAL_RELEVANCE_LOW;
/** @var array */
/** @var string[] */
private $filters = ['valid_to >= NOW'];
/** @var string|null */
private $modelName = null;
/** @var string[] */
private $responseProperties = [];

private function __construct(string $userId, int $count, string $scenario, float $rotationRate, int $rotationTime)
{
private function __construct(
string $userId,
int $count,
string $scenario,
float $rotationRate,
int $rotationTime,
array $responseProperties
) {
$this->setUserId($userId);
$this->setCount($count);
$this->setScenario($scenario);
$this->setRotationRate($rotationRate);
$this->setRotationTime($rotationTime);
$this->setResponseProperties($responseProperties);
}

/**
Expand All @@ -53,16 +62,18 @@ private function __construct(string $userId, int $count, string $scenario, float
* Set from 0.0 for no rotation (same items will be recommended) up to 1.0 (same items should not be recommended).
* @param int $rotationTime Specify for how long will the item's rotationRate be taken in account and so the item
* is penalized for recommendations.
* @param string[] $responseProperties Specify which properties you want to retrieve from Matej alongside the item_id.
* @return static
*/
public static function create(
string $userId,
int $count,
string $scenario,
float $rotationRate,
int $rotationTime
int $rotationTime,
array $responseProperties = []
): self {
return new static($userId, $count, $scenario, $rotationRate, $rotationTime);
return new static($userId, $count, $scenario, $rotationRate, $rotationTime, $responseProperties);
}

/**
Expand Down Expand Up @@ -123,6 +134,33 @@ public function setFilters(array $filters): self
return $this;
}

/**
* Add another response property you want returned. item_id is always returned by Matej.
*/
public function addResponseProperty(string $property): self
{
Assertion::typeIdentifier($property);

$this->responseProperties[] = $property;

return $this;
}

/**
* Set all response properties you want returned. item_id is always returned by Matej, even when you don't specify it.
*
* @param string[] $properties
* @return $this
*/
public function setResponseProperties(array $properties): self
{
Assertion::allTypeIdentifier($properties);

$this->responseProperties = $properties;

return $this;
}

/***
* Set A/B model name
*
Expand Down Expand Up @@ -198,6 +236,7 @@ protected function getCommandParameters(): array
'hard_rotation' => $this->hardRotation,
'min_relevance' => $this->minimalRelevance,
'filter' => $this->assembleFiltersString(),
'properties' => $this->responseProperties,
];

if ($this->modelName !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,28 @@ public function shouldFailOnInvalidModelName(): void
$this->expectExceptionCode(400);
$this->expectExceptionMessage('BAD REQUEST');

$recommendation = $this->createRecommendationCommand('user-a')
->setModelName('invalid-model-name');

$this->createMatejInstance()
->request()
->recommendation($recommendation)
->send();
}

/** @test */
public function shouldFailOnInvalidPropertyName(): void
{
$this->expectException(RequestException::class);
$this->expectExceptionCode(400);
$this->expectExceptionMessage('BAD REQUEST');

$recommendation = $this->createRecommendationCommand('user-a')
->addResponseProperty('unknown-property');

$this->createMatejInstance()
->request()
->recommendation($this->createRecommendationCommand('user-a')->setModelName('invalid-model-name'))
->recommendation($recommendation)
->send();
}

Expand Down
38 changes: 38 additions & 0 deletions tests/unit/Http/Fixtures/response-recommendation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"commands": {
"number_of_commands": 3,
"number_of_failed_commands": 0,
"number_of_skipped_commands": 2,
"number_of_successful_commands": 1,
"responses": [
{
"data": [],
"message": "",
"status": "SKIPPED"
},
{
"data": [],
"message": "",
"status": "SKIPPED"
},
{
"data": [
{"item_id": "jd-5a2ebcb2fc13ae1fdb00016d", "item_url": "http://test-url/5a2ebcb2fc13ae1fdb00016d"},
{"item_id": "jd-5a2ebcb1fc13ae1fdb0000e3", "item_url": "http://test-url/5a2ebcb1fc13ae1fdb0000e3"},
{"item_id": "jd-5a2ebcb2fc13ae1fdb000295", "item_url": "http://test-url/5a2ebcb2fc13ae1fdb000295"},
{"item_id": "jd-5a2ebcb2fc13ae1fdb00022d", "item_url": "http://test-url/5a2ebcb2fc13ae1fdb00022d"},
{"item_id": "jd-5a2ebcb1fc13ae1fdb00010a", "item_url": "http://test-url/5a2ebcb1fc13ae1fdb00010a"},
{"item_id": "jd-5a2ebcb1fc13ae1fdb00010d", "item_url": "http://test-url/5a2ebcb1fc13ae1fdb00010d"},
{"item_id": "jd-5a2ebcb3fc13ae1fdb000389", "item_url": "http://test-url/5a2ebcb3fc13ae1fdb000389"},
{"item_id": "jd-5a2ebcb3fc13ae1fdb000367", "item_url": "http://test-url/5a2ebcb3fc13ae1fdb000367"},
{"item_id": "jd-5a2ebcb2fc13ae1fdb000285", "item_url": "http://test-url/5a2ebcb2fc13ae1fdb000285"},
{"item_id": "jd-5a2ebcb2fc13ae1fdb0002cb", "item_url": "http://test-url/5a2ebcb2fc13ae1fdb0002cb"}
],
"message": "",
"status": "OK"
}
]
},
"message": "",
"status": "OK"
}
19 changes: 19 additions & 0 deletions tests/unit/Http/ResponseDecoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use GuzzleHttp\Psr7\Response;
use Lmc\Matej\Exception\ResponseDecodingException;
use Lmc\Matej\Model\CommandResponse;
use Lmc\Matej\Model\Response\RecommendationsResponse;
use Lmc\Matej\UnitTestCase;

class ResponseDecoderTest extends UnitTestCase
Expand Down Expand Up @@ -96,4 +97,22 @@ public function shouldThrowExceptionWhenJsonWithInvalidDataIsDecoded(): void
$this->expectExceptionMessage('"invalid": [],');
$this->decoder->decode($response);
}

/** @test */
public function shouldDecodeRecommendationResponse(): void
{
$response = $this->createJsonResponseFromFile(__DIR__ . '/Fixtures/response-recommendation.json');

/** @var RecommendationsResponse */
$decodedResponse = $this->decoder->decode($response, RecommendationsResponse::class);

// Recommended items should be stdClasses
$this->assertNotEmpty($decodedResponse->getRecommendation()->getData());
$this->assertContainsOnlyInstancesOf(\stdClass::class, $decodedResponse->getRecommendation()->getData());

foreach ($decodedResponse->getRecommendation()->getData() as $recommendedItem) {
$this->assertObjectHasAttribute('item_id', $recommendedItem);
$this->assertObjectHasAttribute('item_url', $recommendedItem);
}
}
}
18 changes: 18 additions & 0 deletions tests/unit/Model/Command/UserRecommendationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function shouldBeInstantiableViaNamedConstructorWithDefaultValues(): void
'hard_rotation' => false,
'min_relevance' => UserRecommendation::MINIMAL_RELEVANCE_LOW,
'filter' => 'valid_to >= NOW',
'properties' => [],
// intentionally no model name ==> should be absent when not used
],
],
Expand Down Expand Up @@ -62,6 +63,7 @@ public function shouldUseCustomParameters(): void
'hard_rotation' => true,
'min_relevance' => UserRecommendation::MINIMAL_RELEVANCE_HIGH,
'filter' => 'foo = bar and baz = ban',
'properties' => [],
'model_name' => $modelName,
],
],
Expand Down Expand Up @@ -91,4 +93,20 @@ public function shouldAssembleFilters(): void

$this->assertSame('my_filter = 1 and other_filter = foo', $command->jsonSerialize()['parameters']['filter']);
}

/** @test */
public function shouldAllowModificationOfResponseProperties(): void
{
$command = UserRecommendation::create('user-id', 333, 'test-scenario', 1.0, 3600, ['test']);
$this->assertSame(['test'], $command->jsonSerialize()['parameters']['properties']);

// Add some properties
$command->addResponseProperty('url');

$this->assertSame(['test', 'url'], $command->jsonSerialize()['parameters']['properties']);

// Overwrite all properties
$command->setResponseProperties(['position_title']);
$this->assertSame(['position_title'], $command->jsonSerialize()['parameters']['properties']);
}
}

0 comments on commit a3e5789

Please sign in to comment.