├── .gitignore ├── img ├── plus.png └── cancel.png ├── elasticsearch_connector.libraries.yml ├── elasticsearch_connector.links.task.yml ├── tests ├── modules │ └── elasticsearch_test │ │ ├── elasticsearch_test.info.yml │ │ └── config │ │ └── install │ │ ├── search_api.server.elasticsearch_server.yml │ │ └── search_api.index.elasticsearch_index.yml └── src │ ├── Behat │ ├── features │ │ └── bootstrap │ │ │ ├── settings_form.feature │ │ │ └── ElasticsearchConnectorFeatureContext.php │ ├── example.behat.local.yml │ └── behat.yml │ └── Unit │ ├── Entity │ ├── IndexTest.php │ ├── IndexRouteProviderTest.php │ └── ClusterRouteProviderTest.php │ ├── ElasticSearch │ ├── Parameters │ │ ├── Factory │ │ │ ├── IndexFactoryTest.php │ │ │ ├── MappingFactoryTest.php │ │ │ └── FilterFactoryTest.php │ │ └── Builder │ │ │ └── SearchBuilderTest.php │ └── ClientManagerTest.php │ └── ClusterManagerTest.php ├── elasticsearch_connector.links.menu.yml ├── elasticsearch_connector.routing.yml ├── src ├── Exception │ └── ElasticSearchConnectorException.php ├── Plugin │ └── search_api │ │ ├── data_type │ │ └── ObjectDataType.php │ │ ├── backend │ │ └── SearchApiElasticsearchBackendInterface.php │ │ └── processor │ │ └── ExcludeSourceFields.php ├── ElasticSearch │ ├── ClientManagerInterface.php │ ├── ClientManager.php │ └── Parameters │ │ └── Factory │ │ ├── SearchFactory.php │ │ ├── MappingFactory.php │ │ ├── FilterFactory.php │ │ └── IndexFactory.php ├── Event │ ├── BuildIndexParamsEvent.php │ ├── BuildSearchParamsEvent.php │ ├── PrepareIndexEvent.php │ ├── PrepareIndexMappingEvent.php │ ├── PrepareSearchQueryEvent.php │ └── PrepareMappingEvent.php ├── Entity │ ├── IndexRouteProvider.php │ ├── Index.php │ ├── ClusterRouteProvider.php │ └── Cluster.php ├── ClusterManager.php ├── Form │ ├── IndexDeleteForm.php │ ├── ClusterDeleteForm.php │ ├── IndexForm.php │ └── ClusterForm.php └── Controller │ ├── ElasticsearchController.php │ └── ClusterListBuilder.php ├── elasticsearch_connector.info.yml ├── modules └── elasticsearch_connector_views │ ├── elasticsearch_connector_views.info.yml │ ├── src │ └── Plugin │ │ └── views │ │ ├── field │ │ ├── ElasticsearchViewsNumeric.php │ │ ├── ElasticsearchViewsBoolean.php │ │ ├── ElasticsearchViewsStandard.php │ │ ├── ElasticsearchViewsDate.php │ │ ├── ElasticsearchViewsMarkup.php │ │ ├── ElasticsearchViewsFieldTrait.php │ │ ├── ElasticsearchViewsEntityField.php │ │ └── ElasticsearchViewsEntity.php │ │ ├── filter │ │ ├── ElasticsearchViewsDate.php │ │ ├── ElasticsearchViewsStandard.php │ │ ├── ElasticsearchViewsNumericFilter.php │ │ ├── ElasticsearchViewsBooleanOperator.php │ │ ├── ElasticsearchViewsStringFilter.php │ │ └── ElasticsearchViewsFulltextSearch.php │ │ ├── join │ │ └── ElasticsearchViewsJoin.php │ │ └── ElasticsearchViewsHandlerTrait.php │ └── elasticsearch_connector_views.views.inc ├── elasticsearch_connector.links.action.yml ├── config └── schema │ ├── elasticsearch_connector.index.schema.yml │ ├── elasticsearch_connector.cluster.schema.yml │ └── elasticsearch_connector.backend.schema.yml ├── elasticsearch_connector.permissions.yml ├── css └── ec-index.css ├── README.TXT ├── elasticsearch_connector.services.yml ├── js ├── ec-index-child.js └── ec-index.js ├── elasticsearch_connector.api.php ├── composer.json ├── elasticsearch_connector.install ├── phpunit.core.xml.dist ├── .circleci └── config.yml └── elasticsearch_connector.module /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .idea 4 | tests/src/Behat/behat.local.yml 5 | -------------------------------------------------------------------------------- /img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodespark/elasticsearch_connector/HEAD/img/plus.png -------------------------------------------------------------------------------- /img/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodespark/elasticsearch_connector/HEAD/img/cancel.png -------------------------------------------------------------------------------- /elasticsearch_connector.libraries.yml: -------------------------------------------------------------------------------- 1 | drupal.elasticsearch_connector.ec_index: 2 | version: VERSION 3 | css: 4 | theme: 5 | css/ec-index.css: {} 6 | -------------------------------------------------------------------------------- /elasticsearch_connector.links.task.yml: -------------------------------------------------------------------------------- 1 | entity.elasticsearch_connector.cluster.view: 2 | route_name: entity.elasticsearch_cluster.canonical 3 | base_route: entity.elasticsearch_cluster.canonical 4 | title: 'View' 5 | -------------------------------------------------------------------------------- /tests/modules/elasticsearch_test/elasticsearch_test.info.yml: -------------------------------------------------------------------------------- 1 | type: module 2 | name: 'Elasticseach Test' 3 | description: 'Elasticsearch Search API backend tests' 4 | package: 'Search API' 5 | dependencies: 6 | - search_api_test_db 7 | core: 8.x 8 | hidden: true 9 | -------------------------------------------------------------------------------- /elasticsearch_connector.links.menu.yml: -------------------------------------------------------------------------------- 1 | elasticsearch_connector.config_entity: 2 | title: 'Elasticsearch Connector' 3 | description: 'Administer Elasticsearch clusters and indices.' 4 | route_name: elasticsearch_connector.config_entity.list 5 | parent: system.admin_config_search 6 | -------------------------------------------------------------------------------- /elasticsearch_connector.routing.yml: -------------------------------------------------------------------------------- 1 | elasticsearch_connector.config_entity.list: 2 | path: '/admin/config/search/elasticsearch-connector' 3 | defaults: 4 | _title: 'Elasticsearch Connector' 5 | _entity_list: 'elasticsearch_cluster' 6 | requirements: 7 | _permission: 'administer elasticsearch connector' 8 | -------------------------------------------------------------------------------- /src/Exception/ElasticSearchConnectorException.php: -------------------------------------------------------------------------------- 1 | 'foo'], 'elasticsearch_index'); 20 | $this->assertEquals('foo', $index->id()); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /README.TXT: -------------------------------------------------------------------------------- 1 | Elasticsearch Connector is a set of modules designed to build a full Elasticsearch eco system in Drupal. 2 | 3 | Installation 4 | ============ 5 | 6 | Download this module with the following command: 7 | 8 | ``` 9 | cd /path/to/drupal 10 | composer require drupal/elasticsearch_connector 11 | ``` 12 | 13 | Then, either install it with Drush using `drush en elasticsearch_connector` or via 14 | the administration interface. 15 | 16 | Configuration 17 | ============= 18 | 19 | For setting up Drupal to index content into Elasticsearch, follow the 20 | steps at https://www.lullabot.com/articles/indexing-content-from-drupal-8-to-elasticsearch. 21 | 22 | -------------------------------------------------------------------------------- /tests/src/Unit/ElasticSearch/Parameters/Factory/IndexFactoryTest.php: -------------------------------------------------------------------------------- 1 | definition['format'])) { 23 | $this->definition['format'] = filter_default_format(); 24 | } 25 | parent::init($view, $display, $options); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /tests/src/Unit/Entity/IndexRouteProviderTest.php: -------------------------------------------------------------------------------- 1 | prophesize(EntityTypeInterface::class); 22 | 23 | /** @var \Symfony\Component\Routing\RouteCollection $route_collection */ 24 | $route_collection = $cluster_route_provider->getRoutes($entity_type->reveal()); 25 | $this->assertEquals(2, $route_collection->count()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /tests/src/Unit/Entity/ClusterRouteProviderTest.php: -------------------------------------------------------------------------------- 1 | prophesize(EntityTypeInterface::class); 22 | 23 | /** @var \Symfony\Component\Routing\RouteCollection $route_collection */ 24 | $route_collection = $cluster_route_provider->getRoutes($entity_type->reveal()); 25 | $this->assertEquals(4, $route_collection->count()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /tests/src/Behat/example.behat.local.yml: -------------------------------------------------------------------------------- 1 | # Local Behat configuration file. 2 | # 3 | # See the testing section of the [README.md](README.md) to learn how to use this file. 4 | default: 5 | suites: 6 | default: 7 | contexts: 8 | - ElasticsearchConnectorFeatureContext 9 | - Drupal\DrupalExtension\Context\DrupalContext 10 | - Drupal\DrupalExtension\Context\MinkContext 11 | - Drupal\DrupalExtension\Context\MessageContext 12 | extensions: 13 | Behat\MinkExtension: 14 | goutte: ~ 15 | selenium2: 16 | wd_host: http://MY-IP-ADDRESS:4444/wd/hub 17 | base_url: http://localhost:8080 18 | browser_name: 'chrome' 19 | Drupal\DrupalExtension: 20 | blackbox: ~ 21 | api_driver: 'drupal' 22 | drush: 23 | alias: 'local' 24 | drupal: 25 | drupal_root: '/var/www/docroot' 26 | region_map: 27 | footer: "#footer" 28 | selectors: 29 | message_selector: '.messages' 30 | error_message_selector: '.messages.messages--error' 31 | success_message_selector: '.messages.messages--status' 32 | -------------------------------------------------------------------------------- /src/ElasticSearch/ClientManagerInterface.php: -------------------------------------------------------------------------------- 1 | moduleExists('elasticsearch_connector')) { 24 | \Drupal::service('module_installer')->install(['elasticsearch_connector']); 25 | } 26 | 27 | // Also uninstall the inline form errors module for easier testing. 28 | if ($moduleHandler->moduleExists('inline_form_errors')) { 29 | \Drupal::service('module_installer')->uninstall(['inline_form_errors']); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/src/Behat/behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - ElasticsearchConnectorFeatureContext 6 | - Drupal\DrupalExtension\Context\DrupalContext 7 | - Drupal\DrupalExtension\Context\MinkContext 8 | - Drupal\DrupalExtension\Context\MessageContext 9 | extensions: 10 | Behat\MinkExtension: 11 | goutte: ~ 12 | selenium2: ~ 13 | base_url: http://localhost 14 | sessions: 15 | default: 16 | goutte: ~ 17 | javascript: 18 | selenium2: 19 | browser: phantomjs 20 | wd_host: http://localhost:8910/wd/hub 21 | Drupal\DrupalExtension: 22 | blackbox: ~ 23 | api_driver: 'drupal' 24 | drush: 25 | alias: 'local' 26 | drupal: 27 | drupal_root: '/var/www/html' 28 | region_map: 29 | footer: "#footer" 30 | selectors: 31 | message_selector: '.messages' 32 | error_message_selector: '.messages--error' 33 | success_message_selector: '.messages--status' 34 | Bex\Behat\ScreenshotExtension: 35 | screenshot_taking_mode: all_scenarios 36 | image_drivers: 37 | local: 38 | screenshot_directory: '/var/www/html/artifacts/screenshots' 39 | -------------------------------------------------------------------------------- /src/Event/BuildIndexParamsEvent.php: -------------------------------------------------------------------------------- 1 | params = $params; 27 | $this->indexName = $indexName; 28 | } 29 | 30 | /** 31 | * Getter for the params config array. 32 | * 33 | * @return params 34 | */ 35 | public function getElasticIndexParams() { 36 | return $this->params; 37 | } 38 | 39 | /** 40 | * Setter for the params config array. 41 | * 42 | * @param $params 43 | */ 44 | public function setElasticIndexParams($params) { 45 | $this->params = $params; 46 | } 47 | 48 | /** 49 | * Getter for the index name. 50 | * 51 | * @return indexName 52 | */ 53 | public function getIndexName() { 54 | return $this->indexName; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Event/BuildSearchParamsEvent.php: -------------------------------------------------------------------------------- 1 | params = $params; 27 | $this->indexName = $indexName; 28 | } 29 | 30 | /** 31 | * Getter for the params config array. 32 | * 33 | * @return params 34 | */ 35 | public function getElasticSearchParams() { 36 | return $this->params; 37 | } 38 | 39 | /** 40 | * Setter for the params config array. 41 | * 42 | * @param $params 43 | */ 44 | public function setElasticSearchParams($params) { 45 | $this->params = $params; 46 | } 47 | 48 | /** 49 | * Getter for the index name. 50 | * 51 | * @return indexName 52 | */ 53 | public function getIndexName() { 54 | return $this->indexName; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/modules/elasticsearch_test/config/install/search_api.index.elasticsearch_index.yml: -------------------------------------------------------------------------------- 1 | id: elasticsearch_index 2 | name: 'Test index using elasticsearch module' 3 | description: 'An index used for testing.' 4 | read_only: false 5 | options: 6 | cron_limit: -1 7 | index_directly: false 8 | fields: 9 | 'entity:entity_test/id': 10 | type: integer 11 | 'entity:entity_test/name': 12 | type: text 13 | boost: '5.0' 14 | 'entity:entity_test/body': 15 | type: text 16 | 'entity:entity_test/type': 17 | type: string 18 | 'entity:entity_test/keywords': 19 | type: string 20 | search_api_language: 21 | type: string 22 | processors: 23 | language: 24 | status: true 25 | datasources: 26 | - 'entity:entity_test' 27 | datasource_configs: { } 28 | tracker: default 29 | tracker_config: { } 30 | server: elasticsearch_server 31 | status: 1 32 | langcode: en 33 | dependencies: 34 | config: 35 | - field.instance.entity_test.article.body 36 | - field.instance.entity_test.article.keywords 37 | - field.instance.entity_test.item.body 38 | - field.instance.entity_test.item.keywords 39 | - field.storage.entity_test.body 40 | - field.storage.entity_test.keywords 41 | - search_api.server.elasticsearch_server 42 | module: 43 | - entity_test 44 | -------------------------------------------------------------------------------- /src/Event/PrepareIndexEvent.php: -------------------------------------------------------------------------------- 1 | indexConfig = $indexConfig; 27 | $this->indexName = $indexName; 28 | } 29 | 30 | /** 31 | * Getter for the index config array. 32 | * 33 | * @return indexConfig 34 | */ 35 | public function getIndexConfig() { 36 | return $this->indexConfig; 37 | } 38 | 39 | /** 40 | * Setter for the index config array. 41 | * 42 | * @param $indexConfig 43 | */ 44 | public function setIndexConfig($indexConfig) { 45 | $this->indexConfig = $indexConfig; 46 | } 47 | 48 | /** 49 | * Getter for the index name. 50 | * 51 | * @return indexName 52 | */ 53 | public function getIndexName() { 54 | return $this->indexName; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/elasticsearch_connector", 3 | "description": "Elasticsearch Connector module for Drupal.", 4 | "type": "drupal-module", 5 | "homepage": "https://www.drupal.org/project/elasticsearch_connector", 6 | "require": { 7 | "nodespark/des-connector": "6.x-dev", 8 | "makinacorpus/php-lucene": "^1.0.2" 9 | }, 10 | "repositories": { 11 | "drupal": { 12 | "type": "composer", 13 | "url": "https://packages.drupal.org/7" 14 | } 15 | }, 16 | "require-dev": { 17 | "drupal/search_api": "^1.4", 18 | "behat/mink-selenium2-driver": "^1.3", 19 | "drupal/coder": "^8.2", 20 | "drupal/drupal-extension": "master-dev", 21 | "bex/behat-screenshot": "^1.2", 22 | "phpmd/phpmd": "^2.6", 23 | "phpmetrics/phpmetrics": "^2.3" 24 | }, 25 | "license": "GPL-2.0+", 26 | "authors": [ 27 | { 28 | "name": "Nikolay Ignatov", 29 | "email": "nignatov@nodespark.com", 30 | "homepage": "http://www.nodespark.com", 31 | "role": "Creator and Maintainer" 32 | }, 33 | { 34 | "name": "See other contributors", 35 | "homepage": "https://www.drupal.org/node/2159059/committers" 36 | } 37 | ], 38 | "support": { 39 | "issues": "https://www.drupal.org/project/issues/elasticsearch_connector", 40 | "source": "https://github.com/nodespark/elasticsearch_connector" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Event/PrepareIndexMappingEvent.php: -------------------------------------------------------------------------------- 1 | indexMappingParams = $indexMappingParams; 27 | $this->indexName = $indexName; 28 | } 29 | 30 | /** 31 | * Getter for the index params array. 32 | * 33 | * @return indexMappingParams 34 | */ 35 | public function getIndexMappingParams() { 36 | return $this->indexMappingParams; 37 | } 38 | 39 | /** 40 | * Setter for the index params array. 41 | * 42 | * @param $indexMappingParams 43 | */ 44 | public function setIndexMappingParams($indexMappingParams) { 45 | $this->indexMappingParams = $indexMappingParams; 46 | } 47 | 48 | /** 49 | * Getter for the index name. 50 | * 51 | * @return indexName 52 | */ 53 | public function getIndexName() { 54 | return $this->indexName; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Event/PrepareSearchQueryEvent.php: -------------------------------------------------------------------------------- 1 | elasticSearchQuery = $elasticSearchQuery; 27 | $this->indexName = $indexName; 28 | } 29 | 30 | /** 31 | * Getter for the elasticSearchQuery config array. 32 | * 33 | * @return elasticSearchQuery 34 | */ 35 | public function getElasticSearchQuery() { 36 | return $this->elasticSearchQuery; 37 | } 38 | 39 | /** 40 | * Setter for the elasticSearchQuery config array. 41 | * 42 | * @param $elasticSearchQuery 43 | */ 44 | public function setElasticSearchQuery($elasticSearchQuery) { 45 | $this->elasticSearchQuery = $elasticSearchQuery; 46 | } 47 | 48 | /** 49 | * Getter for the index name. 50 | * 51 | * @return indexName 52 | */ 53 | public function getIndexName() { 54 | return $this->indexName; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Entity/IndexRouteProvider.php: -------------------------------------------------------------------------------- 1 | addDefaults( 24 | [ 25 | '_entity_form' => 'elasticsearch_index.delete', 26 | '_title' => 'Delete index', 27 | ] 28 | ) 29 | ->setRequirement('_entity_access', 'elasticsearch_index.delete'); 30 | $route_collection->add('entity.elasticsearch_index.delete_form', $route); 31 | 32 | $route = (new Route('/admin/config/search/elasticsearch-connector/index/add')) 33 | ->addDefaults( 34 | [ 35 | '_entity_form' => 'elasticsearch_index.default', 36 | '_title' => 'Add index', 37 | ] 38 | ) 39 | ->setRequirement('_entity_create_access', 'elasticsearch_index'); 40 | $route_collection->add('entity.elasticsearch_index.add_form', $route); 41 | 42 | return $route_collection; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Event/PrepareMappingEvent.php: -------------------------------------------------------------------------------- 1 | mappingConfig = $mappingConfig; 29 | $this->type = $type; 30 | $this->field = $field; 31 | } 32 | 33 | /** 34 | * Getter for the mapping config array. 35 | * 36 | * @return mappingConfig 37 | */ 38 | public function getMappingConfig() { 39 | return $this->mappingConfig; 40 | } 41 | 42 | /** 43 | * Setter for the mapping config array. 44 | * 45 | * @param $mappingConfig 46 | */ 47 | public function setMappingConfig($mappingConfig) { 48 | $this->mappingConfig = $mappingConfig; 49 | } 50 | 51 | /** 52 | * Getter for the mapping type. 53 | * 54 | * @return type 55 | */ 56 | public function getMappingType() { 57 | return $this->type; 58 | } 59 | 60 | /** 61 | * Getter for the field. 62 | * 63 | * @return field 64 | */ 65 | public function getMappingField() { 66 | return $this->field; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tests/src/Unit/ElasticSearch/Parameters/Builder/SearchBuilderTest.php: -------------------------------------------------------------------------------- 1 | prophesize(IndexInterface::class); 31 | $query = $this->prophesize(QueryInterface::class); 32 | $query->getIndex() 33 | ->willReturn($index->reveal()); 34 | 35 | $this->searchBuilder = new SearchBuilder($query->reveal()); 36 | } 37 | 38 | /** 39 | * @covers ::__construct 40 | */ 41 | public function testConstruct() { 42 | $this->assertInstanceOf(SearchBuilder::class, $this->searchBuilder); 43 | } 44 | 45 | /** 46 | * @covers ::build 47 | */ 48 | public function testBuild() { 49 | // TODO Can't test because IndexFactory is hardcoded 50 | // instead of injected so it can't be mocked. 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/ElasticsearchViewsHandlerTrait.php: -------------------------------------------------------------------------------- 1 | definition['entity_type'])) { 33 | return $this->definition['entity_type']; 34 | } 35 | return parent::getEntityType(); 36 | } 37 | 38 | /** 39 | * Returns the active search index. 40 | * 41 | * @return string 42 | * The index to use with this filter, or NULL if none could be 43 | * loaded. 44 | */ 45 | protected function getIndex() { 46 | // TODO: Implement. 47 | return NULL; 48 | } 49 | 50 | /** 51 | * Retrieves the query plugin. 52 | */ 53 | public function getQuery() { 54 | return NULL; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /config/schema/elasticsearch_connector.backend.schema.yml: -------------------------------------------------------------------------------- 1 | elasticsearch_connector.backend.plugin.elasticsearch: 2 | type: mapping 3 | label: 'Search API Elasticsearch settings' 4 | mapping: 5 | cluster_settings: 6 | type: mapping 7 | label: 'Elasticsearch settings' 8 | mapping: 9 | cluster: 10 | type: string 11 | label: 'The cluster that handles the connection' 12 | scheme: 13 | type: string 14 | label: 'The HTTP protocol to use for sending queries' 15 | host: 16 | type: string 17 | label: 'The host name or IP of the Elasticsearch server' 18 | port: 19 | type: string 20 | label: 'The port of the Elasticsearch server' 21 | path: 22 | type: string 23 | label: 'The path that identifies the Elasticsearch instance to use on the server' 24 | http_user: 25 | type: string 26 | label: 'Username for basic HTTP authentication' 27 | http_pass: 28 | type: string 29 | label: 'Password for basic HTTP authentication' 30 | excerpt: 31 | type: boolean 32 | label: 'Return an excerpt for all results' 33 | retrieve_data: 34 | type: boolean 35 | label: 'Retrieve result data from Elasticsearch' 36 | highlight_data: 37 | type: boolean 38 | label: 'Highlight retrieved data' 39 | http_method: 40 | type: string 41 | label: 'The HTTP method to use for sending queries' 42 | autocorrect_spell: 43 | type: boolean 44 | label: 'Use spellcheck for autocomplete suggestions' 45 | autocorrect_suggest_words: 46 | type: boolean 47 | label: 'Suggest additional words' 48 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/field/ElasticsearchViewsFieldTrait.php: -------------------------------------------------------------------------------- 1 | array( 19 | 'title' => t('The PHP version is not compatible with this module.'), 20 | 'description' => t('The module requires PHP version bigger than or equal to version 5.3.9.'), 21 | 'severity' => REQUIREMENT_ERROR, 22 | 'value' => t('PHP version not compatible.'), 23 | ), 24 | ); 25 | } 26 | } 27 | 28 | if ($phase == 'runtime') { 29 | if (!interface_exists('\nodespark\DESConnector\ClientInterface')) { 30 | return array( 31 | 'elasticsearch_connector' => array( 32 | 'title' => t('The Elasticsearch client library is missing.'), 33 | 'description' => t('The client library for Elasticsearch connection is missing.'), 34 | 'severity' => REQUIREMENT_ERROR, 35 | 'value' => t('Elasticsearch library missing.'), 36 | ), 37 | ); 38 | } 39 | else { 40 | return array( 41 | 'elasticsearch_connector' => array( 42 | 'title' => t('Elasticsearch PHP client library'), 43 | 'description' => t('The client library for Elasticsearch was correctly installed.'), 44 | 'severity' => REQUIREMENT_OK, 45 | 'value' => t('OK'), 46 | ), 47 | ); 48 | } 49 | } 50 | 51 | return array(); 52 | } 53 | -------------------------------------------------------------------------------- /src/Entity/Index.php: -------------------------------------------------------------------------------- 1 | index_id) ? $this->index_id : NULL; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/ClusterManager.php: -------------------------------------------------------------------------------- 1 | state = $state; 35 | $this->entityTypeManager = $entity_type_manager; 36 | } 37 | 38 | /** 39 | * Get the default (cluster) used by Elasticsearch. 40 | * 41 | * @return string 42 | * The cluster identifier. 43 | */ 44 | public function getDefaultCluster() { 45 | return $this->state->get('elasticsearch_connector_get_default_connector', 46 | ''); 47 | } 48 | 49 | /** 50 | * Set the default (cluster) used by Elasticsearch. 51 | * 52 | * @param string $cluster_id 53 | * The new cluster identifier. 54 | */ 55 | public function setDefaultCluster($cluster_id) { 56 | $this->state->set( 57 | 'elasticsearch_connector_get_default_connector', 58 | $cluster_id 59 | ); 60 | } 61 | 62 | /** 63 | * Load all clusters. 64 | * 65 | * @param bool $include_inactive 66 | * 67 | * @return \Drupal\elasticsearch_connector\Entity\Cluster[] 68 | */ 69 | public function loadAllClusters($include_inactive = TRUE) { 70 | $clusters = $this->entityTypeManager->getStorage('elasticsearch_cluster')->loadMultiple(); 71 | foreach ($clusters as $cluster) { 72 | if (!$include_inactive && !$cluster->status) { 73 | unset($clusters[$cluster->cluster_id]); 74 | } 75 | } 76 | 77 | return $clusters; 78 | } 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /tests/src/Unit/ElasticSearch/ClientManagerTest.php: -------------------------------------------------------------------------------- 1 | prophesize(ModuleHandlerInterface::class); 34 | $client_factory = $this->prophesize(ClientFactoryInterface::class); 35 | $client_factory->create(Argument::type('array')) 36 | ->willReturn($this->prophesize(ClientInterface::class)->reveal()); 37 | 38 | $this->clientManager = new ClientManager($module_handler->reveal(), $client_factory->reveal()); 39 | } 40 | 41 | /** 42 | * @covers ::__construct 43 | */ 44 | public function testConstruct() { 45 | $this->assertInstanceOf(ClientManager::class, $this->clientManager); 46 | } 47 | 48 | /** 49 | * @covers ::getClientForCluster 50 | */ 51 | public function testGetClientForCluster() { 52 | $cluster = $this->prophesize(Cluster::class); 53 | $cluster->getRawUrl() 54 | ->willReturn('http://example.com'); 55 | $cluster = $cluster->reveal(); 56 | $cluster->options['use_authentication'] = TRUE; 57 | $cluster->options['username'] = 'Tom'; 58 | $cluster->options['password'] = 'Waits'; 59 | $cluster->options['authentication_type'] = 'basic_auth'; 60 | 61 | $client = $this->clientManager->getClientForCluster($cluster); 62 | $this->assertInstanceOf(ClientInterface::class, $client); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Plugin/search_api/processor/ExcludeSourceFields.php: -------------------------------------------------------------------------------- 1 | t( 36 | 'Select the fields to exclude from the source field in search results. See the Elasticsearch documentation on source filtering for more info.', 37 | [ 38 | ':url' => 'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html', 39 | ] 40 | ); 41 | 42 | foreach ($this->index->getFields() as $field) { 43 | $excluded = !empty($this->configuration['fields'][$field->getFieldIdentifier()]); 44 | $form['fields'][$field->getFieldIdentifier()] = array( 45 | '#type' => 'checkbox', 46 | '#title' => $field->getPrefixedLabel(), 47 | '#default_value' => $excluded, 48 | ); 49 | } 50 | 51 | return $form; 52 | } 53 | 54 | public function preprocessSearchQuery(QueryInterface $query) { 55 | $excluded_fields = array_filter($this->configuration['fields']); 56 | 57 | $query->setOption( 58 | 'elasticsearch_connector_exclude_source_fields', 59 | array_keys($excluded_fields) 60 | ); 61 | } 62 | 63 | public static function supportsIndex(IndexInterface $index) { 64 | $backend = $index->getServerInstance()->getBackend(); 65 | return $backend instanceof SearchApiElasticsearchBackend; 66 | } 67 | } -------------------------------------------------------------------------------- /src/ElasticSearch/ClientManager.php: -------------------------------------------------------------------------------- 1 | moduleHandler = $module_handler; 40 | $this->clientManagerFactory = $clientManagerFactory; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getClientForCluster(Cluster $cluster) { 47 | $hosts = [ 48 | [ 49 | 'url' => $cluster->url, 50 | 'options' => $cluster->options, 51 | ], 52 | ]; 53 | 54 | $hash = json_encode($hosts); 55 | if (!isset($this->clients[$hash])) { 56 | $options = [ 57 | 'hosts' => [ 58 | $cluster->getRawUrl(), 59 | ], 60 | 'options' => [], 61 | 'curl' => [ 62 | CURLOPT_CONNECTTIMEOUT => (!empty($cluster->options['timeout']) ? $cluster->options['timeout'] : Cluster::ELASTICSEARCH_CONNECTOR_DEFAULT_TIMEOUT), 63 | ], 64 | ]; 65 | 66 | if ($cluster->options['use_authentication']) { 67 | $options['auth'] = [ 68 | $cluster->url => [ 69 | 'username' => $cluster->options['username'], 70 | 'password' => $cluster->options['password'], 71 | 'method' => $cluster->options['authentication_type'], 72 | ], 73 | ]; 74 | } 75 | 76 | $this->moduleHandler->alter( 77 | 'elasticsearch_connector_load_library_options', 78 | $options, 79 | $cluster 80 | ); 81 | 82 | $this->clients[$hash] = $this->clientManagerFactory->create($options); 83 | } 84 | 85 | return $this->clients[$hash]; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/ElasticSearch/Parameters/Factory/SearchFactory.php: -------------------------------------------------------------------------------- 1 | build(); 27 | } 28 | 29 | /** 30 | * Parse a Elasticsearch response into a ResultSetInterface. 31 | * 32 | * TODO: Add excerpt handling. 33 | * 34 | * @param \Drupal\search_api\Query\QueryInterface $query 35 | * Search API query. 36 | * @param array $response 37 | * Raw response array back from Elasticsearch. 38 | * 39 | * @return \Drupal\search_api\Query\ResultSetInterface 40 | * The results of the search. 41 | */ 42 | public static function parseResult(QueryInterface $query, array $response) { 43 | $index = $query->getIndex(); 44 | 45 | // Set up the results array. 46 | $results = $query->getResults(); 47 | $results->setExtraData('elasticsearch_response', $response); 48 | $results->setResultCount($response['hits']['total']); 49 | /** @var \Drupal\search_api\Utility\FieldsHelper $fields_helper */ 50 | $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper'); 51 | 52 | // Add each search result to the results array. 53 | if (!empty($response['hits']['hits'])) { 54 | foreach ($response['hits']['hits'] as $result) { 55 | $result_item = $fields_helper->createItem($index, $result['_id']); 56 | $result_item->setScore($result['_score']); 57 | 58 | // Set each item in _source as a field in Search API. 59 | foreach ($result['_source'] as $elasticsearch_property_id => $elasticsearch_property) { 60 | // Make everything a multifield. 61 | if (!is_array($elasticsearch_property)) { 62 | $elasticsearch_property = [$elasticsearch_property]; 63 | } 64 | $field = $fields_helper->createField($index, $elasticsearch_property_id, ['property_path' => $elasticsearch_property_id]); 65 | $field->setValues($elasticsearch_property); 66 | $result_item->setField($elasticsearch_property_id, $field); 67 | } 68 | 69 | $results->addResultItem($result_item); 70 | } 71 | } 72 | 73 | return $results; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/ElasticSearch/Parameters/Factory/MappingFactory.php: -------------------------------------------------------------------------------- 1 | getType(); 24 | $mappingConfig = NULL; 25 | 26 | switch ($type) { 27 | case 'text': 28 | $mappingConfig = [ 29 | 'type' => 'text', 30 | 'boost' => $field->getBoost(), 31 | 'fields' => [ 32 | "keyword" => [ 33 | "type" => 'keyword', 34 | 'ignore_above' => 256, 35 | ] 36 | ] 37 | ]; 38 | break; 39 | 40 | case 'uri': 41 | case 'string': 42 | case 'token': 43 | $mappingConfig = [ 44 | 'type' => 'keyword', 45 | ]; 46 | break; 47 | 48 | case 'integer': 49 | case 'duration': 50 | $mappingConfig = [ 51 | 'type' => 'integer', 52 | ]; 53 | break; 54 | 55 | case 'boolean': 56 | $mappingConfig = [ 57 | 'type' => 'boolean', 58 | ]; 59 | break; 60 | 61 | case 'decimal': 62 | $mappingConfig = [ 63 | 'type' => 'float', 64 | ]; 65 | break; 66 | 67 | case 'date': 68 | $mappingConfig = [ 69 | 'type' => 'date', 70 | 'format' => 'strict_date_optional_time||epoch_second', 71 | ]; 72 | break; 73 | 74 | case 'attachment': 75 | $mappingConfig = [ 76 | 'type' => 'attachment', 77 | ]; 78 | break; 79 | 80 | case 'object': 81 | $mappingConfig = [ 82 | 'type' => 'nested', 83 | ]; 84 | break; 85 | 86 | case 'location': 87 | $mappingConfig = [ 88 | 'type' => 'geo_point', 89 | ]; 90 | break; 91 | } 92 | 93 | // Allow other modules to alter mapping config before we create it. 94 | $dispatcher = \Drupal::service('event_dispatcher'); 95 | $prepareMappingEvent = new PrepareMappingEvent($mappingConfig, $type, $field); 96 | $event = $dispatcher->dispatch(PrepareMappingEvent::PREPARE_MAPPING, $prepareMappingEvent); 97 | $mappingConfig = $event->getMappingConfig(); 98 | 99 | return $mappingConfig; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/Entity/ClusterRouteProvider.php: -------------------------------------------------------------------------------- 1 | addDefaults( 24 | [ 25 | '_controller' => '\Drupal\elasticsearch_connector\Controller\ElasticsearchController::getInfo', 26 | '_title_callback' => '\Drupal\elasticsearch_connector\Controller\ElasticsearchController::pageTitle', 27 | '_title' => 'Cluster Info', 28 | ] 29 | ) 30 | ->setRequirement('_entity_access', 'elasticsearch_cluster.view') 31 | ->setOptions( 32 | [ 33 | 'parameters' => [ 34 | 'elasticsearch_cluster' => [ 35 | 'with_config_overrides' => TRUE, 36 | ], 37 | ], 38 | ] 39 | ); 40 | $route_collection->add('entity.elasticsearch_cluster.canonical', $route); 41 | 42 | $route = (new Route('/admin/config/search/elasticsearch-connector/cluster/{elasticsearch_cluster}/delete')) 43 | ->addDefaults( 44 | [ 45 | '_entity_form' => 'elasticsearch_cluster.delete', 46 | '_title' => 'Delete cluster', 47 | ] 48 | ) 49 | ->setRequirement('_entity_access', 'elasticsearch_cluster.delete'); 50 | $route_collection->add('entity.elasticsearch_cluster.delete_form', $route); 51 | 52 | $route = (new Route('/admin/config/search/elasticsearch-connector/cluster/{elasticsearch_cluster}/edit')) 53 | ->addDefaults( 54 | [ 55 | '_entity_form' => 'elasticsearch_cluster.default', 56 | '_title_callback' => '\Drupal\elasticsearch_connector\Controller\ElasticsearchController::pageTitle', 57 | '_title' => 'Edit cluster', 58 | ] 59 | ) 60 | ->setRequirement('_entity_access', 'elasticsearch_cluster.update'); 61 | $route_collection->add('entity.elasticsearch_cluster.edit_form', $route); 62 | 63 | $route = (new Route('/admin/config/search/elasticsearch-connector/cluster/add')) 64 | ->addDefaults( 65 | [ 66 | '_entity_form' => 'elasticsearch_cluster.default', 67 | '_title' => 'Add cluster', 68 | ] 69 | ) 70 | ->setRequirement('_entity_create_access', 'elasticsearch_cluster'); 71 | $route_collection->add('entity.elasticsearch_cluster.add_form', $route); 72 | 73 | return $route_collection; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /phpunit.core.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ./tests/TestSuites/UnitTestSuite.php 37 | 38 | 39 | ./tests/TestSuites/KernelTestSuite.php 40 | 41 | 42 | ./tests/TestSuites/FunctionalTestSuite.php 43 | 44 | 45 | ./tests/TestSuites/UnitTestSuite.php 46 | ./tests/TestSuites/KernelTestSuite.php 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ../modules/elasticsearch_connector 57 | 58 | ../modules/elasticsearch_connector/tests 59 | ../modules/elasticsearch_connector/test_modules 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/src/Unit/ClusterManagerTest.php: -------------------------------------------------------------------------------- 1 | prophesize(StateInterface::class); 33 | $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); 34 | 35 | $this->clusterManager = new ClusterManager($state->reveal(), $entity_type_manager->reveal()); 36 | } 37 | 38 | /** 39 | * @covers ::__construct 40 | */ 41 | public function testConstruct() { 42 | $this->assertInstanceOf(ClusterManager::class, $this->clusterManager); 43 | } 44 | 45 | /** 46 | * @covers ::getDefaultCluster 47 | * @covers ::setDefaultCluster 48 | */ 49 | public function testGetSetDefaultCluster() { 50 | $state = $this->prophesize(StateInterface::class); 51 | $state->get('elasticsearch_connector_get_default_connector', '') 52 | ->willReturn('foo'); 53 | $state->set('elasticsearch_connector_get_default_connector', 'foo') 54 | ->shouldBeCalled(); 55 | 56 | // Check the get method. 57 | $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); 58 | $this->clusterManager = new ClusterManager($state->reveal(), $entity_type_manager->reveal()); 59 | $this->assertEquals('foo', $this->clusterManager->getDefaultCluster()); 60 | 61 | // Check the set method (a prediction was set above). 62 | $this->clusterManager->setDefaultCluster('foo'); 63 | } 64 | 65 | /** 66 | * @covers ::loadAllClusters 67 | */ 68 | public function testLoadAllClusters() { 69 | $state = $this->prophesize(StateInterface::class); 70 | 71 | $cluster1 = new \stdClass(); 72 | $cluster1->cluster_id = 'foo'; 73 | $cluster1->status = FALSE; 74 | 75 | $cluster2 = new \stdClass(); 76 | $cluster2->cluster_id = 'bar'; 77 | $cluster2->status = TRUE; 78 | 79 | $entity_storage_interface = $this->prophesize(EntityStorageInterface::class); 80 | $entity_storage_interface->loadMultiple() 81 | ->willReturn([ 82 | $cluster1->cluster_id => $cluster1, 83 | $cluster2->cluster_id => $cluster2, 84 | ]); 85 | 86 | $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); 87 | $entity_type_manager->getStorage('elasticsearch_cluster') 88 | ->willReturn($entity_storage_interface->reveal()); 89 | 90 | $this->clusterManager = new ClusterManager($state->reveal(), $entity_type_manager->reveal()); 91 | 92 | $expected_clusters = [ 93 | $cluster2->cluster_id => $cluster2, 94 | ]; 95 | $this->assertEquals($expected_clusters, $this->clusterManager->loadAllClusters(FALSE)); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Entity/Cluster.php: -------------------------------------------------------------------------------- 1 | cluster_id) ? $this->cluster_id : NULL; 99 | } 100 | 101 | 102 | /** 103 | * Get the full base URL of the cluster, including any authentication. 104 | * 105 | * @return string 106 | */ 107 | public function getSafeUrl() { 108 | $options = $this->options; 109 | $url_parsed = parse_url($this->url); 110 | if ($options['use_authentication']) { 111 | return $url_parsed['scheme'] . '://' 112 | . $options['username'] . ':****@' 113 | . $url_parsed['host'] 114 | . (isset($url_parsed['port']) ? ':' . $url_parsed['port'] : ''); 115 | } 116 | else { 117 | return $url_parsed['scheme'] . '://' 118 | . (isset($url_parsed['user']) ? $url_parsed['user'] . ':****@' : '') 119 | . $url_parsed['host'] 120 | . (isset($url_parsed['port']) ? ':' . $url_parsed['port'] : ''); 121 | } 122 | } 123 | 124 | /** 125 | * Get the raw url. 126 | * 127 | * @return string 128 | */ 129 | public function getRawUrl() { 130 | return $this->url; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/Form/IndexDeleteForm.php: -------------------------------------------------------------------------------- 1 | clientManager = $client_manager; 41 | $this->entityTypeManager = $entity_type_manager; 42 | } 43 | 44 | /** 45 | * 46 | */ 47 | static public function create(ContainerInterface $container) { 48 | return new static ( 49 | $container->get('elasticsearch_connector.client_manager'), 50 | $container->get('entity_type.manager'), 51 | $container->get('elasticsearch_connector.cluster_manager') 52 | ); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getQuestion() { 59 | return $this->t('Are you sure you want to delete the index %title?', array('%title' => $this->entity->label())); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function submitForm(array &$form, FormStateInterface $form_state) { 66 | /** @var Cluster $cluster */ 67 | $cluster = $this->entityTypeManager->getStorage('elasticsearch_cluster')->load($this->entity->server); 68 | $client = $this->clientManager->getClientForCluster($cluster); 69 | try { 70 | if ($client->indices() 71 | ->exists(array('index' => $this->entity->index_id)) 72 | ) { 73 | $client->indices()->delete(['index' => $this->entity->index_id]); 74 | } 75 | $this->entity->delete(); 76 | $this->messenger()->addMessage($this->t('The index %title has been deleted.', array('%title' => $this->entity->label()))); 77 | $form_state->setRedirect('elasticsearch_connector.config_entity.list'); 78 | } 79 | catch (Missing404Exception $e) { 80 | // The index was not found, so just remove it anyway. 81 | $this->messenger()->addError($e->getMessage()); 82 | } 83 | catch (\Exception $e) { 84 | $this->messenger()->addError($e->getMessage()); 85 | } 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getConfirmText() { 92 | return $this->t('Delete'); 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function getCancelUrl() { 99 | return new Url('elasticsearch_connector.config_entity.list'); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /js/ec-index.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | Drupal.behaviors.ECIndexDialog = { 4 | attach: function (context, settings) { 5 | $('a.ec-index-dialog').once('ec-index-dialog', function () { 6 | $(this).click(function () { 7 | 8 | Drupal.ECIndexDialog.open($(this).attr('href'), $(this).html()); 9 | Drupal.ECIndexDialog.populateIndex = function (cluster_id, index_name, settings) { 10 | // Add new option and select it. 11 | $('#' + settings.index_element_id) 12 | .append('') 13 | .val(index_name); 14 | 15 | // Trigger change to update the form cache. 16 | $('#' + settings.cluster_element_id).trigger('change'); 17 | }; 18 | 19 | return false; 20 | }, context); 21 | }); 22 | } 23 | }; 24 | 25 | /** 26 | * Our dialog object. Can be used to open a dialog to anywhere. 27 | */ 28 | Drupal.ECIndexDialog = { 29 | dialog_open: false, 30 | open_dialog: null 31 | }; 32 | 33 | /** 34 | * If this property is set to be a function, it 35 | * will be called when an entity is received from an overlay. 36 | */ 37 | Drupal.ECIndexDialog.populateIndex = null; 38 | 39 | /** 40 | * Open a dialog window. 41 | * 42 | * @param href 43 | * @param title 44 | */ 45 | Drupal.ECIndexDialog.open = function (href, title) { 46 | if (!this.dialog_open) { 47 | // Get the current window size and do 75% of the width and 90% of the height. 48 | // @todo Add settings for this so that users can configure this by themselves. 49 | var window_width = $(window).width() / 100 * 75; 50 | var window_height = $(window).height() / 100 * 90; 51 | this.open_dialog = $('').dialog({ 52 | width: window_width, 53 | height: window_height, 54 | modal: true, 55 | resizable: false, 56 | position: ["center", 50], 57 | title: title, 58 | close: function () { 59 | Drupal.ECIndexDialog.dialog_open = false; 60 | } 61 | }).width(window_width - 10).height(window_height); 62 | $(window).bind("resize scroll", function () { 63 | // Move the dialog the main window moves. 64 | if (typeof Drupal.ECIndexDialog == "object" && Drupal.ECIndexDialog.open_dialog != null) { 65 | Drupal.ECIndexDialog.open_dialog.dialog("option", "position", ["center", 10]); 66 | Drupal.ECIndexDialog.setDimensions(); 67 | } 68 | }); 69 | this.dialog_open = true; 70 | } 71 | }; 72 | 73 | /** 74 | * Set dimensions of the dialog depending on the current window size 75 | * and scroll position. 76 | */ 77 | Drupal.ECIndexDialog.setDimensions = function () { 78 | if (typeof Drupal.ECIndexDialog == "object") { 79 | var window_width = $(window).width() / 100 * 75; 80 | var window_height = $(window).height() / 100 * 90; 81 | this.open_dialog.dialog("option", "width", window_width).dialog("option", "height", window_height).width(window_width - 10).height(window_height); 82 | } 83 | }; 84 | 85 | /** 86 | * Close the dialog and provide an entity id and a title 87 | * that we can use in various ways. 88 | */ 89 | Drupal.ECIndexDialog.close = function (cluster_id, index_name, settings) { 90 | this.open_dialog.dialog('close'); 91 | this.open_dialog.dialog('destroy'); 92 | this.open_dialog = null; 93 | this.dialog_open = false; 94 | // Call our populateIndex function if we have one. 95 | // this is used as an event. 96 | if (typeof this.populateIndex == "function") { 97 | this.populateIndex(cluster_id, index_name, settings); 98 | } 99 | } 100 | }(jQuery)); 101 | -------------------------------------------------------------------------------- /tests/src/Unit/ElasticSearch/Parameters/Factory/MappingFactoryTest.php: -------------------------------------------------------------------------------- 1 | prophesize(FieldInterface::class); 22 | $field->getType() 23 | ->willReturn('text'); 24 | $field->getBoost() 25 | ->willReturn(1); 26 | 27 | $expected_mapping = [ 28 | 'type' => 'text', 29 | 'boost' => 1, 30 | 'fields' => [ 31 | "keyword" => [ 32 | "type" => 'keyword', 33 | 'ignore_above' => 256, 34 | ] 35 | ] 36 | ]; 37 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 38 | 39 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 40 | $field = $this->prophesize(FieldInterface::class); 41 | $field->getType() 42 | ->willReturn('uri'); 43 | 44 | $expected_mapping = [ 45 | 'type' => 'keyword', 46 | ]; 47 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 48 | 49 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 50 | $field = $this->prophesize(FieldInterface::class); 51 | $field->getType() 52 | ->willReturn('integer'); 53 | 54 | $expected_mapping = [ 55 | 'type' => 'integer', 56 | ]; 57 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 58 | 59 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 60 | $field = $this->prophesize(FieldInterface::class); 61 | $field->getType() 62 | ->willReturn('boolean'); 63 | 64 | $expected_mapping = [ 65 | 'type' => 'boolean', 66 | ]; 67 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 68 | 69 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 70 | $field = $this->prophesize(FieldInterface::class); 71 | $field->getType() 72 | ->willReturn('decimal'); 73 | 74 | $expected_mapping = [ 75 | 'type' => 'float', 76 | ]; 77 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 78 | 79 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 80 | $field = $this->prophesize(FieldInterface::class); 81 | $field->getType() 82 | ->willReturn('date'); 83 | 84 | $expected_mapping = [ 85 | 'type' => 'date', 86 | 'format' => 'epoch_second', 87 | ]; 88 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 89 | 90 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 91 | $field = $this->prophesize(FieldInterface::class); 92 | $field->getType() 93 | ->willReturn('attachment'); 94 | 95 | $expected_mapping = [ 96 | 'type' => 'attachment', 97 | ]; 98 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 99 | 100 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 101 | $field = $this->prophesize(FieldInterface::class); 102 | $field->getType() 103 | ->willReturn('object'); 104 | 105 | $expected_mapping = [ 106 | 'type' => 'nested', 107 | ]; 108 | $this->assertEquals($expected_mapping, MappingFactory::mappingFromField($field->reveal())); 109 | 110 | /** @var \Prophecy\Prophecy\ObjectProphecy $field_prophecy */ 111 | $field = $this->prophesize(FieldInterface::class); 112 | $field->getType() 113 | ->willReturn('foo'); 114 | 115 | $this->assertNull(MappingFactory::mappingFromField($field->reveal())); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Form/ClusterDeleteForm.php: -------------------------------------------------------------------------------- 1 | entityManager = $entity_manager; 57 | $this->clientManager = $client_manager; 58 | $this->clusterManager = $cluster_manager; 59 | } 60 | 61 | /** 62 | * 63 | */ 64 | static public function create(ContainerInterface $container) { 65 | return new static ( 66 | $container->get('entity_type.manager'), 67 | $container->get('elasticsearch_connector.client_manager'), 68 | $container->get('elasticsearch_connector.cluster_manager') 69 | ); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getQuestion() { 76 | return t( 77 | 'Are you sure you want to delete the cluster %title?', 78 | ['%title' => $this->entity->label()] 79 | ); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function getDescription() { 86 | return $this->t( 87 | 'Deleting a cluster will disable all its indexes and their searches.' 88 | ); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function submitForm(array &$form, FormStateInterface $form_state) { 95 | $storage = $this->entityManager->getStorage('elasticsearch_index'); 96 | $indices = $storage->loadByProperties( 97 | ['server' => $this->entity->cluster_id] 98 | ); 99 | 100 | // TODO: handle indices linked to the cluster being deleted. 101 | if (count($indices)) { 102 | $this->messenger()->addError( 103 | $this->t( 104 | 'The cluster %title cannot be deleted as it still has indices.', 105 | ['%title' => $this->entity->label()] 106 | ) 107 | ); 108 | return; 109 | } 110 | 111 | if ($this->entity->id() == $this->clusterManager->getDefaultCluster()) { 112 | $this->messenger()->addError( 113 | $this->t( 114 | 'The cluster %title cannot be deleted as it is set as the default cluster.', 115 | ['%title' => $this->entity->label()] 116 | ) 117 | ); 118 | } 119 | else { 120 | $this->entity->delete(); 121 | $this->messenger()->addMessage( 122 | $this->t( 123 | 'The cluster %title has been deleted.', 124 | ['%title' => $this->entity->label()] 125 | ) 126 | ); 127 | $form_state->setRedirect('elasticsearch_connector.config_entity.list'); 128 | } 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | */ 134 | public function getConfirmText() { 135 | return $this->t('Delete'); 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function getCancelUrl() { 142 | return new Url('elasticsearch_connector.config_entity.list'); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Default configuration file for Drupal modules. 2 | # 3 | # Use setup.sh to automate setting this up. Otherwise, to use this in a new 4 | # module: 5 | # 1. Copy config.yml to the module's .circleci directory. 6 | # 2. Change 'latest' in the image tag to the latest tag. 7 | # 3. Update the working_directory key. 8 | # 4. Connect CircleCI to the repository through the Circle UI. 9 | # 5. Set the COMPOSER_AUTH environment variable in Circle to grant access to 10 | # any private repositories. 11 | # 6. Create a status badge embed code in Circle and add it to the README.md. 12 | # 13 | # Check https://circleci.com/docs/2.0/language-php/ for more details 14 | # 15 | 16 | defaults: &defaults 17 | docker: 18 | # specify the version you desire here (avoid latest except for testing) 19 | - image: andrewberry/drupal_tests:0.0.11 20 | 21 | # Use our fork until https://github.com/wernight/docker-phantomjs/pull/3 is 22 | # merged. 23 | # - image: wernight/phantomjs:2.1.1 24 | - image: andrewberry/docker-phantomjs:v2.1.1-patch1 25 | command: [phantomjs, --webdriver=8910] 26 | 27 | - image: mariadb:10.3 28 | environment: 29 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 30 | 31 | # Specify service dependencies here if necessary 32 | # CircleCI maintains a library of pre-built images 33 | # documented at https://circleci.com/docs/2.0/circleci-images/ 34 | # - image: circleci/mysql:9.4 35 | 36 | # 'checkout' supports a path key, but not on locals where you test with the 37 | # circleci CLI tool. 38 | # https://discuss.circleci.com/t/bug-circleci-build-command-ignores-checkout-path-config/13004 39 | working_directory: /var/www/html/modules/elasticsearch_connector 40 | 41 | # YAML does not support merging of lists. That means we can't have a default 42 | # 'steps' configuration, though we can have defaults for individual step 43 | # properties. 44 | 45 | # We use the composer.json as a way to determine if we can cache our build. 46 | restore_cache: &restore_cache 47 | keys: 48 | - v1-dependencies-{{ checksum "composer.json" }} 49 | # fallback to using the latest cache if no exact match is found 50 | - v1-dependencies- 51 | 52 | # If composer.json hasn't changed, restore the vendor directory. We don't 53 | # restore the lock file so we ensure we get updated dependencies. 54 | save_cache: &save_cache 55 | paths: 56 | - ./vendor 57 | key: v1-dependencies-{{ checksum "composer.json" }} 58 | 59 | # Run Drupal unit and kernel tests as one job. This command invokes the test.sh 60 | # hook. 61 | unit_kernel_tests: &unit_kernel_tests 62 | <<: *defaults 63 | steps: 64 | - checkout 65 | 66 | - restore_cache: *restore_cache 67 | - save_cache: *save_cache 68 | 69 | - run: 70 | working_directory: /var/www/html 71 | command: | 72 | ./test.sh $CIRCLE_PROJECT_REPONAME 73 | 74 | - store_test_results: 75 | path: /var/www/html/artifacts/phpunit 76 | - store_artifacts: 77 | path: /var/www/html/artifacts 78 | 79 | # Run Behat tests. This command invokes the test-js.sh hook. 80 | behat_tests: &behat_tests 81 | <<: *defaults 82 | steps: 83 | - checkout 84 | 85 | - restore_cache: *restore_cache 86 | - save_cache: *save_cache 87 | 88 | - run: 89 | working_directory: /var/www/html 90 | command: | 91 | ./test-js.sh $CIRCLE_PROJECT_REPONAME 92 | 93 | - store_test_results: 94 | path: /var/www/html/artifacts/behat 95 | - store_artifacts: 96 | path: /var/www/html/artifacts 97 | 98 | # Run code quality tests. This invokes code-sniffer.sh. 99 | code_sniffer: &code_sniffer 100 | <<: *defaults 101 | steps: 102 | - checkout 103 | 104 | - restore_cache: *restore_cache 105 | - save_cache: *save_cache 106 | 107 | - run: 108 | working_directory: /var/www/html 109 | command: | 110 | ./code-sniffer.sh $CIRCLE_PROJECT_REPONAME 111 | 112 | - store_test_results: 113 | path: /var/www/html/artifacts/phpcs 114 | - store_artifacts: 115 | path: /var/www/html/artifacts 116 | 117 | # Run code coverage tests. This invokes code-coverage-stats.sh. 118 | code_coverage: &code_coverage 119 | <<: *defaults 120 | steps: 121 | - checkout 122 | 123 | - restore_cache: *restore_cache 124 | - save_cache: *save_cache 125 | 126 | - run: 127 | working_directory: /var/www/html 128 | command: | 129 | ./code-coverage-stats.sh $CIRCLE_PROJECT_REPONAME 130 | - store_artifacts: 131 | path: /var/www/html/artifacts 132 | 133 | # Declare all of the jobs we should run. 134 | version: 2 135 | jobs: 136 | run-unit-kernel-tests: 137 | <<: *unit_kernel_tests 138 | run-behat-tests: 139 | <<: *behat_tests 140 | run-code-sniffer: 141 | <<: *code_sniffer 142 | run-code-coverage: 143 | <<: *code_coverage 144 | 145 | workflows: 146 | version: 2 147 | 148 | # Declare a workflow that runs all of our jobs in parallel. 149 | test_and_lint: 150 | jobs: 151 | - run-unit-kernel-tests 152 | - run-behat-tests 153 | - run-code-sniffer 154 | - run-code-coverage 155 | -------------------------------------------------------------------------------- /src/ElasticSearch/Parameters/Factory/FilterFactory.php: -------------------------------------------------------------------------------- 1 | getValue())) { 24 | switch ($condition->getOperator()) { 25 | case '<>': 26 | $filter = [ 27 | 'exists' => ['field' => $condition->getField()], 28 | ]; 29 | break; 30 | 31 | case '=': 32 | $filter = [ 33 | 'bool' => [ 34 | 'must_not' => [ 35 | 'exists' => ['field' => $condition->getField()], 36 | ], 37 | ], 38 | ]; 39 | break; 40 | 41 | default: 42 | throw new \Exception( 43 | 'Value is empty for ' . $condition->getField() . '. Incorrect filter criteria is using for searching!' 44 | ); 45 | } 46 | } 47 | // Normal filters. 48 | else { 49 | switch ($condition->getOperator()) { 50 | case '=': 51 | $filter = [ 52 | 'term' => [$condition->getField() => $condition->getValue()], 53 | ]; 54 | break; 55 | 56 | case 'IN': 57 | $filter = [ 58 | 'terms' => [$condition->getField() => array_values($condition->getValue())], 59 | ]; 60 | break; 61 | 62 | case 'NOT IN': 63 | $filter = [ 64 | 'bool' => [ 65 | 'must_not' => [ 66 | 'terms' => [$condition->getField() => array_values($condition->getValue())], 67 | ], 68 | ] 69 | ]; 70 | break; 71 | 72 | case '<>': 73 | $filter = [ 74 | 'bool' => [ 75 | 'must_not' => [ 76 | 'term' => [$condition->getField() => $condition->getValue()], 77 | ], 78 | ] 79 | ]; 80 | break; 81 | 82 | case '>': 83 | $filter = [ 84 | 'range' => [ 85 | $condition->getField() => [ 86 | 'from' => $condition->getValue(), 87 | 'to' => NULL, 88 | 'include_lower' => FALSE, 89 | 'include_upper' => FALSE, 90 | ], 91 | ], 92 | ]; 93 | break; 94 | 95 | case '>=': 96 | $filter = [ 97 | 'range' => [ 98 | $condition->getField() => [ 99 | 'from' => $condition->getValue(), 100 | 'to' => NULL, 101 | 'include_lower' => TRUE, 102 | 'include_upper' => FALSE, 103 | ], 104 | ], 105 | ]; 106 | break; 107 | 108 | case '<': 109 | $filter = [ 110 | 'range' => [ 111 | $condition->getField() => [ 112 | 'from' => NULL, 113 | 'to' => $condition->getValue(), 114 | 'include_lower' => FALSE, 115 | 'include_upper' => FALSE, 116 | ], 117 | ], 118 | ]; 119 | break; 120 | 121 | case '<=': 122 | $filter = [ 123 | 'range' => [ 124 | $condition->getField() => [ 125 | 'from' => NULL, 126 | 'to' => $condition->getValue(), 127 | 'include_lower' => FALSE, 128 | 'include_upper' => TRUE, 129 | ], 130 | ], 131 | ]; 132 | break; 133 | 134 | case 'BETWEEN': 135 | $filter = [ 136 | 'range' => [ 137 | $condition->getField() => [ 138 | 'from' => (!empty($condition->getValue()[0])) ? $condition->getValue()[0] : NULL, 139 | 'to' => (!empty($condition->getValue()[1])) ? $condition->getValue()[1] : NULL, 140 | 'include_lower' => FALSE, 141 | 'include_upper' => FALSE, 142 | ], 143 | ], 144 | ]; 145 | break; 146 | 147 | case 'NOT BETWEEN': 148 | $filter = [ 149 | 'bool' => [ 150 | 'must_not' => [ 151 | 'range' => [ 152 | $condition->getField() => [ 153 | 'from' => (!empty($condition->getValue()[0])) ? $condition->getValue()[0] : NULL, 154 | 'to' => (!empty($condition->getValue()[1])) ? $condition->getValue()[1] : NULL, 155 | 'include_lower' => FALSE, 156 | 'include_upper' => FALSE, 157 | ], 158 | ], 159 | ] 160 | ] 161 | ]; 162 | break; 163 | 164 | default: 165 | throw new \Exception('Undefined operator ' . $condition->getOperator() . ' for ' . $condition->getField() . ' field! Incorrect filter criteria is using for searching!'); 166 | } 167 | } 168 | 169 | return $filter; 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/filter/ElasticsearchViewsStringFilter.php: -------------------------------------------------------------------------------- 1 | get('exposed')) { 31 | $identifier = $this->options['expose']['identifier']; 32 | 33 | if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { 34 | // Exposed and locked. 35 | $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none'; 36 | } 37 | else { 38 | $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]'; 39 | } 40 | } 41 | 42 | if ($which == 'all' || $which == 'value') { 43 | $form['value'] = array( 44 | '#type' => 'textfield', 45 | '#title' => $this->t('Value'), 46 | '#size' => 30, 47 | '#default_value' => $this->value, 48 | ); 49 | $user_input = $form_state->getUserInput(); 50 | if ($exposed && !isset($user_input[$identifier])) { 51 | $user_input[$identifier] = $this->value; 52 | $form_state->setUserInput($user_input); 53 | } 54 | 55 | if ($which == 'all') { 56 | // Setup #states for all operators with one value. 57 | foreach ($this->operatorValues(1) as $operator) { 58 | $form['value']['#states']['visible'][] = array( 59 | $source => array('value' => $operator), 60 | ); 61 | } 62 | } 63 | } 64 | 65 | if (!isset($form['value'])) { 66 | // Ensure there is something in the 'value'. 67 | $form['value'] = array( 68 | '#type' => 'value', 69 | '#value' => NULL, 70 | ); 71 | } 72 | } 73 | 74 | /** 75 | * Helper function to define Options. 76 | */ 77 | protected function defineOptions() { 78 | $options = parent::defineOptions(); 79 | 80 | $options['expose']['contains']['required'] = array('default' => FALSE); 81 | 82 | return $options; 83 | } 84 | 85 | /** 86 | * Helper function to build Admin Summary. 87 | */ 88 | public function adminSummary() { 89 | if ($this->isAGroup()) { 90 | return $this->t('grouped'); 91 | } 92 | if (!empty($this->options['exposed'])) { 93 | return $this->t('exposed'); 94 | } 95 | 96 | $options = $this->operatorOptions('short'); 97 | $output = ''; 98 | if (!empty($options[$this->operator])) { 99 | $output = $options[$this->operator]; 100 | } 101 | if (in_array($this->operator, $this->operatorValues(1))) { 102 | $output .= ' ' . $this->value; 103 | } 104 | return $output; 105 | } 106 | 107 | /** 108 | * Helper function to build operator values. 109 | */ 110 | protected function operatorValues($values = 1) { 111 | $options = array(); 112 | foreach ($this->operators() as $id => $info) { 113 | if (isset($info['values']) && $info['values'] == $values) { 114 | $options[] = $id; 115 | } 116 | } 117 | 118 | return $options; 119 | } 120 | 121 | /** 122 | * Build strings from the operators() for 'select' options 123 | */ 124 | public function operatorOptions($which = 'title') { 125 | $options = array(); 126 | foreach ($this->operators() as $id => $info) { 127 | $options[$id] = $info[$which]; 128 | } 129 | 130 | return $options; 131 | } 132 | 133 | /** 134 | * Helper function to define opertators. 135 | */ 136 | public function operators() { 137 | $operators = array( 138 | '=' => array( 139 | 'title' => $this->t('Is equal to'), 140 | 'short' => $this->t('='), 141 | 'method' => 'opEqual', 142 | 'values' => 1, 143 | ), 144 | '!=' => array( 145 | 'title' => $this->t('Is not equal to'), 146 | 'short' => $this->t('!='), 147 | 'method' => 'opEqual', 148 | 'values' => 1, 149 | ), 150 | 'word' => array( 151 | 'title' => $this->t('Contains any word'), 152 | 'short' => $this->t('has word'), 153 | 'method' => 'opContainsWord', 154 | 'values' => 1, 155 | ), 156 | 'allwords' => array( 157 | 'title' => $this->t('Contains all words'), 158 | 'short' => $this->t('has all'), 159 | 'method' => 'opContainsWord', 160 | 'values' => 1, 161 | ), 162 | 'starts' => array( 163 | 'title' => $this->t('Starts with'), 164 | 'short' => $this->t('begins'), 165 | 'method' => 'opStartsWith', 166 | 'values' => 1, 167 | ), 168 | ); 169 | // If the definition allows for the empty operator, add it. 170 | if (!empty($this->definition['allow empty'])) { 171 | $operators += array( 172 | 'empty' => array( 173 | 'title' => $this->t('Is empty (NULL)'), 174 | 'method' => 'opEmpty', 175 | 'short' => $this->t('empty'), 176 | 'values' => 0, 177 | ), 178 | 'not empty' => array( 179 | 'title' => $this->t('Is not empty (NOT NULL)'), 180 | 'method' => 'opEmpty', 181 | 'short' => $this->t('not empty'), 182 | 'values' => 0, 183 | ), 184 | ); 185 | } 186 | 187 | return $operators; 188 | } 189 | 190 | /** 191 | * Helper function to query. 192 | */ 193 | public function query() { 194 | if (!empty($this->value[0])) { 195 | $this->query->where['conditions'][$this->realField] = $this->value[0]; 196 | } 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/elasticsearch_connector_views.views.inc: -------------------------------------------------------------------------------- 1 | loadAllClusters(FALSE) as $cluster) { 21 | $elasticsearchClient = $clientManager->getClientForCluster($cluster); 22 | if ($elasticsearchClient->isClusterOk()) { 23 | $indices = $elasticsearchClient->indices()->stats(); 24 | // TODO: Handle aliases also, not only indices. 25 | if (!empty($indices['indices'])) { 26 | foreach ($indices['indices'] as $index_name => $index_info) { 27 | // In elasticsearch the table is actually the document type. 28 | // So get all types and build data array. 29 | $mapping = $elasticsearchClient->indices() 30 | ->getMapping(array('index' => $index_name)); 31 | if (!empty($mapping[$index_name]['mappings'])) { 32 | foreach ($mapping[$index_name]['mappings'] as $type_name => $type_settings) { 33 | $name = new FormattableMarkup( 34 | '@cluster (@index_name - @type)', array( 35 | '@cluster' => $cluster->name, 36 | '@index_name' => $index_name, 37 | '@type' => $type_name, 38 | ) 39 | ); 40 | $base_table = 'elsv__' . $cluster->cluster_id . '__' . $index_name . '__' . $type_name; 41 | 42 | $data[$base_table]['table']['group'] = t('Elasticsearch'); 43 | $data[$base_table]['table']['base'] = array( 44 | 'index' => $index_name, 45 | 'cluster_id' => $cluster->cluster_id, 46 | 'type' => $type_name, 47 | 'title' => t('Cluster :name', array(':name' => $name)), 48 | 'help' => t('Searches the site with the Elasticsearch search engine for !name', array('!name' => $name)), 49 | 'query_id' => 'elasticsearch_connector_views_query', 50 | ); 51 | 52 | // Get the list of the fields in index directly from Elasticsearch. 53 | if (!empty($type_settings['properties'])) { 54 | _elasticsearch_connector_views_handle_fields($base_table, $data, $type_settings['properties']); 55 | } 56 | 57 | // Keyword field. 58 | $data[$base_table]['keyword'] = array( 59 | 'title' => t('Search'), 60 | 'help' => t('Fulltext search'), 61 | 'filter' => array( 62 | 'id' => 'elasticsearch_connector_views_fulltext_filter', 63 | ), 64 | ); 65 | 66 | // Snippet field. 67 | $data[$base_table]['snippet'] = array( 68 | 'title' => t('Snippet'), 69 | 'help' => t('Search snippet'), 70 | 'field' => array( 71 | 'handler' => 'elasticsearch_connector_views_snippet_handler_field', 72 | 'click sortable' => TRUE, 73 | ), 74 | ); 75 | 76 | // Score field. 77 | $data[$base_table]['score'] = array( 78 | 'title' => t('Score'), 79 | 'help' => t('Score'), 80 | 'field' => array( 81 | 'id' => 'elasticsearch_connector_views_standard', 82 | 'click sortable' => TRUE, 83 | ), 84 | ); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | return $data; 93 | } 94 | 95 | /** 96 | * Handle the fields mapping and handle nested data types. 97 | * 98 | * @param string $base_table 99 | * The base table value. 100 | * @param array $data 101 | * Data array. 102 | * @param array $fields 103 | * Fields array. 104 | * @param string $base_field_name 105 | * Base field name. 106 | */ 107 | function _elasticsearch_connector_views_handle_fields($base_table, &$data, $fields, $base_field_name = '') { 108 | if (!empty($fields)) { 109 | foreach ($fields as $field_name => $field) { 110 | // TODO: Restrict some fields if needed. 111 | // TODO: Handle boolean. 112 | // TODO: Handle the cases with analyzed and not analyzed. 113 | if (empty($field['type']) && isset($field['properties'])) { 114 | $field_type = 'object'; 115 | } 116 | else { 117 | $field_type = $field['type']; 118 | } 119 | 120 | $filter_handler = 'elasticsearch_connector_views_standard'; 121 | $field_handler = 'elasticsearch_connector_views_standard'; 122 | $set = TRUE; 123 | switch ($field_type) { 124 | case 'object': 125 | if (!empty($field['properties'])) { 126 | _elasticsearch_connector_views_handle_fields($base_table, $data, $field['properties'], $base_field_name . $field_name . '.'); 127 | } 128 | $set = FALSE; 129 | break; 130 | 131 | case 'date': 132 | $filter_handler = 'elasticsearch_connector_views_date'; 133 | $field_handler = 'elasticsearch_connector_views_date'; 134 | break; 135 | 136 | case 'boolean': 137 | $filter_handler = 'elasticsearch_connector_views_boolean'; 138 | $field_handler = 'elasticsearch_connector_views_boolean'; 139 | break; 140 | 141 | case 'text': 142 | case 'string': 143 | // TODO: Handle the analyser and non_analyzed fields. 144 | // TODO: For analysed fields we need to do fulltext search. 145 | if (\Drupal::moduleHandler() 146 | ->moduleExists('views_autocomplete_filters') 147 | ) { 148 | // TODO: Handle autocomplete. 149 | //$filter_handler = 'elasticsearch_connector_views_handler_filter_string_autocomplete'; 150 | } 151 | else { 152 | $field_handler = 'elasticsearch_connector_views_markup'; 153 | $filter_handler = 'elasticsearch_connector_views_string'; 154 | } 155 | break; 156 | 157 | // Handle numeric filter type. 158 | case 'integer': 159 | case 'long': 160 | case 'float': 161 | case 'double': 162 | $filter_handler = 'elasticsearch_connector_views_numeric'; 163 | $field_handler = 'elasticsearch_connector_views_numeric'; 164 | break; 165 | } 166 | 167 | if ($set) { 168 | $data[$base_table][$base_field_name . $field_name] = array( 169 | 'title' => $base_field_name . $field_name, 170 | 'help' => $base_field_name . $field_name, 171 | 'field' => array( 172 | 'id' => $field_handler, 173 | 'click sortable' => TRUE, 174 | ), 175 | 'filter' => array( 176 | 'id' => $filter_handler, 177 | ), 178 | 'sort' => array( 179 | 'id' => 'standard', 180 | ), 181 | // TODO: Handle the argument class. 182 | 'argument' => array( 183 | 'id' => 'standard', 184 | ), 185 | ); 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/filter/ElasticsearchViewsFulltextSearch.php: -------------------------------------------------------------------------------- 1 | options['fields']; 26 | if (!empty($this->value[0])) { 27 | foreach ($fields as $field) { 28 | $this->query->where['conditions'][$field] = $this->value[0]; 29 | } 30 | } 31 | } 32 | 33 | 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function buildOptionsForm(&$form, FormStateInterface $form_state) { 39 | parent::buildOptionsForm($form, $form_state); 40 | 41 | $fields = $this->getFulltextFields(); 42 | if (!empty($fields)) { 43 | $form['fields'] = array( 44 | '#type' => 'select', 45 | '#title' => t('Searched fields'), 46 | '#description' => t('Select the fields that will be searched.'), 47 | '#options' => $fields, 48 | '#size' => min(4, count($fields)), 49 | '#multiple' => TRUE, 50 | '#default_value' => $this->options['fields'], 51 | '#required' => TRUE, 52 | ); 53 | } 54 | else { 55 | $form['fields'] = array( 56 | '#type' => 'value', 57 | '#value' => array(), 58 | ); 59 | } 60 | 61 | if (isset($form['expose'])) { 62 | $form['expose']['#weight'] = -5; 63 | } 64 | 65 | } 66 | 67 | /** 68 | * Choose fulltext fields from ElasticSearch mapping if they are 69 | * type string and are analyzed (there is no index => not_analyzed 70 | * in the mapping) 71 | * 72 | * @return array fields 73 | */ 74 | private function getFulltextFields() { 75 | 76 | $view_id = $this->view->storage->get('base_table'); 77 | $data = Views::viewsData()->get($view_id); 78 | 79 | $index = $data['table']['base']['index']; 80 | $type = (is_array($data['table']['base']['type'])) ? implode(',', $data['table']['base']['type']) : $data['table']['base']['type']; 81 | 82 | $cluster_id = $data['table']['base']['cluster_id']; 83 | 84 | /** @var \Drupal\elasticsearch_connector\Entity\Cluster $elasticsearchCluster */ 85 | $elasticsearchCluster = \Drupal::service('entity.manager')->getStorage('elasticsearch_cluster')->load($cluster_id); 86 | /** @var \Drupal\elasticsearch_connector\ElasticSearch\ClientManagerInterface $clientManager */ 87 | $clientManager = \Drupal::service('elasticsearch_connector.client_manager'); 88 | $client = $clientManager->getClientForCluster($elasticsearchCluster); 89 | 90 | $params = array( 91 | 'index' => $index, 92 | 'type' => $type, 93 | ); 94 | $mapping = $client->indices()->getMapping($params); 95 | 96 | $fulltext_fields = array_keys(array_filter($mapping[$index]['mappings'][$type]['properties'], function($v) { 97 | return $v['type'] == 'text' && (!isset($v['index']) || $v['index'] != 'not_analyzed'); 98 | })); 99 | 100 | return array_combine($fulltext_fields, $fulltext_fields); 101 | } 102 | 103 | 104 | /** 105 | * Provide a simple textfield for equality 106 | */ 107 | protected function valueForm(&$form, FormStateInterface $form_state) { 108 | // We have to make some choices when creating this as an exposed 109 | // filter form. For example, if the operator is locked and thus 110 | // not rendered, we can't render dependencies; instead we only 111 | // render the form items we need. 112 | $which = 'all'; 113 | if (!empty($form['operator'])) { 114 | $source = ':input[name="options[operator]"]'; 115 | } 116 | if ($exposed = $form_state->get('exposed')) { 117 | $identifier = $this->options['expose']['identifier']; 118 | 119 | if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { 120 | // Exposed and locked. 121 | $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none'; 122 | } 123 | else { 124 | $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]'; 125 | } 126 | } 127 | 128 | if ($which == 'all' || $which == 'value') { 129 | $form['value'] = array( 130 | '#type' => 'textfield', 131 | '#title' => $this->t('Value'), 132 | '#size' => 30, 133 | '#default_value' => $this->value, 134 | ); 135 | $user_input = $form_state->getUserInput(); 136 | if ($exposed && !isset($user_input[$identifier])) { 137 | $user_input[$identifier] = $this->value; 138 | $form_state->setUserInput($user_input); 139 | } 140 | 141 | if ($which == 'all') { 142 | // Setup #states for all operators with one value. 143 | foreach ($this->operatorValues(1) as $operator) { 144 | $form['value']['#states']['visible'][] = array( 145 | $source => array('value' => $operator), 146 | ); 147 | } 148 | } 149 | } 150 | 151 | if (!isset($form['value'])) { 152 | // Ensure there is something in the 'value'. 153 | $form['value'] = array( 154 | '#type' => 'value', 155 | '#value' => NULL, 156 | ); 157 | } 158 | } 159 | 160 | /** 161 | * Helper function to define Options. 162 | */ 163 | protected function defineOptions() { 164 | $options = parent::defineOptions(); 165 | 166 | $options['expose']['contains']['required'] = array('default' => FALSE); 167 | 168 | $options['min_length']['default'] = ''; 169 | $options['fields']['default'] = []; 170 | 171 | return $options; 172 | } 173 | 174 | /** 175 | * Helper function to build Admin Summary. 176 | */ 177 | public function adminSummary() { 178 | if ($this->isAGroup()) { 179 | return $this->t('grouped'); 180 | } 181 | if (!empty($this->options['exposed'])) { 182 | return $this->t('exposed'); 183 | } 184 | 185 | $options = $this->operatorOptions('short'); 186 | $output = ''; 187 | if (!empty($options[$this->operator])) { 188 | $output = $options[$this->operator]; 189 | } 190 | if (in_array($this->operator, $this->operatorValues(1))) { 191 | $output .= ' ' . $this->value; 192 | } 193 | return $output; 194 | } 195 | 196 | /** 197 | * Helper function to build operator values. 198 | */ 199 | protected function operatorValues($values = 1) { 200 | $options = array(); 201 | foreach ($this->operators() as $id => $info) { 202 | if (isset($info['values']) && $info['values'] == $values) { 203 | $options[] = $id; 204 | } 205 | } 206 | 207 | return $options; 208 | } 209 | 210 | /** 211 | * Build strings from the operators() for 'select' options 212 | */ 213 | public function operatorOptions($which = 'title') { 214 | $options = array(); 215 | foreach ($this->operators() as $id => $info) { 216 | $options[$id] = $info[$which]; 217 | } 218 | 219 | return $options; 220 | } 221 | 222 | /** 223 | * Helper function to define opertators. 224 | */ 225 | public function operators() { 226 | $operators = array( 227 | 'word' => array( 228 | 'title' => $this->t('Contains any word'), 229 | 'short' => $this->t('has word'), 230 | 'method' => 'opContainsWord', 231 | 'values' => 1, 232 | ), 233 | ); 234 | return $operators; 235 | } 236 | 237 | 238 | 239 | } 240 | -------------------------------------------------------------------------------- /src/Controller/ElasticsearchController.php: -------------------------------------------------------------------------------- 1 | clientManager = $client_manager; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public static function create(ContainerInterface $container) { 36 | return new static ( 37 | $container->get('elasticsearch_connector.client_manager') 38 | ); 39 | } 40 | 41 | /** 42 | * Displays information about an Elasticsearch Cluster. 43 | * 44 | * @param \Drupal\elasticsearch_connector\Entity\Cluster $elasticsearch_cluster 45 | * An instance of Cluster. 46 | * 47 | * @return array 48 | * An array suitable for drupal_render(). 49 | */ 50 | public function page(Cluster $elasticsearch_cluster) { 51 | // Build the Search API index information. 52 | $render = [ 53 | 'view' => [ 54 | '#theme' => 'elasticsearch_cluster', 55 | '#cluster' => $elasticsearch_cluster, 56 | ], 57 | ]; 58 | // Check if the cluster is enabled and can be written to. 59 | if ($elasticsearch_cluster->cluster_id) { 60 | $render['form'] = $this->formBuilder()->getForm( 61 | 'Drupal\elasticsearch_connector\Form\ClusterForm', 62 | $elasticsearch_cluster 63 | ); 64 | } 65 | 66 | return $render; 67 | } 68 | 69 | /** 70 | * Page title callback for a cluster's "View" tab. 71 | * 72 | * @param \Drupal\elasticsearch_connector\Entity\Cluster $elasticsearch_cluster 73 | * The cluster that is displayed. 74 | * 75 | * @return string 76 | * The page title. 77 | */ 78 | public function pageTitle(Cluster $elasticsearch_cluster) { 79 | // TODO: Check if we need string escaping. 80 | return $elasticsearch_cluster->label(); 81 | } 82 | 83 | /** 84 | * Complete information about the Elasticsearch Client. 85 | * 86 | * @param \Drupal\elasticsearch_connector\Entity\Cluster $elasticsearch_cluster 87 | * Elasticsearch cluster. 88 | * 89 | * @return array 90 | * Render array. 91 | */ 92 | public function getInfo(Cluster $elasticsearch_cluster) { 93 | // TODO: Get the statistics differently. 94 | $client_connector = $this->clientManager->getClientForCluster($elasticsearch_cluster); 95 | 96 | $node_rows = []; 97 | $cluster_statistics_rows = []; 98 | $cluster_health_rows = []; 99 | 100 | if ($client_connector->isClusterOk()) { 101 | // Nodes. 102 | $es_node_namespace = $client_connector->getNodesProperties(); 103 | $node_stats = $es_node_namespace['stats']; 104 | 105 | $total_docs = 0; 106 | $total_size = 0; 107 | $node_rows = []; 108 | if (!empty($node_stats['nodes'])) { 109 | // TODO: Better format the results in order to build the 110 | // correct output. 111 | foreach ($node_stats['nodes'] as $node_id => $node_properties) { 112 | $row = []; 113 | $row[] = ['data' => $node_properties['name']]; 114 | $row[] = ['data' => $node_properties['indices']['docs']['count']]; 115 | $row[] = [ 116 | 'data' => format_size( 117 | $node_properties['indices']['store']['size_in_bytes'] 118 | ), 119 | ]; 120 | $total_docs += $node_properties['indices']['docs']['count']; 121 | $total_size += $node_properties['indices']['store']['size_in_bytes']; 122 | $node_rows[] = $row; 123 | } 124 | } 125 | 126 | $cluster_status = $client_connector->getClusterInfo(); 127 | $cluster_statistics_rows = [ 128 | [ 129 | [ 130 | 'data' => $cluster_status['health']['number_of_nodes'] . ' ' . t( 131 | 'Nodes' 132 | ), 133 | ], 134 | [ 135 | 'data' => $cluster_status['health']['active_shards'] + $cluster_status['health']['unassigned_shards'] . ' ' . t( 136 | 'Total Shards' 137 | ), 138 | ], 139 | [ 140 | 'data' => $cluster_status['health']['active_shards'] . ' ' . t( 141 | 'Successful Shards' 142 | ), 143 | ], 144 | [ 145 | 'data' => count( 146 | $cluster_status['state']['metadata']['indices'] 147 | ) . ' ' . t('Indices'), 148 | ], 149 | ['data' => $total_docs . ' ' . t('Total Documents')], 150 | ['data' => format_size($total_size) . ' ' . t('Total Size')], 151 | ], 152 | ]; 153 | 154 | $cluster_health_rows = []; 155 | $cluster_health_mapping = [ 156 | 'cluster_name' => t('Cluster name'), 157 | 'status' => t('Status'), 158 | 'timed_out' => t('Time out'), 159 | 'number_of_nodes' => t('Number of nodes'), 160 | 'number_of_data_nodes' => t('Number of data nodes'), 161 | 'active_primary_shards' => t('Active primary shards'), 162 | 'active_shards' => t('Active shards'), 163 | 'relocating_shards' => t('Relocating shards'), 164 | 'initializing_shards' => t('Initializing shards'), 165 | 'unassigned_shards' => t('Unassigned shards'), 166 | 'delayed_unassigned_shards' => t('Delayed unassigned shards'), 167 | 'number_of_pending_tasks' => t('Number of pending tasks'), 168 | 'number_of_in_flight_fetch' => t('Number of in-flight fetch'), 169 | 'task_max_waiting_in_queue_millis' => t( 170 | 'Task max waiting in queue millis' 171 | ), 172 | 'active_shards_percent_as_number' => t( 173 | 'Active shards percent as number' 174 | ), 175 | ]; 176 | 177 | foreach ($cluster_status['health'] as $health_key => $health_value) { 178 | $row = []; 179 | $row[] = ['data' => $cluster_health_mapping[$health_key]]; 180 | $row[] = ['data' => ($health_value === FALSE ? 'False' : $health_value)]; 181 | $cluster_health_rows[] = $row; 182 | } 183 | } 184 | 185 | $output['cluster_statistics_wrapper'] = [ 186 | '#type' => 'fieldset', 187 | '#title' => t('Cluster statistics'), 188 | '#collapsible' => TRUE, 189 | '#collapsed' => FALSE, 190 | '#attributes' => [], 191 | ]; 192 | 193 | $output['cluster_statistics_wrapper']['nodes'] = [ 194 | '#theme' => 'table', 195 | '#header' => [ 196 | ['data' => t('Node name')], 197 | ['data' => t('Documents')], 198 | ['data' => t('Size')], 199 | ], 200 | '#rows' => $node_rows, 201 | '#attributes' => [], 202 | ]; 203 | 204 | $output['cluster_statistics_wrapper']['cluster_statistics'] = [ 205 | '#theme' => 'table', 206 | '#header' => [ 207 | ['data' => t('Total'), 'colspan' => 6], 208 | ], 209 | '#rows' => $cluster_statistics_rows, 210 | '#attributes' => ['class' => ['admin-elasticsearch-statistics']], 211 | ]; 212 | 213 | $output['cluster_health'] = [ 214 | '#theme' => 'table', 215 | '#header' => [ 216 | ['data' => t('Cluster Health'), 'colspan' => 2], 217 | ], 218 | '#rows' => $cluster_health_rows, 219 | '#attributes' => ['class' => ['admin-elasticsearch-health']], 220 | ]; 221 | 222 | return $output; 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /tests/src/Unit/ElasticSearch/Parameters/Factory/FilterFactoryTest.php: -------------------------------------------------------------------------------- 1 | prophesize(Condition::class); 27 | 28 | $condition->getValue() 29 | ->willReturn(FALSE); 30 | 31 | $condition->getOperator() 32 | ->willReturn('<>'); 33 | 34 | $condition->getField() 35 | ->willReturn('foo'); 36 | 37 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 38 | $expected_filter = [ 39 | 'exists' => 40 | [ 41 | 'field' => 'foo', 42 | ], 43 | ]; 44 | $this->assertEquals($expected_filter, $filter); 45 | 46 | // Thest the = operator. 47 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 48 | $condition = $this->prophesize(Condition::class); 49 | 50 | $condition->getValue() 51 | ->willReturn(FALSE); 52 | 53 | $condition->getOperator() 54 | ->willReturn('='); 55 | 56 | $condition->getField() 57 | ->willReturn('foo'); 58 | 59 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 60 | $expected_filter = [ 61 | 'bool' => [ 62 | 'must_not' => [ 63 | 'exists' => ['field' => 'foo'], 64 | ], 65 | ], 66 | ]; 67 | $this->assertEquals($expected_filter, $filter); 68 | 69 | // Other operators will throw an exception. 70 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 71 | $condition = $this->prophesize(Condition::class); 72 | 73 | $condition->getValue() 74 | ->willReturn(FALSE); 75 | 76 | $condition->getOperator() 77 | ->willReturn('>'); 78 | 79 | $condition->getField() 80 | ->willReturn('foo'); 81 | 82 | $this->setExpectedException(\Exception::class, 'Incorrect filter criteria'); 83 | FilterFactory::filterFromCondition($condition->reveal()); 84 | } 85 | 86 | /** 87 | * @covers ::filterFromCondition 88 | */ 89 | public function testFilterFromConditionB() { 90 | // Normal filters 91 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 92 | $condition = $this->prophesize(Condition::class); 93 | 94 | $condition->getValue() 95 | ->willReturn('bar'); 96 | 97 | $condition->getOperator() 98 | ->willReturn('='); 99 | 100 | $condition->getField() 101 | ->willReturn('foo'); 102 | 103 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 104 | $expected_filter = [ 105 | 'term' => [ 106 | 'foo' => 'bar' 107 | ], 108 | ]; 109 | $this->assertEquals($expected_filter, $filter); 110 | 111 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 112 | $condition = $this->prophesize(Condition::class); 113 | 114 | $condition->getValue() 115 | ->willReturn(['bar', 'baz']); 116 | 117 | $condition->getOperator() 118 | ->willReturn('IN'); 119 | 120 | $condition->getField() 121 | ->willReturn('foo'); 122 | 123 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 124 | $expected_filter = [ 125 | 'terms' => [ 126 | 'foo' => ['bar', 'baz'], 127 | ], 128 | ]; 129 | $this->assertEquals($expected_filter, $filter); 130 | 131 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 132 | $condition = $this->prophesize(Condition::class); 133 | 134 | $condition->getValue() 135 | ->willReturn('bar'); 136 | 137 | $condition->getOperator() 138 | ->willReturn('<>'); 139 | 140 | $condition->getField() 141 | ->willReturn('foo'); 142 | 143 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 144 | $expected_filter = [ 145 | 'not' => [ 146 | 'filter' =>[ 147 | 'term' => ['foo' => 'bar'], 148 | ], 149 | ], 150 | ]; 151 | $this->assertEquals($expected_filter, $filter); 152 | 153 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 154 | $condition = $this->prophesize(Condition::class); 155 | 156 | $condition->getValue() 157 | ->willReturn(1); 158 | 159 | $condition->getOperator() 160 | ->willReturn('>'); 161 | 162 | $condition->getField() 163 | ->willReturn('foo'); 164 | 165 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 166 | $expected_filter = [ 167 | 'range' => [ 168 | 'foo' => [ 169 | 'from' => 1, 170 | 'to' => NULL, 171 | 'include_lower' => FALSE, 172 | 'include_upper' => FALSE, 173 | ], 174 | ], 175 | ]; 176 | $this->assertEquals($expected_filter, $filter); 177 | 178 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 179 | $condition = $this->prophesize(Condition::class); 180 | 181 | $condition->getValue() 182 | ->willReturn(1); 183 | 184 | $condition->getOperator() 185 | ->willReturn('>='); 186 | 187 | $condition->getField() 188 | ->willReturn('foo'); 189 | 190 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 191 | $expected_filter = [ 192 | 'range' => [ 193 | 'foo' => [ 194 | 'from' => 1, 195 | 'to' => NULL, 196 | 'include_lower' => TRUE, 197 | 'include_upper' => FALSE, 198 | ], 199 | ], 200 | ]; 201 | $this->assertEquals($expected_filter, $filter); 202 | 203 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 204 | $condition = $this->prophesize(Condition::class); 205 | 206 | $condition->getValue() 207 | ->willReturn(1); 208 | 209 | $condition->getOperator() 210 | ->willReturn('<'); 211 | 212 | $condition->getField() 213 | ->willReturn('foo'); 214 | 215 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 216 | $expected_filter = [ 217 | 'range' => [ 218 | 'foo' => [ 219 | 'from' => NULL, 220 | 'to' => 1, 221 | 'include_lower' => FALSE, 222 | 'include_upper' => FALSE, 223 | ], 224 | ], 225 | ]; 226 | $this->assertEquals($expected_filter, $filter); 227 | 228 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 229 | $condition = $this->prophesize(Condition::class); 230 | 231 | $condition->getValue() 232 | ->willReturn(1); 233 | 234 | $condition->getOperator() 235 | ->willReturn('<='); 236 | 237 | $condition->getField() 238 | ->willReturn('foo'); 239 | 240 | $filter = FilterFactory::filterFromCondition($condition->reveal()); 241 | $expected_filter = [ 242 | 'range' => [ 243 | 'foo' => [ 244 | 'from' => NULL, 245 | 'to' => 1, 246 | 'include_lower' => FALSE, 247 | 'include_upper' => TRUE, 248 | ], 249 | ], 250 | ]; 251 | $this->assertEquals($expected_filter, $filter); 252 | 253 | // Other operators will throw an exception. 254 | /** @var \Prophecy\Prophecy\ObjectProphecy $condition */ 255 | $condition = $this->prophesize(Condition::class); 256 | 257 | $condition->getValue() 258 | ->willReturn(FALSE); 259 | 260 | $condition->getOperator() 261 | ->willReturn('other-operator'); 262 | 263 | $condition->getField() 264 | ->willReturn('foo'); 265 | 266 | $this->setExpectedException(\Exception::class, 'Incorrect filter criteria'); 267 | FilterFactory::filterFromCondition($condition->reveal()); 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /src/Controller/ClusterListBuilder.php: -------------------------------------------------------------------------------- 1 | indexStorage = $index_storage; 55 | $this->clusterStorage = $cluster_storage; 56 | $this->clientManager = $client_manager; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public static function createInstance( 63 | ContainerInterface $container, 64 | EntityTypeInterface $entity_type 65 | ) { 66 | return new static( 67 | $entity_type, 68 | $container->get('entity_type.manager')->getStorage($entity_type->id()), 69 | $container->get('entity_type.manager')->getStorage('elasticsearch_index'), 70 | $container->get('entity_type.manager')->getStorage('elasticsearch_cluster'), 71 | $container->get('elasticsearch_connector.client_manager') 72 | ); 73 | } 74 | 75 | /** 76 | * Group Elasticsearch indices under their respective clusters. 77 | * 78 | * @return array 79 | * Associative array with the following keys: 80 | * - clusters: Array of cluster groups keyed by cluster id. Each item is 81 | * itself an array with the cluster and any indices as values. 82 | * - lone_indexes: Array of indices without a cluster. 83 | */ 84 | public function group() { 85 | /** @var \Drupal\elasticsearch_connector\Entity\Cluster[] $clusters */ 86 | $clusters = $this->storage->loadMultiple(); 87 | /** @var \Drupal\elasticsearch_connector\Entity\Index[] $indices */ 88 | $indices = $this->indexStorage->loadMultiple(); 89 | 90 | $cluster_groups = []; 91 | $lone_indices = []; 92 | foreach ($clusters as $cluster) { 93 | $cluster_group = [ 94 | 'cluster.' . $cluster->cluster_id => $cluster, 95 | ]; 96 | 97 | foreach ($indices as $index) { 98 | if ($index->server == $cluster->cluster_id) { 99 | $cluster_group['index.' . $index->index_id] = $index; 100 | } 101 | elseif ($index->server == NULL) { 102 | $lone_indices['index.' . $index->index_id] = $index; 103 | } 104 | } 105 | 106 | $cluster_groups['cluster.' . $cluster->cluster_id] = $cluster_group; 107 | } 108 | 109 | return [ 110 | 'clusters' => $cluster_groups, 111 | 'lone_indexes' => $lone_indices, 112 | ]; 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function buildHeader() { 119 | return [ 120 | 'type' => $this->t('Type'), 121 | 'title' => $this->t('Name'), 122 | 'machine_name' => $this->t('Machine Name'), 123 | 'status' => $this->t('Status'), 124 | 'cluster_status' => $this->t('Cluster Status'), 125 | ] + parent::buildHeader(); 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function buildRow(EntityInterface $entity) { 132 | if ($entity instanceof Cluster) { 133 | $client_connector = $this->clientManager->getClientForCluster($entity); 134 | } 135 | elseif ($entity instanceof Index) { 136 | $cluster = $this->clusterStorage->load($entity->server); 137 | $client_connector = $this->clientManager->getClientForCluster($cluster); 138 | } 139 | else { 140 | throw new NotFoundHttpException(); 141 | } 142 | 143 | $row = parent::buildRow($entity); 144 | $result = []; 145 | $status = NULL; 146 | if (isset($entity->cluster_id)) { 147 | $cluster = $this->clusterStorage->load($entity->cluster_id); 148 | 149 | if ($client_connector->isClusterOk()) { 150 | $cluster_health = $client_connector->cluster()->health(); 151 | $version_number = $client_connector->getServerVersion(); 152 | $status = $cluster_health['status']; 153 | } 154 | else { 155 | $status = $this->t('Not available'); 156 | $version_number = $this->t('Unknown version'); 157 | } 158 | $result = [ 159 | 'data' => [ 160 | 'type' => [ 161 | 'data' => $this->t('Cluster'), 162 | ], 163 | 'title' => [ 164 | 'data' => [ 165 | '#type' => 'link', 166 | '#title' => $entity->label() . ' (' . $version_number . ')', 167 | '#url' => new Url('entity.elasticsearch_cluster.edit_form', ['elasticsearch_cluster' => $entity->id()]), 168 | ], 169 | ], 170 | 'machine_name' => [ 171 | 'data' => $entity->id(), 172 | ], 173 | 'status' => [ 174 | 'data' => $cluster->status ? 'Active' : 'Inactive', 175 | ], 176 | 'clusterStatus' => [ 177 | 'data' => $status, 178 | ], 179 | 'operations' => $row['operations'], 180 | ], 181 | 'title' => $this->t( 182 | 'Machine name: @name', 183 | ['@name' => $entity->id()] 184 | ), 185 | ]; 186 | } 187 | elseif (isset($entity->index_id)) { 188 | $result = [ 189 | 'data' => [ 190 | 'type' => [ 191 | 'data' => $this->t('Index'), 192 | 'class' => ['es-list-index'], 193 | ], 194 | 'title' => [ 195 | 'data' => $entity->label(), 196 | ], 197 | 'machine_name' => [ 198 | 'data' => $entity->id(), 199 | ], 200 | 'status' => [ 201 | 'data' => '', 202 | ], 203 | 'clusterStatus' => [ 204 | 'data' => '-', 205 | ], 206 | 'operations' => $row['operations'], 207 | ], 208 | 'title' => $this->t( 209 | 'Machine name: @name', 210 | ['@name' => $entity->id()] 211 | ), 212 | ]; 213 | } 214 | 215 | return $result; 216 | } 217 | 218 | /** 219 | * {@inheritdoc} 220 | */ 221 | public function getDefaultOperations(EntityInterface $entity) { 222 | $operations = []; 223 | 224 | if (isset($entity->cluster_id)) { 225 | $operations['info'] = [ 226 | 'title' => $this->t('Info'), 227 | 'weight' => 19, 228 | 'url' => new Url('entity.elasticsearch_cluster.canonical', ['elasticsearch_cluster' => $entity->id()]), 229 | ]; 230 | $operations['edit'] = [ 231 | 'title' => $this->t('Edit'), 232 | 'weight' => 20, 233 | 'url' => new Url('entity.elasticsearch_cluster.edit_form', ['elasticsearch_cluster' => $entity->id()]), 234 | ]; 235 | $operations['delete'] = [ 236 | 'title' => $this->t('Delete'), 237 | 'weight' => 21, 238 | 'url' => new Url('entity.elasticsearch_cluster.delete_form', ['elasticsearch_cluster' => $entity->id()]), 239 | ]; 240 | } 241 | elseif (isset($entity->index_id)) { 242 | $operations['delete'] = [ 243 | 'title' => $this->t('Delete'), 244 | 'weight' => 20, 245 | 'url' => new Url('entity.elasticsearch_index.delete_form', ['elasticsearch_index' => $entity->id()]), 246 | ]; 247 | } 248 | 249 | return $operations; 250 | } 251 | 252 | /** 253 | * {@inheritdoc} 254 | */ 255 | public function render() { 256 | $entity_groups = $this->group(); 257 | 258 | $rows = []; 259 | foreach ($entity_groups['clusters'] as $cluster_group) { 260 | /** @var \Drupal\elasticsearch_connector\Entity\Cluster|\Drupal\elasticsearch_connector\Entity\Index $entity */ 261 | foreach ($cluster_group as $entity) { 262 | $rows[$entity->id()] = $this->buildRow($entity); 263 | } 264 | } 265 | 266 | $list['#type'] = 'container'; 267 | $list['#attached']['library'][] = 'elasticsearch_connector/drupal.elasticsearch_connector.ec_index'; 268 | $list['clusters'] = [ 269 | '#type' => 'table', 270 | '#header' => $this->buildHeader(), 271 | '#rows' => $rows, 272 | '#empty' => $this->t( 273 | 'No clusters available. Add new cluster.', 274 | [ 275 | '@link' => \Drupal::urlGenerator()->generate( 276 | 'entity.elasticsearch_cluster.add_form' 277 | ), 278 | ] 279 | ), 280 | ]; 281 | 282 | return $list; 283 | } 284 | 285 | } 286 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/field/ElasticsearchViewsEntityField.php: -------------------------------------------------------------------------------- 1 | definition['fallback_handler']) ? $this->definition['fallback_handler'] : 'elasticsearch_connector_views_standard'; 47 | $this->fallbackHandler = Views::handlerManager('field') 48 | ->getHandler($options, $fallback_handler_id); 49 | $options += array('fallback_options' => array()); 50 | $fallback_options = $options['fallback_options'] + $options; 51 | $this->fallbackHandler->init($view, $display, $fallback_options); 52 | 53 | parent::init($view, $display, $options); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function query($use_groupby = false) { 60 | // If we're not using Field API field rendering, just use the query() 61 | // implementation of the fallback handler. 62 | if (!$this->options['field_rendering']) { 63 | $this->fallbackHandler->query(); 64 | return; 65 | } 66 | 67 | // If we do use Field API rendering, we need the entity object for the 68 | // parent property. 69 | $parent_path = $this->getParentPath(); 70 | $property_path = $parent_path ? "$parent_path:_object" : '_object'; 71 | $combined_property_path = Utility::createCombinedId($this->getDatasourceId(), $property_path); 72 | $this->addRetrievedProperty($combined_property_path); 73 | } 74 | 75 | /** 76 | * Retrieves the property path of the parent property. 77 | * 78 | * @return string|null 79 | * The property path of the parent property. 80 | */ 81 | protected function getParentPath() { 82 | if (!isset($this->parentPath)) { 83 | $combined_property_path = $this->getCombinedPropertyPath(); 84 | list(, $property_path) = Utility::splitCombinedId($combined_property_path); 85 | list($this->parentPath) = Utility::splitPropertyPath($property_path); 86 | } 87 | 88 | return $this->parentPath; 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function defineOptions() { 95 | $options = parent::defineOptions(); 96 | 97 | $options['field_rendering'] = array('default' => TRUE); 98 | $options['fallback_handler'] = array('default' => $this->fallbackHandler->getPluginId()); 99 | $options['fallback_options'] = array('contains' => $this->fallbackHandler->defineOptions()); 100 | 101 | return $options; 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function buildOptionsForm(&$form, FormStateInterface $form_state) { 108 | $form['field_rendering'] = array( 109 | '#type' => 'checkbox', 110 | '#title' => $this->t('Use entity field rendering'), 111 | '#description' => $this->t("If checked, Drupal's built-in field rendering mechanism will be used for rendering this field's values, which requires the entity to be loaded. If unchecked, a type-specific, entity-independent rendering mechanism will be used."), 112 | '#default_value' => $this->options['field_rendering'], 113 | ); 114 | 115 | // Wrap the (immediate) parent options in their own field set, to clean up 116 | // the UI when (un)checking the above checkbox. 117 | $form['parent_options'] = array( 118 | '#type' => 'fieldset', 119 | '#title' => $this->t('Render settings'), 120 | '#states' => array( 121 | 'visible' => array( 122 | ':input[name="options[field_rendering]"]' => array('checked' => TRUE), 123 | ), 124 | ), 125 | ); 126 | 127 | // Include the parent options form and move all fields that were added by 128 | // our direct parent (\Drupal\views\Plugin\views\field\Field) to the 129 | // "parent_options" fieldset. 130 | parent::buildOptionsForm($form, $form_state); 131 | $parent_keys = array( 132 | 'multiple_field_settings', 133 | 'click_sort_column', 134 | 'type', 135 | 'field_api_classes', 136 | 'settings', 137 | ); 138 | foreach ($parent_keys as $key) { 139 | if (!empty($form[$key])) { 140 | $form[$key]['#fieldset'] = 'parent_options'; 141 | } 142 | } 143 | // The Core boolean formatter hard-codes the field name to "field_boolean". 144 | // This breaks the parent class's call of rewriteStatesSelector() for fixing 145 | // "#states". We therefore apply that behavior again here. 146 | if (!empty($form['settings'])) { 147 | FormHelper::rewriteStatesSelector($form['settings'], "fields[field_boolean][settings_edit_form]", 'options'); 148 | } 149 | 150 | // Get the options form for the fallback handler. 151 | $fallback_form = array(); 152 | $this->fallbackHandler->buildOptionsForm($fallback_form, $form_state); 153 | // Remove all fields from FieldPluginBase from the fallback form, but leave 154 | // those in that were only added by our immediate parent, 155 | // \Drupal\views\Plugin\views\field\Field. (E.g., the "type" option is 156 | // especially prone to conflicts here.) The others come from the plugin base 157 | // classes and will be identical, so it would be confusing to include them 158 | // twice. 159 | $parent_keys[] = '#pre_render'; 160 | $remove_from_fallback = array_diff_key($form, array_flip($parent_keys)); 161 | $fallback_form = array_diff_key($fallback_form, $remove_from_fallback); 162 | // Fix the "#states" selectors in the fallback form, and put an additional 163 | // "#states" directive on it to only be visible for the corresponding 164 | // "field_rendering" setting. 165 | if ($fallback_form) { 166 | FormHelper::rewriteStatesSelector($fallback_form, '"options[', '"options[fallback_options]['); 167 | $form['fallback_options'] = $fallback_form; 168 | $form['fallback_options']['#type'] = 'fieldset'; 169 | $form['fallback_options']['#title'] = $this->t('Render settings'); 170 | $form['fallback_options']['#states']['visible'][':input[name="options[field_rendering]"]'] = array('checked' => FALSE); 171 | } 172 | } 173 | 174 | /** 175 | * {@inheritdoc} 176 | */ 177 | public function preRender(&$values) { 178 | if ($this->options['field_rendering']) { 179 | parent::preRender($values); 180 | } 181 | else { 182 | $this->fallbackHandler->preRender($values); 183 | } 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | public function render(ResultRow $values) { 190 | if (!$this->options['field_rendering']) { 191 | return $this->fallbackHandler->render($values); 192 | } 193 | return parent::render($values); 194 | } 195 | 196 | /** 197 | * {@inheritdoc} 198 | */ 199 | public function render_item($count, $item) { 200 | if (!$this->options['field_rendering']) { 201 | if ($this->fallbackHandler instanceof MultiItemsFieldHandlerInterface) { 202 | return $this->fallbackHandler->render_item($count, $item); 203 | } 204 | return ''; 205 | } 206 | return parent::render_item($count, $item); 207 | } 208 | 209 | /** 210 | * {@inheritdoc} 211 | */ 212 | protected function getEntityFieldRenderer() { 213 | if (!isset($this->entityFieldRenderer)) { 214 | // This can be invoked during field handler initialization in which case 215 | // view fields are not set yet. 216 | if (!empty($this->view->field)) { 217 | foreach ($this->view->field as $field) { 218 | // An entity field renderer can handle only a single relationship. 219 | if (isset($field->entityFieldRenderer)) { 220 | if ($field->entityFieldRenderer instanceof EntityFieldRenderer && $field->entityFieldRenderer->compatibleWithField($this)) { 221 | $this->entityFieldRenderer = $field->entityFieldRenderer; 222 | break; 223 | } 224 | } 225 | } 226 | } 227 | if (!isset($this->entityFieldRenderer)) { 228 | $entity_type = $this->entityManager->getDefinition($this->getEntityType()); 229 | $this->entityFieldRenderer = new EntityFieldRenderer($this->view, $this->relationship, $this->languageManager, $entity_type, $this->entityManager); 230 | $this->entityFieldRenderer->setDatasourceId($this->getDatasourceId()); 231 | } 232 | } 233 | 234 | return $this->entityFieldRenderer; 235 | } 236 | 237 | /** 238 | * {@inheritdoc} 239 | */ 240 | public function getItems(ResultRow $values) { 241 | return array(); 242 | } 243 | 244 | /** 245 | * {@inheritdoc} 246 | */ 247 | public function renderItems($items) { 248 | if (!$this->options['field_rendering']) { 249 | if ($this->fallbackHandler instanceof MultiItemsFieldHandlerInterface) { 250 | return $this->fallbackHandler->renderItems($items); 251 | } 252 | return ''; 253 | } 254 | 255 | return parent::renderItems($items); 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/ElasticSearch/Parameters/Factory/IndexFactory.php: -------------------------------------------------------------------------------- 1 | id(); 45 | } 46 | 47 | return $params; 48 | } 49 | 50 | /** 51 | * Build parameters required to create an index 52 | * TODO: Add the timeout option. 53 | * 54 | * @param \Drupal\search_api\IndexInterface $index 55 | * 56 | * @return array 57 | */ 58 | public static function create(IndexInterface $index) { 59 | $indexName = static::getIndexName($index); 60 | $indexConfig = [ 61 | 'index' => $indexName, 62 | 'body' => [ 63 | 'settings' => [ 64 | 'number_of_shards' => $index->getOption('number_of_shards', 5), 65 | 'number_of_replicas' => $index->getOption('number_of_replicas', 1), 66 | ], 67 | ], 68 | ]; 69 | 70 | // Allow other modules to alter index config before we create it. 71 | $dispatcher = \Drupal::service('event_dispatcher'); 72 | $prepareIndexEvent = new PrepareIndexEvent($indexConfig, $indexName); 73 | $event = $dispatcher->dispatch(PrepareIndexEvent::PREPARE_INDEX, $prepareIndexEvent); 74 | $indexConfig = $event->getIndexConfig(); 75 | 76 | return $indexConfig; 77 | } 78 | 79 | /** 80 | * Build parameters to bulk delete indexes. 81 | * 82 | * @param \Drupal\search_api\IndexInterface $index 83 | * @param array $ids 84 | * 85 | * @return array 86 | */ 87 | public static function bulkDelete(IndexInterface $index, array $ids) { 88 | $params = IndexFactory::index($index, TRUE); 89 | foreach ($ids as $id) { 90 | $params['body'][] = [ 91 | 'delete' => [ 92 | '_index' => $params['index'], 93 | '_type' => $params['type'], 94 | '_id' => $id, 95 | ], 96 | ]; 97 | } 98 | 99 | return $params; 100 | } 101 | 102 | /** 103 | * Build parameters to bulk delete indexes. 104 | * 105 | * @param \Drupal\search_api\IndexInterface $index 106 | * Index object. 107 | * @param \Drupal\search_api\Item\ItemInterface[] $items 108 | * An array of items to be indexed, keyed by their item IDs. 109 | * 110 | * @return array 111 | * Array of parameters to send along to Elasticsearch to perform the bulk 112 | * index. 113 | */ 114 | public static function bulkIndex(IndexInterface $index, array $items) { 115 | $params = static::index($index, TRUE); 116 | 117 | foreach ($items as $id => $item) { 118 | $data = [ 119 | '_language' => $item->getLanguage(), 120 | ]; 121 | /** @var \Drupal\search_api\Item\FieldInterface $field */ 122 | foreach ($item as $name => $field) { 123 | $field_type = $field->getType(); 124 | if (!empty($field->getValues())) { 125 | $values = array(); 126 | foreach ($field->getValues() as $value) { 127 | switch ($field_type) { 128 | case 'string': 129 | $values[] = (string) $value; 130 | break; 131 | 132 | case 'text': 133 | $values[] = $value->toText(); 134 | break; 135 | 136 | case 'boolean': 137 | $values[] = (boolean) $value; 138 | break; 139 | 140 | default: 141 | $values[] = $value; 142 | } 143 | } 144 | $data[$field->getFieldIdentifier()] = $values; 145 | } 146 | } 147 | $params['body'][] = ['index' => ['_id' => $id]]; 148 | $params['body'][] = $data; 149 | } 150 | 151 | // Allow other modules to alter index params before we send them. 152 | $indexName = IndexFactory::getIndexName($index); 153 | $dispatcher = \Drupal::service('event_dispatcher'); 154 | $buildIndexParamsEvent = new BuildIndexParamsEvent($params, $indexName); 155 | $event = $dispatcher->dispatch(BuildIndexParamsEvent::BUILD_PARAMS, $buildIndexParamsEvent); 156 | $params = $event->getElasticIndexParams(); 157 | 158 | return $params; 159 | } 160 | 161 | /** 162 | * Build parameters required to create an index mapping. 163 | * 164 | * TODO: We need also: 165 | * $params['index'] - (Required) 166 | * ['type'] - The name of the document type 167 | * ['timeout'] - (time) Explicit operation timeout. 168 | * 169 | * @param \Drupal\search_api\IndexInterface $index 170 | * Index object. 171 | * 172 | * @return array 173 | * Parameters required to create an index mapping. 174 | */ 175 | public static function mapping(IndexInterface $index) { 176 | $params = static::index($index, TRUE); 177 | 178 | $properties = [ 179 | 'id' => [ 180 | 'type' => 'keyword', 181 | 'index' => 'true', 182 | ], 183 | ]; 184 | 185 | // Figure out which fields are used for autocompletion if any. 186 | if (\Drupal::moduleHandler()->moduleExists('search_api_autocomplete')) { 187 | $autocompletes = \Drupal::entityTypeManager()->getStorage('search_api_autocomplete_search')->loadMultiple(); 188 | $all_autocompletion_fields = []; 189 | foreach ($autocompletes as $autocomplete) { 190 | $suggester = \Drupal::service('plugin.manager.search_api_autocomplete.suggester'); 191 | $plugin = $suggester->createInstance('server', ['#search' => $autocomplete]); 192 | assert($plugin instanceof SuggesterInterface); 193 | $configuration = $plugin->getConfiguration(); 194 | $autocompletion_fields = isset($configuration['fields']) ? $configuration['fields'] : []; 195 | if (!$autocompletion_fields) { 196 | $autocompletion_fields = $plugin->getSearch()->getIndex()->getFulltextFields(); 197 | } 198 | 199 | // Collect autocompletion fields in an array keyed by field id. 200 | $all_autocompletion_fields += array_flip($autocompletion_fields); 201 | } 202 | } 203 | 204 | // Map index fields. 205 | foreach ($index->getFields() as $field_id => $field_data) { 206 | $properties[$field_id] = MappingFactory::mappingFromField($field_data); 207 | // Enable fielddata for fields that are used with autocompletion. 208 | if (isset($all_autocompletion_fields[$field_id])) { 209 | $properties[$field_id]['fielddata'] = TRUE; 210 | } 211 | } 212 | 213 | $properties['_language'] = [ 214 | 'type' => 'keyword', 215 | ]; 216 | 217 | $params['body'][$params['type']]['properties'] = $properties; 218 | 219 | // Allow other modules to alter index mapping before we create it. 220 | $dispatcher = \Drupal::service('event_dispatcher'); 221 | $prepareIndexMappingEvent = new PrepareIndexMappingEvent($params, $params['index']); 222 | $event = $dispatcher->dispatch(PrepareIndexMappingEvent::PREPARE_INDEX_MAPPING, $prepareIndexMappingEvent); 223 | $params = $event->getIndexMappingParams(); 224 | 225 | return $params; 226 | } 227 | 228 | /** 229 | * Helper function. Returns the Elasticsearch name of an index. 230 | * 231 | * @param \Drupal\search_api\IndexInterface $index 232 | * Index object. 233 | * 234 | * @return string 235 | * The name of the index on the Elasticsearch server. Includes a prefix for 236 | * uniqueness, the database name, and index machine name. 237 | */ 238 | public static function getIndexName(IndexInterface $index) { 239 | 240 | // Get index machine name. 241 | $index_machine_name = is_string($index) ? $index : $index->id(); 242 | 243 | // Get prefix and suffix from the cluster if present. 244 | $cluster_id = $index->getServerInstance()->getBackend()->getCluster(); 245 | $cluster_options = Cluster::load($cluster_id)->options; 246 | 247 | $index_suffix = ''; 248 | if (!empty($cluster_options['rewrite']['rewrite_index'])) { 249 | $index_prefix = isset($cluster_options['rewrite']['index']['prefix']) ? $cluster_options['rewrite']['index']['prefix'] : ''; 250 | if ($index_prefix && substr($index_prefix, -1) !== '_') { 251 | $index_prefix .= '_'; 252 | } 253 | $index_suffix = isset($cluster_options['rewrite']['index']['suffix']) ? $cluster_options['rewrite']['index']['suffix'] : ''; 254 | if ($index_suffix && $index_suffix[0] !== '_') { 255 | $index_suffix = '_' . $index_suffix; 256 | } 257 | } 258 | else { 259 | // If a custom rewrite is not enabled, set prefix to db name by default. 260 | $options = \Drupal::database()->getConnectionOptions(); 261 | $index_prefix = 'elasticsearch_index_' . $options['database'] . '_'; 262 | } 263 | 264 | return strtolower(preg_replace( 265 | '/[^A-Za-z0-9_]+/', 266 | '', 267 | $index_prefix . $index_machine_name . $index_suffix 268 | )); 269 | } 270 | 271 | } 272 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/field/ElasticsearchViewsEntity.php: -------------------------------------------------------------------------------- 1 | setEntityDisplayRepository($container->get('entity_display.repository')); 38 | 39 | return $field; 40 | } 41 | 42 | /** 43 | * Retrieves the entity display repository. 44 | * 45 | * @return \Drupal\Core\Entity\EntityDisplayRepositoryInterface 46 | * The entity entity display repository. 47 | */ 48 | public function getEntityDisplayRepository() { 49 | return $this->entityDisplayRepository ?: \Drupal::service('entity_display.repository'); 50 | } 51 | 52 | /** 53 | * Sets the entity display repository. 54 | * 55 | * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository 56 | * The new entity display repository. 57 | * 58 | * @return $this 59 | */ 60 | public function setEntityDisplayRepository(EntityDisplayRepositoryInterface $entity_display_repository) { 61 | $this->entityDisplayRepository = $entity_display_repository; 62 | return $this; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function defineOptions() { 69 | $options = parent::defineOptions(); 70 | 71 | $options['display_methods'] = array('default' => array()); 72 | 73 | return $options; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function buildOptionsForm(&$form, FormStateInterface $form_state) { 80 | parent::buildOptionsForm($form, $form_state); 81 | 82 | $entity_type_id = $this->getTargetEntityTypeId(); 83 | $view_modes = array(); 84 | $bundles = array(); 85 | if ($entity_type_id) { 86 | $bundles = $this->getEntityManager()->getBundleInfo($entity_type_id); 87 | // In case the field definition specifies the bundles to expect, restrict 88 | // the displayed bundles to those. 89 | $settings = $this->getFieldDefinition()->getSettings(); 90 | if (!empty($settings['handler_settings']['target_bundles'])) { 91 | $bundles = array_intersect_key($bundles, $settings['handler_settings']['target_bundles']); 92 | } 93 | foreach ($bundles as $bundle => $info) { 94 | $view_modes[$bundle] = $this->getEntityDisplayRepository() 95 | ->getViewModeOptionsByBundle($entity_type_id, $bundle); 96 | } 97 | } 98 | 99 | foreach ($bundles as $bundle => $info) { 100 | $args['@bundle'] = $info['label']; 101 | $form['display_methods'][$bundle]['display_method'] = array( 102 | '#type' => 'select', 103 | '#title' => $this->t('Display for "@bundle" bundle', $args), 104 | '#options' => array( 105 | '' => $this->t('Hide'), 106 | 'id' => $this->t('Raw ID'), 107 | 'label' => $this->t('Only label'), 108 | ), 109 | '#default_value' => 'label', 110 | ); 111 | if (isset($this->options['display_methods'][$bundle]['display_method'])) { 112 | $form['display_methods'][$bundle]['display_method']['#default_value'] = $this->options['display_methods'][$bundle]['display_method']; 113 | } 114 | if (!empty($view_modes[$bundle])) { 115 | $form['display_methods'][$bundle]['display_method']['#options']['view_mode'] = $this->t('Entity view'); 116 | if (count($view_modes[$bundle]) > 1) { 117 | $form['display_methods'][$bundle]['view_mode'] = array( 118 | '#type' => 'select', 119 | '#title' => $this->t('View mode for "@bundle" bundle', $args), 120 | '#options' => $view_modes[$bundle], 121 | '#states' => array( 122 | 'visible' => array( 123 | ':input[name="options[display_methods][' . $bundle . '][display_method]"]' => array( 124 | 'value' => 'view_mode', 125 | ), 126 | ), 127 | ), 128 | ); 129 | if (isset($this->options['display_methods'][$bundle]['view_mode'])) { 130 | $form['display_methods'][$bundle]['view_mode']['#default_value'] = $this->options['display_methods'][$bundle]['view_mode']; 131 | } 132 | } 133 | else { 134 | reset($view_modes[$bundle]); 135 | $form['display_methods'][$bundle]['view_mode'] = array( 136 | '#type' => 'value', 137 | '#value' => key($view_modes[$bundle]), 138 | ); 139 | } 140 | } 141 | if (count($bundles) == 1) { 142 | $form['display_methods'][$bundle]['display_method']['#title'] = $this->t('Display method'); 143 | if (!empty($form['display_methods'][$bundle]['view_mode'])) { 144 | $form['display_methods'][$bundle]['view_mode']['#title'] = $this->t('View mode'); 145 | } 146 | } 147 | } 148 | 149 | $form['link_to_item']['#description'] .= ' ' . $this->t('This will only take effect for entities for which only the entity label is displayed.'); 150 | $form['link_to_item']['#weight'] = 5; 151 | } 152 | 153 | /** 154 | * Return the entity type ID of the entity this field handler should display. 155 | * 156 | * @return string|null 157 | * The entity type ID, or NULL if it could not be found. 158 | */ 159 | public function getTargetEntityTypeId() { 160 | $field_definition = $this->getFieldDefinition(); 161 | if ($field_definition->getType() === 'field_item:comment') { 162 | return 'comment'; 163 | } 164 | return $field_definition->getSetting('target_type'); 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function query() { 171 | $this->addRetrievedProperty($this->getCombinedPropertyPath()); 172 | } 173 | 174 | /** 175 | * {@inheritdoc} 176 | */ 177 | public function preRender(&$values) { 178 | parent::preRender($values); 179 | 180 | // The parent method will just have loaded the entity IDs. We now multi-load 181 | // the actual objects. 182 | $property_path = $this->getCombinedPropertyPath(); 183 | foreach ($values as $i => $row) { 184 | if (!empty($row->{$property_path})) { 185 | foreach ((array) $row->{$property_path} as $j => $value) { 186 | if (is_scalar($value)) { 187 | $to_load[$value][] = array($i, $j); 188 | } 189 | } 190 | } 191 | } 192 | 193 | if (empty($to_load)) { 194 | return; 195 | } 196 | 197 | $entities = $this->getEntityManager() 198 | ->getStorage($this->getTargetEntityTypeId()) 199 | ->loadMultiple(array_keys($to_load)); 200 | $account = $this->getQuery()->getAccessAccount(); 201 | foreach ($entities as $id => $entity) { 202 | foreach ($to_load[$id] as list($i, $j)) { 203 | if ($entity->access('view', $account)) { 204 | $values[$i]->{$property_path}[$j] = $entity; 205 | } 206 | } 207 | } 208 | } 209 | 210 | /** 211 | * {@inheritdoc} 212 | */ 213 | public function render_item($count, $item) { 214 | if (is_array($item['value'])) { 215 | return $this->getRenderer()->render($item['value']); 216 | } 217 | return parent::render_item($count, $item); 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | */ 223 | public function getItems(ResultRow $values) { 224 | $property_path = $this->getCombinedPropertyPath(); 225 | if (!empty($values->{$property_path})) { 226 | $items = array(); 227 | foreach ((array) $values->{$property_path} as $value) { 228 | if ($value instanceof EntityInterface) { 229 | $item = $this->getItem($value); 230 | if ($item) { 231 | $items[] = $item; 232 | } 233 | } 234 | } 235 | return $items; 236 | } 237 | return array(); 238 | } 239 | 240 | /** 241 | * Creates an item for the given entity. 242 | * 243 | * @param \Drupal\Core\Entity\EntityInterface $entity 244 | * The entity. 245 | * 246 | * @return array|null 247 | * NULL if the entity should not be displayed. Otherwise, an associative 248 | * array with at least "value" set, to either a string or a render array, 249 | * and possibly also additional alter options. 250 | */ 251 | protected function getItem(EntityInterface $entity) { 252 | $bundle = $entity->bundle(); 253 | if (empty($this->options['display_methods'][$bundle]['display_method'])) { 254 | return NULL; 255 | } 256 | 257 | $display_method = $this->options['display_methods'][$bundle]['display_method']; 258 | if (in_array($display_method, array('id', 'label'))) { 259 | if ($display_method == 'label') { 260 | $item['value'] = $entity->label(); 261 | } 262 | else { 263 | $item['value'] = $entity->id(); 264 | } 265 | 266 | if ($this->options['link_to_item']) { 267 | $item['make_link'] = TRUE; 268 | $item['url'] = $entity->toUrl('canonical'); 269 | } 270 | 271 | return $item; 272 | } 273 | 274 | $view_mode = $this->options['display_methods'][$bundle]['view_mode']; 275 | $build = $this->getEntityManager() 276 | ->getViewBuilder($entity->getEntityTypeId()) 277 | ->view($entity, $view_mode); 278 | return array( 279 | 'value' => $build, 280 | ); 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /src/Form/IndexForm.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_manager; 50 | $this->clientManager = $client_manager; 51 | $this->clusterManager = $cluster_manager; 52 | } 53 | 54 | /** 55 | * @param \Symfony\Component\DependencyInjection\ContainerInterface $container 56 | * 57 | * @return static 58 | */ 59 | static public function create(ContainerInterface $container) { 60 | return new static ( 61 | $container->get('entity_type.manager'), 62 | $container->get('elasticsearch_connector.client_manager'), 63 | $container->get('elasticsearch_connector.cluster_manager') 64 | ); 65 | } 66 | 67 | /** 68 | * Get the entity manager. 69 | * 70 | * @return \Drupal\Core\Entity\EntityManager 71 | * An instance of EntityManager. 72 | */ 73 | protected function getEntityManager() { 74 | return $this->entityTypeManager; 75 | } 76 | 77 | /** 78 | * Get the cluster storage controller. 79 | * 80 | * @return \Drupal\Core\Entity\EntityStorageInterface 81 | * An instance of EntityStorageInterface. 82 | */ 83 | protected function getClusterStorage() { 84 | return $this->getEntityManager()->getStorage('elasticsearch_cluster'); 85 | } 86 | 87 | /** 88 | * Get the index storage controller. 89 | * 90 | * @return \Drupal\Core\Entity\EntityStorageInterface 91 | * An instance of EntityStorageInterface. 92 | */ 93 | protected function getIndexStorage() { 94 | return $this->getEntityManager()->getStorage('elasticsearch_index'); 95 | } 96 | 97 | /** 98 | * Get all clusters. 99 | * 100 | * @return array 101 | * All clusters 102 | */ 103 | protected function getAllClusters() { 104 | $options = array(); 105 | foreach ( 106 | $this->getClusterStorage() 107 | ->loadMultiple() as $cluster_machine_name 108 | ) { 109 | $options[$cluster_machine_name->cluster_id] = $cluster_machine_name; 110 | } 111 | return $options; 112 | } 113 | 114 | /** 115 | * Get cluster field. 116 | * 117 | * @param string $field 118 | * Field name. 119 | * 120 | * @return array 121 | * All clusters' fields. 122 | */ 123 | protected function getClusterField($field) { 124 | $clusters = $this->getAllClusters(); 125 | $options = array(); 126 | foreach ($clusters as $cluster) { 127 | $options[$cluster->$field] = $cluster->$field; 128 | } 129 | return $options; 130 | } 131 | 132 | /** 133 | * Return url of the selected cluster. 134 | * 135 | * @param string $id 136 | * Cluster id. 137 | * 138 | * @return string 139 | * Cluster url. 140 | */ 141 | protected function getSelectedClusterUrl($id) { 142 | $result = NULL; 143 | $clusters = $this->getAllClusters(); 144 | foreach ($clusters as $cluster) { 145 | if ($cluster->cluster_id == $id) { 146 | $result = $cluster->url; 147 | } 148 | } 149 | return $result; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function form(array $form, FormStateInterface $form_state) { 156 | if ($form_state->isRebuilding()) { 157 | $this->entity = $this->buildEntity($form, $form_state); 158 | } 159 | 160 | $form = parent::form($form, $form_state); 161 | $form['#title'] = $this->t('Index'); 162 | 163 | $this->buildEntityForm($form, $form_state); 164 | return $form; 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function buildEntityForm(array &$form, FormStateInterface $form_state) { 171 | // TODO: Provide check and support for other index modules settings. 172 | // TODO: Provide support for the rest of the dynamic settings. 173 | // TODO: Make sure that on edit the static settings cannot be changed. 174 | // @see https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html 175 | $form['index'] = array( 176 | '#type' => 'value', 177 | '#value' => $this->entity, 178 | ); 179 | 180 | $form['name'] = array( 181 | '#type' => 'textfield', 182 | '#title' => t('Index name'), 183 | '#required' => TRUE, 184 | '#default_value' => '', 185 | '#description' => t('Enter the index name.'), 186 | '#weight' => 1, 187 | ); 188 | 189 | $form['index_id'] = array( 190 | '#type' => 'machine_name', 191 | '#title' => t('Index id'), 192 | '#default_value' => '', 193 | '#maxlength' => 125, 194 | '#description' => t('Unique, machine-readable identifier for this Index'), 195 | '#machine_name' => array( 196 | 'exists' => array($this->getIndexStorage(), 'load'), 197 | 'source' => array('name'), 198 | 'replace_pattern' => '[^a-z0-9_]+', 199 | 'replace' => '_', 200 | ), 201 | '#required' => TRUE, 202 | '#disabled' => !empty($this->entity->index_id), 203 | '#weight' => 2, 204 | ); 205 | 206 | // Here server refers to the elasticsearch cluster. 207 | $form['server'] = array( 208 | '#type' => 'radios', 209 | '#title' => $this->t('Server'), 210 | '#default_value' => !empty($this->entity->server) ? $this->entity->server : $this->clusterManager->getDefaultCluster(), 211 | '#description' => $this->t('Select the server this index should reside on. Index can not be enabled without connection to valid server.'), 212 | '#options' => $this->getClusterField('cluster_id'), 213 | '#weight' => 9, 214 | '#required' => TRUE, 215 | ); 216 | 217 | $form['num_of_shards'] = array( 218 | '#type' => 'textfield', 219 | '#title' => t('Number of shards'), 220 | '#required' => TRUE, 221 | '#default_value' => 5, 222 | '#description' => t('Enter the number of shards for the index.'), 223 | '#weight' => 3, 224 | ); 225 | 226 | $form['num_of_replica'] = array( 227 | '#type' => 'textfield', 228 | '#title' => t('Number of replica'), 229 | '#default_value' => 1, 230 | '#description' => t('Enter the number of shards replicas.'), 231 | '#weight' => 4, 232 | ); 233 | 234 | $form['codec'] = array( 235 | '#type' => 'select', 236 | '#title' => t('Codec'), 237 | '#default_value' => (!empty($this->entity->codec) ? $this->entity->codec : 'default'), 238 | '#description' => t('Select compression for stored data. Defaults to: LZ4.'), 239 | '#options' => array( 240 | 'default' => 'LZ4', 241 | 'best_compression' => 'DEFLATE', 242 | ), 243 | '#weight' => 5, 244 | ); 245 | } 246 | 247 | /** 248 | * {@inheritdoc} 249 | */ 250 | public function validateForm(array &$form, FormStateInterface $form_state) { 251 | parent::validateForm($form, $form_state); 252 | 253 | $values = $form_state->getValues(); 254 | 255 | if (!preg_match('/^[a-z][a-z0-9_]*$/i', $values['index_id'])) { 256 | $form_state->setErrorByName('name', t('Enter an index name that begins with a letter and contains only letters, numbers, and underscores.')); 257 | } 258 | 259 | if (!is_numeric($values['num_of_shards']) || $values['num_of_shards'] < 1) { 260 | $form_state->setErrorByName('num_of_shards', t('Invalid number of shards.')); 261 | } 262 | 263 | if (!is_numeric($values['num_of_replica'])) { 264 | $form_state->setErrorByName('num_of_replica', t('Invalid number of replica.')); 265 | } 266 | } 267 | 268 | /** 269 | * {@inheritdoc} 270 | */ 271 | public function save(array $form, FormStateInterface $form_state) { 272 | $cluster = $this->entityTypeManager->getStorage('elasticsearch_cluster')->load($this->entity->server); 273 | $client = $this->clientManager->getClientForCluster($cluster); 274 | 275 | $index_params['index'] = $this->entity->index_id; 276 | $index_params['body']['settings']['number_of_shards'] = $form_state->getValue('num_of_shards'); 277 | $index_params['body']['settings']['number_of_replicas'] = $form_state->getValue('num_of_replica'); 278 | $index_params['body']['settings']['codec'] = $form_state->getValue('codec'); 279 | 280 | try { 281 | $response = $client->indices()->create($index_params); 282 | if ($client->CheckResponseAck($response)) { 283 | $this->messenger()->addMessage( 284 | t( 285 | 'The index %index having id %index_id has been successfully created.', 286 | array( 287 | '%index' => $form_state->getValue('name'), 288 | '%index_id' => $form_state->getValue('index_id'), 289 | ) 290 | ) 291 | ); 292 | } 293 | else { 294 | $this->messenger()->addError( 295 | t( 296 | 'Fail to create the index %index having id @index_id', 297 | array( 298 | '%index' => $form_state->getValue('name'), 299 | '@index_id' => $form_state->getValue('index_id'), 300 | ) 301 | ) 302 | ); 303 | } 304 | 305 | parent::save($form, $form_state); 306 | 307 | $this->messenger()->addMessage(t('Index %label has been added.', array('%label' => $this->entity->label()))); 308 | 309 | $form_state->setRedirect('elasticsearch_connector.config_entity.list'); 310 | } 311 | catch (\Exception $e) { 312 | $this->messenger()->addError($e->getMessage()); 313 | } 314 | } 315 | 316 | } 317 | -------------------------------------------------------------------------------- /src/Form/ClusterForm.php: -------------------------------------------------------------------------------- 1 | clientManager = $client_manager; 40 | $this->clusterManager = $cluster_manager; 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | static public function create(ContainerInterface $container) { 47 | return new static ( 48 | $container->get('elasticsearch_connector.client_manager'), 49 | $container->get('elasticsearch_connector.cluster_manager') 50 | ); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function form(array $form, FormStateInterface $form_state) { 57 | if ($form_state->isRebuilding()) { 58 | $this->entity = $this->buildEntity($form, $form_state); 59 | } 60 | $form = parent::form($form, $form_state); 61 | if ($this->entity->isNew()) { 62 | $form['#title'] = $this->t('Add Elasticsearch Cluster'); 63 | } 64 | else { 65 | $form['#title'] = $this->t( 66 | 'Edit Elasticsearch Cluster @label', 67 | array('@label' => $this->entity->label()) 68 | ); 69 | } 70 | 71 | $this->buildEntityForm($form, $form_state); 72 | 73 | return $form; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function buildEntityForm(array &$form, FormStateInterface $form_state) { 80 | $form['cluster'] = array( 81 | '#type' => 'value', 82 | '#value' => $this->entity, 83 | ); 84 | 85 | $form['name'] = array( 86 | '#type' => 'textfield', 87 | '#title' => t('Administrative cluster name'), 88 | '#default_value' => empty($this->entity->name) ? '' : $this->entity->name, 89 | '#description' => t( 90 | 'Enter the administrative cluster name that will be your Elasticsearch cluster unique identifier.' 91 | ), 92 | '#required' => TRUE, 93 | '#weight' => 1, 94 | ); 95 | 96 | $form['cluster_id'] = array( 97 | '#type' => 'machine_name', 98 | '#title' => t('Cluster id'), 99 | '#default_value' => !empty($this->entity->cluster_id) ? $this->entity->cluster_id : '', 100 | '#maxlength' => 125, 101 | '#description' => t( 102 | 'A unique machine-readable name for this Elasticsearch cluster.' 103 | ), 104 | '#machine_name' => array( 105 | 'exists' => ['Drupal\elasticsearch_connector\Entity\Cluster', 'load'], 106 | 'source' => array('name'), 107 | ), 108 | '#required' => TRUE, 109 | '#disabled' => !empty($this->entity->cluster_id), 110 | '#weight' => 2, 111 | ); 112 | 113 | $form['url'] = array( 114 | '#type' => 'textfield', 115 | '#title' => t('Server URL'), 116 | '#default_value' => !empty($this->entity->url) ? $this->entity->url : '', 117 | '#description' => t( 118 | 'URL and port of a server (node) in the cluster. ' . 119 | 'Please, always enter the port even if it is default one. ' . 120 | 'Nodes will be automatically discovered. ' . 121 | 'Examples: http://localhost:9200 or https://localhost:443.' 122 | ), 123 | '#required' => TRUE, 124 | '#weight' => 3, 125 | ); 126 | 127 | $form['status_info'] = $this->clusterFormInfo(); 128 | 129 | $default = $this->clusterManager->getDefaultCluster(); 130 | $form['default'] = array( 131 | '#type' => 'checkbox', 132 | '#title' => t('Make this cluster default connection'), 133 | '#description' => t( 134 | 'If the cluster connection is not specified the API will use the default connection.' 135 | ), 136 | '#default_value' => (empty($default) || (!empty($this->entity->cluster_id) && $this->entity->cluster_id == $default)) ? '1' : '0', 137 | '#weight' => 4, 138 | ); 139 | 140 | $form['options'] = array( 141 | '#tree' => TRUE, 142 | '#weight' => 5, 143 | ); 144 | 145 | $form['options']['multiple_nodes_connection'] = array( 146 | '#type' => 'checkbox', 147 | '#title' => t('Use multiple nodes connection'), 148 | '#description' => t( 149 | 'Automatically discover all nodes and use them in the cluster connection. ' . 150 | 'Then the Elasticsearch client can distribute the query execution on random base between nodes.' 151 | ), 152 | '#default_value' => (!empty($this->entity->options['multiple_nodes_connection']) ? 1 : 0), 153 | '#weight' => 5.1, 154 | ); 155 | 156 | $form['status'] = array( 157 | '#type' => 'radios', 158 | '#title' => t('Status'), 159 | '#default_value' => isset($this->entity->status) ? $this->entity->status : Cluster::ELASTICSEARCH_CONNECTOR_STATUS_ACTIVE, 160 | '#options' => array( 161 | Cluster::ELASTICSEARCH_CONNECTOR_STATUS_ACTIVE => t('Active'), 162 | Cluster::ELASTICSEARCH_CONNECTOR_STATUS_INACTIVE => t('Inactive'), 163 | ), 164 | '#required' => TRUE, 165 | '#weight' => 6, 166 | ); 167 | 168 | $form['options']['use_authentication'] = array( 169 | '#type' => 'checkbox', 170 | '#title' => t('Use authentication'), 171 | '#description' => t( 172 | 'Use HTTP authentication method to connect to Elasticsearch.' 173 | ), 174 | '#default_value' => (!empty($this->entity->options['use_authentication']) ? 1 : 0), 175 | '#suffix' => '
 
