├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── ---feature-request.md └── workflows │ ├── code-style.yml │ ├── stale.yml │ ├── static-analysis.yml │ └── test.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── phpstan.neon.dist ├── phpunit.xml.dist ├── src ├── Client.php ├── Documents │ ├── Document.php │ ├── DocumentManager.php │ └── Routing.php ├── Exceptions │ ├── BulkOperationException.php │ └── RawResultReadOnlyException.php ├── Indices │ ├── Alias.php │ ├── Index.php │ ├── IndexManager.php │ ├── Mapping.php │ ├── MappingProperties.php │ └── Settings.php └── Search │ ├── Aggregation.php │ ├── Bucket.php │ ├── Explanation.php │ ├── Highlight.php │ ├── Hit.php │ ├── PointInTimeManager.php │ ├── RawResult.php │ ├── SearchParameters.php │ ├── SearchResult.php │ └── Suggestion.php └── tests ├── Extensions └── BypassFinals.php └── Unit ├── Documents ├── DocumentManagerTest.php ├── DocumentTest.php └── RoutingTest.php ├── Exceptions └── BulkOperationExceptionTest.php ├── Indices ├── AliasTest.php ├── IndexManagerTest.php ├── IndexTest.php ├── MappingPropertiesTest.php ├── MappingTest.php └── SettingsTest.php └── Search ├── AggregationTest.php ├── BucketTest.php ├── ExplanationTest.php ├── HighlightTest.php ├── HitTest.php ├── PointInTimeManagerTest.php ├── SearchParametersTest.php ├── SearchResultTest.php └── SuggestionTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: ivanbabenko 2 | custom: ['https://paypal.me/babenkoi'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | | Software | Version 17 | | ------------- | --------------- 18 | | PHP | x.y.z 19 | | Elasticsearch | x.y.z 20 | 21 | **Describe the bug** 22 | 23 | 24 | **To Reproduce** 25 | 26 | 27 | **Current behavior** 28 | 29 | 30 | **Expected behavior** 31 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/code-style.yml: -------------------------------------------------------------------------------- 1 | name: Code style 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | style-check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v2 11 | 12 | - name: Install php and composer 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: 8.2 16 | coverage: none 17 | tools: composer:v2 18 | 19 | - name: Get composer cache directory 20 | id: composer-cache 21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 22 | 23 | - name: Restore composer cache 24 | uses: actions/cache@v4 25 | with: 26 | path: ${{ steps.composer-cache.outputs.dir }} 27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 28 | restore-keys: ${{ runner.os }}-composer- 29 | 30 | - name: Install dependencies 31 | run: composer install --no-interaction 32 | 33 | - name: Check code style 34 | run: make style-check 35 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v3 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' 15 | stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' 16 | stale-issue-label: 'stale' 17 | stale-pr-label: 'stale' 18 | days-before-stale: 30 19 | days-before-close: 7 20 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static analysis 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | static-analysis: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v2 11 | 12 | - name: Install php and composer 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: 8.2 16 | coverage: none 17 | tools: composer:v2 18 | 19 | - name: Get composer cache directory 20 | id: composer-cache 21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 22 | 23 | - name: Restore composer cache 24 | uses: actions/cache@v4 25 | with: 26 | path: ${{ steps.composer-cache.outputs.dir }} 27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 28 | restore-keys: ${{ runner.os }}-composer- 29 | 30 | - name: Install dependencies 31 | run: composer install --no-interaction 32 | 33 | - name: Analyse code 34 | run: make static-analysis 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | php: [8.2] 11 | include: 12 | - php: 8.2 13 | testbench: 9.0 14 | phpunit: 11.0 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Install php and composer 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | coverage: none 24 | tools: composer:v2 25 | 26 | - name: Get composer cache directory 27 | id: composer-cache 28 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 29 | 30 | - name: Restore composer cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.composer-cache.outputs.dir }} 34 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 35 | restore-keys: ${{ runner.os }}-composer- 36 | 37 | - name: Install dependencies 38 | run: composer require --no-interaction --dev orchestra/testbench:^${{ matrix.testbench }} phpunit/phpunit:^${{ matrix.phpunit }} 39 | 40 | - name: Run tests 41 | run: make test 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /bin 3 | /vendor 4 | /composer.lock 5 | /phpunit.xml 6 | /.phpunit.cache 7 | /.phpunit.result.cache 8 | /.php_cs 9 | /.php_cs.cache 10 | /.php-cs-fixer.cache 11 | /phpstan.neon 12 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 8 | ->in(__DIR__ . '/tests') 9 | ->name('*.php'); 10 | 11 | return (new Config()) 12 | ->setFinder($finder) 13 | ->setRules([ 14 | '@PSR2' => true, 15 | 'array_syntax' => ['syntax' => 'short'], 16 | 'compact_nullable_type_declaration' => true, 17 | 'concat_space' => ['spacing' => 'one'], 18 | 'declare_strict_types' => true, 19 | 'dir_constant' => true, 20 | 'self_static_accessor' => false, 21 | 'fully_qualified_strict_types' => true, 22 | 'function_to_constant' => true, 23 | 'type_declaration_spaces' => true, 24 | 'header_comment' => false, 25 | 'list_syntax' => ['syntax' => 'short'], 26 | 'lowercase_cast' => true, 27 | 'magic_method_casing' => true, 28 | 'modernize_types_casting' => true, 29 | 'multiline_comment_opening_closing' => true, 30 | 'native_constant_invocation' => true, 31 | 'no_alias_functions' => true, 32 | 'no_alternative_syntax' => true, 33 | 'no_blank_lines_after_phpdoc' => true, 34 | 'no_empty_comment' => true, 35 | 'no_empty_phpdoc' => true, 36 | 'no_extra_blank_lines' => true, 37 | 'no_leading_import_slash' => true, 38 | 'no_leading_namespace_whitespace' => true, 39 | 'no_spaces_around_offset' => true, 40 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], 41 | 'no_trailing_comma_in_singleline' => true, 42 | 'no_unneeded_control_parentheses' => true, 43 | 'no_unset_cast' => true, 44 | 'no_unused_imports' => true, 45 | 'no_useless_else' => true, 46 | 'no_useless_return' => true, 47 | 'no_whitespace_in_blank_line' => true, 48 | 'normalize_index_brace' => true, 49 | 'ordered_imports' => true, 50 | 'php_unit_construct' => true, 51 | 'php_unit_dedicate_assert' => ['target' => 'newest'], 52 | 'php_unit_dedicate_assert_internal_type' => ['target' => 'newest'], 53 | 'php_unit_expectation' => ['target' => 'newest'], 54 | 'php_unit_mock' => ['target' => 'newest'], 55 | 'php_unit_mock_short_will_return' => true, 56 | 'php_unit_no_expectation_annotation' => ['target' => 'newest'], 57 | 'php_unit_test_annotation' => ['style' => 'prefix'], 58 | 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], 59 | 'phpdoc_align' => ['align' => 'vertical'], 60 | 'phpdoc_line_span' => ['method' => 'multi', 'property' => 'multi'], 61 | 'phpdoc_no_package' => true, 62 | 'phpdoc_no_useless_inheritdoc' => true, 63 | 'phpdoc_scalar' => true, 64 | 'phpdoc_separation' => true, 65 | 'phpdoc_single_line_var_spacing' => true, 66 | 'phpdoc_trim' => true, 67 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 68 | 'phpdoc_types' => true, 69 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 70 | 'phpdoc_var_without_name' => true, 71 | 'return_assignment' => true, 72 | 'short_scalar_cast' => true, 73 | 'single_trait_insert_per_statement' => true, 74 | 'standardize_not_equals' => true, 75 | 'static_lambda' => true, 76 | 'ternary_to_null_coalescing' => true, 77 | 'trim_array_spaces' => true, 78 | 'array_indentation' => true, 79 | 'trailing_comma_in_multiline' => true, 80 | 'visibility_required' => true, 81 | 'yoda_style' => false, 82 | 'use_arrow_functions' => true, 83 | 'phpdoc_to_property_type' => true, 84 | ]); 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at babenko.i.a@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Workflow 2 | 3 | * Fork the project and clone it locally 4 | * Create a new branch for every new feature or a bug fix 5 | * Do the necessary code changes 6 | * Cover the new or fixed code with tests 7 | * Write a comprehensive commit message in a format `Add the xxx feature` or `Fix the xxx bug` 8 | * Push to the forked repository 9 | * Create a Pull Request to the master branch of the original repository 10 | * Make a new commit with a fix if one or more checks are failing (code analysis, tests, etc.) 11 | 12 | ## Pull Request Requirements 13 | 14 | * Follow [PSR-2 coding style standard](https://www.php-fig.org/psr/psr-2/) 15 | * Write tests 16 | * Document every new feature or an interface change in the README file 17 | * Make one Pull Request per feature / bug fix 18 | 19 | ## Running the Test Suite 20 | 21 | To run tests locally you need PHP (7.2 or higher) and [Composer](https://getcomposer.org/download/). 22 | 23 | Install the project dependencies: 24 | ``` 25 | composer install 26 | ``` 27 | 28 | Run the test suite: 29 | ``` 30 | make test 31 | ``` 32 | 33 | ## Code Analysis 34 | 35 | To ensure, that your code follows PSR-2 standards you can run: 36 | ``` 37 | make style-check 38 | ``` 39 | 40 | It is also recommended to perform static code analysis before opening a PR: 41 | ``` 42 | make static-analysis 43 | ``` 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ivan Babenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test coverage style-check static-analysis help 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | test: ## Run tests 6 | @printf "\033[93m→ Running tests\033[0m\n" 7 | @bin/phpunit --testdox 8 | @printf "\n\033[92m✔︎ Tests are completed\033[0m\n" 9 | 10 | coverage: ## Run tests and generate the code coverage report 11 | @printf "\033[93m→ Running tests and generating the code coverage report\033[0m\n" 12 | @XDEBUG_MODE=coverage bin/phpunit --testdox --coverage-text 13 | @printf "\n\033[92m✔︎ Tests are completed and the report is generated\033[0m\n" 14 | 15 | style-check: ## Check the code style 16 | @printf "\033[93m→ Checking the code style\033[0m\n" 17 | @bin/php-cs-fixer fix --allow-risky=yes --dry-run --diff --show-progress=dots --verbose 18 | @printf "\n\033[92m✔︎ Code style is checked\033[0m\n" 19 | 20 | static-analysis: ## Do static code analysis 21 | @printf "\033[93m→ Analysing the code\033[0m\n" 22 | @bin/phpstan analyse 23 | @printf "\n\033[92m✔︎ Code static analysis is completed\033[0m\n" 24 | 25 | help: ## Show help 26 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elastic Adapter 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/babenkoivan/elastic-adapter/v/stable)](https://packagist.org/packages/babenkoivan/elastic-adapter) 4 | [![Total Downloads](https://poser.pugx.org/babenkoivan/elastic-adapter/downloads)](https://packagist.org/packages/babenkoivan/elastic-adapter) 5 | [![License](https://poser.pugx.org/babenkoivan/elastic-adapter/license)](https://packagist.org/packages/babenkoivan/elastic-adapter) 6 | [![Tests](https://github.com/babenkoivan/elastic-adapter/workflows/Tests/badge.svg)](https://github.com/babenkoivan/elastic-adapter/actions?query=workflow%3ATests) 7 | [![Code style](https://github.com/babenkoivan/elastic-adapter/workflows/Code%20style/badge.svg)](https://github.com/babenkoivan/elastic-adapter/actions?query=workflow%3A%22Code+style%22) 8 | [![Static analysis](https://github.com/babenkoivan/elastic-adapter/workflows/Static%20analysis/badge.svg)](https://github.com/babenkoivan/elastic-adapter/actions?query=workflow%3A%22Static+analysis%22) 9 | [![Donate PayPal](https://img.shields.io/badge/donate-paypal-blue)](https://paypal.me/babenkoi) 10 | 11 |

12 | Support the project! 13 |

