Skip to content

Commit

Permalink
Merge e56fd1f into 58b464d
Browse files Browse the repository at this point in the history
  • Loading branch information
foglcz committed Aug 30, 2018
2 parents 58b464d + e56fd1f commit 94f433b
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 16 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
<!-- 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.
- **BC BREAK** | `UserRecommendation` does not have default filter (was previously set to: `valid_to >= NOW`).
- **BC BREAK** | `UserRecommendation` now uses MQL query language by default for filtering.

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

### Added
- `UserRecommendation` now sends which item properties should be returned alongside with item_ids.

## 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
60 changes: 58 additions & 2 deletions 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 @@ -184,7 +184,7 @@ You can also set more granular options of the recommendation command:

```php
$recommendation = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600);
$recommendation->setFilters(['valid_to >= NOW']) // Note this filter is present by default
$recommendation->setFilters(['for_recommendation = 1'])
->setMinimalRelevance(UserRecommendation::MINIMAL_RELEVANCE_HIGH)
->enableHardRotation();

Expand All @@ -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_id', '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
80 changes: 72 additions & 8 deletions src/Model/Command/UserRecommendation.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class UserRecommendation extends AbstractCommand implements UserAwareInterface
public const MINIMAL_RELEVANCE_LOW = 'low';
public const MINIMAL_RELEVANCE_MEDIUM = 'medium';
public const MINIMAL_RELEVANCE_HIGH = 'high';
public const FILTER_TYPE_RGX = 'rgx';
public const FILTER_TYPE_MQL = 'mql';

/** @var string */
protected $filterOperator = 'and';
Expand All @@ -29,18 +31,29 @@ class UserRecommendation extends AbstractCommand implements UserAwareInterface
private $hardRotation = false;
/** @var string */
private $minimalRelevance = self::MINIMAL_RELEVANCE_LOW;
/** @var array */
private $filters = ['valid_to >= NOW'];
/** @var string[] */
private $filters = [];
/** @var string */
private $filterType = self::FILTER_TYPE_MQL;
/** @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 +66,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 @@ -98,7 +113,7 @@ public function setMinimalRelevance(string $minimalRelevance): self
}

/**
* Add a filter to already added filters (including the default filter).
* Add a filter(s) to recommendation request. This can be called multiple times.
*
* @return $this
*/
Expand All @@ -110,7 +125,7 @@ public function addFilter(string $filter): self
}

/**
* Overwrite all filters by custom one. Note this will override also the default filter.
* Overwrite all filters by custom one.
*
* @return $this
*/
Expand All @@ -123,6 +138,49 @@ public function setFilters(array $filters): self
return $this;
}

/**
* Specify the filter type being used.
*/
public function setFilterType(string $filterType): self
{
Assertion::typeIdentifier($filterType);
Assertion::choice(
$filterType,
[static::FILTER_TYPE_RGX, static::FILTER_TYPE_MQL]
);

$this->filterType = $filterType;

return $this;
}

/**
* Specify which item property you want returned.
*/
public function addResponseProperty(string $property): self
{
Assertion::typeIdentifier($property);

$this->responseProperties[] = $property;

return $this;
}

/**
* Overwrite all properties by custom specified list. Note that this will overried the defaults.
*
* @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,8 +256,14 @@ protected function getCommandParameters(): array
'hard_rotation' => $this->hardRotation,
'min_relevance' => $this->minimalRelevance,
'filter' => $this->assembleFiltersString(),
'filter_type' => $this->filterType,
'properties' => $this->responseProperties,
];

if ($this->filterType === self::FILTER_TYPE_RGX) {
unset($parameters['filter_type']);
}

if ($this->modelName !== null) {
$parameters['model_name'] = $this->modelName;
}
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-v1.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": [
"jd-5a2ebcb2fc13ae1fdb00016d",
"jd-5a2ebcb1fc13ae1fdb0000e3",
"jd-5a2ebcb2fc13ae1fdb000295",
"jd-5a2ebcb2fc13ae1fdb00022d",
"jd-5a2ebcb1fc13ae1fdb00010a",
"jd-5a2ebcb1fc13ae1fdb00010d",
"jd-5a2ebcb3fc13ae1fdb000389",
"jd-5a2ebcb3fc13ae1fdb000367",
"jd-5a2ebcb2fc13ae1fdb000285",
"jd-5a2ebcb2fc13ae1fdb0002cb"
],
"message": "",
"status": "OK"
}
]
},
"message": "",
"status": "OK"
}
38 changes: 38 additions & 0 deletions tests/unit/Http/Fixtures/response-recommendation-v2.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"
}
31 changes: 31 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,34 @@ public function shouldThrowExceptionWhenJsonWithInvalidDataIsDecoded(): void
$this->expectExceptionMessage('"invalid": [],');
$this->decoder->decode($response);
}

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

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

$this->assertNotEmpty($decodedResponse->getRecommendation()->getData());
$this->assertContainsOnly('string', $decodedResponse->getRecommendation()->getData());
}

/** @test */
public function shouldDecodeVersionTwoRecommendationResponse(): void
{
$response = $this->createJsonResponseFromFile(__DIR__ . '/Fixtures/response-recommendation-v2.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);
}
}
}

0 comments on commit 94f433b

Please sign in to comment.