', 176 | '#weight' => 5.2, 177 | ); 178 | 179 | $form['options']['authentication_type'] = array( 180 | '#type' => 'radios', 181 | '#title' => t('Authentication type'), 182 | '#description' => t('Select the http authentication type.'), 183 | '#options' => array( 184 | 'Basic' => t('Basic'), 185 | 'Digest' => t('Digest'), 186 | 'NTLM' => t('NTLM'), 187 | ), 188 | '#default_value' => (!empty($this->entity->options['authentication_type']) ? $this->entity->options['authentication_type'] : 'Basic'), 189 | '#states' => array( 190 | 'visible' => array( 191 | ':input[name="options[use_authentication]"]' => array('checked' => TRUE), 192 | ), 193 | ), 194 | '#weight' => 5.3, 195 | ); 196 | 197 | $form['options']['username'] = array( 198 | '#type' => 'textfield', 199 | '#title' => t('Username'), 200 | '#description' => t('The username for authentication.'), 201 | '#default_value' => (!empty($this->entity->options['username']) ? $this->entity->options['username'] : ''), 202 | '#states' => array( 203 | 'visible' => array( 204 | ':input[name="options[use_authentication]"]' => array('checked' => TRUE), 205 | ), 206 | ), 207 | '#weight' => 5.4, 208 | ); 209 | 210 | $form['options']['password'] = array( 211 | '#type' => 'textfield', 212 | '#title' => t('Password'), 213 | '#description' => t('The password for authentication.'), 214 | '#default_value' => (!empty($this->entity->options['password']) ? $this->entity->options['password'] : ''), 215 | '#states' => array( 216 | 'visible' => array( 217 | ':input[name="options[use_authentication]"]' => array('checked' => TRUE), 218 | ), 219 | ), 220 | '#weight' => 5.5, 221 | ); 222 | 223 | $form['options']['timeout'] = array( 224 | '#type' => 'number', 225 | '#title' => t('Connection timeout'), 226 | '#size' => 20, 227 | '#required' => TRUE, 228 | '#description' => t( 229 | 'After how many seconds the connection should timeout if there is no connection to Elasticsearch.' 230 | ), 231 | '#default_value' => (!empty($this->entity->options['timeout']) ? $this->entity->options['timeout'] : Cluster::ELASTICSEARCH_CONNECTOR_DEFAULT_TIMEOUT), 232 | '#weight' => 5.6, 233 | ); 234 | 235 | $form['options']['rewrite'] = [ 236 | '#tree' => TRUE, 237 | '#weight' => 6, 238 | ]; 239 | 240 | $form['options']['rewrite']['rewrite_index'] = [ 241 | '#title' => $this->t('Alter the Elasticsearch index name.'), 242 | '#type' => 'checkbox', 243 | '#default_value' => (!empty($this->entity->options['rewrite']['rewrite_index']) ? 1 : 0), 244 | '#description' => $this->t('Alter the name of the Elasticsearch index by optionally adding a prefix and suffix to the Search API index name.') 245 | ]; 246 | 247 | $form['options']['rewrite']['index']['prefix'] = [ 248 | '#type' => 'textfield', 249 | '#title' => $this->t('Index name prefix'), 250 | '#default_value' => (!empty($this->entity->options['rewrite']['index']['prefix']) ? $this->entity->options['rewrite']['index']['prefix'] : ''), 251 | '#description' => $this->t( 252 | 'If a value is provided it will be prepended to the index name.' 253 | ), 254 | '#states' => [ 255 | 'visible' => [ 256 | ':input[name="options[rewrite][rewrite_index]"]' => ['checked' => TRUE], 257 | ], 258 | ], 259 | '#weight' => 5, 260 | ]; 261 | 262 | $form['options']['rewrite']['index']['suffix'] = [ 263 | '#type' => 'textfield', 264 | '#title' => $this->t('Index name suffix'), 265 | '#default_value' => (!empty($this->entity->options['rewrite']['index']['suffix']) ? $this->entity->options['rewrite']['index']['suffix'] : ''), 266 | '#description' => $this->t( 267 | 'If a value is provided it will be appended to the index name.' 268 | ), 269 | '#states' => [ 270 | 'visible' => [ 271 | ':input[name="options[rewrite][rewrite_index]"]' => ['checked' => TRUE], 272 | ], 273 | ], 274 | '#weight' => 10, 275 | ]; 276 | } 277 | 278 | /** 279 | * {@inheritdoc} 280 | */ 281 | public function validateForm(array &$form, FormStateInterface $form_state) { 282 | parent::validateForm($form, $form_state); 283 | $values = $form_state->getValues(); 284 | 285 | // TODO: Check for valid URL when we are submitting the form. 286 | // Set default cluster. 287 | $default = $this->clusterManager->getDefaultCluster(); 288 | if (empty($default) && !$values['default']) { 289 | $default = $this->clusterManager->setDefaultCluster($values['cluster_id']); 290 | } 291 | elseif ($values['default']) { 292 | $default = $this->clusterManager->setDefaultCluster($values['cluster_id']); 293 | } 294 | 295 | if ($values['default'] == 0 && !empty($default) && $default == $values['cluster_id']) { 296 | $this->messenger()->addWarning( 297 | t( 298 | 'There must be a default connection. %name is still the default 299 | connection. Please change the default setting on the cluster you wish 300 | to set as default.', 301 | array( 302 | '%name' => $values['name'], 303 | ) 304 | ) 305 | ); 306 | } 307 | } 308 | 309 | /** 310 | * Build the cluster info table for the edit page. 311 | * 312 | * @return array 313 | */ 314 | protected function clusterFormInfo() { 315 | $element = array(); 316 | 317 | if (isset($this->entity->url)) { 318 | try { 319 | $client_connector = $this->clientManager->getClientForCluster($this->entity); 320 | 321 | $cluster_info = $client_connector->getClusterInfo(); 322 | if ($cluster_info) { 323 | $headers = array( 324 | array('data' => t('Cluster name')), 325 | array('data' => t('Status')), 326 | array('data' => t('Number of nodes')), 327 | ); 328 | 329 | if (isset($cluster_info['state'])) { 330 | $rows = array( 331 | array( 332 | $cluster_info['health']['cluster_name'], 333 | $cluster_info['health']['status'], 334 | $cluster_info['health']['number_of_nodes'], 335 | ), 336 | ); 337 | 338 | $element = array( 339 | '#theme' => 'table', 340 | '#header' => $headers, 341 | '#rows' => $rows, 342 | '#attributes' => array( 343 | 'class' => array('admin-elasticsearch'), 344 | 'id' => 'cluster-info', 345 | ), 346 | ); 347 | } 348 | else { 349 | $rows = array( 350 | array( 351 | t('Unknown'), 352 | t('Unavailable'), 353 | '', 354 | ), 355 | ); 356 | 357 | $element = array( 358 | '#theme' => 'table', 359 | '#header' => $headers, 360 | '#rows' => $rows, 361 | '#attributes' => array( 362 | 'class' => array('admin-elasticsearch'), 363 | 'id' => 'cluster-info', 364 | ), 365 | ); 366 | } 367 | } 368 | else { 369 | $element['#type'] = 'markup'; 370 | $element['#markup'] = '
 
