├── NOTICE.txt
├── docs
├── assets
│ ├── config-engine.png
│ ├── logo-app-search.png
│ └── config-credentials.png
└── MAGENTO-COULD.md
├── dev
├── images
│ └── magento
│ │ ├── etc
│ │ ├── php.ini
│ │ └── vhost.conf
│ │ ├── env.php
│ │ └── Dockerfile
├── magento.env.sample
└── tests
│ └── unit
│ └── phpunit.xml.dist
├── .gitignore
├── phpcs.xml
├── src
├── module-search
│ ├── registration.php
│ ├── etc
│ │ ├── config.xml
│ │ ├── module.xml
│ │ └── adminhtml
│ │ │ └── system.xml
│ └── Model
│ │ └── Search.php
├── framework-search
│ ├── registration.php
│ ├── SearchResponseBuilder.php
│ ├── Test
│ │ └── Unit
│ │ │ ├── ResponseTest.php
│ │ │ └── RequestTest.php
│ ├── Response.php
│ └── Request.php
├── module-synonyms
│ ├── registration.php
│ ├── etc
│ │ ├── search_engine.xml
│ │ ├── mview.xml
│ │ ├── module.xml
│ │ ├── indexer.xml
│ │ └── di.xml
│ └── Model
│ │ └── Indexer
│ │ ├── Mview
│ │ └── Action.php
│ │ ├── Plugin
│ │ └── SynonymGroupPlugin.php
│ │ └── IndexerHandlerFactory.php
├── framework-app-search
│ ├── registration.php
│ ├── Engine
│ │ ├── Field
│ │ │ ├── FieldValueMapperInterface.php
│ │ │ ├── ValueMapper
│ │ │ │ ├── TextValueMapper.php
│ │ │ │ ├── NumberValueMapper.php
│ │ │ │ └── AbstractValueMapper.php
│ │ │ ├── FieldTypeResolverInterface.php
│ │ │ ├── FieldMapperResolverInterface.php
│ │ │ ├── FieldTypeResolver.php
│ │ │ ├── FieldProviderInterface.php
│ │ │ ├── FieldNameResolverInterface.php
│ │ │ ├── FieldMapperInterface.php
│ │ │ ├── Config
│ │ │ │ ├── Config.php
│ │ │ │ ├── SchemaLocator.php
│ │ │ │ ├── Field.php
│ │ │ │ ├── Converter.php
│ │ │ │ └── FilesystemReader.php
│ │ │ ├── FieldMapperResolver.php
│ │ │ ├── FieldInterface.php
│ │ │ ├── FieldNameResolver.php
│ │ │ └── FieldProvider
│ │ │ │ └── CompositeFieldProvider.php
│ │ ├── SchemaProviderInterface.php
│ │ ├── Schema.php
│ │ ├── SchemaResolverInterface.php
│ │ ├── Schema
│ │ │ ├── BuilderInterface.php
│ │ │ ├── CompositeSchemaProvider.php
│ │ │ └── Builder.php
│ │ ├── SchemaInterface.php
│ │ ├── SchemaResolver.php
│ │ └── EngineNameResolver.php
│ ├── Document
│ │ ├── DataProviderInterface.php
│ │ ├── BatchDataMapperInterface.php
│ │ ├── BatchDataMapperResolverInterface.php
│ │ ├── BatchDataMapperResolver.php
│ │ └── SyncManagerInterface.php
│ ├── Client
│ │ ├── ConnectionManagerInterface.php
│ │ ├── ClientConfigurationInterface.php
│ │ └── ConnectionManager.php
│ ├── SearchAdapter
│ │ ├── RequestExecutor
│ │ │ ├── Facet
│ │ │ │ ├── Dynamic
│ │ │ │ │ └── AlgorithmInterface.php
│ │ │ │ ├── FacetBuilderInterface.php
│ │ │ │ └── DynamicRangeProvider.php
│ │ │ ├── RescorerResolverInterface.php
│ │ │ ├── SearchParamsProviderInterface.php
│ │ │ ├── Fulltext
│ │ │ │ ├── QueryTextResolverInterface.php
│ │ │ │ └── QueryTextResolver.php
│ │ │ ├── Filter
│ │ │ │ ├── FilterBuilderInterface.php
│ │ │ │ ├── QueryFilterBuilderInterface.php
│ │ │ │ ├── Filter
│ │ │ │ │ ├── WildcardFilterBuilder.php
│ │ │ │ │ ├── TermFilterBuilder.php
│ │ │ │ │ └── RangeFilterBuilder.php
│ │ │ │ ├── QueryFilter
│ │ │ │ │ ├── MatchQueryFilterBuilder.php
│ │ │ │ │ └── FilteredQueryFilterBuilder.php
│ │ │ │ ├── FilterBuilder.php
│ │ │ │ └── QueryFilterBuilder.php
│ │ │ ├── ResponseProcessorInterface.php
│ │ │ ├── QueryLocatorInterface.php
│ │ │ ├── Analytics
│ │ │ │ └── SearchParamsProvider.php
│ │ │ ├── RescorerInterface.php
│ │ │ ├── ResponseProcessor.php
│ │ │ ├── RescorerResolver.php
│ │ │ ├── QueryLocator.php
│ │ │ ├── Rescorer
│ │ │ │ ├── ResponseProcessor.php
│ │ │ │ └── ResultSorter.php
│ │ │ ├── EngineResolver.php
│ │ │ ├── Page
│ │ │ │ └── SearchParamsProvider.php
│ │ │ └── SearchParamsProvider.php
│ │ └── Response
│ │ │ ├── DocumentCountResolver.php
│ │ │ └── DocumentFactory.php
│ ├── EngineResolverInterface.php
│ ├── Test
│ │ └── Unit
│ │ │ ├── Engine
│ │ │ ├── SchemaTest.php
│ │ │ ├── Field
│ │ │ │ ├── ValueMapper
│ │ │ │ │ ├── NumberValueMapperTest.php
│ │ │ │ │ └── TextValueMapperTest.php
│ │ │ │ ├── FieldMapperResolverTest.php
│ │ │ │ ├── FieldTypeResolverTest.php
│ │ │ │ └── FieldNameResolverTest.php
│ │ │ ├── LanguageResolverTest.php
│ │ │ ├── SchemaResolverTest.php
│ │ │ └── EngineNameResolverTest.php
│ │ │ ├── SearchAdapter
│ │ │ ├── RequestExecutor
│ │ │ │ ├── Analytics
│ │ │ │ │ └── SearchParamsProviderTest.php
│ │ │ │ ├── Filter
│ │ │ │ │ ├── QueryFilter
│ │ │ │ │ │ └── MatchQueryFilterBuilderTest.php
│ │ │ │ │ ├── Filter
│ │ │ │ │ │ ├── TermFilterBuilderTest.php
│ │ │ │ │ │ └── RangeFilterBuilderTest.php
│ │ │ │ │ └── FilterBuilderTest.php
│ │ │ │ ├── Page
│ │ │ │ │ └── SearchParamsProviderTest.php
│ │ │ │ └── Facet
│ │ │ │ │ └── DynamicRangeProviderTest.php
│ │ │ └── Response
│ │ │ │ └── DocumentCountResolverTest.php
│ │ │ ├── EngineTest.php
│ │ │ ├── SearchAdapterTest.php
│ │ │ ├── Document
│ │ │ └── BatchDataMapperResolverTest.php
│ │ │ └── EngineResolverTest.php
│ ├── EngineInterface.php
│ ├── etc
│ │ └── app_search_fields.xsd
│ ├── SearchAdapter.php
│ ├── EngineManagerInterface.php
│ └── Engine.php
├── module-catalog-search
│ ├── registration.php
│ ├── etc
│ │ ├── config.xml
│ │ ├── crontab.xml
│ │ ├── app_search_fields.xml
│ │ ├── module.xml
│ │ └── adminhtml
│ │ │ └── system.xml
│ ├── view
│ │ └── frontend
│ │ │ ├── layout
│ │ │ └── catalogsearch_result_index.xml
│ │ │ └── templates
│ │ │ └── log-clicktrough.phtml
│ └── Model
│ │ ├── Product
│ │ ├── Document
│ │ │ └── BatchDataMapper.php
│ │ └── Engine
│ │ │ └── Field
│ │ │ └── FieldNameResolver.php
│ │ └── ResourceModel
│ │ └── Engine.php
└── module-catalog-graph-ql
│ ├── registration.php
│ ├── etc
│ └── module.xml
│ └── Model
│ └── ResourceModel
│ └── Product
│ └── SearchResultCollection.php
├── docker-compose.yml
└── composer.json
/NOTICE.txt:
--------------------------------------------------------------------------------
1 | Elastic App Search Magento module.
2 |
3 | Copyright 2012-2019 Elasticsearch B.V.
--------------------------------------------------------------------------------
/docs/assets/config-engine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elastic/app-search-magento/master/docs/assets/config-engine.png
--------------------------------------------------------------------------------
/docs/assets/logo-app-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elastic/app-search-magento/master/docs/assets/logo-app-search.png
--------------------------------------------------------------------------------
/docs/assets/config-credentials.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elastic/app-search-magento/master/docs/assets/config-credentials.png
--------------------------------------------------------------------------------
/dev/images/magento/etc/php.ini:
--------------------------------------------------------------------------------
1 | ; This file is created automatically by the docker build
2 |
3 | memory_limit = ${PHP_MEMORY_LIMIT}
4 |
5 | opcache.enable=1
6 | opcache.enable_cli=1
7 | opcache.memory_consumption=256
8 | opcache.max_accelerated_files=130986
9 | opcache.revalidate_freq=2
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Standard composer ignored paths
2 | composer.phar
3 | /vendor/
4 | composer.lock
5 |
6 | # Ignore devbox environment file
7 | dev/magento.env
8 |
9 | # Standard IDEs ignored paths
10 | .metadata
11 | *.ide/
12 | *.tmp
13 | *.bak
14 | *.swp
15 | *~.nib
16 | local.properties
17 | .settings/
18 | .loadpath
19 | .project
20 | .buildpath
21 | .idea/
22 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | The coding standard for Elastic App Search Client.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/module-search/registration.php:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/dev/magento.env.sample:
--------------------------------------------------------------------------------
1 | # MySQL Database configuration
2 | MYSQL_HOST=mysql
3 | MYSQL_DATABASE=magento_db
4 | MYSQL_USER=magento_user
5 | MYSQL_PASSWORD=magento_password
6 | MYSQL_RANDOM_ROOT_PASSWORD=1
7 | MYSQL_PREFIX=mage_
8 |
9 | # Redis configuration
10 | REDIS_HOST=redis
11 | REDIS_PORT=6379
12 | REDIS_CACHE_DB=1
13 | REDIS_FPC_DB=2
14 |
15 | # AppSearch configuration
16 | #CONFIG__DEFAULT__CATALOG_SEARCH_ENGINE=elastic_appsearch
17 | #CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__API_ENDPOINT=https://host-changeme
18 | #CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__PRIVATE_API_KEY=private-changeme
19 | #CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__SEARCH_API_KEY=search-changeme
20 | #CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__ENGINE_PREFIX=magento2
21 |
--------------------------------------------------------------------------------
/src/module-search/etc/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 | magento2
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/module-search/etc/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/FieldValueMapperInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/ValueMapper/NumberValueMapper.php:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/module-catalog-search/etc/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 | 1
22 | 1
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/module-catalog-search/etc/crontab.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
19 |
20 |
21 | 0 * * * *
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/framework-app-search/Document/DataProviderInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/FieldMapperResolverInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/framework-app-search/Document/BatchDataMapperInterface.php:
--------------------------------------------------------------------------------
1 | getType() ?: SchemaInterface::FIELD_TYPE_TEXT;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/framework-app-search/Document/BatchDataMapperResolverInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 | Search Synonyms
20 | Rebuild search engine synonyms
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/RescorerResolverInterface.php:
--------------------------------------------------------------------------------
1 | fields = $fields;
35 | }
36 |
37 | /**
38 | * {@inheritDoc}
39 | */
40 | public function getFields(): array
41 | {
42 | return $this->fields;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/SchemaResolverInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/ResponseProcessorInterface.php:
--------------------------------------------------------------------------------
1 | assertEquals($fields, $schema->getFields());
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/framework-search/SearchResponseBuilder.php:
--------------------------------------------------------------------------------
1 | setTotalCount($response->count());
33 |
34 | return $searchResult;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 |
5 | mysql:
6 | image: mysql:5.7
7 | env_file:
8 | - ./dev/magento.env
9 |
10 | redis:
11 | image: redis:5
12 |
13 | magento:
14 | build: ./dev/images/magento
15 | volumes:
16 | - .:/var/www/magento/app-search-module
17 | env_file:
18 | - ./dev/magento.env
19 | ports:
20 | - 8888:80
21 |
22 | elasticsearch:
23 | image: docker.elastic.co/elasticsearch/elasticsearch:7.2.0
24 | environment:
25 | - "node.name=es-node"
26 | - "discovery.type=single-node"
27 | - "cluster.name=app-search-docker-cluster"
28 | - "bootstrap.memory_lock=true"
29 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
30 | ulimits:
31 | memlock:
32 | soft: -1
33 | hard: -1
34 | volumes:
35 | - esdata:/usr/share/elasticsearch/data
36 |
37 | app-search:
38 | image: docker.elastic.co/app-search/app-search:7.2.0
39 | environment:
40 | - "elasticsearch.host=http://elasticsearch:9200"
41 | - "allow_es_settings_modification=true"
42 | - "app_search.listen_host=0.0.0.0"
43 | ports:
44 | - 3002:3002
45 |
46 | volumes:
47 | esdata:
48 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/QueryLocatorInterface.php:
--------------------------------------------------------------------------------
1 | getConstructArguments(Response::class, ['documents' => [], 'count' => 1]);
33 | $response = $objectManager->getObject(Response::class, $args);
34 |
35 | $this->assertEquals(1, $response->count());
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Schema/BuilderInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 | ../../../vendor/elastic/app-search-magento/src/*/Test/Unit
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ../../../vendor/elastic/app-search-magento/src/*
20 |
21 | ../../../vendor/elastic/app-search-magento/src/*/Test
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Filter/Filter/WildcardFilterBuilder.php:
--------------------------------------------------------------------------------
1 | getField())
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/SchemaInterface.php:
--------------------------------------------------------------------------------
1 | createMock(\Magento\Framework\Api\SortOrder::class);
33 |
34 | $args = $objectManager->getConstructArguments(Request::class, ['sort' => [$sortOrder]]);
35 | $request = $objectManager->getObject(Request::class, $args);
36 |
37 | $this->assertCount(1, $request->getSort());
38 | $this->assertEquals($sortOrder, current($request->getSort()));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/Engine/Field/ValueMapper/NumberValueMapperTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expectedResult, $valueMapper->mapValue($sourceValue));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Analytics/SearchParamsProvider.php:
--------------------------------------------------------------------------------
1 | [self::TAGS_PARAM_NAME => [$request->getName()]]];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/FieldMapperInterface.php:
--------------------------------------------------------------------------------
1 | assertEquals($expectedResult, $valueMapper->mapValue($sourceValue));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/framework-app-search/etc/app_search_fields.xsd:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/framework-search/Response.php:
--------------------------------------------------------------------------------
1 | count = $count;
41 | }
42 |
43 | /**
44 | * {@inheritDoc}
45 | */
46 | public function count()
47 | {
48 | return $this->count;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/dev/images/magento/etc/vhost.conf:
--------------------------------------------------------------------------------
1 |
2 | # The ServerName directive sets the request scheme, hostname and port that
3 | # the server uses to identify itself. This is used when creating
4 | # redirection URLs. In the context of virtual hosts, the ServerName
5 | # specifies what hostname must appear in the request's Host: header to
6 | # match this virtual host. For the default virtual host (this file) this
7 | # value is not decisive as it is used as a last resort host regardless.
8 | # However, you must set it for any further virtual host explicitly.
9 | #ServerName www.example.com
10 |
11 | ServerAdmin webmaster@localhost
12 | DocumentRoot /var/www/magento/pub
13 |
14 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
15 | # error, crit, alert, emerg.
16 | # It is also possible to configure the loglevel for particular
17 | # modules, e.g.
18 | #LogLevel info ssl:warn
19 |
20 | ErrorLog ${APACHE_LOG_DIR}/error.log
21 | CustomLog ${APACHE_LOG_DIR}/access.log combined
22 |
23 | # For most configuration files from conf-available/, which are
24 | # enabled or disabled at a global level, it is possible to
25 | # include a line for only one particular virtual host. For example the
26 | # following line enables the CGI configuration for this host only
27 | # after it has been globally disabled with "a2disconf".
28 | #Include conf-available/serve-cgi-bin.conf
29 |
30 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/RescorerInterface.php:
--------------------------------------------------------------------------------
1 | indexerRegistry = $indexerRegistry;
39 | }
40 |
41 | /**
42 | * {@inheritDoc}
43 | */
44 | public function execute($ids)
45 | {
46 | $indexer = $this->indexerRegistry->get(Indexer::INDEXER_ID);
47 | $indexer->invalidate();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/ResponseProcessor.php:
--------------------------------------------------------------------------------
1 | processors = $processors;
37 | }
38 |
39 | /**
40 | * {@inheritDoc}
41 | */
42 | public function process(RequestInterface $request, array $response): array
43 | {
44 | foreach ($this->processors as $processor) {
45 | $response = $processor->process($request, $response);
46 | }
47 |
48 | return $response;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/framework-app-search/Document/BatchDataMapperResolver.php:
--------------------------------------------------------------------------------
1 | mappers = $mappers;
37 | }
38 |
39 | /**
40 | * {@inheritDoc}
41 | */
42 | public function getMapper(string $engineIdentifier): BatchDataMapperInterface
43 | {
44 | if (!isset($this->mappers[$engineIdentifier])) {
45 | throw new LocalizedException(__('Could not localize batch data mapper for engine %1', $engineIdentifier));
46 | }
47 |
48 | return $this->mappers[$engineIdentifier];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/Config/Config.php:
--------------------------------------------------------------------------------
1 | createMock(RequestInterface::class);
31 | $request->method('getName')->willReturn('request_name');
32 |
33 | $paramsProvider = new SearchParamsProvider();
34 |
35 | $params = $paramsProvider->getParams($request);
36 |
37 | $this->assertEquals(['analytics' => ['tags' => ['request_name']]], $params);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/EngineTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($identifier, $engine->getIdentifier());
40 | $this->assertEquals($storeId, $engine->getStoreId());
41 | $this->assertEquals($name, $engine->getName());
42 | $this->assertEquals($language, $engine->getLanguage());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/FieldMapperResolver.php:
--------------------------------------------------------------------------------
1 | fieldMappers = $fieldMappers;
37 | }
38 |
39 | /**
40 | * {@inheritDoc}
41 | */
42 | public function getFieldMapper(string $engineIdentifier): FieldMapperInterface
43 | {
44 | if (!isset($this->fieldMappers[$engineIdentifier])) {
45 | throw new LocalizedException(__('Unable to locate field mapper for engine %1', $engineIdentifier));
46 | }
47 |
48 | return $this->fieldMappers[$engineIdentifier];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapter/Response/DocumentCountResolverTest.php:
--------------------------------------------------------------------------------
1 | ['page' => ['total_results' => 10]]];
31 | $this->assertEquals(10, $resolver->getDocumentCount($response));
32 | }
33 |
34 | /**
35 | * Test 0 is returned when meta are missing into the response.
36 | */
37 | public function testEmptyResponse()
38 | {
39 | $resolver = new DocumentCountResolver();
40 | $response = [];
41 | $this->assertEquals(0, $resolver->getDocumentCount($response));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Fulltext/QueryTextResolver.php:
--------------------------------------------------------------------------------
1 | queryLocator = $queryLocator;
38 | }
39 |
40 | /**
41 | * {@inheritDoc}
42 | */
43 | public function getText(RequestInterface $request): string
44 | {
45 | $query = $this->queryLocator->getQuery($request);
46 |
47 | return $query ? (string) $query->getValue() : '';
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/FieldInterface.php:
--------------------------------------------------------------------------------
1 | rescorers = $rescorers;
39 | }
40 |
41 | /**
42 | * {@inheritDoc}
43 | */
44 | public function getRescorer(RequestInterface $request): RescorerInterface
45 | {
46 | return $this->rescorers[$request->getName()] ?? $this->rescorers['default'];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/module-synonyms/etc/di.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | - Elastic\AppSearch\Synonyms\Model\Indexer\IndexerHandler
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | - catalogsearch_fulltext
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/Engine/Field/FieldMapperResolverTest.php:
--------------------------------------------------------------------------------
1 | $this->createMock(FieldMapperInterface::class)]);
31 |
32 | $this->assertInstanceOf(FieldMapperInterface::class, $resolver->getFieldMapper('foo'));
33 | }
34 |
35 | /**
36 | * Test field mapper resolution.
37 | *
38 | * @expectedException \Magento\Framework\Exception\LocalizedException
39 | */
40 | public function testGetInvalidFieldMapper()
41 | {
42 | $resolver = new FieldMapperResolver();
43 | $resolver->getFieldMapper('foo');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/framework-app-search/Client/ClientConfigurationInterface.php:
--------------------------------------------------------------------------------
1 | getName() !== QueryLocatorInterface::FULLTEXT_QUERY_NAME) {
33 | throw new LocalizedException(
34 | __("Query can contains only one match query with name %1", QueryLocatorInterface::FULLTEXT_QUERY_NAME)
35 | );
36 | }
37 |
38 | return [];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/Engine/LanguageResolverTest.php:
--------------------------------------------------------------------------------
1 | createMock(ScopeConfigInterface::class);
39 |
40 | $scopeConfig->expects($this->any())->method('getValue')->willReturn($magentoLanguage);
41 |
42 | $languageResolver = new LanguageResolver($scopeConfig);
43 |
44 | $this->assertEquals($appSearchLanguage, $languageResolver->getLanguage(0));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Schema/CompositeSchemaProvider.php:
--------------------------------------------------------------------------------
1 | builder = $builder;
38 | $this->providers = $providers;
39 | }
40 |
41 | /**
42 | * {@inheritDoc}
43 | */
44 | public function getSchema(): SchemaInterface
45 | {
46 | foreach ($this->providers as $schemaProvider) {
47 | $schema = $schemaProvider->getSchema();
48 | $this->builder->addFields($schema->getFields());
49 | }
50 |
51 | return $this->builder->build();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/Engine/Field/FieldTypeResolverTest.php:
--------------------------------------------------------------------------------
1 | createMock(FieldInterface::class);
33 |
34 | $this->assertEquals('text', $resolver->getFieldType($field));
35 | }
36 |
37 | /**
38 | * Test type resolution when set in the field.
39 | */
40 | public function testGetFieldType($type = 'number')
41 | {
42 | $resolver = new FieldTypeResolver();
43 |
44 | $field = $this->createMock(FieldInterface::class);
45 | $field->method('getType')->willReturn($type);
46 |
47 | $this->assertEquals($type, $resolver->getFieldType($field));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/dev/images/magento/env.php:
--------------------------------------------------------------------------------
1 | [
5 | 'frontName' => 'admin'
6 | ],
7 | 'db' => [
8 | 'table_prefix' => '',
9 | 'connection' => [
10 | 'default' => [
11 | 'host' => $_ENV['MYSQL_HOST'],
12 | 'dbname' => $_ENV['MYSQL_DATABASE'],
13 | 'username' => $_ENV['MYSQL_USER'],
14 | 'password' => $_ENV['MYSQL_PASSWORD'],
15 | 'model' => 'mysql4',
16 | 'engine' => 'innodb',
17 | 'initStatements' => 'SET NAMES utf8;',
18 | 'active' => '1'
19 | ]
20 | ]
21 | ],
22 | 'session' => [
23 | 'save' => 'redis',
24 | 'redis' => [
25 | 'host' => $_ENV['REDIS_HOST'],
26 | 'port' => $_ENV['REDIS_PORT'],
27 | ]
28 | ],
29 | 'cache' => [
30 | 'frontend' => [
31 | 'default' => [
32 | 'backend' => 'Cm_Cache_Backend_Redis',
33 | 'backend_options' => [
34 | 'server' => $_ENV['REDIS_HOST'],
35 | 'port' => $_ENV['REDIS_PORT'],
36 | 'database' => $_ENV['REDIS_CACHE_DB'],
37 | ]
38 | ],
39 | 'page_cache' => [
40 | 'backend' => 'Cm_Cache_Backend_Redis',
41 | 'backend_options' => [
42 | 'server' => $_ENV['REDIS_HOST'],
43 | 'port' => $_ENV['REDIS_PORT'],
44 | 'database' => $_ENV['REDIS_FPC_DB'],
45 | ]
46 | ]
47 | ]
48 | ]
49 | ];
50 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapterTest.php:
--------------------------------------------------------------------------------
1 | createMock(RequestInterface::class);
31 |
32 | $requestExecutor = $this->createMock(RequestExecutor::class);
33 | $requestExecutor->expects($this->once())->method('execute')->willReturn([]);
34 |
35 | $responseBuilder = $this->createMock(ResponseBuilder::class);
36 | $responseBuilder->expects($this->once())->method('buildResponse');
37 |
38 | $searchAdapter = new SearchAdapter($requestExecutor, $responseBuilder);
39 |
40 | $searchResponse = $searchAdapter->query($request);
41 |
42 | $this->assertInstanceOf(ResponseInterface::class, $searchResponse);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/Config/SchemaLocator.php:
--------------------------------------------------------------------------------
1 | urnResolver = $urnResolver;
43 | }
44 |
45 | /**
46 | * {@inheritDoc}
47 | */
48 | public function getSchema()
49 | {
50 | return $this->urnResolver->getRealPath(self::XSD_FILE_PATH);
51 | }
52 |
53 | /**
54 | * {@inheritDoc}
55 | */
56 | public function getPerFileSchema()
57 | {
58 | return $this->urnResolver->getRealPath(self::XSD_FILE_PATH);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "elastic/app-search-magento",
3 | "description" : "Elastic App Search official integration for Magento 2",
4 | "homepage": "https://github.com/elastic/app-search-magento",
5 | "keywords" : [
6 | "search",
7 | "client",
8 | "elastic",
9 | "magento",
10 | "magento2"
11 | ],
12 | "license": "OSL-3.0",
13 | "type": "magento2-module",
14 | "authors" : [
15 | {"name" : "Aurélien FOUCRET", "email": "aurelien.foucret@elastic.co"}
16 | ],
17 | "require" : {
18 | "php" : "^7.1",
19 | "elastic/app-search" : "^1.0.0",
20 | "magento/framework": ">=101.0.0",
21 | "magento/module-catalog": ">=102.0.6"
22 | },
23 | "require-dev" : {
24 | "squizlabs/php_codesniffer": "@stable",
25 | "phpmd/phpmd": "@stable",
26 | "overtrue/phplint": "@stable"
27 | },
28 | "autoload" : {
29 | "psr-4" : {
30 | "Elastic\\AppSearch\\Framework\\Search\\" : "src/framework-search",
31 | "Elastic\\AppSearch\\Framework\\AppSearch\\" : "src/framework-app-search",
32 | "Elastic\\AppSearch\\Search\\" : "src/module-search",
33 | "Elastic\\AppSearch\\CatalogSearch\\" : "src/module-catalog-search",
34 | "Elastic\\AppSearch\\CatalogGraphQl\\" : "src/module-catalog-graph-ql",
35 | "Elastic\\AppSearch\\Synonyms\\" : "src/module-synonyms"
36 | },
37 | "files": [
38 | "src/framework-search/registration.php",
39 | "src/framework-app-search/registration.php",
40 | "src/module-search/registration.php",
41 | "src/module-catalog-search/registration.php",
42 | "src/module-catalog-graph-ql/registration.php",
43 | "src/module-synonyms/registration.php"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter.php:
--------------------------------------------------------------------------------
1 | requestExecutor = $requestExecutor;
46 | $this->responseBuilder = $responseBuilder;
47 | }
48 |
49 | /**
50 | * {@inheritDoc}
51 | */
52 | public function query(RequestInterface $request)
53 | {
54 | return $this->responseBuilder->buildResponse($this->requestExecutor->execute($request));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/module-catalog-search/etc/adminhtml/system.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Magento\Config\Model\Config\Source\Yesno
24 |
25 | elastic_appsearch
26 |
27 |
28 |
29 |
30 | validate-digits
31 |
32 | 1
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/module-catalog-search/view/frontend/layout/catalogsearch_result_index.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | catalogsearch_fulltext
23 | .product-item
24 | a.product-item-link, a.product-item-photo
25 | .price-box
26 | data-product-id
27 | quick_search_container
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/SchemaResolver.php:
--------------------------------------------------------------------------------
1 | providers = $providers;
42 | }
43 |
44 | /**
45 | * {@inheritDoc}
46 | */
47 | public function getSchema(string $engineIdentifier): SchemaInterface
48 | {
49 | if (!isset($this->schemas[$engineIdentifier]) && !isset($this->providers[$engineIdentifier])) {
50 | throw new LocalizedException(__('Could not localize schema for engine %1', $engineIdentifier));
51 | } elseif (!isset($this->schemas[$engineIdentifier])) {
52 | $this->schemas[$engineIdentifier] = $this->providers[$engineIdentifier]->getSchema();
53 | }
54 |
55 | return $this->schemas[$engineIdentifier];
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/Document/BatchDataMapperResolverTest.php:
--------------------------------------------------------------------------------
1 | getResolver()->getMapper("foo");
31 | $this->assertInstanceOf(BatchDataMapperInterface::class, $mapper);
32 | }
33 |
34 | /**
35 | * Check an exceptiion is thrown when an invalid mapper is requested.
36 | *
37 | * @expectedException \Magento\Framework\Exception\LocalizedException
38 | */
39 | public function testGetInvalidMapper()
40 | {
41 | $mapper = $this->getResolver()->getMapper("invalid");
42 | $this->assertInstanceOf(BatchDataMapperInterface::class, $mapper);
43 | }
44 |
45 | private function getResolver()
46 | {
47 | $mappers = ['foo' => $this->createMock(BatchDataMapperInterface::class)];
48 |
49 | return new BatchDataMapperResolver($mappers);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/framework-app-search/Document/SyncManagerInterface.php:
--------------------------------------------------------------------------------
1 | getName();
40 |
41 | if (isset($context['type']) && $this->useValueField($field, $context['type'])) {
42 | $fieldName = $fieldName . self::VALUE_SUFFIX;
43 | }
44 |
45 | return $fieldName;
46 | }
47 |
48 | /**
49 | * Check if the field need a _value suffix for the current context.
50 | *
51 | * @param FieldInterface $field
52 | * @param array $context
53 | *
54 | * @return boolean
55 | */
56 | private function useValueField(FieldInterface $field, string $type)
57 | {
58 | return $field->useValueField() && in_array($type, $this->valueFieldContexts);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/EngineResolverTest.php:
--------------------------------------------------------------------------------
1 | createMock(EngineInterface::class);
35 |
36 | $engineFactory = $this->createMock('Elastic\AppSearch\Framework\AppSearch\EngineInterfaceFactory');
37 | $engineFactory->expects($this->any())->method('create')->willReturn($engine);
38 |
39 | $engineNameResolver = $this->createMock(EngineNameResolver::class);
40 | $languageResolver = $this->createMock(LanguageResolver::class);
41 |
42 | $engineResolver = new EngineResolver($engineFactory, $engineNameResolver, $languageResolver);
43 |
44 | $result = $engineResolver->getEngine('engine_identifier', 1);
45 |
46 | $this->assertInstanceOf(EngineInterface::class, $result);
47 | $this->assertEquals($engine, $result);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Filter/Filter/TermFilterBuilder.php:
--------------------------------------------------------------------------------
1 | fieldMapper = $fieldMapper;
40 | }
41 |
42 | /**
43 | * {@inheritDoc}
44 | */
45 | public function getFilter(FilterInterface $filter): array
46 | {
47 | $context = ['type' => SchemaInterface::CONTEXT_FILTER];
48 |
49 | $filterdName = $this->fieldMapper->getFieldName($filter->getField(), $context);
50 | $filterValue = $this->fieldMapper->mapValue($filter->getField(), $filter->getValue());
51 |
52 | return [$filterdName => $filterValue];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/module-catalog-search/view/frontend/templates/log-clicktrough.phtml:
--------------------------------------------------------------------------------
1 |
17 |
18 | isEnabled()): ?>
19 |
20 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Schema/Builder.php:
--------------------------------------------------------------------------------
1 | schemaFactory = $schemaFactory;
43 | }
44 |
45 | /**
46 | * {@inheritDoc}
47 | */
48 | public function addField(string $fieldName, string $fieldType)
49 | {
50 | $this->fields[$fieldName] = $fieldType;
51 |
52 | return $this;
53 | }
54 |
55 | /**
56 | * {@inheritDoc}
57 | */
58 | public function addFields(array $fields)
59 | {
60 | $this->fields = array_merge($this->fields, $fields);
61 |
62 | return $this;
63 | }
64 |
65 | /**
66 | * {@inheritDoc}
67 | */
68 | public function build(): SchemaInterface
69 | {
70 | $schema = $this->schemaFactory->create(['fields' => $this->fields]);
71 |
72 | $this->fields = [];
73 |
74 | return $schema;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/framework-search/Request.php:
--------------------------------------------------------------------------------
1 | sort = $sort;
55 | }
56 |
57 | /**
58 | * Request sort orders.
59 | *
60 | * @return \Magento\Framework\Api\SortOrder[]
61 | */
62 | public function getSort()
63 | {
64 | return $this->sort;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/EngineNameResolver.php:
--------------------------------------------------------------------------------
1 | scopeConfig = $scopeConfig;
44 | $this->filter = $filter;
45 | }
46 |
47 | /**
48 | * Return real App Search engine name.
49 | *
50 | * @param string $engineIdentifier Engine identifier (e.g. catalogsearch_fulltext)
51 | * @param int $storeId Store id.
52 | *
53 | * @return string
54 | */
55 | public function getEngineName(string $engineIdentifier, int $storeId): string
56 | {
57 | $enginePrefix = (string) $this->scopeConfig->getValue('elastic_appsearch/client/engine_prefix');
58 | $engineName = sprintf('%s-%s-%s', $enginePrefix, $engineIdentifier, $storeId);
59 |
60 | return $this->filter->translitUrl($engineName);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/QueryLocator.php:
--------------------------------------------------------------------------------
1 | queryName = $queryName;
33 | }
34 |
35 | /**
36 | * {@inheritDoc}
37 | */
38 | public function getQuery(RequestInterface $request): ?QueryInterface
39 | {
40 | return $request->getQuery() ? $this->extractQuery($request->getQuery()) : null;
41 | }
42 |
43 | /**
44 | * Extract the fulltext query from a parent query.
45 | *
46 | * @param QueryInterface $query
47 | *
48 | * @return QueryInterface|NULL
49 | */
50 | private function extractQuery(QueryInterface $query): ?QueryInterface
51 | {
52 | $searchQuery = null;
53 |
54 | if ($query->getType() == QueryInterface::TYPE_BOOL) {
55 | $queries = array_merge($query->getMust(), $query->getShould());
56 | $searchQuery = current(array_filter(array_map([$this, 'extractQuery'], $queries))) ?: null;
57 | } elseif ($query->getName() == $this->queryName) {
58 | $searchQuery = $query;
59 | }
60 |
61 | return $searchQuery;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/Config/Field.php:
--------------------------------------------------------------------------------
1 | name = $name;
43 | $this->type = $type;
44 | }
45 |
46 | /**
47 | * {@inheritDoc}
48 | */
49 | public function getName(): string
50 | {
51 | return $this->name;
52 | }
53 |
54 | /**
55 | * {@inheritDoc}
56 | */
57 | public function getType(): string
58 | {
59 | return $this->type;
60 | }
61 |
62 | /**
63 | * {@inheritDoc}
64 | */
65 | public function isSearchable(): bool
66 | {
67 | return true;
68 | }
69 |
70 | /**
71 | * {@inheritDoc}
72 | */
73 | public function isFilterable(): bool
74 | {
75 | return true;
76 | }
77 |
78 | /**
79 | * {@inheritDoc}
80 | */
81 | public function isSortable(): bool
82 | {
83 | return true;
84 | }
85 |
86 | /**
87 | * {@inheritDoc}
88 | */
89 | public function useValueField(): bool
90 | {
91 | return false;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/framework-app-search/EngineManagerInterface.php:
--------------------------------------------------------------------------------
1 | getSchemaResolver()->getSchema('engine_name');
34 |
35 | $this->assertInstanceOf(SchemaInterface::class, $schema);
36 | }
37 |
38 | /**
39 | * Test getting a schema when no provider have been registred.
40 | *
41 | * @expectedException \Magento\Framework\Exception\LocalizedException
42 | *
43 | * @return void
44 | */
45 | public function testGetInvalidSchema()
46 | {
47 | $this->getSchemaResolver()->getSchema('invalid_engine_name');
48 | }
49 |
50 | /**
51 | * Init the schema resolver used in tests.
52 | *
53 | * @return SchemaResolver
54 | */
55 | private function getSchemaResolver()
56 | {
57 | $schemaMock = $this->createMock(SchemaInterface::class);
58 | $schemaProviderMock = $this->createMock(SchemaProviderInterface::class);
59 | $schemaProviderMock->method('getSchema')->willReturn($schemaMock);
60 | $providers = ['engine_name' => $schemaProviderMock];
61 |
62 | return new SchemaResolver($providers);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Filter/Filter/RangeFilterBuilder.php:
--------------------------------------------------------------------------------
1 | fieldMapper = $fieldMapper;
40 | }
41 |
42 | /**
43 | * {@inheritDoc}
44 | */
45 | public function getFilter(FilterInterface $filter): array
46 | {
47 | $context = ['type' => SchemaInterface::CONTEXT_FILTER];
48 | $filterName = $this->fieldMapper->getFieldName($filter->getField(), $context);
49 |
50 | $range = [];
51 |
52 | if ($filter->getFrom() !== null) {
53 | $range['from'] = $filter->getFrom();
54 | }
55 |
56 | if ($filter->getTo() !== null) {
57 | $range['to'] = $filter->getTo();
58 | }
59 |
60 | return !empty($range) ? [$filterName => $this->fieldMapper->mapValue($filter->getField(), $range)] : [];
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/module-catalog-search/Model/Product/Document/BatchDataMapper.php:
--------------------------------------------------------------------------------
1 | attributeMapper = $attributeMapper;
46 | $this->additionalDataProviders = $additionalDataProviders;
47 | }
48 |
49 | /**
50 | * {@inheritDoc}
51 | */
52 | public function map(array $documentData, int $storeId): array
53 | {
54 | $documents = $this->attributeMapper->map($documentData, $storeId);
55 |
56 | foreach ($this->additionalDataProviders as $dataProvider) {
57 | foreach ($dataProvider->getData(array_keys($documents), $storeId) as $entityId => $currentData) {
58 | $documents[$entityId] = array_merge($documents[$entityId], $currentData);
59 | }
60 | }
61 |
62 | return array_values($documents);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapter/RequestExecutor/Filter/QueryFilter/MatchQueryFilterBuilderTest.php:
--------------------------------------------------------------------------------
1 | createMock(QueryInterface::class);
32 | $query->method('getName')->willReturn("search");
33 |
34 | $this->assertEquals([], $this->getQueryBuilder()->getFilter($query));
35 | }
36 |
37 | /**
38 | * An exception is thrown when trying to build a query that is different of the main search query.
39 | *
40 | * @expectedException \Magento\Framework\Exception\LocalizedException
41 | */
42 | public function testBuildInvalidQuery()
43 | {
44 | $query = $this->createMock(QueryInterface::class);
45 | $query->method('getName')->willReturn("not_search");
46 |
47 | $this->getQueryBuilder()->getFilter($query);
48 | }
49 |
50 | /**
51 | * Create the builder used in tests.
52 | *
53 | * @return MatchQueryFilterBuilder
54 | */
55 | private function getQueryBuilder()
56 | {
57 | return new MatchQueryFilterBuilder();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/FieldProvider/CompositeFieldProvider.php:
--------------------------------------------------------------------------------
1 | providers = $providers;
44 | }
45 |
46 | /**
47 | * {@inheritDoc}
48 | */
49 | public function getFields(): array
50 | {
51 | if ($this->fields === null) {
52 | $this->fields = [];
53 | foreach ($this->providers as $provider) {
54 | $this->fields = array_merge($this->fields, $provider->getFields());
55 | }
56 | }
57 |
58 | return $this->fields;
59 | }
60 |
61 | /**
62 | * {@inheritDoc}
63 | */
64 | public function getField(string $name): FieldInterface
65 | {
66 | $fields = $this->getFields();
67 |
68 | if (!isset($fields[$name])) {
69 | throw new LocalizedException(__('Unable to find field %1.', $name));
70 | }
71 |
72 | return $fields[$name];
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine.php:
--------------------------------------------------------------------------------
1 | identifier = $identifier;
53 | $this->storeId = $storeId;
54 | $this->name = $name;
55 | $this->language = $language;
56 | }
57 |
58 | /**
59 | * {@inheritDoc}
60 | */
61 | public function getIdentifier(): string
62 | {
63 | return $this->identifier;
64 | }
65 |
66 | /**
67 | * {@inheritDoc}
68 | */
69 | public function getStoreId(): int
70 | {
71 | return $this->storeId;
72 | }
73 |
74 | /**
75 | * {@inheritDoc}
76 | */
77 | public function getName(): string
78 | {
79 | return $this->name;
80 | }
81 |
82 | /**
83 | * {@inheritDoc}
84 | */
85 | public function getLanguage(): ?string
86 | {
87 | return $this->language;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Filter/QueryFilter/FilteredQueryFilterBuilder.php:
--------------------------------------------------------------------------------
1 | filterBuilder = $filterBuilderFactory->create(['fieldMapper' => $fieldMapper]);
43 | }
44 |
45 | /**
46 | * {@inheritDoc}
47 | */
48 | public function getFilter(QueryInterface $query): array
49 | {
50 | $filter = [];
51 |
52 | if ($query->getReference() && $query->getReference() instanceof FilterInterface) {
53 | $filter = $this->filterBuilder->getFilter($query->getReference());
54 | }
55 |
56 | return $filter;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/module-catalog-search/Model/Product/Engine/Field/FieldNameResolver.php:
--------------------------------------------------------------------------------
1 | customerSession = $customerSession;
39 | }
40 |
41 | /**
42 | * {@inheritDoc}
43 | */
44 | public function getFieldName(FieldInterface $field, array $context = []): string
45 | {
46 | $fieldName = parent::getFieldName($field, $context);
47 |
48 | if ($fieldName == 'price') {
49 | $fieldName = $this->getPriceFieldName($fieldName, $context);
50 | }
51 |
52 | return $fieldName;
53 | }
54 |
55 | /**
56 | * Add customer group id to the field name.
57 | *
58 | * @deprecated Will be replace by a specific product resolver in the future.
59 | *
60 | * @param string $fieldName Original field name
61 | * @param array $context
62 | *
63 | * @return string
64 | */
65 | private function getPriceFieldName(string $fieldName, array $context)
66 | {
67 | $groupId = $context['customer_group_id'] ?? $this->customerSession->getCustomerGroupId();
68 |
69 | return $fieldName . '_' . (int) $groupId;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/Config/Converter.php:
--------------------------------------------------------------------------------
1 | ['name' => 'deleted', 'type' => SchemaInterface::FIELD_TYPE_TEXT],
32 | 'sync_id' => ['name' => 'sync_id', 'type' => SchemaInterface::FIELD_TYPE_TEXT],
33 | ];
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function convert($source)
39 | {
40 | $result = [];
41 |
42 | foreach ($source->documentElement->getElementsByTagName('engine') as $engine) {
43 | $engineIdentifier = (string) $engine->getAttribute('identifier');
44 | $result[$engineIdentifier] = $this->defaultFields;
45 | foreach ($engine->getElementsByTagName('field') as $field) {
46 | $fieldConfig = $this->getFieldConfig($field);
47 | $fieldName = $fieldConfig['name'];
48 | $result[$engineIdentifier][$fieldName] = $fieldConfig;
49 | }
50 | }
51 |
52 | return $result;
53 | }
54 |
55 | /**
56 | * Parse field config.
57 | *
58 | * @param \DOMElement $field
59 | *
60 | * @return array
61 | */
62 | private function getFieldConfig(\DOMElement $field): array
63 | {
64 | return [
65 | 'name' => (string) $field->getAttribute('name'),
66 | 'type' => (string) $field->getAttribute('type'),
67 | ];
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Rescorer/ResponseProcessor.php:
--------------------------------------------------------------------------------
1 | rescorerResolver = $rescorerResolver;
40 | }
41 |
42 | /**
43 | * {@inheritDoc}
44 | */
45 | public function process(RequestInterface $request, array $response): array
46 | {
47 | if (!isset($response['results'])) {
48 | $response['results'] = [];
49 | }
50 |
51 | if (!empty($response['results'])) {
52 | $response['results'] = $this->getRescorer($request)->rescoreResults($request, $response['results']);
53 | }
54 |
55 | return $response;
56 | }
57 |
58 | /**
59 | * Get rescorer for the current request.
60 | *
61 | * @param RequestInterface $request
62 | *
63 | * @return RescorerInterface
64 | */
65 | private function getRescorer(RequestInterface $request): RescorerInterface
66 | {
67 | return $this->rescorerResolver->getRescorer($request);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/module-catalog-graph-ql/Model/ResourceModel/Product/SearchResultCollection.php:
--------------------------------------------------------------------------------
1 | _totalRecords = $searchResult->getTotalCount();
42 | $this->aggregations = $searchResult->getAggregations();
43 | }
44 |
45 | /**
46 | * Return field faceted data from faceted search result.
47 | *
48 | * @param string $field
49 | *
50 | * @return array
51 | */
52 | public function getFacetedData($field)
53 | {
54 | $result = [];
55 |
56 | if (null !== $this->aggregations) {
57 | $bucket = $this->aggregations->getBucket($field . RequestGenerator::BUCKET_SUFFIX);
58 | if ($bucket) {
59 | foreach ($bucket->getValues() as $value) {
60 | $metrics = $value->getMetrics();
61 | $result[$metrics['value']] = $metrics;
62 | }
63 | }
64 | }
65 |
66 | return $result;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/EngineResolver.php:
--------------------------------------------------------------------------------
1 | engineResolver = $engineResolver;
46 | $this->scopeResolver = $scopeResolver;
47 | }
48 |
49 | /**
50 | * Resolve the engine of the current request.
51 | *
52 | * @param RequestInterface $request
53 | *
54 | * @return EngineInterface
55 | */
56 | public function getEngine(RequestInterface $request): EngineInterface
57 | {
58 | return $this->engineResolver->getEngine($request->getIndex(), $this->getScopeId($request));
59 | }
60 |
61 | /**
62 | * Resolve scope id from the search request.
63 | *
64 | * @param RequestInterface $request
65 | *
66 | * @return int
67 | */
68 | private function getScopeId(RequestInterface $request): int
69 | {
70 | $dimension = current($request->getDimensions());
71 |
72 | return $this->scopeResolver->getScope($dimension->getValue())->getId();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/Engine/EngineNameResolverTest.php:
--------------------------------------------------------------------------------
1 | createMock(ScopeConfigInterface::class);
33 | $scopeConfig->expects($this->any())->method('getValue')->willReturn('engine_prefix');
34 |
35 | $engineNameResolver = new EngineNameResolver($scopeConfig, $this->getFilterManager());
36 |
37 | $engineName = $engineNameResolver->getEngineName('index_identifier', 1);
38 |
39 | $this->assertEquals('engine-prefix-index-identifier-1', $engineName);
40 | }
41 |
42 | /**
43 | * Init the filter manager used in test case.
44 | *
45 | * @return FilterManager
46 | */
47 | private function getFilterManager()
48 | {
49 | $scopeConfig = $this->createMock(ScopeConfigInterface::class);
50 | $scopeConfig->expects($this->any())->method('getValue')->willReturn(null);
51 |
52 | $urlTranslitFilter = new TranslitUrl($scopeConfig);
53 |
54 | $translitUrl = function ($value) use ($urlTranslitFilter) {
55 | return $urlTranslitFilter->filter($value);
56 | };
57 |
58 | $filterManager = $this->createPartialMock(FilterManager::class, ['translitUrl']);
59 | $filterManager->expects($this->any())->method('translitUrl')->willReturnCallback($translitUrl);
60 |
61 | return $filterManager;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapter/RequestExecutor/Page/SearchParamsProviderTest.php:
--------------------------------------------------------------------------------
1 | createMock(RequestInterface::class);
48 | $request->method('getFrom')->willReturn($requestFrom);
49 | $request->method('getSize')->willReturn($requestSize);
50 |
51 | $pageParams = $searchParamsProvider->getParams($request);
52 |
53 | $this->assertArrayHasKey('page', $pageParams);
54 | $this->assertArrayHasKey('current', $pageParams['page']);
55 | $this->assertEquals($currentPage, $pageParams['page']['current']);
56 |
57 | $this->assertArrayHasKey('size', $pageParams['page']);
58 | $this->assertEquals($pageSize, $pageParams['page']['size']);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/module-search/Model/Search.php:
--------------------------------------------------------------------------------
1 | requestBuilder = $requestBuilder;
60 | $this->searchEngine = $searchEngine;
61 | $this->searchResponseBuilder = $searchResponseBuilder;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function search(SearchCriteriaInterface $searchCriteria)
68 | {
69 | $request = $this->requestBuilder->create($searchCriteria);
70 | $searchResponse = $this->searchEngine->search($request);
71 |
72 | return $this->searchResponseBuilder->build($searchResponse)->setSearchCriteria($searchCriteria);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/Engine/Field/FieldNameResolverTest.php:
--------------------------------------------------------------------------------
1 | createMock(FieldInterface::class);
44 | $field->method('getName')->willReturn($fieldName);
45 | $field->method('useValueField')->willReturn($useValue);
46 | $field->method('isSearchable')->willReturn($searchable);
47 | $field->method('isSortable')->willReturn($sortable);
48 |
49 | $this->assertEquals($expectedName, $resolver->getFieldName($field, $context));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Page/SearchParamsProvider.php:
--------------------------------------------------------------------------------
1 | $this->getCurrentPage($request),
52 | self::PAGE_SIZE_PARAM_NAME => $this->getPageSize($request),
53 | ];
54 |
55 | return [self::PAGE_PARAM_NAME => $pageParams];
56 | }
57 |
58 | /**
59 | * Return page size for the request.
60 | *
61 | * @param RequestInterface $request
62 | *
63 | * @return int
64 | */
65 | private function getPageSize(RequestInterface $request)
66 | {
67 | return (int) min(self::MAX_PAGE_SIZE, $request->getSize() ?? self::MAX_PAGE_SIZE);
68 | }
69 |
70 | /**
71 | * Return current page for the request.
72 | *
73 | * @param RequestInterface $request
74 | *
75 | * @return int
76 | */
77 | private function getCurrentPage(RequestInterface $request)
78 | {
79 | return (int) floor(($request->getFrom() ?? 0) / max(1, $this->getPageSize($request))) + 1;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/docs/MAGENTO-COULD.md:
--------------------------------------------------------------------------------
1 | # Using App Search with Magento Cloud
2 |
3 | This documentation will help you to run App Search against your Magento Cloud project.
4 |
5 | **Note:**
6 |
7 | It is assumed that you have an App Search instance running and accessible from the Magento cloud instance.
8 |
9 | If it is not yet the case, you should try to create an account [here](https://app.swiftype.com/signup).
10 |
11 | ## Configure your project to use App Search as search engine:
12 |
13 | When using Magento Cloud, the search engine can not be changed from the admin or by using the `env.php` file.
14 |
15 | Instead you have to set the `SEARCH_CONFIGURATION` variable into your project:
16 |
17 | ```bash
18 | magento-cloud project:variable:set --json SEARCH_CONFIGURATION '{"engine":"elastic_appsearch"}'
19 | ```
20 |
21 | ## App Search module configuration:
22 |
23 | Then you can set the App Search endpoint base URL and API keys by using:
24 |
25 | ```
26 | magento-cloud project:variable:set --name CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__API_ENDPOINT --value "https://host-XXXXX.api.swiftype.com"
27 | magento-cloud project:variable:set --name CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__PRIVATE_API_KEY --value "private-XXXXX"
28 | magento-cloud project:variable:set --name CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__SEARCH_API_KEY --value "search-XXXXX"
29 | ```
30 |
31 | You may want to use a different configuration for each of your Magento Cloud environment (staging, production, ...). You can use the `variable:set` command to override the project configuration:
32 |
33 | ```
34 | magento-cloud variable:set -e staging --name CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__API_ENDPOINT --value "https://host-XXXXX.api.swiftype.com"
35 | magento-cloud variable:set -e staging --name CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__PRIVATE_API_KEY --value "private-XXXXX"
36 | magento-cloud variable:set -e staging --name CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__SEARCH_API_KEY --value "search-XXXXX"
37 | ```
38 |
39 | ## Environment config:
40 |
41 | To avoid inconsistencies, you have to use a different engine for each environment you will deploy.
42 | This can be achieved by configuring the engine prefix configuration that control how the engines created by the module are named.
43 |
44 | ```
45 | magento-cloud variable:set -e staging --name CONFIG__DEFAULT__ELASTIC_APPSEARCH__CLIENT__ENGINE_PREFIX --value "mysite-staging"
46 | ```
47 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapter/RequestExecutor/Filter/Filter/TermFilterBuilderTest.php:
--------------------------------------------------------------------------------
1 | createMock(FieldMapperInterface::class);
37 | $fieldMapper->expects($this->once())->method('getFieldName')->will($this->returnArgument(0));
38 | $fieldMapper->method('mapValue')->will($this->returnArgument(1));
39 |
40 | $builder = new TermFilterBuilder($fieldMapper);
41 |
42 | $this->assertEquals($expectedResult, $builder->getFilter($filter));
43 | }
44 |
45 | /**
46 | * List of filter to get build and expected results.
47 | *
48 | * @return array
49 | */
50 | public function filterDataProvider()
51 | {
52 | return [
53 | [new TermFilter('', 'bar', 'foo'), ['foo' => 'bar']],
54 | [new TermFilter('', 1, 'foo'), ['foo' => 1]],
55 | [new TermFilter('', ['bar'], 'foo'), ['foo' => ['bar']]],
56 | [new TermFilter('', ['bar', 'baz'], 'foo'), ['foo' => ['bar', 'baz']]],
57 | [new TermFilter('', [], 'foo'), ['foo' => []]],
58 | ];
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/dev/images/magento/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG base_image=php:7.1-apache
2 | FROM $base_image
3 |
4 | # Installing packages required to run Magento.
5 | RUN apt-get update \
6 | && apt-get install -y libfreetype6-dev libicu-dev libjpeg62-turbo-dev libmcrypt-dev libpng-dev libxslt1-dev libsodium-dev sendmail-bin sendmail unzip sudo \
7 | && rm -rf /var/lib/apt/lists/*
8 |
9 | # Building PHP extensions required to run Magento.
10 | RUN docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
11 | && docker-php-ext-install dom gd intl mbstring pdo_mysql xsl zip soap bcmath opcache mcrypt
12 |
13 | # Installing composer as a globally available system command.
14 | RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
15 | && php composer-setup.php \
16 | && php -r "unlink('composer-setup.php');" \
17 | && mv composer.phar /usr/local/bin/composer
18 |
19 | # Configure PHP and Apache to run Magento.
20 | ENV PHP_MEMORY_LIMIT 2G
21 | ENV MAGENTO_ROOT /var/www/magento
22 | ADD etc/php.ini /usr/local/etc/php/conf.d/zz-magento.ini
23 | ADD etc/vhost.conf /etc/apache2/sites-available/000-default.conf
24 | RUN a2enmod rewrite \
25 | && chown -R www-data:www-data /var/www
26 |
27 | # Using user www-data to install Magento.
28 | USER www-data
29 |
30 | # Setting composer auth to be able to fetch packages from Magento composer repo.
31 | ARG public_key
32 | ARG private_key
33 | ENV COMPOSER_AUTH {\"http-basic\": {\"repo.magento.com\": {\"username\":\"$public_key\", \"password\": \"$private_key\"}}}
34 |
35 | # Install prestissimo to speedup install
36 | RUN composer global require hirak/prestissimo
37 |
38 | # Prefetch Magento packages.
39 | ARG magento_version=2.3.0
40 | ARG magento_edition=community
41 | RUN composer create-project --repository=https://repo.magento.com/ magento/project-${magento_edition}-edition:$magento_version $MAGENTO_ROOT
42 |
43 | # Install sample data if required.
44 | ARG use_sample_data=1
45 | WORKDIR $MAGENTO_ROOT
46 | RUN if [ $use_sample_data -eq 1 ]; then bin/magento sampledata:deploy; fi
47 |
48 | # Preconfigure Magento
49 | COPY --chown=www-data env.php app/etc/env.php
50 |
51 | # Fix perms in Magento directories. Ensure all command are run as www-data.
52 | ENV HOME /var/www
53 |
54 | # Add local repo to work on the extension.
55 | RUN composer config repositories.app-search '{"type": "path", "url": "./app-search-module"}'
56 |
57 | # Revert original user (root) to run Apache.
58 | USER root
59 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/Response/DocumentFactory.php:
--------------------------------------------------------------------------------
1 | documentFactory = $documentFactory;
48 | $this->attributeValueFactory = $attributeValueFactory;
49 | }
50 |
51 | /**
52 | * Create a document from an App Search result.
53 | *
54 | * @param array $rawDocument
55 | *
56 | * @return DocumentInterface
57 | */
58 | public function create(array $rawDocument): DocumentInterface
59 | {
60 | $documentId = (string) $rawDocument['id']['raw'];
61 | $documentScore = (float) $rawDocument['_meta']['score'];
62 |
63 | $attributes = [
64 | 'score' => $this->attributeValueFactory->create()->setAttributeCode('score')->setValue($documentScore),
65 | ];
66 |
67 | $documentData = [
68 | DocumentInterface::ID => $documentId,
69 | CustomAttributesDataInterface::CUSTOM_ATTRIBUTES => $attributes,
70 | ];
71 |
72 | return $this->documentFactory->create(['data' => $documentData]);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapter/RequestExecutor/Filter/Filter/RangeFilterBuilderTest.php:
--------------------------------------------------------------------------------
1 | createMock(FieldMapperInterface::class);
37 | $fieldMapper->expects($this->once())->method('getFieldName')->will($this->returnArgument(0));
38 | $fieldMapper->method('mapValue')->will($this->returnArgument(1));
39 |
40 | $builder = new RangeFilterBuilder($fieldMapper);
41 |
42 | $this->assertEquals($expectedResult, $builder->getFilter($filter));
43 | }
44 |
45 | /**
46 | * List of filter to get build and expected results.
47 | *
48 | * @return array
49 | */
50 | public function filterDataProvider()
51 | {
52 | return [
53 | [new RangeFilter('', 'foo', 0, 100), ['foo' => ['from' => 0, 'to' => 100]]],
54 | [new RangeFilter('', 'foo', '0', '100'), ['foo' => ['from' => '0', 'to' => '100']]],
55 | [new RangeFilter('', 'foo', 0, null), ['foo' => ['from' => 0]]],
56 | [new RangeFilter('', 'foo', null, 100), ['foo' => ['to' => 100]]],
57 | [new RangeFilter('', 'foo', null, null), []],
58 | ];
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/module-synonyms/Model/Indexer/Plugin/SynonymGroupPlugin.php:
--------------------------------------------------------------------------------
1 | indexerRegistry = $indexerRegistry;
36 | }
37 |
38 | /**
39 | * Invalidate the index after synonym group have been saved.
40 | *
41 | * @SuppressWarnings(PHPMD.UnusedFormalParameter)
42 | *
43 | * @param SynonymGroupResourceModel $resourceModel
44 | * @param SynonymGroupResourceModel $result
45 | *
46 | * @return SynonymGroupResourceModel
47 | */
48 | public function afterSave(SynonymGroupResourceModel $resourceModel, $result)
49 | {
50 | $this->invalidateIndex();
51 |
52 | return $result;
53 | }
54 |
55 |
56 | /**
57 | * Invalidate the index after synonym group deletion.
58 | *
59 | * @SuppressWarnings(PHPMD.UnusedFormalParameter)
60 | *
61 | * @param SynonymGroupResourceModel $resourceModel
62 | * @param SynonymGroupResourceModel $result
63 | *
64 | * @return SynonymGroupResourceModel
65 | */
66 | public function afterDelete(SynonymGroupResourceModel $resourceModel, $result)
67 | {
68 | $this->invalidateIndex();
69 |
70 | return $result;
71 | }
72 |
73 | /**
74 | * Invalidate the synonym indexer.
75 | *
76 | * @return void
77 | */
78 | private function invalidateIndex()
79 | {
80 | $indexer = $this->indexerRegistry->get(Indexer::INDEXER_ID);
81 | $indexer->invalidate();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/module-catalog-search/Model/ResourceModel/Engine.php:
--------------------------------------------------------------------------------
1 | catalogProductVisibility = $catalogProductVisibility;
49 | $this->indexScopeResolver = $indexScopeResolver;
50 | }
51 |
52 | /**
53 | * {@inheritDoc}
54 | */
55 | public function getAllowedVisibility()
56 | {
57 | return $this->catalogProductVisibility->getVisibleInSiteIds();
58 | }
59 |
60 | /**
61 | *
62 | * {@inheritDoc}
63 | */
64 | public function allowAdvancedIndex()
65 | {
66 | return false;
67 | }
68 | /**
69 | * {@inheritdoc}
70 | */
71 | public function processAttributeValue($attribute, $value)
72 | {
73 | return $value;
74 | }
75 |
76 | /**
77 | * @SuppressWarnings(PHPMD.UnusedFormalParameter)
78 | * {@inheritDoc}
79 | */
80 | public function prepareEntityIndex($index, $separator = ' ')
81 | {
82 | return $index;
83 | }
84 |
85 | /**
86 | * {@inheritdoc}
87 | */
88 | public function isAvailable()
89 | {
90 | return true;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Rescorer/ResultSorter.php:
--------------------------------------------------------------------------------
1 | getDocScore($doc1) - $this->getDocScore($doc2);
50 |
51 | if ($result === 0 && is_numeric($this->getDocId($doc1)) && is_numeric($this->getDocId($doc2))) {
52 | $result = $this->getDocId($doc1) - $this->getDocId($doc2);
53 | } elseif ($result === 0) {
54 | $result = strcmp($this->getDocId($doc1), $this->getDocId($doc2));
55 | }
56 |
57 | return $result < 0 ? -1 : 1;
58 | }
59 |
60 | /**
61 | * Extract score from a document.
62 | *
63 | * @param array $doc
64 | *
65 | * @return float
66 | */
67 | private function getDocScore(array $doc): float
68 | {
69 | return (float) $doc['_meta']['score'];
70 | }
71 |
72 | /**
73 | * Extract id from a document.
74 | *
75 | * @param array $doc
76 | *
77 | * @return string
78 | */
79 | private function getDocId(array $doc): string
80 | {
81 | return (string) $doc['entity_id']['raw'];
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Filter/FilterBuilder.php:
--------------------------------------------------------------------------------
1 | builders = array_map(
41 | function ($factory) use ($fieldMapper) {
42 | return $this->createBuilder($factory, $fieldMapper);
43 | },
44 | $builderFactories
45 | );
46 | }
47 |
48 | /**
49 | * {@inheritDoc}
50 | */
51 | public function getFilter(FilterInterface $filter): array
52 | {
53 | if (!isset($this->builders[$filter->getType()])) {
54 | throw new LocalizedException(
55 | __("Unable to find query builder for filter with type %1", $filter->getType())
56 | );
57 | }
58 |
59 | return $this->builders[$filter->getType()]->getFilter($filter);
60 | }
61 |
62 | /**
63 | * Create a filter builder instance.
64 | *
65 | * @param UniversalFactory $factory
66 | * @param FieldMapperInterface fieldMapper
67 | *
68 | * @return FilterBuilderInterface
69 | */
70 | private function createBuilder($factory, $fieldMapper): FilterBuilderInterface
71 | {
72 | return $factory->create(['fieldMapper' => $fieldMapper, 'filterBuilder' => $this]);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Filter/QueryFilterBuilder.php:
--------------------------------------------------------------------------------
1 | builders = array_map(
41 | function ($factory) use ($fieldMapper) {
42 | return $this->createBuilder($factory, $fieldMapper);
43 | },
44 | $builderFactories
45 | );
46 | }
47 |
48 | /**
49 | * {@inheritDoc}
50 | */
51 | public function getFilter(QueryInterface $query): array
52 | {
53 | if (!isset($this->builders[$query->getType()])) {
54 | throw new LocalizedException(
55 | __("Unable to find query builder for query with type %1", $query->getType())
56 | );
57 | }
58 |
59 | return $this->builders[$query->getType()]->getFilter($query);
60 | }
61 |
62 | /**
63 | * Create a query filter builder instance.
64 | *
65 | * @param UniversalFactory $factory
66 | * @param FieldMapperInterface $fieldMapper
67 | *
68 | * @return FilterBuilderInterface
69 | */
70 | private function createBuilder($factory, $fieldMapper): QueryFilterBuilderInterface
71 | {
72 | return $factory->create(['fieldMapper' => $fieldMapper, 'queryBuilder' => $this]);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/framework-app-search/Engine/Field/Config/FilesystemReader.php:
--------------------------------------------------------------------------------
1 | 'identifier',
42 | '/engines/engine/field' => 'name',
43 | ];
44 |
45 | // phpcs:enable
46 |
47 | /**
48 | * Constructor.
49 | *
50 | * @param FileResolverInterface $fileResolver
51 | * @param Converter $converter
52 | * @param SchemaLocator $schemaLocator
53 | * @param ValidationStateInterface $validationState
54 | * @param string $fileName
55 | * @param array $idAttributes
56 | * @param string $domDocumentClass
57 | * @param string $defaultScope
58 | */
59 | public function __construct(
60 | FileResolverInterface $fileResolver,
61 | Converter $converter,
62 | SchemaLocator $schemaLocator,
63 | ValidationStateInterface $validationState,
64 | string $fileName = self::CONFIG_FILE_NAME,
65 | array $idAttributes = [],
66 | string $domDocumentClass = \Magento\Framework\Config\Dom::class,
67 | string $defaultScope = 'global'
68 | ) {
69 | parent::__construct(
70 | $fileResolver,
71 | $converter,
72 | $schemaLocator,
73 | $validationState,
74 | $fileName,
75 | $idAttributes,
76 | $domDocumentClass,
77 | $defaultScope
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapter/RequestExecutor/Facet/DynamicRangeProviderTest.php:
--------------------------------------------------------------------------------
1 | createMock(BucketInterface::class);
37 |
38 | $dynamicRangeProvider = new DynamicRangeProvider($this->getRangeFactory());
39 |
40 | $ranges = $dynamicRangeProvider->getRanges($bucket);
41 |
42 | $from = 0;
43 | foreach ($ranges as $range) {
44 | $this->assertInstanceOf(Range::class, $range);
45 | $this->assertEquals($from, $range->getFrom());
46 | $expectedSize = pow(10, max(1, strlen($range->getFrom()) - 2));
47 | $this->assertEquals($expectedSize, $range->getTo() - $range->getFrom());
48 | $from = $range->getTo();
49 | }
50 |
51 |
52 | $this->assertEquals(pow(10, 6), end($ranges)->getTo());
53 | }
54 |
55 | /**
56 | * Mock range factory used during test.
57 | *
58 | * @return RangeFactory
59 | */
60 | private function getRangeFactory()
61 | {
62 | $createMethod = function ($range) {
63 | return new Range($range['from'], $range['to']);
64 | };
65 |
66 | $rangeFactory = $this->createMock(RangeFactory::class);
67 | $rangeFactory->method('create')->will($this->returnCallback($createMethod));
68 |
69 | return $rangeFactory;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/framework-app-search/Client/ConnectionManager.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
50 | $this->clientConfig = $clientConfig;
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function getClient(array $options = []): Client
57 | {
58 | if (null === $this->client) {
59 | $this->client = $this->createClient($options);
60 | }
61 |
62 | return $this->client;
63 | }
64 |
65 | /**
66 | * Create the App Search client.
67 | *
68 | * @SuppressWarnings(PHPMD.StaticAccess)
69 | *
70 | * @param array $options
71 | *
72 | * @return \Elastic\AppSearch\Client\Client
73 | */
74 | private function createClient($options = [])
75 | {
76 | $apiEndpoint = $options['api_endpoint'] ?? $this->clientConfig->getApiEndpoint();
77 | $apiKey = $options['api_key'] ?? $this->clientConfig->getPrivateApiKey();
78 |
79 | $clientBuilder = ClientBuilder::create($apiEndpoint, $apiKey);
80 |
81 | $clientBuilder->setLogger($this->logger);
82 | $clientBuilder->setIntegration($this->clientConfig->getIntegrationName());
83 |
84 | if ($this->clientConfig->isDebug()) {
85 | $clientBuilder->setTracer($this->logger);
86 | }
87 |
88 | return $clientBuilder->build();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/SearchParamsProvider.php:
--------------------------------------------------------------------------------
1 | providers = $providers;
43 | $this->rescorerResolver = $rescorerResolver;
44 | }
45 |
46 | /**
47 | * {@inheritDoc}
48 | */
49 | public function getParams(RequestInterface $request): array
50 | {
51 | return $this->getRescorer($request)->prepareSearchParams($request, $this->collectSearchParams($request));
52 | }
53 |
54 | /**
55 | * Collect search params from child providers.
56 | *
57 | * @param RequestInterface $request
58 | *
59 | * @return array
60 | */
61 | private function collectSearchParams(RequestInterface $request): array
62 | {
63 | $searchParams = array_map(
64 | function (SearchParamsProviderInterface $provider) use ($request) {
65 | return $provider->getParams($request);
66 | },
67 | $this->providers
68 | );
69 |
70 | return !empty($searchParams) ? array_merge_recursive(...array_values($searchParams)) : [];
71 | }
72 |
73 | /**
74 | * Get rescorer for the current search request.
75 | *
76 | * @param RequestInterface $request
77 | *
78 | * @return RescorerInterface
79 | */
80 | private function getRescorer(RequestInterface $request): RescorerInterface
81 | {
82 | return $this->rescorerResolver->getRescorer($request);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/module-search/etc/adminhtml/system.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 | general
22 | ElasticAppSearch_CatalogSearch::config_elastic_appsearch
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Magento\Config\Model\Config\Backend\Encrypted
31 |
32 |
33 |
34 | Magento\Config\Model\Config\Backend\Encrypted
35 |
36 |
37 |
38 | Magento\Config\Model\Config\Source\Yesno
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/module-synonyms/Model/Indexer/IndexerHandlerFactory.php:
--------------------------------------------------------------------------------
1 | objectManager = $objectManager;
54 | $this->handlers = $handlers;
55 | $this->engineResolver = $engineResolver;
56 | }
57 |
58 | /**
59 | * Create indexer handler
60 | *
61 | * @SuppressWarnings(PHPMD.MissingImport)
62 | *
63 | * @param array $data
64 | *
65 | * @return IndexerInterface
66 | */
67 | public function create(array $data = []): ?IndexerInterface
68 | {
69 | $indexer = null;
70 |
71 | $searchEngine = $this->engineResolver->getCurrentSearchEngine();
72 |
73 | if (isset($this->handlers[$searchEngine])) {
74 | $indexer = $this->objectManager->create($this->handlers[$searchEngine], $data);
75 |
76 | if (!$indexer instanceof IndexerInterface) {
77 | $message = $searchEngine . ' indexer handler doesn\'t implement ' . IndexerInterface::class;
78 | throw new \InvalidArgumentException($message);
79 | }
80 |
81 | if ($indexer && !$indexer->isAvailable()) {
82 | throw new \LogicException('Indexer handler is not available: ' . $searchEngine);
83 | }
84 | }
85 |
86 | return $indexer;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/framework-app-search/Test/Unit/SearchAdapter/RequestExecutor/Filter/FilterBuilderTest.php:
--------------------------------------------------------------------------------
1 | getFilterBuilder();
34 |
35 | $filter = $this->createMock(FilterInterface::class);
36 | $filter->method('getType')->willReturn('myFilterType');
37 |
38 | $this->assertEquals(["filterContent"], $builder->getFilter($filter));
39 | }
40 |
41 | /**
42 | * Check an exception is thrown when using an invalid filter.
43 | *
44 | * @expectedException \Magento\Framework\Exception\LocalizedException
45 | */
46 | public function testBuildInvalidFilter()
47 | {
48 | $builder = $this->getFilterBuilder();
49 |
50 | $filter = $this->createMock(FilterInterface::class);
51 | $filter->method('getType')->willReturn('unknownFilterType');
52 |
53 | $builder->getFilter($filter);
54 | }
55 |
56 | /**
57 | * Create the builder used in tests.
58 | *
59 | * @return FilterBuilder
60 | */
61 | private function getFilterBuilder()
62 | {
63 | $builder = $this->createMock(FilterBuilderInterface::class);
64 | $builder->method('getFilter')->willReturn(['filterContent']);
65 |
66 | $builderFactory = $this->createMock(UniversalFactory::class);
67 | $builderFactory->method('create')->willReturn($builder);
68 |
69 | $fieldMapper = $this->createMock(FieldMapperInterface::class);
70 |
71 | return new FilterBuilder($fieldMapper, ['myFilterType' => $builderFactory]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/framework-app-search/SearchAdapter/RequestExecutor/Facet/DynamicRangeProvider.php:
--------------------------------------------------------------------------------
1 | rangeFactory = $rangeFactory;
53 | $this->maxPow = $maxPow;
54 | }
55 |
56 | /**
57 | * Build bucket ranges.
58 | *
59 | * TODO: Add support for manual step config.
60 | *
61 | * @SuppressWarnings(PHPMD.UnusedFormalParameter)
62 | *
63 | * @param BucketInterface $bucket
64 | *
65 | * @return Range[]
66 | */
67 | public function getRanges(BucketInterface $bucket): array
68 | {
69 | return $this->getAutomaticRanges();
70 | }
71 |
72 | /**
73 | * Build automatic interval that will be reworked by post processing.
74 | *
75 | * @return Range[]
76 | */
77 | private function getAutomaticRanges(): array
78 | {
79 | $ranges = [];
80 | $from = 0;
81 |
82 | foreach (range(3, $this->maxPow) as $pow) {
83 | $intervalSize = pow(10, $pow - 2);
84 | $upperLmit = pow(10, $pow);
85 |
86 | while ($from + $intervalSize <= $upperLmit) {
87 | $to = $from + $intervalSize;
88 | $ranges[] = $this->rangeFactory->create(['from' => $from, 'to' => $to]);
89 | $from = $to;
90 | }
91 | }
92 |
93 | return $ranges;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------