├── .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 | [](https://packagist.org/packages/babenkoivan/elastic-adapter)
4 | [](https://packagist.org/packages/babenkoivan/elastic-adapter)
5 | [](https://packagist.org/packages/babenkoivan/elastic-adapter)
6 | [](https://github.com/babenkoivan/elastic-adapter/actions?query=workflow%3ATests)
7 | [](https://github.com/babenkoivan/elastic-adapter/actions?query=workflow%3A%22Code+style%22)
8 | [](https://github.com/babenkoivan/elastic-adapter/actions?query=workflow%3A%22Static+analysis%22)
9 | [](https://paypal.me/babenkoi)
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------