Skip to content

Commit

Permalink
Merge pull request Smile-SA#49 from Elastic-Suite/feature-recommender…
Browse files Browse the repository at this point in the history
…-graphql

Implementing resolvers for cross-sell, upsell and related
  • Loading branch information
romainruaud authored Jul 19, 2021
2 parents 6aacc29 + e8f5e6f commit 546bede
Show file tree
Hide file tree
Showing 15 changed files with 640 additions and 3 deletions.
11 changes: 10 additions & 1 deletion .github/workflows/20-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,17 @@ jobs:
INSTALL_COMMAND="$INSTALL_COMMAND --elasticsearch-host=elasticsearch"
INSTALL_COMMAND="$INSTALL_COMMAND --elasticsearch-port=9200"
fi
DISABLE_MODULES = ""
if [ "$MAGENTO_EDITION" = "community" ]; then
INSTALL_COMMAND="$INSTALL_COMMAND --disable-modules=Smile_ElasticsuiteCatalogOptimizerCustomerSegment,Smile_ElasticsuiteAbCampaignCustomerSegment"
DISABLE_MODULES="$DISABLE_MODULES Smile_ElasticsuiteCatalogOptimizerCustomerSegment,Smile_ElasticsuiteAbCampaignCustomerSegment"
fi
if [ $(version $MAGENTO_VERSION) -lt $(version "2.3.4") ]; then
DISABLE_MODULES="$DISABLE_MODULES Smile_ElasticsuiteRecommenderGraphQl"
fi
if [ "$DISABLE_MODULES" != "" ]; then
INSTALL_COMMAND="$INSTALL_COMMAND --disable-modules=$DISABLE_MODULES"
fi
$INSTALL_COMMAND --quiet
Expand Down
21 changes: 21 additions & 0 deletions Resources/tests/graphql/productdetail/query.gql
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ query productDetail($urlKey: String, $onServer: Boolean!) {
__typename
}
url_key
upsell_products {
id
name
sku
url_key
__typename
},
crosssell_products {
id
name
sku
url_key
__typename
},
related_products {
id
name
sku
url_key
__typename
}
... on ConfigurableProduct {
configurable_options {
attribute_code
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"smile/module-elasticsuite-instant-search": "self.version",
"smile/module-elasticsuite-merchandising-gauge": "self.version",
"smile/module-elasticsuite-recommender": "self.version",
"smile/module-elasticsuite-recommender-graph-ql": "self.version",
"smile/module-elasticsuite-virtual-attribute": "self.version"
},
"require-dev": {
Expand All @@ -73,6 +74,7 @@
"src/module-elasticsuite-instant-search/registration.php",
"src/module-elasticsuite-merchandising-gauge/registration.php",
"src/module-elasticsuite-recommender/registration.php",
"src/module-elasticsuite-recommender-graph-ql/registration.php",
"src/module-elasticsuite-virtual-attribute/registration.php"
],
"psr-4": {
Expand All @@ -89,6 +91,7 @@
"Smile\\ElasticsuiteInstantSearch\\": "src/module-elasticsuite-instant-search",
"Smile\\ElasticsuiteMerchandisingGauge\\": "src/module-elasticsuite-merchandising-gauge",
"Smile\\ElasticsuiteRecommender\\": "src/module-elasticsuite-recommender",
"Smile\\ElasticsuiteRecommenderGraphQl\\": "src/module-elasticsuite-recommender-graph-ql",
"Smile\\ElasticsuiteVirtualAttribute\\": "src/module-elasticsuite-virtual-attribute"
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php
/**
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade Smile ElasticSuite to newer
* versions in the future.
*
*
* @category Smile
* @package Smile\ElasticsuiteRecommenderGraphQl
* @author Romain Ruaud <romain.ruaud@smile.fr>
* @copyright 2021 Smile
* @license Licensed to Smile-SA. All rights reserved. No warranty, explicit or implicit, provided.
* Unauthorized copying of this file, via any medium, is strictly prohibited.
*/

namespace Smile\ElasticsuiteRecommenderGraphQl\Model\Resolver\Batch;

use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\Resolver\BatchResponse;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider;

/**
* Abstract Resolver for linked products
*
* @category Smile
* @package Smile\ElasticsuiteRecommenderGraphQl
* @author Romain Ruaud <romain.ruaud@smile.fr>
*/
abstract class AbstractLinkedProducts
{
/**
* @var \Smile\ElasticsuiteRecommender\Helper\Data
*/
private $helper;

/**
* @var \Smile\ElasticsuiteRecommender\Model\Product\Matcher
*/
private $model;

/**
* @var \Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector
*/
private $productFieldsSelector;

/**
* @var SearchCriteriaBuilder
*/
private $searchCriteriaBuilder;

/**
* @var ProductDataProvider
*/
private $productDataProvider;

/**
* RelatedProducts constructor.
*
* @param \Smile\ElasticsuiteRecommender\Model\Product\Matcher $model Recommender model
* @param \Smile\ElasticsuiteRecommender\Helper\Data $helper Helper
* @param \Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector $productFieldsSelector Field Selector
* @param SearchCriteriaBuilder $searchCriteriaBuilder Search Criteria Builder
* @param ProductDataProvider $productDataProvider Product Data Provider
*/
public function __construct(
\Smile\ElasticsuiteRecommender\Model\Product\Matcher $model,
\Smile\ElasticsuiteRecommender\Helper\Data $helper,
ProductFieldsSelector $productFieldsSelector,
SearchCriteriaBuilder $searchCriteriaBuilder,
ProductDataProvider $productDataProvider
) {
$this->helper = $helper;
$this->model = $model;
$this->productFieldsSelector = $productFieldsSelector;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->productDataProvider = $productDataProvider;
}

/**
* {@inheritDoc}
*/
public function resolve(ContextInterface $context, Field $field, array $requests): BatchResponse
{
/** @var \Magento\Catalog\Api\Data\ProductInterface[] $products */
$products = [];
$fields = [];
/** @var \Magento\Framework\GraphQl\Query\Resolver\BatchRequestItemInterface $request */
foreach ($requests as $request) {
if (empty($request->getValue()['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}
$products[] = $request->getValue()['model'];
$fields[] = $this->productFieldsSelector->getProductFieldsFromInfo($request->getInfo(), $this->getNode());
}

$fields = array_unique(array_merge(...$fields));
$related = $this->findRelations($products, $fields);
$response = new BatchResponse();

/** @var \Magento\Framework\GraphQl\Query\Resolver\BatchRequestItemInterface $request */
foreach ($requests as $request) {
/** @var \Magento\Catalog\Api\Data\ProductInterface $product */
$product = $request->getValue()['model'];
$result = [];
if (array_key_exists($product->getId(), $related)) {
$result = array_map(
function ($relatedProduct) {
$data = $relatedProduct->getData();
$data['model'] = $relatedProduct;

return $data;
},
$related[$product->getId()]
);
}
$response->addResponse($request, $result);
}

return $response;
}

/**
* Find related products.
*
* @param \Magento\Catalog\Api\Data\ProductInterface[] $products The products
* @param string[] $loadAttributes The attributes to load
*
* @return \Magento\Catalog\Api\Data\ProductInterface[][] Products
*/
protected function findRelations(array $products, array $loadAttributes): array
{
$relations = [];

foreach ($products as $product) {
$items = $this->model->getItems($product, $this->getBehavior(), $this->getPositionLimit());
foreach ($items as $item) {
$relations[$product->getId()][] = $item->getId();
}
}

if (!$relations) {
return [];
}

$relatedIds = array_values($relations);
$relatedIds = array_unique(array_merge(...$relatedIds));

$this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in');
$relatedSearchResult = $this->productDataProvider->getList(
$this->searchCriteriaBuilder->create(),
$loadAttributes,
false,
true
);

/** @var \Magento\Catalog\Api\Data\ProductInterface[] $relatedProducts */
$relatedProducts = [];
/** @var \Magento\Catalog\Api\Data\ProductInterface $item */
foreach ($relatedSearchResult->getItems() as $item) {
$relatedProducts[$item->getId()] = $item;
}

$relationsData = [];
foreach ($relations as $productId => $relatedIds) {
$relationsData[$productId] = array_map(
function ($pid) use ($relatedProducts) {
return $relatedProducts[$pid];
},
$relatedIds
);
}

return $relationsData;
}

/**
* Type of linked products to be resolved.
*
* @return int
*/
abstract protected function getType(): string;

/**
* Number of recommendations to load.
*
* @return number
*/
private function getPositionLimit()
{
return $this->helper->getPositionLimit($this->getType());
}

/**
* Recommendations loading behavior.
*
* @return int
*/
private function getBehavior()
{
return $this->helper->getBehavior($this->getType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
/**
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade Smile ElasticSuite to newer
* versions in the future.
*
*
* @category Smile
* @package Smile\ElasticsuiteRecommenderGraphQl
* @author Romain Ruaud <romain.ruaud@smile.fr>
* @copyright 2021 Smile
* @license Licensed to Smile-SA. All rights reserved. No warranty, explicit or implicit, provided.
* Unauthorized copying of this file, via any medium, is strictly prohibited.
*/

namespace Smile\ElasticsuiteRecommenderGraphQl\Model\Resolver\Batch;

use Magento\Catalog\Model\Product\Link;
use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\GraphQl\Query\Resolver\BatchResolverInterface;

/**
* Resolver for CrossSell Products
*
* @category Smile
* @package Smile\ElasticsuiteRecommenderGraphQl
* @author Romain Ruaud <romain.ruaud@smile.fr>
*/
class CrossSellProducts extends AbstractLinkedProducts implements BatchResolverInterface
{
/**
* Cross Sell Products constructor.
*
* @param \Smile\ElasticsuiteRecommender\Model\Product\Matcher $model Recommender model
* @param \Smile\ElasticsuiteRecommender\Helper\Data $helper Helper
* @param \Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector $productFieldsSelector Field Selector
* @param SearchCriteriaBuilder $searchCriteriaBuilder Search Criteria Builder
* @param ProductDataProvider $productDataProvider Product Data Provider
*/
public function __construct(
\Smile\ElasticsuiteRecommender\Model\Product\Matcher $model,
\Smile\ElasticsuiteRecommender\Helper\Data $helper,
ProductFieldsSelector $productFieldsSelector,
SearchCriteriaBuilder $searchCriteriaBuilder,
ProductDataProvider $productDataProvider
) {
parent::__construct($model, $helper, $productFieldsSelector, $searchCriteriaBuilder, $productDataProvider);
}

/**
* Get type
*
* @return string
*/
public function getType(): string
{
return 'crosssell';
}

/**
* {@inheritDoc}
*/
protected function getNode(): string
{
return 'crosssell_products';
}
}
Loading

0 comments on commit 546bede

Please # to comment.