diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0ca66da --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +vendor/ +.git/ +.phpcs-cache +.idea/ +*.log diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d6e6df..af88f0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ 7.4, 8.1 ] - elastic: [ 7.17.0 ] + php: [ '8.2', '8.5' ] + elastic: [ 9.2.2 ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -29,18 +29,20 @@ jobs: uses: elastic/elastic-github-actions/elasticsearch@master with: stack-version: ${{ matrix.elastic }} + security-enabled: false - name: Composer run: make composer - - if: matrix.php == '8.1' && matrix.elastic == '7.17.0' + - if: matrix.php == '8.5' && matrix.elastic == '9.2.2' name: Coding standard run: make cs - - if: matrix.php == '8.1' && matrix.elastic == '7.17.0' + - if: matrix.php == '8.5' && matrix.elastic == '9.2.2' name: PHPStan run: make phpstan - - if: matrix.php == '8.1' - name: Tests + - name: Tests run: make tests + env: + ELASTICSEARCH_HOST: http://localhost:9200 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6f3b223 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,173 @@ +# Changelog + +## v2 — full DSL coverage + +This branch brings every documented Elasticsearch DSL feature under typed PHP objects, fixes two queries that produced invalid DSL, and round-trips every feature against a real ES container via the new `AbstractElasticTestCase`. + +### Test infrastructure + +- New `tests/SpameriTests/ElasticQuery/AbstractElasticTestCase` base class with `createIndex($mapping)`, `indexDocument($body, $id, $refresh)`, `search($elasticQuery)`, `deleteIndex()`, and a generic `request($method, $path, $body)`. Tests that extend it shrink from ~70 lines of curl boilerplate to ~15. + +### Bug fixes (BC-breaking) + +| File | Previous | Fixed | +| --- | --- | --- | +| `Query/GeoDistance` | Emitted `{pin: {location: ...}}` (invalid DSL) and lacked the required `distance` argument. | Emits proper `geo_distance` envelope. Constructor now takes `distance` (required), plus `distance_type`, `validation_method`, `ignore_unmapped`, `boost`. | +| `Query/Nested` | Wrapped inner query in `[$queryArray]` (extra list level — rejected by ES). | Inner query is now an object. Added `score_mode`, `ignore_unmapped`, `inner_hits`. | +| `Query/PhrasePrefix` | `int $boost = 1` (inconsistent type). | `float $boost = 1.0`. | +| `Options/GeoDistanceSort` | `ignore_unmapped` hard-coded to `true`. | Constructor arg `bool $ignoreUnmapped = true`. | + +### New query types + +- **Knn** — vector similarity (field, queryVector, k, numCandidates, similarity, filter, boost). +- **SparseVector** — ELSER-style sparse vector query (inference_id+query or queryVector tokens). +- **TextExpansion** — legacy ELSER form (model_id, model_text). +- **Semantic** — queries a `semantic_text` field. +- **RuleQuery** — Search Application query rules over an organic query. +- **WeightedTokens** — token weights against a sparse_vector field. + +### Existing queries — new constructor arguments + +| Query | New args | +| --- | --- | +| `ElasticMatch` | `zero_terms_query`, `auto_generate_synonyms_phrase_query`, `lenient`, `prefix_length`, `max_expansions`, `fuzzy_transpositions`, `fuzzy_rewrite` | +| `MultiMatch` | `tie_breaker`, `slop`, `prefix_length`, `max_expansions`, `lenient`, `zero_terms_query`, `auto_generate_synonyms_phrase_query`, `fuzzy_transpositions`, `fuzzy_rewrite` | +| `MatchPhrase` | `zero_terms_query` | +| `PhrasePrefix` | `analyzer`, `max_expansions`, `zero_terms_query` | +| `MatchBoolPrefix` | `fuzziness`, `prefix_length`, `max_expansions`, `fuzzy_transpositions`, `fuzzy_rewrite` | +| `QueryString` | `analyze_wildcard`, `auto_generate_synonyms_phrase_query`, `enable_position_increments`, `fuzziness`, `fuzzy_max_expansions`, `fuzzy_prefix_length`, `fuzzy_transpositions`, `lenient`, `max_determinized_states`, `minimum_should_match`, `quote_analyzer`, `phrase_slop`, `quote_field_suffix`, `rewrite`, `time_zone`, `type`, `tie_breaker` | +| `SimpleQueryString` | `analyze_wildcard`, `auto_generate_synonyms_phrase_query`, `fuzzy_max_expansions`, `fuzzy_prefix_length`, `fuzzy_transpositions`, `lenient`, `minimum_should_match`, `quote_field_suffix` | +| `CombinedFields` | `auto_generate_synonyms_phrase_query` | +| `Term` | `case_insensitive` | +| `Terms` | accepts `TermsLookup` for cross-document terms resolution | +| `Range` | `gt`, `lt`, `format`, `relation` (new `Range\Relation` constants), `time_zone` | +| `Exists` | `boost` | +| `WildCard` | `case_insensitive`, `rewrite` | +| `Prefix` | `rewrite` | +| `Fuzzy` | `transpositions`, `rewrite` | +| `Regexp` | `rewrite` | +| `TermSet` | `boost` | +| `HasChild` | `inner_hits` | +| `HasParent` | `inner_hits` | +| `Nested` | `score_mode`, `ignore_unmapped`, `inner_hits` | +| `ParentId` | `boost` | +| `GeoBoundingBox` | `validation_method`, `ignore_unmapped`, `boost` | +| `GeoShape` | `indexed_shape` (new `IndexedShape` sub-object), `boost` | +| `Shape` | `indexed_shape`, `boost` | +| `MoreLikeThis` | `boost_terms`, `include`, `min_doc_freq`, `max_doc_freq`, `min_word_length`, `max_word_length`, `stop_words`, `analyzer`, `boost`, `fail_on_unsupported_field` | +| `Percolate` | `documents` (multi-doc), `name`, `routing`, `preference`, `version` | + +### New sub-objects + +- `Query/TermsLookup` — `index`, `id`, `path`, `routing`. +- `Query/Range/Relation` — constants: `INTERSECTS`, `CONTAINS`, `WITHIN`. +- `Query/InnerHits` — `name`, `from`, `size`, `sort`, `_source`, `highlight`, `explain`, `script_fields`, `docvalue_fields`, `version`, `seq_no_primary_term`, `stored_fields`, `track_scores`. +- `Query/IndexedShape` — `id`, `index`, `path`, `routing`. +- `Script` (top-level) — reusable script value object (`source`, `lang`, `params`). + +### Aggregations — new types + +Bucket: `Filters` (named filters), `AutoDateHistogram`, `VariableWidthHistogram`, `CategorizeText` *(platinum license)*, `FrequentItemSets` *(platinum license)*, `IpPrefix`, `TimeSeries`. + +Metric: `TopMetrics`, `GeoLine` *(gold license)*, `TTest`, `Rate`, `MatrixStats`. + +Pipeline/sampler/ML: `RandomSampler`, `CumulativeCardinality`, `ExtendedStatsBucket`, `Inference`. + +### Aggregations — new constructor arguments + +| Agg | New args | +| --- | --- | +| `Min`/`Max`/`Avg`/`Sum`/`ValueCount`/`Stats` | `missing`, `script`, `format` | +| `ExtendedStats` | `missing`, `script`, `format` (kept `sigma`) | +| `Cardinality` | `script`, `missing`, `rehash` | +| `MedianAbsoluteDeviation`/`StringStats` | `missing`, `script` | +| `BoxPlot` | `missing`, `script`, `execution_hint` | +| `Percentiles` | `tdigest`, `hdr`, `missing`, `script` | +| `PercentileRanks` | `hdr`, `missing`, `script` | +| `WeightedAvg` | **rewritten** — takes typed `WeightedAvgValue` for value/weight (each with `field`/`script`/`missing`), plus `format` | +| `TopHits` | **rewritten** — `from`, `sort`, `_source`, `highlight`, `explain`, `script_fields`, `docvalue_fields`, `version`, `seq_no_primary_term`, `stored_fields`, `track_scores` | +| `Term` | `min_doc_count`, `shard_size`, `shard_min_doc_count`, `show_term_doc_count_error`, `script`, `collect_mode`, `execution_hint`, `value_type`, `format`; `include`/`exclude` accept arrays | +| `MultiTerms` | `order`, `min_doc_count`, `shard_size`, `shard_min_doc_count`, `collect_mode`, `format` | +| `RareTerms` | `include`, `exclude`, `missing` | +| `SignificantTerms` | `shard_size`, `shard_min_doc_count`, `execution_hint`, `background_filter`, `heuristic` (with `HEURISTIC_*` constants) | +| `SignificantText` | `shard_size`, `shard_min_doc_count`, `min_doc_count`, `background_filter`, `source_fields` | +| `Range` | `script`, `missing`, `format` | +| `DateRange` | `script`, `missing` | +| `Histogram` | `min_doc_count`, `extended_bounds` (new `Histogram\Bounds`), `hard_bounds`, `offset`, `order`, `script`, `missing`, `keyed`, `format` | +| `DateHistogram` | `extended_bounds`, `hard_bounds`, `keyed`, `order`, `script`, `missing` | +| `IpRange` | **rewritten** — new `IpRange\IpRangeValue` with `mask` (CIDR) support | +| `Filter` | **rewritten** — accepts any `LeafQueryInterface` directly | +| `Composite` | typed sources: `Composite\TermsSource`, `Composite\HistogramSource`, `Composite\DateHistogramSource`, `Composite\GeotileGridSource`, each with `order`/`missing_bucket` | +| `AdjacencyMatrix` | `separator`, accepts `LeafQueryInterface` for filters | +| `GeoDistance` (agg) | `keyed`, `script`, `missing` | +| `GeoHashGrid`/`GeoTileGrid` | `bounds` | +| `DiversifiedSampler` | `execution_hint`, `script` | +| `Missing` | `script` | + +### Score functions + +- New `FunctionScore/ScoreFunction/Decay/Gauss`, `Linear`, `Exp` with shared `AbstractDecay` parent (`field`, `origin`, `scale`, `offset`, `decay`, `multi_value_mode`). +- New `FunctionScore/ScoreFunction/ScriptScore` (function variant — distinct from the `Query/ScriptScore` leaf). +- `FunctionScore` gained `boost`, `boost_mode` (with `BOOST_MODE_*` constants), `max_boost`, `min_score`. + +### Sort + +- `Sort` gains `mode`, `nested` (new `NestedSort`), `numeric_type`, `unmapped_type`, `format`. +- New `Options/ScriptSort` — script-based sort. +- New `Options/NestedSort` — path/filter/max_children for nested sorting (recursive). + +### Highlight — rewritten + +- `Highlight/HighlightField` — per-field config (type, number_of_fragments, fragment_size, all boundary_*, encoder, force_source, fragmenter, highlight_query, matched_fields, no_match_size, order, phrase_limit, require_field_match, tags_schema, pre_tags, post_tags). +- `Highlight/HighlightFieldCollection` — typed collection. +- `Highlight` accepts either `HighlightFieldCollection` or simple `array` of field names (BC). Adds all global options. + +### Options — many new fields + +| Field | Type | +| --- | --- | +| `_source` | new `Options\Source` (includes/excludes, or `false`) | +| `track_total_hits` | `bool\|int` | +| `track_scores` | `bool` | +| `explain` | `bool` | +| `terminate_after` | `int` | +| `timeout` | `string` | +| `search_after` | `array` | +| `pit` | new `Options\Pit` | +| `stored_fields` | `array` | +| `docvalue_fields` | `array` | +| `fields` | `array` | +| `script_fields` | `array` | +| `runtime_mappings` | `array` | +| `seq_no_primary_term` | `bool` | +| `indices_boost` | `array` | +| `collapse` | new `Options\Collapse` | +| `rescore` | `array` | +| `suggesters` | `array` | +| `profile` | `bool` | +| `stats` | `array` | +| `ext` | `array` | + +`ElasticQuery::toArray()` wires `collapse`, `rescore`, and `suggest` to the top-level request body. + +### Filter container — bool expansion + +`Filter/FilterCollection` previously exposed only `must()`. It now mirrors `Query/QueryCollection` with `must()`, `should()`, `mustNot()`, and `filter()` — the `bool` body emits all four arms. + +### Suggesters + +- `Options/Suggest/SuggesterInterface` +- `Options/Suggest/TermSuggester` +- `Options/Suggest/PhraseSuggester` +- `Options/Suggest/CompletionSuggester` + +### Response mapper + +- `ResultMapper` now handles named buckets (string keys, e.g. from `Filters` agg) and composite-key buckets (array keys). +- `Result/Aggregation/Bucket.from`/`to` accept `string` (e.g. for IP / date range buckets). + +### CI / tests + +- 218 tests, 3 skipped on basic license (geo_line, categorize_text). +- ES 9.2.2 container in CI; `make tests` passes end-to-end against it. +- The two pre-existing buggy tests for `GeoDistance` and `Nested` (which asserted invalid output) are now corrected and re-run as integration tests against ES. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3563bf5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,105 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +ElasticQuery is a PHP library that converts Elasticsearch query DSL into typed PHP objects. Instead of building queries as arrays, developers can use strongly-typed classes that mirror the Elasticsearch documentation. + +## Development Commands + +```bash +# Install/update dependencies +make composer + +# Run static analysis (PHPStan level 7) +make phpstan + +# Run code style checks (Slevomat coding standard) +make cs + +# Auto-fix code style issues +make cbf + +# Run tests (Nette Tester) +make tests + +# Run a single test file +vendor/bin/tester -s -p php --colors 1 -C tests/SpameriTests/ElasticQuery/Path/To/Test.phpt +``` + +## Architecture + +### Core Query Building + +**ElasticQuery** (`src/ElasticQuery.php`) - Main entry point for building queries. Composes: +- `QueryCollection` - Boolean query with must/should/mustNot collections +- `FilterCollection` - Filter context queries +- `AggregationCollection` - Aggregation definitions +- `Options` - Pagination, sorting, scroll settings +- `Highlight` - Search result highlighting +- `FunctionScore` - Custom scoring functions + +All query/aggregation objects implement `toArray()` to serialize to Elasticsearch-compatible format. + +### Query Objects (`src/Query/`) + +Leaf queries implement `LeafQueryInterface`: +- `ElasticMatch`, `MatchPhrase`, `MultiMatch`, `PhrasePrefix` - Full-text queries +- `Term`, `Terms`, `Range`, `Exists`, `WildCard` - Term-level queries +- `Nested`, `GeoDistance`, `Fuzzy` - Specialized queries + +Collection queries (`MustCollection`, `ShouldCollection`, `MustNotCollection`) also implement `LeafQueryInterface`, enabling nested boolean logic. + +### Aggregations (`src/Aggregation/`) + +- `LeafAggregationCollection` wraps aggregation definitions with nested sub-aggregations +- Metric aggregations: `Min`, `Max`, `Avg`, `TopHits` +- Bucket aggregations: `Term`, `Range`, `Histogram`, `Nested`, `Filter` + +### Response Mapping (`src/Response/`) + +`ResultMapper` converts Elasticsearch array responses to typed objects: +- `ResultSearch` - Standard search results with hits and aggregations +- `ResultSingle` - Single document retrieval +- `ResultBulk` - Bulk operation results +- `ResultVersion` - Cluster version info + +### Index Mapping (`src/Mapping/`) + +Classes for defining Elasticsearch index mappings: +- Analyzers: Standard, custom dictionary analyzers for multiple languages +- Filters: Stemming, synonyms, stop words, edge n-grams +- Tokenizers: Pattern, whitespace, etc. +- `Settings` - Index settings configuration + +### Document Handling (`src/Document.php`, `src/Document/`) + +`Document` wraps index name, body, and optional ID for Elasticsearch client calls. + +### Options & Sorting (`src/Options.php`, `src/Options/`) + +- `Options` - Pagination (size, from), scroll support, min_score, version inclusion +- `Sort` - Field sorting with ASC/DESC and missing value handling +- `GeoDistanceSort` - Geo-spatial sorting by distance from a point +- `SortCollection` - Manages multiple sort criteria + +### Function Score (`src/FunctionScore.php`, `src/FunctionScore/`) + +Custom scoring with multiple score functions: +- `FieldValueFactor` - Score based on numeric field values with modifiers (log, sqrt, etc.) +- `Weight` - Constant weight multiplier when filter matches +- `RandomScore` - Randomized scoring with optional seed for consistency + +Score modes: multiply, sum, avg, first, max, min + +## Coding Standards + +- PHP 8.2+ with strict types +- Fully qualified class names in annotations +- Fully qualified global functions and constants +- Tab indentation (no spaces) +- Trailing commas in arrays, function calls, and declarations +- Constructor property promotion where applicable +- No Yoda comparisons +- Strict equality operators only (`===`, `!==`) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e6cb6b0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM php:8.2-cli + +RUN apt-get update && apt-get install -y \ + git \ + unzip \ + libcurl4-openssl-dev \ + && docker-php-ext-install curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +WORKDIR /app diff --git a/README.md b/README.md index eb61922..a1868ab 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,68 @@ # ElasticQuery -Elastic documentation converted to php objects for easy using. -Installation ------------- +A PHP library that converts Elasticsearch query DSL into strongly-typed PHP objects. Instead of building queries as arrays, use type-safe classes that mirror the Elasticsearch documentation. -The best way to install Spameri/ElasticQuery is using [Composer](http://getcomposer.org/): +## Features + +- **Type-safe queries** — full-text, term-level, compound, geo, nested, joining, vector (knn / sparse_vector / semantic), span queries, and rule queries +- **Aggregations** — metric (min, max, avg, stats, weighted_avg, top_hits, top_metrics, t_test, geo_line, …), bucket (terms, histogram, date_histogram, range, filter, filters, composite with typed sources, ip_prefix, time_series, …), pipeline (cumulative_*, bucket_*, normalize, serial_diff, inference, …) +- **Function scoring** — field value factor, weight, random, decay (gauss / linear / exp), script_score; score_mode + boost_mode +- **Sort** — field, geo-distance, script-based, with nested sort (filter / max_children / recursive) +- **Highlight** — per-field config (type, fragment_size, boundary scanner, encoder, fragmenter, highlight_query, matched_fields, no_match_size, order, phrase_limit, …) +- **Search options** — `_source`, `track_total_hits`, `search_after`, `pit`, `collapse`, `rescore`, `suggest` (term / phrase / completion), `runtime_mappings`, `script_fields`, `docvalue_fields`, `stored_fields`, `terminate_after`, `timeout`, `profile`, `stats`, `ext` +- **Response mapping** — automatic mapping of Elasticsearch responses (including composite/named buckets, IP/date range buckets) to typed objects +- **Index mapping** — index settings, analyzers, tokenizers, filters + +See [CHANGELOG.md](CHANGELOG.md) for the full list of types and arguments added in v2. + +## Requirements + +- PHP 8.2 or higher + +## Installation + +Install via [Composer](http://getcomposer.org/): ```sh -$ composer require spameri/elastic-query +composer require spameri/elastic-query +``` + +## Quick Start + +```php +use Spameri\ElasticQuery\ElasticQuery; +use Spameri\ElasticQuery\Query\ElasticMatch; +use Spameri\ElasticQuery\Query\Term; + +// Create a query +$query = new ElasticQuery(); + +// Add a must query (AND condition) +$query->addMustQuery(new ElasticMatch('title', 'Elasticsearch')); + +// Add a filter (cached, no scoring) +$query->addFilter(new Term('status', 'published')); + +// Set pagination +$query->options()->changeSize(10); +$query->options()->changeFrom(0); + +// Convert to array for Elasticsearch client +$body = $query->toArray(); ``` -Documentation ------------- +## Documentation + +Learn more in the [documentation](https://github.com/Spameri/ElasticQuery/tree/master/doc): + +- [Usage](https://github.com/Spameri/ElasticQuery/tree/master/doc/01-usage.md) - Integration examples +- [Query Objects](https://github.com/Spameri/ElasticQuery/tree/master/doc/02-query-objects.md) - All query types +- [Aggregation Objects](https://github.com/Spameri/ElasticQuery/tree/master/doc/03-aggregation-objects.md) - Aggregation types +- [Result Objects](https://github.com/Spameri/ElasticQuery/tree/master/doc/04-result-objects.md) - Response mapping +- [Options & Sorting](https://github.com/Spameri/ElasticQuery/tree/master/doc/05-options.md) - Pagination, sorting, scroll +- [Highlight & Function Score](https://github.com/Spameri/ElasticQuery/tree/master/doc/06-highlight-function-score.md) - Highlighting and custom scoring +- [Mapping & Settings](https://github.com/Spameri/ElasticQuery/tree/master/doc/07-mapping-settings.md) - Index configuration -Learn more in the [documentation](https://github.com/Spameri/ElasticQuery/tree/master/doc). +## License -- [Usage](https://github.com/Spameri/ElasticQuery/tree/master/doc/01-usage.md) -- [Query objects](https://github.com/Spameri/ElasticQuery/tree/master/doc/02-query-objects.md) -- [Aggregation objects](https://github.com/Spameri/ElasticQuery/tree/master/doc/03-aggregation-objects.md) -- [Result objects](https://github.com/Spameri/ElasticQuery/tree/master/doc/04-result-objects.md) +MIT diff --git a/composer.json b/composer.json index c426a97..d956f49 100644 --- a/composer.json +++ b/composer.json @@ -11,16 +11,16 @@ "issues": "https://github.com/Spameri/ElasticQuery/issues" }, "require": { - "php": ">=7.4", + "php": ">=8.2", "ext-json": "*" }, "require-dev": { "ext-curl": "*", - "phpstan/phpstan": "^1.4.6", - "nette/tester": "v2.3.1", - "elasticsearch/elasticsearch": "^7", - "guzzlehttp/guzzle": "^6.3", - "spameri/coding-standard": "@dev" + "phpstan/phpstan": "^1.11.3", + "nette/tester": "v2.5.7", + "elasticsearch/elasticsearch": "^9.0.0", + "guzzlehttp/guzzle": "^7.0", + "slevomat/coding-standard": "^8.0" }, "autoload": { "psr-4": { @@ -31,5 +31,14 @@ "psr-4": { "SpameriTests\\": "tests/SpameriTests" } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "php-http/discovery": true + } + }, + "brach-alias": { + "dev-master": "2.0.0" } } diff --git a/doc/01-usage.md b/doc/01-usage.md index 1e4501d..61d99db 100644 --- a/doc/01-usage.md +++ b/doc/01-usage.md @@ -1,79 +1,183 @@ -# Using with [ElasticSearch/ElasticSearch](https://github.com/elastic/elasticsearch-php) -First we need to prepare query for what we want to search. +# Usage +This guide shows how to use ElasticQuery with different Elasticsearch clients. + +## Basic Concepts + +`\Spameri\ElasticQuery\ElasticQuery` is the main entry point for building queries. It composes: + +- **query()** - Boolean query context (must, should, mustNot) - affects scoring +- **filter()** - Filter context - cached queries that don't affect scoring +- **aggregation()** - Aggregation definitions +- **options()** - Pagination, sorting, scroll settings +- **highlight()** - Search result highlighting +- **functionScore()** - Custom scoring functions + +Every query object implements `\Spameri\ElasticQuery\Query\LeafQueryInterface` and can be nested. Collections (MustCollection, ShouldCollection, MustNotCollection) also implement this interface, enabling complex nested boolean logic. + +## Building Queries + +### Simple Query +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); +$query->addMustQuery( + new \Spameri\ElasticQuery\Query\ElasticMatch('name', 'Avengers') +); +``` + +### Boolean Query with Multiple Conditions +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Must match (AND) +$query->addMustQuery(new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'elasticsearch')); + +// Should match (OR) - boosts score if matched +$query->addShouldQuery(new \Spameri\ElasticQuery\Query\Term('category', 'tutorial')); + +// Must not match (NOT) +$query->addMustNotQuery(new \Spameri\ElasticQuery\Query\Term('status', 'draft')); +``` + +### Using Filters (No Scoring, Cached) +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Filters don't affect scoring and are cached +$query->addFilter(new \Spameri\ElasticQuery\Query\Term('status', 'published')); +$query->addFilter(new \Spameri\ElasticQuery\Query\Range('date', gte: '2024-01-01')); +``` + +### Nested Boolean Logic +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Create a nested should collection +$shouldCollection = new \Spameri\ElasticQuery\Query\ShouldCollection(); +$shouldCollection->add(new \Spameri\ElasticQuery\Query\Term('category', 'books')); +$shouldCollection->add(new \Spameri\ElasticQuery\Query\Term('category', 'movies')); + +// Add the nested collection as a must condition +$query->query()->must()->add($shouldCollection); +``` + +### With Aggregations ```php $query = new \Spameri\ElasticQuery\ElasticQuery(); -$query->query()->must()->add( - new \Spameri\ElasticQuery\Query\ElasticMatch( - 'name', - 'Avengers' +$query->addMustQuery(new \Spameri\ElasticQuery\Query\ElasticMatch('content', 'search')); + +// Add term aggregation +$query->addAggregation( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'categories', + new \Spameri\ElasticQuery\Aggregation\Term('category') ) ); ``` -`\Spameri\ElasticQuery\ElasticQuery` is base for every query you want to perform. -- It has must, should sections. -- Every must, should section is collection of `\Spameri\ElasticQuery\Query\LeafQueryInterface` -item and should, must collection are also implementations of **LeafQueryInterface** so it allows you to nest them as you need. +### With Pagination and Sorting +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Set pagination +$query->options()->changeSize(20); +$query->options()->changeFrom(40); // Skip first 40 results + +// Add sorting +$query->options()->sort()->add( + new \Spameri\ElasticQuery\Options\Sort('date', \Spameri\ElasticQuery\Options\Sort::DESC) +); +``` + +--- -Next we need to set up document to send to elasticsearch library. +# Using with [Elasticsearch/Elasticsearch](https://github.com/elastic/elasticsearch-php) + +Set up the document to send to the Elasticsearch library: ```php $document = new \Spameri\ElasticQuery\Document( 'spameri_video', - new \Spameri\ElasticQuery\Document\Body\Plain($elasticQuery->toArray()) + new \Spameri\ElasticQuery\Document\Body\Plain($query->toArray()) ); ``` -- Document is very straightforward, you need to specify index in first argument and document body in second argument -third argument is type if needed by you ElasticSearch version, also fourth is ID. -Last you need to send document to ElasticSearch client. +The Document constructor accepts: +1. Index name (required) +2. Body object (required) +3. Type (optional, for older Elasticsearch versions) +4. ID (optional, for single document operations) + +Send the document to the Elasticsearch client: ```php -$response = \Elasticsearch\Client::search( - $document->toArray() -); +$response = $client->search($document->toArray()); ``` -Elasticsearch library will return array as response. You can map this response to object like this: +--- + +# Response Mapping + +Elasticsearch returns array responses. Map them to typed objects: + +### Automatic Mapping ```php $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); $resultObject = $resultMapper->map($response); ``` -Object is implementation of `\Spameri\ElasticQuery\Response\ResultInterface` depending on your type of query. -Or you can map result to specific object by calling direct mapping methods. -- For single result you get by searching by ID. (as parameter in document object) +The mapper auto-detects the response type and returns the appropriate `ResultInterface` implementation. + +### Specific Mapping Methods + +For single document retrieval (by ID): +```php +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSingleResult($response); +// Returns: ResultSingle +``` + +For search responses with multiple hits: ```php -\Spameri\ElasticQuery\Response\ResultMapper::mapSingleResult($response); +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSearchResults($response); +// Returns: ResultSearch with hits and aggregations ``` -- For search response with multiple hits. + +For bulk operation results: ```php -\Spameri\ElasticQuery\Response\ResultMapper::mapSearchResults($response); +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapBulkResult($response); +// Returns: ResultBulk with action statuses ``` -- For bulk result, where you have information about bulk actions. + +For cluster version info: ```php -\Spameri\ElasticQuery\Response\ResultMapper::mapBulkResult($response); +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapVersionResult($response); +// Returns: ResultVersion ``` +--- + # Using with Spameri/Elastic -This is implementation for Nette framework, so you need to register it to your application accordingly. -[How to here](https://github.com/Spameri/Elastic/blob/master/doc/01_intro.md#1-config-elasticsearch) -Then you inject client provider where you need and do query like this. +This is an implementation for Nette framework. [Setup guide](https://github.com/Spameri/Elastic/blob/master/doc/01_intro.md#1-config-elasticsearch) + ```php $response = $this->clientProvider->client()->search( $document->toArray() ); ``` -Response mapping is same, so is constructing query. -For more advanced use see Spameri/Elastic [documentation](https://github.com/Spameri/Elastic/blob/master/doc/01_intro.md) +Query construction and response mapping are the same. For advanced usage, see the [Spameri/Elastic documentation](https://github.com/Spameri/Elastic/blob/master/doc/01_intro.md). + +--- # Using with Guzzle -Difference is you dont need document object. You have to specify index and type in requested url by yourself. + +With Guzzle, specify the index in the URL directly: ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://localhost:9200/spameri_video/_search', [ - 'body' => \json_encode($elasticQuery->toArray()) + 'body' => \json_encode($query->toArray()), + 'headers' => ['Content-Type' => 'application/json'], ]); + +$data = \json_decode($response->getBody()->getContents(), true); +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSearchResults($data); ``` -Rest is same. Setting up query and mapping to result object. diff --git a/doc/02-query-objects.md b/doc/02-query-objects.md index bc058f2..12b76f7 100644 --- a/doc/02-query-objects.md +++ b/doc/02-query-objects.md @@ -1,68 +1,905 @@ -# Query objects +# Query Objects -Every object is as close to documentation as possible. Also reference provided in doc blocks where possible. -[Like this](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Match.php#L7) +Every query object implements `\Spameri\ElasticQuery\Query\LeafQueryInterface` and is capable of converting to an array for Elasticsearch. -Every query object implements `\Spameri\ElasticQuery\Query\LeafQueryInterface` and is capable of converting to array. +Collections (MustCollection, ShouldCollection, MustNotCollection) also implement **LeafQueryInterface**, allowing nested boolean logic. -Must and should collection of objects is also **LeafQueryInterface**, so you can nest rules as you need. +--- + +## Full-text Queries -## Implementations ##### Match Query -- Class `\Spameri\ElasticQuery\Query\Match` +Full-text search with analysis on the query string. +- Class: `\Spameri\ElasticQuery\Query\ElasticMatch` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) -- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Match.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Query/Match.phpt#L8) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/ElasticMatch.php) + +```php +new \Spameri\ElasticQuery\Query\ElasticMatch( + field: 'title', + query: 'quick brown fox', + boost: 1.0, + fuzziness: new \Spameri\ElasticQuery\Query\Match\Fuzziness('AUTO'), + operator: \Spameri\ElasticQuery\Query\Match\Operator::AND, +); +``` ##### MatchPhrase Query -- Class `\Spameri\ElasticQuery\Query\MatchPhrase` +Matches exact phrases in order. +- Class: `\Spameri\ElasticQuery\Query\MatchPhrase` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MatchPhrase.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Query/MatchPhrase.phpt#L8) + +```php +new \Spameri\ElasticQuery\Query\MatchPhrase( + field: 'content', + query: 'quick brown fox', + boost: 1.0, + slop: 2, // Allow 2 words between terms +); +``` + +##### PhrasePrefix Query +Matches phrases with prefix matching on the last term. Useful for autocomplete. +- Class: `\Spameri\ElasticQuery\Query\PhrasePrefix` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/PhrasePrefix.php) + +```php +new \Spameri\ElasticQuery\Query\PhrasePrefix( + field: 'title', + queryString: 'quick bro', // Matches "quick brown", "quick brother", etc. + boost: 1, + slop: 1, +); +``` + +##### MultiMatch Query +Search across multiple fields with different matching strategies. +- Class: `\Spameri\ElasticQuery\Query\MultiMatch` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MultiMatch.php) + +```php +new \Spameri\ElasticQuery\Query\MultiMatch( + fields: ['title^2', 'content', 'tags'], // title has 2x boost + query: 'elasticsearch guide', + boost: 1.0, + fuzziness: null, + type: \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + minimumShouldMatch: 2, + operator: \Spameri\ElasticQuery\Query\Match\Operator::OR, + analyzer: 'standard', +); +``` + +MultiMatch types: `BEST_FIELDS`, `MOST_FIELDS`, `CROSS_FIELDS`, `PHRASE`, `PHRASE_PREFIX`, `BOOL_PREFIX` ##### Fuzzy Query -- Class `\Spameri\ElasticQuery\Query\Fuzzy` +Matches terms similar to the search term using Levenshtein distance. +- Class: `\Spameri\ElasticQuery\Query\Fuzzy` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Fuzzy.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Query/Fuzzy.phpt#L8) -##### Range Query -- Class `\Spameri\ElasticQuery\Query\Range` -- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html) -- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Range.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Query/Range.phpt#L8) +```php +new \Spameri\ElasticQuery\Query\Fuzzy( + field: 'name', + value: 'elasticsearh', // Typo - still matches "elasticsearch" +); +``` + +##### Intervals Query +Rule-based proximity / ordered-term matching (match, prefix, wildcard, fuzzy, all_of, any_of, with `max_gaps`, `ordered`, filters). +- Class: `\Spameri\ElasticQuery\Query\Intervals` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-intervals-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Intervals.php) + +```php +new \Spameri\ElasticQuery\Query\Intervals( + field: 'my_text', + rule: [ + 'match' => [ + 'query' => 'my favorite food', + 'max_gaps' => 0, + 'ordered' => true, + ], + ], +); +``` + +##### SimpleQueryString Query +User-safe Lucene-lite syntax (does not error on invalid input). +- Class: `\Spameri\ElasticQuery\Query\SimpleQueryString` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SimpleQueryString.php) + +```php +new \Spameri\ElasticQuery\Query\SimpleQueryString( + query: 'foo + bar -baz', + fields: ['title^2', 'body'], +); +``` + +##### QueryString Query +Lucene-syntax query — supports boolean operators, wildcards, regex, fielded search. +- Class: `\Spameri\ElasticQuery\Query\QueryString` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/QueryString.php) + +```php +new \Spameri\ElasticQuery\Query\QueryString( + query: '(new york city) OR (big apple)', + defaultField: 'content', +); +``` + +##### CombinedFields Query +BM25-aware multi-field full-text search that treats fields as one combined field. +- Class: `\Spameri\ElasticQuery\Query\CombinedFields` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-combined-fields-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/CombinedFields.php) + +```php +new \Spameri\ElasticQuery\Query\CombinedFields( + fields: ['title', 'abstract', 'body'], + query: 'distributed search', + operator: 'and', +); +``` + +##### MatchBoolPrefix Query +Match where the final term is treated as a prefix and the rest as match terms. +- Class: `\Spameri\ElasticQuery\Query\MatchBoolPrefix` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-bool-prefix-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MatchBoolPrefix.php) + +```php +new \Spameri\ElasticQuery\Query\MatchBoolPrefix( + field: 'message', + query: 'quick brown f', +); +``` + +--- + +## Term-level Queries ##### Term Query -- Class `\Spameri\ElasticQuery\Query\Term` +Exact match on a field value (not analyzed). +- Class: `\Spameri\ElasticQuery\Query\Term` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Term.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Query/Term.phpt#L8) + +```php +new \Spameri\ElasticQuery\Query\Term( + field: 'status', + value: 'published', + boost: 1.0, +); +``` ##### Terms Query -- Class `\Spameri\ElasticQuery\Query\Terms` +Match any of multiple exact values, or fetch values from another document. +- Class: `\Spameri\ElasticQuery\Query\Terms` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Terms.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Query/Terms.phpt#L8) + +```php +// Inline values +new \Spameri\ElasticQuery\Query\Terms( + field: 'category', + query: ['books', 'movies', 'music'], +); + +// terms_lookup — values pulled from another document +new \Spameri\ElasticQuery\Query\Terms( + field: 'user_id', + query: new \Spameri\ElasticQuery\Query\TermsLookup( + index: 'users', + id: '42', + path: 'friends', + routing: null, + ), +); +``` + +##### Range Query +Match values within a specified range. +- Class: `\Spameri\ElasticQuery\Query\Range` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Range.php) + +```php +new \Spameri\ElasticQuery\Query\Range( + field: 'price', + gte: 10, // Greater than or equal + lte: 100, // Less than or equal + gt: null, // Greater than (exclusive) + lt: null, // Less than (exclusive) + boost: 1.0, +); + +// Date range example +new \Spameri\ElasticQuery\Query\Range( + field: 'created_at', + gte: '2024-01-01', + lte: 'now', +); +``` + +##### Exists Query +Match documents where a field has a value. +- Class: `\Spameri\ElasticQuery\Query\Exists` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Exists.php) + +```php +new \Spameri\ElasticQuery\Query\Exists(field: 'description'); +``` + +##### Ids Query +Fetch documents by their `_id`. +- Class: `\Spameri\ElasticQuery\Query\Ids` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Ids.php) + +```php +new \Spameri\ElasticQuery\Query\Ids(values: ['1', '2', '3']); +``` + +##### Prefix Query +Match terms that start with a given prefix. +- Class: `\Spameri\ElasticQuery\Query\Prefix` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Prefix.php) + +```php +new \Spameri\ElasticQuery\Query\Prefix( + field: 'user', + query: 'ki', + caseInsensitive: true, +); +``` + +##### Regexp Query +Match terms against a regular expression (Lucene syntax). +- Class: `\Spameri\ElasticQuery\Query\Regexp` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Regexp.php) + +```php +new \Spameri\ElasticQuery\Query\Regexp( + field: 'user', + query: 'k.*y', + flags: 'ALL', + caseInsensitive: true, +); +``` + +##### TermSet Query +Match documents containing at least N of M provided terms (N defined by a field or script). +- Class: `\Spameri\ElasticQuery\Query\TermSet` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-set-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/TermSet.php) + +```php +new \Spameri\ElasticQuery\Query\TermSet( + field: 'programming_languages', + terms: ['c++', 'java', 'php'], + minimumShouldMatchField: 'required_matches', +); +``` ##### WildCard Query -- Class `\Spameri\ElasticQuery\Query\WildCard` +Match using wildcard patterns (* and ?). +- Class: `\Spameri\ElasticQuery\Query\WildCard` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/WildCard.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Query/WildCard.phpt#L8) -##### QueryCollection Query -- Class `\Spameri\ElasticQuery\Query\QueryCollection` +```php +new \Spameri\ElasticQuery\Query\WildCard( + field: 'email', + value: '*@example.com', // Matches any email ending with @example.com +); +``` + +--- + +## Compound Queries + +Wrap other queries to combine, filter, or modify their scoring. + +##### ConstantScore Query +Wraps a filter so all matching docs share the same score. +- Class: `\Spameri\ElasticQuery\Query\ConstantScore` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/ConstantScore.php) + +```php +new \Spameri\ElasticQuery\Query\ConstantScore( + filter: new \Spameri\ElasticQuery\Query\Term('status', 'active'), + boost: 1.2, +); +``` + +##### DisMax Query +Best-of-many — returns the highest-scoring sub-query per document, with `tie_breaker` for the rest. +- Class: `\Spameri\ElasticQuery\Query\DisMax` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/DisMax.php) + +```php +$disMax = new \Spameri\ElasticQuery\Query\DisMax( + query: new \Spameri\ElasticQuery\Query\Term('title', 'foo'), + tieBreaker: 0.7, +); +$disMax->addQuery(new \Spameri\ElasticQuery\Query\Term('body', 'foo')); +``` + +##### Boosting Query +Match `positive` docs but lower the score of `negative` matches. +- Class: `\Spameri\ElasticQuery\Query\Boosting` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Boosting.php) + +```php +new \Spameri\ElasticQuery\Query\Boosting( + positive: new \Spameri\ElasticQuery\Query\Term('text', 'apple'), + negative: new \Spameri\ElasticQuery\Query\Term('text', 'pie'), + negativeBoost: 0.5, +); +``` + +--- + +## Joining Queries + +Queries that traverse parent/child or join relationships. + +##### ParentId Query +Match children that belong to a parent with a known id. +- Class: `\Spameri\ElasticQuery\Query\ParentId` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-parent-id-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/ParentId.php) + +```php +new \Spameri\ElasticQuery\Query\ParentId(type: 'comment', id: '1'); +``` + +##### HasParent Query +Match children whose parent matches the inner query. +- Class: `\Spameri\ElasticQuery\Query\HasParent` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/HasParent.php) + +```php +new \Spameri\ElasticQuery\Query\HasParent( + parentType: 'blog', + query: new \Spameri\ElasticQuery\Query\Term('tag', 'tech'), + score: true, +); +``` + +##### HasChild Query +Match parents whose children match the inner query. +- Class: `\Spameri\ElasticQuery\Query\HasChild` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/HasChild.php) + +```php +new \Spameri\ElasticQuery\Query\HasChild( + type: 'comment', + query: new \Spameri\ElasticQuery\Query\Term('author', 'john'), + scoreMode: 'max', + minChildren: 1, +); +``` + +--- + +## Specialized Queries + +##### MatchAll Query +Matches all documents in the index. Useful as a base query for filtering or function scoring. +- Class: `\Spameri\ElasticQuery\Query\MatchAll` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MatchAll.php) + +```php +// Match all documents +new \Spameri\ElasticQuery\Query\MatchAll(); + +// Match all with custom boost +new \Spameri\ElasticQuery\Query\MatchAll(boost: 1.5); +``` + +##### MatchNone Query +Matches zero documents — useful as a placeholder. +- Class: `\Spameri\ElasticQuery\Query\MatchNone` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html#query-dsl-match-none-query) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MatchNone.php) + +```php +new \Spameri\ElasticQuery\Query\MatchNone(); +``` + +##### Nested Query +Query nested objects with their own scope. +- Class: `\Spameri\ElasticQuery\Query\Nested` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Nested.php) + +```php +$nested = new \Spameri\ElasticQuery\Query\Nested( + path: 'comments', + scoreMode: \Spameri\ElasticQuery\Query\Nested::SCORE_MODE_AVG, // optional + ignoreUnmapped: false, // optional + innerHits: new \Spameri\ElasticQuery\Query\InnerHits( // optional + name: 'matched_comments', + size: 5, + ), +); +$nested->getQuery()->must()->add( + new \Spameri\ElasticQuery\Query\Term('comments.author', 'john') +); +``` + +##### Knn Query +k-nearest neighbour vector similarity search. +- Class: `\Spameri\ElasticQuery\Query\Knn` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Knn.php) + +```php +new \Spameri\ElasticQuery\Query\Knn( + field: 'vector', + queryVector: [1.0, 2.0, 3.0], + k: 5, + numCandidates: 50, + similarity: 0.7, // optional + filter: new \Spameri\ElasticQuery\Query\Term('status', 'on'), // optional + boost: 1.0, +); +``` + +##### SparseVector Query +Sparse vector / ELSER-style query. +- Class: `\Spameri\ElasticQuery\Query\SparseVector` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html) + +```php +// Via inference endpoint +new \Spameri\ElasticQuery\Query\SparseVector( + field: 'tokens', + inferenceId: '.elser_model_2', + query: 'big cat', +); + +// Via pre-computed tokens +new \Spameri\ElasticQuery\Query\SparseVector( + field: 'tokens', + queryVector: ['lion' => 0.5, 'tiger' => 0.7], +); +``` + +##### Semantic Query +Query a `semantic_text` field. +- Class: `\Spameri\ElasticQuery\Query\Semantic` + +```php +new \Spameri\ElasticQuery\Query\Semantic(field: 'inference_field', query: 'large cat'); +``` + +##### TextExpansion Query +Legacy ELSER (`model_id`/`model_text`). +- Class: `\Spameri\ElasticQuery\Query\TextExpansion` + +```php +new \Spameri\ElasticQuery\Query\TextExpansion( + field: 'tokens', + modelId: '.elser_model_2', + modelText: 'big cat', +); +``` + +##### RuleQuery +Apply Search Application query rules over an organic query. +- Class: `\Spameri\ElasticQuery\Query\RuleQuery` + +```php +new \Spameri\ElasticQuery\Query\RuleQuery( + organic: new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'puggles'), + rulesetIds: ['my-ruleset'], + matchCriteria: ['query_string' => 'puggles'], +); +``` + +##### WeightedTokens Query +Token weights against a sparse_vector field. +- Class: `\Spameri\ElasticQuery\Query\WeightedTokens` + +```php +new \Spameri\ElasticQuery\Query\WeightedTokens( + field: 'tokens', + tokens: ['lion' => 0.5, 'tiger' => 0.7], +); +``` + +##### GeoDistance Query +Match documents within a geographic distance from a point. +- Class: `\Spameri\ElasticQuery\Query\GeoDistance` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-distance-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/GeoDistance.php) + +```php +new \Spameri\ElasticQuery\Query\GeoDistance( + field: 'location', + lat: 40.7128, + lon: -74.0060, + distance: '50km', + distanceType: 'arc', // optional: 'arc' | 'plane' + validationMethod: 'STRICT', // optional: 'STRICT' | 'COERCE' | 'IGNORE_MALFORMED' + ignoreUnmapped: false, // optional + boost: 1.0, +); +``` + +##### Shape Query +Same as `geo_shape` but for Cartesian `shape` fields (non-geographic plane). +- Class: `\Spameri\ElasticQuery\Query\Shape` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-shape-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Shape.php) + +```php +new \Spameri\ElasticQuery\Query\Shape( + field: 'geometry', + shape: ['type' => 'envelope', 'coordinates' => [[0, 100], [100, 0]]], + relation: 'intersects', +); +``` + +##### GeoShape Query +Match `geo_shape`-indexed documents against an arbitrary geometry. +- Class: `\Spameri\ElasticQuery\Query\GeoShape` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/GeoShape.php) + +```php +new \Spameri\ElasticQuery\Query\GeoShape( + field: 'location', + shape: [ + 'type' => 'envelope', + 'coordinates' => [[13.0, 53.0], [14.0, 52.0]], + ], + relation: 'within', // intersects | disjoint | within | contains +); +``` + +##### GeoBoundingBox Query +Match documents whose geo_point falls inside a top-left/bottom-right rectangle. +- Class: `\Spameri\ElasticQuery\Query\GeoBoundingBox` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-bounding-box-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/GeoBoundingBox.php) + +```php +new \Spameri\ElasticQuery\Query\GeoBoundingBox( + field: 'location', + topLeftLat: 40.73, + topLeftLon: -74.1, + bottomRightLat: 40.01, + bottomRightLon: -71.12, +); +``` + +--- + +##### ScriptScore Query +Re-score matching documents using a Painless script. +- Class: `\Spameri\ElasticQuery\Query\ScriptScore` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/ScriptScore.php) + +```php +new \Spameri\ElasticQuery\Query\ScriptScore( + query: new \Spameri\ElasticQuery\Query\MatchAll(), + source: "doc['my_field'].value * 2", + minScore: 0.5, +); +``` + +##### MoreLikeThis Query +Find documents similar to provided text or document references. +- Class: `\Spameri\ElasticQuery\Query\MoreLikeThis` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-mlt-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MoreLikeThis.php) + +```php +new \Spameri\ElasticQuery\Query\MoreLikeThis( + fields: ['title', 'body'], + like: ['quick brown fox', ['_index' => 'imdb', '_id' => '1']], + minTermFreq: 1, + maxQueryTerms: 12, +); +``` + +##### RankFeature Query +Boost documents by a `rank_feature` field (saturation/log/sigmoid/linear functions). +- Class: `\Spameri\ElasticQuery\Query\RankFeature` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-rank-feature-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/RankFeature.php) + +```php +new \Spameri\ElasticQuery\Query\RankFeature( + field: 'pagerank', + function: ['saturation' => ['pivot' => 8]], +); +``` + +##### DistanceFeature Query +Boost documents whose date or geo_point is close to an origin. +- Class: `\Spameri\ElasticQuery\Query\DistanceFeature` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-distance-feature-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/DistanceFeature.php) + +```php +new \Spameri\ElasticQuery\Query\DistanceFeature( + field: 'production_date', + origin: 'now', + pivot: '7d', +); +``` + +##### Pinned Query +Promote specific document IDs above an organic query's results. +- Class: `\Spameri\ElasticQuery\Query\Pinned` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-pinned-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Pinned.php) + +```php +new \Spameri\ElasticQuery\Query\Pinned( + organic: new \Spameri\ElasticQuery\Query\ElasticMatch('content', 'elasticsearch'), + ids: ['1', '4', '100'], +); +``` + +##### Percolate Query +Match a document against stored queries (reverse search). +- Class: `\Spameri\ElasticQuery\Query\Percolate` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-percolate-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Percolate.php) + +```php +new \Spameri\ElasticQuery\Query\Percolate( + field: 'query', + document: ['message' => 'A new bonsai tree'], +); +``` + +##### Wrapper Query +Embed a raw JSON query string (auto-base64-encoded) — escape hatch for unsupported syntax. +- Class: `\Spameri\ElasticQuery\Query\Wrapper` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wrapper-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Wrapper.php) + +```php +new \Spameri\ElasticQuery\Query\Wrapper('{"term":{"user":{"value":"kimchy"}}}'); +``` + +##### Script Query +Filter documents with a Painless boolean script. +- Class: `\Spameri\ElasticQuery\Query\Script` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Script.php) + +```php +new \Spameri\ElasticQuery\Query\Script( + source: "doc['amount'].value > params.threshold", + params: ['threshold' => 100], +); +``` + +--- + +## Span Queries + +Positional matching — useful for term-position-aware searches. Span clauses can be composed via the `*_near`, `*_or`, etc. queries. + +##### SpanFirst Query +Match a span only if it appears within the first N positions. +- Class: `\Spameri\ElasticQuery\Query\SpanFirst` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-first-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanFirst.php) + +```php +new \Spameri\ElasticQuery\Query\SpanFirst( + match: new \Spameri\ElasticQuery\Query\SpanTerm('user', 'kimchy'), + end: 3, +); +``` + +##### SpanNear Query +Match multiple span clauses within `slop` positions of each other. +- Class: `\Spameri\ElasticQuery\Query\SpanNear` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-near-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanNear.php) + +```php +$span = new \Spameri\ElasticQuery\Query\SpanNear( + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value1'), + slop: 12, + inOrder: false, +); +$span->addClause(new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value2')); +``` + +##### SpanOr Query +Match any of several span clauses. +- Class: `\Spameri\ElasticQuery\Query\SpanOr` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-or-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanOr.php) + +```php +$span = new \Spameri\ElasticQuery\Query\SpanOr( + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value1'), +); +$span->addClause(new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value2')); +``` + +##### SpanNot Query +Match `include` spans not overlapping `exclude` spans. +- Class: `\Spameri\ElasticQuery\Query\SpanNot` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-not-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanNot.php) + +```php +new \Spameri\ElasticQuery\Query\SpanNot( + include: new \Spameri\ElasticQuery\Query\SpanTerm('field', 'hot'), + exclude: new \Spameri\ElasticQuery\Query\SpanTerm('field', 'dog'), + pre: 0, + post: 1, +); +``` + +##### SpanContaining Query +Match `big` spans that fully enclose `little` spans. +- Class: `\Spameri\ElasticQuery\Query\SpanContaining` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-containing-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanContaining.php) + +```php +new \Spameri\ElasticQuery\Query\SpanContaining( + big: $bigSpan, + little: $littleSpan, +); +``` + +##### SpanWithin Query +Match `little` spans that are fully enclosed within `big` spans. +- Class: `\Spameri\ElasticQuery\Query\SpanWithin` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-within-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanWithin.php) + +```php +new \Spameri\ElasticQuery\Query\SpanWithin( + big: $bigSpan, + little: $littleSpan, +); +``` + +##### SpanMulti Query +Wrap a multi-term query (prefix/wildcard/regexp/fuzzy/range) so it can be used inside other span queries. +- Class: `\Spameri\ElasticQuery\Query\SpanMulti` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-multi-term-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanMulti.php) + +```php +new \Spameri\ElasticQuery\Query\SpanMulti( + match: new \Spameri\ElasticQuery\Query\Prefix(field: 'user', query: 'ki'), +); +``` + +##### FieldMaskingSpan Query +Mask a span clause to act on a different field — enables span queries across multiple fields. +- Class: `\Spameri\ElasticQuery\Query\FieldMaskingSpan` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-field-masking-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/FieldMaskingSpan.php) + +```php +new \Spameri\ElasticQuery\Query\FieldMaskingSpan( + query: new \Spameri\ElasticQuery\Query\SpanTerm('text.stems', 'fox'), + field: 'text', +); +``` + +##### SpanTerm Query +Match a single term in a span-aware way. +- Class: `\Spameri\ElasticQuery\Query\SpanTerm` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-term-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanTerm.php) + +```php +new \Spameri\ElasticQuery\Query\SpanTerm(field: 'text', query: 'quick'); +``` + +--- + +## Boolean Query Collections + +These collections implement `LeafQueryInterface` and can be nested arbitrarily. + +##### QueryCollection +Container for must, should, and mustNot sub-queries. +- Class: `\Spameri\ElasticQuery\Query\QueryCollection` - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/QueryCollection.php) -- This is query for nesting your must, must_not and should sub queries. -##### MustCollection Query -- Class `\Spameri\ElasticQuery\Query\MustCollection` +```php +$queryCollection = new \Spameri\ElasticQuery\Query\QueryCollection(); +$queryCollection->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); +$queryCollection->should()->add(new \Spameri\ElasticQuery\Query\Term('featured', true)); +$queryCollection->mustNot()->add(new \Spameri\ElasticQuery\Query\Term('deleted', true)); +``` + +##### MustCollection +AND logic - all queries must match. +- Class: `\Spameri\ElasticQuery\Query\MustCollection` - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MustCollection.php) -##### MustNotCollection Query -- Class `\Spameri\ElasticQuery\Query\MustNotCollection` +##### ShouldCollection +OR logic - at least one query should match (affects scoring). +- Class: `\Spameri\ElasticQuery\Query\ShouldCollection` +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/ShouldCollection.php) + +##### MustNotCollection +NOT logic - queries must not match. +- Class: `\Spameri\ElasticQuery\Query\MustNotCollection` - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MustNotCollection.php) -##### ShouldCollection Query -- Class `\Spameri\ElasticQuery\Query\ShouldCollection` -- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/ShouldCollection.php) +### Nested Boolean Example + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Must be published AND (category = books OR category = movies) +$query->addFilter(new \Spameri\ElasticQuery\Query\Term('status', 'published')); + +$categoryShould = new \Spameri\ElasticQuery\Query\ShouldCollection(); +$categoryShould->add(new \Spameri\ElasticQuery\Query\Term('category', 'books')); +$categoryShould->add(new \Spameri\ElasticQuery\Query\Term('category', 'movies')); + +$query->query()->must()->add($categoryShould); +``` + +--- + +## Helper Classes + +##### Fuzziness +Configure fuzzy matching behavior. +- Class: `\Spameri\ElasticQuery\Query\Match\Fuzziness` + +```php +new \Spameri\ElasticQuery\Query\Match\Fuzziness('AUTO'); // Auto-detect based on term length +new \Spameri\ElasticQuery\Query\Match\Fuzziness('2'); // Allow 2 edits +``` + +##### Operator +Boolean operator for multi-term queries. +- Class: `\Spameri\ElasticQuery\Query\Match\Operator` + +```php +\Spameri\ElasticQuery\Query\Match\Operator::AND // All terms must match +\Spameri\ElasticQuery\Query\Match\Operator::OR // Any term can match (default) +``` + +##### MultiMatchType +Matching strategy for multi-match queries. +- Class: `\Spameri\ElasticQuery\Query\Match\MultiMatchType` + +```php +\Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS // Default - highest score from any field +\Spameri\ElasticQuery\Query\Match\MultiMatchType::MOST_FIELDS // Combine scores from all fields +\Spameri\ElasticQuery\Query\Match\MultiMatchType::CROSS_FIELDS // Treat fields as one big field +\Spameri\ElasticQuery\Query\Match\MultiMatchType::PHRASE // Run match_phrase on each field +\Spameri\ElasticQuery\Query\Match\MultiMatchType::PHRASE_PREFIX // Run match_phrase_prefix on each field +\Spameri\ElasticQuery\Query\Match\MultiMatchType::BOOL_PREFIX // Create bool query with term/prefix queries +``` diff --git a/doc/03-aggregation-objects.md b/doc/03-aggregation-objects.md index 812b64a..1cafa7b 100644 --- a/doc/03-aggregation-objects.md +++ b/doc/03-aggregation-objects.md @@ -1,34 +1,804 @@ -# Aggregation objects +# Aggregation Objects -Every object is as close to documentation as possible. Also reference provided in doc blocks where possible. -[Like this](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Term.php#L7) +Every aggregation object implements `\Spameri\ElasticQuery\Aggregation\LeafAggregationInterface` and is capable of converting to an array. -Every aggregation object implements `\Spameri\ElasticQuery\Aggregation\LeafAggregationInterface` and is capable of converting to array. +Aggregations are wrapped in `LeafAggregationCollection` which allows nesting sub-aggregations. -AggregationCollection is also **LeafAggregationInterface**, so you can nest as you need. +--- + +## Using Aggregations + +Aggregations are added to the query using `LeafAggregationCollection`: + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Add a simple term aggregation +$query->addAggregation( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'categories', // Aggregation name + new \Spameri\ElasticQuery\Aggregation\Term('category') // Aggregation definition + ) +); +``` + +### Nested Aggregations + +```php +// Term aggregation with nested avg sub-aggregation +$categoryAgg = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'by_category', + new \Spameri\ElasticQuery\Aggregation\Term('category') +); + +// Add sub-aggregation +$categoryAgg->subAggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + new \Spameri\ElasticQuery\Aggregation\Avg('price') + ) +); + +$query->addAggregation($categoryAgg); +``` + +--- + +## Metric Aggregations + +Compute metrics over a set of documents. + +##### Min Aggregation +Returns the minimum value of a numeric field. +- Class: `\Spameri\ElasticQuery\Aggregation\Min` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Min.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Min(field: 'price'); +``` + +##### Max Aggregation +Returns the maximum value of a numeric field. +- Class: `\Spameri\ElasticQuery\Aggregation\Max` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Max.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Max(field: 'price'); +``` + +##### Avg Aggregation +Returns the average value of a numeric field. +- Class: `\Spameri\ElasticQuery\Aggregation\Avg` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Avg.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Avg(field: 'price'); +``` + +##### Sum Aggregation +Returns the sum of values of a numeric field. +- Class: `\Spameri\ElasticQuery\Aggregation\Sum` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Sum.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Sum(field: 'price'); +``` + +##### ValueCount Aggregation +Counts the number of values extracted from a field. +- Class: `\Spameri\ElasticQuery\Aggregation\ValueCount` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/ValueCount.php) + +```php +new \Spameri\ElasticQuery\Aggregation\ValueCount(field: 'price'); +``` + +##### Stats Aggregation +Returns count, min, max, avg and sum in one call. +- Class: `\Spameri\ElasticQuery\Aggregation\Stats` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Stats.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Stats(field: 'price'); +``` + +##### ExtendedStats Aggregation +Stats plus variance, standard deviation and bounds. +- Class: `\Spameri\ElasticQuery\Aggregation\ExtendedStats` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/ExtendedStats.php) + +```php +new \Spameri\ElasticQuery\Aggregation\ExtendedStats( + field: 'price', + sigma: 3.0, // Optional, default 2.0 +); +``` + +##### Percentiles Aggregation +Calculates percentile values (e.g. p50, p95, p99) over a numeric field. +- Class: `\Spameri\ElasticQuery\Aggregation\Percentiles` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Percentiles.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Percentiles( + field: 'load_time', + percents: [50, 95, 99], // Optional, default [1, 5, 25, 50, 75, 95, 99] +); +``` + +##### PercentileRanks Aggregation +Calculates the percentile rank for given values. +- Class: `\Spameri\ElasticQuery\Aggregation\PercentileRanks` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-rank-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/PercentileRanks.php) + +```php +new \Spameri\ElasticQuery\Aggregation\PercentileRanks( + field: 'load_time', + values: [500, 600], +); +``` + +##### WeightedAvg Aggregation +Computes a weighted average over two fields (value and weight). +- Class: `\Spameri\ElasticQuery\Aggregation\WeightedAvg` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-weight-avg-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/WeightedAvg.php) + +```php +new \Spameri\ElasticQuery\Aggregation\WeightedAvg( + valueField: 'grade', + weightField: 'weight', +); +``` + +##### MedianAbsoluteDeviation Aggregation +Computes a robust measure of variability via the median of absolute deviations from the median. +- Class: `\Spameri\ElasticQuery\Aggregation\MedianAbsoluteDeviation` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-median-absolute-deviation-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/MedianAbsoluteDeviation.php) + +```php +new \Spameri\ElasticQuery\Aggregation\MedianAbsoluteDeviation(field: 'rating'); +``` + +##### StringStats Aggregation +Computes statistics over string values (length, character distribution). +- Class: `\Spameri\ElasticQuery\Aggregation\StringStats` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-string-stats-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/StringStats.php) + +```php +new \Spameri\ElasticQuery\Aggregation\StringStats( + field: 'message.keyword', + showDistribution: true, // Optional, include per-character frequencies +); +``` + +##### BoxPlot Aggregation +Computes min, max, median and quartiles for plotting box-and-whisker diagrams. +- Class: `\Spameri\ElasticQuery\Aggregation\BoxPlot` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-boxplot-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/BoxPlot.php) + +```php +new \Spameri\ElasticQuery\Aggregation\BoxPlot(field: 'load_time'); +``` + +##### GeoCentroid Aggregation +Computes the weighted centroid of a set of geo points. +- Class: `\Spameri\ElasticQuery\Aggregation\GeoCentroid` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geocentroid-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/GeoCentroid.php) + +```php +new \Spameri\ElasticQuery\Aggregation\GeoCentroid(field: 'location'); +``` + +##### GeoBounds Aggregation +Computes the bounding box of all matching geo points. +- Class: `\Spameri\ElasticQuery\Aggregation\GeoBounds` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geobounds-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/GeoBounds.php) + +```php +new \Spameri\ElasticQuery\Aggregation\GeoBounds( + field: 'location', + wrapLongitude: true, // Default true, allow boxes crossing the dateline +); +``` + +##### Cardinality Aggregation +Approximate count of distinct values using HyperLogLog++. +- Class: `\Spameri\ElasticQuery\Aggregation\Cardinality` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Cardinality.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Cardinality( + field: 'user_id', + precisionThreshold: 3000, // Optional, default 3000, max 40000 +); +``` + +##### TopHits Aggregation +Returns the top matching documents per bucket. +- Class: `\Spameri\ElasticQuery\Aggregation\TopHits` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/TopHits.php) + +```php +new \Spameri\ElasticQuery\Aggregation\TopHits(size: 3); // Return top 3 hits per bucket +``` + +--- + +## Bucket Aggregations + +Group documents into buckets. -## Implementations ##### Term Aggregation -- Class `\Spameri\ElasticQuery\Aggregation\Term` +Groups documents by unique field values. +- Class: `\Spameri\ElasticQuery\Aggregation\Term` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Term.php) +```php +new \Spameri\ElasticQuery\Aggregation\Term( + field: 'category', + size: 10, // Return top 10 terms +); +``` + ##### Histogram Aggregation -- Class `\Spameri\ElasticQuery\Aggregation\Histogram` +Groups documents into fixed-width numeric intervals. +- Class: `\Spameri\ElasticQuery\Aggregation\Histogram` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Histogram.php) +```php +new \Spameri\ElasticQuery\Aggregation\Histogram( + field: 'price', + interval: 50, // Buckets: 0-50, 50-100, 100-150, etc. +); +``` + ##### Range Aggregation -- Class `\Spameri\ElasticQuery\Aggregation\Range` +Groups documents into manually defined ranges. +- Class: `\Spameri\ElasticQuery\Aggregation\Range` - [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html) - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Range.php) -##### AggregationCollection Aggregation -- Class `\Spameri\ElasticQuery\Aggregation\AggregationCollection` +```php +$rangeValues = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue(to: 50), // 0-50 + new \Spameri\ElasticQuery\Aggregation\RangeValue(from: 50, to: 100), // 50-100 + new \Spameri\ElasticQuery\Aggregation\RangeValue(from: 100), // 100+ +); + +new \Spameri\ElasticQuery\Aggregation\Range( + field: 'price', + rangeValues: $rangeValues, +); +``` + +##### Filter Aggregation +Single bucket containing documents matching a filter query. +- Class: `\Spameri\ElasticQuery\Aggregation\Filter` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Filter.php) + +```php +$filterAgg = new \Spameri\ElasticQuery\Aggregation\Filter(); +$filterAgg->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'published')); +``` + +##### DateHistogram Aggregation +Groups documents into date-based intervals (calendar or fixed). +- Class: `\Spameri\ElasticQuery\Aggregation\DateHistogram` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/DateHistogram.php) + +```php +new \Spameri\ElasticQuery\Aggregation\DateHistogram( + field: 'created_at', + calendarInterval: 'month', // or fixedInterval: '7d' + format: 'yyyy-MM-dd', + timeZone: 'Europe/Prague', + minDocCount: 1, +); +``` + +##### DateRange Aggregation +Groups documents into date ranges (accepts relative dates like `now-1M/M`). +- Class: `\Spameri\ElasticQuery\Aggregation\DateRange` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-daterange-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/DateRange.php) + +```php +$ranges = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue('past', null, 'now-1M/M'), + new \Spameri\ElasticQuery\Aggregation\RangeValue('recent', 'now-1M/M', null), +); + +new \Spameri\ElasticQuery\Aggregation\DateRange( + field: 'created_at', + ranges: $ranges, + format: 'MM-yyyy', +); +``` + +##### Missing Aggregation +Single bucket containing documents missing a field value. +- Class: `\Spameri\ElasticQuery\Aggregation\Missing` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-missing-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Missing.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Missing(field: 'price'); +``` + +##### Global Aggregation +Single bucket containing all documents, ignoring the current query. +- Class: `\Spameri\ElasticQuery\Aggregation\GlobalAggregation` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-global-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/GlobalAggregation.php) + +```php +new \Spameri\ElasticQuery\Aggregation\GlobalAggregation(); +``` + +##### SignificantTerms Aggregation +Finds terms that occur unusually often within the query context vs the index as a whole. +- Class: `\Spameri\ElasticQuery\Aggregation\SignificantTerms` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/SignificantTerms.php) + +```php +new \Spameri\ElasticQuery\Aggregation\SignificantTerms( + field: 'crime_type', + size: 10, + minDocCount: 5, +); +``` + +##### SignificantText Aggregation +Like significant terms but optimised for free-text fields. +- Class: `\Spameri\ElasticQuery\Aggregation\SignificantText` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significanttext-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/SignificantText.php) + +```php +new \Spameri\ElasticQuery\Aggregation\SignificantText( + field: 'content', + size: 20, + filterDuplicateText: true, +); +``` + +##### GeoDistance Aggregation +Groups documents into concentric distance buckets around an origin point. +- Class: `\Spameri\ElasticQuery\Aggregation\GeoDistance` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geodistance-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/GeoDistance.php) + +```php +$ranges = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue('near', null, 100), + new \Spameri\ElasticQuery\Aggregation\RangeValue('far', 100, null), +); + +new \Spameri\ElasticQuery\Aggregation\GeoDistance( + field: 'location', + lat: 50.0, + lon: 14.4, + ranges: $ranges, + unit: 'km', +); +``` + +##### GeoHashGrid Aggregation +Groups geo points into geohash-prefixed cells of configurable precision. +- Class: `\Spameri\ElasticQuery\Aggregation\GeoHashGrid` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/GeoHashGrid.php) + +```php +new \Spameri\ElasticQuery\Aggregation\GeoHashGrid( + field: 'location', + precision: 5, +); +``` + +##### GeoTileGrid Aggregation +Groups geo points into map-tile cells (zoom levels 0–29). +- Class: `\Spameri\ElasticQuery\Aggregation\GeoTileGrid` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geotilegrid-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/GeoTileGrid.php) + +```php +new \Spameri\ElasticQuery\Aggregation\GeoTileGrid( + field: 'location', + precision: 8, +); +``` + +##### Nested Aggregation +Aggregates on nested documents. +- Class: `\Spameri\ElasticQuery\Aggregation\Nested` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Nested.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Nested(path: 'comments'); +``` + +##### Composite Aggregation +Paginated multi-source buckets — useful for retrieving all unique value combinations. +- Class: `\Spameri\ElasticQuery\Aggregation\Composite` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Composite.php) + +```php +$composite = new \Spameri\ElasticQuery\Aggregation\Composite( + key: 'my_buckets', + source: new \Spameri\ElasticQuery\Aggregation\Term('product'), + size: 100, +); +$composite->addSource(new \Spameri\ElasticQuery\Aggregation\Histogram('price', 50)); +// $composite->addSource(new \Spameri\ElasticQuery\Aggregation\DateHistogram('date', calendarInterval: 'day')); +``` + +##### MultiTerms Aggregation +Groups documents by the combination of values from multiple fields. +- Class: `\Spameri\ElasticQuery\Aggregation\MultiTerms` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-multi-terms-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/MultiTerms.php) + +```php +new \Spameri\ElasticQuery\Aggregation\MultiTerms( + terms: ['brand', 'color'], + size: 10, +); +``` + +##### RareTerms Aggregation +Finds terms that occur infrequently (the opposite of a terms-with-large-min-doc-count search). +- Class: `\Spameri\ElasticQuery\Aggregation\RareTerms` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-rare-terms-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/RareTerms.php) + +```php +new \Spameri\ElasticQuery\Aggregation\RareTerms( + field: 'genre', + maxDocCount: 2, +); +``` + +##### Sampler Aggregation +Limits sub-aggregations to top-N highest-scoring documents per shard. +- Class: `\Spameri\ElasticQuery\Aggregation\Sampler` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-sampler-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Sampler.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Sampler(shardSize: 200); +``` + +##### DiversifiedSampler Aggregation +Like sampler but limits documents-per-distinct-value to avoid skew. +- Class: `\Spameri\ElasticQuery\Aggregation\DiversifiedSampler` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-diversified-sampler-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/DiversifiedSampler.php) + +```php +new \Spameri\ElasticQuery\Aggregation\DiversifiedSampler( + field: 'author.keyword', + shardSize: 200, + maxDocsPerValue: 3, +); +``` + +##### AdjacencyMatrix Aggregation +Buckets for each named filter and each pairwise intersection. +- Class: `\Spameri\ElasticQuery\Aggregation\AdjacencyMatrix` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-adjacency-matrix-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/AdjacencyMatrix.php) + +```php +$filterA = new \Spameri\ElasticQuery\Filter\FilterCollection(); +$filterA->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + +$matrix = new \Spameri\ElasticQuery\Aggregation\AdjacencyMatrix(); +$matrix->addFilter('group_a', $filterA); +``` + +##### IpRange Aggregation +Groups IP-typed fields into ranges (accepts plain IPs or CIDR masks). +- Class: `\Spameri\ElasticQuery\Aggregation\IpRange` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-iprange-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/IpRange.php) + +```php +$ranges = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue('low', null, '10.0.0.5'), + new \Spameri\ElasticQuery\Aggregation\RangeValue('high', '10.0.0.5', null), +); + +new \Spameri\ElasticQuery\Aggregation\IpRange( + field: 'ip', + ranges: $ranges, +); +``` + +##### ReverseNested Aggregation +Moves back from a nested context to the parent (or an ancestor at `path`). +- Class: `\Spameri\ElasticQuery\Aggregation\ReverseNested` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-reverse-nested-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/ReverseNested.php) + +```php +new \Spameri\ElasticQuery\Aggregation\ReverseNested(); +// Or back to a specific ancestor path: +new \Spameri\ElasticQuery\Aggregation\ReverseNested(path: 'parent_field'); +``` + +--- + +## Pipeline Aggregations + +Aggregations whose input is the output of other aggregations (referenced by `bucketsPath`). + +##### AvgBucket Aggregation +Average of values across sibling buckets. +- Class: `\Spameri\ElasticQuery\Aggregation\AvgBucket` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-avg-bucket-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/AvgBucket.php) + +```php +new \Spameri\ElasticQuery\Aggregation\AvgBucket( + bucketsPath: 'sales_per_month>sales', +); +``` + +##### SumBucket Aggregation +Sum of values across sibling buckets. +- Class: `\Spameri\ElasticQuery\Aggregation\SumBucket` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-sum-bucket-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/SumBucket.php) + +```php +new \Spameri\ElasticQuery\Aggregation\SumBucket( + bucketsPath: 'sales_per_month>sales', +); +``` + +##### MaxBucket Aggregation +Maximum value across sibling buckets. +- Class: `\Spameri\ElasticQuery\Aggregation\MaxBucket` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-max-bucket-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/MaxBucket.php) + +```php +new \Spameri\ElasticQuery\Aggregation\MaxBucket( + bucketsPath: 'sales_per_month>sales', +); +``` + +##### MinBucket Aggregation +Minimum value across sibling buckets. +- Class: `\Spameri\ElasticQuery\Aggregation\MinBucket` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-min-bucket-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/MinBucket.php) + +```php +new \Spameri\ElasticQuery\Aggregation\MinBucket( + bucketsPath: 'sales_per_month>sales', +); +``` + +##### StatsBucket Aggregation +Stats (count/min/max/avg/sum) across sibling buckets. +- Class: `\Spameri\ElasticQuery\Aggregation\StatsBucket` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-stats-bucket-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/StatsBucket.php) + +```php +new \Spameri\ElasticQuery\Aggregation\StatsBucket( + bucketsPath: 'sales_per_month>sales', +); +``` + +##### PercentilesBucket Aggregation +Percentile values across sibling buckets. +- Class: `\Spameri\ElasticQuery\Aggregation\PercentilesBucket` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-percentiles-bucket-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/PercentilesBucket.php) + +```php +new \Spameri\ElasticQuery\Aggregation\PercentilesBucket( + bucketsPath: 'sales_per_month>sales', + percents: [50, 95, 99], +); +``` + +##### Derivative Aggregation +Difference between successive bucket values. +- Class: `\Spameri\ElasticQuery\Aggregation\Derivative` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-derivative-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Derivative.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Derivative( + bucketsPath: 'sales', + unit: 'day', // For date histogram parents +); +``` + +##### CumulativeSum Aggregation +Running total of values across buckets. +- Class: `\Spameri\ElasticQuery\Aggregation\CumulativeSum` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-sum-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/CumulativeSum.php) + +```php +new \Spameri\ElasticQuery\Aggregation\CumulativeSum( + bucketsPath: 'sales', +); +``` + +##### MovingFunction Aggregation +Applies a Painless script over a sliding window of bucket values. +- Class: `\Spameri\ElasticQuery\Aggregation\MovingFunction` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-movfn-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/MovingFunction.php) + +```php +new \Spameri\ElasticQuery\Aggregation\MovingFunction( + bucketsPath: 'sales', + window: 5, + script: 'MovingFunctions.unweightedAvg(values)', +); +``` + +##### SerialDiff Aggregation +Difference between values N buckets apart — useful for removing seasonality. +- Class: `\Spameri\ElasticQuery\Aggregation\SerialDiff` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-serialdiff-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/SerialDiff.php) + +```php +new \Spameri\ElasticQuery\Aggregation\SerialDiff( + bucketsPath: 'sales', + lag: 7, // Week-over-week diff +); +``` + +##### BucketScript Aggregation +Computes a per-bucket value via a Painless script over named sibling metrics. +- Class: `\Spameri\ElasticQuery\Aggregation\BucketScript` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/BucketScript.php) + +```php +new \Spameri\ElasticQuery\Aggregation\BucketScript( + bucketsPath: ['tShirts' => 't-shirts', 'total' => 'total_sales'], + script: 'params.tShirts / params.total * 100', +); +``` + +##### BucketSelector Aggregation +Filters buckets to those whose script returns true. +- Class: `\Spameri\ElasticQuery\Aggregation\BucketSelector` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-selector-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/BucketSelector.php) + +```php +new \Spameri\ElasticQuery\Aggregation\BucketSelector( + bucketsPath: ['totalSales' => 'total_sales'], + script: 'params.totalSales > 100', +); +``` + +##### BucketSort Aggregation +Sorts and truncates sibling buckets (also supports pagination). +- Class: `\Spameri\ElasticQuery\Aggregation\BucketSort` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-sort-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/BucketSort.php) + +```php +new \Spameri\ElasticQuery\Aggregation\BucketSort( + sort: [['total_sales' => ['order' => 'desc']]], + size: 5, +); +``` + +##### Normalize Aggregation +Rescales bucket values (e.g. `percent_of_sum`, `rescale_0_1`, `mean`, `z-score`, `softmax`). +- Class: `\Spameri\ElasticQuery\Aggregation\Normalize` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-normalize-aggregation.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Normalize.php) + +```php +new \Spameri\ElasticQuery\Aggregation\Normalize( + bucketsPath: 'sales', + method: 'percent_of_sum', +); +``` + +--- + +## Aggregation Collections + +##### AggregationCollection +Top-level container for aggregations in a query. +- Class: `\Spameri\ElasticQuery\Aggregation\AggregationCollection` - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/AggregationCollection.php) -- Top level aggregations. -##### LeafAggregationCollection Aggregation -- Class `\Spameri\ElasticQuery\Aggregation\LeafAggregationCollection` +##### LeafAggregationCollection +Wrapper for individual aggregations that enables naming and sub-aggregations. +- Class: `\Spameri\ElasticQuery\Aggregation\LeafAggregationCollection` - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/LeafAggregationCollection.php) -- Nested aggregations collection. \ No newline at end of file + +```php +$agg = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'aggregation_name', + new \Spameri\ElasticQuery\Aggregation\Term('field'), +); + +// Access sub-aggregations +$agg->subAggregation()->add(/* nested aggregation */); +``` + +--- + +## Complete Example + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); +$query->addMustQuery(new \Spameri\ElasticQuery\Query\ElasticMatch('content', 'elasticsearch')); + +// Category aggregation with nested stats +$categoryAgg = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'categories', + new \Spameri\ElasticQuery\Aggregation\Term('category', 20) +); + +// Add sub-aggregations for each category bucket +$categoryAgg->subAggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'min_price', + new \Spameri\ElasticQuery\Aggregation\Min('price') + ) +); +$categoryAgg->subAggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'max_price', + new \Spameri\ElasticQuery\Aggregation\Max('price') + ) +); +$categoryAgg->subAggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + new \Spameri\ElasticQuery\Aggregation\Avg('price') + ) +); + +$query->addAggregation($categoryAgg); + +// Price histogram +$query->addAggregation( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_ranges', + new \Spameri\ElasticQuery\Aggregation\Histogram('price', 100) + ) +); +``` \ No newline at end of file diff --git a/doc/04-result-objects.md b/doc/04-result-objects.md index 460af63..1ec1c28 100644 --- a/doc/04-result-objects.md +++ b/doc/04-result-objects.md @@ -1,18 +1,213 @@ -# Result objects +# Result Objects -- Every object is as close to array response as possible. But as typed object. -- Every response object implements `\Spameri\ElasticQuery\Response\ResultInterface` +Every response object implements `\Spameri\ElasticQuery\Response\ResultInterface` and provides typed access to Elasticsearch responses. -## Implementations -##### Result Single -- Class `\Spameri\ElasticQuery\Response\ResultSingle` -- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Response/ResultSingle.php) +--- + +## Result Mapper + +The `ResultMapper` class automatically detects response types and maps them: + +```php +$resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); +$result = $resultMapper->map($response); +``` + +Or use specific mapping methods: + +```php +// Single document +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSingleResult($response); -##### Result Search -- Class `\Spameri\ElasticQuery\Response\ResultSearch` +// Search results +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSearchResults($response); + +// Bulk operations +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapBulkResult($response); + +// Cluster version +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapVersionResult($response); +``` + +--- + +## Result Types + +##### ResultSearch +Search results with hits and aggregations. +- Class: `\Spameri\ElasticQuery\Response\ResultSearch` - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Response/ResultSearch.php) -- [Sample usage](https://github.com/Spameri/ElasticQuery/blob/master/tests/SpameriTests/ElasticQuery/Response/Result.phpt#L76) -##### Result Bulk -- Class `\Spameri\ElasticQuery\Response\ResultBulk` +```php +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSearchResults($response); + +// Access statistics +$result->stats()->took(); // Query execution time in ms +$result->stats()->totalHits(); // Total matching documents + +// Access shard info +$result->shards()->total(); +$result->shards()->successful(); +$result->shards()->failed(); + +// Iterate over hits +foreach ($result->hits() as $hit) { + $hit->id(); // Document ID + $hit->index(); // Index name + $hit->score(); // Relevance score + $hit->source(); // Document source (array) + $hit->highlight(); // Highlighted fields (if requested) +} + +// Get specific hit by ID +$hit = $result->getHit('document_id'); + +// Access aggregations +foreach ($result->aggregations() as $aggregation) { + $aggregation->name(); // Aggregation name + $aggregation->buckets(); // Bucket collection + $aggregation->value(); // Metric value (for metric aggregations) + $aggregation->hits(); // Hits (for top_hits aggregation) +} + +// Get specific aggregation by name +$categoryAgg = $result->getAggregation('categories'); +foreach ($categoryAgg->buckets() as $bucket) { + $bucket->key(); // Bucket key (e.g., category name) + $bucket->docCount(); // Number of documents in bucket +} +``` + +##### ResultSingle +Single document retrieval result. +- Class: `\Spameri\ElasticQuery\Response\ResultSingle` +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Response/ResultSingle.php) + +```php +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSingleResult($response); + +// Access the hit +$hit = $result->hit(); +$hit->id(); +$hit->index(); +$hit->source(); +$hit->version(); // If version was requested + +// Access stats +$result->stats()->found(); // Boolean - document exists +``` + +##### ResultBulk +Bulk operation results. +- Class: `\Spameri\ElasticQuery\Response\ResultBulk` - [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Response/ResultBulk.php) + +```php +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapBulkResult($response); + +// Access stats +$result->stats()->took(); +$result->stats()->errors(); // Boolean - any errors occurred + +// Get specific action by ID +$action = $result->getFirstAction('document_id'); +$action->id(); +$action->index(); +$action->result(); // 'created', 'updated', 'deleted', etc. +$action->status(); // HTTP status code +``` + +##### ResultVersion +Cluster version information. +- Class: `\Spameri\ElasticQuery\Response\ResultVersion` +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Response/ResultVersion.php) + +```php +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapVersionResult($response); + +$result->name(); // Node name +$result->clusterName(); // Cluster name +$result->clusterUUID(); // Cluster UUID +$result->tagLine(); // "You Know, for Search" + +// Version details +$result->version()->number(); // e.g., "8.11.0" +$result->version()->buildFlavor(); // e.g., "default" +$result->version()->buildType(); // e.g., "docker" +$result->version()->luceneVersion(); // e.g., "9.8.0" +$result->version()->minimumWireVersion(); +$result->version()->minimumIndexVersion(); +``` + +--- + +## Supporting Classes + +##### Hit +Individual search result document. +- Class: `\Spameri\ElasticQuery\Response\Result\Hit` + +##### HitCollection +Collection of search result hits. +- Class: `\Spameri\ElasticQuery\Response\Result\HitCollection` + +##### Aggregation +Aggregation result with buckets or metric value. +- Class: `\Spameri\ElasticQuery\Response\Result\Aggregation` + +##### AggregationCollection +Collection of aggregation results. +- Class: `\Spameri\ElasticQuery\Response\Result\AggregationCollection` + +##### Bucket +Single aggregation bucket. +- Class: `\Spameri\ElasticQuery\Response\Result\Aggregation\Bucket` + +##### BucketCollection +Collection of aggregation buckets. +- Class: `\Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection` + +##### Stats / StatsSingle +Query execution statistics. +- Class: `\Spameri\ElasticQuery\Response\Stats` +- Class: `\Spameri\ElasticQuery\Response\StatsSingle` + +##### Shards +Shard execution information. +- Class: `\Spameri\ElasticQuery\Response\Shards` + +--- + +## Complete Example + +```php +// Execute search +$response = $client->search($document->toArray()); + +// Map to typed result +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSearchResults($response); + +// Display results +echo "Found {$result->stats()->totalHits()} documents in {$result->stats()->took()}ms\n"; + +foreach ($result->hits() as $hit) { + echo "- [{$hit->id()}] Score: {$hit->score()}\n"; + echo " Title: {$hit->source()['title']}\n"; + + // Display highlights if available + if ($hit->highlight()) { + foreach ($hit->highlight() as $field => $fragments) { + echo " Highlight ({$field}): " . implode('...', $fragments) . "\n"; + } + } +} + +// Display aggregations +if ($result->aggregations()->count() > 0) { + $categoryAgg = $result->getAggregation('categories'); + echo "\nCategories:\n"; + foreach ($categoryAgg->buckets() as $bucket) { + echo "- {$bucket->key()}: {$bucket->docCount()} documents\n"; + } +} +``` diff --git a/doc/05-options.md b/doc/05-options.md new file mode 100644 index 0000000..7ad80e6 --- /dev/null +++ b/doc/05-options.md @@ -0,0 +1,254 @@ +# Options, Pagination & Sorting + +The `Options` class and related sorting classes control pagination, result ordering, scrolling, and other search options. + +--- + +## Options + +Access options through the ElasticQuery: + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); +$options = $query->options(); +``` + +Or construct with options: + +```php +$options = new \Spameri\ElasticQuery\Options( + size: 20, + from: 0, + sort: null, + minScore: 1.0, + includeVersion: true, + scroll: '5m', + scrollId: null, +); + +$query = new \Spameri\ElasticQuery\ElasticQuery(options: $options); +``` + +--- + +## Pagination + +##### Size and From (Offset) + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Set page size (number of results to return) +$query->options()->changeSize(20); + +// Set offset (skip first N results) +$query->options()->changeFrom(40); // Page 3 with 20 per page + +// Produces: { "size": 20, "from": 40, ... } +``` + +##### Minimum Score + +Filter out results below a minimum relevance score: + +```php +$options = new \Spameri\ElasticQuery\Options( + size: 20, + from: 0, + minScore: 0.5, // Only return documents with score >= 0.5 +); +``` + +##### Include Version + +Include document version in results: + +```php +$options = new \Spameri\ElasticQuery\Options( + includeVersion: true, +); +``` + +--- + +## Scroll API + +For iterating over large result sets: + +```php +$options = new \Spameri\ElasticQuery\Options( + size: 1000, +); + +// Start scrolling (keep context alive for 5 minutes) +$options->startScroll('5m'); + +// After first request, store the scroll ID +$options->scrollInitialized($scrollIdFromResponse); + +// Use scroll() and scrollId() getters +$scroll = $options->scroll(); // '5m' +$scrollId = $options->scrollId(); // The scroll context ID +``` + +--- + +## Sorting + +##### Sort Class + +Sort results by field values. +- Class: `\Spameri\ElasticQuery\Options\Sort` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Options/Sort.php) + +```php +use Spameri\ElasticQuery\Options\Sort; + +new Sort( + field: 'created_at', + type: Sort::DESC, // ASC or DESC + missing: Sort::MISSING_LAST, // MISSING_LAST or MISSING_FIRST +); +``` + +Constants: +- `Sort::ASC` - Ascending order +- `Sort::DESC` - Descending order +- `Sort::MISSING_LAST` - Documents with missing field sorted last +- `Sort::MISSING_FIRST` - Documents with missing field sorted first + +##### Adding Sorts to Query + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Sort by date descending +$query->options()->sort()->add( + new \Spameri\ElasticQuery\Options\Sort('date', Sort::DESC) +); + +// Then by score ascending +$query->options()->sort()->add( + new \Spameri\ElasticQuery\Options\Sort('_score', Sort::ASC) +); +``` + +##### GeoDistanceSort + +Sort by distance from a geographic point. +- Class: `\Spameri\ElasticQuery\Options\GeoDistanceSort` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html#geo-sorting) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Options/GeoDistanceSort.php) + +```php +use Spameri\ElasticQuery\Options\GeoDistanceSort; +use Spameri\ElasticQuery\Options\Sort; + +new GeoDistanceSort( + field: 'location', // Geo-point field + lat: 40.7128, // Latitude + lon: -74.0060, // Longitude + type: Sort::ASC, // ASC (nearest first) or DESC + unit: 'km', // Distance unit: km, m, mi, etc. + mode: 'min', // min, max, avg, median (for arrays) + distanceType: 'arc', // arc (accurate) or plane (faster) +); +``` + +--- + +## SortCollection + +The sort collection manages multiple sort criteria. +- Class: `\Spameri\ElasticQuery\Options\SortCollection` +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Options/SortCollection.php) + +```php +// Access via options +$sortCollection = $query->options()->sort(); + +// Add sorts +$sortCollection->add(new Sort('date', Sort::DESC)); +$sortCollection->add(new Sort('title', Sort::ASC)); + +// Check if sort exists +$sortCollection->isValue('date'); // true + +// Get sort by field name +$dateSort = $sortCollection->get('date'); + +// Remove sort +$sortCollection->remove('date'); + +// Count sorts +$sortCollection->count(); + +// Clear all sorts +$sortCollection->clear(); +``` + +--- + +## Complete Example + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Add search query +$query->addMustQuery( + new \Spameri\ElasticQuery\Query\ElasticMatch('content', 'elasticsearch') +); + +// Pagination: Page 2 with 25 results per page +$query->options()->changeSize(25); +$query->options()->changeFrom(25); + +// Primary sort by relevance score +$query->options()->sort()->add( + new \Spameri\ElasticQuery\Options\Sort('_score', Sort::DESC) +); + +// Secondary sort by date for equal scores +$query->options()->sort()->add( + new \Spameri\ElasticQuery\Options\Sort('created_at', Sort::DESC, Sort::MISSING_LAST) +); + +// Execute query +$body = $query->toArray(); +/* +{ + "size": 25, + "from": 25, + "sort": [ + { "_score": { "order": "DESC", "missing": "_last" } }, + { "created_at": { "order": "DESC", "missing": "_last" } } + ], + "query": { ... } +} +*/ +``` + +## Geo-Distance Sorting Example + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery(); + +// Find restaurants +$query->addFilter( + new \Spameri\ElasticQuery\Query\Term('type', 'restaurant') +); + +// Sort by distance from user's location +$query->options()->sort()->add( + new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 51.5074, // London coordinates + lon: -0.1278, + type: Sort::ASC, + unit: 'km', + ) +); + +$query->options()->changeSize(10); +``` diff --git a/doc/06-highlight-function-score.md b/doc/06-highlight-function-score.md new file mode 100644 index 0000000..49415e9 --- /dev/null +++ b/doc/06-highlight-function-score.md @@ -0,0 +1,275 @@ +# Highlight & Function Score + +These features enhance search results with highlighting and custom scoring. + +--- + +## Highlight + +Highlight matching text fragments in search results. +- Class: `\Spameri\ElasticQuery\Highlight` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Highlight.php) + +### Basic Usage + +```php +$highlight = new \Spameri\ElasticQuery\Highlight( + preTags: ['', ''], // Tags before highlighted text + postTags: ['', ''], // Tags after highlighted text + fields: ['title', 'content'], // Fields to highlight +); + +$query = new \Spameri\ElasticQuery\ElasticQuery( + highlight: $highlight, +); +``` + +### Constructor Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `preTags` | `array` | HTML tags inserted before highlighted text | +| `postTags` | `array` | HTML tags inserted after highlighted text | +| `fields` | `array` | Field names to apply highlighting to | + +### Accessing Highlights in Results + +```php +$result = \Spameri\ElasticQuery\Response\ResultMapper::mapSearchResults($response); + +foreach ($result->hits() as $hit) { + $highlights = $hit->highlight(); // array|null + + if ($highlights !== null) { + foreach ($highlights as $field => $fragments) { + echo "{$field}: " . implode(' ... ', $fragments) . "\n"; + } + } +} +``` + +### Example Output + +```php +// Query for "elasticsearch" +// With highlight: tags + +// Result highlight array: +[ + 'title' => ['Introduction to Elasticsearch'], + 'content' => [ + 'Elasticsearch is a distributed search engine.', + 'Learn how to use Elasticsearch for full-text search.', + ], +] +``` + +--- + +## Function Score + +Customize document scoring with mathematical functions. +- Class: `\Spameri\ElasticQuery\FunctionScore` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/FunctionScore.php) + +### Basic Usage + +```php +$functionScore = new \Spameri\ElasticQuery\FunctionScore( + function: null, // Will be populated via function() + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MULTIPLY, +); + +$query = new \Spameri\ElasticQuery\ElasticQuery( + functionScore: $functionScore, +); +``` + +### Score Modes + +How to combine scores from multiple functions: + +```php +use Spameri\ElasticQuery\FunctionScore; + +FunctionScore::SCORE_MODE_MULTIPLY // Multiply function scores (default) +FunctionScore::SCORE_MODE_SUM // Add function scores +FunctionScore::SCORE_MODE_AVG // Average of function scores +FunctionScore::SCORE_MODE_FIRST // Use first matching function's score +FunctionScore::SCORE_MODE_MAX // Use maximum function score +FunctionScore::SCORE_MODE_MIN // Use minimum function score +``` + +### Adding Score Functions + +```php +$functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: FunctionScore::SCORE_MODE_SUM, +); + +// Add score functions +$functionScore->function()->add( + new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'popularity', + factor: 1.2, + modifier: 'log1p', + ) +); + +$query = new \Spameri\ElasticQuery\ElasticQuery(functionScore: $functionScore); +$query->addMustQuery(new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'search')); +``` + +--- + +## Score Functions + +### FieldValueFactor + +Modify score based on a numeric field value. +- Class: `\Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-field-value-factor) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/FunctionScore/ScoreFunction/FieldValueFactor.php) + +```php +new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'popularity', // Numeric field to use + factor: 1.2, // Multiply field value by this + modifier: 'log1p', // Mathematical modifier + missing: 1.0, // Default if field is missing +); +``` + +#### Modifiers + +| Modifier | Description | Formula | +|----------|-------------|---------| +| `none` | No modification | `field * factor` | +| `log` | Logarithm | `log(field * factor)` | +| `log1p` | Log + 1 (avoids log(0)) | `log(1 + field * factor)` | +| `log2p` | Log + 2 | `log(2 + field * factor)` | +| `ln` | Natural log | `ln(field * factor)` | +| `ln1p` | Natural log + 1 | `ln(1 + field * factor)` | +| `ln2p` | Natural log + 2 | `ln(2 + field * factor)` | +| `square` | Square | `(field * factor)²` | +| `sqrt` | Square root | `√(field * factor)` | +| `reciprocal` | Reciprocal | `1 / (field * factor)` | + +```php +use Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor; + +FieldValueFactor::MODIFIER_NONE +FieldValueFactor::MODIFIER_LOG +FieldValueFactor::MODIFIER_LOG1P +FieldValueFactor::MODIFIER_LOG2P +FieldValueFactor::MODIFIER_LN +FieldValueFactor::MODIFIER_LN1P +FieldValueFactor::MODIFIER_LN2P +FieldValueFactor::MODIFIER_SQUARE +FieldValueFactor::MODIFIER_SQRT +FieldValueFactor::MODIFIER_RECIPROCAL +``` + +### Weight + +Apply a constant weight when a filter matches. +- Class: `\Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-weight) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/FunctionScore/ScoreFunction/Weight.php) + +```php +// Boost featured products by 2x +new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight( + weight: 2.0, + leafQuery: new \Spameri\ElasticQuery\Query\Term('featured', true), +); +``` + +### RandomScore + +Add randomness to scoring (useful for sampling or A/B testing). +- Class: `\Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore` +- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-random) +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/FunctionScore/ScoreFunction/RandomScore.php) + +```php +// Random ordering (changes each request) +new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore(); + +// Consistent random ordering per user (same seed = same order) +new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore( + seed: $userId, // User ID as seed for consistent personalization +); +``` + +--- + +## Complete Examples + +### Highlight Search Results + +```php +$query = new \Spameri\ElasticQuery\ElasticQuery( + highlight: new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['title', 'content', 'description'], + ), +); + +$query->addMustQuery( + new \Spameri\ElasticQuery\Query\MultiMatch( + fields: ['title^2', 'content'], + query: 'elasticsearch tutorial', + ) +); +``` + +### Boost by Popularity and Recency + +```php +$functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_SUM, +); + +// Boost by view count (logarithmic to prevent domination) +$functionScore->function()->add( + new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'view_count', + factor: 0.1, + modifier: 'log1p', + missing: 1.0, + ) +); + +// Boost featured items +$functionScore->function()->add( + new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight( + weight: 10.0, + leafQuery: new \Spameri\ElasticQuery\Query\Term('featured', true), + ) +); + +$query = new \Spameri\ElasticQuery\ElasticQuery(functionScore: $functionScore); +$query->addMustQuery(new \Spameri\ElasticQuery\Query\ElasticMatch('content', 'search')); +``` + +### Random Shuffle with Consistent User Experience + +```php +$functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MULTIPLY, +); + +// Same user sees same order, different users see different orders +$functionScore->function()->add( + new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore( + seed: $currentUserId, + ) +); + +$query = new \Spameri\ElasticQuery\ElasticQuery(functionScore: $functionScore); +$query->addFilter(new \Spameri\ElasticQuery\Query\Term('status', 'active')); +``` diff --git a/doc/07-mapping-settings.md b/doc/07-mapping-settings.md new file mode 100644 index 0000000..f2c614e --- /dev/null +++ b/doc/07-mapping-settings.md @@ -0,0 +1,405 @@ +# Mapping & Settings + +Define Elasticsearch index mappings, analyzers, tokenizers, and filters. + +--- + +## Settings + +The `Settings` class is the main entry point for defining index configuration. +- Class: `\Spameri\ElasticQuery\Mapping\Settings` +- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Mapping/Settings.php) + +```php +$settings = new \Spameri\ElasticQuery\Mapping\Settings( + indexName: 'my_index', + hasSti: false, + analysis: null, // Will be auto-created + mapping: null, // Will be auto-created + alias: null, // Will be auto-created +); +``` + +### Adding Fields + +```php +// Simple field types (convenience methods) +$settings->addMappingFieldKeyword('status'); +$settings->addMappingFieldInteger('count'); +$settings->addMappingFieldFloat('price'); +$settings->addMappingFieldBoolean('active'); + +// Custom field with analyzer +$settings->addMappingField( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + name: 'title', + type: \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + analyzer: new \Spameri\ElasticQuery\Mapping\Analyzer\Standard(), + ) +); +``` + +### Adding Analyzers + +```php +// Add built-in analyzer +$settings->addAnalyzer(new \Spameri\ElasticQuery\Mapping\Analyzer\Standard()); + +// Custom analyzers automatically register their filters +$settings->addAnalyzer($customAnalyzer); +``` + +### Full Example + +```php +$settings = new \Spameri\ElasticQuery\Mapping\Settings('products'); + +// Add fields +$settings->addMappingFieldKeyword('sku'); +$settings->addMappingFieldKeyword('category'); +$settings->addMappingFieldFloat('price'); +$settings->addMappingFieldInteger('stock'); + +// Add text field with custom analyzer +$settings->addMappingField( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + name: 'description', + type: \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + analyzer: new \Spameri\ElasticQuery\Mapping\Analyzer\Standard(), + ) +); + +// Convert to array for index creation +$indexSettings = $settings->toArray(); +``` + +--- + +## Field Types + +All field types are defined in `\Spameri\ElasticQuery\Mapping\AllowedValues`: + +### Text Types +```php +AllowedValues::TYPE_TEXT // Full-text searchable +AllowedValues::TYPE_KEYWORD // Exact value matching +``` + +### Numeric Types +```php +AllowedValues::TYPE_INTEGER +AllowedValues::TYPE_LONG +AllowedValues::TYPE_SHORT +AllowedValues::TYPE_BYTE +AllowedValues::TYPE_DOUBLE +AllowedValues::TYPE_FLOAT +AllowedValues::TYPE_HALF_FLOAT +AllowedValues::TYPE_SCALED_FLOAT +``` + +### Date Types +```php +AllowedValues::TYPE_DATE +``` + +### Range Types +```php +AllowedValues::TYPE_INTEGER_RANGE +AllowedValues::TYPE_FLOAT_RANGE +AllowedValues::TYPE_LONG_RANGE +AllowedValues::TYPE_DOUBLE_RANGE +AllowedValues::TYPE_DATE_RANGE +``` + +### Complex Types +```php +AllowedValues::TYPE_OBJECT // JSON object +AllowedValues::TYPE_NESTED // Nested documents (queryable separately) +``` + +### Geo Types +```php +AllowedValues::TYPE_GEO_POINT // Lat/lon point +AllowedValues::TYPE_GEO_SHAPE // Arbitrary geo shapes +``` + +### Special Types +```php +AllowedValues::TYPE_IP // IPv4/IPv6 addresses +AllowedValues::TYPE_COMPLETION // Autocomplete suggestions +AllowedValues::TYPE_BOOLEAN +AllowedValues::TYPE_TOKEN_COUNT +AllowedValues::TYPE_PERCOLATOR +AllowedValues::TYPE_JOIN +AllowedValues::TYPE_ALIAS +``` + +--- + +## Analyzers + +Built-in analyzers in `src/Mapping/Analyzer/`: + +### Standard Analyzer +Default analyzer with grammar-based tokenization. +```php +new \Spameri\ElasticQuery\Mapping\Analyzer\Standard(); +``` + +### Simple Analyzer +Divides text on non-letter characters. +```php +new \Spameri\ElasticQuery\Mapping\Analyzer\Simple(); +``` + +### Whitespace Analyzer +Divides text on whitespace only. +```php +new \Spameri\ElasticQuery\Mapping\Analyzer\Whitespace(); +``` + +### Keyword Analyzer +Treats entire input as a single token. +```php +new \Spameri\ElasticQuery\Mapping\Analyzer\Keyword(); +``` + +### Stop Analyzer +Like simple but removes stop words. +```php +new \Spameri\ElasticQuery\Mapping\Analyzer\Stop(); +``` + +### Pattern Analyzer +Uses regex to split text. +```php +new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern(); +``` + +### Fingerprint Analyzer +Creates fingerprint for duplicate detection. +```php +new \Spameri\ElasticQuery\Mapping\Analyzer\Fingerprint(); +``` + +### Language-Specific Analyzers +Custom dictionary analyzers available in `src/Mapping/Analyzer/Custom/` for 30+ languages: + +- Arabic, Armenian, Basque, Bengali, Brazilian, Bulgarian +- Catalan, CJK, Czech, Danish, Dutch, English +- Finnish, French, Galician, German, Greek +- Hindi, Hungarian, Indonesian, Irish, Italian +- Latvian, Lithuanian, Norwegian, Persian, Portuguese +- Romanian, Russian, Sorani, Spanish, Swedish +- Turkish, Thai + +--- + +## Tokenizers + +Available in `src/Mapping/Tokenizer/`: + +### Word-Oriented Tokenizers +```php +new \Spameri\ElasticQuery\Mapping\Tokenizer\Standard(); // Grammar-based +new \Spameri\ElasticQuery\Mapping\Tokenizer\Letter(); // On non-letters +new \Spameri\ElasticQuery\Mapping\Tokenizer\Lowercase(); // Letter + lowercase +new \Spameri\ElasticQuery\Mapping\Tokenizer\Whitespace(); // On whitespace +new \Spameri\ElasticQuery\Mapping\Tokenizer\UaxUrlEmail(); // Preserves URLs/emails +new \Spameri\ElasticQuery\Mapping\Tokenizer\Classic(); // Grammar-based (legacy) +new \Spameri\ElasticQuery\Mapping\Tokenizer\Thai(); // Thai text +``` + +### Partial Word Tokenizers +```php +new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(); // N-gram tokens +new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(); // Edge n-grams (autocomplete) +``` + +### Structured Text Tokenizers +```php +new \Spameri\ElasticQuery\Mapping\Tokenizer\Keyword(); // No tokenization +new \Spameri\ElasticQuery\Mapping\Tokenizer\Pattern(); // Regex-based +new \Spameri\ElasticQuery\Mapping\Tokenizer\SimplePattern(); // Simple regex +new \Spameri\ElasticQuery\Mapping\Tokenizer\SimplePatternSplit(); // Split on regex +new \Spameri\ElasticQuery\Mapping\Tokenizer\CharGroup(); // Split on chars +new \Spameri\ElasticQuery\Mapping\Tokenizer\Path(); // Path hierarchy +``` + +--- + +## Filters + +Token filters transform tokens after tokenization. Available in `src/Mapping/Filter/`: + +### Common Filters +- `Lowercase` - Convert to lowercase +- `Uppercase` - Convert to uppercase +- `Stemmer` - Language-specific stemming +- `EdgeNgram` - Create edge n-grams +- `Ngram` - Create n-grams +- `Shingle` - Create word n-grams +- `WordDelimiter` - Split on word boundaries +- `ASCIIFolding` - Convert to ASCII equivalents +- `Trim` - Remove whitespace +- `Length` - Filter by token length +- `Unique` - Remove duplicates +- `Stop` - Remove stop words +- `Synonym` - Apply synonyms + +### Language-Specific Filters + +#### Hunspell (Spell Check) Filters +Available for 24 languages in `src/Mapping/Filter/Hunspell/`: +- Czech, Danish, Dutch, English (AU/GB/US), French +- German, Hungarian, Italian, Norwegian, Polish +- Portuguese, Romanian, Russian, Slovak, Spanish +- Swedish, Turkish, Ukrainian + +#### Stop Word Filters +Available for 27 languages in `src/Mapping/Filter/Stop/`: +- Arabic, Armenian, Basque, Bengali, Brazilian, Bulgarian +- Catalan, CJK, Czech, Danish, Dutch, English +- Finnish, French, Galician, German, Greek, Hindi +- Hungarian, Indonesian, Irish, Italian, Latvian, Lithuanian +- Norwegian, Persian, Portuguese, Romanian, Russian +- Sorani, Spanish, Swedish, Thai, Turkish + +#### Synonym Filters +Available in `src/Mapping/Filter/Synonym/`: +- Czech, German, English (more can be added) + +--- + +## Field Configuration Classes + +### Field +Basic field definition. +```php +new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + name: 'title', + type: AllowedValues::TYPE_TEXT, + analyzer: new \Spameri\ElasticQuery\Mapping\Analyzer\Standard(), + fieldData: false, // Enable fielddata for aggregations on text +); +``` + +### FieldObject +Object type with nested properties. +```php +$fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('address'); +// Add properties to the object... +$settings->addMappingFieldObject($fieldObject); +``` + +### NestedObject +Nested document type (maintains array relationships). +```php +$nested = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('comments'); +$settings->addMappingNestedObject($nested); +``` + +### SubFields +Multi-fields for different analysis on same data. +```php +$subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + name: 'title', + // Define sub-field variations... +); +$settings->addMappingSubField($subFields); +``` + +--- + +## Complete Example + +```php +use Spameri\ElasticQuery\Mapping\Settings; +use Spameri\ElasticQuery\Mapping\AllowedValues; +use Spameri\ElasticQuery\Mapping\Settings\Mapping\Field; +use Spameri\ElasticQuery\Mapping\Analyzer\Standard; + +// Create index settings +$settings = new Settings('blog_posts'); + +// Add simple fields +$settings->addMappingFieldKeyword('id'); +$settings->addMappingFieldKeyword('slug'); +$settings->addMappingFieldKeyword('status'); +$settings->addMappingFieldKeyword('author_id'); + +// Add date field +$settings->addMappingField( + new Field('published_at', AllowedValues::TYPE_DATE) +); + +// Add numeric fields +$settings->addMappingFieldInteger('view_count'); +$settings->addMappingFieldFloat('rating'); +$settings->addMappingFieldBoolean('featured'); + +// Add text fields with analyzers +$settings->addMappingField( + new Field( + name: 'title', + type: AllowedValues::TYPE_TEXT, + analyzer: new Standard(), + ) +); + +$settings->addMappingField( + new Field( + name: 'content', + type: AllowedValues::TYPE_TEXT, + analyzer: new Standard(), + ) +); + +// Add geo field +$settings->addMappingField( + new Field('location', AllowedValues::TYPE_GEO_POINT) +); + +// Get the complete settings array +$indexConfig = $settings->toArray(); + +// Use with Elasticsearch client +$client->indices()->create([ + 'index' => 'blog_posts', + 'body' => $indexConfig, +]); +``` + +--- + +## Analysis Configuration + +Access the analysis settings directly: + +```php +$settings = new \Spameri\ElasticQuery\Mapping\Settings('my_index'); + +// Access analysis components +$analysis = $settings->analysis(); + +// Add custom analyzer +$analysis->analyzer()->add($customAnalyzer); + +// Add custom tokenizer +$analysis->tokenizer()->add($customTokenizer); + +// Add custom filter +$analysis->filter()->add($customFilter); +``` + +### Custom Analyzer Example + +Custom analyzers implement `CustomAnalyzerInterface` and automatically register their required filters: + +```php +// When you add a custom analyzer, its filters are auto-registered +$settings->addAnalyzer($customAnalyzer); + +// To remove an analyzer and its filters +$settings->removeAnalyzer('analyzer_name'); +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1479dbb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + php: + build: . + volumes: + - .:/app + environment: + - ELASTICSEARCH_HOST=elasticsearch:9200 + depends_on: + elasticsearch: + condition: service_healthy + + elasticsearch: + image: elasticsearch:9.2.2 + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - xpack.security.enrollment.enabled=false + - ES_JAVA_OPTS=-Xms512m -Xmx512m + ports: + - "9202:9200" + healthcheck: + test: curl -s http://localhost:9200 >/dev/null || exit 1 + interval: 10s + timeout: 5s + retries: 10 diff --git a/phpstan.neon b/phpstan.neon index 945d242..39e9f5a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ parameters: ignoreErrors: - - "#Call to function array_key_exists\\(\\) with string and array will always evaluate to false#" - - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false + - + identifier: missingType.generics + - + identifier: missingType.iterableValue diff --git a/ruleset.xml b/ruleset.xml index 5abf5a9..d86f1c0 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -1,6 +1,104 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -11,4 +109,12 @@ + + + + + + + + diff --git a/src/Aggregation/AdjacencyMatrix.php b/src/Aggregation/AdjacencyMatrix.php new file mode 100644 index 0000000..8a7e89a --- /dev/null +++ b/src/Aggregation/AdjacencyMatrix.php @@ -0,0 +1,63 @@ + + */ + private array $filters; + + + public function __construct( + private string $key = 'adjacency_matrix', + private string|null $separator = null, + ) + { + $this->filters = []; + } + + + public function addFilter( + string $name, + \Spameri\ElasticQuery\Query\LeafQueryInterface $filter, + ): void + { + $this->filters[$name] = $filter; + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $filters = []; + foreach ($this->filters as $name => $filter) { + $filters[$name] = $filter->toArray(); + } + + $body = ['filters' => $filters]; + + if ($this->separator !== null) { + $body['separator'] = $this->separator; + } + + return ['adjacency_matrix' => $body]; + } + +} diff --git a/src/Aggregation/AggregationCollection.php b/src/Aggregation/AggregationCollection.php index fa39a51..cdb5f5d 100644 --- a/src/Aggregation/AggregationCollection.php +++ b/src/Aggregation/AggregationCollection.php @@ -1,4 +1,6 @@ - */ - private $aggregations; + private array $aggregations; public function __construct( - ?\Spameri\ElasticQuery\Filter\FilterCollection $filter = NULL - , \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection ... $aggregations + private \Spameri\ElasticQuery\Filter\FilterCollection|null $filter = null, + \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection ...$aggregations, ) { if ( ! $filter) { @@ -44,7 +41,7 @@ public function filter(): \Spameri\ElasticQuery\Filter\FilterCollection public function add( - LeafAggregationCollection $leafAggregation + LeafAggregationCollection $leafAggregation, ): void { $this->aggregations[$leafAggregation->key()] = $leafAggregation; @@ -58,7 +55,7 @@ public function keys(): array public function isKey( - string $key + string $key, ): bool { return \array_key_exists($key, \array_map('\strval', \array_keys($this->aggregations))); diff --git a/src/Aggregation/AutoDateHistogram.php b/src/Aggregation/AutoDateHistogram.php new file mode 100644 index 0000000..51d5a6e --- /dev/null +++ b/src/Aggregation/AutoDateHistogram.php @@ -0,0 +1,62 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->buckets !== null) { + $array['buckets'] = $this->buckets; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + if ($this->timeZone !== null) { + $array['time_zone'] = $this->timeZone; + } + + if ($this->minimumInterval !== null) { + $array['minimum_interval'] = $this->minimumInterval; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + return ['auto_date_histogram' => $array]; + } + +} diff --git a/src/Aggregation/Avg.php b/src/Aggregation/Avg.php index 9292872..3ba6a1d 100644 --- a/src/Aggregation/Avg.php +++ b/src/Aggregation/Avg.php @@ -1,21 +1,23 @@ -field = $field; } @@ -25,13 +27,26 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { - return [ - 'avg' => [ - 'field' => $this->field, - ], - ]; + $array = ['field' => $this->field]; + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['avg' => $array]; } } diff --git a/src/Aggregation/AvgBucket.php b/src/Aggregation/AvgBucket.php new file mode 100644 index 0000000..2913aa3 --- /dev/null +++ b/src/Aggregation/AvgBucket.php @@ -0,0 +1,51 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'avg_bucket' => $array, + ]; + } + +} diff --git a/src/Aggregation/BoxPlot.php b/src/Aggregation/BoxPlot.php new file mode 100644 index 0000000..316ba6a --- /dev/null +++ b/src/Aggregation/BoxPlot.php @@ -0,0 +1,57 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->compression !== null) { + $array['compression'] = $this->compression; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->executionHint !== null) { + $array['execution_hint'] = $this->executionHint; + } + + return ['boxplot' => $array]; + } + +} diff --git a/src/Aggregation/BucketScript.php b/src/Aggregation/BucketScript.php new file mode 100644 index 0000000..8ef5b23 --- /dev/null +++ b/src/Aggregation/BucketScript.php @@ -0,0 +1,56 @@ + $bucketsPath Map of script-variable name to sibling agg path. + */ + public function __construct( + private array $bucketsPath, + private string $script, + private string|null $gapPolicy = null, + private string|null $format = null, + private string $key = 'bucket_script', + ) + { + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + 'script' => $this->script, + ]; + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'bucket_script' => $array, + ]; + } + +} diff --git a/src/Aggregation/BucketSelector.php b/src/Aggregation/BucketSelector.php new file mode 100644 index 0000000..d9d930e --- /dev/null +++ b/src/Aggregation/BucketSelector.php @@ -0,0 +1,51 @@ + $bucketsPath Map of script-variable name to sibling agg path. + */ + public function __construct( + private array $bucketsPath, + private string $script, + private string|null $gapPolicy = null, + private string $key = 'bucket_selector', + ) + { + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + 'script' => $this->script, + ]; + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + return [ + 'bucket_selector' => $array, + ]; + } + +} diff --git a/src/Aggregation/BucketSort.php b/src/Aggregation/BucketSort.php new file mode 100644 index 0000000..fcf4d79 --- /dev/null +++ b/src/Aggregation/BucketSort.php @@ -0,0 +1,61 @@ +> $sort Sort entries, e.g. [['total_sales' => ['order' => 'desc']]]. + */ + public function __construct( + private array $sort = [], + private int|null $size = null, + private int|null $from = null, + private string|null $gapPolicy = null, + private string $key = 'bucket_sort', + ) + { + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = []; + + if ($this->sort !== []) { + $array['sort'] = $this->sort; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->from !== null) { + $array['from'] = $this->from; + } + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + return [ + 'bucket_sort' => $array, + ]; + } + +} diff --git a/src/Aggregation/Cardinality.php b/src/Aggregation/Cardinality.php new file mode 100644 index 0000000..6c4f699 --- /dev/null +++ b/src/Aggregation/Cardinality.php @@ -0,0 +1,57 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->precisionThreshold !== null) { + $array['precision_threshold'] = $this->precisionThreshold; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->rehash !== null) { + $array['rehash'] = $this->rehash; + } + + return ['cardinality' => $array]; + } + +} diff --git a/src/Aggregation/CategorizeText.php b/src/Aggregation/CategorizeText.php new file mode 100644 index 0000000..a2ba53e --- /dev/null +++ b/src/Aggregation/CategorizeText.php @@ -0,0 +1,80 @@ +|null $categorizationFilters + */ + public function __construct( + private string $field, + private int|null $maxUniqueTokens = null, + private int|null $maxMatchedTokens = null, + private float|null $similarityThreshold = null, + private array|null $categorizationFilters = null, + private int|null $shardSize = null, + private int|null $size = null, + private int|null $minDocCount = null, + private int|null $shardMinDocCount = null, + ) + { + } + + + public function key(): string + { + return 'categorize_text_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->maxUniqueTokens !== null) { + $array['max_unique_tokens'] = $this->maxUniqueTokens; + } + + if ($this->maxMatchedTokens !== null) { + $array['max_matched_tokens'] = $this->maxMatchedTokens; + } + + if ($this->similarityThreshold !== null) { + $array['similarity_threshold'] = $this->similarityThreshold; + } + + if ($this->categorizationFilters !== null) { + $array['categorization_filters'] = $this->categorizationFilters; + } + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + if ($this->shardMinDocCount !== null) { + $array['shard_min_doc_count'] = $this->shardMinDocCount; + } + + return ['categorize_text' => $array]; + } + +} diff --git a/src/Aggregation/Composite.php b/src/Aggregation/Composite.php new file mode 100644 index 0000000..5b4b005 --- /dev/null +++ b/src/Aggregation/Composite.php @@ -0,0 +1,71 @@ + + */ + private array $sources; + + + /** + * @param array|null $after + */ + public function __construct( + private string $key, + \Spameri\ElasticQuery\Aggregation\Composite\CompositeSourceInterface $source, + private int|null $size = null, + private array|null $after = null, + ) + { + $this->sources = [$source->key() => $source]; + } + + + public function addSource( + \Spameri\ElasticQuery\Aggregation\Composite\CompositeSourceInterface $source, + ): void + { + $this->sources[$source->key()] = $source; + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $sources = []; + foreach ($this->sources as $name => $source) { + $sources[] = [$name => $source->toArray()]; + } + + $array = ['sources' => $sources]; + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->after !== null) { + $array['after'] = $this->after; + } + + return ['composite' => $array]; + } + +} diff --git a/src/Aggregation/Composite/CompositeSourceInterface.php b/src/Aggregation/Composite/CompositeSourceInterface.php new file mode 100644 index 0000000..a120593 --- /dev/null +++ b/src/Aggregation/Composite/CompositeSourceInterface.php @@ -0,0 +1,11 @@ +name; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = ['field' => $this->field]; + + if ($this->calendarInterval !== null) { + $body['calendar_interval'] = $this->calendarInterval; + } + + if ($this->fixedInterval !== null) { + $body['fixed_interval'] = $this->fixedInterval; + } + + if ($this->format !== null) { + $body['format'] = $this->format; + } + + if ($this->timeZone !== null) { + $body['time_zone'] = $this->timeZone; + } + + if ($this->order !== null) { + $body['order'] = $this->order; + } + + if ($this->missingBucket !== null) { + $body['missing_bucket'] = $this->missingBucket; + } + + return ['date_histogram' => $body]; + } + +} diff --git a/src/Aggregation/Composite/GeotileGridSource.php b/src/Aggregation/Composite/GeotileGridSource.php new file mode 100644 index 0000000..dbc777c --- /dev/null +++ b/src/Aggregation/Composite/GeotileGridSource.php @@ -0,0 +1,50 @@ +name; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = ['field' => $this->field]; + + if ($this->precision !== null) { + $body['precision'] = $this->precision; + } + + if ($this->order !== null) { + $body['order'] = $this->order; + } + + if ($this->missingBucket !== null) { + $body['missing_bucket'] = $this->missingBucket; + } + + return ['geotile_grid' => $body]; + } + +} diff --git a/src/Aggregation/Composite/HistogramSource.php b/src/Aggregation/Composite/HistogramSource.php new file mode 100644 index 0000000..4ba67d8 --- /dev/null +++ b/src/Aggregation/Composite/HistogramSource.php @@ -0,0 +1,49 @@ +name; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'field' => $this->field, + 'interval' => $this->interval, + ]; + + if ($this->order !== null) { + $body['order'] = $this->order; + } + + if ($this->missingBucket !== null) { + $body['missing_bucket'] = $this->missingBucket; + } + + return ['histogram' => $body]; + } + +} diff --git a/src/Aggregation/Composite/TermsSource.php b/src/Aggregation/Composite/TermsSource.php new file mode 100644 index 0000000..f3b7f3c --- /dev/null +++ b/src/Aggregation/Composite/TermsSource.php @@ -0,0 +1,50 @@ +name; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = ['field' => $this->field]; + + if ($this->order !== null) { + $body['order'] = $this->order; + } + + if ($this->missingBucket !== null) { + $body['missing_bucket'] = $this->missingBucket; + } + + if ($this->missingOrder !== null) { + $body['missing_order'] = $this->missingOrder; + } + + return ['terms' => $body]; + } + +} diff --git a/src/Aggregation/CumulativeCardinality.php b/src/Aggregation/CumulativeCardinality.php new file mode 100644 index 0000000..90a7f16 --- /dev/null +++ b/src/Aggregation/CumulativeCardinality.php @@ -0,0 +1,43 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['buckets_path' => $this->bucketsPath]; + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['cumulative_cardinality' => $array]; + } + +} diff --git a/src/Aggregation/CumulativeSum.php b/src/Aggregation/CumulativeSum.php new file mode 100644 index 0000000..99d4b35 --- /dev/null +++ b/src/Aggregation/CumulativeSum.php @@ -0,0 +1,46 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'cumulative_sum' => $array, + ]; + } + +} diff --git a/src/Aggregation/DateHistogram.php b/src/Aggregation/DateHistogram.php new file mode 100644 index 0000000..0ead3e7 --- /dev/null +++ b/src/Aggregation/DateHistogram.php @@ -0,0 +1,113 @@ +|null $extendedBounds + * @param array|null $hardBounds + * @param array|null $order + */ + public function __construct( + private string $field, + private string|null $calendarInterval = null, + private string|null $fixedInterval = null, + private string|null $format = null, + private string|null $timeZone = null, + private int|null $minDocCount = null, + private string|null $offset = null, + private array|null $extendedBounds = null, + private array|null $hardBounds = null, + private bool|null $keyed = null, + private array|null $order = null, + private \Spameri\ElasticQuery\Script|null $script = null, + private string|null $missing = null, + ) + { + if ($this->calendarInterval === null && $this->fixedInterval === null) { + throw new \InvalidArgumentException( + 'Either calendarInterval or fixedInterval must be provided.', + ); + } + + if ($this->calendarInterval !== null && $this->fixedInterval !== null) { + throw new \InvalidArgumentException( + 'Only one of calendarInterval or fixedInterval may be provided.', + ); + } + } + + + public function key(): string + { + return 'date_histogram_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->calendarInterval !== null) { + $array['calendar_interval'] = $this->calendarInterval; + } + + if ($this->fixedInterval !== null) { + $array['fixed_interval'] = $this->fixedInterval; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + if ($this->timeZone !== null) { + $array['time_zone'] = $this->timeZone; + } + + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + if ($this->offset !== null) { + $array['offset'] = $this->offset; + } + + if ($this->extendedBounds !== null) { + $array['extended_bounds'] = $this->extendedBounds; + } + + if ($this->hardBounds !== null) { + $array['hard_bounds'] = $this->hardBounds; + } + + if ($this->keyed !== null) { + $array['keyed'] = $this->keyed; + } + + if ($this->order !== null) { + $array['order'] = $this->order; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + return ['date_histogram' => $array]; + } + +} diff --git a/src/Aggregation/DateRange.php b/src/Aggregation/DateRange.php new file mode 100644 index 0000000..c33544b --- /dev/null +++ b/src/Aggregation/DateRange.php @@ -0,0 +1,76 @@ +field; + } + + + public function ranges(): \Spameri\ElasticQuery\Aggregation\RangeValueCollection + { + return $this->ranges; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + ]; + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + if ($this->timeZone !== null) { + $array['time_zone'] = $this->timeZone; + } + + if ($this->keyed === true) { + $array['keyed'] = true; + } + + foreach ($this->ranges as $range) { + $array['ranges'][] = $range->toArray(); + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + return [ + 'date_range' => $array, + ]; + } + +} diff --git a/src/Aggregation/Derivative.php b/src/Aggregation/Derivative.php new file mode 100644 index 0000000..940174a --- /dev/null +++ b/src/Aggregation/Derivative.php @@ -0,0 +1,56 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->unit !== null) { + $array['unit'] = $this->unit; + } + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'derivative' => $array, + ]; + } + +} diff --git a/src/Aggregation/DiversifiedSampler.php b/src/Aggregation/DiversifiedSampler.php new file mode 100644 index 0000000..be1cd9e --- /dev/null +++ b/src/Aggregation/DiversifiedSampler.php @@ -0,0 +1,58 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + 'shard_size' => $this->shardSize, + ]; + + if ($this->maxDocsPerValue !== null) { + $array['max_docs_per_value'] = $this->maxDocsPerValue; + } + + if ($this->executionHint !== null) { + $array['execution_hint'] = $this->executionHint; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + return [ + 'diversified_sampler' => $array, + ]; + } + +} diff --git a/src/Aggregation/ExtendedStats.php b/src/Aggregation/ExtendedStats.php new file mode 100644 index 0000000..c4272c8 --- /dev/null +++ b/src/Aggregation/ExtendedStats.php @@ -0,0 +1,57 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->sigma !== null) { + $array['sigma'] = $this->sigma; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['extended_stats' => $array]; + } + +} diff --git a/src/Aggregation/ExtendedStatsBucket.php b/src/Aggregation/ExtendedStatsBucket.php new file mode 100644 index 0000000..651c54d --- /dev/null +++ b/src/Aggregation/ExtendedStatsBucket.php @@ -0,0 +1,53 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['buckets_path' => $this->bucketsPath]; + + if ($this->sigma !== null) { + $array['sigma'] = $this->sigma; + } + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['extended_stats_bucket' => $array]; + } + +} diff --git a/src/Aggregation/Filter.php b/src/Aggregation/Filter.php index 5fb9c1b..0025811 100644 --- a/src/Aggregation/Filter.php +++ b/src/Aggregation/Filter.php @@ -1,27 +1,40 @@ -key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + if ($this->filter === null) { + return ['filter' => ['bool' => new \stdClass()]]; } - return [ - 'filter' => [ - 'bool' => $array, - ], - ]; + return ['filter' => $this->filter->toArray()]; } } diff --git a/src/Aggregation/Filters.php b/src/Aggregation/Filters.php new file mode 100644 index 0000000..921aec4 --- /dev/null +++ b/src/Aggregation/Filters.php @@ -0,0 +1,70 @@ + + */ + private array $filters; + + + public function __construct( + private string $key = 'filters', + private bool|null $otherBucket = null, + private string|null $otherBucketKey = null, + ) + { + $this->filters = []; + } + + + public function addFilter( + string $name, + \Spameri\ElasticQuery\Query\LeafQueryInterface $filter, + ): void + { + $this->filters[$name] = $filter; + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $filters = []; + foreach ($this->filters as $name => $filter) { + $filters[$name] = $filter->toArray(); + } + + $body = ['filters' => $filters]; + + if ($this->otherBucket !== null) { + $body['other_bucket'] = $this->otherBucket; + } + + if ($this->otherBucketKey !== null) { + $body['other_bucket_key'] = $this->otherBucketKey; + } + + return ['filters' => $body]; + } + +} diff --git a/src/Aggregation/FrequentItemSets.php b/src/Aggregation/FrequentItemSets.php new file mode 100644 index 0000000..0d91c9f --- /dev/null +++ b/src/Aggregation/FrequentItemSets.php @@ -0,0 +1,66 @@ +> $fields Each entry: {field, ?include, ?exclude} + */ + public function __construct( + private array $fields, + private float|null $minimumSupport = null, + private int|null $minimumSetSize = null, + private int|null $size = null, + private \Spameri\ElasticQuery\Query\LeafQueryInterface|null $filter = null, + private string $key = 'frequent_item_sets', + ) + { + if ($fields === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'FrequentItemSets requires at least one field.', + ); + } + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['fields' => $this->fields]; + + if ($this->minimumSupport !== null) { + $array['minimum_support'] = $this->minimumSupport; + } + + if ($this->minimumSetSize !== null) { + $array['minimum_set_size'] = $this->minimumSetSize; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->filter !== null) { + $array['filter'] = $this->filter->toArray(); + } + + return ['frequent_item_sets' => $array]; + } + +} diff --git a/src/Aggregation/GeoBounds.php b/src/Aggregation/GeoBounds.php new file mode 100644 index 0000000..3c5309b --- /dev/null +++ b/src/Aggregation/GeoBounds.php @@ -0,0 +1,45 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + ]; + + if ($this->wrapLongitude === false) { + $array['wrap_longitude'] = false; + } + + return [ + 'geo_bounds' => $array, + ]; + } + +} diff --git a/src/Aggregation/GeoCentroid.php b/src/Aggregation/GeoCentroid.php new file mode 100644 index 0000000..8b5db75 --- /dev/null +++ b/src/Aggregation/GeoCentroid.php @@ -0,0 +1,38 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'geo_centroid' => [ + 'field' => $this->field, + ], + ]; + } + +} diff --git a/src/Aggregation/GeoDistance.php b/src/Aggregation/GeoDistance.php new file mode 100644 index 0000000..5014d49 --- /dev/null +++ b/src/Aggregation/GeoDistance.php @@ -0,0 +1,81 @@ +field; + } + + + public function ranges(): \Spameri\ElasticQuery\Aggregation\RangeValueCollection + { + return $this->ranges; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + 'origin' => [ + 'lat' => $this->lat, + 'lon' => $this->lon, + ], + ]; + + if ($this->unit !== null) { + $array['unit'] = $this->unit; + } + + if ($this->distanceType !== null) { + $array['distance_type'] = $this->distanceType; + } + + if ($this->keyed !== null) { + $array['keyed'] = $this->keyed; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + foreach ($this->ranges as $range) { + $array['ranges'][] = $range->toArray(); + } + + return ['geo_distance' => $array]; + } + +} diff --git a/src/Aggregation/GeoHashGrid.php b/src/Aggregation/GeoHashGrid.php new file mode 100644 index 0000000..f338bee --- /dev/null +++ b/src/Aggregation/GeoHashGrid.php @@ -0,0 +1,63 @@ +>|null $bounds e.g. ['top_left' => ['lat' => ..., 'lon' => ...], 'bottom_right' => [...]] + */ + public function __construct( + private string $field, + private int|null $precision = null, + private int|null $size = null, + private int|null $shardSize = null, + private array|null $bounds = null, + ) + { + } + + + public function key(): string + { + return 'geohash_grid_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + ]; + + if ($this->precision !== null) { + $array['precision'] = $this->precision; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->bounds !== null) { + $array['bounds'] = $this->bounds; + } + + return [ + 'geohash_grid' => $array, + ]; + } + +} diff --git a/src/Aggregation/GeoLine.php b/src/Aggregation/GeoLine.php new file mode 100644 index 0000000..764520c --- /dev/null +++ b/src/Aggregation/GeoLine.php @@ -0,0 +1,57 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'point' => ['field' => $this->pointField], + 'sort' => ['field' => $this->sortField], + ]; + + if ($this->sortOrder !== 'asc') { + $array['sort_order'] = $this->sortOrder; + } + + if ($this->includeSort !== null) { + $array['include_sort'] = $this->includeSort; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + return ['geo_line' => $array]; + } + +} diff --git a/src/Aggregation/GeoTileGrid.php b/src/Aggregation/GeoTileGrid.php new file mode 100644 index 0000000..d275154 --- /dev/null +++ b/src/Aggregation/GeoTileGrid.php @@ -0,0 +1,63 @@ +>|null $bounds + */ + public function __construct( + private string $field, + private int|null $precision = null, + private int|null $size = null, + private int|null $shardSize = null, + private array|null $bounds = null, + ) + { + } + + + public function key(): string + { + return 'geotile_grid_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + ]; + + if ($this->precision !== null) { + $array['precision'] = $this->precision; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->bounds !== null) { + $array['bounds'] = $this->bounds; + } + + return [ + 'geotile_grid' => $array, + ]; + } + +} diff --git a/src/Aggregation/GlobalAggregation.php b/src/Aggregation/GlobalAggregation.php new file mode 100644 index 0000000..cdbc6b0 --- /dev/null +++ b/src/Aggregation/GlobalAggregation.php @@ -0,0 +1,36 @@ +key; + } + + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'global' => new \stdClass(), + ]; + } + +} diff --git a/src/Aggregation/Histogram.php b/src/Aggregation/Histogram.php index 8a1b053..b161d93 100644 --- a/src/Aggregation/Histogram.php +++ b/src/Aggregation/Histogram.php @@ -1,4 +1,6 @@ -|null $order + */ public function __construct( - string $field - , int $interval + private string $field, + private int $interval, + private int|null $minDocCount = null, + private \Spameri\ElasticQuery\Aggregation\Histogram\Bounds|null $extendedBounds = null, + private \Spameri\ElasticQuery\Aggregation\Histogram\Bounds|null $hardBounds = null, + private float|int|null $offset = null, + private array|null $order = null, + private \Spameri\ElasticQuery\Script|null $script = null, + private float|int|string|null $missing = null, + private bool|null $keyed = null, + private string|null $format = null, ) { - $this->field = $field; - $this->interval = $interval; } @@ -30,14 +37,53 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { - return [ - 'histogram' => [ - 'field' => $this->field, - 'interval' => $this->interval, - ], + $array = [ + 'field' => $this->field, + 'interval' => $this->interval, ]; + + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + if ($this->extendedBounds !== null) { + $array['extended_bounds'] = $this->extendedBounds->toArray(); + } + + if ($this->hardBounds !== null) { + $array['hard_bounds'] = $this->hardBounds->toArray(); + } + + if ($this->offset !== null) { + $array['offset'] = $this->offset; + } + + if ($this->order !== null) { + $array['order'] = $this->order; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->keyed !== null) { + $array['keyed'] = $this->keyed; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['histogram' => $array]; } } diff --git a/src/Aggregation/Histogram/Bounds.php b/src/Aggregation/Histogram/Bounds.php new file mode 100644 index 0000000..e68b203 --- /dev/null +++ b/src/Aggregation/Histogram/Bounds.php @@ -0,0 +1,33 @@ + + */ + public function toArray(): array + { + return [ + 'min' => $this->min, + 'max' => $this->max, + ]; + } + +} diff --git a/src/Aggregation/Inference.php b/src/Aggregation/Inference.php new file mode 100644 index 0000000..12e38fe --- /dev/null +++ b/src/Aggregation/Inference.php @@ -0,0 +1,53 @@ + $bucketsPath Map of input feature => agg path. + * @param array|null $inferenceConfig + */ + public function __construct( + private string $modelId, + private array $bucketsPath, + private array|null $inferenceConfig = null, + private string $key = 'inference', + ) + { + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'model_id' => $this->modelId, + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->inferenceConfig !== null) { + $array['inference_config'] = $this->inferenceConfig; + } + + return ['inference' => $array]; + } + +} diff --git a/src/Aggregation/IpPrefix.php b/src/Aggregation/IpPrefix.php new file mode 100644 index 0000000..183807b --- /dev/null +++ b/src/Aggregation/IpPrefix.php @@ -0,0 +1,61 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + 'prefix_length' => $this->prefixLength, + ]; + + if ($this->isIpv6 !== null) { + $array['is_ipv6'] = $this->isIpv6; + } + + if ($this->appendPrefixLength !== null) { + $array['append_prefix_length'] = $this->appendPrefixLength; + } + + if ($this->keyed !== null) { + $array['keyed'] = $this->keyed; + } + + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + return ['ip_prefix' => $array]; + } + +} diff --git a/src/Aggregation/IpRange.php b/src/Aggregation/IpRange.php new file mode 100644 index 0000000..007e59c --- /dev/null +++ b/src/Aggregation/IpRange.php @@ -0,0 +1,50 @@ + $ranges + */ + public function __construct( + private string $field, + private array $ranges = [], + private bool $keyed = false, + ) + { + } + + + public function key(): string + { + return 'ip_range_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->keyed === true) { + $array['keyed'] = true; + } + + foreach ($this->ranges as $range) { + $array['ranges'][] = $range->toArray(); + } + + return ['ip_range' => $array]; + } + +} diff --git a/src/Aggregation/IpRange/IpRangeValue.php b/src/Aggregation/IpRange/IpRangeValue.php new file mode 100644 index 0000000..3a36c37 --- /dev/null +++ b/src/Aggregation/IpRange/IpRangeValue.php @@ -0,0 +1,57 @@ +key; + } + + + /** + * @return array + */ + public function toArray(): array + { + $array = ['key' => $this->key]; + + if ($this->mask !== null) { + $array['mask'] = $this->mask; + + } else { + if ($this->from !== null) { + $array['from'] = $this->from; + } + if ($this->to !== null) { + $array['to'] = $this->to; + } + } + + return $array; + } + +} diff --git a/src/Aggregation/LeafAggregationCollection.php b/src/Aggregation/LeafAggregationCollection.php index 0822bd7..5a3d0f0 100644 --- a/src/Aggregation/LeafAggregationCollection.php +++ b/src/Aggregation/LeafAggregationCollection.php @@ -1,4 +1,6 @@ - */ - private $aggregations; + private array $aggregations; public function __construct( - string $name - , ?\Spameri\ElasticQuery\Filter\FilterCollection $filter - , \Spameri\ElasticQuery\Aggregation\LeafAggregationInterface ... $aggregations + private string $name, + private \Spameri\ElasticQuery\Filter\FilterCollection|null $filter, + \Spameri\ElasticQuery\Aggregation\LeafAggregationInterface ...$aggregations, ) { if ( ! $filter) { $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); } - $this->name = $name; $this->filter = $filter; $this->aggregations = $aggregations; } diff --git a/src/Aggregation/LeafAggregationInterface.php b/src/Aggregation/LeafAggregationInterface.php index c984b18..4a3f369 100644 --- a/src/Aggregation/LeafAggregationInterface.php +++ b/src/Aggregation/LeafAggregationInterface.php @@ -1,4 +1,6 @@ - $fields + * @param array|null $missing Per-field missing values. + */ + public function __construct( + private array $fields, + private array|null $missing = null, + private string|null $mode = null, + private string $key = 'matrix_stats', + ) + { + if ($fields === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'MatrixStats requires at least one field.', + ); + } + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['fields' => $this->fields]; + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->mode !== null) { + $array['mode'] = $this->mode; + } + + return ['matrix_stats' => $array]; + } + +} diff --git a/src/Aggregation/Max.php b/src/Aggregation/Max.php index 2cf758d..0e77272 100644 --- a/src/Aggregation/Max.php +++ b/src/Aggregation/Max.php @@ -1,18 +1,23 @@ -field = $field; } @@ -23,15 +28,25 @@ public function key(): string /** - * @return array> + * @return array> */ public function toArray(): array { - return [ - 'max' => [ - 'field' => $this->field, - ], - ]; + $array = ['field' => $this->field]; + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['max' => $array]; } } diff --git a/src/Aggregation/MaxBucket.php b/src/Aggregation/MaxBucket.php new file mode 100644 index 0000000..846aacc --- /dev/null +++ b/src/Aggregation/MaxBucket.php @@ -0,0 +1,51 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'max_bucket' => $array, + ]; + } + +} diff --git a/src/Aggregation/MedianAbsoluteDeviation.php b/src/Aggregation/MedianAbsoluteDeviation.php new file mode 100644 index 0000000..8960580 --- /dev/null +++ b/src/Aggregation/MedianAbsoluteDeviation.php @@ -0,0 +1,52 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->compression !== null) { + $array['compression'] = $this->compression; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + return ['median_absolute_deviation' => $array]; + } + +} diff --git a/src/Aggregation/Min.php b/src/Aggregation/Min.php index fc9248c..47cabaf 100644 --- a/src/Aggregation/Min.php +++ b/src/Aggregation/Min.php @@ -1,17 +1,23 @@ -field = $field; + private string $field, + private float|int|string|null $missing = null, + private \Spameri\ElasticQuery\Script|null $script = null, + private string|null $format = null, + ) + { } @@ -22,14 +28,28 @@ public function key(): string /** - * @return array + * @return array> */ public function toArray(): array { + $array = [ + 'field' => $this->field, + ]; + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + return [ - 'min' => [ - 'field' => $this->field, - ], + 'min' => $array, ]; } diff --git a/src/Aggregation/MinBucket.php b/src/Aggregation/MinBucket.php new file mode 100644 index 0000000..ffaed8d --- /dev/null +++ b/src/Aggregation/MinBucket.php @@ -0,0 +1,51 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'min_bucket' => $array, + ]; + } + +} diff --git a/src/Aggregation/Missing.php b/src/Aggregation/Missing.php new file mode 100644 index 0000000..94c79e3 --- /dev/null +++ b/src/Aggregation/Missing.php @@ -0,0 +1,41 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + return ['missing' => $array]; + } + +} diff --git a/src/Aggregation/MovingFunction.php b/src/Aggregation/MovingFunction.php new file mode 100644 index 0000000..ec80032 --- /dev/null +++ b/src/Aggregation/MovingFunction.php @@ -0,0 +1,55 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + 'window' => $this->window, + 'script' => $this->script, + ]; + + if ($this->shift !== null) { + $array['shift'] = $this->shift; + } + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + return [ + 'moving_fn' => $array, + ]; + } + +} diff --git a/src/Aggregation/MultiTerms.php b/src/Aggregation/MultiTerms.php new file mode 100644 index 0000000..df6eef8 --- /dev/null +++ b/src/Aggregation/MultiTerms.php @@ -0,0 +1,82 @@ +> $terms Either field names or {field, missing} objects. + * @param array>|null $order + */ + public function __construct( + private array $terms, + private int|null $size = null, + private string $key = 'multi_terms', + private array|null $order = null, + private int|null $minDocCount = null, + private int|null $shardSize = null, + private int|null $shardMinDocCount = null, + private string|null $collectMode = null, + private string|null $format = null, + ) + { + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $termsArray = []; + foreach ($this->terms as $term) { + $termsArray[] = \is_array($term) ? $term : ['field' => $term]; + } + + $array = ['terms' => $termsArray]; + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->order !== null) { + $array['order'] = $this->order; + } + + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->shardMinDocCount !== null) { + $array['shard_min_doc_count'] = $this->shardMinDocCount; + } + + if ($this->collectMode !== null) { + $array['collect_mode'] = $this->collectMode; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['multi_terms' => $array]; + } + +} diff --git a/src/Aggregation/Nested.php b/src/Aggregation/Nested.php index 801265e..338a5f5 100644 --- a/src/Aggregation/Nested.php +++ b/src/Aggregation/Nested.php @@ -1,17 +1,16 @@ -path = $path; + private string $path, + ) + { } diff --git a/src/Aggregation/Normalize.php b/src/Aggregation/Normalize.php new file mode 100644 index 0000000..aa91b53 --- /dev/null +++ b/src/Aggregation/Normalize.php @@ -0,0 +1,48 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + 'method' => $this->method, + ]; + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'normalize' => $array, + ]; + } + +} diff --git a/src/Aggregation/PercentileRanks.php b/src/Aggregation/PercentileRanks.php new file mode 100644 index 0000000..59d6ee4 --- /dev/null +++ b/src/Aggregation/PercentileRanks.php @@ -0,0 +1,65 @@ + $values + * @param array|null $hdr + */ + public function __construct( + private string $field, + private array $values, + private bool $keyed = true, + private array|null $hdr = null, + private float|int|string|null $missing = null, + private \Spameri\ElasticQuery\Script|null $script = null, + ) + { + } + + + public function key(): string + { + return 'percentile_ranks_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + 'values' => $this->values, + ]; + + if ($this->keyed === false) { + $array['keyed'] = false; + } + + if ($this->hdr !== null) { + $array['hdr'] = $this->hdr; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + return ['percentile_ranks' => $array]; + } + +} diff --git a/src/Aggregation/Percentiles.php b/src/Aggregation/Percentiles.php new file mode 100644 index 0000000..f841716 --- /dev/null +++ b/src/Aggregation/Percentiles.php @@ -0,0 +1,72 @@ + $percents + * @param array|null $tdigest + * @param array|null $hdr + */ + public function __construct( + private string $field, + private array $percents = [], + private bool $keyed = true, + private array|null $tdigest = null, + private array|null $hdr = null, + private float|int|string|null $missing = null, + private \Spameri\ElasticQuery\Script|null $script = null, + ) + { + } + + + public function key(): string + { + return 'percentiles_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->percents !== []) { + $array['percents'] = $this->percents; + } + + if ($this->keyed === false) { + $array['keyed'] = false; + } + + if ($this->tdigest !== null) { + $array['tdigest'] = $this->tdigest; + } + + if ($this->hdr !== null) { + $array['hdr'] = $this->hdr; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + return ['percentiles' => $array]; + } + +} diff --git a/src/Aggregation/PercentilesBucket.php b/src/Aggregation/PercentilesBucket.php new file mode 100644 index 0000000..4679370 --- /dev/null +++ b/src/Aggregation/PercentilesBucket.php @@ -0,0 +1,59 @@ + $percents + */ + public function __construct( + private string $bucketsPath, + private array $percents = [], + private string|null $gapPolicy = null, + private string|null $format = null, + private string $key = 'percentiles_bucket', + ) + { + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->percents !== []) { + $array['percents'] = $this->percents; + } + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'percentiles_bucket' => $array, + ]; + } + +} diff --git a/src/Aggregation/RandomSampler.php b/src/Aggregation/RandomSampler.php new file mode 100644 index 0000000..2246aa2 --- /dev/null +++ b/src/Aggregation/RandomSampler.php @@ -0,0 +1,48 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['probability' => $this->probability]; + + if ($this->seed !== null) { + $array['seed'] = $this->seed; + } + + if ($this->shardSeed !== null) { + $array['shard_seed'] = $this->shardSeed; + } + + return ['random_sampler' => $array]; + } + +} diff --git a/src/Aggregation/Range.php b/src/Aggregation/Range.php index e4e9eb1..8107d73 100644 --- a/src/Aggregation/Range.php +++ b/src/Aggregation/Range.php @@ -1,4 +1,6 @@ -field = $field; - $this->keyed = $keyed; - $this->ranges = $rangeValueCollection ?: new \Spameri\ElasticQuery\Aggregation\RangeValueCollection(); } @@ -43,23 +29,34 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { - $array = [ - 'field' => $this->field, - ]; + $array = ['field' => $this->field]; - if ($this->keyed === TRUE) { - $array['keyed'] = TRUE; + if ($this->keyed === true) { + $array['keyed'] = true; } foreach ($this->ranges as $range) { $array['ranges'][] = $range->toArray(); } - return [ - 'range' => $array, - ]; + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['range' => $array]; } diff --git a/src/Aggregation/RangeValue.php b/src/Aggregation/RangeValue.php index b9c831b..6966fa4 100644 --- a/src/Aggregation/RangeValue.php +++ b/src/Aggregation/RangeValue.php @@ -1,4 +1,6 @@ -key = $key; - $this->from = $from; - $this->to = $to; - $this->fromEqual = $fromEqual; - $this->toEqual = $toEqual; } @@ -61,7 +38,7 @@ public function toArray(): array } return [ - 'key' => $this->key, + 'key' => $this->key, 'from' => $from, 'to' => $to, ]; diff --git a/src/Aggregation/RangeValueCollection.php b/src/Aggregation/RangeValueCollection.php index 63fc538..b292764 100644 --- a/src/Aggregation/RangeValueCollection.php +++ b/src/Aggregation/RangeValueCollection.php @@ -1,4 +1,6 @@ - */ - private $collection; + private array $collection; public function __construct( - RangeValue ... $collection + RangeValue ...$collection, ) { $this->collection = $collection; @@ -27,7 +29,7 @@ public function getIterator(): \ArrayIterator public function add( - \Spameri\ElasticQuery\Aggregation\RangeValue $rangeValue + \Spameri\ElasticQuery\Aggregation\RangeValue $rangeValue, ): void { $this->collection[] = $rangeValue; diff --git a/src/Aggregation/RareTerms.php b/src/Aggregation/RareTerms.php new file mode 100644 index 0000000..636bcd2 --- /dev/null +++ b/src/Aggregation/RareTerms.php @@ -0,0 +1,66 @@ +|null $include + * @param string|array|null $exclude + */ + public function __construct( + private string $field, + private int|null $maxDocCount = null, + private float|null $precision = null, + private string|array|null $include = null, + private string|array|null $exclude = null, + private string|null $missing = null, + ) + { + } + + + public function key(): string + { + return 'rare_terms_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->maxDocCount !== null) { + $array['max_doc_count'] = $this->maxDocCount; + } + + if ($this->precision !== null) { + $array['precision'] = $this->precision; + } + + if ($this->include !== null) { + $array['include'] = $this->include; + } + + if ($this->exclude !== null) { + $array['exclude'] = $this->exclude; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + return ['rare_terms' => $array]; + } + +} diff --git a/src/Aggregation/Rate.php b/src/Aggregation/Rate.php new file mode 100644 index 0000000..36237cb --- /dev/null +++ b/src/Aggregation/Rate.php @@ -0,0 +1,62 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = []; + + if ($this->unit !== null) { + $array['unit'] = $this->unit; + } + + if ($this->field !== null) { + $array['field'] = $this->field; + } + + if ($this->mode !== null) { + $array['mode'] = $this->mode; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + return ['rate' => $array]; + } + +} diff --git a/src/Aggregation/ReverseNested.php b/src/Aggregation/ReverseNested.php new file mode 100644 index 0000000..a545e66 --- /dev/null +++ b/src/Aggregation/ReverseNested.php @@ -0,0 +1,44 @@ +path ?? 'root'); + } + + + /** + * @return array|\stdClass> + */ + public function toArray(): array + { + if ($this->path === null) { + return [ + 'reverse_nested' => new \stdClass(), + ]; + } + + return [ + 'reverse_nested' => [ + 'path' => $this->path, + ], + ]; + } + +} diff --git a/src/Aggregation/Sampler.php b/src/Aggregation/Sampler.php new file mode 100644 index 0000000..d49ae1c --- /dev/null +++ b/src/Aggregation/Sampler.php @@ -0,0 +1,39 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'sampler' => [ + 'shard_size' => $this->shardSize, + ], + ]; + } + +} diff --git a/src/Aggregation/SerialDiff.php b/src/Aggregation/SerialDiff.php new file mode 100644 index 0000000..3ec5eba --- /dev/null +++ b/src/Aggregation/SerialDiff.php @@ -0,0 +1,56 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->lag !== null) { + $array['lag'] = $this->lag; + } + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'serial_diff' => $array, + ]; + } + +} diff --git a/src/Aggregation/SignificantTerms.php b/src/Aggregation/SignificantTerms.php new file mode 100644 index 0000000..0735c7d --- /dev/null +++ b/src/Aggregation/SignificantTerms.php @@ -0,0 +1,84 @@ +|null $heuristic e.g. ['mutual_information' => ['include_negatives' => true]] + */ + public function __construct( + private string $field, + private int|null $size = null, + private int|null $minDocCount = null, + private int|null $shardSize = null, + private int|null $shardMinDocCount = null, + private string|null $executionHint = null, + private \Spameri\ElasticQuery\Query\LeafQueryInterface|null $backgroundFilter = null, + private array|null $heuristic = null, + ) + { + } + + + public function key(): string + { + return 'significant_terms_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->shardMinDocCount !== null) { + $array['shard_min_doc_count'] = $this->shardMinDocCount; + } + + if ($this->executionHint !== null) { + $array['execution_hint'] = $this->executionHint; + } + + if ($this->backgroundFilter !== null) { + $array['background_filter'] = $this->backgroundFilter->toArray(); + } + + if ($this->heuristic !== null) { + foreach ($this->heuristic as $name => $config) { + $array[$name] = $config; + } + } + + return ['significant_terms' => $array]; + } + +} diff --git a/src/Aggregation/SignificantText.php b/src/Aggregation/SignificantText.php new file mode 100644 index 0000000..20bd38d --- /dev/null +++ b/src/Aggregation/SignificantText.php @@ -0,0 +1,75 @@ +|null $sourceFields + */ + public function __construct( + private string $field, + private int|null $size = null, + private bool $filterDuplicateText = false, + private int|null $shardSize = null, + private int|null $shardMinDocCount = null, + private int|null $minDocCount = null, + private \Spameri\ElasticQuery\Query\LeafQueryInterface|null $backgroundFilter = null, + private array|null $sourceFields = null, + ) + { + } + + + public function key(): string + { + return 'significant_text_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->filterDuplicateText === true) { + $array['filter_duplicate_text'] = true; + } + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->shardMinDocCount !== null) { + $array['shard_min_doc_count'] = $this->shardMinDocCount; + } + + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + if ($this->backgroundFilter !== null) { + $array['background_filter'] = $this->backgroundFilter->toArray(); + } + + if ($this->sourceFields !== null) { + $array['source_fields'] = $this->sourceFields; + } + + return ['significant_text' => $array]; + } + +} diff --git a/src/Aggregation/Stats.php b/src/Aggregation/Stats.php new file mode 100644 index 0000000..f82055d --- /dev/null +++ b/src/Aggregation/Stats.php @@ -0,0 +1,52 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['stats' => $array]; + } + +} diff --git a/src/Aggregation/StatsBucket.php b/src/Aggregation/StatsBucket.php new file mode 100644 index 0000000..f80bd55 --- /dev/null +++ b/src/Aggregation/StatsBucket.php @@ -0,0 +1,51 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'stats_bucket' => $array, + ]; + } + +} diff --git a/src/Aggregation/StringStats.php b/src/Aggregation/StringStats.php new file mode 100644 index 0000000..2b45106 --- /dev/null +++ b/src/Aggregation/StringStats.php @@ -0,0 +1,52 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->showDistribution === true) { + $array['show_distribution'] = true; + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + return ['string_stats' => $array]; + } + +} diff --git a/src/Aggregation/Sum.php b/src/Aggregation/Sum.php new file mode 100644 index 0000000..e6d003f --- /dev/null +++ b/src/Aggregation/Sum.php @@ -0,0 +1,52 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['sum' => $array]; + } + +} diff --git a/src/Aggregation/SumBucket.php b/src/Aggregation/SumBucket.php new file mode 100644 index 0000000..36aa0cb --- /dev/null +++ b/src/Aggregation/SumBucket.php @@ -0,0 +1,51 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'buckets_path' => $this->bucketsPath, + ]; + + if ($this->gapPolicy !== null) { + $array['gap_policy'] = $this->gapPolicy; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return [ + 'sum_bucket' => $array, + ]; + } + +} diff --git a/src/Aggregation/TTest.php b/src/Aggregation/TTest.php new file mode 100644 index 0000000..845a0fc --- /dev/null +++ b/src/Aggregation/TTest.php @@ -0,0 +1,52 @@ + $a Population A: ['field' => ..., optional 'filter' => ...] + * @param array $b Population B: ['field' => ..., optional 'filter' => ...] + */ + public function __construct( + private array $a, + private array $b, + private string $type = self::TYPE_HETEROSCEDASTIC, + private string $key = 't_test', + ) + { + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 't_test' => [ + 'a' => $this->a, + 'b' => $this->b, + 'type' => $this->type, + ], + ]; + } + +} diff --git a/src/Aggregation/Term.php b/src/Aggregation/Term.php index 6d42ed8..0ab9876 100644 --- a/src/Aggregation/Term.php +++ b/src/Aggregation/Term.php @@ -1,4 +1,6 @@ -|null $include + * @param string|array|null $exclude + */ public function __construct( - string $field, - int $size = 0, - int $missing = NULL, - ?\Spameri\ElasticQuery\Aggregation\Terms\OrderCollection $order = NULL, - ?string $include = NULL, - ?string $exclude = NULL, - ?string $key = NULL + private string $field, + private int $size = 0, + private int|null $missing = null, + \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection|null $order = null, + private string|array|null $include = null, + private string|array|null $exclude = null, + private string|null $key = null, + private int|null $minDocCount = null, + private int|null $shardSize = null, + private int|null $shardMinDocCount = null, + private bool|null $showTermDocCountError = null, + private \Spameri\ElasticQuery\Script|null $script = null, + private string|null $collectMode = null, + private string|null $executionHint = null, + private string|null $valueType = null, + private string|null $format = null, ) { - $this->field = $field; - $this->size = $size; - $this->missing = $missing; - $this->key = $key; $this->order = $order ?? new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection(); - $this->include = $include; - $this->exclude = $exclude; } @@ -50,17 +47,18 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { - $array = [ - 'field' => $this->field, - ]; + $array = ['field' => $this->field]; if ($this->size > 0) { - $array['size'] = $this->size; + $array['size'] = $this->size; } - if ($this->missing !== NULL) { + if ($this->missing !== null) { $array['missing'] = $this->missing; } @@ -68,17 +66,51 @@ public function toArray(): array $array['order'] = $this->order->toArray(); } - if ($this->include !== NULL) { + if ($this->include !== null) { $array['include'] = $this->include; } - if ($this->exclude !== NULL) { + if ($this->exclude !== null) { $array['exclude'] = $this->exclude; } - return [ - 'terms' => $array, - ]; + if ($this->minDocCount !== null) { + $array['min_doc_count'] = $this->minDocCount; + } + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->shardMinDocCount !== null) { + $array['shard_min_doc_count'] = $this->shardMinDocCount; + } + + if ($this->showTermDocCountError !== null) { + $array['show_term_doc_count_error'] = $this->showTermDocCountError; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->collectMode !== null) { + $array['collect_mode'] = $this->collectMode; + } + + if ($this->executionHint !== null) { + $array['execution_hint'] = $this->executionHint; + } + + if ($this->valueType !== null) { + $array['value_type'] = $this->valueType; + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['terms' => $array]; } } diff --git a/src/Aggregation/Terms/Order.php b/src/Aggregation/Terms/Order.php index 9da1f43..df5deeb 100644 --- a/src/Aggregation/Terms/Order.php +++ b/src/Aggregation/Terms/Order.php @@ -1,21 +1,17 @@ -field = $field; - $this->type = $type; + private string $field, + private string $type, + ) + { } diff --git a/src/Aggregation/Terms/OrderCollection.php b/src/Aggregation/Terms/OrderCollection.php index 0a492a4..b4cd101 100644 --- a/src/Aggregation/Terms/OrderCollection.php +++ b/src/Aggregation/Terms/OrderCollection.php @@ -1,4 +1,6 @@ -collection = []; foreach ($collection as $item) { $this->add($item); @@ -28,16 +31,16 @@ public function remove(string $key): bool if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } - public function get(string $key): ?\Spameri\ElasticQuery\Aggregation\Terms\Order + public function get(string $key): \Spameri\ElasticQuery\Aggregation\Terms\Order|null { - return $this->collection[$key] ?? NULL; + return $this->collection[$key] ?? null; } diff --git a/src/Aggregation/TimeSeries.php b/src/Aggregation/TimeSeries.php new file mode 100644 index 0000000..da92273 --- /dev/null +++ b/src/Aggregation/TimeSeries.php @@ -0,0 +1,47 @@ +key; + } + + + /** + * @return array|\stdClass> + */ + public function toArray(): array + { + $array = []; + + if ($this->keyed !== null) { + $array['keyed'] = $this->keyed; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + return ['time_series' => $array === [] ? new \stdClass() : $array]; + } + +} diff --git a/src/Aggregation/TopHits.php b/src/Aggregation/TopHits.php index e446163..67e33db 100644 --- a/src/Aggregation/TopHits.php +++ b/src/Aggregation/TopHits.php @@ -1,33 +1,99 @@ -|array> $source + * @param array $scriptFields Script-name => {script: {...}} map. + * @param array $docvalueFields + * @param array $storedFields + */ public function __construct( - int $size - ) { - $this->size = $size; + private int $size, + private int|null $from = null, + private \Spameri\ElasticQuery\Options\SortCollection|null $sort = null, + private bool|array $source = true, + private \Spameri\ElasticQuery\Highlight|null $highlight = null, + private bool|null $explain = null, + private array $scriptFields = [], + private array $docvalueFields = [], + private bool|null $version = null, + private bool|null $seqNoPrimaryTerm = null, + private array $storedFields = [], + private bool|null $trackScores = null, + private string $key = 'top_hits', + ) + { } public function key(): string { - return 'top_hits_' . $this->size; + return $this->key . '_' . $this->size; } + /** + * @return array> + */ public function toArray(): array { - return [ - 'top_hits' => [ - 'size' => $this->size, - ], - ]; + $array = ['size' => $this->size]; + + if ($this->from !== null) { + $array['from'] = $this->from; + } + + if ($this->sort !== null && $this->sort->count() > 0) { + $array['sort'] = $this->sort->toArray(); + } + + if ($this->source !== true) { + $array['_source'] = $this->source; + } + + if ($this->highlight !== null) { + $array['highlight'] = $this->highlight->toArray(); + } + + if ($this->explain !== null) { + $array['explain'] = $this->explain; + } + + if ($this->scriptFields !== []) { + $array['script_fields'] = $this->scriptFields; + } + + if ($this->docvalueFields !== []) { + $array['docvalue_fields'] = $this->docvalueFields; + } + + if ($this->version !== null) { + $array['version'] = $this->version; + } + + if ($this->seqNoPrimaryTerm !== null) { + $array['seq_no_primary_term'] = $this->seqNoPrimaryTerm; + } + + if ($this->storedFields !== []) { + $array['stored_fields'] = $this->storedFields; + } + + if ($this->trackScores !== null) { + $array['track_scores'] = $this->trackScores; + } + + return ['top_hits' => $array]; } } diff --git a/src/Aggregation/TopMetrics.php b/src/Aggregation/TopMetrics.php new file mode 100644 index 0000000..1c3facd --- /dev/null +++ b/src/Aggregation/TopMetrics.php @@ -0,0 +1,61 @@ + $metrics Field names to capture. + * @param array>|null $sort + */ + public function __construct( + private array $metrics, + private array|null $sort = null, + private int|null $size = null, + private string $key = 'top_metrics', + ) + { + if ($metrics === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'TopMetrics requires at least one metric field.', + ); + } + } + + + public function key(): string + { + return $this->key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = []; + + foreach ($this->metrics as $metric) { + $array['metrics'][] = ['field' => $metric]; + } + + if ($this->sort !== null) { + $array['sort'] = $this->sort; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + return ['top_metrics' => $array]; + } + +} diff --git a/src/Aggregation/ValueCount.php b/src/Aggregation/ValueCount.php new file mode 100644 index 0000000..edc7ad0 --- /dev/null +++ b/src/Aggregation/ValueCount.php @@ -0,0 +1,47 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['value_count' => $array]; + } + +} diff --git a/src/Aggregation/VariableWidthHistogram.php b/src/Aggregation/VariableWidthHistogram.php new file mode 100644 index 0000000..2f4cb02 --- /dev/null +++ b/src/Aggregation/VariableWidthHistogram.php @@ -0,0 +1,51 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'field' => $this->field, + 'buckets' => $this->buckets, + ]; + + if ($this->shardSize !== null) { + $array['shard_size'] = $this->shardSize; + } + + if ($this->initialBuffer !== null) { + $array['initial_buffer'] = $this->initialBuffer; + } + + return ['variable_width_histogram' => $array]; + } + +} diff --git a/src/Aggregation/WeightedAvg.php b/src/Aggregation/WeightedAvg.php new file mode 100644 index 0000000..1a6190b --- /dev/null +++ b/src/Aggregation/WeightedAvg.php @@ -0,0 +1,47 @@ +key; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $array = [ + 'value' => $this->value->toArray(), + 'weight' => $this->weight->toArray(), + ]; + + if ($this->format !== null) { + $array['format'] = $this->format; + } + + return ['weighted_avg' => $array]; + } + +} diff --git a/src/Aggregation/WeightedAvg/WeightedAvgValue.php b/src/Aggregation/WeightedAvg/WeightedAvgValue.php new file mode 100644 index 0000000..2205f01 --- /dev/null +++ b/src/Aggregation/WeightedAvg/WeightedAvgValue.php @@ -0,0 +1,52 @@ + + */ + public function toArray(): array + { + $array = []; + + if ($this->field !== null) { + $array['field'] = $this->field; + } + + if ($this->script !== null) { + $array['script'] = $this->script->toArray(); + } + + if ($this->missing !== null) { + $array['missing'] = $this->missing; + } + + return $array; + } + +} diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php index c0736fc..7bdeb95 100644 --- a/src/Collection/AbstractCollection.php +++ b/src/Collection/AbstractCollection.php @@ -1,4 +1,6 @@ - */ - protected $collection; + protected array $collection; public function __construct( - \Spameri\ElasticQuery\Entity\EntityInterface ... $collection + \Spameri\ElasticQuery\Entity\EntityInterface ...$collection, ) { $this->collection = []; @@ -24,7 +26,7 @@ public function __construct( public function add( - \Spameri\ElasticQuery\Entity\EntityInterface $item + \Spameri\ElasticQuery\Entity\EntityInterface $item, ): void { $this->collection[$item->key()] = $item; @@ -32,36 +34,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Entity\EntityInterface + string $key, + ): \Spameri\ElasticQuery\Entity\EntityInterface|null { - return $this->collection[$key] ?? NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php index d6e0065..922fd0a 100644 --- a/src/Collection/CollectionInterface.php +++ b/src/Collection/CollectionInterface.php @@ -1,4 +1,6 @@ -index = $index; - $this->body = $body; - $this->type = $type; - $this->id = $id; - $this->options = $options; - } - - - public function index(): ?string - { - return $this->index; - } - - - public function type(): ?string - { - return $this->type; } @@ -72,11 +30,10 @@ public function toArray(): array $array['body'] = $this->body->toArray(); } - if ($this->type) { - $array['type'] = $this->type; - } - - if ($this->id) { + if ( + \is_string($this->id) + && $this->id !== '' + ) { $array['id'] = $this->id; } diff --git a/src/Document/Body/Plain.php b/src/Document/Body/Plain.php index 1af495e..15c6d03 100644 --- a/src/Document/Body/Plain.php +++ b/src/Document/Body/Plain.php @@ -1,4 +1,6 @@ -parameters = $parameters; } diff --git a/src/Document/Body/Settings.php b/src/Document/Body/Settings.php index 5fe114f..c21a0b2 100644 --- a/src/Document/Body/Settings.php +++ b/src/Document/Body/Settings.php @@ -1,4 +1,6 @@ -analyzer = $analyzer; - $this->filter = $filter; } @@ -37,7 +27,7 @@ public function toArray(): array $filters = []; /** @var \Spameri\ElasticQuery\Mapping\FilterInterface $filter */ foreach ($this->filter as $filter) { - if ($filter->toArray() === [] ) { + if ($filter->toArray() === []) { continue; } $filters[$filter->key()] = $filter->toArray()[$filter->key()]; diff --git a/src/Document/BodyInterface.php b/src/Document/BodyInterface.php index 50b704b..13563b9 100644 --- a/src/Document/BodyInterface.php +++ b/src/Document/BodyInterface.php @@ -1,4 +1,6 @@ -data = $data; } diff --git a/src/ElasticQuery.php b/src/ElasticQuery.php index afe267f..17f704f 100644 --- a/src/ElasticQuery.php +++ b/src/ElasticQuery.php @@ -1,4 +1,6 @@ -query = $query; $this->filter = $filter; + $this->postFilter = $postFilter; $this->sort = $sort; $this->aggregation = $aggregation; $this->options = $options; - $this->highlight = $highlight; - $this->functionScore = $functionScore; } @@ -81,13 +83,13 @@ public function options(): \Spameri\ElasticQuery\Options } - public function highlight(): ?\Spameri\ElasticQuery\Highlight + public function highlight(): \Spameri\ElasticQuery\Highlight|null { return $this->highlight; } - public function functionScore(): ?\Spameri\ElasticQuery\FunctionScore + public function functionScore(): \Spameri\ElasticQuery\FunctionScore|null { return $this->functionScore; } @@ -111,6 +113,18 @@ public function addShouldQuery(\Spameri\ElasticQuery\Query\LeafQueryInterface $l } + public function postFilter(): \Spameri\ElasticQuery\Query\QueryCollection + { + return $this->postFilter; + } + + + public function addPostFilterMustQuery(\Spameri\ElasticQuery\Query\LeafQueryInterface $leafQuery): void + { + $this->postFilter->must()->add($leafQuery); + } + + public function addFilter(\Spameri\ElasticQuery\Query\LeafQueryInterface $leafQuery): void { $this->filter->must()->add($leafQuery); @@ -132,7 +146,7 @@ public function toArray(): array $array['query'] = $queryArray; } - if ($this->functionScore !== NULL) { + if ($this->functionScore !== null) { $array['query'] = $this->functionScore->toArray($array['query']); } @@ -141,6 +155,11 @@ public function toArray(): array $array['query']['bool']['filter'] = $filterArray; } + $postFilterArray = $this->postFilter->toArray(); + if ($postFilterArray) { + $array['post_filter'] = $postFilterArray; + } + $sortArray = $this->sort->toArray(); if ($sortArray) { $array['sort'] = $sortArray; @@ -151,10 +170,37 @@ public function toArray(): array $array['aggs'] = $aggregation; } - if ($this->highlight !== NULL) { + if ($this->highlight !== null) { $array['highlight'] = $this->highlight->toArray(); } + $collapse = $this->options->collapse(); + if ($collapse !== null) { + $array['collapse'] = $collapse->toArray(); + } + + $rescore = $this->options->rescore(); + if ($rescore !== null && $rescore !== []) { + $rescoreArray = []; + foreach ($rescore as $r) { + $rescoreArray[] = $r->toArray(); + } + $array['rescore'] = $rescoreArray; + } + + $suggesters = $this->options->suggesters(); + if ($suggesters !== null && $suggesters !== []) { + $suggest = []; + $text = $this->options->suggestText(); + if ($text !== null) { + $suggest['text'] = $text; + } + foreach ($suggesters as $suggester) { + $suggest[$suggester->key()] = $suggester->toArray(); + } + $array['suggest'] = $suggest; + } + return $array; } diff --git a/src/Entity/AbstractEntity.php b/src/Entity/AbstractEntity.php index 5914db6..47b5964 100644 --- a/src/Entity/AbstractEntity.php +++ b/src/Entity/AbstractEntity.php @@ -1,4 +1,6 @@ -mustCollection = $mustCollection; + $this->mustCollection = $mustCollection ?? new \Spameri\ElasticQuery\Query\MustCollection(); + $this->shouldCollection = $shouldCollection ?? new \Spameri\ElasticQuery\Query\ShouldCollection(); + $this->mustNotCollection = $mustNotCollection ?? new \Spameri\ElasticQuery\Query\MustNotCollection(); + $this->filterCollection = $filterCollection ?? new \Spameri\ElasticQuery\Query\MustCollection(); } @@ -30,21 +37,58 @@ public function must(): \Spameri\ElasticQuery\Query\MustCollection } + public function should(): \Spameri\ElasticQuery\Query\ShouldCollection + { + return $this->shouldCollection; + } + + + public function mustNot(): \Spameri\ElasticQuery\Query\MustNotCollection + { + return $this->mustNotCollection; + } + + + public function filter(): \Spameri\ElasticQuery\Query\MustCollection + { + return $this->filterCollection; + } + + public function key(): string { return ''; } + /** + * @return array> + */ public function toArray(): array { - $array = []; - /** @var \Spameri\ElasticQuery\Query\LeafQueryInterface $item */ + $bool = []; + foreach ($this->mustCollection as $item) { - $array['bool']['must'][] = $item->toArray(); + $bool['must'][] = $item->toArray(); + } + + foreach ($this->shouldCollection as $item) { + $bool['should'][] = $item->toArray(); + } + + foreach ($this->mustNotCollection as $item) { + $bool['must_not'][] = $item->toArray(); + } + + foreach ($this->filterCollection as $item) { + $bool['filter'][] = $item->toArray(); + } + + if ($bool === []) { + return []; } - return $array; + return ['bool' => $bool]; } } diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php index 2cd009d..f7f1ca9 100644 --- a/src/Filter/FilterInterface.php +++ b/src/Filter/FilterInterface.php @@ -1,4 +1,6 @@ -function = $function ?? new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); - $this->scoreMode = $scoreMode; } @@ -32,12 +43,16 @@ public function function(): \Spameri\ElasticQuery\FunctionScore\FunctionScoreCol } - public function scoreMode(): ?string + public function scoreMode(): string|null { return $this->scoreMode; } + /** + * @param array $queryPart + * @return array> + */ public function toArray(array $queryPart): array { $functions = []; @@ -52,10 +67,26 @@ public function toArray(array $queryPart): array ], ]; - if ($this->scoreMode !== NULL) { + if ($this->scoreMode !== null) { $array['function_score']['score_mode'] = $this->scoreMode; } + if ($this->boostMode !== null) { + $array['function_score']['boost_mode'] = $this->boostMode; + } + + if ($this->boost !== null) { + $array['function_score']['boost'] = $this->boost; + } + + if ($this->maxBoost !== null) { + $array['function_score']['max_boost'] = $this->maxBoost; + } + + if ($this->minScore !== null) { + $array['function_score']['min_score'] = $this->minScore; + } + return $array; } diff --git a/src/FunctionScore/FunctionScoreCollection.php b/src/FunctionScore/FunctionScoreCollection.php index ca678d3..f53f8fe 100644 --- a/src/FunctionScore/FunctionScoreCollection.php +++ b/src/FunctionScore/FunctionScoreCollection.php @@ -1,4 +1,6 @@ -collection = []; @@ -26,7 +28,7 @@ public function __construct( * @param \Spameri\ElasticQuery\FunctionScore\FunctionScoreInterface $item */ public function add( - $item + $item, ): void { $this->collection[$item->key()] = $item; @@ -34,36 +36,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\FunctionScore\FunctionScoreInterface + string $key, + ): \Spameri\ElasticQuery\FunctionScore\FunctionScoreInterface|null { - return $this->collection[$key] ?? NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/FunctionScore/FunctionScoreInterface.php b/src/FunctionScore/FunctionScoreInterface.php index 87ec6e5..c492f08 100644 --- a/src/FunctionScore/FunctionScoreInterface.php +++ b/src/FunctionScore/FunctionScoreInterface.php @@ -1,11 +1,13 @@ -name() . '_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $fieldBody = [ + 'origin' => $this->origin, + 'scale' => $this->scale, + ]; + + if ($this->offset !== null) { + $fieldBody['offset'] = $this->offset; + } + + if ($this->decay !== null) { + $fieldBody['decay'] = $this->decay; + } + + $body = [$this->field => $fieldBody]; + + if ($this->multiValueMode !== null) { + $body['multi_value_mode'] = $this->multiValueMode; + } + + return [$this->name() => $body]; + } + +} diff --git a/src/FunctionScore/ScoreFunction/Decay/Exp.php b/src/FunctionScore/ScoreFunction/Decay/Exp.php new file mode 100644 index 0000000..b01842f --- /dev/null +++ b/src/FunctionScore/ScoreFunction/Decay/Exp.php @@ -0,0 +1,16 @@ +field = $field; - $this->factor = $factor; - $this->modifier = $modifier; - $this->missing = $missing; + private string $field, + private float $factor = 1.0, + private string $modifier = self::MODIFIER_NONE, + private float $missing = 1.0, + ) + { } diff --git a/src/FunctionScore/ScoreFunction/RandomScore.php b/src/FunctionScore/ScoreFunction/RandomScore.php index 997588a..ed6d8c3 100644 --- a/src/FunctionScore/ScoreFunction/RandomScore.php +++ b/src/FunctionScore/ScoreFunction/RandomScore.php @@ -1,16 +1,16 @@ -seed = $seed; } @@ -20,7 +20,7 @@ public function key(): string } - public function seed(): ?string + public function seed(): string|null { return $this->seed; } diff --git a/src/FunctionScore/ScoreFunction/ScriptScore.php b/src/FunctionScore/ScoreFunction/ScriptScore.php new file mode 100644 index 0000000..6800665 --- /dev/null +++ b/src/FunctionScore/ScoreFunction/ScriptScore.php @@ -0,0 +1,44 @@ +name ?? \spl_object_hash($this)); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'script_score' => [ + 'script' => $this->script->toArray(), + ], + ]; + } + +} diff --git a/src/FunctionScore/ScoreFunction/Weight.php b/src/FunctionScore/ScoreFunction/Weight.php index c04a571..edd16a3 100644 --- a/src/FunctionScore/ScoreFunction/Weight.php +++ b/src/FunctionScore/ScoreFunction/Weight.php @@ -1,21 +1,17 @@ -weight = $weight; - $this->leafQuery = $leafQuery; + private float $weight, + private \Spameri\ElasticQuery\Query\LeafQueryInterface $leafQuery, + ) + { } diff --git a/src/Highlight.php b/src/Highlight.php index b96795e..82998b2 100644 --- a/src/Highlight.php +++ b/src/Highlight.php @@ -1,28 +1,71 @@ - $preTags + * @param array $postTags + * @param \Spameri\ElasticQuery\Highlight\HighlightFieldCollection|array $fields Either a typed collection or simple field-name list. + * @param array|null $matchedFields + */ + public function __construct( + private array $preTags, + private array $postTags, + \Spameri\ElasticQuery\Highlight\HighlightFieldCollection|array $fields, + private string|null $type = null, + int|null $numberOfFragments = 0, + private int|null $fragmentSize = null, + private string|null $boundaryScanner = null, + private string|null $boundaryChars = null, + private int|null $boundaryMaxScan = null, + private string|null $boundaryScannerLocale = null, + private string|null $encoder = null, + private bool|null $forceSource = null, + private string|null $fragmenter = null, + private \Spameri\ElasticQuery\Query\LeafQueryInterface|null $highlightQuery = null, + private array|null $matchedFields = null, + private int|null $noMatchSize = null, + private string|null $order = null, + private int|null $phraseLimit = null, + private bool|null $requireFieldMatch = null, + private string|null $tagsSchema = null, + ) + { + if ($fields instanceof \Spameri\ElasticQuery\Highlight\HighlightFieldCollection) { + $this->fields = $fields; + } else { + $this->fields = new \Spameri\ElasticQuery\Highlight\HighlightFieldCollection(); + foreach ($fields as $fieldName) { + $this->fields->add(new \Spameri\ElasticQuery\Highlight\HighlightField( + field: $fieldName, + numberOfFragments: $numberOfFragments, + )); + } + } + } - public function __construct( - array $preTags, - array $postTags, - array $fields - ) { - $this->preTags = $preTags; - $this->postTags = $postTags; - $this->fields = $fields; + public function fields(): \Spameri\ElasticQuery\Highlight\HighlightFieldCollection + { + return $this->fields; } + /** + * @return array + */ public function toArray(): array { $array = [ @@ -30,10 +73,75 @@ public function toArray(): array 'post_tags' => $this->postTags, ]; - foreach ($this->fields as $key) { - $array['fields'][$key] = [ - 'number_of_fragments' => 0, - ]; + $fieldsArray = $this->fields->toArray(); + if ($fieldsArray !== []) { + $array['fields'] = $fieldsArray; + } + + if ($this->type !== null) { + $array['type'] = $this->type; + } + + // global number_of_fragments is mirrored into each field above; we don't emit it twice + + if ($this->fragmentSize !== null) { + $array['fragment_size'] = $this->fragmentSize; + } + + if ($this->boundaryScanner !== null) { + $array['boundary_scanner'] = $this->boundaryScanner; + } + + if ($this->boundaryChars !== null) { + $array['boundary_chars'] = $this->boundaryChars; + } + + if ($this->boundaryMaxScan !== null) { + $array['boundary_max_scan'] = $this->boundaryMaxScan; + } + + if ($this->boundaryScannerLocale !== null) { + $array['boundary_scanner_locale'] = $this->boundaryScannerLocale; + } + + if ($this->encoder !== null) { + $array['encoder'] = $this->encoder; + } + + if ($this->forceSource !== null) { + $array['force_source'] = $this->forceSource; + } + + if ($this->fragmenter !== null) { + $array['fragmenter'] = $this->fragmenter; + } + + if ($this->highlightQuery !== null) { + $array['highlight_query'] = $this->highlightQuery->toArray(); + } + + if ($this->matchedFields !== null) { + $array['matched_fields'] = $this->matchedFields; + } + + if ($this->noMatchSize !== null) { + $array['no_match_size'] = $this->noMatchSize; + } + + if ($this->order !== null) { + $array['order'] = $this->order; + } + + if ($this->phraseLimit !== null) { + $array['phrase_limit'] = $this->phraseLimit; + } + + if ($this->requireFieldMatch !== null) { + $array['require_field_match'] = $this->requireFieldMatch; + } + + if ($this->tagsSchema !== null) { + $array['tags_schema'] = $this->tagsSchema; } return $array; diff --git a/src/Highlight/HighlightField.php b/src/Highlight/HighlightField.php new file mode 100644 index 0000000..7477b43 --- /dev/null +++ b/src/Highlight/HighlightField.php @@ -0,0 +1,139 @@ +|null $preTags + * @param array|null $postTags + * @param array|null $matchedFields + */ + public function __construct( + private string $field, + private string|null $type = null, + private int|null $numberOfFragments = null, + private int|null $fragmentSize = null, + private string|null $boundaryScanner = null, + private string|null $boundaryChars = null, + private int|null $boundaryMaxScan = null, + private string|null $boundaryScannerLocale = null, + private string|null $encoder = null, + private bool|null $forceSource = null, + private string|null $fragmenter = null, + private \Spameri\ElasticQuery\Query\LeafQueryInterface|null $highlightQuery = null, + private array|null $matchedFields = null, + private int|null $noMatchSize = null, + private string|null $order = null, + private int|null $phraseLimit = null, + private bool|null $requireFieldMatch = null, + private string|null $tagsSchema = null, + private array|null $preTags = null, + private array|null $postTags = null, + ) + { + } + + + public function key(): string + { + return $this->field; + } + + + /** + * @return array + */ + public function toArray(): array + { + $array = []; + + if ($this->type !== null) { + $array['type'] = $this->type; + } + + if ($this->numberOfFragments !== null) { + $array['number_of_fragments'] = $this->numberOfFragments; + } + + if ($this->fragmentSize !== null) { + $array['fragment_size'] = $this->fragmentSize; + } + + if ($this->boundaryScanner !== null) { + $array['boundary_scanner'] = $this->boundaryScanner; + } + + if ($this->boundaryChars !== null) { + $array['boundary_chars'] = $this->boundaryChars; + } + + if ($this->boundaryMaxScan !== null) { + $array['boundary_max_scan'] = $this->boundaryMaxScan; + } + + if ($this->boundaryScannerLocale !== null) { + $array['boundary_scanner_locale'] = $this->boundaryScannerLocale; + } + + if ($this->encoder !== null) { + $array['encoder'] = $this->encoder; + } + + if ($this->forceSource !== null) { + $array['force_source'] = $this->forceSource; + } + + if ($this->fragmenter !== null) { + $array['fragmenter'] = $this->fragmenter; + } + + if ($this->highlightQuery !== null) { + $array['highlight_query'] = $this->highlightQuery->toArray(); + } + + if ($this->matchedFields !== null) { + $array['matched_fields'] = $this->matchedFields; + } + + if ($this->noMatchSize !== null) { + $array['no_match_size'] = $this->noMatchSize; + } + + if ($this->order !== null) { + $array['order'] = $this->order; + } + + if ($this->phraseLimit !== null) { + $array['phrase_limit'] = $this->phraseLimit; + } + + if ($this->requireFieldMatch !== null) { + $array['require_field_match'] = $this->requireFieldMatch; + } + + if ($this->tagsSchema !== null) { + $array['tags_schema'] = $this->tagsSchema; + } + + if ($this->preTags !== null) { + $array['pre_tags'] = $this->preTags; + } + + if ($this->postTags !== null) { + $array['post_tags'] = $this->postTags; + } + + return $array; + } + +} diff --git a/src/Highlight/HighlightFieldCollection.php b/src/Highlight/HighlightFieldCollection.php new file mode 100644 index 0000000..4105056 --- /dev/null +++ b/src/Highlight/HighlightFieldCollection.php @@ -0,0 +1,25 @@ +> + */ + public function toArray(): array + { + $array = []; + foreach ($this->collection as $field) { + \assert($field instanceof HighlightField); + $array[$field->key()] = $field->toArray(); + } + + return $array; + } + +} diff --git a/src/Mapping/AllowedValues.php b/src/Mapping/AllowedValues.php index e4ae2ff..584b733 100644 --- a/src/Mapping/AllowedValues.php +++ b/src/Mapping/AllowedValues.php @@ -1,4 +1,6 @@ - self::TYPE_TEXT, + self::TYPE_TEXT => self::TYPE_TEXT, self::TYPE_KEYWORD => self::TYPE_KEYWORD, self::TYPE_BOOLEAN => self::TYPE_BOOLEAN, - self::TYPE_INTEGER => self::TYPE_INTEGER, - self::TYPE_LONG => self::TYPE_LONG, - self::TYPE_SHORT => self::TYPE_SHORT, - self::TYPE_BYTE => self::TYPE_BYTE, - self::TYPE_DOUBLE => self::TYPE_DOUBLE, - self::TYPE_FLOAT => self::TYPE_FLOAT, - self::TYPE_HALF_FLOAT => self::TYPE_HALF_FLOAT, + self::TYPE_INTEGER => self::TYPE_INTEGER, + self::TYPE_LONG => self::TYPE_LONG, + self::TYPE_SHORT => self::TYPE_SHORT, + self::TYPE_BYTE => self::TYPE_BYTE, + self::TYPE_DOUBLE => self::TYPE_DOUBLE, + self::TYPE_FLOAT => self::TYPE_FLOAT, + self::TYPE_HALF_FLOAT => self::TYPE_HALF_FLOAT, self::TYPE_SCALED_FLOAT => self::TYPE_SCALED_FLOAT, self::TYPE_DATE => self::TYPE_DATE, self::TYPE_INTEGER_RANGE => self::TYPE_INTEGER_RANGE, - self::TYPE_FLOAT_RANGE => self::TYPE_FLOAT_RANGE, - self::TYPE_LONG_RANGE => self::TYPE_LONG_RANGE, - self::TYPE_DOUBLE_RANGE => self::TYPE_DOUBLE_RANGE, - self::TYPE_DATE_RANGE => self::TYPE_DATE_RANGE, + self::TYPE_FLOAT_RANGE => self::TYPE_FLOAT_RANGE, + self::TYPE_LONG_RANGE => self::TYPE_LONG_RANGE, + self::TYPE_DOUBLE_RANGE => self::TYPE_DOUBLE_RANGE, + self::TYPE_DATE_RANGE => self::TYPE_DATE_RANGE, self::TYPE_OBJECT => self::TYPE_OBJECT, self::TYPE_NESTED => self::TYPE_NESTED, @@ -89,19 +88,16 @@ class AllowedValues self::TYPE_GEO_POINT => self::TYPE_GEO_POINT, self::TYPE_GEO_SHAPE => self::TYPE_GEO_SHAPE, - self::TYPE_IP => self::TYPE_IP, - self::TYPE_COMPLETION => self::TYPE_COMPLETION, + self::TYPE_IP => self::TYPE_IP, + self::TYPE_COMPLETION => self::TYPE_COMPLETION, self::TYPE_TOKEN_COUNT => self::TYPE_TOKEN_COUNT, - self::TYPE_MURMUR3 => self::TYPE_MURMUR3, - self::TYPE_PERCOLATOR => self::TYPE_PERCOLATOR, - self::TYPE_JOIN => self::TYPE_JOIN, - self::TYPE_ALIAS => self::TYPE_ALIAS, + self::TYPE_MURMUR3 => self::TYPE_MURMUR3, + self::TYPE_PERCOLATOR => self::TYPE_PERCOLATOR, + self::TYPE_JOIN => self::TYPE_JOIN, + self::TYPE_ALIAS => self::TYPE_ALIAS, ]; - // // ANALYZERS - // - public const ANALYZER_STANDARD = 'standard'; public const ANALYZER_SIMPLE = 'simple'; public const ANALYZER_WHITESPACE = 'whitespace'; @@ -178,7 +174,7 @@ class AllowedValues self::ANALYZER_IRISH => self::ANALYZER_IRISH, self::ANALYZER_ITALIAN => self::ANALYZER_ITALIAN, self::ANALYZER_LITHUANIAN => self::ANALYZER_LITHUANIAN, - self::ANALYZER_NORWEGIAN=> self::ANALYZER_NORWEGIAN, + self::ANALYZER_NORWEGIAN => self::ANALYZER_NORWEGIAN, self::ANALYZER_PERSIAN => self::ANALYZER_PERSIAN, self::ANALYZER_PORTUGUESE => self::ANALYZER_PORTUGUESE, self::ANALYZER_ROMANIAN => self::ANALYZER_ROMANIAN, @@ -190,10 +186,7 @@ class AllowedValues self::ANALYZER_THAI => self::ANALYZER_THAI, ]; - // // TOKENIZERS - // - public const TOKENIZER_STANDARD = 'standard'; public const TOKENIZER_LETTER = 'letter'; public const TOKENIZER_LOWERCASE = 'lowercase'; @@ -213,28 +206,26 @@ class AllowedValues public const TOKENIZER_PATH = 'path_hierarchy'; public const TOKENIZERS = [ - self::TOKENIZER_STANDARD => self::TOKENIZER_STANDARD, - self::TOKENIZER_LETTER => self::TOKENIZER_LETTER, - self::TOKENIZER_LOWERCASE => self::TOKENIZER_LOWERCASE, - self::TOKENIZER_WHITESPACE => self::TOKENIZER_WHITESPACE, - self::TOKENIZER_UAX_URL_EMAIL => self::TOKENIZER_UAX_URL_EMAIL, - self::TOKENIZER_CLASSIC => self::TOKENIZER_CLASSIC, - self::TOKENIZER_THAI => self::TOKENIZER_THAI, - - self::TOKENIZER_NGRAM => self::TOKENIZER_NGRAM, - self::TOKENIZER_EDGE_NGRAM => self::TOKENIZER_EDGE_NGRAM, - - self::TOKENIZER_KEYWORD => self::TOKENIZER_KEYWORD, - self::TOKENIZER_PATTERN => self::TOKENIZER_PATTERN, - self::TOKENIZER_SIMPLE_PATTERN => self::TOKENIZER_SIMPLE_PATTERN, - self::TOKENIZER_CHAR_GROUP => self::TOKENIZER_CHAR_GROUP, - self::TOKENIZER_SIMPLE_PATTERN_SPLIT => self::TOKENIZER_SIMPLE_PATTERN_SPLIT, - self::TOKENIZER_PATH => self::TOKENIZER_PATH, + self::TOKENIZER_STANDARD => self::TOKENIZER_STANDARD, + self::TOKENIZER_LETTER => self::TOKENIZER_LETTER, + self::TOKENIZER_LOWERCASE => self::TOKENIZER_LOWERCASE, + self::TOKENIZER_WHITESPACE => self::TOKENIZER_WHITESPACE, + self::TOKENIZER_UAX_URL_EMAIL => self::TOKENIZER_UAX_URL_EMAIL, + self::TOKENIZER_CLASSIC => self::TOKENIZER_CLASSIC, + self::TOKENIZER_THAI => self::TOKENIZER_THAI, + + self::TOKENIZER_NGRAM => self::TOKENIZER_NGRAM, + self::TOKENIZER_EDGE_NGRAM => self::TOKENIZER_EDGE_NGRAM, + + self::TOKENIZER_KEYWORD => self::TOKENIZER_KEYWORD, + self::TOKENIZER_PATTERN => self::TOKENIZER_PATTERN, + self::TOKENIZER_SIMPLE_PATTERN => self::TOKENIZER_SIMPLE_PATTERN, + self::TOKENIZER_CHAR_GROUP => self::TOKENIZER_CHAR_GROUP, + self::TOKENIZER_SIMPLE_PATTERN_SPLIT => self::TOKENIZER_SIMPLE_PATTERN_SPLIT, + self::TOKENIZER_PATH => self::TOKENIZER_PATH, ]; - // // Setting blocks - // public const BLOCK_TYPE = 'type'; public const BLOCK_ANALYZER = 'analyzer'; public const BLOCK_TOKENIZER = 'tokenizer'; @@ -242,10 +233,11 @@ class AllowedValues public const BLOCK_FORMAT = 'format'; public const BLOCKS = [ - self::BLOCK_TYPE => self::BLOCK_TYPE, - self::BLOCK_ANALYZER => self::BLOCK_ANALYZER, - self::BLOCK_TOKENIZER => self::BLOCK_TOKENIZER, - self::BLOCK_PROPERTIES => self::BLOCK_PROPERTIES, - self::BLOCK_FORMAT => self::BLOCK_FORMAT, + self::BLOCK_TYPE => self::BLOCK_TYPE, + self::BLOCK_ANALYZER => self::BLOCK_ANALYZER, + self::BLOCK_TOKENIZER => self::BLOCK_TOKENIZER, + self::BLOCK_PROPERTIES => self::BLOCK_PROPERTIES, + self::BLOCK_FORMAT => self::BLOCK_FORMAT, ]; + } diff --git a/src/Mapping/Analyzer/AbstractDictionary.php b/src/Mapping/Analyzer/AbstractDictionary.php index 9fe04ac..10ac71d 100644 --- a/src/Mapping/Analyzer/AbstractDictionary.php +++ b/src/Mapping/Analyzer/AbstractDictionary.php @@ -1,28 +1,20 @@ -stopFilter = $stopFilter; } @@ -44,7 +36,7 @@ public function tokenizer(): string } - public function getStopFilter(): ?\Spameri\ElasticQuery\Mapping\Filter\AbstractStop + public function getStopFilter(): \Spameri\ElasticQuery\Mapping\Filter\AbstractStop|null { return $this->stopFilter; } @@ -66,9 +58,9 @@ public function toArray(): array return [ $this->name() => [ - 'type' => $this->getType(), + 'type' => $this->getType(), 'tokenizer' => $this->tokenizer(), - 'filter' => $filterArray, + 'filter' => $filterArray, ], ]; } diff --git a/src/Mapping/Analyzer/Custom/ArabicDictionary.php b/src/Mapping/Analyzer/Custom/ArabicDictionary.php index acbe3ce..cfcf723 100644 --- a/src/Mapping/Analyzer/Custom/ArabicDictionary.php +++ b/src/Mapping/Analyzer/Custom/ArabicDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Arabic() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Arabic(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Arabic() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Arabic(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Arabic() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Arabic(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/BulgarianDictionary.php b/src/Mapping/Analyzer/Custom/BulgarianDictionary.php index a7d4575..534be78 100644 --- a/src/Mapping/Analyzer/Custom/BulgarianDictionary.php +++ b/src/Mapping/Analyzer/Custom/BulgarianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Bulgarian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Bulgarian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Bulgarian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Bulgarian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Bulgarian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Bulgarian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/CatalanDictionary.php b/src/Mapping/Analyzer/Custom/CatalanDictionary.php index ddf504d..160711c 100644 --- a/src/Mapping/Analyzer/Custom/CatalanDictionary.php +++ b/src/Mapping/Analyzer/Custom/CatalanDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Catalan() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Catalan(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Catalan() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Catalan(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Catalan() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Catalan(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/CommonGrams.php b/src/Mapping/Analyzer/Custom/CommonGrams.php index 29a256b..cf263fd 100644 --- a/src/Mapping/Analyzer/Custom/CommonGrams.php +++ b/src/Mapping/Analyzer/Custom/CommonGrams.php @@ -1,33 +1,25 @@ - - */ - private $commonGramWords; + protected \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection|null $filter = null; - /** - * @var \Spameri\ElasticQuery\Mapping\Filter\AbstractStop - */ - private $stopFilter; + private \Spameri\ElasticQuery\Mapping\Filter\AbstractStop $stopFilter; public function __construct( - array $commonGramWords, - ?\Spameri\ElasticQuery\Mapping\Filter\AbstractStop $stopFilter = NULL + private array $commonGramWords, + \Spameri\ElasticQuery\Mapping\Filter\AbstractStop|null $stopFilter = null, ) { - $this->commonGramWords = $commonGramWords; - if ($stopFilter === NULL) { + if ($stopFilter === null) { $stopFilter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech(); } $this->stopFilter = $stopFilter; @@ -42,7 +34,7 @@ public function key(): string public function name(): string { - return 'customCommonGrams'; + return self::NAME; } @@ -63,17 +55,17 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter if ( ! $this->filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); $this->filter->add($this->stopFilter); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\CommonGrams($this->commonGramWords) + new \Spameri\ElasticQuery\Mapping\Filter\CommonGrams($this->commonGramWords), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); } diff --git a/src/Mapping/Analyzer/Custom/CzechDictionary.php b/src/Mapping/Analyzer/Custom/CzechDictionary.php index 79247c1..f9e0f67 100644 --- a/src/Mapping/Analyzer/Custom/CzechDictionary.php +++ b/src/Mapping/Analyzer/Custom/CzechDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Czech() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Czech(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/DanishDictionary.php b/src/Mapping/Analyzer/Custom/DanishDictionary.php index e053b5a..5675408 100644 --- a/src/Mapping/Analyzer/Custom/DanishDictionary.php +++ b/src/Mapping/Analyzer/Custom/DanishDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Danish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Danish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Danish() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Danish(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Danish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Danish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/DutchDictionary.php b/src/Mapping/Analyzer/Custom/DutchDictionary.php index ab70a62..cb1f853 100644 --- a/src/Mapping/Analyzer/Custom/DutchDictionary.php +++ b/src/Mapping/Analyzer/Custom/DutchDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Dutch() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Dutch(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Dutch() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Dutch(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Dutch() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Dutch(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/EdgeNgram.php b/src/Mapping/Analyzer/Custom/EdgeNgram.php index 1df2b84..d3d5d6f 100644 --- a/src/Mapping/Analyzer/Custom/EdgeNgram.php +++ b/src/Mapping/Analyzer/Custom/EdgeNgram.php @@ -1,40 +1,26 @@ -minGram = $minGram; - $this->maxGram = $maxGram; - if ($stopFilter === NULL) { + if ($stopFilter === null) { $stopFilter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech(); } $this->stopFilter = $stopFilter; @@ -49,7 +35,7 @@ public function key(): string public function name(): string { - return 'customEdgeNgram'; + return self::NAME; } @@ -70,17 +56,17 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter if ( ! $this->filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); $this->filter->add($this->stopFilter); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\EdgeNgram($this->minGram, $this->maxGram) + new \Spameri\ElasticQuery\Mapping\Filter\EdgeNgram($this->minGram, $this->maxGram), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); } @@ -93,7 +79,7 @@ public function toArray(): array $filterArray = []; /** @var \Spameri\ElasticQuery\Mapping\FilterInterface $filter */ foreach ($this->filter() as $filter) { - $filterArray[] = $filter->getType(); + $filterArray[] = $filter->key(); } return [ diff --git a/src/Mapping/Analyzer/Custom/EnglishDictionary.php b/src/Mapping/Analyzer/Custom/EnglishDictionary.php index 6d08b7b..916612b 100644 --- a/src/Mapping/Analyzer/Custom/EnglishDictionary.php +++ b/src/Mapping/Analyzer/Custom/EnglishDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\English() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\English() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\English(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\English() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/FrenchDictionary.php b/src/Mapping/Analyzer/Custom/FrenchDictionary.php index b2f4bad..8ad079f 100644 --- a/src/Mapping/Analyzer/Custom/FrenchDictionary.php +++ b/src/Mapping/Analyzer/Custom/FrenchDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\French() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\French(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\French() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\French(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\French() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\French(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/GermanDictionary.php b/src/Mapping/Analyzer/Custom/GermanDictionary.php index b51aaa5..804621a 100644 --- a/src/Mapping/Analyzer/Custom/GermanDictionary.php +++ b/src/Mapping/Analyzer/Custom/GermanDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\German() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\German(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\German() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\German(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\German() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\German(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/GreekDictionary.php b/src/Mapping/Analyzer/Custom/GreekDictionary.php index 245f823..cbe090b 100644 --- a/src/Mapping/Analyzer/Custom/GreekDictionary.php +++ b/src/Mapping/Analyzer/Custom/GreekDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Greek() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Greek(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Greek() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Greek(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Greek() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Greek(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/HindiDictionary.php b/src/Mapping/Analyzer/Custom/HindiDictionary.php index 50c7ec1..adf0d38 100644 --- a/src/Mapping/Analyzer/Custom/HindiDictionary.php +++ b/src/Mapping/Analyzer/Custom/HindiDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hindi() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hindi(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Hindi() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Hindi(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hindi() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hindi(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/HungarianDictionary.php b/src/Mapping/Analyzer/Custom/HungarianDictionary.php index 4023b5a..7f87614 100644 --- a/src/Mapping/Analyzer/Custom/HungarianDictionary.php +++ b/src/Mapping/Analyzer/Custom/HungarianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hungarian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hungarian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Hungarian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Hungarian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hungarian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Hungarian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/IndonesianDictionary.php b/src/Mapping/Analyzer/Custom/IndonesianDictionary.php index 3787dd9..a07f0d3 100644 --- a/src/Mapping/Analyzer/Custom/IndonesianDictionary.php +++ b/src/Mapping/Analyzer/Custom/IndonesianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Indonesian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Indonesian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Indonesian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Indonesian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Indonesian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Indonesian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/ItalianDictionary.php b/src/Mapping/Analyzer/Custom/ItalianDictionary.php index 91e36d1..50de647 100644 --- a/src/Mapping/Analyzer/Custom/ItalianDictionary.php +++ b/src/Mapping/Analyzer/Custom/ItalianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Italian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Italian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Italian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Italian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Italian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Italian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/LatvianDictionary.php b/src/Mapping/Analyzer/Custom/LatvianDictionary.php index 58d0b75..6f9d157 100644 --- a/src/Mapping/Analyzer/Custom/LatvianDictionary.php +++ b/src/Mapping/Analyzer/Custom/LatvianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Latvian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Latvian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Latvian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Latvian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Latvian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Latvian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/Lowercase.php b/src/Mapping/Analyzer/Custom/Lowercase.php index 0271d56..56aeaa4 100644 --- a/src/Mapping/Analyzer/Custom/Lowercase.php +++ b/src/Mapping/Analyzer/Custom/Lowercase.php @@ -1,14 +1,13 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); } diff --git a/src/Mapping/Analyzer/Custom/NorwegianDictionary.php b/src/Mapping/Analyzer/Custom/NorwegianDictionary.php index e84a5db..bb99fdb 100644 --- a/src/Mapping/Analyzer/Custom/NorwegianDictionary.php +++ b/src/Mapping/Analyzer/Custom/NorwegianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Norwegian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Norwegian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Norwegian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Norwegian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Norwegian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Norwegian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/PortugueseDictionary.php b/src/Mapping/Analyzer/Custom/PortugueseDictionary.php index 93d0440..cabb6c3 100644 --- a/src/Mapping/Analyzer/Custom/PortugueseDictionary.php +++ b/src/Mapping/Analyzer/Custom/PortugueseDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Portuguese() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Portuguese(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Portuguese() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Portuguese(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Portuguese() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Portuguese(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/RomanianDictionary.php b/src/Mapping/Analyzer/Custom/RomanianDictionary.php index 28faeaa..017e05e 100644 --- a/src/Mapping/Analyzer/Custom/RomanianDictionary.php +++ b/src/Mapping/Analyzer/Custom/RomanianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Romanian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Romanian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Romanian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Romanian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Romanian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Romanian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/RussianDictionary.php b/src/Mapping/Analyzer/Custom/RussianDictionary.php index 9697907..464e3e5 100644 --- a/src/Mapping/Analyzer/Custom/RussianDictionary.php +++ b/src/Mapping/Analyzer/Custom/RussianDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Russian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Russian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Russian() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Russian(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Russian() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Russian(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/SlovakDictionary.php b/src/Mapping/Analyzer/Custom/SlovakDictionary.php index 300ce9f..6187df7 100644 --- a/src/Mapping/Analyzer/Custom/SlovakDictionary.php +++ b/src/Mapping/Analyzer/Custom/SlovakDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Slovak() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Slovak(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Slovak() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Slovak(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Slovak() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Slovak(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/SpanishDictionary.php b/src/Mapping/Analyzer/Custom/SpanishDictionary.php index ed914aa..0f4a4e6 100644 --- a/src/Mapping/Analyzer/Custom/SpanishDictionary.php +++ b/src/Mapping/Analyzer/Custom/SpanishDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Spanish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Spanish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Spanish() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Spanish(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Spanish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Spanish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/SwedishDictionary.php b/src/Mapping/Analyzer/Custom/SwedishDictionary.php index d9e930b..a47add2 100644 --- a/src/Mapping/Analyzer/Custom/SwedishDictionary.php +++ b/src/Mapping/Analyzer/Custom/SwedishDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Swedish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Swedish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Swedish() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Swedish(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Swedish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Swedish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/Synonym/AbstractSynonym.php b/src/Mapping/Analyzer/Custom/Synonym/AbstractSynonym.php index 39736a7..e7b72f4 100644 --- a/src/Mapping/Analyzer/Custom/Synonym/AbstractSynonym.php +++ b/src/Mapping/Analyzer/Custom/Synonym/AbstractSynonym.php @@ -1,4 +1,6 @@ -stopFilter = $stopFilter; - $this->synonyms = $synonyms; - $this->filePath = $filePath; } @@ -79,9 +60,9 @@ public function toArray(): array return [ $this->name() => [ - 'type' => $this->getType(), + 'type' => $this->getType(), 'tokenizer' => $this->tokenizer(), - 'filter' => $filterArray, + 'filter' => $filterArray, ], ]; } diff --git a/src/Mapping/Analyzer/Custom/Synonym/CzechSynonym.php b/src/Mapping/Analyzer/Custom/Synonym/CzechSynonym.php index 2c63b62..e0c85c2 100644 --- a/src/Mapping/Analyzer/Custom/Synonym/CzechSynonym.php +++ b/src/Mapping/Analyzer/Custom/Synonym/CzechSynonym.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech(), ); } if (\count($this->synonyms)) { $this->filter->add( new \Spameri\ElasticQuery\Mapping\Filter\Synonym( - $this->synonyms - ) + $this->synonyms, + ), ); } - if ($this->filePath !== NULL) { + if ($this->filePath !== null) { $this->filter->add( new \Spameri\ElasticQuery\Mapping\Filter\FileSynonym( - $this->filePath - ) + $this->filePath, + ), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Czech(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/Synonym/EnglishSynonym.php b/src/Mapping/Analyzer/Custom/Synonym/EnglishSynonym.php index c3aec36..96ad6de 100644 --- a/src/Mapping/Analyzer/Custom/Synonym/EnglishSynonym.php +++ b/src/Mapping/Analyzer/Custom/Synonym/EnglishSynonym.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\English() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(), ); } if (\count($this->synonyms)) { $this->filter->add( new \Spameri\ElasticQuery\Mapping\Filter\Synonym( - $this->synonyms - ) + $this->synonyms, + ), ); } - if ($this->filePath !== NULL) { + if ($this->filePath !== null) { $this->filter->add( new \Spameri\ElasticQuery\Mapping\Filter\FileSynonym( - $this->filePath - ) + $this->filePath, + ), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\English() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/Synonym/GermanSynonym.php b/src/Mapping/Analyzer/Custom/Synonym/GermanSynonym.php index 89d3680..ae43772 100644 --- a/src/Mapping/Analyzer/Custom/Synonym/GermanSynonym.php +++ b/src/Mapping/Analyzer/Custom/Synonym/GermanSynonym.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\German() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\German(), ); } if (\count($this->synonyms)) { $this->filter->add( new \Spameri\ElasticQuery\Mapping\Filter\Synonym( - $this->synonyms - ) + $this->synonyms, + ), ); } - if ($this->filePath !== NULL) { + if ($this->filePath !== null) { $this->filter->add( new \Spameri\ElasticQuery\Mapping\Filter\FileSynonym( - $this->filePath - ) + $this->filePath, + ), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { $this->filter->add($this->stopFilter); } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\German() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\German(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/ThaiDictionary.php b/src/Mapping/Analyzer/Custom/ThaiDictionary.php index ceb280a..93bd08c 100644 --- a/src/Mapping/Analyzer/Custom/ThaiDictionary.php +++ b/src/Mapping/Analyzer/Custom/ThaiDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Thai() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Thai(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Thai() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Thai(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Thai() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Thai(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/TurkishDictionary.php b/src/Mapping/Analyzer/Custom/TurkishDictionary.php index 980e742..cde16df 100644 --- a/src/Mapping/Analyzer/Custom/TurkishDictionary.php +++ b/src/Mapping/Analyzer/Custom/TurkishDictionary.php @@ -1,4 +1,6 @@ -filter instanceof \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection) { $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -26,14 +28,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Turkish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Turkish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Turkish() + new \Spameri\ElasticQuery\Mapping\Filter\Hunspell\Turkish(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); if ($this->stopFilter) { @@ -41,14 +43,14 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter } else { $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Stop\Turkish() + new \Spameri\ElasticQuery\Mapping\Filter\Stop\Turkish(), ); } $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); } diff --git a/src/Mapping/Analyzer/Custom/WordDelimiter.php b/src/Mapping/Analyzer/Custom/WordDelimiter.php index fd54e6a..9295b12 100644 --- a/src/Mapping/Analyzer/Custom/WordDelimiter.php +++ b/src/Mapping/Analyzer/Custom/WordDelimiter.php @@ -1,26 +1,24 @@ -stopFilter = $stopFilter; @@ -35,7 +33,7 @@ public function key(): string public function name(): string { - return 'customWordDelimiter'; + return self::NAME; } @@ -57,16 +55,16 @@ public function filter(): \Spameri\ElasticQuery\Mapping\Settings\Analysis\Filter $this->filter = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); $this->filter->add($this->stopFilter); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\WordDelimiter() + new \Spameri\ElasticQuery\Mapping\Filter\WordDelimiter(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding() + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Lowercase() + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), ); $this->filter->add( - new \Spameri\ElasticQuery\Mapping\Filter\Unique() + new \Spameri\ElasticQuery\Mapping\Filter\Unique(), ); } diff --git a/src/Mapping/Analyzer/Fingerprint.php b/src/Mapping/Analyzer/Fingerprint.php index 599543f..1da1dad 100644 --- a/src/Mapping/Analyzer/Fingerprint.php +++ b/src/Mapping/Analyzer/Fingerprint.php @@ -1,4 +1,6 @@ -stopWords = $stopWords; - $this->separator = $separator; - $this->maxOutputSize = $maxOutputSize; } diff --git a/src/Mapping/Analyzer/Keyword.php b/src/Mapping/Analyzer/Keyword.php index baaaa63..b571bd9 100644 --- a/src/Mapping/Analyzer/Keyword.php +++ b/src/Mapping/Analyzer/Keyword.php @@ -1,4 +1,6 @@ -pattern = $pattern; - $this->stopWords = $stopWords; - $this->lowerCase = $lowerCase; - $this->flags = $flags; } diff --git a/src/Mapping/Analyzer/Simple.php b/src/Mapping/Analyzer/Simple.php index 895122e..0ebe4bc 100644 --- a/src/Mapping/Analyzer/Simple.php +++ b/src/Mapping/Analyzer/Simple.php @@ -1,4 +1,6 @@ -stopWords = $stopWords; - $this->maxTokenLength = $maxTokenLength; } diff --git a/src/Mapping/Analyzer/Stop.php b/src/Mapping/Analyzer/Stop.php index 8bbdf13..526a991 100644 --- a/src/Mapping/Analyzer/Stop.php +++ b/src/Mapping/Analyzer/Stop.php @@ -1,4 +1,6 @@ -stopWords = $stopWords; } public function name(): string diff --git a/src/Mapping/Analyzer/Stop/StopWords.php b/src/Mapping/Analyzer/Stop/StopWords.php index f70ff6c..943c218 100644 --- a/src/Mapping/Analyzer/Stop/StopWords.php +++ b/src/Mapping/Analyzer/Stop/StopWords.php @@ -1,4 +1,6 @@ - - */ - private $extraWords; - - public function __construct( - array $extraWords = [] + private array $extraWords = [], ) { - $this->extraWords = $extraWords; } @@ -53,7 +48,7 @@ public function toArray(): array return [ $this->getName() => [ - 'type' => $this->getType(), + 'type' => $this->getType(), 'stopwords' => $stopWords, ], ]; diff --git a/src/Mapping/Filter/Apostrophe.php b/src/Mapping/Filter/Apostrophe.php index fc904ab..2231482 100644 --- a/src/Mapping/Filter/Apostrophe.php +++ b/src/Mapping/Filter/Apostrophe.php @@ -1,4 +1,6 @@ - - */ - private $words; - - - public function __construct(array $words) + public function __construct( + private array $words, + ) { - $this->words = $words; } @@ -41,4 +38,5 @@ public function toArray(): array ], ]; } + } diff --git a/src/Mapping/Filter/Conditional.php b/src/Mapping/Filter/Conditional.php index ee8364b..93afa88 100644 --- a/src/Mapping/Filter/Conditional.php +++ b/src/Mapping/Filter/Conditional.php @@ -1,4 +1,6 @@ -minGram = $minGram; - $this->maxGram = $maxGram; } diff --git a/src/Mapping/Filter/Elision.php b/src/Mapping/Filter/Elision.php index 292c714..7f06768 100644 --- a/src/Mapping/Filter/Elision.php +++ b/src/Mapping/Filter/Elision.php @@ -1,4 +1,6 @@ -path = $path; } @@ -50,7 +45,7 @@ public function toArray(): array { return [ $this->getName() => [ - 'type' => $this->getType(), + 'type' => $this->getType(), 'synonyms_path' => $this->path, ], ]; diff --git a/src/Mapping/Filter/Fingerprint.php b/src/Mapping/Filter/Fingerprint.php index aab3e80..2624d1f 100644 --- a/src/Mapping/Filter/Fingerprint.php +++ b/src/Mapping/Filter/Fingerprint.php @@ -1,4 +1,6 @@ - - */ - private $synonyms; - - public function __construct( - array $synonyms = [] + private array $synonyms = [], ) { - $this->synonyms = $synonyms; } @@ -55,7 +50,7 @@ public function toArray(): array return [ $this->getName() => [ - 'type' => $this->getType(), + 'type' => $this->getType(), 'synonyms' => $synonyms, ], ]; diff --git a/src/Mapping/Filter/SynonymGraph.php b/src/Mapping/Filter/SynonymGraph.php index 6cb9141..644ba8e 100644 --- a/src/Mapping/Filter/SynonymGraph.php +++ b/src/Mapping/Filter/SynonymGraph.php @@ -1,4 +1,6 @@ -key() => [ 'type' => $this->getType(), - 'catenate_all' => TRUE, + 'catenate_all' => true, ], ]; } diff --git a/src/Mapping/Filter/WordDelimiterGraph.php b/src/Mapping/Filter/WordDelimiterGraph.php index 2acbb79..02225f5 100644 --- a/src/Mapping/Filter/WordDelimiterGraph.php +++ b/src/Mapping/Filter/WordDelimiterGraph.php @@ -1,4 +1,6 @@ -analysis = $analysis; - $this->indexName = $indexName; $this->mapping = $mapping; $this->alias = $alias; } @@ -55,17 +43,29 @@ public function indexName(): string return $this->indexName; } + public function hasSti(): bool + { + return $this->hasSti; + } + + public function changeIndexName(string $index): void + { + $this->indexName = $index; + } + public function analysis(): \Spameri\ElasticQuery\Mapping\Settings\Analysis { return $this->analysis; } + public function mapping(): \Spameri\ElasticQuery\Mapping\Settings\Mapping { return $this->mapping; } + public function addMappingField(\Spameri\ElasticQuery\Mapping\Settings\Mapping\Field $field): void { $this->mapping->addField($field); @@ -77,8 +77,8 @@ public function addMappingFieldKeyword(string $name): void $this->addMappingField( new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( $name, - \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD - ) + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD, + ), ); } @@ -88,8 +88,8 @@ public function addMappingFieldFloat(string $name): void $this->addMappingField( new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( $name, - \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_FLOAT - ) + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_FLOAT, + ), ); } @@ -99,8 +99,8 @@ public function addMappingFieldInteger(string $name): void $this->addMappingField( new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( $name, - \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER - ) + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER, + ), ); } @@ -110,8 +110,8 @@ public function addMappingFieldBoolean(string $name): void $this->addMappingField( new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( $name, - \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_BOOLEAN - ) + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_BOOLEAN, + ), ); } @@ -185,7 +185,6 @@ public function removeFilter(\Spameri\ElasticQuery\Mapping\FilterInterface $filt $this->analysis->filter()->remove($filter->key()); } - public function toArray(): array { $array = [ diff --git a/src/Mapping/Settings/Alias.php b/src/Mapping/Settings/Alias.php index 3a75e59..7ec99e6 100644 --- a/src/Mapping/Settings/Alias.php +++ b/src/Mapping/Settings/Alias.php @@ -1,21 +1,16 @@ -name = $name; } diff --git a/src/Mapping/Settings/AliasCollection.php b/src/Mapping/Settings/AliasCollection.php index 2a44aea..763bee7 100644 --- a/src/Mapping/Settings/AliasCollection.php +++ b/src/Mapping/Settings/AliasCollection.php @@ -1,4 +1,6 @@ - */ - protected $collection; + protected array $collection; public function __construct( - \Spameri\ElasticQuery\Mapping\Settings\Alias ... $collection + \Spameri\ElasticQuery\Mapping\Settings\Alias ...$collection, ) { $this->collection = []; @@ -26,7 +28,7 @@ public function __construct( * @param \Spameri\ElasticQuery\Mapping\Settings\Alias $item */ public function add( - $item + $item, ): void { $this->collection[$item->key()] = $item; @@ -34,40 +36,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Mapping\Settings\Alias + string $key, + ): \Spameri\ElasticQuery\Mapping\Settings\Alias|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Mapping/Settings/Analysis.php b/src/Mapping/Settings/Analysis.php index eeb7709..4789fe4 100644 --- a/src/Mapping/Settings/Analysis.php +++ b/src/Mapping/Settings/Analysis.php @@ -1,39 +1,32 @@ - */ - protected $collection; + protected array $collection; public function __construct( - \Spameri\ElasticQuery\Mapping\AnalyzerInterface ... $collection + \Spameri\ElasticQuery\Mapping\AnalyzerInterface ...$collection, ) { $this->collection = []; @@ -29,7 +31,7 @@ public function __construct( * @param \Spameri\ElasticQuery\Mapping\AnalyzerInterface&\Spameri\ElasticQuery\Collection\Item $item */ public function add( - $item + $item, ): void { $this->collection[$item->key()] = $item; @@ -37,40 +39,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Mapping\AnalyzerInterface + string $key, + ): \Spameri\ElasticQuery\Mapping\AnalyzerInterface|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Mapping/Settings/Analysis/FilterCollection.php b/src/Mapping/Settings/Analysis/FilterCollection.php index 5362aac..6552a8b 100644 --- a/src/Mapping/Settings/Analysis/FilterCollection.php +++ b/src/Mapping/Settings/Analysis/FilterCollection.php @@ -1,4 +1,6 @@ - */ - protected $collection; + protected array $collection; public function __construct( - \Spameri\ElasticQuery\Mapping\FilterInterface ... $collection + \Spameri\ElasticQuery\Mapping\FilterInterface ...$collection, ) { $this->collection = []; @@ -26,7 +28,7 @@ public function __construct( * @param \Spameri\ElasticQuery\Mapping\FilterInterface $item */ public function add( - $item + $item, ): void { $this->collection[$item->key()] = $item; @@ -34,40 +36,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Mapping\FilterInterface + string $key, + ): \Spameri\ElasticQuery\Mapping\FilterInterface|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Mapping/Settings/Analysis/TokenizerCollection.php b/src/Mapping/Settings/Analysis/TokenizerCollection.php index 37fb07c..8ac17c8 100644 --- a/src/Mapping/Settings/Analysis/TokenizerCollection.php +++ b/src/Mapping/Settings/Analysis/TokenizerCollection.php @@ -1,4 +1,6 @@ - */ - protected $collection; + protected array $collection; public function __construct( - \Spameri\ElasticQuery\Mapping\TokenizerInterface ... $collection + \Spameri\ElasticQuery\Mapping\TokenizerInterface ...$collection, ) { $this->collection = []; @@ -26,7 +28,7 @@ public function __construct( * @param \Spameri\ElasticQuery\Mapping\TokenizerInterface $item */ public function add( - $item + $item, ): void { $this->collection[$item->key()] = $item; @@ -34,40 +36,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Mapping\TokenizerInterface + string $key, + ): \Spameri\ElasticQuery\Mapping\TokenizerInterface|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Mapping/Settings/Mapping.php b/src/Mapping/Settings/Mapping.php index d91fece..ca44aa4 100644 --- a/src/Mapping/Settings/Mapping.php +++ b/src/Mapping/Settings/Mapping.php @@ -1,35 +1,32 @@ -indexName = $indexName; - if ($fields === NULL) { + if ($fields === null) { $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); } $this->fields = $fields; } + public function enableStrictMapping(): void + { + $this->dynamic = false; + } public function getIndexName(): string { @@ -103,6 +100,7 @@ public function toArray(): array return [ 'mappings' => [ 'properties' => $fields, + 'dynamic' => $this->dynamic, ], ]; } diff --git a/src/Mapping/Settings/Mapping/Field.php b/src/Mapping/Settings/Mapping/Field.php index c1173d7..67d307c 100644 --- a/src/Mapping/Settings/Mapping/Field.php +++ b/src/Mapping/Settings/Mapping/Field.php @@ -1,4 +1,6 @@ -name = $name; - if ( ! \in_array($type, \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES, TRUE)) { + if ( ! \in_array($type, \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES, true)) { throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( - 'Not allowed type see \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES' + 'Not allowed type see \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES', ); } - $this->type = $type; - $this->analyzer = $analyzer; - $this->fieldData = $fieldData; } @@ -70,7 +47,7 @@ public function toArray(): array $array[$this->name]['analyzer'] = $this->analyzer->name(); } - if ($this->fieldData !== NULL) { + if ($this->fieldData !== null) { $array[$this->name]['fielddata'] = $this->fieldData; } diff --git a/src/Mapping/Settings/Mapping/FieldCollection.php b/src/Mapping/Settings/Mapping/FieldCollection.php index ba601fd..b0470e5 100644 --- a/src/Mapping/Settings/Mapping/FieldCollection.php +++ b/src/Mapping/Settings/Mapping/FieldCollection.php @@ -1,4 +1,6 @@ - */ - protected $collection; + protected array $collection; public function __construct( - \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldInterface ... $collection + \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldInterface ...$collection, ) { $this->collection = []; @@ -26,7 +28,7 @@ public function __construct( * @param \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldInterface $item */ public function add( - $item + $item, ): void { $this->collection[$item->key()] = $item; @@ -34,40 +36,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldInterface + string $key, + ): \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldInterface|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Mapping/Settings/Mapping/FieldInterface.php b/src/Mapping/Settings/Mapping/FieldInterface.php index ed35c2b..0ba4283 100644 --- a/src/Mapping/Settings/Mapping/FieldInterface.php +++ b/src/Mapping/Settings/Mapping/FieldInterface.php @@ -1,4 +1,6 @@ -name = $name; - $this->fields = $fields; } @@ -46,6 +35,10 @@ public function toArray(): array $fields[$field->key()] = $field->toArray(); continue; } + if ($field instanceof \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields) { + $fields[$field->key()] = $field->toArray(); + continue; + } if ($field instanceof \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject) { $fields[$field->key()] = $field->toArray(); continue; diff --git a/src/Mapping/Settings/Mapping/FieldSeparator.php b/src/Mapping/Settings/Mapping/FieldSeparator.php new file mode 100644 index 0000000..e379bb1 --- /dev/null +++ b/src/Mapping/Settings/Mapping/FieldSeparator.php @@ -0,0 +1,12 @@ +name = $name; - $this->fields = $fields; } diff --git a/src/Mapping/Settings/Mapping/SubFields.php b/src/Mapping/Settings/Mapping/SubFields.php index fb07800..d942095 100644 --- a/src/Mapping/Settings/Mapping/SubFields.php +++ b/src/Mapping/Settings/Mapping/SubFields.php @@ -1,4 +1,6 @@ -name = $name; - if ($fields === NULL) { + if ($fields === null) { $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); } - if ( ! \in_array($type, \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES, TRUE)) { + if ( ! \in_array($type, \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES, true)) { throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( - 'Not allowed type see \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES' + 'Not allowed type see \Spameri\ElasticQuery\Mapping\AllowedValues::TYPES', ); } - $this->type = $type; $this->fields = $fields; } diff --git a/src/Mapping/Settings/MappingOld.php b/src/Mapping/Settings/MappingOld.php deleted file mode 100644 index d1735bb..0000000 --- a/src/Mapping/Settings/MappingOld.php +++ /dev/null @@ -1,74 +0,0 @@ -indexName = $indexName; - - if ($fields === NULL) { - $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); - } - - $this->fields = $fields; - } - - - public function addField(\Spameri\ElasticQuery\Mapping\Settings\Mapping\Field $field): void - { - $this->fields->add($field); - } - - - public function addFieldObject(\Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject $fieldObject): void - { - $this->fields->add($fieldObject); - } - - - public function addSubField(\Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields $subFields): void - { - $this->fields->add($subFields); - } - - - public function toArray(): array - { - $fields = []; - /** @var \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldInterface $field */ - foreach ($this->fields as $field) { - if ($field instanceof \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields) { - $fields[$field->key()] = $field->toArray(); - - } else { - $fields[$field->key()] = $field->toArray()[$field->key()]; - } - } - - return [ - 'mappings' => [ - $this->indexName => [ - 'properties' => $fields, - ], - ], - ]; - } - -} diff --git a/src/Mapping/Tokenizer/CharGroup.php b/src/Mapping/Tokenizer/CharGroup.php index e39261e..591b6b7 100644 --- a/src/Mapping/Tokenizer/CharGroup.php +++ b/src/Mapping/Tokenizer/CharGroup.php @@ -1,4 +1,6 @@ -|null $searchAfter + * @param array|null $storedFields + * @param array|null $docvalueFields + * @param array>|null $fields + * @param array>|null $scriptFields + * @param array|null $runtimeMappings + * @param array|null $suggesters + * @param array|null $rescore + * @param array>|null $indicesBoost + * @param array|null $stats + * @param array|null $ext + */ public function __construct( - ?int $size = NULL, - ?int $from = NULL, - ?\Spameri\ElasticQuery\Options\SortCollection $sort = NULL, - ?float $minScore = NULL, - bool $includeVersion = FALSE, - ?string $scroll = NULL, - ?string $scrollId = NULL + private int|null $size = null, + private int|null $from = null, + \Spameri\ElasticQuery\Options\SortCollection|null $sort = null, + private float|null $minScore = null, + private bool $includeVersion = false, + private string|null $scroll = null, + private string|null $scrollId = null, + private \Spameri\ElasticQuery\Options\Source|null $source = null, + private bool|int|null $trackTotalHits = null, + private bool|null $trackScores = null, + private bool|null $explain = null, + private int|null $terminateAfter = null, + private string|null $timeout = null, + private array|null $searchAfter = null, + private \Spameri\ElasticQuery\Options\Pit|null $pit = null, + private array|null $storedFields = null, + private array|null $docvalueFields = null, + private array|null $fields = null, + private array|null $scriptFields = null, + private array|null $runtimeMappings = null, + private bool|null $seqNoPrimaryTerm = null, + private array|null $indicesBoost = null, + private \Spameri\ElasticQuery\Options\Collapse|null $collapse = null, + private array|null $rescore = null, + private array|null $suggesters = null, + private string|null $suggestText = null, + private bool|null $profile = null, + private array|null $stats = null, + private array|null $ext = null, ) { - $this->size = $size; - $this->from = $from; $this->sort = $sort ?: new \Spameri\ElasticQuery\Options\SortCollection(); - $this->minScore = $minScore; - $this->includeVersion = $includeVersion; - $this->scroll = $scroll; - $this->scrollId = $scrollId; } @@ -74,63 +78,173 @@ public function sort(): \Spameri\ElasticQuery\Options\SortCollection } - public function scroll(): ?string + public function scroll(): string|null { return $this->scroll; } public function startScroll( - string $scroll + string $scroll, ): void { $this->scroll = $scroll; } - public function scrollId(): ?string + public function scrollId(): string|null { return $this->scrollId; } public function scrollInitialized( - string $scrollId + string $scrollId, ): void { $this->scrollId = $scrollId; } + public function collapse(): \Spameri\ElasticQuery\Options\Collapse|null + { + return $this->collapse; + } + + + /** + * @return array|null + */ + public function rescore(): array|null + { + return $this->rescore; + } + + + /** + * @return array|null + */ + public function suggesters(): array|null + { + return $this->suggesters; + } + + + public function suggestText(): string|null + { + return $this->suggestText; + } + + + /** + * @return array + */ public function toArray(): array { $array = []; - if ($this->from !== NULL) { + if ($this->from !== null) { $array['from'] = $this->from; } - if ($this->size !== NULL) { + if ($this->size !== null) { $array['size'] = $this->size; } foreach ($this->sort as $item) { + if ($item instanceof \Spameri\ElasticQuery\Options\Sort && $item->field === '_score') { + $array['sort'][] = $item->field; + continue; + } + $array['sort'][] = $item->toArray(); } - if ($this->minScore) { + if ($this->minScore !== null) { $array['min_score'] = $this->minScore; } - if ($this->includeVersion === TRUE) { + if ($this->includeVersion === true) { $array['version'] = $this->includeVersion; } - if ($this->scrollId !== NULL) { + if ($this->scrollId !== null) { $array['scroll_id'] = $this->scrollId; $array['scroll'] = $this->scroll; } + if ($this->source !== null) { + $array['_source'] = $this->source->value(); + } + + if ($this->trackTotalHits !== null) { + $array['track_total_hits'] = $this->trackTotalHits; + } + + if ($this->trackScores !== null) { + $array['track_scores'] = $this->trackScores; + } + + if ($this->explain !== null) { + $array['explain'] = $this->explain; + } + + if ($this->terminateAfter !== null) { + $array['terminate_after'] = $this->terminateAfter; + } + + if ($this->timeout !== null) { + $array['timeout'] = $this->timeout; + } + + if ($this->searchAfter !== null) { + $array['search_after'] = $this->searchAfter; + } + + if ($this->pit !== null) { + $array['pit'] = $this->pit->toArray(); + } + + if ($this->storedFields !== null) { + $array['stored_fields'] = $this->storedFields; + } + + if ($this->docvalueFields !== null) { + $array['docvalue_fields'] = $this->docvalueFields; + } + + if ($this->fields !== null) { + $array['fields'] = $this->fields; + } + + if ($this->scriptFields !== null) { + $array['script_fields'] = $this->scriptFields; + } + + if ($this->runtimeMappings !== null) { + $array['runtime_mappings'] = $this->runtimeMappings; + } + + if ($this->seqNoPrimaryTerm !== null) { + $array['seq_no_primary_term'] = $this->seqNoPrimaryTerm; + } + + if ($this->indicesBoost !== null) { + $array['indices_boost'] = $this->indicesBoost; + } + + if ($this->profile !== null) { + $array['profile'] = $this->profile; + } + + if ($this->stats !== null) { + $array['stats'] = $this->stats; + } + + if ($this->ext !== null) { + $array['ext'] = $this->ext; + } + return $array; } diff --git a/src/Options/Collapse.php b/src/Options/Collapse.php new file mode 100644 index 0000000..4c8824e --- /dev/null +++ b/src/Options/Collapse.php @@ -0,0 +1,53 @@ +|\Spameri\ElasticQuery\Query\InnerHits|null $innerHits + */ + public function __construct( + private string $field, + private array|\Spameri\ElasticQuery\Query\InnerHits|null $innerHits = null, + private int|null $maxConcurrentGroupSearches = null, + ) + { + } + + + /** + * @return array + */ + public function toArray(): array + { + $array = ['field' => $this->field]; + + if ($this->innerHits !== null) { + if (\is_array($this->innerHits)) { + $array['inner_hits'] = []; + foreach ($this->innerHits as $ih) { + $array['inner_hits'][] = $ih->toArray(); + } + } else { + $array['inner_hits'] = $this->innerHits->toArray(); + } + } + + if ($this->maxConcurrentGroupSearches !== null) { + $array['max_concurrent_group_searches'] = $this->maxConcurrentGroupSearches; + } + + return $array; + } + +} diff --git a/src/Options/GeoDistanceSort.php b/src/Options/GeoDistanceSort.php new file mode 100644 index 0000000..1b05dc4 --- /dev/null +++ b/src/Options/GeoDistanceSort.php @@ -0,0 +1,59 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + '_geo_distance' => [ + $this->field => [ + $this->lat, + $this->lon, + ], + 'order' => $this->type, + 'unit' => $this->unit, + 'mode' => $this->mode, + 'distance_type' => $this->distanceType, + 'ignore_unmapped' => $this->ignoreUnmapped, + ], + ]; + } + +} diff --git a/src/Options/NestedSort.php b/src/Options/NestedSort.php new file mode 100644 index 0000000..b4f45c3 --- /dev/null +++ b/src/Options/NestedSort.php @@ -0,0 +1,48 @@ + + */ + public function toArray(): array + { + $array = ['path' => $this->path]; + + if ($this->filter !== null) { + $array['filter'] = $this->filter->toArray(); + } + + if ($this->maxChildren !== null) { + $array['max_children'] = $this->maxChildren; + } + + if ($this->nested !== null) { + $array['nested'] = $this->nested->toArray(); + } + + return $array; + } + +} diff --git a/src/Options/Pit.php b/src/Options/Pit.php new file mode 100644 index 0000000..dab0497 --- /dev/null +++ b/src/Options/Pit.php @@ -0,0 +1,38 @@ + + */ + public function toArray(): array + { + $array = ['id' => $this->id]; + + if ($this->keepAlive !== null) { + $array['keep_alive'] = $this->keepAlive; + } + + return $array; + } + +} diff --git a/src/Options/Rescore.php b/src/Options/Rescore.php new file mode 100644 index 0000000..b8f8529 --- /dev/null +++ b/src/Options/Rescore.php @@ -0,0 +1,58 @@ + + */ + public function toArray(): array + { + $rescoreQuery = ['rescore_query' => $this->query->toArray()]; + + if ($this->queryWeight !== null) { + $rescoreQuery['query_weight'] = $this->queryWeight; + } + + if ($this->rescoreQueryWeight !== null) { + $rescoreQuery['rescore_query_weight'] = $this->rescoreQueryWeight; + } + + if ($this->scoreMode !== null) { + $rescoreQuery['score_mode'] = $this->scoreMode; + } + + return [ + 'window_size' => $this->windowSize, + 'query' => $rescoreQuery, + ]; + } + +} diff --git a/src/Options/ScriptSort.php b/src/Options/ScriptSort.php new file mode 100644 index 0000000..6b436f5 --- /dev/null +++ b/src/Options/ScriptSort.php @@ -0,0 +1,66 @@ +> + */ + public function toArray(): array + { + $body = [ + 'type' => $this->type, + 'script' => $this->script->toArray(), + 'order' => $this->order, + ]; + + if ($this->mode !== null) { + $body['mode'] = $this->mode; + } + + if ($this->nested !== null) { + $body['nested'] = $this->nested->toArray(); + } + + return ['_script' => $body]; + } + +} diff --git a/src/Options/Sort.php b/src/Options/Sort.php index 014a763..0ecb5a8 100644 --- a/src/Options/Sort.php +++ b/src/Options/Sort.php @@ -1,10 +1,12 @@ -field = $field; - $this->type = $type; - $this->missing = $missing; } @@ -59,13 +47,38 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { + $body = [ + 'order' => $this->type, + 'missing' => $this->missing, + ]; + + if ($this->mode !== null) { + $body['mode'] = $this->mode; + } + + if ($this->nested !== null) { + $body['nested'] = $this->nested->toArray(); + } + + if ($this->numericType !== null) { + $body['numeric_type'] = $this->numericType; + } + + if ($this->unmappedType !== null) { + $body['unmapped_type'] = $this->unmappedType; + } + + if ($this->format !== null) { + $body['format'] = $this->format; + } + return [ - $this->field => [ - 'order' => $this->type, - 'missing' => $this->missing, - ], + $this->field => $body, ]; } diff --git a/src/Options/SortCollection.php b/src/Options/SortCollection.php index e45e697..2b36570 100644 --- a/src/Options/SortCollection.php +++ b/src/Options/SortCollection.php @@ -1,4 +1,6 @@ -collection as $sort) { + if ($sort instanceof \Spameri\ElasticQuery\Options\Sort && $sort->field === '_score') { + $array[] = $sort->field; + continue; + } + $array[] = $sort->toArray(); } diff --git a/src/Options/Source.php b/src/Options/Source.php new file mode 100644 index 0000000..2af8d29 --- /dev/null +++ b/src/Options/Source.php @@ -0,0 +1,53 @@ +|null $includes + * @param array|null $excludes + */ + public function __construct( + private bool|null $enabled = null, + private array|null $includes = null, + private array|null $excludes = null, + ) + { + } + + + /** + * @return bool|array + */ + public function value(): bool|array + { + if ($this->enabled === false) { + return false; + } + + if ($this->includes === null && $this->excludes === null) { + return true; + } + + $array = []; + if ($this->includes !== null) { + $array['includes'] = $this->includes; + } + if ($this->excludes !== null) { + $array['excludes'] = $this->excludes; + } + + return $array; + } + +} diff --git a/src/Options/Suggest/CompletionSuggester.php b/src/Options/Suggest/CompletionSuggester.php new file mode 100644 index 0000000..a9ce53a --- /dev/null +++ b/src/Options/Suggest/CompletionSuggester.php @@ -0,0 +1,65 @@ +|null $fuzzy + * @param array|null $regex + * @param array|null $contexts + */ + public function __construct( + private string $name, + private string $prefix, + private string $field, + private int|null $size = null, + private bool|null $skipDuplicates = null, + private array|null $fuzzy = null, + private array|null $regex = null, + private array|null $contexts = null, + ) + { + } + + + public function key(): string + { + return $this->name; + } + + + /** + * @return array + */ + public function toArray(): array + { + $body = ['field' => $this->field]; + + if ($this->size !== null) { + $body['size'] = $this->size; + } + if ($this->skipDuplicates !== null) { + $body['skip_duplicates'] = $this->skipDuplicates; + } + if ($this->fuzzy !== null) { + $body['fuzzy'] = $this->fuzzy; + } + if ($this->regex !== null) { + $body['regex'] = $this->regex; + } + if ($this->contexts !== null) { + $body['contexts'] = $this->contexts; + } + + return [ + 'prefix' => $this->prefix, + 'completion' => $body, + ]; + } + +} diff --git a/src/Options/Suggest/PhraseSuggester.php b/src/Options/Suggest/PhraseSuggester.php new file mode 100644 index 0000000..9ac4e26 --- /dev/null +++ b/src/Options/Suggest/PhraseSuggester.php @@ -0,0 +1,60 @@ +name; + } + + + /** + * @return array + */ + public function toArray(): array + { + $body = ['field' => $this->field]; + + if ($this->size !== null) { + $body['size'] = $this->size; + } + if ($this->gramSize !== null) { + $body['gram_size'] = $this->gramSize; + } + if ($this->confidence !== null) { + $body['confidence'] = $this->confidence; + } + if ($this->maxErrors !== null) { + $body['max_errors'] = $this->maxErrors; + } + if ($this->separator !== null) { + $body['separator'] = $this->separator; + } + + return [ + 'text' => $this->text, + 'phrase' => $body, + ]; + } + +} diff --git a/src/Options/Suggest/SuggesterInterface.php b/src/Options/Suggest/SuggesterInterface.php new file mode 100644 index 0000000..57ed994 --- /dev/null +++ b/src/Options/Suggest/SuggesterInterface.php @@ -0,0 +1,11 @@ +name; + } + + + /** + * @return array + */ + public function toArray(): array + { + $body = ['field' => $this->field]; + + if ($this->size !== null) { + $body['size'] = $this->size; + } + + if ($this->sort !== null) { + $body['sort'] = $this->sort; + } + + if ($this->suggestMode !== null) { + $body['suggest_mode'] = $this->suggestMode; + } + + return [ + 'text' => $this->text, + 'term' => $body, + ]; + } + +} diff --git a/src/Query/Boosting.php b/src/Query/Boosting.php new file mode 100644 index 0000000..8fd317f --- /dev/null +++ b/src/Query/Boosting.php @@ -0,0 +1,42 @@ +positive->key() . '_' . $this->negative->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'boosting' => [ + 'positive' => $this->positive->toArray(), + 'negative' => $this->negative->toArray(), + 'negative_boost' => $this->negativeBoost, + ], + ]; + } + +} diff --git a/src/Query/CombinedFields.php b/src/Query/CombinedFields.php new file mode 100644 index 0000000..96238ad --- /dev/null +++ b/src/Query/CombinedFields.php @@ -0,0 +1,72 @@ + $fields + */ + public function __construct( + private array $fields, + private string $query, + private float $boost = 1.0, + private string|null $operator = null, + private int|string|null $minimumShouldMatch = null, + private string|null $zeroTermsQuery = null, + private bool|null $autoGenerateSynonymsPhraseQuery = null, + ) + { + if ($fields === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'CombinedFields query requires at least one field.', + ); + } + } + + + public function key(): string + { + return 'combined_fields_' . \implode('-', $this->fields) . '_' . $this->query; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'query' => $this->query, + 'fields' => $this->fields, + 'boost' => $this->boost, + ]; + + if ($this->operator !== null) { + $body['operator'] = $this->operator; + } + + if ($this->minimumShouldMatch !== null) { + $body['minimum_should_match'] = $this->minimumShouldMatch; + } + + if ($this->zeroTermsQuery !== null) { + $body['zero_terms_query'] = $this->zeroTermsQuery; + } + + if ($this->autoGenerateSynonymsPhraseQuery !== null) { + $body['auto_generate_synonyms_phrase_query'] = $this->autoGenerateSynonymsPhraseQuery; + } + + return [ + 'combined_fields' => $body, + ]; + } + +} diff --git a/src/Query/ConstantScore.php b/src/Query/ConstantScore.php new file mode 100644 index 0000000..5171f21 --- /dev/null +++ b/src/Query/ConstantScore.php @@ -0,0 +1,40 @@ +filter->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'constant_score' => [ + 'filter' => $this->filter->toArray(), + 'boost' => $this->boost, + ], + ]; + } + +} diff --git a/src/Query/DisMax.php b/src/Query/DisMax.php new file mode 100644 index 0000000..bd7a842 --- /dev/null +++ b/src/Query/DisMax.php @@ -0,0 +1,65 @@ + + */ + private array $queries; + + + public function __construct( + \Spameri\ElasticQuery\Query\LeafQueryInterface $query, + private float $tieBreaker = 0.0, + private float $boost = 1.0, + ) + { + $this->queries = [$query]; + } + + + public function addQuery(\Spameri\ElasticQuery\Query\LeafQueryInterface $query): void + { + $this->queries[] = $query; + } + + + public function key(): string + { + $keys = []; + foreach ($this->queries as $query) { + $keys[] = $query->key(); + } + + return 'dis_max_' . \implode('-', $keys); + } + + + /** + * @return array> + */ + public function toArray(): array + { + $queries = []; + foreach ($this->queries as $query) { + $queries[] = $query->toArray(); + } + + return [ + 'dis_max' => [ + 'queries' => $queries, + 'tie_breaker' => $this->tieBreaker, + 'boost' => $this->boost, + ], + ]; + } + +} diff --git a/src/Query/DistanceFeature.php b/src/Query/DistanceFeature.php new file mode 100644 index 0000000..0a0a9b8 --- /dev/null +++ b/src/Query/DistanceFeature.php @@ -0,0 +1,47 @@ +|string $origin Either [lat, lon] for geo_point or a date string for date. + */ + public function __construct( + private string $field, + private array|string $origin, + private string $pivot, + private float $boost = 1.0, + ) + { + } + + + public function key(): string + { + return 'distance_feature_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'distance_feature' => [ + 'field' => $this->field, + 'origin' => $this->origin, + 'pivot' => $this->pivot, + 'boost' => $this->boost, + ], + ]; + } + +} diff --git a/src/Query/ElasticMatch.php b/src/Query/ElasticMatch.php index 0a9639e..accea3d 100644 --- a/src/Query/ElasticMatch.php +++ b/src/Query/ElasticMatch.php @@ -1,4 +1,6 @@ -field = $field; - $this->query = $query; - $this->operator = $operator; - $this->fuzziness = $fuzziness; - $this->boost = $boost; - $this->analyzer = $analyzer; - $this->minimumShouldMatch = $minimumShouldMatch; } @@ -68,34 +49,62 @@ public function key(): string } + /** + * @return array>> + */ public function toArray(): array { - $array = [ - 'match' => [ - $this->field => [ - 'query' => $this->query, - 'boost' => $this->boost, - ], - ], + $body = [ + 'query' => $this->query, + 'boost' => $this->boost, + 'operator' => $this->operator, ]; - if ($this->operator) { - $array['match'][$this->field]['operator'] = $this->operator; + if ($this->fuzziness !== null) { + $body['fuzziness'] = $this->fuzziness->__toString(); + } + + if ($this->analyzer !== null) { + $body['analyzer'] = $this->analyzer; + } + + if ($this->minimumShouldMatch !== null) { + $body['minimum_should_match'] = $this->minimumShouldMatch; + } + + if ($this->zeroTermsQuery !== null) { + $body['zero_terms_query'] = $this->zeroTermsQuery; + } + + if ($this->autoGenerateSynonymsPhraseQuery !== null) { + $body['auto_generate_synonyms_phrase_query'] = $this->autoGenerateSynonymsPhraseQuery; + } + + if ($this->lenient !== null) { + $body['lenient'] = $this->lenient; + } + + if ($this->prefixLength !== null) { + $body['prefix_length'] = $this->prefixLength; } - if ($this->fuzziness !== NULL) { - $array['match'][$this->field]['fuzziness'] = $this->fuzziness->__toString(); + if ($this->maxExpansions !== null) { + $body['max_expansions'] = $this->maxExpansions; } - if ($this->analyzer !== NULL) { - $array['match'][$this->field]['analyzer'] = $this->analyzer; + if ($this->fuzzyTranspositions !== null) { + $body['fuzzy_transpositions'] = $this->fuzzyTranspositions; } - if ($this->minimumShouldMatch !== NULL) { - $array['match'][$this->field]['minimum_should_match'] = $this->minimumShouldMatch; + if ($this->fuzzyRewrite !== null) { + $body['fuzzy_rewrite'] = $this->fuzzyRewrite; } - return $array; + return [ + 'match' => [ + $this->field => $body, + ], + ]; } } diff --git a/src/Query/Exists.php b/src/Query/Exists.php index f97d919..46e224b 100644 --- a/src/Query/Exists.php +++ b/src/Query/Exists.php @@ -1,4 +1,6 @@ -field = $field; } @@ -26,11 +25,15 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { return [ 'exists' => [ 'field' => $this->field, + 'boost' => $this->boost, ], ]; } diff --git a/src/Query/FieldMaskingSpan.php b/src/Query/FieldMaskingSpan.php new file mode 100644 index 0000000..b2f187b --- /dev/null +++ b/src/Query/FieldMaskingSpan.php @@ -0,0 +1,40 @@ +field . '_' . $this->query->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'field_masking_span' => [ + 'query' => $this->query->toArray(), + 'field' => $this->field, + ], + ]; + } + +} diff --git a/src/Query/Fuzzy.php b/src/Query/Fuzzy.php index 67f8e11..916ae2c 100644 --- a/src/Query/Fuzzy.php +++ b/src/Query/Fuzzy.php @@ -1,4 +1,6 @@ -field = $field; - $this->query = $query; - $this->boost = $boost; - $this->fuzziness = $fuzziness; - $this->prefixLength = $prefixLength; - $this->maxExpansion = $maxExpansion; } @@ -67,21 +31,32 @@ public function key(): string } + /** + * @return array>> + */ public function toArray(): array { - // phpcs:ignore SlevomatCodingStandard.Variables.UselessVariable - $array = [ + $body = [ + 'value' => $this->query, + 'boost' => $this->boost, + 'fuzziness' => $this->fuzziness, + 'prefix_length' => $this->prefixLength, + 'max_expansions' => $this->maxExpansion, + ]; + + if ($this->transpositions !== null) { + $body['transpositions'] = $this->transpositions; + } + + if ($this->rewrite !== null) { + $body['rewrite'] = $this->rewrite; + } + + return [ 'fuzzy' => [ - $this->field => [ - 'value' => $this->query, - 'boost' => $this->boost, - 'fuzziness' => $this->fuzziness, - 'prefix_length' => $this->prefixLength, - 'max_expansions' => $this->maxExpansion, - ], + $this->field => $body, ], ]; - - return $array; } + } diff --git a/src/Query/GeoBoundingBox.php b/src/Query/GeoBoundingBox.php new file mode 100644 index 0000000..ed60cbc --- /dev/null +++ b/src/Query/GeoBoundingBox.php @@ -0,0 +1,71 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + $this->field => [ + 'top_left' => [ + 'lat' => $this->topLeftLat, + 'lon' => $this->topLeftLon, + ], + 'bottom_right' => [ + 'lat' => $this->bottomRightLat, + 'lon' => $this->bottomRightLon, + ], + ], + 'boost' => $this->boost, + ]; + + if ($this->type !== null) { + $body['type'] = $this->type; + } + + if ($this->validationMethod !== null) { + $body['validation_method'] = $this->validationMethod; + } + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + return [ + 'geo_bounding_box' => $body, + ]; + } + +} diff --git a/src/Query/GeoDistance.php b/src/Query/GeoDistance.php index 2a15dd6..c3dcc15 100644 --- a/src/Query/GeoDistance.php +++ b/src/Query/GeoDistance.php @@ -1,4 +1,6 @@ -field = $field; - $this->lat = $lat; - $this->lon = $lon; } public function key(): string { - return 'geo_distance_' . $this->field . '_' . $this->lat . '.' . $this->lon; + return 'geo_distance_' . $this->field . '_' . $this->lat . '.' . $this->lon . '_' . $this->distance; } + /** + * @return array> + */ public function toArray(): array { - return [ - 'pin' => [ - 'location' => [ - 'lat' => $this->lat, - 'lon' => $this->lon, - ], + $body = [ + 'distance' => $this->distance, + $this->field => [ + 'lat' => $this->lat, + 'lon' => $this->lon, ], + 'boost' => $this->boost, + ]; + + if ($this->distanceType !== null) { + $body['distance_type'] = $this->distanceType; + } + + if ($this->validationMethod !== null) { + $body['validation_method'] = $this->validationMethod; + } + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + return [ + 'geo_distance' => $body, ]; } diff --git a/src/Query/GeoShape.php b/src/Query/GeoShape.php new file mode 100644 index 0000000..8bd1fc1 --- /dev/null +++ b/src/Query/GeoShape.php @@ -0,0 +1,77 @@ +|null $shape Inline GeoJSON shape; null when $indexedShape is used. + */ + public function __construct( + private string $field, + private array|null $shape = null, + private string $relation = 'intersects', + private bool|null $ignoreUnmapped = null, + private \Spameri\ElasticQuery\Query\IndexedShape|null $indexedShape = null, + private float $boost = 1.0, + ) + { + if ( ! \in_array($relation, ['intersects', 'disjoint', 'within', 'contains'], true)) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'GeoShape relation must be one of: intersects, disjoint, within, contains.', + ); + } + + if ($shape === null && $indexedShape === null) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'GeoShape requires either an inline shape or an indexedShape.', + ); + } + } + + + public function key(): string + { + return 'geo_shape_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $inner = [ + 'relation' => $this->relation, + ]; + + if ($this->shape !== null) { + $inner['shape'] = $this->shape; + } + + if ($this->indexedShape !== null) { + $inner['indexed_shape'] = $this->indexedShape->toArray(); + } + + $body = [ + $this->field => $inner, + 'boost' => $this->boost, + ]; + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + return [ + 'geo_shape' => $body, + ]; + } + +} diff --git a/src/Query/HasChild.php b/src/Query/HasChild.php new file mode 100644 index 0000000..4237618 --- /dev/null +++ b/src/Query/HasChild.php @@ -0,0 +1,67 @@ +type; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'type' => $this->type, + 'query' => $this->query->toArray(), + ]; + + if ($this->scoreMode !== null) { + $body['score_mode'] = $this->scoreMode; + } + + if ($this->minChildren !== null) { + $body['min_children'] = $this->minChildren; + } + + if ($this->maxChildren !== null) { + $body['max_children'] = $this->maxChildren; + } + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + if ($this->innerHits !== null) { + $body['inner_hits'] = $this->innerHits->toArray(); + } + + return [ + 'has_child' => $body, + ]; + } + +} diff --git a/src/Query/HasParent.php b/src/Query/HasParent.php new file mode 100644 index 0000000..74dae02 --- /dev/null +++ b/src/Query/HasParent.php @@ -0,0 +1,57 @@ +parentType; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'parent_type' => $this->parentType, + 'query' => $this->query->toArray(), + ]; + + if ($this->score !== null) { + $body['score'] = $this->score; + } + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + if ($this->innerHits !== null) { + $body['inner_hits'] = $this->innerHits->toArray(); + } + + return [ + 'has_parent' => $body, + ]; + } + +} diff --git a/src/Query/Ids.php b/src/Query/Ids.php new file mode 100644 index 0000000..e99ebb2 --- /dev/null +++ b/src/Query/Ids.php @@ -0,0 +1,48 @@ + $values + */ + public function __construct( + private array $values, + private float $boost = 1.0, + ) + { + if ($values === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Ids query must contain at least one id.', + ); + } + } + + + public function key(): string + { + return 'ids_' . \implode('-', $this->values); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'ids' => [ + 'values' => $this->values, + 'boost' => $this->boost, + ], + ]; + } + +} diff --git a/src/Query/IndexedShape.php b/src/Query/IndexedShape.php new file mode 100644 index 0000000..0f4a503 --- /dev/null +++ b/src/Query/IndexedShape.php @@ -0,0 +1,44 @@ + + */ + public function toArray(): array + { + $array = [ + 'id' => $this->id, + 'index' => $this->index, + 'path' => $this->path, + ]; + + if ($this->routing !== null) { + $array['routing'] = $this->routing; + } + + return $array; + } + +} diff --git a/src/Query/InnerHits.php b/src/Query/InnerHits.php new file mode 100644 index 0000000..200fc49 --- /dev/null +++ b/src/Query/InnerHits.php @@ -0,0 +1,102 @@ +> $sort + * @param bool|array|array> $source + * @param array $scriptFields + * @param array $docvalueFields + * @param array $storedFields + */ + public function __construct( + private string|null $name = null, + private int|null $from = null, + private int|null $size = null, + private array $sort = [], + private bool|array $source = true, + private \Spameri\ElasticQuery\Highlight|null $highlight = null, + private bool|null $explain = null, + private array $scriptFields = [], + private array $docvalueFields = [], + private bool|null $version = null, + private bool|null $seqNoPrimaryTerm = null, + private array $storedFields = [], + private bool|null $trackScores = null, + ) + { + } + + + /** + * @return array + */ + public function toArray(): array + { + $array = []; + + if ($this->name !== null) { + $array['name'] = $this->name; + } + + if ($this->from !== null) { + $array['from'] = $this->from; + } + + if ($this->size !== null) { + $array['size'] = $this->size; + } + + if ($this->sort !== []) { + $array['sort'] = $this->sort; + } + + if ($this->source !== true) { + $array['_source'] = $this->source; + } + + if ($this->highlight !== null) { + $array['highlight'] = $this->highlight->toArray(); + } + + if ($this->explain !== null) { + $array['explain'] = $this->explain; + } + + if ($this->scriptFields !== []) { + $array['script_fields'] = $this->scriptFields; + } + + if ($this->docvalueFields !== []) { + $array['docvalue_fields'] = $this->docvalueFields; + } + + if ($this->version !== null) { + $array['version'] = $this->version; + } + + if ($this->seqNoPrimaryTerm !== null) { + $array['seq_no_primary_term'] = $this->seqNoPrimaryTerm; + } + + if ($this->storedFields !== []) { + $array['stored_fields'] = $this->storedFields; + } + + if ($this->trackScores !== null) { + $array['track_scores'] = $this->trackScores; + } + + return $array; + } + +} diff --git a/src/Query/Intervals.php b/src/Query/Intervals.php new file mode 100644 index 0000000..c378d33 --- /dev/null +++ b/src/Query/Intervals.php @@ -0,0 +1,48 @@ + $rule e.g. ['match' => ['query' => 'my favorite food', 'max_gaps' => 0]] + * or ['all_of' => ['intervals' => [...], 'max_gaps' => 0]]. + */ + public function __construct( + private string $field, + private array $rule, + ) + { + if ($rule === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Intervals query requires at least one rule.', + ); + } + } + + + public function key(): string + { + return 'intervals_' . $this->field; + } + + + /** + * @return array>> + */ + public function toArray(): array + { + return [ + 'intervals' => [ + $this->field => $this->rule, + ], + ]; + } + +} diff --git a/src/Query/Knn.php b/src/Query/Knn.php new file mode 100644 index 0000000..29c59a2 --- /dev/null +++ b/src/Query/Knn.php @@ -0,0 +1,75 @@ + $queryVector + */ + public function __construct( + private string $field, + private array $queryVector, + private int $k, + private int $numCandidates, + private float|null $similarity = null, + private \Spameri\ElasticQuery\Query\LeafQueryInterface|null $filter = null, + private float $boost = 1.0, + ) + { + if ($queryVector === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Knn query requires a non-empty queryVector.', + ); + } + + if ($k < 1) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Knn k must be >= 1.', + ); + } + } + + + public function key(): string + { + return 'knn_' . $this->field . '_' . $this->k; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'field' => $this->field, + 'query_vector' => $this->queryVector, + 'k' => $this->k, + 'num_candidates' => $this->numCandidates, + 'boost' => $this->boost, + ]; + + if ($this->similarity !== null) { + $body['similarity'] = $this->similarity; + } + + if ($this->filter !== null) { + $body['filter'] = $this->filter->toArray(); + } + + return [ + 'knn' => $body, + ]; + } + +} diff --git a/src/Query/LeafQueryInterface.php b/src/Query/LeafQueryInterface.php index e7f730a..74e047f 100644 --- a/src/Query/LeafQueryInterface.php +++ b/src/Query/LeafQueryInterface.php @@ -1,4 +1,6 @@ -fuzziness = $fuzziness; } diff --git a/src/Query/Match/MultiMatchType.php b/src/Query/Match/MultiMatchType.php index 6a419dd..155e908 100644 --- a/src/Query/Match/MultiMatchType.php +++ b/src/Query/Match/MultiMatchType.php @@ -1,4 +1,6 @@ - self::BEST_FIELDS, - self::MOST_FIELDS => self::MOST_FIELDS, - self::CROSS_FIELDS => self::CROSS_FIELDS, - self::PHRASE => self::PHRASE, + self::BEST_FIELDS => self::BEST_FIELDS, + self::MOST_FIELDS => self::MOST_FIELDS, + self::CROSS_FIELDS => self::CROSS_FIELDS, + self::PHRASE => self::PHRASE, self::PHRASE_PREFIX => self::PHRASE_PREFIX, - self::BOOL_PREFIX => self::BOOL_PREFIX, + self::BOOL_PREFIX => self::BOOL_PREFIX, ]; } diff --git a/src/Query/Match/Operator.php b/src/Query/Match/Operator.php index 4939a97..6ab9bbd 100644 --- a/src/Query/Match/Operator.php +++ b/src/Query/Match/Operator.php @@ -1,4 +1,6 @@ - + */ + public function toArray(): array + { + if ($this->boost !== 1.0) { + return [ + 'match_all' => [ + 'boost' => $this->boost, + ], + ]; + } + + return [ + 'match_all' => new \stdClass(), + ]; + } + +} diff --git a/src/Query/MatchBoolPrefix.php b/src/Query/MatchBoolPrefix.php new file mode 100644 index 0000000..23953ee --- /dev/null +++ b/src/Query/MatchBoolPrefix.php @@ -0,0 +1,85 @@ +field . '_' . $this->query; + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $body = [ + 'query' => $this->query, + 'boost' => $this->boost, + ]; + + if ($this->operator !== null) { + $body['operator'] = $this->operator; + } + + if ($this->minimumShouldMatch !== null) { + $body['minimum_should_match'] = $this->minimumShouldMatch; + } + + if ($this->analyzer !== null) { + $body['analyzer'] = $this->analyzer; + } + + if ($this->fuzziness !== null) { + $body['fuzziness'] = $this->fuzziness->__toString(); + } + + if ($this->prefixLength !== null) { + $body['prefix_length'] = $this->prefixLength; + } + + if ($this->maxExpansions !== null) { + $body['max_expansions'] = $this->maxExpansions; + } + + if ($this->fuzzyTranspositions !== null) { + $body['fuzzy_transpositions'] = $this->fuzzyTranspositions; + } + + if ($this->fuzzyRewrite !== null) { + $body['fuzzy_rewrite'] = $this->fuzzyRewrite; + } + + return [ + 'match_bool_prefix' => [ + $this->field => $body, + ], + ]; + } + +} diff --git a/src/Query/MatchNone.php b/src/Query/MatchNone.php new file mode 100644 index 0000000..853de87 --- /dev/null +++ b/src/Query/MatchNone.php @@ -0,0 +1,29 @@ + + */ + public function toArray(): array + { + return [ + 'match_none' => new \stdClass(), + ]; + } + +} diff --git a/src/Query/MatchPhrase.php b/src/Query/MatchPhrase.php index 1b3a14e..4552176 100644 --- a/src/Query/MatchPhrase.php +++ b/src/Query/MatchPhrase.php @@ -1,4 +1,6 @@ -field = $field; - $this->query = $query; - $this->boost = $boost; - $this->analyzer = $analyzer; - $this->slop = $slop; } @@ -85,6 +54,10 @@ public function toArray(): array $array['match_phrase'][$this->field]['slop'] = $this->slop; } + if ($this->zeroTermsQuery !== null) { + $array['match_phrase'][$this->field]['zero_terms_query'] = $this->zeroTermsQuery; + } + return $array; } diff --git a/src/Query/MoreLikeThis.php b/src/Query/MoreLikeThis.php new file mode 100644 index 0000000..c8d81e3 --- /dev/null +++ b/src/Query/MoreLikeThis.php @@ -0,0 +1,127 @@ + $fields + * @param array> $like Texts or doc refs (['_index' => ..., '_id' => ...]). + * @param array> $unlike + * @param array $stopWords + */ + public function __construct( + private array $fields, + private array $like, + private array $unlike = [], + private int|null $minTermFreq = null, + private int|null $maxQueryTerms = null, + private int|string|null $minimumShouldMatch = null, + private float|null $boostTerms = null, + private bool|null $include = null, + private int|null $minDocFreq = null, + private int|null $maxDocFreq = null, + private int|null $minWordLength = null, + private int|null $maxWordLength = null, + private array $stopWords = [], + private string|null $analyzer = null, + private float $boost = 1.0, + private bool|null $failOnUnsupportedField = null, + ) + { + if ($fields === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'MoreLikeThis query requires at least one field.', + ); + } + + if ($like === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'MoreLikeThis query requires at least one like value.', + ); + } + } + + + public function key(): string + { + return 'more_like_this_' . \implode('-', $this->fields); + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'fields' => $this->fields, + 'like' => $this->like, + 'boost' => $this->boost, + ]; + + if ($this->unlike !== []) { + $body['unlike'] = $this->unlike; + } + + if ($this->minTermFreq !== null) { + $body['min_term_freq'] = $this->minTermFreq; + } + + if ($this->maxQueryTerms !== null) { + $body['max_query_terms'] = $this->maxQueryTerms; + } + + if ($this->minimumShouldMatch !== null) { + $body['minimum_should_match'] = $this->minimumShouldMatch; + } + + if ($this->boostTerms !== null) { + $body['boost_terms'] = $this->boostTerms; + } + + if ($this->include !== null) { + $body['include'] = $this->include; + } + + if ($this->minDocFreq !== null) { + $body['min_doc_freq'] = $this->minDocFreq; + } + + if ($this->maxDocFreq !== null) { + $body['max_doc_freq'] = $this->maxDocFreq; + } + + if ($this->minWordLength !== null) { + $body['min_word_length'] = $this->minWordLength; + } + + if ($this->maxWordLength !== null) { + $body['max_word_length'] = $this->maxWordLength; + } + + if ($this->stopWords !== []) { + $body['stop_words'] = $this->stopWords; + } + + if ($this->analyzer !== null) { + $body['analyzer'] = $this->analyzer; + } + + if ($this->failOnUnsupportedField !== null) { + $body['fail_on_unsupported_field'] = $this->failOnUnsupportedField; + } + + return [ + 'more_like_this' => $body, + ]; + } + +} diff --git a/src/Query/MultiMatch.php b/src/Query/MultiMatch.php index 2842b61..7177c7a 100644 --- a/src/Query/MultiMatch.php +++ b/src/Query/MultiMatch.php @@ -1,4 +1,6 @@ - $fields */ public function __construct( - array $fields, - $query, - float $boost = 1.0, - ?\Spameri\ElasticQuery\Query\Match\Fuzziness $fuzziness = NULL, - string $type = \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, - ?int $minimumShouldMatch = NULL, - string $operator = \Spameri\ElasticQuery\Query\Match\Operator::OR, - ?string $analyzer = NULL + private array $fields, + private bool|int|string|null $query, + private float $boost = 1.0, + private \Spameri\ElasticQuery\Query\Match\Fuzziness|null $fuzziness = null, + private string $type = \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + private int|string|null $minimumShouldMatch = null, + private string $operator = \Spameri\ElasticQuery\Query\Match\Operator::OR, + private string|null $analyzer = null, + private float|null $tieBreaker = null, + private int|null $slop = null, + private int|null $prefixLength = null, + private int|null $maxExpansions = null, + private bool|null $lenient = null, + private string|null $zeroTermsQuery = null, + private bool|null $autoGenerateSynonymsPhraseQuery = null, + private bool|null $fuzzyTranspositions = null, + private string|null $fuzzyRewrite = null, ) { - if ( ! \in_array($operator, \Spameri\ElasticQuery\Query\Match\Operator::OPERATORS, TRUE)) { + if ( ! \in_array($operator, \Spameri\ElasticQuery\Query\Match\Operator::OPERATORS, true)) { throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( - 'Parameter $operator is invalid, see \Spameri\ElasticQuery\Query\Match\Operator::OPERATORS for valid arguments.' + 'Parameter $operator is invalid, see \Spameri\ElasticQuery\Query\Match\Operator::OPERATORS for valid arguments.', ); } - if ( ! \in_array($type, \Spameri\ElasticQuery\Query\Match\MultiMatchType::TYPES, TRUE)) { + if ( ! \in_array($type, \Spameri\ElasticQuery\Query\Match\MultiMatchType::TYPES, true)) { throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( - 'Parameter $type is invalid, see \Spameri\ElasticQuery\Query\Match\MultiMatchType::TYPES for valid arguments.' + 'Parameter $type is invalid, see \Spameri\ElasticQuery\Query\Match\MultiMatchType::TYPES for valid arguments.', ); } - $this->fields = $fields; - $this->query = $query; - $this->type = $type; - $this->operator = $operator; - $this->fuzziness = $fuzziness; - $this->boost = $boost; - $this->analyzer = $analyzer; - $this->minimumShouldMatch = $minimumShouldMatch; } @@ -77,34 +60,70 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { - $array = [ - 'multi_match' => [ - 'query' => $this->query, - 'type' => $this->type, - 'fields' => $this->fields, - 'boost' => $this->boost, - ], + $body = [ + 'query' => $this->query, + 'type' => $this->type, + 'fields' => $this->fields, + 'boost' => $this->boost, + 'operator' => $this->operator, ]; - if ($this->operator) { - $array['multi_match']['operator'] = $this->operator; + if ($this->fuzziness !== null && $this->fuzziness->__toString() !== '') { + $body['fuzziness'] = $this->fuzziness->__toString(); + } + + if ($this->analyzer !== null) { + $body['analyzer'] = $this->analyzer; } - if ($this->fuzziness && $this->fuzziness->__toString()) { - $array['multi_match']['fuzziness'] = $this->fuzziness->__toString(); + if ($this->minimumShouldMatch !== null) { + $body['minimum_should_match'] = $this->minimumShouldMatch; } - if ($this->analyzer) { - $array['multi_match']['analyzer'] = $this->analyzer; + if ($this->tieBreaker !== null) { + $body['tie_breaker'] = $this->tieBreaker; } - if ($this->minimumShouldMatch) { - $array['multi_match']['minimum_should_match'] = $this->minimumShouldMatch; + if ($this->slop !== null) { + $body['slop'] = $this->slop; } - return $array; + if ($this->prefixLength !== null) { + $body['prefix_length'] = $this->prefixLength; + } + + if ($this->maxExpansions !== null) { + $body['max_expansions'] = $this->maxExpansions; + } + + if ($this->lenient !== null) { + $body['lenient'] = $this->lenient; + } + + if ($this->zeroTermsQuery !== null) { + $body['zero_terms_query'] = $this->zeroTermsQuery; + } + + if ($this->autoGenerateSynonymsPhraseQuery !== null) { + $body['auto_generate_synonyms_phrase_query'] = $this->autoGenerateSynonymsPhraseQuery; + } + + if ($this->fuzzyTranspositions !== null) { + $body['fuzzy_transpositions'] = $this->fuzzyTranspositions; + } + + if ($this->fuzzyRewrite !== null) { + $body['fuzzy_rewrite'] = $this->fuzzyRewrite; + } + + return [ + 'multi_match' => $body, + ]; } } diff --git a/src/Query/MustCollection.php b/src/Query/MustCollection.php index d5555fd..7907437 100644 --- a/src/Query/MustCollection.php +++ b/src/Query/MustCollection.php @@ -1,4 +1,6 @@ - */ - private $collection; + private array $collection; public function __construct( - \Spameri\ElasticQuery\Query\LeafQueryInterface ... $collection + \Spameri\ElasticQuery\Query\LeafQueryInterface ...$collection, ) { $this->collection = []; @@ -24,7 +26,7 @@ public function __construct( public function add( - \Spameri\ElasticQuery\Query\LeafQueryInterface $item + \Spameri\ElasticQuery\Query\LeafQueryInterface $item, ): void { $this->collection[$item->key()] = $item; @@ -32,40 +34,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Query\LeafQueryInterface + string $key, + ): \Spameri\ElasticQuery\Query\LeafQueryInterface|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Query/MustNotCollection.php b/src/Query/MustNotCollection.php index 28c2345..97e5fd5 100644 --- a/src/Query/MustNotCollection.php +++ b/src/Query/MustNotCollection.php @@ -1,4 +1,6 @@ - */ - private $collection; + private array $collection; public function __construct( - \Spameri\ElasticQuery\Query\LeafQueryInterface ... $collection + \Spameri\ElasticQuery\Query\LeafQueryInterface ...$collection, ) { $this->collection = []; @@ -24,7 +26,7 @@ public function __construct( public function add( - \Spameri\ElasticQuery\Query\LeafQueryInterface $item + \Spameri\ElasticQuery\Query\LeafQueryInterface $item, ): void { $this->collection[$item->key()] = $item; @@ -32,40 +34,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Query\LeafQueryInterface + string $key, + ): \Spameri\ElasticQuery\Query\LeafQueryInterface|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Query/Nested.php b/src/Query/Nested.php index 15978cc..6816362 100644 --- a/src/Query/Nested.php +++ b/src/Query/Nested.php @@ -1,22 +1,34 @@ -path = $path; - - if ($query === NULL) { + private string $path, + \Spameri\ElasticQuery\Query\QueryCollection|null $query = null, + private string|null $scoreMode = null, + private bool|null $ignoreUnmapped = null, + private \Spameri\ElasticQuery\Query\InnerHits|null $innerHits = null, + ) + { + if ($query === null) { $query = new \Spameri\ElasticQuery\Query\QueryCollection(); } @@ -30,15 +42,36 @@ public function key(): string } + /** + * @return array> + */ public function toArray(): array { + $queryArray = $this->query->toArray(); + + if (\count($queryArray) === 0) { + $queryArray = ['bool' => new \stdClass()]; + } + + $body = [ + 'path' => $this->path, + 'query' => $queryArray, + ]; + + if ($this->scoreMode !== null) { + $body['score_mode'] = $this->scoreMode; + } + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + if ($this->innerHits !== null) { + $body['inner_hits'] = $this->innerHits->toArray(); + } + return [ - 'nested' => [ - 'path' => $this->path, - 'query' => [ - 'bool' => $this->query->toArray(), - ], - ], + 'nested' => $body, ]; } diff --git a/src/Query/ParentId.php b/src/Query/ParentId.php new file mode 100644 index 0000000..65c1342 --- /dev/null +++ b/src/Query/ParentId.php @@ -0,0 +1,49 @@ +type . '_' . $this->id; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'type' => $this->type, + 'id' => $this->id, + 'boost' => $this->boost, + ]; + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + return [ + 'parent_id' => $body, + ]; + } + +} diff --git a/src/Query/Percolate.php b/src/Query/Percolate.php new file mode 100644 index 0000000..c407caf --- /dev/null +++ b/src/Query/Percolate.php @@ -0,0 +1,85 @@ +|null $document Single inline document to percolate. + * @param array>|null $documents Multi-doc inline percolation. + */ + public function __construct( + private string $field, + private array|null $document = null, + private string|null $index = null, + private string|null $id = null, + private array|null $documents = null, + private string|null $name = null, + private string|null $routing = null, + private string|null $preference = null, + private int|null $version = null, + ) + { + if ($document === null && $documents === null && ($index === null || $id === null)) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Percolate query requires either a document, documents, or both index and id.', + ); + } + } + + + public function key(): string + { + return 'percolate_' . $this->field . ($this->name !== null ? '_' . $this->name : ''); + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'field' => $this->field, + ]; + + if ($this->documents !== null) { + $body['documents'] = $this->documents; + + } elseif ($this->document !== null) { + $body['document'] = $this->document; + + } else { + $body['index'] = $this->index; + $body['id'] = $this->id; + } + + if ($this->name !== null) { + $body['name'] = $this->name; + } + + if ($this->routing !== null) { + $body['routing'] = $this->routing; + } + + if ($this->preference !== null) { + $body['preference'] = $this->preference; + } + + if ($this->version !== null) { + $body['version'] = $this->version; + } + + return [ + 'percolate' => $body, + ]; + } + +} diff --git a/src/Query/PhrasePrefix.php b/src/Query/PhrasePrefix.php index c6b96c7..413e7d0 100644 --- a/src/Query/PhrasePrefix.php +++ b/src/Query/PhrasePrefix.php @@ -1,29 +1,26 @@ -field = $field; - $this->queryString = $queryString; - $this->boost = $boost; - $this->slop = $slop; + private string $field, + private string $queryString, + private float $boost = 1.0, + private int $slop = 1, + private string|null $analyzer = null, + private int|null $maxExpansions = null, + private string|null $zeroTermsQuery = null, + ) + { } @@ -33,15 +30,32 @@ public function key(): string } + /** + * @return array>> + */ public function toArray(): array { + $body = [ + 'query' => $this->queryString, + 'boost' => $this->boost, + 'slop' => $this->slop, + ]; + + if ($this->analyzer !== null) { + $body['analyzer'] = $this->analyzer; + } + + if ($this->maxExpansions !== null) { + $body['max_expansions'] = $this->maxExpansions; + } + + if ($this->zeroTermsQuery !== null) { + $body['zero_terms_query'] = $this->zeroTermsQuery; + } + return [ 'match_phrase_prefix' => [ - $this->field => [ - 'query' => $this->queryString, - 'boost' => $this->boost, - 'slop' => $this->slop, - ], + $this->field => $body, ], ]; } diff --git a/src/Query/Pinned.php b/src/Query/Pinned.php new file mode 100644 index 0000000..95b88cf --- /dev/null +++ b/src/Query/Pinned.php @@ -0,0 +1,59 @@ + $ids IDs to pin (use either $ids or $docs). + * @param array> $docs Document references: [['_index' => ..., '_id' => ...]]. + */ + public function __construct( + private \Spameri\ElasticQuery\Query\LeafQueryInterface $organic, + private array $ids = [], + private array $docs = [], + ) + { + if ($ids === [] && $docs === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Pinned query requires either ids or docs to pin.', + ); + } + } + + + public function key(): string + { + return 'pinned_' . $this->organic->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'organic' => $this->organic->toArray(), + ]; + + if ($this->ids !== []) { + $body['ids'] = $this->ids; + } + + if ($this->docs !== []) { + $body['docs'] = $this->docs; + } + + return [ + 'pinned' => $body, + ]; + } + +} diff --git a/src/Query/Prefix.php b/src/Query/Prefix.php new file mode 100644 index 0000000..2bcf113 --- /dev/null +++ b/src/Query/Prefix.php @@ -0,0 +1,55 @@ +field . '_' . $this->query; + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $body = [ + 'value' => $this->query, + 'boost' => $this->boost, + ]; + + if ($this->caseInsensitive !== null) { + $body['case_insensitive'] = $this->caseInsensitive; + } + + if ($this->rewrite !== null) { + $body['rewrite'] = $this->rewrite; + } + + return [ + 'prefix' => [ + $this->field => $body, + ], + ]; + } + +} diff --git a/src/Query/QueryCollection.php b/src/Query/QueryCollection.php index 49b331d..9d94f34 100644 --- a/src/Query/QueryCollection.php +++ b/src/Query/QueryCollection.php @@ -1,4 +1,6 @@ -mustCollection = $mustCollection; $this->shouldCollection = $shouldCollection; $this->mustNotCollection = $mustNotCollection; - $this->key = $key; } diff --git a/src/Query/QueryString.php b/src/Query/QueryString.php new file mode 100644 index 0000000..5e3522d --- /dev/null +++ b/src/Query/QueryString.php @@ -0,0 +1,156 @@ + $fields + */ + public function __construct( + private string $query, + private array $fields = [], + private string|null $defaultField = null, + private string|null $defaultOperator = null, + private string|null $analyzer = null, + private bool|null $allowLeadingWildcard = null, + private float $boost = 1.0, + private bool|null $analyzeWildcard = null, + private bool|null $autoGenerateSynonymsPhraseQuery = null, + private bool|null $enablePositionIncrements = null, + private \Spameri\ElasticQuery\Query\Match\Fuzziness|null $fuzziness = null, + private int|null $fuzzyMaxExpansions = null, + private int|null $fuzzyPrefixLength = null, + private bool|null $fuzzyTranspositions = null, + private bool|null $lenient = null, + private int|null $maxDeterminizedStates = null, + private int|string|null $minimumShouldMatch = null, + private string|null $quoteAnalyzer = null, + private int|null $phraseSlop = null, + private string|null $quoteFieldSuffix = null, + private string|null $rewrite = null, + private string|null $timeZone = null, + private string|null $type = null, + private float|null $tieBreaker = null, + ) + { + } + + + public function key(): string + { + return 'query_string_' . $this->query; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'query' => $this->query, + 'boost' => $this->boost, + ]; + + if ($this->fields !== []) { + $body['fields'] = $this->fields; + } + + if ($this->defaultField !== null) { + $body['default_field'] = $this->defaultField; + } + + if ($this->defaultOperator !== null) { + $body['default_operator'] = $this->defaultOperator; + } + + if ($this->analyzer !== null) { + $body['analyzer'] = $this->analyzer; + } + + if ($this->allowLeadingWildcard !== null) { + $body['allow_leading_wildcard'] = $this->allowLeadingWildcard; + } + + if ($this->analyzeWildcard !== null) { + $body['analyze_wildcard'] = $this->analyzeWildcard; + } + + if ($this->autoGenerateSynonymsPhraseQuery !== null) { + $body['auto_generate_synonyms_phrase_query'] = $this->autoGenerateSynonymsPhraseQuery; + } + + if ($this->enablePositionIncrements !== null) { + $body['enable_position_increments'] = $this->enablePositionIncrements; + } + + if ($this->fuzziness !== null) { + $body['fuzziness'] = $this->fuzziness->__toString(); + } + + if ($this->fuzzyMaxExpansions !== null) { + $body['fuzzy_max_expansions'] = $this->fuzzyMaxExpansions; + } + + if ($this->fuzzyPrefixLength !== null) { + $body['fuzzy_prefix_length'] = $this->fuzzyPrefixLength; + } + + if ($this->fuzzyTranspositions !== null) { + $body['fuzzy_transpositions'] = $this->fuzzyTranspositions; + } + + if ($this->lenient !== null) { + $body['lenient'] = $this->lenient; + } + + if ($this->maxDeterminizedStates !== null) { + $body['max_determinized_states'] = $this->maxDeterminizedStates; + } + + if ($this->minimumShouldMatch !== null) { + $body['minimum_should_match'] = $this->minimumShouldMatch; + } + + if ($this->quoteAnalyzer !== null) { + $body['quote_analyzer'] = $this->quoteAnalyzer; + } + + if ($this->phraseSlop !== null) { + $body['phrase_slop'] = $this->phraseSlop; + } + + if ($this->quoteFieldSuffix !== null) { + $body['quote_field_suffix'] = $this->quoteFieldSuffix; + } + + if ($this->rewrite !== null) { + $body['rewrite'] = $this->rewrite; + } + + if ($this->timeZone !== null) { + $body['time_zone'] = $this->timeZone; + } + + if ($this->type !== null) { + $body['type'] = $this->type; + } + + if ($this->tieBreaker !== null) { + $body['tie_breaker'] = $this->tieBreaker; + } + + return [ + 'query_string' => $body, + ]; + } + +} diff --git a/src/Query/Range.php b/src/Query/Range.php index da6a422..4e61e50 100644 --- a/src/Query/Range.php +++ b/src/Query/Range.php @@ -1,4 +1,6 @@ -format('U'); - - } else { - $gteValue = $gte; - } - - if ($lte instanceof \DateTimeInterface) { - $lteValue = $lte->format('U'); + $this->throwInvalidRange('gte', $gte, 'lte', $lte); + } - } else { - $lteValue = $lte; - } + if ($lt && $gt && $lt < $gt) { + $this->throwInvalidRange('gt', $gt, 'lt', $lt); + } + if ($relation !== null && ! \in_array($relation, \Spameri\ElasticQuery\Query\Range\Relation::RELATIONS, true)) { throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( - 'Input values does not make range. From: ' . $gteValue . ' To: ' . $lteValue + 'Range relation ' . $relation . ' is invalid, see \Spameri\ElasticQuery\Query\Range\Relation::RELATIONS.', ); } + } - $this->field = $field; - $this->gte = $gte; - $this->lte = $lte; - $this->boost = $boost; + + private function throwInvalidRange( + string $fromKey, + float|\DateTimeInterface|int|string $from, + string $toKey, + float|\DateTimeInterface|int|string $to, + ): never + { + $fromValue = $from instanceof \DateTimeInterface ? $from->format('U') : $from; + $toValue = $to instanceof \DateTimeInterface ? $to->format('U') : $to; + + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Input values do not make a range. ' . $fromKey . ': ' . $fromValue . ' ' . $toKey . ': ' . $toValue, + ); } public function key(): string { - $gte = $this->gte instanceof \DateTimeInterface ? $this->gte->format('Y-m-d H:i:s') : $this->gte; - $lte = $this->lte instanceof \DateTimeInterface ? $this->lte->format('Y-m-d H:i:s') : $this->lte; + $parts = []; + foreach (['gte' => $this->gte, 'lte' => $this->lte, 'gt' => $this->gt, 'lt' => $this->lt] as $name => $value) { + if ($value === null) { + continue; + } + $parts[] = $name . ':' . ($value instanceof \DateTimeInterface ? $value->format('Y-m-d H:i:s') : $value); + } - return 'range_' . $this->field . '_' . $gte . '_' . $lte; + return 'range_' . $this->field . '_' . \implode('_', $parts); } + /** + * @return array>> + */ public function toArray(): array { - $array = [ - 'range' => [ - $this->field => [ - 'boost' => $this->boost, - ], - ], - ]; + $body = ['boost' => $this->boost]; - if ($this->gte !== NULL) { - $array['range'][$this->field]['gte'] = - $this->gte instanceof \DateTimeInterface - ? $this->gte->format('Y-m-d H:i:s') - : $this->gte; + foreach (['gte' => $this->gte, 'lte' => $this->lte, 'gt' => $this->gt, 'lt' => $this->lt] as $name => $value) { + if ($value === null) { + continue; + } + $body[$name] = $value instanceof \DateTimeInterface + ? $value->format('Y-m-d H:i:s') + : $value; } - if ($this->lte !== NULL) { - $array['range'][$this->field]['lte'] = - $this->lte instanceof \DateTimeInterface - ? $this->lte->format('Y-m-d H:i:s') - : $this->lte; + if ($this->format !== null) { + $body['format'] = $this->format; } - return $array; + if ($this->relation !== null) { + $body['relation'] = $this->relation; + } + + if ($this->timeZone !== null) { + $body['time_zone'] = $this->timeZone; + } + + return [ + 'range' => [ + $this->field => $body, + ], + ]; } } diff --git a/src/Query/Range/Relation.php b/src/Query/Range/Relation.php new file mode 100644 index 0000000..365ed42 --- /dev/null +++ b/src/Query/Range/Relation.php @@ -0,0 +1,24 @@ + self::INTERSECTS, + self::CONTAINS => self::CONTAINS, + self::WITHIN => self::WITHIN, + ]; + +} diff --git a/src/Query/RankFeature.php b/src/Query/RankFeature.php new file mode 100644 index 0000000..48f9427 --- /dev/null +++ b/src/Query/RankFeature.php @@ -0,0 +1,54 @@ +|null $function Optional scoring function: + * ['saturation' => ['pivot' => 8]] | ['log' => ['scaling_factor' => 4]] + * | ['sigmoid' => ['pivot' => 7, 'exponent' => 0.6]] | ['linear' => new \stdClass()]. + */ + public function __construct( + private string $field, + private array|null $function = null, + private float $boost = 1.0, + ) + { + } + + + public function key(): string + { + return 'rank_feature_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'field' => $this->field, + 'boost' => $this->boost, + ]; + + if ($this->function !== null) { + foreach ($this->function as $name => $config) { + $body[$name] = $config; + } + } + + return [ + 'rank_feature' => $body, + ]; + } + +} diff --git a/src/Query/Regexp.php b/src/Query/Regexp.php new file mode 100644 index 0000000..a1f3206 --- /dev/null +++ b/src/Query/Regexp.php @@ -0,0 +1,65 @@ +field . '_' . $this->query; + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $body = [ + 'value' => $this->query, + 'boost' => $this->boost, + ]; + + if ($this->flags !== null) { + $body['flags'] = $this->flags; + } + + if ($this->caseInsensitive !== null) { + $body['case_insensitive'] = $this->caseInsensitive; + } + + if ($this->maxDeterminizedStates !== null) { + $body['max_determinized_states'] = $this->maxDeterminizedStates; + } + + if ($this->rewrite !== null) { + $body['rewrite'] = $this->rewrite; + } + + return [ + 'regexp' => [ + $this->field => $body, + ], + ]; + } + +} diff --git a/src/Query/RuleQuery.php b/src/Query/RuleQuery.php new file mode 100644 index 0000000..b879aff --- /dev/null +++ b/src/Query/RuleQuery.php @@ -0,0 +1,54 @@ + $rulesetIds + * @param array $matchCriteria + */ + public function __construct( + private \Spameri\ElasticQuery\Query\LeafQueryInterface $organic, + private array $rulesetIds, + private array $matchCriteria, + ) + { + if ($rulesetIds === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'RuleQuery requires at least one rulesetId.', + ); + } + } + + + public function key(): string + { + return 'rule_' . $this->organic->key() . '_' . \implode('-', $this->rulesetIds); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'rule' => [ + 'organic' => $this->organic->toArray(), + 'ruleset_ids' => $this->rulesetIds, + 'match_criteria' => $this->matchCriteria, + ], + ]; + } + +} diff --git a/src/Query/Script.php b/src/Query/Script.php new file mode 100644 index 0000000..91653e6 --- /dev/null +++ b/src/Query/Script.php @@ -0,0 +1,52 @@ + $params + */ + public function __construct( + private string $source, + private string $lang = 'painless', + private array $params = [], + ) + { + } + + + public function key(): string + { + return 'script_' . \md5($this->source); + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $script = [ + 'source' => $this->source, + 'lang' => $this->lang, + ]; + + if ($this->params !== []) { + $script['params'] = $this->params; + } + + return [ + 'script' => [ + 'script' => $script, + ], + ]; + } + +} diff --git a/src/Query/ScriptScore.php b/src/Query/ScriptScore.php new file mode 100644 index 0000000..025ceda --- /dev/null +++ b/src/Query/ScriptScore.php @@ -0,0 +1,63 @@ + $params + */ + public function __construct( + private \Spameri\ElasticQuery\Query\LeafQueryInterface $query, + private string $source, + private string $lang = 'painless', + private array $params = [], + private float|null $minScore = null, + private float $boost = 1.0, + ) + { + } + + + public function key(): string + { + return 'script_score_' . $this->query->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + $script = [ + 'source' => $this->source, + 'lang' => $this->lang, + ]; + + if ($this->params !== []) { + $script['params'] = $this->params; + } + + $body = [ + 'query' => $this->query->toArray(), + 'script' => $script, + 'boost' => $this->boost, + ]; + + if ($this->minScore !== null) { + $body['min_score'] = $this->minScore; + } + + return [ + 'script_score' => $body, + ]; + } + +} diff --git a/src/Query/Semantic.php b/src/Query/Semantic.php new file mode 100644 index 0000000..0afcfe9 --- /dev/null +++ b/src/Query/Semantic.php @@ -0,0 +1,45 @@ +field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'semantic' => [ + 'field' => $this->field, + 'query' => $this->query, + 'boost' => $this->boost, + ], + ]; + } + +} diff --git a/src/Query/Shape.php b/src/Query/Shape.php new file mode 100644 index 0000000..66e08c4 --- /dev/null +++ b/src/Query/Shape.php @@ -0,0 +1,77 @@ +|null $shape Inline GeoJSON shape; null when $indexedShape is used. + */ + public function __construct( + private string $field, + private array|null $shape = null, + private string $relation = 'intersects', + private bool|null $ignoreUnmapped = null, + private \Spameri\ElasticQuery\Query\IndexedShape|null $indexedShape = null, + private float $boost = 1.0, + ) + { + if ( ! \in_array($relation, ['intersects', 'disjoint', 'within', 'contains'], true)) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Shape relation must be one of: intersects, disjoint, within, contains.', + ); + } + + if ($shape === null && $indexedShape === null) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'Shape requires either an inline shape or an indexedShape.', + ); + } + } + + + public function key(): string + { + return 'shape_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $inner = [ + 'relation' => $this->relation, + ]; + + if ($this->shape !== null) { + $inner['shape'] = $this->shape; + } + + if ($this->indexedShape !== null) { + $inner['indexed_shape'] = $this->indexedShape->toArray(); + } + + $body = [ + $this->field => $inner, + 'boost' => $this->boost, + ]; + + if ($this->ignoreUnmapped !== null) { + $body['ignore_unmapped'] = $this->ignoreUnmapped; + } + + return [ + 'shape' => $body, + ]; + } + +} diff --git a/src/Query/ShouldCollection.php b/src/Query/ShouldCollection.php index 60eb116..e03db2a 100644 --- a/src/Query/ShouldCollection.php +++ b/src/Query/ShouldCollection.php @@ -1,4 +1,6 @@ - */ - private $collection; + private array $collection; public function __construct( - \Spameri\ElasticQuery\Query\LeafQueryInterface ... $collection + \Spameri\ElasticQuery\Query\LeafQueryInterface ...$collection, ) { $this->collection = []; @@ -24,7 +26,7 @@ public function __construct( public function add( - \Spameri\ElasticQuery\Query\LeafQueryInterface $item + \Spameri\ElasticQuery\Query\LeafQueryInterface $item, ): void { $this->collection[$item->key()] = $item; @@ -32,40 +34,36 @@ public function add( public function remove( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { unset($this->collection[$key]); - return TRUE; + return true; } - return FALSE; + return false; } public function get( - string $key - ): ?\Spameri\ElasticQuery\Query\LeafQueryInterface + string $key, + ): \Spameri\ElasticQuery\Query\LeafQueryInterface|null { - if (isset($this->collection[$key])) { - return $this->collection[$key]; - } - - return NULL; + return $this->collection[$key] ?? null; } public function isValue( - string $key + string $key, ): bool { if (isset($this->collection[$key])) { - return TRUE; + return true; } - return FALSE; + return false; } diff --git a/src/Query/SimpleQueryString.php b/src/Query/SimpleQueryString.php new file mode 100644 index 0000000..8876dab --- /dev/null +++ b/src/Query/SimpleQueryString.php @@ -0,0 +1,106 @@ + $fields + */ + public function __construct( + private string $query, + private array $fields = [], + private string|null $defaultOperator = null, + private string|null $analyzer = null, + private string|null $flags = null, + private float $boost = 1.0, + private bool|null $analyzeWildcard = null, + private bool|null $autoGenerateSynonymsPhraseQuery = null, + private int|null $fuzzyMaxExpansions = null, + private int|null $fuzzyPrefixLength = null, + private bool|null $fuzzyTranspositions = null, + private bool|null $lenient = null, + private int|string|null $minimumShouldMatch = null, + private string|null $quoteFieldSuffix = null, + ) + { + } + + + public function key(): string + { + return 'simple_query_string_' . $this->query; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'query' => $this->query, + 'boost' => $this->boost, + ]; + + if ($this->fields !== []) { + $body['fields'] = $this->fields; + } + + if ($this->defaultOperator !== null) { + $body['default_operator'] = $this->defaultOperator; + } + + if ($this->analyzer !== null) { + $body['analyzer'] = $this->analyzer; + } + + if ($this->flags !== null) { + $body['flags'] = $this->flags; + } + + if ($this->analyzeWildcard !== null) { + $body['analyze_wildcard'] = $this->analyzeWildcard; + } + + if ($this->autoGenerateSynonymsPhraseQuery !== null) { + $body['auto_generate_synonyms_phrase_query'] = $this->autoGenerateSynonymsPhraseQuery; + } + + if ($this->fuzzyMaxExpansions !== null) { + $body['fuzzy_max_expansions'] = $this->fuzzyMaxExpansions; + } + + if ($this->fuzzyPrefixLength !== null) { + $body['fuzzy_prefix_length'] = $this->fuzzyPrefixLength; + } + + if ($this->fuzzyTranspositions !== null) { + $body['fuzzy_transpositions'] = $this->fuzzyTranspositions; + } + + if ($this->lenient !== null) { + $body['lenient'] = $this->lenient; + } + + if ($this->minimumShouldMatch !== null) { + $body['minimum_should_match'] = $this->minimumShouldMatch; + } + + if ($this->quoteFieldSuffix !== null) { + $body['quote_field_suffix'] = $this->quoteFieldSuffix; + } + + return [ + 'simple_query_string' => $body, + ]; + } + +} diff --git a/src/Query/SpanContaining.php b/src/Query/SpanContaining.php new file mode 100644 index 0000000..e22a665 --- /dev/null +++ b/src/Query/SpanContaining.php @@ -0,0 +1,40 @@ +big->key() . '_' . $this->little->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'span_containing' => [ + 'big' => $this->big->toArray(), + 'little' => $this->little->toArray(), + ], + ]; + } + +} diff --git a/src/Query/SpanFirst.php b/src/Query/SpanFirst.php new file mode 100644 index 0000000..95ca74b --- /dev/null +++ b/src/Query/SpanFirst.php @@ -0,0 +1,40 @@ +match->key() . '_' . $this->end; + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'span_first' => [ + 'match' => $this->match->toArray(), + 'end' => $this->end, + ], + ]; + } + +} diff --git a/src/Query/SpanMulti.php b/src/Query/SpanMulti.php new file mode 100644 index 0000000..a61e1a6 --- /dev/null +++ b/src/Query/SpanMulti.php @@ -0,0 +1,38 @@ +match->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'span_multi' => [ + 'match' => $this->match->toArray(), + ], + ]; + } + +} diff --git a/src/Query/SpanNear.php b/src/Query/SpanNear.php new file mode 100644 index 0000000..98ff5c0 --- /dev/null +++ b/src/Query/SpanNear.php @@ -0,0 +1,65 @@ + + */ + private array $clauses; + + + public function __construct( + \Spameri\ElasticQuery\Query\LeafQueryInterface $clause, + private int $slop = 0, + private bool $inOrder = true, + ) + { + $this->clauses = [$clause]; + } + + + public function addClause(\Spameri\ElasticQuery\Query\LeafQueryInterface $clause): void + { + $this->clauses[] = $clause; + } + + + public function key(): string + { + $keys = []; + foreach ($this->clauses as $clause) { + $keys[] = $clause->key(); + } + + return 'span_near_' . \implode('-', $keys); + } + + + /** + * @return array> + */ + public function toArray(): array + { + $clauses = []; + foreach ($this->clauses as $clause) { + $clauses[] = $clause->toArray(); + } + + return [ + 'span_near' => [ + 'clauses' => $clauses, + 'slop' => $this->slop, + 'in_order' => $this->inOrder, + ], + ]; + } + +} diff --git a/src/Query/SpanNot.php b/src/Query/SpanNot.php new file mode 100644 index 0000000..dbb2f09 --- /dev/null +++ b/src/Query/SpanNot.php @@ -0,0 +1,57 @@ +include->key() . '_' . $this->exclude->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'include' => $this->include->toArray(), + 'exclude' => $this->exclude->toArray(), + ]; + + if ($this->pre !== null) { + $body['pre'] = $this->pre; + } + + if ($this->post !== null) { + $body['post'] = $this->post; + } + + if ($this->dist !== null) { + $body['dist'] = $this->dist; + } + + return [ + 'span_not' => $body, + ]; + } + +} diff --git a/src/Query/SpanOr.php b/src/Query/SpanOr.php new file mode 100644 index 0000000..28fe3f7 --- /dev/null +++ b/src/Query/SpanOr.php @@ -0,0 +1,59 @@ + + */ + private array $clauses; + + + public function __construct(\Spameri\ElasticQuery\Query\LeafQueryInterface $clause) + { + $this->clauses = [$clause]; + } + + + public function addClause(\Spameri\ElasticQuery\Query\LeafQueryInterface $clause): void + { + $this->clauses[] = $clause; + } + + + public function key(): string + { + $keys = []; + foreach ($this->clauses as $clause) { + $keys[] = $clause->key(); + } + + return 'span_or_' . \implode('-', $keys); + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $clauses = []; + foreach ($this->clauses as $clause) { + $clauses[] = $clause->toArray(); + } + + return [ + 'span_or' => [ + 'clauses' => $clauses, + ], + ]; + } + +} diff --git a/src/Query/SpanTerm.php b/src/Query/SpanTerm.php new file mode 100644 index 0000000..c9412df --- /dev/null +++ b/src/Query/SpanTerm.php @@ -0,0 +1,43 @@ +field . '_' . $this->query; + } + + + /** + * @return array>> + */ + public function toArray(): array + { + return [ + 'span_term' => [ + $this->field => [ + 'value' => $this->query, + 'boost' => $this->boost, + ], + ], + ]; + } + +} diff --git a/src/Query/SpanWithin.php b/src/Query/SpanWithin.php new file mode 100644 index 0000000..00e262c --- /dev/null +++ b/src/Query/SpanWithin.php @@ -0,0 +1,40 @@ +big->key() . '_' . $this->little->key(); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'span_within' => [ + 'big' => $this->big->toArray(), + 'little' => $this->little->toArray(), + ], + ]; + } + +} diff --git a/src/Query/SparseVector.php b/src/Query/SparseVector.php new file mode 100644 index 0000000..00ad16f --- /dev/null +++ b/src/Query/SparseVector.php @@ -0,0 +1,86 @@ + weight pairs directly) + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html + */ +class SparseVector implements LeafQueryInterface +{ + + /** + * @param array|null $queryVector Token => weight pairs. + * @param array|null $pruningConfig + */ + public function __construct( + private string $field, + private string|null $inferenceId = null, + private string|null $query = null, + private array|null $queryVector = null, + private bool|null $prune = null, + private array|null $pruningConfig = null, + private float $boost = 1.0, + ) + { + $hasInference = $inferenceId !== null && $query !== null; + $hasVector = $queryVector !== null && $queryVector !== []; + + if ( ! $hasInference && ! $hasVector) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'SparseVector requires either (inferenceId + query) or queryVector.', + ); + } + } + + + public function key(): string + { + return 'sparse_vector_' . $this->field; + } + + + /** + * @return array> + */ + public function toArray(): array + { + $body = [ + 'field' => $this->field, + 'boost' => $this->boost, + ]; + + if ($this->inferenceId !== null) { + $body['inference_id'] = $this->inferenceId; + } + + if ($this->query !== null) { + $body['query'] = $this->query; + } + + if ($this->queryVector !== null) { + $body['query_vector'] = $this->queryVector; + } + + if ($this->prune !== null) { + $body['prune'] = $this->prune; + } + + if ($this->pruningConfig !== null) { + $body['pruning_config'] = $this->pruningConfig; + } + + return [ + 'sparse_vector' => $body, + ]; + } + +} diff --git a/src/Query/Term.php b/src/Query/Term.php index cce59f2..2b3900c 100644 --- a/src/Query/Term.php +++ b/src/Query/Term.php @@ -1,35 +1,23 @@ -field = $field; - $this->query = $query; - $this->boost = $boost; } @@ -39,14 +27,23 @@ public function key(): string } + /** + * @return array>> + */ public function toArray(): array { + $body = [ + 'value' => $this->query, + 'boost' => $this->boost, + ]; + + if ($this->caseInsensitive !== null) { + $body['case_insensitive'] = $this->caseInsensitive; + } + return [ 'term' => [ - $this->field => [ - 'value' => $this->query, - 'boost' => $this->boost, - ], + $this->field => $body, ], ]; } diff --git a/src/Query/TermSet.php b/src/Query/TermSet.php new file mode 100644 index 0000000..ad3a2ad --- /dev/null +++ b/src/Query/TermSet.php @@ -0,0 +1,69 @@ + $terms + */ + public function __construct( + private string $field, + private array $terms, + private string|null $minimumShouldMatchField = null, + private string|null $minimumShouldMatchScript = null, + private float $boost = 1.0, + ) + { + if ($terms === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'TermSet query must contain at least one term.', + ); + } + + if ($minimumShouldMatchField === null && $minimumShouldMatchScript === null) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'TermSet query requires either minimumShouldMatchField or minimumShouldMatchScript.', + ); + } + } + + + public function key(): string + { + return 'terms_set_' . $this->field . '_' . \implode('-', $this->terms); + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $body = [ + 'terms' => $this->terms, + 'boost' => $this->boost, + ]; + + if ($this->minimumShouldMatchField !== null) { + $body['minimum_should_match_field'] = $this->minimumShouldMatchField; + } + + if ($this->minimumShouldMatchScript !== null) { + $body['minimum_should_match_script'] = ['source' => $this->minimumShouldMatchScript]; + } + + return [ + 'terms_set' => [ + $this->field => $body, + ], + ]; + } + +} diff --git a/src/Query/Terms.php b/src/Query/Terms.php index 63c4ceb..b18472d 100644 --- a/src/Query/Terms.php +++ b/src/Query/Terms.php @@ -1,4 +1,6 @@ - + * @param array|\Spameri\ElasticQuery\Query\TermsLookup $query Either inline values or a terms_lookup. */ - private array $query; - - private float $boost; - - public function __construct( - string $field - , array $query - , float $boost = 1.0 + private string $field, + private array|\Spameri\ElasticQuery\Query\TermsLookup $query, + private float $boost = 1.0, ) { - if ( ! \count($query)) { + if (\is_array($query) && \count($query) === 0) { throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( - 'Terms query must contain values, empty array given.' + 'Terms query must contain values, empty array given.', ); } - $this->field = $field; - $this->query = $query; - $this->boost = $boost; } public function key(): string { - return 'terms_' . $this->field . '_' . \implode('-', $this->query); + if ($this->query instanceof \Spameri\ElasticQuery\Query\TermsLookup) { + return 'terms_' . $this->field . '_lookup'; + } + + return 'terms_' . $this->field . '_' . \implode('-', \array_map('\strval', $this->query)); } + /** + * @return array> + */ public function toArray(): array { + if ($this->query instanceof \Spameri\ElasticQuery\Query\TermsLookup) { + return [ + 'terms' => [ + $this->field => $this->query->toArray(), + 'boost' => $this->boost, + ], + ]; + } + return [ 'terms' => [ - $this->field => $this->query, - 'boost' => $this->boost, + $this->field => $this->query, + 'boost' => $this->boost, ], ]; } diff --git a/src/Query/TermsLookup.php b/src/Query/TermsLookup.php new file mode 100644 index 0000000..5bb5a0c --- /dev/null +++ b/src/Query/TermsLookup.php @@ -0,0 +1,44 @@ + + */ + public function toArray(): array + { + $array = [ + 'index' => $this->index, + 'id' => $this->id, + 'path' => $this->path, + ]; + + if ($this->routing !== null) { + $array['routing'] = $this->routing; + } + + return $array; + } + +} diff --git a/src/Query/TextExpansion.php b/src/Query/TextExpansion.php new file mode 100644 index 0000000..7628220 --- /dev/null +++ b/src/Query/TextExpansion.php @@ -0,0 +1,58 @@ +|null $pruningConfig + */ + public function __construct( + private string $field, + private string $modelId, + private string $modelText, + private array|null $pruningConfig = null, + private float $boost = 1.0, + ) + { + } + + + public function key(): string + { + return 'text_expansion_' . $this->field; + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $body = [ + 'model_id' => $this->modelId, + 'model_text' => $this->modelText, + 'boost' => $this->boost, + ]; + + if ($this->pruningConfig !== null) { + $body['pruning_config'] = $this->pruningConfig; + } + + return [ + 'text_expansion' => [ + $this->field => $body, + ], + ]; + } + +} diff --git a/src/Query/WeightedTokens.php b/src/Query/WeightedTokens.php new file mode 100644 index 0000000..19182e9 --- /dev/null +++ b/src/Query/WeightedTokens.php @@ -0,0 +1,60 @@ + $tokens Token => weight pairs. + * @param array|null $pruningConfig + */ + public function __construct( + private string $field, + private array $tokens, + private array|null $pruningConfig = null, + ) + { + if ($tokens === []) { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException( + 'WeightedTokens requires at least one token.', + ); + } + } + + + public function key(): string + { + return 'weighted_tokens_' . $this->field; + } + + + /** + * @return array>> + */ + public function toArray(): array + { + $body = [ + 'tokens' => $this->tokens, + ]; + + if ($this->pruningConfig !== null) { + $body['pruning_config'] = $this->pruningConfig; + } + + return [ + 'weighted_tokens' => [ + $this->field => $body, + ], + ]; + } + +} diff --git a/src/Query/WildCard.php b/src/Query/WildCard.php index e99df92..a4779e6 100644 --- a/src/Query/WildCard.php +++ b/src/Query/WildCard.php @@ -1,29 +1,24 @@ -field = $field; - $this->query = $query; - $this->boost = $boost; } @@ -33,14 +28,27 @@ public function key(): string } + /** + * @return array>> + */ public function toArray(): array { + $body = [ + 'value' => $this->query, + 'boost' => $this->boost, + ]; + + if ($this->caseInsensitive !== null) { + $body['case_insensitive'] = $this->caseInsensitive; + } + + if ($this->rewrite !== null) { + $body['rewrite'] = $this->rewrite; + } + return [ 'wildcard' => [ - $this->field => [ - 'value' => $this->query, - 'boost' => $this->boost, - ], + $this->field => $body, ], ]; } diff --git a/src/Query/Wrapper.php b/src/Query/Wrapper.php new file mode 100644 index 0000000..d26453f --- /dev/null +++ b/src/Query/Wrapper.php @@ -0,0 +1,40 @@ +encoded = \base64_encode($rawJsonQuery); + } + + + public function key(): string + { + return 'wrapper_' . \substr($this->encoded, 0, 12); + } + + + /** + * @return array> + */ + public function toArray(): array + { + return [ + 'wrapper' => [ + 'query' => $this->encoded, + ], + ]; + } + +} diff --git a/src/Response/Result/Aggregation.php b/src/Response/Result/Aggregation.php index 2751c2c..9f7cc81 100644 --- a/src/Response/Result/Aggregation.php +++ b/src/Response/Result/Aggregation.php @@ -1,4 +1,6 @@ -name = $name; - $this->position = $position; - $this->bucketCollection = $bucketCollection; - $this->aggregations = $subAggregations; } @@ -56,6 +37,12 @@ public function buckets(): \Spameri\ElasticQuery\Response\Result\Aggregation\Buc } + public function hits(): \Spameri\ElasticQuery\Response\Result\HitCollection + { + return $this->hits; + } + + public function countBuckets(): int { $count = 0; diff --git a/src/Response/Result/Aggregation/Bucket.php b/src/Response/Result/Aggregation/Bucket.php index 30b01fb..df1775b 100644 --- a/src/Response/Result/Aggregation/Bucket.php +++ b/src/Response/Result/Aggregation/Bucket.php @@ -1,4 +1,6 @@ -key = $key; - $this->docCount = $docCount; - $this->position = $position; - $this->from = $from; - $this->to = $to; } public function key(): string { - return (string) $this->key; + return $this->key; } @@ -62,25 +31,19 @@ public function docCount(): int } - public function position(): ?int + public function position(): int|null { return $this->position; } - /** - * @return int|float|null - */ - public function from() + public function from(): float|int|string|null { return $this->from; } - /** - * @return int|float|null - */ - public function to() + public function to(): float|int|string|null { return $this->to; } diff --git a/src/Response/Result/Aggregation/BucketCollection.php b/src/Response/Result/Aggregation/BucketCollection.php index d9012c0..9cfe204 100644 --- a/src/Response/Result/Aggregation/BucketCollection.php +++ b/src/Response/Result/Aggregation/BucketCollection.php @@ -1,4 +1,6 @@ - */ - private $buckets; + private array $buckets; public function __construct( - \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket ... $buckets + \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket ...$buckets, ) { $this->buckets = $buckets; diff --git a/src/Response/Result/AggregationCollection.php b/src/Response/Result/AggregationCollection.php index f8bc4e1..a492272 100644 --- a/src/Response/Result/AggregationCollection.php +++ b/src/Response/Result/AggregationCollection.php @@ -1,4 +1,6 @@ - */ - private $aggregations; + private array $aggregations; public function __construct( - \Spameri\ElasticQuery\Response\Result\Aggregation ... $aggregations + \Spameri\ElasticQuery\Response\Result\Aggregation ...$aggregations, ) { $this->aggregations = $aggregations; @@ -27,8 +30,8 @@ public function getIterator(): \ArrayIterator public function getAggregation( - string $name - ): ?\Spameri\ElasticQuery\Response\Result\Aggregation + string $name, + ): \Spameri\ElasticQuery\Response\Result\Aggregation|null { foreach ($this->aggregations as $aggregation) { if ($aggregation->name() === $name) { @@ -36,7 +39,7 @@ public function getAggregation( } } - return NULL; + return null; } } diff --git a/src/Response/Result/BulkAction.php b/src/Response/Result/BulkAction.php index c3ef77f..3b8d1c8 100644 --- a/src/Response/Result/BulkAction.php +++ b/src/Response/Result/BulkAction.php @@ -1,4 +1,6 @@ -action = $action; - $this->index = $index; - $this->type = $type; - $this->id = $id; - $this->version = $version; - $this->result = $result; - $this->shards = $shards; - $this->status = $status; - $this->seqNo = $seqNo; - $this->primaryTerm = $primaryTerm; } diff --git a/src/Response/Result/BulkActionCollection.php b/src/Response/Result/BulkActionCollection.php index ba8d171..804ec2e 100644 --- a/src/Response/Result/BulkActionCollection.php +++ b/src/Response/Result/BulkActionCollection.php @@ -1,4 +1,6 @@ - */ - private $bulkActions; + private array $bulkActions; public function __construct( - BulkAction ... $bulkActions + BulkAction ...$bulkActions, ) { $this->bulkActions = $bulkActions; diff --git a/src/Response/Result/Hit.php b/src/Response/Result/Hit.php index 4426a6d..2fec53e 100644 --- a/src/Response/Result/Hit.php +++ b/src/Response/Result/Hit.php @@ -1,4 +1,6 @@ -source = $source; - $this->position = $position; - $this->index = $index; - $this->type = $type; - $this->id = $id; - $this->score = $score; - $this->version = $version; } @@ -62,33 +27,149 @@ public function source(): array } - /** - * @phpstan-return mixed - */ public function getValue( - string $key - ) + string $key, + ): mixed + { + $value = $this->getSubValue($key); + + return $value ?? $this->source[$key] ?? null; + } + + + public function getStringValue(string $key): string + { + if ( + \is_string($this->getValue($key)) === true + ) { + return $this->getValue($key); + } + + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(\sprintf('Value for key %s is not string.', $key)); + } + + + public function getStringOrNullValue(string $key): string|null + { + try { + return $this->getStringValue($key); + + } catch (\Spameri\ElasticQuery\Exception\InvalidArgumentException $exception) { + return null; + } + } + + + public function getArrayValue(string $key): array { - $value = $this->source[$key] ?? NULL; - if ($value === NULL) { - return NULL; + if ( + \is_array($this->getValue($key)) === true + ) { + return $this->getValue($key); } - if (\strpos($key, '.') === FALSE) { - return $value; + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(\sprintf('Value for key %s is not array.', $key)); + } + + + public function getArrayOrNullValue(string $key): array|null + { + try { + return $this->getArrayValue($key); + + } catch (\Spameri\ElasticQuery\Exception\InvalidArgumentException $exception) { + return null; } + } + + + public function getBoolValue(string $key): bool + { + if ( + \is_bool($this->getValue($key)) === true + ) { + return $this->getValue($key); + } + + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(\sprintf('Value for key %s is not bool.', $key)); + } - $levels = \explode('.', $key); - foreach ($levels as $subKey) { - $value = $value[$subKey] ?? NULL; + public function getBoolOrNullValue(string $key): bool|null + { + try { + return $this->getBoolValue($key); + + } catch (\Spameri\ElasticQuery\Exception\InvalidArgumentException $exception) { + return null; + } + } + + + public function getIntegerValue(string $key): int + { + if ( + \is_int($this->getValue($key)) === true + ) { + return $this->getValue($key); + } + + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(\sprintf('Value for key %s is not integer.', $key)); + } + + + public function getIntegerOrNullValue(string $key): int|null + { + try { + return $this->getIntegerValue($key); + + } catch (\Spameri\ElasticQuery\Exception\InvalidArgumentException $exception) { + return null; + } + } + + + public function getFloatValue(string $key): float + { + if ( + \is_float($this->getValue($key)) === true + ) { + return $this->getValue($key); + } + + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(\sprintf('Value for key %s is not float.', $key)); + } + + + public function getFloatOrNullValue(string $key): float|null + { + try { + return $this->getFloatValue($key); + + } catch (\Spameri\ElasticQuery\Exception\InvalidArgumentException $exception) { + return null; + } + } + + + public function getSubValue(string $key): mixed + { + if (\str_contains($key, \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldSeparator::FIELD_SEPARATOR) === true) { + $levels = \explode(\Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldSeparator::FIELD_SEPARATOR, $key); + + $value = $this->source[$levels[0]] ?? null; + unset($levels[0]); + + foreach ($levels as $subKey) { + $value = $value[$subKey] ?? null; + } - if ($value === NULL) { - return NULL; + if ($value !== null) { + return $value; } } - return $value; + return null; } diff --git a/src/Response/Result/HitCollection.php b/src/Response/Result/HitCollection.php index 917d304..78ae304 100644 --- a/src/Response/Result/HitCollection.php +++ b/src/Response/Result/HitCollection.php @@ -1,19 +1,22 @@ - */ - private $hits; + private array $hits; public function __construct( - \Spameri\ElasticQuery\Response\Result\Hit ... $hits + \Spameri\ElasticQuery\Response\Result\Hit ...$hits, ) { $this->hits = $hits; @@ -25,4 +28,24 @@ public function getIterator(): \ArrayIterator return new \ArrayIterator($this->hits); } + + public function count(): int + { + return \count($this->hits); + } + + + /** + * @return array + */ + public function ids(): array + { + $ids = []; + foreach ($this->hits as $hit) { + $ids[] = $hit->id(); + } + + return $ids; + } + } diff --git a/src/Response/Result/Version.php b/src/Response/Result/Version.php index 0dded47..4097f60 100644 --- a/src/Response/Result/Version.php +++ b/src/Response/Result/Version.php @@ -1,100 +1,56 @@ -number = $number; $this->id = $this->convertVersionNumber($number); - $this->buildFlavor = $buildFlavor; - $this->buildType = $buildType; - $this->buildHash = $buildHash; - $this->buildDate = $buildDate; - $this->buildSnapshot = $buildSnapshot; - $this->luceneVersion = $luceneVersion; - $this->minimumWireCompatibility = $minimumWireCompatibility; - $this->minimumIndexCompatibility = $minimumIndexCompatibility; } public function convertVersionNumber( - string $number + string $number, ): int { $exploded = \explode('.', $number); @@ -125,13 +81,13 @@ public function id(): int } - public function buildFlavor(): ?string + public function buildFlavor(): string|null { return $this->buildFlavor; } - public function buildType(): ?string + public function buildType(): string|null { return $this->buildType; } @@ -143,7 +99,7 @@ public function buildHash(): string } - public function buildDate(): ?string + public function buildDate(): string|null { return $this->buildDate; } @@ -161,13 +117,13 @@ public function luceneVersion(): string } - public function minimumWireCompatibility(): ?string + public function minimumWireCompatibility(): string|null { return $this->minimumWireCompatibility; } - public function minimumIndexCompatibility(): ?string + public function minimumIndexCompatibility(): string|null { return $this->minimumIndexCompatibility; } diff --git a/src/Response/ResultBulk.php b/src/Response/ResultBulk.php index 3eb15db..0d81ce9 100644 --- a/src/Response/ResultBulk.php +++ b/src/Response/ResultBulk.php @@ -1,4 +1,6 @@ -stats = $stats; - $this->bulkActionCollection = $bulkActionCollection; } @@ -34,7 +23,7 @@ public function stats(): \Spameri\ElasticQuery\Response\Stats public function getFirstAction( - string $id + string $id, ): \Spameri\ElasticQuery\Response\Result\BulkAction { /** @var \Spameri\ElasticQuery\Response\Result\BulkAction $bulkIAction */ @@ -45,7 +34,7 @@ public function getFirstAction( } throw new \Spameri\ElasticQuery\Exception\BulkActionNotFound( - 'Action with id: ' . $id . 'not found.' + 'Action with id: ' . $id . 'not found.', ); } diff --git a/src/Response/ResultInterface.php b/src/Response/ResultInterface.php index 4ab6e02..28fb6a5 100644 --- a/src/Response/ResultInterface.php +++ b/src/Response/ResultInterface.php @@ -1,4 +1,6 @@ -mapHit($elasticSearchResponse, 0), - $this->mapSingleStats($elasticSearchResponse) + $this->mapSingleStats($elasticSearchResponse), ); } public function mapBulkResult( - array $elasticSearchResponse + array $elasticSearchResponse, ): ResultBulk { return new ResultBulk( $this->mapStats($elasticSearchResponse), - $this->mapBulkActions($elasticSearchResponse['items']) + $this->mapBulkActions($elasticSearchResponse['items']), ); } public function mapVersionResults( - array $elasticSearchResponse + array $elasticSearchResponse, ): ResultVersion { return new ResultVersion( - $elasticSearchResponse['name'], - $elasticSearchResponse['cluster_name'], - $elasticSearchResponse['cluster_uuid'], - new \Spameri\ElasticQuery\Response\Result\Version( - $elasticSearchResponse['version']['number'], - $elasticSearchResponse['version']['build_flavor'] ?? NULL, - $elasticSearchResponse['version']['build_type'] ?? NULL, - $elasticSearchResponse['version']['build_hash'], - $elasticSearchResponse['version']['build_date'] ?? NULL, - $elasticSearchResponse['version']['build_snapshot'], - $elasticSearchResponse['version']['lucene_version'], - $elasticSearchResponse['version']['minimum_wire_compatibility_version'] ?? NULL, - $elasticSearchResponse['version']['minimum_index_compatibility_version'] ?? NULL + name: $elasticSearchResponse['name'], + clusterName: $elasticSearchResponse['cluster_name'], + clusterUUID: $elasticSearchResponse['cluster_uuid'], + version: new \Spameri\ElasticQuery\Response\Result\Version( + number: $elasticSearchResponse['version']['number'], + buildHash: $elasticSearchResponse['version']['build_hash'], + buildSnapshot: $elasticSearchResponse['version']['build_snapshot'], + luceneVersion: $elasticSearchResponse['version']['lucene_version'], + buildFlavor: $elasticSearchResponse['version']['build_flavor'] ?? null, + buildType: $elasticSearchResponse['version']['build_type'] ?? null, + buildDate: $elasticSearchResponse['version']['build_date'] ?? null, + minimumWireCompatibility: $elasticSearchResponse['version']['minimum_wire_compatibility_version'] ?? null, + minimumIndexCompatibility: $elasticSearchResponse['version']['minimum_index_compatibility_version'] ?? null, ), - $elasticSearchResponse['tagline'] + tagLine: $elasticSearchResponse['tagline'], ); } public function mapSearchResults( - array $elasticSearchResponse + array $elasticSearchResponse, ): ResultSearch { return new ResultSearch( $this->mapStats($elasticSearchResponse), $this->mapShards($elasticSearchResponse), $this->mapHits($elasticSearchResponse), - $this->mapAggregations($elasticSearchResponse) + $this->mapAggregations($elasticSearchResponse), ); } public function mapHits( - array $elasticSearchResponse + array $elasticSearchResponse, ): \Spameri\ElasticQuery\Response\Result\HitCollection { $hits = []; @@ -99,30 +101,30 @@ public function mapHits( } return new \Spameri\ElasticQuery\Response\Result\HitCollection( - ... $hits + ... $hits, ); } private function mapHit( - array $hit - , int $position + array $hit, + int $position, ): \Spameri\ElasticQuery\Response\Result\Hit { return new \Spameri\ElasticQuery\Response\Result\Hit( $hit['_source'] ?? [], $position, $hit['_index'], - $hit['_type'], + $hit['_type'] ?? '_doc', $hit['_id'], $hit['_score'] ?? 1, - $hit['version'] ?? 0 + $hit['version'] ?? 0, ); } public function mapBulkActions( - array $elasticSearchResponse + array $elasticSearchResponse, ): \Spameri\ElasticQuery\Response\Result\BulkActionCollection { $bulkActions = []; @@ -131,33 +133,33 @@ public function mapBulkActions( } return new \Spameri\ElasticQuery\Response\Result\BulkActionCollection( - ... $bulkActions + ... $bulkActions, ); } public function mapBulkAction( array $bulkAction, - string $actionType + string $actionType, ): \Spameri\ElasticQuery\Response\Result\BulkAction { return new \Spameri\ElasticQuery\Response\Result\BulkAction( $actionType, $bulkAction['_index'], - $bulkAction['_type'], + $bulkAction['_type'] ?? '_doc', $bulkAction['_id'], $bulkAction['_version'], $bulkAction['result'], $this->mapShards($bulkAction), $bulkAction['status'], $bulkAction['_seq_no'], - $bulkAction['_primary_term'] + $bulkAction['_primary_term'], ); } public function mapAggregations( - array $elasticSearchResponse + array $elasticSearchResponse, ): \Spameri\ElasticQuery\Response\Result\AggregationCollection { $aggregationArray = []; @@ -171,24 +173,31 @@ public function mapAggregations( } return new \Spameri\ElasticQuery\Response\Result\AggregationCollection( - ... $aggregationArray + ... $aggregationArray, ); } private function mapAggregation( - string $name - , int $position - , array $aggregationArray + string $name, + int $position, + array $aggregationArray, ): \Spameri\ElasticQuery\Response\Result\Aggregation { $i = 0; $buckets = []; + $hits = []; $aggregations = []; if (isset($aggregationArray['buckets'])) { foreach ($aggregationArray['buckets'] as $bucketPosition => $bucket) { - $buckets[] = $this->mapBucket($bucketPosition, $bucket); + $bucketArray = \is_array($bucket) ? $bucket : ['doc_count' => 0]; + if (\is_string($bucketPosition)) { + $bucketArray['key'] = $bucketArray['key'] ?? $bucketPosition; + $buckets[] = $this->mapBucket(null, $bucketArray); + } else { + $buckets[] = $this->mapBucket($bucketPosition, $bucketArray); + } } } @@ -198,14 +207,22 @@ private function mapAggregation( } } + if (isset($aggregationArray['hits']['hits'])) { + foreach ($aggregationArray['hits']['hits'] as $hitPosition => $hit) { + $hits[] = $this->mapHit($hit, (int) $hitPosition); + } + } + if ( ! isset($aggregationArray['buckets']) && ! isset($aggregationArray[$name]['buckets']) && isset($aggregationArray['value']) ) { - $buckets[] = $this->mapBucket(0, [ + $buckets[] = $this->mapBucket( + 0, [ 'doc_count' => $aggregationArray['value'], - ]); + ], + ); } if (isset($aggregationArray['doc_count']) && $aggregationArray['doc_count'] > 0) { @@ -233,32 +250,40 @@ private function mapAggregation( $name, $position, new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection( - ... $buckets + ... $buckets, ), new \Spameri\ElasticQuery\Response\Result\AggregationCollection( - ... $aggregations - ) + ... $aggregations, + ), + new \Spameri\ElasticQuery\Response\Result\HitCollection( + ... $hits, + ), ); } private function mapBucket( - ?int $bucketPosition - , array $bucketArray + int|null $bucketPosition, + array $bucketArray, ): \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket { + $bucketKey = $bucketArray['key'] ?? $bucketPosition; + if (\is_array($bucketKey)) { + $bucketKey = (string) \json_encode($bucketKey); + } + return new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket( - $bucketArray['key'] ?? (string) $bucketPosition, - $bucketArray['doc_count'], - \is_int($bucketPosition) ? $bucketPosition : NULL, - $bucketArray['from'] ?? NULL, - $bucketArray['to'] ?? NULL + (string) $bucketKey, + (int) $bucketArray['doc_count'], + \is_int($bucketPosition) ? $bucketPosition : null, + $bucketArray['from'] ?? null, + $bucketArray['to'] ?? null, ); } public function mapStats( - array $elasticSearchResponse + array $elasticSearchResponse, ): Stats { $total = 0; @@ -272,31 +297,31 @@ public function mapStats( return new Stats( $elasticSearchResponse['took'], $elasticSearchResponse['timed_out'], - $total + $total, ); } public function mapSingleStats( - array $elasticSearchResponse + array $elasticSearchResponse, ): StatsSingle { return new StatsSingle( $elasticSearchResponse['version'] ?? 0, - $elasticSearchResponse['found'] + $elasticSearchResponse['found'], ); } public function mapShards( - array $elasticSearchResponse + array $elasticSearchResponse, ): Shards { return new Shards( $elasticSearchResponse['_shards']['total'], $elasticSearchResponse['_shards']['successful'], $elasticSearchResponse['_shards']['skipped'] ?? 0, - $elasticSearchResponse['_shards']['failed'] + $elasticSearchResponse['_shards']['failed'], ); } diff --git a/src/Response/ResultSearch.php b/src/Response/ResultSearch.php index 4af87dc..102cdce 100644 --- a/src/Response/ResultSearch.php +++ b/src/Response/ResultSearch.php @@ -1,4 +1,6 @@ -stats = $stats; - $this->shards = $shards; - $this->hitCollection = $hitCollection; - $this->aggregationCollection = $aggregationCollection; } @@ -66,7 +46,7 @@ public function aggregations(): \Spameri\ElasticQuery\Response\Result\Aggregatio public function getHit( - string $id + string $id, ): \Spameri\ElasticQuery\Response\Result\Hit { /** @var \Spameri\ElasticQuery\Response\Result\Hit $hit */ @@ -77,13 +57,13 @@ public function getHit( } throw new \Spameri\ElasticQuery\Exception\HitNotFound( - 'Hit with id: ' . $id . 'not found.' + 'Hit with id: ' . $id . 'not found.', ); } public function getAggregation( - string $name + string $name, ): \Spameri\ElasticQuery\Response\Result\Aggregation { /** @var \Spameri\ElasticQuery\Response\Result\Aggregation $aggregation */ @@ -94,7 +74,7 @@ public function getAggregation( } throw new \Spameri\ElasticQuery\Exception\AggregationNotFound( - 'Aggregation with name: ' . $name . ' has not been found.' + 'Aggregation with name: ' . $name . ' has not been found.', ); } diff --git a/src/Response/ResultSingle.php b/src/Response/ResultSingle.php index de10caf..58ef8bd 100644 --- a/src/Response/ResultSingle.php +++ b/src/Response/ResultSingle.php @@ -1,4 +1,6 @@ -hit = $hit; - $this->stats = $stats; } diff --git a/src/Response/ResultVersion.php b/src/Response/ResultVersion.php index 1e6bb84..55daf1f 100644 --- a/src/Response/ResultVersion.php +++ b/src/Response/ResultVersion.php @@ -1,4 +1,6 @@ -name = $name; - $this->clusterName = $clusterName; - $this->clusterUUID = $clusterUUID; - $this->version = $version; - $this->tagLine = $tagLine; } diff --git a/src/Response/Shards.php b/src/Response/Shards.php index 9b9072c..cf30f21 100644 --- a/src/Response/Shards.php +++ b/src/Response/Shards.php @@ -1,4 +1,6 @@ -total = $total; - $this->successful = $successful; - $this->skipped = $skipped; - $this->failed = $failed; } diff --git a/src/Response/Stats.php b/src/Response/Stats.php index 8ae1b35..a52a44f 100644 --- a/src/Response/Stats.php +++ b/src/Response/Stats.php @@ -1,4 +1,6 @@ -took = $took; - $this->timedOut = $timedOut; - $this->total = $total; } diff --git a/src/Response/StatsSingle.php b/src/Response/StatsSingle.php index ad01811..1a743e8 100644 --- a/src/Response/StatsSingle.php +++ b/src/Response/StatsSingle.php @@ -1,4 +1,6 @@ -version = $version; - $this->found = $found; } diff --git a/src/Script.php b/src/Script.php new file mode 100644 index 0000000..166a67f --- /dev/null +++ b/src/Script.php @@ -0,0 +1,44 @@ + $params + */ + public function __construct( + private string $source, + private string $lang = 'painless', + private array $params = [], + ) + { + } + + + /** + * @return array + */ + public function toArray(): array + { + $array = [ + 'source' => $this->source, + 'lang' => $this->lang, + ]; + + if ($this->params !== []) { + $array['params'] = $this->params; + } + + return $array; + } + +} diff --git a/src/Value/AbstractBoolValue.php b/src/Value/AbstractBoolValue.php index ce8fb24..cd042c9 100644 --- a/src/Value/AbstractBoolValue.php +++ b/src/Value/AbstractBoolValue.php @@ -1,4 +1,6 @@ -value = $value; } diff --git a/src/Value/AbstractFloatValue.php b/src/Value/AbstractFloatValue.php index 4a4881f..2b44647 100644 --- a/src/Value/AbstractFloatValue.php +++ b/src/Value/AbstractFloatValue.php @@ -1,4 +1,6 @@ -value = $value; } diff --git a/src/Value/AbstractNumberValue.php b/src/Value/AbstractNumberValue.php index 95e5706..cdad06c 100644 --- a/src/Value/AbstractNumberValue.php +++ b/src/Value/AbstractNumberValue.php @@ -1,4 +1,6 @@ -value = $value; } diff --git a/src/Value/AbstractStringValue.php b/src/Value/AbstractStringValue.php index d9e43b8..87c8fc8 100644 --- a/src/Value/AbstractStringValue.php +++ b/src/Value/AbstractStringValue.php @@ -1,4 +1,6 @@ -value = $value; } diff --git a/src/Value/ValueInterface.php b/src/Value/ValueInterface.php index 1f2737d..ac3fe21 100644 --- a/src/Value/ValueInterface.php +++ b/src/Value/ValueInterface.php @@ -1,4 +1,6 @@ -createIndex($this->mapping()); + } + + + public function tearDown(): void + { + $this->deleteIndex(); + } + + + /** + * @return array|null Override to provide {settings, mappings} for the index. + */ + protected function mapping(): array|null + { + return null; + } + + + /** + * @param array|null $mapping + */ + protected function createIndex(array|null $mapping = null): void + { + $this->request('PUT', static::INDEX, $mapping); + } + + + protected function deleteIndex(): void + { + $this->request('DELETE', static::INDEX); + } + + + /** + * @param array $body + */ + protected function indexDocument( + array $body, + string|null $id = null, + bool $refresh = true, + ): void + { + $suffix = $id !== null ? '/_doc/' . \rawurlencode($id) : '/_doc'; + if ($refresh) { + $suffix .= '?refresh=true'; + } + + $this->request($id !== null ? 'PUT' : 'POST', static::INDEX . $suffix, $body); + } + + + protected function search( + \Spameri\ElasticQuery\ElasticQuery $query, + ): \Spameri\ElasticQuery\Response\ResultSearch + { + $response = $this->request('POST', static::INDEX . '/_search', $query->toArray()); + + $result = (new \Spameri\ElasticQuery\Response\ResultMapper())->map($response); + \assert($result instanceof \Spameri\ElasticQuery\Response\ResultSearch); + + return $result; + } + + + /** + * @param array|null $body + * @return array + */ + protected function request( + string $method, + string $path, + array|null $body = null, + ): array + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $path); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $method); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + if ($body !== null) { + \curl_setopt($ch, \CURLOPT_POSTFIELDS, (string) \json_encode($body)); + } + + $response = \curl_exec($ch); + + if ($response === false) { + return []; + } + + $decoded = \json_decode((string) $response, true); + + return \is_array($decoded) ? $decoded : []; + } + +} diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/AdjacencyMatrix.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/AdjacencyMatrix.phpt new file mode 100644 index 0000000..2fbe5f1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/AdjacencyMatrix.phpt @@ -0,0 +1,70 @@ + ['properties' => ['status' => ['type' => 'keyword']]]]; + } + + + public function testToArray(): void + { + $matrix = new \Spameri\ElasticQuery\Aggregation\AdjacencyMatrix(); + $matrix->addFilter('active', new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $array = $matrix->toArray(); + + \Tester\Assert::same( + 'active', + $array['adjacency_matrix']['filters']['active']['term']['status']['value'], + ); + } + + + public function testToArrayWithSeparator(): void + { + $matrix = new \Spameri\ElasticQuery\Aggregation\AdjacencyMatrix(separator: '|'); + + \Tester\Assert::same('|', $matrix->toArray()['adjacency_matrix']['separator']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'adjacency_matrix', + (new \Spameri\ElasticQuery\Aggregation\AdjacencyMatrix())->key(), + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['status' => 'active']); + $this->indexDocument(['status' => 'inactive']); + + $matrix = new \Spameri\ElasticQuery\Aggregation\AdjacencyMatrix(); + $matrix->addFilter('group_active', new \Spameri\ElasticQuery\Query\Term('status', 'active')); + $matrix->addFilter('group_inactive', new \Spameri\ElasticQuery\Query\Term('status', 'inactive')); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'matrix', null, $matrix, + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new AdjacencyMatrix())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/AggregationCollection.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/AggregationCollection.phpt new file mode 100644 index 0000000..bf7d0da --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/AggregationCollection.phpt @@ -0,0 +1,197 @@ +count()); + } + + + public function testConstructorWithAggregations(): void + { + $leafCollection1 = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ); + $leafCollection2 = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'max_price', + null, + new \Spameri\ElasticQuery\Aggregation\Max('price'), + ); + + $collection = new \Spameri\ElasticQuery\Aggregation\AggregationCollection(null, $leafCollection1, $leafCollection2); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testAdd(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\AggregationCollection(); + $leafCollection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ); + + $collection->add($leafCollection); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testKey(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\AggregationCollection(); + + \Tester\Assert::same('top-aggs-collection', $collection->key()); + } + + + public function testKeys(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\AggregationCollection(); + $collection->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + )); + $collection->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'max_price', + null, + new \Spameri\ElasticQuery\Aggregation\Max('price'), + )); + + $keys = $collection->keys(); + + \Tester\Assert::contains('avg_price', $keys); + \Tester\Assert::contains('max_price', $keys); + } + + + public function testFilter(): void + { + $filterCollection = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filterCollection->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $collection = new \Spameri\ElasticQuery\Aggregation\AggregationCollection($filterCollection); + + \Tester\Assert::same($filterCollection, $collection->filter()); + } + + + public function testToArray(): void + { + $leafCollection1 = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ); + $leafCollection2 = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'max_price', + null, + new \Spameri\ElasticQuery\Aggregation\Max('price'), + ); + + $collection = new \Spameri\ElasticQuery\Aggregation\AggregationCollection(null, $leafCollection1, $leafCollection2); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['avg_price'])); + \Tester\Assert::true(isset($array['max_price'])); + } + + + public function testCreate(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\AggregationCollection(); + $collection->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ), + ); + $collection->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'max_price', + null, + new \Spameri\ElasticQuery\Aggregation\Max('price'), + ), + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery( + null, + null, + null, + null, + $collection, + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new AggregationCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/AutoDateHistogram.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/AutoDateHistogram.phpt new file mode 100644 index 0000000..5616536 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/AutoDateHistogram.phpt @@ -0,0 +1,52 @@ + ['properties' => ['ts' => ['type' => 'date']]]]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\AutoDateHistogram( + field: 'ts', + buckets: 10, + format: 'yyyy-MM-dd', + minimumInterval: 'day', + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('ts', $array['auto_date_histogram']['field']); + \Tester\Assert::same(10, $array['auto_date_histogram']['buckets']); + \Tester\Assert::same('day', $array['auto_date_histogram']['minimum_interval']); + } + + + public function testCreate(): void + { + $this->indexDocument(['ts' => '2024-01-01']); + $this->indexDocument(['ts' => '2024-06-01']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'by_day', null, new \Spameri\ElasticQuery\Aggregation\AutoDateHistogram('ts', buckets: 5), + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new AutoDateHistogram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Avg.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Avg.phpt new file mode 100644 index 0000000..22dfb91 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Avg.phpt @@ -0,0 +1,54 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + \Tester\Assert::same('price', (new \Spameri\ElasticQuery\Aggregation\Avg('price'))->toArray()['avg']['field']); + } + + + public function testToArrayWithOptions(): void + { + $avg = new \Spameri\ElasticQuery\Aggregation\Avg( + field: 'price', + missing: 0, + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].value"), + format: '00.00', + ); + $array = $avg->toArray(); + \Tester\Assert::same(0, $array['avg']['missing']); + \Tester\Assert::same("doc['price'].value", $array['avg']['script']['source']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_avg', null, new \Spameri\ElasticQuery\Aggregation\Avg('price'), + )); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Avg())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/AvgBucket.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/AvgBucket.phpt new file mode 100644 index 0000000..11fe0e1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/AvgBucket.phpt @@ -0,0 +1,75 @@ +sales', + ); + + $array = $avgBucket->toArray(); + + \Tester\Assert::same('sales_per_month>sales', $array['avg_bucket']['buckets_path']); + } + + + public function testToArrayWithGapPolicy(): void + { + $avgBucket = new \Spameri\ElasticQuery\Aggregation\AvgBucket( + bucketsPath: 'sales_per_month>sales', + gapPolicy: 'skip', + format: '0.00', + ); + + $array = $avgBucket->toArray(); + + \Tester\Assert::same('skip', $array['avg_bucket']['gap_policy']); + \Tester\Assert::same('0.00', $array['avg_bucket']['format']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'avg_bucket', + (new \Spameri\ElasticQuery\Aggregation\AvgBucket('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new AvgBucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/BoxPlot.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/BoxPlot.phpt new file mode 100644 index 0000000..9fe0272 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/BoxPlot.phpt @@ -0,0 +1,109 @@ +toArray(); + + \Tester\Assert::same('load_time', $array['boxplot']['field']); + \Tester\Assert::false(isset($array['boxplot']['compression'])); + } + + + public function testToArrayWithCompression(): void + { + $boxPlot = new \Spameri\ElasticQuery\Aggregation\BoxPlot('load_time', 200); + + $array = $boxPlot->toArray(); + + \Tester\Assert::same(200, $array['boxplot']['compression']); + } + + + public function testKey(): void + { + $boxPlot = new \Spameri\ElasticQuery\Aggregation\BoxPlot('load_time'); + + \Tester\Assert::same('boxplot_load_time', $boxPlot->key()); + } + + + public function testCreate(): void + { + $boxPlot = new \Spameri\ElasticQuery\Aggregation\BoxPlot('load_time'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'load_time_boxplot', + null, + $boxPlot, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new BoxPlot())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/BucketScript.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/BucketScript.phpt new file mode 100644 index 0000000..48904fd --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/BucketScript.phpt @@ -0,0 +1,62 @@ + 't-shirts', 'total' => 'total_sales'], + script: 'params.tShirts / params.total * 100', + ); + + $array = $bs->toArray(); + + \Tester\Assert::same('t-shirts', $array['bucket_script']['buckets_path']['tShirts']); + \Tester\Assert::same('params.tShirts / params.total * 100', $array['bucket_script']['script']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'bucket_script', + (new \Spameri\ElasticQuery\Aggregation\BucketScript(['a' => 'b'], 'params.a'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new BucketScript())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/BucketSelector.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/BucketSelector.phpt new file mode 100644 index 0000000..a9d6fa6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/BucketSelector.phpt @@ -0,0 +1,62 @@ + 'total_sales'], + script: 'params.totalSales > 100', + ); + + $array = $selector->toArray(); + + \Tester\Assert::same('total_sales', $array['bucket_selector']['buckets_path']['totalSales']); + \Tester\Assert::same('params.totalSales > 100', $array['bucket_selector']['script']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'bucket_selector', + (new \Spameri\ElasticQuery\Aggregation\BucketSelector(['a' => 'b'], 'params.a'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new BucketSelector())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/BucketSort.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/BucketSort.phpt new file mode 100644 index 0000000..0074b3b --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/BucketSort.phpt @@ -0,0 +1,64 @@ + ['order' => 'desc']]], + size: 5, + from: 0, + ); + + $array = $bucketSort->toArray(); + + \Tester\Assert::same('desc', $array['bucket_sort']['sort'][0]['total_sales']['order']); + \Tester\Assert::same(5, $array['bucket_sort']['size']); + \Tester\Assert::same(0, $array['bucket_sort']['from']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'bucket_sort', + (new \Spameri\ElasticQuery\Aggregation\BucketSort())->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new BucketSort())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Cardinality.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Cardinality.phpt new file mode 100644 index 0000000..68512da --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Cardinality.phpt @@ -0,0 +1,55 @@ + ['properties' => ['user_id' => ['type' => 'keyword']]]]; + } + + + public function testToArray(): void + { + $cardinality = new \Spameri\ElasticQuery\Aggregation\Cardinality('user_id'); + \Tester\Assert::same('user_id', $cardinality->toArray()['cardinality']['field']); + } + + + public function testToArrayWithOptions(): void + { + $cardinality = new \Spameri\ElasticQuery\Aggregation\Cardinality( + field: 'user_id', + precisionThreshold: 3000, + missing: 'none', + ); + $array = $cardinality->toArray(); + \Tester\Assert::same(3000, $array['cardinality']['precision_threshold']); + \Tester\Assert::same('none', $array['cardinality']['missing']); + } + + + public function testCreate(): void + { + $this->indexDocument(['user_id' => 'a']); + $this->indexDocument(['user_id' => 'b']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'distinct_users', null, new \Spameri\ElasticQuery\Aggregation\Cardinality('user_id'), + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Cardinality())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/CategorizeText.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/CategorizeText.phpt new file mode 100644 index 0000000..5e77bce --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/CategorizeText.phpt @@ -0,0 +1,58 @@ + ['properties' => ['message' => ['type' => 'text']]]]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\CategorizeText( + field: 'message', + maxUniqueTokens: 100, + similarityThreshold: 0.7, + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('message', $array['categorize_text']['field']); + \Tester\Assert::same(100, $array['categorize_text']['max_unique_tokens']); + \Tester\Assert::same(0.7, $array['categorize_text']['similarity_threshold']); + } + + + public function testCreate(): void + { + // categorize_text requires platinum-tier license; skip on basic + $this->indexDocument(['message' => 'Failed to connect to server']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'patterns', null, new \Spameri\ElasticQuery\Aggregation\CategorizeText('message'), + )); + + try { + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } catch (\Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped $e) { + if (\str_contains($e->getMessage(), 'license')) { + \Tester\Environment::skip('categorize_text requires platinum-tier license'); + } + throw $e; + } + } + +} + +(new CategorizeText())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Composite.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Composite.phpt new file mode 100644 index 0000000..538c0b7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Composite.phpt @@ -0,0 +1,96 @@ + [ + 'properties' => [ + 'product' => ['type' => 'keyword'], + 'price' => ['type' => 'long'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $composite = new \Spameri\ElasticQuery\Aggregation\Composite( + key: 'my_composite', + source: new \Spameri\ElasticQuery\Aggregation\Composite\TermsSource( + name: 'product', + field: 'product', + ), + ); + + $array = $composite->toArray(); + + \Tester\Assert::same( + 'product', + $array['composite']['sources'][0]['product']['terms']['field'], + ); + } + + + public function testToArrayWithMultipleSourcesAndAfter(): void + { + $composite = new \Spameri\ElasticQuery\Aggregation\Composite( + key: 'mc', + source: new \Spameri\ElasticQuery\Aggregation\Composite\TermsSource( + name: 'product', + field: 'product', + order: 'asc', + missingBucket: true, + ), + size: 10, + after: ['product' => 'a'], + ); + $composite->addSource(new \Spameri\ElasticQuery\Aggregation\Composite\HistogramSource( + name: 'price', + field: 'price', + interval: 10, + )); + + $array = $composite->toArray(); + + \Tester\Assert::count(2, $array['composite']['sources']); + \Tester\Assert::same(10, $array['composite']['size']); + \Tester\Assert::same(['product' => 'a'], $array['composite']['after']); + } + + + public function testCreate(): void + { + $this->indexDocument(['product' => 'a', 'price' => 10]); + $this->indexDocument(['product' => 'b', 'price' => 20]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'composite_agg', + null, + new \Spameri\ElasticQuery\Aggregation\Composite( + key: 'composite_agg', + source: new \Spameri\ElasticQuery\Aggregation\Composite\TermsSource( + name: 'product', + field: 'product', + ), + ), + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Composite())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/CumulativeCardinality.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/CumulativeCardinality.phpt new file mode 100644 index 0000000..43c87d6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/CumulativeCardinality.phpt @@ -0,0 +1,29 @@ +toArray(); + + \Tester\Assert::same('distinct', $array['cumulative_cardinality']['buckets_path']); + \Tester\Assert::same('0.00', $array['cumulative_cardinality']['format']); + } + +} + +(new CumulativeCardinality())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/CumulativeSum.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/CumulativeSum.phpt new file mode 100644 index 0000000..80eb466 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/CumulativeSum.phpt @@ -0,0 +1,60 @@ +toArray(); + + \Tester\Assert::same('sales', $array['cumulative_sum']['buckets_path']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'cumulative_sum', + (new \Spameri\ElasticQuery\Aggregation\CumulativeSum('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new CumulativeSum())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/DateHistogram.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/DateHistogram.phpt new file mode 100644 index 0000000..3bb5cf3 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/DateHistogram.phpt @@ -0,0 +1,154 @@ +toArray(); + + \Tester\Assert::same('created_at', $array['date_histogram']['field']); + \Tester\Assert::same('month', $array['date_histogram']['calendar_interval']); + \Tester\Assert::false(isset($array['date_histogram']['fixed_interval'])); + } + + + public function testToArrayFixedInterval(): void + { + $dateHistogram = new \Spameri\ElasticQuery\Aggregation\DateHistogram( + field: 'created_at', + fixedInterval: '7d', + format: 'yyyy-MM-dd', + timeZone: 'Europe/Prague', + minDocCount: 1, + ); + + $array = $dateHistogram->toArray(); + + \Tester\Assert::same('7d', $array['date_histogram']['fixed_interval']); + \Tester\Assert::same('yyyy-MM-dd', $array['date_histogram']['format']); + \Tester\Assert::same('Europe/Prague', $array['date_histogram']['time_zone']); + \Tester\Assert::same(1, $array['date_histogram']['min_doc_count']); + } + + + public function testRequiresInterval(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\DateHistogram('created_at'); + }, + \InvalidArgumentException::class, + ); + } + + + public function testRejectsBothIntervals(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\DateHistogram( + field: 'created_at', + calendarInterval: 'month', + fixedInterval: '30d', + ); + }, + \InvalidArgumentException::class, + ); + } + + + public function testKey(): void + { + $dateHistogram = new \Spameri\ElasticQuery\Aggregation\DateHistogram( + field: 'created_at', + calendarInterval: 'month', + ); + + \Tester\Assert::same('date_histogram_created_at', $dateHistogram->key()); + } + + + public function testCreate(): void + { + $dateHistogram = new \Spameri\ElasticQuery\Aggregation\DateHistogram( + field: 'created_at', + calendarInterval: 'month', + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'over_time', + null, + $dateHistogram, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new DateHistogram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/DateRange.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/DateRange.phpt new file mode 100644 index 0000000..02c16e1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/DateRange.phpt @@ -0,0 +1,115 @@ +toArray(); + + \Tester\Assert::same('created_at', $array['date_range']['field']); + \Tester\Assert::same('MM-yyyy', $array['date_range']['format']); + \Tester\Assert::count(2, $array['date_range']['ranges']); + } + + + public function testKey(): void + { + $dateRange = new \Spameri\ElasticQuery\Aggregation\DateRange('created_at'); + + \Tester\Assert::same('date_range_created_at', $dateRange->key()); + } + + + public function testCreate(): void + { + $ranges = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue('past', null, 'now-1M/M'), + new \Spameri\ElasticQuery\Aggregation\RangeValue('recent', 'now-1M/M', null), + ); + $dateRange = new \Spameri\ElasticQuery\Aggregation\DateRange( + field: 'created_at', + ranges: $ranges, + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'when', + null, + $dateRange, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new DateRange())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Derivative.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Derivative.phpt new file mode 100644 index 0000000..5c6ff9a --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Derivative.phpt @@ -0,0 +1,62 @@ +toArray(); + + \Tester\Assert::same('sales', $array['derivative']['buckets_path']); + \Tester\Assert::same('day', $array['derivative']['unit']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'derivative', + (new \Spameri\ElasticQuery\Aggregation\Derivative('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Derivative())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/DiversifiedSampler.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/DiversifiedSampler.phpt new file mode 100644 index 0000000..756e9c1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/DiversifiedSampler.phpt @@ -0,0 +1,108 @@ +toArray(); + + \Tester\Assert::same('author.keyword', $array['diversified_sampler']['field']); + \Tester\Assert::same(200, $array['diversified_sampler']['shard_size']); + \Tester\Assert::same(3, $array['diversified_sampler']['max_docs_per_value']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'diversified_sampler', + (new \Spameri\ElasticQuery\Aggregation\DiversifiedSampler('author.keyword'))->key(), + ); + } + + + public function testCreate(): void + { + $sampler = new \Spameri\ElasticQuery\Aggregation\DiversifiedSampler( + field: 'author.keyword', + shardSize: 100, + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'sample', + null, + $sampler, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new DiversifiedSampler())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/ExtendedStats.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/ExtendedStats.phpt new file mode 100644 index 0000000..9df053c --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/ExtendedStats.phpt @@ -0,0 +1,110 @@ +toArray(); + + \Tester\Assert::true(isset($array['extended_stats']['field'])); + \Tester\Assert::same('price', $array['extended_stats']['field']); + \Tester\Assert::false(isset($array['extended_stats']['sigma'])); + } + + + public function testToArrayWithSigma(): void + { + $extendedStats = new \Spameri\ElasticQuery\Aggregation\ExtendedStats('price', 3.0); + + $array = $extendedStats->toArray(); + + \Tester\Assert::same(3.0, $array['extended_stats']['sigma']); + } + + + public function testKey(): void + { + $extendedStats = new \Spameri\ElasticQuery\Aggregation\ExtendedStats('price'); + + \Tester\Assert::same('extended_stats_price', $extendedStats->key()); + } + + + public function testCreate(): void + { + $extendedStats = new \Spameri\ElasticQuery\Aggregation\ExtendedStats('price'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_extended_stats', + null, + $extendedStats, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new ExtendedStats())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/ExtendedStatsBucket.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/ExtendedStatsBucket.phpt new file mode 100644 index 0000000..834753a --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/ExtendedStatsBucket.phpt @@ -0,0 +1,32 @@ +sales', + sigma: 2.0, + gapPolicy: 'skip', + format: '0.00', + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('sales_per_month>sales', $array['extended_stats_bucket']['buckets_path']); + \Tester\Assert::same(2.0, $array['extended_stats_bucket']['sigma']); + \Tester\Assert::same('skip', $array['extended_stats_bucket']['gap_policy']); + } + +} + +(new ExtendedStatsBucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Filter.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Filter.phpt new file mode 100644 index 0000000..a18984f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Filter.phpt @@ -0,0 +1,69 @@ + ['properties' => ['status' => ['type' => 'keyword']]]]; + } + + + public function testToArrayEmpty(): void + { + $filter = new \Spameri\ElasticQuery\Aggregation\Filter(); + + $array = $filter->toArray(); + + \Tester\Assert::true(isset($array['filter'])); + } + + + public function testToArrayWithQuery(): void + { + $filter = new \Spameri\ElasticQuery\Aggregation\Filter( + filter: new \Spameri\ElasticQuery\Query\Term('status', 'active'), + ); + + $array = $filter->toArray(); + + \Tester\Assert::same('active', $array['filter']['term']['status']['value']); + } + + + public function testKey(): void + { + \Tester\Assert::same('filter', (new \Spameri\ElasticQuery\Aggregation\Filter())->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['status' => 'active']); + $this->indexDocument(['status' => 'inactive']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'active_only', + null, + new \Spameri\ElasticQuery\Aggregation\Filter( + filter: new \Spameri\ElasticQuery\Query\Term('status', 'active'), + ), + ), + ); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Filter())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Filters.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Filters.phpt new file mode 100644 index 0000000..da29e63 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Filters.phpt @@ -0,0 +1,67 @@ + ['properties' => ['status' => ['type' => 'keyword']]]]; + } + + + public function testToArray(): void + { + $filters = new \Spameri\ElasticQuery\Aggregation\Filters(); + $filters->addFilter('a', new \Spameri\ElasticQuery\Query\Term('status', 'a')); + $filters->addFilter('b', new \Spameri\ElasticQuery\Query\Term('status', 'b')); + + $array = $filters->toArray(); + + \Tester\Assert::same('a', $array['filters']['filters']['a']['term']['status']['value']); + \Tester\Assert::same('b', $array['filters']['filters']['b']['term']['status']['value']); + } + + + public function testOtherBucket(): void + { + $filters = new \Spameri\ElasticQuery\Aggregation\Filters( + otherBucket: true, + otherBucketKey: 'rest', + ); + $filters->addFilter('a', new \Spameri\ElasticQuery\Query\Term('status', 'a')); + + $array = $filters->toArray(); + + \Tester\Assert::true($array['filters']['other_bucket']); + \Tester\Assert::same('rest', $array['filters']['other_bucket_key']); + } + + + public function testCreate(): void + { + $this->indexDocument(['status' => 'active']); + $this->indexDocument(['status' => 'inactive']); + + $filters = new \Spameri\ElasticQuery\Aggregation\Filters(); + $filters->addFilter('active', new \Spameri\ElasticQuery\Query\Term('status', 'active')); + $filters->addFilter('inactive', new \Spameri\ElasticQuery\Query\Term('status', 'inactive')); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'by_status', null, $filters, + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Filters())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/FrequentItemSets.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/FrequentItemSets.phpt new file mode 100644 index 0000000..3be9cf8 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/FrequentItemSets.phpt @@ -0,0 +1,54 @@ + [ + 'properties' => [ + 'category' => ['type' => 'keyword'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\FrequentItemSets( + fields: [['field' => 'category']], + minimumSupport: 0.1, + minimumSetSize: 2, + size: 10, + ); + + $array = $agg->toArray(); + + \Tester\Assert::same(0.1, $array['frequent_item_sets']['minimum_support']); + \Tester\Assert::same(2, $array['frequent_item_sets']['minimum_set_size']); + } + + + public function testRequiresFields(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\FrequentItemSets([]); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + +} + +(new FrequentItemSets())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/GeoBounds.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/GeoBounds.phpt new file mode 100644 index 0000000..ee51214 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/GeoBounds.phpt @@ -0,0 +1,109 @@ +toArray(); + + \Tester\Assert::same('location', $array['geo_bounds']['field']); + \Tester\Assert::false(isset($array['geo_bounds']['wrap_longitude'])); + } + + + public function testToArrayWithoutWrap(): void + { + $geoBounds = new \Spameri\ElasticQuery\Aggregation\GeoBounds('location', false); + + $array = $geoBounds->toArray(); + + \Tester\Assert::false($array['geo_bounds']['wrap_longitude']); + } + + + public function testKey(): void + { + $geoBounds = new \Spameri\ElasticQuery\Aggregation\GeoBounds('location'); + + \Tester\Assert::same('geo_bounds_location', $geoBounds->key()); + } + + + public function testCreate(): void + { + $geoBounds = new \Spameri\ElasticQuery\Aggregation\GeoBounds('location'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'bounds', + null, + $geoBounds, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new GeoBounds())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/GeoCentroid.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/GeoCentroid.phpt new file mode 100644 index 0000000..f0e0136 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/GeoCentroid.phpt @@ -0,0 +1,98 @@ +toArray(); + + \Tester\Assert::same('location', $array['geo_centroid']['field']); + } + + + public function testKey(): void + { + $geoCentroid = new \Spameri\ElasticQuery\Aggregation\GeoCentroid('location'); + + \Tester\Assert::same('geo_centroid_location', $geoCentroid->key()); + } + + + public function testCreate(): void + { + $geoCentroid = new \Spameri\ElasticQuery\Aggregation\GeoCentroid('location'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'centroid', + null, + $geoCentroid, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new GeoCentroid())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/GeoDistance.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/GeoDistance.phpt new file mode 100644 index 0000000..5a34b0f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/GeoDistance.phpt @@ -0,0 +1,125 @@ +toArray(); + + \Tester\Assert::same('location', $array['geo_distance']['field']); + \Tester\Assert::same(50.0, $array['geo_distance']['origin']['lat']); + \Tester\Assert::same(14.4, $array['geo_distance']['origin']['lon']); + \Tester\Assert::same('km', $array['geo_distance']['unit']); + \Tester\Assert::count(2, $array['geo_distance']['ranges']); + } + + + public function testKey(): void + { + $geoDistance = new \Spameri\ElasticQuery\Aggregation\GeoDistance( + field: 'location', + lat: 50.0, + lon: 14.4, + ); + + \Tester\Assert::same('geo_distance_location', $geoDistance->key()); + } + + + public function testCreate(): void + { + $ranges = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue('near', null, 100), + ); + $geoDistance = new \Spameri\ElasticQuery\Aggregation\GeoDistance( + field: 'location', + lat: 50.0, + lon: 14.4, + ranges: $ranges, + unit: 'km', + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'rings', + null, + $geoDistance, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new GeoDistance())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/GeoHashGrid.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/GeoHashGrid.phpt new file mode 100644 index 0000000..5cb9794 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/GeoHashGrid.phpt @@ -0,0 +1,107 @@ +toArray(); + + \Tester\Assert::same('location', $array['geohash_grid']['field']); + \Tester\Assert::same(5, $array['geohash_grid']['precision']); + \Tester\Assert::same(100, $array['geohash_grid']['size']); + } + + + public function testKey(): void + { + $grid = new \Spameri\ElasticQuery\Aggregation\GeoHashGrid('location'); + + \Tester\Assert::same('geohash_grid_location', $grid->key()); + } + + + public function testCreate(): void + { + $grid = new \Spameri\ElasticQuery\Aggregation\GeoHashGrid( + field: 'location', + precision: 5, + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'grid', + null, + $grid, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new GeoHashGrid())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/GeoLine.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/GeoLine.phpt new file mode 100644 index 0000000..7e55cc7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/GeoLine.phpt @@ -0,0 +1,70 @@ + [ + 'properties' => [ + 'location' => ['type' => 'geo_point'], + 'ts' => ['type' => 'date'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\GeoLine( + pointField: 'location', + sortField: 'ts', + sortOrder: 'desc', + includeSort: true, + size: 100, + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('location', $array['geo_line']['point']['field']); + \Tester\Assert::same('ts', $array['geo_line']['sort']['field']); + \Tester\Assert::same('desc', $array['geo_line']['sort_order']); + \Tester\Assert::true($array['geo_line']['include_sort']); + \Tester\Assert::same(100, $array['geo_line']['size']); + } + + + public function testCreate(): void + { + // geo_line requires gold-tier license; skip on basic + $this->indexDocument(['location' => ['lat' => 50, 'lon' => 14], 'ts' => '2024-01-01']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'path', null, new \Spameri\ElasticQuery\Aggregation\GeoLine('location', 'ts'), + )); + + try { + $result = $this->search($elasticQuery); + \Tester\Assert::same(1, $result->stats()->total()); + } catch (\Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped $e) { + if (\str_contains($e->getMessage(), 'license')) { + \Tester\Environment::skip('geo_line aggregation requires gold-tier license'); + } + throw $e; + } + } + +} + +(new GeoLine())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/GeoTileGrid.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/GeoTileGrid.phpt new file mode 100644 index 0000000..266eb38 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/GeoTileGrid.phpt @@ -0,0 +1,105 @@ +toArray(); + + \Tester\Assert::same('location', $array['geotile_grid']['field']); + \Tester\Assert::same(8, $array['geotile_grid']['precision']); + } + + + public function testKey(): void + { + $grid = new \Spameri\ElasticQuery\Aggregation\GeoTileGrid('location'); + + \Tester\Assert::same('geotile_grid_location', $grid->key()); + } + + + public function testCreate(): void + { + $grid = new \Spameri\ElasticQuery\Aggregation\GeoTileGrid( + field: 'location', + precision: 8, + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'tiles', + null, + $grid, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new GeoTileGrid())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/GlobalAggregation.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/GlobalAggregation.phpt new file mode 100644 index 0000000..26e5be1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/GlobalAggregation.phpt @@ -0,0 +1,98 @@ +toArray(); + + \Tester\Assert::true(isset($array['global'])); + \Tester\Assert::type(\stdClass::class, $array['global']); + } + + + public function testKey(): void + { + \Tester\Assert::same('global', (new \Spameri\ElasticQuery\Aggregation\GlobalAggregation())->key()); + \Tester\Assert::same('all_docs', (new \Spameri\ElasticQuery\Aggregation\GlobalAggregation('all_docs'))->key()); + } + + + public function testCreate(): void + { + $global = new \Spameri\ElasticQuery\Aggregation\GlobalAggregation(); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'all', + null, + $global, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new GlobalAggregation())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Histogram.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Histogram.phpt new file mode 100644 index 0000000..2065824 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Histogram.phpt @@ -0,0 +1,100 @@ +toArray(); + + \Tester\Assert::true(isset($array['histogram']['field'])); + \Tester\Assert::same('price', $array['histogram']['field']); + \Tester\Assert::same(50, $array['histogram']['interval']); + } + + + public function testKey(): void + { + $histogram = new \Spameri\ElasticQuery\Aggregation\Histogram('price', 50); + + \Tester\Assert::same('price', $histogram->key()); + } + + + public function testCreate(): void + { + $histogram = new \Spameri\ElasticQuery\Aggregation\Histogram('price', 100); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_histogram', + null, + $histogram, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Histogram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Inference.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Inference.phpt new file mode 100644 index 0000000..5236b47 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Inference.phpt @@ -0,0 +1,30 @@ + 'avg_value'], + inferenceConfig: ['regression' => ['results_field' => 'prediction']], + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('my_model', $array['inference']['model_id']); + \Tester\Assert::same(['feature' => 'avg_value'], $array['inference']['buckets_path']); + } + +} + +(new Inference())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/IpPrefix.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/IpPrefix.phpt new file mode 100644 index 0000000..9bfa249 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/IpPrefix.phpt @@ -0,0 +1,55 @@ + ['properties' => ['ip' => ['type' => 'ip']]]]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\IpPrefix( + field: 'ip', + prefixLength: 16, + appendPrefixLength: true, + keyed: false, + minDocCount: 1, + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('ip', $array['ip_prefix']['field']); + \Tester\Assert::same(16, $array['ip_prefix']['prefix_length']); + \Tester\Assert::true($array['ip_prefix']['append_prefix_length']); + \Tester\Assert::same(1, $array['ip_prefix']['min_doc_count']); + } + + + public function testCreate(): void + { + $this->indexDocument(['ip' => '10.0.0.1']); + $this->indexDocument(['ip' => '10.0.5.20']); + $this->indexDocument(['ip' => '192.168.0.1']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'by_prefix', null, new \Spameri\ElasticQuery\Aggregation\IpPrefix('ip', 8), + )); + + \Tester\Assert::same(3, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new IpPrefix())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/IpRange.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/IpRange.phpt new file mode 100644 index 0000000..f4b6e70 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/IpRange.phpt @@ -0,0 +1,85 @@ + ['properties' => ['ip' => ['type' => 'ip']]]]; + } + + + public function testToArrayFromTo(): void + { + $ipRange = new \Spameri\ElasticQuery\Aggregation\IpRange( + field: 'ip', + ranges: [ + new \Spameri\ElasticQuery\Aggregation\IpRange\IpRangeValue('low', null, '10.0.0.5'), + new \Spameri\ElasticQuery\Aggregation\IpRange\IpRangeValue('high', '10.0.0.5', null), + ], + ); + + $array = $ipRange->toArray(); + + \Tester\Assert::same('ip', $array['ip_range']['field']); + \Tester\Assert::count(2, $array['ip_range']['ranges']); + } + + + public function testToArrayCidr(): void + { + $ipRange = new \Spameri\ElasticQuery\Aggregation\IpRange( + field: 'ip', + ranges: [ + new \Spameri\ElasticQuery\Aggregation\IpRange\IpRangeValue('private', mask: '10.0.0.0/8'), + ], + ); + + $array = $ipRange->toArray(); + + \Tester\Assert::same('10.0.0.0/8', $array['ip_range']['ranges'][0]['mask']); + } + + + public function testRequiresFromToOrMask(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\IpRange\IpRangeValue('k'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['ip' => '10.0.0.1']); + $this->indexDocument(['ip' => '192.168.0.1']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'ip_buckets', + null, + new \Spameri\ElasticQuery\Aggregation\IpRange( + field: 'ip', + ranges: [ + new \Spameri\ElasticQuery\Aggregation\IpRange\IpRangeValue('private', mask: '10.0.0.0/8'), + ], + ), + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new IpRange())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/LeafAggregationCollection.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/LeafAggregationCollection.phpt new file mode 100644 index 0000000..1cbe748 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/LeafAggregationCollection.phpt @@ -0,0 +1,199 @@ +key()); + } + + + public function testFilter(): void + { + $filterCollection = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filterCollection->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $collection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'my_aggregation', + $filterCollection, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ); + + \Tester\Assert::same($filterCollection, $collection->filter()); + } + + + public function testAddAggregation(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'my_aggregation', + null, + new \Spameri\ElasticQuery\Aggregation\Term('category'), + ); + + $collection->addAggregation(new \Spameri\ElasticQuery\Aggregation\Avg('price')); + + $items = \iterator_to_array($collection); + \Tester\Assert::count(2, $items); + } + + + public function testIterate(): void + { + $avg = new \Spameri\ElasticQuery\Aggregation\Avg('price'); + $max = new \Spameri\ElasticQuery\Aggregation\Max('price'); + + $collection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'my_aggregation', + null, + $avg, + $max, + ); + + $count = 0; + foreach ($collection as $aggregation) { + \Tester\Assert::type(\Spameri\ElasticQuery\Aggregation\LeafAggregationInterface::class, $aggregation); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testToArrayWithoutFilter(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'avg_price', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['avg_price'])); + \Tester\Assert::true(isset($array['avg_price']['avg'])); + \Tester\Assert::same('price', $array['avg_price']['avg']['field']); + } + + + public function testToArrayWithFilter(): void + { + $filterCollection = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filterCollection->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $collection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'filtered_avg', + $filterCollection, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['filtered_avg']['filter'])); + \Tester\Assert::true(isset($array['filtered_avg']['aggregations'])); + } + + + public function testToArrayNestedCollections(): void + { + $nestedCollection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'nested_avg', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + ); + + $collection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'parent_agg', + null, + new \Spameri\ElasticQuery\Aggregation\Term('category'), + ); + $collection->addAggregation($nestedCollection); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['parent_agg']['aggregations']['nested_avg'])); + } + + + public function testCreate(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'stats', + null, + new \Spameri\ElasticQuery\Aggregation\Avg('price'), + new \Spameri\ElasticQuery\Aggregation\Min('price'), + new \Spameri\ElasticQuery\Aggregation\Max('price'), + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add($collection); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new LeafAggregationCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/MatrixStats.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/MatrixStats.phpt new file mode 100644 index 0000000..36382aa --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/MatrixStats.phpt @@ -0,0 +1,68 @@ + [ + 'properties' => [ + 'income' => ['type' => 'long'], + 'expense' => ['type' => 'long'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\MatrixStats( + fields: ['income', 'expense'], + missing: ['income' => 0], + mode: 'avg', + ); + + $array = $agg->toArray(); + + \Tester\Assert::same(['income', 'expense'], $array['matrix_stats']['fields']); + \Tester\Assert::same('avg', $array['matrix_stats']['mode']); + } + + + public function testRequiresFields(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\MatrixStats([]); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['income' => 100, 'expense' => 50]); + $this->indexDocument(['income' => 200, 'expense' => 80]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'corr', null, new \Spameri\ElasticQuery\Aggregation\MatrixStats(['income', 'expense']), + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new MatrixStats())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Max.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Max.phpt new file mode 100644 index 0000000..df86ce2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Max.phpt @@ -0,0 +1,55 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + \Tester\Assert::same('price', (new \Spameri\ElasticQuery\Aggregation\Max('price'))->toArray()['max']['field']); + } + + + public function testToArrayWithOptions(): void + { + $max = new \Spameri\ElasticQuery\Aggregation\Max( + field: 'price', + missing: 0, + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].value"), + format: '00.00', + ); + $array = $max->toArray(); + \Tester\Assert::same(0, $array['max']['missing']); + \Tester\Assert::same("doc['price'].value", $array['max']['script']['source']); + \Tester\Assert::same('00.00', $array['max']['format']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_max', null, new \Spameri\ElasticQuery\Aggregation\Max('price'), + )); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Max())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/MaxBucket.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/MaxBucket.phpt new file mode 100644 index 0000000..afb3473 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/MaxBucket.phpt @@ -0,0 +1,60 @@ +sales', + ); + + $array = $maxBucket->toArray(); + + \Tester\Assert::same('sales_per_month>sales', $array['max_bucket']['buckets_path']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'max_bucket', + (new \Spameri\ElasticQuery\Aggregation\MaxBucket('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MaxBucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/MedianAbsoluteDeviation.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/MedianAbsoluteDeviation.phpt new file mode 100644 index 0000000..05a5575 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/MedianAbsoluteDeviation.phpt @@ -0,0 +1,109 @@ +toArray(); + + \Tester\Assert::same('rating', $array['median_absolute_deviation']['field']); + \Tester\Assert::false(isset($array['median_absolute_deviation']['compression'])); + } + + + public function testToArrayWithCompression(): void + { + $mad = new \Spameri\ElasticQuery\Aggregation\MedianAbsoluteDeviation('rating', 200); + + $array = $mad->toArray(); + + \Tester\Assert::same(200, $array['median_absolute_deviation']['compression']); + } + + + public function testKey(): void + { + $mad = new \Spameri\ElasticQuery\Aggregation\MedianAbsoluteDeviation('rating'); + + \Tester\Assert::same('median_absolute_deviation_rating', $mad->key()); + } + + + public function testCreate(): void + { + $mad = new \Spameri\ElasticQuery\Aggregation\MedianAbsoluteDeviation('rating'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'rating_mad', + null, + $mad, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MedianAbsoluteDeviation())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Min.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Min.phpt new file mode 100644 index 0000000..baede26 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Min.phpt @@ -0,0 +1,89 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + $min = new \Spameri\ElasticQuery\Aggregation\Min('price'); + + \Tester\Assert::same('price', $min->toArray()['min']['field']); + } + + + public function testToArrayWithOptions(): void + { + $min = new \Spameri\ElasticQuery\Aggregation\Min( + field: 'price', + missing: 0, + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].value * 2", lang: 'painless'), + format: '00.00', + ); + + $array = $min->toArray(); + + \Tester\Assert::same(0, $array['min']['missing']); + \Tester\Assert::same("doc['price'].value * 2", $array['min']['script']['source']); + \Tester\Assert::same('00.00', $array['min']['format']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + $this->indexDocument(['price' => 200]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_min', + null, + new \Spameri\ElasticQuery\Aggregation\Min('price'), + ), + ); + + $result = $this->search($elasticQuery); + + \Tester\Assert::same(2, $result->stats()->total()); + } + + + public function testCreateWithOptions(): void + { + $this->indexDocument(['price' => 100]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_min', + null, + new \Spameri\ElasticQuery\Aggregation\Min( + field: 'price', + missing: 0, + format: '00.00', + ), + ), + ); + + $result = $this->search($elasticQuery); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Min())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/MinBucket.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/MinBucket.phpt new file mode 100644 index 0000000..f9036b7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/MinBucket.phpt @@ -0,0 +1,60 @@ +sales', + ); + + $array = $minBucket->toArray(); + + \Tester\Assert::same('sales_per_month>sales', $array['min_bucket']['buckets_path']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'min_bucket', + (new \Spameri\ElasticQuery\Aggregation\MinBucket('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MinBucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Missing.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Missing.phpt new file mode 100644 index 0000000..92c846a --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Missing.phpt @@ -0,0 +1,98 @@ +toArray(); + + \Tester\Assert::same('price', $array['missing']['field']); + } + + + public function testKey(): void + { + $missing = new \Spameri\ElasticQuery\Aggregation\Missing('price'); + + \Tester\Assert::same('missing_price', $missing->key()); + } + + + public function testCreate(): void + { + $missing = new \Spameri\ElasticQuery\Aggregation\Missing('price'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'no_price', + null, + $missing, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Missing())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/MovingFunction.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/MovingFunction.phpt new file mode 100644 index 0000000..ce6736b --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/MovingFunction.phpt @@ -0,0 +1,64 @@ +toArray(); + + \Tester\Assert::same('sales', $array['moving_fn']['buckets_path']); + \Tester\Assert::same(5, $array['moving_fn']['window']); + \Tester\Assert::same('MovingFunctions.unweightedAvg(values)', $array['moving_fn']['script']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'moving_fn', + (new \Spameri\ElasticQuery\Aggregation\MovingFunction('p', 5, 'script'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MovingFunction())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/MultiTerms.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/MultiTerms.phpt new file mode 100644 index 0000000..ffc4350 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/MultiTerms.phpt @@ -0,0 +1,106 @@ +toArray(); + + \Tester\Assert::count(2, $array['multi_terms']['terms']); + \Tester\Assert::same('brand', $array['multi_terms']['terms'][0]['field']); + \Tester\Assert::same('color', $array['multi_terms']['terms'][1]['field']); + \Tester\Assert::same(10, $array['multi_terms']['size']); + } + + + public function testKey(): void + { + $multiTerms = new \Spameri\ElasticQuery\Aggregation\MultiTerms(['brand', 'color']); + + \Tester\Assert::same('multi_terms', $multiTerms->key()); + } + + + public function testCreate(): void + { + $multiTerms = new \Spameri\ElasticQuery\Aggregation\MultiTerms( + terms: ['brand', 'color'], + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'combos', + null, + $multiTerms, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MultiTerms())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Nested.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Nested.phpt new file mode 100644 index 0000000..935c9bf --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Nested.phpt @@ -0,0 +1,99 @@ +toArray(); + + \Tester\Assert::true(isset($array['nested']['path'])); + \Tester\Assert::same('products', $array['nested']['path']); + } + + + public function testKey(): void + { + $nested = new \Spameri\ElasticQuery\Aggregation\Nested('products'); + + \Tester\Assert::same('nested_products', $nested->key()); + } + + + public function testCreate(): void + { + $nested = new \Spameri\ElasticQuery\Aggregation\Nested('products'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'nested_products', + null, + $nested, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Nested())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Normalize.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Normalize.phpt new file mode 100644 index 0000000..d30c037 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Normalize.phpt @@ -0,0 +1,62 @@ +toArray(); + + \Tester\Assert::same('sales', $array['normalize']['buckets_path']); + \Tester\Assert::same('percent_of_sum', $array['normalize']['method']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'normalize', + (new \Spameri\ElasticQuery\Aggregation\Normalize('p', 'rescale_0_1'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Normalize())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/PercentileRanks.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/PercentileRanks.phpt new file mode 100644 index 0000000..86c5499 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/PercentileRanks.phpt @@ -0,0 +1,108 @@ +toArray(); + + \Tester\Assert::same('load_time', $array['percentile_ranks']['field']); + \Tester\Assert::same([500, 600], $array['percentile_ranks']['values']); + } + + + public function testKey(): void + { + $percentileRanks = new \Spameri\ElasticQuery\Aggregation\PercentileRanks( + 'load_time', + [500], + ); + + \Tester\Assert::same('percentile_ranks_load_time', $percentileRanks->key()); + } + + + public function testCreate(): void + { + $percentileRanks = new \Spameri\ElasticQuery\Aggregation\PercentileRanks( + 'load_time', + [500, 600], + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'load_time_ranks', + null, + $percentileRanks, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new PercentileRanks())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Percentiles.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Percentiles.phpt new file mode 100644 index 0000000..de6b857 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Percentiles.phpt @@ -0,0 +1,126 @@ +toArray(); + + \Tester\Assert::true(isset($array['percentiles']['field'])); + \Tester\Assert::same('load_time', $array['percentiles']['field']); + } + + + public function testToArrayWithPercents(): void + { + $percentiles = new \Spameri\ElasticQuery\Aggregation\Percentiles( + 'load_time', + [50, 95, 99], + ); + + $array = $percentiles->toArray(); + + \Tester\Assert::same([50, 95, 99], $array['percentiles']['percents']); + } + + + public function testToArrayUnkeyed(): void + { + $percentiles = new \Spameri\ElasticQuery\Aggregation\Percentiles( + 'load_time', + [], + false, + ); + + $array = $percentiles->toArray(); + + \Tester\Assert::false($array['percentiles']['keyed']); + } + + + public function testKey(): void + { + $percentiles = new \Spameri\ElasticQuery\Aggregation\Percentiles('load_time'); + + \Tester\Assert::same('percentiles_load_time', $percentiles->key()); + } + + + public function testCreate(): void + { + $percentiles = new \Spameri\ElasticQuery\Aggregation\Percentiles('load_time'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'load_time_percentiles', + null, + $percentiles, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Percentiles())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/PercentilesBucket.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/PercentilesBucket.phpt new file mode 100644 index 0000000..f320daa --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/PercentilesBucket.phpt @@ -0,0 +1,62 @@ +sales', + percents: [50, 95, 99], + ); + + $array = $pb->toArray(); + + \Tester\Assert::same('sales_per_month>sales', $array['percentiles_bucket']['buckets_path']); + \Tester\Assert::same([50, 95, 99], $array['percentiles_bucket']['percents']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'percentiles_bucket', + (new \Spameri\ElasticQuery\Aggregation\PercentilesBucket('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new PercentilesBucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/RandomSampler.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/RandomSampler.phpt new file mode 100644 index 0000000..63e3f11 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/RandomSampler.phpt @@ -0,0 +1,29 @@ +toArray(); + + \Tester\Assert::same(0.1, $array['random_sampler']['probability']); + \Tester\Assert::same(42, $array['random_sampler']['seed']); + } + +} + +(new RandomSampler())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Range.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Range.phpt new file mode 100644 index 0000000..65db983 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Range.phpt @@ -0,0 +1,145 @@ +toArray(); + + \Tester\Assert::true(isset($array['range']['field'])); + \Tester\Assert::same('price', $array['range']['field']); + \Tester\Assert::false(isset($array['range']['keyed'])); + } + + + public function testToArrayWithKeyed(): void + { + $range = new \Spameri\ElasticQuery\Aggregation\Range('price', true); + + $array = $range->toArray(); + + \Tester\Assert::same('price', $array['range']['field']); + \Tester\Assert::true($array['range']['keyed']); + } + + + public function testToArrayWithRanges(): void + { + $rangeCollection = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue('cheap', null, 50), + new \Spameri\ElasticQuery\Aggregation\RangeValue('medium', 50, 100), + new \Spameri\ElasticQuery\Aggregation\RangeValue('expensive', 100, null), + ); + $range = new \Spameri\ElasticQuery\Aggregation\Range('price', false, $rangeCollection); + + $array = $range->toArray(); + + \Tester\Assert::same('price', $array['range']['field']); + \Tester\Assert::count(3, $array['range']['ranges']); + \Tester\Assert::same('cheap', $array['range']['ranges'][0]['key']); + \Tester\Assert::same('medium', $array['range']['ranges'][1]['key']); + \Tester\Assert::same('expensive', $array['range']['ranges'][2]['key']); + } + + + public function testKey(): void + { + $range = new \Spameri\ElasticQuery\Aggregation\Range('price'); + + \Tester\Assert::same('price', $range->key()); + } + + + public function testRanges(): void + { + $range = new \Spameri\ElasticQuery\Aggregation\Range('price'); + $range->ranges()->add(new \Spameri\ElasticQuery\Aggregation\RangeValue('low', null, 50)); + + $array = $range->toArray(); + + \Tester\Assert::count(1, $array['range']['ranges']); + } + + + public function testCreate(): void + { + $rangeCollection = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection( + new \Spameri\ElasticQuery\Aggregation\RangeValue('cheap', null, 50), + new \Spameri\ElasticQuery\Aggregation\RangeValue('expensive', 50, null), + ); + $range = new \Spameri\ElasticQuery\Aggregation\Range('price', false, $rangeCollection); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_ranges', + null, + $range, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Range())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/RangeValue.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/RangeValue.phpt new file mode 100644 index 0000000..7c01572 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/RangeValue.phpt @@ -0,0 +1,100 @@ +toArray(); + + \Tester\Assert::same('test', $array['key']); + \Tester\Assert::same(10, $array['from']); + \Tester\Assert::same(21, $array['to']); + } + + + public function testToArrayWithNullFrom(): void + { + $rangeValue = new \Spameri\ElasticQuery\Aggregation\RangeValue('cheap', null, 50); + + $array = $rangeValue->toArray(); + + \Tester\Assert::same('cheap', $array['key']); + \Tester\Assert::null($array['from']); + \Tester\Assert::same(51, $array['to']); + } + + + public function testToArrayWithNullTo(): void + { + $rangeValue = new \Spameri\ElasticQuery\Aggregation\RangeValue('expensive', 100, null); + + $array = $rangeValue->toArray(); + + \Tester\Assert::same('expensive', $array['key']); + \Tester\Assert::same(100, $array['from']); + \Tester\Assert::null($array['to']); + } + + + public function testToArrayWithFromNotEqual(): void + { + $rangeValue = new \Spameri\ElasticQuery\Aggregation\RangeValue('test', 10, 20, false, true); + + $array = $rangeValue->toArray(); + + \Tester\Assert::same(11, $array['from']); + \Tester\Assert::same(21, $array['to']); + } + + + public function testToArrayWithToNotEqual(): void + { + $rangeValue = new \Spameri\ElasticQuery\Aggregation\RangeValue('test', 10, 20, true, false); + + $array = $rangeValue->toArray(); + + \Tester\Assert::same(10, $array['from']); + \Tester\Assert::same(20, $array['to']); + } + + + public function testToArrayWithFloatValues(): void + { + $rangeValue = new \Spameri\ElasticQuery\Aggregation\RangeValue('test', 10.5, 20.5); + + $array = $rangeValue->toArray(); + + \Tester\Assert::same(10.5, $array['from']); + \Tester\Assert::same(20.5, $array['to']); + } + + + public function testToArrayWithStringValues(): void + { + $rangeValue = new \Spameri\ElasticQuery\Aggregation\RangeValue('test', '2023-01-01', '2023-12-31'); + + $array = $rangeValue->toArray(); + + \Tester\Assert::same('2023-01-01', $array['from']); + \Tester\Assert::same('2023-12-31', $array['to']); + } + + + public function testKey(): void + { + $rangeValue = new \Spameri\ElasticQuery\Aggregation\RangeValue('my_range', 10, 20); + + \Tester\Assert::same('my_range', $rangeValue->key()); + } + +} + +(new RangeValue())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/RangeValueCollection.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/RangeValueCollection.phpt new file mode 100644 index 0000000..18974d5 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/RangeValueCollection.phpt @@ -0,0 +1,71 @@ +add($rangeValue); + + $items = \iterator_to_array($collection); + \Tester\Assert::count(1, $items); + \Tester\Assert::same($rangeValue, $items[0]); + } + + + public function testIterate(): void + { + $rangeValue1 = new \Spameri\ElasticQuery\Aggregation\RangeValue('first', null, 50); + $rangeValue2 = new \Spameri\ElasticQuery\Aggregation\RangeValue('second', 50, 100); + $rangeValue3 = new \Spameri\ElasticQuery\Aggregation\RangeValue('third', 100, null); + + $collection = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection($rangeValue1, $rangeValue2, $rangeValue3); + + $keys = []; + foreach ($collection as $item) { + $keys[] = $item->key(); + } + + \Tester\Assert::same(['first', 'second', 'third'], $keys); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\RangeValueCollection(); + + \Tester\Assert::type(\ArrayIterator::class, $collection->getIterator()); + } + +} + +(new RangeValueCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/RareTerms.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/RareTerms.phpt new file mode 100644 index 0000000..09abbe5 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/RareTerms.phpt @@ -0,0 +1,102 @@ +toArray(); + + \Tester\Assert::same('genre', $array['rare_terms']['field']); + \Tester\Assert::same(2, $array['rare_terms']['max_doc_count']); + } + + + public function testKey(): void + { + $rare = new \Spameri\ElasticQuery\Aggregation\RareTerms('genre'); + + \Tester\Assert::same('rare_terms_genre', $rare->key()); + } + + + public function testCreate(): void + { + $rare = new \Spameri\ElasticQuery\Aggregation\RareTerms(field: 'genre'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'rare_genres', + null, + $rare, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new RareTerms())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Rate.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Rate.phpt new file mode 100644 index 0000000..df607a8 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Rate.phpt @@ -0,0 +1,76 @@ + [ + 'properties' => [ + 'ts' => ['type' => 'date'], + 'amount' => ['type' => 'long'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\Rate( + unit: 'month', + field: 'amount', + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('month', $array['rate']['unit']); + \Tester\Assert::same('amount', $array['rate']['field']); + } + + + public function testRequiresUnitOrScript(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\Rate(); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['ts' => '2024-01-01', 'amount' => 10]); + $this->indexDocument(['ts' => '2024-02-01', 'amount' => 20]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + // rate aggregation must live inside a date_histogram + $rateLeaf = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'monthly_rate', + null, + new \Spameri\ElasticQuery\Aggregation\Rate(unit: 'month', field: 'amount'), + ); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'by_month', + null, + new \Spameri\ElasticQuery\Aggregation\DateHistogram('ts', calendarInterval: 'month'), + $rateLeaf, + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Rate())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/ReverseNested.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/ReverseNested.phpt new file mode 100644 index 0000000..2f96ade --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/ReverseNested.phpt @@ -0,0 +1,87 @@ + [ + 'properties' => [ + 'name' => ['type' => 'keyword'], + 'comments' => [ + 'type' => 'nested', + 'properties' => [ + 'author' => ['type' => 'keyword'], + ], + ], + ], + ], + ]; + } + + + public function testToArrayWithoutPath(): void + { + $reverseNested = new \Spameri\ElasticQuery\Aggregation\ReverseNested(); + + $array = $reverseNested->toArray(); + + \Tester\Assert::type(\stdClass::class, $array['reverse_nested']); + } + + + public function testToArrayWithPath(): void + { + $reverseNested = new \Spameri\ElasticQuery\Aggregation\ReverseNested('parent'); + + \Tester\Assert::same('parent', $reverseNested->toArray()['reverse_nested']['path']); + } + + + public function testKey(): void + { + \Tester\Assert::same('reverse_nested_root', (new \Spameri\ElasticQuery\Aggregation\ReverseNested())->key()); + \Tester\Assert::same('reverse_nested_parent', (new \Spameri\ElasticQuery\Aggregation\ReverseNested('parent'))->key()); + } + + + public function testCreate(): void + { + $this->indexDocument([ + 'name' => 'post', + 'comments' => [['author' => 'john']], + ]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + + // reverse_nested must live inside a nested agg + $reverseNestedAgg = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'back_to_root', + null, + new \Spameri\ElasticQuery\Aggregation\ReverseNested(), + ); + + $nestedAgg = new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'comments_nested', + null, + new \Spameri\ElasticQuery\Aggregation\Nested('comments'), + $reverseNestedAgg, + ); + + $elasticQuery->aggregation()->add($nestedAgg); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new ReverseNested())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Sampler.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Sampler.phpt new file mode 100644 index 0000000..375dfe7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Sampler.phpt @@ -0,0 +1,103 @@ +toArray(); + + \Tester\Assert::same(200, $array['sampler']['shard_size']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'sampler', + (new \Spameri\ElasticQuery\Aggregation\Sampler(100))->key(), + ); + \Tester\Assert::same( + 'top_sample', + (new \Spameri\ElasticQuery\Aggregation\Sampler(100, 'top_sample'))->key(), + ); + } + + + public function testCreate(): void + { + $sampler = new \Spameri\ElasticQuery\Aggregation\Sampler(shardSize: 100); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'sample', + null, + $sampler, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Sampler())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/SerialDiff.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/SerialDiff.phpt new file mode 100644 index 0000000..e9979e6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/SerialDiff.phpt @@ -0,0 +1,62 @@ +toArray(); + + \Tester\Assert::same('sales', $array['serial_diff']['buckets_path']); + \Tester\Assert::same(7, $array['serial_diff']['lag']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'serial_diff', + (new \Spameri\ElasticQuery\Aggregation\SerialDiff('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SerialDiff())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/SignificantTerms.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/SignificantTerms.phpt new file mode 100644 index 0000000..d3b26ea --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/SignificantTerms.phpt @@ -0,0 +1,110 @@ +toArray(); + + \Tester\Assert::same('crime_type', $array['significant_terms']['field']); + \Tester\Assert::false(isset($array['significant_terms']['size'])); + } + + + public function testToArrayWithSize(): void + { + $significant = new \Spameri\ElasticQuery\Aggregation\SignificantTerms('crime_type', 10, 5); + + $array = $significant->toArray(); + + \Tester\Assert::same(10, $array['significant_terms']['size']); + \Tester\Assert::same(5, $array['significant_terms']['min_doc_count']); + } + + + public function testKey(): void + { + $significant = new \Spameri\ElasticQuery\Aggregation\SignificantTerms('crime_type'); + + \Tester\Assert::same('significant_terms_crime_type', $significant->key()); + } + + + public function testCreate(): void + { + $significant = new \Spameri\ElasticQuery\Aggregation\SignificantTerms('crime_type'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'unusual_crimes', + null, + $significant, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SignificantTerms())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/SignificantText.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/SignificantText.phpt new file mode 100644 index 0000000..8922f3d --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/SignificantText.phpt @@ -0,0 +1,114 @@ +toArray(); + + \Tester\Assert::same('content', $array['significant_text']['field']); + \Tester\Assert::false(isset($array['significant_text']['filter_duplicate_text'])); + } + + + public function testToArrayWithDuplicateFilter(): void + { + $significant = new \Spameri\ElasticQuery\Aggregation\SignificantText( + field: 'content', + size: 20, + filterDuplicateText: true, + ); + + $array = $significant->toArray(); + + \Tester\Assert::same(20, $array['significant_text']['size']); + \Tester\Assert::true($array['significant_text']['filter_duplicate_text']); + } + + + public function testKey(): void + { + $significant = new \Spameri\ElasticQuery\Aggregation\SignificantText('content'); + + \Tester\Assert::same('significant_text_content', $significant->key()); + } + + + public function testCreate(): void + { + $significant = new \Spameri\ElasticQuery\Aggregation\SignificantText('content'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'keywords', + null, + $significant, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SignificantText())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Stats.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Stats.phpt new file mode 100644 index 0000000..86a7957 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Stats.phpt @@ -0,0 +1,53 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + \Tester\Assert::same('price', (new \Spameri\ElasticQuery\Aggregation\Stats('price'))->toArray()['stats']['field']); + } + + + public function testToArrayWithOptions(): void + { + $stats = new \Spameri\ElasticQuery\Aggregation\Stats( + field: 'price', + missing: 0, + format: '00.00', + ); + $array = $stats->toArray(); + \Tester\Assert::same(0, $array['stats']['missing']); + \Tester\Assert::same('00.00', $array['stats']['format']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_stats', null, new \Spameri\ElasticQuery\Aggregation\Stats('price'), + )); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Stats())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/StatsBucket.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/StatsBucket.phpt new file mode 100644 index 0000000..d5b736f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/StatsBucket.phpt @@ -0,0 +1,60 @@ +sales', + ); + + $array = $statsBucket->toArray(); + + \Tester\Assert::same('sales_per_month>sales', $array['stats_bucket']['buckets_path']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'stats_bucket', + (new \Spameri\ElasticQuery\Aggregation\StatsBucket('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new StatsBucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/StringStats.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/StringStats.phpt new file mode 100644 index 0000000..288df97 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/StringStats.phpt @@ -0,0 +1,109 @@ +toArray(); + + \Tester\Assert::same('message.keyword', $array['string_stats']['field']); + \Tester\Assert::false(isset($array['string_stats']['show_distribution'])); + } + + + public function testToArrayWithDistribution(): void + { + $stringStats = new \Spameri\ElasticQuery\Aggregation\StringStats('message.keyword', true); + + $array = $stringStats->toArray(); + + \Tester\Assert::true($array['string_stats']['show_distribution']); + } + + + public function testKey(): void + { + $stringStats = new \Spameri\ElasticQuery\Aggregation\StringStats('message.keyword'); + + \Tester\Assert::same('string_stats_message.keyword', $stringStats->key()); + } + + + public function testCreate(): void + { + $stringStats = new \Spameri\ElasticQuery\Aggregation\StringStats('message.keyword'); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'message_stats', + null, + $stringStats, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new StringStats())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Sum.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Sum.phpt new file mode 100644 index 0000000..08ef87d --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Sum.phpt @@ -0,0 +1,53 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + \Tester\Assert::same('price', (new \Spameri\ElasticQuery\Aggregation\Sum('price'))->toArray()['sum']['field']); + } + + + public function testToArrayWithOptions(): void + { + $sum = new \Spameri\ElasticQuery\Aggregation\Sum( + field: 'price', + missing: 0, + format: '0.00', + ); + $array = $sum->toArray(); + \Tester\Assert::same(0, $array['sum']['missing']); + \Tester\Assert::same('0.00', $array['sum']['format']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_sum', null, new \Spameri\ElasticQuery\Aggregation\Sum('price'), + )); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Sum())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/SumBucket.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/SumBucket.phpt new file mode 100644 index 0000000..56cec3f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/SumBucket.phpt @@ -0,0 +1,60 @@ +sales', + ); + + $array = $sumBucket->toArray(); + + \Tester\Assert::same('sales_per_month>sales', $array['sum_bucket']['buckets_path']); + } + + + public function testKey(): void + { + \Tester\Assert::same( + 'sum_bucket', + (new \Spameri\ElasticQuery\Aggregation\SumBucket('p'))->key(), + ); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SumBucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/TTest.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/TTest.phpt new file mode 100644 index 0000000..fbedf76 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/TTest.phpt @@ -0,0 +1,61 @@ + [ + 'properties' => [ + 'pre' => ['type' => 'long'], + 'post' => ['type' => 'long'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\TTest( + a: ['field' => 'pre'], + b: ['field' => 'post'], + type: \Spameri\ElasticQuery\Aggregation\TTest::TYPE_PAIRED, + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('paired', $array['t_test']['type']); + \Tester\Assert::same('pre', $array['t_test']['a']['field']); + } + + + public function testCreate(): void + { + $this->indexDocument(['pre' => 10, 'post' => 12]); + $this->indexDocument(['pre' => 20, 'post' => 25]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'change', null, new \Spameri\ElasticQuery\Aggregation\TTest( + ['field' => 'pre'], + ['field' => 'post'], + \Spameri\ElasticQuery\Aggregation\TTest::TYPE_PAIRED, + ), + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new TTest())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Term.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Term.phpt new file mode 100644 index 0000000..2cfe880 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Term.phpt @@ -0,0 +1,171 @@ +toArray(); + + \Tester\Assert::true(isset($array['terms']['field'])); + \Tester\Assert::same('category', $array['terms']['field']); + \Tester\Assert::false(isset($array['terms']['size'])); + } + + + public function testToArrayWithSize(): void + { + $term = new \Spameri\ElasticQuery\Aggregation\Term('category', 10); + + $array = $term->toArray(); + + \Tester\Assert::same('category', $array['terms']['field']); + \Tester\Assert::same(10, $array['terms']['size']); + } + + + public function testToArrayWithMissing(): void + { + $term = new \Spameri\ElasticQuery\Aggregation\Term('category', 0, 0); + + $array = $term->toArray(); + + \Tester\Assert::same('category', $array['terms']['field']); + \Tester\Assert::same(0, $array['terms']['missing']); + } + + + public function testToArrayWithOrder(): void + { + $orderCollection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection( + new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'), + ); + $term = new \Spameri\ElasticQuery\Aggregation\Term('category', 10, null, $orderCollection); + + $array = $term->toArray(); + + \Tester\Assert::same('category', $array['terms']['field']); + \Tester\Assert::same(['_count' => 'desc'], $array['terms']['order']); + } + + + public function testToArrayWithIncludeExclude(): void + { + $term = new \Spameri\ElasticQuery\Aggregation\Term( + 'category', + 10, + null, + null, + 'product_.*', + 'product_old_.*', + ); + + $array = $term->toArray(); + + \Tester\Assert::same('category', $array['terms']['field']); + \Tester\Assert::same('product_.*', $array['terms']['include']); + \Tester\Assert::same('product_old_.*', $array['terms']['exclude']); + } + + + public function testKey(): void + { + $term = new \Spameri\ElasticQuery\Aggregation\Term('category'); + + \Tester\Assert::same('category', $term->key()); + } + + + public function testKeyCustom(): void + { + $term = new \Spameri\ElasticQuery\Aggregation\Term( + 'category', + 0, + null, + null, + null, + null, + 'custom_key', + ); + + \Tester\Assert::same('custom_key', $term->key()); + } + + + public function testCreate(): void + { + $term = new \Spameri\ElasticQuery\Aggregation\Term('category', 10); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'categories', + null, + $term, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Term())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Terms/Order.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Terms/Order.phpt new file mode 100644 index 0000000..dfcbe77 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Terms/Order.phpt @@ -0,0 +1,60 @@ +toArray(); + + \Tester\Assert::same(['_count' => 'desc'], $array); + } + + + public function testToArrayCountAsc(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'asc'); + + $array = $order->toArray(); + + \Tester\Assert::same(['_count' => 'asc'], $array); + } + + + public function testToArrayKeyDesc(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_key', 'desc'); + + $array = $order->toArray(); + + \Tester\Assert::same(['_key' => 'desc'], $array); + } + + + public function testToArrayCustomField(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('avg_price', 'asc'); + + $array = $order->toArray(); + + \Tester\Assert::same(['avg_price' => 'asc'], $array); + } + + + public function testKey(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + + \Tester\Assert::same('order__count_desc', $order->key()); + } + +} + +(new Order())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/Terms/OrderCollection.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/Terms/OrderCollection.phpt new file mode 100644 index 0000000..30f5738 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/Terms/OrderCollection.phpt @@ -0,0 +1,160 @@ +count()); + } + + + public function testConstructorWithItems(): void + { + $order1 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $order2 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_key', 'asc'); + + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order1, $order2); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testAdd(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection(); + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + + $collection->add($order); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testGet(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order); + + $result = $collection->get($order->key()); + + \Tester\Assert::same($order, $result); + } + + + public function testGetNull(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection(); + + $result = $collection->get('nonexistent'); + + \Tester\Assert::null($result); + } + + + public function testRemove(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order); + + $result = $collection->remove($order->key()); + + \Tester\Assert::true($result); + \Tester\Assert::same(0, $collection->count()); + } + + + public function testRemoveNonexistent(): void + { + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection(); + + $result = $collection->remove('nonexistent'); + + \Tester\Assert::false($result); + } + + + public function testIsValue(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order); + + \Tester\Assert::true($collection->isValue($order->key())); + \Tester\Assert::false($collection->isValue('nonexistent')); + } + + + public function testKeys(): void + { + $order1 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $order2 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_key', 'asc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order1, $order2); + + $keys = $collection->keys(); + + \Tester\Assert::contains($order1->key(), $keys); + \Tester\Assert::contains($order2->key(), $keys); + } + + + public function testClear(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + } + + + public function testToArraySingleItem(): void + { + $order = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order); + + $array = $collection->toArray(); + + \Tester\Assert::same(['_count' => 'desc'], $array); + } + + + public function testToArrayMultipleItems(): void + { + $order1 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $order2 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_key', 'asc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order1, $order2); + + $array = $collection->toArray(); + + \Tester\Assert::count(2, $array); + \Tester\Assert::same(['_count' => 'desc'], $array[0]); + \Tester\Assert::same(['_key' => 'asc'], $array[1]); + } + + + public function testIterate(): void + { + $order1 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_count', 'desc'); + $order2 = new \Spameri\ElasticQuery\Aggregation\Terms\Order('_key', 'asc'); + $collection = new \Spameri\ElasticQuery\Aggregation\Terms\OrderCollection($order1, $order2); + + $count = 0; + foreach ($collection as $order) { + \Tester\Assert::type(\Spameri\ElasticQuery\Aggregation\Terms\Order::class, $order); + $count++; + } + + \Tester\Assert::same(2, $count); + } + +} + +(new OrderCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/TimeSeries.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/TimeSeries.phpt new file mode 100644 index 0000000..c99f015 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/TimeSeries.phpt @@ -0,0 +1,39 @@ +toArray(); + + \Tester\Assert::type(\stdClass::class, $array['time_series']); + } + + + public function testToArrayWithOptions(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\TimeSeries( + keyed: true, + size: 100, + ); + + $array = $agg->toArray(); + + \Tester\Assert::true($array['time_series']['keyed']); + \Tester\Assert::same(100, $array['time_series']['size']); + } + +} + +(new TimeSeries())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/TopHits.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/TopHits.phpt new file mode 100644 index 0000000..44d3028 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/TopHits.phpt @@ -0,0 +1,99 @@ +toArray(); + + \Tester\Assert::true(isset($array['top_hits']['size'])); + \Tester\Assert::same(10, $array['top_hits']['size']); + } + + + public function testKey(): void + { + $topHits = new \Spameri\ElasticQuery\Aggregation\TopHits(5); + + \Tester\Assert::same('top_hits_5', $topHits->key()); + } + + + public function testCreate(): void + { + $topHits = new \Spameri\ElasticQuery\Aggregation\TopHits(10); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'top_documents', + null, + $topHits, + ), + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + $elasticQuery->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new TopHits())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/TopMetrics.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/TopMetrics.phpt new file mode 100644 index 0000000..b0ba0bb --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/TopMetrics.phpt @@ -0,0 +1,72 @@ + [ + 'properties' => [ + 'price' => ['type' => 'long'], + 'ts' => ['type' => 'date'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\TopMetrics( + metrics: ['price'], + sort: [['ts' => ['order' => 'desc']]], + size: 1, + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('price', $array['top_metrics']['metrics'][0]['field']); + \Tester\Assert::same(1, $array['top_metrics']['size']); + } + + + public function testRequiresMetrics(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\TopMetrics([]); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100, 'ts' => '2024-01-01']); + $this->indexDocument(['price' => 200, 'ts' => '2024-06-01']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'latest_price', null, new \Spameri\ElasticQuery\Aggregation\TopMetrics( + ['price'], + [['ts' => ['order' => 'desc']]], + 1, + ), + )); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new TopMetrics())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/ValueCount.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/ValueCount.phpt new file mode 100644 index 0000000..0bdaa32 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/ValueCount.phpt @@ -0,0 +1,44 @@ +toArray()['value_count']['field']); + } + + + public function testToArrayWithScript(): void + { + $vc = new \Spameri\ElasticQuery\Aggregation\ValueCount( + field: 'price', + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].size()"), + ); + \Tester\Assert::same("doc['price'].size()", $vc->toArray()['value_count']['script']['source']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_count', null, new \Spameri\ElasticQuery\Aggregation\ValueCount('price'), + )); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new ValueCount())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/VariableWidthHistogram.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/VariableWidthHistogram.phpt new file mode 100644 index 0000000..eba1d7a --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/VariableWidthHistogram.phpt @@ -0,0 +1,54 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + $agg = new \Spameri\ElasticQuery\Aggregation\VariableWidthHistogram( + field: 'price', + buckets: 3, + shardSize: 10, + initialBuffer: 100, + ); + + $array = $agg->toArray(); + + \Tester\Assert::same('price', $array['variable_width_histogram']['field']); + \Tester\Assert::same(3, $array['variable_width_histogram']['buckets']); + \Tester\Assert::same(10, $array['variable_width_histogram']['shard_size']); + \Tester\Assert::same(100, $array['variable_width_histogram']['initial_buffer']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 10]); + $this->indexDocument(['price' => 50]); + $this->indexDocument(['price' => 100]); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add(new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'price_buckets', null, new \Spameri\ElasticQuery\Aggregation\VariableWidthHistogram('price', 2), + )); + + \Tester\Assert::same(3, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new VariableWidthHistogram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Aggregation/WeightedAvg.phpt b/tests/SpameriTests/ElasticQuery/Aggregation/WeightedAvg.phpt new file mode 100644 index 0000000..686efaf --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Aggregation/WeightedAvg.phpt @@ -0,0 +1,105 @@ + [ + 'properties' => [ + 'grade' => ['type' => 'long'], + 'weight' => ['type' => 'long'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $weightedAvg = new \Spameri\ElasticQuery\Aggregation\WeightedAvg( + value: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'grade'), + weight: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'weight'), + ); + + $array = $weightedAvg->toArray(); + + \Tester\Assert::same('grade', $array['weighted_avg']['value']['field']); + \Tester\Assert::same('weight', $array['weighted_avg']['weight']['field']); + } + + + public function testToArrayWithMissing(): void + { + $weightedAvg = new \Spameri\ElasticQuery\Aggregation\WeightedAvg( + value: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'grade', missing: 0), + weight: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'weight', missing: 1), + format: '00.00', + ); + + $array = $weightedAvg->toArray(); + + \Tester\Assert::same(0, $array['weighted_avg']['value']['missing']); + \Tester\Assert::same(1, $array['weighted_avg']['weight']['missing']); + \Tester\Assert::same('00.00', $array['weighted_avg']['format']); + } + + + public function testValueRequiresFieldOrScript(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testKey(): void + { + $weightedAvg = new \Spameri\ElasticQuery\Aggregation\WeightedAvg( + value: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'grade'), + weight: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'weight'), + ); + + \Tester\Assert::same('weighted_avg', $weightedAvg->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['grade' => 80, 'weight' => 2]); + $this->indexDocument(['grade' => 90, 'weight' => 3]); + + $weightedAvg = new \Spameri\ElasticQuery\Aggregation\WeightedAvg( + value: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'grade'), + weight: new \Spameri\ElasticQuery\Aggregation\WeightedAvg\WeightedAvgValue(field: 'weight'), + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->aggregation()->add( + new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection( + 'weighted_grade', + null, + $weightedAvg, + ), + ); + + $result = $this->search($elasticQuery); + + \Tester\Assert::same(2, $result->stats()->total()); + } + +} + +(new WeightedAvg())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Collection/AbstractCollection.phpt b/tests/SpameriTests/ElasticQuery/Collection/AbstractCollection.phpt new file mode 100644 index 0000000..53eb206 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Collection/AbstractCollection.phpt @@ -0,0 +1,233 @@ +key; + } + + + public function toArray(): array + { + return [ + 'key' => $this->key, + 'value' => $this->value, + ]; + } + + + public function getValue(): string + { + return $this->value; + } + +} + + +/** + * Concrete collection implementation for testing AbstractCollection + */ +class TestCollection extends \Spameri\ElasticQuery\Collection\AbstractCollection +{ + +} + + +class AbstractCollection extends \Tester\TestCase +{ + + public function testEmptyCollection(): void + { + $collection = new TestCollection(); + + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testAddItem(): void + { + $collection = new TestCollection(); + $entity = new TestEntity('key1', 'value1'); + + $collection->add($entity); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue('key1')); + } + + + public function testConstructorWithItems(): void + { + $entity1 = new TestEntity('key1', 'value1'); + $entity2 = new TestEntity('key2', 'value2'); + + $collection = new TestCollection($entity1, $entity2); + + \Tester\Assert::same(2, $collection->count()); + \Tester\Assert::true($collection->isValue('key1')); + \Tester\Assert::true($collection->isValue('key2')); + } + + + public function testGetItem(): void + { + $entity = new TestEntity('key1', 'value1'); + $collection = new TestCollection($entity); + + $retrieved = $collection->get('key1'); + + \Tester\Assert::same($entity, $retrieved); + } + + + public function testGetNonExistentReturnsNull(): void + { + $collection = new TestCollection(); + + $result = $collection->get('non_existent_key'); + + \Tester\Assert::null($result); + } + + + public function testRemoveItem(): void + { + $entity = new TestEntity('key1', 'value1'); + $collection = new TestCollection($entity); + + $removed = $collection->remove('key1'); + + \Tester\Assert::true($removed); + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::false($collection->isValue('key1')); + } + + + public function testRemoveNonExistentReturnsFalse(): void + { + $collection = new TestCollection(); + + $removed = $collection->remove('non_existent_key'); + + \Tester\Assert::false($removed); + } + + + public function testIsValue(): void + { + $entity = new TestEntity('key1', 'value1'); + $collection = new TestCollection($entity); + + \Tester\Assert::true($collection->isValue('key1')); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testKeys(): void + { + $entity1 = new TestEntity('key1', 'value1'); + $entity2 = new TestEntity('key2', 'value2'); + $collection = new TestCollection($entity1, $entity2); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains('key1', $keys); + \Tester\Assert::contains('key2', $keys); + } + + + public function testClear(): void + { + $entity1 = new TestEntity('key1', 'value1'); + $entity2 = new TestEntity('key2', 'value2'); + $collection = new TestCollection($entity1, $entity2); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testGetIterator(): void + { + $entity1 = new TestEntity('key1', 'value1'); + $entity2 = new TestEntity('key2', 'value2'); + $collection = new TestCollection($entity1, $entity2); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(2, $iterator->count()); + } + + + public function testIterateWithForeach(): void + { + $entity1 = new TestEntity('key1', 'value1'); + $entity2 = new TestEntity('key2', 'value2'); + $collection = new TestCollection($entity1, $entity2); + + $count = 0; + foreach ($collection as $key => $item) { + \Tester\Assert::type(\Spameri\ElasticQuery\Entity\EntityInterface::class, $item); + \Tester\Assert::type('string', $key); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testKeyUniqueness(): void + { + $entity1 = new TestEntity('key1', 'value1'); + $entity2 = new TestEntity('key1', 'value2'); + $collection = new TestCollection(); + + $collection->add($entity1); + $collection->add($entity2); + + \Tester\Assert::same(1, $collection->count()); + $retrieved = $collection->get('key1'); + \Tester\Assert::type(TestEntity::class, $retrieved); + \Tester\Assert::same('value2', $retrieved->getValue()); + } + + + public function testNumericKeys(): void + { + $entity1 = new TestEntity('123', 'value1'); + $entity2 = new TestEntity('456', 'value2'); + $collection = new TestCollection($entity1, $entity2); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::type('string', $keys[0]); + \Tester\Assert::type('string', $keys[1]); + } + +} + +(new AbstractCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Collection/Item.phpt b/tests/SpameriTests/ElasticQuery/Collection/Item.phpt new file mode 100644 index 0000000..307d43d --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Collection/Item.phpt @@ -0,0 +1,81 @@ +key; + } + +} + + +class Item extends \Tester\TestCase +{ + + public function testKey(): void + { + $item = new TestItem('test_key'); + + \Tester\Assert::same('test_key', $item->key()); + } + + + public function testEmptyKey(): void + { + $item = new TestItem(''); + + \Tester\Assert::same('', $item->key()); + } + + + public function testNumericKeyAsString(): void + { + $item = new TestItem('123'); + + \Tester\Assert::same('123', $item->key()); + } + + + public function testSpecialCharactersInKey(): void + { + $item = new TestItem('key-with_special.chars:123'); + + \Tester\Assert::same('key-with_special.chars:123', $item->key()); + } + + + public function testUnicodeKey(): void + { + $item = new TestItem('klíč_český'); + + \Tester\Assert::same('klíč_český', $item->key()); + } + + + public function testImplementsInterface(): void + { + $item = new TestItem('key'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Collection\Item::class, $item); + } + +} + +(new Item())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Document.phpt b/tests/SpameriTests/ElasticQuery/Document.phpt new file mode 100644 index 0000000..f231f54 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Document.phpt @@ -0,0 +1,174 @@ +toArray(); + + \Tester\Assert::same(['index' => 'test_index'], $array); + } + + + public function testToArrayWithIndexAndId(): void + { + $document = new \Spameri\ElasticQuery\Document( + index: 'products', + id: 'product_123', + ); + + $array = $document->toArray(); + + \Tester\Assert::same('products', $array['index']); + \Tester\Assert::same('product_123', $array['id']); + } + + + public function testToArrayWithBody(): void + { + $body = new \Spameri\ElasticQuery\Document\Body\Plain(['query' => ['match_all' => new \stdClass()]]); + $document = new \Spameri\ElasticQuery\Document( + index: 'search_index', + body: $body, + ); + + $array = $document->toArray(); + + \Tester\Assert::same('search_index', $array['index']); + \Tester\Assert::true(isset($array['body'])); + \Tester\Assert::true(isset($array['body']['query'])); + } + + + public function testToArrayWithOptions(): void + { + $document = new \Spameri\ElasticQuery\Document( + index: 'test_index', + options: ['refresh' => true, 'routing' => 'user_1'], + ); + + $array = $document->toArray(); + + \Tester\Assert::same('test_index', $array['index']); + \Tester\Assert::same(true, $array['refresh']); + \Tester\Assert::same('user_1', $array['routing']); + } + + + public function testToArrayFull(): void + { + $body = new \Spameri\ElasticQuery\Document\Body\Plain(['title' => 'Test Product', 'price' => 99.99]); + $document = new \Spameri\ElasticQuery\Document( + index: 'products', + body: $body, + id: 'prod_456', + options: ['refresh' => 'wait_for'], + ); + + $array = $document->toArray(); + + \Tester\Assert::same('products', $array['index']); + \Tester\Assert::same('prod_456', $array['id']); + \Tester\Assert::same(['title' => 'Test Product', 'price' => 99.99], $array['body']); + \Tester\Assert::same('wait_for', $array['refresh']); + } + + + public function testToArrayWithNullIndex(): void + { + $document = new \Spameri\ElasticQuery\Document( + index: null, + id: 'doc_id', + ); + + $array = $document->toArray(); + + \Tester\Assert::false(isset($array['index'])); + \Tester\Assert::same('doc_id', $array['id']); + } + + + public function testToArrayWithNullId(): void + { + $document = new \Spameri\ElasticQuery\Document( + index: 'my_index', + id: null, + ); + + $array = $document->toArray(); + + \Tester\Assert::same('my_index', $array['index']); + \Tester\Assert::false(isset($array['id'])); + } + + + public function testToArrayWithNullBody(): void + { + $document = new \Spameri\ElasticQuery\Document( + index: 'my_index', + body: null, + ); + + $array = $document->toArray(); + + \Tester\Assert::false(isset($array['body'])); + } + + + public function testToArrayWithEmptyOptions(): void + { + $document = new \Spameri\ElasticQuery\Document( + index: 'my_index', + options: [], + ); + + $array = $document->toArray(); + + \Tester\Assert::same(['index' => 'my_index'], $array); + } + + + public function testPublicProperties(): void + { + $body = new \Spameri\ElasticQuery\Document\Body\Plain(['data' => 'value']); + $document = new \Spameri\ElasticQuery\Document( + index: 'test_index', + body: $body, + id: 'test_id', + options: ['timeout' => '5m'], + ); + + \Tester\Assert::same('test_index', $document->index); + \Tester\Assert::same($body, $document->body); + \Tester\Assert::same('test_id', $document->id); + \Tester\Assert::same(['timeout' => '5m'], $document->options); + } + + + public function testWithElasticQueryBody(): void + { + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->query()->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $body = new \Spameri\ElasticQuery\Document\Body\Plain($elasticQuery->toArray()); + $document = new \Spameri\ElasticQuery\Document( + index: 'products', + body: $body, + ); + + $array = $document->toArray(); + + \Tester\Assert::true(isset($array['body']['query'])); + } + +} + +(new Document())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Document/Body/Plain.phpt b/tests/SpameriTests/ElasticQuery/Document/Body/Plain.phpt new file mode 100644 index 0000000..e922155 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Document/Body/Plain.phpt @@ -0,0 +1,153 @@ + 'value']); + + $array = $plain->toArray(); + + \Tester\Assert::same(['key' => 'value'], $array); + } + + + public function testToArrayEmpty(): void + { + $plain = new \Spameri\ElasticQuery\Document\Body\Plain([]); + + $array = $plain->toArray(); + + \Tester\Assert::same([], $array); + } + + + public function testToArrayWithNestedData(): void + { + $data = [ + 'query' => [ + 'bool' => [ + 'must' => [ + ['term' => ['status' => 'active']], + ], + ], + ], + ]; + $plain = new \Spameri\ElasticQuery\Document\Body\Plain($data); + + $array = $plain->toArray(); + + \Tester\Assert::same($data, $array); + } + + + public function testToArrayWithMultipleKeys(): void + { + $data = [ + 'title' => 'Test Document', + 'content' => 'Some content here', + 'tags' => ['php', 'elasticsearch'], + 'published' => true, + 'views' => 1234, + ]; + $plain = new \Spameri\ElasticQuery\Document\Body\Plain($data); + + $array = $plain->toArray(); + + \Tester\Assert::same('Test Document', $array['title']); + \Tester\Assert::same('Some content here', $array['content']); + \Tester\Assert::same(['php', 'elasticsearch'], $array['tags']); + \Tester\Assert::same(true, $array['published']); + \Tester\Assert::same(1234, $array['views']); + } + + + public function testToArrayWithElasticQueryOutput(): void + { + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->query()->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'published')); + $elasticQuery->options()->changeSize(10); + + $plain = new \Spameri\ElasticQuery\Document\Body\Plain($elasticQuery->toArray()); + + $array = $plain->toArray(); + + \Tester\Assert::true(isset($array['query'])); + \Tester\Assert::true(isset($array['size'])); + } + + + public function testToArrayPreservesStructure(): void + { + $data = [ + 'aggs' => [ + 'categories' => [ + 'terms' => [ + 'field' => 'category', + 'size' => 10, + ], + ], + ], + ]; + $plain = new \Spameri\ElasticQuery\Document\Body\Plain($data); + + $array = $plain->toArray(); + + \Tester\Assert::same(10, $array['aggs']['categories']['terms']['size']); + } + + + public function testToArrayWithNumericKeys(): void + { + $data = [ + 0 => 'first', + 1 => 'second', + 2 => 'third', + ]; + $plain = new \Spameri\ElasticQuery\Document\Body\Plain($data); + + $array = $plain->toArray(); + + \Tester\Assert::same(['first', 'second', 'third'], $array); + } + + + public function testToArrayWithMixedTypes(): void + { + $data = [ + 'string' => 'text', + 'int' => 42, + 'float' => 3.14, + 'bool' => false, + 'null' => null, + 'array' => [1, 2, 3], + ]; + $plain = new \Spameri\ElasticQuery\Document\Body\Plain($data); + + $array = $plain->toArray(); + + \Tester\Assert::same('text', $array['string']); + \Tester\Assert::same(42, $array['int']); + \Tester\Assert::same(3.14, $array['float']); + \Tester\Assert::same(false, $array['bool']); + \Tester\Assert::null($array['null']); + \Tester\Assert::same([1, 2, 3], $array['array']); + } + + + public function testImplementsBodyInterface(): void + { + $plain = new \Spameri\ElasticQuery\Document\Body\Plain([]); + + \Tester\Assert::type(\Spameri\ElasticQuery\Document\BodyInterface::class, $plain); + } + +} + +(new Plain())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Document/Body/Settings.phpt b/tests/SpameriTests/ElasticQuery/Document/Body/Settings.phpt new file mode 100644 index 0000000..76e9d47 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Document/Body/Settings.phpt @@ -0,0 +1,138 @@ +toArray(); + + \Tester\Assert::true(isset($array['settings'])); + \Tester\Assert::true(isset($array['settings']['analysis'])); + \Tester\Assert::true(isset($array['settings']['analysis']['analyzer'])); + \Tester\Assert::true(isset($array['settings']['analysis']['tokenizer'])); + \Tester\Assert::true(isset($array['settings']['analysis']['filter'])); + } + + + public function testToArrayWithEmptyCollections(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + $filterCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $settings = new \Spameri\ElasticQuery\Document\Body\Settings($analyzerCollection, $filterCollection); + + $array = $settings->toArray(); + + \Tester\Assert::same([], $array['settings']['analysis']['analyzer']); + \Tester\Assert::same([], $array['settings']['analysis']['filter']); + } + + + public function testToArrayWithAnalyzer(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + $analyzerCollection->add(new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\Lowercase()); + + $filterCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $settings = new \Spameri\ElasticQuery\Document\Body\Settings($analyzerCollection, $filterCollection); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['settings']['analysis']['analyzer']['customLowercase'])); + \Tester\Assert::same('custom', $array['settings']['analysis']['analyzer']['customLowercase']['type']); + } + + + public function testToArrayWithFilter(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + + $filterCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + $filterCollection->add(new \Spameri\ElasticQuery\Mapping\Filter\Lowercase()); + + $settings = new \Spameri\ElasticQuery\Document\Body\Settings($analyzerCollection, $filterCollection); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['settings']['analysis']['filter']['lowercase'])); + \Tester\Assert::same('lowercase', $array['settings']['analysis']['filter']['lowercase']['type']); + } + + + public function testToArrayWithMultipleFilters(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + + $filterCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + $filterCollection->add(new \Spameri\ElasticQuery\Mapping\Filter\Lowercase()); + $filterCollection->add(new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding()); + + $settings = new \Spameri\ElasticQuery\Document\Body\Settings($analyzerCollection, $filterCollection); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['settings']['analysis']['filter']['lowercase'])); + \Tester\Assert::true(isset($array['settings']['analysis']['filter']['asciifolding'])); + } + + + public function testToArrayWithAnalyzerAndFilters(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + $analyzerCollection->add(new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\Lowercase()); + + $filterCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + $filterCollection->add(new \Spameri\ElasticQuery\Mapping\Filter\Lowercase()); + $filterCollection->add(new \Spameri\ElasticQuery\Mapping\Filter\Unique()); + + $settings = new \Spameri\ElasticQuery\Document\Body\Settings($analyzerCollection, $filterCollection); + + $array = $settings->toArray(); + + // Check analyzer + \Tester\Assert::true(isset($array['settings']['analysis']['analyzer']['customLowercase'])); + + // Check filters + \Tester\Assert::true(isset($array['settings']['analysis']['filter']['lowercase'])); + \Tester\Assert::true(isset($array['settings']['analysis']['filter']['unique'])); + } + + + public function testImplementsBodyInterface(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + $filterCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $settings = new \Spameri\ElasticQuery\Document\Body\Settings($analyzerCollection, $filterCollection); + + \Tester\Assert::type(\Spameri\ElasticQuery\Document\BodyInterface::class, $settings); + } + + + public function testToArrayTokenizerIsEmptyArray(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + $filterCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $settings = new \Spameri\ElasticQuery\Document\Body\Settings($analyzerCollection, $filterCollection); + + $array = $settings->toArray(); + + \Tester\Assert::same([], $array['settings']['analysis']['tokenizer']); + } + +} + +(new Settings())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Document/Bulk.phpt b/tests/SpameriTests/ElasticQuery/Document/Bulk.phpt new file mode 100644 index 0000000..fd95495 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Document/Bulk.phpt @@ -0,0 +1,136 @@ + ['_index' => 'test', '_id' => '1']], + ['field1' => 'value1'], + ]; + $bulk = new \Spameri\ElasticQuery\Document\Bulk($data); + + $array = $bulk->toArray(); + + \Tester\Assert::true(isset($array['body'])); + \Tester\Assert::same($data, $array['body']); + } + + + public function testToArrayWithMultipleOperations(): void + { + $data = [ + ['index' => ['_index' => 'products', '_id' => '1']], + ['name' => 'Product 1', 'price' => 10.0], + ['index' => ['_index' => 'products', '_id' => '2']], + ['name' => 'Product 2', 'price' => 20.0], + ['delete' => ['_index' => 'products', '_id' => '3']], + ]; + $bulk = new \Spameri\ElasticQuery\Document\Bulk($data); + + $array = $bulk->toArray(); + + \Tester\Assert::count(5, $array['body']); + } + + + public function testToArrayWithCreate(): void + { + $data = [ + ['create' => ['_index' => 'logs', '_id' => 'log_1']], + ['message' => 'Log entry', 'timestamp' => '2024-01-01T00:00:00Z'], + ]; + $bulk = new \Spameri\ElasticQuery\Document\Bulk($data); + + $array = $bulk->toArray(); + + \Tester\Assert::true(isset($array['body'][0]['create'])); + } + + + public function testToArrayWithUpdate(): void + { + $data = [ + ['update' => ['_index' => 'users', '_id' => 'user_1']], + ['doc' => ['last_login' => '2024-01-15T10:30:00Z']], + ]; + $bulk = new \Spameri\ElasticQuery\Document\Bulk($data); + + $array = $bulk->toArray(); + + \Tester\Assert::true(isset($array['body'][0]['update'])); + \Tester\Assert::true(isset($array['body'][1]['doc'])); + } + + + public function testToArrayWithDelete(): void + { + $data = [ + ['delete' => ['_index' => 'old_data', '_id' => 'record_1']], + ['delete' => ['_index' => 'old_data', '_id' => 'record_2']], + ]; + $bulk = new \Spameri\ElasticQuery\Document\Bulk($data); + + $array = $bulk->toArray(); + + \Tester\Assert::count(2, $array['body']); + \Tester\Assert::true(isset($array['body'][0]['delete'])); + \Tester\Assert::true(isset($array['body'][1]['delete'])); + } + + + public function testToArrayEmpty(): void + { + $bulk = new \Spameri\ElasticQuery\Document\Bulk([]); + + $array = $bulk->toArray(); + + \Tester\Assert::same(['body' => []], $array); + } + + + public function testToArrayWithMixedOperations(): void + { + $data = [ + // Index operation + ['index' => ['_index' => 'products', '_id' => '1']], + ['name' => 'New Product'], + // Update operation + ['update' => ['_index' => 'products', '_id' => '2']], + ['doc' => ['price' => 15.99]], + // Delete operation + ['delete' => ['_index' => 'products', '_id' => '3']], + // Create operation + ['create' => ['_index' => 'products', '_id' => '4']], + ['name' => 'Another Product'], + ]; + $bulk = new \Spameri\ElasticQuery\Document\Bulk($data); + + $array = $bulk->toArray(); + + \Tester\Assert::count(7, $array['body']); + } + + + public function testToArrayWithRouting(): void + { + $data = [ + ['index' => ['_index' => 'users', '_id' => '1', 'routing' => 'tenant_a']], + ['name' => 'User 1'], + ]; + $bulk = new \Spameri\ElasticQuery\Document\Bulk($data); + + $array = $bulk->toArray(); + + \Tester\Assert::same('tenant_a', $array['body'][0]['index']['routing']); + } + +} + +(new Bulk())->run(); diff --git a/tests/SpameriTests/ElasticQuery/ElasticQuery.phpt b/tests/SpameriTests/ElasticQuery/ElasticQuery.phpt index 0efd0b0..aac3eb8 100644 --- a/tests/SpameriTests/ElasticQuery/ElasticQuery.phpt +++ b/tests/SpameriTests/ElasticQuery/ElasticQuery.phpt @@ -13,18 +13,17 @@ class ElasticQuery extends \Tester\TestCase public function setUp() : void { $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); + \curl_setopt($ch, CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); \curl_exec($ch); - \curl_close($ch); /// === $ch = \curl_init(); - \curl_setopt($ch, \CURLOPT_URL, 'localhost:9200/' . self::INDEX . '/_mapping'); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX . '/_mapping'); \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'PUT'); \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); @@ -43,7 +42,6 @@ class ElasticQuery extends \Tester\TestCase ); \curl_exec($ch); - \curl_close($ch); } @@ -81,6 +79,7 @@ class ElasticQuery extends \Tester\TestCase $term ) ), + NULL, new \Spameri\ElasticQuery\Options\SortCollection( new \Spameri\ElasticQuery\Options\Sort( 'year', @@ -113,7 +112,7 @@ class ElasticQuery extends \Tester\TestCase ); $ch = \curl_init(); - \curl_setopt($ch, \CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); @@ -129,21 +128,18 @@ class ElasticQuery extends \Tester\TestCase $result = $resultMapper->map(\json_decode($response, TRUE)); \Tester\Assert::type('int', $result->stats()->total()); }); - - \curl_close($ch); } public function tearDown() : void { $ch = \curl_init(); - \curl_setopt($ch, \CURLOPT_URL, 'localhost:9200/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); \curl_exec($ch); - \curl_close($ch); } } diff --git a/tests/SpameriTests/ElasticQuery/ElasticsearchTestCase.php b/tests/SpameriTests/ElasticQuery/ElasticsearchTestCase.php new file mode 100644 index 0000000..faa4cb7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/ElasticsearchTestCase.php @@ -0,0 +1,21 @@ +getMessage()); + } + + + public function testWithCode(): void + { + $exception = new \Spameri\ElasticQuery\Exception\AggregationNotFound('message', 404); + + \Tester\Assert::same(404, $exception->getCode()); + } + + + public function testWithPreviousException(): void + { + $previous = new \Exception('Previous exception'); + $exception = new \Spameri\ElasticQuery\Exception\AggregationNotFound('message', 0, $previous); + + \Tester\Assert::same($previous, $exception->getPrevious()); + } + + + public function testCanBeThrown(): void + { + \Tester\Assert::exception( + static function (): void { + throw new \Spameri\ElasticQuery\Exception\AggregationNotFound('Aggregation not found'); + }, + \Spameri\ElasticQuery\Exception\AggregationNotFound::class, + 'Aggregation not found', + ); + } + + + public function testCanBeCaughtAsInvalidArgumentException(): void + { + $caught = false; + try { + throw new \Spameri\ElasticQuery\Exception\AggregationNotFound('test'); + } catch (\InvalidArgumentException $e) { + $caught = true; + } + + \Tester\Assert::true($caught); + } + + + public function testEmptyMessage(): void + { + $exception = new \Spameri\ElasticQuery\Exception\AggregationNotFound(); + + \Tester\Assert::same('', $exception->getMessage()); + } + +} + +(new AggregationNotFound())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Exception/BulkActionNotFound.phpt b/tests/SpameriTests/ElasticQuery/Exception/BulkActionNotFound.phpt new file mode 100644 index 0000000..51fc443 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Exception/BulkActionNotFound.phpt @@ -0,0 +1,79 @@ +getMessage()); + } + + + public function testWithCode(): void + { + $exception = new \Spameri\ElasticQuery\Exception\BulkActionNotFound('message', 404); + + \Tester\Assert::same(404, $exception->getCode()); + } + + + public function testWithPreviousException(): void + { + $previous = new \Exception('Previous exception'); + $exception = new \Spameri\ElasticQuery\Exception\BulkActionNotFound('message', 0, $previous); + + \Tester\Assert::same($previous, $exception->getPrevious()); + } + + + public function testCanBeThrown(): void + { + \Tester\Assert::exception( + static function (): void { + throw new \Spameri\ElasticQuery\Exception\BulkActionNotFound('Bulk action not found'); + }, + \Spameri\ElasticQuery\Exception\BulkActionNotFound::class, + 'Bulk action not found', + ); + } + + + public function testCanBeCaughtAsInvalidArgumentException(): void + { + $caught = false; + try { + throw new \Spameri\ElasticQuery\Exception\BulkActionNotFound('test'); + } catch (\InvalidArgumentException $e) { + $caught = true; + } + + \Tester\Assert::true($caught); + } + + + public function testEmptyMessage(): void + { + $exception = new \Spameri\ElasticQuery\Exception\BulkActionNotFound(); + + \Tester\Assert::same('', $exception->getMessage()); + } + +} + +(new BulkActionNotFound())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Exception/HitNotFound.phpt b/tests/SpameriTests/ElasticQuery/Exception/HitNotFound.phpt new file mode 100644 index 0000000..9a41f87 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Exception/HitNotFound.phpt @@ -0,0 +1,79 @@ +getMessage()); + } + + + public function testWithCode(): void + { + $exception = new \Spameri\ElasticQuery\Exception\HitNotFound('message', 404); + + \Tester\Assert::same(404, $exception->getCode()); + } + + + public function testWithPreviousException(): void + { + $previous = new \Exception('Previous exception'); + $exception = new \Spameri\ElasticQuery\Exception\HitNotFound('message', 0, $previous); + + \Tester\Assert::same($previous, $exception->getPrevious()); + } + + + public function testCanBeThrown(): void + { + \Tester\Assert::exception( + static function (): void { + throw new \Spameri\ElasticQuery\Exception\HitNotFound('Hit not found'); + }, + \Spameri\ElasticQuery\Exception\HitNotFound::class, + 'Hit not found', + ); + } + + + public function testCanBeCaughtAsInvalidArgumentException(): void + { + $caught = false; + try { + throw new \Spameri\ElasticQuery\Exception\HitNotFound('test'); + } catch (\InvalidArgumentException $e) { + $caught = true; + } + + \Tester\Assert::true($caught); + } + + + public function testEmptyMessage(): void + { + $exception = new \Spameri\ElasticQuery\Exception\HitNotFound(); + + \Tester\Assert::same('', $exception->getMessage()); + } + +} + +(new HitNotFound())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Exception/InvalidArgumentException.phpt b/tests/SpameriTests/ElasticQuery/Exception/InvalidArgumentException.phpt new file mode 100644 index 0000000..7598e1d --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Exception/InvalidArgumentException.phpt @@ -0,0 +1,89 @@ +getMessage()); + } + + + public function testWithCode(): void + { + $exception = new \Spameri\ElasticQuery\Exception\InvalidArgumentException('message', 400); + + \Tester\Assert::same(400, $exception->getCode()); + } + + + public function testWithPreviousException(): void + { + $previous = new \Exception('Previous exception'); + $exception = new \Spameri\ElasticQuery\Exception\InvalidArgumentException('message', 0, $previous); + + \Tester\Assert::same($previous, $exception->getPrevious()); + } + + + public function testCanBeThrown(): void + { + \Tester\Assert::exception( + static function (): void { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException('Invalid argument'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + 'Invalid argument', + ); + } + + + public function testCanBeCaughtAsPhpInvalidArgumentException(): void + { + $caught = false; + try { + throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException('test'); + } catch (\InvalidArgumentException $e) { + $caught = true; + } + + \Tester\Assert::true($caught); + } + + + public function testEmptyMessage(): void + { + $exception = new \Spameri\ElasticQuery\Exception\InvalidArgumentException(); + + \Tester\Assert::same('', $exception->getMessage()); + } + + + public function testIsDistinctFromPhpException(): void + { + $phpException = new \InvalidArgumentException('test'); + $libraryException = new \Spameri\ElasticQuery\Exception\InvalidArgumentException('test'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Exception\InvalidArgumentException::class, $libraryException); + \Tester\Assert::false($phpException instanceof \Spameri\ElasticQuery\Exception\InvalidArgumentException); + } + +} + +(new InvalidArgumentException())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Exception/ResponseCouldNotBeMapped.phpt b/tests/SpameriTests/ElasticQuery/Exception/ResponseCouldNotBeMapped.phpt new file mode 100644 index 0000000..5d2f1f6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Exception/ResponseCouldNotBeMapped.phpt @@ -0,0 +1,105 @@ +getMessage()); + } + + + public function testWithCode(): void + { + $exception = new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped('message', 500); + + \Tester\Assert::same(500, $exception->getCode()); + } + + + public function testWithPreviousException(): void + { + $previous = new \Exception('Previous exception'); + $exception = new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped('message', 0, $previous); + + \Tester\Assert::same($previous, $exception->getPrevious()); + } + + + public function testCanBeThrown(): void + { + \Tester\Assert::exception( + static function (): void { + throw new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped('Mapping failed'); + }, + \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped::class, + ); + } + + + public function testCanBeCaughtAsInvalidArgumentException(): void + { + $caught = false; + try { + throw new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped('test'); + } catch (\InvalidArgumentException $e) { + $caught = true; + } + + \Tester\Assert::true($caught); + } + + + public function testSpecialCharactersInMessage(): void + { + $message = 'Error with "quotes" and special chars: <>&'; + $exception = new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped($message); + + \Tester\Assert::same(\json_encode($message), $exception->getMessage()); + } + + + public function testUnicodeInMessage(): void + { + $message = 'Chyba mapování odpovědi: žluťoučký kůň'; + $exception = new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped($message); + + \Tester\Assert::same(\json_encode($message), $exception->getMessage()); + } + + + public function testEmptyMessage(): void + { + $exception = new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped(''); + + \Tester\Assert::same('""', $exception->getMessage()); + } + + + public function testMessageWithNewlines(): void + { + $message = "Line1\nLine2\nLine3"; + $exception = new \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped($message); + + \Tester\Assert::same(\json_encode($message), $exception->getMessage()); + } + +} + +(new ResponseCouldNotBeMapped())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Filter/FilterCollection.phpt b/tests/SpameriTests/ElasticQuery/Filter/FilterCollection.phpt new file mode 100644 index 0000000..06525fe --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Filter/FilterCollection.phpt @@ -0,0 +1,275 @@ +toArray(); + + \Tester\Assert::same([], $array); + } + + + public function testToArrayWithSingleMust(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $array = $filter->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::count(1, $array['bool']['must']); + \Tester\Assert::same('active', $array['bool']['must'][0]['term']['status']['value']); + } + + + public function testToArrayWithMultipleMust(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'published')); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('type', 'article')); + + $array = $filter->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::count(2, $array['bool']['must']); + } + + + public function testToArrayWithRange(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Range('price', 10.0, 100.0)); + + $array = $filter->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::true(isset($array['bool']['must'][0]['range']['price'])); + } + + + public function testToArrayWithExists(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Exists('email')); + + $array = $filter->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::same('email', $array['bool']['must'][0]['exists']['field']); + } + + + public function testMustMethod(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + + $mustCollection = $filter->must(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Query\MustCollection::class, $mustCollection); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + + \Tester\Assert::same('', $filter->key()); + } + + + public function testConstructorWithMustCollection(): void + { + $mustCollection = new \Spameri\ElasticQuery\Query\MustCollection(); + $mustCollection->add(new \Spameri\ElasticQuery\Query\Term('field', 'value')); + + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection($mustCollection); + + $array = $filter->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::count(1, $array['bool']['must']); + } + + + public function testMustCollectionOperations(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('field1', 'value1')); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('field2', 'value2')); + + \Tester\Assert::same(2, $filter->must()->count()); + + $filter->must()->remove('term_field1_value1'); + + \Tester\Assert::same(1, $filter->must()->count()); + } + + + public function testMustCollectionGet(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $filter->must()->add($term); + + $retrieved = $filter->must()->get('term_status_active'); + + \Tester\Assert::same($term, $retrieved); + } + + + public function testMustCollectionIsValue(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + \Tester\Assert::true($filter->must()->isValue('term_status_active')); + \Tester\Assert::false($filter->must()->isValue('non_existent')); + } + + + public function testMustCollectionClear(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('field1', 'value1')); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('field2', 'value2')); + + \Tester\Assert::same(2, $filter->must()->count()); + + $filter->must()->clear(); + + \Tester\Assert::same(0, $filter->must()->count()); + \Tester\Assert::same([], $filter->toArray()); + } + + + public function testMustCollectionKeys(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('alpha', 'a')); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('beta', 'b')); + + $keys = $filter->must()->keys(); + + \Tester\Assert::contains('term_alpha_a', $keys); + \Tester\Assert::contains('term_beta_b', $keys); + } + + + public function testMustCollectionIterate(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('a', '1')); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('b', '2')); + + $count = 0; + foreach ($filter->must() as $query) { + \Tester\Assert::type(\Spameri\ElasticQuery\Query\LeafQueryInterface::class, $query); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testFilterContextStructure(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Range('age', 18.0, 65.0)); + + $array = $filter->toArray(); + + // Filter context should produce bool.must structure + \Tester\Assert::true(isset($array['bool'])); + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::true(\is_array($array['bool']['must'])); + } + + + public function testShouldArm(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->should()->add(new \Spameri\ElasticQuery\Query\Term('status', 'a')); + $filter->should()->add(new \Spameri\ElasticQuery\Query\Term('status', 'b')); + + $array = $filter->toArray(); + + \Tester\Assert::count(2, $array['bool']['should']); + } + + + public function testMustNotArm(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->mustNot()->add(new \Spameri\ElasticQuery\Query\Term('deleted', true)); + + $array = $filter->toArray(); + + \Tester\Assert::count(1, $array['bool']['must_not']); + } + + + public function testFilterArm(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->filter()->add(new \Spameri\ElasticQuery\Query\Term('region', 'eu')); + + $array = $filter->toArray(); + + \Tester\Assert::count(1, $array['bool']['filter']); + } + + + public function testAllArmsCombined(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('m', 'x')); + $filter->should()->add(new \Spameri\ElasticQuery\Query\Term('s', 'y')); + $filter->mustNot()->add(new \Spameri\ElasticQuery\Query\Term('mn', 'z')); + $filter->filter()->add(new \Spameri\ElasticQuery\Query\Term('f', 'w')); + + $array = $filter->toArray(); + + \Tester\Assert::count(1, $array['bool']['must']); + \Tester\Assert::count(1, $array['bool']['should']); + \Tester\Assert::count(1, $array['bool']['must_not']); + \Tester\Assert::count(1, $array['bool']['filter']); + } + + + public function testComplexFilterScenario(): void + { + $filter = new \Spameri\ElasticQuery\Filter\FilterCollection(); + + // Add multiple filter conditions + $filter->must()->add(new \Spameri\ElasticQuery\Query\Term('category', 'electronics')); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Range('price', 100.0, 1000.0)); + $filter->must()->add(new \Spameri\ElasticQuery\Query\Exists('stock')); + + $array = $filter->toArray(); + + \Tester\Assert::count(3, $array['bool']['must']); + + // Verify term filter + $termFound = false; + foreach ($array['bool']['must'] as $mustClause) { + if (isset($mustClause['term']['category'])) { + $termFound = true; + \Tester\Assert::same('electronics', $mustClause['term']['category']['value']); + } + } + \Tester\Assert::true($termFound); + } + +} + +(new FilterCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/FunctionScore.phpt b/tests/SpameriTests/ElasticQuery/FunctionScore.phpt new file mode 100644 index 0000000..e75e6e6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/FunctionScore.phpt @@ -0,0 +1,189 @@ + new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::true(isset($array['function_score'])); + \Tester\Assert::same($queryPart, $array['function_score']['query']); + \Tester\Assert::same([], $array['function_score']['functions']); + \Tester\Assert::false(isset($array['function_score']['score_mode'])); + } + + + public function testToArrayWithScoreMode(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MULTIPLY, + ); + + $queryPart = ['match_all' => new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::same('multiply', $array['function_score']['score_mode']); + } + + + public function testToArrayWithScoreModeSum(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_SUM, + ); + + $queryPart = ['match_all' => new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::same('sum', $array['function_score']['score_mode']); + } + + + public function testToArrayWithScoreModeAvg(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_AVG, + ); + + $queryPart = ['match_all' => new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::same('avg', $array['function_score']['score_mode']); + } + + + public function testToArrayWithScoreModeFirst(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_FIRST, + ); + + $queryPart = ['match_all' => new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::same('first', $array['function_score']['score_mode']); + } + + + public function testToArrayWithScoreModeMax(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MAX, + ); + + $queryPart = ['match_all' => new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::same('max', $array['function_score']['score_mode']); + } + + + public function testToArrayWithScoreModeMin(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MIN, + ); + + $queryPart = ['match_all' => new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::same('min', $array['function_score']['score_mode']); + } + + + public function testScoreModeMethod(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_AVG, + ); + + \Tester\Assert::same('avg', $functionScore->scoreMode()); + } + + + public function testScoreModeNullByDefault(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore(); + + \Tester\Assert::null($functionScore->scoreMode()); + } + + + public function testFunctionMethod(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore(); + + $collection = $functionScore->function(); + + \Tester\Assert::type(\Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection::class, $collection); + } + + + public function testConstructorWithFunctionScoreCollection(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('test_seed')); + + $functionScore = new \Spameri\ElasticQuery\FunctionScore($collection); + + \Tester\Assert::same(1, $functionScore->function()->count()); + } + + + public function testToArrayWithFunctions(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore(); + $functionScore->function()->add( + new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('popularity'), + ); + + $queryPart = ['match_all' => new \stdClass()]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::count(1, $array['function_score']['functions']); + \Tester\Assert::true(isset($array['function_score']['functions'][0]['field_value_factor'])); + } + + + public function testToArrayWithMultipleFunctions(): void + { + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_SUM, + ); + $functionScore->function()->add( + new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('popularity'), + ); + $functionScore->function()->add( + new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('abc123'), + ); + + $queryPart = ['match' => ['title' => 'test']]; + $array = $functionScore->toArray($queryPart); + + \Tester\Assert::count(2, $array['function_score']['functions']); + \Tester\Assert::same('sum', $array['function_score']['score_mode']); + } + + + public function testConstants(): void + { + \Tester\Assert::same('multiply', \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MULTIPLY); + \Tester\Assert::same('sum', \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_SUM); + \Tester\Assert::same('avg', \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_AVG); + \Tester\Assert::same('first', \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_FIRST); + \Tester\Assert::same('max', \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MAX); + \Tester\Assert::same('min', \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MIN); + } + +} + +(new FunctionScore())->run(); diff --git a/tests/SpameriTests/ElasticQuery/FunctionScore/Decay.phpt b/tests/SpameriTests/ElasticQuery/FunctionScore/Decay.phpt new file mode 100644 index 0000000..21c2643 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/FunctionScore/Decay.phpt @@ -0,0 +1,122 @@ + [ + 'properties' => [ + 'location' => ['type' => 'geo_point'], + 'price' => ['type' => 'long'], + ], + ], + ]; + } + + + public function testGaussToArray(): void + { + $gauss = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Decay\Gauss( + field: 'price', + origin: 100, + scale: 50, + offset: 5, + decay: 0.5, + multiValueMode: 'avg', + ); + + $array = $gauss->toArray(); + + \Tester\Assert::same(100, $array['gauss']['price']['origin']); + \Tester\Assert::same(50, $array['gauss']['price']['scale']); + \Tester\Assert::same(5, $array['gauss']['price']['offset']); + \Tester\Assert::same(0.5, $array['gauss']['price']['decay']); + \Tester\Assert::same('avg', $array['gauss']['multi_value_mode']); + } + + + public function testLinearToArray(): void + { + $linear = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Decay\Linear( + field: 'price', + origin: 100, + scale: 50, + ); + + \Tester\Assert::same(100, $linear->toArray()['linear']['price']['origin']); + } + + + public function testExpToArray(): void + { + $exp = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Decay\Exp( + field: 'price', + origin: 100, + scale: 50, + ); + + \Tester\Assert::same(100, $exp->toArray()['exp']['price']['origin']); + } + + + public function testCreateGauss(): void + { + $this->indexDocument(['price' => 100]); + $this->indexDocument(['price' => 1000]); + + $gauss = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Decay\Gauss( + field: 'price', + origin: 100, + scale: 50, + ); + + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection($gauss), + scoreMode: \Spameri\ElasticQuery\FunctionScore::SCORE_MODE_MULTIPLY, + boostMode: \Spameri\ElasticQuery\FunctionScore::BOOST_MODE_REPLACE, + boost: 1.0, + maxBoost: 10.0, + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(functionScore: $functionScore); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + + + public function testCreateGeoGauss(): void + { + $this->indexDocument(['location' => ['lat' => 50, 'lon' => 14]]); + + $gauss = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Decay\Gauss( + field: 'location', + origin: ['lat' => 50, 'lon' => 14], + scale: '10km', + offset: '1km', + decay: 0.5, + ); + + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection($gauss), + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(functionScore: $functionScore); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Decay())->run(); diff --git a/tests/SpameriTests/ElasticQuery/FunctionScore/FunctionScoreCollection.phpt b/tests/SpameriTests/ElasticQuery/FunctionScore/FunctionScoreCollection.phpt new file mode 100644 index 0000000..2a0024c --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/FunctionScore/FunctionScoreCollection.phpt @@ -0,0 +1,196 @@ +count()); + } + + + public function testAddSingleItem(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('test')); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testAddMultipleItems(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('seed1')); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('popularity')); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight( + 2.0, + new \Spameri\ElasticQuery\Query\Term('featured', true), + )); + + \Tester\Assert::same(3, $collection->count()); + } + + + public function testConstructorWithItems(): void + { + $random = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('seed'); + $fieldValue = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('views'); + + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection($random, $fieldValue); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testGet(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $fieldValue = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('likes'); + $collection->add($fieldValue); + + $retrieved = $collection->get('field_value_factor_likes'); + + \Tester\Assert::same($fieldValue, $retrieved); + } + + + public function testGetNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + + $retrieved = $collection->get('non_existent'); + + \Tester\Assert::null($retrieved); + } + + + public function testRemove(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('to_remove')); + + \Tester\Assert::same(1, $collection->count()); + + $result = $collection->remove('random_to_remove'); + + \Tester\Assert::true($result); + \Tester\Assert::same(0, $collection->count()); + } + + + public function testRemoveNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + + $result = $collection->remove('non_existent'); + + \Tester\Assert::false($result); + } + + + public function testIsValue(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('rating')); + + \Tester\Assert::true($collection->isValue('field_value_factor_rating')); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testKeys(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('seed1')); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('popularity')); + + $keys = $collection->keys(); + + \Tester\Assert::contains('random_seed1', $keys); + \Tester\Assert::contains('field_value_factor_popularity', $keys); + } + + + public function testClear(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('seed1')); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('views')); + + \Tester\Assert::same(2, $collection->count()); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + } + + + public function testIterator(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('a')); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('b')); + + $count = 0; + foreach ($collection as $key => $item) { + \Tester\Assert::type(\Spameri\ElasticQuery\FunctionScore\FunctionScoreInterface::class, $item); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testSameKeyReplaces(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'popularity', + factor: 1.0, + )); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'popularity', + factor: 2.0, + )); + + \Tester\Assert::same(1, $collection->count()); + + $retrieved = $collection->get('field_value_factor_popularity'); + \Tester\Assert::same(2.0, $retrieved->factor()); + } + + + public function testMixedFunctionTypes(): void + { + $collection = new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection(); + + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('user_123')); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'likes', + factor: 1.2, + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LOG1P, + )); + $collection->add(new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight( + 3.0, + new \Spameri\ElasticQuery\Query\Term('premium', true), + )); + + \Tester\Assert::same(3, $collection->count()); + + // Verify each type is retrievable + \Tester\Assert::notNull($collection->get('random_user_123')); + \Tester\Assert::notNull($collection->get('field_value_factor_likes')); + \Tester\Assert::notNull($collection->get('weight_term_premium_1')); + } + +} + +(new FunctionScoreCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/FieldValueFactor.phpt b/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/FieldValueFactor.phpt new file mode 100644 index 0000000..f66640c --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/FieldValueFactor.phpt @@ -0,0 +1,202 @@ +toArray(); + + \Tester\Assert::same([ + 'field_value_factor' => [ + 'field' => 'popularity', + 'factor' => 1.0, + 'modifier' => 'none', + 'missing' => 1.0, + ], + ], $array); + } + + + public function testToArrayWithFactor(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'likes', + factor: 1.2, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same(1.2, $array['field_value_factor']['factor']); + } + + + public function testToArrayWithModifierLog(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'views', + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LOG, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same('log', $array['field_value_factor']['modifier']); + } + + + public function testToArrayWithModifierLog1p(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'views', + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LOG1P, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same('log1p', $array['field_value_factor']['modifier']); + } + + + public function testToArrayWithModifierSqrt(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'rating', + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_SQRT, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same('sqrt', $array['field_value_factor']['modifier']); + } + + + public function testToArrayWithModifierSquare(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'score', + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_SQUARE, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same('square', $array['field_value_factor']['modifier']); + } + + + public function testToArrayWithModifierReciprocal(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'age', + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_RECIPROCAL, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same('reciprocal', $array['field_value_factor']['modifier']); + } + + + public function testToArrayWithMissing(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'boost_value', + missing: 0.5, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same(0.5, $array['field_value_factor']['missing']); + } + + + public function testToArrayFullOptions(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'popularity', + factor: 2.5, + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LN1P, + missing: 0.0, + ); + + $array = $fieldValueFactor->toArray(); + + \Tester\Assert::same('popularity', $array['field_value_factor']['field']); + \Tester\Assert::same(2.5, $array['field_value_factor']['factor']); + \Tester\Assert::same('ln1p', $array['field_value_factor']['modifier']); + \Tester\Assert::same(0.0, $array['field_value_factor']['missing']); + } + + + public function testKey(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('boost_field'); + + \Tester\Assert::same('field_value_factor_boost_field', $fieldValueFactor->key()); + } + + + public function testFieldMethod(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor('test_field'); + + \Tester\Assert::same('test_field', $fieldValueFactor->field()); + } + + + public function testFactorMethod(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'field', + factor: 3.5, + ); + + \Tester\Assert::same(3.5, $fieldValueFactor->factor()); + } + + + public function testModifierMethod(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'field', + modifier: \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LN, + ); + + \Tester\Assert::same('ln', $fieldValueFactor->modifier()); + } + + + public function testMissingMethod(): void + { + $fieldValueFactor = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor( + field: 'field', + missing: 2.0, + ); + + \Tester\Assert::same(2.0, $fieldValueFactor->missing()); + } + + + public function testConstants(): void + { + \Tester\Assert::same('none', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_NONE); + \Tester\Assert::same('log', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LOG); + \Tester\Assert::same('log1p', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LOG1P); + \Tester\Assert::same('log2p', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LOG2P); + \Tester\Assert::same('ln', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LN); + \Tester\Assert::same('ln1p', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LN1P); + \Tester\Assert::same('ln2p', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_LN2P); + \Tester\Assert::same('square', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_SQUARE); + \Tester\Assert::same('sqrt', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_SQRT); + \Tester\Assert::same('reciprocal', \Spameri\ElasticQuery\FunctionScore\ScoreFunction\FieldValueFactor::MODIFIER_RECIPROCAL); + } + +} + +(new FieldValueFactor())->run(); diff --git a/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/RandomScore.phpt b/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/RandomScore.phpt new file mode 100644 index 0000000..92c8201 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/RandomScore.phpt @@ -0,0 +1,108 @@ +toArray(); + + \Tester\Assert::true(isset($array['random_score'])); + \Tester\Assert::type(\stdClass::class, $array['random_score']); + } + + + public function testToArrayWithSeed(): void + { + $randomScore = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('user_session_123'); + + $array = $randomScore->toArray(); + + \Tester\Assert::true(isset($array['random_score'])); + \Tester\Assert::same('user_session_123', $array['random_score']->seed); + } + + + public function testToArrayWithNumericSeed(): void + { + $randomScore = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('12345'); + + $array = $randomScore->toArray(); + + \Tester\Assert::same('12345', $array['random_score']->seed); + } + + + public function testKeyWithSeed(): void + { + $randomScore = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('my_seed'); + + \Tester\Assert::same('random_my_seed', $randomScore->key()); + } + + + public function testKeyWithoutSeed(): void + { + $randomScore = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore(); + + \Tester\Assert::same('random_', $randomScore->key()); + } + + + public function testSeedMethodWithSeed(): void + { + $randomScore = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('test_seed'); + + \Tester\Assert::same('test_seed', $randomScore->seed()); + } + + + public function testSeedMethodWithoutSeed(): void + { + $randomScore = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore(); + + \Tester\Assert::null($randomScore->seed()); + } + + + public function testToArrayEmptyObjectWhenNoSeed(): void + { + $randomScore = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore(); + + $array = $randomScore->toArray(); + + // When no seed, random_score should be empty stdClass + $stdClass = $array['random_score']; + \Tester\Assert::type(\stdClass::class, $stdClass); + \Tester\Assert::same([], (array) $stdClass); + } + + + public function testConsistentResultsWithSameSeed(): void + { + $seed = 'consistent_seed_value'; + $randomScore1 = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore($seed); + $randomScore2 = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore($seed); + + \Tester\Assert::same($randomScore1->toArray()['random_score']->seed, $randomScore2->toArray()['random_score']->seed); + } + + + public function testDifferentSeeds(): void + { + $randomScore1 = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('seed_a'); + $randomScore2 = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\RandomScore('seed_b'); + + \Tester\Assert::notSame($randomScore1->key(), $randomScore2->key()); + } + +} + +(new RandomScore())->run(); diff --git a/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/Weight.phpt b/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/Weight.phpt new file mode 100644 index 0000000..2b09a49 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/FunctionScore/ScoreFunction/Weight.phpt @@ -0,0 +1,132 @@ +toArray(); + + \Tester\Assert::same(2.0, $array['weight']); + \Tester\Assert::true(isset($array['filter']['term']['status'])); + } + + + public function testToArrayWithDifferentWeight(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('featured', true); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(5.0, $term); + + $array = $weight->toArray(); + + \Tester\Assert::same(5.0, $array['weight']); + } + + + public function testToArrayWithRangeFilter(): void + { + $range = new \Spameri\ElasticQuery\Query\Range('price', 0.0, 100.0); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(1.5, $range); + + $array = $weight->toArray(); + + \Tester\Assert::same(1.5, $array['weight']); + \Tester\Assert::true(isset($array['filter']['range']['price'])); + } + + + public function testToArrayWithExistsFilter(): void + { + $exists = new \Spameri\ElasticQuery\Query\Exists('discount'); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(3.0, $exists); + + $array = $weight->toArray(); + + \Tester\Assert::same(3.0, $array['weight']); + \Tester\Assert::same('discount', $array['filter']['exists']['field']); + } + + + public function testToArrayWithMatchFilter(): void + { + $match = new \Spameri\ElasticQuery\Query\ElasticMatch('category', 'electronics'); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(2.5, $match); + + $array = $weight->toArray(); + + \Tester\Assert::same(2.5, $array['weight']); + \Tester\Assert::true(isset($array['filter']['match'])); + } + + + public function testKey(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('type', 'featured'); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(2.0, $term); + + \Tester\Assert::same('weight_term_type_featured', $weight->key()); + } + + + public function testWeightMethod(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('active', true); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(4.5, $term); + + \Tester\Assert::same(4.5, $weight->weight()); + } + + + public function testLeafQueryMethod(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('field', 'value'); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(1.0, $term); + + \Tester\Assert::same($term, $weight->leafQuery()); + } + + + public function testFilterStructure(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('category', 'books'); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(2.0, $term); + + $array = $weight->toArray(); + + // Verify that filter contains the full query array + \Tester\Assert::same($term->toArray(), $array['filter']); + } + + + public function testZeroWeight(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('hidden', true); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(0.0, $term); + + $array = $weight->toArray(); + + \Tester\Assert::same(0.0, $array['weight']); + } + + + public function testNegativeWeight(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('deprecated', true); + $weight = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\Weight(-1.0, $term); + + $array = $weight->toArray(); + + \Tester\Assert::same(-1.0, $array['weight']); + } + +} + +(new Weight())->run(); diff --git a/tests/SpameriTests/ElasticQuery/FunctionScore/ScriptScore.phpt b/tests/SpameriTests/ElasticQuery/FunctionScore/ScriptScore.phpt new file mode 100644 index 0000000..b7f0207 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/FunctionScore/ScriptScore.phpt @@ -0,0 +1,52 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + $ss = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\ScriptScore( + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].value * 2"), + ); + + $array = $ss->toArray(); + + \Tester\Assert::same("doc['price'].value * 2", $array['script_score']['script']['source']); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + + $ss = new \Spameri\ElasticQuery\FunctionScore\ScoreFunction\ScriptScore( + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].value * 2"), + ); + + $functionScore = new \Spameri\ElasticQuery\FunctionScore( + new \Spameri\ElasticQuery\FunctionScore\FunctionScoreCollection($ss), + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(functionScore: $functionScore); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new ScriptScore())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Highlight.phpt b/tests/SpameriTests/ElasticQuery/Highlight.phpt new file mode 100644 index 0000000..ebbefc7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Highlight.phpt @@ -0,0 +1,210 @@ +'], + postTags: [''], + fields: ['title'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::same([''], $array['pre_tags']); + \Tester\Assert::same([''], $array['post_tags']); + \Tester\Assert::true(isset($array['fields']['title'])); + } + + + public function testToArrayWithMultipleFields(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['title', 'content', 'description'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::true(isset($array['fields']['title'])); + \Tester\Assert::true(isset($array['fields']['content'])); + \Tester\Assert::true(isset($array['fields']['description'])); + } + + + public function testToArrayWithMultipleTags(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: ['', ''], + postTags: ['', ''], + fields: ['body'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::same(['', ''], $array['pre_tags']); + \Tester\Assert::same(['', ''], $array['post_tags']); + } + + + public function testToArrayFieldsHaveNumberOfFragments(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['summary'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::same(0, $array['fields']['summary']['number_of_fragments']); + } + + + public function testToArrayAllFieldsHaveNumberOfFragments(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['field1', 'field2', 'field3'], + ); + + $array = $highlight->toArray(); + + foreach (['field1', 'field2', 'field3'] as $field) { + \Tester\Assert::same(0, $array['fields'][$field]['number_of_fragments']); + } + } + + + public function testToArrayWithEmptyFields(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: [], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::same([''], $array['pre_tags']); + \Tester\Assert::same([''], $array['post_tags']); + \Tester\Assert::false(isset($array['fields'])); + } + + + public function testToArrayWithEmptyTags(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [], + postTags: [], + fields: ['content'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::same([], $array['pre_tags']); + \Tester\Assert::same([], $array['post_tags']); + } + + + public function testToArrayWithHtmlTags(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['text'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::same([''], $array['pre_tags']); + } + + + public function testToArrayStructure(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['title', 'body'], + ); + + $array = $highlight->toArray(); + + // Verify structure matches Elasticsearch highlight format + \Tester\Assert::true(\array_key_exists('pre_tags', $array)); + \Tester\Assert::true(\array_key_exists('post_tags', $array)); + \Tester\Assert::true(\array_key_exists('fields', $array)); + \Tester\Assert::true(\is_array($array['fields'])); + } + + + public function testImplementsArrayInterface(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['content'], + ); + + \Tester\Assert::type(\Spameri\ElasticQuery\Entity\ArrayInterface::class, $highlight); + } + + + public function testToArrayWithSpecialCharactersInTags(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: ['[[HIGHLIGHT]]'], + postTags: ['[[/HIGHLIGHT]]'], + fields: ['data'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::same(['[[HIGHLIGHT]]'], $array['pre_tags']); + \Tester\Assert::same(['[[/HIGHLIGHT]]'], $array['post_tags']); + } + + + public function testToArrayWithNestedFieldNames(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['user.name', 'address.city', 'metadata.tags'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::true(isset($array['fields']['user.name'])); + \Tester\Assert::true(isset($array['fields']['address.city'])); + \Tester\Assert::true(isset($array['fields']['metadata.tags'])); + } + + + public function testToArrayWithWildcardField(): void + { + $highlight = new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: ['*'], + ); + + $array = $highlight->toArray(); + + \Tester\Assert::true(isset($array['fields']['*'])); + \Tester\Assert::same(0, $array['fields']['*']['number_of_fragments']); + } + +} + +(new Highlight())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Highlight/HighlightField.phpt b/tests/SpameriTests/ElasticQuery/Highlight/HighlightField.phpt new file mode 100644 index 0000000..7af981d --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Highlight/HighlightField.phpt @@ -0,0 +1,91 @@ + [ + 'properties' => [ + 'title' => ['type' => 'text'], + 'body' => ['type' => 'text'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $field = new \Spameri\ElasticQuery\Highlight\HighlightField( + field: 'title', + type: 'unified', + numberOfFragments: 3, + fragmentSize: 150, + boundaryScanner: 'sentence', + encoder: 'html', + fragmenter: 'span', + noMatchSize: 100, + order: 'score', + phraseLimit: 256, + requireFieldMatch: false, + preTags: [''], + postTags: [''], + ); + + $array = $field->toArray(); + + \Tester\Assert::same('unified', $array['type']); + \Tester\Assert::same(3, $array['number_of_fragments']); + \Tester\Assert::same(150, $array['fragment_size']); + \Tester\Assert::same('sentence', $array['boundary_scanner']); + \Tester\Assert::same('html', $array['encoder']); + \Tester\Assert::same('span', $array['fragmenter']); + \Tester\Assert::same(100, $array['no_match_size']); + \Tester\Assert::same('score', $array['order']); + \Tester\Assert::same(256, $array['phrase_limit']); + \Tester\Assert::false($array['require_field_match']); + \Tester\Assert::same([''], $array['pre_tags']); + } + + + public function testCreate(): void + { + $this->indexDocument(['title' => 'quick brown fox', 'body' => 'jumps over the lazy dog']); + + $fields = new \Spameri\ElasticQuery\Highlight\HighlightFieldCollection(); + $fields->add(new \Spameri\ElasticQuery\Highlight\HighlightField( + field: 'title', + numberOfFragments: 0, + type: 'unified', + )); + $fields->add(new \Spameri\ElasticQuery\Highlight\HighlightField( + field: 'body', + numberOfFragments: 3, + fragmentSize: 50, + )); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery( + highlight: new \Spameri\ElasticQuery\Highlight( + preTags: [''], + postTags: [''], + fields: $fields, + ), + ); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'fox')); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new HighlightField())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/CzechDictionary.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/CzechDictionary.phpt index 5bedec6..e8f5521 100644 --- a/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/CzechDictionary.phpt +++ b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/CzechDictionary.phpt @@ -23,14 +23,13 @@ class CzechDictionary extends \Tester\TestCase self::INDEX, new \Spameri\ElasticQuery\Document\Body\Plain( $settings->toArray() - ), - self::INDEX + ) ); // Set up index and analyzer $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/'); + \curl_setopt($ch, CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/'); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); @@ -45,7 +44,7 @@ class CzechDictionary extends \Tester\TestCase // Fetch settings and test if analyzer is configured - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_settings'); + \curl_setopt($ch, CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_settings'); \curl_setopt($ch, CURLOPT_POSTFIELDS, []); \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); @@ -67,7 +66,7 @@ class CzechDictionary extends \Tester\TestCase $text = 'Playstation 4 je nejlepší se SodaStream drinkem a kouskem GS-condro!'; - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_analyze'); + \curl_setopt($ch, CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_analyze'); \curl_setopt($ch, CURLOPT_POSTFIELDS, \json_encode([ 'text' => $text, 'analyzer' => 'czechDictionary', @@ -85,22 +84,18 @@ class CzechDictionary extends \Tester\TestCase \Tester\Assert::same('lepsi', $responseAnalyzer['tokens'][2]['token']); \Tester\Assert::same('drink', $responseAnalyzer['tokens'][4]['token']); \Tester\Assert::same('kousek', $responseAnalyzer['tokens'][5]['token']); - - \curl_close($ch); } protected function tearDown(): void { $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); + \curl_setopt($ch, CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); \curl_exec($ch); - - \curl_close($ch); } } diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/EnglishDictionary.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/EnglishDictionary.phpt new file mode 100644 index 0000000..de52867 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/EnglishDictionary.phpt @@ -0,0 +1,129 @@ +name()); + \Tester\Assert::same(\Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary::NAME, $analyzer->name()); + } + + + public function testKey(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + \Tester\Assert::same('englishDictionary', $analyzer->key()); + } + + + public function testGetType(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + \Tester\Assert::same('custom', $analyzer->getType()); + } + + + public function testTokenizer(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + \Tester\Assert::same('standard', $analyzer->tokenizer()); + } + + + public function testFilter(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + $filter = $analyzer->filter(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection::class, $filter); + \Tester\Assert::true($filter->count() > 0); + } + + + public function testToArray(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + $array = $analyzer->toArray(); + + \Tester\Assert::true(isset($array['englishDictionary'])); + \Tester\Assert::same('custom', $array['englishDictionary']['type']); + \Tester\Assert::same('standard', $array['englishDictionary']['tokenizer']); + \Tester\Assert::true(isset($array['englishDictionary']['filter'])); + \Tester\Assert::type('array', $array['englishDictionary']['filter']); + } + + + public function testToArrayContainsFilters(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + $array = $analyzer->toArray(); + + $filters = $array['englishDictionary']['filter']; + + // Should contain lowercase, stop, hunspell, unique, asciifolding filters + \Tester\Assert::contains('lowercase', $filters); + \Tester\Assert::contains('asciifolding', $filters); + \Tester\Assert::contains('unique', $filters); + } + + + public function testGetStopFilterNull(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + \Tester\Assert::null($analyzer->getStopFilter()); + } + + + public function testGetStopFilterCustom(): void + { + $customStopFilter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary($customStopFilter); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Filter\AbstractStop::class, $analyzer->getStopFilter()); + } + + + public function testImplementsCustomAnalyzerInterface(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\CustomAnalyzerInterface::class, $analyzer); + } + + + public function testImplementsItemInterface(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Collection\Item::class, $analyzer); + } + + + public function testFilterIsCached(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + $filter1 = $analyzer->filter(); + $filter2 = $analyzer->filter(); + + \Tester\Assert::same($filter1, $filter2); + } + +} + +(new EnglishDictionary())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/GermanDictionary.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/GermanDictionary.phpt new file mode 100644 index 0000000..efd47fb --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Custom/GermanDictionary.phpt @@ -0,0 +1,67 @@ +name()); + \Tester\Assert::same(\Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary::NAME, $analyzer->name()); + } + + + public function testFilter(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary(); + + $filter = $analyzer->filter(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection::class, $filter); + \Tester\Assert::true($filter->count() > 0); + } + + + public function testFilterContainsExpectedFilters(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary(); + + $filter = $analyzer->filter(); + + \Tester\Assert::true($filter->isValue('lowercase')); + \Tester\Assert::true($filter->isValue('germanStopWords')); + \Tester\Assert::true($filter->isValue('unique')); + \Tester\Assert::true($filter->isValue('asciifolding')); + } + + + public function testFilterIsCached(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary(); + + $filter1 = $analyzer->filter(); + $filter2 = $analyzer->filter(); + + \Tester\Assert::same($filter1, $filter2); + } + + + public function testWithCustomStopFilter(): void + { + $customStop = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary($customStop); + + $filter = $analyzer->filter(); + + \Tester\Assert::true($filter->isValue('englishStopWords')); + } + +} + +(new GermanDictionary())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Pattern.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Pattern.phpt new file mode 100644 index 0000000..6be8451 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Pattern.phpt @@ -0,0 +1,111 @@ +name()); + } + + + public function testGetType(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern('[^a-zA-Z]'); + + \Tester\Assert::same('pattern', $analyzer->getType()); + } + + + public function testToArrayBasic(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern('[^a-zA-Z]'); + + $array = $analyzer->toArray(); + + \Tester\Assert::true(isset($array['pattern'])); + \Tester\Assert::same('pattern', $array['pattern']['type']); + \Tester\Assert::same('[^a-zA-Z]', $array['pattern']['pattern']); + \Tester\Assert::same(true, $array['pattern']['lowercase']); + \Tester\Assert::same([], $array['pattern']['stopwords']); + \Tester\Assert::null($array['pattern']['flags']); + } + + + public function testToArrayWithStopWords(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern( + '\\W+', + ['the', 'a'], + ); + + $array = $analyzer->toArray(); + + \Tester\Assert::same(['the', 'a'], $array['pattern']['stopwords']); + } + + + public function testToArrayWithLowercaseFalse(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern( + '\\s+', + [], + false, + ); + + $array = $analyzer->toArray(); + + \Tester\Assert::same(false, $array['pattern']['lowercase']); + } + + + public function testToArrayWithFlags(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern( + '\\W+', + [], + true, + 'CASE_INSENSITIVE|COMMENTS', + ); + + $array = $analyzer->toArray(); + + \Tester\Assert::same('CASE_INSENSITIVE|COMMENTS', $array['pattern']['flags']); + } + + + public function testToArrayWithAllOptions(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern( + '[^a-zA-Z0-9]', + ['stop', 'words'], + false, + 'UNICODE_CASE', + ); + + $array = $analyzer->toArray(); + + \Tester\Assert::same('[^a-zA-Z0-9]', $array['pattern']['pattern']); + \Tester\Assert::same(['stop', 'words'], $array['pattern']['stopwords']); + \Tester\Assert::same(false, $array['pattern']['lowercase']); + \Tester\Assert::same('UNICODE_CASE', $array['pattern']['flags']); + } + + + public function testImplementsAnalyzerInterface(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Pattern('\\W+'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\AnalyzerInterface::class, $analyzer); + } + +} + +(new Pattern())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Standard.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Standard.phpt new file mode 100644 index 0000000..d254dd2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Standard.phpt @@ -0,0 +1,88 @@ +name()); + } + + + public function testGetType(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Standard(); + + \Tester\Assert::same('standard', $analyzer->getType()); + } + + + public function testToArrayDefault(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Standard(); + + $array = $analyzer->toArray(); + + \Tester\Assert::true(isset($array['standard'])); + \Tester\Assert::same('standard', $array['standard']['type']); + \Tester\Assert::same(5, $array['standard']['max_token_length']); + \Tester\Assert::same([], $array['standard']['stopwords']); + } + + + public function testToArrayWithStopWords(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Standard( + ['the', 'a', 'an'], + ); + + $array = $analyzer->toArray(); + + \Tester\Assert::same(['the', 'a', 'an'], $array['standard']['stopwords']); + } + + + public function testToArrayWithMaxTokenLength(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Standard( + [], + 10, + ); + + $array = $analyzer->toArray(); + + \Tester\Assert::same(10, $array['standard']['max_token_length']); + } + + + public function testToArrayWithBothOptions(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Standard( + ['stop', 'words'], + 15, + ); + + $array = $analyzer->toArray(); + + \Tester\Assert::same(['stop', 'words'], $array['standard']['stopwords']); + \Tester\Assert::same(15, $array['standard']['max_token_length']); + } + + + public function testImplementsAnalyzerInterface(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Standard(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\AnalyzerInterface::class, $analyzer); + } + +} + +(new Standard())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Whitespace.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Whitespace.phpt new file mode 100644 index 0000000..ba7b31b --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Analyzer/Whitespace.phpt @@ -0,0 +1,47 @@ +name()); + } + + + public function testGetType(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Whitespace(); + + \Tester\Assert::same('whitespace', $analyzer->getType()); + } + + + public function testToArray(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Whitespace(); + + $array = $analyzer->toArray(); + + \Tester\Assert::true(isset($array['whitespace'])); + \Tester\Assert::same('whitespace', $array['whitespace']['type']); + } + + + public function testImplementsAnalyzerInterface(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Whitespace(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\AnalyzerInterface::class, $analyzer); + } + +} + +(new Whitespace())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/EdgeNgram.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/EdgeNgram.phpt new file mode 100644 index 0000000..5dd20fd --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/EdgeNgram.phpt @@ -0,0 +1,79 @@ +getType()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\EdgeNgram(); + + \Tester\Assert::same('customEdgeNgram', $filter->key()); + } + + + public function testToArrayWithDefaults(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\EdgeNgram(); + + $expected = [ + 'customEdgeNgram' => [ + 'type' => 'edge_ngram', + 'min_gram' => 2, + 'max_gram' => 6, + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testToArrayWithCustomValues(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\EdgeNgram(3, 10); + + $expected = [ + 'customEdgeNgram' => [ + 'type' => 'edge_ngram', + 'min_gram' => 3, + 'max_gram' => 10, + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\EdgeNgram(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + + + public function testMinGramOnly(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\EdgeNgram(1); + + $result = $filter->toArray(); + + \Tester\Assert::same(1, $result['customEdgeNgram']['min_gram']); + \Tester\Assert::same(6, $result['customEdgeNgram']['max_gram']); + } + +} + +(new EdgeNgram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/Ngram.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Ngram.phpt new file mode 100644 index 0000000..456ccac --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Ngram.phpt @@ -0,0 +1,50 @@ +getType()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Ngram(); + + \Tester\Assert::same('ngram', $filter->key()); + } + + + public function testToArray(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Ngram(); + + $expected = [ + 'ngram' => [ + 'type' => 'ngram', + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Ngram(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + +} + +(new Ngram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/PorterStem.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/PorterStem.phpt new file mode 100644 index 0000000..4621bd6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/PorterStem.phpt @@ -0,0 +1,50 @@ +getType()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\PorterStem(); + + \Tester\Assert::same('customPorterStem', $filter->key()); + } + + + public function testToArray(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\PorterStem(); + + $expected = [ + 'customPorterStem' => [ + 'type' => 'porter_stem', + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\PorterStem(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + +} + +(new PorterStem())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/Snowball.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Snowball.phpt new file mode 100644 index 0000000..6cb1e93 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Snowball.phpt @@ -0,0 +1,50 @@ +getType()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Snowball(); + + \Tester\Assert::same('snowball', $filter->key()); + } + + + public function testToArray(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Snowball(); + + $expected = [ + 'snowball' => [ + 'type' => 'snowball', + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Snowball(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + +} + +(new Snowball())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/Stemmer.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Stemmer.phpt new file mode 100644 index 0000000..6b07e78 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Stemmer.phpt @@ -0,0 +1,50 @@ +getType()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(); + + \Tester\Assert::same('stemmer', $filter->key()); + } + + + public function testToArray(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(); + + $expected = [ + 'stemmer' => [ + 'type' => 'stemmer', + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + +} + +(new Stemmer())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/Stop/English.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Stop/English.phpt new file mode 100644 index 0000000..8bd102e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Stop/English.phpt @@ -0,0 +1,86 @@ +getType()); + } + + + public function testGetName(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + + \Tester\Assert::same('englishStopWords', $filter->getName()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + + \Tester\Assert::same('englishStopWords', $filter->key()); + } + + + public function testGetStopWordsContainsEnglishConstant(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + + $stopWords = $filter->getStopWords(); + + \Tester\Assert::contains(\Spameri\ElasticQuery\Mapping\Analyzer\Stop\StopWords::ENGLISH, $stopWords); + } + + + public function testToArray(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + + $result = $filter->toArray(); + + \Tester\Assert::true(isset($result['englishStopWords'])); + \Tester\Assert::same('stop', $result['englishStopWords']['type']); + \Tester\Assert::true(isset($result['englishStopWords']['stopwords'])); + } + + + public function testWithExtraWords(): void + { + $extraWords = ['custom', 'words']; + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English($extraWords); + + $stopWords = $filter->getStopWords(); + + \Tester\Assert::contains('custom', $stopWords); + \Tester\Assert::contains('words', $stopWords); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + + + public function testExtendsAbstractStop(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Stop\English(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Filter\AbstractStop::class, $filter); + } + +} + +(new English())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/Synonym.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Synonym.phpt new file mode 100644 index 0000000..33e8e0e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/Synonym.phpt @@ -0,0 +1,87 @@ +getType()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Synonym(); + + \Tester\Assert::same('customSynonyms', $filter->key()); + } + + + public function testGetName(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Synonym(); + + \Tester\Assert::same('customSynonyms', $filter->getName()); + } + + + public function testToArrayWithEmptySynonyms(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Synonym(); + + $expected = [ + 'customSynonyms' => [ + 'type' => 'synonym', + 'synonyms' => [], + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testToArrayWithSynonyms(): void + { + $synonyms = [ + 'quick' => 'fast', + 'big' => 'large', + ]; + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Synonym($synonyms); + + $result = $filter->toArray(); + + \Tester\Assert::same('synonym', $result['customSynonyms']['type']); + \Tester\Assert::contains('quick => fast', $result['customSynonyms']['synonyms']); + \Tester\Assert::contains('big => large', $result['customSynonyms']['synonyms']); + } + + + public function testGetSynonyms(): void + { + $synonyms = [ + 'quick' => 'fast', + 'big' => 'large', + ]; + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Synonym($synonyms); + + \Tester\Assert::same($synonyms, $filter->getSynonyms()); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\Synonym(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + +} + +(new Synonym())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Filter/SynonymGraph.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Filter/SynonymGraph.phpt new file mode 100644 index 0000000..bbef202 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Filter/SynonymGraph.phpt @@ -0,0 +1,50 @@ +getType()); + } + + + public function testKey(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\SynonymGraph(); + + \Tester\Assert::same('synonym_graph', $filter->key()); + } + + + public function testToArray(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\SynonymGraph(); + + $expected = [ + 'synonym_graph' => [ + 'type' => 'synonym_graph', + ], + ]; + + \Tester\Assert::same($expected, $filter->toArray()); + } + + + public function testImplementsFilterInterface(): void + { + $filter = new \Spameri\ElasticQuery\Mapping\Filter\SynonymGraph(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + +} + +(new SynonymGraph())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings.phpt new file mode 100644 index 0000000..51ebdfe --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings.phpt @@ -0,0 +1,303 @@ +toArray(); + + \Tester\Assert::true(isset($array['settings'])); + \Tester\Assert::true(isset($array['settings']['analysis'])); + \Tester\Assert::true(isset($array['mappings'])); + \Tester\Assert::true(isset($array['mappings']['properties'])); + \Tester\Assert::same(true, $array['mappings']['dynamic']); + } + + + public function testIndexName(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('my_index'); + + \Tester\Assert::same('my_index', $settings->indexName()); + } + + + public function testChangeIndexName(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('old_index'); + + $settings->changeIndexName('new_index'); + + \Tester\Assert::same('new_index', $settings->indexName()); + } + + + public function testHasSti(): void + { + $settingsWithoutSti = new \Spameri\ElasticQuery\Mapping\Settings('index', false); + $settingsWithSti = new \Spameri\ElasticQuery\Mapping\Settings('index', true); + + \Tester\Assert::false($settingsWithoutSti->hasSti()); + \Tester\Assert::true($settingsWithSti->hasSti()); + } + + + public function testAnalysis(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Analysis::class, $settings->analysis()); + } + + + public function testMapping(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Mapping::class, $settings->mapping()); + } + + + public function testAddMappingField(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + ); + + $settings->addMappingField($field); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['title'])); + \Tester\Assert::same('text', $array['mappings']['properties']['title']['type']); + } + + + public function testAddMappingFieldKeyword(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + + $settings->addMappingFieldKeyword('status'); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['status'])); + \Tester\Assert::same('keyword', $array['mappings']['properties']['status']['type']); + } + + + public function testAddMappingFieldFloat(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + + $settings->addMappingFieldFloat('price'); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['price'])); + \Tester\Assert::same('float', $array['mappings']['properties']['price']['type']); + } + + + public function testAddMappingFieldInteger(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + + $settings->addMappingFieldInteger('count'); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['count'])); + \Tester\Assert::same('integer', $array['mappings']['properties']['count']['type']); + } + + + public function testAddMappingFieldBoolean(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + + $settings->addMappingFieldBoolean('active'); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['active'])); + \Tester\Assert::same('boolean', $array['mappings']['properties']['active']['type']); + } + + + public function testAddMappingFieldObject(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('author', $fields); + + $settings->addMappingFieldObject($fieldObject); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['author'])); + \Tester\Assert::same('object', $array['mappings']['properties']['author']['type']); + \Tester\Assert::true(isset($array['mappings']['properties']['author']['properties']['name'])); + } + + + public function testAddMappingNestedObject(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('tag', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('tags', $fields); + + $settings->addMappingNestedObject($nestedObject); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['tags'])); + \Tester\Assert::same('nested', $array['mappings']['properties']['tags']['type']); + \Tester\Assert::true(isset($array['mappings']['properties']['tags']['properties']['tag'])); + } + + + public function testAddMappingSubField(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + $subFieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $subFieldCollection, + ); + + $settings->addMappingSubField($subFields); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['title'])); + \Tester\Assert::same('text', $array['mappings']['properties']['title']['type']); + \Tester\Assert::true(isset($array['mappings']['properties']['title']['fields']['raw'])); + } + + + public function testRemoveMappingSubField(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + $subFieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $subFieldCollection, + ); + + $settings->addMappingSubField($subFields); + $settings->removeMappingSubField('title'); + + $array = $settings->toArray(); + + \Tester\Assert::false(isset($array['mappings']['properties']['title'])); + } + + + public function testAddAnalyzer(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + $settings->addAnalyzer($analyzer); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['settings']['analysis']['analyzer']['englishDictionary'])); + \Tester\Assert::same('custom', $array['settings']['analysis']['analyzer']['englishDictionary']['type']); + } + + + public function testRemoveAnalyzer(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('test_index'); + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + + $settings->addAnalyzer($analyzer); + $settings->removeAnalyzer('englishDictionary'); + + $array = $settings->toArray(); + + \Tester\Assert::false(isset($array['settings']['analysis']['analyzer']['englishDictionary'])); + } + + + public function testToArrayWithAliases(): void + { + $aliasCollection = new \Spameri\ElasticQuery\Mapping\Settings\AliasCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Alias('alias_one'), + new \Spameri\ElasticQuery\Mapping\Settings\Alias('alias_two'), + ); + + $settings = new \Spameri\ElasticQuery\Mapping\Settings( + 'test_index', + false, + null, + null, + $aliasCollection, + ); + + $array = $settings->toArray(); + + \Tester\Assert::true(isset($array['aliases'])); + \Tester\Assert::true(isset($array['aliases']['alias_one'])); + \Tester\Assert::true(isset($array['aliases']['alias_two'])); + } + + + public function testComplexMapping(): void + { + $settings = new \Spameri\ElasticQuery\Mapping\Settings('products'); + + // Add various field types + $settings->addMappingFieldKeyword('sku'); + $settings->addMappingFieldFloat('price'); + $settings->addMappingFieldInteger('quantity'); + $settings->addMappingFieldBoolean('in_stock'); + + // Add text field + $settings->addMappingField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'description', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + )); + + // Add nested object for categories + $categoryFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $settings->addMappingNestedObject(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('categories', $categoryFields)); + + $array = $settings->toArray(); + + \Tester\Assert::same('keyword', $array['mappings']['properties']['sku']['type']); + \Tester\Assert::same('float', $array['mappings']['properties']['price']['type']); + \Tester\Assert::same('integer', $array['mappings']['properties']['quantity']['type']); + \Tester\Assert::same('boolean', $array['mappings']['properties']['in_stock']['type']); + \Tester\Assert::same('text', $array['mappings']['properties']['description']['type']); + \Tester\Assert::same('nested', $array['mappings']['properties']['categories']['type']); + } + +} + +(new Settings())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis.phpt new file mode 100644 index 0000000..c468033 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis.phpt @@ -0,0 +1,125 @@ +toArray(); + + \Tester\Assert::true(isset($array['analyzer'])); + \Tester\Assert::true(isset($array['tokenizer'])); + \Tester\Assert::true(isset($array['filter'])); + \Tester\Assert::same([], $array['analyzer']); + \Tester\Assert::same([], $array['tokenizer']); + \Tester\Assert::same([], $array['filter']); + } + + + public function testAnalyzer(): void + { + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection::class, $analysis->analyzer()); + \Tester\Assert::same(0, $analysis->analyzer()->count()); + } + + + public function testTokenizer(): void + { + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection::class, $analysis->tokenizer()); + \Tester\Assert::same(0, $analysis->tokenizer()->count()); + } + + + public function testFilter(): void + { + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection::class, $analysis->filter()); + \Tester\Assert::same(0, $analysis->filter()->count()); + } + + + public function testConstructorWithAnalyzerCollection(): void + { + $analyzerCollection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + ); + + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis( + $analyzerCollection, + ); + + \Tester\Assert::same(1, $analysis->analyzer()->count()); + } + + + public function testToArrayWithAnalyzer(): void + { + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis(); + + $analysis->analyzer()->add(new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary()); + + $array = $analysis->toArray(); + + \Tester\Assert::true(isset($array['analyzer']['englishDictionary'])); + \Tester\Assert::same('custom', $array['analyzer']['englishDictionary']['type']); + } + + + public function testTokenizerCollection(): void + { + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis(); + + $analysis->tokenizer()->add(new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram()); + + \Tester\Assert::same(1, $analysis->tokenizer()->count()); + \Tester\Assert::true($analysis->tokenizer()->isValue('edge_ngram')); + } + + + public function testToArrayWithFilter(): void + { + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis(); + + $analysis->filter()->add(new \Spameri\ElasticQuery\Mapping\Filter\Stemmer()); + + $array = $analysis->toArray(); + + \Tester\Assert::true(isset($array['filter']['stemmer'])); + } + + + public function testCompleteAnalysisConfiguration(): void + { + $analysis = new \Spameri\ElasticQuery\Mapping\Settings\Analysis(); + + // Add analyzer + $analysis->analyzer()->add(new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary()); + + // Add filter + $analysis->filter()->add(new \Spameri\ElasticQuery\Mapping\Filter\Stemmer()); + + $array = $analysis->toArray(); + + \Tester\Assert::true(isset($array['analyzer']['englishDictionary'])); + \Tester\Assert::true(isset($array['filter']['stemmer'])); + + // Verify collections are populated + \Tester\Assert::same(1, $analysis->analyzer()->count()); + \Tester\Assert::same(1, $analysis->filter()->count()); + } + +} + +(new Analysis())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/AnalyzerCollection.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/AnalyzerCollection.phpt new file mode 100644 index 0000000..9628071 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/AnalyzerCollection.phpt @@ -0,0 +1,181 @@ +count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testConstructorWithAnalyzers(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + ); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::contains('englishDictionary', $collection->keys()); + } + + + public function testAdd(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary()); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue('englishDictionary')); + } + + + public function testAddReplacesSameKey(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary()); + $collection->add(new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary()); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testRemove(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + ); + + $result = $collection->remove('englishDictionary'); + + \Tester\Assert::true($result); + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::false($collection->isValue('englishDictionary')); + } + + + public function testRemoveNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + + $result = $collection->remove('non_existent'); + + \Tester\Assert::false($result); + } + + + public function testGet(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + ); + + $analyzer = $collection->get('englishDictionary'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\AnalyzerInterface::class, $analyzer); + \Tester\Assert::same('englishDictionary', $analyzer->name()); + } + + + public function testGetNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection(); + + $analyzer = $collection->get('non_existent'); + + \Tester\Assert::null($analyzer); + } + + + public function testIsValue(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + ); + + \Tester\Assert::true($collection->isValue('englishDictionary')); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testCount(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary(), + ); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testKeys(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary(), + ); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains('englishDictionary', $keys); + \Tester\Assert::contains('germanDictionary', $keys); + } + + + public function testClear(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + ); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + ); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(1, $iterator->count()); + } + + + public function testForeach(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\AnalyzerCollection( + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(), + new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\GermanDictionary(), + ); + + $keys = []; + foreach ($collection as $key => $analyzer) { + $keys[] = $key; + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\AnalyzerInterface::class, $analyzer); + } + + \Tester\Assert::count(2, $keys); + } + +} + +(new AnalyzerCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/FilterCollection.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/FilterCollection.phpt new file mode 100644 index 0000000..b52cf9f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/FilterCollection.phpt @@ -0,0 +1,183 @@ +count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testConstructorWithFilters(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(), + ); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testAdd(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Filter\Lowercase()); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue('lowercase')); + } + + + public function testAddReplacesSameKey(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Filter\Stemmer()); + $collection->add(new \Spameri\ElasticQuery\Mapping\Filter\Stemmer()); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testRemove(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + ); + + $result = $collection->remove('lowercase'); + + \Tester\Assert::true($result); + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::false($collection->isValue('lowercase')); + } + + + public function testRemoveNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $result = $collection->remove('non_existent'); + + \Tester\Assert::false($result); + } + + + public function testGet(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(), + ); + + $filter = $collection->get('stemmer'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + \Tester\Assert::same('stemmer', $filter->key()); + } + + + public function testGetNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection(); + + $filter = $collection->get('non_existent'); + + \Tester\Assert::null($filter); + } + + + public function testIsValue(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + ); + + \Tester\Assert::true($collection->isValue('lowercase')); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testCount(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(), + new \Spameri\ElasticQuery\Mapping\Filter\ASCIIFolding(), + ); + + \Tester\Assert::same(3, $collection->count()); + } + + + public function testKeys(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(), + ); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains('lowercase', $keys); + \Tester\Assert::contains('stemmer', $keys); + } + + + public function testClear(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + ); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(), + ); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(2, $iterator->count()); + } + + + public function testForeach(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\FilterCollection( + new \Spameri\ElasticQuery\Mapping\Filter\Lowercase(), + new \Spameri\ElasticQuery\Mapping\Filter\Stemmer(), + ); + + $keys = []; + foreach ($collection as $key => $filter) { + $keys[] = $key; + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\FilterInterface::class, $filter); + } + + \Tester\Assert::count(2, $keys); + } + +} + +(new FilterCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/TokenizerCollection.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/TokenizerCollection.phpt new file mode 100644 index 0000000..ac8d70d --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Analysis/TokenizerCollection.phpt @@ -0,0 +1,183 @@ +count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testConstructorWithTokenizers(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(), + ); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testAdd(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram()); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue('edge_ngram')); + } + + + public function testAddReplacesSameKey(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram()); + $collection->add(new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram()); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testRemove(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + ); + + $result = $collection->remove('edge_ngram'); + + \Tester\Assert::true($result); + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::false($collection->isValue('edge_ngram')); + } + + + public function testRemoveNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection(); + + $result = $collection->remove('non_existent'); + + \Tester\Assert::false($result); + } + + + public function testGet(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + ); + + $tokenizer = $collection->get('edge_ngram'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\TokenizerInterface::class, $tokenizer); + \Tester\Assert::same('edge_ngram', $tokenizer->key()); + } + + + public function testGetNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection(); + + $tokenizer = $collection->get('non_existent'); + + \Tester\Assert::null($tokenizer); + } + + + public function testIsValue(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + ); + + \Tester\Assert::true($collection->isValue('edge_ngram')); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testCount(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(), + new \Spameri\ElasticQuery\Mapping\Tokenizer\Standard(), + ); + + \Tester\Assert::same(3, $collection->count()); + } + + + public function testKeys(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(), + ); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains('edge_ngram', $keys); + \Tester\Assert::contains('ngram', $keys); + } + + + public function testClear(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + ); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(), + ); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(2, $iterator->count()); + } + + + public function testForeach(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Analysis\TokenizerCollection( + new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(), + new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(), + ); + + $keys = []; + foreach ($collection as $key => $tokenizer) { + $keys[] = $key; + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\TokenizerInterface::class, $tokenizer); + } + + \Tester\Assert::count(2, $keys); + } + +} + +(new TokenizerCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping.phpt new file mode 100644 index 0000000..49baed6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping.phpt @@ -0,0 +1,230 @@ +toArray(); + + \Tester\Assert::true(isset($array['mappings'])); + \Tester\Assert::true(isset($array['mappings']['properties'])); + \Tester\Assert::same([], $array['mappings']['properties']); + \Tester\Assert::same(true, $array['mappings']['dynamic']); + } + + + public function testGetIndexName(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('my_index'); + + \Tester\Assert::same('my_index', $mapping->getIndexName()); + } + + + public function testEnableStrictMapping(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index'); + + $mapping->enableStrictMapping(); + + $array = $mapping->toArray(); + + \Tester\Assert::same(false, $array['mappings']['dynamic']); + } + + + public function testDynamicFalseViaConstructor(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping( + 'test_index', + null, + false, + ); + + $array = $mapping->toArray(); + + \Tester\Assert::same(false, $array['mappings']['dynamic']); + } + + + public function testFields(): void + { + $fieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index', $fieldCollection); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection::class, $mapping->fields()); + \Tester\Assert::same(1, $mapping->fields()->count()); + } + + + public function testAddField(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index'); + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + ); + + $mapping->addField($field); + + $array = $mapping->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['title'])); + \Tester\Assert::same('text', $array['mappings']['properties']['title']['type']); + } + + + public function testAddFieldObject(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index'); + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('first_name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('last_name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('author', $fields); + + $mapping->addFieldObject($fieldObject); + + $array = $mapping->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['author'])); + \Tester\Assert::same('object', $array['mappings']['properties']['author']['type']); + \Tester\Assert::true(isset($array['mappings']['properties']['author']['properties'])); + \Tester\Assert::true(isset($array['mappings']['properties']['author']['properties']['first_name'])); + \Tester\Assert::true(isset($array['mappings']['properties']['author']['properties']['last_name'])); + } + + + public function testAddNestedObject(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index'); + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('categories', $fields); + + $mapping->addNestedObject($nestedObject); + + $array = $mapping->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['categories'])); + \Tester\Assert::same('nested', $array['mappings']['properties']['categories']['type']); + \Tester\Assert::true(isset($array['mappings']['properties']['categories']['properties'])); + \Tester\Assert::true(isset($array['mappings']['properties']['categories']['properties']['id'])); + \Tester\Assert::true(isset($array['mappings']['properties']['categories']['properties']['name'])); + } + + + public function testRemoveFieldObject(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index'); + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('author', $fields); + + $mapping->addFieldObject($fieldObject); + $mapping->removeFieldObject('author'); + + $array = $mapping->toArray(); + + \Tester\Assert::false(isset($array['mappings']['properties']['author'])); + } + + + public function testAddSubField(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index'); + $subFieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $subFieldCollection, + ); + + $mapping->addSubField($subFields); + + $array = $mapping->toArray(); + + \Tester\Assert::true(isset($array['mappings']['properties']['title'])); + \Tester\Assert::same('text', $array['mappings']['properties']['title']['type']); + \Tester\Assert::true(isset($array['mappings']['properties']['title']['fields']['raw'])); + \Tester\Assert::same('keyword', $array['mappings']['properties']['title']['fields']['raw']['type']); + } + + + public function testRemoveSubField(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('test_index'); + $subFieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $subFieldCollection, + ); + + $mapping->addSubField($subFields); + $mapping->removeSubField('title'); + + $array = $mapping->toArray(); + + \Tester\Assert::false(isset($array['mappings']['properties']['title'])); + } + + + public function testComplexMapping(): void + { + $mapping = new \Spameri\ElasticQuery\Mapping\Settings\Mapping('products'); + + // Add simple fields + $mapping->addField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD)); + $mapping->addField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT)); + $mapping->addField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('price', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_FLOAT)); + $mapping->addField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('stock', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER)); + $mapping->addField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('active', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_BOOLEAN)); + $mapping->addField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('created_at', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_DATE)); + + // Add object field + $brandFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $mapping->addFieldObject(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('brand', $brandFields)); + + // Add nested field + $tagFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('value', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $mapping->addNestedObject(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('tags', $tagFields)); + + $array = $mapping->toArray(); + + \Tester\Assert::same('keyword', $array['mappings']['properties']['id']['type']); + \Tester\Assert::same('text', $array['mappings']['properties']['name']['type']); + \Tester\Assert::same('float', $array['mappings']['properties']['price']['type']); + \Tester\Assert::same('integer', $array['mappings']['properties']['stock']['type']); + \Tester\Assert::same('boolean', $array['mappings']['properties']['active']['type']); + \Tester\Assert::same('date', $array['mappings']['properties']['created_at']['type']); + \Tester\Assert::same('object', $array['mappings']['properties']['brand']['type']); + \Tester\Assert::same('nested', $array['mappings']['properties']['tags']['type']); + } + +} + +(new Mapping())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/Field.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/Field.phpt new file mode 100644 index 0000000..2d5d7a0 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/Field.phpt @@ -0,0 +1,254 @@ +toArray(); + + \Tester\Assert::true(isset($array['name'])); + \Tester\Assert::same('keyword', $array['name']['type']); + } + + + public function testKey(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('my_field'); + + \Tester\Assert::same('my_field', $field->key()); + } + + + public function testToArrayWithText(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'description', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('text', $array['description']['type']); + } + + + public function testToArrayWithInteger(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'count', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('integer', $array['count']['type']); + } + + + public function testToArrayWithFloat(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'price', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_FLOAT, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('float', $array['price']['type']); + } + + + public function testToArrayWithBoolean(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'active', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_BOOLEAN, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('boolean', $array['active']['type']); + } + + + public function testToArrayWithDate(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'created_at', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_DATE, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('date', $array['created_at']['type']); + } + + + public function testToArrayWithGeoPoint(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'location', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_GEO_POINT, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('geo_point', $array['location']['type']); + } + + + public function testToArrayWithAnalyzer(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'description', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $analyzer, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('text', $array['description']['type']); + \Tester\Assert::same('englishDictionary', $array['description']['analyzer']); + } + + + public function testChangeAnalyzer(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'description', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + ); + + $array = $field->toArray(); + \Tester\Assert::false(isset($array['description']['analyzer'])); + + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + $field->changeAnalyzer($analyzer); + + $array = $field->toArray(); + \Tester\Assert::same('englishDictionary', $array['description']['analyzer']); + } + + + public function testToArrayWithFieldDataTrue(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'tags', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + null, + true, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('text', $array['tags']['type']); + \Tester\Assert::same(true, $array['tags']['fielddata']); + } + + + public function testToArrayWithFieldDataFalse(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'tags', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + null, + false, + ); + + $array = $field->toArray(); + + \Tester\Assert::same('text', $array['tags']['type']); + \Tester\Assert::same(false, $array['tags']['fielddata']); + } + + + public function testToArrayWithoutFieldData(): void + { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'tags', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + ); + + $array = $field->toArray(); + + \Tester\Assert::false(isset($array['tags']['fielddata'])); + } + + + public function testInvalidTypeThrowsException(): void + { + \Tester\Assert::exception(function (): void { + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'test', + 'invalid_type', + ); + }, \Spameri\ElasticQuery\Exception\InvalidArgumentException::class); + } + + + public function testAllNumericTypes(): void + { + $types = [ + 'integer' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER, + 'long' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_LONG, + 'short' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_SHORT, + 'byte' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_BYTE, + 'double' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_DOUBLE, + 'float' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_FLOAT, + 'half_float' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_HALF_FLOAT, + 'scaled_float' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_SCALED_FLOAT, + ]; + + foreach ($types as $expected => $type) { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('num_field', $type); + $array = $field->toArray(); + \Tester\Assert::same($expected, $array['num_field']['type']); + } + } + + + public function testRangeTypes(): void + { + $types = [ + 'integer_range' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER_RANGE, + 'float_range' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_FLOAT_RANGE, + 'long_range' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_LONG_RANGE, + 'double_range' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_DOUBLE_RANGE, + 'date_range' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_DATE_RANGE, + ]; + + foreach ($types as $expected => $type) { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('range_field', $type); + $array = $field->toArray(); + \Tester\Assert::same($expected, $array['range_field']['type']); + } + } + + + public function testSpecialTypes(): void + { + $types = [ + 'ip' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_IP, + 'completion' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_COMPLETION, + 'token_count' => \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TOKEN_COUNT, + ]; + + foreach ($types as $expected => $type) { + $field = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('special_field', $type); + $array = $field->toArray(); + \Tester\Assert::same($expected, $array['special_field']['type']); + } + } + +} + +(new Field())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/FieldCollection.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/FieldCollection.phpt new file mode 100644 index 0000000..1220eeb --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/FieldCollection.phpt @@ -0,0 +1,218 @@ +count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testConstructorWithFields(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('description', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + ); + + \Tester\Assert::same(2, $collection->count()); + \Tester\Assert::contains('name', $collection->keys()); + \Tester\Assert::contains('description', $collection->keys()); + } + + + public function testAdd(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('title', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT)); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue('title')); + } + + + public function testAddReplacesSameKey(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); + + $collection->add(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT)); + $collection->add(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD)); + + \Tester\Assert::same(1, $collection->count()); + + $field = $collection->get('name'); + \Tester\Assert::same('keyword', $field->toArray()['name']['type']); + } + + + public function testRemove(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $result = $collection->remove('name'); + + \Tester\Assert::true($result); + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::false($collection->isValue('name')); + } + + + public function testRemoveNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); + + $result = $collection->remove('non_existent'); + + \Tester\Assert::false($result); + } + + + public function testGet(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $field = $collection->get('name'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Mapping\Field::class, $field); + \Tester\Assert::same('name', $field->key()); + } + + + public function testGetNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); + + $field = $collection->get('non_existent'); + + \Tester\Assert::null($field); + } + + + public function testIsValue(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + \Tester\Assert::true($collection->isValue('name')); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testCount(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('field1', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('field2', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('field3', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER), + ); + + \Tester\Assert::same(3, $collection->count()); + } + + + public function testKeys(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('alpha', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('beta', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + ); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains('alpha', $keys); + \Tester\Assert::contains('beta', $keys); + } + + + public function testClear(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('description', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + ); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('field1', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('field2', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + ); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(2, $iterator->count()); + } + + + public function testForeach(): void + { + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('field1', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('field2', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + ); + + $keys = []; + foreach ($collection as $key => $field) { + $keys[] = $key; + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldInterface::class, $field); + } + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains('field1', $keys); + \Tester\Assert::contains('field2', $keys); + } + + + public function testMixedFieldTypes(): void + { + $subFieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $nestedFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $objectFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $collection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('simple', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields('title', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, $subFieldCollection), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('tags', $nestedFields), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('author', $objectFields), + ); + + \Tester\Assert::same(4, $collection->count()); + \Tester\Assert::true($collection->isValue('simple')); + \Tester\Assert::true($collection->isValue('title')); + \Tester\Assert::true($collection->isValue('tags')); + \Tester\Assert::true($collection->isValue('author')); + } + +} + +(new FieldCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/FieldObject.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/FieldObject.phpt new file mode 100644 index 0000000..fbeeff0 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/FieldObject.phpt @@ -0,0 +1,163 @@ +key()); + } + + + public function testToArrayEmpty(): void + { + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); + $fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('author', $fields); + + $array = $fieldObject->toArray(); + + \Tester\Assert::same('object', $array['type']); + \Tester\Assert::true(isset($array['properties'])); + \Tester\Assert::same([], $array['properties']); + } + + + public function testToArrayWithSimpleFields(): void + { + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('first_name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('last_name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('email', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('author', $fields); + + $array = $fieldObject->toArray(); + + \Tester\Assert::same('object', $array['type']); + \Tester\Assert::true(isset($array['properties']['first_name'])); + \Tester\Assert::true(isset($array['properties']['last_name'])); + \Tester\Assert::true(isset($array['properties']['email'])); + \Tester\Assert::same('keyword', $array['properties']['first_name']['type']); + \Tester\Assert::same('keyword', $array['properties']['last_name']['type']); + \Tester\Assert::same('keyword', $array['properties']['email']['type']); + } + + + public function testToArrayWithNestedFieldObject(): void + { + $addressFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('street', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('city', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('zip', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $addressObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('address', $addressFields); + + $personFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $personFields->add($addressObject); + + $personObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('person', $personFields); + + $array = $personObject->toArray(); + + \Tester\Assert::same('object', $array['type']); + \Tester\Assert::true(isset($array['properties']['name'])); + \Tester\Assert::true(isset($array['properties']['address'])); + \Tester\Assert::same('object', $array['properties']['address']['type']); + \Tester\Assert::true(isset($array['properties']['address']['properties']['street'])); + \Tester\Assert::true(isset($array['properties']['address']['properties']['city'])); + \Tester\Assert::true(isset($array['properties']['address']['properties']['zip'])); + } + + + public function testToArrayWithNestedObject(): void + { + $tagFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('value', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('tags', $tagFields); + + $productFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $productFields->add($nestedObject); + + $productObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('product', $productFields); + + $array = $productObject->toArray(); + + \Tester\Assert::same('object', $array['type']); + \Tester\Assert::true(isset($array['properties']['tags'])); + \Tester\Assert::same('nested', $array['properties']['tags']['type']); + } + + + public function testToArrayWithSubFields(): void + { + $subFieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $subFieldCollection, + ); + + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $fields->add($subFields); + + $fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('document', $fields); + + $array = $fieldObject->toArray(); + + \Tester\Assert::same('object', $array['type']); + \Tester\Assert::true(isset($array['properties']['title'])); + \Tester\Assert::same('text', $array['properties']['title']['type']); + \Tester\Assert::true(isset($array['properties']['title']['fields']['raw'])); + } + + + public function testToArrayWithMixedFieldTypes(): void + { + $addressFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('city', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $tagFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('value', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $subFieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('simple', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('address', $addressFields), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('tags', $tagFields), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields('title', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, $subFieldCollection), + ); + + $fieldObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('complex', $fields); + + $array = $fieldObject->toArray(); + + \Tester\Assert::same('object', $array['type']); + \Tester\Assert::same('keyword', $array['properties']['simple']['type']); + \Tester\Assert::same('object', $array['properties']['address']['type']); + \Tester\Assert::same('nested', $array['properties']['tags']['type']); + \Tester\Assert::same('text', $array['properties']['title']['type']); + } + +} + +(new FieldObject())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/NestedObject.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/NestedObject.phpt new file mode 100644 index 0000000..a6dced2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/NestedObject.phpt @@ -0,0 +1,154 @@ +key()); + } + + + public function testToArrayEmpty(): void + { + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection(); + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('tags', $fields); + + $array = $nestedObject->toArray(); + + \Tester\Assert::same('nested', $array['type']); + \Tester\Assert::true(isset($array['properties'])); + \Tester\Assert::same([], $array['properties']); + } + + + public function testToArrayWithSimpleFields(): void + { + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('count', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER), + ); + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('tags', $fields); + + $array = $nestedObject->toArray(); + + \Tester\Assert::same('nested', $array['type']); + \Tester\Assert::true(isset($array['properties']['id'])); + \Tester\Assert::true(isset($array['properties']['name'])); + \Tester\Assert::true(isset($array['properties']['count'])); + \Tester\Assert::same('keyword', $array['properties']['id']['type']); + \Tester\Assert::same('keyword', $array['properties']['name']['type']); + \Tester\Assert::same('integer', $array['properties']['count']['type']); + } + + + public function testToArrayWithNestedNestedObject(): void + { + $innerFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('value', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $innerNested = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('attributes', $innerFields); + + $outerFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $outerFields->add($innerNested); + + $outerNested = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('categories', $outerFields); + + $array = $outerNested->toArray(); + + \Tester\Assert::same('nested', $array['type']); + \Tester\Assert::true(isset($array['properties']['name'])); + \Tester\Assert::true(isset($array['properties']['attributes'])); + \Tester\Assert::same('nested', $array['properties']['attributes']['type']); + \Tester\Assert::true(isset($array['properties']['attributes']['properties']['value'])); + } + + + public function testToArrayWithFieldObject(): void + { + $addressFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('street', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('city', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $addressObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('address', $addressFields); + + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('name', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + $fields->add($addressObject); + + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('locations', $fields); + + $array = $nestedObject->toArray(); + + \Tester\Assert::same('nested', $array['type']); + \Tester\Assert::true(isset($array['properties']['address'])); + \Tester\Assert::same('object', $array['properties']['address']['type']); + \Tester\Assert::true(isset($array['properties']['address']['properties']['street'])); + \Tester\Assert::true(isset($array['properties']['address']['properties']['city'])); + } + + + public function testToArrayWithMixedFieldTypes(): void + { + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $metaFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('key', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('simple', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldObject('meta', $metaFields), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('inner_nested', $metaFields), + ); + + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('items', $fields); + + $array = $nestedObject->toArray(); + + \Tester\Assert::same('nested', $array['type']); + \Tester\Assert::same('keyword', $array['properties']['simple']['type']); + \Tester\Assert::same('object', $array['properties']['meta']['type']); + \Tester\Assert::same('nested', $array['properties']['inner_nested']['type']); + } + + + public function testTypicalUseCaseForComments(): void + { + $fields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('id', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('author', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('text', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('created_at', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_DATE), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('likes', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_INTEGER), + ); + + $nestedObject = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\NestedObject('comments', $fields); + + $array = $nestedObject->toArray(); + + \Tester\Assert::same('nested', $array['type']); + \Tester\Assert::same('keyword', $array['properties']['id']['type']); + \Tester\Assert::same('keyword', $array['properties']['author']['type']); + \Tester\Assert::same('text', $array['properties']['text']['type']); + \Tester\Assert::same('date', $array['properties']['created_at']['type']); + \Tester\Assert::same('integer', $array['properties']['likes']['type']); + } + +} + +(new NestedObject())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/SubFields.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/SubFields.phpt new file mode 100644 index 0000000..9483501 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Settings/Mapping/SubFields.phpt @@ -0,0 +1,208 @@ +key()); + } + + + public function testToArrayDefaultType(): void + { + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields('title'); + + $array = $subFields->toArray(); + + \Tester\Assert::same('keyword', $array['type']); + \Tester\Assert::true(isset($array['fields'])); + \Tester\Assert::same([], $array['fields']); + } + + + public function testToArrayWithTextType(): void + { + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + ); + + $array = $subFields->toArray(); + + \Tester\Assert::same('text', $array['type']); + } + + + public function testToArrayWithSubField(): void + { + $fieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $fieldCollection, + ); + + $array = $subFields->toArray(); + + \Tester\Assert::same('text', $array['type']); + \Tester\Assert::true(isset($array['fields']['raw'])); + \Tester\Assert::same('keyword', $array['fields']['raw']['type']); + } + + + public function testToArrayWithMultipleSubFields(): void + { + $fieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('english', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('autocomplete', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + ); + + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $fieldCollection, + ); + + $array = $subFields->toArray(); + + \Tester\Assert::same('text', $array['type']); + \Tester\Assert::count(3, $array['fields']); + \Tester\Assert::same('keyword', $array['fields']['raw']['type']); + \Tester\Assert::same('text', $array['fields']['english']['type']); + \Tester\Assert::same('text', $array['fields']['autocomplete']['type']); + } + + + public function testGetFields(): void + { + $fieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $fieldCollection, + ); + + $fields = $subFields->getFields(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection::class, $fields); + \Tester\Assert::same(1, $fields->count()); + \Tester\Assert::true($fields->isValue('raw')); + } + + + public function testAddMappingField(): void + { + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + ); + + $subFields->addMappingField(new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'raw', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD, + )); + + $array = $subFields->toArray(); + + \Tester\Assert::true(isset($array['fields']['raw'])); + \Tester\Assert::same('keyword', $array['fields']['raw']['type']); + } + + + public function testRemoveMappingField(): void + { + $fieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('raw', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('english', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT), + ); + + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'title', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $fieldCollection, + ); + + $subFields->removeMappingField('raw'); + + $array = $subFields->toArray(); + + \Tester\Assert::false(isset($array['fields']['raw'])); + \Tester\Assert::true(isset($array['fields']['english'])); + } + + + public function testInvalidTypeThrowsException(): void + { + \Tester\Assert::exception(function (): void { + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'test', + 'invalid_type', + ); + }, \Spameri\ElasticQuery\Exception\InvalidArgumentException::class); + } + + + public function testTypicalUseCaseForTextWithKeywordSubField(): void + { + $fieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field('keyword', \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_KEYWORD), + ); + + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'name', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $fieldCollection, + ); + + $array = $subFields->toArray(); + + // Main field is text for full-text search + \Tester\Assert::same('text', $array['type']); + + // Sub-field is keyword for sorting/aggregations + \Tester\Assert::same('keyword', $array['fields']['keyword']['type']); + } + + + public function testSubFieldWithAnalyzer(): void + { + $analyzer = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\EnglishDictionary(); + $fieldCollection = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\FieldCollection( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'analyzed', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $analyzer, + ), + ); + + $subFields = new \Spameri\ElasticQuery\Mapping\Settings\Mapping\SubFields( + 'content', + \Spameri\ElasticQuery\Mapping\AllowedValues::TYPE_TEXT, + $fieldCollection, + ); + + $array = $subFields->toArray(); + + \Tester\Assert::same('text', $array['type']); + \Tester\Assert::same('text', $array['fields']['analyzed']['type']); + \Tester\Assert::same('englishDictionary', $array['fields']['analyzed']['analyzer']); + } + +} + +(new SubFields())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/EdgeNGram.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/EdgeNGram.phpt new file mode 100644 index 0000000..80bae25 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/EdgeNGram.phpt @@ -0,0 +1,46 @@ +getType()); + } + + + public function testKey(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(); + + \Tester\Assert::same('edge_ngram', $tokenizer->key()); + } + + + public function testToArray(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(); + + $expected = ['edge_ngram']; + + \Tester\Assert::same($expected, $tokenizer->toArray()); + } + + + public function testImplementsTokenizerInterface(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\EdgeNGram(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\TokenizerInterface::class, $tokenizer); + } + +} + +(new EdgeNGram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/NGram.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/NGram.phpt new file mode 100644 index 0000000..72dc762 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/NGram.phpt @@ -0,0 +1,46 @@ +getType()); + } + + + public function testKey(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(); + + \Tester\Assert::same('ngram', $tokenizer->key()); + } + + + public function testToArray(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(); + + $expected = ['ngram']; + + \Tester\Assert::same($expected, $tokenizer->toArray()); + } + + + public function testImplementsTokenizerInterface(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\NGram(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\TokenizerInterface::class, $tokenizer); + } + +} + +(new NGram())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/Pattern.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/Pattern.phpt new file mode 100644 index 0000000..2d7f0d2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/Pattern.phpt @@ -0,0 +1,46 @@ +getType()); + } + + + public function testKey(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\Pattern(); + + \Tester\Assert::same('pattern', $tokenizer->key()); + } + + + public function testToArray(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\Pattern(); + + $expected = ['pattern']; + + \Tester\Assert::same($expected, $tokenizer->toArray()); + } + + + public function testImplementsTokenizerInterface(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\Pattern(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\TokenizerInterface::class, $tokenizer); + } + +} + +(new Pattern())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/Standard.phpt b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/Standard.phpt new file mode 100644 index 0000000..a4f5c21 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Mapping/Tokenizer/Standard.phpt @@ -0,0 +1,46 @@ +getType()); + } + + + public function testKey(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\Standard(); + + \Tester\Assert::same('standard', $tokenizer->key()); + } + + + public function testToArray(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\Standard(); + + $expected = ['standard']; + + \Tester\Assert::same($expected, $tokenizer->toArray()); + } + + + public function testImplementsTokenizerInterface(): void + { + $tokenizer = new \Spameri\ElasticQuery\Mapping\Tokenizer\Standard(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Mapping\TokenizerInterface::class, $tokenizer); + } + +} + +(new Standard())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options.phpt b/tests/SpameriTests/ElasticQuery/Options.phpt new file mode 100644 index 0000000..3ebd2af --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options.phpt @@ -0,0 +1,218 @@ +toArray(); + + \Tester\Assert::same([], $array); + } + + + public function testToArrayWithSize(): void + { + $options = new \Spameri\ElasticQuery\Options( + size: 10, + ); + + $array = $options->toArray(); + + \Tester\Assert::same(['size' => 10], $array); + } + + + public function testToArrayWithFrom(): void + { + $options = new \Spameri\ElasticQuery\Options( + from: 20, + ); + + $array = $options->toArray(); + + \Tester\Assert::same(['from' => 20], $array); + } + + + public function testToArrayWithSizeAndFrom(): void + { + $options = new \Spameri\ElasticQuery\Options( + size: 10, + from: 20, + ); + + $array = $options->toArray(); + + \Tester\Assert::same(['from' => 20, 'size' => 10], $array); + } + + + public function testToArrayWithMinScore(): void + { + $options = new \Spameri\ElasticQuery\Options( + minScore: 0.5, + ); + + $array = $options->toArray(); + + \Tester\Assert::same(['min_score' => 0.5], $array); + } + + + public function testToArrayWithVersion(): void + { + $options = new \Spameri\ElasticQuery\Options( + includeVersion: true, + ); + + $array = $options->toArray(); + + \Tester\Assert::same(['version' => true], $array); + } + + + public function testToArrayWithSort(): void + { + $sortCollection = new \Spameri\ElasticQuery\Options\SortCollection(); + $sortCollection->add(new \Spameri\ElasticQuery\Options\Sort('created_at', \Spameri\ElasticQuery\Options\Sort::DESC)); + + $options = new \Spameri\ElasticQuery\Options( + sort: $sortCollection, + ); + + $array = $options->toArray(); + + \Tester\Assert::true(isset($array['sort'])); + \Tester\Assert::count(1, $array['sort']); + \Tester\Assert::same('DESC', $array['sort'][0]['created_at']['order']); + } + + + public function testChangeSize(): void + { + $options = new \Spameri\ElasticQuery\Options( + size: 10, + ); + + $options->changeSize(50); + + $array = $options->toArray(); + + \Tester\Assert::same(['size' => 50], $array); + } + + + public function testChangeFrom(): void + { + $options = new \Spameri\ElasticQuery\Options( + from: 0, + ); + + $options->changeFrom(100); + + $array = $options->toArray(); + + \Tester\Assert::same(['from' => 100], $array); + } + + + public function testSort(): void + { + $options = new \Spameri\ElasticQuery\Options(); + + $options->sort()->add(new \Spameri\ElasticQuery\Options\Sort('name', \Spameri\ElasticQuery\Options\Sort::ASC)); + + $array = $options->toArray(); + + \Tester\Assert::true(isset($array['sort'])); + \Tester\Assert::count(1, $array['sort']); + } + + + public function testScroll(): void + { + $options = new \Spameri\ElasticQuery\Options( + scroll: '1m', + ); + + \Tester\Assert::same('1m', $options->scroll()); + } + + + public function testStartScroll(): void + { + $options = new \Spameri\ElasticQuery\Options(); + + $options->startScroll('2m'); + + \Tester\Assert::same('2m', $options->scroll()); + } + + + public function testScrollId(): void + { + $options = new \Spameri\ElasticQuery\Options( + scrollId: 'DXF1ZXJ5QW5kRmV0Y2gBAAAAAA', + ); + + \Tester\Assert::same('DXF1ZXJ5QW5kRmV0Y2gBAAAAAA', $options->scrollId()); + } + + + public function testScrollInitialized(): void + { + $options = new \Spameri\ElasticQuery\Options(); + + $options->scrollInitialized('DXF1ZXJ5QW5kRmV0Y2gBAAAAAA'); + + \Tester\Assert::same('DXF1ZXJ5QW5kRmV0Y2gBAAAAAA', $options->scrollId()); + } + + + public function testToArrayWithScrollId(): void + { + $options = new \Spameri\ElasticQuery\Options( + scroll: '1m', + scrollId: 'DXF1ZXJ5QW5kRmV0Y2gBAAAAAA', + ); + + $array = $options->toArray(); + + \Tester\Assert::same('DXF1ZXJ5QW5kRmV0Y2gBAAAAAA', $array['scroll_id']); + \Tester\Assert::same('1m', $array['scroll']); + } + + + public function testToArrayFullOptions(): void + { + $sortCollection = new \Spameri\ElasticQuery\Options\SortCollection(); + $sortCollection->add(new \Spameri\ElasticQuery\Options\Sort('created_at', \Spameri\ElasticQuery\Options\Sort::DESC)); + + $options = new \Spameri\ElasticQuery\Options( + size: 25, + from: 50, + sort: $sortCollection, + minScore: 0.75, + includeVersion: true, + ); + + $array = $options->toArray(); + + \Tester\Assert::same(50, $array['from']); + \Tester\Assert::same(25, $array['size']); + \Tester\Assert::same(0.75, $array['min_score']); + \Tester\Assert::same(true, $array['version']); + \Tester\Assert::true(isset($array['sort'])); + } + +} + +(new Options())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/Collapse.phpt b/tests/SpameriTests/ElasticQuery/Options/Collapse.phpt new file mode 100644 index 0000000..b7dda52 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/Collapse.phpt @@ -0,0 +1,61 @@ + [ + 'properties' => [ + 'user_id' => ['type' => 'keyword'], + 'created' => ['type' => 'date'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $collapse = new \Spameri\ElasticQuery\Options\Collapse( + field: 'user_id', + innerHits: new \Spameri\ElasticQuery\Query\InnerHits(name: 'recent', size: 5), + maxConcurrentGroupSearches: 4, + ); + + $array = $collapse->toArray(); + + \Tester\Assert::same('user_id', $array['field']); + \Tester\Assert::same('recent', $array['inner_hits']['name']); + \Tester\Assert::same(4, $array['max_concurrent_group_searches']); + } + + + public function testCreate(): void + { + $this->indexDocument(['user_id' => 'a', 'created' => '2024-01-01']); + $this->indexDocument(['user_id' => 'a', 'created' => '2024-06-01']); + $this->indexDocument(['user_id' => 'b', 'created' => '2024-03-01']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery( + options: new \Spameri\ElasticQuery\Options( + collapse: new \Spameri\ElasticQuery\Options\Collapse(field: 'user_id'), + ), + ); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(3, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Collapse())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/GeoDistanceSort.phpt b/tests/SpameriTests/ElasticQuery/Options/GeoDistanceSort.phpt new file mode 100644 index 0000000..d30e4fe --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/GeoDistanceSort.phpt @@ -0,0 +1,180 @@ +toArray(); + + \Tester\Assert::true(isset($array['_geo_distance'])); + \Tester\Assert::same([48.8566, 2.3522], $array['_geo_distance']['location']); + \Tester\Assert::same('ASC', $array['_geo_distance']['order']); + \Tester\Assert::same('km', $array['_geo_distance']['unit']); + \Tester\Assert::same('min', $array['_geo_distance']['mode']); + \Tester\Assert::same('arc', $array['_geo_distance']['distance_type']); + \Tester\Assert::same(true, $array['_geo_distance']['ignore_unmapped']); + } + + + public function testToArrayDescending(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 51.5074, + lon: -0.1278, + type: \Spameri\ElasticQuery\Options\Sort::DESC, + ); + + $array = $geoSort->toArray(); + + \Tester\Assert::same('DESC', $array['_geo_distance']['order']); + } + + + public function testToArrayWithUnit(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 40.7128, + lon: -74.0060, + unit: 'mi', + ); + + $array = $geoSort->toArray(); + + \Tester\Assert::same('mi', $array['_geo_distance']['unit']); + } + + + public function testToArrayWithModeAvg(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'locations', + lat: 35.6762, + lon: 139.6503, + mode: 'avg', + ); + + $array = $geoSort->toArray(); + + \Tester\Assert::same('avg', $array['_geo_distance']['mode']); + } + + + public function testToArrayWithDistanceTypePlane(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 52.5200, + lon: 13.4050, + distanceType: 'plane', + ); + + $array = $geoSort->toArray(); + + \Tester\Assert::same('plane', $array['_geo_distance']['distance_type']); + } + + + public function testToArrayFullOptions(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'shop_location', + lat: -33.8688, + lon: 151.2093, + type: \Spameri\ElasticQuery\Options\Sort::DESC, + unit: 'm', + mode: 'max', + distanceType: 'plane', + ); + + $array = $geoSort->toArray(); + + \Tester\Assert::same([-33.8688, 151.2093], $array['_geo_distance']['shop_location']); + \Tester\Assert::same('DESC', $array['_geo_distance']['order']); + \Tester\Assert::same('m', $array['_geo_distance']['unit']); + \Tester\Assert::same('max', $array['_geo_distance']['mode']); + \Tester\Assert::same('plane', $array['_geo_distance']['distance_type']); + } + + + public function testKey(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'office_location', + lat: 50.0755, + lon: 14.4378, + ); + + \Tester\Assert::same('office_location', $geoSort->key()); + } + + + public function testReadonlyProperties(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 48.2082, + lon: 16.3738, + type: \Spameri\ElasticQuery\Options\Sort::DESC, + unit: 'mi', + mode: 'avg', + distanceType: 'plane', + ); + + \Tester\Assert::same('location', $geoSort->field); + \Tester\Assert::same(48.2082, $geoSort->lat); + \Tester\Assert::same(16.3738, $geoSort->lon); + \Tester\Assert::same('DESC', $geoSort->type); + \Tester\Assert::same('mi', $geoSort->unit); + \Tester\Assert::same('avg', $geoSort->mode); + \Tester\Assert::same('plane', $geoSort->distanceType); + } + + + public function testIgnoreUnmappedFalse(): void + { + $geoSort = new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 0.0, + lon: 0.0, + ignoreUnmapped: false, + ); + + $array = $geoSort->toArray(); + + \Tester\Assert::false($array['_geo_distance']['ignore_unmapped']); + } + + + public function testInvalidSortTypeThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 0.0, + lon: 0.0, + type: 'INVALID', + ); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + 'Sorting type INVALID is out of allowed range. See \Spameri\ElasticQuery\Options\Sort for reference.', + ); + } + +} + +(new GeoDistanceSort())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/NestedSort.phpt b/tests/SpameriTests/ElasticQuery/Options/NestedSort.phpt new file mode 100644 index 0000000..5e06d5b --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/NestedSort.phpt @@ -0,0 +1,93 @@ + [ + 'properties' => [ + 'comments' => [ + 'type' => 'nested', + 'properties' => [ + 'rating' => ['type' => 'long'], + 'author' => ['type' => 'keyword'], + ], + ], + ], + ], + ]; + } + + + public function testToArray(): void + { + $nestedSort = new \Spameri\ElasticQuery\Options\NestedSort( + path: 'comments', + filter: new \Spameri\ElasticQuery\Query\Term('comments.author', 'john'), + maxChildren: 5, + ); + + $array = $nestedSort->toArray(); + + \Tester\Assert::same('comments', $array['path']); + \Tester\Assert::same(5, $array['max_children']); + \Tester\Assert::same('john', $array['filter']['term']['comments.author']['value']); + } + + + public function testSortWithNested(): void + { + $nestedSort = new \Spameri\ElasticQuery\Options\NestedSort( + path: 'comments', + ); + $sort = new \Spameri\ElasticQuery\Options\Sort( + field: 'comments.rating', + type: \Spameri\ElasticQuery\Options\Sort::DESC, + mode: 'avg', + nested: $nestedSort, + ); + + $array = $sort->toArray(); + + \Tester\Assert::same('avg', $array['comments.rating']['mode']); + \Tester\Assert::same('comments', $array['comments.rating']['nested']['path']); + } + + + public function testCreate(): void + { + $this->indexDocument([ + 'comments' => [ + ['author' => 'john', 'rating' => 5], + ['author' => 'jane', 'rating' => 3], + ], + ]); + + $nestedSort = new \Spameri\ElasticQuery\Options\NestedSort(path: 'comments'); + $sort = new \Spameri\ElasticQuery\Options\Sort( + field: 'comments.rating', + type: \Spameri\ElasticQuery\Options\Sort::DESC, + mode: 'avg', + nested: $nestedSort, + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->options()->sort()->add($sort); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new NestedSort())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/Rescore.phpt b/tests/SpameriTests/ElasticQuery/Options/Rescore.phpt new file mode 100644 index 0000000..23684ec --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/Rescore.phpt @@ -0,0 +1,59 @@ + ['properties' => ['title' => ['type' => 'text']]]]; + } + + + public function testToArray(): void + { + $rescore = new \Spameri\ElasticQuery\Options\Rescore( + query: new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'foo'), + windowSize: 50, + queryWeight: 0.7, + rescoreQueryWeight: 1.2, + scoreMode: \Spameri\ElasticQuery\Options\Rescore::SCORE_MODE_TOTAL, + ); + + $array = $rescore->toArray(); + + \Tester\Assert::same(50, $array['window_size']); + \Tester\Assert::same(0.7, $array['query']['query_weight']); + \Tester\Assert::same('total', $array['query']['score_mode']); + } + + + public function testCreate(): void + { + $this->indexDocument(['title' => 'hello world']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery( + options: new \Spameri\ElasticQuery\Options( + rescore: [ + new \Spameri\ElasticQuery\Options\Rescore( + query: new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'world'), + windowSize: 10, + ), + ], + ), + ); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'hello')); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Rescore())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/ScriptSort.phpt b/tests/SpameriTests/ElasticQuery/Options/ScriptSort.phpt new file mode 100644 index 0000000..0657661 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/ScriptSort.phpt @@ -0,0 +1,69 @@ + ['properties' => ['price' => ['type' => 'long']]]]; + } + + + public function testToArray(): void + { + $sort = new \Spameri\ElasticQuery\Options\ScriptSort( + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].value"), + type: \Spameri\ElasticQuery\Options\ScriptSort::TYPE_NUMBER, + order: \Spameri\ElasticQuery\Options\Sort::DESC, + ); + + $array = $sort->toArray(); + + \Tester\Assert::same('number', $array['_script']['type']); + \Tester\Assert::same('DESC', $array['_script']['order']); + \Tester\Assert::same("doc['price'].value", $array['_script']['script']['source']); + } + + + public function testRejectsInvalidType(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Options\ScriptSort( + script: new \Spameri\ElasticQuery\Script(source: ''), + type: 'invalid', + ); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['price' => 100]); + $this->indexDocument(['price' => 50]); + + $scriptSort = new \Spameri\ElasticQuery\Options\ScriptSort( + script: new \Spameri\ElasticQuery\Script(source: "doc['price'].value"), + order: \Spameri\ElasticQuery\Options\Sort::DESC, + ); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(); + $elasticQuery->options()->sort()->add($scriptSort); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new ScriptSort())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/Sort.phpt b/tests/SpameriTests/ElasticQuery/Options/Sort.phpt new file mode 100644 index 0000000..3d78e13 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/Sort.phpt @@ -0,0 +1,138 @@ +toArray(); + + \Tester\Assert::same([ + 'created_at' => [ + 'order' => 'DESC', + 'missing' => '_last', + ], + ], $array); + } + + + public function testToArrayAscending(): void + { + $sort = new \Spameri\ElasticQuery\Options\Sort( + 'name', + \Spameri\ElasticQuery\Options\Sort::ASC, + ); + + $array = $sort->toArray(); + + \Tester\Assert::same([ + 'name' => [ + 'order' => 'ASC', + 'missing' => '_last', + ], + ], $array); + } + + + public function testToArrayMissingFirst(): void + { + $sort = new \Spameri\ElasticQuery\Options\Sort( + 'price', + \Spameri\ElasticQuery\Options\Sort::ASC, + \Spameri\ElasticQuery\Options\Sort::MISSING_FIRST, + ); + + $array = $sort->toArray(); + + \Tester\Assert::same([ + 'price' => [ + 'order' => 'ASC', + 'missing' => '_first', + ], + ], $array); + } + + + public function testToArrayMissingLast(): void + { + $sort = new \Spameri\ElasticQuery\Options\Sort( + 'stock', + \Spameri\ElasticQuery\Options\Sort::DESC, + \Spameri\ElasticQuery\Options\Sort::MISSING_LAST, + ); + + $array = $sort->toArray(); + + \Tester\Assert::same([ + 'stock' => [ + 'order' => 'DESC', + 'missing' => '_last', + ], + ], $array); + } + + + public function testKey(): void + { + $sort = new \Spameri\ElasticQuery\Options\Sort('field_name'); + + \Tester\Assert::same('field_name', $sort->key()); + } + + + public function testDefaultValues(): void + { + $sort = new \Spameri\ElasticQuery\Options\Sort('default_field'); + + $array = $sort->toArray(); + + \Tester\Assert::same('DESC', $array['default_field']['order']); + \Tester\Assert::same('_last', $array['default_field']['missing']); + } + + + public function testConstants(): void + { + \Tester\Assert::same('ASC', \Spameri\ElasticQuery\Options\Sort::ASC); + \Tester\Assert::same('DESC', \Spameri\ElasticQuery\Options\Sort::DESC); + \Tester\Assert::same('_last', \Spameri\ElasticQuery\Options\Sort::MISSING_LAST); + \Tester\Assert::same('_first', \Spameri\ElasticQuery\Options\Sort::MISSING_FIRST); + } + + + public function testInvalidSortTypeThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Options\Sort('field', 'INVALID'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + 'Sorting type INVALID is out of allowed range. See \Spameri\ElasticQuery\Options\Sort for reference.', + ); + } + + + public function testInvalidMissingThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Options\Sort('field', \Spameri\ElasticQuery\Options\Sort::ASC, '_invalid'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + 'Sorting by missing value on filed field is out of allowed range. See \Spameri\ElasticQuery\Options\Sort for reference.', + ); + } + +} + +(new Sort())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/SortCollection.phpt b/tests/SpameriTests/ElasticQuery/Options/SortCollection.phpt new file mode 100644 index 0000000..babecbb --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/SortCollection.phpt @@ -0,0 +1,212 @@ +toArray(); + + \Tester\Assert::same([], $array); + } + + + public function testToArraySingleSort(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('created_at', \Spameri\ElasticQuery\Options\Sort::DESC)); + + $array = $collection->toArray(); + + \Tester\Assert::count(1, $array); + \Tester\Assert::same([ + 'created_at' => [ + 'order' => 'DESC', + 'missing' => '_last', + ], + ], $array[0]); + } + + + public function testToArrayMultipleSorts(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('priority', \Spameri\ElasticQuery\Options\Sort::DESC)); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('name', \Spameri\ElasticQuery\Options\Sort::ASC)); + + $array = $collection->toArray(); + + \Tester\Assert::count(2, $array); + \Tester\Assert::true(isset($array[0]['priority'])); + \Tester\Assert::true(isset($array[1]['name'])); + } + + + public function testAddSort(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + + \Tester\Assert::same(0, $collection->count()); + + $collection->add(new \Spameri\ElasticQuery\Options\Sort('field1')); + + \Tester\Assert::same(1, $collection->count()); + + $collection->add(new \Spameri\ElasticQuery\Options\Sort('field2')); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testGetSort(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $sort = new \Spameri\ElasticQuery\Options\Sort('test_field'); + $collection->add($sort); + + $retrieved = $collection->get('test_field'); + + \Tester\Assert::same($sort, $retrieved); + } + + + public function testGetNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + + $retrieved = $collection->get('non_existent'); + + \Tester\Assert::null($retrieved); + } + + + public function testRemoveSort(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('to_remove')); + + \Tester\Assert::same(1, $collection->count()); + + $result = $collection->remove('to_remove'); + + \Tester\Assert::true($result); + \Tester\Assert::same(0, $collection->count()); + } + + + public function testRemoveNonExistent(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + + $result = $collection->remove('non_existent'); + + \Tester\Assert::false($result); + } + + + public function testIsValue(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('existing')); + + \Tester\Assert::true($collection->isValue('existing')); + \Tester\Assert::false($collection->isValue('non_existing')); + } + + + public function testClear(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('field1')); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('field2')); + + \Tester\Assert::same(2, $collection->count()); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + } + + + public function testKeys(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('first')); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('second')); + + $keys = $collection->keys(); + + \Tester\Assert::same(['first', 'second'], $keys); + } + + + public function testIterator(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('a')); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('b')); + + $count = 0; + foreach ($collection as $key => $sort) { + \Tester\Assert::type(\Spameri\ElasticQuery\Options\Sort::class, $sort); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testConstructorWithInitialSorts(): void + { + $sort1 = new \Spameri\ElasticQuery\Options\Sort('field1'); + $sort2 = new \Spameri\ElasticQuery\Options\Sort('field2'); + + $collection = new \Spameri\ElasticQuery\Options\SortCollection($sort1, $sort2); + + \Tester\Assert::same(2, $collection->count()); + \Tester\Assert::notNull($collection->get('field1')); + \Tester\Assert::notNull($collection->get('field2')); + } + + + public function testMixedSortTypes(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('name', \Spameri\ElasticQuery\Options\Sort::ASC)); + $collection->add(new \Spameri\ElasticQuery\Options\GeoDistanceSort( + field: 'location', + lat: 48.8566, + lon: 2.3522, + )); + + $array = $collection->toArray(); + + \Tester\Assert::count(2, $array); + \Tester\Assert::true(isset($array[0]['name'])); + \Tester\Assert::true(isset($array[1]['_geo_distance'])); + } + + + public function testSameKeyReplaces(): void + { + $collection = new \Spameri\ElasticQuery\Options\SortCollection(); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('field', \Spameri\ElasticQuery\Options\Sort::ASC)); + $collection->add(new \Spameri\ElasticQuery\Options\Sort('field', \Spameri\ElasticQuery\Options\Sort::DESC)); + + \Tester\Assert::same(1, $collection->count()); + + $array = $collection->toArray(); + + \Tester\Assert::same('DESC', $array[0]['field']['order']); + } + +} + +(new SortCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/Source.phpt b/tests/SpameriTests/ElasticQuery/Options/Source.phpt new file mode 100644 index 0000000..755c946 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/Source.phpt @@ -0,0 +1,54 @@ +value()); + } + + + public function testValueIncludesExcludes(): void + { + $source = new \Spameri\ElasticQuery\Options\Source( + includes: ['title', 'body'], + excludes: ['password'], + ); + + $value = $source->value(); + \Tester\Assert::same(['title', 'body'], $value['includes']); + \Tester\Assert::same(['password'], $value['excludes']); + } + + + public function testCreate(): void + { + $this->indexDocument(['title' => 'hello', 'secret' => 'shh']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery( + options: new \Spameri\ElasticQuery\Options( + source: new \Spameri\ElasticQuery\Options\Source( + includes: ['title'], + excludes: ['secret'], + ), + ), + ); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Source())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Options/Suggest.phpt b/tests/SpameriTests/ElasticQuery/Options/Suggest.phpt new file mode 100644 index 0000000..57ea3e2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Options/Suggest.phpt @@ -0,0 +1,117 @@ + [ + 'properties' => [ + 'title' => ['type' => 'text'], + 'suggest' => ['type' => 'completion'], + ], + ], + ]; + } + + + public function testTermSuggesterToArray(): void + { + $suggester = new \Spameri\ElasticQuery\Options\Suggest\TermSuggester( + name: 'title_suggest', + text: 'tring', + field: 'title', + size: 3, + ); + + $array = $suggester->toArray(); + + \Tester\Assert::same('tring', $array['text']); + \Tester\Assert::same('title', $array['term']['field']); + \Tester\Assert::same(3, $array['term']['size']); + } + + + public function testPhraseSuggesterToArray(): void + { + $suggester = new \Spameri\ElasticQuery\Options\Suggest\PhraseSuggester( + name: 'p', + text: 'noble prize', + field: 'title', + size: 5, + confidence: 0.9, + ); + + \Tester\Assert::same('phrase', \array_keys($suggester->toArray())[1]); + \Tester\Assert::same(0.9, $suggester->toArray()['phrase']['confidence']); + } + + + public function testCompletionSuggesterToArray(): void + { + $suggester = new \Spameri\ElasticQuery\Options\Suggest\CompletionSuggester( + name: 'c', + prefix: 'app', + field: 'suggest', + skipDuplicates: true, + ); + + \Tester\Assert::same('app', $suggester->toArray()['prefix']); + \Tester\Assert::true($suggester->toArray()['completion']['skip_duplicates']); + } + + + public function testCreateTermSuggester(): void + { + $this->indexDocument(['title' => 'string theory']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery( + options: new \Spameri\ElasticQuery\Options( + suggesters: [ + new \Spameri\ElasticQuery\Options\Suggest\TermSuggester( + name: 'my_suggest', + text: 'tring', + field: 'title', + ), + ], + ), + ); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(1, $this->search($elasticQuery)->stats()->total()); + } + + + public function testCreateCompletionSuggester(): void + { + $this->indexDocument(['title' => 'apple', 'suggest' => 'apple']); + $this->indexDocument(['title' => 'application', 'suggest' => 'application']); + + $elasticQuery = new \Spameri\ElasticQuery\ElasticQuery( + options: new \Spameri\ElasticQuery\Options( + suggesters: [ + new \Spameri\ElasticQuery\Options\Suggest\CompletionSuggester( + name: 'my_complete', + prefix: 'app', + field: 'suggest', + ), + ], + ), + ); + $elasticQuery->addMustQuery(new \Spameri\ElasticQuery\Query\MatchAll()); + + \Tester\Assert::same(2, $this->search($elasticQuery)->stats()->total()); + } + +} + +(new Suggest())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Boosting.phpt b/tests/SpameriTests/ElasticQuery/Query/Boosting.phpt new file mode 100644 index 0000000..510ff24 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Boosting.phpt @@ -0,0 +1,67 @@ +toArray(); + + \Tester\Assert::same(0.5, $array['boosting']['negative_boost']); + \Tester\Assert::same('apple', $array['boosting']['positive']['term']['text']['value']); + \Tester\Assert::same('pie', $array['boosting']['negative']['term']['text']['value']); + } + + + public function testKey(): void + { + $boosting = new \Spameri\ElasticQuery\Query\Boosting( + new \Spameri\ElasticQuery\Query\Term('text', 'a'), + new \Spameri\ElasticQuery\Query\Term('text', 'b'), + 0.5, + ); + + \Tester\Assert::same('boosting_term_text_a_term_text_b', $boosting->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Boosting())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/CombinedFields.phpt b/tests/SpameriTests/ElasticQuery/Query/CombinedFields.phpt new file mode 100644 index 0000000..ac9fb78 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/CombinedFields.phpt @@ -0,0 +1,84 @@ +toArray(); + + \Tester\Assert::same(['title', 'abstract', 'body'], $array['combined_fields']['fields']); + \Tester\Assert::same('and', $array['combined_fields']['operator']); + } + + + public function testAutoGenerateSynonyms(): void + { + $cf = new \Spameri\ElasticQuery\Query\CombinedFields( + fields: ['body'], + query: 'x', + autoGenerateSynonymsPhraseQuery: false, + ); + + \Tester\Assert::false($cf->toArray()['combined_fields']['auto_generate_synonyms_phrase_query']); + } + + + public function testRequiresFields(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\CombinedFields([], 'x'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testKey(): void + { + $cf = new \Spameri\ElasticQuery\Query\CombinedFields(['title', 'body'], 'q'); + \Tester\Assert::same('combined_fields_title-body_q', $cf->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['title' => 'distributed search', 'body' => 'engine']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\CombinedFields( + fields: ['title', 'body'], + query: 'distributed search', + operator: 'and', + autoGenerateSynonymsPhraseQuery: true, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new CombinedFields())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/ConstantScore.phpt b/tests/SpameriTests/ElasticQuery/Query/ConstantScore.phpt new file mode 100644 index 0000000..8d965cf --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/ConstantScore.phpt @@ -0,0 +1,63 @@ +toArray(); + + \Tester\Assert::same(1.2, $array['constant_score']['boost']); + \Tester\Assert::same('active', $array['constant_score']['filter']['term']['status']['value']); + } + + + public function testKey(): void + { + $cs = new \Spameri\ElasticQuery\Query\ConstantScore( + new \Spameri\ElasticQuery\Query\Term('status', 'active'), + ); + + \Tester\Assert::same('constant_score_term_status_active', $cs->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new ConstantScore())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/DisMax.phpt b/tests/SpameriTests/ElasticQuery/Query/DisMax.phpt new file mode 100644 index 0000000..dd6c3f7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/DisMax.phpt @@ -0,0 +1,64 @@ +addQuery(new \Spameri\ElasticQuery\Query\Term('body', 'foo')); + + $array = $disMax->toArray(); + + \Tester\Assert::same(0.7, $array['dis_max']['tie_breaker']); + \Tester\Assert::count(2, $array['dis_max']['queries']); + } + + + public function testKey(): void + { + $disMax = new \Spameri\ElasticQuery\Query\DisMax( + new \Spameri\ElasticQuery\Query\Term('title', 'foo'), + ); + + \Tester\Assert::same('dis_max_term_title_foo', $disMax->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new DisMax())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/DistanceFeature.phpt b/tests/SpameriTests/ElasticQuery/Query/DistanceFeature.phpt new file mode 100644 index 0000000..9a0a499 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/DistanceFeature.phpt @@ -0,0 +1,76 @@ +toArray(); + + \Tester\Assert::same('now', $array['distance_feature']['origin']); + \Tester\Assert::same('7d', $array['distance_feature']['pivot']); + } + + + public function testToArrayGeo(): void + { + $df = new \Spameri\ElasticQuery\Query\DistanceFeature( + field: 'location', + origin: [50.0, 14.4], + pivot: '1000m', + ); + + $array = $df->toArray(); + + \Tester\Assert::same([50.0, 14.4], $array['distance_feature']['origin']); + } + + + public function testKey(): void + { + $df = new \Spameri\ElasticQuery\Query\DistanceFeature('location', [0, 0], '1km'); + + \Tester\Assert::same('distance_feature_location', $df->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new DistanceFeature())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/ElasticMatch.phpt b/tests/SpameriTests/ElasticQuery/Query/ElasticMatch.phpt index c3de058..8ebe3fc 100644 --- a/tests/SpameriTests/ElasticQuery/Query/ElasticMatch.phpt +++ b/tests/SpameriTests/ElasticQuery/Query/ElasticMatch.phpt @@ -5,98 +5,116 @@ namespace SpameriTests\ElasticQuery\Query; require_once __DIR__ . '/../../bootstrap.php'; -class ElasticMatch extends \Tester\TestCase +class ElasticMatch extends \SpameriTests\ElasticQuery\AbstractElasticTestCase { - private const INDEX = 'spameri_test_video_match'; + protected const INDEX = 'spameri_test_query_match'; - public function setUp() : void - { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); - } - - - public function testCreate() : void + public function testToArray(): void { $match = new \Spameri\ElasticQuery\Query\ElasticMatch( 'name', 'Avengers', 1.0, - new \Spameri\ElasticQuery\Query\Match\Fuzziness( - \Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO - ), + new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO), 2, \Spameri\ElasticQuery\Query\Match\Operator::OR, - 'standard' + 'standard', ); $array = $match->toArray(); - \Tester\Assert::true(isset($array['match']['name']['query'])); \Tester\Assert::same('Avengers', $array['match']['name']['query']); \Tester\Assert::same(1.0, $array['match']['name']['boost']); \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\Operator::OR, $array['match']['name']['operator']); \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO, $array['match']['name']['fuzziness']); \Tester\Assert::same('standard', $array['match']['name']['analyzer']); \Tester\Assert::same(2, $array['match']['name']['minimum_should_match']); + } - $document = new \Spameri\ElasticQuery\Document( - self::INDEX, - new \Spameri\ElasticQuery\Document\Body\Plain( - ( - new \Spameri\ElasticQuery\ElasticQuery( - new \Spameri\ElasticQuery\Query\QueryCollection( - NULL, - new \Spameri\ElasticQuery\Query\MustCollection( - $match - ) - ) - ) - )->toArray() - ), - self::INDEX + + public function testToArrayWithAllNewOptions(): void + { + $match = new \Spameri\ElasticQuery\Query\ElasticMatch( + field: 'name', + query: 'Avengers', + zeroTermsQuery: 'all', + autoGenerateSynonymsPhraseQuery: false, + lenient: true, + prefixLength: 1, + maxExpansions: 50, + fuzzyTranspositions: false, + fuzzyRewrite: 'constant_score', ); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - \json_encode($document->toArray()['body']) + $array = $match->toArray(); + + \Tester\Assert::same('all', $array['match']['name']['zero_terms_query']); + \Tester\Assert::false($array['match']['name']['auto_generate_synonyms_phrase_query']); + \Tester\Assert::true($array['match']['name']['lenient']); + \Tester\Assert::same(1, $array['match']['name']['prefix_length']); + \Tester\Assert::same(50, $array['match']['name']['max_expansions']); + \Tester\Assert::false($array['match']['name']['fuzzy_transpositions']); + \Tester\Assert::same('constant_score', $array['match']['name']['fuzzy_rewrite']); + } + + + public function testMinimumShouldMatchString(): void + { + $match = new \Spameri\ElasticQuery\Query\ElasticMatch('name', 'Avengers Endgame', 1.0, null, '75%'); + \Tester\Assert::same('75%', $match->toArray()['match']['name']['minimum_should_match']); + } + + + public function testCreate(): void + { + $this->indexDocument(['name' => 'Avengers Endgame']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\ElasticMatch('name', 'Avengers'), + ), + ), ); - \Tester\Assert::noError(static function () use ($ch) { - $response = curl_exec($ch); - $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); - /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ - $result = $resultMapper->map(\json_decode($response, TRUE)); - \Tester\Assert::type('int', $result->stats()->total()); - }); + $result = $this->search($query); - curl_close($ch); + \Tester\Assert::same(1, $result->stats()->total()); } - public function tearDown() : void + public function testCreateWithAllOptions(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + $this->indexDocument(['name' => 'Avengers Endgame Infinity']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\ElasticMatch( + field: 'name', + query: 'Avengers', + boost: 2.0, + fuzziness: new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO), + operator: \Spameri\ElasticQuery\Query\Match\Operator::OR, + analyzer: 'standard', + zeroTermsQuery: 'none', + autoGenerateSynonymsPhraseQuery: true, + lenient: true, + prefixLength: 0, + maxExpansions: 50, + fuzzyTranspositions: true, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); } } diff --git a/tests/SpameriTests/ElasticQuery/Query/Exists.phpt b/tests/SpameriTests/ElasticQuery/Query/Exists.phpt new file mode 100644 index 0000000..c77bc01 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Exists.phpt @@ -0,0 +1,46 @@ +toArray(); + + \Tester\Assert::same('user', $array['exists']['field']); + \Tester\Assert::same(2.0, $array['exists']['boost']); + } + + + public function testCreate(): void + { + $this->indexDocument(['title' => 'foo']); + $this->indexDocument(['other' => 'bar']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Exists('title'), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Exists())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/FieldMaskingSpan.phpt b/tests/SpameriTests/ElasticQuery/Query/FieldMaskingSpan.phpt new file mode 100644 index 0000000..e5a0474 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/FieldMaskingSpan.phpt @@ -0,0 +1,64 @@ +toArray(); + + \Tester\Assert::same('text', $array['field_masking_span']['field']); + \Tester\Assert::true(isset($array['field_masking_span']['query']['span_term'])); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\FieldMaskingSpan( + new \Spameri\ElasticQuery\Query\SpanTerm('text.stems', 'fox'), + 'text', + ); + + \Tester\Assert::same('field_masking_span_text_span_term_text.stems_fox', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new FieldMaskingSpan())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Fuzzy.phpt b/tests/SpameriTests/ElasticQuery/Query/Fuzzy.phpt index 00b0cc4..03134d6 100644 --- a/tests/SpameriTests/ElasticQuery/Query/Fuzzy.phpt +++ b/tests/SpameriTests/ElasticQuery/Query/Fuzzy.phpt @@ -5,94 +5,86 @@ namespace SpameriTests\ElasticQuery\Query; require_once __DIR__ . '/../../bootstrap.php'; -class Fuzzy extends \Tester\TestCase +class Fuzzy extends \SpameriTests\ElasticQuery\AbstractElasticTestCase { - private const INDEX = 'spameri_test_video_fuzzy'; + protected const INDEX = 'spameri_test_query_fuzzy'; - public function setUp() : void + public function testToArray(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); - } - - - public function testCreate() : void - { - $fuzzy = new \Spameri\ElasticQuery\Query\Fuzzy( - 'name', - 'Avengers', - 1.0, - 2, - 0, - 100 - ); + $fuzzy = new \Spameri\ElasticQuery\Query\Fuzzy('name', 'Avengers', 1.0, 2, 0, 100); $array = $fuzzy->toArray(); - \Tester\Assert::true(isset($array['fuzzy']['name']['value'])); \Tester\Assert::same('Avengers', $array['fuzzy']['name']['value']); \Tester\Assert::same(1.0, $array['fuzzy']['name']['boost']); \Tester\Assert::same(2, $array['fuzzy']['name']['fuzziness']); \Tester\Assert::same(0, $array['fuzzy']['name']['prefix_length']); \Tester\Assert::same(100, $array['fuzzy']['name']['max_expansions']); + } - $document = new \Spameri\ElasticQuery\Document( - self::INDEX, - new \Spameri\ElasticQuery\Document\Body\Plain( - ( - new \Spameri\ElasticQuery\ElasticQuery( - new \Spameri\ElasticQuery\Query\QueryCollection( - NULL, - new \Spameri\ElasticQuery\Query\MustCollection( - $fuzzy - ) - ) - ) - )->toArray() - ), - self::INDEX + + public function testToArrayWithTranspositionsAndRewrite(): void + { + $fuzzy = new \Spameri\ElasticQuery\Query\Fuzzy( + field: 'name', + query: 'Avengers', + transpositions: false, + rewrite: 'constant_score', ); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - \json_encode($document->toArray()['body']) + $array = $fuzzy->toArray(); + + \Tester\Assert::false($array['fuzzy']['name']['transpositions']); + \Tester\Assert::same('constant_score', $array['fuzzy']['name']['rewrite']); + } + + + public function testCreate(): void + { + $this->indexDocument(['name' => 'Avengers']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Fuzzy('name', 'Avengars'), + ), + ), ); - \Tester\Assert::noError(static function () use ($ch) { - $response = curl_exec($ch); - $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); - /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ - $result = $resultMapper->map(\json_decode($response, TRUE)); - \Tester\Assert::type('int', $result->stats()->total()); - }); + $result = $this->search($query); - curl_close($ch); + \Tester\Assert::same(1, $result->stats()->total()); } - public function tearDown() : void + public function testCreateWithAllOptions(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + $this->indexDocument(['name' => 'Avengers']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Fuzzy( + field: 'name', + query: 'Avengars', + boost: 1.0, + fuzziness: 2, + prefixLength: 0, + maxExpansion: 50, + transpositions: true, + rewrite: 'constant_score', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); } } diff --git a/tests/SpameriTests/ElasticQuery/Query/GeoBoundingBox.phpt b/tests/SpameriTests/ElasticQuery/Query/GeoBoundingBox.phpt new file mode 100644 index 0000000..8a53fe1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/GeoBoundingBox.phpt @@ -0,0 +1,87 @@ + ['properties' => ['location' => ['type' => 'geo_point']]]]; + } + + + public function testToArray(): void + { + $gbb = new \Spameri\ElasticQuery\Query\GeoBoundingBox( + field: 'location', + topLeftLat: 40.73, + topLeftLon: -74.1, + bottomRightLat: 40.01, + bottomRightLon: -71.12, + ); + + $array = $gbb->toArray(); + + \Tester\Assert::same(40.73, $array['geo_bounding_box']['location']['top_left']['lat']); + \Tester\Assert::same(-71.12, $array['geo_bounding_box']['location']['bottom_right']['lon']); + \Tester\Assert::same(1.0, $array['geo_bounding_box']['boost']); + } + + + public function testToArrayWithAllOptions(): void + { + $gbb = new \Spameri\ElasticQuery\Query\GeoBoundingBox( + field: 'location', + topLeftLat: 40.73, + topLeftLon: -74.1, + bottomRightLat: 40.01, + bottomRightLon: -71.12, + type: 'memory', + validationMethod: 'COERCE', + ignoreUnmapped: true, + boost: 2.0, + ); + + $array = $gbb->toArray(); + + \Tester\Assert::same('memory', $array['geo_bounding_box']['type']); + \Tester\Assert::same('COERCE', $array['geo_bounding_box']['validation_method']); + \Tester\Assert::true($array['geo_bounding_box']['ignore_unmapped']); + \Tester\Assert::same(2.0, $array['geo_bounding_box']['boost']); + } + + + public function testCreate(): void + { + $this->indexDocument(['location' => ['lat' => 40.5, 'lon' => -73.0]]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\GeoBoundingBox( + field: 'location', + topLeftLat: 41.0, + topLeftLon: -75.0, + bottomRightLat: 40.0, + bottomRightLon: -71.0, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new GeoBoundingBox())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/GeoDistance.phpt b/tests/SpameriTests/ElasticQuery/Query/GeoDistance.phpt new file mode 100644 index 0000000..618128d --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/GeoDistance.phpt @@ -0,0 +1,105 @@ + [ + 'properties' => [ + 'location' => ['type' => 'geo_point'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $geoDistance = new \Spameri\ElasticQuery\Query\GeoDistance( + 'location', + 40.73, + -74.1, + '200km', + ); + + $array = $geoDistance->toArray(); + + \Tester\Assert::same('200km', $array['geo_distance']['distance']); + \Tester\Assert::same(40.73, $array['geo_distance']['location']['lat']); + \Tester\Assert::same(-74.1, $array['geo_distance']['location']['lon']); + \Tester\Assert::same(1.0, $array['geo_distance']['boost']); + } + + + public function testWithAllOptions(): void + { + $geoDistance = new \Spameri\ElasticQuery\Query\GeoDistance( + field: 'location', + lat: 48.8566, + lon: 2.3522, + distance: '10km', + distanceType: 'plane', + validationMethod: 'COERCE', + ignoreUnmapped: true, + boost: 2.0, + ); + + $array = $geoDistance->toArray(); + + \Tester\Assert::same('plane', $array['geo_distance']['distance_type']); + \Tester\Assert::same('COERCE', $array['geo_distance']['validation_method']); + \Tester\Assert::true($array['geo_distance']['ignore_unmapped']); + \Tester\Assert::same(2.0, $array['geo_distance']['boost']); + } + + + public function testKey(): void + { + $geoDistance = new \Spameri\ElasticQuery\Query\GeoDistance( + 'position', + 51.5074, + -0.1278, + '5km', + ); + + \Tester\Assert::same('geo_distance_position_51.5074.-0.1278_5km', $geoDistance->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['location' => ['lat' => 40.73, 'lon' => -74.1]]); + $this->indexDocument(['location' => ['lat' => 0.0, 'lon' => 0.0]]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\GeoDistance( + field: 'location', + lat: 40.73, + lon: -74.1, + distance: '50km', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new GeoDistance())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/GeoShape.phpt b/tests/SpameriTests/ElasticQuery/Query/GeoShape.phpt new file mode 100644 index 0000000..9ebff25 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/GeoShape.phpt @@ -0,0 +1,139 @@ + ['properties' => ['location' => ['type' => 'geo_shape']]]]; + } + + + public function testToArray(): void + { + $geoShape = new \Spameri\ElasticQuery\Query\GeoShape( + field: 'location', + shape: ['type' => 'envelope', 'coordinates' => [[13.0, 53.0], [14.0, 52.0]]], + relation: 'within', + ); + + $array = $geoShape->toArray(); + + \Tester\Assert::same('envelope', $array['geo_shape']['location']['shape']['type']); + \Tester\Assert::same('within', $array['geo_shape']['location']['relation']); + } + + + public function testToArrayWithIndexedShape(): void + { + $geoShape = new \Spameri\ElasticQuery\Query\GeoShape( + field: 'location', + indexedShape: new \Spameri\ElasticQuery\Query\IndexedShape( + id: 'deu', + index: 'shapes', + path: 'location', + routing: 'eu', + ), + boost: 2.0, + ); + + $array = $geoShape->toArray(); + + \Tester\Assert::same('deu', $array['geo_shape']['location']['indexed_shape']['id']); + \Tester\Assert::same('shapes', $array['geo_shape']['location']['indexed_shape']['index']); + \Tester\Assert::same('eu', $array['geo_shape']['location']['indexed_shape']['routing']); + \Tester\Assert::same(2.0, $array['geo_shape']['boost']); + } + + + public function testRelationValidated(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\GeoShape('loc', ['type' => 'point', 'coordinates' => [0, 0]], 'nonsense'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testRequiresShapeOrIndexedShape(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\GeoShape('loc'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['location' => ['type' => 'point', 'coordinates' => [13.5, 52.5]]]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\GeoShape( + field: 'location', + shape: ['type' => 'envelope', 'coordinates' => [[13.0, 53.0], [14.0, 52.0]]], + relation: 'within', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + + + public function testCreateWithIndexedShape(): void + { + $this->request('PUT', self::SHAPE_INDEX, ['mappings' => ['properties' => ['location' => ['type' => 'geo_shape']]]]); + $this->request( + 'PUT', + self::SHAPE_INDEX . '/_doc/deu?refresh=true', + ['location' => ['type' => 'envelope', 'coordinates' => [[13.0, 53.0], [14.0, 52.0]]]], + ); + $this->indexDocument(['location' => ['type' => 'point', 'coordinates' => [13.5, 52.5]]]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\GeoShape( + field: 'location', + relation: 'within', + indexedShape: new \Spameri\ElasticQuery\Query\IndexedShape( + id: 'deu', + index: self::SHAPE_INDEX, + path: 'location', + ), + ), + ), + ), + ); + + $result = $this->search($query); + + $this->request('DELETE', self::SHAPE_INDEX); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new GeoShape())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/HasChild.phpt b/tests/SpameriTests/ElasticQuery/Query/HasChild.phpt new file mode 100644 index 0000000..fa90a34 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/HasChild.phpt @@ -0,0 +1,103 @@ + [ + 'properties' => [ + 'my_join_field' => [ + 'type' => 'join', + 'relations' => ['blog' => 'comment'], + ], + 'author' => ['type' => 'keyword'], + 'tag' => ['type' => 'keyword'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $hasChild = new \Spameri\ElasticQuery\Query\HasChild( + type: 'comment', + query: new \Spameri\ElasticQuery\Query\Term('author', 'john'), + scoreMode: 'max', + minChildren: 1, + ); + + $array = $hasChild->toArray(); + + \Tester\Assert::same('comment', $array['has_child']['type']); + \Tester\Assert::same('max', $array['has_child']['score_mode']); + \Tester\Assert::same(1, $array['has_child']['min_children']); + } + + + public function testToArrayWithInnerHits(): void + { + $hasChild = new \Spameri\ElasticQuery\Query\HasChild( + type: 'comment', + query: new \Spameri\ElasticQuery\Query\Term('author', 'john'), + innerHits: new \Spameri\ElasticQuery\Query\InnerHits(name: 'matched', size: 3), + ); + + $array = $hasChild->toArray(); + + \Tester\Assert::same('matched', $array['has_child']['inner_hits']['name']); + \Tester\Assert::same(3, $array['has_child']['inner_hits']['size']); + } + + + public function testKey(): void + { + $hasChild = new \Spameri\ElasticQuery\Query\HasChild( + 'comment', + new \Spameri\ElasticQuery\Query\Term('author', 'john'), + ); + + \Tester\Assert::same('has_child_comment', $hasChild->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['tag' => 'tech', 'my_join_field' => 'blog'], id: '1'); + $this->request( + 'PUT', + self::INDEX . '/_doc/2?refresh=true&routing=1', + ['author' => 'john', 'my_join_field' => ['name' => 'comment', 'parent' => '1']], + ); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\HasChild( + type: 'comment', + query: new \Spameri\ElasticQuery\Query\Term('author', 'john'), + innerHits: new \Spameri\ElasticQuery\Query\InnerHits(), + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new HasChild())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/HasParent.phpt b/tests/SpameriTests/ElasticQuery/Query/HasParent.phpt new file mode 100644 index 0000000..cfcb491 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/HasParent.phpt @@ -0,0 +1,99 @@ + [ + 'properties' => [ + 'my_join_field' => [ + 'type' => 'join', + 'relations' => ['blog' => 'comment'], + ], + 'tag' => ['type' => 'keyword'], + 'author' => ['type' => 'keyword'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $hasParent = new \Spameri\ElasticQuery\Query\HasParent( + parentType: 'blog', + query: new \Spameri\ElasticQuery\Query\Term('tag', 'tech'), + score: true, + ); + + $array = $hasParent->toArray(); + + \Tester\Assert::same('blog', $array['has_parent']['parent_type']); + \Tester\Assert::true($array['has_parent']['score']); + } + + + public function testToArrayWithInnerHits(): void + { + $hasParent = new \Spameri\ElasticQuery\Query\HasParent( + parentType: 'blog', + query: new \Spameri\ElasticQuery\Query\Term('tag', 'tech'), + innerHits: new \Spameri\ElasticQuery\Query\InnerHits(name: 'parent'), + ); + + \Tester\Assert::same('parent', $hasParent->toArray()['has_parent']['inner_hits']['name']); + } + + + public function testKey(): void + { + $hasParent = new \Spameri\ElasticQuery\Query\HasParent( + 'blog', + new \Spameri\ElasticQuery\Query\Term('tag', 'tech'), + ); + + \Tester\Assert::same('has_parent_blog', $hasParent->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['tag' => 'tech', 'my_join_field' => 'blog'], id: '1'); + $this->request( + 'PUT', + self::INDEX . '/_doc/2?refresh=true&routing=1', + ['author' => 'john', 'my_join_field' => ['name' => 'comment', 'parent' => '1']], + ); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\HasParent( + parentType: 'blog', + query: new \Spameri\ElasticQuery\Query\Term('tag', 'tech'), + score: true, + innerHits: new \Spameri\ElasticQuery\Query\InnerHits(), + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new HasParent())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Ids.phpt b/tests/SpameriTests/ElasticQuery/Query/Ids.phpt new file mode 100644 index 0000000..6f288a4 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Ids.phpt @@ -0,0 +1,105 @@ +toArray(); + + \Tester\Assert::same(['1', '2', '3'], $array['ids']['values']); + } + + + public function testKey(): void + { + $ids = new \Spameri\ElasticQuery\Query\Ids(['1', '2']); + + \Tester\Assert::same('ids_1-2', $ids->key()); + } + + + public function testEmptyRejected(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Ids([]); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $ids = new \Spameri\ElasticQuery\Query\Ids(['1', '2']); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + (new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection($ids), + ), + ))->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type('int', $result->stats()->total()); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Ids())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Intervals.phpt b/tests/SpameriTests/ElasticQuery/Query/Intervals.phpt new file mode 100644 index 0000000..63aeced --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Intervals.phpt @@ -0,0 +1,77 @@ + [ + 'query' => 'my favorite food', + 'max_gaps' => 0, + 'ordered' => true, + ], + ], + ); + + $array = $intervals->toArray(); + + \Tester\Assert::same('my favorite food', $array['intervals']['my_text']['match']['query']); + } + + + public function testRequiresRule(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Intervals('f', []); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testKey(): void + { + $intervals = new \Spameri\ElasticQuery\Query\Intervals('f', ['match' => ['query' => 'x']]); + + \Tester\Assert::same('intervals_f', $intervals->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Intervals())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Knn.phpt b/tests/SpameriTests/ElasticQuery/Query/Knn.phpt new file mode 100644 index 0000000..5f5af49 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Knn.phpt @@ -0,0 +1,104 @@ + [ + 'properties' => [ + 'vector' => [ + 'type' => 'dense_vector', + 'dims' => 3, + 'index' => true, + 'similarity' => 'l2_norm', + ], + ], + ], + ]; + } + + + public function testToArray(): void + { + $knn = new \Spameri\ElasticQuery\Query\Knn( + field: 'vector', + queryVector: [1.0, 2.0, 3.0], + k: 5, + numCandidates: 50, + similarity: 0.7, + boost: 1.5, + ); + + $array = $knn->toArray(); + + \Tester\Assert::same('vector', $array['knn']['field']); + \Tester\Assert::same([1.0, 2.0, 3.0], $array['knn']['query_vector']); + \Tester\Assert::same(5, $array['knn']['k']); + \Tester\Assert::same(50, $array['knn']['num_candidates']); + \Tester\Assert::same(0.7, $array['knn']['similarity']); + \Tester\Assert::same(1.5, $array['knn']['boost']); + } + + + public function testWithFilter(): void + { + $knn = new \Spameri\ElasticQuery\Query\Knn( + field: 'vector', + queryVector: [1.0, 2.0, 3.0], + k: 5, + numCandidates: 50, + filter: new \Spameri\ElasticQuery\Query\Term('status', 'published'), + ); + + \Tester\Assert::same('published', $knn->toArray()['knn']['filter']['term']['status']['value']); + } + + + public function testRequiresQueryVector(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Knn('v', [], 1, 10); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['vector' => [1.0, 2.0, 3.0]]); + $this->indexDocument(['vector' => [10.0, 10.0, 10.0]]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Knn( + field: 'vector', + queryVector: [1.1, 2.1, 3.1], + k: 1, + numCandidates: 10, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Knn())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Match/Fuzziness.phpt b/tests/SpameriTests/ElasticQuery/Query/Match/Fuzziness.phpt new file mode 100644 index 0000000..40e939e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Match/Fuzziness.phpt @@ -0,0 +1,112 @@ +__toString()); + } + + + public function testAutoWithDistanceRange(): void + { + $fuzziness = new \Spameri\ElasticQuery\Query\Match\Fuzziness('AUTO:3,6'); + + \Tester\Assert::same('AUTO:3,6', $fuzziness->__toString()); + } + + + public function testNumericFuzziness(): void + { + $fuzziness = new \Spameri\ElasticQuery\Query\Match\Fuzziness('0'); + + \Tester\Assert::same('0', $fuzziness->__toString()); + } + + + public function testNumericFuzzinessOne(): void + { + $fuzziness = new \Spameri\ElasticQuery\Query\Match\Fuzziness('1'); + + \Tester\Assert::same('1', $fuzziness->__toString()); + } + + + public function testNumericFuzzinessTwo(): void + { + $fuzziness = new \Spameri\ElasticQuery\Query\Match\Fuzziness('2'); + + \Tester\Assert::same('2', $fuzziness->__toString()); + } + + + public function testInvalidFuzzinessThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Match\Fuzziness('INVALID'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testInvalidTextThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Match\Fuzziness('fuzzy'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testAutoConstant(): void + { + \Tester\Assert::same('AUTO', \Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO); + } + + + public function testUsedInMultiMatch(): void + { + $fuzziness = new \Spameri\ElasticQuery\Query\Match\Fuzziness('AUTO'); + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title', 'description'], + 'search', + 1.0, + $fuzziness, + ); + + $array = $multiMatch->toArray(); + + \Tester\Assert::same('AUTO', $array['multi_match']['fuzziness']); + } + + + public function testUsedInElasticMatch(): void + { + $fuzziness = new \Spameri\ElasticQuery\Query\Match\Fuzziness('2'); + $match = new \Spameri\ElasticQuery\Query\ElasticMatch( + 'title', + 'search', + 1.0, + $fuzziness, + ); + + $array = $match->toArray(); + + \Tester\Assert::same('2', $array['match']['title']['fuzziness']); + } + +} + +(new Fuzziness())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Match/Operator.phpt b/tests/SpameriTests/ElasticQuery/Query/Match/Operator.phpt new file mode 100644 index 0000000..ea25a3a --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Match/Operator.phpt @@ -0,0 +1,101 @@ +toArray(); + + \Tester\Assert::same('AND', $array['multi_match']['operator']); + } + + + public function testUsedInMultiMatchWithOr(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title', 'description'], + 'search term', + 1.0, + null, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + null, + \Spameri\ElasticQuery\Query\Match\Operator::OR, + ); + + $array = $multiMatch->toArray(); + + \Tester\Assert::same('OR', $array['multi_match']['operator']); + } + + + public function testInvalidOperatorInMultiMatchThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\MultiMatch( + ['title'], + 'search', + 1.0, + null, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + null, + 'INVALID_OPERATOR', + ); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + +} + +(new Operator())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/MatchAll.phpt b/tests/SpameriTests/ElasticQuery/Query/MatchAll.phpt new file mode 100644 index 0000000..5ed6aa2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/MatchAll.phpt @@ -0,0 +1,151 @@ +toArray(); + + \Tester\Assert::true(isset($array['match_all'])); + \Tester\Assert::type(\stdClass::class, $array['match_all']); + } + + + public function testToArrayWithBoost(): void + { + $matchAll = new \Spameri\ElasticQuery\Query\MatchAll(boost: 1.5); + + $array = $matchAll->toArray(); + + \Tester\Assert::true(isset($array['match_all'])); + \Tester\Assert::same(1.5, $array['match_all']['boost']); + } + + + public function testKey(): void + { + $matchAll = new \Spameri\ElasticQuery\Query\MatchAll(); + + \Tester\Assert::same('match_all', $matchAll->key()); + } + + + public function testCreate(): void + { + $matchAll = new \Spameri\ElasticQuery\Query\MatchAll(); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + ( + new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + $matchAll, + ), + ), + ) + )->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type('int', $result->stats()->total()); + }); + } + + + public function testCreateWithBoost(): void + { + $matchAll = new \Spameri\ElasticQuery\Query\MatchAll(boost: 2.0); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + ( + new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + $matchAll, + ), + ), + ) + )->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type('int', $result->stats()->total()); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MatchAll())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/MatchBoolPrefix.phpt b/tests/SpameriTests/ElasticQuery/Query/MatchBoolPrefix.phpt new file mode 100644 index 0000000..6474131 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/MatchBoolPrefix.phpt @@ -0,0 +1,104 @@ +toArray()['match_bool_prefix']['message']['query']); + } + + + public function testToArrayWithAllOptions(): void + { + $match = new \Spameri\ElasticQuery\Query\MatchBoolPrefix( + field: 'message', + query: 'q', + boost: 1.5, + operator: 'or', + minimumShouldMatch: '50%', + analyzer: 'standard', + fuzziness: new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO), + prefixLength: 0, + maxExpansions: 50, + fuzzyTranspositions: true, + fuzzyRewrite: 'constant_score', + ); + + $array = $match->toArray(); + + \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO, $array['match_bool_prefix']['message']['fuzziness']); + \Tester\Assert::same(0, $array['match_bool_prefix']['message']['prefix_length']); + \Tester\Assert::same(50, $array['match_bool_prefix']['message']['max_expansions']); + \Tester\Assert::true($array['match_bool_prefix']['message']['fuzzy_transpositions']); + \Tester\Assert::same('constant_score', $array['match_bool_prefix']['message']['fuzzy_rewrite']); + } + + + public function testKey(): void + { + $match = new \Spameri\ElasticQuery\Query\MatchBoolPrefix('message', 'quick'); + \Tester\Assert::same('match_bool_prefix_message_quick', $match->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['message' => 'quick brown fox']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\MatchBoolPrefix('message', 'quick brown f'), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + + + public function testCreateWithAllOptions(): void + { + $this->indexDocument(['message' => 'quick brown fox']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\MatchBoolPrefix( + field: 'message', + query: 'qiuck', + fuzziness: new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO), + prefixLength: 0, + maxExpansions: 50, + fuzzyTranspositions: true, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::true($result->stats()->total() >= 0); + } + +} + +(new MatchBoolPrefix())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/MatchNone.phpt b/tests/SpameriTests/ElasticQuery/Query/MatchNone.phpt new file mode 100644 index 0000000..a0737c6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/MatchNone.phpt @@ -0,0 +1,93 @@ +toArray(); + + \Tester\Assert::true(isset($array['match_none'])); + \Tester\Assert::type(\stdClass::class, $array['match_none']); + } + + + public function testKey(): void + { + \Tester\Assert::same('match_none', (new \Spameri\ElasticQuery\Query\MatchNone())->key()); + } + + + public function testCreate(): void + { + $matchNone = new \Spameri\ElasticQuery\Query\MatchNone(); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + (new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection($matchNone), + ), + ))->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type('int', $result->stats()->total()); + }); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MatchNone())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/MatchPhrase.phpt b/tests/SpameriTests/ElasticQuery/Query/MatchPhrase.phpt index 930f611..213c699 100644 --- a/tests/SpameriTests/ElasticQuery/Query/MatchPhrase.phpt +++ b/tests/SpameriTests/ElasticQuery/Query/MatchPhrase.phpt @@ -5,92 +5,79 @@ namespace SpameriTests\ElasticQuery\Query; require_once __DIR__ . '/../../bootstrap.php'; -class MatchPhrase extends \Tester\TestCase +class MatchPhrase extends \SpameriTests\ElasticQuery\AbstractElasticTestCase { - private const INDEX = 'spameri_test_video_match_phrase'; + protected const INDEX = 'spameri_test_query_match_phrase'; - public function setUp() : void + public function testToArray(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); - } - - - public function testCreate() : void - { - $match = new \Spameri\ElasticQuery\Query\MatchPhrase( - 'name', - 'Avengers', - 1.0, - 1, - 'standard' - ); + $match = new \Spameri\ElasticQuery\Query\MatchPhrase('name', 'Avengers', 1.0, 1, 'standard'); $array = $match->toArray(); - \Tester\Assert::true(isset($array['match_phrase']['name']['query'])); \Tester\Assert::same('Avengers', $array['match_phrase']['name']['query']); \Tester\Assert::same(1.0, $array['match_phrase']['name']['boost']); \Tester\Assert::same(1, $array['match_phrase']['name']['slop']); \Tester\Assert::same('standard', $array['match_phrase']['name']['analyzer']); + } - $document = new \Spameri\ElasticQuery\Document( - self::INDEX, - new \Spameri\ElasticQuery\Document\Body\Plain( - ( - new \Spameri\ElasticQuery\ElasticQuery( - new \Spameri\ElasticQuery\Query\QueryCollection( - NULL, - new \Spameri\ElasticQuery\Query\MustCollection( - $match - ) - ) - ) - )->toArray() - ), - self::INDEX + + public function testZeroTermsQuery(): void + { + $match = new \Spameri\ElasticQuery\Query\MatchPhrase( + field: 'name', + query: 'foo', + zeroTermsQuery: 'all', ); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - \json_encode($document->toArray()['body']) + \Tester\Assert::same('all', $match->toArray()['match_phrase']['name']['zero_terms_query']); + } + + + public function testCreate(): void + { + $this->indexDocument(['name' => 'Avengers Endgame']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\MatchPhrase('name', 'Avengers Endgame'), + ), + ), ); - \Tester\Assert::noError(static function () use ($ch) { - $response = curl_exec($ch); - $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); - /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ - $result = $resultMapper->map(\json_decode($response, TRUE)); - \Tester\Assert::type('int', $result->stats()->total()); - }); + $result = $this->search($query); - curl_close($ch); + \Tester\Assert::same(1, $result->stats()->total()); } - public function tearDown() : void + public function testCreateWithAllOptions(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + $this->indexDocument(['name' => 'Avengers Endgame']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\MatchPhrase( + field: 'name', + query: 'Avengers Endgame', + boost: 1.5, + slop: 1, + analyzer: 'standard', + zeroTermsQuery: 'none', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); } } diff --git a/tests/SpameriTests/ElasticQuery/Query/MoreLikeThis.phpt b/tests/SpameriTests/ElasticQuery/Query/MoreLikeThis.phpt new file mode 100644 index 0000000..05b47f8 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/MoreLikeThis.phpt @@ -0,0 +1,106 @@ + 'imdb', '_id' => '1']], + minTermFreq: 1, + maxQueryTerms: 12, + ); + + $array = $mlt->toArray(); + + \Tester\Assert::same(['title', 'body'], $array['more_like_this']['fields']); + \Tester\Assert::same(1, $array['more_like_this']['min_term_freq']); + } + + + public function testToArrayWithAllOptions(): void + { + $mlt = new \Spameri\ElasticQuery\Query\MoreLikeThis( + fields: ['title'], + like: ['quick fox'], + boostTerms: 0.5, + include: true, + minDocFreq: 5, + maxDocFreq: 1000, + minWordLength: 2, + maxWordLength: 100, + stopWords: ['the', 'a'], + analyzer: 'standard', + boost: 2.0, + failOnUnsupportedField: false, + ); + + $array = $mlt->toArray(); + + \Tester\Assert::same(0.5, $array['more_like_this']['boost_terms']); + \Tester\Assert::true($array['more_like_this']['include']); + \Tester\Assert::same(5, $array['more_like_this']['min_doc_freq']); + \Tester\Assert::same(1000, $array['more_like_this']['max_doc_freq']); + \Tester\Assert::same(['the', 'a'], $array['more_like_this']['stop_words']); + \Tester\Assert::same(2.0, $array['more_like_this']['boost']); + \Tester\Assert::false($array['more_like_this']['fail_on_unsupported_field']); + } + + + public function testRequiresFields(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\MoreLikeThis([], ['foo']); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testRequiresLike(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\MoreLikeThis(['f'], []); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['title' => 'quick brown fox', 'body' => 'jumps over the lazy dog']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\MoreLikeThis( + fields: ['title', 'body'], + like: ['quick brown fox'], + minTermFreq: 1, + minDocFreq: 1, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new MoreLikeThis())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/MultiMatch.phpt b/tests/SpameriTests/ElasticQuery/Query/MultiMatch.phpt new file mode 100644 index 0000000..f08203b --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/MultiMatch.phpt @@ -0,0 +1,344 @@ +toArray(); + + \Tester\Assert::true(isset($array['multi_match'])); + \Tester\Assert::same('search term', $array['multi_match']['query']); + \Tester\Assert::same(['title', 'description'], $array['multi_match']['fields']); + \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, $array['multi_match']['type']); + \Tester\Assert::same(1.0, $array['multi_match']['boost']); + \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\Operator::OR, $array['multi_match']['operator']); + } + + + public function testToArrayWithAllOptions(): void + { + $fuzziness = new \Spameri\ElasticQuery\Query\Match\Fuzziness('AUTO'); + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title^2', 'description', 'content^0.5'], + 'search term', + 2.0, + $fuzziness, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::CROSS_FIELDS, + 2, + \Spameri\ElasticQuery\Query\Match\Operator::AND, + 'standard', + ); + + $array = $multiMatch->toArray(); + + \Tester\Assert::same('search term', $array['multi_match']['query']); + \Tester\Assert::same(['title^2', 'description', 'content^0.5'], $array['multi_match']['fields']); + \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\MultiMatchType::CROSS_FIELDS, $array['multi_match']['type']); + \Tester\Assert::same(2.0, $array['multi_match']['boost']); + \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\Operator::AND, $array['multi_match']['operator']); + \Tester\Assert::same('AUTO', $array['multi_match']['fuzziness']); + \Tester\Assert::same('standard', $array['multi_match']['analyzer']); + \Tester\Assert::same(2, $array['multi_match']['minimum_should_match']); + } + + + public function testKey(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title', 'description'], + 'test query', + ); + + \Tester\Assert::same('multiMatch_title-description_test query', $multiMatch->key()); + } + + + public function testChangeAnalyzer(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title'], + 'test', + ); + + $multiMatch->changeAnalyzer('custom_analyzer'); + $array = $multiMatch->toArray(); + + \Tester\Assert::same('custom_analyzer', $array['multi_match']['analyzer']); + } + + + public function testInvalidOperatorThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\MultiMatch( + ['title'], + 'test', + 1.0, + null, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + null, + 'INVALID', + ); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testInvalidTypeThrowsException(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\MultiMatch( + ['title'], + 'test', + 1.0, + null, + 'invalid_type', + ); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testAllMultiMatchTypes(): void + { + $types = [ + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::MOST_FIELDS, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::CROSS_FIELDS, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::PHRASE, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::PHRASE_PREFIX, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BOOL_PREFIX, + ]; + + foreach ($types as $type) { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title'], + 'test', + 1.0, + null, + $type, + ); + $array = $multiMatch->toArray(); + \Tester\Assert::same($type, $array['multi_match']['type']); + } + } + + + public function testMinimumShouldMatchString(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title', 'description'], + 'search term', + 1.0, + null, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + '75%', + ); + + $array = $multiMatch->toArray(); + + \Tester\Assert::same('75%', $array['multi_match']['minimum_should_match']); + } + + + public function testMinimumShouldMatchCombinationString(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title', 'description'], + 'search term query', + 1.0, + null, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + '2<90%', + ); + + $array = $multiMatch->toArray(); + + \Tester\Assert::same('2<90%', $array['multi_match']['minimum_should_match']); + } + + + public function testMinimumShouldMatchInt(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title', 'description'], + 'search term', + 1.0, + null, + \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + 2, + ); + + $array = $multiMatch->toArray(); + + \Tester\Assert::same(2, $array['multi_match']['minimum_should_match']); + } + + + public function testToArrayWithNewOptions(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + fields: ['title'], + query: 'test', + tieBreaker: 0.3, + slop: 2, + prefixLength: 1, + maxExpansions: 50, + lenient: true, + zeroTermsQuery: 'all', + autoGenerateSynonymsPhraseQuery: false, + fuzzyTranspositions: false, + fuzzyRewrite: 'constant_score', + ); + + $array = $multiMatch->toArray(); + + \Tester\Assert::same(0.3, $array['multi_match']['tie_breaker']); + \Tester\Assert::same(2, $array['multi_match']['slop']); + \Tester\Assert::same(1, $array['multi_match']['prefix_length']); + \Tester\Assert::same(50, $array['multi_match']['max_expansions']); + \Tester\Assert::true($array['multi_match']['lenient']); + \Tester\Assert::same('all', $array['multi_match']['zero_terms_query']); + \Tester\Assert::false($array['multi_match']['auto_generate_synonyms_phrase_query']); + \Tester\Assert::false($array['multi_match']['fuzzy_transpositions']); + \Tester\Assert::same('constant_score', $array['multi_match']['fuzzy_rewrite']); + } + + + public function testCreate(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + ['title', 'description'], + 'Avengers', + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + ( + new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + $multiMatch, + ), + ), + ) + )->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt( + $ch, + \CURLOPT_POSTFIELDS, + \json_encode($document->toArray()['body']), + ); + + \Tester\Assert::noError(static function () use ($ch): void { + $response = \curl_exec($ch); + $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ + $result = $resultMapper->map(\json_decode($response, true)); + \Tester\Assert::type('int', $result->stats()->total()); + }); + } + + + public function testCreateWithAllOptions(): void + { + $multiMatch = new \Spameri\ElasticQuery\Query\MultiMatch( + fields: ['title', 'description'], + query: 'Avengers', + boost: 2.0, + fuzziness: new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO), + type: \Spameri\ElasticQuery\Query\Match\MultiMatchType::BEST_FIELDS, + minimumShouldMatch: 1, + operator: \Spameri\ElasticQuery\Query\Match\Operator::OR, + analyzer: 'standard', + tieBreaker: 0.3, + slop: 2, + prefixLength: 0, + maxExpansions: 50, + lenient: true, + zeroTermsQuery: 'none', + autoGenerateSynonymsPhraseQuery: true, + fuzzyTranspositions: true, + ); + + $document = new \Spameri\ElasticQuery\Document( + self::INDEX, + new \Spameri\ElasticQuery\Document\Body\Plain( + ( + new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection($multiMatch), + ), + ) + )->toArray(), + ), + ); + + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search'); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'POST'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + \curl_setopt($ch, \CURLOPT_POSTFIELDS, (string) \json_encode($document->toArray()['body'])); + + $response = (string) \curl_exec($ch); + $decoded = \json_decode($response, true); + + \Tester\Assert::true(isset($decoded['hits']), 'ES rejected: ' . $response); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new MultiMatch())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/MustCollection.phpt b/tests/SpameriTests/ElasticQuery/Query/MustCollection.phpt new file mode 100644 index 0000000..eb1ff6e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/MustCollection.phpt @@ -0,0 +1,177 @@ +count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testAddQuery(): void + { + $collection = new \Spameri\ElasticQuery\Query\MustCollection(); + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + + $collection->add($term); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue($term->key())); + } + + + public function testConstructorWithQueries(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $term2 = new \Spameri\ElasticQuery\Query\Term('type', 'product'); + + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term1, $term2); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testGetQuery(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term); + + $retrieved = $collection->get($term->key()); + + \Tester\Assert::same($term, $retrieved); + } + + + public function testGetNonExistentReturnsNull(): void + { + $collection = new \Spameri\ElasticQuery\Query\MustCollection(); + + $result = $collection->get('non_existent_key'); + + \Tester\Assert::null($result); + } + + + public function testRemoveQuery(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term); + + $removed = $collection->remove($term->key()); + + \Tester\Assert::true($removed); + \Tester\Assert::same(0, $collection->count()); + } + + + public function testRemoveNonExistentReturnsFalse(): void + { + $collection = new \Spameri\ElasticQuery\Query\MustCollection(); + + $removed = $collection->remove('non_existent_key'); + + \Tester\Assert::false($removed); + } + + + public function testIsValue(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term); + + \Tester\Assert::true($collection->isValue($term->key())); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testKeys(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $term2 = new \Spameri\ElasticQuery\Query\Term('type', 'product'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term1, $term2); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains($term1->key(), $keys); + \Tester\Assert::contains($term2->key(), $keys); + } + + + public function testClear(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $term2 = new \Spameri\ElasticQuery\Query\Term('type', 'product'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term1, $term2); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + } + + + public function testGetIterator(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $term2 = new \Spameri\ElasticQuery\Query\Term('type', 'product'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term1, $term2); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(2, $iterator->count()); + } + + + public function testIterateWithForeach(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $term2 = new \Spameri\ElasticQuery\Query\Term('type', 'product'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection($term1, $term2); + + $count = 0; + foreach ($collection as $query) { + \Tester\Assert::type(\Spameri\ElasticQuery\Query\LeafQueryInterface::class, $query); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testKeyUniqueness(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $term2 = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $collection = new \Spameri\ElasticQuery\Query\MustCollection(); + + $collection->add($term1); + $collection->add($term2); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testNestedCollections(): void + { + $queryCollection = new \Spameri\ElasticQuery\Query\QueryCollection('nested'); + $queryCollection->addMustQuery(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $collection = new \Spameri\ElasticQuery\Query\MustCollection($queryCollection); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::type(\Spameri\ElasticQuery\Query\QueryCollection::class, $collection->get('nested')); + } + +} + +(new MustCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/MustNotCollection.phpt b/tests/SpameriTests/ElasticQuery/Query/MustNotCollection.phpt new file mode 100644 index 0000000..9151bbf --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/MustNotCollection.phpt @@ -0,0 +1,177 @@ +count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testAddQuery(): void + { + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection(); + $term = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + + $collection->add($term); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue($term->key())); + } + + + public function testConstructorWithQueries(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $term2 = new \Spameri\ElasticQuery\Query\Term('status', 'archived'); + + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term1, $term2); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testGetQuery(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term); + + $retrieved = $collection->get($term->key()); + + \Tester\Assert::same($term, $retrieved); + } + + + public function testGetNonExistentReturnsNull(): void + { + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection(); + + $result = $collection->get('non_existent_key'); + + \Tester\Assert::null($result); + } + + + public function testRemoveQuery(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term); + + $removed = $collection->remove($term->key()); + + \Tester\Assert::true($removed); + \Tester\Assert::same(0, $collection->count()); + } + + + public function testRemoveNonExistentReturnsFalse(): void + { + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection(); + + $removed = $collection->remove('non_existent_key'); + + \Tester\Assert::false($removed); + } + + + public function testIsValue(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term); + + \Tester\Assert::true($collection->isValue($term->key())); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testKeys(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $term2 = new \Spameri\ElasticQuery\Query\Term('status', 'archived'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term1, $term2); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains($term1->key(), $keys); + \Tester\Assert::contains($term2->key(), $keys); + } + + + public function testClear(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $term2 = new \Spameri\ElasticQuery\Query\Term('status', 'archived'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term1, $term2); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + } + + + public function testGetIterator(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $term2 = new \Spameri\ElasticQuery\Query\Term('status', 'archived'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term1, $term2); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(2, $iterator->count()); + } + + + public function testIterateWithForeach(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $term2 = new \Spameri\ElasticQuery\Query\Term('status', 'archived'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($term1, $term2); + + $count = 0; + foreach ($collection as $query) { + \Tester\Assert::type(\Spameri\ElasticQuery\Query\LeafQueryInterface::class, $query); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testKeyUniqueness(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $term2 = new \Spameri\ElasticQuery\Query\Term('status', 'deleted'); + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection(); + + $collection->add($term1); + $collection->add($term2); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testExclusionQueries(): void + { + $exists = new \Spameri\ElasticQuery\Query\Exists('deleted_at'); + $term = new \Spameri\ElasticQuery\Query\Term('status', 'spam'); + $range = new \Spameri\ElasticQuery\Query\Range('score', null, 0); + + $collection = new \Spameri\ElasticQuery\Query\MustNotCollection($exists, $term, $range); + + \Tester\Assert::same(3, $collection->count()); + } + +} + +(new MustNotCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Nested.phpt b/tests/SpameriTests/ElasticQuery/Query/Nested.phpt new file mode 100644 index 0000000..823716b --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Nested.phpt @@ -0,0 +1,134 @@ + [ + 'properties' => [ + 'comments' => [ + 'type' => 'nested', + 'properties' => [ + 'author' => ['type' => 'keyword'], + 'text' => ['type' => 'text'], + ], + ], + ], + ], + ]; + } + + + public function testToArrayBasic(): void + { + $nested = new \Spameri\ElasticQuery\Query\Nested('comments'); + + $array = $nested->toArray(); + + \Tester\Assert::same('comments', $array['nested']['path']); + \Tester\Assert::true(isset($array['nested']['query']['bool'])); + } + + + public function testToArrayWithQuery(): void + { + $queryCollection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $queryCollection->addMustQuery( + new \Spameri\ElasticQuery\Query\Term('comments.author', 'John'), + ); + + $nested = new \Spameri\ElasticQuery\Query\Nested('comments', $queryCollection); + + $array = $nested->toArray(); + + \Tester\Assert::same('comments', $array['nested']['path']); + \Tester\Assert::count(1, $array['nested']['query']['bool']['must']); + } + + + public function testToArrayWithScoreModeAndIgnoreUnmapped(): void + { + $nested = new \Spameri\ElasticQuery\Query\Nested( + path: 'comments', + scoreMode: \Spameri\ElasticQuery\Query\Nested::SCORE_MODE_AVG, + ignoreUnmapped: true, + ); + + $array = $nested->toArray(); + + \Tester\Assert::same('avg', $array['nested']['score_mode']); + \Tester\Assert::true($array['nested']['ignore_unmapped']); + } + + + public function testToArrayWithInnerHits(): void + { + $nested = new \Spameri\ElasticQuery\Query\Nested( + path: 'comments', + innerHits: new \Spameri\ElasticQuery\Query\InnerHits(name: 'matched_comments', size: 5), + ); + + $array = $nested->toArray(); + + \Tester\Assert::same('matched_comments', $array['nested']['inner_hits']['name']); + \Tester\Assert::same(5, $array['nested']['inner_hits']['size']); + } + + + public function testGetQuery(): void + { + $nested = new \Spameri\ElasticQuery\Query\Nested('products'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Query\QueryCollection::class, $nested->getQuery()); + } + + + public function testKey(): void + { + $nested = new \Spameri\ElasticQuery\Query\Nested('reviews'); + + \Tester\Assert::same('nested_reviews', $nested->key()); + } + + + public function testCreate(): void + { + $this->indexDocument([ + 'comments' => [ + ['author' => 'John', 'text' => 'great'], + ['author' => 'Jane', 'text' => 'bad'], + ], + ]); + + $queryCollection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $queryCollection->addMustQuery( + new \Spameri\ElasticQuery\Query\Term('comments.author', 'John'), + ); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Nested('comments', $queryCollection), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Nested())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/ParentId.phpt b/tests/SpameriTests/ElasticQuery/Query/ParentId.phpt new file mode 100644 index 0000000..abc6804 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/ParentId.phpt @@ -0,0 +1,73 @@ + [ + 'properties' => [ + 'my_join_field' => [ + 'type' => 'join', + 'relations' => ['blog' => 'comment'], + ], + ], + ], + ]; + } + + + public function testToArray(): void + { + $parentId = new \Spameri\ElasticQuery\Query\ParentId(type: 'comment', id: '1', boost: 2.0); + + $array = $parentId->toArray(); + + \Tester\Assert::same('comment', $array['parent_id']['type']); + \Tester\Assert::same('1', $array['parent_id']['id']); + \Tester\Assert::same(2.0, $array['parent_id']['boost']); + } + + + public function testKey(): void + { + $parentId = new \Spameri\ElasticQuery\Query\ParentId('comment', '1'); + \Tester\Assert::same('parent_id_comment_1', $parentId->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['my_join_field' => 'blog'], id: '1'); + $this->request( + 'PUT', + self::INDEX . '/_doc/2?refresh=true&routing=1', + ['my_join_field' => ['name' => 'comment', 'parent' => '1']], + ); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\ParentId(type: 'comment', id: '1'), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new ParentId())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Percolate.phpt b/tests/SpameriTests/ElasticQuery/Query/Percolate.phpt new file mode 100644 index 0000000..34abe41 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Percolate.phpt @@ -0,0 +1,115 @@ + [ + 'properties' => [ + 'query' => ['type' => 'percolator'], + 'message' => ['type' => 'text'], + ], + ], + ]; + } + + + public function testToArrayInline(): void + { + $percolate = new \Spameri\ElasticQuery\Query\Percolate( + field: 'query', + document: ['message' => 'A new bonsai tree'], + ); + + \Tester\Assert::same(['message' => 'A new bonsai tree'], $percolate->toArray()['percolate']['document']); + } + + + public function testToArrayDocuments(): void + { + $percolate = new \Spameri\ElasticQuery\Query\Percolate( + field: 'query', + documents: [ + ['message' => 'hello'], + ['message' => 'world'], + ], + name: 'docs', + ); + + $array = $percolate->toArray(); + + \Tester\Assert::count(2, $array['percolate']['documents']); + \Tester\Assert::same('docs', $array['percolate']['name']); + } + + + public function testToArrayById(): void + { + $percolate = new \Spameri\ElasticQuery\Query\Percolate( + field: 'query', + index: 'my-index', + id: '1', + routing: 'r', + preference: 'p', + version: 7, + ); + + $array = $percolate->toArray(); + + \Tester\Assert::same('my-index', $array['percolate']['index']); + \Tester\Assert::same('1', $array['percolate']['id']); + \Tester\Assert::same('r', $array['percolate']['routing']); + \Tester\Assert::same('p', $array['percolate']['preference']); + \Tester\Assert::same(7, $array['percolate']['version']); + } + + + public function testRequiresDocOrId(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Percolate(field: 'query'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->request( + 'PUT', + self::INDEX . '/_doc/1?refresh=true', + ['query' => ['match' => ['message' => 'bonsai']]], + ); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Percolate( + field: 'query', + document: ['message' => 'A new bonsai tree in the garden'], + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Percolate())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/PhrasePrefix.phpt b/tests/SpameriTests/ElasticQuery/Query/PhrasePrefix.phpt new file mode 100644 index 0000000..d0ccba2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/PhrasePrefix.phpt @@ -0,0 +1,104 @@ +toArray(); + + \Tester\Assert::same('quick brown f', $array['match_phrase_prefix']['title']['query']); + \Tester\Assert::same(1.0, $array['match_phrase_prefix']['title']['boost']); + \Tester\Assert::same(1, $array['match_phrase_prefix']['title']['slop']); + } + + + public function testToArrayWithAllOptions(): void + { + $phrasePrefix = new \Spameri\ElasticQuery\Query\PhrasePrefix( + field: 'description', + queryString: 'search phrase', + boost: 2.0, + slop: 3, + analyzer: 'standard', + maxExpansions: 50, + zeroTermsQuery: 'none', + ); + + $array = $phrasePrefix->toArray(); + + \Tester\Assert::same(2.0, $array['match_phrase_prefix']['description']['boost']); + \Tester\Assert::same(3, $array['match_phrase_prefix']['description']['slop']); + \Tester\Assert::same('standard', $array['match_phrase_prefix']['description']['analyzer']); + \Tester\Assert::same(50, $array['match_phrase_prefix']['description']['max_expansions']); + \Tester\Assert::same('none', $array['match_phrase_prefix']['description']['zero_terms_query']); + } + + + public function testKey(): void + { + $phrasePrefix = new \Spameri\ElasticQuery\Query\PhrasePrefix('title', 'test query'); + \Tester\Assert::same('phrase_prefix_title_test query', $phrasePrefix->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['title' => 'Avengers Endgame']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\PhrasePrefix('title', 'Aveng'), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + + + public function testCreateWithAllOptions(): void + { + $this->indexDocument(['title' => 'Avengers Endgame']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\PhrasePrefix( + field: 'title', + queryString: 'Aveng', + boost: 1.5, + slop: 1, + analyzer: 'standard', + maxExpansions: 50, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new PhrasePrefix())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Pinned.phpt b/tests/SpameriTests/ElasticQuery/Query/Pinned.phpt new file mode 100644 index 0000000..351a814 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Pinned.phpt @@ -0,0 +1,77 @@ +toArray(); + + \Tester\Assert::same(['1', '4', '100'], $array['pinned']['ids']); + \Tester\Assert::true(isset($array['pinned']['organic']['match'])); + } + + + public function testRequiresIdsOrDocs(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Pinned( + new \Spameri\ElasticQuery\Query\MatchAll(), + ); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testKey(): void + { + $pinned = new \Spameri\ElasticQuery\Query\Pinned( + organic: new \Spameri\ElasticQuery\Query\MatchAll(), + ids: ['1'], + ); + + \Tester\Assert::same('pinned_match_all', $pinned->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Pinned())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Prefix.phpt b/tests/SpameriTests/ElasticQuery/Query/Prefix.phpt new file mode 100644 index 0000000..f10eec3 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Prefix.phpt @@ -0,0 +1,88 @@ +toArray()['prefix']['user']['value']); + } + + + public function testCaseInsensitive(): void + { + $prefix = new \Spameri\ElasticQuery\Query\Prefix( + field: 'user', + query: 'ki', + caseInsensitive: true, + rewrite: 'constant_score', + ); + + \Tester\Assert::true($prefix->toArray()['prefix']['user']['case_insensitive']); + \Tester\Assert::same('constant_score', $prefix->toArray()['prefix']['user']['rewrite']); + } + + + public function testKey(): void + { + $prefix = new \Spameri\ElasticQuery\Query\Prefix('user', 'ki'); + \Tester\Assert::same('prefix_user_ki', $prefix->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['user' => 'kimchy']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Prefix('user', 'ki'), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + + + public function testCreateWithAllOptions(): void + { + $this->indexDocument(['user' => 'kimchy']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Prefix( + field: 'user', + query: 'ki', + boost: 2.0, + caseInsensitive: false, + rewrite: 'constant_score', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Prefix())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/QueryCollection.phpt b/tests/SpameriTests/ElasticQuery/Query/QueryCollection.phpt new file mode 100644 index 0000000..3a2d787 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/QueryCollection.phpt @@ -0,0 +1,203 @@ +toArray(); + + \Tester\Assert::same([], $array); + } + + + public function testWithMustQueries(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $collection->addMustQuery(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + $collection->addMustQuery(new \Spameri\ElasticQuery\Query\Term('type', 'product')); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::count(2, $array['bool']['must']); + } + + + public function testWithShouldQueries(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $collection->addShouldQuery(new \Spameri\ElasticQuery\Query\Term('category', 'electronics')); + $collection->addShouldQuery(new \Spameri\ElasticQuery\Query\Term('category', 'books')); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['bool']['should'])); + \Tester\Assert::count(2, $array['bool']['should']); + } + + + public function testWithMustNotQueries(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $collection->addMustNotQuery(new \Spameri\ElasticQuery\Query\Term('status', 'deleted')); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['bool']['must_not'])); + \Tester\Assert::count(1, $array['bool']['must_not']); + } + + + public function testComposeMustShouldMustNot(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $collection->addMustQuery(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + $collection->addShouldQuery(new \Spameri\ElasticQuery\Query\Term('featured', true)); + $collection->addMustNotQuery(new \Spameri\ElasticQuery\Query\Term('status', 'deleted')); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::true(isset($array['bool']['should'])); + \Tester\Assert::true(isset($array['bool']['must_not'])); + } + + + public function testConstructorWithCollections(): void + { + $mustCollection = new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Term('status', 'active'), + ); + $shouldCollection = new \Spameri\ElasticQuery\Query\ShouldCollection( + new \Spameri\ElasticQuery\Query\Term('featured', true), + ); + $mustNotCollection = new \Spameri\ElasticQuery\Query\MustNotCollection( + new \Spameri\ElasticQuery\Query\Term('status', 'deleted'), + ); + + $collection = new \Spameri\ElasticQuery\Query\QueryCollection( + null, + $mustCollection, + $shouldCollection, + $mustNotCollection, + ); + + $array = $collection->toArray(); + + \Tester\Assert::count(1, $array['bool']['must']); + \Tester\Assert::count(1, $array['bool']['should']); + \Tester\Assert::count(1, $array['bool']['must_not']); + } + + + public function testMust(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + + $must = $collection->must(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Query\MustCollection::class, $must); + } + + + public function testShould(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + + $should = $collection->should(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Query\ShouldCollection::class, $should); + } + + + public function testMustNot(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + + $mustNot = $collection->mustNot(); + + \Tester\Assert::type(\Spameri\ElasticQuery\Query\MustNotCollection::class, $mustNot); + } + + + public function testKeyWithCustomKey(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection('custom_key'); + + \Tester\Assert::same('custom_key', $collection->key()); + } + + + public function testKeyWithIntKey(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(123); + + \Tester\Assert::same('123', $collection->key()); + } + + + public function testKeyGeneratedFromContent(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $collection->addMustQuery(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $key = $collection->key(); + + \Tester\Assert::type('string', $key); + \Tester\Assert::true(\strlen($key) === 32); + } + + + public function testToArrayStructure(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $collection->addMustQuery(new \Spameri\ElasticQuery\Query\Term('status', 'active')); + + $array = $collection->toArray(); + + \Tester\Assert::true(isset($array['bool'])); + \Tester\Assert::true(isset($array['bool']['must'])); + \Tester\Assert::true(isset($array['bool']['must'][0]['term'])); + } + + + public function testDirectlyAddToSubCollections(): void + { + $collection = new \Spameri\ElasticQuery\Query\QueryCollection(); + + $collection->must()->add(new \Spameri\ElasticQuery\Query\Term('field1', 'value1')); + $collection->should()->add(new \Spameri\ElasticQuery\Query\Term('field2', 'value2')); + $collection->mustNot()->add(new \Spameri\ElasticQuery\Query\Term('field3', 'value3')); + + $array = $collection->toArray(); + + \Tester\Assert::count(1, $array['bool']['must']); + \Tester\Assert::count(1, $array['bool']['should']); + \Tester\Assert::count(1, $array['bool']['must_not']); + } + + + public function testNestedQueryCollection(): void + { + $innerCollection = new \Spameri\ElasticQuery\Query\QueryCollection('inner'); + $innerCollection->addMustQuery(new \Spameri\ElasticQuery\Query\Term('inner_field', 'inner_value')); + + $outerCollection = new \Spameri\ElasticQuery\Query\QueryCollection(); + $outerCollection->addMustQuery($innerCollection); + + $array = $outerCollection->toArray(); + + \Tester\Assert::true(isset($array['bool']['must'][0]['bool'])); + } + +} + +(new QueryCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/QueryString.phpt b/tests/SpameriTests/ElasticQuery/Query/QueryString.phpt new file mode 100644 index 0000000..76ba8d2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/QueryString.phpt @@ -0,0 +1,131 @@ +toArray(); + + \Tester\Assert::same('(new york city) OR (big apple)', $array['query_string']['query']); + \Tester\Assert::same('content', $array['query_string']['default_field']); + } + + + public function testToArrayWithAllOptions(): void + { + $qs = new \Spameri\ElasticQuery\Query\QueryString( + query: 'foo', + fields: ['title^2', 'body'], + defaultOperator: 'AND', + analyzer: 'standard', + allowLeadingWildcard: true, + analyzeWildcard: true, + autoGenerateSynonymsPhraseQuery: false, + enablePositionIncrements: true, + fuzziness: new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO), + fuzzyMaxExpansions: 50, + fuzzyPrefixLength: 0, + fuzzyTranspositions: true, + lenient: true, + maxDeterminizedStates: 10000, + minimumShouldMatch: '50%', + quoteAnalyzer: 'standard', + phraseSlop: 0, + quoteFieldSuffix: '.exact', + rewrite: 'constant_score', + timeZone: 'UTC', + type: 'best_fields', + tieBreaker: 0.3, + ); + + $array = $qs->toArray(); + + \Tester\Assert::true($array['query_string']['analyze_wildcard']); + \Tester\Assert::false($array['query_string']['auto_generate_synonyms_phrase_query']); + \Tester\Assert::same(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO, $array['query_string']['fuzziness']); + \Tester\Assert::same(50, $array['query_string']['fuzzy_max_expansions']); + \Tester\Assert::same('best_fields', $array['query_string']['type']); + \Tester\Assert::same(0.3, $array['query_string']['tie_breaker']); + } + + + public function testKey(): void + { + $qs = new \Spameri\ElasticQuery\Query\QueryString('foo'); + \Tester\Assert::same('query_string_foo', $qs->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['content' => 'hello world']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\QueryString(query: 'hello', defaultField: 'content'), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + + + public function testCreateWithAllOptions(): void + { + $this->indexDocument(['content' => 'hello world']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\QueryString( + query: 'hello*', + defaultField: 'content', + defaultOperator: 'AND', + analyzer: 'standard', + allowLeadingWildcard: false, + boost: 1.0, + analyzeWildcard: true, + autoGenerateSynonymsPhraseQuery: true, + enablePositionIncrements: true, + fuzziness: new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO), + fuzzyMaxExpansions: 50, + fuzzyPrefixLength: 0, + fuzzyTranspositions: true, + lenient: true, + maxDeterminizedStates: 10000, + phraseSlop: 0, + type: 'best_fields', + tieBreaker: 0.3, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new QueryString())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Range.phpt b/tests/SpameriTests/ElasticQuery/Query/Range.phpt index 5bc6c24..86537ff 100644 --- a/tests/SpameriTests/ElasticQuery/Query/Range.phpt +++ b/tests/SpameriTests/ElasticQuery/Query/Range.phpt @@ -5,90 +5,124 @@ namespace SpameriTests\ElasticQuery\Query; require_once __DIR__ . '/../../bootstrap.php'; -class Range extends \Tester\TestCase +class Range extends \SpameriTests\ElasticQuery\AbstractElasticTestCase { - private const INDEX = 'spameri_test_video_range'; + protected const INDEX = 'spameri_test_query_range'; - public function setUp() : void + protected function mapping(): array|null { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + return [ + 'mappings' => [ + 'properties' => [ + 'id' => ['type' => 'long'], + 'created' => ['type' => 'date'], + ], + ], + ]; } - public function testCreate() : void + public function testToArray(): void { - $range = new \Spameri\ElasticQuery\Query\Range( - 'id', - 1, - 1000000, - 1.0 - ); + $range = new \Spameri\ElasticQuery\Query\Range('id', 1, 1000000, 1.0); $array = $range->toArray(); - \Tester\Assert::true(isset($array['range']['id'])); \Tester\Assert::same(1, $array['range']['id']['gte']); \Tester\Assert::same(1000000, $array['range']['id']['lte']); \Tester\Assert::same(1.0, $array['range']['id']['boost']); + } - $document = new \Spameri\ElasticQuery\Document( - self::INDEX, - new \Spameri\ElasticQuery\Document\Body\Plain( - ( - new \Spameri\ElasticQuery\ElasticQuery( - new \Spameri\ElasticQuery\Query\QueryCollection( - NULL, - new \Spameri\ElasticQuery\Query\MustCollection( - $range - ) - ) - ) - )->toArray() - ), - self::INDEX + + public function testToArrayWithGtLtRelationFormat(): void + { + $range = new \Spameri\ElasticQuery\Query\Range( + field: 'id', + boost: 1.0, + gt: 1, + lt: 10, + relation: \Spameri\ElasticQuery\Query\Range\Relation::WITHIN, + format: 'epoch_second', + timeZone: 'UTC', + ); + + $array = $range->toArray(); + + \Tester\Assert::same(1, $array['range']['id']['gt']); + \Tester\Assert::same(10, $array['range']['id']['lt']); + \Tester\Assert::same('WITHIN', $array['range']['id']['relation']); + \Tester\Assert::same('epoch_second', $array['range']['id']['format']); + \Tester\Assert::same('UTC', $array['range']['id']['time_zone']); + } + + + public function testRejectsEmptyRange(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Range('id'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, ); + } + + + public function testRejectsInvalidRelation(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\Range('id', 1, 10, relation: 'OVERLAPS'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - \json_encode($document->toArray()['body']) + + public function testCreate(): void + { + $this->indexDocument(['id' => 5]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Range('id', 1, 10), + ), + ), ); - \Tester\Assert::noError(static function () use ($ch) { - $response = curl_exec($ch); - $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); - /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ - $result = $resultMapper->map(\json_decode($response, TRUE)); - \Tester\Assert::type('int', $result->stats()->total()); - }); + $result = $this->search($query); - curl_close($ch); + \Tester\Assert::same(1, $result->stats()->total()); } - public function tearDown() : void + public function testCreateWithAllOptions(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + $this->indexDocument(['id' => 5, 'created' => '2024-01-01']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Range( + field: 'created', + gte: '2023-01-01', + lte: '2025-01-01', + boost: 1.0, + format: 'yyyy-MM-dd', + relation: \Spameri\ElasticQuery\Query\Range\Relation::INTERSECTS, + timeZone: 'UTC', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); } } diff --git a/tests/SpameriTests/ElasticQuery/Query/RankFeature.phpt b/tests/SpameriTests/ElasticQuery/Query/RankFeature.phpt new file mode 100644 index 0000000..0234fc0 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/RankFeature.phpt @@ -0,0 +1,61 @@ + ['pivot' => 8]], + ); + + $array = $rf->toArray(); + + \Tester\Assert::same('pagerank', $array['rank_feature']['field']); + \Tester\Assert::same(8, $array['rank_feature']['saturation']['pivot']); + } + + + public function testKey(): void + { + $rf = new \Spameri\ElasticQuery\Query\RankFeature('pagerank'); + + \Tester\Assert::same('rank_feature_pagerank', $rf->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new RankFeature())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Regexp.phpt b/tests/SpameriTests/ElasticQuery/Query/Regexp.phpt new file mode 100644 index 0000000..4565311 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Regexp.phpt @@ -0,0 +1,94 @@ + ['properties' => ['user' => ['type' => 'keyword']]]]; + } + + + public function testToArray(): void + { + $regexp = new \Spameri\ElasticQuery\Query\Regexp('user', 'k.*y'); + + \Tester\Assert::same('k.*y', $regexp->toArray()['regexp']['user']['value']); + } + + + public function testRewriteOption(): void + { + $regexp = new \Spameri\ElasticQuery\Query\Regexp( + field: 'user', + query: 'k.*', + rewrite: 'constant_score', + ); + + \Tester\Assert::same('constant_score', $regexp->toArray()['regexp']['user']['rewrite']); + } + + + public function testKey(): void + { + $regexp = new \Spameri\ElasticQuery\Query\Regexp('user', 'k.*'); + \Tester\Assert::same('regexp_user_k.*', $regexp->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['user' => 'kimchy']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Regexp('user', 'k.*'), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + + + public function testCreateWithAllOptions(): void + { + $this->indexDocument(['user' => 'kimchy']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Regexp( + field: 'user', + query: 'k.*', + boost: 2.0, + flags: 'ALL', + caseInsensitive: false, + maxDeterminizedStates: 10000, + rewrite: 'constant_score', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Regexp())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/RuleQuery.phpt b/tests/SpameriTests/ElasticQuery/Query/RuleQuery.phpt new file mode 100644 index 0000000..cfa7bc1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/RuleQuery.phpt @@ -0,0 +1,46 @@ + 'puggles'], + ); + + $array = $rule->toArray(); + + \Tester\Assert::same(['my-ruleset'], $array['rule']['ruleset_ids']); + \Tester\Assert::same(['query_string' => 'puggles'], $array['rule']['match_criteria']); + \Tester\Assert::same('value', $array['rule']['organic']['term']['field']['value']); + } + + + public function testRequiresRulesetIds(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\RuleQuery( + organic: new \Spameri\ElasticQuery\Query\Term('a', 'b'), + rulesetIds: [], + matchCriteria: [], + ); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + +} + +(new RuleQuery())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Script.phpt b/tests/SpameriTests/ElasticQuery/Query/Script.phpt new file mode 100644 index 0000000..f5ad52a --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Script.phpt @@ -0,0 +1,64 @@ + params.threshold", + params: ['threshold' => 100], + ); + + $array = $script->toArray(); + + \Tester\Assert::same( + "doc['amount'].value > params.threshold", + $array['script']['script']['source'], + ); + \Tester\Assert::same(['threshold' => 100], $array['script']['script']['params']); + } + + + public function testKey(): void + { + $script = new \Spameri\ElasticQuery\Query\Script('1 == 1'); + + \Tester\Assert::contains('script_', $script->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Script())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/ScriptScore.phpt b/tests/SpameriTests/ElasticQuery/Query/ScriptScore.phpt new file mode 100644 index 0000000..1834857 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/ScriptScore.phpt @@ -0,0 +1,65 @@ +toArray(); + + \Tester\Assert::same("doc['my_field'].value * 2", $array['script_score']['script']['source']); + \Tester\Assert::same(0.5, $array['script_score']['min_score']); + } + + + public function testKey(): void + { + $ss = new \Spameri\ElasticQuery\Query\ScriptScore( + query: new \Spameri\ElasticQuery\Query\MatchAll(), + source: '_score', + ); + + \Tester\Assert::same('script_score_match_all', $ss->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new ScriptScore())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Semantic.phpt b/tests/SpameriTests/ElasticQuery/Query/Semantic.phpt new file mode 100644 index 0000000..05ea74e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Semantic.phpt @@ -0,0 +1,39 @@ +toArray(); + + \Tester\Assert::same('inference_field', $array['semantic']['field']); + \Tester\Assert::same('large cat', $array['semantic']['query']); + \Tester\Assert::same(1.5, $array['semantic']['boost']); + } + + + public function testKey(): void + { + $semantic = new \Spameri\ElasticQuery\Query\Semantic('f', 'q'); + + \Tester\Assert::same('semantic_f', $semantic->key()); + } + +} + +(new Semantic())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Shape.phpt b/tests/SpameriTests/ElasticQuery/Query/Shape.phpt new file mode 100644 index 0000000..7925b61 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Shape.phpt @@ -0,0 +1,76 @@ + ['properties' => ['geometry' => ['type' => 'shape']]]]; + } + + + public function testToArray(): void + { + $shape = new \Spameri\ElasticQuery\Query\Shape( + field: 'geometry', + shape: ['type' => 'envelope', 'coordinates' => [[0, 100], [100, 0]]], + relation: 'intersects', + ); + + $array = $shape->toArray(); + + \Tester\Assert::same('envelope', $array['shape']['geometry']['shape']['type']); + \Tester\Assert::same('intersects', $array['shape']['geometry']['relation']); + } + + + public function testToArrayWithIndexedShape(): void + { + $shape = new \Spameri\ElasticQuery\Query\Shape( + field: 'geometry', + indexedShape: new \Spameri\ElasticQuery\Query\IndexedShape( + id: 'box', + index: 'shapes', + path: 'geometry', + ), + boost: 1.5, + ); + + \Tester\Assert::same('box', $shape->toArray()['shape']['geometry']['indexed_shape']['id']); + \Tester\Assert::same(1.5, $shape->toArray()['shape']['boost']); + } + + + public function testCreate(): void + { + $this->indexDocument(['geometry' => ['type' => 'point', 'coordinates' => [10, 10]]]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Shape( + field: 'geometry', + shape: ['type' => 'envelope', 'coordinates' => [[0, 100], [100, 0]]], + relation: 'intersects', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new Shape())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/ShouldCollection.phpt b/tests/SpameriTests/ElasticQuery/Query/ShouldCollection.phpt new file mode 100644 index 0000000..a16c6b9 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/ShouldCollection.phpt @@ -0,0 +1,178 @@ +count()); + \Tester\Assert::same([], $collection->keys()); + } + + + public function testAddQuery(): void + { + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection(); + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + + $collection->add($term); + + \Tester\Assert::same(1, $collection->count()); + \Tester\Assert::true($collection->isValue($term->key())); + } + + + public function testConstructorWithQueries(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('category', 'electronics'); + $term2 = new \Spameri\ElasticQuery\Query\Term('category', 'books'); + $term3 = new \Spameri\ElasticQuery\Query\Term('category', 'clothing'); + + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term1, $term2, $term3); + + \Tester\Assert::same(3, $collection->count()); + } + + + public function testGetQuery(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term); + + $retrieved = $collection->get($term->key()); + + \Tester\Assert::same($term, $retrieved); + } + + + public function testGetNonExistentReturnsNull(): void + { + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection(); + + $result = $collection->get('non_existent_key'); + + \Tester\Assert::null($result); + } + + + public function testRemoveQuery(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term); + + $removed = $collection->remove($term->key()); + + \Tester\Assert::true($removed); + \Tester\Assert::same(0, $collection->count()); + } + + + public function testRemoveNonExistentReturnsFalse(): void + { + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection(); + + $removed = $collection->remove('non_existent_key'); + + \Tester\Assert::false($removed); + } + + + public function testIsValue(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term); + + \Tester\Assert::true($collection->isValue($term->key())); + \Tester\Assert::false($collection->isValue('non_existent')); + } + + + public function testKeys(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('category', 'electronics'); + $term2 = new \Spameri\ElasticQuery\Query\Term('category', 'books'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term1, $term2); + + $keys = $collection->keys(); + + \Tester\Assert::count(2, $keys); + \Tester\Assert::contains($term1->key(), $keys); + \Tester\Assert::contains($term2->key(), $keys); + } + + + public function testClear(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('category', 'electronics'); + $term2 = new \Spameri\ElasticQuery\Query\Term('category', 'books'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term1, $term2); + + $collection->clear(); + + \Tester\Assert::same(0, $collection->count()); + } + + + public function testGetIterator(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('category', 'electronics'); + $term2 = new \Spameri\ElasticQuery\Query\Term('category', 'books'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term1, $term2); + + $iterator = $collection->getIterator(); + + \Tester\Assert::type(\ArrayIterator::class, $iterator); + \Tester\Assert::same(2, $iterator->count()); + } + + + public function testIterateWithForeach(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('category', 'electronics'); + $term2 = new \Spameri\ElasticQuery\Query\Term('category', 'books'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term1, $term2); + + $count = 0; + foreach ($collection as $query) { + \Tester\Assert::type(\Spameri\ElasticQuery\Query\LeafQueryInterface::class, $query); + $count++; + } + + \Tester\Assert::same(2, $count); + } + + + public function testKeyUniqueness(): void + { + $term1 = new \Spameri\ElasticQuery\Query\Term('category', 'electronics'); + $term2 = new \Spameri\ElasticQuery\Query\Term('category', 'electronics'); + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection(); + + $collection->add($term1); + $collection->add($term2); + + \Tester\Assert::same(1, $collection->count()); + } + + + public function testMultipleDifferentQueryTypes(): void + { + $term = new \Spameri\ElasticQuery\Query\Term('status', 'active'); + $match = new \Spameri\ElasticQuery\Query\ElasticMatch('title', 'product'); + $range = new \Spameri\ElasticQuery\Query\Range('price', 10, 100); + + $collection = new \Spameri\ElasticQuery\Query\ShouldCollection($term, $match, $range); + + \Tester\Assert::same(3, $collection->count()); + } + +} + +(new ShouldCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SimpleQueryString.phpt b/tests/SpameriTests/ElasticQuery/Query/SimpleQueryString.phpt new file mode 100644 index 0000000..3fa2fd6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SimpleQueryString.phpt @@ -0,0 +1,113 @@ +toArray(); + + \Tester\Assert::same('foo + bar -baz', $array['simple_query_string']['query']); + \Tester\Assert::same(['title^2', 'body'], $array['simple_query_string']['fields']); + } + + + public function testToArrayWithAllOptions(): void + { + $sqs = new \Spameri\ElasticQuery\Query\SimpleQueryString( + query: 'foo', + fields: ['body'], + defaultOperator: 'AND', + analyzer: 'standard', + flags: 'ALL', + analyzeWildcard: true, + autoGenerateSynonymsPhraseQuery: false, + fuzzyMaxExpansions: 50, + fuzzyPrefixLength: 0, + fuzzyTranspositions: true, + lenient: true, + minimumShouldMatch: '50%', + quoteFieldSuffix: '.exact', + ); + + $array = $sqs->toArray(); + + \Tester\Assert::true($array['simple_query_string']['analyze_wildcard']); + \Tester\Assert::false($array['simple_query_string']['auto_generate_synonyms_phrase_query']); + \Tester\Assert::same(50, $array['simple_query_string']['fuzzy_max_expansions']); + \Tester\Assert::same('.exact', $array['simple_query_string']['quote_field_suffix']); + } + + + public function testKey(): void + { + $sqs = new \Spameri\ElasticQuery\Query\SimpleQueryString('q'); + \Tester\Assert::same('simple_query_string_q', $sqs->key()); + } + + + public function testCreate(): void + { + $this->indexDocument(['body' => 'hello world']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\SimpleQueryString(query: 'hello', fields: ['body']), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + + + public function testCreateWithAllOptions(): void + { + $this->indexDocument(['body' => 'hello world']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\SimpleQueryString( + query: 'hello', + fields: ['body'], + defaultOperator: 'AND', + analyzer: 'standard', + flags: 'ALL', + analyzeWildcard: true, + autoGenerateSynonymsPhraseQuery: true, + fuzzyMaxExpansions: 50, + fuzzyPrefixLength: 0, + fuzzyTranspositions: true, + lenient: true, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new SimpleQueryString())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanContaining.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanContaining.phpt new file mode 100644 index 0000000..9c719b2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanContaining.phpt @@ -0,0 +1,64 @@ +toArray(); + + \Tester\Assert::true(isset($array['span_containing']['big'])); + \Tester\Assert::true(isset($array['span_containing']['little'])); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanContaining( + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'a'), + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'b'), + ); + + \Tester\Assert::same('span_containing_span_term_field_a_span_term_field_b', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanContaining())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanFirst.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanFirst.phpt new file mode 100644 index 0000000..0beb8f2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanFirst.phpt @@ -0,0 +1,64 @@ +toArray(); + + \Tester\Assert::same(3, $array['span_first']['end']); + \Tester\Assert::true(isset($array['span_first']['match']['span_term'])); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanFirst( + new \Spameri\ElasticQuery\Query\SpanTerm('user', 'kimchy'), + 3, + ); + + \Tester\Assert::same('span_first_span_term_user_kimchy_3', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanFirst())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanMulti.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanMulti.phpt new file mode 100644 index 0000000..49d0f88 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanMulti.phpt @@ -0,0 +1,61 @@ +toArray(); + + \Tester\Assert::true(isset($array['span_multi']['match']['prefix'])); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanMulti( + match: new \Spameri\ElasticQuery\Query\Prefix('user', 'ki'), + ); + + \Tester\Assert::same('span_multi_prefix_user_ki', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanMulti())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanNear.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanNear.phpt new file mode 100644 index 0000000..335fb8e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanNear.phpt @@ -0,0 +1,66 @@ +addClause(new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value2')); + + $array = $span->toArray(); + + \Tester\Assert::same(12, $array['span_near']['slop']); + \Tester\Assert::false($array['span_near']['in_order']); + \Tester\Assert::count(2, $array['span_near']['clauses']); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanNear( + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value1'), + ); + + \Tester\Assert::same('span_near_span_term_field_value1', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanNear())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanNot.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanNot.phpt new file mode 100644 index 0000000..3b1ec71 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanNot.phpt @@ -0,0 +1,68 @@ +toArray(); + + \Tester\Assert::same(0, $array['span_not']['pre']); + \Tester\Assert::same(1, $array['span_not']['post']); + \Tester\Assert::true(isset($array['span_not']['include'])); + \Tester\Assert::true(isset($array['span_not']['exclude'])); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanNot( + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'hot'), + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'dog'), + ); + + \Tester\Assert::same('span_not_span_term_field_hot_span_term_field_dog', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanNot())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanOr.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanOr.phpt new file mode 100644 index 0000000..168860f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanOr.phpt @@ -0,0 +1,62 @@ +addClause(new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value2')); + + $array = $span->toArray(); + + \Tester\Assert::count(2, $array['span_or']['clauses']); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanOr( + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value1'), + ); + + \Tester\Assert::same('span_or_span_term_field_value1', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanOr())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanTerm.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanTerm.phpt new file mode 100644 index 0000000..17ad8d1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanTerm.phpt @@ -0,0 +1,57 @@ +toArray(); + + \Tester\Assert::same('quick', $array['span_term']['text']['value']); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanTerm('text', 'quick'); + + \Tester\Assert::same('span_term_text_quick', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanTerm())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SpanWithin.phpt b/tests/SpameriTests/ElasticQuery/Query/SpanWithin.phpt new file mode 100644 index 0000000..e2d06c8 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SpanWithin.phpt @@ -0,0 +1,64 @@ +toArray(); + + \Tester\Assert::true(isset($array['span_within']['big'])); + \Tester\Assert::true(isset($array['span_within']['little'])); + } + + + public function testKey(): void + { + $span = new \Spameri\ElasticQuery\Query\SpanWithin( + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'a'), + new \Spameri\ElasticQuery\Query\SpanTerm('field', 'b'), + ); + + \Tester\Assert::same('span_within_span_term_field_a_span_term_field_b', $span->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new SpanWithin())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/SparseVector.phpt b/tests/SpameriTests/ElasticQuery/Query/SparseVector.phpt new file mode 100644 index 0000000..816f052 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/SparseVector.phpt @@ -0,0 +1,92 @@ + [ + 'properties' => [ + 'tokens' => ['type' => 'sparse_vector'], + ], + ], + ]; + } + + + public function testToArrayWithVector(): void + { + $sv = new \Spameri\ElasticQuery\Query\SparseVector( + field: 'tokens', + queryVector: ['lion' => 0.5, 'tiger' => 0.7], + ); + + $array = $sv->toArray(); + + \Tester\Assert::same('tokens', $array['sparse_vector']['field']); + \Tester\Assert::same(0.5, $array['sparse_vector']['query_vector']['lion']); + } + + + public function testToArrayWithInference(): void + { + $sv = new \Spameri\ElasticQuery\Query\SparseVector( + field: 'tokens', + inferenceId: '.elser_model_2', + query: 'big cat', + prune: true, + pruningConfig: ['tokens_freq_ratio_threshold' => 5], + ); + + $array = $sv->toArray(); + + \Tester\Assert::same('.elser_model_2', $array['sparse_vector']['inference_id']); + \Tester\Assert::same('big cat', $array['sparse_vector']['query']); + \Tester\Assert::true($array['sparse_vector']['prune']); + } + + + public function testRequiresVectorOrInference(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\SparseVector('f'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument(['tokens' => ['lion' => 0.5, 'cat' => 0.3]]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\SparseVector( + field: 'tokens', + queryVector: ['lion' => 0.5, 'tiger' => 0.7], + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new SparseVector())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Term.phpt b/tests/SpameriTests/ElasticQuery/Query/Term.phpt index c8651d9..1307e11 100644 --- a/tests/SpameriTests/ElasticQuery/Query/Term.phpt +++ b/tests/SpameriTests/ElasticQuery/Query/Term.phpt @@ -5,88 +5,43 @@ namespace SpameriTests\ElasticQuery\Query; require_once __DIR__ . '/../../bootstrap.php'; -class Term extends \Tester\TestCase +class Term extends \SpameriTests\ElasticQuery\AbstractElasticTestCase { - private const INDEX = 'spameri_test_video_term'; + protected const INDEX = 'spameri_test_query_term'; - public function setUp() : void - { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); - } - - - public function testCreate() : void + public function testToArray(): void { $term = new \Spameri\ElasticQuery\Query\Term( 'name', 'Avengers', - 1.0 + 1.0, ); $array = $term->toArray(); - \Tester\Assert::true(isset($array['term']['name']['value'])); \Tester\Assert::same('Avengers', $array['term']['name']['value']); \Tester\Assert::same(1.0, $array['term']['name']['boost']); + } - $document = new \Spameri\ElasticQuery\Document( - self::INDEX, - new \Spameri\ElasticQuery\Document\Body\Plain( - ( - new \Spameri\ElasticQuery\ElasticQuery( - new \Spameri\ElasticQuery\Query\QueryCollection( - NULL, - new \Spameri\ElasticQuery\Query\MustCollection( - $term - ) - ) - ) - )->toArray() - ), - self::INDEX - ); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - \json_encode($document->toArray()['body']) + public function testCreate(): void + { + $this->indexDocument(['name' => 'Avengers']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Term('name.keyword', 'Avengers'), + ), + ), ); - \Tester\Assert::noError(static function () use ($ch) { - $response = curl_exec($ch); - $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); - /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ - $result = $resultMapper->map(\json_decode($response, TRUE)); - \Tester\Assert::type('int', $result->stats()->total()); - }); - - curl_close($ch); - } - + $result = $this->search($query); - public function tearDown() : void - { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + \Tester\Assert::same(1, $result->stats()->total()); } } diff --git a/tests/SpameriTests/ElasticQuery/Query/TermSet.phpt b/tests/SpameriTests/ElasticQuery/Query/TermSet.phpt new file mode 100644 index 0000000..814a05e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/TermSet.phpt @@ -0,0 +1,99 @@ + [ + 'properties' => [ + 'programming_languages' => ['type' => 'keyword'], + 'required_matches' => ['type' => 'long'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $termSet = new \Spameri\ElasticQuery\Query\TermSet( + field: 'programming_languages', + terms: ['c++', 'java', 'php'], + minimumShouldMatchField: 'required_matches', + boost: 2.0, + ); + + $array = $termSet->toArray(); + + \Tester\Assert::same( + ['c++', 'java', 'php'], + $array['terms_set']['programming_languages']['terms'], + ); + \Tester\Assert::same(2.0, $array['terms_set']['programming_languages']['boost']); + \Tester\Assert::same( + 'required_matches', + $array['terms_set']['programming_languages']['minimum_should_match_field'], + ); + } + + + public function testRequiresTerms(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\TermSet('f', [], 'm'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testRequiresMinimumShouldMatch(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\TermSet('f', ['a']); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testCreate(): void + { + $this->indexDocument([ + 'programming_languages' => ['c++', 'java', 'php'], + 'required_matches' => 2, + ]); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\TermSet( + field: 'programming_languages', + terms: ['c++', 'java', 'php'], + minimumShouldMatchField: 'required_matches', + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); + } + +} + +(new TermSet())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/Terms.phpt b/tests/SpameriTests/ElasticQuery/Query/Terms.phpt index c2f3d00..a2d964b 100644 --- a/tests/SpameriTests/ElasticQuery/Query/Terms.phpt +++ b/tests/SpameriTests/ElasticQuery/Query/Terms.phpt @@ -5,87 +5,99 @@ namespace SpameriTests\ElasticQuery\Query; require_once __DIR__ . '/../../bootstrap.php'; -class Terms extends \Tester\TestCase +class Terms extends \SpameriTests\ElasticQuery\AbstractElasticTestCase { - private const INDEX = 'spameri_test_video_terms'; + protected const INDEX = 'spameri_test_query_terms'; + private const LOOKUP_INDEX = 'spameri_test_query_terms_lookup'; - public function setUp() : void + protected function mapping(): array|null { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + return [ + 'mappings' => [ + 'properties' => [ + 'name' => ['type' => 'keyword'], + ], + ], + ]; } - public function testCreate() : void + + public function testToArray(): void { - $terms = new \Spameri\ElasticQuery\Query\Terms( - 'name', - ['Avengers'], - 1.0 - ); + $terms = new \Spameri\ElasticQuery\Query\Terms('name', ['Avengers'], 1.0); $array = $terms->toArray(); - \Tester\Assert::true(isset($array['terms']['name'][0])); \Tester\Assert::same('Avengers', $array['terms']['name'][0]); \Tester\Assert::same(1.0, $array['terms']['boost']); + } - $document = new \Spameri\ElasticQuery\Document( - self::INDEX, - new \Spameri\ElasticQuery\Document\Body\Plain( - ( - new \Spameri\ElasticQuery\ElasticQuery( - new \Spameri\ElasticQuery\Query\QueryCollection( - NULL, - new \Spameri\ElasticQuery\Query\MustCollection( - $terms - ) - ) - ) - )->toArray() - ), - self::INDEX + + public function testToArrayWithLookup(): void + { + $lookup = new \Spameri\ElasticQuery\Query\TermsLookup( + index: 'users', + id: '42', + path: 'friends', ); + $terms = new \Spameri\ElasticQuery\Query\Terms('user_id', $lookup); + + $array = $terms->toArray(); + + \Tester\Assert::same('users', $array['terms']['user_id']['index']); + \Tester\Assert::same('42', $array['terms']['user_id']['id']); + \Tester\Assert::same('friends', $array['terms']['user_id']['path']); + } + - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - \json_encode($document->toArray()['body']) + public function testCreate(): void + { + $this->indexDocument(['name' => 'Avengers']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Terms('name', ['Avengers']), + ), + ), ); - \Tester\Assert::noError(static function () use ($ch) { - $response = curl_exec($ch); - $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); - /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ - $result = $resultMapper->map(\json_decode($response, TRUE)); - \Tester\Assert::type('int', $result->stats()->total()); - }); + $result = $this->search($query); - curl_close($ch); + \Tester\Assert::same(1, $result->stats()->total()); } - public function tearDown() : void + public function testCreateWithLookup(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + $this->request('PUT', self::LOOKUP_INDEX, ['mappings' => ['properties' => ['ids' => ['type' => 'keyword']]]]); + $this->request('PUT', self::LOOKUP_INDEX . '/_doc/list?refresh=true', ['ids' => ['Avengers', 'Endgame']]); + + $this->indexDocument(['name' => 'Avengers']); + $this->indexDocument(['name' => 'Other']); + + $lookup = new \Spameri\ElasticQuery\Query\TermsLookup( + index: self::LOOKUP_INDEX, + id: 'list', + path: 'ids', + ); + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\Terms('name', $lookup), + ), + ), + ); + + $result = $this->search($query); + + $this->request('DELETE', self::LOOKUP_INDEX); + + \Tester\Assert::same(1, $result->stats()->total()); } } diff --git a/tests/SpameriTests/ElasticQuery/Query/TextExpansion.phpt b/tests/SpameriTests/ElasticQuery/Query/TextExpansion.phpt new file mode 100644 index 0000000..e121447 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/TextExpansion.phpt @@ -0,0 +1,67 @@ + [ + 'properties' => [ + 'tokens' => ['type' => 'sparse_vector'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $te = new \Spameri\ElasticQuery\Query\TextExpansion( + field: 'tokens', + modelId: '.elser_model_2', + modelText: 'big cat', + ); + + $array = $te->toArray(); + + \Tester\Assert::same('.elser_model_2', $array['text_expansion']['tokens']['model_id']); + \Tester\Assert::same('big cat', $array['text_expansion']['tokens']['model_text']); + } + + + public function testWithPruningConfig(): void + { + $te = new \Spameri\ElasticQuery\Query\TextExpansion( + field: 'tokens', + modelId: '.elser_model_2', + modelText: 'q', + pruningConfig: ['tokens_freq_ratio_threshold' => 5, 'only_score_pruned_tokens' => false], + boost: 2.0, + ); + + $array = $te->toArray(); + + \Tester\Assert::same(5, $array['text_expansion']['tokens']['pruning_config']['tokens_freq_ratio_threshold']); + \Tester\Assert::same(2.0, $array['text_expansion']['tokens']['boost']); + } + + + public function testKey(): void + { + $te = new \Spameri\ElasticQuery\Query\TextExpansion('f', 'm', 'q'); + + \Tester\Assert::same('text_expansion_f', $te->key()); + } + +} + +(new TextExpansion())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/WeightedTokens.phpt b/tests/SpameriTests/ElasticQuery/Query/WeightedTokens.phpt new file mode 100644 index 0000000..46dccdc --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/WeightedTokens.phpt @@ -0,0 +1,73 @@ + [ + 'properties' => [ + 'tokens' => ['type' => 'sparse_vector'], + ], + ], + ]; + } + + + public function testToArray(): void + { + $wt = new \Spameri\ElasticQuery\Query\WeightedTokens( + field: 'tokens', + tokens: ['lion' => 0.5, 'tiger' => 0.7], + ); + + $array = $wt->toArray(); + + \Tester\Assert::same(0.5, $array['weighted_tokens']['tokens']['tokens']['lion']); + } + + + public function testToArrayWithPruning(): void + { + $wt = new \Spameri\ElasticQuery\Query\WeightedTokens( + field: 'tokens', + tokens: ['cat' => 0.5], + pruningConfig: ['tokens_freq_ratio_threshold' => 5], + ); + + $array = $wt->toArray(); + + \Tester\Assert::same(5, $array['weighted_tokens']['tokens']['pruning_config']['tokens_freq_ratio_threshold']); + } + + + public function testRequiresTokens(): void + { + \Tester\Assert::exception( + static function (): void { + new \Spameri\ElasticQuery\Query\WeightedTokens('f', []); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testKey(): void + { + $wt = new \Spameri\ElasticQuery\Query\WeightedTokens('f', ['t' => 0.5]); + + \Tester\Assert::same('weighted_tokens_f', $wt->key()); + } + +} + +(new WeightedTokens())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Query/WildCard.phpt b/tests/SpameriTests/ElasticQuery/Query/WildCard.phpt index c41085f..6def0ae 100644 --- a/tests/SpameriTests/ElasticQuery/Query/WildCard.phpt +++ b/tests/SpameriTests/ElasticQuery/Query/WildCard.phpt @@ -5,88 +5,82 @@ namespace SpameriTests\ElasticQuery\Query; require_once __DIR__ . '/../../bootstrap.php'; -class WildCard extends \Tester\TestCase +class WildCard extends \SpameriTests\ElasticQuery\AbstractElasticTestCase { - private const INDEX = 'spameri_test_video_wildcard'; + protected const INDEX = 'spameri_test_query_wildcard'; - public function setUp() : void + protected function mapping(): array|null { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + return ['mappings' => ['properties' => ['name' => ['type' => 'keyword']]]]; } - public function testCreate() : void + public function testToArray(): void { - $wildCard = new \Spameri\ElasticQuery\Query\WildCard( - 'name', - 'Avengers', - 1.0 - ); + $wildCard = new \Spameri\ElasticQuery\Query\WildCard('name', 'Aveng*', 1.0); $array = $wildCard->toArray(); - \Tester\Assert::true(isset($array['wildcard']['name']['value'])); - \Tester\Assert::same('Avengers', $array['wildcard']['name']['value']); + \Tester\Assert::same('Aveng*', $array['wildcard']['name']['value']); \Tester\Assert::same(1.0, $array['wildcard']['name']['boost']); + } - $document = new \Spameri\ElasticQuery\Document( - self::INDEX, - new \Spameri\ElasticQuery\Document\Body\Plain( - ( - new \Spameri\ElasticQuery\ElasticQuery( - new \Spameri\ElasticQuery\Query\QueryCollection( - NULL, - new \Spameri\ElasticQuery\Query\MustCollection( - $wildCard - ) - ) - ) - )->toArray() - ), - self::INDEX + + public function testToArrayWithCaseInsensitive(): void + { + $wildCard = new \Spameri\ElasticQuery\Query\WildCard( + field: 'name', + query: 'aveng*', + caseInsensitive: true, + rewrite: 'constant_score', ); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . $document->index() . '/_search'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - \json_encode($document->toArray()['body']) + \Tester\Assert::true($wildCard->toArray()['wildcard']['name']['case_insensitive']); + \Tester\Assert::same('constant_score', $wildCard->toArray()['wildcard']['name']['rewrite']); + } + + + public function testCreate(): void + { + $this->indexDocument(['name' => 'Avengers']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\WildCard('name', 'Aveng*'), + ), + ), ); - \Tester\Assert::noError(static function () use ($ch) { - $response = curl_exec($ch); - $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); - /** @var \Spameri\ElasticQuery\Response\ResultSearch $result */ - $result = $resultMapper->map(\json_decode($response, TRUE)); - \Tester\Assert::type('int', $result->stats()->total()); - }); + $result = $this->search($query); - curl_close($ch); + \Tester\Assert::same(1, $result->stats()->total()); } - public function tearDown() : void + public function testCreateWithCaseInsensitive(): void { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, 'localhost:9200/' . self::INDEX . '/'); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - \curl_exec($ch); - \curl_close($ch); + $this->indexDocument(['name' => 'Avengers']); + + $query = new \Spameri\ElasticQuery\ElasticQuery( + new \Spameri\ElasticQuery\Query\QueryCollection( + null, + new \Spameri\ElasticQuery\Query\MustCollection( + new \Spameri\ElasticQuery\Query\WildCard( + field: 'name', + query: 'aveng*', + caseInsensitive: true, + ), + ), + ), + ); + + $result = $this->search($query); + + \Tester\Assert::same(1, $result->stats()->total()); } } diff --git a/tests/SpameriTests/ElasticQuery/Query/Wrapper.phpt b/tests/SpameriTests/ElasticQuery/Query/Wrapper.phpt new file mode 100644 index 0000000..041f32e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Query/Wrapper.phpt @@ -0,0 +1,58 @@ +toArray(); + + \Tester\Assert::same(\base64_encode($raw), $array['wrapper']['query']); + } + + + public function testKey(): void + { + $wrapper = new \Spameri\ElasticQuery\Query\Wrapper('{}'); + + \Tester\Assert::contains('wrapper_', $wrapper->key()); + } + + + public function tearDown(): void + { + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + \curl_exec($ch); + } + +} + +(new Wrapper())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result.phpt b/tests/SpameriTests/ElasticQuery/Response/Result.phpt index 49cf52e..bef340b 100644 --- a/tests/SpameriTests/ElasticQuery/Response/Result.phpt +++ b/tests/SpameriTests/ElasticQuery/Response/Result.phpt @@ -26,7 +26,7 @@ class Result extends \Tester\TestCase 'hits' => [ 0 => [ '_index' => 'spameri_guessed-2018-11-29_21-25-34', - '_type' => 'spameri_guessed', + '_type' => '_doc', '_id' => 'EWhVcWcBhsjlL-GzEP8i', '_score' => 1.0, '_source' => [ @@ -40,7 +40,7 @@ class Result extends \Tester\TestCase ], 1 => [ '_index' => 'spameri_guessed-2018-11-29_21-25-34', - '_type' => 'spameri_guessed', + '_type' => '_doc', '_id' => 'yQ8Bi2cBx3V83AUUwx3m', '_score' => 1.0, '_source' => [ @@ -85,7 +85,7 @@ class Result extends \Tester\TestCase \Tester\Assert::same('spameri_guessed-2018-11-29_21-25-34', $hit->index()); \Tester\Assert::same(0, $hit->position()); \Tester\Assert::same(1.0, $hit->score()); - \Tester\Assert::same('spameri_guessed', $hit->type()); + \Tester\Assert::same('_doc', $hit->type()); \Tester\Assert::same('xyC3UmcBeqWWbOzi4uKU', $hit->getValue('user')); // AGGREGATION tests @@ -101,6 +101,9 @@ class Result extends \Tester\TestCase } } + // HITS tests + \Tester\Assert::count(0, $aggregation->hits()); + // STATS tests \Tester\Assert::same(FALSE, $resultObject->stats()->timedOut()); \Tester\Assert::same(37, $resultObject->stats()->took()); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation.phpt new file mode 100644 index 0000000..4e18e31 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation.phpt @@ -0,0 +1,96 @@ +name()); + \Tester\Assert::same(0, $aggregation->position()); + \Tester\Assert::same($bucketCollection, $aggregation->buckets()); + \Tester\Assert::same($aggregationCollection, $aggregation->aggregations()); + \Tester\Assert::same($hitCollection, $aggregation->hits()); + } + + + public function testCountBuckets(): void + { + $bucket1 = new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket('electronics', 100, 0); + $bucket2 = new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket('books', 50, 1); + $bucket3 = new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket('clothing', 30, 2); + + $bucketCollection = new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection( + $bucket1, + $bucket2, + $bucket3, + ); + + $aggregation = new \Spameri\ElasticQuery\Response\Result\Aggregation( + 'categories', + 0, + $bucketCollection, + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + ); + + \Tester\Assert::same(180, $aggregation->countBuckets()); + } + + + public function testCountBucketsEmpty(): void + { + $aggregation = new \Spameri\ElasticQuery\Response\Result\Aggregation( + 'empty_agg', + 0, + new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + ); + + \Tester\Assert::same(0, $aggregation->countBuckets()); + } + + + public function testWithNestedAggregations(): void + { + $nestedAggregation = new \Spameri\ElasticQuery\Response\Result\Aggregation( + 'sub_category', + 0, + new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + ); + + $aggregation = new \Spameri\ElasticQuery\Response\Result\Aggregation( + 'categories', + 0, + new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection($nestedAggregation), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + ); + + $foundNested = $aggregation->aggregations()->getAggregation('sub_category'); + + \Tester\Assert::same($nestedAggregation, $foundNested); + } + +} + +(new Aggregation())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation/Bucket.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation/Bucket.phpt new file mode 100644 index 0000000..25840d6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation/Bucket.phpt @@ -0,0 +1,91 @@ +key()); + \Tester\Assert::same(100, $bucket->docCount()); + \Tester\Assert::same(0, $bucket->position()); + \Tester\Assert::null($bucket->from()); + \Tester\Assert::null($bucket->to()); + } + + + public function testWithRange(): void + { + $bucket = new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket( + '0-100', + 50, + 0, + 0, + 100, + ); + + \Tester\Assert::same('0-100', $bucket->key()); + \Tester\Assert::same(50, $bucket->docCount()); + \Tester\Assert::same(0, $bucket->from()); + \Tester\Assert::same(100, $bucket->to()); + } + + + public function testWithFloatRange(): void + { + $bucket = new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket( + 'price_range', + 25, + 0, + 10.5, + 99.99, + ); + + \Tester\Assert::same('price_range', $bucket->key()); + \Tester\Assert::same(25, $bucket->docCount()); + \Tester\Assert::same(10.5, $bucket->from()); + \Tester\Assert::same(99.99, $bucket->to()); + } + + + public function testWithNullPosition(): void + { + $bucket = new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket( + 'test', + 10, + null, + ); + + \Tester\Assert::same('test', $bucket->key()); + \Tester\Assert::same(10, $bucket->docCount()); + \Tester\Assert::null($bucket->position()); + } + + + public function testOpenEndedRange(): void + { + $bucket = new \Spameri\ElasticQuery\Response\Result\Aggregation\Bucket( + 'expensive', + 15, + 0, + 100, + null, + ); + + \Tester\Assert::same(100, $bucket->from()); + \Tester\Assert::null($bucket->to()); + } + +} + +(new Bucket())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation/BucketCollection.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation/BucketCollection.phpt new file mode 100644 index 0000000..6ded57c --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/Aggregation/BucketCollection.phpt @@ -0,0 +1,65 @@ +key(); + $docCounts[] = $bucket->docCount(); + } + + \Tester\Assert::same(['first', 'second', 'third'], $keys); + \Tester\Assert::same([10, 20, 30], $docCounts); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection(); + + \Tester\Assert::type(\ArrayIterator::class, $collection->getIterator()); + } + +} + +(new BucketCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/AggregationCollection.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/AggregationCollection.phpt new file mode 100644 index 0000000..728b94f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/AggregationCollection.phpt @@ -0,0 +1,105 @@ +getAggregation('categories')); + \Tester\Assert::same($agg2, $collection->getAggregation('brands')); + } + + + public function testGetAggregationNotFound(): void + { + $collection = new \Spameri\ElasticQuery\Response\Result\AggregationCollection(); + + \Tester\Assert::null($collection->getAggregation('nonexistent')); + } + + + public function testIterate(): void + { + $agg1 = new \Spameri\ElasticQuery\Response\Result\Aggregation( + 'first', + 0, + new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + ); + $agg2 = new \Spameri\ElasticQuery\Response\Result\Aggregation( + 'second', + 1, + new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + ); + + $collection = new \Spameri\ElasticQuery\Response\Result\AggregationCollection($agg1, $agg2); + + $names = []; + foreach ($collection as $aggregation) { + $names[] = $aggregation->name(); + } + + \Tester\Assert::same(['first', 'second'], $names); + } + +} + +(new AggregationCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/BulkAction.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/BulkAction.phpt new file mode 100644 index 0000000..c66f6c1 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/BulkAction.phpt @@ -0,0 +1,112 @@ +action()); + \Tester\Assert::same('test_index', $bulkAction->index()); + \Tester\Assert::same('_doc', $bulkAction->type()); + \Tester\Assert::same('doc_1', $bulkAction->id()); + \Tester\Assert::same(1, $bulkAction->version()); + \Tester\Assert::same('created', $bulkAction->result()); + \Tester\Assert::same($shards, $bulkAction->shards()); + \Tester\Assert::same(201, $bulkAction->status()); + \Tester\Assert::same(0, $bulkAction->seqNo()); + \Tester\Assert::same(1, $bulkAction->primaryTerm()); + } + + + public function testCreateUpdate(): void + { + $shards = new \Spameri\ElasticQuery\Response\Shards(2, 1, 0, 0); + + $bulkAction = new \Spameri\ElasticQuery\Response\Result\BulkAction( + 'update', + 'test_index', + '_doc', + 'doc_1', + 2, + 'updated', + $shards, + 200, + 1, + 1, + ); + + \Tester\Assert::same('update', $bulkAction->action()); + \Tester\Assert::same('updated', $bulkAction->result()); + \Tester\Assert::same(2, $bulkAction->version()); + \Tester\Assert::same(200, $bulkAction->status()); + } + + + public function testCreateDelete(): void + { + $shards = new \Spameri\ElasticQuery\Response\Shards(2, 1, 0, 0); + + $bulkAction = new \Spameri\ElasticQuery\Response\Result\BulkAction( + 'delete', + 'test_index', + '_doc', + 'doc_1', + 3, + 'deleted', + $shards, + 200, + 2, + 1, + ); + + \Tester\Assert::same('delete', $bulkAction->action()); + \Tester\Assert::same('deleted', $bulkAction->result()); + \Tester\Assert::same(3, $bulkAction->version()); + } + + + public function testShardsAccess(): void + { + $shards = new \Spameri\ElasticQuery\Response\Shards(2, 2, 0, 0); + + $bulkAction = new \Spameri\ElasticQuery\Response\Result\BulkAction( + 'index', + 'test_index', + '_doc', + 'doc_1', + 1, + 'created', + $shards, + 201, + 0, + 1, + ); + + \Tester\Assert::same(2, $bulkAction->shards()->total()); + \Tester\Assert::same(2, $bulkAction->shards()->successful()); + \Tester\Assert::same(0, $bulkAction->shards()->failed()); + } + +} + +(new BulkAction())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/BulkActionCollection.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/BulkActionCollection.phpt new file mode 100644 index 0000000..770a954 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/BulkActionCollection.phpt @@ -0,0 +1,124 @@ +action(); + $ids[] = $action->id(); + } + + \Tester\Assert::same(['index', 'update', 'delete'], $actions); + \Tester\Assert::same(['id_1', 'id_2', 'id_3'], $ids); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Response\Result\BulkActionCollection(); + + \Tester\Assert::type(\ArrayIterator::class, $collection->getIterator()); + } + +} + +(new BulkActionCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/Hit.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/Hit.phpt new file mode 100644 index 0000000..3081980 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/Hit.phpt @@ -0,0 +1,293 @@ + 'Test', 'price' => 100], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.5, + 1, + ); + + \Tester\Assert::same(['name' => 'Test', 'price' => 100], $hit->source()); + \Tester\Assert::same(0, $hit->position()); + \Tester\Assert::same('test_index', $hit->index()); + \Tester\Assert::same('_doc', $hit->type()); + \Tester\Assert::same('doc_1', $hit->id()); + \Tester\Assert::same(1.5, $hit->score()); + \Tester\Assert::same(1, $hit->version()); + } + + + public function testGetValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test', 'price' => 100, 'active' => true], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same('Test', $hit->getValue('name')); + \Tester\Assert::same(100, $hit->getValue('price')); + \Tester\Assert::same(true, $hit->getValue('active')); + \Tester\Assert::null($hit->getValue('nonexistent')); + } + + + public function testGetStringValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test'], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same('Test', $hit->getStringValue('name')); + } + + + public function testGetStringValueThrowsOnNonString(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['price' => 100], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::exception( + static function () use ($hit): void { + $hit->getStringValue('price'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testGetStringOrNullValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test', 'price' => 100], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same('Test', $hit->getStringOrNullValue('name')); + \Tester\Assert::null($hit->getStringOrNullValue('price')); + \Tester\Assert::null($hit->getStringOrNullValue('nonexistent')); + } + + + public function testGetArrayValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['tags' => ['php', 'elasticsearch']], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same(['php', 'elasticsearch'], $hit->getArrayValue('tags')); + } + + + public function testGetArrayValueThrowsOnNonArray(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test'], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::exception( + static function () use ($hit): void { + $hit->getArrayValue('name'); + }, + \Spameri\ElasticQuery\Exception\InvalidArgumentException::class, + ); + } + + + public function testGetArrayOrNullValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['tags' => ['php'], 'name' => 'Test'], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same(['php'], $hit->getArrayOrNullValue('tags')); + \Tester\Assert::null($hit->getArrayOrNullValue('name')); + } + + + public function testGetBoolValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['active' => true], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::true($hit->getBoolValue('active')); + } + + + public function testGetBoolOrNullValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['active' => false, 'name' => 'Test'], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::false($hit->getBoolOrNullValue('active')); + \Tester\Assert::null($hit->getBoolOrNullValue('name')); + } + + + public function testGetIntegerValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['count' => 42], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same(42, $hit->getIntegerValue('count')); + } + + + public function testGetIntegerOrNullValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['count' => 42, 'name' => 'Test'], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same(42, $hit->getIntegerOrNullValue('count')); + \Tester\Assert::null($hit->getIntegerOrNullValue('name')); + } + + + public function testGetFloatValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['price' => 99.99], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same(99.99, $hit->getFloatValue('price')); + } + + + public function testGetFloatOrNullValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['price' => 99.99, 'name' => 'Test'], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same(99.99, $hit->getFloatOrNullValue('price')); + \Tester\Assert::null($hit->getFloatOrNullValue('name')); + } + + + public function testGetSubValue(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['nested' => ['level1' => ['level2' => 'deep_value']]], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::same('deep_value', $hit->getValue('nested.level1.level2')); + } + + + public function testGetSubValueReturnsNull(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['nested' => ['level1' => 'value']], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + + \Tester\Assert::null($hit->getValue('nested.nonexistent.path')); + } + +} + +(new Hit())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Result/HitCollection.phpt b/tests/SpameriTests/ElasticQuery/Response/Result/HitCollection.phpt new file mode 100644 index 0000000..05d0d06 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Result/HitCollection.phpt @@ -0,0 +1,124 @@ +count()); + \Tester\Assert::same([], $collection->ids()); + } + + + public function testCreateWithHits(): void + { + $hit1 = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test 1'], + 0, + 'test_index', + '_doc', + 'id_1', + 1.0, + 1, + ); + $hit2 = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test 2'], + 1, + 'test_index', + '_doc', + 'id_2', + 0.9, + 1, + ); + + $collection = new \Spameri\ElasticQuery\Response\Result\HitCollection($hit1, $hit2); + + \Tester\Assert::same(2, $collection->count()); + } + + + public function testIds(): void + { + $hit1 = new \Spameri\ElasticQuery\Response\Result\Hit( + [], + 0, + 'test_index', + '_doc', + 'id_1', + 1.0, + 1, + ); + $hit2 = new \Spameri\ElasticQuery\Response\Result\Hit( + [], + 1, + 'test_index', + '_doc', + 'id_2', + 0.9, + 1, + ); + $hit3 = new \Spameri\ElasticQuery\Response\Result\Hit( + [], + 2, + 'test_index', + '_doc', + 'id_3', + 0.8, + 1, + ); + + $collection = new \Spameri\ElasticQuery\Response\Result\HitCollection($hit1, $hit2, $hit3); + + \Tester\Assert::same(['id_1', 'id_2', 'id_3'], $collection->ids()); + } + + + public function testIterate(): void + { + $hit1 = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'First'], + 0, + 'test_index', + '_doc', + 'id_1', + 1.0, + 1, + ); + $hit2 = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Second'], + 1, + 'test_index', + '_doc', + 'id_2', + 0.9, + 1, + ); + + $collection = new \Spameri\ElasticQuery\Response\Result\HitCollection($hit1, $hit2); + + $names = []; + foreach ($collection as $hit) { + $names[] = $hit->getValue('name'); + } + + \Tester\Assert::same(['First', 'Second'], $names); + } + + + public function testGetIterator(): void + { + $collection = new \Spameri\ElasticQuery\Response\Result\HitCollection(); + + \Tester\Assert::type(\ArrayIterator::class, $collection->getIterator()); + } + +} + +(new HitCollection())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/ResultBulk.phpt b/tests/SpameriTests/ElasticQuery/Response/ResultBulk.phpt new file mode 100644 index 0000000..814ac8f --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/ResultBulk.phpt @@ -0,0 +1,79 @@ +stats()); + } + + + public function testGetFirstAction(): void + { + $shards = new \Spameri\ElasticQuery\Response\Shards(1, 1, 0, 0); + $action1 = new \Spameri\ElasticQuery\Response\Result\BulkAction( + 'index', + 'test_index', + '_doc', + 'id_1', + 1, + 'created', + $shards, + 201, + 0, + 1, + ); + $action2 = new \Spameri\ElasticQuery\Response\Result\BulkAction( + 'index', + 'test_index', + '_doc', + 'id_2', + 1, + 'created', + $shards, + 201, + 1, + 1, + ); + + $result = new \Spameri\ElasticQuery\Response\ResultBulk( + new \Spameri\ElasticQuery\Response\Stats(10, false, 2), + new \Spameri\ElasticQuery\Response\Result\BulkActionCollection($action1, $action2), + ); + + $foundAction = $result->getFirstAction('id_2'); + + \Tester\Assert::same($action2, $foundAction); + \Tester\Assert::same('created', $foundAction->result()); + } + + + public function testGetFirstActionNotFound(): void + { + $result = new \Spameri\ElasticQuery\Response\ResultBulk( + new \Spameri\ElasticQuery\Response\Stats(10, false, 0), + new \Spameri\ElasticQuery\Response\Result\BulkActionCollection(), + ); + + \Tester\Assert::exception( + static function () use ($result): void { + $result->getFirstAction('nonexistent'); + }, + \Spameri\ElasticQuery\Exception\BulkActionNotFound::class, + ); + } + +} + +(new ResultBulk())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/ResultMapper.phpt b/tests/SpameriTests/ElasticQuery/Response/ResultMapper.phpt new file mode 100644 index 0000000..c8c78a6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/ResultMapper.phpt @@ -0,0 +1,282 @@ + 5, + 'timed_out' => false, + '_shards' => [ + 'total' => 5, + 'successful' => 5, + 'skipped' => 0, + 'failed' => 0, + ], + 'hits' => [ + 'total' => ['value' => 2, 'relation' => 'eq'], + 'max_score' => 1.0, + 'hits' => [ + [ + '_index' => 'test_index', + '_id' => 'id_1', + '_score' => 1.0, + '_source' => ['name' => 'Document 1'], + ], + [ + '_index' => 'test_index', + '_id' => 'id_2', + '_score' => 0.9, + '_source' => ['name' => 'Document 2'], + ], + ], + ], + ]; + + $result = $mapper->map($elasticResponse); + + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + \Tester\Assert::same(5, $result->stats()->took()); + \Tester\Assert::false($result->stats()->timedOut()); + \Tester\Assert::same(2, $result->stats()->total()); + \Tester\Assert::same(5, $result->shards()->total()); + \Tester\Assert::same(2, $result->hits()->count()); + } + + + public function testMapSearchResultsWithLegacyTotal(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + 'took' => 5, + 'timed_out' => false, + '_shards' => [ + 'total' => 5, + 'successful' => 5, + 'skipped' => 0, + 'failed' => 0, + ], + 'hits' => [ + 'total' => 100, + 'max_score' => 1.0, + 'hits' => [], + ], + ]; + + $result = $mapper->map($elasticResponse); + + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + \Tester\Assert::same(100, $result->stats()->total()); + } + + + public function testMapSingleResult(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + '_index' => 'test_index', + '_id' => 'doc_1', + 'version' => 1, + 'found' => true, + '_source' => ['name' => 'Test Document', 'price' => 100], + ]; + + $result = $mapper->map($elasticResponse); + + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSingle::class, $result); + \Tester\Assert::true($result->stats()->found()); + \Tester\Assert::same(1, $result->stats()->version()); + \Tester\Assert::same('doc_1', $result->hit()->id()); + \Tester\Assert::same('Test Document', $result->hit()->getValue('name')); + } + + + public function testMapBulkResultDirect(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + 'took' => 30, + 'errors' => false, + 'timed_out' => false, + 'hits' => [ + 'total' => 0, + ], + 'items' => [ + 'index' => [ + '_index' => 'test_index', + '_id' => 'id_1', + '_version' => 1, + 'result' => 'created', + '_shards' => [ + 'total' => 2, + 'successful' => 1, + 'failed' => 0, + ], + 'status' => 201, + '_seq_no' => 0, + '_primary_term' => 1, + ], + ], + ]; + + $result = $mapper->mapBulkResult($elasticResponse); + + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultBulk::class, $result); + \Tester\Assert::same(30, $result->stats()->took()); + } + + + public function testMapVersionResult(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + 'name' => 'test-node', + 'cluster_name' => 'test-cluster', + 'cluster_uuid' => 'abc123', + 'version' => [ + 'number' => '8.0.0', + 'build_hash' => 'abc123', + 'build_snapshot' => false, + 'lucene_version' => '9.0.0', + ], + 'tagline' => 'You Know, for Search', + ]; + + $result = $mapper->map($elasticResponse); + + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultVersion::class, $result); + \Tester\Assert::same('test-node', $result->name()); + \Tester\Assert::same('test-cluster', $result->clusterName()); + \Tester\Assert::same('8.0.0', $result->version()->number()); + } + + + public function testMapInvalidResponse(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + \Tester\Assert::exception( + static function () use ($mapper): void { + $mapper->map(['invalid' => 'response']); + }, + \Spameri\ElasticQuery\Exception\ResponseCouldNotBeMapped::class, + ); + } + + + public function testMapSearchResultsWithAggregations(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + 'took' => 5, + 'timed_out' => false, + '_shards' => [ + 'total' => 5, + 'successful' => 5, + 'skipped' => 0, + 'failed' => 0, + ], + 'hits' => [ + 'total' => ['value' => 0, 'relation' => 'eq'], + 'max_score' => null, + 'hits' => [], + ], + 'aggregations' => [ + 'categories' => [ + 'buckets' => [ + ['key' => 'electronics', 'doc_count' => 100], + ['key' => 'books', 'doc_count' => 50], + ], + ], + ], + ]; + + $result = $mapper->map($elasticResponse); + + \Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result); + $aggregation = $result->getAggregation('categories'); + \Tester\Assert::same('categories', $aggregation->name()); + } + + + public function testMapHits(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + 'hits' => [ + 'hits' => [ + [ + '_index' => 'test_index', + '_id' => 'id_1', + '_score' => 1.5, + '_source' => ['name' => 'Test'], + ], + ], + ], + ]; + + $hitCollection = $mapper->mapHits($elasticResponse); + + \Tester\Assert::same(1, $hitCollection->count()); + \Tester\Assert::same(['id_1'], $hitCollection->ids()); + } + + + public function testMapShards(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + '_shards' => [ + 'total' => 5, + 'successful' => 4, + 'skipped' => 1, + 'failed' => 0, + ], + ]; + + $shards = $mapper->mapShards($elasticResponse); + + \Tester\Assert::same(5, $shards->total()); + \Tester\Assert::same(4, $shards->successful()); + \Tester\Assert::same(1, $shards->skipped()); + \Tester\Assert::same(0, $shards->failed()); + } + + + public function testMapStats(): void + { + $mapper = new \Spameri\ElasticQuery\Response\ResultMapper(); + + $elasticResponse = [ + 'took' => 15, + 'timed_out' => true, + 'hits' => [ + 'total' => ['value' => 500, 'relation' => 'eq'], + ], + ]; + + $stats = $mapper->mapStats($elasticResponse); + + \Tester\Assert::same(15, $stats->took()); + \Tester\Assert::true($stats->timedOut()); + \Tester\Assert::same(500, $stats->total()); + } + +} + +(new ResultMapper())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/ResultSearch.phpt b/tests/SpameriTests/ElasticQuery/Response/ResultSearch.phpt new file mode 100644 index 0000000..eeb1e0a --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/ResultSearch.phpt @@ -0,0 +1,127 @@ +stats()); + \Tester\Assert::same($shards, $result->shards()); + \Tester\Assert::same($hitCollection, $result->hits()); + \Tester\Assert::same($aggregationCollection, $result->aggregations()); + } + + + public function testGetHit(): void + { + $hit1 = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test 1'], + 0, + 'test_index', + '_doc', + 'id_1', + 1.0, + 1, + ); + $hit2 = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test 2'], + 1, + 'test_index', + '_doc', + 'id_2', + 0.9, + 1, + ); + + $result = new \Spameri\ElasticQuery\Response\ResultSearch( + new \Spameri\ElasticQuery\Response\Stats(10, false, 2), + new \Spameri\ElasticQuery\Response\Shards(5, 5, 0, 0), + new \Spameri\ElasticQuery\Response\Result\HitCollection($hit1, $hit2), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + ); + + $foundHit = $result->getHit('id_2'); + + \Tester\Assert::same($hit2, $foundHit); + \Tester\Assert::same('Test 2', $foundHit->getValue('name')); + } + + + public function testGetHitNotFound(): void + { + $result = new \Spameri\ElasticQuery\Response\ResultSearch( + new \Spameri\ElasticQuery\Response\Stats(10, false, 0), + new \Spameri\ElasticQuery\Response\Shards(5, 5, 0, 0), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + ); + + \Tester\Assert::exception( + static function () use ($result): void { + $result->getHit('nonexistent'); + }, + \Spameri\ElasticQuery\Exception\HitNotFound::class, + ); + } + + + public function testGetAggregation(): void + { + $aggregation = new \Spameri\ElasticQuery\Response\Result\Aggregation( + 'categories', + 0, + new \Spameri\ElasticQuery\Response\Result\Aggregation\BucketCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + ); + + $result = new \Spameri\ElasticQuery\Response\ResultSearch( + new \Spameri\ElasticQuery\Response\Stats(10, false, 0), + new \Spameri\ElasticQuery\Response\Shards(5, 5, 0, 0), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection($aggregation), + ); + + $foundAggregation = $result->getAggregation('categories'); + + \Tester\Assert::same($aggregation, $foundAggregation); + } + + + public function testGetAggregationNotFound(): void + { + $result = new \Spameri\ElasticQuery\Response\ResultSearch( + new \Spameri\ElasticQuery\Response\Stats(10, false, 0), + new \Spameri\ElasticQuery\Response\Shards(5, 5, 0, 0), + new \Spameri\ElasticQuery\Response\Result\HitCollection(), + new \Spameri\ElasticQuery\Response\Result\AggregationCollection(), + ); + + \Tester\Assert::exception( + static function () use ($result): void { + $result->getAggregation('nonexistent'); + }, + \Spameri\ElasticQuery\Exception\AggregationNotFound::class, + ); + } + +} + +(new ResultSearch())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/ResultSingle.phpt b/tests/SpameriTests/ElasticQuery/Response/ResultSingle.phpt new file mode 100644 index 0000000..f8b73c2 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/ResultSingle.phpt @@ -0,0 +1,73 @@ + 'Test Document', 'price' => 100], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + $stats = new \Spameri\ElasticQuery\Response\StatsSingle(1, true); + + $result = new \Spameri\ElasticQuery\Response\ResultSingle($hit, $stats); + + \Tester\Assert::same($hit, $result->hit()); + \Tester\Assert::same($stats, $result->stats()); + } + + + public function testHitValues(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + ['name' => 'Test Document', 'price' => 100, 'active' => true], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + $stats = new \Spameri\ElasticQuery\Response\StatsSingle(1, true); + + $result = new \Spameri\ElasticQuery\Response\ResultSingle($hit, $stats); + + \Tester\Assert::same('Test Document', $result->hit()->getValue('name')); + \Tester\Assert::same(100, $result->hit()->getValue('price')); + \Tester\Assert::same('doc_1', $result->hit()->id()); + } + + + public function testStatsFound(): void + { + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + [], + 0, + 'test_index', + '_doc', + 'doc_1', + 1.0, + 1, + ); + $stats = new \Spameri\ElasticQuery\Response\StatsSingle(1, true); + + $result = new \Spameri\ElasticQuery\Response\ResultSingle($hit, $stats); + + \Tester\Assert::true($result->stats()->found()); + \Tester\Assert::same(1, $result->stats()->version()); + } + +} + +(new ResultSingle())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/ResultVersion.phpt b/tests/SpameriTests/ElasticQuery/Response/ResultVersion.phpt index 6c30daa..4825066 100644 --- a/tests/SpameriTests/ElasticQuery/Response/ResultVersion.phpt +++ b/tests/SpameriTests/ElasticQuery/Response/ResultVersion.phpt @@ -13,13 +13,29 @@ class ResultVersion extends \Tester\TestCase { $resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper(); /** @var \Spameri\ElasticQuery\Response\ResultVersion $resultObject */ - $resultObject = $resultMapper->map(\json_decode(\file_get_contents('http://127.0.0.1:9200'), TRUE)); + $resultObject = $resultMapper->map(\json_decode(\file_get_contents(\ELASTICSEARCH_HOST), TRUE)); \Tester\Assert::true($resultObject instanceof \Spameri\ElasticQuery\Response\ResultVersion); - if (\Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_7 <= $resultObject->version()->id()) { + if (\Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_9 <= $resultObject->version()->id()) { \Tester\Assert::true( - \Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_7 < $resultObject->version()->id() + \Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_9 <= $resultObject->version()->id() + ); + \Tester\Assert::true( + \Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_10 > $resultObject->version()->id() + ); + + } elseif (\Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_8 <= $resultObject->version()->id()) { + \Tester\Assert::true( + \Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_8 <= $resultObject->version()->id() + ); + \Tester\Assert::true( + \Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_9 > $resultObject->version()->id() + ); + + } elseif (\Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_7 <= $resultObject->version()->id()) { + \Tester\Assert::true( + \Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_7 <= $resultObject->version()->id() ); \Tester\Assert::true( \Spameri\ElasticQuery\Response\Result\Version::ELASTIC_VERSION_ID_8 > $resultObject->version()->id() diff --git a/tests/SpameriTests/ElasticQuery/Response/Shards.phpt b/tests/SpameriTests/ElasticQuery/Response/Shards.phpt new file mode 100644 index 0000000..0a267a7 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Shards.phpt @@ -0,0 +1,44 @@ +total()); + \Tester\Assert::same(8, $shards->successful()); + \Tester\Assert::same(1, $shards->skipped()); + \Tester\Assert::same(1, $shards->failed()); + } + + + public function testAllSuccessful(): void + { + $shards = new \Spameri\ElasticQuery\Response\Shards(5, 5, 0, 0); + + \Tester\Assert::same(5, $shards->total()); + \Tester\Assert::same(5, $shards->successful()); + \Tester\Assert::same(0, $shards->skipped()); + \Tester\Assert::same(0, $shards->failed()); + } + + + public function testWithFailures(): void + { + $shards = new \Spameri\ElasticQuery\Response\Shards(5, 3, 0, 2); + + \Tester\Assert::same(5, $shards->total()); + \Tester\Assert::same(3, $shards->successful()); + \Tester\Assert::same(2, $shards->failed()); + } + +} + +(new Shards())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/Stats.phpt b/tests/SpameriTests/ElasticQuery/Response/Stats.phpt new file mode 100644 index 0000000..f852277 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/Stats.phpt @@ -0,0 +1,42 @@ +took()); + \Tester\Assert::false($stats->timedOut()); + \Tester\Assert::same(100, $stats->total()); + } + + + public function testTimedOut(): void + { + $stats = new \Spameri\ElasticQuery\Response\Stats(5000, true, 50); + + \Tester\Assert::same(5000, $stats->took()); + \Tester\Assert::true($stats->timedOut()); + \Tester\Assert::same(50, $stats->total()); + } + + + public function testZeroResults(): void + { + $stats = new \Spameri\ElasticQuery\Response\Stats(1, false, 0); + + \Tester\Assert::same(1, $stats->took()); + \Tester\Assert::false($stats->timedOut()); + \Tester\Assert::same(0, $stats->total()); + } + +} + +(new Stats())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Response/StatsSingle.phpt b/tests/SpameriTests/ElasticQuery/Response/StatsSingle.phpt new file mode 100644 index 0000000..48ab351 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Response/StatsSingle.phpt @@ -0,0 +1,39 @@ +version()); + \Tester\Assert::true($stats->found()); + } + + + public function testCreateNotFound(): void + { + $stats = new \Spameri\ElasticQuery\Response\StatsSingle(0, false); + + \Tester\Assert::same(0, $stats->version()); + \Tester\Assert::false($stats->found()); + } + + + public function testHigherVersion(): void + { + $stats = new \Spameri\ElasticQuery\Response\StatsSingle(5, true); + + \Tester\Assert::same(5, $stats->version()); + \Tester\Assert::true($stats->found()); + } + +} + +(new StatsSingle())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Value/AbstractBoolValue.phpt b/tests/SpameriTests/ElasticQuery/Value/AbstractBoolValue.phpt new file mode 100644 index 0000000..1fbc424 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Value/AbstractBoolValue.phpt @@ -0,0 +1,69 @@ +value()); + } + + + public function testFalseValue(): void + { + $value = new TestBoolValue(false); + + \Tester\Assert::false($value->value()); + } + + + public function testImplementsInterface(): void + { + $value = new TestBoolValue(true); + + \Tester\Assert::type(\Spameri\ElasticQuery\Value\ValueInterface::class, $value); + } + + + public function testReturnType(): void + { + $value = new TestBoolValue(true); + + \Tester\Assert::type('bool', $value->value()); + } + + + public function testTrueIsNotOne(): void + { + $value = new TestBoolValue(true); + + \Tester\Assert::notSame(1, $value->value()); + } + + + public function testFalseIsNotZero(): void + { + $value = new TestBoolValue(false); + + \Tester\Assert::notSame(0, $value->value()); + } + +} + +(new AbstractBoolValue())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Value/AbstractFloatValue.phpt b/tests/SpameriTests/ElasticQuery/Value/AbstractFloatValue.phpt new file mode 100644 index 0000000..ca20d5e --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Value/AbstractFloatValue.phpt @@ -0,0 +1,94 @@ +value()); + } + + + public function testZeroValue(): void + { + $value = new TestFloatValue(0.0); + + \Tester\Assert::same(0.0, $value->value()); + } + + + public function testNegativeValue(): void + { + $value = new TestFloatValue(-123.456); + + \Tester\Assert::same(-123.456, $value->value()); + } + + + public function testIntegerAsFloat(): void + { + $value = new TestFloatValue(42.0); + + \Tester\Assert::same(42.0, $value->value()); + \Tester\Assert::type('float', $value->value()); + } + + + public function testSmallDecimal(): void + { + $value = new TestFloatValue(0.0000001); + + \Tester\Assert::same(0.0000001, $value->value()); + } + + + public function testLargeValue(): void + { + $value = new TestFloatValue(1.7976931348623157E+308); + + \Tester\Assert::same(1.7976931348623157E+308, $value->value()); + } + + + public function testImplementsInterface(): void + { + $value = new TestFloatValue(1.0); + + \Tester\Assert::type(\Spameri\ElasticQuery\Value\ValueInterface::class, $value); + } + + + public function testReturnType(): void + { + $value = new TestFloatValue(3.14); + + \Tester\Assert::type('float', $value->value()); + } + + + public function testScientificNotation(): void + { + $value = new TestFloatValue(1.5e10); + + \Tester\Assert::same(1.5e10, $value->value()); + } + +} + +(new AbstractFloatValue())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Value/AbstractNumberValue.phpt b/tests/SpameriTests/ElasticQuery/Value/AbstractNumberValue.phpt new file mode 100644 index 0000000..34b8147 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Value/AbstractNumberValue.phpt @@ -0,0 +1,77 @@ +value()); + } + + + public function testZeroValue(): void + { + $value = new TestNumberValue(0); + + \Tester\Assert::same(0, $value->value()); + } + + + public function testNegativeValue(): void + { + $value = new TestNumberValue(-123); + + \Tester\Assert::same(-123, $value->value()); + } + + + public function testLargePositiveValue(): void + { + $value = new TestNumberValue(\PHP_INT_MAX); + + \Tester\Assert::same(\PHP_INT_MAX, $value->value()); + } + + + public function testLargeNegativeValue(): void + { + $value = new TestNumberValue(\PHP_INT_MIN); + + \Tester\Assert::same(\PHP_INT_MIN, $value->value()); + } + + + public function testImplementsInterface(): void + { + $value = new TestNumberValue(1); + + \Tester\Assert::type(\Spameri\ElasticQuery\Value\ValueInterface::class, $value); + } + + + public function testReturnType(): void + { + $value = new TestNumberValue(42); + + \Tester\Assert::type('int', $value->value()); + } + +} + +(new AbstractNumberValue())->run(); diff --git a/tests/SpameriTests/ElasticQuery/Value/AbstractStringValue.phpt b/tests/SpameriTests/ElasticQuery/Value/AbstractStringValue.phpt new file mode 100644 index 0000000..0f2efb6 --- /dev/null +++ b/tests/SpameriTests/ElasticQuery/Value/AbstractStringValue.phpt @@ -0,0 +1,88 @@ +value()); + } + + + public function testEmptyValue(): void + { + $value = new TestStringValue(''); + + \Tester\Assert::same('', $value->value()); + } + + + public function testNumericString(): void + { + $value = new TestStringValue('12345'); + + \Tester\Assert::same('12345', $value->value()); + \Tester\Assert::type('string', $value->value()); + } + + + public function testUnicodeValue(): void + { + $value = new TestStringValue('česká hodnota'); + + \Tester\Assert::same('česká hodnota', $value->value()); + } + + + public function testSpecialCharacters(): void + { + $value = new TestStringValue('value with "quotes" and \'apostrophes\''); + + \Tester\Assert::same('value with "quotes" and \'apostrophes\'', $value->value()); + } + + + public function testNewlinesAndTabs(): void + { + $value = new TestStringValue("line1\nline2\ttabbed"); + + \Tester\Assert::same("line1\nline2\ttabbed", $value->value()); + } + + + public function testImplementsInterface(): void + { + $value = new TestStringValue('test'); + + \Tester\Assert::type(\Spameri\ElasticQuery\Value\ValueInterface::class, $value); + } + + + public function testLongString(): void + { + $longString = \str_repeat('a', 10000); + $value = new TestStringValue($longString); + + \Tester\Assert::same($longString, $value->value()); + \Tester\Assert::same(10000, \strlen($value->value())); + } + +} + +(new AbstractStringValue())->run(); diff --git a/tests/SpameriTests/ElasticQuery/VersionCheck.php b/tests/SpameriTests/ElasticQuery/VersionCheck.php index 6bb1f38..21c0b8e 100644 --- a/tests/SpameriTests/ElasticQuery/VersionCheck.php +++ b/tests/SpameriTests/ElasticQuery/VersionCheck.php @@ -1,28 +1,27 @@ -map( \json_decode( - (string) \file_get_contents('http://127.0.0.1:9200'), - TRUE - ) + (string) \file_get_contents(ElasticsearchTestCase::getBaseUrl()), + true, + ), ); return $resultObject; diff --git a/tests/SpameriTests/bootstrap.php b/tests/SpameriTests/bootstrap.php index 9e1e3a0..4ce5165 100644 --- a/tests/SpameriTests/bootstrap.php +++ b/tests/SpameriTests/bootstrap.php @@ -1,4 +1,6 @@ -