Skip to content

Commit

Permalink
feat(recommend): add RecommendClient class (#686)
Browse files Browse the repository at this point in the history
  • Loading branch information
damcou committed Aug 31, 2021
1 parent b9adf08 commit e7820ce
Show file tree
Hide file tree
Showing 3 changed files with 361 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/Config/RecommendConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Algolia\AlgoliaSearch\Config;

final class RecommendConfig extends AbstractConfig
{
public static function create($appId = null, $apiKey = null, $region = null)
{
$config = [
'appId' => null !== $appId ? $appId : getenv('ALGOLIA_APP_ID'),
'apiKey' => null !== $apiKey ? $apiKey : getenv('ALGOLIA_API_KEY'),
'region' => null !== $region ? $region : 'us',
];

return new static($config);
}

public function setRegion($region)
{
$this->config['region'] = $region;

return $this;
}

public function getRegion()
{
return $this->config['region'];
}
}
141 changes: 141 additions & 0 deletions src/RecommendClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

namespace Algolia\AlgoliaSearch;

use Algolia\AlgoliaSearch\Config\RecommendConfig;
use Algolia\AlgoliaSearch\Exceptions\AlgoliaException;
use Algolia\AlgoliaSearch\RequestOptions\RequestOptions;
use Algolia\AlgoliaSearch\RetryStrategy\ApiWrapper;
use Algolia\AlgoliaSearch\RetryStrategy\ClusterHosts;

final class RecommendClient
{
const RELATED_PRODUCTS = 'related-products';
const BOUGHT_TOGETHER = 'bought-together';

/**
* @var ApiWrapper
*/
private $api;

/**
* @var RecommendConfig
*/
private $config;

public function __construct(ApiWrapper $api, RecommendConfig $config)
{
$this->api = $api;
$this->config = $config;
}

public static function create($appId = null, $apiKey = null, $region = null)
{
$config = RecommendConfig::create($appId, $apiKey, $region);

return static::createWithConfig($config);
}

public static function createWithConfig(RecommendConfig $config)
{
$config = clone $config;

if ($hosts = $config->getHosts()) {
// If a list of hosts was passed, we ignore the cache
$clusterHosts = ClusterHosts::create($hosts);
} else {
$clusterHosts = ClusterHosts::createFromAppId($config->getAppId());
}

$apiWrapper = new ApiWrapper(
Algolia::getHttpClient(),
$config,
$clusterHosts
);

return new static($apiWrapper, $config);
}

/**
* Get recommendations.
*
* @param array|RequestOptions $requestOptions
*
* @return array
*/
public function getRecommendations(array $queries, $requestOptions = [])
{
foreach ($queries as $key => $query) {
// The `threshold` param is required by the endpoint to make it easier to provide a default value later,
// so we default it in the client so that users don't have to provide a value.
if (!isset($query['threshold'])) {
$queries[$key]['threshold'] = 0;
}
// Unset fallbackParameters if the model is 'bought-together'
if (self::BOUGHT_TOGETHER === $query['model'] && isset($query['fallbackParameters'])) {
unset($queries[$key]['fallbackParameters']);
}
}

$requests = [
'requests' => $queries,
];

return $this->api->write(
'POST',
api_path('/1/indexes/*/recommendations'),
$requests,
$requestOptions
);
}

/**
* Get Related products.
*
* @param array|RequestOptions $requestOptions
*
* @return array
*
* @throws AlgoliaException
*/
public function getRelatedProducts(array $queries, $requestOptions = [])
{
$queries = $this->setModel($queries, self::RELATED_PRODUCTS);

return $this->getRecommendations($queries, $requestOptions);
}

/**
* Get product frequently bought together.
*
* @param array|RequestOptions $requestOptions
*
* @return array
*
* @throws AlgoliaException
*/
public function getFrequentlyBoughtTogether(array $queries, $requestOptions = [])
{
$queries = $this->setModel($queries, self::BOUGHT_TOGETHER);

return $this->getRecommendations($queries, $requestOptions);
}

/**
* Add the model for related products and product frequently bought together.
*
* @param string $model can be either 'related-products' or 'bought-together'
*
* @return array
*
* @throws AlgoliaException
*/
private function setModel(array $queries, $model)
{
foreach ($queries as $key => $query) {
$queries[$key]['model'] = $model;
}

return $queries;
}
}
191 changes: 191 additions & 0 deletions tests/Integration/RecommendClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

namespace Algolia\AlgoliaSearch\Tests\Integration;

use Algolia\AlgoliaSearch\Config\RecommendConfig;
use Algolia\AlgoliaSearch\Http\HttpClientInterface;
use Algolia\AlgoliaSearch\Http\Psr7\Response;
use Algolia\AlgoliaSearch\RecommendClient;
use Algolia\AlgoliaSearch\RetryStrategy\ApiWrapper;
use Algolia\AlgoliaSearch\RetryStrategy\ClusterHosts;
use Psr\Http\Message\RequestInterface;

class RecommendClientTest extends BaseTest implements HttpClientInterface
{
/**
* @var RequestInterface[]
*/
private $recordedRequests = [];

protected function assertRequests(array $requests)
{
$this->assertGreaterThan(0, count($requests));
$this->assertEquals(count($requests), count($this->recordedRequests));

foreach ($requests as $i => $request) {
$recordedRequest = $this->recordedRequests[$i];

$this->assertEquals($request['method'], $recordedRequest->getMethod());
$this->assertEquals($request['path'], $recordedRequest->getUri()->getPath());
$this->assertEquals($request['body'], $recordedRequest->getBody()->getContents());
}
}

public function sendRequest(RequestInterface $request, $timeout, $connectTimeout)
{
$this->recordedRequests[] = $request;

return new Response(200, [], '{}');
}

protected function getClient()
{
$api = new ApiWrapper($this, RecommendConfig::create(), ClusterHosts::create('127.0.0.1'));
$config = RecommendConfig::create('foo', 'bar');

return new RecommendClient($api, $config);
}

public function testGetRecommendations()
{
$client = $this->getClient();

// Test method with 'bought-together' model
$client->getRecommendations([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE',
'model' => RecommendClient::BOUGHT_TOGETHER,
],
]);

// Test method with 'related-products' model
$client->getRecommendations([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE',
'model' => RecommendClient::RELATED_PRODUCTS,
],
]);

// Test method with multiple requests and specified thresholds
$client->getRecommendations([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE-1',
'model' => RecommendClient::RELATED_PRODUCTS,
'threshold' => 0,
],
[
'indexName' => 'products',
'objectID' => 'B018APC4LE-2',
'model' => RecommendClient::RELATED_PRODUCTS,
'threshold' => 0,
],
]);

