├── 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 | --------------------------------------------------------------------------------