diff --git a/.github/workflows/02-phpstan.yml b/.github/workflows/02-phpstan.yml new file mode 100644 index 000000000..3d6175ccd --- /dev/null +++ b/.github/workflows/02-phpstan.yml @@ -0,0 +1,198 @@ +name: PHPStan Code Quality + +on: + push: + branches: + - 2.10.x + + pull_request_target: + branches: + - 2.10.x + types: [labeled,synchronize] + +jobs: + build: + if: (github.event_name != 'pull_request') || contains(github.event.pull_request.labels.*.name, 'safe to test') + runs-on: ubuntu-20.04 + name: PHPStan Quality + + strategy: + fail-fast: false + matrix: + php-versions: ['7.4'] + magento-versions: ['2.4.2', '2.4.2-p1', '2.4.2-p2', '2.4.3', '2.4.3-p1'] + magento-editions: ['enterprise'] + experimental: [false] + include: + - php-versions: '8.1' + magento-versions: '2.4.4' + magento-editions: 'enterprise' + experimental: false + - php-versions: '8.1' + magento-versions: '2.4.5' + magento-editions: 'enterprise' + experimental: true + - php-versions: '8.1' + magento-versions: '2.4.5-p1' + magento-editions: 'enterprise' + experimental: true + + continue-on-error: ${{ matrix.experimental }} + + env: + magento-directory: /var/www/magento + MAGENTO_USERNAME: ${{ secrets.MAGENTO_USERNAME }} + MAGENTO_PASSWORD: ${{ secrets.MAGENTO_PASSWORD }} + + steps: + - name: "[Init] Checkout" + uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: "[Init] Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: hash, iconv, mbstring, intl, bcmath, ctype, gd, pdo, mysql, curl, zip, dom, sockets, soap, openssl, simplexml, xsl + ini-values: post_max_size=256M, max_execution_time=180 + + - name: "[Init] Setup Magento Directory" + env: + MAGENTO_ROOT: ${{ env.magento-directory }} + version: ${{ matrix.php-versions }} + run: | + sudo usermod -a -G www-data $USER + sudo mkdir -p $MAGENTO_ROOT + sudo chown -R $USER:www-data $MAGENTO_ROOT + + - name: "[Init] Downgrade Composer" + env: + MAGENTO_VERSION: ${{ matrix.magento-versions }} + run: | + function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } + if [ $(version $MAGENTO_VERSION) -lt $(version "2.4.2") ]; then + composer self-update --1 + else + composer self-update 2.1.14 + fi + + - name: "[Init] Optimize Composer" + env: + MAGENTO_VERSION: ${{ matrix.magento-versions }} + run: | + function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } + if [ $(version $MAGENTO_VERSION) -lt $(version "2.4.2") ]; then + composer global require hirak/prestissimo:0.3.10 + fi + + - name: "[Init] Determine composer cache directory" + id: composer-cache-directory + run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" + + - name: "[Init] Cache Composer cache" + id: composer-cache + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache-directory.outputs.directory }} + key: composer-${{ matrix.php-versions }}-${{ matrix.magento-editions }}-${{ matrix.magento-versions }} + restore-keys: | + composer-${{ matrix.php-versions }}-${{ matrix.magento-editions }}-${{ matrix.magento-versions }} + composer-${{ matrix.php-versions }}-${{ matrix.magento-editions }}- + composer-${{ matrix.php-versions }}- + + - name: "[Init] Cache Magento install" + id: magento-cache + uses: actions/cache@v2 + with: + path: ${{ env.magento-directory }} + key: magento-${{ matrix.php-versions }}-${{ matrix.magento-editions }}-${{ matrix.magento-versions }} + restore-keys: | + magento-${{ matrix.php-versions }}-${{ matrix.magento-editions }}-${{ matrix.magento-versions }} + + - name: "[Init] Prepare credentials" + if: ${{env.MAGENTO_USERNAME}} != 0 + run: composer config -g http-basic.repo.magento.com "$MAGENTO_USERNAME" "$MAGENTO_PASSWORD" + + - name: "[Init] Unconditionally add phpstan/phpstan" + working-directory: ${{ env.magento-directory }} + run: composer require --no-update --dev smile/magento2-smilelab-phpstan ^1.0 + + - name: "[Init] Prepare Magento install if needed" + if: steps.magento-cache.outputs.cache-hit == 'true' + working-directory: ${{ env.magento-directory }} + run: | + rm -rf app/etc/env.php app/etc/config.php + composer config discard-changes true + composer remove smile/elasticsuite --no-update --no-interaction + composer update --no-interaction --ignore-platform-reqs smile/elasticsuite + composer config discard-changes false + + - name: "[Init] Install proper version of Magento through Composer" + if: steps.magento-cache.outputs.cache-hit != 'true' + env: + MAGENTO_VERSION: ${{ matrix.magento-versions }} + MAGENTO_EDITION: ${{ matrix.magento-editions }} + MAGENTO_ROOT: ${{ env.magento-directory }} + EXPERIMENTAL: ${{ matrix.experimental }} + run: | + STABILITY="--stability=stable" + if [ $EXPERIMENTAL = true ]; then + STABILITY="" + fi + sudo rm -rf $MAGENTO_ROOT + sudo mkdir -p $MAGENTO_ROOT + sudo chown -R $USER:www-data $MAGENTO_ROOT + composer create-project --repository-url=https://repo.magento.com magento/project-$MAGENTO_EDITION-edition=$MAGENTO_VERSION $STABILITY $MAGENTO_ROOT --quiet + + - name: "[Init] Fix symfony/console and symfony string version" + working-directory: ${{ env.magento-directory }} + env: + MAGENTO_VERSION: ${{ matrix.magento-versions }} + run: | + function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } + if [ $(version $MAGENTO_VERSION) -lt $(version "2.4.4") ]; then + composer require symfony/console:4.4.26 --ignore-platform-reqs + composer require symfony/string:5.4.2 --ignore-platform-reqs + fi + + - name: "[Init] Add current build of Elasticsuite" + working-directory: ${{ env.magento-directory }} + run: | + composer require --dev "smile/elasticsuite:${GITHUB_BASE_REF:-${GITHUB_REF##*/}}-dev" --ignore-platform-reqs + rm -rf vendor/smile/elasticsuite/** + cp -Rf $GITHUB_WORKSPACE/* vendor/smile/elasticsuite/ + + - name: "[Init] Fix Magento directory permissions" + env: + MAGENTO_ROOT: ${{ env.magento-directory }} + working-directory: ${{ env.magento-directory }} + run: | + sudo find . -type f -exec chmod 644 {} \; + sudo find . -type d -exec chmod 755 {} \; + sudo find var pub/static pub/media app/etc generated/ -type f -exec chmod g+w {} \; + sudo find var pub/static pub/media app/etc generated/ -type d -exec chmod g+ws {} \; + sudo chown -R $USER:www-data . + sudo chmod u+x bin/magento + + - name: "[Init] Enabling modules" + working-directory: ${{ env.magento-directory }} + run: php bin/magento module:enable --all + + - name: "[Init] Compile" + working-directory: ${{ env.magento-directory }} + run: php bin/magento setup:di:compile + + - name: "[Test] PHPStan" + working-directory: ${{ env.magento-directory }} + run: | + sudo chmod u+x vendor/bin/phpstan + vendor/bin/phpstan analyze --level=0 vendor/smile/elasticsuite + + - name: "[End] Job failed, gathering logs" + env: + MAGENTO_ROOT: ${{ env.magento-directory }} + if: ${{ failure() }} + run: | + tail -n 100 $MAGENTO_ROOT/var/log/*.log diff --git a/.github/workflows/20-integration.yml b/.github/workflows/20-integration.yml index 44c85d453..a2b89b6a8 100644 --- a/.github/workflows/20-integration.yml +++ b/.github/workflows/20-integration.yml @@ -8,7 +8,7 @@ on: pull_request_target: branches: - 2.10.x - types: [labeled] + types: [labeled,synchronize] jobs: build: @@ -366,6 +366,11 @@ jobs: exit 2; fi; + - name: "[Test] Rest : Schema" + run: | + echo "==> Testing Rest Schema..." + curl -i -X GET http://localhost/rest/all/schema?services=all + - name: "[Test] GraphQl : Schema" run: | echo "==> Testing GraphQL Schema..." diff --git a/Resources/tests/graphql/search/filter/query.gql b/Resources/tests/graphql/search/filter/query.gql index c42ec4ad5..2c63df72b 100644 --- a/Resources/tests/graphql/search/filter/query.gql +++ b/Resources/tests/graphql/search/filter/query.gql @@ -22,6 +22,13 @@ query productSearch($inputText: String!, $categoryId: String) { __typename } total_count + page_info { + current_page + page_size + total_pages + is_spellchecked + __typename + } filters { name filter_items_count diff --git a/Resources/tests/graphql/search/query.gql b/Resources/tests/graphql/search/query.gql index 3b6659c2c..509f64d12 100644 --- a/Resources/tests/graphql/search/query.gql +++ b/Resources/tests/graphql/search/query.gql @@ -22,6 +22,13 @@ query productSearch($inputText: String!, $categoryId: String) { __typename } total_count + page_info { + current_page + page_size + total_pages + is_spellchecked + __typename + } filters { name filter_items_count diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..249767b72 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + level: 6 + checkMissingIterableValueType: false + paths: + - src + +includes: + - vendor/smile/magento2-smilelab-phpstan/extension.neon diff --git a/src/module-elasticsuite-catalog-graph-ql/DataProvider/Product/LayeredNavigation/Builder/CategoryUid.php b/src/module-elasticsuite-catalog-graph-ql/DataProvider/Product/LayeredNavigation/Builder/CategoryUid.php new file mode 100644 index 000000000..a2f5eb851 --- /dev/null +++ b/src/module-elasticsuite-catalog-graph-ql/DataProvider/Product/LayeredNavigation/Builder/CategoryUid.php @@ -0,0 +1,236 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder; + +use Magento\Catalog\Model\Product\Attribute\Repository as AttributeRepository; +use Magento\CatalogGraphQl\DataProvider\Category\Query\CategoryAttributeQuery; +use Magento\CatalogGraphQl\DataProvider\CategoryAttributesMapper; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\RootCategoryProvider; +use Magento\Framework\Api\Search\AggregationInterface; +use Magento\Framework\Api\Search\AggregationValueInterface; +use Magento\Framework\Api\Search\BucketInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Layered Navigation Builder for Category items. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @category Smile + * @package Smile\ElasticsuiteCatalogGraphQl + * @author Vadym Honcharuk + */ +class CategoryUid implements LayerBuilderInterface +{ + /** + * @var string + */ + const CATEGORY_BUCKET = 'categories'; + + /** + * @var array + */ + private static $bucketMap = [ + self::CATEGORY_BUCKET => [ + 'request_name' => 'category_uid', + 'label' => 'Category', + ], + ]; + + /** + * @var CategoryAttributeQuery + */ + private $categoryAttributeQuery; + + /** + * @var CategoryAttributesMapper + */ + private $attributesMapper; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var RootCategoryProvider + */ + private $rootCategoryProvider; + + /** + * @var LayerFormatter + */ + private $layerFormatter; + + /** + * @var AttributeRepository + */ + private $attributeRepository; + + /** + * @var Uid + */ + private $uidEncoder; + + /** + * @var string + */ + private $attributeCode; + + /** + * @param CategoryAttributeQuery $categoryAttributeQuery Category Attribute Query + * @param CategoryAttributesMapper $categoryAttributesMapper Category Attributes Mapper + * @param RootCategoryProvider $rootCategoryProvider Root Category Provider + * @param LayerFormatter $layerFormatter Layer Formatter + * @param ResourceConnection $resourceConnection Resource Connection + * @param AttributeRepository $attributeRepository Product attribute repository + * @param Uid $uidEncoder Encoder Uid + * @param string $attributeCode Product attribute code used to load the localized frontend label + */ + public function __construct( + CategoryAttributeQuery $categoryAttributeQuery, + CategoryAttributesMapper $categoryAttributesMapper, + RootCategoryProvider $rootCategoryProvider, + LayerFormatter $layerFormatter, + ResourceConnection $resourceConnection, + AttributeRepository $attributeRepository, + Uid $uidEncoder, + string $attributeCode = 'category_ids' + ) { + $this->categoryAttributeQuery = $categoryAttributeQuery; + $this->attributesMapper = $categoryAttributesMapper; + $this->rootCategoryProvider = $rootCategoryProvider; + $this->layerFormatter = $layerFormatter; + $this->resourceConnection = $resourceConnection; + $this->attributeRepository = $attributeRepository; + $this->uidEncoder = $uidEncoder; + $this->attributeCode = $attributeCode; + } + + /** + * {@inheritdoc} + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Select_Exception + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $bucket = $aggregation->getBucket(self::CATEGORY_BUCKET); + + if ($this->isBucketEmpty($bucket)) { + return []; + } + + $categoryIds = \array_map( + function (AggregationValueInterface $value) { + return (int) $value->getValue(); + }, + $bucket->getValues() + ); + + $categoryIds = \array_diff($categoryIds, [$this->rootCategoryProvider->getRootCategory($storeId)]); + $categoryLabels = \array_column( + $this->attributesMapper->getAttributesValues( + $this->resourceConnection->getConnection()->fetchAll( + $this->categoryAttributeQuery->getQuery($categoryIds, ['name'], $storeId) + ) + ), + 'name', + 'entity_id' + ); + + if (!$categoryLabels) { + return []; + } + + $label = __(self::$bucketMap[self::CATEGORY_BUCKET]['label']); + if ($frontendLabel = $this->getFrontendLabel($storeId)) { + $label = $frontendLabel; + } + $result = $this->layerFormatter->buildLayer( + $label, + \count($categoryIds), + self::$bucketMap[self::CATEGORY_BUCKET]['request_name'] + ); + + foreach ($bucket->getValues() as $value) { + $categoryId = $value->getValue(); + if (!\in_array($categoryId, $categoryIds, true)) { + continue; + } + + $optionValue = $categoryId; + if (!empty($result['attribute_code']) && $result['attribute_code'] === 'category_uid') { + $optionValue = $this->uidEncoder->encode((string) $categoryId); + } + + $result['options'][] = $this->layerFormatter->buildItem( + $categoryLabels[$categoryId] ?? $categoryId, + $optionValue, + $value->getMetrics()['count'] + ); + } + + $result['has_more'] = false; + + $attribute = $this->attributeRepository->get($this->attributeCode); + $result['frontend_input'] = $attribute->getFrontendInput(); + + return ['category_uid' => $result]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket Bucket + * + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } + + /** + * Return the frontend label of the configured attribute for the given store, if available. + * + * @param int|null $storeId Store ID. + * + * @return string|null + */ + private function getFrontendLabel(?int $storeId): ?string + { + $label = null; + + try { + $attribute = $this->attributeRepository->get($this->attributeCode); + $label = $attribute->getDefaultFrontendLabel(); + $frontendLabels = array_filter( + $attribute->getFrontendLabels(), + function ($frontendLabel) use ($storeId) { + return $frontendLabel->getStoreId() == $storeId; + } + ); + if (!empty($frontendLabels)) { + $label = reset($frontendLabels)->getLabel(); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + ; + } + + return $label; + } +} diff --git a/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Aggregations.php b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Aggregations.php index 81977773d..8993c3b30 100644 --- a/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Aggregations.php +++ b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Aggregations.php @@ -126,7 +126,7 @@ private function sortFilters($layerType, $filters) $categoryFilters = $this->filtersProvider->getFilters($layerType); /** @var \Magento\Catalog\Model\Layer\Filter\AbstractFilter $filter */ - $order = ['category_id']; // The category filter is always displayed first in legacy frontend. + $order = ['category_id', 'category_uid']; // The category filter is always displayed first in legacy frontend. foreach ($categoryFilters as $filter) { if (!$filter->hasAttributeModel()) { continue; diff --git a/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products.php b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products.php index db6508381..ea6dfe10c 100644 --- a/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products.php +++ b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products.php @@ -76,6 +76,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value 'page_size' => $searchResult->getPageSize(), 'current_page' => $searchResult->getCurrentPage(), 'total_pages' => $searchResult->getTotalPages(), + 'is_spellchecked' => $searchResult->isSpellchecked(), ], 'search_result' => $searchResult, 'layer_type' => $layerType, diff --git a/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products/Query/Search.php b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products/Query/Search.php index bad49ff99..55a00ede3 100644 --- a/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products/Query/Search.php +++ b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products/Query/Search.php @@ -113,6 +113,7 @@ public function getResult(array $args, ResolveInfo $info, ContextInterface $cont 'pageSize' => $searchCriteria->getPageSize(), 'currentPage' => $searchCriteria->getCurrentPage(), 'totalPages' => $maxPages, + 'isSpellchecked' => $searchResults->__toArray()['is_spellchecked'] ?? false, ]); } diff --git a/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products/SearchResult.php b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products/SearchResult.php new file mode 100644 index 000000000..607919936 --- /dev/null +++ b/src/module-elasticsuite-catalog-graph-ql/Model/Resolver/Products/SearchResult.php @@ -0,0 +1,47 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCatalogGraphQl\Model\Resolver\Products; + +/** + * Elasticsuite search result for GraphQL. + * Overridden to add the fact that the query was spellchecked or not. + * + * @category Smile + * @package Smile\ElasticsuiteCatalogGraphQl + * @author Romain Ruaud + */ +class SearchResult extends \Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult +{ + /** + * @var array + */ + private $data; + + /** + * @param array $data Object Data + */ + public function __construct(array $data) + { + $this->data = $data; + parent::__construct($data); + } + + /** + * @return bool + */ + public function isSpellchecked() + { + return (bool) $this->data['isSpellchecked'] ?? false; + } +} diff --git a/src/module-elasticsuite-catalog-graph-ql/etc/graphql/di.xml b/src/module-elasticsuite-catalog-graph-ql/etc/graphql/di.xml index f527867e3..c93305985 100644 --- a/src/module-elasticsuite-catalog-graph-ql/etc/graphql/di.xml +++ b/src/module-elasticsuite-catalog-graph-ql/etc/graphql/di.xml @@ -18,6 +18,7 @@ type="Smile\ElasticsuiteCatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder"/> + @@ -39,6 +40,13 @@ + + + + Smile\ElasticsuiteCatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\CategoryUid + + + getPreviewObject()->getData(); $json = $this->jsonHelper->jsonEncode($responseData); - $this->getResponse()->representJson($json); + return $this->getResponse()->representJson($json); } /** diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Config/Source/BoostMode.php b/src/module-elasticsuite-catalog-optimizer/Model/Config/Source/BoostMode.php new file mode 100644 index 000000000..4f64f1b4c --- /dev/null +++ b/src/module-elasticsuite-catalog-optimizer/Model/Config/Source/BoostMode.php @@ -0,0 +1,62 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalogOptimizer\Model\Config\Source; + +use Magento\Framework\Option\ArrayInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\FunctionScore; + +/** + * Boost Mode value config source model. + * + * @category Smile + * @package Smile\ElasticsuiteCatalogOptimizer + * @author Vadym Honcharuk + */ +class BoostMode implements ArrayInterface +{ + /** + * Returns option array + * + * @return array + */ + public function toOptionArray() + { + return [ + [ + 'value' => FunctionScore::BOOST_MODE_MULTIPLY, + 'label' => __('Multiply'), + ], + [ + 'value' => FunctionScore::BOOST_MODE_REPLACE, + 'label' => __('Replace'), + ], + [ + 'value' => FunctionScore::BOOST_MODE_SUM, + 'label' => __('Sum'), + ], + [ + 'value' => FunctionScore::BOOST_MODE_AVG, + 'label' => __('Average'), + ], + [ + 'value' => FunctionScore::BOOST_MODE_MAX, + 'label' => __('Max'), + ], + [ + 'value' => FunctionScore::BOOST_MODE_MIN, + 'label' => __('Min'), + ], + ]; + } +} diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Config/Source/ScoreMode.php b/src/module-elasticsuite-catalog-optimizer/Model/Config/Source/ScoreMode.php new file mode 100644 index 000000000..82ead294e --- /dev/null +++ b/src/module-elasticsuite-catalog-optimizer/Model/Config/Source/ScoreMode.php @@ -0,0 +1,62 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalogOptimizer\Model\Config\Source; + +use Magento\Framework\Option\ArrayInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\FunctionScore; + +/** + * Score Mode value config source model. + * + * @category Smile + * @package Smile\ElasticsuiteCatalogOptimizer + * @author Vadym Honcharuk + */ +class ScoreMode implements ArrayInterface +{ + /** + * Returns option array + * + * @return array + */ + public function toOptionArray() + { + return [ + [ + 'value' => FunctionScore::SCORE_MODE_MULTIPLY, + 'label' => __('Multiply'), + ], + [ + 'value' => FunctionScore::SCORE_MODE_SUM, + 'label' => __('Sum'), + ], + [ + 'value' => FunctionScore::SCORE_MODE_AVG, + 'label' => __('Average'), + ], + [ + 'value' => FunctionScore::SCORE_MODE_FIRST, + 'label' => __('First'), + ], + [ + 'value' => FunctionScore::SCORE_MODE_MAX, + 'label' => __('Max'), + ], + [ + 'value' => FunctionScore::SCORE_MODE_MIN, + 'label' => __('Min'), + ], + ]; + } +} diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php index 95b7117bc..e1fd76429 100644 --- a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php @@ -96,7 +96,7 @@ public function beforeSave() { $this->parseDateFields(); - parent::beforeSave(); + return parent::beforeSave(); } /** diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/ApplierList.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/ApplierList.php index 0f235bfe1..f44420f29 100644 --- a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/ApplierList.php +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/ApplierList.php @@ -13,6 +13,7 @@ */ namespace Smile\ElasticsuiteCatalogOptimizer\Model\Optimizer; +use Magento\Framework\App\Config\ScopeConfigInterface; use Smile\ElasticsuiteCatalogOptimizer\Api\Data\OptimizerInterface; use Smile\ElasticsuiteCatalogOptimizer\Model\ResourceModel\Optimizer\Collection; use Smile\ElasticsuiteCore\Api\Search\Request\ContainerConfigurationInterface; @@ -40,6 +41,11 @@ class ApplierList */ private $functionsProvider; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @var OptimizerFilterInterface[] */ @@ -50,15 +56,18 @@ class ApplierList * * @param \Smile\ElasticsuiteCore\Search\Request\Query\QueryFactory $queryFactory Query factory. * @param \Smile\ElasticsuiteCatalogOptimizer\Model\Optimizer\Functions\ProviderInterface $functionsProvider Functions provider. + * @param ScopeConfigInterface $scopeConfig Scope configuration. * @param OptimizerFilterInterface[] $filters Optimizer filters. */ public function __construct( \Smile\ElasticsuiteCore\Search\Request\Query\QueryFactory $queryFactory, \Smile\ElasticsuiteCatalogOptimizer\Model\Optimizer\Functions\ProviderInterface $functionsProvider, + ScopeConfigInterface $scopeConfig, array $filters = [] ) { $this->queryFactory = $queryFactory; $this->functionsProvider = $functionsProvider; + $this->scopeConfig = $scopeConfig; $this->filters = $filters; } @@ -92,12 +101,15 @@ public function applyOptimizers(ContainerConfigurationInterface $containerConfig */ private function applyFunctions(QueryInterface $query, $functions = []) { + $scoreModeConfig = $this->scopeConfig->getValue('smile_elasticsuite_optimizers/score_mode_configuration/score_mode'); + $boostModeConfig = $this->scopeConfig->getValue('smile_elasticsuite_optimizers/boost_mode_configuration/boost_mode'); + if (!empty($functions)) { $queryParams = [ 'query' => $query, 'functions' => $functions, - 'scoreMode' => FunctionScore::SCORE_MODE_MULTIPLY, - 'boostMode' => FunctionScore::BOOST_MODE_MULTIPLY, + 'scoreMode' => $scoreModeConfig, + 'boostMode' => $boostModeConfig, ]; $query = $this->queryFactory->create(QueryInterface::TYPE_FUNCTIONSCORE, $queryParams); } diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Functions/Provider/DefaultProvider.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Functions/Provider/DefaultProvider.php index e137fbb3f..3a094fc9d 100644 --- a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Functions/Provider/DefaultProvider.php +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Functions/Provider/DefaultProvider.php @@ -64,6 +64,11 @@ class DefaultProvider implements ProviderInterface */ private $cacheLifeTime; + /** + * @var array + */ + private $functions; + /** * Provider constructor. * diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview.php index 72dfbe446..c70d0a9cb 100644 --- a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview.php +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview.php @@ -44,6 +44,11 @@ class Preview */ private $optimizer; + /** + * @var ApplierListFactory + */ + private $applierListFactory; + /** * @var ContainerConfigurationInterface */ diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/ResultsBuilder.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/ResultsBuilder.php index f076c1a5d..40825895b 100644 --- a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/ResultsBuilder.php +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/ResultsBuilder.php @@ -26,6 +26,16 @@ */ class ResultsBuilder { + /** + * @var \Magento\Framework\Search\SearchEngineInterface + */ + private $searchEngine; + + /** + * @var RequestBuilder + */ + private $requestBuilder; + /** * ResultsBuilder constructor. * diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/SearchQuery.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/SearchQuery.php index b05f556f8..e8d63dbef 100644 --- a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/SearchQuery.php +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Preview/SearchQuery.php @@ -125,6 +125,9 @@ private function getSpellingType(ContainerConfigurationInterface $containerConfi 'index' => $containerConfig->getIndexName(), 'queryText' => $queryText, 'cutoffFrequency' => $containerConfig->getRelevanceConfig()->getCutOffFrequency(), + 'isUsingAllTokens' => $containerConfig->getRelevanceConfig()->isUsingAllTokens(), + 'isUsingReference' => $containerConfig->getRelevanceConfig()->isUsingReferenceAnalyzer(), + 'isUsingEdgeNgram' => $containerConfig->getRelevanceConfig()->isUsingEdgeNgramAnalyzer(), ]; $spellcheckRequest = $this->spellcheckRequestFactory->create($spellcheckRequestParams); diff --git a/src/module-elasticsuite-catalog-optimizer/Setup/OptimizerSetup.php b/src/module-elasticsuite-catalog-optimizer/Setup/OptimizerSetup.php index 326934ca6..c72bf7699 100644 --- a/src/module-elasticsuite-catalog-optimizer/Setup/OptimizerSetup.php +++ b/src/module-elasticsuite-catalog-optimizer/Setup/OptimizerSetup.php @@ -250,7 +250,7 @@ public function createOptimizerLimitationTable(SchemaSetupInterface $setup) $setup->getFkName( OptimizerInterface::TABLE_NAME_LIMITATION, 'category_id', - 'catalog_category_entity', + $setup->getTable('catalog_category_entity'), $categoryIdField ), 'category_id', diff --git a/src/module-elasticsuite-catalog-optimizer/Test/Unit/Model/Optimizer/Preview/SearchQueryTest.php b/src/module-elasticsuite-catalog-optimizer/Test/Unit/Model/Optimizer/Preview/SearchQueryTest.php new file mode 100644 index 000000000..ba8c72115 --- /dev/null +++ b/src/module-elasticsuite-catalog-optimizer/Test/Unit/Model/Optimizer/Preview/SearchQueryTest.php @@ -0,0 +1,170 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCatalogOptimizer\Test\Unit\Model\Optimizer\Preview; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Smile\ElasticsuiteCatalogOptimizer\Model\Optimizer\Preview\SearchQuery; +use Smile\ElasticsuiteCore\Api\Search\Request\ContainerConfigurationInterface; +use Smile\ElasticsuiteCore\Api\Search\Request\Container\RelevanceConfigurationInterface; +use Smile\ElasticsuiteCore\Api\Search\Spellchecker\RequestInterfaceFactory as SpellcheckRequestFactory; +use Smile\ElasticsuiteCore\Api\Search\SpellcheckerInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\Builder as QueryBuilder; +use Smile\ElasticsuiteCore\Search\Request\Query\QueryFactory; +use Smile\ElasticsuiteCore\Search\Spellchecker\Request as SpellcheckerRequest; + +/** + * Optimiser Preview Search Query unit testing. + * + * @category Smile + * @package Smile\ElasticsuiteCatalogOptimizer + * @author Richard BAYET + */ +class SearchQueryTest extends TestCase +{ + /** + * Tests the correct creation of a SpellcheckerInterface with regards to parameters + * (introduction/removal of experimental relevance settings) + * @covers \Smile\ElasticsuiteCatalogOptimizer\Model\Optimizer\Preview\SearchQuery::getSpellingType + * + * @return void + */ + public function testSpellcheckRequestConstructor(): void + { + $searchQueryPreview = new SearchQuery( + $this->getQueryBuilderMock(), + $this->getQueryFactoryMock(), + $this->getSpellcheckerRequestFactoryMock(), + $this->getSpellcheckerInterfaceMock() + ); + + $fulltextQuery = $searchQueryPreview->getFullTextQuery( + $this->getContainerConfigurationInterfaceMock(), + 'test' + ); + $this->assertEquals([], $fulltextQuery); + + $fulltextQuery = $searchQueryPreview->getFullTextQuery( + $this->getContainerConfigurationInterfaceMock(), + ['test1', 'test2'] + ); + $this->assertEquals([], $fulltextQuery); + } + + /** + * Get Query Builder mock object + * + * @return MockObject + */ + private function getQueryBuilderMock(): MockObject + { + $queryBuilder = $this->getMockBuilder(QueryBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $queryBuilder->method('createFulltextQuery')->willReturn([]); + $queryBuilder->method('createFilterQuery')->willReturn([]); + + return $queryBuilder; + } + + /** + * Get Query Factory mock object + * + * @return MockObject + */ + private function getQueryFactoryMock(): MockObject + { + $queryFactory = $this->getMockBuilder(QueryFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $queryFactory->method('create')->willReturn([]); + + return $queryFactory; + } + + /** + * Get Spellchecker Request Factory mock object + * + * @return MockObject + */ + private function getSpellcheckerRequestFactoryMock(): MockObject + { + $spellcheckRequestFactory = $this->getMockBuilder(SpellcheckRequestFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $spellcheckRequestFactory->method('create')->willReturnCallback(function ($args) { + return new SpellcheckerRequest(...array_values($args)); + }); + + return $spellcheckRequestFactory; + } + + /** + * Get Container Configuration mock object + * + * @return MockObject + */ + private function getContainerConfigurationInterfaceMock(): MockObject + { + $containerConfiguration = $this->getMockBuilder(ContainerConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $containerConfiguration->method('getIndexName')->willReturn('Dummy'); + $containerConfiguration->method('getRelevanceConfig')->willReturn($this->getRelevanceConfigurationInterfaceMock()); + $containerConfiguration->method('getFilters')->willReturn([]); + + return $containerConfiguration; + } + + /** + * Get Relevance Configuration mock object + * + * @return MockObject + */ + private function getRelevanceConfigurationInterfaceMock(): MockObject + { + $relevanceConfiguration = $this->getMockBuilder(RelevanceConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $relevanceConfiguration->method('getCutOffFrequency')->willReturn(0.15); + $relevanceConfiguration->method('isUsingAllTokens')->willReturn(false); + $relevanceConfiguration->method('isUsingReferenceAnalyzer')->willReturn(false); + $relevanceConfiguration->method('isUsingEdgeNgramAnalyzer')->willReturn(false); + + return $relevanceConfiguration; + } + + /** + * Get Spellchecker mock object + * + * @return MockObject + */ + private function getSpellcheckerInterfaceMock(): MockObject + { + $spellChecker = $this->getMockBuilder(SpellcheckerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $spellChecker->method('getSpellingType')->willReturn(SpellcheckerInterface::SPELLING_TYPE_EXACT); + + return $spellChecker; + } +} diff --git a/src/module-elasticsuite-catalog-optimizer/Test/Unit/Model/Optimizer/PreviewTest.php b/src/module-elasticsuite-catalog-optimizer/Test/Unit/Model/Optimizer/PreviewTest.php index 1f2759e4f..7dc01c434 100644 --- a/src/module-elasticsuite-catalog-optimizer/Test/Unit/Model/Optimizer/PreviewTest.php +++ b/src/module-elasticsuite-catalog-optimizer/Test/Unit/Model/Optimizer/PreviewTest.php @@ -11,7 +11,7 @@ * @copyright 2020 Smile * @license Open Software License ("OSL") v. 3.0 */ -namespace Smile\ElasticsuiteCore\Test\Unit\Model; +namespace Smile\ElasticsuiteCatalogOptimizer\Test\Unit\Model\Optimizer; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/src/module-elasticsuite-catalog-optimizer/Ui/Component/Optimizer/Listing/DataProvider.php b/src/module-elasticsuite-catalog-optimizer/Ui/Component/Optimizer/Listing/DataProvider.php index 70d593bfd..7bca5673c 100644 --- a/src/module-elasticsuite-catalog-optimizer/Ui/Component/Optimizer/Listing/DataProvider.php +++ b/src/module-elasticsuite-catalog-optimizer/Ui/Component/Optimizer/Listing/DataProvider.php @@ -94,6 +94,8 @@ public function addField($field, $alias = null) /** * {@inheritdoc} + * + * @return void */ public function addFilter(\Magento\Framework\Api\Filter $filter) { diff --git a/src/module-elasticsuite-catalog-optimizer/etc/acl.xml b/src/module-elasticsuite-catalog-optimizer/etc/acl.xml index 8b366d709..6f0879151 100644 --- a/src/module-elasticsuite-catalog-optimizer/etc/acl.xml +++ b/src/module-elasticsuite-catalog-optimizer/etc/acl.xml @@ -20,7 +20,14 @@ - + + + + + + + + diff --git a/src/module-elasticsuite-catalog-optimizer/etc/adminhtml/system.xml b/src/module-elasticsuite-catalog-optimizer/etc/adminhtml/system.xml new file mode 100644 index 000000000..000e7950a --- /dev/null +++ b/src/module-elasticsuite-catalog-optimizer/etc/adminhtml/system.xml @@ -0,0 +1,43 @@ + + + +
+ + smile_elasticsuite + Magento_Backend::smile_elasticsuite_optimizers + + + + + Smile\ElasticsuiteCatalogOptimizer\Model\Config\Source\ScoreMode +
The parameter score_mode specifies how the computed scores are combined. + Learn more about Function score query configuration.]]>
+
+
+ + + + + Smile\ElasticsuiteCatalogOptimizer\Model\Config\Source\BoostMode +
The parameter boost_mode specifies how the boosted scores of each product will be added to their base score. + Learn more about Function score query configuration.]]>
+
+
+
+
+
diff --git a/src/module-elasticsuite-catalog-optimizer/etc/config.xml b/src/module-elasticsuite-catalog-optimizer/etc/config.xml new file mode 100644 index 000000000..15a7fa496 --- /dev/null +++ b/src/module-elasticsuite-catalog-optimizer/etc/config.xml @@ -0,0 +1,29 @@ + + + + + + + multiply + + + multiply + + + + diff --git a/src/module-elasticsuite-catalog-optimizer/view/adminhtml/web/css/source/_module.less b/src/module-elasticsuite-catalog-optimizer/view/adminhtml/web/css/source/_module.less index 5ea7fc2fc..7c112e814 100644 --- a/src/module-elasticsuite-catalog-optimizer/view/adminhtml/web/css/source/_module.less +++ b/src/module-elasticsuite-catalog-optimizer/view/adminhtml/web/css/source/_module.less @@ -273,3 +273,14 @@ div.fieldset-wrapper[data-index="attribute_value"] { } } } + +code.literal { + font-family: Consolas,Menlo,DejaVu Sans Mono,Bitstream Vera Sans Mono,Lucida Console,monospace; + padding: 0 3px; + font-weight: 600; + font-size: .9em; + display: inline; + white-space: pre-wrap; + background: #e9e9e9; + color: #555; +} diff --git a/src/module-elasticsuite-catalog-rule/Controller/Adminhtml/Product/Rule/Conditions.php b/src/module-elasticsuite-catalog-rule/Controller/Adminhtml/Product/Rule/Conditions.php index c8e1cea51..0284e4190 100644 --- a/src/module-elasticsuite-catalog-rule/Controller/Adminhtml/Product/Rule/Conditions.php +++ b/src/module-elasticsuite-catalog-rule/Controller/Adminhtml/Product/Rule/Conditions.php @@ -82,7 +82,8 @@ public function execute() $model->setData('url_params', $this->getRequest()->getParams()); $result = $model->asHtmlRecursive(); } - $this->getResponse()->setBody($result); + + return $this->getResponse()->setBody($result); } /** diff --git a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product.php b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product.php index 99bad4acd..4ba5def3b 100644 --- a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product.php +++ b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product.php @@ -19,6 +19,7 @@ use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Eav\Model\Config; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Locale\FormatInterface; use Magento\Rule\Model\Condition\Context; use Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Product\AttributeList; @@ -27,6 +28,7 @@ /** * Product attribute search engine rule. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @category Smile * @package Smile\ElasticsuiteCatalogRule @@ -49,6 +51,17 @@ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct */ private $specialAttributesProvider; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var string + */ + const ATTRIBUTE_OPTIONS_ALPHABETICAL_SORT_XML_PATH + = 'smile_elasticsuite_catalogsearch_settings/catalogrule/force_sorting_select_options'; + /** * Constructor. * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -63,7 +76,8 @@ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct * @param ProductResource $productResource Product resource model. * @param Collection $attrSetCollection Attribute set collection. * @param FormatInterface $localeFormat Locale format. - * @param SpecialAttributesProvider $specialAttributesProvider Special Attributes Provider + * @param SpecialAttributesProvider $specialAttributesProvider Special Attributes Provider. + * @param ScopeConfigInterface $scopeConfig Scope configuration. * @param array $data Additional data. */ public function __construct( @@ -78,11 +92,13 @@ public function __construct( Collection $attrSetCollection, FormatInterface $localeFormat, SpecialAttributesProvider $specialAttributesProvider, + ScopeConfigInterface $scopeConfig, array $data = [] ) { $this->attributeList = $attributeList; $this->queryBuilder = $queryBuilder; $this->specialAttributesProvider = $specialAttributesProvider; + $this->scopeConfig = $scopeConfig; parent::__construct( $context, @@ -309,5 +325,22 @@ protected function _prepareValueOptions() } else { parent::_prepareValueOptions(); } + + if ($this->scopeConfig->isSetFlag(self::ATTRIBUTE_OPTIONS_ALPHABETICAL_SORT_XML_PATH)) { + // Sort by labels. + $selectReady = $this->getData('value_select_options'); + if ($selectReady) { + $labels = array_column($selectReady, 'label'); + array_multisort($labels, SORT_STRING | SORT_NATURAL, $selectReady); + $this->setData('value_select_options', $selectReady); + } + + $hashedReady = $this->getData('value_option'); + if ($hashedReady) { + asort($hashedReady, SORT_STRING | SORT_NATURAL); + } + } + + return $this; } } diff --git a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php index a05e563f7..8b33bf582 100644 --- a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php +++ b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php @@ -309,7 +309,9 @@ private function prepareFieldValue(ProductCondition $productCondition) $value = $productCondition->getValue(); if (is_array($value)) { - $value = array_filter($value); + // The call to array_values ensures the array keys are re-numbered correctly from 0. + // This prevent the Elasticsearch client to cast this array as an object in queries. + $value = array_values(array_filter($value, 'strlen')); } $productCondition->setValue($value); diff --git a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/SpecialAttribute/StockQty.php b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/SpecialAttribute/StockQty.php new file mode 100644 index 000000000..07d849306 --- /dev/null +++ b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/SpecialAttribute/StockQty.php @@ -0,0 +1,107 @@ + + * @copyright 2021 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Product\SpecialAttribute; + +use Smile\ElasticsuiteCatalogRule\Api\Rule\Condition\Product\SpecialAttributeInterface; +use Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Product as ProductCondition; + +/** + * Class StockQty + * + * @category Elasticsuite + * @package Elasticsuite\CatalogRule + * @author Richard Bayet + */ +class StockQty implements SpecialAttributeInterface +{ + /** + * {@inheritdoc} + */ + public function getAttributeCode() + { + return 'stock.qty'; + } + + /** + * {@inheritdoc} + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getSearchQuery(ProductCondition $condition) + { + // Query can be computed directly with the attribute code and value. (eg. stock.qty < 5). + return null; + } + + /** + * {@inheritdoc} + */ + public function getOperatorName() + { + return null; + } + + /** + * {@inheritdoc} + */ + public function getInputType() + { + return 'numeric'; + } + + /** + * {@inheritdoc} + */ + public function getValueElementType() + { + return 'text'; + } + + /** + * {@inheritdoc} + */ + public function getValueName($value) + { + if ($value === null || '' === $value) { + return '...'; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function getValue($rawValue) + { + return $rawValue; + } + + /** + * {@inheritdoc} + */ + public function getValueOptions() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function getLabel() + { + return __('Stock qty'); + } +} diff --git a/src/module-elasticsuite-catalog-rule/etc/adminhtml/system.xml b/src/module-elasticsuite-catalog-rule/etc/adminhtml/system.xml new file mode 100644 index 000000000..d5659cf96 --- /dev/null +++ b/src/module-elasticsuite-catalog-rule/etc/adminhtml/system.xml @@ -0,0 +1,31 @@ + + + + +
+ + + + + Magento\Config\Model\Config\Source\Yesno + + + +
+
+
diff --git a/src/module-elasticsuite-catalog-rule/etc/config.xml b/src/module-elasticsuite-catalog-rule/etc/config.xml new file mode 100644 index 000000000..bc0d3a9bb --- /dev/null +++ b/src/module-elasticsuite-catalog-rule/etc/config.xml @@ -0,0 +1,26 @@ + + + + + + + 0 + + + + diff --git a/src/module-elasticsuite-catalog-rule/etc/di.xml b/src/module-elasticsuite-catalog-rule/etc/di.xml index 4cc808cbd..5d99c3f11 100644 --- a/src/module-elasticsuite-catalog-rule/etc/di.xml +++ b/src/module-elasticsuite-catalog-rule/etc/di.xml @@ -30,6 +30,7 @@ isGiftCardProduct isGroupedProduct isSimpleProduct + Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Product\SpecialAttribute\StockQty
diff --git a/src/module-elasticsuite-catalog-rule/i18n/en_US.csv b/src/module-elasticsuite-catalog-rule/i18n/en_US.csv index a3630b58c..0b9e129d6 100644 --- a/src/module-elasticsuite-catalog-rule/i18n/en_US.csv +++ b/src/module-elasticsuite-catalog-rule/i18n/en_US.csv @@ -9,3 +9,7 @@ "Only giftcard products","Only giftcard products" "Only grouped products","Only grouped products" "Only simple products","Only simple products" +"Catalog Rules Configuration","Catalog Rules Configuration" +"Alphabetical sorting of attribute options in the rule engine","Alphabetical sorting of attribute options in the rule engine" +"If enabled, will forcefully sort select and multiselect attributes' option labels alphabetically when displaying them in the rule engine used by Optimizers and Virtual Categories. Useful if you have product attributes with very long list of options.","If enabled, will forcefully sort select and multiselect attributes' option labels alphabetically when displaying them in the rule engine used by Optimizers and Virtual Categories. Useful if you have product attributes with very long list of options." +"Stock qty","Stock qty" diff --git a/src/module-elasticsuite-catalog-rule/i18n/fr_FR.csv b/src/module-elasticsuite-catalog-rule/i18n/fr_FR.csv index 727808741..e2801f084 100644 --- a/src/module-elasticsuite-catalog-rule/i18n/fr_FR.csv +++ b/src/module-elasticsuite-catalog-rule/i18n/fr_FR.csv @@ -9,3 +9,7 @@ "Only giftcard products","Seulement les produits carte cadeau" "Only grouped products","Seulement les produits groupés" "Only simple products","Seulement les produits simples" +"Catalog Rules Configuration","Configuration des règles catalogue" +"Alphabetical sorting of attribute options in the rule engine","Tri alphabétique des options d'attributs dans le moteur de règles" +"If enabled, will forcefully sort select and multiselect attributes' option labels alphabetically when displaying them in the rule engine used by Optimizers and Virtual Categories. Useful if you have product attributes with very long list of options.","Si activé, forcera un tri alphabétique des labels des options d'attributs select et multiselect lors de leur affichage dans le moteur de règles utilisé par les Optimiseurs et les Catégories Virtuelles. Utile si vous avez des attributs produits avec un très grand nombre d'options." +"Stock qty","Quantité en stock" diff --git a/src/module-elasticsuite-catalog/Api/LayeredNavAttributeInterface.php b/src/module-elasticsuite-catalog/Api/LayeredNavAttributeInterface.php index 3ddacb78b..587be2fcd 100644 --- a/src/module-elasticsuite-catalog/Api/LayeredNavAttributeInterface.php +++ b/src/module-elasticsuite-catalog/Api/LayeredNavAttributeInterface.php @@ -14,6 +14,8 @@ namespace Smile\ElasticsuiteCatalog\Api; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + /** * LayeredNavAttributeInterface class. * @@ -37,6 +39,15 @@ public function getAttributeCode(): string; */ public function getFilterField(): string; + /** + * Get filter query. + * + * @param string|array $value Filter value. + * + * @return array|QueryInterface + */ + public function getFilterQuery($value); + /** * Get additional aggregation data. * diff --git a/src/module-elasticsuite-catalog/Block/Navigation/FilterRenderer.php b/src/module-elasticsuite-catalog/Block/Navigation/FilterRenderer.php index d8d323a16..02ae9e22a 100644 --- a/src/module-elasticsuite-catalog/Block/Navigation/FilterRenderer.php +++ b/src/module-elasticsuite-catalog/Block/Navigation/FilterRenderer.php @@ -47,7 +47,7 @@ public function _toHtml() $html = ''; foreach ($this->getChildNames() as $childName) { - if ($html === '') { + if (trim((string) $html) === '') { $renderer = $this->getChildBlock($childName); $html = $renderer->render($this->getFilter()); } diff --git a/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php b/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php index 67340fa5a..2d04db0c2 100644 --- a/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php +++ b/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php @@ -52,21 +52,6 @@ class Slider extends AbstractRenderer */ protected $catalogSliderHelper; - /** - * @var array - */ - protected $intervals; - - /** - * @var boolean - */ - protected $showAdaptiveSlider; - - /** - * @var array - */ - protected $adaptiveIntervals; - /** * * @param Context $context Template context. @@ -124,42 +109,39 @@ public function getDataRole() */ public function showAdaptiveSlider(): bool { - if (null === $this->showAdaptiveSlider) { - $this->showAdaptiveSlider = false; - if ($this->catalogSliderHelper->isAdaptiveSliderEnabled() - && ($this->getFilter()->getItemsCount() >= CatalogSliderHelper::ADAPTIVE_MINIMUM_ITEMS) - ) { - $hasDispersedData = false; - try { - $layer = $this->getFilter()->getLayer(); - $attributeModel = $this->getFilter()->getAttributeModel(); - if ($layer && $attributeModel) { - $facetName = $this->catalogSliderHelper->getStatsAggregation($attributeModel->getAttributeCode()); - $stats = $layer->getProductCollection()->getFacetedData($facetName); - $stats = current($stats); - /* Coefficient of Variation */ - $cv = ($stats['std_deviation'] ?? 0) / ($stats['avg'] ?? 1); - $hasDispersedData = ($cv > 1.0); - $lowerStdDevBound = $stats['std_deviation_bounds']['lower'] ?? 0; - $upperStdDevBound = $stats['std_deviation_bounds']['upper'] ?? 0; - if ($lowerStdDevBound && $upperStdDevBound) { - $hasDispersedData = ( - $hasDispersedData || ( - ($this->getMinValue() < $lowerStdDevBound) - || ($this->getMaxValue() > $upperStdDevBound) - ) - ); - } + $showAdaptiveSlider = false; + if ($this->catalogSliderHelper->isAdaptiveSliderEnabled() + && ($this->getFilter()->getItemsCount() >= CatalogSliderHelper::ADAPTIVE_MINIMUM_ITEMS) + ) { + $hasDispersedData = false; + try { + $layer = $this->getFilter()->getLayer(); + $attributeModel = $this->getFilter()->getAttributeModel(); + if ($layer && $attributeModel) { + $facetName = $this->catalogSliderHelper->getStatsAggregation($attributeModel->getAttributeCode()); + $stats = $layer->getProductCollection()->getFacetedData($facetName); + $stats = current($stats); + /* Coefficient of Variation */ + $cv = ($stats['std_deviation'] ?? 0) / ($stats['avg'] ?? 1); + $hasDispersedData = ($cv > 1.0); + $lowerStdDevBound = $stats['std_deviation_bounds']['lower'] ?? 0; + $upperStdDevBound = $stats['std_deviation_bounds']['upper'] ?? 0; + if ($lowerStdDevBound && $upperStdDevBound) { + $hasDispersedData = ( + $hasDispersedData || ( + ($this->getMinValue() < $lowerStdDevBound) + || ($this->getMaxValue() > $upperStdDevBound) + ) + ); } - } catch (\Magento\Framework\Exception\LocalizedException $e) { - ; } - - $this->showAdaptiveSlider = $hasDispersedData; + } catch (\Magento\Framework\Exception\LocalizedException $e) { + ; } + $showAdaptiveSlider = $hasDispersedData; } - return $this->showAdaptiveSlider; + return $showAdaptiveSlider; } /** @@ -266,15 +248,12 @@ private function getCurrentValue() */ private function getIntervals() { - if (null === $this->intervals) { - $intervals = []; - foreach ($this->getFilter()->getItems() as $item) { - $intervals[] = ['value' => $item->getValue(), 'count' => $item->getCount()]; - } - $this->intervals = $intervals; + $intervals = []; + foreach ($this->getFilter()->getItems() as $item) { + $intervals[] = ['value' => $item->getValue(), 'count' => $item->getCount()]; } - return $this->intervals; + return $intervals; } /** @@ -284,14 +263,12 @@ private function getIntervals() */ private function getAdaptiveIntervals(): array { - if (null === $this->adaptiveIntervals) { - $this->adaptiveIntervals = []; - if ($this->showAdaptiveSlider()) { - $this->adaptiveIntervals = $this->prepareAdaptiveIntervals(); - } + $adaptiveIntervals = []; + if ($this->showAdaptiveSlider()) { + $adaptiveIntervals = $this->prepareAdaptiveIntervals(); } - return $this->adaptiveIntervals; + return $adaptiveIntervals; } /** diff --git a/src/module-elasticsuite-catalog/Block/Plugin/Adminhtml/Product/Attribute/Edit/Tab/FrontPlugin.php b/src/module-elasticsuite-catalog/Block/Plugin/Adminhtml/Product/Attribute/Edit/Tab/FrontPlugin.php index f8b4bbdbb..e53c19c1f 100644 --- a/src/module-elasticsuite-catalog/Block/Plugin/Adminhtml/Product/Attribute/Edit/Tab/FrontPlugin.php +++ b/src/module-elasticsuite-catalog/Block/Plugin/Adminhtml/Product/Attribute/Edit/Tab/FrontPlugin.php @@ -18,10 +18,12 @@ use Magento\CatalogSearch\Model\Source\Weight; use Magento\Framework\Data\Form; use Magento\Framework\Registry; +use Magento\Framework\UrlInterface; use Smile\ElasticsuiteCatalog\Model\Attribute\Source\FilterBooleanLogic; use Smile\ElasticsuiteCatalog\Model\Attribute\Source\FilterSortOrder; use Magento\Framework\Data\Form\Element\Fieldset; use Magento\Catalog\Api\Data\EavAttributeInterface; +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; /** * Plugin that happend custom fields dedicated to search configuration @@ -38,12 +40,13 @@ class FrontPlugin * @var array */ private $movedFields = [ - 'is_searchable', - 'is_visible_in_advanced_search', - 'is_filterable', - 'is_filterable_in_search', - 'used_for_sort_by', - 'search_weight', + 'is_searchable' => 'elasticsuite_catalog_attribute_fieldset', + 'is_visible_in_advanced_search' => 'elasticsuite_catalog_attribute_fieldset', + 'is_filterable' => 'elasticsuite_catalog_attribute_navigation_fieldset', + 'is_filterable_in_search' => 'elasticsuite_catalog_attribute_navigation_fieldset', + 'used_for_sort_by' => 'elasticsuite_catalog_attribute_fieldset', + 'search_weight' => 'elasticsuite_catalog_attribute_fieldset', + 'position' => 'elasticsuite_catalog_attribute_navigation_fieldset', ]; /** @@ -56,6 +59,11 @@ class FrontPlugin */ private $booleanSource; + /** + * @var Registry + */ + private $coreRegistry; + /** * @var \Smile\ElasticsuiteCatalog\Model\Attribute\Source\FilterSortOrder */ @@ -66,6 +74,11 @@ class FrontPlugin */ private $filterBooleanLogic; + /** + * @var UrlInterface + */ + private $urlBuilder; + /** * Class constructor * @@ -74,24 +87,29 @@ class FrontPlugin * @param Registry $registry Core registry. * @param FilterSortOrder $filterSortOrder Filter Sort Order. * @param FilterBooleanLogic $filterBooleanLogic Filter boolean logic source model. + * @param UrlInterface $urlBuilder Url Builder. */ public function __construct( Yesno $booleanSource, Weight $weightSource, Registry $registry, FilterSortOrder $filterSortOrder, - FilterBooleanLogic $filterBooleanLogic + FilterBooleanLogic $filterBooleanLogic, + UrlInterface $urlBuilder ) { $this->weightSource = $weightSource; $this->booleanSource = $booleanSource; $this->coreRegistry = $registry; $this->filterSortOrder = $filterSortOrder; $this->filterBooleanLogic = $filterBooleanLogic; + $this->urlBuilder = $urlBuilder; } /** * Append ES specifics fields into the attribute edit store front tab. * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * * @param Front $subject The StoreFront tab * @param Front $result Result * @param Form $form The form @@ -100,14 +118,16 @@ public function __construct( */ public function afterSetForm(Front $subject, Front $result, Form $form) { - $fieldset = $this->createFieldset($form, $subject); - - $this->moveOrginalFields($form); - $this->addSearchFields($fieldset); - $this->addAutocompleteFields($fieldset); - $this->addFacetFields($fieldset); - $this->addSortFields($fieldset); - $this->addRelNofollowFields($fieldset); + $searchFieldset = $this->createSearchFieldset($form, $subject); + $layeredNavigationFieldset = $this->createLayeredNavigationFieldset($form, $subject); + $advancedFieldset = $this->createAdvancedFieldset($form, $subject); + + $this->moveOriginalFields($form); + $this->addSearchFields($searchFieldset); + $this->addAutocompleteFields($searchFieldset); + $this->addFacetFields($layeredNavigationFieldset); + $this->addSortFields($searchFieldset); + $this->addRelNofollowFields($layeredNavigationFieldset); $this->appendSliderDisplayRelatedFields($form, $subject); if ($this->getAttribute()->getAttributeCode() == 'name') { @@ -121,7 +141,16 @@ public function afterSetForm(Front $subject, Front $result, Form $form) || (in_array($this->getAttribute()->getFrontendInput(), ['select', 'multiselect']) || $this->getAttribute()->getSourceModel() != '') ) { - $this->addIncludeZeroFalseField($fieldset); + $this->addIncludeZeroFalseField($advancedFieldset); + } + + $this->addDefaultAnalyzer($advancedFieldset); + + if (($this->getAttribute()->getBackendType() == 'varchar') + || (in_array($this->getAttribute()->getFrontendInput(), ['select', 'multiselect'])) + ) { + $this->addIsSpannableField($advancedFieldset); + $this->addDisableNormsField($advancedFieldset); } $this->appendFieldsDependency($subject); @@ -147,7 +176,7 @@ private function getAttribute() * * @return Fieldset */ - private function createFieldset(Form $form, Front $subject) + private function createSearchFieldset(Form $form, Front $subject) { $fieldset = $form->addFieldset( 'elasticsuite_catalog_attribute_fieldset', @@ -163,6 +192,54 @@ private function createFieldset(Form $form, Front $subject) return $fieldset; } + /** + * Append the "Search Configuration" fieldset to the tab. + * + * @param Form $form Target form. + * @param Front $subject Target tab. + * + * @return Fieldset + */ + private function createLayeredNavigationFieldset(Form $form, Front $subject) + { + $fieldset = $form->addFieldset( + 'elasticsuite_catalog_attribute_navigation_fieldset', + [ + 'legend' => __('Layered Navigation Configuration'), + 'collapsable' => $subject->getRequest()->has('popup'), + ], + 'elasticsuite_catalog_attribute_fieldset' + ); + + $fieldset->addClass('es-esfeature__logo'); + + return $fieldset; + } + + /** + * Append the "Advanced Search Configuration" fieldset to the tab. + * + * @param Form $form Target form. + * @param Front $subject Target tab. + * + * @return Fieldset + */ + private function createAdvancedFieldset(Form $form, Front $subject) + { + $fieldset = $form->addFieldset( + 'elasticsuite_catalog_attribute_advanced_fieldset', + [ + 'legend' => __('Advanced Elasticsuite Configuration'), + 'collapsable' => $subject->getRequest()->has('popup'), + ], + 'elasticsuite_catalog_attribute_navigation_fieldset' + ); + + $fieldset->addClass('es-esfeature__logo'); + + return $fieldset; + } + /** * Move original fields to the new fieldset. * @@ -170,14 +247,14 @@ private function createFieldset(Form $form, Front $subject) * * @return FrontPlugin */ - private function moveOrginalFields(Form $form) + private function moveOriginalFields(Form $form) { $originalFieldset = $form->getElement('front_fieldset'); - $targetFieldset = $form->getElement('elasticsuite_catalog_attribute_fieldset'); - foreach ($this->movedFields as $elementId) { + foreach ($this->movedFields as $elementId => $fieldset) { $element = $form->getElement($elementId); if ($element) { + $targetFieldset = $form->getElement($fieldset); $originalFieldset->removeField($elementId); $targetFieldset->addElement($element); } @@ -388,7 +465,6 @@ private function addRelNofollowFields(Fieldset $fieldset) return $this; } - /** * Append the "Slider Display Configuration" fieldset to the tab. * @@ -497,6 +573,116 @@ private function addIncludeZeroFalseField(Fieldset $fieldset) return $this; } + /** + * Add field allowing to configure if a field can be used for span queries. + * + * @param Fieldset $fieldset Target fieldset + * + * @return FrontPlugin + */ + private function addIsSpannableField(Fieldset $fieldset) + { + $isSpannableNote = __( + // phpcs:ignore Generic.Files.LineLength + 'Default : No. If set to Yes, the engine will try to match the current query string at the beginning of this string.' + . ' Eg: when enabled on "name", if a customer search for "red dress", the engine will give an higher score to products having' + . ' a name beginning by "red dress". This requires the Span Match Boost feature to be enabled.' + ); + $fieldset->addField( + 'is_spannable', + 'select', + [ + 'name' => 'is_spannable', + 'label' => __('Use this field for span queries'), + 'values' => $this->booleanSource->toOptionArray(), + // phpcs:ignore Generic.Files.LineLength + 'note' => $isSpannableNote, + ], + 'default_analyzer' + ); + + return $this; + } + + /** + * Add field allowing to configure if zero/false values should be indexed or ignored. + * + * @param Fieldset $fieldset Target fieldset + * + * @return FrontPlugin + */ + private function addDisableNormsField(Fieldset $fieldset) + { + $disableNormsNote = __( + // phpcs:ignore Generic.Files.LineLength + 'Default : No. By default, the score of a text match in a field will vary according to the field length.' + . ' Eg: when searching for "dress", a product named "red dress" will have an higher score than a product named' + . ' "red dress with long sleeves". You can set this to "Yes" to discard this behavior.' + ); + $fieldset->addField( + 'norms_disabled', + 'select', + [ + 'name' => 'norms_disabled', + 'label' => __('Discard the field length for scoring'), + 'values' => $this->booleanSource->toOptionArray(), + // phpcs:ignore Generic.Files.LineLength + 'note' => $disableNormsNote, + ], + 'is_spannable' + ); + + return $this; + } + + /** + * Add field allowing to configure if a field can be used for span queries. + * + * @param Fieldset $fieldset Target fieldset + * + * @return FrontPlugin + */ + private function addDefaultAnalyzer(Fieldset $fieldset) + { + $link = sprintf( + '%s', + $this->urlBuilder->getUrl('smile_elasticsuite_indices/analysis/index', ['_query' => []]), + __("Analysis Page") + ); + + $defaultAnalyzerNote = __( + // phpcs:ignore Generic.Files.LineLength + 'Default : standard. The default analyzer for this field. Should be set to "reference" for SKU-like fields.' + . ' You can check the %1 screen to view how these analyzers behave.', + $link + ); + + $config = [ + 'name' => 'default_analyzer', + 'label' => __('Default Search Analyzer'), + 'values' => [ + ['value' => FieldInterface::ANALYZER_STANDARD, 'label' => __('standard')], + ['value' => FieldInterface::ANALYZER_REFERENCE, 'label' => __('reference')], + ['value' => FieldInterface::ANALYZER_EDGE_NGRAM, 'label' => __('standard_edge_ngram')], + ], + // phpcs:ignore Generic.Files.LineLength + 'note' => $defaultAnalyzerNote, + ]; + + if ($this->getAttribute()->getAttributeCode() === "sku") { + $config['value'] = FieldInterface::ANALYZER_REFERENCE; + } + + $fieldset->addField( + 'default_analyzer', + 'select', + $config, + 'is_used_in_spellcheck' + ); + + return $this; + } + /** * Manage dependency between fields. * @@ -520,8 +706,16 @@ private function appendFieldsDependency($subject) ->addFieldMap('sort_order_asc_missing', 'sort_order_asc_missing') ->addFieldMap('sort_order_desc_missing', 'sort_order_desc_missing') ->addFieldMap('is_display_rel_nofollow', 'is_display_rel_nofollow') + ->addFieldMap('is_spannable', 'is_spannable') + ->addFieldMap('norms_disabled', 'norms_disabled') + ->addFieldMap('default_analyzer', 'default_analyzer') + ->addFieldMap('search_weight', 'search_weight') ->addFieldDependence('is_displayed_in_autocomplete', 'is_filterable_in_search', '1') ->addFieldDependence('is_used_in_spellcheck', 'is_searchable', '1') + ->addFieldDependence('is_spannable', 'is_searchable', '1') + ->addFieldDependence('norms_disabled', 'is_searchable', '1') + ->addFieldDependence('search_weight', 'is_searchable', '1') + ->addFieldDependence('default_analyzer', 'is_searchable', '1') ->addFieldDependence('sort_order_asc_missing', 'used_for_sort_by', '1') ->addFieldDependence('sort_order_desc_missing', 'used_for_sort_by', '1') ->addFieldDependence('is_display_rel_nofollow', 'is_filterable', '1'); diff --git a/src/module-elasticsuite-catalog/Controller/Adminhtml/Product/Attribute/ExportProductAttributeCsv.php b/src/module-elasticsuite-catalog/Controller/Adminhtml/Product/Attribute/ExportProductAttributeCsv.php new file mode 100644 index 000000000..b038ad4d9 --- /dev/null +++ b/src/module-elasticsuite-catalog/Controller/Adminhtml/Product/Attribute/ExportProductAttributeCsv.php @@ -0,0 +1,162 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Controller\Adminhtml\Product\Attribute; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Filesystem; +use Magento\Framework\View\Result\PageFactory; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Smile\ElasticsuiteCatalog\Model\Import\ProductAttribute as ProductAttributeImport; + +/** + * Product attribute export to CSV controller. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Vadym Honcharuk + */ +class ExportProductAttributeCsv extends Action +{ + /** + * @var PageFactory + */ + protected $resultPageFactory; + + /** + * @var FileFactory + */ + protected $fileFactory; + + /** + * @var CollectionFactory + */ + protected $attributeCollectionFactory; + + /** + * @var Filesystem + */ + protected $filesystem; + + /** + * @var array + */ + private $columns; + + /** + * Constructor. + * + * @param Context $context Application context. + * @param PageFactory $resultPageFactory Result Page factory. + * @param FileFactory $fileFactory File Factory. + * @param Filesystem $filesystem File System. + * @param CollectionFactory $attributeCollectionFactory Attribute Collection Factory. + * @param ProductAttributeImport $productAttributeImport Product Attribute Import Model. + */ + public function __construct( + Context $context, + PageFactory $resultPageFactory, + FileFactory $fileFactory, + Filesystem $filesystem, + CollectionFactory $attributeCollectionFactory, + ProductAttributeImport $productAttributeImport + ) { + parent::__construct($context); + $this->resultPageFactory = $resultPageFactory; + $this->fileFactory = $fileFactory; + $this->filesystem = $filesystem; + $this->attributeCollectionFactory = $attributeCollectionFactory; + $this->columns = $productAttributeImport->getValidColumnNames(); + } + + /** + * Execute. + * + * @return ResponseInterface + * + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function execute() + { + // Prepare product attributes grid collection. + $attributeCollectionFactory = $this->attributeCollectionFactory->create(); + $attributeCollection = $attributeCollectionFactory->addVisibleFilter() + ->setOrder('attribute_code', 'ASC'); + + $content = []; + + // Add header row. + $header = []; + foreach ($this->columns as $column) { + $header[] = $column; + } + $content[] = $header; + + // Add content row. + foreach ($attributeCollection as $attribute) { + $row = [ + $attribute->getAttributeCode(), + $attribute->getDefaultFrontendLabel(), + $attribute->getIsSearchable(), + $attribute->getSearchWeight(), + $attribute->getIsUsedInSpellcheck(), + $attribute->getIsDisplayedInAutocomplete(), + $attribute->getIsFilterable(), + $attribute->getIsFilterableInSearch(), + $attribute->getIsUsedForPromoRules(), + $attribute->getUsedForSortBy(), + $attribute->getIsDisplayRelNofollow(), + $attribute->getFacetMaxSize(), + $attribute->getFacetSortOrder(), + $attribute->getFacetMinCoverageRate(), + $attribute->getFacetBooleanLogic(), + $attribute->getPosition(), + $attribute->getDefaultAnalyzer(), + $attribute->getNormsDisabled(), + $attribute->getIsSpannable(), + $attribute->getIncludeZeroFalseValues(), + ]; + $content[] = $row; + } + + // Prepare and send the CSV file to the browser. + $date = date('Ymd_His'); + $fileName = 'elasticsuite_product_attribute-' . $date . '.csv'; + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $stream = $directory->openFile($fileName, 'w+'); + foreach ($content as $line) { + $stream->writeCsv($line); + } + $stream->close(); + + return $this->fileFactory->create( + $fileName, + [ + 'type' => 'filename', + 'value' => $fileName, + 'rm' => true, + ], + DirectoryList::VAR_DIR, + 'application/csv' + ); + } +} diff --git a/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Load.php b/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Load.php index 921298e64..8974c5337 100644 --- a/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Load.php +++ b/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Load.php @@ -96,6 +96,7 @@ public function execute() } $json = $this->jsonHelper->jsonEncode($responseData); - $this->getResponse()->representJson($json); + + return $this->getResponse()->representJson($json); } } diff --git a/src/module-elasticsuite-catalog/Controller/Navigation/Filter/Ajax.php b/src/module-elasticsuite-catalog/Controller/Navigation/Filter/Ajax.php index e7470eec7..95c0cef63 100644 --- a/src/module-elasticsuite-catalog/Controller/Navigation/Filter/Ajax.php +++ b/src/module-elasticsuite-catalog/Controller/Navigation/Filter/Ajax.php @@ -43,24 +43,24 @@ class Ajax extends \Magento\Framework\App\Action\Action private $filterListPool; /** - * @var \Magento\Catalog\Api\Data\CategoryInterfaceFactory + * @var \Magento\Catalog\Api\CategoryRepositoryInterfaceFactory */ - private $categoryFactory; + private $categoryRepository; /** * Constructor. * - * @param \Magento\Framework\App\Action\Context $context Controller action context. - * @param \Magento\Framework\Controller\Result\JsonFactory $jsonResultFactory JSON result factory. - * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver Layer resolver. - * @param \Magento\Catalog\Api\Data\CategoryInterfaceFactory $categoryFactory Category factory. - * @param \Magento\Catalog\Model\Layer\FilterList[] $filterListPool Filter list pool. + * @param \Magento\Framework\App\Action\Context $context Controller action context. + * @param \Magento\Framework\Controller\Result\JsonFactory $jsonResultFactory JSON result factory. + * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver Layer resolver. + * @param \Magento\Catalog\Api\CategoryRepositoryInterfaceFactory $categoryRepository Category factory. + * @param \Magento\Catalog\Model\Layer\FilterList[] $filterListPool Filter list pool. */ public function __construct( \Magento\Framework\App\Action\Context $context, \Magento\Framework\Controller\Result\JsonFactory $jsonResultFactory, \Magento\Catalog\Model\Layer\Resolver $layerResolver, - \Magento\Catalog\Api\Data\CategoryInterfaceFactory $categoryFactory, + \Magento\Catalog\Api\CategoryRepositoryInterfaceFactory $categoryRepository, $filterListPool = [] ) { parent::__construct($context); @@ -68,7 +68,7 @@ public function __construct( $this->jsonResultFactory = $jsonResultFactory; $this->layerResolver = $layerResolver; $this->filterListPool = $filterListPool; - $this->categoryFactory = $categoryFactory; + $this->categoryRepository = $categoryRepository; } /** @@ -110,7 +110,11 @@ private function initLayer() $this->layerResolver->create($this->getLayerType()); if ($this->getRequest()->getParam('cat')) { - $category = $this->categoryFactory->create()->setId($this->getRequest()->getParam('cat')); + $category = $this->categoryRepository->create()->get( + $this->getRequest()->getParam('cat'), + $this->layerResolver->get()->getCurrentStore()->getId() + ); + $this->layerResolver->get()->setCurrentCategory($category); } diff --git a/src/module-elasticsuite-catalog/Files/Sample/elasticsuite_product_attribute.csv b/src/module-elasticsuite-catalog/Files/Sample/elasticsuite_product_attribute.csv new file mode 100644 index 000000000..e1f80b48a --- /dev/null +++ b/src/module-elasticsuite-catalog/Files/Sample/elasticsuite_product_attribute.csv @@ -0,0 +1,3 @@ +attribute_code,attribute_label,is_searchable,search_weight,is_used_in_spellcheck,is_displayed_in_autocomplete,is_filterable,is_filterable_in_search,is_used_for_promo_rules,used_for_sort_by,is_display_rel_nofollow,facet_max_size,facet_sort_order,facet_min_coverage_rate,facet_boolean_logic,position,default_analyzer,norms_disabled,is_spannable,include_zero_false_values +activity,Activity,0,1,1,0,1,0,1,0,0,10,_count,90,1,0,standard,0,0,0 +allow_open_amount,Open Amount,0,1,1,0,0,0,0,0,0,10,_count,90,0,0,standard,0,0,0 diff --git a/src/module-elasticsuite-catalog/Helper/AbstractAttribute.php b/src/module-elasticsuite-catalog/Helper/AbstractAttribute.php index 975006b8a..18bfe3eef 100644 --- a/src/module-elasticsuite-catalog/Helper/AbstractAttribute.php +++ b/src/module-elasticsuite-catalog/Helper/AbstractAttribute.php @@ -143,6 +143,18 @@ public function getMappingFieldOptions(AttributeInterface $attribute) $options['sort_order_desc_missing'] = $attribute->getSortOrderDescMissing(); } + if ($attribute->getIsSpannable()) { + $options['is_spannable'] = $attribute->getIsSpannable(); + } + + if ($attribute->getNormsDisabled()) { + $options['norms_disabled'] = $attribute->getNormsDisabled(); + } + + if ($attribute->getDefaultAnalyzer()) { + $options['default_search_analyzer'] = $attribute->getDefaultAnalyzer(); + } + return $options; } diff --git a/src/module-elasticsuite-catalog/Helper/IgnorePositions.php b/src/module-elasticsuite-catalog/Helper/IgnorePositions.php new file mode 100644 index 000000000..fbc20b787 --- /dev/null +++ b/src/module-elasticsuite-catalog/Helper/IgnorePositions.php @@ -0,0 +1,88 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +declare(strict_types = 1); + +namespace Smile\ElasticsuiteCatalog\Helper; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Ignore positions helper + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Richard Bayet + */ +class IgnorePositions +{ + const XML_PATH_IGNORE_OOS_PRODUCT_POSITIONS = 'smile_elasticsuite_catalogsearch_settings/catalogsearch/ignore_oos_product_positions'; + + /** + * Scope configuration + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Stock configuration + * + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var array + */ + private $removeOosPositionsConfig = []; + + /** + * Constructor + * + * @param ScopeConfigInterface $scopeConfig Scope configuration. + * @param StockConfigurationInterface $stockConfiguration Stock configuration. + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StockConfigurationInterface $stockConfiguration + ) { + $this->scopeConfig = $scopeConfig; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Returns true if categories and search queries positions should be ignored for Out of Stock products. + * + * @param integer $storeId Store id + * + * @return bool + */ + public function isIgnoreOosPositions($storeId): bool + { + if (!array_key_exists($storeId, $this->removeOosPositionsConfig)) { + $removeOosPositions = $this->scopeConfig->isSetFlag( + self::XML_PATH_IGNORE_OOS_PRODUCT_POSITIONS, + ScopeInterface::SCOPE_STORE, + $storeId + ); + $removeOosPositions = $removeOosPositions && $this->stockConfiguration->isShowOutOfStock($storeId); + + $this->removeOosPositionsConfig[$storeId] = $removeOosPositions; + } + + return $this->removeOosPositionsConfig[$storeId]; + } +} diff --git a/src/module-elasticsuite-catalog/Model/Import/ProductAttribute.php b/src/module-elasticsuite-catalog/Model/Import/ProductAttribute.php new file mode 100644 index 000000000..678692734 --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/Import/ProductAttribute.php @@ -0,0 +1,258 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Model\Import; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\ImportExport\Helper\Data as ImportHelper; +use Magento\ImportExport\Model\Import\Entity\AbstractEntity; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\ImportExport\Model\ResourceModel\Helper; +use Magento\ImportExport\Model\ResourceModel\Import\Data; + +/** + * Product attribute import model. + * + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + * @SuppressWarnings(PHPMD.CamelCasePropertyName) + * @SuppressWarnings(PHPMD.ElseExpression) + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Vadym Honcharuk + */ +class ProductAttribute extends AbstractEntity +{ + /** + * Entity type code. + */ + const ENTITY_TYPE_CODE = 'elasticsuite_product_attribute'; + + /** + * Permanent entity columns. + * + * @var array + */ + protected $_permanentAttributes = [ + 'attribute_code', + 'attribute_label', + ]; + + /** + * Valid column names. + * + * @var array + */ + protected $validColumnNames = [ + 'attribute_code', + 'attribute_label', + 'is_searchable', + 'search_weight', + 'is_used_in_spellcheck', + 'is_displayed_in_autocomplete', + 'is_filterable', + 'is_filterable_in_search', + 'is_used_for_promo_rules', + 'used_for_sort_by', + 'is_display_rel_nofollow', + 'facet_max_size', + 'facet_sort_order', + 'facet_min_coverage_rate', + 'facet_boolean_logic', + 'position', + 'default_analyzer', + 'norms_disabled', + 'is_spannable', + 'include_zero_false_values', + ]; + + /** + * Count if updated items. + * + * @var integer + */ + protected $countItemsUpdated = 0; + + /** + * Need to log in import history. + * + * @var boolean + */ + protected $logInHistory = true; + + /** + * @var Config + */ + private $_eavConfig; + + /** + * Import constructor. + * + * @param JsonHelper $jsonHelper Json Helper. + * @param ImportHelper $importExportData Import Helper. + * @param Data $importData Import Data. + * @param Config $eavConfig EAV Config. + * @param Helper $resourceHelper Resource Helper. + * @param ProcessingErrorAggregatorInterface $errorAggregator Error Aggregator. + */ + public function __construct( + JsonHelper $jsonHelper, + ImportHelper $importExportData, + Data $importData, + Config $eavConfig, + Helper $resourceHelper, + ProcessingErrorAggregatorInterface $errorAggregator + ) { + $this->jsonHelper = $jsonHelper; + $this->_importExportData = $importExportData; + $this->_dataSourceModel = $importData; + $this->_eavConfig = $eavConfig; + $this->_resourceHelper = $resourceHelper; + $this->errorAggregator = $errorAggregator; + } + + /** + * Entity type code getter. + * + * @return string + */ + public function getEntityTypeCode(): string + { + return self::ENTITY_TYPE_CODE; + } + + /** + * Get available columns. + * + * @return array + */ + public function getValidColumnNames(): array + { + return $this->validColumnNames; + } + + /** + * Validate data row. + * + * @param array $rowData Data. + * @param int $rowNum Row number. + * + * @return bool + * @throws LocalizedException + */ + public function validateRow(array $rowData, $rowNum) + { + $errors = []; + + // Validate if attribute exists. + $attributeCode = isset($rowData['attribute_code']) ? trim($rowData['attribute_code']) : ''; + if (!$attributeCode) { + $errors[] = __('Attribute code is required.'); + } else { + $attribute = $this->_eavConfig->getAttribute('catalog_product', $attributeCode); + if (!$attribute->getId()) { + $errors[] = __('Attribute with code %1 does not exist.', $attributeCode); + } + } + + // Check if all the required columns are present. + foreach ($this->validColumnNames as $columnName) { + if (!isset($rowData[$columnName])) { + $errors[] = __('Column %1 is missing.', $columnName); + } + } + + if (!empty($errors)) { + foreach ($errors as $error) { + $this->addRowError($error, $rowNum); + } + + return false; + } + + return true; + } + + /** + * Import data rows. + * + * @return bool + * @throws LocalizedException + */ + protected function _importData() + { + // Add import logic. + while ($bunch = $this->_dataSourceModel->getNextBunch()) { + foreach ($bunch as $rowData) { + $attributeCode = isset($rowData['attribute_code']) ? trim($rowData['attribute_code']) : ''; + $attribute = $this->_eavConfig->getAttribute('catalog_product', $attributeCode); + $result = $this->updateAttributeData($attribute, $rowData); + if ($result) { + $this->countItemsUpdated++; + } + } + } + + return true; + } + + /** + * Update attribute data with new values from CSV. + * + * @param Attribute $attribute Attribute. + * @param array $rowData Row Data. + * @return bool + */ + private function updateAttributeData($attribute, $rowData) + { + $dataChanged = false; + + foreach ($rowData as $key => $value) { + // Skip permanent attributes. + if (in_array($key, $this->_permanentAttributes)) { + continue; + } + + // Skip empty values. + if (!isset($value) || $value === '') { + continue; + } + + // Update attribute data if new value is different from current value. + if ($attribute->getData($key) != $value) { + $attribute->setData($key, $value); + $dataChanged = true; + } + } + + // Check if attribute data has changed. + if ($dataChanged) { + try { + $attribute->save(); + } catch (\Exception $e) { + $this->_errors[] = __( + 'Row with attribute_code "%1" cannot be updated. Error: %2', + $attribute->getAttributeCode(), + $e->getMessage() + ); + + return false; + } + } + + return true; + } +} diff --git a/src/module-elasticsuite-catalog/Model/Layer/Filter/Boolean.php b/src/module-elasticsuite-catalog/Model/Layer/Filter/Boolean.php index b3d82de54..cc6db4dec 100644 --- a/src/module-elasticsuite-catalog/Model/Layer/Filter/Boolean.php +++ b/src/module-elasticsuite-catalog/Model/Layer/Filter/Boolean.php @@ -106,7 +106,8 @@ public function apply(\Magento\Framework\App\RequestInterface $request) /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $productCollection */ $productCollection = $this->getLayer()->getProductCollection(); - $productCollection->addFieldToFilter($this->getFilterField(), $attributeValue); + $filterField = $this->getFilterField(); + $productCollection->addFieldToFilter($filterField, $this->getFilterValue($attributeValue)); $layerState = $this->getLayer()->getState(); foreach ($this->currentFilterValue as $currentFilter) { @@ -194,4 +195,23 @@ protected function _initItems() return $this; } + + /** + * Get filter value. + * + * @param mixed $value Filter value. + * + * @return mixed + */ + private function getFilterValue(array $value) + { + $field = $this->getAttributeModel()->getAttributeCode(); + + $layeredNavAttribute = $this->layeredNavAttributesProvider->getLayeredNavAttribute($field); + if ($layeredNavAttribute instanceof LayeredNavAttributeInterface) { + return $layeredNavAttribute->getFilterQuery($value); + } + + return $value; + } } diff --git a/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/AttributeData.php b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/AttributeData.php index 5819e3d9a..3f2a5717c 100644 --- a/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/AttributeData.php +++ b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/AttributeData.php @@ -52,6 +52,11 @@ class AttributeData extends AbstractAttributeData implements DatasourceInterface */ private $forbiddenChildrenAttributes = []; + /** + * @var boolean + */ + private $isIndexingChildProductSkuEnabled; + /** * Constructor * diff --git a/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/IgnoreOutOfStockPositionsData.php b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/IgnoreOutOfStockPositionsData.php new file mode 100644 index 000000000..4cb76bd4a --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/IgnoreOutOfStockPositionsData.php @@ -0,0 +1,78 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +declare(strict_types = 1); + +namespace Smile\ElasticsuiteCatalog\Model\Product\Indexer\Fulltext\Datasource; + +use Smile\ElasticsuiteCore\Api\Index\DatasourceInterface; +use Smile\ElasticsuiteCatalog\Helper\IgnorePositions; + +/** + * Ignore out of stock product positions datasource. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Richard Bayet + */ +class IgnoreOutOfStockPositionsData implements DatasourceInterface +{ + const XML_PATH_IGNORE_OOS_PRODUCT_POSITIONS = 'smile_elasticsuite_catalogsearch_settings/catalogsearch/ignore_oos_product_positions'; + + /** + * Ignore positions helper. + * + * @var IgnorePositions + */ + private $ignorePositions; + + /** + * Constructor + * + * @param IgnorePositions $ignorePositions Ignore positions helper. + */ + public function __construct( + IgnorePositions $ignorePositions + ) { + $this->ignorePositions = $ignorePositions; + } + + /** + * Remove category positions of out-of-stock products if configured so. + * {@inheritdoc} + */ + public function addData($storeId, array $indexData): array + { + if ($this->ignorePositions->isIgnoreOosPositions($storeId)) { + foreach ($indexData as &$productData) { + if (isset($productData['stock']['is_in_stock']) && (bool) $productData['stock']['is_in_stock'] === false) { + if (array_key_exists('category', $productData)) { + // Remove categories product position. + foreach ($productData['category'] as &$categoryDataRow) { + unset($categoryDataRow['position']); + } + } + + if (array_key_exists('search_query', $productData)) { + // Remove search queries position. + foreach ($productData['search_query'] as &$searchQueryDataRow) { + unset($searchQueryDataRow['position']); + } + } + } + } + } + + return $indexData; + } +} diff --git a/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/PriceData.php b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/PriceData.php index eb3a857c9..727f9f96e 100644 --- a/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/PriceData.php +++ b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/PriceData.php @@ -17,9 +17,9 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Store\Model\ScopeInterface; -use Smile\ElasticsuiteCore\Api\Index\DatasourceInterface; -use Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource\PriceData as ResourceModel; use Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource\AttributeData as AttributeResourceModel; +use Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource\PriceData as ResourceModel; +use Smile\ElasticsuiteCore\Api\Index\DatasourceInterface; /** * Datasource used to append prices data to product during indexing. @@ -67,7 +67,7 @@ class PriceData implements DatasourceInterface * @param ResourceModel $resourceModel Resource model * @param AttributeResourceModel $attributeResourceModel Attribute Resource model * @param PriceData\PriceDataReaderInterface[] $priceReaderPool Price modifiers pool. - * @param ScopeConfigInterface $scopeConfig Scope Config. + * @param ScopeConfigInterface|null $scopeConfig Scope Config. */ public function __construct( ResourceModel $resourceModel, @@ -107,16 +107,19 @@ public function addData($storeId, array $indexData) $isDiscount = $price < $originalPrice; - if ($this->isComputeChildDiscountEnabled()) { - if (in_array($productTypeId, $this->attributeResourceModel->getCompositeTypes())) { - $isDiscount = false; - $priceModifier = $this->getPriceDataReader('default'); - foreach ($childPriceData as $childPrice) { - if ($childPrice['customer_group_id'] == $priceDataRow['customer_group_id']) { - if ($priceModifier->getPrice($childPrice) < $priceModifier->getOriginalPrice($childPrice)) { - $isDiscount = true; - break; - } + if ($this->isComputeChildDiscountEnabled() && + in_array($productTypeId, $this->attributeResourceModel->getCompositeTypes()) + ) { + $isDiscount = false; + $priceModifier = $this->getPriceDataReader('default'); + foreach ($childPriceData as $childPrice) { + foreach ($allChildrenIds[$childPrice['entity_id']] as $childIdsData) { + if ($childIdsData['parent_id'] === $productId + && $childPrice['customer_group_id'] == $priceDataRow['customer_group_id'] + && $priceModifier->getPrice($childPrice) < $priceModifier->getOriginalPrice($childPrice) + ) { + $isDiscount = true; + break 2; } } } diff --git a/src/module-elasticsuite-catalog/Model/Product/Search/Position.php b/src/module-elasticsuite-catalog/Model/Product/Search/Position.php index 423d68671..2fbae8e07 100644 --- a/src/module-elasticsuite-catalog/Model/Product/Search/Position.php +++ b/src/module-elasticsuite-catalog/Model/Product/Search/Position.php @@ -79,7 +79,7 @@ public function __construct( * @param array $newProductPositions Product positions. * @param array $blacklistedProducts Blacklisted product ids. * - * @return \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position + * @return void */ public function saveProductPositions($queryId, $newProductPositions, $blacklistedProducts = []) { diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php index c1ae6ba75..d71897532 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php @@ -17,6 +17,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Indexer; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as AttributeCollection; @@ -104,51 +105,52 @@ public function addIndexedFilterToAttributeCollection(AttributeCollection $attri */ public function getAttributesRawData($storeId, array $entityIds, $tableName, array $attributeIds) { - $select = $this->connection->select(); - // The field modelizing the link between entity table and attribute values table. Either row_id or entity_id. $linkField = $this->getEntityMetaData($this->getEntityTypeId())->getLinkField(); // The legacy entity_id field. $entityIdField = $this->getEntityMetaData($this->getEntityTypeId())->getIdentifierField(); + $entityTable = $this->getEntityMetaData($this->getEntityTypeId())->getEntityTable(); - $joinDefaultValuesCondition = [ - new \Zend_Db_Expr("entity.$linkField = t_default.$linkField"), - 't_default.attribute_id = attr.attribute_id', - $this->connection->quoteInto('t_default.store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID), + // Define store related conditions, keep the order of the array elements! + $storeConditions = [ + 'default' => $this->connection->quoteInto('t_attribute.store_id = ?', Store::DEFAULT_STORE_ID), + 'store' => $this->connection->quoteInto('t_attribute.store_id = ?', $storeId), ]; - $joinDefaultValuesCondition = implode(' AND ', $joinDefaultValuesCondition); - $joinStoreValuesConditionClauses = [ - new \Zend_Db_Expr("entity.$linkField = t_store.$linkField"), - 't_store.attribute_id = attr.attribute_id', - $this->connection->quoteInto('t_store.store_id = ?', $storeId), - ]; - $joinStoreValuesCondition = implode(' AND ', $joinStoreValuesConditionClauses); - - $select->from(['entity' => $this->getEntityMetaData($this->getEntityTypeId())->getEntityTable()], [$entityIdField]) - ->joinInner( - ['attr' => $this->getTable('eav_attribute')], - $this->connection->quoteInto('attr.attribute_id IN (?)', $attributeIds), - ['attribute_id', 'attribute_code'] - ) - ->joinLeft( - ['t_default' => $tableName], - $joinDefaultValuesCondition, - [] - ) - ->joinLeft( - ['t_store' => $tableName], - $joinStoreValuesCondition, - [] - ) - ->where("entity.{$entityIdField} IN (?)", $entityIds) - ->having('value IS NOT NULL') - ->columns( - ['value' => new \Zend_Db_Expr('if(t_store.value_id IS NOT NULL, t_store.value, t_default.value)')] - ); - - return $this->connection->fetchAll($select); + $result = []; + foreach ($storeConditions as $condition) { + $joinAttributeValuesCondition = [ + new \Zend_Db_Expr("entity.$linkField = t_attribute.$linkField"), + $condition, + ]; + + $joinAttributeValuesCondition = implode(' AND ', $joinAttributeValuesCondition); + + $select = $this->connection->select(); + $select->from(['entity' => $entityTable], [$entityIdField]) + ->joinLeft( + ['t_attribute' => $tableName], + $joinAttributeValuesCondition, + ['attribute_id', 'value'] + ) + ->joinInner( + ['attr' => $this->getTable('eav_attribute')], + "t_attribute.attribute_id = attr.attribute_id", + ['attribute_id', 'attribute_code'] + ) + ->where("entity.{$entityIdField} IN (?)", $entityIds) + ->where("t_attribute.attribute_id IN (?)", $attributeIds) + ->where("t_attribute.value IS NOT NULL"); + + // Get the result and override values from a previous loop. + foreach ($this->connection->fetchAll($select) as $row) { + $key = "{$row['entity_id']}-{$row['attribute_id']}"; + $result[$key] = $row; + } + } + + return array_values($result); } /** diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/FilterableAttribute/Category/Collection.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/FilterableAttribute/Category/Collection.php index ffad57201..5f8dc2758 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/FilterableAttribute/Category/Collection.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/FilterableAttribute/Category/Collection.php @@ -118,6 +118,8 @@ protected function _beforeLoad() if ($this->category && $this->category->getId()) { $this->applyCategory(); } + + return $this; } /** diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php index 08c083e6b..8d517c1d4 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php @@ -544,7 +544,7 @@ function (\Magento\Framework\Api\Search\Document $doc) { $this->isSpellchecked = $searchRequest->isSpellchecked(); - return parent::_renderFiltersBefore(); + parent::_renderFiltersBefore(); } /** diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Setup/PropertyMapper.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Setup/PropertyMapper.php new file mode 100644 index 000000000..d35055b23 --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Setup/PropertyMapper.php @@ -0,0 +1,55 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Model\ResourceModel\Setup; + +use Magento\Eav\Model\Entity\Setup\PropertyMapperAbstract; + +/** + * Elasticsearch catalog EAV attribute mappings. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Benjamin Rosenberger + */ +class PropertyMapper extends PropertyMapperAbstract +{ + /** + * {@inheritdoc} + */ + public function map(array $input, $entityTypeId) + { + return [ + 'is_displayed_in_autocomplete' => $this->_getValue($input, 'is_displayed_in_autocomplete', 0), + 'is_used_in_spellcheck' => $this->_getValue($input, 'is_used_in_spellcheck', 0), + 'facet_min_coverage_rate' => $this->_getValue($input, 'facet_min_coverage_rate', 90), + 'facet_max_size' => $this->_getValue($input, 'facet_max_size', 10), + 'facet_sort_order' => $this->_getValue( + $input, + 'facet_sort_order', + \Smile\ElasticsuiteCore\Search\Request\BucketInterface::SORT_ORDER_COUNT + ), + 'display_pattern' => $this->_getValue($input, 'display_pattern', null), + 'display_precision' => $this->_getValue($input, 'display_precision', 0), + 'sort_order_asc_missing' => $this->_getValue($input, 'sort_order_asc_missing', '_last'), + 'sort_order_desc_missing' => $this->_getValue($input, 'sort_order_desc_missing', '_last'), + 'facet_boolean_logic' => $this->_getValue( + $input, + 'facet_boolean_logic', + \Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface::FILTER_LOGICAL_OPERATOR_OR + ), + 'is_display_rel_nofollow' => $this->_getValue($input, 'is_display_rel_nofollow', 0), + 'include_zero_false_values' => $this->_getValue($input, 'include_zero_false_values', 0), + ]; + } +} diff --git a/src/module-elasticsuite-catalog/Model/Source/Import/Behavior/Custom.php b/src/module-elasticsuite-catalog/Model/Source/Import/Behavior/Custom.php new file mode 100644 index 000000000..f25c6c930 --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/Source/Import/Behavior/Custom.php @@ -0,0 +1,49 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Model\Source\Import\Behavior; + +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Source\Import\AbstractBehavior; + +/** + * Custom import behavior source model used for defining the behavior during the product attributes import. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Vadym Honcharuk + */ +class Custom extends AbstractBehavior +{ + /** + * Get array of possible values. + * + * @return array + */ + public function toArray() + { + return [ + Import::BEHAVIOR_APPEND => __('Update'), + ]; + } + + /** + * Get current behaviour group code. + * + * @return string + */ + public function getCode() + { + return 'elasticsuite_product_attribute_import_custom_behavior'; + } +} diff --git a/src/module-elasticsuite-catalog/Observer/Grid/ProductAttributeGridColumnObserver.php b/src/module-elasticsuite-catalog/Observer/Grid/ProductAttributeGridColumnObserver.php new file mode 100644 index 000000000..0a1e88b80 --- /dev/null +++ b/src/module-elasticsuite-catalog/Observer/Grid/ProductAttributeGridColumnObserver.php @@ -0,0 +1,73 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Observer\Grid; + +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; + +/** + * This observer class observes the "product_attribute_grid_build" event. + * + * And adds two columns "Search Weight" and "Is filterable in search" to the product attribute grid. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Vadym Honcharuk + */ +class ProductAttributeGridColumnObserver implements ObserverInterface +{ + /** + * Execute. + * + * @param Observer $observer Observer. + * @return void + */ + public function execute(Observer $observer) + { + /** @var \Magento\Catalog\Block\Adminhtml\Product\Attribute\Grid $grid */ + $grid = $observer->getGrid(); + + // Add "Search Weight" column after "Searchable" column. + $grid->addColumnAfter( + 'search_weight', + [ + 'header' => __('Search Weight'), + 'index' => 'search_weight', + 'type' => 'text', + 'align' => 'center', + 'sortable' => true, + 'escape' => true, + ], + 'is_searchable' + ); + + // Add "Is filterable in search" column after "Use in layered navigation" column. + $grid->addColumnAfter( + 'is_filterable_in_search', + [ + 'header' => __('Is Filterable in Search'), + 'index' => 'is_filterable_in_search', + 'type' => 'options', + 'options' => ['1' => __('Yes'), '0' => __('No')], + 'align' => 'center', + 'sortable' => true, + 'escape' => true, + ], + 'is_filterable' + ); + + // Sort columns by predefined order. + $grid->sortColumnsByOrder(); + } +} diff --git a/src/module-elasticsuite-catalog/Observer/Grid/ProductAttributeGridExportObserver.php b/src/module-elasticsuite-catalog/Observer/Grid/ProductAttributeGridExportObserver.php new file mode 100644 index 000000000..80059e90f --- /dev/null +++ b/src/module-elasticsuite-catalog/Observer/Grid/ProductAttributeGridExportObserver.php @@ -0,0 +1,46 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Observer\Grid; + +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; + +/** + * This observer class observes the "backend_block_widget_grid_prepare_grid_before" event. + * + * And adds a button for exporting product attributes to a CSV file on the product attribute grid. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Vadym Honcharuk + */ +class ProductAttributeGridExportObserver implements ObserverInterface +{ + /** + * Execute. + * + * @param Observer $observer Observer. + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(Observer $observer) + { + /** @var \Magento\Catalog\Block\Adminhtml\Product\Attribute\Grid $grid */ + $grid = $observer->getGrid(); + + if ($grid instanceof \Magento\Catalog\Block\Adminhtml\Product\Attribute\Grid) { + $grid->addExportType('*/*/exportProductAttributeCsv', __('CSV')); + } + } +} diff --git a/src/module-elasticsuite-catalog/Plugin/Catalog/Eav/AttributePlugin.php b/src/module-elasticsuite-catalog/Plugin/Catalog/Eav/AttributePlugin.php index 291e365f7..ac197c1b7 100644 --- a/src/module-elasticsuite-catalog/Plugin/Catalog/Eav/AttributePlugin.php +++ b/src/module-elasticsuite-catalog/Plugin/Catalog/Eav/AttributePlugin.php @@ -41,6 +41,8 @@ class AttributePlugin 'is_used_for_sort_by', 'is_used_in_spellcheck', 'include_zero_false_values', + 'disable_norms', + 'default_search_analyzer', ]; /** @@ -54,6 +56,9 @@ class AttributePlugin EavAttributeInterface::USED_FOR_SORT_BY, EavAttributeInterface::IS_VISIBLE_IN_ADVANCED_SEARCH, 'search_weight', + 'disable_norms', + 'is_spannable', + 'default_analyzer', ]; /** @@ -173,6 +178,7 @@ public function afterSave( * Check if operations (clean cache, mapping update, invalide index) must be triggered for current attribute. * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) * * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $subject Attribute being saved * @@ -190,14 +196,21 @@ private function checkUpdateNeeded($subject) $options = $this->getMappingFieldOptions($subject); foreach ($this->updateMappingFields as $field) { - $origValue = (int) ($origOptions[$field] ?? false); - $value = (int) ($options[$field] ?? false); + $origValue = ($origOptions[$field] ?? false); + $value = ($options[$field] ?? false); if ($origValue !== $value) { + if ($field === 'default_search_analyzer') { + $cleanCache = true; + $updateMapping = true; + $invalidateIndex = true; + continue; + } + if ($field === 'search_weight') { // Search weight has changed. Cache needs to be cleaned. $cleanCache = true; - if (($origValue === 1) && ($value > $origValue)) { + if (((int) $origValue === 1) && ((int) $value > (int) $origValue)) { // Search weight moved from 1 to more. Mapping will change, so data need to be reindexed. $updateMapping = true; $invalidateIndex = true; diff --git a/src/module-elasticsuite-catalog/Plugin/Index/Indices/Config/ReaderPlugin.php b/src/module-elasticsuite-catalog/Plugin/Index/Indices/Config/ReaderPlugin.php index 41e09e516..39a5df322 100644 --- a/src/module-elasticsuite-catalog/Plugin/Index/Indices/Config/ReaderPlugin.php +++ b/src/module-elasticsuite-catalog/Plugin/Index/Indices/Config/ReaderPlugin.php @@ -34,6 +34,11 @@ class ReaderPlugin */ const XML_CATEGORY_NAME_WEIGHT = 'smile_elasticsuite_catalogsearch_settings/catalogsearch/category_name_weight'; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * ConfigPlugin constructor. * diff --git a/src/module-elasticsuite-catalog/Plugin/Indexer/AbstractIndexerPlugin.php b/src/module-elasticsuite-catalog/Plugin/Indexer/AbstractIndexerPlugin.php index 23ee99602..af8447900 100644 --- a/src/module-elasticsuite-catalog/Plugin/Indexer/AbstractIndexerPlugin.php +++ b/src/module-elasticsuite-catalog/Plugin/Indexer/AbstractIndexerPlugin.php @@ -24,6 +24,11 @@ */ class AbstractIndexerPlugin { + /** + * @var \Magento\Framework\Indexer\IndexerRegistry + */ + private $indexerRegistry; + /** * @var \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Action\Full */ diff --git a/src/module-elasticsuite-catalog/Plugin/Search/RequestMapperPlugin.php b/src/module-elasticsuite-catalog/Plugin/Search/RequestMapperPlugin.php index 15ca4ec88..6e7b3c012 100644 --- a/src/module-elasticsuite-catalog/Plugin/Search/RequestMapperPlugin.php +++ b/src/module-elasticsuite-catalog/Plugin/Search/RequestMapperPlugin.php @@ -159,6 +159,8 @@ public function afterGetSortOrders( * @param SearchCriteriaInterface $searchCriteria Search criteria. * * @return array[] + * + * @SuppressWarnings(PHPMD.ElseExpression) */ public function afterGetFilters( RequestMapper $subject, @@ -170,8 +172,15 @@ public function afterGetFilters( $filters = []; foreach ($result as $fieldName => $filterValue) { - $fieldName = $this->getMappingField($containerConfiguration, $fieldName); - $filters[$fieldName] = $this->getFieldValue($containerConfiguration, $fieldName, $filterValue); + $layeredNavAttribute = $this->layeredNavAttributesProvider->getLayeredNavAttribute($fieldName); + if ($layeredNavAttribute instanceof LayeredNavAttributeInterface) { + $fieldName = $layeredNavAttribute->getFilterField(); + // Use reset to remove graphql operator. + $filters[$fieldName] = $layeredNavAttribute->getFilterQuery(reset($filterValue)); + } else { + $fieldName = $this->getMappingField($containerConfiguration, $fieldName); + $filters[$fieldName] = $this->getFieldValue($containerConfiguration, $fieldName, $filterValue); + } } $result = $filters; @@ -227,11 +236,6 @@ private function getMappingField(ContainerConfigurationInterface $containerConfi { $fieldName = $this->requestFieldMapper->getMappedFieldName($fieldName); - $layeredNavAttribute = $this->layeredNavAttributesProvider->getLayeredNavAttribute($fieldName); - if ($layeredNavAttribute instanceof LayeredNavAttributeInterface) { - return $layeredNavAttribute->getFilterField(); - } - try { $field = $containerConfiguration->getMapping()->getField($fieldName); } catch (\Exception $e) { diff --git a/src/module-elasticsuite-catalog/Search/Request/Product/Coverage/Provider.php b/src/module-elasticsuite-catalog/Search/Request/Product/Coverage/Provider.php index 17a0119ae..b5f0282ae 100644 --- a/src/module-elasticsuite-catalog/Search/Request/Product/Coverage/Provider.php +++ b/src/module-elasticsuite-catalog/Search/Request/Product/Coverage/Provider.php @@ -27,6 +27,11 @@ class Provider */ private $searchEngine; + /** + * @var \Smile\ElasticsuiteCore\Search\RequestInterface + */ + private $request; + /** * @var array */ diff --git a/src/module-elasticsuite-catalog/Setup/CatalogSetup.php b/src/module-elasticsuite-catalog/Setup/CatalogSetup.php index b501a71b3..68b9fc6e6 100644 --- a/src/module-elasticsuite-catalog/Setup/CatalogSetup.php +++ b/src/module-elasticsuite-catalog/Setup/CatalogSetup.php @@ -20,6 +20,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Setup\SchemaSetupInterface; +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; /** * Generic Setup for ElasticsuiteCatalog module. @@ -205,6 +206,32 @@ public function updateDefaultValuesForNameAttributes($eavSetup) } } + /** + * Update default values for the sku field of product entity. + * + * @param \Magento\Eav\Setup\EavSetup $eavSetup EAV module Setup + * + * @return void + */ + public function updateDefaultValuesForSkuAttribute($eavSetup) + { + $setup = $eavSetup->getSetup(); + $connection = $setup->getConnection(); + $table = $setup->getTable('catalog_eav_attribute'); + + $attributeIds = [ + $eavSetup->getAttributeId(\Magento\Catalog\Model\Product::ENTITY, 'sku'), + ]; + + foreach ($attributeIds as $attributeId) { + $connection->update( + $table, + ['default_analyzer' => FieldInterface::ANALYZER_REFERENCE], + $connection->quoteInto('attribute_id = ?', $attributeId) + ); + } + } + /** * Add custom fields to catalog_eav_attribute table. * @@ -710,6 +737,84 @@ public function addIncludeZeroFalseValues(SchemaSetupInterface $setup) ); } + /** + * Add "is_spannable" field to catalog_eav_attribute table. + * + * @param \Magento\Framework\Setup\SchemaSetupInterface $setup Schema Setup + * + * @return void + */ + public function addIsSpannableAttributeProperty(\Magento\Framework\Setup\SchemaSetupInterface $setup) + { + $connection = $setup->getConnection(); + $table = $setup->getTable('catalog_eav_attribute'); + + // Append a column 'is_spannable' into the db. + $connection->addColumn( + $table, + 'is_spannable', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_BOOLEAN, + 'nullable' => false, + 'default' => 0, + 'size' => 1, + 'comment' => 'Should this field be used for span queries.', + ] + ); + } + + /** + * Add "norms_disabled" field to catalog_eav_attribute table. + * + * @param \Magento\Framework\Setup\SchemaSetupInterface $setup Schema Setup + * + * @return void + */ + public function addNormsDisabledAttributeProperty(\Magento\Framework\Setup\SchemaSetupInterface $setup) + { + $connection = $setup->getConnection(); + $table = $setup->getTable('catalog_eav_attribute'); + + // Append a column 'norms_disabled' into the db. + $connection->addColumn( + $table, + 'norms_disabled', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_BOOLEAN, + 'nullable' => false, + 'default' => 0, + 'size' => 1, + 'comment' => 'If this field should have norms:false in Elasticsearch.', + ] + ); + } + + /** + * Add "default_analyzer" field to catalog_eav_attribute table. + * + * @param \Magento\Framework\Setup\SchemaSetupInterface $setup Schema Setup + * + * @return void + */ + public function addDefaultAnalyzer(\Magento\Framework\Setup\SchemaSetupInterface $setup) + { + $connection = $setup->getConnection(); + $table = $setup->getTable('catalog_eav_attribute'); + + // Append a column 'default_analyzer' into the db. + $connection->addColumn( + $table, + 'default_analyzer', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'nullable' => false, + 'default' => (string) FieldInterface::ANALYZER_STANDARD, + 'length' => 30, + 'comment' => 'Default analyzer for this field', + ] + ); + } + /** * Update attribute value for an entity with a default value. * All existing values are erased by the new value. diff --git a/src/module-elasticsuite-catalog/Setup/InstallData.php b/src/module-elasticsuite-catalog/Setup/InstallData.php index 417afef34..9c3ea54a7 100644 --- a/src/module-elasticsuite-catalog/Setup/InstallData.php +++ b/src/module-elasticsuite-catalog/Setup/InstallData.php @@ -92,6 +92,7 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $this->catalogSetup->updateDefaultValuesForNameAttributes($eavSetup); $this->catalogSetup->updateCategorySearchableAttributes($eavSetup); $this->catalogSetup->updateImageAttribute($eavSetup); + $this->catalogSetup->updateDefaultValuesForSkuAttribute($eavSetup); $this->getIndexer('elasticsuite_categories_fulltext')->reindexAll(); diff --git a/src/module-elasticsuite-catalog/Setup/InstallSchema.php b/src/module-elasticsuite-catalog/Setup/InstallSchema.php index bce8571cb..f2f3503ab 100644 --- a/src/module-elasticsuite-catalog/Setup/InstallSchema.php +++ b/src/module-elasticsuite-catalog/Setup/InstallSchema.php @@ -78,6 +78,11 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con // Introduced in version 1.6.1. $this->catalogSetup->addIncludeZeroFalseValues($setup); + // Introduced in version 1.7.0. + $this->catalogSetup->addIsSpannableAttributeProperty($setup); + $this->catalogSetup->addNormsDisabledAttributeProperty($setup); + $this->catalogSetup->addDefaultAnalyzer($setup); + $setup->endSetup(); } } diff --git a/src/module-elasticsuite-catalog/Setup/UpgradeData.php b/src/module-elasticsuite-catalog/Setup/UpgradeData.php index 4c484d072..206bfbdcd 100644 --- a/src/module-elasticsuite-catalog/Setup/UpgradeData.php +++ b/src/module-elasticsuite-catalog/Setup/UpgradeData.php @@ -83,6 +83,10 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $this->catalogSetup->updateIsDisplayInAutocompleteAttribute($eavSetup); } + if (version_compare($context->getVersion(), '1.7.0', '<')) { + $this->catalogSetup->updateDefaultValuesForSkuAttribute($eavSetup); + } + $setup->endSetup(); } } diff --git a/src/module-elasticsuite-catalog/Setup/UpgradeSchema.php b/src/module-elasticsuite-catalog/Setup/UpgradeSchema.php index 4216d4db0..aeceff457 100644 --- a/src/module-elasticsuite-catalog/Setup/UpgradeSchema.php +++ b/src/module-elasticsuite-catalog/Setup/UpgradeSchema.php @@ -94,6 +94,12 @@ public function upgrade( $this->catalogSetup->addIncludeZeroFalseValues($setup); } + if (version_compare($context->getVersion(), '1.7.0', '<')) { + $this->catalogSetup->addIsSpannableAttributeProperty($setup); + $this->catalogSetup->addNormsDisabledAttributeProperty($setup); + $this->catalogSetup->addDefaultAnalyzer($setup); + } + $setup->endSetup(); } } diff --git a/src/module-elasticsuite-catalog/Ui/Component/Search/Term/Listing/DataProvider.php b/src/module-elasticsuite-catalog/Ui/Component/Search/Term/Listing/DataProvider.php index 9603a4c7f..8f697a580 100644 --- a/src/module-elasticsuite-catalog/Ui/Component/Search/Term/Listing/DataProvider.php +++ b/src/module-elasticsuite-catalog/Ui/Component/Search/Term/Listing/DataProvider.php @@ -85,6 +85,8 @@ public function addField($field, $alias = null) /** * {@inheritdoc} + * + * @return void */ public function addFilter(\Magento\Framework\Api\Filter $filter) { diff --git a/src/module-elasticsuite-catalog/etc/adminhtml/di.xml b/src/module-elasticsuite-catalog/etc/adminhtml/di.xml index a6ecf6cc0..c609876b8 100644 --- a/src/module-elasticsuite-catalog/etc/adminhtml/di.xml +++ b/src/module-elasticsuite-catalog/etc/adminhtml/di.xml @@ -58,4 +58,29 @@ fulltextProductCollectionWithoutAggregationsBuilder + + + + + \Smile\ElasticsuiteCatalog\Search\Request\Product\Coverage\Builder + + + + + + Smile\ElasticsuiteCatalog\Search\Request\Product\Aggregation\Provider\FilterableAttributes\Search\AttributeList + + Smile\ElasticsuiteCatalog\Search\Request\Product\Aggregation\Provider\FilterableAttributes\Modifier\Coverage + + + + + + + Smile\ElasticsuiteCatalog\Search\Request\Product\Aggregation\Provider\FilterableAttributes\Category\AttributeList + + Smile\ElasticsuiteCatalog\Search\Request\Product\Aggregation\Provider\FilterableAttributes\Modifier\Coverage + + + diff --git a/src/module-elasticsuite-catalog/etc/adminhtml/events.xml b/src/module-elasticsuite-catalog/etc/adminhtml/events.xml new file mode 100644 index 000000000..c9443046a --- /dev/null +++ b/src/module-elasticsuite-catalog/etc/adminhtml/events.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/src/module-elasticsuite-catalog/etc/adminhtml/routes.xml b/src/module-elasticsuite-catalog/etc/adminhtml/routes.xml index c3c799094..28ec34268 100644 --- a/src/module-elasticsuite-catalog/etc/adminhtml/routes.xml +++ b/src/module-elasticsuite-catalog/etc/adminhtml/routes.xml @@ -20,5 +20,8 @@ + + + diff --git a/src/module-elasticsuite-catalog/etc/adminhtml/system.xml b/src/module-elasticsuite-catalog/etc/adminhtml/system.xml index b4b136157..5b7bed07e 100644 --- a/src/module-elasticsuite-catalog/etc/adminhtml/system.xml +++ b/src/module-elasticsuite-catalog/etc/adminhtml/system.xml @@ -85,11 +85,16 @@ Magento\Config\Model\Config\Source\Yesno Note when changing this setting, it is recommended to perform a full reindex of the Catalog Search indexer.]]> - + Magento\Config\Model\Config\Source\Yesno + + + Magento\Config\Model\Config\Source\Yesno + + diff --git a/src/module-elasticsuite-catalog/etc/di.xml b/src/module-elasticsuite-catalog/etc/di.xml index 21d0c89ce..c2fc5ea80 100644 --- a/src/module-elasticsuite-catalog/etc/di.xml +++ b/src/module-elasticsuite-catalog/etc/di.xml @@ -346,6 +346,7 @@ Smile\ElasticsuiteCatalog\Model\Product\Indexer\Fulltext\Datasource\AttributeData Smile\ElasticsuiteCatalog\Model\Product\Indexer\Fulltext\Datasource\InventoryData Smile\ElasticsuiteCatalog\Model\Product\Indexer\Fulltext\Datasource\SearchPositionData + Smile\ElasticsuiteCatalog\Model\Product\Indexer\Fulltext\Datasource\IgnoreOutOfStockPositionsData Smile\ElasticsuiteCatalog\Model\Category\Indexer\Fulltext\Datasource\AttributeData @@ -401,5 +402,21 @@ Magento\Customer\Model\Session\Proxy - + + + + + + Smile\ElasticsuiteCatalog\Model\ResourceModel\Setup\PropertyMapper + + + + + + + + Smile_ElasticsuiteCatalog + + + diff --git a/src/module-elasticsuite-catalog/etc/import.xml b/src/module-elasticsuite-catalog/etc/import.xml new file mode 100644 index 000000000..a46396390 --- /dev/null +++ b/src/module-elasticsuite-catalog/etc/import.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/src/module-elasticsuite-catalog/etc/module.xml b/src/module-elasticsuite-catalog/etc/module.xml index ce5c44649..f6eb5e3c3 100644 --- a/src/module-elasticsuite-catalog/etc/module.xml +++ b/src/module-elasticsuite-catalog/etc/module.xml @@ -16,7 +16,7 @@ */ --> - + @@ -28,6 +28,7 @@ + diff --git a/src/module-elasticsuite-catalog/i18n/en_US.csv b/src/module-elasticsuite-catalog/i18n/en_US.csv index f08769195..1e0aa53c3 100755 --- a/src/module-elasticsuite-catalog/i18n/en_US.csv +++ b/src/module-elasticsuite-catalog/i18n/en_US.csv @@ -119,3 +119,14 @@ Attributes,Attributes "If enabled, child products SKUs of composite products will be indexed in a separate field and the ""sku"" field will only contain the parent product sku.","If enabled, child products SKUs of composite products will be indexed in a separate field and the ""sku"" field will only contain the parent product sku." "Enable indexing discount on child products","Enable indexing discount on child products" "Enable this if your catalog contains configurable products that are having childrens with different prices that could have separated discounts.","Enable this if your catalog contains configurable products that are having childrens with different prices that could have separated discounts." +"Use this field for span queries","Use this field for span queries" +"Discard the field length for scoring","Discard the field length for scoring" +"Default : No. If set to Yes, the engine will try to match the current query string at the beginning of this string. Eg: when enabled on ""name"", if a customer search for ""red dress"", the engine will give an higher score to products having a name beginning by ""red dress"". This requires the Span Match Boost feature to be enabled.","Default : No. If set to Yes, the engine will try to match the current query string at the beginning of this string. Eg: when enabled on ""name"", if a customer search for ""red dress"", the engine will give an higher score to products having a name beginning by ""red dress"". This requires the Span Match Boost feature to be enabled." +"Default : No. By default, the score of a text match in a field will vary according to the field length. Eg: when searching for ""dress"", a product named ""red dress"" will have an higher score than a product named """red dress with long sleeves"". You can set this to ""Yes"" to discard this behavior.","Default : No. By default, the score of a text match in a field will vary according to the field length. Eg: when searching for ""dress"", a product named ""red dress"" will have an higher score than a product named """red dress with long sleeves"". You can set this to ""Yes"" to discard this behavior." +"Default Search Analyzer","Default Search Analyzer" +"Default : standard. The default analyzer for this field. Should be set to """reference"" for SKU-like fields. You can check the %1 screen to view how these analyzers behave.","Default : standard. The default analyzer for this field. Should be set to """reference"" for SKU-like fields. You can check the %1 screen to view how these analyzers behave." +"Layered Navigation Configuration","Layered Navigation Configuration" +"Advanced Elasticsuite Configuration","Advanced Elasticsuite Configuration" +"Analysis Page","Analysis Page" +"Ignore manual positions of out of stock products","Ignore manual positions of out of stock products" +"If you show out of stock products in the frontend (through the ""Catalog / Inventory / Stock Options / Display Out of Stock Products"" stores configuration setting), switch this setting to Yes to make sure that a manually positionned product (in categories or in search queries) is no longer shown in the configured manual positions when it becomes out of stock.","If you show out of stock products in the frontend (through the ""Catalog / Inventory / Stock Options / Display Out of Stock Products"" stores configuration setting), switch this setting to Yes to make sure that a manually positionned product (in categories or in search queries) is no longer shown in the configured manual positions when it becomes out of stock." diff --git a/src/module-elasticsuite-catalog/i18n/fr_FR.csv b/src/module-elasticsuite-catalog/i18n/fr_FR.csv index 6ee7356f5..34a13af14 100755 --- a/src/module-elasticsuite-catalog/i18n/fr_FR.csv +++ b/src/module-elasticsuite-catalog/i18n/fr_FR.csv @@ -119,3 +119,14 @@ Attributes,Attributs "If enabled, child products SKUs of composite products will be indexed in a separate field and the ""sku"" field will only contain the parent product sku.","Si activé, les SKUs des produits enfants des produits composites seront stockés dans un champ à part. Le champ ""sku"" du parent ne contiendra que son SKU." "Enable indexing discount on child products","Calculer les discounts sur les produits enfants" "Enable this if your catalog contains configurable products that are having childrens with different prices that could have separated discounts.","Activez cette option si votre catalogue contient des produits configurables ayant des enfants dont les prix de base sont différents et pouvant avoir des prix barrés différents." +"Use this field for span queries","Booster les matchs sur le début du champ" +"Discard the field length for scoring","Ignorer la longueur du champ dans le scoring" +"Default : No. If set to Yes, the engine will try to match the current query string at the beginning of this string. Eg: when enabled on ""name"", if a customer search for ""red dress"", the engine will give an higher score to products having a name beginning by ""red dress"". This requires the Span Match Boost feature to be enabled.","Par défaut : Non. Quand cette option est activée, le moteur donnera plus de poids aux produits pour lesquels ce champ commence par les mêmes mots que la recherche de l'internaute. Ex: si activé sur ""name"", et qu'un client recherche ""robe rouge"", le moteur donnera un score plus élevé aux produits ayant un nom commençant par ""robe rouge"". Cette configuration nécessite d'activer la ""Recherche par le début des champs"" dans la configuration de la pertinence Elasticsuite." +"Default : No. By default, the score of a text match in a field will vary according to the field length. Eg: when searching for ""dress"", a product named ""red dress"" will have an higher score than a product named ""red dress with long sleeves"". You can set this to ""Yes"" to discard this behavior.","Par défaut : Non. Par défaut, le score d'un champ varie en fonction de sa longueur. Par exemple: si un client cherche ""robe"", un produit nommé ""robe rouge"" aura un meilleur score qu'un produit nommé ""robe rouge à manche longues"". Vous pouvez mettre cette option à ""Oui"" pour désactiver ce comportement." +"Default Search Analyzer","Analyseur par défaut" +"Default : standard. The default analyzer for this field. Should be set to ""reference"" for SKU-like fields. You can check the %1 screen to view how these analyzers behave.","Par défaut : standard. L'analyseur par défaut pour ce champ. Il peut être réglé sur ""reference"" pour les champs contenant des ""codes"" semblables à des SKUs. Vous pouvez utiliser la page %1 pour vérifier l'effet des analyseurs." +"Layered Navigation Configuration","Configuration de la navigation à facettes" +"Advanced Elasticsuite Configuration","Configuration avancée d'Elasticsuite" +"Analysis Page","page d'analyse textuelle" +"Ignore manual positions of out of stock products","Ignorer les positions manuelles des produits épuisés" +"If you show out of stock products in the frontend (through the ""Catalog / Inventory / Stock Options / Display Out of Stock Products"" stores configuration setting), switch this setting to Yes to make sure that a manually positionned product (in categories or in search queries) is no longer shown in the configured manual positions when it becomes out of stock.","Si vous affichez les produits épuisés en front (grâce à l'option de configuration magasin ""Catalogue / Inventory / Stock Options / Display Out of Stock Products""), basculez ce paramètre à Oui pour vous assurer qu'un produit positionné manuellement (dans des catégories ou des requêtes de recherche) ne sera plus montré à ces positions manuelles lorsqu'il devient épuisé." diff --git a/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less b/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less index 3bc2aa2f2..a445f5472 100644 --- a/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less +++ b/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less @@ -66,15 +66,18 @@ } .product-list { - + clear: right; + display: flex; + flex-wrap: wrap; margin: 0; - display: inline-block; li { box-sizing: content-box; - float: left; border: 1px solid #d6d6d6; background: #FFFFFF; + display: flex; + flex-direction: column; + /*justify-content: space-between;*/ width: 200px; min-height: 221px; margin: 5px 0 0 5px; @@ -96,6 +99,7 @@ .info-wrapper { position: absolute; bottom: 0; + width: 100%; } .info { @@ -119,6 +123,9 @@ display: block; width: 100%; margin-bottom: 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } h1 { @@ -292,3 +299,13 @@ } } } + +#attributeGrid_table { + .col-search_weight { + width: 115px; + } + + .col-is_filterable_in_search { + width: 162px; + } +} diff --git a/src/module-elasticsuite-core/Api/Index/IndexOperationInterface.php b/src/module-elasticsuite-core/Api/Index/IndexOperationInterface.php index fb2dc9771..a1b1fb325 100644 --- a/src/module-elasticsuite-core/Api/Index/IndexOperationInterface.php +++ b/src/module-elasticsuite-core/Api/Index/IndexOperationInterface.php @@ -72,7 +72,7 @@ public function createIndex($indexIdentifier, $store); * @param integer|string|\Magento\Store\Api\Data\StoreInterface $store Store (id, identifier or object). * @param array $fields The fields to update. Default to all. * - * @return \Smile\ElasticsuiteCore\Api\Index\IndexInterface + * @return void */ public function updateMapping($indexIdentifier, $store, $fields = []); diff --git a/src/module-elasticsuite-core/Api/Index/Mapping/FieldInterface.php b/src/module-elasticsuite-core/Api/Index/Mapping/FieldInterface.php index d43bd3fcd..9f34d7f61 100644 --- a/src/module-elasticsuite-core/Api/Index/Mapping/FieldInterface.php +++ b/src/module-elasticsuite-core/Api/Index/Mapping/FieldInterface.php @@ -48,6 +48,8 @@ interface FieldInterface const ANALYZER_PHONETIC = 'phonetic'; const ANALYZER_UNTOUCHED = 'untouched'; const ANALYZER_KEYWORD = 'keyword'; + const ANALYZER_REFERENCE = 'reference'; + const ANALYZER_EDGE_NGRAM = 'standard_edge_ngram'; /** * Field filter logical operators. @@ -77,6 +79,16 @@ public function getType(); */ public function isSearchable(); + /** + * Is the field searchable and contains reference (sku) data. + */ + public function isSearchableReference(); + + /** + * Is the field searchable and using an edge ngram based analyzer. + */ + public function isSearchableEdgeNgram(); + /** * Is the field filterable in navigation. * @@ -177,4 +189,18 @@ public function getFilterLogicalOperator(); * @return array */ public function getConfig(); + + /** + * If "norms" of the field in mapping should be set to false. + * + * @return bool + */ + public function normsDisabled(); + + /** + * Is the field should be used for span queries. + * + * @return boolean + */ + public function isSpannable(); } diff --git a/src/module-elasticsuite-core/Api/Index/MappingInterface.php b/src/module-elasticsuite-core/Api/Index/MappingInterface.php index 85a694900..103e4d014 100644 --- a/src/module-elasticsuite-core/Api/Index/MappingInterface.php +++ b/src/module-elasticsuite-core/Api/Index/MappingInterface.php @@ -29,6 +29,8 @@ interface MappingInterface const DEFAULT_SEARCH_FIELD = 'search'; const DEFAULT_SPELLING_FIELD = 'spelling'; const DEFAULT_AUTOCOMPLETE_FIELD = 'autocomplete'; + const DEFAULT_REFERENCE_FIELD = 'reference'; + const DEFAULT_EDGE_NGRAM_FIELD = 'edge_ngram'; /** * List of the properties of the mapping. diff --git a/src/module-elasticsuite-core/Api/Search/Request/Container/RelevanceConfigurationInterface.php b/src/module-elasticsuite-core/Api/Search/Request/Container/RelevanceConfigurationInterface.php index af758e70b..629bddf63 100644 --- a/src/module-elasticsuite-core/Api/Search/Request/Container/RelevanceConfigurationInterface.php +++ b/src/module-elasticsuite-core/Api/Search/Request/Container/RelevanceConfigurationInterface.php @@ -65,4 +65,84 @@ public function isPhoneticSearchEnabled(); * @return \Smile\ElasticsuiteCore\Api\Search\Request\Container\RelevanceConfiguration\FuzzinessConfigurationInterface|null */ public function getFuzzinessConfiguration(); + + /** + * Retrieve span match boost value if enabled. + * + * @return false|int + */ + public function getSpanMatchBoost(); + + /** + * Retrieve span number value if enabled. + * + * @return false|int + */ + public function getSpanSize(); + + /** + * Retrieve min_score value if enabled. + * + * @return false|int + */ + public function getMinScore(); + + /** + * Check if the reference collector field should be used instead of the simple 'sku' field + * when building the exact match filter query. + * + * @return bool + */ + public function isUsingReferenceInExactMatchFilter(); + + /** + * Check if all tokens of the term vectors response should be used. + * + * @return bool + */ + public function isUsingAllTokens(); + + /** + * Check if the term vectors request should also include the reference analyzer collector field. + * + * @return bool + */ + public function isUsingReferenceAnalyzer(); + + /** + * Check if the term vectors request should also include the edge ngram analyzer(s) collector field. + * + * @return bool + */ + public function isUsingEdgeNgramAnalyzer(); + + /** + * If we should use the default analyzer of each field when building the exact match filter query. + * + * @return bool + */ + public function isUsingDefaultAnalyzerInExactMatchFilter(); + + /** + * Are the exact match boosts on whitespace and sortable version of searchable attributes/fields + * customized. + * + * @return bool + */ + public function areExactMatchSingleTermBoostsCustomized(); + + /** + * Returns the exact match boost for whitespace version of searchable attributes/fields, + * used instead of the shingle version of attributes/fields when a single term is searched. + * + * @return int + */ + public function getExactMatchSingleTermPhraseMatchBoost(); + + /** + * Returns the exact match boost for sortable version of searchable+sortable attributes/fields. + * + * @return int + */ + public function getExactMatchSingleTermSortableBoost(); } diff --git a/src/module-elasticsuite-core/Api/Search/Spellchecker/RequestInterface.php b/src/module-elasticsuite-core/Api/Search/Spellchecker/RequestInterface.php index 5570f3a5b..bc7a8025a 100644 --- a/src/module-elasticsuite-core/Api/Search/Spellchecker/RequestInterface.php +++ b/src/module-elasticsuite-core/Api/Search/Spellchecker/RequestInterface.php @@ -43,4 +43,25 @@ public function getQueryText(); * @return float */ public function getCutoffFrequency(); + + /** + * Is the spellcheck request using all tokens returned by the term vectors. + * + * @return boolean + */ + public function isUsingAllTokens(); + + /** + * Should the spellcheck request target the 'reference' collector field. + * + * @return boolean + */ + public function isUsingReference(); + + /** + * Should the spellcheck request target the 'edge_ngram' collector field. + * + * @return boolean + */ + public function isUsingEdgeNgram(); } diff --git a/src/module-elasticsuite-core/Block/Adminhtml/Search/Request/RelevanceConfig/Scope/Switcher.php b/src/module-elasticsuite-core/Block/Adminhtml/Search/Request/RelevanceConfig/Scope/Switcher.php index 8c42d4d3d..eb3198227 100644 --- a/src/module-elasticsuite-core/Block/Adminhtml/Search/Request/RelevanceConfig/Scope/Switcher.php +++ b/src/module-elasticsuite-core/Block/Adminhtml/Search/Request/RelevanceConfig/Scope/Switcher.php @@ -284,13 +284,16 @@ public function getCurrentSelectionName() */ public function getCurrentStoreName() { + $storeName = ''; if ($this->getStoreId() !== null) { $store = $this->storeFactory->create(); $store->load($this->getStoreId()); if ($store->getId()) { - return $store->getName(); + $storeName = $store->getName(); } } + + return $storeName; } /** @@ -300,13 +303,16 @@ public function getCurrentStoreName() */ public function getCurrentContainerName() { + $containerName = ''; if ($this->getContainerCode() !== null) { $container = $this->containersSource->get($this->getContainerCode()); if ($this->getContainerName($container)) { - return $this->getContainerName($container); + $containerName = $this->getContainerName($container); } } + + return $containerName; } /** @@ -328,13 +334,16 @@ public function getContainerName($container) */ public function getCurrentContainerLabel() { + $containerLabel = ''; if ($this->getContainerCode() !== null) { $container = $this->containersSource->get($this->getContainerCode()); if ($this->getContainerLabel($container)) { - return $this->getContainerLabel($container); + $containerLabel = $this->getContainerLabel($container); } } + + return $containerLabel; } /** diff --git a/src/module-elasticsuite-core/Index/AsyncIndexOperation.php b/src/module-elasticsuite-core/Index/AsyncIndexOperation.php index 15b6768df..9c4287348 100644 --- a/src/module-elasticsuite-core/Index/AsyncIndexOperation.php +++ b/src/module-elasticsuite-core/Index/AsyncIndexOperation.php @@ -63,6 +63,8 @@ public function __construct( /** * {@inheritDoc} + * + * @return void */ public function executeBulk(\Smile\ElasticsuiteCore\Api\Index\Bulk\BulkRequestInterface $bulk) { diff --git a/src/module-elasticsuite-core/Index/Mapping.php b/src/module-elasticsuite-core/Index/Mapping.php index 87d319951..507625de8 100644 --- a/src/module-elasticsuite-core/Index/Mapping.php +++ b/src/module-elasticsuite-core/Index/Mapping.php @@ -62,6 +62,16 @@ class Mapping implements MappingInterface FieldInterface::ANALYZER_WHITESPACE, FieldInterface::ANALYZER_SHINGLE, ], + self::DEFAULT_REFERENCE_FIELD => [ + FieldInterface::ANALYZER_REFERENCE, + FieldInterface::ANALYZER_WHITESPACE, + FieldInterface::ANALYZER_SHINGLE, + ], + self::DEFAULT_EDGE_NGRAM_FIELD => [ + FieldInterface::ANALYZER_EDGE_NGRAM, + FieldInterface::ANALYZER_WHITESPACE, + FieldInterface::ANALYZER_SHINGLE, + ], ]; /** @@ -72,6 +82,8 @@ class Mapping implements MappingInterface private $copyFieldMap = [ 'isSearchable' => self::DEFAULT_SEARCH_FIELD, 'isUsedInSpellcheck' => self::DEFAULT_SPELLING_FIELD, + 'isSearchableReference' => self::DEFAULT_REFERENCE_FIELD, + 'isSearchableEdgeNgram' => self::DEFAULT_EDGE_NGRAM_FIELD, ]; /** diff --git a/src/module-elasticsuite-core/Index/Mapping/Field.php b/src/module-elasticsuite-core/Index/Mapping/Field.php index 993be49b1..603e1b15d 100644 --- a/src/module-elasticsuite-core/Index/Mapping/Field.php +++ b/src/module-elasticsuite-core/Index/Mapping/Field.php @@ -70,6 +70,7 @@ class Field implements FieldInterface 'default_search_analyzer' => self::ANALYZER_STANDARD, 'filter_logical_operator' => self::FILTER_LOGICAL_OPERATOR_OR, 'norms_disabled' => false, + 'is_spannable' => false, ]; /** @@ -117,6 +118,22 @@ public function isSearchable(): bool return (bool) $this->config['is_searchable']; } + /** + * {@inheritdoc} + */ + public function isSearchableReference(): bool + { + return ($this->isSearchable() && (FieldInterface::ANALYZER_REFERENCE === $this->config['default_search_analyzer'])); + } + + /** + * {@inheritDoc} + */ + public function isSearchableEdgeNgram(): bool + { + return ($this->isSearchable() && (FieldInterface::ANALYZER_EDGE_NGRAM === $this->config['default_search_analyzer'])); + } + /** * {@inheritdoc} */ @@ -149,6 +166,14 @@ public function normsDisabled(): bool return (bool) $this->config['norms_disabled']; } + /** + * {@inheritDoc} + */ + public function isSpannable(): bool + { + return (bool) $this->config['is_spannable']; + } + /** * {@inheritdoc} */ @@ -247,6 +272,7 @@ public function mergeConfig(array $config = []) { $config = array_merge($this->config, $config); + // @phpstan-ignore-next-line return new static($this->name, $config['type'] ?? $this->type, $this->nestedPath, $config); } @@ -391,9 +417,13 @@ private function getPropertyConfig($analyzer = self::ANALYZER_UNTOUCHED): array if ($analyzer === self::ANALYZER_UNTOUCHED) { $fieldMapping['type'] = self::FIELD_TYPE_KEYWORD; $fieldMapping['ignore_above'] = self::IGNORE_ABOVE_COUNT; + $fieldMapping['normalizer'] = self::ANALYZER_UNTOUCHED; } if ($analyzer !== self::ANALYZER_UNTOUCHED) { $fieldMapping['analyzer'] = $analyzer; + if ($analyzer === self::ANALYZER_EDGE_NGRAM) { + $fieldMapping['search_analyzer'] = self::ANALYZER_STANDARD; + } if ($this->normsDisabled() || ($analyzer === self::ANALYZER_KEYWORD)) { $fieldMapping['norms'] = false; diff --git a/src/module-elasticsuite-core/Model/ProductMetadata/ComposerInformation.php b/src/module-elasticsuite-core/Model/ProductMetadata/ComposerInformation.php index b75151000..4b7155331 100644 --- a/src/module-elasticsuite-core/Model/ProductMetadata/ComposerInformation.php +++ b/src/module-elasticsuite-core/Model/ProductMetadata/ComposerInformation.php @@ -36,6 +36,11 @@ class ComposerInformation extends \Magento\Framework\Composer\ComposerInformatio */ private $locker; + /** + * @var ComposerFactory + */ + private $composerFactory; + /** * @param \Magento\Framework\Composer\ComposerFactory $composerFactory Composer Factory */ diff --git a/src/module-elasticsuite-core/Model/Search.php b/src/module-elasticsuite-core/Model/Search.php index 9a86b45a0..cee7a93de 100644 --- a/src/module-elasticsuite-core/Model/Search.php +++ b/src/module-elasticsuite-core/Model/Search.php @@ -71,6 +71,7 @@ public function search(\Magento\Framework\Api\Search\SearchCriteriaInterface $se $totalCount = $searchResponse->count(); $searchResult->setTotalCount($totalCount); $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setData('is_spellchecked', (bool) $searchRequest->isSpellchecked()); return $searchResult; } diff --git a/src/module-elasticsuite-core/Model/Search/Request/RelevanceConfig/Reader/ContainerStore.php b/src/module-elasticsuite-core/Model/Search/Request/RelevanceConfig/Reader/ContainerStore.php index 179b3251e..fc56ded00 100644 --- a/src/module-elasticsuite-core/Model/Search/Request/RelevanceConfig/Reader/ContainerStore.php +++ b/src/module-elasticsuite-core/Model/Search/Request/RelevanceConfig/Reader/ContainerStore.php @@ -48,6 +48,11 @@ class ContainerStore */ protected $containerReader; + /** + * @var StoreManagerInterface + */ + protected $storeManager; + /** * Constructor * diff --git a/src/module-elasticsuite-core/Plugin/Deprecation/Client/ClientPlugin.php b/src/module-elasticsuite-core/Plugin/Deprecation/Client/ClientPlugin.php index df1ba166c..fb911d04b 100644 --- a/src/module-elasticsuite-core/Plugin/Deprecation/Client/ClientPlugin.php +++ b/src/module-elasticsuite-core/Plugin/Deprecation/Client/ClientPlugin.php @@ -71,7 +71,7 @@ public function __construct( * @param string $indexName Index Name * @param array $mapping Mapping as array * - * @return mixed + * @return void */ public function aroundPutMapping( ClientInterface $client, diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Mapper.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Mapper.php index 89fe8d119..b39e3ed41 100644 --- a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Mapper.php +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Mapper.php @@ -95,10 +95,13 @@ public function buildSearchRequest(RequestInterface $request) $searchRequest['track_total_hits'] = $request->getTrackTotalHits(); + if ((int) $request->getMinScore() > 0) { + $searchRequest['min_score'] = $request->getMinScore(); + } + return $searchRequest; } - /** * Extract and build the root query of the search request. * diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/MoreLikeThis.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/MoreLikeThis.php index da88a5aea..0d2dbbb3f 100644 --- a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/MoreLikeThis.php +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/MoreLikeThis.php @@ -45,6 +45,8 @@ public function buildQuery(QueryInterface $query) 'min_doc_freq' => $query->getMinDocFreq(), 'max_doc_freq' => $query->getMaxDocFreq(), 'max_query_terms' => $query->getMaxQueryTerms(), + 'min_word_length' => $query->getMinWordLength(), + 'max_word_length' => $query->getMaxWordLength(), 'include' => $query->includeOriginalDocs(), ]; diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Prefix.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Prefix.php new file mode 100644 index 000000000..e73331bbd --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Prefix.php @@ -0,0 +1,51 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; + +/** + * Build an ES prefix query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class Prefix implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== QueryInterface::TYPE_PREFIX) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + $searchQueryParams = [ + 'value' => $query->getValue(), + 'boost' => $query->getBoost(), + ]; + + $searchQuery = ['prefix' => [$query->getField() => $searchQueryParams]]; + + if ($query->getName()) { + $searchQuery['prefix']['_name'] = $query->getName(); + } + + return $searchQuery; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanContaining.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanContaining.php new file mode 100644 index 000000000..2f6d1d31c --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanContaining.php @@ -0,0 +1,48 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_containing query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanContaining extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_CONTAINING) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + return [ + 'span_containing' => [ + 'boost' => $query->getBoost(), + 'little' => $this->parentBuilder->buildQuery($query->getLittle()), + 'big' => $this->parentBuilder->buildQuery($query->getBig()), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanFieldMasking.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanFieldMasking.php new file mode 100644 index 000000000..ce940df4e --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanFieldMasking.php @@ -0,0 +1,48 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_field_masking query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanFieldMasking extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_FIELD_MASKING) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + return [ + 'span_field_masking' => [ + 'boost' => $query->getBoost(), + 'query' => $this->parentBuilder->buildQuery($query->getQuery()), + 'field' => $query->getField(), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanFirst.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanFirst.php new file mode 100644 index 000000000..7aab83f8b --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanFirst.php @@ -0,0 +1,48 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_first query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanFirst extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_FIRST) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + return [ + 'span_first' => [ + 'boost' => $query->getBoost(), + 'match' => $this->parentBuilder->buildQuery($query->getMatch()), + 'end' => $query->getEnd(), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanMultiTerm.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanMultiTerm.php new file mode 100644 index 000000000..4a3645cc2 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanMultiTerm.php @@ -0,0 +1,47 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_multi query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanMultiTerm extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_MULTI_TERM) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + return [ + 'span_multi' => [ + 'boost' => $query->getBoost(), + 'match' => $this->parentBuilder->buildQuery($query->getMatch()), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanNear.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanNear.php new file mode 100644 index 000000000..7672f0af5 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanNear.php @@ -0,0 +1,51 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_near query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanNear extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_NEAR) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + $clauses = array_map([$this->parentBuilder, 'buildQuery'], $query->getClauses()); + + return [ + 'span_near' => [ + 'boost' => $query->getBoost(), + 'clauses' => array_filter($clauses), + 'slop' => $query->getSlop(), + 'in_order' => $query->getInOrder(), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanNot.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanNot.php new file mode 100644 index 000000000..dbaf21ff1 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanNot.php @@ -0,0 +1,48 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_not query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanNot extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_NOT) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + return [ + 'span_not' => [ + 'boost' => $query->getBoost(), + 'include' => $this->parentBuilder->buildQuery($query->getInclude()), + 'exclude' => $this->parentBuilder->buildQuery($query->getExclude()), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanOr.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanOr.php new file mode 100644 index 000000000..bd6b57c25 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanOr.php @@ -0,0 +1,49 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_or query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanOr extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_OR) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + $clauses = array_map([$this->parentBuilder, 'buildQuery'], $query->getClauses()); + + return [ + 'span_or' => [ + 'boost' => $query->getBoost(), + 'clauses' => array_filter($clauses), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanTerm.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanTerm.php new file mode 100644 index 000000000..fa92677b3 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanTerm.php @@ -0,0 +1,52 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_term query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanTerm implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_TERM) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + $searchQueryParams = [ + 'value' => $query->getValue(), + 'boost' => $query->getBoost(), + ]; + + $searchQuery = ['span_term' => [$query->getField() => $searchQueryParams]]; + + if ($query->getName()) { + $searchQuery['span_term']['_name'] = $query->getName(); + } + + return $searchQuery; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanWithin.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanWithin.php new file mode 100644 index 000000000..a235f5d95 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Span/SpanWithin.php @@ -0,0 +1,48 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\AbstractComplexBuilder; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Build an ES span_within query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanWithin extends AbstractComplexBuilder implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + if ($query->getType() !== SpanQueryInterface::TYPE_SPAN_WITHIN) { + throw new \InvalidArgumentException("Query builder : invalid query type {$query->getType()}"); + } + + return [ + 'span_within' => [ + 'boost' => $query->getBoost(), + 'little' => $this->parentBuilder->buildQuery($query->getLittle()), + 'big' => $this->parentBuilder->buildQuery($query->getBig()), + ], + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/DocumentFactory.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/DocumentFactory.php index 020201528..5e19c6da0 100644 --- a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/DocumentFactory.php +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/DocumentFactory.php @@ -32,6 +32,11 @@ class DocumentFactory */ private $entityMetadata; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * @var string */ diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Spellchecker.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Spellchecker.php index d432ce9b4..1e4302278 100644 --- a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Spellchecker.php +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Spellchecker.php @@ -20,6 +20,7 @@ use Smile\ElasticsuiteCore\Api\Index\MappingInterface; use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; use Smile\ElasticsuiteCore\Helper\Cache as CacheHelper; +use Smile\ElasticsuiteCore\Search\Request\RelevanceConfig\App\Config\ScopePool; /** * Spellchecker Elasticsearch implementation. @@ -41,6 +42,11 @@ class Spellchecker implements SpellcheckerInterface */ private $cacheHelper; + /** + * @var array + */ + private $indexStatsCache = []; + /** * Constructor. * @@ -64,7 +70,7 @@ public function getSpellingType(RequestInterface $request) if ($spellingType === false) { $spellingType = $this->loadSpellingType($request); - $this->cacheHelper->saveCache($cacheKey, $spellingType, [$request->getIndex()]); + $this->cacheHelper->saveCache($cacheKey, $spellingType, [$request->getIndex(), ScopePool::CACHE_TAG]); } return $spellingType; @@ -84,7 +90,7 @@ private function loadSpellingType(RequestInterface $request) try { $cutoffFrequencyLimit = $this->getCutoffrequencyLimit($request); $termVectors = $this->getTermVectors($request); - $queryTermStats = $this->parseTermVectors($termVectors, $cutoffFrequencyLimit); + $queryTermStats = $this->parseTermVectors($termVectors, $cutoffFrequencyLimit, $request->isUsingAllTokens()); if ($queryTermStats['total'] == $queryTermStats['stop']) { $spellingType = self::SPELLING_TYPE_PURE_STOPWORDS; @@ -124,7 +130,7 @@ private function getCacheKey(RequestInterface $request) */ private function getCutoffrequencyLimit(RequestInterface $request) { - $indexStatsResponse = $this->client->indexStats($request->getIndex()); + $indexStatsResponse = $this->getIndexStats($request->getIndex()); $indexStats = current($indexStatsResponse['indices']); $totalIndexedDocs = $indexStats['total']['docs']['count']; @@ -140,7 +146,7 @@ private function getCutoffrequencyLimit(RequestInterface $request) */ private function getTermVectors(RequestInterface $request) { - $stats = $this->client->indexStats($request->getIndex()); + $stats = $this->getIndexStats($request->getIndex()); // Get number of shards. $shards = (int) ($stats['_shards']['successful'] ?? 1); @@ -159,6 +165,16 @@ private function getTermVectors(RequestInterface $request) ], ]; + if ($request->isUsingReference()) { + $doc['fields'][] = MappingInterface::DEFAULT_REFERENCE_FIELD . "." . FieldInterface::ANALYZER_REFERENCE; + $doc['doc'][MappingInterface::DEFAULT_REFERENCE_FIELD] = $request->getQueryText(); + } + + if ($request->isUsingEdgeNgram()) { + $doc['fields'][] = MappingInterface::DEFAULT_EDGE_NGRAM_FIELD . "." . FieldInterface::ANALYZER_EDGE_NGRAM; + $doc['doc'][MappingInterface::DEFAULT_EDGE_NGRAM_FIELD] = $request->getQueryText(); + } + $docs = []; // Compute the mtermvector query on all shards to ensure exhaustive results. @@ -181,15 +197,18 @@ private function getTermVectors(RequestInterface $request) * - missing : number of terms of the query not found into the index * - standard : number of terms of the query found using the standard analyzer. * - * @param array $termVectors The term vector query response. - * @param int $cutoffFrequencyLimit Cutoff freq (max absolute number of docs to consider term as a stopword). + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * + * @param array $termVectors The term vector query response. + * @param int $cutoffFrequencyLimit Cutoff freq (max absolute number of docs to consider term as a stopword). + * @param boolean $useAllTokens Whether to use all tokens or not * * @return array */ - private function parseTermVectors($termVectors, $cutoffFrequencyLimit) + private function parseTermVectors($termVectors, $cutoffFrequencyLimit, $useAllTokens = false) { $queryTermStats = ['stop' => 0, 'exact' => 0, 'standard' => 0, 'missing' => 0]; - $statByPosition = $this->extractTermStatsByPosition($termVectors); + $statByPosition = $this->extractTermStatsByPosition($termVectors, $useAllTokens); foreach ($statByPosition as $positionStat) { $type = 'missing'; @@ -199,6 +218,10 @@ private function parseTermVectors($termVectors, $cutoffFrequencyLimit) $type = 'stop'; } elseif (in_array(FieldInterface::ANALYZER_WHITESPACE, $positionStat['analyzers'])) { $type = 'exact'; + } elseif (in_array(FieldInterface::ANALYZER_REFERENCE, $positionStat['analyzers'])) { + $type = 'exact'; + } elseif (in_array(FieldInterface::ANALYZER_EDGE_NGRAM, $positionStat['analyzers'])) { + $type = 'exact'; } } $queryTermStats[$type]++; @@ -211,18 +234,25 @@ private function parseTermVectors($termVectors, $cutoffFrequencyLimit) /** * Extract term stats by position from a term vectors query response. - * Wil return an array of doc_freq, analayzers and term by position. + * Will return an array of doc_freq, analyzers and term by position. * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * - * @param array $termVectors The term vector query response. + * @param array $termVectors The term vector query response. + * @param boolean $useAllTokens Whether to use all tokens returned in the term vector response. * * @return array */ - private function extractTermStatsByPosition($termVectors) + private function extractTermStatsByPosition($termVectors, $useAllTokens = false) { $statByPosition = []; - $analyzers = [FieldInterface::ANALYZER_STANDARD, FieldInterface::ANALYZER_WHITESPACE]; + $analyzers = [ + FieldInterface::ANALYZER_STANDARD, + FieldInterface::ANALYZER_WHITESPACE, + FieldInterface::ANALYZER_REFERENCE, + FieldInterface::ANALYZER_EDGE_NGRAM, + ]; if (is_array($termVectors) && isset($termVectors['docs'])) { foreach ($termVectors['docs'] as $termVector) { @@ -232,6 +262,9 @@ private function extractTermStatsByPosition($termVectors) foreach ($fieldData['terms'] as $term => $termStats) { foreach ($termStats['tokens'] as $token) { $positionKey = $token['position']; + if ($useAllTokens) { + $positionKey = "{$token['position']}_{$token['start_offset']}_{$token['end_offset']}"; + } if (!isset($termStats['doc_freq'])) { $termStats['doc_freq'] = 0; @@ -261,7 +294,7 @@ private function extractTermStatsByPosition($termVectors) } /** - * Extract analayser from a mapping property name. + * Extract analyser from a mapping property name. * * @param string $propertyName Property name (eg. : search.whitespace) * @@ -278,4 +311,20 @@ private function getAnalyzer($propertyName) return $analyzer; } + + /** + * Get index stats. + * + * @param string $indexName The index name + * + * @return array + */ + private function getIndexStats(string $indexName): array + { + if (!isset($this->indexStatsCache[$indexName])) { + $this->indexStatsCache[$indexName] = $this->client->indexStats(['index' => $indexName]); + } + + return $this->indexStatsCache[$indexName]; + } } diff --git a/src/module-elasticsuite-core/Search/Request.php b/src/module-elasticsuite-core/Search/Request.php index 5febf8528..76207a5c2 100644 --- a/src/module-elasticsuite-core/Search/Request.php +++ b/src/module-elasticsuite-core/Search/Request.php @@ -14,11 +14,11 @@ namespace Smile\ElasticsuiteCore\Search; -use Smile\ElasticsuiteCore\Search\Request\SortOrderInterface; -use Magento\Framework\Search\Request\QueryInterface; -use Smile\ElasticsuiteCore\Search\Request\BucketInterface; use Magento\Framework\Search\Request\Dimension; +use Magento\Framework\Search\Request\QueryInterface; use Smile\ElasticsuiteCore\Api\Search\SpellcheckerInterface; +use Smile\ElasticsuiteCore\Search\Request\BucketInterface; +use Smile\ElasticsuiteCore\Search\Request\SortOrderInterface; /** * Default implementation of ElasticSuite search request. @@ -49,6 +49,11 @@ class Request extends \Magento\Framework\Search\Request implements RequestInterf */ private $trackTotalHits = \Smile\ElasticsuiteCore\Helper\IndexSettings::PER_SHARD_MAX_RESULT_WINDOW; + /** + * @var boolean|integer + */ + private $minScore; + /** * Constructor. * @@ -65,6 +70,7 @@ class Request extends \Magento\Framework\Search\Request implements RequestInterf * @param BucketInterface[] $buckets Search request aggregations definition. * @param string $spellingType For fulltext query : the type of spellchecked applied. * @param bool|int $trackTotalHits Value of the 'track_total_hits' ES parameter. + * @param bool|int $minScore Value of the 'min_score' ES parameter. */ public function __construct( $name, @@ -77,7 +83,8 @@ public function __construct( array $dimensions = [], array $buckets = [], $spellingType = null, - $trackTotalHits = null + $trackTotalHits = null, + $minScore = null ) { parent::__construct($name, $indexName, $query, $from, $size, $dimensions, $buckets); $this->filter = $filter; @@ -90,6 +97,10 @@ public function __construct( if ($trackTotalHits !== null) { $this->trackTotalHits = $this->parseTrackTotalHits($trackTotalHits); } + + if ($minScore !== null) { + $this->minScore = $minScore; + } } /** @@ -116,6 +127,14 @@ public function getTrackTotalHits() return $this->trackTotalHits; } + /** + * {@inheritDoc} + */ + public function getMinScore() + { + return $this->minScore; + } + /** * {@inheritDoc} */ diff --git a/src/module-elasticsuite-core/Search/Request/Aggregation/Bucket/Histogram.php b/src/module-elasticsuite-core/Search/Request/Aggregation/Bucket/Histogram.php index 687951b27..c81895521 100644 --- a/src/module-elasticsuite-core/Search/Request/Aggregation/Bucket/Histogram.php +++ b/src/module-elasticsuite-core/Search/Request/Aggregation/Bucket/Histogram.php @@ -38,6 +38,11 @@ class Histogram extends AbstractBucket */ private $minDocCount; + /** + * @var array + */ + private $extendedBounds; + /** * Constructor. * diff --git a/src/module-elasticsuite-core/Search/Request/Builder.php b/src/module-elasticsuite-core/Search/Request/Builder.php index cd05f3f53..b77ee241d 100644 --- a/src/module-elasticsuite-core/Search/Request/Builder.php +++ b/src/module-elasticsuite-core/Search/Request/Builder.php @@ -169,6 +169,11 @@ public function create( 'trackTotalHits' => $containerConfig->getTrackTotalHits(), ]; + // Use min_score only for fulltext queries. + if ($query !== null) { + $requestParams['minScore'] = $containerConfig->getRelevanceConfig()->getMinScore(); + } + if (!empty($facetFilters)) { $requestParams['filter'] = $this->queryBuilder->createFilterQuery($containerConfig, $facetFilters); } @@ -223,6 +228,9 @@ private function getSpellingType(ContainerConfigurationInterface $containerConfi 'index' => $containerConfig->getIndexName(), 'queryText' => $queryText, 'cutoffFrequency' => $containerConfig->getRelevanceConfig()->getCutOffFrequency(), + 'isUsingAllTokens' => $containerConfig->getRelevanceConfig()->isUsingAllTokens(), + 'isUsingReference' => $containerConfig->getRelevanceConfig()->isUsingReferenceAnalyzer(), + 'isUsingEdgeNgram' => $containerConfig->getRelevanceConfig()->isUsingEdgeNgramAnalyzer(), ]; $spellcheckRequest = $this->spellcheckRequestFactory->create($spellcheckRequestParams); diff --git a/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/BaseConfig.php b/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/BaseConfig.php index 92ae9c0f9..a54e42874 100644 --- a/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/BaseConfig.php +++ b/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/BaseConfig.php @@ -105,6 +105,8 @@ private function addFilters() $this->_data[$requestName]['filters'] = $filters; } } + + return $this; } /** @@ -125,6 +127,8 @@ private function addAggregationProviders() $this->_data[$requestName]['aggregationsProviders'] = $providers; } } + + return $this; } /** @@ -145,6 +149,8 @@ private function addAggregationFilters() $this->_data[$requestName]['aggregations'] = $aggregations; } } + + return $this; } /** diff --git a/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig.php b/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig.php index 6cc402cfd..59d416deb 100644 --- a/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig.php +++ b/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig.php @@ -19,6 +19,8 @@ /** * Relevance Configuration object * + * @SuppressWarnings(PHPMD.TooManyFields) + * * @category Smile * @package Smile\ElasticsuiteCore * @author Romain Ruaud @@ -55,18 +57,96 @@ class RelevanceConfig implements RelevanceConfigurationInterface */ private $enablePhoneticSearch; + /** + * @var integer|null + */ + private $spanMatchBoost; + + /** + * @var integer|null + */ + private $spanSize; + + /** + * @var integer|null + */ + private $minScore; + + /** + * @var boolean + */ + private $useReferenceInExactMatchFilter; + + /** + * @var boolean + */ + private $useDefaultAnalyzerInExactMatchFilter; + + /** + * @var boolean + */ + private $useAllTokens; + + /** + * @var boolean + */ + private $useReferenceAnalyzer; + + /** + * @var boolean + */ + private $useEdgeNgramAnalyzer; + + /** + * @var boolean + */ + private $exactMatchSingleTermBoostsCustomized; + + /** + * @var integer|null + */ + private $exactMatchSingleTermPhraseMatchBoost; + + /** + * @var integer|null + */ + private $exactMatchSingleTermSortableBoost; + /** * RelevanceConfiguration constructor. * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) * - * @param string $minimumShouldMatch Minimum should match clause of the text query. - * @param float $tieBreaker Tie breaker for multimatch queries. - * @param int|null $phraseMatchBoost The Phrase match boost value, or null if not - * enabled - * @param float $cutOffFrequency The cutoff Frequency value - * @param FuzzinessConfigurationInterface|null $fuzziness The fuzziness Configuration, or null - * @param boolean $enablePhoneticSearch The phonetic Configuration, or null + * @param string $minimumShouldMatch Minimum should match clause of the text query. + * @param float $tieBreaker Tie breaker for multimatch queries. + * @param int|null $phraseMatchBoost The Phrase match boost value, or null if not + * enabled + * @param float $cutOffFrequency The cutoff Frequency value + * @param FuzzinessConfigurationInterface|null $fuzziness The fuzziness Configuration, or null + * @param boolean $enablePhoneticSearch The phonetic Configuration, or null + * @param int|null $spanMatchBoost The Span match boost value, or null if not + * enabled + * @param int|null $spanSize The number of terms to match in span queries + * @param int|null $minScore The Min Score value, or null if not enabled + * @param boolean $useReferenceInExactMatchFilter Whether to use the reference collector field + * instead of 'sku' field in the exact match filter + * @param boolean $useDefaultAnalyzerInExactMatchFilter Whether to use 'field' or 'field.default_analyzer' + * in the exact match filter query + * @param boolean $useAllTokens Whether to take into account all term vector tokens + * @param boolean $useReferenceAnalyzer Whether to include the collector field associated + * with the reference analyzer in term vectors request + * @param boolean $useEdgeNgramAnalyzer Whether to include the collector field associated + * with the edge ngram analyzer(s) in the term vectors + * request + * @param boolean $exactMatchSingleTermBoostsCustomized Are the exact match boost values on whitespace + * and sortable fields customized. + * @param int|null $exactMatchSingleTermPhraseMatchBoost The whitespace boost value for exact match, + * or null if the default (phrase match boost value) + * should apply + * @param int|null $exactMatchSingleTermSortableBoost The sortable boost value for exact match, + * or null if the default (twice the phrase match + * boost value) should apply */ public function __construct( $minimumShouldMatch, @@ -74,7 +154,18 @@ public function __construct( $phraseMatchBoost, $cutOffFrequency, FuzzinessConfigurationInterface $fuzziness = null, - $enablePhoneticSearch = false + $enablePhoneticSearch = false, + $spanMatchBoost = null, + $spanSize = null, + $minScore = null, + $useReferenceInExactMatchFilter = false, + $useDefaultAnalyzerInExactMatchFilter = false, + $useAllTokens = false, + $useReferenceAnalyzer = false, + $useEdgeNgramAnalyzer = false, + $exactMatchSingleTermBoostsCustomized = false, + $exactMatchSingleTermPhraseMatchBoost = null, + $exactMatchSingleTermSortableBoost = null ) { $this->minimumShouldMatch = $minimumShouldMatch; $this->tieBreaker = $tieBreaker; @@ -82,6 +173,17 @@ public function __construct( $this->cutOffFrequency = $cutOffFrequency; $this->fuzzinessConfiguration = $fuzziness; $this->enablePhoneticSearch = $enablePhoneticSearch; + $this->spanMatchBoost = $spanMatchBoost; + $this->spanSize = $spanSize; + $this->minScore = $minScore; + $this->useReferenceInExactMatchFilter = $useReferenceInExactMatchFilter; + $this->useAllTokens = $useAllTokens; + $this->useReferenceAnalyzer = $useReferenceAnalyzer; + $this->useEdgeNgramAnalyzer = $useEdgeNgramAnalyzer; + $this->useDefaultAnalyzerInExactMatchFilter = $useDefaultAnalyzerInExactMatchFilter; + $this->exactMatchSingleTermBoostsCustomized = $exactMatchSingleTermBoostsCustomized; + $this->exactMatchSingleTermPhraseMatchBoost = $exactMatchSingleTermPhraseMatchBoost; + $this->exactMatchSingleTermSortableBoost = $exactMatchSingleTermSortableBoost; } /** @@ -145,4 +247,92 @@ public function isPhoneticSearchEnabled() { return (bool) $this->enablePhoneticSearch; } + + /** + * {@inheritDoc} + */ + public function getSpanMatchBoost() + { + return (int) $this->spanMatchBoost; + } + + /** + * {@inheritDoc} + */ + public function getSpanSize() + { + return (int) $this->spanSize; + } + + /** + * {@inheritDoc} + */ + public function getMinScore() + { + return (int) $this->minScore; + } + + /** + * {@inheritDoc} + */ + public function isUsingReferenceInExactMatchFilter() + { + return (bool) $this->useReferenceInExactMatchFilter; + } + + /** + * {@inheritDoc} + */ + public function isUsingDefaultAnalyzerInExactMatchFilter() + { + return (bool) $this->useDefaultAnalyzerInExactMatchFilter; + } + + /** + * {@inheritDoc} + */ + public function isUsingAllTokens() + { + return (bool) $this->useAllTokens; + } + + /** + * {@inheritDoc} + */ + public function isUsingReferenceAnalyzer() + { + return (bool) $this->useReferenceAnalyzer; + } + + /** + * {@inheritDoc} + */ + public function isUsingEdgeNgramAnalyzer() + { + return (bool) $this->useEdgeNgramAnalyzer; + } + + /** + * {@inheritDoc} + */ + public function areExactMatchSingleTermBoostsCustomized() + { + return (bool) $this->exactMatchSingleTermBoostsCustomized; + } + + /** + * {@inheritDoc} + */ + public function getExactMatchSingleTermPhraseMatchBoost() + { + return (int) $this->exactMatchSingleTermPhraseMatchBoost; + } + + /** + * {@inheritDoc} + */ + public function getExactMatchSingleTermSortableBoost() + { + return (int) $this->exactMatchSingleTermSortableBoost; + } } diff --git a/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig/Factory.php b/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig/Factory.php index 73db7a6b0..0f7a7d058 100644 --- a/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig/Factory.php +++ b/src/module-elasticsuite-core/Search/Request/ContainerConfiguration/RelevanceConfig/Factory.php @@ -63,6 +63,36 @@ class Factory */ const PHONETIC_CONFIG_XML_PATH = 'spellchecking/phonetic/enable'; + /** + * XML node for span match configuration + */ + const SPAN_MATCH_CONFIG_XML_PREFIX = 'span_match_configuration'; + + /** + * XML node for min_score configuration + */ + const MIN_SCORE_CONFIG_XML_PREFIX = 'min_score_configuration'; + + /** + * XML node for exact match configuration + */ + const EXACT_MATCH_CONFIG_XML_PREFIX = 'exact_match_configuration'; + + /** + * XML node for tokens usage in term vectors configuration. + */ + const TERM_VECTORS_TOKENS_CONFIG_XML_PATH = 'spellchecking/term_vectors/use_all_tokens'; + + /** + * XML node for reference analyzer usage in term vectors configuration. + */ + const TERM_VECTORS_USE_REFERENCE_CONFIG_XML_PATH = 'spellchecking/term_vectors/use_reference_analyzer'; + + /** + * XML node for edge ngram analyzer(s) usage in term vectors configuration. + */ + const TERM_VECTORS_USE_EDGE_NGRAM_CONFIG_XML_PATH = 'spellchecking/term_vectors/use_edge_ngram_analyzer'; + /** * @var RelevanceConfigurationInterface[] */ @@ -134,6 +164,17 @@ protected function loadConfiguration($scopeCode) 'cutOffFrequency' => $this->getCutoffFrequencyConfiguration($scopeCode), 'fuzziness' => $this->getFuzzinessConfiguration($scopeCode), 'enablePhoneticSearch' => $this->isPhoneticSearchEnabled($scopeCode), + 'spanMatchBoost' => $this->getSpanMatchBoostConfiguration($scopeCode), + 'spanSize' => $this->getSpanSize($scopeCode), + 'minScore' => $this->getMinScoreConfiguration($scopeCode), + 'useReferenceInExactMatchFilter' => $this->isUsingReferenceInExactMatchFilter($scopeCode), + 'useAllTokens' => $this->isUsingAllTokensConfiguration($scopeCode), + 'useReferenceAnalyzer' => $this->isUsingReferenceAnalyzerConfiguration($scopeCode), + 'useEdgeNgramAnalyzer' => $this->isUsingEdgeNgramAnalyzerConfiguration($scopeCode), + 'useDefaultAnalyzerInExactMatchFilter' => $this->isUsingDefaultAnalyzerInExactMatchFilter($scopeCode), + 'exactMatchSingleTermBoostsCustomized' => $this->areExactMatchCustomBoostValuesEnabled($scopeCode), + 'exactMatchSingleTermPhraseMatchBoost' => $this->getExactMatchSingleTermPhraseMatchBoostConfiguration($scopeCode), + 'exactMatchSingleTermSortableBoost' => $this->getExactMatchSortableBoostConfiguration($scopeCode), ]; return $configurationParams; @@ -281,4 +322,171 @@ private function getScopeCode($storeId, $containerName) { return sprintf("%s|%s", $containerName, $storeId); } + + /** + * Retrieve span boost configuration for a container. + * + * @param string $scopeCode The scope code + * + * @return bool|int + */ + private function getSpanMatchBoostConfiguration($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::SPAN_MATCH_CONFIG_XML_PREFIX; + + $boost = (bool) $this->getConfigValue($path . "/enable_span_match", $scopeCode); + + if ($boost === true) { + $boost = (int) $this->getConfigValue($path . "/span_match_boost_value", $scopeCode); + } + + return $boost; + } + + /** + * Retrieve span boost size configuration for a container. + * + * @param string $scopeCode The scope code + * + * @return bool|int + */ + private function getSpanSize($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::SPAN_MATCH_CONFIG_XML_PREFIX; + + $size = (bool) $this->getConfigValue($path . "/enable_span_match", $scopeCode); + + if ($size === true) { + $size = (int) $this->getConfigValue($path . "/span_size", $scopeCode); + } + + return $size; + } + + /** + * Retrieve min_score configuration for a container. + * + * @param string $scopeCode The scope code + * + * @return bool|int + */ + private function getMinScoreConfiguration($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::MIN_SCORE_CONFIG_XML_PREFIX; + + $minScore = (bool) $this->getConfigValue($path . "/enable_use_min_score", $scopeCode); + + if ($minScore === true) { + $minScore = (int) $this->getConfigValue($path . "/min_score_value", $scopeCode); + } + + return $minScore; + } + + /** + * Retrieve reference collector field usage configuration for a container. + * + * @param @param string $scopeCode The scope code + * + * @return bool + */ + private function isUsingReferenceInExactMatchFilter($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::EXACT_MATCH_CONFIG_XML_PREFIX; + + return (bool) $this->getConfigValue($path . "/use_reference_in_filter", $scopeCode); + } + + /** + * Retrieve term vectors extensive tokens usage configuration for a container. + * + * @param string $scopeCode The scope code + * + * @return bool + */ + private function isUsingAllTokensConfiguration($scopeCode) + { + return (bool) $this->getConfigValue(self::TERM_VECTORS_TOKENS_CONFIG_XML_PATH, $scopeCode); + } + + /** + * Retrieve term vectors reference analyzer usage configuration for a container. + * + * @param string $scopeCode The scope code + * + * @return bool + */ + private function isUsingReferenceAnalyzerConfiguration($scopeCode) + { + return (bool) $this->getConfigValue(self::TERM_VECTORS_USE_REFERENCE_CONFIG_XML_PATH, $scopeCode); + } + + /** + * Retrieve term vectors edge ngram analyzer usage configuration for a container. + * + * @param string $scopeCode The scope code + * + * @return bool + */ + private function isUsingEdgeNgramAnalyzerConfiguration($scopeCode) + { + return (bool) $this->getConfigValue(self::TERM_VECTORS_USE_EDGE_NGRAM_CONFIG_XML_PATH, $scopeCode); + } + + /** + * Check if we should use the default analyzer of each field when building the exact match filter query. + * + * @param string $scopeCode The scope code + * + * @return bool + */ + private function isUsingDefaultAnalyzerInExactMatchFilter($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::EXACT_MATCH_CONFIG_XML_PREFIX; + + return (bool) $this->getConfigValue($path . "/use_default_analyzer", $scopeCode); + } + + /** + * Check if custom boost values for exact match in whitespace and sortable version of fields + * should be applied. + * + * @param string $scopeCode The scope code + * + * @return bool + */ + private function areExactMatchCustomBoostValuesEnabled($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::EXACT_MATCH_CONFIG_XML_PREFIX; + + return (bool) $this->getConfigValue($path . "/enable_single_term_custom_boost_values", $scopeCode); + } + + /** + * Return the configured custom boost value for whitespace fields in exact match queries. + * + * @param string $scopeCode The scope code + * + * @return int + */ + private function getExactMatchSingleTermPhraseMatchBoostConfiguration($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::EXACT_MATCH_CONFIG_XML_PREFIX; + + return (int) $this->getConfigValue($path . "/single_term_phrase_match_boost_value", $scopeCode); + } + + /** + * Return the configured custom boost value for sortable fields in exact match queries. + * + * @param string $scopeCode The scope code + * + * @return int + */ + private function getExactMatchSortableBoostConfiguration($scopeCode) + { + $path = self::BASE_RELEVANCE_CONFIG_XML_PREFIX . "/" . self::EXACT_MATCH_CONFIG_XML_PREFIX; + + return (int) $this->getConfigValue($path . "/sortable_boost_value", $scopeCode); + } } diff --git a/src/module-elasticsuite-core/Search/Request/Query/Fulltext/NonStandardFuzzyFieldFilter.php b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/NonStandardFuzzyFieldFilter.php new file mode 100644 index 000000000..64f2410d9 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/NonStandardFuzzyFieldFilter.php @@ -0,0 +1,35 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query\Fulltext; + +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; + +/** + * Indicates if a field is used in fuzzy search with a non-default analyzer. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class NonStandardFuzzyFieldFilter extends FuzzyFieldFilter +{ + /** + * {@inheritDoc} + */ + public function filterField(FieldInterface $field) + { + return parent::filterField($field) && ($field->getDefaultSearchAnalyzer() !== FieldInterface::ANALYZER_STANDARD); + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Fulltext/NonStandardSearchableFieldFilter.php b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/NonStandardSearchableFieldFilter.php new file mode 100644 index 000000000..ed1b0df13 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/NonStandardSearchableFieldFilter.php @@ -0,0 +1,35 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query\Fulltext; + +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; + +/** + * Indicates if a field is used in search with a non-default analyzer. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class NonStandardSearchableFieldFilter extends SearchableFieldFilter +{ + /** + * {@inheritDoc} + */ + public function filterField(FieldInterface $field) + { + return parent::filterField($field) && ($field->getDefaultSearchAnalyzer() !== FieldInterface::ANALYZER_STANDARD); + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php index 37eb3c247..fe31ca220 100644 --- a/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php +++ b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php @@ -14,6 +14,8 @@ namespace Smile\ElasticsuiteCore\Search\Request\Query\Fulltext; +use Smile\ElasticsuiteCore\Model\Search\Request\RelevanceConfig\Reader\Container; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; use Smile\ElasticsuiteCore\Search\Request\QueryInterface; use Smile\ElasticsuiteCore\Api\Index\MappingInterface; use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; @@ -89,6 +91,19 @@ public function create(ContainerConfigurationInterface $containerConfig, $queryT 'boost' => $boost, ]; $query = $this->queryFactory->create(QueryInterface::TYPE_FILTER, $queryParams); + + $relevanceConfig = $containerConfig->getRelevanceConfig(); + if ($relevanceConfig->getSpanMatchBoost()) { + $spanQuery = $this->getSpanQuery($containerConfig, $queryText, $relevanceConfig->getSpanMatchBoost()); + if ($spanQuery !== null) { + $queryParams = [ + 'must' => [$query], + 'should' => [$spanQuery], + 'minimumShouldMatch' => 0, + ]; + $query = $this->queryFactory->create(QueryInterface::TYPE_BOOL, $queryParams); + } + } } return $query; @@ -105,9 +120,28 @@ public function create(ContainerConfigurationInterface $containerConfig, $queryT private function getCutoffFrequencyQuery(ContainerConfigurationInterface $containerConfig, $queryText) { $relevanceConfig = $containerConfig->getRelevanceConfig(); + $fields = array_fill_keys([MappingInterface::DEFAULT_SEARCH_FIELD, 'sku'], 1); + + if ($containerConfig->getRelevanceConfig()->isUsingDefaultAnalyzerInExactMatchFilter()) { + $nonStandardSearchableFieldFilter = $this->fieldFilters['nonStandardSearchableFieldFilter']; + + $fields = $fields + $this->getWeightedFields( + $containerConfig, + null, + $nonStandardSearchableFieldFilter, + MappingInterface::DEFAULT_SEARCH_FIELD + ); + } + + if ($containerConfig->getRelevanceConfig()->isUsingReferenceInExactMatchFilter()) { + $fields += array_fill_keys( + [MappingInterface::DEFAULT_SEARCH_FIELD, MappingInterface::DEFAULT_REFERENCE_FIELD . ".reference"], + 1 + ); + } $queryParams = [ - 'fields' => array_fill_keys([MappingInterface::DEFAULT_SEARCH_FIELD, 'sku'], 1), + 'fields' => array_fill_keys(array_keys($fields), 1), 'queryText' => $queryText, 'cutoffFrequency' => $relevanceConfig->getCutOffFrequency(), 'minimumShouldMatch' => $relevanceConfig->getMinimumShouldMatch(), @@ -132,15 +166,19 @@ private function getWeightedSearchQuery(ContainerConfigurationInterface $contain $searchableFieldFilter = $this->fieldFilters['searchableFieldFilter']; $sortableAnalyzer = FieldInterface::ANALYZER_SORTABLE; $phraseAnalyzer = FieldInterface::ANALYZER_WHITESPACE; + $sortableMatchBoost = 2 * $phraseMatchBoost; if (is_string($queryText) && str_word_count($queryText) > 1) { $phraseAnalyzer = FieldInterface::ANALYZER_SHINGLE; + } elseif ($relevanceConfig->areExactMatchSingleTermBoostsCustomized()) { + $phraseMatchBoost = $relevanceConfig->getExactMatchSingleTermPhraseMatchBoost(); + $sortableMatchBoost = $relevanceConfig->getExactMatchSingleTermSortableBoost(); } $searchFields = array_merge( $this->getWeightedFields($containerConfig, null, $searchableFieldFilter, $defaultSearchField), $this->getWeightedFields($containerConfig, $phraseAnalyzer, $searchableFieldFilter, $defaultSearchField, $phraseMatchBoost), - $this->getWeightedFields($containerConfig, $sortableAnalyzer, $searchableFieldFilter, null, 2 * $phraseMatchBoost) + $this->getWeightedFields($containerConfig, $sortableAnalyzer, $searchableFieldFilter, null, $sortableMatchBoost) ); $queryParams = [ @@ -189,7 +227,7 @@ private function getPureStopwordsQuery(ContainerConfigurationInterface $containe } /** - * Spellcheked query building. + * Spellchecked query building. * * @param ContainerConfigurationInterface $containerConfig Search request container configuration. * @param string $queryText The text query. @@ -248,10 +286,15 @@ private function getFuzzyQuery(ContainerConfigurationInterface $containerConfig, } $fuzzyFieldFilter = $this->fieldFilters['fuzzyFieldFilter']; + $nonStandardFuzzyFieldFilter = $this->fieldFilters['nonStandardFuzzyFieldFilter']; $searchFields = array_merge( $this->getWeightedFields($containerConfig, $standardAnalyzer, $fuzzyFieldFilter, $defaultSearchField), - $this->getWeightedFields($containerConfig, $phraseAnalyzer, $fuzzyFieldFilter, $defaultSearchField, $phraseMatchBoost) + $this->getWeightedFields($containerConfig, $phraseAnalyzer, $fuzzyFieldFilter, $defaultSearchField, $phraseMatchBoost), + // Allow fuzzy query to contain fields using for fuzzy search with their default analyzer. + // Same logic as defined in getWeightedSearchQuery(). + // This will automatically include sku.reference and any other fields having defaultSearchAnalyzer. + $this->getWeightedFields($containerConfig, null, $nonStandardFuzzyFieldFilter, $defaultSearchField), ); $queryParams = [ @@ -318,4 +361,68 @@ private function getWeightedFields( return $mapping->getWeightedSearchProperties($analyzer, $defaultField, $boost, $fieldFilter); } + + /** + * Build a span query to raise score of fields beginning by the query text. + * + * @param ContainerConfigurationInterface $containerConfig The container configuration + * @param string $queryText The query text + * @param int $boost The boost applied to the span query + * + * @return \Smile\ElasticsuiteCore\Search\Request\QueryInterface + */ + private function getSpanQuery(ContainerConfigurationInterface $containerConfig, $queryText, $boost) + { + $query = null; + $terms = explode(' ', $queryText); + + $relevanceConfig = $containerConfig->getRelevanceConfig(); + $spanSize = $relevanceConfig->getSpanSize(); + + if ((int) $spanSize === 0) { + return $query; + } + + $terms = array_slice($terms, 0, $spanSize); + $wordCount = count($terms); + $spanFieldsFilter = $this->fieldFilters['spannableFieldFilter']; + $spanFields = $containerConfig->getMapping()->getFields(); + $spanFields = array_filter($spanFields, [$spanFieldsFilter, 'filterField']); + $spanQueryParams = ['boost' => $boost, 'end' => $wordCount]; + $spanQueryType = SpanQueryInterface::TYPE_SPAN_FIRST; + + if (count($spanFields) > 0) { + $queries = []; + foreach ($spanFields as $field) { + $clauses = []; + foreach ($terms as $term) { + $clauses[] = $this->queryFactory->create( + SpanQueryInterface::TYPE_SPAN_TERM, + [ + 'field' => $field->getMappingProperty(FieldInterface::ANALYZER_WHITESPACE) ?? $field->getName(), + 'value' => $term, + ] + ); + } + + $spanQueryParams['match'] = $this->queryFactory->create( + SpanQueryInterface::TYPE_SPAN_NEAR, + [ + 'clauses' => $clauses, + 'slop' => 0, + 'inOrder' => true, + ] + ); + + $queries[] = $this->queryFactory->create($spanQueryType, $spanQueryParams); + } + + $query = current($queries); + if (count($queries) > 1) { + $query = $this->queryFactory->create(QueryInterface::TYPE_BOOL, ['should' => $queries]); + } + } + + return $query; + } } diff --git a/src/module-elasticsuite-core/Search/Request/Query/Fulltext/SpannableFieldFilter.php b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/SpannableFieldFilter.php new file mode 100644 index 000000000..63d396275 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/SpannableFieldFilter.php @@ -0,0 +1,39 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query\Fulltext; + +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldFilterInterface; + +/** + * Indicates if a field can be used for span queries. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpannableFieldFilter implements FieldFilterInterface +{ + /** + * {@inheritDoc} + */ + public function filterField(FieldInterface $field) + { + return $field->getType() == FieldInterface::FIELD_TYPE_TEXT + && $field->isSearchable() + && $field->isSpannable() + && $field->isNested() === false; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/FunctionScore.php b/src/module-elasticsuite-core/Search/Request/Query/FunctionScore.php index 1187e3858..891cc7703 100644 --- a/src/module-elasticsuite-core/Search/Request/Query/FunctionScore.php +++ b/src/module-elasticsuite-core/Search/Request/Query/FunctionScore.php @@ -64,9 +64,9 @@ class FunctionScore implements QueryInterface * Boost mode functions. */ const BOOST_MODE_MULTIPLY = 'multiply'; + const BOOST_MODE_REPLACE = 'replace'; const BOOST_MODE_SUM = 'sum'; const BOOST_MODE_AVG = 'avg'; - const BOOST_MODE_FIRST = 'first'; const BOOST_MODE_MAX = 'max'; const BOOST_MODE_MIN = 'min'; diff --git a/src/module-elasticsuite-core/Search/Request/Query/MoreLikeThis.php b/src/module-elasticsuite-core/Search/Request/Query/MoreLikeThis.php index 3c5449729..812880682 100644 --- a/src/module-elasticsuite-core/Search/Request/Query/MoreLikeThis.php +++ b/src/module-elasticsuite-core/Search/Request/Query/MoreLikeThis.php @@ -53,7 +53,17 @@ class MoreLikeThis implements QueryInterface /** * @var integer */ - const DEFAULT_MAX_DOC_FREQ = 100; + const DEFAULT_MAX_DOC_FREQ = 2147483647; + + /** + * @var integer + */ + const DEFAULT_MIN_WORD_LENGTH = 0; + + /** + * @var integer + */ + const DEFAULT_MAX_WORD_LENGTH = 0; /** * @var string @@ -100,6 +110,16 @@ class MoreLikeThis implements QueryInterface */ private $maxDocFreq; + /** + * @var integer + */ + private $minWordLength; + + /** + * @var integer + */ + private $maxWordLength; + /** * @var integer */ @@ -124,6 +144,8 @@ class MoreLikeThis implements QueryInterface * @param integer $minDocFreq Minimum doc freq for a term to be considered. * @param integer $maxDocFreq Maximum doc freq for a term to be considered. * @param integer $maxQueryTerms Maximum number of term in generated queries. + * @param integer $minWordLength Minimum length of word to consider. + * @param integer $maxWordLength Maximum length of word to consider. * @param integer $includeOriginalDocs Include original doc in the result set. * @param string $name Query name. * @param integer $boost Query boost. @@ -137,6 +159,8 @@ public function __construct( $minDocFreq = self::DEFAULT_MIN_DOC_FREQ, $maxDocFreq = self::DEFAULT_MAX_DOC_FREQ, $maxQueryTerms = self::DEFAULT_MAX_QUERY_TERMS, + $minWordLength = self::DEFAULT_MIN_WORD_LENGTH, + $maxWordLength = self::DEFAULT_MAX_WORD_LENGTH, $includeOriginalDocs = false, $name = null, $boost = QueryInterface::DEFAULT_BOOST_VALUE @@ -152,6 +176,8 @@ public function __construct( $this->name = $name; $this->boost = $boost; $this->includeOriginalDocs = $includeOriginalDocs; + $this->minWordLength = $minWordLength; + $this->maxWordLength = $maxWordLength; } /** @@ -225,7 +251,7 @@ public function getBoostTerms() */ public function getMinTermFreq() { - return $this->minTermFreq; + return (int) $this->minTermFreq; } /** @@ -235,7 +261,7 @@ public function getMinTermFreq() */ public function getMinDocFreq() { - return $this->minDocFreq; + return (int) $this->minDocFreq; } /** @@ -245,7 +271,7 @@ public function getMinDocFreq() */ public function getMaxDocFreq() { - return $this->maxDocFreq; + return (int) $this->maxDocFreq; } /** @@ -255,7 +281,7 @@ public function getMaxDocFreq() */ public function getMaxQueryTerms() { - return $this->maxQueryTerms; + return (int) $this->maxQueryTerms; } /** @@ -267,4 +293,24 @@ public function includeOriginalDocs() { return $this->includeOriginalDocs; } + + /** + * Minimum doc freq for a term to be considered. + * + * @return integer + */ + public function getMinWordLength() + { + return (int) $this->minWordLength; + } + + /** + * Maximum doc freq for a term to be considered. + * + * @return integer + */ + public function getMaxWordLength() + { + return (int) $this->maxWordLength; + } } diff --git a/src/module-elasticsuite-core/Search/Request/Query/Prefix.php b/src/module-elasticsuite-core/Search/Request/Query/Prefix.php new file mode 100644 index 000000000..aaca63010 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Prefix.php @@ -0,0 +1,108 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Prefix query implementation. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class Prefix implements QueryInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * @var string + */ + private $value; + + /** + * @var string + */ + private $field; + + /** + * The prefix query produce an Elasticsearch prefix query. + * + * @param string $value Search value. + * @param string $field Search field. + * @param string $name Name of the query. + * @param integer $boost Query boost. + */ + public function __construct($value, $field, $name = null, $boost = QueryInterface::DEFAULT_BOOST_VALUE) + { + $this->name = $name; + $this->value = $value; + $this->field = $field; + $this->boost = $boost; + } + + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return QueryInterface::TYPE_PREFIX; + } + + /** + * Search value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Search field. + * + * @return string + */ + public function getField() + { + return $this->field; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanContaining.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanContaining.php new file mode 100644 index 000000000..7bc746012 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanContaining.php @@ -0,0 +1,110 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_containing query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-span-containing-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanContaining implements SpanQueryInterface +{ + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $big; + + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $little; + + /** + * @var string|null + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * The SpanContaining query produce an Elasticsearch span_containing query. + * + * @param \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface $big Span Query of the "big" clause + * @param \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface $little Span Query of the "little" clause + * @param string $name Query Name + * @param int $boost Query Boost + */ + public function __construct( + SpanQueryInterface $big, + SpanQueryInterface $little, + string $name = null, + int $boost = QueryInterface::DEFAULT_BOOST_VALUE + ) { + $this->big = $big; + $this->little = $little; + $this->name = $name; + $this->boost = $boost; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getBig(): SpanQueryInterface + { + return $this->big; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getLittle(): SpanQueryInterface + { + return $this->little; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_CONTAINING; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanFieldMasking.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanFieldMasking.php new file mode 100644 index 000000000..f7f36704e --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanFieldMasking.php @@ -0,0 +1,109 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_field_masking query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-field-masking-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanFieldMasking implements SpanQueryInterface +{ + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $query; + + /** + * @var string + */ + private $field; + + /** + * @var string|null + */ + private $name; + + /** + * @var integer|mixed + */ + private $boost; + + /** + * The SpanFieldMasking query produce an Elasticsearch span_field_masking query. + * + * @param \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface $query Span Query + * @param string $field Field + * @param string|null $name Query Name + * @param $boost Boost + */ + public function __construct( + SpanQueryInterface $query, + string $field, + string $name = null, + $boost = QueryInterface::DEFAULT_BOOST_VALUE + ) { + $this->query = $query; + $this->field = $field; + $this->name = $name; + $this->boost = $boost; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getQuery(): SpanQueryInterface + { + return $this->query; + } + + /** + * @return string + */ + public function getField(): string + { + return $this->field; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_FIELD_MASKING; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanFirst.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanFirst.php new file mode 100644 index 000000000..043d50c9e --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanFirst.php @@ -0,0 +1,106 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_first query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-first-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanFirst implements SpanQueryInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * @var integer + */ + private $end; + + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $match; + + /** + * The SpanFirst query produce an Elasticsearch span_first query. + * + * @param SpanQueryInterface $match Another span query to match. + * @param int $end The maximum end position permitted in a match. + * @param string $name Name of the query. + * @param integer $boost Query boost. + */ + public function __construct(SpanQueryInterface $match, int $end, $name = null, $boost = QueryInterface::DEFAULT_BOOST_VALUE) + { + $this->name = $name; + $this->match = $match; + $this->end = $end; + $this->boost = $boost; + } + + /** + * @return int + */ + public function getEnd(): int + { + return $this->end; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getMatch(): SpanQueryInterface + { + return $this->match; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_FIRST; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanMultiTerm.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanMultiTerm.php new file mode 100644 index 000000000..52b12c56f --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanMultiTerm.php @@ -0,0 +1,91 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_multi query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-multi-term-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanMultiTerm implements SpanQueryInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * @var QueryInterface + */ + private $match; + + /** + * The SpanMultiTerm query produce an Elasticsearch span_multi query. + * + * @param QueryInterface $match Query. + * @param string $name Name of the query. + * @param integer $boost Query boost. + */ + public function __construct(QueryInterface $match, $name = null, $boost = QueryInterface::DEFAULT_BOOST_VALUE) + { + $this->match = $match; + $this->name = $name; + $this->boost = $boost; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\QueryInterface + */ + public function getMatch(): QueryInterface + { + return $this->match; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_MULTI_TERM; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanNear.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanNear.php new file mode 100644 index 000000000..654cf1a71 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanNear.php @@ -0,0 +1,130 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_near query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-near-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanNear implements SpanQueryInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * @var array + */ + private $clauses = []; + + /** + * @var integer + */ + private int $slop; + + /** + * @var boolean + */ + private bool $inOrder; + + /** + * The SpanNear query produce an Elasticsearch span_near query. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * + * @param array $clauses Span clauses + * @param int $slop Maximum number of intervening unmatched positions + * @param bool $inOrder Whether matches are required to be in-order + * @param string|null $name Query name + * @param string $boost Query boost + */ + public function __construct( + array $clauses = [], + int $slop = 12, + bool $inOrder = true, + string $name = null, + string $boost = QueryInterface::DEFAULT_BOOST_VALUE + ) { + $this->clauses = $clauses; + $this->slop = $slop; + $this->inOrder = $inOrder; + $this->name = $name; + $this->boost = $boost; + } + + /** + * @return array + */ + public function getClauses(): array + { + return $this->clauses; + } + + /** + * @return int + */ + public function getSlop(): int + { + return $this->slop; + } + + /** + * @SuppressWarnings(PHPMD.BooleanGetMethodName) + * + * @return bool + */ + public function getInOrder(): bool + { + return $this->inOrder; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_NEAR; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanNot.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanNot.php new file mode 100644 index 000000000..bf01b2275 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanNot.php @@ -0,0 +1,109 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_not query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-not-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanNot implements SpanQueryInterface +{ + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $include; + + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $exclude; + + /** + * @var string|null + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * The SpanNot query produce an Elasticsearch span_not query. + * + * @param \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface $include Span Query of the "include" clause + * @param \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface $exclude Span Query of the "exclude" clause + * @param string $name Query Name + * @param int $boost Query Boost + */ + public function __construct( + SpanQueryInterface $include, + SpanQueryInterface $exclude, + string $name = null, + int $boost = QueryInterface::DEFAULT_BOOST_VALUE + ) { + $this->include = $include; + $this->exclude = $exclude; + $this->name = $name; + $this->boost = $boost; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getInclude(): SpanQueryInterface + { + return $this->include; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getExclude(): SpanQueryInterface + { + return $this->exclude; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_NOT; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanOr.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanOr.php new file mode 100644 index 000000000..bb82d56cd --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanOr.php @@ -0,0 +1,93 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_or query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-span-or-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanOr implements SpanQueryInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * @var array + */ + private $clauses = []; + + /** + * The SpanOr query produce an Elasticsearch span_or query. + * + * @param array $clauses Span clauses + * @param string|null $name Query name + * @param string $boost Query boost + */ + public function __construct( + array $clauses = [], + string $name = null, + string $boost = QueryInterface::DEFAULT_BOOST_VALUE + ) { + $this->clauses = $clauses; + $this->name = $name; + $this->boost = $boost; + } + + /** + * @return array + */ + public function getClauses(): array + { + return $this->clauses; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_OR; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanTerm.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanTerm.php new file mode 100644 index 000000000..2fe46cfc2 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanTerm.php @@ -0,0 +1,109 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_term query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-term-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanTerm implements SpanQueryInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * @var string + */ + private $value; + + /** + * @var string + */ + private $field; + + /** + * The SpanTerm query produce an Elasticsearch span_term query. + * + * @param string $value Search value. + * @param string $field Search field. + * @param string $name Name of the query. + * @param integer $boost Query boost. + */ + public function __construct($value, $field, $name = null, $boost = QueryInterface::DEFAULT_BOOST_VALUE) + { + $this->name = $name; + $this->value = $value; + $this->field = $field; + $this->boost = $boost; + } + + /** + * Search value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Search field. + * + * @return string + */ + public function getField() + { + return $this->field; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_TERM; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/Span/SpanWithin.php b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanWithin.php new file mode 100644 index 000000000..3323f8c01 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Span/SpanWithin.php @@ -0,0 +1,109 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCore\Search\Request\Query\Span; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface; + +/** + * ElasticSuite request span_within query. + * + * Documentation : @see https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-span-within-query.html + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Romain Ruaud + */ +class SpanWithin implements SpanQueryInterface +{ + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $big; + + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + private $little; + + /** + * @var string|null + */ + private $name; + + /** + * @var integer + */ + private $boost; + + /** + * The SpanContaining query produce an Elasticsearch span_containing query. + * + * @param \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface $big Span Query of the "big" clause + * @param \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface $little Span Query of the "little" clause + * @param string $name Query Name + * @param int $boost Query Boost + */ + public function __construct( + SpanQueryInterface $big, + SpanQueryInterface $little, + string $name = null, + int $boost = QueryInterface::DEFAULT_BOOST_VALUE + ) { + $this->big = $big; + $this->little = $little; + $this->name = $name; + $this->boost = $boost; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getBig(): SpanQueryInterface + { + return $this->big; + } + + /** + * @return \Smile\ElasticsuiteCore\Search\Request\Query\SpanQueryInterface + */ + public function getLittle(): SpanQueryInterface + { + return $this->little; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return SpanQueryInterface::TYPE_SPAN_WITHIN; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/SpanQueryInterface.php b/src/module-elasticsuite-core/Search/Request/Query/SpanQueryInterface.php new file mode 100644 index 000000000..d6ff07e6c --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/SpanQueryInterface.php @@ -0,0 +1,36 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Define span query types usable in ElasticSuite. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Romain Ruaud + */ +interface SpanQueryInterface extends QueryInterface +{ + const TYPE_SPAN_CONTAINING = 'spanContainingQuery'; + const TYPE_SPAN_FIELD_MASKING = 'spanFieldMaskingQuery'; + const TYPE_SPAN_FIRST = 'spanFirstQuery'; + const TYPE_SPAN_MULTI_TERM = 'spanMultiTermQuery'; + const TYPE_SPAN_NEAR = 'spanNearQuery'; + const TYPE_SPAN_NOT = 'spanNotQuery'; + const TYPE_SPAN_OR = 'spanOrQuery'; + const TYPE_SPAN_TERM = 'spanTermQuery'; + const TYPE_SPAN_WITHIN = 'spanWithinQuery'; +} diff --git a/src/module-elasticsuite-core/Search/Request/QueryInterface.php b/src/module-elasticsuite-core/Search/Request/QueryInterface.php index 0f831434a..e04d5f088 100644 --- a/src/module-elasticsuite-core/Search/Request/QueryInterface.php +++ b/src/module-elasticsuite-core/Search/Request/QueryInterface.php @@ -37,4 +37,5 @@ interface QueryInterface extends \Magento\Framework\Search\Request\QueryInterfac const TYPE_FUNCTIONSCORE = 'functionScore'; const TYPE_MORELIKETHIS = 'moreLikeThisQuery'; const TYPE_MATCHPHRASEPREFIX = 'matchPhrasePrefixQuery'; + const TYPE_PREFIX = 'prefixQuery'; } diff --git a/src/module-elasticsuite-core/Search/RequestInterface.php b/src/module-elasticsuite-core/Search/RequestInterface.php index 1bbb322a4..49211a81a 100644 --- a/src/module-elasticsuite-core/Search/RequestInterface.php +++ b/src/module-elasticsuite-core/Search/RequestInterface.php @@ -61,4 +61,11 @@ public function isSpellchecked(); * @return int|bool */ public function getTrackTotalHits(); + + /** + * Get the value of the min_score parameter, if any. + * + * @return int|bool + */ + public function getMinScore(); } diff --git a/src/module-elasticsuite-core/Search/Spellchecker/Request.php b/src/module-elasticsuite-core/Search/Spellchecker/Request.php index e78bbaffe..c87d759fc 100644 --- a/src/module-elasticsuite-core/Search/Spellchecker/Request.php +++ b/src/module-elasticsuite-core/Search/Spellchecker/Request.php @@ -40,20 +40,49 @@ class Request implements RequestInterface /** * @var float */ - private $cufoffFrequency; + private $cutoffFrequency; + + /** + * @var boolean + */ + private $isUsingAllTokens; + + /** + * @var boolean + */ + private $isUsingReference; + + /** + * @var boolean + */ + private $isUsingEdgeNgram; /** * Constructor. * - * @param string $index Spellcheck request index name. - * @param string $queryText Spellcheck fulltext query. - * @param float $cutoffFrequency Spellcheck cutoff frequency (used to detect stopwords). + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * + * @param string $index Spellcheck request index name. + * @param string $queryText Spellcheck fulltext query. + * @param float $cutoffFrequency Spellcheck cutoff frequency (used to detect stopwords). + * @param boolean $isUsingAllTokens Is spellcheck using all tokens returned by term vectors. + * @param boolean $isUsingReference Should the reference analyzer be included in the spellcheck request. + * @param boolean $isUsingEdgeNgram Should the edge ngram based analyzers be included in the spellcheck request. */ - public function __construct($index, $queryText, $cutoffFrequency) - { + public function __construct( + $index, + $queryText, + $cutoffFrequency, + $isUsingAllTokens, + $isUsingReference, + $isUsingEdgeNgram + ) { $this->index = $index; $this->queryText = $queryText; - $this->cufoffFrequency = $cutoffFrequency; + $this->cutoffFrequency = $cutoffFrequency; + $this->isUsingAllTokens = $isUsingAllTokens; + $this->isUsingReference = $isUsingReference; + $this->isUsingEdgeNgram = $isUsingEdgeNgram; } /** @@ -77,6 +106,30 @@ public function getQueryText() */ public function getCutoffFrequency() { - return $this->cufoffFrequency; + return $this->cutoffFrequency; + } + + /** + * {@inheritDoc} + */ + public function isUsingAllTokens() + { + return $this->isUsingAllTokens; + } + + /** + * {@inheritDoc} + */ + public function isUsingReference() + { + return $this->isUsingReference; + } + + /** + * {@inheritDoc} + */ + public function isUsingEdgeNgram() + { + return $this->isUsingEdgeNgram; } } diff --git a/src/module-elasticsuite-core/Setup/Validator.php b/src/module-elasticsuite-core/Setup/Validator.php index 8405f1ba6..c4deac936 100644 --- a/src/module-elasticsuite-core/Setup/Validator.php +++ b/src/module-elasticsuite-core/Setup/Validator.php @@ -27,6 +27,11 @@ */ class Validator implements ValidatorInterface { + /** + * @var ClientInterface + */ + private $client; + /** * Validator constructor. * diff --git a/src/module-elasticsuite-core/Test/Unit/Index/IndexOperationTest.php b/src/module-elasticsuite-core/Test/Unit/Index/IndexOperationTest.php index ed401e519..5609f4d03 100644 --- a/src/module-elasticsuite-core/Test/Unit/Index/IndexOperationTest.php +++ b/src/module-elasticsuite-core/Test/Unit/Index/IndexOperationTest.php @@ -232,7 +232,7 @@ private function getObjectManagerMock() /** * Client factory mocking. * - * @return \PHPUnit\Framework\MockObject\MockObject + * @return void */ private function initClientMock() { diff --git a/src/module-elasticsuite-core/Test/Unit/Index/MappingTest.php b/src/module-elasticsuite-core/Test/Unit/Index/MappingTest.php index 53a2e8c21..b7c901424 100644 --- a/src/module-elasticsuite-core/Test/Unit/Index/MappingTest.php +++ b/src/module-elasticsuite-core/Test/Unit/Index/MappingTest.php @@ -101,6 +101,38 @@ public function testDefaultSpellingProperty() $this->assertArrayHasKey(FieldInterface::ANALYZER_PHONETIC, $properties[Mapping::DEFAULT_SPELLING_FIELD]['fields']); } + /** + * Test the reference collector field mapping generation is correct. + * + * @return void + */ + public function testReferenceProperty() + { + $properties = $this->mapping->getProperties(); + + $this->assertArrayHasKey(Mapping::DEFAULT_REFERENCE_FIELD, $properties); + $this->assertEquals(FieldInterface::FIELD_TYPE_TEXT, $properties[Mapping::DEFAULT_REFERENCE_FIELD]['type']); + $this->assertArrayHasKey(FieldInterface::ANALYZER_REFERENCE, $properties[Mapping::DEFAULT_REFERENCE_FIELD]['fields']); + $this->assertArrayHasKey(FieldInterface::ANALYZER_WHITESPACE, $properties[Mapping::DEFAULT_REFERENCE_FIELD]['fields']); + $this->assertArrayHasKey(FieldInterface::ANALYZER_SHINGLE, $properties[Mapping::DEFAULT_REFERENCE_FIELD]['fields']); + } + + /** + * Test the edge_ngram collector field mapping generation is correct. + * + * @return void + */ + public function testEdgeNgramProperty() + { + $properties = $this->mapping->getProperties(); + + $this->assertArrayHasKey(Mapping::DEFAULT_EDGE_NGRAM_FIELD, $properties); + $this->assertEquals(FieldInterface::FIELD_TYPE_TEXT, $properties[Mapping::DEFAULT_EDGE_NGRAM_FIELD]['type']); + $this->assertArrayHasKey(FieldInterface::ANALYZER_EDGE_NGRAM, $properties[Mapping::DEFAULT_EDGE_NGRAM_FIELD]['fields']); + $this->assertArrayHasKey(FieldInterface::ANALYZER_WHITESPACE, $properties[Mapping::DEFAULT_EDGE_NGRAM_FIELD]['fields']); + $this->assertArrayHasKey(FieldInterface::ANALYZER_SHINGLE, $properties[Mapping::DEFAULT_EDGE_NGRAM_FIELD]['fields']); + } + /** * Test the basic fields mapping generation is correct. * @@ -112,7 +144,7 @@ public function testBasicFields() $properties = $this->mapping->getProperties(); $this->assertCount(5, $fields); - $this->assertCount(6, $properties); + $this->assertCount(8, $properties); $this->assertEquals('entity_id', $this->mapping->getIdField()->getName()); $this->assertArrayHasKey('entity_id', $fields); @@ -167,7 +199,7 @@ public function testMappingGeneration() { $mapping = $this->mapping->asArray(); $this->assertArrayHasKey('properties', $mapping); - $this->assertCount(6, $mapping['properties']); + $this->assertCount(8, $mapping['properties']); } /** diff --git a/src/module-elasticsuite-core/Test/Unit/Search/Adapter/Elasticsuite/Request/Aggregation/Builder/HistorgramTest.php b/src/module-elasticsuite-core/Test/Unit/Search/Adapter/Elasticsuite/Request/Aggregation/Builder/HistogramTest.php similarity index 100% rename from src/module-elasticsuite-core/Test/Unit/Search/Adapter/Elasticsuite/Request/Aggregation/Builder/HistorgramTest.php rename to src/module-elasticsuite-core/Test/Unit/Search/Adapter/Elasticsuite/Request/Aggregation/Builder/HistogramTest.php diff --git a/src/module-elasticsuite-core/Test/Unit/Search/Request/BuilderTest.php b/src/module-elasticsuite-core/Test/Unit/Search/Request/BuilderTest.php new file mode 100644 index 000000000..debef040d --- /dev/null +++ b/src/module-elasticsuite-core/Test/Unit/Search/Request/BuilderTest.php @@ -0,0 +1,274 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Test\Unit\Search\Request; + +use Magento\Framework\Search\Request\DimensionFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Smile\ElasticsuiteCore\Api\Search\Request\ContainerConfigurationInterface; +use Smile\ElasticsuiteCore\Api\Search\Request\ContainerConfiguration\AggregationResolverInterface; +use Smile\ElasticsuiteCore\Api\Search\Request\Container\RelevanceConfigurationInterface; +use Smile\ElasticsuiteCore\Api\Search\Spellchecker\RequestInterfaceFactory as SpellcheckRequestFactory; +use Smile\ElasticsuiteCore\Api\Search\SpellcheckerInterface; +use Smile\ElasticsuiteCore\Search\Request\Aggregation\AggregationBuilder; +use Smile\ElasticsuiteCore\Search\Request\Query\Builder as QueryBuilder; +use Smile\ElasticsuiteCore\Search\Request\Builder; +use Smile\ElasticsuiteCore\Search\Request\ContainerConfigurationFactory; +use Smile\ElasticsuiteCore\Search\Request\SortOrder\SortOrderBuilder; +use Smile\ElasticsuiteCore\Search\RequestFactory; +use Smile\ElasticsuiteCore\Search\Spellchecker\Request as SpellcheckerRequest; + +/** + * Search Request Builder unit testing. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Richard BAYET + */ +class BuilderTest extends TestCase +{ + /** + * Tests the correct creation of a SpellcheckerInterface with regards to parameters + * (introduction/removal of experimental relevance settings) + * @covers \Smile\ElasticsuiteCore\Search\Request\Builder::getSpellingType + * + * @return void + */ + public function testSpellcheckRequestConstructor(): void + { + $requestBuilder = new Builder( + $this->getSearchRequestFactoryMock(), + $this->getDimensionFactoryMock(), + $this->getQueryBuilderMock(), + $this->getSortOrderBuilderMock(), + $this->getAggregationBuilderMock(), + $this->getContainerConfigurationFactoryMock(), + $this->getSpellcheckerRequestFactoryMock(), + $this->getSpellcheckerInterfaceMock(), + $this->getAggregationResolverMock() + ); + + + $request = $requestBuilder->create( + '1', + 'quick_search_container', + 0, + 1, + 'test' + ); + $this->assertEquals([], $request); + + $request = $requestBuilder->create( + '1', + 'quick_search_container', + 0, + 1, + ['test1', 'test2'] + ); + $this->assertEquals([], $request); + } + + /** + * Get Query Builder mock object + * + * @return MockObject + */ + private function getQueryBuilderMock(): MockObject + { + $queryBuilder = $this->getMockBuilder(QueryBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $queryBuilder->method('createFulltextQuery')->willReturn([]); + $queryBuilder->method('createFilterQuery')->willReturn([]); + + return $queryBuilder; + } + + /** + * Get Spellchecker Request Factory mock object + * + * @return MockObject + */ + private function getSpellcheckerRequestFactoryMock(): MockObject + { + $spellcheckRequestFactory = $this->getMockBuilder(SpellcheckRequestFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $spellcheckRequestFactory->method('create')->willReturnCallback(function ($args) { + return new SpellcheckerRequest(...array_values($args)); + }); + + return $spellcheckRequestFactory; + } + + /** + * Get Container Configuration Factory mock object + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @return MockObject + */ + private function getContainerConfigurationFactoryMock(): MockObject + { + $containerConfigurationFactory = $this->getMockBuilder(ContainerConfigurationFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $containerConfigurationFactory->method('create')->willReturnCallback(function ($args) { + return $this->getContainerConfigurationInterfaceMock(); + }); + + return $containerConfigurationFactory; + } + + /** + * Get Container Configuration mock object + * + * @return MockObject + */ + private function getContainerConfigurationInterfaceMock(): MockObject + { + $containerConfiguration = $this->getMockBuilder(ContainerConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $containerConfiguration->method('getIndexName')->willReturn('Dummy'); + $containerConfiguration->method('getRelevanceConfig')->willReturn($this->getRelevanceConfigurationInterfaceMock()); + $containerConfiguration->method('getFilters')->willReturn([]); + + return $containerConfiguration; + } + + /** + * Get Relevance Configuration mock object + * + * @return MockObject + */ + private function getRelevanceConfigurationInterfaceMock(): MockObject + { + $relevanceConfiguration = $this->getMockBuilder(RelevanceConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $relevanceConfiguration->method('getCutOffFrequency')->willReturn(0.15); + $relevanceConfiguration->method('isUsingAllTokens')->willReturn(false); + $relevanceConfiguration->method('isUsingReferenceAnalyzer')->willReturn(false); + $relevanceConfiguration->method('isUsingEdgeNgramAnalyzer')->willReturn(false); + + return $relevanceConfiguration; + } + + /** + * Get Spellchecker mock object + * + * @return MockObject + */ + private function getSpellcheckerInterfaceMock(): MockObject + { + $spellChecker = $this->getMockBuilder(SpellcheckerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $spellChecker->method('getSpellingType')->willReturn(SpellcheckerInterface::SPELLING_TYPE_EXACT); + + return $spellChecker; + } + + /** + * Get Search Request Factory mock object + * + * @return MockObject + */ + private function getSearchRequestFactoryMock(): MockObject + { + $requestFactory = $this->getMockBuilder(RequestFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $requestFactory->method('create')->willReturn([]); + + return $requestFactory; + } + + /** + * Get Dimension Factory mock object + * + * @return MockObject + */ + private function getDimensionFactoryMock(): MockObject + { + $dimensionFactory = $this->getMockBuilder(DimensionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $dimensionFactory->method('create')->willReturn([]); + + return $dimensionFactory; + } + + /** + * Get Sort Order Builder mock object + * + * @return MockObject + */ + private function getSortOrderBuilderMock(): MockObject + { + $sortOrderBuilder = $this->getMockBuilder(SortOrderBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $sortOrderBuilder->method('buildSordOrders')->willReturn([]); + + return $sortOrderBuilder; + } + + /** + * Get Aggregation Builder mock object + * + * @return MockObject + */ + private function getAggregationBuilderMock(): MockObject + { + $sortOrderBuilder = $this->getMockBuilder(AggregationBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $sortOrderBuilder->method('buildAggregations')->willReturn([]); + + return $sortOrderBuilder; + } + + /** + * Get Aggregation Resolver mock object + * + * @return MockObject + */ + private function getAggregationResolverMock(): MockObject + { + $aggregationResolver = $this->getMockBuilder(AggregationResolverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $aggregationResolver->method('getContainerAggregations')->willReturn([]); + + return $aggregationResolver; + } +} diff --git a/src/module-elasticsuite-core/Test/Unit/Search/Request/Query/Fulltext/QueryBuilderTest.php b/src/module-elasticsuite-core/Test/Unit/Search/Request/Query/Fulltext/QueryBuilderTest.php index 65aff74c5..9186d037e 100644 --- a/src/module-elasticsuite-core/Test/Unit/Search/Request/Query/Fulltext/QueryBuilderTest.php +++ b/src/module-elasticsuite-core/Test/Unit/Search/Request/Query/Fulltext/QueryBuilderTest.php @@ -226,6 +226,10 @@ private function getFieldFilters() $fieldFilterMock->method('filterField')->will($this->returnValue(true)); - return ['searchableFieldFilter' => $fieldFilterMock, 'fuzzyFieldFilter' => $fieldFilterMock]; + return [ + 'searchableFieldFilter' => $fieldFilterMock, + 'fuzzyFieldFilter' => $fieldFilterMock, + 'nonStandardFuzzyFieldFilter' => $fieldFilterMock, + ]; } } diff --git a/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml b/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml index d5661f18b..2b71a98e8 100644 --- a/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml +++ b/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml @@ -58,6 +58,79 @@ + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + 1 + + integer validate-greater-than-zero + + + + + 1 + + integer validate-greater-than-zero + + + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + 1 + + integer validate-no-empty validate-greater-than-zero + min_score are not included in the search results.]]> + + + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + 1 + + integer validate-no-empty validate-not-negative-number + + + + + + 1 + + integer validate-no-empty validate-not-negative-number + + +
@@ -107,6 +180,24 @@ + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + Magento\Config\Model\Config\Source\Yesno + + +
diff --git a/src/module-elasticsuite-core/etc/di.xml b/src/module-elasticsuite-core/etc/di.xml index 806b6b567..afa4a2e70 100644 --- a/src/module-elasticsuite-core/etc/di.xml +++ b/src/module-elasticsuite-core/etc/di.xml @@ -80,7 +80,10 @@ Smile\ElasticsuiteCore\Search\Request\Query\Fulltext\SearchableFieldFilter + Smile\ElasticsuiteCore\Search\Request\Query\Fulltext\NonStandardSearchableFieldFilter Smile\ElasticsuiteCore\Search\Request\Query\Fulltext\FuzzyFieldFilter + Smile\ElasticsuiteCore\Search\Request\Query\Fulltext\NonStandardFuzzyFieldFilter + Smile\ElasticsuiteCore\Search\Request\Query\Fulltext\SpannableFieldFilter @@ -103,6 +106,16 @@ moreLikeThisQueryFactory matchPhrasePrefixFactory functionScoreFactory + spanContainingFactory + spanFieldMaskingFactory + spanFirstFactory + spanMultiTermFactory + spanNearFactory + spanNotFactory + spanOrFactory + spanTermFactory + spanWithinFactory + prefixQueryFactory @@ -122,6 +135,16 @@ + + + + + + + + + + @@ -141,6 +164,16 @@ Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\FunctionScore\Proxy Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\MoreLikeThis\Proxy Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\MatchPhrasePrefix\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanContaining\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanFieldMasking\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanFirst\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanMultiTerm\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanNear\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanNot\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanOr\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanTerm\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Span\SpanWithin\Proxy + Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Prefix\Proxy diff --git a/src/module-elasticsuite-core/etc/elasticsuite_analysis.xml b/src/module-elasticsuite-core/etc/elasticsuite_analysis.xml index 6ddc928aa..16cff2c13 100644 --- a/src/module-elasticsuite-core/etc/elasticsuite_analysis.xml +++ b/src/module-elasticsuite-core/etc/elasticsuite_analysis.xml @@ -178,6 +178,10 @@ greek + + 3 + 20 + @@ -263,5 +267,41 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/module-elasticsuite-core/etc/elasticsuite_relevance.xml b/src/module-elasticsuite-core/etc/elasticsuite_relevance.xml index 54208e2a3..9146f167d 100644 --- a/src/module-elasticsuite-core/etc/elasticsuite_relevance.xml +++ b/src/module-elasticsuite-core/etc/elasticsuite_relevance.xml @@ -29,6 +29,22 @@ 0.15 + + 0 + 10 + 1 + + + 0 + 0 + + + 0 + 0 + 0 + 10 + 20 + @@ -40,6 +56,11 @@ 1 + + 0 + 0 + 0 + diff --git a/src/module-elasticsuite-core/i18n/en_US.csv b/src/module-elasticsuite-core/i18n/en_US.csv index 8272c5255..1551bfc15 100644 --- a/src/module-elasticsuite-core/i18n/en_US.csv +++ b/src/module-elasticsuite-core/i18n/en_US.csv @@ -74,3 +74,29 @@ Autocomplete,Autocomplete "The value should be different to zero.","The value should be different to zero." "The number of replicas configured for Elasticsuite is incorrect. You cannot use %1 replicas since there is only %2 nodes in your Elasticsearch cluster.","The number of replicas configured for Elasticsuite is incorrect. You cannot use %1 replicas since there is only %2 nodes in your Elasticsearch cluster." "Click here to go to the Elasticsuite Config page and change your Number of Replicas per Index parameter according to our wiki page.","Click here to go to the Elasticsuite Config page and change your Number of Replicas per Index parameter according to our wiki page." +"Span Match Configuration","Span Match Configuration" +"Enable Boost on Span Match","Enable Boost on Span Match" +"Span Match Boost Value","Span Match Boost Value" +"Number of words to match at the beginning of the string","Number of words to match at the beginning of the string" +"Exact Match Configuration","Exact Match Configuration" +"[Experimental] Use reference analyzer in exact matching filter query","[Experimental] Use reference analyzer in exact matching filter query" +"Experimental: Instead of the 'sku' field, use the 'reference' collector field which contains all searchable fields using the 'reference' analyzer when building the exact/most exact matching filter query. Useful for sku/reference matching.","Experimental: Instead of the 'sku' field, use the 'reference' collector field which contains all searchable fields using the 'reference' analyzer when building the exact/most exact matching filter query. Useful for sku/reference matching." +"Term Vectors Configuration","Term Vectors Configuration" +"[Experimental] Use all tokens from term vectors","[Experimental] Use all tokens from term vectors" +"Experimental: Take into account all tokens from the term vectors response, instead of one token per position. Useful for sku/reference matching.","Experimental: Take into account all tokens from the term vectors response, instead of one token per position. Useful for sku/reference matching." +"[Experimental] Use reference analyzer in term vectors","[Experimental] Use reference analyzer in term vectors" +"Experimental: Include the 'reference' collector field which contains all searchable fields using the 'reference' analyzer when performing the term vectors request. Useful for sku/reference matching.","Experimental: Include the 'reference' collector field which contains all searchable fields using the 'reference' analyzer when performing the term vectors request. Useful for sku/reference matching." +"[Experimental] Use edge ngram analyzer in term vectors","[Experimental] Use edge ngram analyzer in term vectors" +"Experimental: Include the 'edge_ngram' collector field which contains all searchable fields using an analyzer containing an edge ngram component (like 'standard_edge_ngram') when performing the term vectors request. Useful for triggering exact matching on partial search terms that are only present in fields using an edge ngram component.","Experimental: Include the 'edge_ngram' collector field which contains all searchable fields using an analyzer containing an edge ngram component (like 'standard_edge_ngram') when performing the term vectors request. Useful for triggering exact matching on partial search terms that are only present in fields using an edge ngram component." +"[Experimental] Use default analyzer in exact matching filter query","[Experimental] Use default analyzer in exact matching filter query" +"Experimental: Use the default analyzer of each field instead of the 'standard' analyzer for exact matching. Eg : for sku, it will try to match on 'sku.reference' field. If a field is using 'standard_edge_ngram' analyzer, it will try to match on 'field.standard_edge_ngram'. Useful for partial matching, or matching on beginning of words.","Experimental: Use the default analyzer of each field instead of the 'standard' analyzer for exact matching. Eg : for sku, it will try to match on 'sku.reference' field. If a field is using 'standard_edge_ngram' analyzer, it will try to match on 'field.standard_edge_ngram'. Useful for partial matching, or matching on beginning of words." +"Minimum Score Configuration","Minimum Score Configuration" +"Use Min Score","Use Min Score" +"Min Score Value","Min Score Value" +"An integer greater than 0. Documents with a score lower than min_score are not included in the search results.","An integer greater than 0. Documents with a score lower than min_score are not included in the search results." +"[Experimental] Enable single term custom boost values","[Experimental] Enable single term custom boost values" +"Experimental: By default, in an exact match query, when a single term is searched, the phrase match boost value (defaults to 10) is used to both a) boost matches in the 'whitespace' version of searchable attributes/fields, in addition of their own search weight and b) apply a double boost (so defaults to 20) on matches in the 'sortable' version of searchable+sortable attributes/fields, in addition of their own search weight. This puts a huge emphasis on accurate ""exactly as the user typed it"" matches compared to possible singular/plural/conjugated verbs variations. Enable this setting to allow you to choose alternative boost values to use in each case.","Experimental: By default, in an exact match query, when a single term is searched, the phrase match boost value (defaults to 10) is used to both a) boost matches in the 'whitespace' version of searchable attributes/fields, in addition of their own search weight and b) apply a double boost (so defaults to 20) on matches in the 'sortable' version of searchable+sortable attributes/fields, in addition of their own search weight. This puts a huge emphasis on accurate ""exactly as the user typed it"" matches compared to possible singular/plural/conjugated verbs variations. Enable this setting to allow you to choose alternative boost values to use in each case." +"[Experimental] Single term phrase match boost value","[Experimental] Single term phrase match boost value" +"Experimental: When a single term is searched, a 'whitespace' (instead of 'shingle') part of the exact match query combines the attribute/field search weight with the phrase match boost value (defaults to 10). So for instance, the 'whitespace' version of a product 'name' attribute with a search weight of 5 would be boosted by 50. You can lower that boost value to reduce the scoring gaps between 'standard' exact matches and 'whitespace' exact matches.","Experimental: When a single term is searched, a 'whitespace' (instead of 'shingle') part of the exact match query combines the attribute/field search weight with the phrase match boost value (defaults to 10). So for instance, the 'whitespace' version of a product 'name' attribute with a search weight of 5 would be boosted by 50. You can lower that boost value to reduce the scoring gaps between 'standard' exact matches and 'whitespace' exact matches." +"[Experimental] Single term sortable matches boost value","[Experimental] Single term sortable matches boost value" +"Experimental: The 'sortable' part of the exact match query combines the attribute/field search weight with twice the phrase match boost value (so defaults to 20). So for instance, the 'sortable' version of a product 'name' attribute with a search weight of 5 would be boosted by 100. You can lower that boost value to reduce the scoring gaps between 'standard' exact matches and 'sortable' exact matches.","Experimental: The 'sortable' part of the exact match query combines the attribute/field search weight with twice the phrase match boost value (so defaults to 20). So for instance, the 'sortable' version of a product 'name' attribute with a search weight of 5 would be boosted by 100. You can lower that boost value to reduce the scoring gaps between 'standard' exact matches and 'sortable' exact matches." diff --git a/src/module-elasticsuite-core/i18n/fr_FR.csv b/src/module-elasticsuite-core/i18n/fr_FR.csv index 5663d4557..27ee53478 100644 --- a/src/module-elasticsuite-core/i18n/fr_FR.csv +++ b/src/module-elasticsuite-core/i18n/fr_FR.csv @@ -74,3 +74,29 @@ General,Général "The value should be different to zero.","La valeur doit être différente de zéro." "The number of replicas configured for Elasticsuite is incorrect. You cannot use %1 replicas since there is only %2 nodes in your Elasticsearch cluster.","Le nombre de replicas configuré pour Elasticsuite est incorrect. Vous ne pouvez pas utiliser %1 replicas car il n'y a que %2 nodes dans votre cluster Elasticsearch." "Click here to go to the Elasticsuite Config page and change your Number of Replicas per Index parameter according to our wiki page.","Cliquez ici pour accéder à la configuration Elasticsuite et changer le paramètre Nombre de replicas par Index conformément à notre page de wiki." +"Span Match Configuration","Recherche sur le début des champs" +"Enable Boost on Span Match","Activer le boost sur le début des champs" +"Span Match Boost Value","Valeur du boost" +"Number of words to match at the beginning of the string","Nombre de mots à matcher au début des champs" +"Exact Match Configuration","Configuration Recherche Exacte" +"[Experimental] Use reference analyzer in exact matching filter query","[Expérimental] Utiliser l'analyseur reference dans le filtre de requête d'exact matching" +"Experimental: Instead of the 'sku' field, use the 'reference' collector field which contains all searchable fields using the 'reference' analyzer when building the exact/most exact matching filter query. Useful for sku/reference matching.","Expérimental: Plutôt que le champ 'sku' seul, utiliser le champ collecteur 'reference' qui contient tous les champs cherchables utilisant l'analyseur 'reference' lors de la construction du filtre de requête exact/most exact. Utile pour le matching de sku/références produit." +"Term Vectors Configuration","Configuration des Term Vectors" +"[Experimental] Use all tokens from term vectors","[Expérimental] Utiliser tous les tokens des term vectors" +"Experimental: Take into account all tokens from the term vectors response, instead of one token per position. Useful for sku/reference matching.","Expérimental: Prendre en considération tous les tokens de la réponse des term vectors, plutôt qu'un seul token par position. Utile pour matcher les sku/références." +"[Experimental] Use reference analyzer in term vectors","[Expérimental] Utiliser l'analyseur reference dans les term vectors" +"Experimental: Include the 'reference' collector field which contains all searchable fields using the 'reference' analyzer when performing the term vectors request. Useful for sku/reference matching.","Expérimental: Inclure le champ collecteur 'reference' qui contient tous les champs cherchables utilisant l'analyseur 'reference' lors de la requête des term vectors. Utile pour le matching de sku/références produit." +"[Experimental] Use edge ngram analyzer in term vectors","[Expérimental] Utiliser l'analyseur edge ngram dans les term vectors" +"Experimental: Include the 'edge_ngram' collector field which contains all searchable fields using an analyzer containing an edge ngram component (like 'standard_edge_ngram') when performing the term vectors request. Useful for triggering exact matching on partial search terms that are only present in fields using an edge ngram component.","Expérimental: Inclure le champ collecteur 'edge_ngram' qui contient tous les champs cherchables utilisant un analyseur contenant un composant edge ngram (comme 'standard_edge_ngram') lors de la requête des terms vectors. Utile pour déclencher une recherche exacte sur des termes de recherche partiels uniquement contenus dans les champs utilisant un composant edge ngram." +"[Experimental] Use default analyzer in exact matching filter query","[Expérimental] Utiliser l'analyseur par défaut des champs dans la recherche exacte" +"Experimental: Use the default analyzer of each field instead of the 'standard' analyzer for exact matching. Eg : for sku, it will try to match on 'sku.reference' field. If a field is using 'standard_edge_ngram' analyzer, it will try to match on 'field.standard_edge_ngram'. Useful for partial matching, or matching on beginning of words.","Expérimental: Utiliser l'analyseur par défaut de chaque champ au lieu de l'analyseur 'standard' pour la recherche exacte. Ex : pour le sku, le moteur essayera de trouver une correspondance exacte sur sku.reference. Si un champ a pour analyseur 'standard_edge_ngram', le moteur tentera de trouver une correspondance exacte sur 'champ.standard_edge_ngram'. Utile pour le matching partiel, ou le matching sur les débuts de mots." +"Minimum Score Configuration","Score minimum" +"Use Min Score","Utiliser un score minimum" +"Min Score Value","Score minimum" +"An integer greater than 0. Documents with a score lower than min_score are not included in the search results.","Un entier supérieur à 0. Les documents ayant un score inférieur à min_score ne seront plus inclus dans les résultats de recherche." +"[Experimental] Enable single term custom boost values","[Expérimental] Activer des valeurs de boost spécifiques pour les recherches à terme unique" +"Experimental: By default, in an exact match query, when a single term is searched, the phrase match boost value (defaults to 10) is used to both a) boost matches in the 'whitespace' version of searchable attributes/fields, in addition of their own search weight and b) apply a double boost (so defaults to 20) on matches in the 'sortable' version of searchable+sortable attributes/fields, in addition of their own search weight. This puts a huge emphasis on accurate ""exactly as the user typed it"" matches compared to possible singular/plural/conjugated verbs variations. Enable this setting to allow you to choose alternative boost values to use in each case.","Expérimental: Par défaut, dans une requête de recherche exacte, lorsqu'on cherche un seul mot, le valeur du boost pour les phrases (par défaut 10) est utilisé pour à la fois a) booster les matches dans la version 'whitespace' des attributs/champs recherchables, en plus de leur propre poids dans la recherche et b) appliquer un double boost (donc par défaut 20) sur les matches de la version 'sortable' des attributs/champs recherches et triables, en plus de leur propre poids dans la recherche. Cela donne un poids considérable sur les matches de type ""exactement de la façon dont l'utilisateur l'a saisi"" comparativement à ceux sur les variations singulier/pluriel/verbe conjugué. Activer ce paramètre permet de choisir des valeurs de boost alternatives dans chaque cas." +"[Experimental] Single term phrase match boost value","[Expérimental] Valeur de boost sur les phrases pour les recherches à terme unique" +"Experimental: When a single term is searched, a 'whitespace' (instead of 'shingle') part of the exact match query combines the attribute/field search weight with the phrase match boost value (defaults to 10). So for instance, the 'whitespace' version of a product 'name' attribute with a search weight of 5 would be boosted by 50. You can lower that boost value to reduce the scoring gaps between 'standard' exact matches and 'whitespace' exact matches.","Expérimental: Lorsqu'un terme unique est recherché, une portion 'whitespace' (au lieu de 'shingle') de la requête de recherche exacte combine le poids dans la recherche de l'attribute/champ avec la valeur du boost pour les phrases (10 par défaut). Donc par exemple, la version 'whitespace' de l'attribut produit 'name' avec un poids de 5 serait boosté avec un poids de 50. Vous pouvez abaisser la valeur de ce boost pour réduire l'écart de score entre les matches exacts 'standard' et les matches exacts 'whitespace'." +"[Experimental] Single term sortable matches boost value","[Expérimental] Valeur de boost sur les champs triables pour les recherches à terme unique" +"Experimental: The 'sortable' part of the exact match query combines the attribute/field search weight with twice the phrase match boost value (so defaults to 20). So for instance, the 'sortable' version of a product 'name' attribute with a search weight of 5 would be boosted by 100. You can lower that boost value to reduce the scoring gaps between 'standard' exact matches and 'sortable' exact matches.","Expérimental: La portion 'sortable' d'une requête de recherche exacte combine le poids dans la recherche de l'attribut/champ avec le double de la valeur du boost de la recherche par phrase (donc par défaut 20). Donc par exemple, la version 'sortable' de l'attribut produit 'name' avec un poids de 5 serait boosté par 100. Vous pouvez abaisser la valeur de ce boost pour réduire l'écart de score entre les matches exacts 'standard' et les matches exacts 'sortables'." diff --git a/src/module-elasticsuite-indices/Block/Adminhtml/Analysis/Analyzer.php b/src/module-elasticsuite-indices/Block/Adminhtml/Analysis/Analyzer.php index 1729751dd..7aea42a8d 100644 --- a/src/module-elasticsuite-indices/Block/Adminhtml/Analysis/Analyzer.php +++ b/src/module-elasticsuite-indices/Block/Adminhtml/Analysis/Analyzer.php @@ -17,7 +17,7 @@ use Magento\Backend\Block\Template; use Smile\ElasticsuiteIndices\Block\Widget\Grid\Column\Renderer\IndexStatus; use Smile\ElasticsuiteIndices\Model\IndexStatsProvider; -use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexSettings\CollectionFactory as IndexSettingsFactory; +use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexSettings\CollectionFactory; /** * Adminhtml Analysis by Analyzer Block. @@ -34,26 +34,26 @@ class Analyzer extends Template protected $indexStatsProvider; /** - * @var IndexSettingsFactory + * @var CollectionFactory */ - protected $indexSettingsFactory; + protected $collectionFactory; /** * Analyzer Constructor. * - * @param Template\Context $context The current context. - * @param IndexStatsProvider $indexStatsProvider Index stats provider. - * @param IndexSettingsFactory $indexSettingsFactory Index settings factory. - * @param array $data Data. + * @param Template\Context $context The current context. + * @param IndexStatsProvider $indexStatsProvider Index stats provider. + * @param CollectionFactory $collectionFactory Index settings factory. + * @param array $data Data. */ public function __construct( Template\Context $context, IndexStatsProvider $indexStatsProvider, - IndexSettingsFactory $indexSettingsFactory, + CollectionFactory $collectionFactory, array $data = [] ) { - $this->indexStatsProvider = $indexStatsProvider; - $this->indexSettingsFactory = $indexSettingsFactory; + $this->indexStatsProvider = $indexStatsProvider; + $this->collectionFactory = $collectionFactory; parent::__construct($context, $data); } @@ -77,7 +77,7 @@ public function getElasticSuiteIndices(): ?array foreach ($elasticSuiteIndices as $indexName => $indexAlias) { $indexData = $this->indexStatsProvider->indexStats($indexName, $indexAlias); - $indexCollection = $this->indexSettingsFactory->create(['name' => $indexData['index_name']])->load(); + $indexCollection = $this->collectionFactory->create(['name' => $indexData['index_name']])->load(); if (array_key_exists('index_status', $indexData) && !in_array($indexData['index_status'], $excludedIndexStatus)) { diff --git a/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Mapping.php b/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Mapping.php index 3ecffc3a7..1ee78874e 100644 --- a/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Mapping.php +++ b/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Mapping.php @@ -15,7 +15,7 @@ use Magento\Backend\Block\Template; use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexMapping\Collection; -use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexMapping\CollectionFactory as IndexMappingFactory; +use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexMapping\CollectionFactory; /** * Adminhtml Index mapping items grid @@ -27,23 +27,23 @@ class Mapping extends Template { /** - * @var IndexMappingFactory + * @var CollectionFactory */ - protected $indexMappingFactory; + protected $collectionFactory; /** * Index mapping items constructor. * - * @param Template\Context $context The current context. - * @param IndexMappingFactory $indexMappingFactory Index mapping factory. - * @param array $data Data. + * @param Template\Context $context The current context. + * @param CollectionFactory $collectionFactory Index mapping factory. + * @param array $data Data. */ public function __construct( Template\Context $context, - IndexMappingFactory $indexMappingFactory, + CollectionFactory $collectionFactory, array $data = [] ) { - $this->indexMappingFactory = $indexMappingFactory; + $this->collectionFactory = $collectionFactory; parent::__construct($context, $data); } @@ -54,6 +54,6 @@ public function __construct( */ public function getItemsCollection(): Collection { - return $this->indexMappingFactory->create(['name' => $this->getRequest()->getParam('name')])->load(); + return $this->collectionFactory->create(['name' => $this->getRequest()->getParam('name')])->load(); } } diff --git a/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Settings.php b/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Settings.php index 992e5ef47..9cb4f9aa4 100644 --- a/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Settings.php +++ b/src/module-elasticsuite-indices/Block/Adminhtml/IndexView/Settings.php @@ -15,7 +15,7 @@ use Magento\Backend\Block\Template; use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexSettings\Collection; -use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexSettings\CollectionFactory as IndexSettingsFactory; +use Smile\ElasticsuiteIndices\Model\ResourceModel\IndexSettings\CollectionFactory; /** * Adminhtml Index mapping items grid @@ -27,23 +27,23 @@ class Settings extends Template { /** - * @var IndexSettingsFactory + * @var CollectionFactory */ - protected $indexSettingsFactory; + protected $collectionFactory; /** * Index mapping items constructor. * - * @param Template\Context $context The current context. - * @param IndexSettingsFactory $indexSettingsFactory Index mapping factory. - * @param array $data Data. + * @param Template\Context $context The current context. + * @param CollectionFactory $collectionFactory Index mapping factory. + * @param array $data Data. */ public function __construct( Template\Context $context, - IndexSettingsFactory $indexSettingsFactory, + CollectionFactory $collectionFactory, array $data = [] ) { - $this->indexSettingsFactory = $indexSettingsFactory; + $this->collectionFactory = $collectionFactory; parent::__construct($context, $data); } @@ -54,6 +54,6 @@ public function __construct( */ public function getItemsCollection(): Collection { - return $this->indexSettingsFactory->create(['name' => $this->getRequest()->getParam('name')])->load(); + return $this->collectionFactory->create(['name' => $this->getRequest()->getParam('name')])->load(); } } diff --git a/src/module-elasticsuite-indices/Controller/Adminhtml/Analysis/Request.php b/src/module-elasticsuite-indices/Controller/Adminhtml/Analysis/Request.php index 7b11ba1c0..522338ef8 100644 --- a/src/module-elasticsuite-indices/Controller/Adminhtml/Analysis/Request.php +++ b/src/module-elasticsuite-indices/Controller/Adminhtml/Analysis/Request.php @@ -55,7 +55,7 @@ public function __construct( $this->client = $client; $this->resultJsonFactory = $resultJsonFactory; - return parent::__construct($context); + parent::__construct($context); } /** diff --git a/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Mapping.php b/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Mapping.php index b9ec71c2b..969122698 100644 --- a/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Mapping.php +++ b/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Mapping.php @@ -71,7 +71,7 @@ public function execute() { $indexName = $this->getRequest()->getParam('name'); try { - $index = $this->indexMappingProvider->getMapping($indexName); + $this->indexMappingProvider->getMapping($indexName); } catch (\Exception $e) { $resultForward = $this->resultForwardFactory->create(); $resultForward->forward('noroute'); @@ -79,13 +79,10 @@ public function execute() return $resultForward; } - if ($index) { - $resultPage = $this->resultPageFactory->create(); - $resultPage->getLayout()->getBlock('smile_elasticsuite_indices_index_mapping'); + $resultPage = $this->resultPageFactory->create(); + $resultPage->getLayout()->getBlock('smile_elasticsuite_indices_index_mapping'); + $resultPage->getConfig()->getTitle()->prepend(__('Mapping for index:') . ' ' . $indexName); - $resultPage->getConfig()->getTitle()->prepend(__('Mapping for index:') . ' ' . $indexName); - - return $resultPage; - } + return $resultPage; } } diff --git a/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Settings.php b/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Settings.php index 870944e93..4dd9b886d 100644 --- a/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Settings.php +++ b/src/module-elasticsuite-indices/Controller/Adminhtml/Index/Settings.php @@ -71,7 +71,7 @@ public function execute() { $indexName = $this->getRequest()->getParam('name'); try { - $index = $this->indexSettingsProvider->getSettings($indexName); + $this->indexSettingsProvider->getSettings($indexName); } catch (\Exception $e) { $resultForward = $this->resultForwardFactory->create(); $resultForward->forward('noroute'); @@ -79,13 +79,10 @@ public function execute() return $resultForward; } - if ($index) { - $resultPage = $this->resultPageFactory->create(); - $resultPage->getLayout()->getBlock('smile_elasticsuite_indices_index_settings'); + $resultPage = $this->resultPageFactory->create(); + $resultPage->getLayout()->getBlock('smile_elasticsuite_indices_index_settings'); + $resultPage->getConfig()->getTitle()->prepend(__('Settings for index:') . ' ' . $indexName); - $resultPage->getConfig()->getTitle()->prepend(__('Settings for index:') . ' ' . $indexName); - - return $resultPage; - } + return $resultPage; } } diff --git a/src/module-elasticsuite-indices/Model/IndexStatsProvider.php b/src/module-elasticsuite-indices/Model/IndexStatsProvider.php index ac0fc603e..f4a4f30d1 100644 --- a/src/module-elasticsuite-indices/Model/IndexStatsProvider.php +++ b/src/module-elasticsuite-indices/Model/IndexStatsProvider.php @@ -86,6 +86,7 @@ public function getElasticSuiteIndices($params = []): array $this->elasticsuiteIndices[$name] = $aliases ? key($aliases['aliases']) : null; } } + ksort($this->elasticsuiteIndices, SORT_STRING | SORT_NATURAL); } return $this->elasticsuiteIndices; diff --git a/src/module-elasticsuite-indices/etc/config.xml b/src/module-elasticsuite-indices/etc/config.xml index f9c63166c..18b587811 100644 --- a/src/module-elasticsuite-indices/etc/config.xml +++ b/src/module-elasticsuite-indices/etc/config.xml @@ -19,7 +19,7 @@ - {"_1585151585492_492":{"key":"elasticsuite_categories_fulltext","value":"catalog_category"},"_1585151601029_29":{"key":"elasticsuite_categories_fulltext","value":"catalog_product"}} + {"_1585151585492_492":{"key":"elasticsuite_categories_fulltext","value":"catalog_category"},"_1585151601029_29":{"key":"catalogsearch_fulltext","value":"catalog_product"}} diff --git a/src/module-elasticsuite-indices/view/adminhtml/templates/view/items.phtml b/src/module-elasticsuite-indices/view/adminhtml/templates/view/items.phtml index 0bb4a90ca..a10039b11 100644 --- a/src/module-elasticsuite-indices/view/adminhtml/templates/view/items.phtml +++ b/src/module-elasticsuite-indices/view/adminhtml/templates/view/items.phtml @@ -21,7 +21,7 @@
getItemsCollection(); ?> - +
diff --git a/src/module-elasticsuite-swatches/Model/Plugin/ProductSubstitute.php b/src/module-elasticsuite-swatches/Model/Plugin/ProductSubstitute.php deleted file mode 100644 index a561b44ce..000000000 --- a/src/module-elasticsuite-swatches/Model/Plugin/ProductSubstitute.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @copyright 2020 Smile - * @license Open Software License ("OSL") v. 3.0 - */ -namespace Smile\ElasticsuiteSwatches\Model\Plugin; - -use Smile\ElasticsuiteSwatches\Helper\Swatches; - -/** - * ProductSubstitute Plugin. Used to load Swatches variations. - * - * @category Smile - * @package Smile\ElasticsuiteSwatches - * @author Romain Ruaud - * @since Magento 2.1.6 - */ -class ProductSubstitute -{ - /** - * @var \Magento\Eav\Model\Config - */ - private $eavConfig; - - /** - * @var \Smile\ElasticsuiteSwatches\Helper\Swatches - */ - private $swatchHelper; - - /** - * ProductSubstitute constructor. - * - * @param \Magento\Eav\Model\Config $config EAV Config - * @param \Smile\ElasticsuiteSwatches\Helper\Swatches $swatchHelper Swatch Helper - */ - public function __construct(\Magento\Eav\Model\Config $config, Swatches $swatchHelper) - { - $this->eavConfig = $config; - $this->swatchHelper = $swatchHelper; - } - - /** - * Build proper array for swatches rendering. Especially in product listing where values may come as label - * instead of option Ids. - * - * @param \Magento\Swatches\Model\ProductSubstitute $productSubstitute Original ProductSubstitute class - * @param \Closure $proceed ProductSubstitute::getFilterArray() - * @param array $request Request - * - * @return mixed - */ - public function aroundGetFilterArray( - \Magento\Swatches\Model\ProductSubstitute $productSubstitute, - \Closure $proceed, - array $request - ) { - $filterArray = $proceed($request); - - $attributeCodes = $this->eavConfig->getEntityAttributeCodes(\Magento\Catalog\Model\Product::ENTITY); - - foreach ($request as $code => $value) { - if (in_array($code, $attributeCodes)) { - $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $code); - - if (isset($filterArray[$code]) && !is_array($filterArray[$code])) { - $filterArray[$code] = [$filterArray[$code]]; - } - - if ($attribute->getId() && $productSubstitute->canReplaceImageWithSwatch($attribute)) { - $filterArray[$code][] = $this->swatchHelper->getOptionIds($attribute, $value); - } - } - } - - return $filterArray; - } -} diff --git a/src/module-elasticsuite-swatches/etc/di.xml b/src/module-elasticsuite-swatches/etc/di.xml index 4aed6f820..cd24318cc 100644 --- a/src/module-elasticsuite-swatches/etc/di.xml +++ b/src/module-elasticsuite-swatches/etc/di.xml @@ -43,12 +43,4 @@ - - - - Smile\ElasticsuiteSwatches\Helper\Swatches - - - - diff --git a/src/module-elasticsuite-thesaurus/Api/ThesaurusRepositoryInterface.php b/src/module-elasticsuite-thesaurus/Api/ThesaurusRepositoryInterface.php index aef4453be..45e253370 100644 --- a/src/module-elasticsuite-thesaurus/Api/ThesaurusRepositoryInterface.php +++ b/src/module-elasticsuite-thesaurus/Api/ThesaurusRepositoryInterface.php @@ -34,7 +34,7 @@ interface ThesaurusRepositoryInterface public function getById($thesaurusId); /** - * save a Thesaurus + * Save a Thesaurus * * @param \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus Thesaurus * @@ -44,7 +44,7 @@ public function getById($thesaurusId); public function save(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus); /** - * delete a Thesaurus + * Delete a Thesaurus * * @param \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus Thesaurus * @@ -52,4 +52,24 @@ public function save(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $t * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function delete(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus); + + /** + * Enable a Thesaurus + * + * @param \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus Thesaurus + * + * @return \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function enable(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus); + + /** + * Disable a Thesaurus + * + * @param \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus Thesaurus + * + * @return \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function disable(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus); } diff --git a/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Expansions.php b/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Expansions.php index 6ad40d097..dd5239517 100644 --- a/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Expansions.php +++ b/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Expansions.php @@ -33,7 +33,7 @@ protected function _construct() $this->addColumn('reference_term', ['label' => __('Reference Term')]); $this->addColumn('values', ['label' => __('Expansion terms')]); $this->_addAfter = false; - $this->_addButtonLabel = __('Add Expansion'); + $this->_addButtonLabel = __('Add Expansion Rule'); parent::_construct(); } diff --git a/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Synonyms.php b/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Synonyms.php index 9032b0e50..00ea7e7a7 100644 --- a/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Synonyms.php +++ b/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Renderer/Synonyms.php @@ -41,7 +41,7 @@ protected function _construct() ); $this->_addAfter = false; - $this->_addButtonLabel = __('Add Synonym'); + $this->_addButtonLabel = __('Add Synonym Rule'); parent::_construct(); } diff --git a/src/module-elasticsuite-thesaurus/Controller/Adminhtml/Thesaurus/MassDisable.php b/src/module-elasticsuite-thesaurus/Controller/Adminhtml/Thesaurus/MassDisable.php new file mode 100644 index 000000000..8cf3d4cd8 --- /dev/null +++ b/src/module-elasticsuite-thesaurus/Controller/Adminhtml/Thesaurus/MassDisable.php @@ -0,0 +1,62 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteThesaurus\Controller\Adminhtml\Thesaurus; + +use Smile\ElasticsuiteThesaurus\Controller\Adminhtml\AbstractThesaurus as ThesaurusController; + +/** + * Massive disable action for Thesaurus + * + * @category Smile + * @package Smile\ElasticsuiteThesaurus + * @author Vadym Honcharuk + */ +class MassDisable extends ThesaurusController +{ + /** + * Disable selected Thesaurus + * + * @return \Magento\Backend\Model\View\Result\Redirect + */ + public function execute() + { + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + + $thesauriIds = $this->getRequest()->getParam('thesauri'); + if (!is_array($thesauriIds)) { + $this->messageManager->addError(__('Please select thesaurus to disable.')); + + return $resultRedirect->setPath('*/*/index'); + } + + try { + foreach ($thesauriIds as $thesaurusId) { + $model = $this->thesaurusFactory->create(); + $model->load($thesaurusId); + if (!$model->getThesaurusId()) { + $this->messageManager->addError(__('This thesaurus no longer exists.')); + + return $resultRedirect->setPath('*/*/index'); + } + $this->thesaurusRepository->disable($model); + } + $this->messageManager->addSuccess(__('Total of %1 thesaurus were disabled.', count($thesauriIds))); + } catch (\Exception $e) { + $this->messageManager->addError($e->getMessage()); + } + + return $resultRedirect->setPath('*/*/index'); + } +} diff --git a/src/module-elasticsuite-thesaurus/Controller/Adminhtml/Thesaurus/MassEnable.php b/src/module-elasticsuite-thesaurus/Controller/Adminhtml/Thesaurus/MassEnable.php new file mode 100644 index 000000000..ce881a3cc --- /dev/null +++ b/src/module-elasticsuite-thesaurus/Controller/Adminhtml/Thesaurus/MassEnable.php @@ -0,0 +1,62 @@ + + * @copyright 2023 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteThesaurus\Controller\Adminhtml\Thesaurus; + +use Smile\ElasticsuiteThesaurus\Controller\Adminhtml\AbstractThesaurus as ThesaurusController; + +/** + * Massive enable action for Thesaurus + * + * @category Smile + * @package Smile\ElasticsuiteThesaurus + * @author Vadym Honcharuk + */ +class MassEnable extends ThesaurusController +{ + /** + * Enable selected Thesaurus + * + * @return \Magento\Backend\Model\View\Result\Redirect + */ + public function execute() + { + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + + $thesauriIds = $this->getRequest()->getParam('thesauri'); + if (!is_array($thesauriIds)) { + $this->messageManager->addError(__('Please select thesaurus to enable.')); + + return $resultRedirect->setPath('*/*/index'); + } + + try { + foreach ($thesauriIds as $thesaurusId) { + $model = $this->thesaurusFactory->create(); + $model->load($thesaurusId); + if (!$model->getThesaurusId()) { + $this->messageManager->addError(__('This thesaurus no longer exists.')); + + return $resultRedirect->setPath('*/*/index'); + } + $this->thesaurusRepository->enable($model); + } + $this->messageManager->addSuccess(__('Total of %1 thesaurus were enabled.', count($thesauriIds))); + } catch (\Exception $e) { + $this->messageManager->addError($e->getMessage()); + } + + return $resultRedirect->setPath('*/*/index'); + } +} diff --git a/src/module-elasticsuite-thesaurus/Model/Index.php b/src/module-elasticsuite-thesaurus/Model/Index.php index 042192caa..1613a315b 100644 --- a/src/module-elasticsuite-thesaurus/Model/Index.php +++ b/src/module-elasticsuite-thesaurus/Model/Index.php @@ -216,12 +216,13 @@ private function getConfig(ContainerConfigurationInterface $containerConfig) */ private function getSynonymRewrites($storeId, $queryText, $type, $maxRewrites) { - $indexName = $this->getIndexAlias($storeId); - $analyzedQueries = $this->getQueryCombinations($storeId, $queryText); - $synonymByPositions = []; - $synonyms = []; + $indexName = $this->getIndexAlias($storeId); + $analyzedQueries = $this->getQueryCombinations($storeId, $queryText); + $synonyms = []; foreach ($analyzedQueries as $query) { + $synonymByPositions = []; + try { $analysis = $this->client->analyze( ['index' => $indexName, 'body' => ['text' => (string) $query, 'analyzer' => $type]] @@ -234,7 +235,12 @@ private function getSynonymRewrites($storeId, $queryText, $type, $maxRewrites) if ($token['type'] == 'SYNONYM') { $positionKey = sprintf('%s_%s', $token['start_offset'], $token['end_offset']); $token['token'] = str_replace('_', ' ', $token['token']); - $synonymByPositions[$positionKey][] = $token; + // Prevent a token already contained in the query to be added. + // Eg : you have a synonym between "dress" and "red dress". + // If someone search for "red dress", you don't want the final query to be "red red dress". + if (array_search($token['token'], str_replace('_', ' ', $analyzedQueries)) === false) { + $synonymByPositions[$positionKey][] = $token; + } } } // Use + instead of array_merge because keys of the array can be purely numeric and would be casted to 0 by array_merge. diff --git a/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php b/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php index 436c3d1f6..781caf6ad 100644 --- a/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php +++ b/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php @@ -176,10 +176,10 @@ private function saveStoreRelation(\Magento\Framework\Model\AbstractModel $objec */ private function saveTermsRelation(\Magento\Framework\Model\AbstractModel $object) { - $termRelations = $object->getTermsRelations(); + $termRelations = $object->getTermsRelations() ?? []; $termRelations = array_filter($termRelations); - if (is_array($termRelations) && (count($termRelations) > 0)) { + if (count($termRelations) > 0) { $expansionTermLinks = []; $referenceTermLinks = []; diff --git a/src/module-elasticsuite-thesaurus/Model/ThesaurusRepository.php b/src/module-elasticsuite-thesaurus/Model/ThesaurusRepository.php index 6ea4571cf..391cddab3 100644 --- a/src/module-elasticsuite-thesaurus/Model/ThesaurusRepository.php +++ b/src/module-elasticsuite-thesaurus/Model/ThesaurusRepository.php @@ -152,6 +152,38 @@ public function delete(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface return $thesaurus; } + /** + * Enable a thesaurus + * + * @param \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus Thesaurus data + * + * @return \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function enable(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus) + { + $thesaurus->setIsActive(true); + $thesaurus->save(); + + return $thesaurus; + } + + /** + * Disable a thesaurus + * + * @param \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus Thesaurus data + * + * @return \Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function disable(\Smile\ElasticsuiteThesaurus\Api\Data\ThesaurusInterface $thesaurus) + { + $thesaurus->setIsActive(false); + $thesaurus->save(); + + return $thesaurus; + } + /** * Validate thesaurus values * diff --git a/src/module-elasticsuite-thesaurus/Plugin/QueryRewrite.php b/src/module-elasticsuite-thesaurus/Plugin/QueryRewrite.php index b3faea7c8..7811f2dfc 100644 --- a/src/module-elasticsuite-thesaurus/Plugin/QueryRewrite.php +++ b/src/module-elasticsuite-thesaurus/Plugin/QueryRewrite.php @@ -126,10 +126,8 @@ private function getWeightedRewrites($queryText, $containerConfig, $originalBoos } foreach ($queryText as $currentQueryText) { - $rewrites = array_merge( - $rewrites, - $this->index->getQueryRewrites($containerConfig, $currentQueryText, $originalBoost) - ); + // Use + instead of array_merge because $queryText can be purely numeric and would be casted to 0 by array_merge. + $rewrites = $rewrites + $this->index->getQueryRewrites($containerConfig, $currentQueryText, $originalBoost); } return $rewrites; diff --git a/src/module-elasticsuite-thesaurus/i18n/en_US.csv b/src/module-elasticsuite-thesaurus/i18n/en_US.csv index ff25f1a81..57eefe7b3 100644 --- a/src/module-elasticsuite-thesaurus/i18n/en_US.csv +++ b/src/module-elasticsuite-thesaurus/i18n/en_US.csv @@ -18,9 +18,9 @@ Store,Store Synonyms,Synonyms "Reference Term","Reference Term" "Expansion terms","Expansion terms" -"Add Expansion","Add Expansion" +"Add Expansion Rule","Add Expansion Rule" "Synonym terms","Synonym terms" -"Add Synonym","Add Synonym" +"Add Synonym Rule","Add Synonym Rule" "This thesaurus no longer exists.","This thesaurus no longer exists." "You deleted the thesaurus %1.","You deleted the thesaurus %1." "Edit %1 (%2)","Edit %1 (%2)" @@ -48,3 +48,18 @@ Status,Status Inactive,Inactive Action,Action Edit,Edit +Any,Any +Mass Actions,Mass Actions +Enable,Enable +Disable,Disable +Select All,Select All +Unselect All,Unselect All +Select Visible,Select Visible +Unselect Visible,Unselect Visible +"Are you sure you want to enable the selected thesaurus?","Are you sure you want to enable the selected thesaurus?" +"Are you sure you want to disable the selected thesaurus?","Are you sure you want to disable the selected thesaurus?" +"Are you sure you want to delete the selected thesaurus?","Are you sure you want to delete the selected thesaurus?" +"Please select thesaurus to disable.","Please select thesaurus to disable." +"Total of %1 thesaurus were disabled.","Total of %1 thesaurus were disabled." +"Please select thesaurus to enable.","Please select thesaurus to enable." +"Total of %1 thesaurus were enabled.","Total of %1 thesaurus were enabled." diff --git a/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv b/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv index 321044b9a..d133accbf 100644 --- a/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv +++ b/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv @@ -1,8 +1,8 @@ Thesaurus,Thesaurus -"Add New Thesaurus","Add New Thesaurus" -"Create Thesaurus","Create Thesaurus" -"Create a Thesaurus","Create a Thesaurus" -Synonym,Synonym +"Add New Thesaurus","Ajouter un nouveau Thésaurus" +"Create Thesaurus","Créer Thésaurus" +"Create a Thesaurus","Créer un Thésaurus" +Synonym,Synonyme Expansion,Expansion Type,Type "Thesaurus Type","Type de thésaurus" @@ -18,9 +18,9 @@ Store,Vue magasin Synonyms,Synonymes "Reference Term","Terme de référence" "Expansion terms","Termes étendus" -"Add Expansion","Ajouter une expansion" +"Add Expansion Rule","Ajouter une règle d'expansion" "Synonym terms","Termes synonymes" -"Add Synonym","Ajouter un synonyme" +"Add Synonym Rule","Ajouter une liste de synonymes" "This thesaurus no longer exists.","Ce thésaurus n'existe plus." "You deleted the thesaurus %1.","Vous avez supprimé le thésaurus %1." "Edit %1 (%2)","Édition : %1 (%2)" @@ -48,3 +48,18 @@ Status,Status Inactive,Désactivé Action,Action Edit,Editer +Any,Tous +Mass Actions,Actions de masse +Enable,Activer +Disable,Désactiver +Select All,Tout sélectionner +Unselect All,Tout déselectionner +Select Visible,Sélectionner visibles +Unselect Visible,Désectionner visibles +"Are you sure you want to enable the selected thesaurus?","Êtes-vous sûr(e) de vouloir activer les thésaurus sélectionnés ?" +"Are you sure you want to disable the selected thesaurus?","Êtes-vous sûr(e) de vouloir désactiver les thésaurus sélectionnés ?" +"Are you sure you want to delete the selected thesaurus?","Êtes-vous sûr(e) de vouloir supprimer les thésaurus sélectionnés ?" +"Please select thesaurus to disable.","Veuillez sélectionner un thésaurus à désactiver." +"Total of %1 thesaurus were disabled.","Un total de %1 thesaurus ont été désactivés." +"Please select thesaurus to enable.","Veuillez sélectionner un thésaurus à activer." +"Total of %1 thesaurus were enabled.","Un total de %1 thesaurus ont été activés." diff --git a/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml b/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml index 6824dbb9c..a25a36597 100644 --- a/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml +++ b/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml @@ -44,14 +44,25 @@ thesauri 1 + + Enable + */*/massEnable + Are you sure you want to enable the selected thesaurus? + + + Disable + */*/massDisable + Are you sure you want to disable the selected thesaurus? + Delete */*/massDelete - Are you sure? + Are you sure you want to delete the selected thesaurus? + diff --git a/src/module-elasticsuite-tracker/Api/CustomerTrackingServiceInterface.php b/src/module-elasticsuite-tracker/Api/CustomerTrackingServiceInterface.php index 4abb270b3..a53387ae9 100644 --- a/src/module-elasticsuite-tracker/Api/CustomerTrackingServiceInterface.php +++ b/src/module-elasticsuite-tracker/Api/CustomerTrackingServiceInterface.php @@ -36,7 +36,7 @@ public function hit($eventData): void; * * @param array $eventData The event Data * - * @return mixed + * @return void */ public function addEvent($eventData); diff --git a/src/module-elasticsuite-tracker/Block/Variables/Page/AbstractBlock.php b/src/module-elasticsuite-tracker/Block/Variables/Page/AbstractBlock.php index eebc25574..2852f0224 100644 --- a/src/module-elasticsuite-tracker/Block/Variables/Page/AbstractBlock.php +++ b/src/module-elasticsuite-tracker/Block/Variables/Page/AbstractBlock.php @@ -13,7 +13,10 @@ */ namespace Smile\ElasticsuiteTracker\Block\Variables\Page; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; +use Smile\ElasticsuiteTracker\Helper\Data as TrackerHelper; /** * Abstract block for page tracking, inherited by all other page tracking blocks @@ -27,21 +30,21 @@ class AbstractBlock extends \Smile\ElasticsuiteTracker\Block\Variables\AbstractB /** * Set the default template for page variable blocks * - * @param Template\Context $context The template context - * @param \Magento\Framework\Json\Helper\Data $jsonHelper The Magento's JSON Helper - * @param \Smile\ElasticsuiteTracker\Helper\Data $trackerHelper The Smile Tracker helper - * @param \Magento\Framework\Registry $registry Magento Core Registry - * @param array $data The block data + * @param Template\Context $context The template context + * @param Data $jsonHelper The Magento's JSON Helper + * @param TrackerHelper $trackerHelper The Smile Tracker helper + * @param Registry $registry Magento Core Registry + * @param array $data The block data */ public function __construct( Template\Context $context, - \Magento\Framework\Json\Helper\Data $jsonHelper, - \Smile\ElasticsuiteTracker\Helper\Data $trackerHelper, - \Magento\Framework\Registry $registry, + Data $jsonHelper, + TrackerHelper $trackerHelper, + Registry $registry, array $data = [] ) { $data['template'] = 'Smile_ElasticsuiteTracker::/variables/page.phtml'; - return parent::__construct($context, $jsonHelper, $trackerHelper, $registry, $data); + parent::__construct($context, $jsonHelper, $trackerHelper, $registry, $data); } } diff --git a/src/module-elasticsuite-tracker/Block/Variables/Page/Base.php b/src/module-elasticsuite-tracker/Block/Variables/Page/Base.php index 1f57fc5e5..4ba8281c6 100644 --- a/src/module-elasticsuite-tracker/Block/Variables/Page/Base.php +++ b/src/module-elasticsuite-tracker/Block/Variables/Page/Base.php @@ -13,8 +13,13 @@ */ namespace Smile\ElasticsuiteTracker\Block\Variables\Page; -use Magento\Framework\App\Cache\Type; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Layout\PageType\Config as PageTypeConfig; +use Smile\ElasticsuiteTracker\Helper\Data as TrackerHelper; /** * Base variables block for page tracking, exposes all base tracking variables @@ -23,52 +28,52 @@ * @package Smile\ElasticsuiteTracker * @author Romain Ruaud */ -class Base extends \Smile\ElasticsuiteTracker\Block\Variables\Page\AbstractBlock +class Base extends AbstractBlock { /** - * @var \Magento\Framework\View\Layout\PageType\Config The page type configuration + * @var PageTypeConfig The page type configuration */ private $pageTypeConfig; /** * Magento Locale Resolver * - * @var \Magento\Framework\Locale\ResolverInterface + * @var ResolverInterface */ protected $localeResolver; /** - * @var \Magento\Framework\App\RequestInterface + * @var RequestInterface */ protected $requestInterface; /** * Set the default template for page variable blocks * - * @param Template\Context $context The template context - * @param \Magento\Framework\Json\Helper\Data $jsonHelper The Magento's JSON Helper - * @param \Smile\ElasticsuiteTracker\Helper\Data $trackerHelper The Smile Tracker helper - * @param \Magento\Framework\Registry $registry Magento Core Registry - * @param \Magento\Framework\View\Layout\PageType\Config $pageTypeConfig The page type configuration - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver Locale Resolver - * @param \Magento\Framework\App\RequestInterface $requestInterface RequestInterface - * @param array $data The block data + * @param Template\Context $context The template context + * @param Data $jsonHelper The Magento's JSON Helper + * @param TrackerHelper $trackerHelper The Smile Tracker helper + * @param Registry $registry Magento Core Registry + * @param PageTypeConfig $pageTypeConfig The page type configuration + * @param ResolverInterface $localeResolver Locale Resolver + * @param RequestInterface $requestInterface RequestInterface + * @param array $data The block data */ public function __construct( Template\Context $context, - \Magento\Framework\Json\Helper\Data $jsonHelper, - \Smile\ElasticsuiteTracker\Helper\Data $trackerHelper, - \Magento\Framework\Registry $registry, - \Magento\Framework\View\Layout\PageType\Config $pageTypeConfig, - \Magento\Framework\Locale\ResolverInterface $localeResolver, - \Magento\Framework\App\RequestInterface $requestInterface, + Data $jsonHelper, + TrackerHelper $trackerHelper, + Registry $registry, + PageTypeConfig $pageTypeConfig, + ResolverInterface $localeResolver, + RequestInterface $requestInterface, array $data = [] ) { $this->pageTypeConfig = $pageTypeConfig; $this->localeResolver = $localeResolver; $this->requestInterface = $requestInterface; - return parent::__construct($context, $jsonHelper, $trackerHelper, $registry, $data); + parent::__construct($context, $jsonHelper, $trackerHelper, $registry, $data); } /** diff --git a/src/module-elasticsuite-tracker/Controller/Tracker/Hit.php b/src/module-elasticsuite-tracker/Controller/Tracker/Hit.php index 9517970d7..7b5740c12 100644 --- a/src/module-elasticsuite-tracker/Controller/Tracker/Hit.php +++ b/src/module-elasticsuite-tracker/Controller/Tracker/Hit.php @@ -49,6 +49,8 @@ public function __construct( /** * {@inheritDoc} + * + * @return void */ public function execute() { diff --git a/src/module-elasticsuite-tracker/Cron/CleanTrackingData.php b/src/module-elasticsuite-tracker/Cron/CleanTrackingData.php index 4f356bd83..163ace872 100644 --- a/src/module-elasticsuite-tracker/Cron/CleanTrackingData.php +++ b/src/module-elasticsuite-tracker/Cron/CleanTrackingData.php @@ -27,6 +27,11 @@ class CleanTrackingData */ private $indexManager; + /** + * @var \Smile\ElasticsuiteTracker\Helper\Data + */ + private $helper; + /** * Constructor. * diff --git a/src/module-elasticsuite-tracker/Model/IndexResolver.php b/src/module-elasticsuite-tracker/Model/IndexResolver.php index ed2673125..e947c0cda 100644 --- a/src/module-elasticsuite-tracker/Model/IndexResolver.php +++ b/src/module-elasticsuite-tracker/Model/IndexResolver.php @@ -29,31 +29,19 @@ class IndexResolver private $indices = []; /** - * @var \Smile\ElasticsuiteCore\Api\Index\IndexInterfaceFactory + * @var IndexManager */ - private $indexFactory; - - /** - * @var \Smile\ElasticsuiteCore\Api\Index\IndexSettingsInterface - */ - private $indexSettings; - + private $indexManager; /** * Constructor. * - * @param \Smile\ElasticsuiteCore\Api\Index\IndexSettingsInterface $indexSettings Index settings. - * @param \Smile\ElasticsuiteCore\Api\Index\IndexInterfaceFactory $indexFactory Index factory. - * @param \Smile\ElasticsuiteTracker\Model\IndexManager $indexManager Index Manager. + * @param \Smile\ElasticsuiteTracker\Model\IndexManager $indexManager Index Manager. */ public function __construct( - \Smile\ElasticsuiteCore\Api\Index\IndexSettingsInterface $indexSettings, - \Smile\ElasticsuiteCore\Api\Index\IndexInterfaceFactory $indexFactory, IndexManager $indexManager ) { - $this->indexFactory = $indexFactory; - $this->indexSettings = $indexSettings; - $this->indexManager = $indexManager; + $this->indexManager = $indexManager; } /** diff --git a/src/module-elasticsuite-tracker/view/frontend/layout/catalogsearch_result_index.xml b/src/module-elasticsuite-tracker/view/frontend/layout/catalogsearch_result_index.xml index 9626d9307..fca599acc 100644 --- a/src/module-elasticsuite-tracker/view/frontend/layout/catalogsearch_result_index.xml +++ b/src/module-elasticsuite-tracker/view/frontend/layout/catalogsearch_result_index.xml @@ -16,10 +16,11 @@ */ --> - - + diff --git a/src/module-elasticsuite-tracker/view/frontend/layout/checkout_onepage_success.xml b/src/module-elasticsuite-tracker/view/frontend/layout/checkout_onepage_success.xml index 90358e4e8..98566384b 100644 --- a/src/module-elasticsuite-tracker/view/frontend/layout/checkout_onepage_success.xml +++ b/src/module-elasticsuite-tracker/view/frontend/layout/checkout_onepage_success.xml @@ -16,10 +16,11 @@ */ --> - - + diff --git a/src/module-elasticsuite-tracker/view/frontend/layout/cms_page_view.xml b/src/module-elasticsuite-tracker/view/frontend/layout/cms_page_view.xml index 6b184664e..4a7abe775 100644 --- a/src/module-elasticsuite-tracker/view/frontend/layout/cms_page_view.xml +++ b/src/module-elasticsuite-tracker/view/frontend/layout/cms_page_view.xml @@ -18,7 +18,9 @@ - + diff --git a/src/module-elasticsuite-tracker/view/frontend/layout/default.xml b/src/module-elasticsuite-tracker/view/frontend/layout/default.xml index 7cce9bdfe..ea0b0275d 100644 --- a/src/module-elasticsuite-tracker/view/frontend/layout/default.xml +++ b/src/module-elasticsuite-tracker/view/frontend/layout/default.xml @@ -21,7 +21,10 @@ - + Smile_ElasticsuiteTracker/js/user-consent @@ -32,8 +35,12 @@ - - + + diff --git a/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php b/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php index a9ad954ef..f5b27fd4a 100644 --- a/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php +++ b/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php @@ -70,7 +70,7 @@ public function execute() $responseData = $this->getPreviewObject()->getData(); $json = $this->jsonHelper->jsonEncode($responseData); - $this->getResponse()->representJson($json); + return $this->getResponse()->representJson($json); } /** diff --git a/src/module-elasticsuite-virtual-category/Controller/Router.php b/src/module-elasticsuite-virtual-category/Controller/Router.php index d6caf2f1f..c83fe4621 100644 --- a/src/module-elasticsuite-virtual-category/Controller/Router.php +++ b/src/module-elasticsuite-virtual-category/Controller/Router.php @@ -15,6 +15,7 @@ namespace Smile\ElasticsuiteVirtualCategory\Controller; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder; use Magento\Framework\App\ActionFactory; use Magento\Framework\App\ActionInterface; use Magento\Framework\App\RequestInterface; @@ -106,7 +107,7 @@ public function match(RequestInterface $request): ?ActionInterface $this->virtualCategoryRoot->setAppliedRootCategory($appliedRoot); $productRewrite = $this->getProductRewrite($identifier); - if ($productRewrite) { + if ($productRewrite && $productRewrite->getEntityType() === UrlRewriteFinder::ENTITY_TYPE_PRODUCT) { $request->setAlias(UrlInterface::REWRITE_REQUEST_PATH_ALIAS, $productRewrite->getRequestPath()); $request->setPathInfo('/' . $productRewrite->getTargetPath()); diff --git a/src/module-elasticsuite-virtual-category/Helper/Rule.php b/src/module-elasticsuite-virtual-category/Helper/Rule.php index 2a4c62f41..aa2d72b5d 100644 --- a/src/module-elasticsuite-virtual-category/Helper/Rule.php +++ b/src/module-elasticsuite-virtual-category/Helper/Rule.php @@ -14,7 +14,9 @@ namespace Smile\ElasticsuiteVirtualCategory\Helper; +use Magento\Catalog\Api\CategoryRepositoryInterfaceFactory; use Magento\Catalog\Api\Data\CategoryInterface; +use Smile\ElasticsuiteVirtualCategory\Model\Category\Attribute\VirtualRule\ReadHandler; /** * Smile Elasticsuite virtual category cache helper. @@ -35,18 +37,34 @@ class Rule */ private $customerSession; + /** + * @var \Smile\ElasticsuiteVirtualCategory\Model\Category\Attribute\VirtualRule\ReadHandler + */ + private $readHandler; + + /** + * @var \Magento\Catalog\Api\CategoryRepositoryInterfaceFactory + */ + private $categoryRepository; + /** * Provider constructor. * - * @param \Magento\Framework\App\CacheInterface $cache Cache. - * @param \Magento\Customer\Model\Session $customerSession Customer session. + * @param \Magento\Framework\App\CacheInterface $cache Cache. + * @param \Magento\Customer\Model\Session $customerSession Customer session. + * @param ReadHandler $readHandler Rule read handler. + * @param \Magento\Catalog\Api\CategoryRepositoryInterfaceFactory $categoryRepository Category factory. */ public function __construct( \Magento\Framework\App\CacheInterface $cache, - \Magento\Customer\Model\Session $customerSession + \Magento\Customer\Model\Session $customerSession, + ReadHandler $readHandler, + CategoryRepositoryInterfaceFactory $categoryRepository ) { $this->cache = $cache; $this->customerSession = $customerSession; + $this->readHandler = $readHandler; + $this->categoryRepository = $categoryRepository; } /** @@ -73,6 +91,22 @@ public function loadUsingCache(CategoryInterface $category, $callback) if ($data === false) { $virtualRule = $category->getVirtualRule(); + + if (null === $virtualRule) { + // If virtual rule is null, probably the category himself was not properly loaded. + // So we load it through the repository and we ensure the readhandler will be called properly. + $repository = $this->categoryRepository->create(); + $category = $repository->get($category->getId(), $category->getStoreId()); + $category = $this->readHandler->execute($category); + $virtualRule = $category->getVirtualRule(); + } elseif (!is_object($virtualRule)) { + // If virtual rule is not an object, probably the rule was not properly loaded. + // @see https://github.com/Smile-SA/elasticsuite/issues/1985. + // In such cases, we go through the readHandler once again. + $category = $this->readHandler->execute($category); + $virtualRule = $category->getVirtualRule(); + } + $data = call_user_func_array([$virtualRule, $callback], [$category]); $cacheData = serialize($data); $this->cache->save($cacheData, $cacheKey, $category->getCacheTags()); diff --git a/src/module-elasticsuite-virtual-category/Model/Rule/Condition/Product.php b/src/module-elasticsuite-virtual-category/Model/Rule/Condition/Product.php index 5d3173a67..ceb5d2098 100644 --- a/src/module-elasticsuite-virtual-category/Model/Rule/Condition/Product.php +++ b/src/module-elasticsuite-virtual-category/Model/Rule/Condition/Product.php @@ -13,6 +13,7 @@ */ namespace Smile\ElasticsuiteVirtualCategory\Model\Rule\Condition; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\NoSuchEntityException; use Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Product\SpecialAttributesProvider; use Smile\ElasticsuiteCore\Search\Request\QueryInterface; @@ -50,6 +51,7 @@ class Product extends \Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Produc * @param \Magento\Framework\Locale\FormatInterface $localeFormat Locale format. * @param SpecialAttributesProvider $specialAttributesProvider Special attributes * provider. + * @param ScopeConfigInterface $scopeConfig Scope configuration. * @param \Smile\ElasticsuiteCore\Search\Request\Query\QueryFactory $queryFactory Search query factory. * @param array $data Additional data. */ @@ -65,6 +67,7 @@ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attrSetCollection, \Magento\Framework\Locale\FormatInterface $localeFormat, SpecialAttributesProvider $specialAttributesProvider, + ScopeConfigInterface $scopeConfig, \Smile\ElasticsuiteCore\Search\Request\Query\QueryFactory $queryFactory, array $data = [] ) { @@ -80,6 +83,7 @@ public function __construct( $attrSetCollection, $localeFormat, $specialAttributesProvider, + $scopeConfig, $data ); $this->queryFactory = $queryFactory; diff --git a/src/module-elasticsuite-virtual-category/Model/Url.php b/src/module-elasticsuite-virtual-category/Model/Url.php index cfa879c69..be4115012 100644 --- a/src/module-elasticsuite-virtual-category/Model/Url.php +++ b/src/module-elasticsuite-virtual-category/Model/Url.php @@ -18,6 +18,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\UrlInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\UrlRewrite\Model\UrlFinderInterface; @@ -66,6 +67,11 @@ class Url */ private $urlFinder; + /** + * @var UrlInterface + */ + private $urlBuilder; + /** * @var VirtualCategoryRoot */ @@ -78,6 +84,7 @@ class Url * @param StoreManagerInterface $storeManager Store Manager Interface * @param CategoryCollectionFactory $categoryCollectionFactory Category Collection Factory * @param UrlFinderInterface $urlFinder URL Finder + * @param UrlInterface $urlBuilder URL Builder * @param VirtualCategoryRoot $virtualCategoryRoot Virtual Category Root model */ public function __construct( @@ -85,13 +92,15 @@ public function __construct( StoreManagerInterface $storeManager, CategoryCollectionFactory $categoryCollectionFactory, UrlFinderInterface $urlFinder, + UrlInterface $urlBuilder, VirtualCategoryRoot $virtualCategoryRoot ) { $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; $this->categoryCollectionFactory = $categoryCollectionFactory; $this->urlFinder = $urlFinder; - $this->virtualCategoryRoot = $virtualCategoryRoot; + $this->urlBuilder = $urlBuilder; + $this->virtualCategoryRoot = $virtualCategoryRoot; } /** diff --git a/src/module-elasticsuite-virtual-category/Model/VirtualCategory/Root.php b/src/module-elasticsuite-virtual-category/Model/VirtualCategory/Root.php index 513bfad40..ca1c4dc7c 100644 --- a/src/module-elasticsuite-virtual-category/Model/VirtualCategory/Root.php +++ b/src/module-elasticsuite-virtual-category/Model/VirtualCategory/Root.php @@ -76,6 +76,7 @@ public function __construct( */ public function setAppliedRootCategory(CategoryInterface $category) { + $this->coreRegistry->unregister('applied_virtual_root_category'); $this->coreRegistry->register('applied_virtual_root_category', $category); return $this; @@ -181,9 +182,16 @@ public function getSubtreePathIds($appliedRoot, $category) */ public function useVirtualRootCategorySubtree($category) { - $rootCategory = $this->getVirtualCategoryRoot($category); + $useVirtualRootCategorySubtree = false; + if ($category->getIsVirtualCategory()) { + $rootCategory = $this->getVirtualCategoryRoot($category); - return ($rootCategory && $rootCategory->getId() && (bool) $category->getGenerateRootCategorySubtree()); + $useVirtualRootCategorySubtree = ( + $rootCategory && $rootCategory->getId() && (bool) $category->getGenerateRootCategorySubtree() + ); + } + + return $useVirtualRootCategorySubtree; } /** diff --git a/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/ChooserPlugin.php b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/ChooserPlugin.php index 070c2407d..7bfa25e28 100644 --- a/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/ChooserPlugin.php +++ b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/ChooserPlugin.php @@ -87,7 +87,7 @@ public function aroundExecute( $this->getIds($controller) ); - $controller->getResponse()->setBody($block->toHtml()); + return $controller->getResponse()->setBody($block->toHtml()); } /**