// Test overrides undefined threshold with default value
$client->getRecommendations([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE',
'model' => RecommendClient::BOUGHT_TOGETHER,
'threshold' => null,
],
]);

// Test threshold is overriden by specified value
$client->getRecommendations([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE',
'model' => RecommendClient::BOUGHT_TOGETHER,
'threshold' => 42,
],
]);

$this->assertRequests([
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}',
],
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"related-products","threshold":0}]}',
],
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE-1","model":"related-products","threshold":0},{"indexName":"products","objectID":"B018APC4LE-2","model":"related-products","threshold":0}]}',
],
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}',
],
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":42}]}',
],
]);
}

public function testGetRelatedProducts()
{
$client = $this->getClient();

$client->getRelatedProducts([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE',
],
]);

$this->assertRequests([
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"related-products","threshold":0}]}',
],
]);
}

public function testGetFrequentlyBoughtTogether()
{
$client = $this->getClient();

$client->getFrequentlyBoughtTogether([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE',
],
]);

// Check if `fallbackParameters` param is not passed for 'bought-together' method
$client->getFrequentlyBoughtTogether([
[
'indexName' => 'products',
'objectID' => 'B018APC4LE',
'fallbackParameters' => [
'facetFilters' => [],
],
],
]);

$this->assertRequests([
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}',
],
[
'path' => '/1/indexes/*/recommendations',
'method' => 'POST',
'body' => '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}',
],
]);
}
}

0 comments on commit e7820ce

Please sign in to comment.