'; 371 | } 372 | } 373 | catch (\Exception $e) { 374 | $this->messenger()->addError($e->getMessage()); 375 | } 376 | } 377 | 378 | return $element; 379 | } 380 | 381 | /** 382 | * {@inheritdoc} 383 | */ 384 | public function save(array $form, FormStateInterface $form_state) { 385 | // Only save the server if the form doesn't need to be rebuilt. 386 | if (!$form_state->isRebuilding()) { 387 | try { 388 | parent::save($form, $form_state); 389 | $this->messenger()->addMessage(t('Cluster %label has been updated.', array('%label' => $this->entity->label()))); 390 | $form_state->setRedirect('elasticsearch_connector.config_entity.list'); 391 | } 392 | catch (EntityStorageException $e) { 393 | $form_state->setRebuild(); 394 | watchdog_exception('elasticsearch_connector', $e); 395 | $this->messenger()->addError( 396 | $this->t('The cluster could not be saved.') 397 | ); 398 | } 399 | } 400 | } 401 | 402 | } 403 | -------------------------------------------------------------------------------- /elasticsearch_connector.module: -------------------------------------------------------------------------------- 1 | ' . t('About') . ''; 19 | $output .= '

' . t('Abstraction of making connection to the elasticsearch server. This module is API for a whole bunch of functionality connected with this module. Provides an interface to connect to a elasticsearch cluster and implements the official Elasticsearch-php library.') . '