14 | 15 | --- 16 | 17 | Elastic Adapter is an adapter for the official PHP Elasticsearch client. It's designed to simplify basic index and document 18 | operations. 19 | 20 | ## Contents 21 | 22 | * [Compatibility](#compatibility) 23 | * [Installation](#installation) 24 | * [Configuration](#configuration) 25 | * [Index Management](#index-management) 26 | * [Document Management](#document-management) 27 | * [Point in Time Management](#point-in-time-management) 28 | 29 | ## Compatibility 30 | 31 | The current version of Elastic Adapter has been tested with the following configuration: 32 | 33 | * PHP 8.2 34 | * Elasticsearch 8.x 35 | * Laravel 11.x 36 | 37 | If your project uses older Laravel (or PHP) version check [the previous major version](https://github.com/babenkoivan/elastic-adapter/tree/v3.5.0#compatibility) of the package. 38 | 39 | ## Installation 40 | 41 | The library can be installed via Composer: 42 | 43 | ```bash 44 | composer require babenkoivan/elastic-adapter 45 | ``` 46 | 47 | ## Configuration 48 | 49 | Elastic Adapter uses [babenkoivan/elastic-client](https://github.com/babenkoivan/elastic-client) as a dependency. 50 | To change the client settings you need to publish the configuration file first: 51 | 52 | ```bash 53 | php artisan vendor:publish --provider="Elastic\Client\ServiceProvider" 54 | ``` 55 | 56 | In the newly created `config/elastic.client.php` file you can define the default connection name and describe multiple 57 | connections using configuration hashes. Please, refer to 58 | the [elastic-client documentation](https://github.com/babenkoivan/elastic-client) for more details. 59 | 60 | ## Index Management 61 | 62 | `\Elastic\Adapter\Indices\IndexManager` is used to manipulate indices. 63 | 64 | ### Create 65 | 66 | Create an index, either with the default settings and mapping: 67 | 68 | ```php 69 | $index = new \Elastic\Adapter\Indices\Index('my_index'); 70 | 71 | $indexManager->create($index); 72 | ``` 73 | 74 | or configured according to your needs: 75 | 76 | ```php 77 | $mapping = (new \Elastic\Adapter\Indices\Mapping()) 78 | ->text('title', [ 79 | 'boost' => 2, 80 | ]) 81 | ->keyword('tag', [ 82 | 'null_value' => 'NULL' 83 | ]) 84 | ->geoPoint('location') 85 | ->dynamic(true) 86 | ->dynamicTemplate('no_doc_values', [ 87 | 'match_mapping_type' => '*', 88 | 'mapping' => [ 89 | 'type' => '{dynamic_type}', 90 | 'doc_values' => false, 91 | ], 92 | ]); 93 | 94 | $settings = (new \Elastic\Adapter\Indices\Settings()) 95 | ->index([ 96 | 'number_of_replicas' => 2, 97 | 'refresh_interval' => -1 98 | ]); 99 | 100 | $index = new \Elastic\Adapter\Indices\Index('my_index', $mapping, $settings); 101 | 102 | $indexManager->create($index); 103 | ``` 104 | 105 | Alternatively, you can create an index using raw input: 106 | 107 | ```php 108 | $mapping = [ 109 | 'properties' => [ 110 | 'title' => [ 111 | 'type' => 'text' 112 | ] 113 | ] 114 | ]; 115 | 116 | $settings = [ 117 | 'number_of_replicas' => 2 118 | ]; 119 | 120 | $indexManager->createRaw('my_index', $mapping, $settings); 121 | ``` 122 | 123 | ### Drop 124 | 125 | Delete an index: 126 | 127 | ```php 128 | $indexManager->drop('my_index'); 129 | ``` 130 | 131 | ### Put Mapping 132 | 133 | Update an index mapping using builder: 134 | 135 | ```php 136 | $mapping = (new \Elastic\Adapter\Indices\Mapping()) 137 | ->text('title', [ 138 | 'boost' => 2, 139 | ]) 140 | ->keyword('tag', [ 141 | 'null_value' => 'NULL' 142 | ]) 143 | ->geoPoint('location'); 144 | 145 | $indexManager->putMapping('my_index', $mapping); 146 | ``` 147 | 148 | or using raw input: 149 | 150 | ```php 151 | $mapping = [ 152 | 'properties' => [ 153 | 'title' => [ 154 | 'type' => 'text' 155 | ] 156 | ] 157 | ]; 158 | 159 | $indexManager->putMappingRaw('my_index', $mapping); 160 | ``` 161 | 162 | ### Put Settings 163 | 164 | Update an index settings using builder: 165 | 166 | ```php 167 | $settings = (new \Elastic\Adapter\Indices\Settings()) 168 | ->analysis([ 169 | 'analyzer' => [ 170 | 'content' => [ 171 | 'type' => 'custom', 172 | 'tokenizer' => 'whitespace' 173 | ] 174 | ] 175 | ]); 176 | 177 | $indexManager->putSettings('my_index', $settings); 178 | ``` 179 | 180 | or using raw input: 181 | 182 | ```php 183 | $settings = [ 184 | 'number_of_replicas' => 2 185 | ]; 186 | 187 | $indexManager->putSettingsRaw('my_index', $settings); 188 | ``` 189 | 190 | ### Exists 191 | 192 | Check if an index exists: 193 | 194 | ```php 195 | $indexManager->exists('my_index'); 196 | ``` 197 | 198 | ### Open 199 | 200 | Open an index: 201 | 202 | ```php 203 | $indexManager->open('my_index'); 204 | ``` 205 | 206 | ### Close 207 | 208 | Close an index: 209 | 210 | ```php 211 | $indexManager->close('my_index'); 212 | ``` 213 | 214 | ### Put Alias 215 | 216 | Create an alias: 217 | 218 | ```php 219 | $alias = new \Elastic\Adapter\Indices\Alias('my_alias', true, [ 220 | 'term' => [ 221 | 'user_id' => 12, 222 | ], 223 | ]); 224 | 225 | $indexManager->putAlias('my_index', $alias); 226 | ``` 227 | 228 | The same with raw input: 229 | 230 | ```php 231 | $settings = [ 232 | 'is_write_index' => true, 233 | 'filter' => [ 234 | 'term' => [ 235 | 'user_id' => 12, 236 | ], 237 | ], 238 | ]; 239 | 240 | $indexManager->putAliasRaw('my_index', 'my_alias', $settings); 241 | ``` 242 | 243 | ### Get Aliases 244 | 245 | Get index aliases: 246 | 247 | ```php 248 | $indexManager->getAliases('my_index'); 249 | ``` 250 | 251 | ### Delete Alias 252 | 253 | Delete an alias: 254 | 255 | ```php 256 | $indexManager->deleteAlias('my_index', 'my_alias'); 257 | ``` 258 | 259 | ### Connection 260 | 261 | Switch Elasticsearch connection: 262 | 263 | ```php 264 | $indexManager->connection('my_connection'); 265 | ``` 266 | 267 | ## Document Management 268 | 269 | `\Elastic\Adapter\Documents\DocumentManager` is used to manage and search documents. 270 | 271 | ### Index 272 | 273 | Add a document to the index: 274 | 275 | ```php 276 | $documents = collect([ 277 | new \Elastic\Adapter\Documents\Document('1', ['title' => 'foo']), 278 | new \Elastic\Adapter\Documents\Document('2', ['title' => 'bar']), 279 | ]); 280 | 281 | $documentManager->index('my_index', $documents); 282 | ``` 283 | 284 | There is also an option to refresh index immediately: 285 | 286 | ```php 287 | $documentManager->index('my_index', $documents, true); 288 | ``` 289 | 290 | Finally, you can set a custom routing: 291 | 292 | ```php 293 | $routing = (new \Elastic\Adapter\Documents\Routing()) 294 | ->add('1', 'value1') 295 | ->add('2', 'value2'); 296 | 297 | $documentManager->index('my_index', $documents, false, $routing); 298 | ``` 299 | 300 | ### Delete 301 | 302 | Remove a document from the index: 303 | 304 | ```php 305 | $documentIds = ['1', '2']; 306 | 307 | $documentManager->delete('my_index', $documentIds); 308 | ``` 309 | 310 | If you want the index to be refreshed immediately pass `true` as the third argument: 311 | 312 | ```php 313 | $documentManager->delete('my_index', $documentIds, true); 314 | ``` 315 | 316 | You can also set a custom routing: 317 | 318 | ```php 319 | $routing = (new \Elastic\Adapter\Documents\Routing()) 320 | ->add('1', 'value1') 321 | ->add('2', 'value2'); 322 | 323 | $documentManager->delete('my_index', $documentIds, false, $routing); 324 | ``` 325 | 326 | Finally, you can delete documents using query: 327 | 328 | ```php 329 | $documentManager->deleteByQuery('my_index', ['match_all' => new \stdClass()]); 330 | ``` 331 | 332 | ### Search 333 | 334 | Search documents in the index: 335 | 336 | ```php 337 | // configure search parameters 338 | $searchParameters = new \Elastic\Adapter\Search\SearchParameters(); 339 | 340 | // specify indices to search in 341 | $searchParameters->indices(['my_index1', 'my_index2']); 342 | 343 | // define the query 344 | $searchParameters->query([ 345 | 'match' => [ 346 | 'message' => 'test' 347 | ] 348 | ]); 349 | 350 | // configure highlighting 351 | $searchParameters->highlight([ 352 | 'fields' => [ 353 | 'message' => [ 354 | 'type' => 'plain', 355 | 'fragment_size' => 15, 356 | 'number_of_fragments' => 3, 357 | 'fragmenter' => 'simple' 358 | ] 359 | ] 360 | ]); 361 | 362 | // add suggestions 363 | $searchParameters->suggest([ 364 | 'message_suggest' => [ 365 | 'text' => 'test', 366 | 'term' => [ 367 | 'field' => 'message' 368 | ] 369 | ] 370 | ]); 371 | 372 | // enable source filtering 373 | $searchParameters->source(['message', 'post_date']); 374 | 375 | // collapse fields 376 | $searchParameters->collapse([ 377 | 'field' => 'user' 378 | ]); 379 | 380 | // aggregate data 381 | $searchParameters->aggregations([ 382 | 'max_likes' => [ 383 | 'max' => [ 384 | 'field' => 'likes' 385 | ] 386 | ] 387 | ]); 388 | 389 | // sort documents 390 | $searchParameters->sort([ 391 | ['post_date' => ['order' => 'asc']], 392 | '_score' 393 | ]); 394 | 395 | // rescore documents 396 | $searchParameters->rescore([ 397 | 'window_size' => 50, 398 | 'query' => [ 399 | 'rescore_query' => [ 400 | 'match_phrase' => [ 401 | 'message' => [ 402 | 'query' => 'the quick brown', 403 | 'slop' => 2, 404 | ], 405 | ], 406 | ], 407 | 'query_weight' => 0.7, 408 | 'rescore_query_weight' => 1.2, 409 | ] 410 | ]); 411 | 412 | // add a post filter 413 | $searchParameters->postFilter([ 414 | 'term' => [ 415 | 'cover' => 'hard' 416 | ] 417 | ]); 418 | 419 | // track total hits 420 | $searchParameters->trackTotalHits(true); 421 | 422 | // track scores 423 | $searchParameters->trackScores(true); 424 | 425 | // script fields 426 | $searchParameters->scriptFields([ 427 | 'my_doubled_field' => [ 428 | 'script' => [ 429 | 'lang' => 'painless', 430 | 'source' => 'doc[params.field] * params.multiplier', 431 | 'params' => [ 432 | 'field' => 'my_field', 433 | 'multiplier' => 2, 434 | ], 435 | ], 436 | ], 437 | ]); 438 | 439 | // boost indices 440 | $searchParameters->indicesBoost([ 441 | ['my-alias' => 1.4], 442 | ['my-index' => 1.3], 443 | ]); 444 | 445 | // define the search type 446 | $searchParameters->searchType('query_then_fetch'); 447 | 448 | // set the preference 449 | $searchParameters->preference('_local'); 450 | 451 | // use pagination 452 | $searchParameters->from(0)->size(20); 453 | 454 | // search after 455 | $searchParameters->pointInTime([ 456 | 'id' => '46ToAwMDaWR5BXV1', 457 | 'keep_alive' => '1m', 458 | ]); 459 | 460 | $searchParameters->searchAfter([ 461 | '2021-05-20T05:30:04.832Z', 462 | 4294967298, 463 | ]); 464 | 465 | // use custom routing 466 | $searchParameters->routing(['user1', 'user2']); 467 | 468 | // enable explanation 469 | $searchParameters->explain(true); 470 | 471 | // set maximum number of documents to collect for each shard 472 | $searchParameters->terminateAfter(10); 473 | 474 | // enable caching 475 | $searchParameters->requestCache(true); 476 | 477 | // perform the search and get the result 478 | $searchResult = $documentManager->search($searchParameters); 479 | 480 | // get the total number of matching documents 481 | $total = $searchResult->total(); 482 | 483 | // get the corresponding hits 484 | $hits = $searchResult->hits(); 485 | 486 | // every hit provides access to the related index name, the score, the document, the highlight and more 487 | // in addition, you can get a raw representation of the hit 488 | foreach ($hits as $hit) { 489 | $indexName = $hit->indexName(); 490 | $score = $hit->score(); 491 | $document = $hit->document(); 492 | $highlight = $hit->highlight(); 493 | $innerHits = $hit->innerHits(); 494 | $innerHitsTotal = $hit->innerHitsTotal(); 495 | $raw = $hit->raw(); 496 | 497 | // get an explanation 498 | $explanation = $searchResult->explanation(); 499 | 500 | // every explanation includes a value, a description and details 501 | // it is also possible to get its raw representation 502 | $value = $explanation->value(); 503 | $description = $explanation->description(); 504 | $details = $explanation->details(); 505 | $raw = $explanation->raw(); 506 | } 507 | 508 | // get suggestions 509 | $suggestions = $searchResult->suggestions(); 510 | 511 | // get aggregations 512 | $aggregations = $searchResult->aggregations(); 513 | ``` 514 | 515 | ### Connection 516 | 517 | Switch Elasticsearch connection: 518 | 519 | ```php 520 | $documentManager->connection('my_connection'); 521 | ``` 522 | 523 | ## Point in Time Management 524 | 525 | `\Elastic\Adapter\Search\PointInTimeManager` is used to control points in time. 526 | 527 | ### Open 528 | 529 | Open a point in time: 530 | 531 | ```php 532 | $pointInTimeId = $pointInTimeManager->open('my_index', '1m'); 533 | ``` 534 | ### Close 535 | 536 | Close a point in time: 537 | 538 | ```php 539 | $pointInTimeManager->close($pointInTimeId); 540 | ``` 541 | 542 | ### Connection 543 | 544 | Switch Elasticsearch connection: 545 | 546 | ```php 547 | $pointInTimeManager->connection('my_connection'); 548 | ``` 549 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babenkoivan/elastic-adapter", 3 | "description": "Adapter for official PHP Elasticsearch client", 4 | "keywords": [ 5 | "elastic", 6 | "elasticsearch", 7 | "adapter", 8 | "client", 9 | "php" 10 | ], 11 | "type": "library", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Ivan Babenko", 16 | "email": "babenko.i.a@gmail.com" 17 | } 18 | ], 19 | "funding": [ 20 | { 21 | "type": "ko-fi", 22 | "url": "https://ko-fi.com/ivanbabenko" 23 | }, 24 | { 25 | "type": "paypal", 26 | "url": "https://paypal.me/babenkoi" 27 | } 28 | ], 29 | "autoload": { 30 | "psr-4": { 31 | "Elastic\\Adapter\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Elastic\\Adapter\\Tests\\": "tests" 37 | } 38 | }, 39 | "require": { 40 | "php": "^8.2", 41 | "babenkoivan/elastic-client": "^3.0" 42 | }, 43 | "require-dev": { 44 | "phpunit/phpunit": "^11.0", 45 | "friendsofphp/php-cs-fixer": "^3.14", 46 | "phpstan/phpstan": "^1.10", 47 | "orchestra/testbench": "^9.0", 48 | "dg/bypass-finals": "^1.7" 49 | }, 50 | "config": { 51 | "bin-dir": "bin", 52 | "allow-plugins": { 53 | "php-http/discovery": true 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | ignoreErrors: 6 | - identifier: missingType.iterableValue 7 | - identifier: missingType.generics 8 | - '#Unable to resolve the template type (TKey|TValue) in call to function collect#' 9 | - '#Parameter .+? of method Illuminate\\Support\\Collection<.+?>::.+?\(\) expects .+? given#' 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | tests/Unit 17 | 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | clientBuilder = $clientBuilder; 16 | $this->client = $clientBuilder->default()->setAsync(false); 17 | } 18 | 19 | public function connection(string $name): self 20 | { 21 | $self = clone $this; 22 | $self->client = $self->clientBuilder->connection($name)->setAsync(false); 23 | return $self; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Documents/Document.php: -------------------------------------------------------------------------------- 1 | id = $id; 16 | $this->content = $content; 17 | } 18 | 19 | public function id(): string 20 | { 21 | return $this->id; 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public function content(?string $key = null) 28 | { 29 | return Arr::get($this->content, $key); 30 | } 31 | 32 | public function toArray(): array 33 | { 34 | return [ 35 | 'id' => $this->id, 36 | 'content' => $this->content, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Documents/DocumentManager.php: -------------------------------------------------------------------------------- 1 | $indexName, 27 | 'refresh' => $refresh ? 'true' : 'false', 28 | 'body' => [], 29 | ]; 30 | 31 | foreach ($documents as $document) { 32 | $index = ['_id' => $document->id()]; 33 | 34 | if ($routing && $routing->has($document->id())) { 35 | $index['routing'] = $routing->get($document->id()); 36 | } 37 | 38 | $params['body'][] = compact('index'); 39 | $params['body'][] = $document->content(); 40 | } 41 | 42 | /** @var Elasticsearch $response */ 43 | $response = $this->client->bulk($params); 44 | $rawResult = $response->asArray(); 45 | 46 | if ($rawResult['errors'] ?? false) { 47 | throw new BulkOperationException($rawResult); 48 | } 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @param string[] $documentIds 55 | */ 56 | public function delete( 57 | string $indexName, 58 | array $documentIds, 59 | bool $refresh = false, 60 | ?Routing $routing = null 61 | ): self { 62 | $params = [ 63 | 'index' => $indexName, 64 | 'refresh' => $refresh ? 'true' : 'false', 65 | 'body' => [], 66 | ]; 67 | 68 | foreach ($documentIds as $documentId) { 69 | $delete = ['_id' => $documentId]; 70 | 71 | if ($routing && $routing->has($documentId)) { 72 | $delete['routing'] = $routing->get($documentId); 73 | } 74 | 75 | $params['body'][] = compact('delete'); 76 | } 77 | 78 | /** @var Elasticsearch $response */ 79 | $response = $this->client->bulk($params); 80 | $rawResult = $response->asArray(); 81 | 82 | if ($rawResult['errors'] ?? false) { 83 | throw new BulkOperationException($rawResult); 84 | } 85 | 86 | return $this; 87 | } 88 | 89 | public function deleteByQuery(string $indexName, array $query, bool $refresh = false): self 90 | { 91 | $params = [ 92 | 'index' => $indexName, 93 | 'refresh' => $refresh ? 'true' : 'false', 94 | 'body' => compact('query'), 95 | ]; 96 | 97 | $this->client->deleteByQuery($params); 98 | 99 | return $this; 100 | } 101 | 102 | public function search(SearchParameters $searchParameters): SearchResult 103 | { 104 | $params = $searchParameters->toArray(); 105 | 106 | /** @var Elasticsearch $response */ 107 | $response = $this->client->search($params); 108 | $rawResult = $response->asArray(); 109 | 110 | return new SearchResult($rawResult); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Documents/Routing.php: -------------------------------------------------------------------------------- 1 | routing[$documentId] = $value; 12 | return $this; 13 | } 14 | 15 | public function has(string $documentId): bool 16 | { 17 | return isset($this->routing[$documentId]); 18 | } 19 | 20 | public function get(string $documentId): ?string 21 | { 22 | return $this->routing[$documentId] ?? null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exceptions/BulkOperationException.php: -------------------------------------------------------------------------------- 1 | rawResult = $rawResult; 14 | 15 | parent::__construct($this->makeErrorMessage()); 16 | } 17 | 18 | public function context(): array 19 | { 20 | return [ 21 | 'rawResult' => $this->rawResult, 22 | ]; 23 | } 24 | 25 | public function rawResult(): array 26 | { 27 | return $this->rawResult; 28 | } 29 | 30 | private function makeErrorMessage(): string 31 | { 32 | $items = $this->rawResult['items'] ?? []; 33 | $count = count($items); 34 | 35 | $reason = sprintf( 36 | '%s did not complete successfully.', 37 | $count > 0 ? $count . ' bulk operation(s)' : 'One or more' 38 | ); 39 | 40 | $failedOperations = $items[0] ?? []; 41 | $firstOperation = reset($failedOperations); 42 | $firstError = ($firstOperation ?? [])['error'] ?? null; 43 | 44 | if (isset($firstError['type'], $firstError['reason'])) { 45 | $reason .= sprintf( 46 | ' %s: %s. Reason: %s.', 47 | $count > 1 ? 'First error' : 'Error', 48 | $firstError['type'], 49 | $firstError['reason'] 50 | ); 51 | } 52 | 53 | return sprintf( 54 | '%s Catch the exception and use the %s::rawResult() method to get more details.', 55 | $reason, 56 | self::class 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exceptions/RawResultReadOnlyException.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | $this->isWriteIndex = $isWriteIndex; 20 | $this->filter = $filter; 21 | $this->routing = $routing; 22 | } 23 | 24 | public function name(): string 25 | { 26 | return $this->name; 27 | } 28 | 29 | public function isWriteIndex(): bool 30 | { 31 | return $this->isWriteIndex; 32 | } 33 | 34 | public function filter(): ?array 35 | { 36 | return $this->filter; 37 | } 38 | 39 | public function routing(): ?string 40 | { 41 | return $this->routing; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Indices/Index.php: -------------------------------------------------------------------------------- 1 | name = $name; 14 | $this->mapping = $mapping; 15 | $this->settings = $settings; 16 | } 17 | 18 | public function name(): string 19 | { 20 | return $this->name; 21 | } 22 | 23 | public function mapping(): ?Mapping 24 | { 25 | return $this->mapping; 26 | } 27 | 28 | public function settings(): ?Settings 29 | { 30 | return $this->settings; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Indices/IndexManager.php: -------------------------------------------------------------------------------- 1 | client->indices()->open([ 16 | 'index' => $indexName, 17 | ]); 18 | 19 | return $this; 20 | } 21 | 22 | public function close(string $indexName): self 23 | { 24 | $this->client->indices()->close([ 25 | 'index' => $indexName, 26 | ]); 27 | 28 | return $this; 29 | } 30 | 31 | public function exists(string $indexName): bool 32 | { 33 | /** @var Elasticsearch $response */ 34 | $response = $this->client->indices()->exists([ 35 | 'index' => $indexName, 36 | ]); 37 | 38 | return $response->asBool(); 39 | } 40 | 41 | public function create(Index $index): self 42 | { 43 | $params = [ 44 | 'index' => $index->name(), 45 | ]; 46 | 47 | $mapping = $index->mapping() === null ? [] : $index->mapping()->toArray(); 48 | $settings = $index->settings() === null ? [] : $index->settings()->toArray(); 49 | 50 | if (!empty($mapping)) { 51 | $params['body']['mappings'] = $mapping; 52 | } 53 | 54 | if (!empty($settings)) { 55 | $params['body']['settings'] = $settings; 56 | } 57 | 58 | $this->client->indices()->create($params); 59 | 60 | return $this; 61 | } 62 | 63 | public function createRaw(string $indexName, ?array $mapping = null, ?array $settings = null): self 64 | { 65 | $params = [ 66 | 'index' => $indexName, 67 | ]; 68 | 69 | if (isset($mapping)) { 70 | $params['body']['mappings'] = $mapping; 71 | } 72 | 73 | if (isset($settings)) { 74 | $params['body']['settings'] = $settings; 75 | } 76 | 77 | $this->client->indices()->create($params); 78 | 79 | return $this; 80 | } 81 | 82 | public function putMapping(string $indexName, Mapping $mapping): self 83 | { 84 | $this->client->indices()->putMapping([ 85 | 'index' => $indexName, 86 | 'body' => $mapping->toArray(), 87 | ]); 88 | 89 | return $this; 90 | } 91 | 92 | public function putMappingRaw(string $indexName, array $mapping): self 93 | { 94 | $this->client->indices()->putMapping([ 95 | 'index' => $indexName, 96 | 'body' => $mapping, 97 | ]); 98 | 99 | return $this; 100 | } 101 | 102 | public function putSettings(string $indexName, Settings $settings): self 103 | { 104 | $this->client->indices()->putSettings([ 105 | 'index' => $indexName, 106 | 'body' => [ 107 | 'settings' => $settings->toArray(), 108 | ], 109 | ]); 110 | 111 | return $this; 112 | } 113 | 114 | public function putSettingsRaw(string $indexName, array $settings): self 115 | { 116 | $this->client->indices()->putSettings([ 117 | 'index' => $indexName, 118 | 'body' => [ 119 | 'settings' => $settings, 120 | ], 121 | ]); 122 | 123 | return $this; 124 | } 125 | 126 | public function drop(string $indexName): self 127 | { 128 | $this->client->indices()->delete([ 129 | 'index' => $indexName, 130 | ]); 131 | 132 | return $this; 133 | } 134 | 135 | public function putAlias(string $indexName, Alias $alias): self 136 | { 137 | $params = [ 138 | 'index' => $indexName, 139 | 'name' => $alias->name(), 140 | ]; 141 | 142 | if ($alias->isWriteIndex()) { 143 | $params['body']['is_write_index'] = $alias->isWriteIndex(); 144 | } 145 | 146 | if ($alias->routing()) { 147 | $params['body']['routing'] = $alias->routing(); 148 | } 149 | 150 | if ($alias->filter()) { 151 | $params['body']['filter'] = $alias->filter(); 152 | } 153 | 154 | $this->client->indices()->putAlias($params); 155 | 156 | return $this; 157 | } 158 | 159 | public function putAliasRaw(string $indexName, string $aliasName, ?array $settings = null): self 160 | { 161 | $params = [ 162 | 'index' => $indexName, 163 | 'name' => $aliasName, 164 | ]; 165 | 166 | if (isset($settings)) { 167 | $params['body'] = $settings; 168 | } 169 | 170 | $this->client->indices()->putAlias($params); 171 | 172 | return $this; 173 | } 174 | 175 | public function deleteAlias(string $indexName, string $aliasName): self 176 | { 177 | $this->client->indices()->deleteAlias([ 178 | 'index' => $indexName, 179 | 'name' => $aliasName, 180 | ]); 181 | 182 | return $this; 183 | } 184 | 185 | public function getAliases(string $indexName): Collection 186 | { 187 | /** @var Elasticsearch $response */ 188 | $response = $this->client->indices()->getAlias([ 189 | 'index' => $indexName, 190 | ]); 191 | 192 | $rawResult = $response->asArray(); 193 | 194 | return collect(array_keys($rawResult[$indexName]['aliases'] ?? [])); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Indices/Mapping.php: -------------------------------------------------------------------------------- 1 | properties = new MappingProperties(); 65 | } 66 | 67 | public function enableFieldNames(): self 68 | { 69 | $this->isFieldNamesEnabled = true; 70 | 71 | return $this; 72 | } 73 | 74 | public function disableFieldNames(): self 75 | { 76 | $this->isFieldNamesEnabled = false; 77 | 78 | return $this; 79 | } 80 | 81 | public function enableSource(): self 82 | { 83 | $this->isSourceEnabled = true; 84 | 85 | return $this; 86 | } 87 | 88 | public function disableSource(): self 89 | { 90 | $this->isSourceEnabled = false; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @param string|bool $dynamic 97 | */ 98 | public function dynamic($dynamic): self 99 | { 100 | $this->dynamic = $dynamic; 101 | 102 | return $this; 103 | } 104 | 105 | public function dynamicTemplate(string $name, array $parameters): self 106 | { 107 | $this->dynamicTemplates[] = [$name => $parameters]; 108 | return $this; 109 | } 110 | 111 | public function __call(string $method, array $parameters): self 112 | { 113 | $this->forwardCallTo($this->properties, $method, $parameters); 114 | return $this; 115 | } 116 | 117 | public function toArray(): array 118 | { 119 | $mapping = []; 120 | $properties = $this->properties->toArray(); 121 | 122 | if (isset($this->isFieldNamesEnabled)) { 123 | $mapping['_field_names'] = [ 124 | 'enabled' => $this->isFieldNamesEnabled, 125 | ]; 126 | } 127 | 128 | if (isset($this->isSourceEnabled)) { 129 | $mapping['_source'] = [ 130 | 'enabled' => $this->isSourceEnabled, 131 | ]; 132 | } 133 | 134 | if (isset($this->dynamic)) { 135 | $mapping['dynamic'] = $this->dynamic; 136 | } 137 | 138 | if (!empty($properties)) { 139 | $mapping['properties'] = $properties; 140 | } 141 | 142 | if (!empty($this->dynamicTemplates)) { 143 | $mapping['dynamic_templates'] = $this->dynamicTemplates; 144 | } 145 | 146 | return $mapping; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Indices/MappingProperties.php: -------------------------------------------------------------------------------- 1 | properties[$name] = ['type' => 'object']; 61 | 62 | if (isset($parameters)) { 63 | $this->properties[$name] += $this->normalizeParametersWithProperties($parameters); 64 | } 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @param Closure|array $parameters 71 | */ 72 | public function nested(string $name, $parameters = null): self 73 | { 74 | $this->properties[$name] = ['type' => 'nested']; 75 | 76 | if (isset($parameters)) { 77 | $this->properties[$name] += $this->normalizeParametersWithProperties($parameters); 78 | } 79 | 80 | return $this; 81 | } 82 | 83 | public function __call(string $method, array $arguments): self 84 | { 85 | $argumentsCount = count($arguments); 86 | 87 | if ($argumentsCount === 0 || $argumentsCount > 2) { 88 | throw new BadMethodCallException(sprintf('Invalid number of arguments for %s method', $method)); 89 | } 90 | 91 | $property = ['type' => Str::snake($method)]; 92 | 93 | if (isset($arguments[1])) { 94 | $property += $arguments[1]; 95 | } 96 | 97 | $this->properties[$arguments[0]] = $property; 98 | 99 | return $this; 100 | } 101 | 102 | public function toArray(): array 103 | { 104 | return $this->properties; 105 | } 106 | 107 | /** 108 | * @param Closure|array $parameters 109 | */ 110 | private function normalizeParametersWithProperties($parameters): array 111 | { 112 | if ($parameters instanceof Closure) { 113 | $parameters = $parameters(new self()); 114 | } 115 | 116 | if ($parameters['properties'] instanceof self) { 117 | $parameters['properties'] = $parameters['properties']->toArray(); 118 | } 119 | 120 | return $parameters; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Indices/Settings.php: -------------------------------------------------------------------------------- 1 | 1) { 22 | throw new BadMethodCallException(sprintf('Invalid number of arguments for %s method', $method)); 23 | } 24 | 25 | $this->settings[Str::snake($method)] = $arguments[0]; 26 | 27 | return $this; 28 | } 29 | 30 | public function toArray(): array 31 | { 32 | return $this->settings; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Search/Aggregation.php: -------------------------------------------------------------------------------- 1 | rawResult['buckets'] ?? [])->mapInto(Bucket::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Search/Bucket.php: -------------------------------------------------------------------------------- 1 | rawResult['doc_count'] ?? 0; 14 | } 15 | 16 | /** 17 | * @return mixed 18 | */ 19 | public function key() 20 | { 21 | return $this->rawResult['key']; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Search/Explanation.php: -------------------------------------------------------------------------------- 1 | rawResult['value']; 15 | } 16 | 17 | public function description(): string 18 | { 19 | return $this->rawResult['description']; 20 | } 21 | 22 | public function details(): Collection 23 | { 24 | return collect($this->rawResult['details'] ?? [])->mapInto(self::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Search/Highlight.php: -------------------------------------------------------------------------------- 1 | rawResult[$field] ?? []); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Search/Hit.php: -------------------------------------------------------------------------------- 1 | rawResult['_index']; 16 | } 17 | 18 | public function score(): ?float 19 | { 20 | return $this->rawResult['_score'] ?? null; 21 | } 22 | 23 | public function sort(): ?array 24 | { 25 | return $this->rawResult['sort'] ?? null; 26 | } 27 | 28 | public function document(): Document 29 | { 30 | return new Document($this->rawResult['_id'], $this->rawResult['_source'] ?? []); 31 | } 32 | 33 | public function highlight(): ?Highlight 34 | { 35 | return isset($this->rawResult['highlight']) ? new Highlight($this->rawResult['highlight']) : null; 36 | } 37 | 38 | public function innerHits(): Collection 39 | { 40 | return collect($this->rawResult['inner_hits'] ?? [])->map( 41 | static fn (array $rawHits) => collect($rawHits['hits']['hits'])->mapInto(self::class) 42 | ); 43 | } 44 | 45 | public function innerHitsTotal(): Collection 46 | { 47 | return collect($this->rawResult['inner_hits'] ?? [])->map( 48 | static fn (array $rawHits) => $rawHits['hits']['total']['value'] ?? null 49 | ); 50 | } 51 | 52 | public function explanation(): ?Explanation 53 | { 54 | return isset($this->rawResult['_explanation']) ? new Explanation($this->rawResult['_explanation']) : null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Search/PointInTimeManager.php: -------------------------------------------------------------------------------- 1 | $indexName]; 15 | 16 | if (isset($keepAlive)) { 17 | $params['keep_alive'] = $keepAlive; 18 | } 19 | 20 | /** @var Elasticsearch $response */ 21 | $response = $this->client->openPointInTime($params); 22 | $rawResult = $response->asArray(); 23 | 24 | return $rawResult['id']; 25 | } 26 | 27 | public function close(string $pointInTimeId): self 28 | { 29 | $this->client->closePointInTime([ 30 | 'body' => [ 31 | 'id' => $pointInTimeId, 32 | ], 33 | ]); 34 | 35 | return $this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Search/RawResult.php: -------------------------------------------------------------------------------- 1 | rawResult = $rawResult; 15 | } 16 | 17 | /** 18 | * @param mixed $offset 19 | * 20 | * @return bool 21 | */ 22 | #[ReturnTypeWillChange] 23 | public function offsetExists($offset) 24 | { 25 | return isset($this->rawResult[$offset]); 26 | } 27 | 28 | /** 29 | * @param mixed $offset 30 | * 31 | * @return mixed 32 | */ 33 | #[ReturnTypeWillChange] 34 | public function offsetGet($offset) 35 | { 36 | return $this->rawResult[$offset] ?? null; 37 | } 38 | 39 | /** 40 | * @param mixed $offset 41 | * @param mixed $value 42 | * 43 | * @return void 44 | */ 45 | #[ReturnTypeWillChange] 46 | public function offsetSet($offset, $value) 47 | { 48 | throw new RawResultReadOnlyException(); 49 | } 50 | 51 | /** 52 | * @param mixed $offset 53 | * 54 | * @return void 55 | */ 56 | #[ReturnTypeWillChange] 57 | public function offsetUnset($offset) 58 | { 59 | throw new RawResultReadOnlyException(); 60 | } 61 | 62 | public function raw(): array 63 | { 64 | return $this->rawResult; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Search/SearchParameters.php: -------------------------------------------------------------------------------- 1 | params['index'] = implode(',', $indexNames); 14 | return $this; 15 | } 16 | 17 | public function query(array $query): self 18 | { 19 | $this->params['body']['query'] = $query; 20 | return $this; 21 | } 22 | 23 | public function highlight(array $highlight): self 24 | { 25 | $this->params['body']['highlight'] = $highlight; 26 | return $this; 27 | } 28 | 29 | public function sort(array $sort): self 30 | { 31 | $this->params['body']['sort'] = $sort; 32 | return $this; 33 | } 34 | 35 | public function rescore(array $rescore): self 36 | { 37 | $this->params['body']['rescore'] = $rescore; 38 | return $this; 39 | } 40 | 41 | public function from(int $from): self 42 | { 43 | $this->params['body']['from'] = $from; 44 | return $this; 45 | } 46 | 47 | public function size(int $size): self 48 | { 49 | $this->params['body']['size'] = $size; 50 | return $this; 51 | } 52 | 53 | public function suggest(array $suggest): self 54 | { 55 | $this->params['body']['suggest'] = $suggest; 56 | return $this; 57 | } 58 | 59 | /** 60 | * @param bool|string|array $source 61 | */ 62 | public function source($source): self 63 | { 64 | $this->params['body']['_source'] = $source; 65 | return $this; 66 | } 67 | 68 | public function collapse(array $collapse): self 69 | { 70 | $this->params['body']['collapse'] = $collapse; 71 | return $this; 72 | } 73 | 74 | public function aggregations(array $aggregations): self 75 | { 76 | $this->params['body']['aggregations'] = $aggregations; 77 | return $this; 78 | } 79 | 80 | public function postFilter(array $postFilter): self 81 | { 82 | $this->params['body']['post_filter'] = $postFilter; 83 | return $this; 84 | } 85 | 86 | /** 87 | * @param int|bool $trackTotalHits 88 | */ 89 | public function trackTotalHits($trackTotalHits): self 90 | { 91 | $this->params['body']['track_total_hits'] = $trackTotalHits; 92 | return $this; 93 | } 94 | 95 | public function indicesBoost(array $indicesBoost): self 96 | { 97 | $this->params['body']['indices_boost'] = $indicesBoost; 98 | return $this; 99 | } 100 | 101 | public function trackScores(bool $trackScores): self 102 | { 103 | $this->params['body']['track_scores'] = $trackScores; 104 | return $this; 105 | } 106 | 107 | public function minScore(float $minScore): self 108 | { 109 | $this->params['body']['min_score'] = $minScore; 110 | return $this; 111 | } 112 | 113 | public function scriptFields(array $scriptFields): self 114 | { 115 | $this->params['body']['script_fields'] = $scriptFields; 116 | return $this; 117 | } 118 | 119 | public function searchType(string $searchType): self 120 | { 121 | $this->params['search_type'] = $searchType; 122 | return $this; 123 | } 124 | 125 | public function preference(string $preference): self 126 | { 127 | $this->params['preference'] = $preference; 128 | return $this; 129 | } 130 | 131 | public function pointInTime(array $pointInTime): self 132 | { 133 | $this->params['body']['pit'] = $pointInTime; 134 | return $this; 135 | } 136 | 137 | public function searchAfter(array $searchAfter): self 138 | { 139 | $this->params['body']['search_after'] = $searchAfter; 140 | return $this; 141 | } 142 | 143 | public function routing(array $routing): self 144 | { 145 | $this->params['routing'] = implode(',', $routing); 146 | return $this; 147 | } 148 | 149 | public function explain(bool $explain): self 150 | { 151 | $this->params['body']['explain'] = $explain; 152 | return $this; 153 | } 154 | 155 | public function terminateAfter(int $terminateAfter): self 156 | { 157 | $this->params['terminate_after'] = $terminateAfter; 158 | return $this; 159 | } 160 | 161 | public function requestCache(bool $requestCache): self 162 | { 163 | $this->params['request_cache'] = $requestCache; 164 | return $this; 165 | } 166 | 167 | public function toArray(): array 168 | { 169 | return $this->params; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Search/SearchResult.php: -------------------------------------------------------------------------------- 1 | rawResult['hits']['hits'])->mapInto(Hit::class); 18 | } 19 | 20 | public function total(): ?int 21 | { 22 | return $this->rawResult['hits']['total']['value'] ?? null; 23 | } 24 | 25 | public function suggestions(): Collection 26 | { 27 | return collect($this->rawResult['suggest'] ?? [])->map( 28 | static fn (array $rawSuggestions) => collect($rawSuggestions)->mapInto(Suggestion::class) 29 | ); 30 | } 31 | 32 | /** 33 | * @return Collection|Aggregation[] 34 | */ 35 | public function aggregations(): Collection 36 | { 37 | return collect($this->rawResult['aggregations'] ?? [])->mapInto(Aggregation::class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Search/Suggestion.php: -------------------------------------------------------------------------------- 1 | rawResult['text']; 15 | } 16 | 17 | public function offset(): int 18 | { 19 | return $this->rawResult['offset']; 20 | } 21 | 22 | public function length(): int 23 | { 24 | return $this->rawResult['length']; 25 | } 26 | 27 | public function options(): Collection 28 | { 29 | return collect($this->rawResult['options']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Extensions/BypassFinals.php: -------------------------------------------------------------------------------- 1 | client = $this->createMock(Client::class); 42 | $this->client->method('setAsync')->willReturnSelf(); 43 | 44 | $clientBuilder = $this->createMock(ClientBuilderInterface::class); 45 | $clientBuilder->method('default')->willReturn($this->client); 46 | 47 | $this->documentManager = new DocumentManager($clientBuilder); 48 | } 49 | 50 | public function test_documents_can_be_indexed_with_refresh(): void 51 | { 52 | $this->client 53 | ->expects($this->once()) 54 | ->method('bulk') 55 | ->with([ 56 | 'index' => 'test', 57 | 'refresh' => 'true', 58 | 'body' => [ 59 | ['index' => ['_id' => '1']], 60 | ['title' => 'Doc 1'], 61 | ['index' => ['_id' => '2']], 62 | ['title' => 'Doc 2'], 63 | ], 64 | ]) 65 | ->willReturn( 66 | $this->createMock(Elasticsearch::class) 67 | ); 68 | 69 | $documents = collect([ 70 | new Document('1', ['title' => 'Doc 1']), 71 | new Document('2', ['title' => 'Doc 2']), 72 | ]); 73 | 74 | $this->assertSame($this->documentManager, $this->documentManager->index('test', $documents, true)); 75 | } 76 | 77 | public function test_documents_can_be_indexed_without_refresh(): void 78 | { 79 | $this->client 80 | ->expects($this->once()) 81 | ->method('bulk') 82 | ->with([ 83 | 'index' => 'test', 84 | 'refresh' => 'false', 85 | 'body' => [ 86 | ['index' => ['_id' => '1']], 87 | ['title' => 'Doc 1'], 88 | ], 89 | ]) 90 | ->willReturn( 91 | $this->createMock(Elasticsearch::class) 92 | ); 93 | 94 | $documents = collect([ 95 | new Document('1', ['title' => 'Doc 1']), 96 | ]); 97 | 98 | $this->assertSame($this->documentManager, $this->documentManager->index('test', $documents)); 99 | } 100 | 101 | public function test_documents_can_be_indexed_with_custom_routing(): void 102 | { 103 | $this->client 104 | ->expects($this->once()) 105 | ->method('bulk') 106 | ->with([ 107 | 'index' => 'test', 108 | 'refresh' => 'true', 109 | 'body' => [ 110 | ['index' => ['_id' => '1', 'routing' => 'Doc1']], 111 | ['title' => 'Doc 1'], 112 | ['index' => ['_id' => '2', 'routing' => 'Doc2']], 113 | ['title' => 'Doc 2'], 114 | ], 115 | ]) 116 | ->willReturn( 117 | $this->createMock(Elasticsearch::class) 118 | ); 119 | 120 | $documents = collect([ 121 | new Document('1', ['title' => 'Doc 1']), 122 | new Document('2', ['title' => 'Doc 2']), 123 | ]); 124 | 125 | $routing = (new Routing()) 126 | ->add('1', 'Doc1') 127 | ->add('2', 'Doc2'); 128 | 129 | $this->assertSame($this->documentManager, $this->documentManager->index('test', $documents, true, $routing)); 130 | } 131 | 132 | public function test_documents_can_be_deleted_with_refresh(): void 133 | { 134 | $this->client 135 | ->expects($this->once()) 136 | ->method('bulk') 137 | ->with([ 138 | 'index' => 'test', 139 | 'refresh' => 'true', 140 | 'body' => [ 141 | ['delete' => ['_id' => '1']], 142 | ['delete' => ['_id' => '2']], 143 | ], 144 | ]) 145 | ->willReturn( 146 | $this->createMock(Elasticsearch::class) 147 | ); 148 | 149 | $documentIds = ['1', '2']; 150 | 151 | $this->assertSame($this->documentManager, $this->documentManager->delete('test', $documentIds, true)); 152 | } 153 | 154 | public function test_documents_can_be_deleted_without_refresh(): void 155 | { 156 | $this->client 157 | ->expects($this->once()) 158 | ->method('bulk') 159 | ->with([ 160 | 'index' => 'test', 161 | 'refresh' => 'false', 162 | 'body' => [ 163 | ['delete' => ['_id' => '1']], 164 | ], 165 | ]) 166 | ->willReturn( 167 | $this->createMock(Elasticsearch::class) 168 | ); 169 | 170 | $documentIds = ['1']; 171 | 172 | $this->assertSame($this->documentManager, $this->documentManager->delete('test', $documentIds, false)); 173 | } 174 | 175 | public function test_documents_can_be_deleted_with_custom_routing(): void 176 | { 177 | $this->client 178 | ->expects($this->once()) 179 | ->method('bulk') 180 | ->with([ 181 | 'index' => 'test', 182 | 'refresh' => 'true', 183 | 'body' => [ 184 | ['delete' => ['_id' => '1', 'routing' => 'Doc1']], 185 | ['delete' => ['_id' => '2', 'routing' => 'Doc2']], 186 | ], 187 | ]) 188 | ->willReturn( 189 | $this->createMock(Elasticsearch::class) 190 | ); 191 | 192 | $documentIds = ['1', '2']; 193 | 194 | $routing = (new Routing()) 195 | ->add('1', 'Doc1') 196 | ->add('2', 'Doc2'); 197 | 198 | $this->assertSame($this->documentManager, $this->documentManager->delete('test', $documentIds, true, $routing)); 199 | } 200 | 201 | public function test_documents_can_be_deleted_by_query_with_refresh(): void 202 | { 203 | $this->client 204 | ->expects($this->once()) 205 | ->method('deleteByQuery') 206 | ->with([ 207 | 'index' => 'test', 208 | 'refresh' => 'true', 209 | 'body' => [ 210 | 'query' => ['match_all' => new stdClass()], 211 | ], 212 | ]); 213 | 214 | $query = ['match_all' => new stdClass()]; 215 | 216 | $this->assertSame($this->documentManager, $this->documentManager->deleteByQuery('test', $query, true)); 217 | } 218 | 219 | public function test_documents_can_be_deleted_by_query_without_refresh(): void 220 | { 221 | $this->client 222 | ->expects($this->once()) 223 | ->method('deleteByQuery') 224 | ->with([ 225 | 'index' => 'test', 226 | 'refresh' => 'false', 227 | 'body' => [ 228 | 'query' => ['match_all' => new stdClass()], 229 | ], 230 | ]); 231 | 232 | $query = ['match_all' => new stdClass()]; 233 | 234 | $this->assertSame($this->documentManager, $this->documentManager->deleteByQuery('test', $query)); 235 | } 236 | 237 | public function test_documents_can_be_found(): void 238 | { 239 | $response = $this->createMock(Elasticsearch::class); 240 | 241 | $response 242 | ->expects($this->once()) 243 | ->method('asArray') 244 | ->willReturn([ 245 | 'hits' => [ 246 | 'total' => [ 247 | 'value' => 1, 248 | 'relation' => 'eq', 249 | ], 250 | 'max_score' => 1.601195, 251 | 'hits' => [ 252 | [ 253 | '_index' => 'test', 254 | '_id' => '1', 255 | '_score' => 1.601195, 256 | '_source' => ['content' => 'foo'], 257 | ], 258 | ], 259 | ], 260 | ]); 261 | 262 | $this->client 263 | ->expects($this->once()) 264 | ->method('search') 265 | ->with([ 266 | 'index' => 'test', 267 | 'body' => [ 268 | 'query' => [ 269 | 'match' => ['content' => 'foo'], 270 | ], 271 | ], 272 | ]) 273 | ->willReturn($response); 274 | 275 | $searchParameters = (new SearchParameters()) 276 | ->indices(['test']) 277 | ->query([ 278 | 'match' => [ 279 | 'content' => 'foo', 280 | ], 281 | ]); 282 | 283 | $searchResult = $this->documentManager->search($searchParameters); 284 | $this->assertSame(1, $searchResult->total()); 285 | 286 | /** @var Hit $firstHit */ 287 | $firstHit = $searchResult->hits()[0]; 288 | $this->assertEquals(new Document('1', ['content' => 'foo']), $firstHit->document()); 289 | } 290 | 291 | public function test_exception_is_thrown_when_index_operation_was_unsuccessful(): void 292 | { 293 | $response = $this->createMock(Elasticsearch::class); 294 | 295 | $response 296 | ->expects($this->once()) 297 | ->method('asArray') 298 | ->willReturn([ 299 | 'took' => 0, 300 | 'errors' => true, 301 | 'items' => [], 302 | ]); 303 | 304 | $this->client 305 | ->expects($this->once()) 306 | ->method('bulk') 307 | ->with([ 308 | 'index' => 'test', 309 | 'refresh' => 'false', 310 | 'body' => [ 311 | ['index' => ['_id' => '1']], 312 | ['title' => 'Doc 1'], 313 | ], 314 | ]) 315 | ->willReturn($response); 316 | 317 | $this->expectException(BulkOperationException::class); 318 | 319 | $documents = collect([ 320 | new Document('1', ['title' => 'Doc 1']), 321 | ]); 322 | 323 | $this->documentManager->index('test', $documents); 324 | } 325 | 326 | /** 327 | * @noinspection ClassMockingCorrectnessInspection 328 | * @noinspection PhpUnitInvalidMockingEntityInspection 329 | */ 330 | public function test_connection_can_be_changed(): void 331 | { 332 | $defaultClient = $this->createMock(Client::class); 333 | $defaultClient->method('setAsync')->willReturnSelf(); 334 | 335 | $defaultClient 336 | ->expects($this->never()) 337 | ->method('bulk'); 338 | 339 | $testClient = $this->createMock(Client::class); 340 | $testClient->method('setAsync')->willReturnSelf(); 341 | 342 | $testClient 343 | ->expects($this->once()) 344 | ->method('bulk') 345 | ->with([ 346 | 'index' => 'docs', 347 | 'refresh' => 'false', 348 | 'body' => [ 349 | ['index' => ['_id' => '1']], 350 | ['title' => 'Doc 1'], 351 | ], 352 | ]) 353 | ->willReturn( 354 | $this->createMock(Elasticsearch::class) 355 | ); 356 | 357 | $clientBuilder = $this->createMock(ClientBuilderInterface::class); 358 | $clientBuilder->method('default')->willReturn($defaultClient); 359 | $clientBuilder->method('connection')->with('test')->willReturn($testClient); 360 | 361 | (new DocumentManager($clientBuilder)) 362 | ->connection('test') 363 | ->index('docs', collect([new Document('1', ['title' => 'Doc 1'])])); 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /tests/Unit/Documents/DocumentTest.php: -------------------------------------------------------------------------------- 1 | 'book', 'price' => 10]); 15 | 16 | $this->assertSame('123456', $document->id()); 17 | $this->assertSame(['title' => 'book', 'price' => 10], $document->content()); 18 | $this->assertSame('book', $document->content('title')); 19 | } 20 | 21 | public function test_array_casting(): void 22 | { 23 | $document = new Document('1', ['title' => 'test']); 24 | 25 | $this->assertSame([ 26 | 'id' => '1', 27 | 'content' => ['title' => 'test'], 28 | ], $document->toArray()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Documents/RoutingTest.php: -------------------------------------------------------------------------------- 1 | add('1', 'user1') 16 | ->add('2', 'user2'); 17 | 18 | $this->assertTrue($routing->has('1')); 19 | $this->assertSame('user1', $routing->get('1')); 20 | $this->assertTrue($routing->has('2')); 21 | $this->assertSame('user2', $routing->get('2')); 22 | $this->assertFalse($routing->has('3')); 23 | $this->assertNull($routing->get('3')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Unit/Exceptions/BulkOperationExceptionTest.php: -------------------------------------------------------------------------------- 1 | 486, 16 | 'errors' => true, 17 | 'items' => [ 18 | [ 19 | 'update' => [ 20 | '_index' => 'index1', 21 | '_type' => '_doc', 22 | '_id' => '5', 23 | 'status' => 404, 24 | 'error' => [ 25 | 'type' => 'document_missing_exception', 26 | 'reason' => '[_doc][5]: document missing', 27 | 'index_uuid' => 'aAsFqTI0Tc2W0LCWgPNrOA', 28 | 'shard' => '0', 29 | 'index' => 'index1', 30 | ], 31 | ], 32 | ], 33 | ], 34 | ]; 35 | 36 | $exception = new BulkOperationException($rawResult); 37 | 38 | $this->assertSame($rawResult, $exception->rawResult()); 39 | } 40 | 41 | public function test_first_error_message_from_result_is_given_in_exception_message(): void 42 | { 43 | $rawResult = [ 44 | 'took' => 486, 45 | 'errors' => true, 46 | 'items' => [ 47 | [ 48 | 'update' => [ 49 | '_index' => 'index1', 50 | '_type' => '_doc', 51 | '_id' => '5', 52 | 'status' => 404, 53 | 'error' => [ 54 | 'type' => 'document_missing_exception', 55 | 'reason' => '[_doc][5]: document missing', 56 | 'index_uuid' => 'aAsFqTI0Tc2W0LCWgPNrOA', 57 | 'shard' => '0', 58 | 'index' => 'index1', 59 | ], 60 | ], 61 | ], 62 | ], 63 | ]; 64 | 65 | $exception = new BulkOperationException($rawResult); 66 | 67 | $this->assertEquals( 68 | '1 bulk operation(s) did not complete successfully. Error: document_missing_exception. Reason: [_doc][5]: document missing. Catch the exception and use the Elastic\Adapter\Exceptions\BulkOperationException::rawResult() method to get more details.', 69 | $exception->getMessage() 70 | ); 71 | } 72 | 73 | public function test_exception_can_be_throw_with_many_errors_in_result(): void 74 | { 75 | $rawResult = [ 76 | 'took' => 486, 77 | 'errors' => true, 78 | 'items' => [ 79 | [ 80 | 'update' => [ 81 | '_index' => 'index1', 82 | '_type' => '_doc', 83 | '_id' => '5', 84 | 'status' => 404, 85 | 'error' => [ 86 | 'type' => 'document_missing_exception', 87 | 'reason' => '[_doc][5]: document missing', 88 | 'index_uuid' => 'aAsFqTI0Tc2W0LCWgPNrOA', 89 | 'shard' => '0', 90 | 'index' => 'index1', 91 | ], 92 | ], 93 | ], 94 | [ 95 | 'index' => [ 96 | '_index' => 'index1', 97 | '_type' => '_doc', 98 | '_id' => '5', 99 | 'status' => 404, 100 | 'error' => [ 101 | 'type' => 'mapper_parsing_exception', 102 | 'reason' => 'failed to parse field', 103 | 'index_uuid' => 'aAsFqTI0Tc2W0LCWgPNrOA', 104 | 'shard' => '0', 105 | 'index' => 'index1', 106 | ], 107 | ], 108 | ], 109 | ], 110 | ]; 111 | 112 | $exception = new BulkOperationException($rawResult); 113 | 114 | $this->assertEquals( 115 | '2 bulk operation(s) did not complete successfully. First error: document_missing_exception. Reason: [_doc][5]: document missing. Catch the exception and use the Elastic\Adapter\Exceptions\BulkOperationException::rawResult() method to get more details.', 116 | $exception->getMessage() 117 | ); 118 | } 119 | 120 | public function test_exception_can_be_throw_with_missing_error_in_result(): void 121 | { 122 | $rawResult = [ 123 | 'took' => 486, 124 | 'errors' => true, 125 | 'items' => [ 126 | [ 127 | 'update' => [ 128 | '_index' => 'index1', 129 | '_type' => '_doc', 130 | '_id' => '5', 131 | 'status' => 404, 132 | ], 133 | ], 134 | ], 135 | ]; 136 | 137 | $exception = new BulkOperationException($rawResult); 138 | 139 | $this->assertEquals( 140 | '1 bulk operation(s) did not complete successfully. Catch the exception and use the Elastic\Adapter\Exceptions\BulkOperationException::rawResult() method to get more details.', 141 | $exception->getMessage() 142 | ); 143 | } 144 | 145 | public function test_exception_can_be_throw_with_missing_items_in_result(): void 146 | { 147 | $rawResult = [ 148 | 'took' => 486, 149 | 'errors' => true, 150 | 'items' => [], 151 | ]; 152 | 153 | $exception = new BulkOperationException($rawResult); 154 | 155 | $this->assertEquals( 156 | 'One or more did not complete successfully. Catch the exception and use the Elastic\Adapter\Exceptions\BulkOperationException::rawResult() method to get more details.', 157 | $exception->getMessage() 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/Unit/Indices/AliasTest.php: -------------------------------------------------------------------------------- 1 | ['year' => 2030]], 'year'); 15 | 16 | $this->assertSame('2030', $alias->name()); 17 | $this->assertTrue($alias->isWriteIndex()); 18 | $this->assertSame(['term' => ['year' => 2030]], $alias->filter()); 19 | $this->assertSame('year', $alias->routing()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Indices/IndexManagerTest.php: -------------------------------------------------------------------------------- 1 | indices = $this->createMock(Indices::class); 40 | 41 | $client = $this->createMock(Client::class); 42 | $client->method('setAsync')->willReturnSelf(); 43 | $client->method('indices')->willReturn($this->indices); 44 | 45 | $clientBuilder = $this->createMock(ClientBuilderInterface::class); 46 | $clientBuilder->method('default')->willReturn($client); 47 | 48 | $this->indexManager = new IndexManager($clientBuilder); 49 | } 50 | 51 | public function test_index_can_be_opened(): void 52 | { 53 | $indexName = 'foo'; 54 | 55 | $this->indices 56 | ->expects($this->once()) 57 | ->method('open') 58 | ->with([ 59 | 'index' => $indexName, 60 | ]); 61 | 62 | $this->assertSame($this->indexManager, $this->indexManager->open($indexName)); 63 | } 64 | 65 | public function test_index_can_be_closed(): void 66 | { 67 | $indexName = 'foo'; 68 | 69 | $this->indices 70 | ->expects($this->once()) 71 | ->method('close') 72 | ->with([ 73 | 'index' => $indexName, 74 | ]); 75 | 76 | $this->assertSame($this->indexManager, $this->indexManager->close($indexName)); 77 | } 78 | 79 | public function test_index_existence_can_be_checked(): void 80 | { 81 | $indexName = 'foo'; 82 | 83 | $response = $this->createMock(Elasticsearch::class); 84 | $response->method('asBool')->willReturn(true); 85 | 86 | $this->indices 87 | ->expects($this->once()) 88 | ->method('exists') 89 | ->with([ 90 | 'index' => $indexName, 91 | ]) 92 | ->willReturn($response); 93 | 94 | $this->assertTrue($this->indexManager->exists($indexName)); 95 | } 96 | 97 | public function test_index_can_be_created_without_mapping_and_settings(): void 98 | { 99 | $index = new Index('foo'); 100 | 101 | $this->indices 102 | ->expects($this->once()) 103 | ->method('create') 104 | ->with([ 105 | 'index' => $index->name(), 106 | ]); 107 | 108 | $this->assertSame($this->indexManager, $this->indexManager->create($index)); 109 | } 110 | 111 | public function test_index_can_be_created_without_mapping(): void 112 | { 113 | $settings = (new Settings())->index(['number_of_replicas' => 2]); 114 | $index = new Index('foo', null, $settings); 115 | 116 | $this->indices 117 | ->expects($this->once()) 118 | ->method('create') 119 | ->with([ 120 | 'index' => $index->name(), 121 | 'body' => [ 122 | 'settings' => [ 123 | 'index' => [ 124 | 'number_of_replicas' => 2, 125 | ], 126 | ], 127 | ], 128 | ]); 129 | 130 | $this->assertSame($this->indexManager, $this->indexManager->create($index)); 131 | } 132 | 133 | public function test_index_can_be_created_without_settings(): void 134 | { 135 | $mapping = (new Mapping())->text('foo'); 136 | $index = new Index('bar', $mapping); 137 | 138 | $this->indices 139 | ->expects($this->once()) 140 | ->method('create') 141 | ->with([ 142 | 'index' => $index->name(), 143 | 'body' => [ 144 | 'mappings' => [ 145 | 'properties' => [ 146 | 'foo' => [ 147 | 'type' => 'text', 148 | ], 149 | ], 150 | ], 151 | ], 152 | ]); 153 | 154 | $this->assertSame($this->indexManager, $this->indexManager->create($index)); 155 | } 156 | 157 | public function test_index_can_be_created_with_empty_settings_and_mapping(): void 158 | { 159 | $index = new Index('foo', new Mapping(), new Settings()); 160 | 161 | $this->indices 162 | ->expects($this->once()) 163 | ->method('create') 164 | ->with([ 165 | 'index' => $index->name(), 166 | ]); 167 | 168 | $this->assertSame($this->indexManager, $this->indexManager->create($index)); 169 | } 170 | 171 | public function test_index_can_be_created_with_raw_mapping_and_settings(): void 172 | { 173 | $indexName = 'foo'; 174 | $mapping = ['properties' => ['bar' => ['type' => 'text']]]; 175 | $settings = ['index' => ['number_of_replicas' => 2]]; 176 | 177 | $this->indices 178 | ->expects($this->once()) 179 | ->method('create') 180 | ->with([ 181 | 'index' => $indexName, 182 | 'body' => [ 183 | 'mappings' => $mapping, 184 | 'settings' => $settings, 185 | ], 186 | ]); 187 | 188 | $this->assertSame($this->indexManager, $this->indexManager->createRaw($indexName, $mapping, $settings)); 189 | } 190 | 191 | public function test_mapping_can_be_updated(): void 192 | { 193 | $indexName = 'foo'; 194 | $mapping = (new Mapping())->text('bar'); 195 | 196 | $this->indices 197 | ->expects($this->once()) 198 | ->method('putMapping') 199 | ->with([ 200 | 'index' => $indexName, 201 | 'body' => [ 202 | 'properties' => [ 203 | 'bar' => [ 204 | 'type' => 'text', 205 | ], 206 | ], 207 | ], 208 | ]); 209 | 210 | $this->assertSame($this->indexManager, $this->indexManager->putMapping($indexName, $mapping)); 211 | } 212 | 213 | public function test_mapping_can_be_updated_with_raw_data(): void 214 | { 215 | $indexName = 'foo'; 216 | $mapping = ['properties' => ['bar' => ['type' => 'text']]]; 217 | 218 | $this->indices 219 | ->expects($this->once()) 220 | ->method('putMapping') 221 | ->with([ 222 | 'index' => $indexName, 223 | 'body' => $mapping, 224 | ]); 225 | 226 | $this->assertSame($this->indexManager, $this->indexManager->putMappingRaw($indexName, $mapping)); 227 | } 228 | 229 | public function test_settings_can_be_updated(): void 230 | { 231 | $indexName = 'foo'; 232 | $settings = (new Settings())->index(['number_of_replicas' => 2]); 233 | 234 | $this->indices 235 | ->expects($this->once()) 236 | ->method('putSettings') 237 | ->with([ 238 | 'index' => $indexName, 239 | 'body' => [ 240 | 'settings' => [ 241 | 'index' => [ 242 | 'number_of_replicas' => 2, 243 | ], 244 | ], 245 | ], 246 | ]); 247 | 248 | $this->assertSame($this->indexManager, $this->indexManager->putSettings($indexName, $settings)); 249 | } 250 | 251 | public function test_settings_can_be_updated_with_raw_data(): void 252 | { 253 | $indexName = 'foo'; 254 | $settings = ['index' => ['number_of_replicas' => 2]]; 255 | 256 | $this->indices 257 | ->expects($this->once()) 258 | ->method('putSettings') 259 | ->with([ 260 | 'index' => $indexName, 261 | 'body' => [ 262 | 'settings' => $settings, 263 | ], 264 | ]); 265 | 266 | $this->assertSame($this->indexManager, $this->indexManager->putSettingsRaw($indexName, $settings)); 267 | } 268 | 269 | public function test_index_can_be_dropped(): void 270 | { 271 | $indexName = 'foo'; 272 | 273 | $this->indices 274 | ->expects($this->once()) 275 | ->method('delete') 276 | ->with([ 277 | 'index' => $indexName, 278 | ]); 279 | 280 | $this->assertSame($this->indexManager, $this->indexManager->drop($indexName)); 281 | } 282 | 283 | public function test_alias_can_be_created(): void 284 | { 285 | $indexName = 'foo'; 286 | $alias = (new Alias('bar', true, ['term' => ['user_id' => 12]], '12')); 287 | 288 | $this->indices 289 | ->expects($this->once()) 290 | ->method('putAlias') 291 | ->with([ 292 | 'index' => $indexName, 293 | 'name' => $alias->name(), 294 | 'body' => [ 295 | 'is_write_index' => true, 296 | 'routing' => '12', 297 | 'filter' => [ 298 | 'term' => [ 299 | 'user_id' => 12, 300 | ], 301 | ], 302 | ], 303 | ]); 304 | 305 | $this->assertSame($this->indexManager, $this->indexManager->putAlias($indexName, $alias)); 306 | } 307 | 308 | public function test_alias_can_be_created_with_raw_data(): void 309 | { 310 | $indexName = 'foo'; 311 | $aliasName = 'bar'; 312 | $settings = ['routing' => '1']; 313 | 314 | $this->indices 315 | ->expects($this->once()) 316 | ->method('putAlias') 317 | ->with([ 318 | 'index' => $indexName, 319 | 'name' => $aliasName, 320 | 'body' => [ 321 | 'routing' => '1', 322 | ], 323 | ]); 324 | 325 | $this->assertSame($this->indexManager, $this->indexManager->putAliasRaw($indexName, $aliasName, $settings)); 326 | } 327 | 328 | public function test_alias_can_be_deleted(): void 329 | { 330 | $indexName = 'foo'; 331 | $aliasName = 'bar'; 332 | 333 | $this->indices 334 | ->expects($this->once()) 335 | ->method('deleteAlias') 336 | ->with([ 337 | 'index' => $indexName, 338 | 'name' => $aliasName, 339 | ]); 340 | 341 | $this->assertSame($this->indexManager, $this->indexManager->deleteAlias($indexName, $aliasName)); 342 | } 343 | 344 | public function test_aliases_can_be_retrieved(): void 345 | { 346 | $indexName = 'foo'; 347 | $aliasName = 'bar'; 348 | 349 | $response = $this->createMock(Elasticsearch::class); 350 | 351 | $response 352 | ->expects($this->once()) 353 | ->method('asArray') 354 | ->willReturn([ 355 | $indexName => [ 356 | 'aliases' => [ 357 | $aliasName => [], 358 | ], 359 | ], 360 | ]); 361 | 362 | $this->indices 363 | ->expects($this->once()) 364 | ->method('getAlias') 365 | ->with([ 366 | 'index' => $indexName, 367 | ]) 368 | ->willReturn($response); 369 | 370 | $this->assertEquals( 371 | collect([$aliasName]), 372 | $this->indexManager->getAliases($indexName) 373 | ); 374 | } 375 | 376 | /** 377 | * @noinspection ClassMockingCorrectnessInspection 378 | * @noinspection PhpUnitInvalidMockingEntityInspection 379 | */ 380 | public function test_connection_can_be_changed(): void 381 | { 382 | $defaultIndices = $this->createMock(Indices::class); 383 | $defaultIndices->expects($this->never())->method('create'); 384 | 385 | $defaultClient = $this->createMock(Client::class); 386 | $defaultClient->method('setAsync')->willReturnSelf(); 387 | $defaultClient->method('indices')->willReturn($defaultIndices); 388 | 389 | $testIndices = $this->createMock(Indices::class); 390 | $testIndices->expects($this->once())->method('open')->with(['index' => 'docs']); 391 | 392 | $testClient = $this->createMock(Client::class); 393 | $testClient->method('setAsync')->willReturnSelf(); 394 | $testClient->method('indices')->willReturn($testIndices); 395 | 396 | $clientBuilder = $this->createMock(ClientBuilderInterface::class); 397 | $clientBuilder->method('default')->willReturn($defaultClient); 398 | $clientBuilder->method('connection')->with('test')->willReturn($testClient); 399 | 400 | (new IndexManager($clientBuilder))->connection('test')->open('docs'); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /tests/Unit/Indices/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertNull($index->settings()); 21 | $this->assertNull($index->mapping()); 22 | } 23 | 24 | public function test_index_getters(): void 25 | { 26 | $mapping = new Mapping(); 27 | $settings = new Settings(); 28 | $index = new Index('foo', $mapping, $settings); 29 | 30 | $this->assertSame('foo', $index->name()); 31 | $this->assertSame($mapping, $index->mapping()); 32 | $this->assertSame($settings, $index->settings()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/Indices/MappingPropertiesTest.php: -------------------------------------------------------------------------------- 1 | 'geoPoint', 21 | 'name' => 'location', 22 | 'parameters' => [ 23 | 'null_value' => null, 24 | ], 25 | 'expected' => [ 26 | 'location' => [ 27 | 'type' => 'geo_point', 28 | 'null_value' => null, 29 | ], 30 | ], 31 | ], 32 | [ 33 | 'type' => 'text', 34 | 'name' => 'description', 35 | 'parameters' => [ 36 | 'boost' => 1, 37 | ], 38 | 'expected' => [ 39 | 'description' => [ 40 | 'type' => 'text', 41 | 'boost' => 1, 42 | ], 43 | ], 44 | ], 45 | [ 46 | 'type' => 'keyword', 47 | 'name' => 'age', 48 | 'parameters' => null, 49 | 'expected' => [ 50 | 'age' => [ 51 | 'type' => 'keyword', 52 | ], 53 | ], 54 | ], 55 | [ 56 | 'type' => 'object', 57 | 'name' => 'user', 58 | 'parameters' => static function (MappingProperties $properties) { 59 | $properties->integer('age'); 60 | 61 | return [ 62 | 'properties' => $properties, 63 | 'dynamic' => true, 64 | ]; 65 | }, 66 | 'expected' => [ 67 | 'user' => [ 68 | 'type' => 'object', 69 | 'properties' => [ 70 | 'age' => [ 71 | 'type' => 'integer', 72 | ], 73 | ], 74 | 'dynamic' => true, 75 | ], 76 | ], 77 | ], 78 | [ 79 | 'type' => 'object', 80 | 'name' => 'user', 81 | 'parameters' => [ 82 | 'properties' => [ 83 | 'age' => [ 84 | 'type' => 'keyword', 85 | ], 86 | ], 87 | ], 88 | 'expected' => [ 89 | 'user' => [ 90 | 'type' => 'object', 91 | 'properties' => [ 92 | 'age' => [ 93 | 'type' => 'keyword', 94 | ], 95 | ], 96 | ], 97 | ], 98 | ], 99 | [ 100 | 'type' => 'object', 101 | 'name' => 'user', 102 | 'parameters' => [ 103 | 'properties' => (new MappingProperties())->keyword('age'), 104 | ], 105 | 'expected' => [ 106 | 'user' => [ 107 | 'type' => 'object', 108 | 'properties' => [ 109 | 'age' => [ 110 | 'type' => 'keyword', 111 | ], 112 | ], 113 | ], 114 | ], 115 | ], 116 | [ 117 | 'type' => 'object', 118 | 'name' => 'user', 119 | 'parameters' => null, 120 | 'expected' => [ 121 | 'user' => [ 122 | 'type' => 'object', 123 | ], 124 | ], 125 | ], 126 | [ 127 | 'type' => 'nested', 128 | 'name' => 'user', 129 | 'parameters' => static function (MappingProperties $properties) { 130 | $properties->keyword('age'); 131 | 132 | return [ 133 | 'properties' => $properties, 134 | 'dynamic' => true, 135 | ]; 136 | }, 137 | 'expected' => [ 138 | 'user' => [ 139 | 'type' => 'nested', 140 | 'properties' => [ 141 | 'age' => [ 142 | 'type' => 'keyword', 143 | ], 144 | ], 145 | 'dynamic' => true, 146 | ], 147 | ], 148 | ], 149 | ]; 150 | } 151 | 152 | /** 153 | * @param Closure|array $parameters 154 | */ 155 | #[DataProvider('parametersProvider')] 156 | #[TestDox('Test $type property setter')] 157 | public function test_property_setter(string $type, string $name, $parameters, array $expected): void 158 | { 159 | $actual = (new MappingProperties())->$type($name, $parameters); 160 | $this->assertEquals($expected, $actual->toArray()); 161 | } 162 | 163 | public function test_exception_is_thrown_when_setter_receives_invalid_number_of_arguments(): void 164 | { 165 | $this->expectException(BadMethodCallException::class); 166 | (new MappingProperties())->text(); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tests/Unit/Indices/MappingTest.php: -------------------------------------------------------------------------------- 1 | disableFieldNames(); 18 | 19 | $this->assertSame([ 20 | '_field_names' => [ 21 | 'enabled' => false, 22 | ], 23 | ], $mapping->toArray()); 24 | } 25 | 26 | public function test_field_names_can_be_enabled(): void 27 | { 28 | $mapping = (new Mapping())->enableFieldNames(); 29 | 30 | $this->assertSame([ 31 | '_field_names' => [ 32 | 'enabled' => true, 33 | ], 34 | ], $mapping->toArray()); 35 | } 36 | 37 | public function test_source_can_be_disabled(): void 38 | { 39 | $mapping = (new Mapping())->disableSource(); 40 | 41 | $this->assertSame([ 42 | '_source' => [ 43 | 'enabled' => false, 44 | ], 45 | ], $mapping->toArray()); 46 | } 47 | 48 | public function test_source_can_be_enabled(): void 49 | { 50 | $mapping = (new Mapping())->enableSource(); 51 | 52 | $this->assertSame([ 53 | '_source' => [ 54 | 'enabled' => true, 55 | ], 56 | ], $mapping->toArray()); 57 | } 58 | 59 | public function test_dynamic_mapping_can_be_configured(): void 60 | { 61 | $mapping = (new Mapping())->dynamic('strict'); 62 | 63 | $this->assertSame([ 64 | 'dynamic' => 'strict', 65 | ], $mapping->toArray()); 66 | } 67 | 68 | public function test_default_array_casting(): void 69 | { 70 | $this->assertSame([], (new Mapping())->toArray()); 71 | } 72 | 73 | public function test_configured_array_casting(): void 74 | { 75 | $mapping = (new Mapping()) 76 | ->disableFieldNames() 77 | ->enableSource() 78 | ->text('foo') 79 | ->boolean('bar', [ 80 | 'boost' => 1, 81 | ]) 82 | ->dynamicTemplate('integers', [ 83 | 'match_mapping_type' => 'long', 84 | 'mapping' => [ 85 | 'type' => 'integer', 86 | ], 87 | ]); 88 | 89 | $this->assertSame([ 90 | '_field_names' => [ 91 | 'enabled' => false, 92 | ], 93 | '_source' => [ 94 | 'enabled' => true, 95 | ], 96 | 'properties' => [ 97 | 'foo' => [ 98 | 'type' => 'text', 99 | ], 100 | 'bar' => [ 101 | 'type' => 'boolean', 102 | 'boost' => 1, 103 | ], 104 | ], 105 | 'dynamic_templates' => [ 106 | [ 107 | 'integers' => [ 108 | 'match_mapping_type' => 'long', 109 | 'mapping' => [ 110 | 'type' => 'integer', 111 | ], 112 | ], 113 | ], 114 | ], 115 | ], $mapping->toArray()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Unit/Indices/SettingsTest.php: -------------------------------------------------------------------------------- 1 | 'index', 20 | 'configuration' => [ 21 | 'number_of_replicas' => 2, 22 | ], 23 | 'expected' => [ 24 | 'index' => [ 25 | 'number_of_replicas' => 2, 26 | ], 27 | ], 28 | ], 29 | [ 30 | 'option' => 'index', 31 | 'configuration' => [ 32 | 'number_of_replicas' => 2, 33 | 'refresh_interval' => -1, 34 | ], 35 | 'expected' => [ 36 | 'index' => [ 37 | 'number_of_replicas' => 2, 38 | 'refresh_interval' => -1, 39 | ], 40 | ], 41 | ], 42 | [ 43 | 'option' => 'analysis', 44 | 'configuration' => [ 45 | 'analyzer' => [ 46 | 'content' => [ 47 | 'type' => 'custom', 48 | 'tokenizer' => 'whitespace', 49 | ], 50 | ], 51 | ], 52 | 'expected' => [ 53 | 'analysis' => [ 54 | 'analyzer' => [ 55 | 'content' => [ 56 | 'type' => 'custom', 57 | 'tokenizer' => 'whitespace', 58 | ], 59 | ], 60 | ], 61 | ], 62 | ], 63 | ]; 64 | } 65 | 66 | #[TestDox('Test $option option setter')] 67 | #[DataProvider('optionsProvider')] 68 | public function test_option_setter(string $option, array $configuration, array $expected): void 69 | { 70 | $actual = (new Settings())->$option($configuration); 71 | $this->assertSame($expected, $actual->toArray()); 72 | } 73 | 74 | public function test_exception_is_thrown_when_setter_receives_invalid_number_of_arguments(): void 75 | { 76 | $this->expectException(BadMethodCallException::class); 77 | (new Settings())->index(); 78 | } 79 | 80 | public function test_default_array_casting(): void 81 | { 82 | $this->assertSame([], (new Settings())->toArray()); 83 | } 84 | 85 | public function test_configured_array_casting(): void 86 | { 87 | $settings = (new Settings()) 88 | ->index([ 89 | 'number_of_replicas' => 2, 90 | 'refresh_interval' => -1, 91 | ]) 92 | ->analysis([ 93 | 'analyzer' => [ 94 | 'content' => [ 95 | 'type' => 'custom', 96 | 'tokenizer' => 'whitespace', 97 | ], 98 | ], 99 | ]); 100 | 101 | $this->assertSame([ 102 | 'index' => [ 103 | 'number_of_replicas' => 2, 104 | 'refresh_interval' => -1, 105 | ], 106 | 'analysis' => [ 107 | 'analyzer' => [ 108 | 'content' => [ 109 | 'type' => 'custom', 110 | 'tokenizer' => 'whitespace', 111 | ], 112 | ], 113 | ], 114 | ], $settings->toArray()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/Unit/Search/AggregationTest.php: -------------------------------------------------------------------------------- 1 | aggregation = new Aggregation([ 22 | 'doc_count_error_upper_bound' => 0, 23 | 'sum_other_doc_count' => 0, 24 | 'buckets' => [ 25 | [ 26 | 'key' => 'electronic', 27 | 'doc_count' => 6, 28 | ], 29 | ], 30 | ]); 31 | } 32 | 33 | public function test_buckets_can_be_retrieved(): void 34 | { 35 | $this->assertEquals( 36 | collect([ 37 | new Bucket([ 38 | 'key' => 'electronic', 39 | 'doc_count' => 6, 40 | ]), 41 | ]), 42 | $this->aggregation->buckets() 43 | ); 44 | } 45 | 46 | public function test_bucket_keys_can_be_plucked(): void 47 | { 48 | $this->assertEquals( 49 | collect(['electronic']), 50 | $this->aggregation->buckets()->pluck('key') 51 | ); 52 | } 53 | 54 | public function test_raw_representation_can_be_retrieved(): void 55 | { 56 | $this->assertSame([ 57 | 'doc_count_error_upper_bound' => 0, 58 | 'sum_other_doc_count' => 0, 59 | 'buckets' => [ 60 | [ 61 | 'key' => 'electronic', 62 | 'doc_count' => 6, 63 | ], 64 | ], 65 | ], $this->aggregation->raw()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Unit/Search/BucketTest.php: -------------------------------------------------------------------------------- 1 | bucket = new Bucket([ 19 | 'key' => 'electronic', 20 | 'doc_count' => 6, 21 | ]); 22 | } 23 | 24 | public function test_key_can_be_retrieved(): void 25 | { 26 | $this->assertSame('electronic', $this->bucket->key()); 27 | } 28 | 29 | public function test_doc_count_can_be_retrieved(): void 30 | { 31 | $this->assertSame(6, $this->bucket->docCount()); 32 | } 33 | 34 | public function test_raw_representation_can_be_retrieved(): void 35 | { 36 | $this->assertSame([ 37 | 'key' => 'electronic', 38 | 'doc_count' => 6, 39 | ], $this->bucket->raw()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Search/ExplanationTest.php: -------------------------------------------------------------------------------- 1 | explanation = new Explanation([ 19 | 'value' => 1.6943598, 20 | 'description' => 'weight(message:elasticsearch in 0) [PerFieldSimilarity], result of:', 21 | 'details' => [ 22 | [ 23 | 'value' => 1.3862944, 24 | 'description' => 'score(freq=1.0), computed as boost * idf * tf from:', 25 | 'details' => [], 26 | ], 27 | ], 28 | ]); 29 | } 30 | 31 | public function test_value_can_be_retrieved(): void 32 | { 33 | $this->assertSame(1.6943598, $this->explanation->value()); 34 | } 35 | 36 | public function test_description_can_be_retrieved(): void 37 | { 38 | $this->assertSame( 39 | 'weight(message:elasticsearch in 0) [PerFieldSimilarity], result of:', 40 | $this->explanation->description() 41 | ); 42 | } 43 | 44 | public function test_details_can_be_retrieved(): void 45 | { 46 | $this->assertCount(1, $this->explanation->details()); 47 | $this->assertSame(1.3862944, $this->explanation->details()->first()->value()); 48 | } 49 | 50 | public function test_raw_representation_can_be_retrieved(): void 51 | { 52 | $this->assertSame([ 53 | 'value' => 1.6943598, 54 | 'description' => 'weight(message:elasticsearch in 0) [PerFieldSimilarity], result of:', 55 | 'details' => [ 56 | [ 57 | 'value' => 1.3862944, 58 | 'description' => 'score(freq=1.0), computed as boost * idf * tf from:', 59 | 'details' => [], 60 | ], 61 | ], 62 | ], $this->explanation->raw()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Unit/Search/HighlightTest.php: -------------------------------------------------------------------------------- 1 | [ 16 | ' with the number', 17 | ' 1', 18 | ], 19 | ]); 20 | 21 | $this->assertEquals(collect([ 22 | ' with the number', 23 | ' 1', 24 | ]), $highlight->snippets('message')); 25 | } 26 | 27 | public function test_empty_collection_is_returned_when_trying_to_retrieve_snippets_for_non_existing_field(): void 28 | { 29 | $highlight = new Highlight([ 30 | 'foo' => [ 31 | 'test fragment', 32 | ], 33 | ]); 34 | 35 | $this->assertEquals(collect(), $highlight->snippets('bar')); 36 | } 37 | 38 | public function test_raw_representation_can_be_retrieved(): void 39 | { 40 | $highlight = new Highlight([ 41 | 'foo' => [ 42 | 'test fragment 1', 43 | ], 44 | 'bar' => [ 45 | 'test fragment 2', 46 | 'test fragment 3', 47 | ], 48 | ]); 49 | 50 | $this->assertSame([ 51 | 'foo' => [ 52 | 'test fragment 1', 53 | ], 54 | 'bar' => [ 55 | 'test fragment 2', 56 | 'test fragment 3', 57 | ], 58 | ], $highlight->raw()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Unit/Search/HitTest.php: -------------------------------------------------------------------------------- 1 | hit = new Hit([ 27 | '_id' => '1', 28 | '_index' => 'test', 29 | '_source' => [ 30 | 'title' => 'foo', 31 | ], 32 | '_score' => 1.3, 33 | 'sort' => [ 34 | '2021-05-20T05:30:04.832Z', 35 | 4294967298, 36 | ], 37 | 'highlight' => [ 38 | 'title' => [ 39 | ' foo ', 40 | ], 41 | ], 42 | 'inner_hits' => [ 43 | 'nested' => [ 44 | 'hits' => [ 45 | 'total' => [ 46 | 'value' => 1, 47 | ], 48 | 'hits' => [ 49 | [ 50 | '_id' => '2', 51 | '_index' => 'test', 52 | '_source' => [ 53 | 'name' => 'bar', 54 | ], 55 | '_score' => 1.6, 56 | ], 57 | ], 58 | ], 59 | ], 60 | ], 61 | '_explanation' => [ 62 | 'value' => 1.6943598, 63 | 'description' => 'result of:', 64 | 'details' => [], 65 | ], 66 | ]); 67 | } 68 | 69 | public function test_index_name_can_be_retrieved(): void 70 | { 71 | $this->assertSame('test', $this->hit->indexName()); 72 | } 73 | 74 | public function test_score_can_be_retrieved(): void 75 | { 76 | $this->assertSame(1.3, $this->hit->score()); 77 | } 78 | 79 | public function test_sort_can_be_retrieved(): void 80 | { 81 | $this->assertSame(['2021-05-20T05:30:04.832Z', 4294967298], $this->hit->sort()); 82 | } 83 | 84 | public function test_document_can_be_retrieved(): void 85 | { 86 | $this->assertEquals( 87 | new Document('1', ['title' => 'foo']), 88 | $this->hit->document() 89 | ); 90 | } 91 | 92 | public function test_highlight_can_be_retrieved_if_present(): void 93 | { 94 | $this->assertEquals( 95 | new Highlight(['title' => [' foo ']]), 96 | $this->hit->highlight() 97 | ); 98 | } 99 | 100 | public function test_nothing_is_returned_when_trying_to_retrieve_highlight_but_it_is_not_present(): void 101 | { 102 | $hit = new Hit(['_id' => '1']); 103 | 104 | $this->assertNull($hit->highlight()); 105 | } 106 | 107 | public function test_inner_hits_can_be_retrieved(): void 108 | { 109 | $innerHit = new Hit([ 110 | '_id' => '2', 111 | '_index' => 'test', 112 | '_source' => [ 113 | 'name' => 'bar', 114 | ], 115 | '_score' => 1.6, 116 | ]); 117 | 118 | /** @var Collection $nestedInnerHits */ 119 | $nestedInnerHits = $this->hit->innerHits()->get('nested'); 120 | 121 | $this->assertCount(1, $nestedInnerHits); 122 | $this->assertEquals($innerHit, $nestedInnerHits->first()); 123 | } 124 | 125 | public function test_inner_hits_total_can_be_retrieved(): void 126 | { 127 | $this->assertSame(1, $this->hit->innerHitsTotal()->get('nested')); 128 | } 129 | 130 | public function test_explanation_can_be_retrieved(): void 131 | { 132 | $this->assertEquals( 133 | new Explanation([ 134 | 'value' => 1.6943598, 135 | 'description' => 'result of:', 136 | 'details' => [], 137 | ]), 138 | $this->hit->explanation() 139 | ); 140 | } 141 | 142 | public function test_raw_representation_can_be_retrieved(): void 143 | { 144 | $this->assertSame([ 145 | '_id' => '1', 146 | '_index' => 'test', 147 | '_source' => [ 148 | 'title' => 'foo', 149 | ], 150 | '_score' => 1.3, 151 | 'sort' => [ 152 | '2021-05-20T05:30:04.832Z', 153 | 4294967298, 154 | ], 155 | 'highlight' => [ 156 | 'title' => [ 157 | ' foo ', 158 | ], 159 | ], 160 | 'inner_hits' => [ 161 | 'nested' => [ 162 | 'hits' => [ 163 | 'total' => [ 164 | 'value' => 1, 165 | ], 166 | 'hits' => [ 167 | [ 168 | '_id' => '2', 169 | '_index' => 'test', 170 | '_source' => [ 171 | 'name' => 'bar', 172 | ], 173 | '_score' => 1.6, 174 | ], 175 | ], 176 | ], 177 | ], 178 | ], 179 | '_explanation' => [ 180 | 'value' => 1.6943598, 181 | 'description' => 'result of:', 182 | 'details' => [], 183 | ], 184 | ], $this->hit->raw()); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/Unit/Search/PointInTimeManagerTest.php: -------------------------------------------------------------------------------- 1 | client = $this->createMock(Client::class); 28 | $this->client->method('setAsync')->willReturnSelf(); 29 | 30 | $clientBuilder = $this->createMock(ClientBuilderInterface::class); 31 | $clientBuilder->method('default')->willReturn($this->client); 32 | 33 | $this->pointInTimeManager = new PointInTimeManager($clientBuilder); 34 | } 35 | 36 | public function test_point_in_time_can_be_opened(): void 37 | { 38 | $response = $this->createMock(Elasticsearch::class); 39 | 40 | $response 41 | ->expects($this->once()) 42 | ->method('asArray') 43 | ->willReturn([ 44 | 'id' => '46ToAwMDaWR5BXV1', 45 | ]); 46 | 47 | $this->client 48 | ->expects($this->once()) 49 | ->method('openPointInTime') 50 | ->with([ 51 | 'index' => 'test', 52 | 'keep_alive' => '1m', 53 | ]) 54 | ->willReturn($response); 55 | 56 | $this->assertSame('46ToAwMDaWR5BXV1', $this->pointInTimeManager->open('test', '1m')); 57 | } 58 | 59 | public function test_point_in_time_can_be_closed(): void 60 | { 61 | $this->client 62 | ->expects($this->once()) 63 | ->method('closePointInTime') 64 | ->with([ 65 | 'body' => [ 66 | 'id' => '46ToAwMDaWR5BXV1', 67 | ], 68 | ]); 69 | 70 | $this->assertSame($this->pointInTimeManager, $this->pointInTimeManager->close('46ToAwMDaWR5BXV1')); 71 | } 72 | 73 | /** 74 | * @noinspection ClassMockingCorrectnessInspection 75 | * @noinspection PhpUnitInvalidMockingEntityInspection 76 | */ 77 | public function test_connection_can_be_changed(): void 78 | { 79 | $defaultClient = $this->createMock(Client::class); 80 | $defaultClient->method('setAsync')->willReturnSelf(); 81 | 82 | $defaultClient 83 | ->expects($this->never()) 84 | ->method('closePointInTime'); 85 | 86 | $testClient = $this->createMock(Client::class); 87 | $testClient->method('setAsync')->willReturnSelf(); 88 | 89 | $testClient 90 | ->expects($this->once()) 91 | ->method('closePointInTime') 92 | ->with([ 93 | 'body' => [ 94 | 'id' => 'foo', 95 | ], 96 | ]); 97 | 98 | $clientBuilder = $this->createMock(ClientBuilderInterface::class); 99 | $clientBuilder->method('default')->willReturn($defaultClient); 100 | $clientBuilder->method('connection')->with('test')->willReturn($testClient); 101 | 102 | (new PointInTimeManager($clientBuilder))->connection('test')->close('foo'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Unit/Search/SearchParametersTest.php: -------------------------------------------------------------------------------- 1 | indices(['foo', 'bar']); 19 | $this->assertSame(['index' => 'foo,bar'], $searchParameters->toArray()); 20 | } 21 | 22 | public function test_array_casting_with_query(): void 23 | { 24 | $searchParameters = (new SearchParameters())->query([ 25 | 'term' => [ 26 | 'user' => 'foo', 27 | ], 28 | ]); 29 | 30 | $this->assertSame([ 31 | 'body' => [ 32 | 'query' => [ 33 | 'term' => [ 34 | 'user' => 'foo', 35 | ], 36 | ], 37 | ], 38 | ], $searchParameters->toArray()); 39 | } 40 | 41 | public function test_array_casting_with_highlight(): void 42 | { 43 | $searchParameters = (new SearchParameters())->highlight([ 44 | 'fields' => [ 45 | 'content' => new stdClass(), 46 | ], 47 | ]); 48 | 49 | $this->assertEquals([ 50 | 'body' => [ 51 | 'highlight' => [ 52 | 'fields' => [ 53 | 'content' => new stdClass(), 54 | ], 55 | ], 56 | ], 57 | ], $searchParameters->toArray()); 58 | } 59 | 60 | public function test_array_casting_with_sort(): void 61 | { 62 | $searchParameters = (new SearchParameters())->sort([ 63 | ['title' => 'asc'], 64 | '_score', 65 | ]); 66 | 67 | $this->assertEquals([ 68 | 'body' => [ 69 | 'sort' => [ 70 | ['title' => 'asc'], 71 | '_score', 72 | ], 73 | ], 74 | ], $searchParameters->toArray()); 75 | } 76 | 77 | public function test_array_casting_with_rescore(): void 78 | { 79 | $searchParameters = (new SearchParameters())->rescore([ 80 | 'window_size' => 50, 81 | 'query' => [ 82 | 'rescore_query' => [ 83 | 'match_phrase' => [ 84 | 'message' => [ 85 | 'query' => 'the quick brown', 86 | 'slop' => 2, 87 | ], 88 | ], 89 | ], 90 | 'query_weight' => 0.7, 91 | 'rescore_query_weight' => 1.2, 92 | ], 93 | ]); 94 | 95 | $this->assertEquals([ 96 | 'body' => [ 97 | 'rescore' => [ 98 | 'window_size' => 50, 99 | 'query' => [ 100 | 'rescore_query' => [ 101 | 'match_phrase' => [ 102 | 'message' => [ 103 | 'query' => 'the quick brown', 104 | 'slop' => 2, 105 | ], 106 | ], 107 | ], 108 | 'query_weight' => 0.7, 109 | 'rescore_query_weight' => 1.2, 110 | ], 111 | ], 112 | ], 113 | ], $searchParameters->toArray()); 114 | } 115 | 116 | public function test_array_casting_with_from(): void 117 | { 118 | $searchParameters = (new SearchParameters())->from(10); 119 | 120 | $this->assertEquals([ 121 | 'body' => [ 122 | 'from' => 10, 123 | ], 124 | ], $searchParameters->toArray()); 125 | } 126 | 127 | public function test_array_casting_with_size(): void 128 | { 129 | $searchParameters = (new SearchParameters())->size(100); 130 | 131 | $this->assertEquals([ 132 | 'body' => [ 133 | 'size' => 100, 134 | ], 135 | ], $searchParameters->toArray()); 136 | } 137 | 138 | public function test_array_casting_with_suggest(): void 139 | { 140 | $searchParameters = (new SearchParameters())->suggest([ 141 | 'color_suggestion' => [ 142 | 'text' => 'red', 143 | 'term' => [ 144 | 'field' => 'color', 145 | ], 146 | ], 147 | ]); 148 | 149 | $this->assertEquals([ 150 | 'body' => [ 151 | 'suggest' => [ 152 | 'color_suggestion' => [ 153 | 'text' => 'red', 154 | 'term' => [ 155 | 'field' => 'color', 156 | ], 157 | ], 158 | ], 159 | ], 160 | ], $searchParameters->toArray()); 161 | } 162 | 163 | public static function sourceProvider(): array 164 | { 165 | return [ 166 | [false], 167 | ['obj1.*'], 168 | [['obj1.*', 'obj2.*']], 169 | [['includes' => ['obj1.*', 'obj2.*'], 'excludes' => ['*.description']]], 170 | ]; 171 | } 172 | 173 | /** 174 | * @param array|string|bool $source 175 | */ 176 | #[DataProvider('sourceProvider')] 177 | public function test_array_casting_with_source($source): void 178 | { 179 | $searchParameters = (new SearchParameters())->source($source); 180 | 181 | $this->assertEquals([ 182 | 'body' => [ 183 | '_source' => $source, 184 | ], 185 | ], $searchParameters->toArray()); 186 | } 187 | 188 | public function test_array_casting_with_collapse(): void 189 | { 190 | $searchParameters = (new SearchParameters())->collapse([ 191 | 'field' => 'user', 192 | ]); 193 | 194 | $this->assertEquals([ 195 | 'body' => [ 196 | 'collapse' => [ 197 | 'field' => 'user', 198 | ], 199 | ], 200 | ], $searchParameters->toArray()); 201 | } 202 | 203 | public function test_array_casting_with_aggregations(): void 204 | { 205 | $searchParameters = (new SearchParameters())->aggregations([ 206 | 'min_price' => [ 207 | 'min' => [ 208 | 'field' => 'price', 209 | ], 210 | ], 211 | ]); 212 | 213 | $this->assertEquals([ 214 | 'body' => [ 215 | 'aggregations' => [ 216 | 'min_price' => [ 217 | 'min' => [ 218 | 'field' => 'price', 219 | ], 220 | ], 221 | ], 222 | ], 223 | ], $searchParameters->toArray()); 224 | } 225 | 226 | public function test_array_casting_with_post_filter(): void 227 | { 228 | $searchParameters = (new SearchParameters())->postFilter([ 229 | 'term' => [ 230 | 'color' => 'red', 231 | ], 232 | ]); 233 | 234 | $this->assertEquals([ 235 | 'body' => [ 236 | 'post_filter' => [ 237 | 'term' => [ 238 | 'color' => 'red', 239 | ], 240 | ], 241 | ], 242 | ], $searchParameters->toArray()); 243 | } 244 | 245 | public function test_array_casting_with_track_total_hits(): void 246 | { 247 | $searchParameters = (new SearchParameters())->trackTotalHits(100); 248 | 249 | $this->assertEquals([ 250 | 'body' => [ 251 | 'track_total_hits' => 100, 252 | ], 253 | ], $searchParameters->toArray()); 254 | } 255 | 256 | public function test_array_casting_with_indices_boost(): void 257 | { 258 | $searchParameters = (new SearchParameters())->indicesBoost([ 259 | ['my-alias' => 1.4], 260 | ['my-index' => 1.3], 261 | ]); 262 | 263 | $this->assertEquals([ 264 | 'body' => [ 265 | 'indices_boost' => [ 266 | ['my-alias' => 1.4], 267 | ['my-index' => 1.3], 268 | ], 269 | ], 270 | ], $searchParameters->toArray()); 271 | } 272 | 273 | public function test_array_casting_with_track_scores(): void 274 | { 275 | $searchParameters = (new SearchParameters())->trackScores(true); 276 | 277 | $this->assertEquals([ 278 | 'body' => [ 279 | 'track_scores' => true, 280 | ], 281 | ], $searchParameters->toArray()); 282 | } 283 | 284 | public function test_array_casting_with_script_fields(): void 285 | { 286 | $searchParameters = (new SearchParameters())->scriptFields([ 287 | 'my_doubled_field' => [ 288 | 'script' => [ 289 | 'lang' => 'painless', 290 | 'source' => 'doc[params.field] * params.multiplier', 291 | 'params' => [ 292 | 'field' => 'my_field', 293 | 'multiplier' => 2, 294 | ], 295 | ], 296 | ], 297 | ]); 298 | 299 | $this->assertEquals([ 300 | 'body' => [ 301 | 'script_fields' => [ 302 | 'my_doubled_field' => [ 303 | 'script' => [ 304 | 'lang' => 'painless', 305 | 'source' => 'doc[params.field] * params.multiplier', 306 | 'params' => [ 307 | 'field' => 'my_field', 308 | 'multiplier' => 2, 309 | ], 310 | ], 311 | ], 312 | ], 313 | ], 314 | ], $searchParameters->toArray()); 315 | } 316 | 317 | public function test_array_casting_with_min_score(): void 318 | { 319 | $searchParameters = (new SearchParameters())->minScore(0.5); 320 | 321 | $this->assertEquals([ 322 | 'body' => [ 323 | 'min_score' => 0.5, 324 | ], 325 | ], $searchParameters->toArray()); 326 | } 327 | 328 | public function test_array_casting_with_search_type(): void 329 | { 330 | $searchParameters = (new SearchParameters())->searchType('query_then_fetch'); 331 | 332 | $this->assertEquals([ 333 | 'search_type' => 'query_then_fetch', 334 | ], $searchParameters->toArray()); 335 | } 336 | 337 | public function test_array_casting_with_preference(): void 338 | { 339 | $searchParameters = (new SearchParameters())->preference('_local'); 340 | 341 | $this->assertEquals([ 342 | 'preference' => '_local', 343 | ], $searchParameters->toArray()); 344 | } 345 | 346 | public function test_array_casting_with_point_in_time(): void 347 | { 348 | $searchParameters = (new SearchParameters())->pointInTime([ 349 | 'id' => '46ToAwMDaWR5BXV1', 350 | 'keep_alive' => '1m', 351 | ]); 352 | 353 | $this->assertEquals([ 354 | 'body' => [ 355 | 'pit' => [ 356 | 'id' => '46ToAwMDaWR5BXV1', 357 | 'keep_alive' => '1m', 358 | ], 359 | ], 360 | ], $searchParameters->toArray()); 361 | } 362 | 363 | public function test_array_casting_with_search_after(): void 364 | { 365 | $searchParameters = (new SearchParameters())->searchAfter([ 366 | '2021-05-20T05:30:04.832Z', 367 | 4294967298, 368 | ]); 369 | 370 | $this->assertEquals([ 371 | 'body' => [ 372 | 'search_after' => [ 373 | '2021-05-20T05:30:04.832Z', 374 | 4294967298, 375 | ], 376 | ], 377 | ], $searchParameters->toArray()); 378 | } 379 | 380 | public function test_array_casting_with_routing(): void 381 | { 382 | $searchParameters = (new SearchParameters())->routing(['foo', 'bar']); 383 | $this->assertSame(['routing' => 'foo,bar'], $searchParameters->toArray()); 384 | } 385 | 386 | public function test_array_casting_with_explain(): void 387 | { 388 | $searchParameters = (new SearchParameters())->explain(true); 389 | 390 | $this->assertEquals([ 391 | 'body' => [ 392 | 'explain' => true, 393 | ], 394 | ], $searchParameters->toArray()); 395 | } 396 | 397 | public function test_array_casting_with_terminate_after(): void 398 | { 399 | $searchParameters = (new SearchParameters())->terminateAfter(10); 400 | $this->assertSame(['terminate_after' => 10], $searchParameters->toArray()); 401 | } 402 | 403 | public function test_array_casting_with_request_cache(): void 404 | { 405 | $searchParameters = (new SearchParameters())->requestCache(true); 406 | $this->assertSame(['request_cache' => true], $searchParameters->toArray()); 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /tests/Unit/Search/SearchResultTest.php: -------------------------------------------------------------------------------- 1 | [ 25 | 'hits' => [ 26 | [ 27 | '_id' => '1', 28 | '_source' => ['title' => 'foo'], 29 | ], 30 | ], 31 | ], 32 | ]); 33 | 34 | $this->assertEquals( 35 | collect([new Hit(['_id' => '1', '_source' => ['title' => 'foo']])]), 36 | $searchResult->hits() 37 | ); 38 | } 39 | 40 | public function test_total_number_of_hits_can_be_retrieved(): void 41 | { 42 | $searchResult = new SearchResult([ 43 | 'hits' => [ 44 | 'total' => ['value' => 100], 45 | ], 46 | ]); 47 | 48 | $this->assertSame(100, $searchResult->total()); 49 | } 50 | 51 | public function test_empty_collection_is_returned_when_suggestions_are_not_present(): void 52 | { 53 | $searchResult = new SearchResult([ 54 | 'hits' => [], 55 | ]); 56 | 57 | $this->assertEquals(collect(), $searchResult->suggestions()); 58 | } 59 | 60 | public function test_suggestions_can_be_retrieved(): void 61 | { 62 | $searchResult = new SearchResult([ 63 | 'hits' => [], 64 | 'suggest' => [ 65 | 'color_suggestion' => [ 66 | [ 67 | 'text' => 'red', 68 | 'offset' => 0, 69 | 'length' => 3, 70 | 'options' => [], 71 | ], 72 | [ 73 | 'text' => 'blue', 74 | 'offset' => 4, 75 | 'length' => 4, 76 | 'options' => [], 77 | ], 78 | ], 79 | ], 80 | ]); 81 | 82 | $this->assertEquals( 83 | collect([ 84 | new Suggestion([ 85 | 'text' => 'red', 86 | 'offset' => 0, 87 | 'length' => 3, 88 | 'options' => [], 89 | ]), 90 | new Suggestion([ 91 | 'text' => 'blue', 92 | 'offset' => 4, 93 | 'length' => 4, 94 | 'options' => [], 95 | ]), 96 | ]), 97 | $searchResult->suggestions()->get('color_suggestion') 98 | ); 99 | } 100 | 101 | public function test_aggregations_can_be_retrieved(): void 102 | { 103 | $searchResult = new SearchResult([ 104 | 'hits' => [], 105 | 'aggregations' => [ 106 | 'min_price' => [ 107 | 'value' => 10, 108 | ], 109 | ], 110 | ]); 111 | 112 | $this->assertEquals( 113 | new Aggregation(['value' => 10]), 114 | $searchResult->aggregations()->get('min_price') 115 | ); 116 | } 117 | 118 | public function test_raw_total_can_be_retrieved(): void 119 | { 120 | $searchResult = new SearchResult([ 121 | 'hits' => [ 122 | 'total' => ['value' => 1], 123 | ], 124 | ]); 125 | 126 | /** @var array $hits */ 127 | $hits = $searchResult['hits']; 128 | $this->assertSame(['value' => 1], $hits['total']); 129 | } 130 | 131 | /** 132 | * @noinspection OnlyWritesOnParameterInspection 133 | */ 134 | public function test_raw_data_can_not_be_modified(): void 135 | { 136 | $this->expectException(RawResultReadOnlyException::class); 137 | 138 | $searchResult = new SearchResult([]); 139 | $searchResult['total'] = 100; 140 | } 141 | 142 | public function test_raw_representation_can_be_retrieved(): void 143 | { 144 | $searchResult = new SearchResult([ 145 | 'hits' => [ 146 | 'total' => ['value' => 100], 147 | 'hits' => [ 148 | [ 149 | '_id' => '1', 150 | '_source' => ['title' => 'foo'], 151 | ], 152 | [ 153 | '_id' => '2', 154 | '_source' => ['title' => 'bar'], 155 | ], 156 | ], 157 | ], 158 | ]); 159 | 160 | $this->assertSame([ 161 | 'hits' => [ 162 | 'total' => ['value' => 100], 163 | 'hits' => [ 164 | [ 165 | '_id' => '1', 166 | '_source' => ['title' => 'foo'], 167 | ], 168 | [ 169 | '_id' => '2', 170 | '_source' => ['title' => 'bar'], 171 | ], 172 | ], 173 | ], 174 | ], $searchResult->raw()); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/Unit/Search/SuggestionTest.php: -------------------------------------------------------------------------------- 1 | 'foo']); 15 | 16 | $this->assertSame('foo', $suggestion->text()); 17 | } 18 | 19 | public function test_offset_can_be_retrieved(): void 20 | { 21 | $suggestion = new Suggestion(['offset' => 0]); 22 | 23 | $this->assertSame(0, $suggestion->offset()); 24 | } 25 | 26 | public function test_length_can_be_retrieved(): void 27 | { 28 | $suggestion = new Suggestion(['length' => 5]); 29 | 30 | $this->assertSame(5, $suggestion->length()); 31 | } 32 | 33 | public function test_options_can_be_retrieved(): void 34 | { 35 | $suggestion = new Suggestion([ 36 | 'options' => [ 37 | [ 38 | 'text' => 'foo', 39 | 'score' => 0.8, 40 | 'freq' => 1, 41 | ], 42 | ], 43 | ]); 44 | 45 | $this->assertEquals(collect([ 46 | [ 47 | 'text' => 'foo', 48 | 'score' => 0.8, 49 | 'freq' => 1, 50 | ], 51 | ]), $suggestion->options()); 52 | } 53 | 54 | public function test_raw_representation_can_be_retrieved(): void 55 | { 56 | $suggestion = new Suggestion([ 57 | 'text' => 'foo', 58 | 'offset' => 0, 59 | 'length' => 5, 60 | 'options' => [], 61 | ]); 62 | 63 | $this->assertSame([ 64 | 'text' => 'foo', 65 | 'offset' => 0, 66 | 'length' => 5, 67 | 'options' => [], 68 | ], $suggestion->raw()); 69 | } 70 | } 71 | --------------------------------------------------------------------------------