From 88d06e13bd0a38498096e70ae03eccf4579d72a5 Mon Sep 17 00:00:00 2001 From: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:29:13 -0400 Subject: [PATCH] Add full-text query documentation (#5428) * Refactor full-text query documentation Signed-off-by: Fanit Kolchina * Add examples and parameter descriptions Signed-off-by: Fanit Kolchina * Add multi-match query Signed-off-by: Fanit Kolchina * Add query string field format Signed-off-by: Fanit Kolchina * Query string examples Signed-off-by: Fanit Kolchina * Add regular expressions and fuzziness Signed-off-by: Fanit Kolchina * Add wildcard and regex warning Signed-off-by: Fanit Kolchina * Added more query string format Signed-off-by: Fanit Kolchina * Added multi-field sections Signed-off-by: Fanit Kolchina * Rewrite minimum should match section Signed-off-by: Fanit Kolchina * Added allow expensive queries section Signed-off-by: Fanit Kolchina * Add simple query string query Signed-off-by: Fanit Kolchina * Small rewrites Signed-off-by: Fanit Kolchina * Add intervals query Signed-off-by: Fanit Kolchina * Include discover in query string syntax Signed-off-by: Fanit Kolchina * Link and index page fix Signed-off-by: Fanit Kolchina * Apply suggestions from code review Co-authored-by: Melissa Vagi Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> * Implemented editorial comments Signed-off-by: Fanit Kolchina --------- Signed-off-by: Fanit Kolchina Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> Co-authored-by: Melissa Vagi --- .../supported-field-types/flat-object.md | 6 +- _query-dsl/compound/bool.md | 2 +- _query-dsl/compound/boosting.md | 2 +- _query-dsl/compound/constant-score.md | 4 +- _query-dsl/compound/disjunction-max.md | 6 +- _query-dsl/compound/function-score.md | 2 +- _query-dsl/compound/index.md | 18 +- _query-dsl/full-text/index.md | 469 +-------- _query-dsl/full-text/intervals.md | 399 ++++++++ _query-dsl/full-text/match-bool-prefix.md | 230 +++++ _query-dsl/full-text/match-phrase-prefix.md | 148 +++ _query-dsl/full-text/match-phrase.md | 274 +++++ _query-dsl/full-text/match.md | 466 +++++++++ _query-dsl/full-text/multi-match.md | 932 ++++++++++++++++++ _query-dsl/full-text/query-string.md | 640 +++++++++++- _query-dsl/full-text/simple-query-string.md | 369 +++++++ _query-dsl/geo-and-xy/geo-bounding-box.md | 14 +- _query-dsl/geo-and-xy/xy.md | 10 +- _query-dsl/index.md | 2 +- _query-dsl/match-all.md | 31 + _query-dsl/term/index.md | 1 + .../searching-data/autocomplete.md | 4 +- _search-plugins/sql/full-text.md | 26 +- .../snapshots/sm-api.md | 2 +- images/discover-lucene-syntax.png | Bin 0 -> 82409 bytes 25 files changed, 3529 insertions(+), 528 deletions(-) create mode 100644 _query-dsl/full-text/intervals.md create mode 100644 _query-dsl/full-text/match-bool-prefix.md create mode 100644 _query-dsl/full-text/match-phrase-prefix.md create mode 100644 _query-dsl/full-text/match-phrase.md create mode 100644 _query-dsl/full-text/match.md create mode 100644 _query-dsl/full-text/multi-match.md create mode 100644 _query-dsl/full-text/simple-query-string.md create mode 100644 _query-dsl/match-all.md create mode 100644 images/discover-lucene-syntax.png diff --git a/_field-types/supported-field-types/flat-object.md b/_field-types/supported-field-types/flat-object.md index adde1f3d..933c385a 100644 --- a/_field-types/supported-field-types/flat-object.md +++ b/_field-types/supported-field-types/flat-object.md @@ -50,10 +50,10 @@ The flat object field type supports the following queries: - [Terms set]({{site.url}}{{site.baseurl}}/query-dsl/term/terms-set/) - [Prefix]({{site.url}}{{site.baseurl}}/query-dsl/term/prefix/) - [Range]({{site.url}}{{site.baseurl}}/query-dsl/term/range/) -- [Match]({{site.url}}{{site.baseurl}}/query-dsl/full-text/#match) -- [Multi-match]({{site.url}}{{site.baseurl}}/query-dsl/full-text/#multi-match) +- [Match]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match/) +- [Multi-match]({{site.url}}{{site.baseurl}}/query-dsl/full-text/multi-match/) - [Query string]({{site.url}}{{site.baseurl}}/query-dsl/full-text/query-string/) -- [Simple query string]({{site.url}}{{site.baseurl}}/query-dsl/full-text/#simple-query-string) +- [Simple query string]({{site.url}}{{site.baseurl}}/query-dsl/full-text/simple-query-string/) - [Exists]({{site.url}}{{site.baseurl}}/query-dsl/term/exists/) ## Limitations diff --git a/_query-dsl/compound/bool.md b/_query-dsl/compound/bool.md index d5d17944..12caea2f 100644 --- a/_query-dsl/compound/bool.md +++ b/_query-dsl/compound/bool.md @@ -10,7 +10,7 @@ redirect_from: - /query-dsl/query-dsl/compound/bool/ --- -# Boolean queries +# Boolean query A Boolean (`bool`) query can combine several query clauses into one advanced query. The clauses are combined with Boolean logic to find matching documents returned in the results. diff --git a/_query-dsl/compound/boosting.md b/_query-dsl/compound/boosting.md index d283a054..7aa9c6c0 100644 --- a/_query-dsl/compound/boosting.md +++ b/_query-dsl/compound/boosting.md @@ -8,7 +8,7 @@ redirect_from: - /query-dsl/query-dsl/compound/boosting/ --- -# Boosting queries +# Boosting query If you're searching for the word "pitcher", your results may relate to either baseball players or containers for liquids. For a search in the context of baseball, you might want to completely exclude results that contain the words "glass" or "water" by using the `must_not` clause. However, if you want to keep those results but downgrade them in relevance, you can do so with `boosting` queries. diff --git a/_query-dsl/compound/constant-score.md b/_query-dsl/compound/constant-score.md index 15279ef5..ed12e33e 100644 --- a/_query-dsl/compound/constant-score.md +++ b/_query-dsl/compound/constant-score.md @@ -8,9 +8,9 @@ redirect_from: - /query-dsl/query-dsl/compound/constant-score/ --- -# Constant score queries +# Constant score query -If you need to return documents that contain a certain word regardless of how many times the word appears, you can use a `constant_ score` query. A `constant_score` query wraps a filter query and assigns all documents in the results a relevance score equal to the value of the `boost` parameter. Thus, all returned documents have an equal relevance score, and term frequency/inverse document frequency (TF/IDF) is not considered. Filter queries do not calculate relevance scores. Further, OpenSearch caches frequently used filter queries to improve performance. +If you need to return documents that contain a certain word regardless of how many times the word appears, you can use a `constant_score` query. A `constant_score` query wraps a filter query and assigns all documents in the results a relevance score equal to the value of the `boost` parameter. Thus, all returned documents have an equal relevance score, and term frequency/inverse document frequency (TF/IDF) is not considered. Filter queries do not calculate relevance scores. Further, OpenSearch caches frequently used filter queries to improve performance. ## Example diff --git a/_query-dsl/compound/disjunction-max.md b/_query-dsl/compound/disjunction-max.md index b6397c17..8dd9e41d 100644 --- a/_query-dsl/compound/disjunction-max.md +++ b/_query-dsl/compound/disjunction-max.md @@ -8,7 +8,7 @@ redirect_from: - /query-dsl/query-dsl/compound/disjunction-max/ --- -# Disjunction max queries +# Disjunction max query A disjunction max (`dis_max`) query returns any document that matches one or more query clauses. For documents that match multiple query clauses, the relevance score is set to the highest relevance score from all matching query clauses. @@ -25,6 +25,7 @@ PUT testindex1/_doc/1 "description": "Top 10 sonnets of England's national poet and the Bard of Avon" } ``` +{% include copy-curl.html %} ```json PUT testindex1/_doc/2 @@ -33,8 +34,9 @@ PUT testindex1/_doc/2 "body": "The poems written by various 16-th century poets" } ``` +{% include copy-curl.html %} -Use a `dis_max` query to search for documents that contain the words "Shakespeare works": +Use a `dis_max` query to search for documents that contain the words "Shakespeare poems": ```json GET testindex1/_search diff --git a/_query-dsl/compound/function-score.md b/_query-dsl/compound/function-score.md index f9f73003..8180058a 100644 --- a/_query-dsl/compound/function-score.md +++ b/_query-dsl/compound/function-score.md @@ -9,7 +9,7 @@ redirect_from: - /query-dsl/query-dsl/compound/function-score/ --- -# Function score queries +# Function score query Use a `function_score` query if you need to alter the relevance scores of documents returned in the results. A `function_score` query defines a query and one or more functions that can be applied to all results or subsets of the results to recalculate their relevance scores. diff --git a/_query-dsl/compound/index.md b/_query-dsl/compound/index.md index 57bb7cf9..d1ad1e26 100644 --- a/_query-dsl/compound/index.md +++ b/_query-dsl/compound/index.md @@ -2,9 +2,10 @@ layout: default title: Compound queries has_children: true +has_toc: false nav_order: 40 redirect_from: - - /opensearch/query-dsl/compound/index/ + - /query-dsl/compound/index/ - /query-dsl/query-dsl/compound/ --- @@ -12,10 +13,13 @@ redirect_from: Compound queries serve as wrappers for multiple leaf or compound clauses either to combine their results or to modify their behavior. -OpenSearch supports the following compound query types: +The following table lists all compound query types. -- **Boolean**: Combines multiple query clauses with Boolean logic. To learn more, see [Boolean queries]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/compound/bool/). -- **Constant score**: Wraps a query or a filter and assigns a constant score to all matching documents. This score is equal to the `boost` value. -- **Disjunction max**: Returns documents that match one or more query clauses. If a document matches multiple query clauses, it is assigned a higher relevance score. The relevance score is calculated using the highest score from any matching clause and, optionally, the scores from the other matching clauses multiplied by the tiebreaker value. -- **Function score**: Recalculates the relevance score of documents that are returned by a query using a function that you define. -- **Boosting**: Changes the relevance score of documents without removing them from the search results. Returns documents that match a `positive` query, but downgrades the relevance of documents in the results that match a `negative` query. \ No newline at end of file +Query type | Description +:--- | :--- +[`bool`]({{site.url}}{{site.baseurl}}/query-dsl/compound/bool/) (Boolean)| Combines multiple query clauses with Boolean logic. +[`boosting`]({{site.url}}{{site.baseurl}}/query-dsl/compound/boosting/) | Changes the relevance score of documents without removing them from the search results. Returns documents that match a `positive` query, but downgrades the relevance of documents in the results that match a `negative` query. +[`constant_score`]({{site.url}}{{site.baseurl}}/query-dsl/compound/constant-score/) | Wraps a query or a filter and assigns a constant score to all matching documents. This score is equal to the `boost` value. +[`dis_max`]({{site.url}}{{site.baseurl}}/query-dsl/compound/disjunction-max/) (disjunction max) | Returns documents that match one or more query clauses. If a document matches multiple query clauses, it is assigned a higher relevance score. The relevance score is calculated using the highest score from any matching clause and, optionally, the scores from the other matching clauses multiplied by the tiebreaker value. +[`function_score`]({{site.url}}{{site.baseurl}}/query-dsl/compound/function-score/) | Recalculates the relevance score of documents that are returned by a query using a function that you define. +[`hybrid`]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) | Combines relevance scores from multiple queries into one score for a given document. diff --git a/_query-dsl/full-text/index.md b/_query-dsl/full-text/index.md index a779f4ef..e1f5ab5f 100644 --- a/_query-dsl/full-text/index.md +++ b/_query-dsl/full-text/index.md @@ -2,6 +2,7 @@ layout: default title: Full-text queries has_children: true +has_toc: false nav_order: 30 redirect_from: - /opensearch/query-dsl/full-text/ @@ -20,459 +21,15 @@ To learn more about search query classes, see [Lucene query JavaDocs](https://lu The full-text query types shown in this section use the standard analyzer, which analyzes text automatically when the query is submitted. - ---- - -#### Table of contents - -1. TOC -{:toc} - ---- - -Common terms queries and the optional query field `cutoff_frequency` are now deprecated. -{: .note } - -## Query types - -OpenSearch Query DSL provides multiple query types that you can use in your searches. - -### Match - -Use the `match` query for full-text search of a specific document field. The `match` query analyzes the provided search string and returns documents that match any of the string's terms. - -You can use Boolean query operators to combine searches. - - - -The following example shows a basic `match` search for the `title` field set to the value `wind`: - -```json -GET _search -{ - "query": { - "match": { - "title": "wind" - } - } -} -``` - -For an example that uses [curl](https://curl.haxx.se/), try: - -```bash -curl --insecure -XGET -u 'admin:admin' https://://_search \ - -H "content-type: application/json" \ - -d '{ - "query": { - "match": { - "title": "wind" - } - } - }' -``` - -The query accepts the following options. For descriptions of each, see [Advanced filter options](#advanced-filter-options). - -```json -GET _search -{ - "query": { - "match": { - "title": { - "query": "wind", - "fuzziness": "AUTO", - "fuzzy_transpositions": true, - "operator": "or", - "minimum_should_match": 1, - "analyzer": "standard", - "zero_terms_query": "none", - "lenient": false, - "prefix_length": 0, - "max_expansions": 50, - "boost": 1 - } - } - } -} -``` - -### Multi-match - -You can use the `multi_match` query type to search multiple fields. Multi-match operation functions similarly to the [match](#match) operation. - -The `^` lets you "boost" certain fields. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. In the following example, a match for "wind" in the title field influences `_score` four times as much as a match in the plot field. The result is that films like *The Wind Rises* and *Gone with the Wind* are near the top of the search results, and films like *Twister* and *Sharknado*, which presumably have "wind" in their plot summaries, are near the bottom. - -```json -GET _search -{ - "query": { - "multi_match": { - "query": "wind", - "fields": ["title^4", "plot"] - } - } -} -``` - -The query accepts the following options. For descriptions of each, see [Advanced filter options](#advanced-filter-options). - -```json -GET _search -{ - "query": { - "multi_match": { - "query": "wind", - "fields": ["title^4", "description"], - "type": "most_fields", - "operator": "and", - "minimum_should_match": 3, - "tie_breaker": 0.0, - "analyzer": "standard", - "boost": 1, - "fuzziness": "AUTO", - "fuzzy_transpositions": true, - "lenient": false, - "prefix_length": 0, - "max_expansions": 50, - "auto_generate_synonyms_phrase_query": true, - "zero_terms_query": "none" - } - } -} -``` - -### Match Boolean prefix - -The `match_bool_prefix` query analyzes the provided search string and creates a `bool` query from the string's terms. It uses every term except the last term as a whole word for matching. The last term is used as a prefix. The `match_bool_prefix` query returns documents that contain either the whole-word terms or terms that start with the prefix term, in any order. - -```json -GET _search -{ - "query": { - "match_bool_prefix": { - "title": "rises wi" - } - } -} -``` - -The query accepts the following options. For descriptions of each, see [Advanced filter options](#advanced-filter-options). - -```json -GET _search -{ - "query": { - "match_bool_prefix": { - "title": { - "query": "rises wi", - "fuzziness": "AUTO", - "fuzzy_transpositions": true, - "max_expansions": 50, - "prefix_length": 0, - "operator": "or", - "minimum_should_match": 2, - "analyzer": "standard" - } - } - } -} -``` - -For more reference information about prefix queries, see the [Lucene documentation](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PrefixQuery.html). - -### Match phrase - -Use the `match_phrase` query to match documents that contain an exact phrase in a specified order. You can add flexibility to phrase matching by providing the `slop` parameter. - -Creates a [phrase query](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html) that matches a sequence of terms. - -```json -GET _search -{ - "query": { - "match_phrase": { - "title": "the wind rises" - } - } -} -``` - -The query accepts the following options. For descriptions of each, see [Advanced filter options](#advanced-filter-options). - -```json -GET _search -{ - "query": { - "match_phrase": { - "title": { - "query": "wind rises the", - "slop": 3, - "analyzer": "standard", - "zero_terms_query": "none" - } - } - } -} -``` - -### Match phrase prefix - -Use the `match_phrase_prefix` query to specify a phrase to match in order. The documents that contain the phrase you specify will be returned. The last partial term in the phrase is interpreted as a prefix, so any documents that contain phrases that begin with the phrase and prefix of the last term will be returned. - -Similar to [match phrase](#match-phrase), but creates a [prefix query](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PrefixQuery.html) out of the last term in the query string. - -```json -GET _search -{ - "query": { - "match_phrase_prefix": { - "title": "the wind ri" - } - } -} -``` - -The query accepts the following options. For descriptions of each, see [Advanced filter options](#advanced-filter-options). - -```json -GET _search -{ - "query": { - "match_phrase_prefix": { - "title": { - "query": "the wind ri", - "analyzer": "standard", - "max_expansions": 50, - "slop": 3 - } - } - } -} -``` - -### Query string - -The query string query splits text based on operators and analyzes each individually. - -If you search using the HTTP request parameters (i.e. `_search?q=wind`), OpenSearch creates a query string query. -{: .note } - -```json -GET _search -{ - "query": { - "query_string": { - "query": "the wind AND (rises OR rising)" - } - } -} -``` - -The query accepts the following options. For descriptions of each, see [Advanced filter options](#advanced-filter-options). - -```json -GET _search -{ - "query": { - "query_string": { - "query": "the wind AND (rises OR rising)", - "default_field": "title", - "type": "best_fields", - "fuzziness": "AUTO", - "fuzzy_transpositions": true, - "fuzzy_max_expansions": 50, - "fuzzy_prefix_length": 0, - "minimum_should_match": 1, - "default_operator": "or", - "analyzer": "standard", - "lenient": false, - "boost": 1, - "allow_leading_wildcard": true, - "enable_position_increments": true, - "phrase_slop": 3, - "max_determinized_states": 10000, - "time_zone": "-08:00", - "quote_field_suffix": "", - "quote_analyzer": "standard", - "analyze_wildcard": false, - "auto_generate_synonyms_phrase_query": true - } - } -} -``` - -### Simple query string - -Use the `simple_query_string` type to specify directly in the query string multiple arguments delineated by regular expressions. Searches with this type will discard any invalid portions of the string. - -```json -GET _search -{ - "query": { - "simple_query_string": { - "query": "\"rises wind the\"~4 | *ising~2", - "fields": ["title"] - } - } -} -``` - -Special character | Behavior -:--- | :--- -`+` | Acts as the `and` operator. -`|` | Acts as the `or` operator. -`*` | Acts as a wildcard. -`""` | Wraps several terms into a phrase. -`()` | Wraps a clause for precedence. -`~n` | When used after a term (for example, `wnid~3`), sets `fuzziness`. When used after a phrase, sets `slop`. [Advanced filter options](#advanced-filter-options). -`-` | Negates the term. - -The query accepts the following options. For descriptions of each, see [Advanced filter options](#advanced-filter-options). - -```json -GET _search -{ - "query": { - "simple_query_string": { - "query": "\"rises wind the\"~4 | *ising~2", - "fields": ["title"], - "flags": "ALL", - "fuzzy_transpositions": true, - "fuzzy_max_expansions": 50, - "fuzzy_prefix_length": 0, - "minimum_should_match": 1, - "default_operator": "or", - "analyzer": "standard", - "lenient": false, - "quote_field_suffix": "", - "analyze_wildcard": false, - "auto_generate_synonyms_phrase_query": true - } - } -} -``` - -### Match all - -The `match_all` query type will return all documents. This type can be useful in testing large document sets if you need to return the entire set. - -```json -GET _search -{ - "query": { - "match_all": {} - } -} -``` - - -## Advanced filter options - -You can filter your query results by using some of the optional query fields, such as wildcards, fuzzy query fields, or synonyms. You can also use analyzers as optional query fields. - -### Wildcard options - -Option | Valid values | Description -:--- | :--- | :--- -`allow_leading_wildcard` | Boolean | Whether `*` and `?` are allowed as the first character of a search term. The default is `true`. -`analyze_wildcard` | Boolean | Whether OpenSearch should attempt to analyze wildcard terms. Some analyzers do a poor job at this task, so the default is false. - -### Fuzzy query options - -Option | Valid values | Description -:--- | :--- | :--- -`fuzziness` | `AUTO`, `0`, or a positive integer | The number of character edits (insert, delete, substitute) that it takes to change one word to another when determining whether a term matched a value. For example, the distance between `wined` and `wind` is 1. The default, `AUTO`, chooses a value based on the length of each term and is a good choice for most use cases. -`fuzzy_transpositions` | Boolean | Setting `fuzzy_transpositions` to true (default) adds swaps of adjacent characters to the insert, delete, and substitute operations of the `fuzziness` option. For example, the distance between `wind` and `wnid` is 1 if `fuzzy_transpositions` is true (swap "n" and "i") and 2 if it is false (delete "n", insert "n"). If `fuzzy_transpositions` is false, `rewind` and `wnid` have the same distance (2) from `wind`, despite the more human-centric opinion that `wnid` is an obvious typo. The default is a good choice for most use cases. -`fuzzy_max_expansions` | Positive integer | Fuzzy queries "expand to" a number of matching terms that are within the distance specified in `fuzziness`. Then OpenSearch tries to match those terms against its indexes. - -### Synonyms in a multiple terms search - -You can also use synonyms with the `terms` query type to search for multiple terms. Use the `auto_generate_synonyms_phrase_query` Boolean field. By default it is set to `true`. It automatically generates phrase queries for multiple term synonyms. For example, if you have the synonym `"ba, batting average"` and search for "ba," OpenSearch searches for `ba OR "batting average"` when the option is `true` or `ba OR (batting AND average)` when the option is `false`. - -To learn more about the multiple terms query type, see [Terms]({{site.url}}{{site.baseurl}}/query-dsl/term/terms/). For more reference information about phrase queries, see the [Lucene documentation](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html). - -### Other advanced options - -You can also use the following optional query fields to filter your query results. - -Option | Valid values | Description -:--- | :--- | :--- -`boost` | Floating-point | Boosts the clause by the given multiplier. Useful for weighing clauses in compound queries. The default is 1.0. -`enable_position_increments` | Boolean | When true, result queries are aware of position increments. This setting is useful when the removal of stop words leaves an unwanted "gap" between terms. The default is true. -`fields` | String array | The list of fields to search (e.g. `"fields": ["title^4", "description"]`). If unspecified, defaults to the `index.query.default_field` setting, which defaults to `["*"]`. -`flags` | String | A `|`-delimited string of [flags](#simple-query-string) to enable (e.g., `AND|OR|NOT`). The default is `ALL`. You can explicitly set the value for `default_field`. For example, to return all titles, set it to `"default_field": "title"`. -`lenient` | Boolean | Setting `lenient` to true lets you ignore data type mismatches between the query and the document field. For example, a query string of "8.2" could match a field of type `float`. The default is false. -`low_freq_operator` | `and, or` | The operator for low-frequency terms. The default is `or`. See also `operator` in this table. -`max_determinized_states` | Positive integer | The maximum number of "[states](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/util/automaton/Operations.html#DEFAULT_MAX_DETERMINIZED_STATES)" (a measure of complexity) that Lucene can create for query strings that contain regular expressions (e.g. `"query": "/wind.+?/"`). Larger numbers allow for queries that use more memory. The default is 10,000. -`max_expansions` | Positive integer | `max_expansions` specifies the maximum number of terms to which the query can expand. The default is 50. -`minimum_should_match` | Positive or negative integer, positive or negative percentage, combination | If the query string contains multiple search terms and you used the `or` operator, the number of terms that need to match for the document to be considered a match. For example, if `minimum_should_match` is 2, "wind often rising" does not match "The Wind Rises." If `minimum_should_match` is 1, it matches. -`operator` | `or, and` | If the query string contains multiple search terms, whether all terms need to match (`and`) or only one term needs to match (`or`) for a document to be considered a match. -`phrase_slop` | `0` (default) or a positive integer | See `slop`. -`prefix_length` | `0` (default) or a positive integer | The number of leading characters that are not considered in fuzziness. -`quote_field_suffix` | String | This option lets you search different fields depending on whether terms are wrapped in quotes. For example, if `quote_field_suffix` is `".exact"` and you search for `"lightly"` (in quotes) in the `title` field, OpenSearch searches the `title.exact` field. This second field might use a different type (e.g. `keyword` rather than `text`) or a different analyzer. The default is null. -`rewrite` | `constant_score, scoring_boolean, constant_score_boolean, top_terms_N, top_terms_boost_N, top_terms_blended_freqs_N` | Determines how OpenSearch rewrites and scores multi-term queries. The default is `constant_score`. -`slop` | `0` (default) or a positive integer | Controls the degree to which words in a query can be misordered and still be considered a match. From the [Lucene documentation](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html#getSlop--): "The number of other words permitted between words in query phrase. For example, to switch the order of two words requires two moves (the first move places the words atop one another), so to permit re-orderings of phrases, the slop must be at least two. A value of zero requires an exact match." -`tie_breaker` | `0.0` (default) to `1.0` | Changes the way OpenSearch scores searches. For example, a `type` of `best_fields` typically uses the highest score from any one field. If you specify a `tie_breaker` value between 0.0 and 1.0, the score changes to highest score + `tie_breaker` * score for all other matching fields. If you specify a value of 1.0, OpenSearch adds together the scores for all matching fields (effectively defeating the purpose of `best_fields`). -`time_zone` | UTC offset hours | Specifies the number of hours to offset the desired time zone from `UTC`. You need to indicate the time zone offset number if the query string contains a date range. For example, set `time_zone": "-08:00"` for a query with a date range such as `"query": "wind rises release_date[2012-01-01 TO 2014-01-01]"`). The default time zone format used to specify number of offset hours is `UTC`. -`type` | `best_fields, most_fields, cross_fields, phrase, phrase_prefix` | Determines how OpenSearch executes the query and scores the results. The default is `best_fields`. -`zero_terms_query` | `none, all` | If the analyzer removes all terms from a query string, whether to match no documents (default) or all documents. For example, the `stop` analyzer removes all terms from the string "an but this." - - +The following table lists all full-text query types. + +Query type | Description +:--- | :--- +[`intervals`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/intervals/) | Allows fine-grained control of the matching terms' proximity and order. +[`match`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match/) | The default full-text query, which can be used for fuzzy matching and phrase or proximity searches. +[`match_bool_prefix`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-bool-prefix/) | Creates a [Boolean query]({{site.url}}{{site.baseurl}}/query-dsl/compound/bool/) that matches all terms in any position, treating the last term as a prefix. +[`match_phrase`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase/) | Similar to the `match` query but matches a whole phrase up to a configurable slop. +[`match_phrase_prefix`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase-prefix/) | Similar to the `match_phrase` query but matches terms as a whole phrase, treating the last term as a prefix. +[`multi_match`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/multi-match/) | Similar to the `match` query but is used on multiple fields. +[`query_string`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/query-string/) | Uses a strict syntax to specify Boolean conditions and multi-field search within a single query string. +[`simple_query_string`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/simple-query-string/) | A simpler, less strict version of `query_string` query. diff --git a/_query-dsl/full-text/intervals.md b/_query-dsl/full-text/intervals.md new file mode 100644 index 00000000..dd401933 --- /dev/null +++ b/_query-dsl/full-text/intervals.md @@ -0,0 +1,399 @@ +--- +layout: default +title: Intervals +nav_order: 80 +parent: Full-text queries +grand_parent: Query DSL +--- + +# Intervals query + +The intervals query matches documents based on the proximity and order of matching terms. It applies a set of _matching rules_ to terms contained in the specified field. The query generates sequences of minimal intervals that span terms in the text. You can combine the intervals and filter them by parent sources. + +Consider an index containing the following documents: + +```json +PUT testindex/_doc/1 +{ + "title": "key-value pairs are efficiently stored in a hash table" +} +``` +{% include copy-curl.html %} + +```json +PUT /testindex/_doc/2 +{ + "title": "store key-value pairs in a hash map" +} +``` +{% include copy-curl.html %} + +For example, the following query searches for documents containing the phrase `key-value pairs` (with no gap separating the terms) followed by either `hash table` or `hash map`: + +```json +GET /testindex/_search +{ + "query": { + "intervals": { + "title": { + "all_of": { + "ordered": true, + "intervals": [ + { + "match": { + "query": "key-value pairs", + "max_gaps": 0, + "ordered": true + } + }, + { + "any_of": { + "intervals": [ + { + "match": { + "query": "hash table" + } + }, + { + "match": { + "query": "hash map" + } + } + ] + } + } + ] + } + } + } + } +} +``` +{% include copy-curl.html %} + +The query returns both documents: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 1011, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": 0.25, + "hits": [ + { + "_index": "testindex", + "_id": "2", + "_score": 0.25, + "_source": { + "title": "store key-value pairs in a hash map" + } + }, + { + "_index": "testindex", + "_id": "1", + "_score": 0.14285713, + "_source": { + "title": "key-value pairs are efficiently stored in a hash table" + } + } + ] + } +} +``` +
+ +## Parameters + +The query accepts the name of the field (``) as a top-level parameter: + +```json +GET _search +{ + "query": { + "intervals": { + "": { + ... + } + } + } +} +``` +{% include copy-curl.html %} + +The `` accepts the following rule objects that are used to match documents based on terms, order, and proximity. + +Rule | Description +:--- | :--- +[`match`](#the-match-rule) | Matches analyzed text. +[`prefix`](#the-prefix-rule) | Matches terms that start with a specified set of characters. +[`wildcard`](#the-wildcard-rule) | Matches terms using a wildcard pattern. +[`fuzzy`](#the-fuzzy-rule) | Matches terms that are similar to the provided term within a specified edit distance. +[`all_of`](#the-all_of-rule) | Combines multiple rules using a conjunction (`AND`). +[`any_of`](#the-any_of-rule) | Combines multiple rules using a disjunction (`OR`). + +## The `match` rule + +The `match` rule matches analyzed text. The following table lists all parameters the `match` rule supports. + +Parameter | Required/Optional | Data type | Description +:--- | :--- | :--- | :--- +`query` | Required | String | Text for which to search. +`analyzer` | Optional | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to analyze the `query` text. Default is the analyzer specified for the ``. +[`filter`](#the-filter-rule) | Optional | Interval filter rule object | A rule used to filter returned intervals. +`max_gaps` | Optional | Integer | The maximum allowed number of positions between the matching terms. Terms further apart than `max_gaps` are not considered matches. If `max_gaps` is not specified or is set to `-1`, terms are considered matches regardless of their position. If `max_gaps` is set to `0`, matching terms must appear next to each other. Default is `-1`. +`ordered` | Optional | Boolean | Specifies whether matching terms must appear in their specified order. Default is `false`. +`use_field` | Optional | String | Specifies to search this field instead of the top-level . Terms are analyzed using the search analyzer specified for this field. By specifying `use_field`, you can search across multiple fields as if they were all the same field. For example, if you index the same text into stemmed and unstemmed fields, you can search for stemmed tokens that are near unstemmed ones. + +## The `prefix` rule + +The `prefix` rule matches terms that start with a specified set of characters (prefix). The prefix can expand to match at most 128 terms. If the prefix matches more than 128 terms, an error is returned. The following table lists all parameters the `prefix` rule supports. + +Parameter | Required/Optional | Data type | Description +:--- | :--- | :--- | :--- +`prefix` | Required | String | The prefix used to match terms. +`analyzer` | Optional | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to normalize the `prefix`. Default is the analyzer specified for the ``. +`use_field` | Optional | String | Specifies to search this field instead of the top-level . The `prefix` is normalized using the search analyzer specified for this field, unless you specify an `analyzer`. + +## The `wildcard` rule + +The `wildcard` rule matches terms using a wildcard pattern. The wildcard pattern can expand to match at most 128 terms. If the pattern matches more than 128 terms, an error is returned. The following table lists all parameters the `wildcard` rule supports. + +Parameter | Required/Optional | Data type | Description +:--- | :--- | :--- | :--- +`pattern` | Required | String | The wildcard pattern used to match terms. Specify `?` to match any single character or `*` to match zero or more characters. +`analyzer` | Optional | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to normalize the `pattern`. Default is the analyzer specified for the ``. +`use_field` | Optional | String | Specifies to search this field instead of the top-level . The `prefix` is normalized using the search analyzer specified for this field, unless you specify an `analyzer`. + +Specifying patterns that start with `*` or `?` can hinder search performance because it increases the number of iterations required to match terms. +{: .important} + +## The `fuzzy` rule + +The `fuzzy` rule matches terms that are similar to the provided term within a specified edit distance. The fuzzy pattern can expand to match at most 128 terms. If the pattern matches more than 128 terms, an error is returned. The following table lists all parameters the `fuzzy` rule supports. + +Parameter | Required/Optional | Data type | Description +:--- | :--- | :--- | :--- +`term` | Required | String | The term to match. +`analyzer` | Optional | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to normalize the `term`. Default is the analyzer specified for the ``. +`fuzziness` | Optional | String | The number of character edits (insert, delete, substitute) that it takes to change one word to another when determining whether a term matched a value. For example, the distance between `wined` and `wind` is 1. Valid values are non-negative integers or `AUTO`. The default, `AUTO`, chooses a value based on the length of each term and is a good choice for most use cases. +`transpositions` | Optional | Boolean | Setting `transpositions` to `true` (default) adds swaps of adjacent characters to the insert, delete, and substitute operations of the `fuzziness` option. For example, the distance between `wind` and `wnid` is 1 if `transpositions` is true (swap "n" and "i") and 2 if it is false (delete "n", insert "n"). If `transpositions` is `false`, `rewind` and `wnid` have the same distance (2) from `wind`, despite the more human-centric opinion that `wnid` is an obvious typo. The default is a good choice for most use cases. +`prefix_length`| Optional | Integer | The number of beginning characters left unchanged for fuzzy matching. Default is 0. +`use_field` | Optional | String | Specifies to search this field instead of the top-level . The `term` is normalized using the search analyzer specified for this field, unless you specify an `analyzer`. + +## The `all_of` rule + +The `all_of` rule combines multiple rules using a conjunction (`AND`). The following table lists all parameters the `all_of` rule supports. + +Parameter | Required/Optional | Data type | Description +:--- | :--- | :--- | :--- +`intervals` | Required | Array of rule objects | An array of rules to combine. A document must match all rules in order to be returned in the results. +[`filter`](#the-filter-rule) | Optional | Interval filter rule object | A rule used to filter returned intervals. +`max_gaps` | Optional | Integer | The maximum allowed number of positions between the matching terms. Terms further apart than `max_gaps` are not considered matches. If `max_gaps` is not specified or is set to `-1`, terms are considered matches regardless of their position. If `max_gaps` is set to `0`, matching terms must appear next to each other. Default is `-1`. +`ordered` | Optional | Boolean | If `true`, intervals generated by the rules should appear in the specified order. Default is `false`. + +## The `any_of` rule + +The `any_of` rule combines multiple rules using a disjunction (`OR`). The following table lists all parameters the `any_of` rule supports. + +Parameter | Required/Optional | Data type | Description +:--- | :--- | :--- | :--- +`intervals` | Required | Array of rule objects | An array of rules to combine. A document must match at least one rule in order to be returned in the results. +[`filter`](#the-filter-rule) | Optional | Interval filter rule object | A rule used to filter returned intervals. + +## The `filter` rule + +The `filter` rule is used to restrict the results. The following table lists all parameters the `filter` rule supports. + +Parameter | Required/Optional | Data type | Description +:--- | :--- | :--- | :--- +`after` | Optional | Query object | A query used to return intervals that follow an interval specified in the filter rule. +`before` | Optional | Query object | A query used to return intervals that are before an interval specified in the filter rule. +`contained_by` | Optional | Query object | A query used to return intervals contained by an interval specified in the filter rule. +`containing` | Optional | Query object | A query used to return intervals that contain an interval specified in the filter rule. +`not_contained_by` | Optional | Query object | A query used to return intervals that are not contained by an interval specified in the filter rule. +`not_containing` | Optional | Query object | A query used to return intervals that do not contain an interval specified in the filter rule. +`not_overlapping` | Optional | Query object | A query used to return intervals that do not overlap with an interval specified in the filter rule. +`overlapping` | Optional | Query object | A query used to return intervals that overlap with an interval specified in the filter rule. +`script` | Optional | Script object | A script used to match documents. This script must return `true` or `false`. + +#### Example: Filters + +The following query searches for documents containing the words `pairs` and `hash` that are within five positions of each other and don't contain the word `efficiently` between them: + +```json +POST /testindex/_search +{ + "query": { + "intervals" : { + "title" : { + "match" : { + "query" : "pairs hash", + "max_gaps" : 5, + "filter" : { + "not_containing" : { + "match" : { + "query" : "efficiently" + } + } + } + } + } + } + } +} +``` +{% include copy-curl.html %} + +The response contains only document 2: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 2, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.25, + "hits": [ + { + "_index": "testindex", + "_id": "2", + "_score": 0.25, + "_source": { + "title": "store key-value pairs in a hash map" + } + } + ] + } +} +``` +
+ +#### Example: Script filters + +Alternatively, you can write your own script filter to include with the `intervals` query using the following variables: + +- `interval.start`: The position (term number) where the interval starts. +- `interval.end`: The position (term number) where the interval ends. +- `interval.gap`: The number of words between the terms. + +For example, the following query searches for the words `map` and `hash` that are next to each other within the specified interval. Terms are numbered starting with 0, so in the text `store key-value pairs in a hash map`, `store` is at position 0, `key`is at position `1`, and so on. The specified interval should start after `a` and end before the end of string: + +```json +POST /testindex/_search +{ + "query": { + "intervals" : { + "title" : { + "match" : { + "query" : "map hash", + "filter" : { + "script" : { + "source" : "interval.start > 5 && interval.end < 8 && interval.gaps == 0" + } + } + } + } + } + } +} +``` +{% include copy-curl.html %} + +The response contains document 2: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 1, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.5, + "hits": [ + { + "_index": "testindex", + "_id": "2", + "_score": 0.5, + "_source": { + "title": "store key-value pairs in a hash map" + } + } + ] + } +} +``` +
+ +## Interval minimization + +To ensure that queries run in linear time, the `intervals` query minimizes the intervals. For example, consider a document containing the text `a b c d c`. You can use the following query to search for `d` that is contained by `a` and `c`: + +```json +POST /testindex/_search +{ + "query": { + "intervals" : { + "my_text" : { + "match" : { + "query" : "d", + "filter" : { + "contained_by" : { + "match" : { + "query" : "a c" + } + } + } + } + } + } + } +} +``` +{% include copy-curl.html %} + +The query returns no results because it matches the first two terms `a c` and finds no `d` between these terms. diff --git a/_query-dsl/full-text/match-bool-prefix.md b/_query-dsl/full-text/match-bool-prefix.md new file mode 100644 index 00000000..8ab0bf50 --- /dev/null +++ b/_query-dsl/full-text/match-bool-prefix.md @@ -0,0 +1,230 @@ +--- +layout: default +title: Match Boolean prefix +parent: Full-text queries +grand_parent: Query DSL +nav_order: 20 +--- + +# Match Boolean prefix query + +The `match_bool_prefix` query analyzes the provided search string and creates a [Boolean query]({{site.url}}{{site.baseurl}}/query-dsl/compound/bool/) from the string's terms. It uses every term except the last term as a whole word for matching. The last term is used as a prefix. The `match_bool_prefix` query returns documents that contain either the whole-word terms or terms that start with the prefix term, in any order. + +The following example shows a basic `match_bool_prefix` query: + +```json +GET _search +{ + "query": { + "match_bool_prefix": { + "title": "the wind" + } + } +} +``` +{% include copy-curl.html %} + +To pass additional parameters, you can use the expanded syntax: + +```json +GET _search +{ + "query": { + "match_bool_prefix": { + "title": { + "query": "the wind", + "analyzer": "stop" + } + } + } +} +``` +{% include copy-curl.html %} + +## Example + +For example, consider an index with the following documents: + +```json +PUT testindex/_doc/1 +{ + "title": "The wind rises" +} +``` +{% include copy-curl.html %} + +```json +PUT testindex/_doc/2 +{ + "title": "Gone with the wind" + +} +``` +{% include copy-curl.html %} + +The following `match_bool_prefix` query searches for the whole word `rises` and the words that start with `wi`, in any order: + +```json +GET testindex/_search +{ + "query": { + "match_bool_prefix": { + "title": "rises wi" + } + } +} +``` +{% include copy-curl.html %} + +The preceding query is equivalent to the following Boolean query: + +```json +GET testindex/_search +{ + "query": { + "bool" : { + "should": [ + { "term": { "title": "rises" }}, + { "prefix": { "title": "wi"}} + ] + } + } +} +``` + +The response contains both documents: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 15, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": 1.73617, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 1.73617, + "_source": { + "title": "The wind rises" + } + }, + { + "_index": "testindex", + "_id": "2", + "_score": 1, + "_source": { + "title": "Gone with the wind" + } + } + ] + } +} +``` + +
+ +## The `match_bool_prefix` and `match_phrase_prefix` queries + +The `match_bool_prefix` query matches terms in any position, while the `match_phrase_prefix` query matches terms as a whole phrase. To illustrate the difference, once again consider the `match_bool_prefix` query from the preceding section: + +```json +GET testindex/_search +{ + "query": { + "match_bool_prefix": { + "title": "rises wi" + } + } +} +``` +{% include copy-curl.html %} + +Both `The wind rises` and `Gone with the wind` match the search terms, so the query returns both documents. + +Now run a `match_phrase_prefix` query on the same index: + +```json +GET testindex/_search +{ + "query": { + "match_phrase_prefix": { + "title": "rises wi" + } + } +} +``` +{% include copy-curl.html %} + +The response returns no documents because none of the documents contain a phrase `rises wi` in the specified order. + +## Analyzer + +By default, when you run a query on a `text` field, the search text is analyzed using the index analyzer associated with the field. You can specify a different search analyzer in the `analyzer` parameter: + +```json +GET testindex/_search +{ + "query": { + "match_bool_prefix": { + "title": { + "query": "rise the wi", + "analyzer": "stop" + } + } + } +} +``` +{% include copy-curl.html %} + +## Parameters + +The query accepts the name of the field (``) as a top-level parameter: + +```json +GET _search +{ + "query": { + "match_bool_prefix": { + "": { + "query": "text to search for", + ... + } + } + } +} +``` +{% include copy-curl.html %} + +The `` accepts the following parameters. All parameters except `query` are optional. + +Parameter | Data type | Description +:--- | :--- | :--- +`query` | String | The text, number, Boolean value, or date to use for search. Required. +`analyzer` | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to tokenize the query string text. Default is the index-time analyzer specified for the `default_field`. If no analyzer is specified for the `default_field`, the `analyzer` is the default analyzer for the index. +`fuzziness` | `AUTO`, `0`, or a positive integer | The number of character edits (insert, delete, substitute) that it takes to change one word to another when determining whether a term matched a value. For example, the distance between `wined` and `wind` is 1. The default, `AUTO`, chooses a value based on the length of each term and is a good choice for most use cases. +`fuzzy_rewrite` | String | Determines how OpenSearch rewrites the query. Valid values are `constant_score`, `scoring_boolean`, `constant_score_boolean`, `top_terms_N`, `top_terms_boost_N`, and `top_terms_blended_freqs_N`. If the `fuzziness` parameter is not `0`, the query uses a `fuzzy_rewrite` method of `top_terms_blended_freqs_${max_expansions}` by default. Default is `constant_score`. +`fuzzy_transpositions` | Boolean | Setting `fuzzy_transpositions` to `true` (default) adds swaps of adjacent characters to the insert, delete, and substitute operations of the `fuzziness` option. For example, the distance between `wind` and `wnid` is 1 if `fuzzy_transpositions` is true (swap "n" and "i") and 2 if it is false (delete "n", insert "n"). If `fuzzy_transpositions` is false, `rewind` and `wnid` have the same distance (2) from `wind`, despite the more human-centric opinion that `wnid` is an obvious typo. The default is a good choice for most use cases. +`max_expansions` | Positive integer | The maximum number of terms to which the query can expand. Fuzzy queries “expand to” a number of matching terms that are within the distance specified in `fuzziness`. Then OpenSearch tries to match those terms. Default is `50`. +`minimum_should_match` | Positive or negative integer, positive or negative percentage, combination | If the query string contains multiple search terms and you use the `or` operator, the number of terms that need to match for the document to be considered a match. For example, if `minimum_should_match` is 2, `wind often rising` does not match `The Wind Rises.` If `minimum_should_match` is `1`, it matches. For details, see [Minimum should match]({{site.url}}{{site.baseurl}}/query-dsl/minimum-should-match/). +`operator` | String | If the query string contains multiple search terms, whether all terms need to match (`and`) or only one term needs to match (`or`) for a document to be considered a match. Valid values are `or` and `and`. Default is `or`. +`prefix_length` | Non-negative integer | The number of leading characters that are not considered in fuzziness. Default is `0`. + +The `fuzziness`, `fuzzy_transpositions`, `fuzzy_rewrite`, `max_expansions`, and `prefix_length` parameters can be applied to the term subqueries constructed for all terms except the final term. They do not have any effect on the prefix query constructed for the final term. +{: .note} \ No newline at end of file diff --git a/_query-dsl/full-text/match-phrase-prefix.md b/_query-dsl/full-text/match-phrase-prefix.md new file mode 100644 index 00000000..f9316d89 --- /dev/null +++ b/_query-dsl/full-text/match-phrase-prefix.md @@ -0,0 +1,148 @@ +--- +layout: default +title: Match phrase prefix +parent: Full-text queries +grand_parent: Query DSL +nav_order: 40 +--- + +# Match phrase prefix query + +Use the `match_phrase_prefix` query to specify a phrase to match in order. The documents that contain the phrase you specify will be returned. The last partial term in the phrase is interpreted as a prefix, so any documents that contain phrases that begin with the phrase and prefix of the last term will be returned. + +Similar to [match phrase]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase/), but creates a [prefix query](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PrefixQuery.html) out of the last term in the query string. + +For differences between the `match_phrase_prefix` and the `match_bool_prefix` queries, see [The `match_bool_prefix` and `match_phrase_prefix` queries]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-bool-prefix/#the-match_bool_prefix-and-match_phrase_prefix-queries). + +The following example shows a basic `match_phrase_prefix` query: + +```json +GET _search +{ + "query": { + "match_phrase_prefix": { + "title": "the wind" + } + } +} +``` +{% include copy-curl.html %} + +To pass additional parameters, you can use the expanded syntax: + +```json +GET _search +{ + "query": { + "match_phrase_prefix": { + "title": { + "query": "the wind", + "analyzer": "stop" + } + } + } +} +``` +{% include copy-curl.html %} + +## Example + +For example, consider an index with the following documents: + +```json +PUT testindex/_doc/1 +{ + "title": "The wind rises" +} +``` +{% include copy-curl.html %} + +```json +PUT testindex/_doc/2 +{ + "title": "Gone with the wind" + +} +``` +{% include copy-curl.html %} + +The following `match_phrase_prefix` query searches for the whole word `wind`, followed by a word that starts with `ri`: + +```json +GET testindex/_search +{ + "query": { + "match_phrase_prefix": { + "title": "wind ri" + } + } +} +``` +{% include copy-curl.html %} + +The response contains the matching document: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 6, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.92980814, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 0.92980814, + "_source": { + "title": "The wind rises" + } + } + ] + } +} +``` +
+ +## Parameters + +The query accepts the name of the field (``) as a top-level parameter: + +```json +GET _search +{ + "query": { + "match_phrase": { + "": { + "query": "text to search for", + ... + } + } + } +} +``` +{% include copy-curl.html %} + +The `` accepts the following parameters. All parameters except `query` are optional. + +Parameter | Data type | Description +:--- | :--- | :--- +`query` | String | The query string to use for search. Required. +`analyzer` | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to tokenize the query. +`max_expansions` | Positive integer | The maximum number of terms to which the query can expand. Fuzzy queries “expand to” a number of matching terms that are within the distance specified in `fuzziness`. Then OpenSearch tries to match those terms. Default is `50`. +`slop` | `0` (default) or a positive integer | Controls the degree to which words in a query can be misordered and still be considered a match. From the [Lucene documentation](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html#getSlop--): "The number of other words permitted between words in query phrase. For example, to switch the order of two words requires two moves (the first move places the words atop one another), so to permit reorderings of phrases, the slop must be at least two. A value of zero requires an exact match." \ No newline at end of file diff --git a/_query-dsl/full-text/match-phrase.md b/_query-dsl/full-text/match-phrase.md new file mode 100644 index 00000000..61b9cf18 --- /dev/null +++ b/_query-dsl/full-text/match-phrase.md @@ -0,0 +1,274 @@ +--- +layout: default +title: Match phrase +parent: Full-text queries +grand_parent: Query DSL +nav_order: 30 +--- + +# Match phrase query + +Use the `match_phrase` query to match documents that contain an exact phrase in a specified order. You can add flexibility to phrase matching by providing the `slop` parameter. + +The `match_phrase` query creates a [phrase query](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html) that matches a sequence of terms. + +The following example shows a basic `match_phrase` query: + +```json +GET _search +{ + "query": { + "match_phrase": { + "title": "the wind" + } + } +} +``` +{% include copy-curl.html %} + +To pass additional parameters, you can use the expanded syntax: + +```json +GET _search +{ + "query": { + "match_phrase": { + "title": { + "query": "the wind", + "analyzer": "stop" + } + } + } +} +``` +{% include copy-curl.html %} + +## Example + +For example, consider an index with the following documents: + +```json +PUT testindex/_doc/1 +{ + "title": "The wind rises" +} +``` +{% include copy-curl.html %} + +```json +PUT testindex/_doc/2 +{ + "title": "Gone with the wind" + +} +``` +{% include copy-curl.html %} + +The following `match_phrase` query searches for the phrase `wind rises`, where the word `wind` is followed by the word `rises`: + +```json +GET testindex/_search +{ + "query": { + "match_phrase": { + "title": "wind rises" + } + } +} +``` +{% include copy-curl.html %} + +The response contains the matching document: + +
+ + Response + + {: .text-delta} + + +```json +{ + "took": 30, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.92980814, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 0.92980814, + "_source": { + "title": "The wind rises" + } + } + ] + } +} +``` +
+ +## Analyzer + +By default, when you run a query on a `text` field, the search text is analyzed using the index analyzer associated with the field. You can specify a different search analyzer in the `analyzer` parameter. For example, the following query uses the `english` analyzer: + +```json +GET testindex/_search +{ + "query": { + "match_phrase": { + "title": { + "query": "the winds", + "analyzer": "english" + } + } + } +} +``` +{% include copy-curl.html %} + +The `english` analyzer removes the stopword `the` and performs stemming, producing the token `wind`. Both documents match this token and are returned in the results: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 2, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": 0.19363807, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 0.19363807, + "_source": { + "title": "The wind rises" + } + }, + { + "_index": "testindex", + "_id": "2", + "_score": 0.17225474, + "_source": { + "title": "Gone with the wind" + } + } + ] + } +} +``` +
+ +## Slop + +If you provide a `slop` parameter, the query tolerates reorderings of the search terms. Slop specifies the number of other words permitted between words in a query phrase. For example, in the following query, the search text is reordered compared to the document text: + +```json +GET _search +{ + "query": { + "match_phrase": { + "title": { + "query": "wind rises the", + "slop": 3 + } + } + } +} +``` + +The query still returns the matching document: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 2, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.44026947, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 0.44026947, + "_source": { + "title": "The wind rises" + } + } + ] + } +} +``` +
+ +## Empty query + +For information about a possible empty query, see the corresponding [match query section]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match/#empty-query). + +## Parameters + +The query accepts the name of the field (``) as a top-level parameter: + +```json +GET _search +{ + "query": { + "match_phrase": { + "": { + "query": "text to search for", + ... + } + } + } +} +``` +{% include copy-curl.html %} + +The `` accepts the following parameters. All parameters except `query` are optional. + +Parameter | Data type | Description +:--- | :--- | :--- +`query` | String | The query string to use for search. Required. +`analyzer` | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to tokenize the query string text. Default is the index-time analyzer specified for the `default_field`. If no analyzer is specified for the `default_field`, the `analyzer` is the default analyzer for the index. +`slop` | `0` (default) or a positive integer | Controls the degree to which words in a query can be misordered and still be considered a match. From the [Lucene documentation](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html#getSlop--): "The number of other words permitted between words in query phrase. For example, to switch the order of two words requires two moves (the first move places the words atop one another), so to permit reorderings of phrases, the slop must be at least two. A value of zero requires an exact match." +`zero_terms_query` | String | In some cases, the analyzer removes all terms from a query string. For example, the `stop` analyzer removes all terms from the string `an but this`. In those cases, `zero_terms_query` specifies whether to match no documents (`none`) or all documents (`all`). Valid values are `none` and `all`. Default is `none`. \ No newline at end of file diff --git a/_query-dsl/full-text/match.md b/_query-dsl/full-text/match.md new file mode 100644 index 00000000..431ecc6a --- /dev/null +++ b/_query-dsl/full-text/match.md @@ -0,0 +1,466 @@ +--- +layout: default +title: Match +parent: Full-text queries +grand_parent: Query DSL +nav_order: 10 +--- + +# Match query + +Use the `match` query for full-text search on a specific document field. If you run a `match` query on a [`text`]({{site.url}}/{{site.baseurl}}/field-types/supported-field-types/text/) field, the `match` query [analyzes]({{site.url}}/{{site.baseurl}}/analyzers/index/) the provided search string and returns documents that match any of the string's terms. If you run a `match` query on an exact-value field, it returns documents that match the exact value. The preferred way to search exact-value fields is to use a filter because, unlike a query, a filter is cached. + +The following example shows a basic `match` query for the word `wind` in the `title`: + +```json +GET _search +{ + "query": { + "match": { + "title": "wind" + } + } +} +``` +{% include copy-curl.html %} + +To pass additional parameters, you can use the expanded syntax: + +```json +GET _search +{ + "query": { + "match": { + "title": { + "query": "wind", + "analyzer": "stop" + } + } + } +} +``` +{% include copy-curl.html %} + +## Examples + +In the following examples, you'll use the index that contains the following documents: + +```json +PUT testindex/_doc/1 +{ + "title": "Let the wind rise" +} +``` +{% include copy-curl.html %} + +```json +PUT testindex/_doc/2 +{ + "title": "Gone with the wind" + +} +``` +{% include copy-curl.html %} + +```json +PUT testindex/_doc/3 +{ + "title": "Rise is gone" +} +``` +{% include copy-curl.html %} + +## Operator + +If a `match` query is run on a `text` field, the text is analyzed with the analyzer specified in the `analyzer` parameter. Then the resulting tokens are combined into a Boolean query using the operator specified in the `operator` parameter. The default operator is `OR`, so the query `wind rise` is changed into `wind OR rise`. In this example, this query returns documents 1--3 because each document has a term that matches the query. To specify the `and` operator, use the following query: + +```json +GET testindex/_search +{ + "query": { + "match": { + "title": { + "query": "wind rise", + "operator": "and" + } + } + } +} +``` +{% include copy-curl.html %} + +The query is constructed as `wind AND rise` and returns document 1 as the matching document: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 17, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1.2667098, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 1.2667098, + "_source": { + "title": "Let the wind rise" + } + } + ] + } +} +``` + +
+ +### Minimum should match + +You can control the minimum number of terms that a document must match to be returned in the results by specifying the [`minimum_should_match`]({{site.url}}{{site.baseurl}}/query-dsl/minimum-should-match/) parameter: + +```json +GET testindex/_search +{ + "query": { + "match": { + "title": { + "query": "wind rise", + "operator": "or", + "minimum_should_match": 2 + } + } + } +} +``` +{% include copy-curl.html %} + +Now documents are required to match both terms, so only document 1 is returned (this is equivalent to the `and` operator): + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 23, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1.2667098, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 1.2667098, + "_source": { + "title": "Let the wind rise" + } + } + ] + } +} +``` +
+ +## Analyzer + +Because in this example you didn't explicitly specify the analyzer, the default `standard` analyzer is used. The default analyzer does not perform stemming, so if you run a query `the wind rises`, you receive no results because the token `rises` does not match the token `rise`. To change the search analyzer, specify it in the `analyzer` field. For example, the following query uses the `english` analyzer: + +```json +GET testindex/_search +{ + "query": { + "match": { + "title": { + "query": "the wind rises", + "operator": "and", + "analyzer": "english" + } + } + } +} +``` +{% include copy-curl.html %} + +The `english` analyzer removes the stopword `the` and performs stemming, producing the tokens `wind` and `rise`. The latter token matches document 1, which is returned in the results: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 19, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1.2667098, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 1.2667098, + "_source": { + "title": "Let the wind rise" + } + } + ] + } +} +``` +
+ +## Empty query + +In some cases, an analyzer might remove all tokens from a query. For example, the `english` analyzer removes stop words, so in a query `and OR or`, all tokens are removed. To check the analyzer behavior, you can use the [Analyze API]({{site.url}}{{site.baseurl}}/api-reference/analyze-apis/#apply-a-built-in-analyzer): + +```json +GET testindex/_analyze +{ + "analyzer" : "english", + "text" : "and OR or" +} +``` +{% include copy-curl.html %} + +As expected, the query produces no tokens: + +```json +{ + "tokens": [] +} +``` + +You can specify the behavior for an empty query in the `zero_terms_query` parameter. Setting `zero_terms_query` to `all` returns all documents in the index and setting it to `none` returns no documents: + +```json +GET testindex/_search +{ + "query": { + "match": { + "title": { + "query": "and OR or", + "analyzer" : "english", + "zero_terms_query": "all" + } + } + } +} +``` +{% include copy-curl.html %} + +## Fuzziness + +To account for typos, you can specify `fuzziness` for your query as either of the following: + +- An integer that specifies the maximum allowed [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) for this edit. +- `AUTO`: + - Strings of 0–2 characters must match exactly. + - Strings of 3–5 characters allow 1 edit. + - Strings longer than 5 characters allow 2 edits. + +Setting `fuzziness` to the default `AUTO` value works best in most cases: + +```json +GET testindex/_search +{ + "query": { + "match": { + "title": { + "query": "wnid", + "fuzziness": "AUTO" + } + } + } +} +``` +{% include copy-curl.html %} + +The token `wnid` matches `wind` and the query returns documents 1 and 2: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 31, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": 0.47501624, + "hits": [ + { + "_index": "testindex", + "_id": "1", + "_score": 0.47501624, + "_source": { + "title": "Let the wind rise" + } + }, + { + "_index": "testindex", + "_id": "2", + "_score": 0.47501624, + "_source": { + "title": "Gone with the wind" + } + } + ] + } +} +``` +
+ +### Prefix length + +Misspellings rarely occur in the beginning of words. Thus, you can specify the minimum length the matched prefix must be to return a document in the results. For example, you can change the preceding query to include a `prefix_length`: + +```json +GET testindex/_search +{ + "query": { + "match": { + "title": { + "query": "wnid", + "fuzziness": "AUTO", + "prefix_length": 2 + } + } + } +} +``` +{% include copy-curl.html %} + +The preceding query returns no results. If you change the `prefix_length` to 1, documents 1 and 2 are returned because the first letter of the token `wnid` is not misspelled. + +### Transpositions + +In the preceding example, the word `wnid` contained a transposition (`in` was changed to `ni`). By default, transpositions are allowed in fuzzy matching, but you can disallow them by setting `fuzzy_transpositions` to `false`: + +```json +GET testindex/_search +{ + "query": { + "match": { + "title": { + "query": "wnid", + "fuzziness": "AUTO", + "fuzzy_transpositions": false + } + } + } +} +``` +{% include copy-curl.html %} + +Now the query returns no results. + +## Synonyms + +If you use a `synonym_graph` filter and `auto_generate_synonyms_phrase_query` is set to `true` (default), OpenSearch parses the query into terms and then combines the terms to generate a [phrase query](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html) for multi-term synonyms. For example, if you specify `ba,batting average` as synonyms and search for `ba`, OpenSearch searches for `ba OR "batting average"`. + +To match multi-term synonyms with conjunctions, set `auto_generate_synonyms_phrase_query` to `false`: + +```json +GET /testindex/_search +{ + "query": { + "match": { + "text": { + "query": "good ba", + "auto_generate_synonyms_phrase_query": false + } + } + } +} +``` +{% include copy-curl.html %} + +The query produced is `ba OR (batting AND average)`. + +## Parameters + +The query accepts the name of the field (``) as a top-level parameter: + +```json +GET _search +{ + "query": { + "match": { + "": { + "query": "text to search for", + ... + } + } + } +} +``` +{% include copy-curl.html %} + +The `` accepts the following parameters. All parameters except `query` are optional. + +Parameter | Data type | Description +:--- | :--- | :--- +`query` | String | The query string to use for search. Required. +`auto_generate_synonyms_phrase_query` | Boolean | Specifies whether to create a [match phrase query]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase/) automatically for multi-term synonyms. For example, if you specify `ba,batting average` as synonyms and search for `ba`, OpenSearch searches for `ba OR "batting average"` (if this option is `true`) or `ba OR (batting AND average)` (if this option is `false`). Default is `true`. +`analyzer` | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to tokenize the query string text. Default is the index-time analyzer specified for the `default_field`. If no analyzer is specified for the `default_field`, the `analyzer` is the default analyzer for the index. +`boost` | Floating-point | Boosts the clause by the given multiplier. Useful for weighing clauses in compound queries. Values in the [0, 1) range decrease relevance, and values greater than 1 increase relevance. Default is `1`. +`enable_position_increments` | Boolean | When `true`, resulting queries are aware of position increments. This setting is useful when the removal of stop words leaves an unwanted "gap" between terms. Default is `true`. +`fuzziness` | String | The number of character edits (insert, delete, substitute) that it takes to change one word to another when determining whether a term matched a value. For example, the distance between `wined` and `wind` is 1. Valid values are non-negative integers or `AUTO`. The default, `AUTO`, chooses a value based on the length of each term and is a good choice for most use cases. +`fuzzy_rewrite` | String | Determines how OpenSearch rewrites the query. Valid values are `constant_score`, `scoring_boolean`, `constant_score_boolean`, `top_terms_N`, `top_terms_boost_N`, and `top_terms_blended_freqs_N`. If the `fuzziness` parameter is not `0`, the query uses a `fuzzy_rewrite` method of `top_terms_blended_freqs_${max_expansions}` by default. Default is `constant_score`. +`fuzzy_transpositions` | Boolean | Setting `fuzzy_transpositions` to `true` (default) adds swaps of adjacent characters to the insert, delete, and substitute operations of the `fuzziness` option. For example, the distance between `wind` and `wnid` is 1 if `fuzzy_transpositions` is true (swap "n" and "i") and 2 if it is false (delete "n", insert "n"). If `fuzzy_transpositions` is false, `rewind` and `wnid` have the same distance (2) from `wind`, despite the more human-centric opinion that `wnid` is an obvious typo. The default is a good choice for most use cases. +`lenient` | Boolean | Setting `lenient` to `true` ignores data type mismatches between the query and the document field. For example, a query string of `"8.2"` could match a field of type `float`. Default is `false`. +`max_expansions` | Positive integer | The maximum number of terms to which the query can expand. Fuzzy queries “expand to” a number of matching terms that are within the distance specified in `fuzziness`. Then OpenSearch tries to match those terms. Default is `50`. +`minimum_should_match` | Positive or negative integer, positive or negative percentage, combination | If the query string contains multiple search terms and you use the `or` operator, the number of terms that need to match for the document to be considered a match. For example, if `minimum_should_match` is 2, `wind often rising` does not match `The Wind Rises.` If `minimum_should_match` is `1`, it matches. For details, see [Minimum should match]({{site.url}}{{site.baseurl}}/query-dsl/minimum-should-match/). +`operator` | String | If the query string contains multiple search terms, whether all terms need to match (`AND`) or only one term needs to match (`OR`) for a document to be considered a match. Valid values are:
- `OR`: The string `to be` is interpreted as `to OR be`
- `AND`: The string `to be` is interpreted as `to AND be`
Default is `OR`. +`prefix_length` | Non-negative integer | The number of leading characters that are not considered in fuzziness. Default is `0`. +`zero_terms_query` | String | In some cases, the analyzer removes all terms from a query string. For example, the `stop` analyzer removes all terms from the string `an but this`. In those cases, `zero_terms_query` specifies whether to match no documents (`none`) or all documents (`all`). Valid values are `none` and `all`. Default is `none`. \ No newline at end of file diff --git a/_query-dsl/full-text/multi-match.md b/_query-dsl/full-text/multi-match.md new file mode 100644 index 00000000..6dda1a79 --- /dev/null +++ b/_query-dsl/full-text/multi-match.md @@ -0,0 +1,932 @@ +--- +layout: default +title: Multi-match +parent: Full-text queries +grand_parent: Query DSL +nav_order: 50 +--- + +# Multi-match queries + +A multi-match operation functions similarly to the [match]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match/) operation. You can use a `multi_match` query to search multiple fields. + +The `^` "boosts" certain fields. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. In the following example, a match for "wind" in the title field influences `_score` four times as much as a match in the plot field: + +```json +GET _search +{ + "query": { + "multi_match": { + "query": "wind", + "fields": ["title^4", "plot"] + } + } +} +``` +{% include copy-curl.html %} + +The result is that films like *The Wind Rises* and *Gone with the Wind* are near the top of the search results, and films like *Twister*, which presumably have "wind" in their plot summaries, are near the bottom. + +You can use wildcards in the field name. For example, the following query will search the `speaker` field and all fields that start with `play_`, for example, `play_name` or `play_title`: + +```json +GET _search +{ + "query": { + "multi_match": { + "query": "hamlet", + "fields": ["speaker", "play_*"] + } + } +} +``` +{% include copy-curl.html %} + +If you don't provide the `fields` parameter, `multi_match` query searches the fields specified in the `index.query. Default_field` setting, which defaults to `*`. The default behavior is to extract all fields in the mapping that are eligible for [term-level queries]({{site.url}}{{site.baseurl}}/query-dsl/term/index/), filter the metadata fields, and combine all extracted fields to build a query. + +The maximum number of clauses in a query is defined in the `indices.query.bool.max_clause_count` setting, which defaults to 1,024. +{: .note} + +## Multi-match query types + +OpenSearch supports the following multi-match query types, which differ in the way the query is executed internally: + +- [`best_fields`](#best-fields) (default): Returns documents that match any field. Uses the `_score` of the best-matching field. +- [`most_fields`](#most-fields): Returns documents that match any field. Uses a combined score of each matching field. +- [`cross_fields`](#cross-fields): Treats all fields as if they were one field. Processes fields with the same `analyzer` and matches words in any field. +- [`phrase`](#phrase): Runs a `match_phrase` query on each field. Uses the `_score` of the best-matching field. +- [`phrase_prefix`](#phrase-prefix): Runs a `match_phrase_prefix` query on each field. Uses the `_score` of the best-matching field. +- [`bool_prefix`](#boolean-prefix): Runs a `match_bool_prefix` query on each field. Uses a combined score of each matched field. + +## Best fields + +If you're searching for two words that specify a concept, you want the results where the two words are next to each other to score higher. + +For example, consider an index that contains the following scientific articles: + +```json +PUT /articles/_doc/1 +{ + "title": "Aurora borealis", + "description": "Northern lights, or aurora borealis, explained" +} +``` +{% include copy-curl.html %} + +```json +PUT /articles/_doc/2 +{ + "title": "Sun deprivation in the Northern countries", + "description": "Using fluorescent lights for therapy" +} +``` +{% include copy-curl.html %} + +You can search for articles containing `northern lights` in the title or description: + +```json +GET articles/_search +{ + "query": { + "multi_match" : { + "query": "northern lights", + "type": "best_fields", + "fields": [ "title", "description" ], + "tie_breaker": 0.3 + } + } +} +``` +{% include copy-curl.html %} + +The preceding query is executed as the following [`dis_max`]({{site.url}}{{site.baseurl}}/query-dsl/compound/disjunction-max/) query with a `match` query for each field: + +```json +GET /articles/_search +{ + "query": { + "dis_max": { + "queries": [ + { "match": { "title": "northern lights" }}, + { "match": { "description": "northern lights" }} + ], + "tie_breaker": 0.3 + } + } +} +``` + +The results contain both documents, but document 1 is scored higher because both words are in the `description` field: + +```json +{ + "took": 30, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": 0.84407747, + "hits": [ + { + "_index": "articles", + "_id": "1", + "_score": 0.84407747, + "_source": { + "title": "Aurora borealis", + "description": "Northern lights, or aurora borealis, explained" + } + }, + { + "_index": "articles", + "_id": "2", + "_score": 0.6322521, + "_source": { + "title": "Sun deprivation in the Northern countries", + "description": "Using fluorescent lights for therapy" + } + } + ] + } +} +``` + +The `best_fields` query uses the score of the best-matching field. If you specify a `tie_breaker`, the score is calculated using the following algorithm: + +Take the score of the best-matching field and add (`tie_breaker` * `_score`) for all other matching fields. + +## Most fields + +Use the `most_fields` query for multiple fields that contain the same text that is analyzed in different ways. For example, the original field may contain text analyzed with the `standard` analyzer and another field may contain the same text analyzed with the `english` analyzer, which performs stemming: + +```json +PUT /articles +{ + "mappings": { + "properties": { + "title": { + "type": "text", + "fields": { + "english": { + "type": "text", + "analyzer": "english" + } + } + } + } + } +} +``` +{% include copy-curl.html %} + +Consider the following two documents that are indexed in the `articles` index: + +```json +PUT /articles/_doc/1 +{ + "title": "Buttered toasts" +} +``` +{% include copy-curl.html %} + +```json +PUT /articles/_doc/2 +{ + "title": "Buttering a toast" +} +``` +{% include copy-curl.html %} + +The `standard` analyzer analyzes the title `Buttered toast` into [`buttered`, `toasts`] and the title `Buttering a toast` into [`buttering`, `a`, `toast`]. On the other hand, the `english` analyzer produces the same token list [`butter`, `toast`] for both titles because of stemming. + +You can use the `most_fields` query in order to return as many documents as possible: + +```json +GET /articles/_search +{ + "query": { + "multi_match": { + "query": "buttered toast", + "fields": [ + "title", + "title.english" + ], + "type": "most_fields" + } + } +} +``` +{% include copy-curl.html %} + +The preceding query is executed as the following Boolean query: + +```json +GET articles/_search +{ + "query": { + "bool": { + "should": [ + { "match": { "title": "buttered toasts" }}, + { "match": { "title.english": "buttered toasts" }} + ] + } + } +} +``` + +To calculate the relevance score, a document's scores for all `match` clauses are added together and then the result is divided by the number of `match` clauses. + +Including the `title.english` field retrieves the second document that matches the stemmed tokens: + +```json +{ + "took": 9, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": 1.4418206, + "hits": [ + { + "_index": "articles", + "_id": "1", + "_score": 1.4418206, + "_source": { + "title": "Buttered toasts" + } + }, + { + "_index": "articles", + "_id": "2", + "_score": 0.09304003, + "_source": { + "title": "Buttering a toast" + } + } + ] + } +} +``` + +Because both `title` and `title.english` fields match for the first document, it has a higher relevance score. + +## Operator and minimum should match + +The `best_fields` and `most_fields` queries generate a match query on a field basis (one per field). Thus, the `minimum_should_match` and `operator` parameters are applied to each field, which is normally not the desired behavior. + +For example, consider a `customers` index with the following documents: + +```json +PUT customers/_doc/1 +{ + "first_name": "John", + "last_name": "Doe" +} +``` +{% include copy-curl.html %} + +```json +PUT customers/_doc/2 +{ + "first_name": "Jane", + "last_name": "Doe" +} +``` +{% include copy-curl.html %} + +If you're searching for `John Doe` in the `customers` index, you might construct the following query: + +```json +GET customers/_validate/query?explain +{ + "query": { + "multi_match" : { + "query": "John Doe", + "type": "best_fields", + "fields": [ "first_name", "last_name" ], + "operator": "and" + } + } +} +``` +{% include copy-curl.html %} + +The intent of the `and` operator in this query is to find a document that matches `John` and `Doe`. However, the query does not return any results. You can learn how the query is executed by running the Validate API: + +```json +GET customers/_validate/query?explain +{ + "query": { + "multi_match" : { + "query": "John Doe", + "type": "best_fields", + "fields": [ "first_name", "last_name" ], + "operator": "and" + } + } +} +``` +{% include copy-curl.html %} + +From the response, you can see that the query is trying to match both `John` and `Doe` to either the `first_name` or `last_name` field: + +```json +{ + "_shards": { + "total": 1, + "successful": 1, + "failed": 0 + }, + "valid": true, + "explanations": [ + { + "index": "customers", + "valid": true, + "explanation": "((+first_name:john +first_name:doe) | (+last_name:john +last_name:doe))" + } + ] +} +``` + +Because neither field contains both words, no results are returned. + +A better alternative for searching across fields is to use the [`cross_fields`](#cross-fields) query. Unlike the field-centric `best_fields` and `most_fields` queries, `cross_fields` query is term-centric. + +## Cross fields + +Use the `cross_fields` query to search for data across multiple fields. For example, if an index contains customer data, the first name and last name of the customer reside in different fields. Yet, when you search for `John Doe`, you want to receive documents in which `John` is in the `first_name` field and `Doe` is in the `last_name` field. + +The `most_fields` query does not work in this case because of the following problems: + +- The [`operator` and `minimum_should_match`](#operator-and-minimum-should-match) parameters are applied on a field basis instead of on a term basis. +- Term frequencies in the `first_name` and `last_name` fields can lead to unexpected results. For example, if someone's first name happens to be `Doe`, a document with this name will be presumed a better match because this first name will not appear in any other documents. + +The `cross_fields` query analyzes the query string into individual terms and then searches for each of the terms in any of the fields, as if they were one field. + +The following is the `cross_fields` query for `John Doe`: + +```json +GET /customers/_search +{ + "query": { + "multi_match" : { + "query": "John Doe", + "type": "cross_fields", + "fields": [ "first_name", "last_name" ], + "operator": "and" + } + } +} +``` +{% include copy-curl.html %} + +The response contains the only document in which both `John` and `Doe` are present: + +```json +{ + "took": 19, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.8754687, + "hits": [ + { + "_index": "customers", + "_id": "1", + "_score": 0.8754687, + "_source": { + "first_name": "John", + "last_name": "Doe" + } + } + ] + } +} +``` + +You can use the Validate API operation to gain insight into how the preceding query is executed: + +```json +GET /customers/_validate/query?explain +{ + "query": { + "multi_match" : { + "query": "John Doe", + "type": "cross_fields", + "fields": [ "first_name", "last_name" ], + "operator": "and" + } + } +} +``` +{% include copy-curl.html %} + +From the response, you can see that the query is searching for all terms in at least one field: + +```json +{ + "_shards": { + "total": 1, + "successful": 1, + "failed": 0 + }, + "valid": true, + "explanations": [ + { + "index": "customers", + "valid": true, + "explanation": "+blended(terms:[last_name:john, first_name:john]) +blended(terms:[last_name:doe, first_name:doe])" + } + ] +} +``` + +Thus, blending the term frequencies for all fields solves the problem of differing term frequencies by correcting for the differences. + +The `cross_fields` query is usually only useful on short string fields with a `boost` of 1. In other cases, the score does not produce a meaningful blend of term statistics because of the way boosts, term frequencies, and length normalization contribute to the score. +{: .note} + +The `fuzziness` parameter is not supported for `cross_fields` queries. +{: .note} + +### Analysis + +The `cross_fields` query only works as a term-centric query on fields with the same analyzer. Fields with the same analyzer are grouped together and these groups are combined with a Boolean query. + +For example, consider an index where the `first_name` and `last_name` fields are analyzed with the default `standard` + analyzer and their `.edge` subfields are analyzed with an edge n-gram analyzer: + +
+ + Response + + {: .text-delta} + +```json +PUT customers +{ + "settings": { + "analysis": { + "analyzer": { + "my_analyzer": { + "tokenizer": "my_tokenizer" + } + }, + "tokenizer": { + "my_tokenizer": { + "type": "edge_ngram", + "min_gram": 2, + "max_gram": 10 + } + } + } + }, + "mappings": { + "properties": { + "first_name": { + "type": "text", + "fields": { + "edge": { + "type": "text", + "analyzer": "my_analyzer" + } + } + }, + "last_name": { + "type": "text", + "fields": { + "edge": { + "type": "text", + "analyzer": "my_analyzer" + } + } + } + } + } +} +``` +{% include copy-curl.html %} + +
+ +You index one document in the `customers` index: + +```json +PUT /customers/_doc/1 +{ + "first": "John", + "last": "Doe" +} +``` +{% include copy-curl.html %} + +You can use a `cross_fields` query to search across the fields for `John Doe`: + +```json +GET /customers/_search +{ + "query": { + "multi_match" : { + "query": "John", + "type": "cross_fields", + "fields": [ + "first_name", "first_name.edge", + "last_name", "last_name.edge" + ] + } + } +} +``` +{% include copy-curl.html %} + +To see how the query is executed, you can run the Validate API: + +```json +GET /customers/_validate/query?explain +{ + "query": { + "multi_match" : { + "query": "John", + "type": "cross_fields", + "fields": [ + "first_name", "first_name.edge", + "last_name", "last_name.edge" + ] + } + } +} +``` +{% include copy-curl.html %} + +The response shows that the `last_name` and `first_name` fields are grouped together and treated as a single field. Similarly, the `last_name.edge` and `first_name.edge` fields are grouped together and treated as a single field: + +```json +{ + "_shards": { + "total": 1, + "successful": 1, + "failed": 0 + }, + "valid": true, + "explanations": [ + { + "index": "customers", + "valid": true, + "explanation": "(blended(terms:[last_name:john, first_name:john]) | (blended(terms:[last_name.edge:Jo, first_name.edge:Jo]) blended(terms:[last_name.edge:Joh, first_name.edge:Joh]) blended(terms:[last_name.edge:John, first_name.edge:John])))" + } + ] +} +``` + +Using the `operator` or `minimum_should_match` parameters with multiple field groups like the preceding ones can lead to the problem described in the [previous section](#operator-and-minimum-should-match). To avoid it, you can rewrite the previous query as two `cross_fields` subqueries combined with a Boolean query and apply the `minimum_should_match` to one of the subqueries: + +```json +GET /customers/_search +{ + "query": { + "bool": { + "should": [ + { + "multi_match": { + "query": "John Doe", + "type": "cross_fields", + "fields": [ + "first_name", + "last_name" + ], + "minimum_should_match": "1" + } + }, + { + "multi_match": { + "query": "John Doe", + "type": "cross_fields", + "fields": [ + "first_name.edge", + "last_name.edge" + ] + } + } + ] + } + } +} +``` +{% include copy-curl.html %} + +To create one group for all fields, specify an analyzer in your query: + +```json +GET customers/_search +{ + "query": { + "multi_match" : { + "query": "John Doe", + "type": "cross_fields", + "analyzer": "standard", + "fields": [ "first_name", "last_name", "*.edge" ] + } + } +} +``` +{% include copy-curl.html %} + +Running the Validate API on the previous query shows how the query is executed: + +```json +{ + "_shards": { + "total": 1, + "successful": 1, + "failed": 0 + }, + "valid": true, + "explanations": [ + { + "index": "customers", + "valid": true, + "explanation": "blended(terms:[last_name.edge:john, last_name:john, first_name:john, first_name.edge:john]) blended(terms:[last_name.edge:doe, last_name:doe, first_name:doe, first_name.edge:doe])" + } + ] +} +``` + +## Phrase + +The `phrase` query behaves similarly to the [`best_fields`](#best-fields) query but uses a `match_phrase` query instead of a `match` query. + +The following is an example `phrase` query for the index described in the [`best_fields`](#best-fields) section: + +```json +GET articles/_search +{ + "query": { + "multi_match" : { + "query": "northern lights", + "type": "phrase", + "fields": [ "title", "description" ] + } + } +} +``` +{% include copy-curl.html %} + +The preceding query is executed as the following [`dis_max`]({{site.url}}{{site.baseurl}}/query-dsl/compound/disjunction-max/) query with a `match_phrase` query for each field: + +```json +GET articles/_search +{ + "query": { + "dis_max": { + "queries": [ + { "match_phrase": { "title": "northern lights" }}, + { "match_phrase": { "description": "northern lights" }} + ] + } + } +} +``` + +Because by default a `phrase` query matches text only when the terms appear in the same order, only document 1 is returned in the results: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 3, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.84407747, + "hits": [ + { + "_index": "articles", + "_id": "1", + "_score": 0.84407747, + "_source": { + "title": "Aurora borealis", + "description": "Northern lights, or aurora borealis, explained" + } + } + ] + } +} +``` +
+ +You can use the `slop` parameter to allow other words between words in query phrase. For example, the following query accepts text as a match if up to two words are between `flourescent` and `therapy`: + +```json +GET articles/_search +{ + "query": { + "multi_match" : { + "query": "fluorescent therapy", + "type": "phrase", + "fields": [ "title", "description" ], + "slop": 2 + } + } +} +``` +{% include copy-curl.html %} + +The response contains document 2: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 3, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.7003825, + "hits": [ + { + "_index": "articles", + "_id": "2", + "_score": 0.7003825, + "_source": { + "title": "Sun deprivation in the Northern countries", + "description": "Using fluorescent lights for therapy" + } + } + ] + } +} +``` +
+ +For `slop` values less than 2, no documents are returned. + +The `fuzziness` parameter is not supported for `phrase` queries. +{: .note} + +## Phrase prefix + +The `phrase_prefix` query behaves similarly to the [`phrase`](#phrase) query but uses a `match_phrase_prefix` query instead of a `match_phrase` query. + +The following is an example `phrase_prefix` query for the index described in the [`best_fields`](#best-fields) section: + +```json +GET articles/_search +{ + "query": { + "multi_match" : { + "query": "northern light", + "type": "phrase_prefix", + "fields": [ "title", "description" ] + } + } +} +``` +{% include copy-curl.html %} + +The preceding query is executed as the following [`dis_max`]({{site.url}}{{site.baseurl}}/query-dsl/compound/disjunction-max/) query with a `match_phrase_prefix` query for each field: + +```json +GET articles/_search +{ + "query": { + "dis_max": { + "queries": [ + { "match_phrase_prefix": { "title": "northern light" }}, + { "match_phrase_prefix": { "description": "northern light" }} + ] + } + } +} +``` + +You can use the `slop` parameter to allow other words between words in query phrase. + +The `fuzziness` parameter is not supported for `phrase_prefix` queries. +{: .note} + +## Boolean prefix + +The `bool_prefix` query scores documents similarly to the [`most_fields`](#most-fields) query but uses a [`match_bool_prefix`]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-bool-prefix/) query instead of a `match` query. + +The following is an example `bool_prefix` query for the index described in the [`best_fields`](#best-fields) section: + +```json +GET articles/_search +{ + "query": { + "multi_match" : { + "query": "li northern", + "type": "bool_prefix", + "fields": [ "title", "description" ] + } + } +} +``` +{% include copy-curl.html %} + +The preceding query is executed as the following [`dis_max`]({{site.url}}{{site.baseurl}}/query-dsl/compound/disjunction-max/) query with a `match_bool_prefix` query for each field: + +```json +GET articles/_search +{ + "query": { + "dis_max": { + "queries": [ + { "match_bool_prefix": { "title": "li northern" }}, + { "match_bool_prefix": { "description": "li northern" }} + ] + } + } +} +``` + +The `fuzziness`, `prefix_length`, `max_expansions`, `fuzzy_rewrite`, and `fuzzy_transpositions` parameters are supported for the terms that are used to construct term queries, but they do not have an effect on the prefix query constructed from the final term. +{: .note} + +## Parameters + +The query accepts the following parameters. All parameters except `query` are optional. + +Parameter | Data type | Description +:--- | :--- | :--- +`query` | String | The query string to use for search. Required. +`auto_generate_synonyms_phrase_query` | Boolean | Specifies whether to create a [match phrase query]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase/) automatically for multi-term synonyms. For example, if you specify `ba,batting average` as synonyms and search for `ba`, OpenSearch searches for `ba OR "batting average"` (if this option is `true`) or `ba OR (batting AND average)` (if this option is `false`). Default is `true`. +`analyzer` | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to tokenize the query string text. Default is the index-time analyzer specified for the `default_field`. If no analyzer is specified for the `default_field`, the `analyzer` is the default analyzer for the index. +`boost` | Floating-point | Boosts the clause by the given multiplier. Useful for weighing clauses in compound queries. Values in the [0, 1) range decrease relevance, and values greater than 1 increase relevance. Default is `1`. +`fields` | Array of strings | The list of fields in which to search. If you don't provide the `fields` parameter, `multi_match` query searches the fields specified in the `index.query. Default_field` setting, which defaults to `*`. +`fuzziness` | String | The number of character edits (insert, delete, substitute) that it takes to change one word to another when determining whether a term matched a value. For example, the distance between `wined` and `wind` is 1. Valid values are non-negative integers or `AUTO`. The default, `AUTO`, chooses a value based on the length of each term and is a good choice for most use cases. Not supported for `phrase`, `phrase_prefix`, and `cross_fields` queries. +`fuzzy_rewrite` | String | Determines how OpenSearch rewrites the query. Valid values are `constant_score`, `scoring_boolean`, `constant_score_boolean`, `top_terms_N`, `top_terms_boost_N`, and `top_terms_blended_freqs_N`. If the `fuzziness` parameter is not `0`, the query uses a `fuzzy_rewrite` method of `top_terms_blended_freqs_${max_expansions}` by default. Default is `constant_score`. +`fuzzy_transpositions` | Boolean | Setting `fuzzy_transpositions` to `true` (default) adds swaps of adjacent characters to the insert, delete, and substitute operations of the `fuzziness` option. For example, the distance between `wind` and `wnid` is 1 if `fuzzy_transpositions` is true (swap "n" and "i") and 2 if it is false (delete "n", insert "n"). If `fuzzy_transpositions` is false, `rewind` and `wnid` have the same distance (2) from `wind`, despite the more human-centric opinion that `wnid` is an obvious typo. The default is a good choice for most use cases. +`lenient` | Boolean | Setting `lenient` to `true` ignores data type mismatches between the query and the document field. For example, a query string of `"8.2"` could match a field of type `float`. Default is `false`. +`max_expansions` | Positive integer | The maximum number of terms to which the query can expand. Fuzzy queries “expand to” a number of matching terms that are within the distance specified in `fuzziness`. Then OpenSearch tries to match those terms. Default is `50`. +`minimum_should_match` | Positive or negative integer, positive or negative percentage, combination | If the query string contains multiple search terms and you use the `or` operator, the number of terms that need to match for the document to be considered a match. For example, if `minimum_should_match` is 2, `wind often rising` does not match `The Wind Rises.` If `minimum_should_match` is `1`, it matches. For details, see [Minimum should match]({{site.url}}{{site.baseurl}}/query-dsl/minimum-should-match/). +`operator` | String | If the query string contains multiple search terms, whether all terms need to match (`AND`) or only one term needs to match (`OR`) for a document to be considered a match. Valid values are:
- `OR`: The string `to be` is interpreted as `to OR be`
- `AND`: The string `to be` is interpreted as `to AND be`
Default is `OR`. +`prefix_length` | Non-negative integer | The number of leading characters that are not considered in fuzziness. Default is `0`. +`slop` | `0` (default) or a positive integer | Controls the degree to which words in a query can be misordered and still be considered a match. From the [Lucene documentation](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/search/PhraseQuery.html#getSlop--): "The number of other words permitted between words in query phrase. For example, to switch the order of two words requires two moves (the first move places the words atop one another), so to permit reorderings of phrases, the slop must be at least two. A value of zero requires an exact match." Supported for `phrase` and `phrase_prefix` query types. +`tie_breaker` | Floating-point | A factor between 0 and 1.0 that is used to give more weight to documents that match multiple query clauses. For more information, see [The `tie_breaker` parameter`](#the-tie_breaker-parameter). +`type` | String | The multi-match query type. Valid values are `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix`, `bool_prefix`. Default is `best_fields`. +`zero_terms_query` | String | In some cases, the analyzer removes all terms from a query string. For example, the `stop` analyzer removes all terms from the string `an but this`. In those cases, `zero_terms_query` specifies whether to match no documents (`none`) or all documents (`all`). Valid values are `none` and `all`. Default is `none`. + +The `fuzziness` parameter is not supported for `phrase`, `phrase_prefix`, and `cross_fields` queries. +{: .note} + +The `slop` parameter is only supported for `phrase` and `phrase_prefix` queries. +{: .note} + +### The `tie_breaker` parameter + +Each term-level blended query calculates the document score as the best score returned by any field in a group. The scores from all blended queries are added together to produce the final score. You can change the way the score is calculated by using the `tie_breaker` parameter. The `tie_breaker` parameter accepts the following values: + +- 0.0 (default for `best_fields`, `cross_fields`, `phrase`, and `phrase_prefix` queries): Take the single best score returned by any field in a group. +- 1.0 (default for `most_fields` and `bool_prefix` queries): Add the scores for all fields in a group. +- A floating-point value in the (0, 1) range: Take the single best score of the best-matching field and add (`tie_breaker` * `_score`) for all other matching fields. \ No newline at end of file diff --git a/_query-dsl/full-text/query-string.md b/_query-dsl/full-text/query-string.md index 267016c6..6571740c 100644 --- a/_query-dsl/full-text/query-string.md +++ b/_query-dsl/full-text/query-string.md @@ -1,33 +1,618 @@ --- layout: default -title: Query string queries +title: Query string parent: Full-text queries grand_parent: Query DSL -nav_order: 25 +nav_order: 60 redirect_from: - /opensearch/query-dsl/full-text/query-string/ - /query-dsl/query-dsl/full-text/query-string/ --- -# Query string queries +# Query string query -A `query_string` query parses the query string based on the `query_string` syntax. It lets you create powerful yet concise queries that can incorporate wildcards and search multiple fields. +A `query_string` query parses the query string based on the [query string syntax](#query-string-syntax). It provides for creating powerful yet concise queries that can incorporate wildcards and search multiple fields. -## Example +Searches with `query_string` queries do not return nested documents. To search nested fields, use the [`nested` query]({{site.url}}{{site.baseurl}}/field-types/supported-field-types/nested/). +{: .note} -The following query searches for the speaker `KING` in the play name that ends with `well`: +Query string query has a strict syntax and returns an error in case of invalid syntax. Therefore, it does not work well for search box applications. For a less strict alternative, consider using [`simple_query_string` query]({{site.url}}{{site.baseurl}}/query-dsl/full-text/simple-query-string/). If you don't need query syntax support, use the [`match` query]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match/). +{: .important} + +## Query string syntax + +Query string syntax is based on [Apache Lucene query syntax](https://lucene.apache.org/core/2_9_4/queryparsersyntax.html). + +You can use query string syntax in the following cases: + +1. In a `query_string` query, for example: + ```json + GET _search + { + "query": { + "query_string": { + "query": "the wind AND (rises OR rising)" + } + } + } + ``` + {% include copy-curl.html %} + +1. In the Discover app of OpenSearch Dashboards, if you turn off DQL, as shown in the following image. + ![Using query string syntax in OpenSearch Dashboards Discover]({{site.url}}{{site.baseurl}}/images/discover-lucene-syntax.png) + For more information, see [Discover]({{site.url}}{{site.baseurl}}/dashboards/discover/index-discover/). + +1. If you search using the HTTP request query parameters, for example: + ```json + GET _search?q=wind + ``` + +A query string consists of _terms_ and _operators_. A term is a single word (for example, in the query `wind rises`, the terms are `wind` and `rises`). If several terms are surrounded by quotation marks, they are treated as one phrase where words are marched in the order they appear (for example, `"wind rises"`). Operators (such as `OR`, `AND`, and `NOT`) specify the Boolean logic used to interpret text in the query string. + +The examples in this section use an index containing the following mapping and documents: ```json -GET shakespeare/_search +PUT /testindex { - "query": { - "query_string": { - "query": "speaker:KING AND play_name: *well" + "mappings": { + "properties": { + "title": { + "type": "text", + "fields": { + "english": { + "type": "text", + "analyzer": "english" + } + } + } } } } ``` +{% include copy-curl.html %} + +```json +PUT /testindex/_doc/1 +{ + "title": "The wind rises" +} +``` +{% include copy-curl.html %} + +```json +PUT /testindex/_doc/2 +{ + "title": "Gone with the wind", + "description": "A 1939 American epic historical film" +} +``` +{% include copy-curl.html %} + +```json +PUT /testindex/_doc/3 +{ + "title": "Windy city" +} +``` +{% include copy-curl.html %} + +```json +PUT /testindex/_doc/4 +{ + "article title": "Wind turbines" +} +``` +{% include copy-curl.html %} + +## Reserved characters + +The following is a list of reserved characters for the query string query: + +`+`, `-`, `=`, `&&`, `||`, `>`, `<`, `!`, `(`, `)`,`{`, `}`, `[`, `]`, `^`, `"`, `~`, `*`, `?`, `:`, `\`, `/` + +Escape reserved characters with a backslash (`\`). When sending a JSON request, use a double backslash (`\\`) to escape reserved characters (because the backslash character is itself reserved, you must escape the backslash with another backslash). +{: .tip} + +For example, to search for an expression `2*3`, specify the query string: `2\\*3`: + +```json +GET /testindex/_search +{ + "query": { + "query_string": { + "query": "title: 2\\*3" + } + } +} +``` +{% include copy-curl.html %} + +The `>` and `<` signs cannot be escaped. They are interpreted as a range query. +{: .important} + +## White space characters and empty queries + +White space characters are not considered operators. If a query string is empty or only contains white space characters, the query does not return results. + +## Field names + +Specify the field name before the colon. The following table contains example queries with field names. + +Query in the `query_string` query | Query in Discover | Criterion for a document to match | Matching documents from the `testindex` index +:--- | :--- | :--- | :--- +`title: wind` | `title: wind` | The `title` field contains the word `wind`. | 1, 2 +`title: (wind OR windy)` | `title: (wind OR windy)` | The `title` field contains the word `wind` or the word `windy`. | 1, 2, 3 +`title: \"wind rises\"` | `title: "wind rises"` | The `title` field contains the phrase `wind rises`. Escape quotation marks with a backslash. | 1 +`article\\ title: wind` | `article\ title: wind` | The `article title` field contains the word `wind`. Escape the space character with a backslash. | 4 +`title.\\*: rise` | `title.\*: rise` | Every field that begins with `title.` (in this example, `title.english`) contains the word `rise`. Escape the wildcard character with a backslash. | 1 +`_exists_: description` | `_exists_: description` | The field `description` exists. | 2 + +## Wildcard expressions + +You can specify wildcard expressions using special characters: `?` replaces a single character and `*` replaces zero or more characters. + +#### Example + +The following query searches for the title containing the word `gone` and a description that contains a word starting with `hist`: + +```json +GET /testindex/_search +{ + "query": { + "query_string": { + "query": "title: gone AND description: hist*" + } + } +} +``` +{% include copy-curl.html %} + +Wildcard queries can use a significant amount of memory, which can degrade performance. Wildcards at the beginning of a word (for example, `*cal`) are the most expensive because matching documents on such wildcards requires examining all terms in the index. To disable leading wildcards, set `allow_leading_wildcard` to `false`. +{: .warning} + +For efficiency, pure wildcards such as `*` are rewritten as `exists` queries. Therefore, the `description: *` wildcard will match documents containing an empty value in the `description` field but will not match documents in which the `description` field is either missing or has a `null` value. + +If you set `analyze_wildcard` to `true`, OpenSearch will analyze queries that end with a `*` (such as `hist*`). Consequently, OpenSearch will build a Boolean query comprising the resulting tokens by taking exact matches on the first n-1 tokens and a prefix match on the last token. + +## Regular expressions + +To specify regular expression patterns in a query string, surround them with forward slashes (`/`), for example `title: /w[a-z]nd/`. + +The `allow_leading_wildcard` parameter does not apply to regular expressions. For example, a query string such as `/.*d/` will examine all terms in the index. +{: .important} + +## Fuzziness + +You can run fuzzy queries using the `~` operator, for example `title: rise~`. + +The query searches for documents containing terms that are similar to the search term within the maximum allowed edit distance. The edit distance is defined as the [Damerau-Levenshtein distance](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance), which measures the number of one-character changes (insertions, deletions, substitutions, or transpositions) needed to change one term to another term. + +The default edit distance of 2 should catch 80% of misspellings. To change the default edit distance, specify the new edit distance after the `~` operator. For example, to set the edit distance to `1`, use the query `title: rise~1`. + +Do not mix fuzzy and wildcard operators. If you specify both fuzzy and wildcard operators, one of the operators will not be applied. For example, if you can search for `wnid*~1`, the wildcard operator `*` will be applied but the fuzzy operator `~1` will not be applied. +{: .important} + +## Proximity queries + +A proximity query does not require the search phrase to be in the specified order. It allows the words in the phrase to be in a different order or separated by other words. A proximity query specifies a maximum edit distance of words in a phrase. For example, the following query allows an edit distance of 4 when matching the words in the specified phrase: + +```json +GET /testindex/_search +{ + "query": { + "query_string": { + "query": "title: \"wind gone\"~4" + } + } +} +``` +{% include copy-curl.html %} + +When OpenSearch matches documents, the closer the words in the document to the word order specified in the query (the less the edit distance), the higher the document's relevance score. + +## Ranges + +To specify a range for a numeric, string, or date field, use square brackets (`[min TO max]`) for an inclusive range and curly braces (`{min TO max}`) for an exclusive range. You can also mix square brackets and curly braces to include or exclude the lower and upper bound (for example, `{min TO max]`). + +The dates for a date range must be provided in the format that you used when mapping the field containing the date. For more information about supported date formats, see [Formats]({{site.url}}{{site.baseurl}}/opensearch/supported-field-types/date/#formats). + +The following table provides range syntax examples. + +Data type | Query | Query string +:--- | :--- | :--- +Numeric | Documents whose account numbers are from 1 to 15, inclusive. | `account_number: [1 TO 15]` or
`account_number: (>=1 AND <=15)` or
`account_number: (+>=1 +<=15)` +| Documents whose account numbers are 15 and greater. | `account_number: [15 TO *]` or
`account_number: >=15` (note no space after the `>=` sign) +String | Documents where last name is from Bates, inclusive, to Duke, exclusive. | `lastname: [Bates TO Duke}` or
`lastname: (>=Bates AND `lastname: - `OR`: The string `to be` is interpreted as `to OR be`
- `AND`: The string `to be` is interpreted as `to AND be`
Default is `OR`. -`enable_position_increments` | Boolean | When true, resulting queries are aware of position increments. This setting is useful when the removal of stop words leaves an unwanted "gap" between terms. Default is `true`. +`analyzer` | String | The [analyzer]({{site.url}}{{site.baseurl}}/analyzers/index/) used to tokenize the query string text. Default is the index-time analyzer specified for the `default_field`. If no analyzer is specified for the `default_field`, the `analyzer` is the default analyzer for the index. +`auto_generate_synonyms_phrase_query` | Boolean | Specifies whether to create a [match phrase query]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase/) automatically for multi-term synonyms. For example, if you specify `ba, batting average` as synonyms and search for `ba`, OpenSearch searches for `ba OR "batting average"` (if this option is `true`) or `ba OR (batting AND average)` (if this option is `false`). Default is `true`. +`boost` | Floating-point | Boosts the clause by the given multiplier. Useful for weighing clauses in compound queries. Values in the [0, 1) range decrease relevance, and values greater than 1 increase relevance. Default is `1`. +`default_field` | String | The field in which to search if the field is not specified in the query string. Supports wildcards. Defaults to the value specified in the `index.query. Default_field` index setting. By default, the `index.query. Default_field` is `*`, which means extract all fields eligible for term query and filter the metadata fields. The extracted fields are combined into a query if the `prefix` is not specified. Eligible fields do not include nested documents. Searching all eligible fields could be a resource-intensive operation. The `indices.query.bool.max_clause_count` search setting defines the maximum value for the product of the number of fields and the number of terms that can be queried at one time. The default value for `indices.query.bool.max_clause_count` is 1,024. +`default_operator`| String | If the query string contains multiple search terms, whether all terms need to match (`AND`) or only one term needs to match (`OR`) for a document to be considered a match. Valid values are:
- `OR`: The string `to be` is interpreted as `to OR be`
- `AND`: The string `to be` is interpreted as `to AND be`
Default is `OR`. +`enable_position_increments` | Boolean | When `true`, resulting queries are aware of position increments. This setting is useful when the removal of stop words leaves an unwanted "gap" between terms. Default is `true`. +`fields` | String array | The list of fields to search (for example, `"fields": ["title^4", "description"]`). Supports wildcards. If unspecified, defaults to the `index.query. Default_field` setting, which defaults to `["*"]`. `fuzziness` | String | The number of character edits (insert, delete, substitute) that it takes to change one word to another when determining whether a term matched a value. For example, the distance between `wined` and `wind` is 1. Valid values are non-negative integers or `AUTO`. The default, `AUTO`, chooses a value based on the length of each term and is a good choice for most use cases. +`fuzzy_max_expansions` | Positive integer | The maximum number of terms to which the query can expand. Fuzzy queries “expand to” a number of matching terms that are within the distance specified in `fuzziness`. Then OpenSearch tries to match those terms. Default is `50`. `fuzzy_transpositions` | Boolean | Setting `fuzzy_transpositions` to `true` (default) adds swaps of adjacent characters to the insert, delete, and substitute operations of the `fuzziness` option. For example, the distance between `wind` and `wnid` is 1 if `fuzzy_transpositions` is true (swap "n" and "i") and 2 if it is false (delete "n", insert "n"). If `fuzzy_transpositions` is false, `rewind` and `wnid` have the same distance (2) from `wind`, despite the more human-centric opinion that `wnid` is an obvious typo. The default is a good choice for most use cases. -`fuzzy_max_expansions` | Positive integer | The maximum number of terms the fuzzy query will match. Default is 50. -`lenient` | Boolean | Setting `lenient` to true lets you ignore data type mismatches between the query and the document field. For example, a query string of "8.2" could match a field of type `float`. Default is `false`. +`lenient` | Boolean | Setting `lenient` to `true` ignores data type mismatches between the query and the document field. For example, a query string of `"8.2"` could match a field of type `float`. Default is `false`. `max_determinized_states` | Positive integer | The maximum number of "[states](https://lucene.apache.org/core/8_9_0/core/org/apache/lucene/util/automaton/Operations.html#DEFAULT_MAX_DETERMINIZED_STATES)" (a measure of complexity) that Lucene can create for query strings that contain regular expressions (for example, `"query": "/wind.+?/"`). Larger numbers allow for queries that use more memory. Default is 10,000. -`time_zone` | String | Specifies the number of hours to offset the desired time zone from `UTC`. You need to indicate the time zone offset number if the query string contains a date range. For example, set `time_zone": "-08:00"` for a query with a date range such as `"query": "wind rises release_date[2012-01-01 TO 2014-01-01]"`). The default time zone format used to specify number of offset hours is `UTC`. \ No newline at end of file +`minimum_should_match` | Positive or negative integer, positive or negative percentage, combination | If the query string contains multiple search terms and you use the `or` operator, the number of terms that need to match for the document to be considered a match. For example, if `minimum_should_match` is 2, `wind often rising` does not match `The Wind Rises.` If `minimum_should_match` is `1`, it matches. For details, see [Minimum should match]({{site.url}}{{site.baseurl}}/query-dsl/minimum-should-match/). +`phrase_slop` | Integer | The maximum number of words that are allowed between the matched words. If `phrase_slop` is 2, a maximum of two words is allowed between matched words in a phrase. Transposed words have a slop of 2. Default is `0` (an exact phrase match where matched words must be next to each other). +`quote_analyzer` | String | The analyzer used to tokenize quoted text in the query string. Overrides the `analyzer` parameter for quoted text. Default is the `search_quote_analyzer` specified for the `default_field`. +`quote_field_suffix` | String | This option supports searching for exact matches (surrounded with quotation marks) using a different analysis method than non-exact matches use. For example, if `quote_field_suffix` is `.exact` and you search for `\"lightly\"` in the `title` field, OpenSearch searches for the word `lightly` in the `title.exact` field. This second field might use a different type (for example, `keyword` rather than `text`) or a different analyzer. +`rewrite` | String | Determines how OpenSearch rewrites and scores multi-term queries. Valid values are `constant_score`, `scoring_boolean`, `constant_score_boolean`, `top_terms_N`, `top_terms_boost_N`, and `top_terms_blended_freqs_N`. Default is `constant_score`. +`time_zone` | String | Specifies the number of hours to offset the desired time zone from `UTC`. You need to indicate the time zone offset number if the query string contains a date range. For example, set `time_zone": "-08:00"` for a query with a date range such as `"query": "wind rises release_date[2012-01-01 TO 2014-01-01]"`). The default time zone format used to specify number of offset hours is `UTC`. + +Query string queries may be internally converted into [prefix queries]({{site.url}}{{site.baseurl}}/query-dsl/term/prefix/). If [`search.allow_expensive_queries`]({{site.url}}{{site.baseurl}}/query-dsl/index/#expensive-queries) is set to `false`, prefix queries are not executed. If `index_prefixes` is enabled, the `search.allow_expensive_queries` setting is ignored and an optimized query is built and executed. +{: .important} \ No newline at end of file diff --git a/_query-dsl/full-text/simple-query-string.md b/_query-dsl/full-text/simple-query-string.md new file mode 100644 index 00000000..f8473541 --- /dev/null +++ b/_query-dsl/full-text/simple-query-string.md @@ -0,0 +1,369 @@ +--- +layout: default +title: Simple query string +parent: Full-text queries +grand_parent: Query DSL +nav_order: 70 +--- + +# Simple query string query + +Use the `simple_query_string` type to specify multiple arguments delineated by regular expressions directly in the query string. Simple query string has a less strict syntax than query string because it discards any invalid portions of the string and does not return errors for invalid syntax. + +This query uses a [simple syntax](#simple-query-string-syntax) to parse the query string based on special operators and split the string into terms. After parsing, the query analyzes each term independently and then returns matching documents. + +The following query performs fuzzy search on the `title` field: + +```json +GET _search +{ + "query": { + "simple_query_string": { + "query": "\"rises wind the\"~4 | *ising~2", + "fields": ["title"] + } + } +} +``` +{% include copy-curl.html %} + +## Simple query string syntax + +A query string consists of _terms_ and _operators_. A term is a single word (for example, in the query `wind rises`, the terms are `wind` and `rises`). If several terms are surrounded by quotation marks, they are treated as one phrase where words are marched in the order they appear (for example, `"wind rises"`). Operators such as `+`, `|`, and `-` specify the Boolean logic used to interpret text in the query string. + +## Operators + +Simple query string syntax supports the following operators. + +Operator | Description +:--- | :--- +`+` | Acts as the `AND` operator. +`|` | Acts as the `OR` operator. +`*` | When used at the end of a term, signifies a prefix query. +`"` | Wraps several terms into a phrase (for example, `"wind rises"`). +`(`, `)` | Wrap a clause for precedence (for example, `wind + (rises | rising)`). +`~n` | When used after a term (for example, `wnid~3`), sets `fuzziness`. When used after a phrase, sets `slop`. +`-` | Negates the term. + +All of the preceding operators are reserved characters. To refer to them as raw characters and not operators, escape any of them with a backslash. When sending a JSON request, use `\\` to escape reserved characters (because the backslash character is itself reserved, you must escape the backslash with another backslash). + +## Default operator + +The default operator is `OR` (unless you set the `default_operator` to `AND`). The default operator dictates the overall query behavior. For example, consider an index containing the following documents: + +```json +PUT /customers/_doc/1 +{ + "first_name":"Amber", + "last_name":"Duke", + "address":"880 Holmes Lane" +} +``` +{% include copy-curl.html %} + +```json +PUT /customers/_doc/2 +{ + "first_name":"Hattie", + "last_name":"Bond", + "address":"671 Bristol Street" +} +``` +{% include copy-curl.html %} + +```json +PUT /customers/_doc/3 +{ + "first_name":"Nanette", + "last_name":"Bates", + "address":"789 Madison St" +} +``` +{% include copy-curl.html %} + +```json +PUT /customers/_doc/4 +{ + "first_name":"Dale", + "last_name":"Amber", + "address":"467 Hutchinson Court" +} +``` +{% include copy-curl.html %} + +The following query attempts to find documents, for which the address contains the words `street` or `st` and does not contain the word `madison`: + +```json +GET /customers/_search +{ + "query": { + "simple_query_string": { + "fields": [ "address" ], + "query": "street st -madison" + } + } +} + +``` +{% include copy-curl.html %} + +However, the results include not only the expected document, but all four documents: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 3, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4, + "relation": "eq" + }, + "max_score": 2.2039728, + "hits": [ + { + "_index": "customers", + "_id": "2", + "_score": 2.2039728, + "_source": { + "first_name": "Hattie", + "last_name": "Bond", + "address": "671 Bristol Street" + } + }, + { + "_index": "customers", + "_id": "3", + "_score": 1.2039728, + "_source": { + "first_name": "Nanette", + "last_name": "Bates", + "address": "789 Madison St" + } + }, + { + "_index": "customers", + "_id": "1", + "_score": 1, + "_source": { + "first_name": "Amber", + "last_name": "Duke", + "address": "880 Holmes Lane" + } + }, + { + "_index": "customers", + "_id": "4", + "_score": 1, + "_source": { + "first_name": "Dale", + "last_name": "Amber", + "address": "467 Hutchinson Court" + } + } + ] + } +} +``` +
+ +Because the default operator is `OR`, this query includes documents that contain the words `street` or `st` (documents 2 and 3) and documents that do not contain the word `madison` (documents 1 and 4). + +To express the query intent correctly, precede `-madison` with `+`: + +```json +GET /customers/_search +{ + "query": { + "simple_query_string": { + "fields": [ "address" ], + "query": "street st +-madison" + } + } +} +``` +{% include copy-curl.html %} + +Alternatively, specify `AND` as the default operator and use disjunction for the words `street` and `st`: + +```json +GET /customers/_search +{ + "query": { + "simple_query_string": { + "fields": [ "address" ], + "query": "st|street -madison", + "default_operator": "AND" + } + } +} +``` +{% include copy-curl.html %} + +The preceding query returns document 2: + +
+ + Response + + {: .text-delta} + +```json +{ + "took": 2, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 2.2039728, + "hits": [ + { + "_index": "customers", + "_id": "2", + "_score": 2.2039728, + "_source": { + "first_name": "Hattie", + "last_name": "Bond", + "address": "671 Bristol Street" + } + } + ] + } +} +``` +
+ +## Limit operators + +To limit the supported operators for the simple query string parser, include the operators that you want to support, separated by `|`, in the `flags` parameter. For example, the following query enables only `OR`, `AND`, and `FUZZY` operators: + +```json +GET /customers/_search +{ + "query": { + "simple_query_string": { + "fields": [ "address" ], + "query": "bristol | madison +stre~2", + "flags": "OR|AND|FUZZY" + } + } +} +``` +{% include copy-curl.html %} + +The following table lists all available operator flags. + +Flag | Description +:--- | :--- +`ALL` (default) | Enables all operators. +`AND` | Enables the `+` (`AND`) operator. +`ESCAPE` | Enables the `\` as an escape character. +`FUZZY` | Enables the `~n` operator after a word, where `n` is an integer denoting the allowed edit distance for matching. +`NEAR` | Enables the `~n` operator after a phrase, where `n` is the maximum number of positions allowed between matching tokens. Same as `SLOP`. +`NONE` | Disables all operators. +`NOT` | Enables the `-` (`NOT`) operator. +`OR` | Enables the `|` (`OR`) operator. +`PHRASE` | Enables the `"` (quotation marks) for phrase search. +`PRECEDENCE` | Enables the `(` and `)` (parentheses) operators for operator precedence. +`PREFIX` | Enables the `*` (prefix) operator. +`SLOP` | Enables the `~n` operator after a phrase, where `n` is the maximum number of positions allowed between matching tokens. Same as `NEAR`. +`WHITESPACE` | Enables white space characters as characters on which the text is split. + +## Wildcard expressions + +You can specify wildcard expressions using the `*` special character, which replaces zero or more characters. For example, the following query searches in all fields that end with `name`: + +```json +GET /customers/_search +{ + "query": { + "simple_query_string" : { + "query": "Amber Bond", + "fields": [ "*name" ] + } + } +} +``` +{% include copy-curl.html %} + +## Boosting + +Use the caret (`^`) boost operator to boost the relevance score of a field by a multiplier. Values in the [0, 1) range decrease relevance, and values greater than 1 increase relevance. Default is `1`. + +For example, the following query searches the `first_name` and `last_name` fields and boosts matches from the `first_name` field by a factor of 2: + +```json +GET /customers/_search +{ + "query": { + "simple_query_string" : { + "query": "Amber", + "fields": [ "first_name^2", "last_name" ] + } + } +} +``` +{% include copy-curl.html %} + +## Multi-position tokens + +For multi-position tokens, simple query string creates a [match phrase query]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase/). Thus, if you specify `ml, machine learning` as synonyms and search for `ml`, OpenSearch searches for `ml OR "machine learning"`. + +Alternatively, you can match multi-position tokens using conjunctions. If you set `auto_generate_synonyms_phrase_query` to `false`, OpenSearch searches for `ml OR (machine AND learning)`. + +For example, the following query searches for the text `ml models` and specifies not to auto-generate a match phrase query for each synonym: + +```json +GET /testindex/_search +{ + "query": { + "simple_query_string": { + "fields": ["title"], + "query": "ml models", + "auto_generate_synonyms_phrase_query": false + } + } +} +``` +{% include copy-curl.html %} + +For this query, OpenSearch creates the following Boolean query: `(ml OR (machine AND learning)) models`. + +## Parameters + +The following table lists the top-level parameters that `simple_query_string` query supports. All parameters except `query` are optional. + +Parameter | Data type | Description +:--- | :--- | :--- +`query`| String | The text that may contain expressions in the [simple query string syntax](#simple-query-string-syntax) to use for search. Required. +`analyze_wildcard` | Boolean | Specifies whether OpenSearch should attempt to analyze wildcard terms. Default is `false`. +`analyzer` | String | The analyzer used to tokenize the query string text. Default is the index-time analyzer specified for the `default_field`. If no analyzer is specified for the `default_field`, the `analyzer` is the default analyzer for the index. +`auto_generate_synonyms_phrase_query` | Boolean | Specifies whether to create [match_phrase queries]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match/) automatically for multi-term synonyms. Default is `true`. +`default_operator`| String | If the query string contains multiple search terms, whether all terms need to match (`AND`) or only one term needs to match (`OR`) for a document to be considered a match. Valid values are:
- `OR`: The string `to be` is interpreted as `to OR be`
- `AND`: The string `to be` is interpreted as `to AND be`
Default is `OR`. +`fields` | String array | The list of fields to search (for example, `"fields": ["title^4", "description"]`). Supports wildcards. If unspecified, defaults to the `index.query. Default_field` setting, which defaults to `["*"]`. The maximum number of fields that can be searched at the same time is defined by `indices.query.bool.max_clause_count`, which is 1,024 by default. +`flags` | String | A `|`-delimited string of [flags]({{site.baseurl}}/query-dsl/full-text/simple-query-string/) to enable (for example, `AND|OR|NOT`). Default is `ALL`. You can explicitly set the value for `default_field`. For example, to return all titles, set it to `"default_field": "title"`. +`fuzzy_max_expansions` | Positive integer | The maximum number of terms to which the query can expand. Fuzzy queries “expand to” a number of matching terms that are within the distance specified in `fuzziness`. Then OpenSearch tries to match those terms. Default is `50`. +`fuzzy_transpositions` | Boolean | Setting `fuzzy_transpositions` to `true` (default) adds swaps of adjacent characters to the insert, delete, and substitute operations of the `fuzziness` option. For example, the distance between `wind` and `wnid` is 1 if `fuzzy_transpositions` is true (swap "n" and "i") and 2 if it is false (delete "n", insert "n"). If `fuzzy_transpositions` is false, `rewind` and `wnid` have the same distance (2) from `wind`, despite the more human-centric opinion that `wnid` is an obvious typo. The default is a good choice for most use cases. +`fuzzy_prefix_length`| Integer | The number of beginning characters left unchanged for fuzzy matching. Default is 0. +`lenient` | Boolean | Setting `lenient` to `true` ignores data type mismatches between the query and the document field. For example, a query string of `"8.2"` could match a field of type `float`. Default is `false`. +`minimum_should_match` | Positive or negative integer, positive or negative percentage, combination | If the query string contains multiple search terms and you use the `or` operator, the number of terms that need to match for the document to be considered a match. For example, if `minimum_should_match` is 2, `wind often rising` does not match `The Wind Rises.` If `minimum_should_match` is `1`, it matches. For details, see [Minimum should match]({{site.url}}{{site.baseurl}}/query-dsl/minimum-should-match/). +`quote_field_suffix` | String | This option supports searching for exact matches (surrounded with quotation marks) using a different analysis method than non-exact matches use. For example, if `quote_field_suffix` is `.exact` and you search for `\"lightly\"` in the `title` field, OpenSearch searches for the word `lightly` in the `title.exact` field. This second field might use a different type (for example, `keyword` rather than `text`) or a different analyzer. \ No newline at end of file diff --git a/_query-dsl/geo-and-xy/geo-bounding-box.md b/_query-dsl/geo-and-xy/geo-bounding-box.md index 394f1ff2..a0ee85f0 100644 --- a/_query-dsl/geo-and-xy/geo-bounding-box.md +++ b/_query-dsl/geo-and-xy/geo-bounding-box.md @@ -1,6 +1,6 @@ --- layout: default -title: Geo-bounding box queries +title: Geo-bounding box parent: Geographic and xy queries grand_parent: Query DSL nav_order: 10 @@ -9,9 +9,9 @@ redirect_from: - /query-dsl/query-dsl/geo-and-xy/geo-bounding-box/ --- -# Geo-bounding box queries +# Geo-bounding box query -To search for documents that contain [geopoint]({{site.url}}{{site.baseurl}}/opensearch/supported-field-types/geo-point) fields, use a geo-bounding box query. The geo-bounding box query returns documents whose geopoints are within the bounding box specified in the query. A document with multiple geopoints matches the query if at least one geopoint is within the bounding box. +To search for documents that contain [geopoint]({{site.url}}{{site.baseurl}}/opensearch/supported-field-types/geo-point/) fields, use a geo-bounding box query. The geo-bounding box query returns documents whose geopoints are within the bounding box specified in the query. A document with multiple geopoints matches the query if at least one geopoint is within the bounding box. ## Example @@ -170,10 +170,10 @@ Geo-bounding box queries accept the following fields. Field | Data type | Description :--- | :--- | :--- -_name | String | The name of the filter. Optional. -validation_method | String | The validation method. Valid values are `IGNORE_MALFORMED` (accept geopoints with invalid coordinates), `COERCE` (try to coerce coordinates to valid values), and `STRICT` (return an error when coordinates are invalid). Default is `STRICT`. -type | String | Specifies how to execute the filter. Valid values are `indexed` (index the filter) and `memory` (execute the filter in memory). Default is `memory`. -ignore_unmapped | Boolean | Specifies whether to ignore an unmapped field. If set to `true`, the query does not return any documents that have an unmapped field. If set to `false`, an exception is thrown when the field is unmapped. Default is `false`. +`_name` | String | The name of the filter. Optional. +`validation_method` | String | The validation method. Valid values are `IGNORE_MALFORMED` (accept geopoints with invalid coordinates), `COERCE` (try to coerce coordinates to valid values), and `STRICT` (return an error when coordinates are invalid). Default is `STRICT`. +`type` | String | Specifies how to execute the filter. Valid values are `indexed` (index the filter) and `memory` (execute the filter in memory). Default is `memory`. +`ignore_unmapped` | Boolean | Specifies whether to ignore an unmapped field. If set to `true`, the query does not return any documents that have an unmapped field. If set to `false`, an exception is thrown when the field is unmapped. Default is `false`. ## Accepted formats diff --git a/_query-dsl/geo-and-xy/xy.md b/_query-dsl/geo-and-xy/xy.md index b1e4a465..3db05c01 100644 --- a/_query-dsl/geo-and-xy/xy.md +++ b/_query-dsl/geo-and-xy/xy.md @@ -1,6 +1,6 @@ --- layout: default -title: xy queries +title: xy parent: Geographic and xy queries grand_parent: Query DSL nav_order: 50 @@ -10,7 +10,7 @@ redirect_from: - /query-dsl/query-dsl/geo-and-xy/xy/ --- -# xy queries +# xy query To search for documents that contain [xy point]({{site.url}}{{site.baseurl}}/opensearch/supported-field-types/xy-point) and [xy shape]({{site.url}}{{site.baseurl}}/opensearch/supported-field-types/xy-shape) fields, use an xy query. @@ -180,9 +180,9 @@ When constructing an xy query, you can also reference the name of a shape pre-in Parameter | Description :--- | :--- -index | The name of the index that contains the pre-indexed shape. -id | The document ID of the document that contains the pre-indexed shape. -path | The field name of the field that contains the pre-indexed shape as a path. +`index` | The name of the index that contains the pre-indexed shape. +`id` | The document ID of the document that contains the pre-indexed shape. +`path` | The field name of the field that contains the pre-indexed shape as a path. The following example illustrates referencing the name of a shape pre-indexed in another index. In this example, the index `pre-indexed-shapes` contains the shape that defines the boundaries, and the index `testindex` contains the shapes whose locations are checked against those boundaries. diff --git a/_query-dsl/index.md b/_query-dsl/index.md index ebb57ba9..c46e8d79 100644 --- a/_query-dsl/index.md +++ b/_query-dsl/index.md @@ -49,7 +49,7 @@ Broadly, you can classify queries into two categories---*leaf queries* and *comp - [Specialized queries]({{site.url}}{{site.baseurl}}/query-dsl/specialized/index/): Specialized queries include all other query types (`distance_feature`, `more_like_this`, `percolate`, `rank_feature`, `script`, `script_score`, and `wrapper`). -- **Compound queries**: Compound queries serve as wrappers for multiple leaf or compound clauses, either to combine their results or to modify their behavior. They include the Boolean, disjunction max, constant score, function score, and boosting query types. To learn more, see [Compound queries]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/compound/index/). +- **Compound queries**: Compound queries serve as wrappers for multiple leaf or compound clauses, either to combine their results or to modify their behavior. They include the Boolean, disjunction max, constant score, function score, and boosting query types. To learn more, see [Compound queries]({{site.url}}{{site.baseurl}}/query-dsl/compound/index/). ## A note on Unicode special characters in text fields diff --git a/_query-dsl/match-all.md b/_query-dsl/match-all.md new file mode 100644 index 00000000..274111f0 --- /dev/null +++ b/_query-dsl/match-all.md @@ -0,0 +1,31 @@ +--- +layout: default +title: Match all queries +nav_order: 65 +--- + +# Match all queries + +The `match_all` query returns all documents. This query can be useful in testing large document sets if you need to return the entire set. + +```json +GET _search +{ + "query": { + "match_all": {} + } +} +``` +{% include copy-curl.html %} + +The `match_all` query has a `match_none` counterpart, which is rarely useful: + +```json +GET _search +{ + "query": { + "match_none": {} + } +} +``` +{% include copy-curl.html %} \ No newline at end of file diff --git a/_query-dsl/term/index.md b/_query-dsl/term/index.md index a80207b0..594ece3f 100644 --- a/_query-dsl/term/index.md +++ b/_query-dsl/term/index.md @@ -2,6 +2,7 @@ layout: default title: Term-level queries has_children: true +has_toc: false nav_order: 20 --- diff --git a/_search-plugins/searching-data/autocomplete.md b/_search-plugins/searching-data/autocomplete.md index ce867ed4..ecbb4ddc 100644 --- a/_search-plugins/searching-data/autocomplete.md +++ b/_search-plugins/searching-data/autocomplete.md @@ -44,7 +44,7 @@ GET shakespeare/_search } ``` -To make the word order and relative positions flexible, specify a `slop` value. To learn about the `slop` option, see [Other advanced options]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#other-advanced-options). +To make the word order and relative positions flexible, specify a `slop` value. To learn about the `slop` option, see [Slop]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase#slop). Prefix matching doesn’t require any special mappings. It works with your data as is. However, it’s a fairly resource-intensive operation. A prefix of `a` could match hundreds of thousands of terms and not be useful to your user. @@ -65,7 +65,7 @@ GET shakespeare/_search } ``` -To learn about the `max_expansions` option, see [Other advanced options]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#other-advanced-options). +The maximum number of terms to which the query can expand. Queries “expand” search terms to a number of matching terms that are within the distance specified in `fuzziness`. The ease of implementing query-time autocomplete comes at the cost of performance. When implementing this feature on a large scale, we recommend an index-time solution. With an index-time solution, you might experience slower indexing, but it’s a price you pay only once and not for every query. The edge n-gram, search-as-you-type, and completion suggester methods are index-time solutions. diff --git a/_search-plugins/sql/full-text.md b/_search-plugins/sql/full-text.md index fa3953c1..73940f07 100644 --- a/_search-plugins/sql/full-text.md +++ b/_search-plugins/sql/full-text.md @@ -38,7 +38,7 @@ You can specify the following options in any order: - `zero_terms_query` - `boost` -Refer to the `match` query [documentation]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#match) for parameter descriptions and supported values. +Refer to the `match` query [documentation]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match/) for parameter descriptions and supported values. ### Example 1: Search the `message` field for the text "this is a test": @@ -119,13 +119,13 @@ To search for text in multiple fields, use `MULTI_MATCH` function. This function ### Syntax -The `MULTI_MATCH` function lets you *boost* certain fields using **^** character. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. The syntax allows to specify the fields in double quotes, single quotes, surrounded by backticks, or unquoted. Use star ``"*"`` to search all fields. Star symbol should be quoted. +The `MULTI_MATCH` function *boosts* certain fields by using **^** character. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. The syntax supports specifying the fields with double quotes, single quotes, backticks, or without any quotes. Use star ``"*"`` to search all fields. Star symbol should be quoted. ```sql multi_match([field_expression+], query_expression[, option=]*) ``` -The weight is optional and is specified after the field name. It could be delimited by the `caret` character -- `^` or by whitespace. Please, refer to examples below: +The weight is optional and is specified after the field name. It could be delimited by the `caret` character -- `^` or by white space. Refer to the following examples: ```sql multi_match(["Tags" ^ 2, 'Title' 3.4, `Body`, Comments ^ 0.3], ...) @@ -150,7 +150,7 @@ You can specify the following options for `MULTI_MATCH` in any order: - `zero_terms_query` - `boost` -Please, refer to `multi_match` query [documentation](#multi-match) for parameter description and supported values. +Refer to `multi_match` query [documentation]({{site.baseurl}}/query-dsl/full-text/multi-match/) for parameter description and supported values. ### For example, REST API search for `Dale` in either the `firstname` or `lastname` fields: @@ -187,13 +187,13 @@ This function maps to the to the `query_string` query used in search engine, to ### Syntax -The `QUERY_STRING` function has syntax similar to `MATCH_QUERY` and lets you *boost* certain fields using **^** character. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. The syntax allows to specify the fields in double quotes, single quotes, surrounded by backticks, or unquoted. Use star ``"*"`` to search all fields. Star symbol should be quoted. +The `QUERY_STRING` function has syntax similar to `MATCH_QUERY` and *boosts* certain fields by using **^** character. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. The syntax supports specifying the fields with double quotes, single quotes, backticks, or without any quotes. Use star ``"*"`` to search all fields. Star symbol should be quoted. ```sql query_string([field_expression+], query_expression[, option=]*) ``` -The weight is optional and is specified after the field name. It could be delimited by the `caret` character -- `^` or by whitespace. Please, refer to examples below: +The weight is optional and is specified after the field name. It could be delimited by the `caret` character -- `^` or by white space. Refer to the following examples: ```sql query_string(["Tags" ^ 2, 'Title' 3.4, `Body`, Comments ^ 0.3], ...) @@ -226,7 +226,7 @@ You can specify the following options for `QUERY_STRING` in any order: - `tie_breaker` - `time_zone` -Refer to the `query_string` query [documentation]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#query-string) for parameter descriptions and supported values. +Refer to the `query_string` query [documentation]({{site.url}}{{site.baseurl}}/query-dsl/full-text/query-string/) for parameter descriptions and supported values. ### Example of using `query_string` in SQL and PPL queries: @@ -283,7 +283,7 @@ The `MATCHPHRASE`/`MATCH_PHRASE` functions let you specify the following options - `zero_terms_query` - `boost` -Refer to the `match_phrase` query [documentation]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#match-phrase) for parameter descriptions and supported values. +Refer to the `match_phrase` query [documentation]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase/) for parameter descriptions and supported values. ### Example of using `match_phrase` in SQL and PPL queries: @@ -323,13 +323,13 @@ The **^** lets you *boost* certain fields. Boosts are multipliers that weigh mat ### Syntax -The syntax allows to specify the fields in double quotes, single quotes, surrounded by backticks, or unquoted. Use star ``"*"`` to search all fields. Star symbol should be quoted. +The syntax supports specifying the fields with double quotes, single quotes, backticks, or without any quotes. Use star ``"*"`` to search all fields. Star symbol should be quoted. ```sql simple_query_string([field_expression+], query_expression[, option=]*) ``` -The weight is optional and is specified after the field name. It could be delimited by the `caret` character -- `^` or by whitespace. Please, refer to examples below: +The weight is optional and is specified after the field name. It could be delimited by the `caret` character -- `^` or by white space. Refer to the following examples: ```sql simple_query_string(["Tags" ^ 2, 'Title' 3.4, `Body`, Comments ^ 0.3], ...) @@ -351,7 +351,7 @@ You can specify the following options for `SIMPLE_QUERY_STRING` in any order: - `minimum_should_match` - `quote_field_suffix` -Refer to the `simple_query_string` query [documentation]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#simple-query-string) for parameter descriptions and supported values. +Refer to the `simple_query_string` query [documentation]({{site.url}}{{site.baseurl}}/query-dsl/full-text/simple-query-string/) for parameter descriptions and supported values. ### *Example* of using `simple_query_string` in SQL and PPL queries: @@ -402,7 +402,7 @@ The `MATCH_PHRASE_PREFIX` function lets you specify the following options in any - `zero_terms_query` - `boost` -Refer to the `match_phrase_prefix` query [documentation]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#match-phrase-prefix) for parameter descriptions and supported values. +Refer to the `match_phrase_prefix` query [documentation]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-phrase-prefix/) for parameter descriptions and supported values. ### *Example* of using `match_phrase_prefix` in SQL and PPL queries: @@ -458,7 +458,7 @@ The `MATCH_BOOL_PREFIX` function lets you specify the following options in any o - `analyzer` - `operator` -Refer to the `match_bool_prefix` query [documentation]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#match-boolean-prefix) for parameter descriptions and supported values. +Refer to the `match_bool_prefix` query [documentation]({{site.url}}{{site.baseurl}}/query-dsl/full-text/match-bool-prefix/) for parameter descriptions and supported values. ### Example of using `match_bool_prefix` in SQL and PPL queries: diff --git a/_tuning-your-cluster/availability-and-recovery/snapshots/sm-api.md b/_tuning-your-cluster/availability-and-recovery/snapshots/sm-api.md index 3f059fa9..cd3a238f 100644 --- a/_tuning-your-cluster/availability-and-recovery/snapshots/sm-api.md +++ b/_tuning-your-cluster/availability-and-recovery/snapshots/sm-api.md @@ -220,7 +220,7 @@ Get all SM policies: ```json GET _plugins/_sm/policies ``` -You can use a [query string]({{site.url}}{{site.baseurl}}/opensearch/query-dsl/full-text/index#query-string) and specify pagination, the field to be sorted by, and sort order: +You can use a [query string]({{site.url}}{{site.baseurl}}/query-dsl/full-text/query-string/) and specify pagination, the field to be sorted by, and sort order: ```json GET _plugins/_sm/policies?from=0&size=20&sortField=sm_policy.name&sortOrder=desc&queryString=* diff --git a/images/discover-lucene-syntax.png b/images/discover-lucene-syntax.png new file mode 100644 index 0000000000000000000000000000000000000000..16ccb38c6025ad3e2681728f75b18c5bf845eba7 GIT binary patch literal 82409 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
  • 5BkMH}OP95@P`0^npjmk`nra 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�)t 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