├── .circleci └── config.yml ├── .gitignore ├── README.TXT ├── composer.json ├── config └── schema │ ├── elasticsearch_connector.backend.schema.yml │ ├── elasticsearch_connector.cluster.schema.yml │ └── elasticsearch_connector.index.schema.yml ├── css └── ec-index.css ├── elasticsearch_connector.api.php ├── elasticsearch_connector.info.yml ├── elasticsearch_connector.install ├── elasticsearch_connector.libraries.yml ├── elasticsearch_connector.links.action.yml ├── elasticsearch_connector.links.menu.yml ├── elasticsearch_connector.links.task.yml ├── elasticsearch_connector.module ├── elasticsearch_connector.permissions.yml ├── elasticsearch_connector.routing.yml ├── elasticsearch_connector.services.yml ├── img ├── cancel.png └── plus.png ├── js ├── ec-index-child.js └── ec-index.js ├── modules └── elasticsearch_connector_views │ ├── elasticsearch_connector_views.info.yml │ ├── elasticsearch_connector_views.views.inc │ └── src │ └── Plugin │ └── views │ ├── ElasticsearchViewsHandlerTrait.php │ ├── field │ ├── ElasticsearchViewsBoolean.php │ ├── ElasticsearchViewsDate.php │ ├── ElasticsearchViewsEntity.php │ ├── ElasticsearchViewsEntityField.php │ ├── ElasticsearchViewsFieldTrait.php │ ├── ElasticsearchViewsMarkup.php │ ├── ElasticsearchViewsNumeric.php │ └── ElasticsearchViewsStandard.php │ ├── filter │ ├── ElasticsearchViewsBooleanOperator.php │ ├── ElasticsearchViewsDate.php │ ├── ElasticsearchViewsFulltextSearch.php │ ├── ElasticsearchViewsNumericFilter.php │ ├── ElasticsearchViewsStandard.php │ └── ElasticsearchViewsStringFilter.php │ ├── join │ └── ElasticsearchViewsJoin.php │ └── query │ └── ElasticsearchViewsQuery.php ├── phpunit.core.xml.dist ├── src ├── ClusterManager.php ├── Controller │ ├── ClusterListBuilder.php │ └── ElasticsearchController.php ├── ElasticSearch │ ├── ClientManager.php │ ├── ClientManagerInterface.php │ └── Parameters │ │ ├── Builder │ │ └── SearchBuilder.php │ │ └── Factory │ │ ├── FilterFactory.php │ │ ├── IndexFactory.php │ │ ├── MappingFactory.php │ │ └── SearchFactory.php ├── Entity │ ├── Cluster.php │ ├── ClusterRouteProvider.php │ ├── Index.php │ └── IndexRouteProvider.php ├── Event │ ├── BuildIndexParamsEvent.php │ ├── BuildSearchParamsEvent.php │ ├── PrepareIndexEvent.php │ ├── PrepareIndexMappingEvent.php │ ├── PrepareMappingEvent.php │ └── PrepareSearchQueryEvent.php ├── Exception │ └── ElasticSearchConnectorException.php ├── Form │ ├── ClusterDeleteForm.php │ ├── ClusterForm.php │ ├── IndexDeleteForm.php │ └── IndexForm.php └── Plugin │ └── search_api │ ├── backend │ ├── SearchApiElasticsearchBackend.php │ └── SearchApiElasticsearchBackendInterface.php │ ├── data_type │ └── ObjectDataType.php │ └── processor │ └── ExcludeSourceFields.php └── tests ├── modules └── elasticsearch_test │ ├── config │ └── install │ │ ├── search_api.index.elasticsearch_index.yml │ │ └── search_api.server.elasticsearch_server.yml │ └── elasticsearch_test.info.yml └── src ├── Behat ├── behat.yml ├── example.behat.local.yml └── features │ └── bootstrap │ ├── ElasticsearchConnectorFeatureContext.php │ └── settings_form.feature ├── Kernel └── ElasticsearchTest.php └── Unit ├── ClusterManagerTest.php ├── ElasticSearch ├── ClientManagerTest.php └── Parameters │ ├── Builder │ └── SearchBuilderTest.php │ └── Factory │ ├── FilterFactoryTest.php │ ├── IndexFactoryTest.php │ └── MappingFactoryTest.php └── Entity ├── ClusterRouteProviderTest.php ├── IndexRouteProviderTest.php └── IndexTest.php /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .idea 4 | tests/src/Behat/behat.local.yml 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/schema/elasticsearch_connector.cluster.schema.yml: -------------------------------------------------------------------------------- 1 | elasticsearch_connector.cluster.*: 2 | type: config_entity 3 | label: 'Elasticsearch Cluster' 4 | mapping: 5 | cluster_id: 6 | type: string 7 | label: 'Cluster ID' 8 | name: 9 | type: string 10 | label: 'Cluster Name' 11 | status: 12 | type: string 13 | label: 'Cluster Status' 14 | url: 15 | type: string 16 | label: 'Cluster URL' 17 | proxy: 18 | type: string 19 | label: 'Cluster Proxy' 20 | options: 21 | type: mapping 22 | label: 'Options' 23 | mapping: 24 | multiple_nodes_connection: 25 | type: boolean 26 | label: 'Multiple Nodes Connection' 27 | locked: 28 | type: boolean 29 | label: 'Locked' 30 | -------------------------------------------------------------------------------- /config/schema/elasticsearch_connector.index.schema.yml: -------------------------------------------------------------------------------- 1 | elasticsearch_connector.index.*: 2 | type: config_entity 3 | label: 'Elasticsearch Index' 4 | mapping: 5 | index_id: 6 | type: string 7 | label: 'Index ID' 8 | name: 9 | type: string 10 | label: 'Index Name' 11 | num_of_shards: 12 | type: integer 13 | label: 'Shards' 14 | num_of_replica: 15 | type: integer 16 | label: 'Replica' 17 | server: 18 | type: string 19 | label: 'Cluster machine name' 20 | -------------------------------------------------------------------------------- /css/ec-index.css: -------------------------------------------------------------------------------- 1 | ul.index-dialog-links, ul.index-dialog-links li { 2 | list-style-type: none; 3 | } 4 | 5 | ul.index-dialog-links li, ul.index-dialog-links li a { 6 | display: inline; 7 | } 8 | 9 | ul.index-dialog-links li.active, ul.index-dialog-links li a.active { 10 | color: #0074BD; 11 | } 12 | 13 | ul.index-dialog-links a { 14 | padding-left: 20px; 15 | margin: 5px; 16 | } 17 | 18 | ul.index-dialog-links li a { 19 | background: url('../img/plus.png') left no-repeat; 20 | } 21 | 22 | .es-list-index { 23 | padding-left: 3em; 24 | } 25 | -------------------------------------------------------------------------------- /elasticsearch_connector.api.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 | -------------------------------------------------------------------------------- /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.action.yml: -------------------------------------------------------------------------------- 1 | entity.elasticsearch_cluster.add_form: 2 | route_name: entity.elasticsearch_cluster.add_form 3 | title: 'Add cluster' 4 | appears_on: 5 | - elasticsearch_connector.config_entity.list 6 | entity.elasticsearch_index.add_form: 7 | route_name: entity.elasticsearch_index.add_form 8 | title: 'Add index' 9 | appears_on: 10 | - elasticsearch_connector.config_entity.list 11 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /elasticsearch_connector.permissions.yml: -------------------------------------------------------------------------------- 1 | administer elasticsearch connector: 2 | title: 'Administer elasticsearch connector' 3 | description: 'Giving you access to administer elasticsearch connector configurations.' 4 | administer elasticsearch cluster: 5 | title: 'Administer elasticsearch cluster' 6 | description: 'Giving you access to administer elasticsearch clusters.' 7 | administer elasticsearch index: 8 | title: 'Administer elasticsearch index' 9 | description: 'Giving you access to administer elasticsearch indices.' 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /elasticsearch_connector.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | elasticsearch_connector.client_factory: 3 | class: nodespark\DESConnector\ClientFactory 4 | 5 | elasticsearch_connector.client_manager: 6 | class: Drupal\elasticsearch_connector\ElasticSearch\ClientManager 7 | arguments: 8 | - '@module_handler' 9 | - '@elasticsearch_connector.client_factory' 10 | 11 | elasticsearch_connector.cluster_manager: 12 | class: Drupal\elasticsearch_connector\ClusterManager 13 | arguments: ['@state', '@entity_type.manager'] 14 | 15 | elasticsearch_connector.index_factory: 16 | class: Drupal\elasticsearch_connector\ElasticSearch\Parameters\Factory\IndexFactory 17 | -------------------------------------------------------------------------------- /img/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodespark/elasticsearch_connector/36a0645ce4d88f2db826972dfaadab9a995ef114/img/cancel.png -------------------------------------------------------------------------------- /img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodespark/elasticsearch_connector/36a0645ce4d88f2db826972dfaadab9a995ef114/img/plus.png -------------------------------------------------------------------------------- /js/ec-index-child.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | /** 3 | * Attach the child dialog behavior to new content. 4 | */ 5 | Drupal.behaviors.ECIndexDialogChild = { 6 | attach: function (context, settings) { 7 | // Get the entity id and title from the settings provided by the views display. 8 | var cluster_id = settings.elasticsearch.dialog.cluster_id; 9 | var index_name = settings.elasticsearch.dialog.index_name; 10 | if (cluster_id != null && cluster_id != '') { 11 | // Close the dialog by communicating with the parent. 12 | parent.Drupal.ECIndexDialog.close(cluster_id, index_name, settings.elasticsearch.dialog); 13 | } 14 | } 15 | } 16 | })(jQuery); 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/elasticsearch_connector_views.info.yml: -------------------------------------------------------------------------------- 1 | name: 2 | 'Elasticsearch Connector Views' 3 | description: 4 | 'Stand alone module for integration between Drupal Views and Elasticsearch indexes.' 5 | core: 8.x 6 | package: Elasticsearch 7 | type: module 8 | dependencies: 9 | - drupal:views 10 | - elasticsearch_connector:elasticsearch_connector 11 | version: VERSION 12 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/field/ElasticsearchViewsBoolean.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/field/ElasticsearchViewsFieldTrait.php: -------------------------------------------------------------------------------- 1 | definition['format'])) { 23 | $this->definition['format'] = filter_default_format(); 24 | } 25 | parent::init($view, $display, $options); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/field/ElasticsearchViewsNumeric.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 | -------------------------------------------------------------------------------- /modules/elasticsearch_connector_views/src/Plugin/views/filter/ElasticsearchViewsNumericFilter.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/src/Plugin/views/join/ElasticsearchViewsJoin.php: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ClientManagerInterface.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /src/Entity/Index.php: -------------------------------------------------------------------------------- 1 | index_id) ? $this->index_id : NULL; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/Exception/ElasticSearchConnectorException.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Plugin/search_api/backend/SearchApiElasticsearchBackendInterface.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/modules/elasticsearch_test/config/install/search_api.server.elasticsearch_server.yml: -------------------------------------------------------------------------------- 1 | id: elasticsearch_server 2 | name: 'Elasticsearch server' 3 | description: 'Testing Elasticsearch backend.' 4 | backend: elasticsearch 5 | backend_config: 6 | scheme: http 7 | host: localhost 8 | port: '9200' 9 | path: '' 10 | http_user: '' 11 | http_pass: '' 12 | excerpt: 0 13 | retrieve_data: 0 14 | highlight_data: 0 15 | http_method: AUTO 16 | status: 1 17 | langcode: en 18 | dependencies: 19 | module: 20 | - elasticsearch_connector 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/src/Behat/features/bootstrap/ElasticsearchConnectorFeatureContext.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/features/bootstrap/settings_form.feature: -------------------------------------------------------------------------------- 1 | @javascript @api 2 | Feature: Test the settings form 3 | In order configure Draco DFP 4 | As an authenticated user 5 | I need to be able to set the module's settings via it's admin form. 6 | 7 | Scenario: Fill and submit the administration form 8 | Given I am logged in as a user with the "administrator" role 9 | When I visit "admin/config/search/elasticsearch-connector" 10 | Then I should see the text "No clusters available." 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/src/Unit/ElasticSearch/Parameters/Factory/IndexFactoryTest.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 | -------------------------------------------------------------------------------- /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/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/IndexTest.php: -------------------------------------------------------------------------------- 1 | 'foo'], 'elasticsearch_index'); 20 | $this->assertEquals('foo', $index->id()); 21 | } 22 | 23 | } 24 | --------------------------------------------------------------------------------