'; 20 | $output .= '

' . t('Uses') . '

'; 21 | $output .= '
'; 22 | $output .= '
' . t('Create cluster') . '
'; 23 | $output .= '
' . t('To be described...') . '
'; 24 | $output .= '
' . t('Create index') . '
'; 25 | $output .= '
' . t('To be described...') . '
'; 26 | $output .= '
'; 27 | 28 | return $output; 29 | } 30 | } 31 | 32 | /** 33 | * Implements hook_cron(). 34 | */ 35 | function elasticsearch_connector_cron() { 36 | // TODO: Check cluster node state and update cluster nodes if any changes. 37 | // Do this only if we have auto-node update configuration enabled. 38 | // The default state of the auto mode will be activated! 39 | } 40 | 41 | /** 42 | * Implements hook_theme(). 43 | */ 44 | function elasticsearch_connector_theme() { 45 | return array( 46 | 'elasticsearch_connector_page' => array( 47 | 'render element' => 'page', 48 | 'template' => 'elasticsearch-connector-dialog-page', 49 | ), 50 | ); 51 | } 52 | 53 | /** 54 | * Implements hook_element_info(). 55 | */ 56 | function elasticsearch_connector_element_info() { 57 | return array( 58 | 'ec_clusters' => array( 59 | '#input' => TRUE, 60 | '#multiple' => FALSE, 61 | '#theme' => 'select', 62 | '#theme_wrappers' => array('form_element'), 63 | '#process' => array('_elasticsearch_ec_clusters_process'), 64 | ), 65 | 'ec_index' => array( 66 | '#input' => TRUE, 67 | '#tree' => TRUE, 68 | '#multiple' => FALSE, 69 | '#theme_wrappers' => array('form_element'), 70 | '#process' => array('_elasticsearch_ec_index_process'), 71 | '#attached' => _elasticsearch_ec_index_attached(), 72 | ), 73 | ); 74 | } 75 | 76 | /** 77 | * Process the ec_cluster element type. 78 | * 79 | * @param array $element 80 | * Form element array. 81 | * @param array $form_state 82 | * Form State array. 83 | * @param array $form 84 | * Form array. 85 | * 86 | * @return array $element 87 | * The altered $element array. 88 | */ 89 | function _elasticsearch_ec_clusters_process(array $element, array &$form_state, array $form) { 90 | $element = form_process_select($element); 91 | 92 | if (empty($element['#skip_default_options'])) { 93 | $element['#only_active'] = isset($element['#only_active']) ? $element['#only_active'] : TRUE; 94 | $element['#empty_option'] = isset($element['#empty_option']) ? $element['#empty_option'] : TRUE; 95 | $clusters = elasticsearch_cluster_load_all($element['#only_active'], $element['#empty_option']); 96 | $element['#options'] = $clusters; 97 | } 98 | 99 | return $element; 100 | } 101 | 102 | /** 103 | * Attach required javascript for the ec_index element. 104 | * 105 | * @return array 106 | * Prepared array with assets to attach. 107 | */ 108 | function _elasticsearch_ec_index_attached() { 109 | return array( 110 | 'js' => array(drupal_get_path('module', 'elasticsearch') . '/js/ec-index.js'), 111 | 'css' => array(drupal_get_path('module', 'elasticsearch') . '/css/ec-index.css'), 112 | 'library' => array(array('system', 'ui.dialog')), 113 | ); 114 | } 115 | 116 | /** 117 | * Checks if other modules have locked the cluster. 118 | * 119 | * In case of major changes on the cluster settings and deletion the cluster 120 | * could be locked. 121 | * Invokes the hooks similar to the module_invoke. 122 | * 123 | * @param object $cluster 124 | * The fully loaded Cluster object. 125 | * 126 | * @return array 127 | * Array with clusters locked for deletion. 128 | */ 129 | function _elasticsearch_check_if_cluster_locked($cluster) { 130 | $locked = array(); 131 | if (!empty($cluster)) { 132 | $type = 'cluster'; 133 | foreach (module_implements('elasticsearch_edit_lock') as $module) { 134 | $function = $module . '_elasticsearch_edit_lock'; 135 | $locked_result = $function($type, $cluster, NULL); 136 | if (!empty($locked_result)) { 137 | $locked[] = $module; 138 | } 139 | } 140 | } 141 | 142 | return $locked; 143 | } 144 | 145 | /** 146 | * Checks if other modules have locked the index. 147 | * 148 | * In case of major changes on the index settings and deletion the index could 149 | * be locked. 150 | * Invokes the hooks similar to the module_invoke. 151 | * 152 | * @param string $cluster 153 | * The fully loaded Cluster object. 154 | * @param string $index 155 | * The fully loaded Index object. 156 | * 157 | * @return array 158 | * Array with indexes locked for deletion. 159 | */ 160 | function _elasticsearch_check_if_index_locked($cluster, $index) { 161 | $locked = array(); 162 | if (!empty($cluster)) { 163 | $type = 'index'; 164 | foreach (module_implements('elasticsearch_edit_lock') as $module) { 165 | $function = $module . '_elasticsearch_edit_lock'; 166 | $locked_result = $function($type, $cluster, $index); 167 | if (!empty($locked_result)) { 168 | $locked[] = $module; 169 | } 170 | } 171 | } 172 | 173 | return $locked; 174 | } 175 | 176 | /** 177 | * Implements hook_elasticsearch_edit_lock(). 178 | */ 179 | function elasticsearch_connector_elasticsearch_edit_lock($type, $cluster, $index = NULL) { 180 | if ($type == 'cluster' && $cluster->cluster_id == elasticsearch_connector_get_default()) { 181 | return TRUE; 182 | } 183 | 184 | return FALSE; 185 | } 186 | 187 | /** 188 | * Build two drop downs, one for the cluster and one for the indices. 189 | * 190 | * @param array $element 191 | * Form element array. 192 | * @param array $form_state 193 | * Form State array. 194 | * @param array $form 195 | * Form array. 196 | * 197 | * @return array $element 198 | * The altered $element array. 199 | */ 200 | function _elasticsearch_ec_index_process(array $element, array &$form_state, array $form) { 201 | $element['#tree'] = TRUE; 202 | $element_id = $element['#id']; 203 | $wrapper_id = $element_id . '-index-wrapper'; 204 | 205 | // TODO: Add icon if the cluster is OK or not. 206 | $element['cluster_id'] = array( 207 | '#type' => 'select', 208 | '#id' => $element_id . '-cluster-id', 209 | '#title' => t('Select cluster'), 210 | '#required' => $element['#required'], 211 | '#default_value' => isset($element['#default_value']) 212 | && is_array($element['#default_value']) 213 | && isset($element['#default_value']['cluster_id']) 214 | ? $element['#default_value']['cluster_id'] 215 | : '', 216 | // TODO: Allow this option to be overwritten and #value if we had such. 217 | '#description' => t('Select the cluster.'), 218 | '#ajax' => array( 219 | 'callback' => '_elasticsearch_ec_index_ajax', 220 | 'wrapper' => $wrapper_id, 221 | 'method' => 'replace', 222 | 'effect' => 'fade', 223 | ), 224 | ); 225 | 226 | if (!isset($element['cluster_id']['#current_path'])) { 227 | $element['cluster_id']['#current_path'] = current_path(); 228 | } 229 | 230 | if (empty($element['#skip_default_options'])) { 231 | $element['#only_active'] = isset($element['#only_active']) ? $element['#only_active'] : TRUE; 232 | $element['#empty_option'] = isset($element['#empty_option']) ? $element['#empty_option'] : TRUE; 233 | $clusters = elasticsearch_cluster_load_all($element['#only_active'], $element['#empty_option']); 234 | $element['cluster_id']['#options'] = $clusters; 235 | } 236 | 237 | // TODO: We need to handle the incoming tree name if such. 238 | $links = array(); 239 | $index_options = array('' => t('Select index')); 240 | if (is_array($element['#value']) && !empty($element['#value']['cluster_id'])) { 241 | $index_options = array(); 242 | try { 243 | $index_options = elasticsearch_get_indices_options($element['#value']['cluster_id'], TRUE); 244 | } 245 | catch (\Exception $e) { 246 | if (!empty($element['#throw_exp'])) { 247 | throw $e; 248 | } 249 | } 250 | $links[] = array( 251 | 'title' => t('Add index'), 252 | 'href' => 'admin/config/elasticsearch/clusters/' . $element['#value']['cluster_id'] . '/indices/add', 253 | 'attributes' => array('target' => '_blank', 'class' => 'ec-index-dialog'), 254 | 'query' => array( 255 | 'render' => 'elasticsearch-dialog', 256 | 'index_element_id' => $element_id . '-index', 257 | 'cluster_element_id' => $element_id . '-cluster-id', 258 | ), 259 | ); 260 | } 261 | 262 | $element['index'] = array( 263 | '#type' => 'select', 264 | '#title' => t('Select index'), 265 | '#id' => $element_id . '-index', 266 | '#required' => $element['#required'], 267 | '#default_value' => isset($element['#default_value']) 268 | && is_array($element['#default_value']) 269 | && isset($element['#default_value']['index']) 270 | ? $element['#default_value']['index'] 271 | : '', 272 | '#description' => t('Select the index.'), 273 | '#options' => $index_options, 274 | '#prefix' => '
', 275 | '#suffix' => '
', 285 | ); 286 | 287 | unset($element['#title']); 288 | $context = array( 289 | 'form' => $form, 290 | ); 291 | drupal_alter('ec_index_process', $element, $form_state, $context); 292 | 293 | return $element; 294 | } 295 | 296 | /** 297 | * Implements hook_page_alter(). 298 | */ 299 | function elasticsearch_connector_page_alter(&$page) { 300 | if (elasticsearch_in_dialog()) { 301 | unset($page['page_top']); 302 | unset($page['page_bottom']); 303 | 304 | $page['#theme'] = 'elasticsearch_page'; 305 | } 306 | } 307 | 308 | /** 309 | * Check if we are in a references dialog. 310 | * 311 | * @return bool 312 | * TRUE if we are in a dialog. 313 | */ 314 | function elasticsearch_connector_in_dialog() { 315 | return (isset($_GET['render']) && $_GET['render'] == 'elasticsearch-dialog'); 316 | } 317 | 318 | /** 319 | * Check if we should close the dialog upon submission. 320 | */ 321 | function elasticsearch_connector_close_on_submit() { 322 | return (!isset($_GET['closeonsubmit']) || $_GET['closeonsubmit']); 323 | } 324 | 325 | /** 326 | * Sets destination parameter to close the dialog after redirect is completed. 327 | */ 328 | function elasticsearch_connector_close_on_redirect($cluster_id, $index_name) { 329 | // We use $_GET['destination'] since that overrides anything that happens 330 | // in the form. It is a hack, but it is very effective, since we don't have 331 | // to be worried about getting overrun by other form submit handlers. 332 | $_GET['destination'] = 'elasticsearch-dialog/redirect/' . 333 | $cluster_id . '/' . $index_name . 334 | '?elasticsearch-dialog-close=1&render=elasticsearch-dialog'; 335 | 336 | if (isset($_GET['cluster_element_id'])) { 337 | $_GET['destination'] .= '&index_element_id=' . $_GET['index_element_id']; 338 | } 339 | 340 | if (isset($_GET['cluster_element_id'])) { 341 | $_GET['destination'] .= '&cluster_element_id=' . $_GET['cluster_element_id']; 342 | } 343 | 344 | } 345 | 346 | /** 347 | * Page callback for our redirect page. 348 | */ 349 | function elasticsearch_connector_redirect_page($cluster, $index_name) { 350 | // Add appropriate javascript that will be used by the parent page to fill in 351 | // the required data. 352 | if (elasticsearch_in_dialog() && isset($_GET['elasticsearch-dialog-close'])) { 353 | drupal_add_js(drupal_get_path('module', 'elasticsearch') . '/js/ec-index-child.js'); 354 | drupal_add_js( 355 | array( 356 | 'elasticsearch' => 357 | array( 358 | 'dialog' => array( 359 | 'cluster_id' => $cluster->cluster_id, 360 | 'index_name' => $index_name, 361 | 'index_element_id' => (string) $_GET['index_element_id'], 362 | 'cluster_element_id' => (string) $_GET['cluster_element_id'], 363 | ), 364 | ), 365 | ), 366 | 'setting' 367 | ); 368 | } 369 | 370 | return ''; 371 | } 372 | 373 | /** 374 | * Ajax callback for the ec_index element. 375 | * 376 | * @param array $form 377 | * Form array. 378 | * @param array $form_state 379 | * Form State array. 380 | */ 381 | function _elasticsearch_ec_index_ajax(array $form, array $form_state) { 382 | $parents = $form_state['triggering_element']['#parents']; 383 | $search_key = array_search('cluster_id', $parents); 384 | $parents[$search_key] = 'index'; 385 | $index_element = drupal_array_get_nested_value($form, $parents); 386 | 387 | return $index_element; 388 | } 389 | 390 | /** 391 | * Get the indices based on cluster id. 392 | * 393 | * @param string $cluster_id 394 | * Cluster id. 395 | * 396 | * @return array Indices 397 | * Array with indices attached to the provided Cluster. 398 | */ 399 | function elasticsearch_connector_get_indices_options($cluster_id, $empty_option = FALSE) { 400 | // TODO in src. 401 | $result = array(); 402 | 403 | $client = elasticsearch_get_client_by_id($cluster_id); 404 | if ($client) { 405 | $indices = $client->indices()->stats(); 406 | drupal_alter('elasticsearch_indices', $indices); 407 | if ($empty_option) { 408 | $result[''] = t('Select index'); 409 | } 410 | if (!empty($indices['indices'])) { 411 | foreach ($indices['indices'] as $index_name => $index_info) { 412 | // TODO: Check index status if such e.g. index closed or s.o. 413 | $result[$index_name] = $index_name; 414 | } 415 | } 416 | } 417 | 418 | return $result; 419 | } 420 | 421 | /** 422 | * Check if the index name has been passed correctly. 423 | * 424 | * @param string $index_name 425 | * Index name. 426 | * 427 | * @return bool 428 | * TRUE or FALSE depending on whether it is a valid name or not. 429 | */ 430 | function elasticsearch_connector_index_valid_load($index_name) { 431 | // TODO in src. 432 | if (preg_match('/^[a-z][a-z0-9_]*$/i', $index_name)) { 433 | return $index_name; 434 | } 435 | 436 | return FALSE; 437 | } 438 | 439 | /** 440 | * Get the nodes stats from elasticsearch server. 441 | * 442 | * @param \Elasticsearch\Client $client 443 | * ElasticSearch client object. 444 | * 445 | * @return array 446 | * Array with cluster stats. 447 | */ 448 | function elasticsearch_connector_get_cluster_nodes_stat(Client $client) { 449 | try { 450 | return $client->nodes()->stats(); 451 | } 452 | catch (\Exception $e) { 453 | \Drupal::messenger()->addError($e->getMessage()); 454 | } 455 | 456 | return array(); 457 | } 458 | 459 | /** 460 | * Check if a specific plugin exists on all nodes. 461 | * 462 | * TODO: This should be changed to check all data Nodes only but for now lets 463 | * check all of them. 464 | * 465 | * @param \Elasticsearch\Client $client 466 | * Fully loaded Client object. 467 | * @param string $plugin_name 468 | * Plugin name. 469 | * 470 | * @return bool 471 | * TRUE or FALSE depending if the plugin exists. 472 | * 473 | * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/modules-plugins.html 474 | */ 475 | function elasticsearch_connector_check_plugin_exists(Client $client, $plugin_name) { 476 | $nodes_plugins = array(); 477 | $result = FALSE; 478 | 479 | try { 480 | $plugins = $client->nodes()->info(array('node_id' => '_all')); 481 | foreach ($plugins['nodes'] as $elastic_node_id => $elastic_node) { 482 | $nodes_plugins[$elastic_node_id][$plugin_name] = FALSE; 483 | foreach ($elastic_node['plugins'] as $plugin) { 484 | if ($plugin['name'] == $plugin_name) { 485 | $nodes_plugins[$elastic_node_id][$plugin_name] = TRUE; 486 | } 487 | } 488 | 489 | if (empty($nodes_plugins[$elastic_node_id][$plugin_name])) { 490 | $result = FALSE; 491 | break; 492 | } 493 | else { 494 | $result = TRUE; 495 | } 496 | } 497 | 498 | return $result; 499 | } 500 | catch (\Exception $e) { 501 | \Drupal::messenger()->addError($e->getMessage()); 502 | return FALSE; 503 | } 504 | } 505 | 506 | /** 507 | * Process variables for references_dialog_page. 508 | */ 509 | function template_process_elasticsearch_page(&$variables) { 510 | // Generate messages last in order to capture as many as possible for the 511 | // current page. 512 | if (!isset($variables['messages'])) { 513 | $variables['messages'] = $variables['page']['#show_messages'] ? theme('status_messages') : ''; 514 | } 515 | } 516 | 517 | /** 518 | * Validates #element_validate of any form element as Elasticsearch TTL setting. 519 | * 520 | * @param array $element 521 | * Form element array. 522 | * @param array $form_state 523 | * Form State array. 524 | * @param array $form 525 | * Form array. 526 | */ 527 | function _elasticsearch_validate_ttl_field(array $element, array &$form_state, array $form) { 528 | if (!empty($element['#value']) && !preg_match('/^([\d]+)(d|m|h|ms|w)$/', $element['#value'])) { 529 | form_error($element, t('Invalid elasticsearch TTL value. Please use the proper syntax e.g. 1d (d (days), m (minutes), h (hours), ms (milliseconds) or w (weeks)).')); 530 | } 531 | } 532 | 533 | /** 534 | * Returns a unique hash for the current site. 535 | * 536 | * This is used to identify documents from different sites within a single 537 | * Elasticsearch server. 538 | * 539 | * @return string 540 | * A unique site hash, containing only alphanumeric characters. 541 | */ 542 | function elasticsearch_connector_site_hash() { 543 | // Copied from apachesolr_site_hash(). 544 | if (!($hash = \Drupal::config('elasticsearch.settings')->get('site_hash'))) { 545 | global $base_url; 546 | $hash = substr(base_convert(sha1(uniqid($base_url, TRUE)), 16, 36), 0, 6); 547 | \Drupal::config('elasticsearch.settings')->set('site_hash', $hash)->save(); 548 | } 549 | return $hash; 550 | } 551 | 552 | /** 553 | * Alter the mapping of Drupal data types to Search API data types. 554 | * 555 | * @param array $mapping 556 | * An array mapping all known (and supported) Drupal data types to their 557 | * corresponding Search API data types. A value of FALSE means that fields of 558 | * that type should be ignored by the Search API. 559 | * 560 | * @see \Drupal\search_api\Utility\DataTypeHelperInterface::getFieldTypeMapping() 561 | */ 562 | function elasticsearch_connector_search_api_field_type_mapping_alter(array &$mapping) { 563 | $mapping['object'] = 'object'; 564 | } 565 | --------------------------------------------------------------------------------