From 85122bfe0573cefa1350a458520ccb213728768b Mon Sep 17 00:00:00 2001 From: BryanFauble <17128019+BryanFauble@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:32:51 +0000 Subject: [PATCH 1/2] Review fixes for SearchIndex entity and supporting functionality - Fix store_async crash when upserting onto an existing SearchIndex: a freshly-constructed instance has no _last_persistent_instance, which store_entity dereferenced unconditionally for the version_label check. - Guard force_version in store_entity for models that do not define it. - Add SearchIndex to the get_id() type union in services/search.py. - Flatten SearchIndex.to_synapse_request() to return the entity body directly instead of a single-key {"entity": ...} wrapper the only caller immediately unwrapped. - Add reference docs for SearchIndex and the search configuration resources, resolving the dangling mkdocstrings cross-references to SearchQuery, SearchIndexQuery, search_dsl.Query, TextAnalyzer, and SynonymSet. - Rewrite point-in-time comments as durable descriptions of what the code is. - Add unit coverage for the upsert-onto-existing-index path. --- .../experimental/async/search_index.md | 30 + .../experimental/async/search_management.md | 46 + .../experimental/sync/search_index.md | 39 + .../experimental/sync/search_management.md | 51 + mkdocs.yml | 4 + synapseclient/models/mixins/__init__.py | 6 +- synapseclient/models/mixins/enum_coercion.py | 33 +- .../models/mixins/table_components.py | 2 +- synapseclient/models/organization.py | 4 +- .../models/protocols/search_index_protocol.py | 94 +- synapseclient/models/search_dsl.py | 1205 +++++++++++++++++ synapseclient/models/search_index.py | 195 ++- synapseclient/models/search_management.py | 221 +-- synapseclient/models/services/search.py | 2 + .../models/services/storable_entity.py | 7 +- synapseclient/operations/delete_operations.py | 12 +- .../operations/factory_operations.py | 15 + synapseclient/operations/store_operations.py | 14 +- .../models/async/test_search_index_async.py | 13 +- .../async/unit_test_search_index_async.py | 180 ++- .../unit_test_search_management_async.py | 10 + .../operations/unit_test_delete_operations.py | 19 + .../unit_test_factory_operations.py | 30 + .../operations/unit_test_store_operations.py | 24 + 24 files changed, 2092 insertions(+), 164 deletions(-) create mode 100644 docs/reference/experimental/async/search_index.md create mode 100644 docs/reference/experimental/async/search_management.md create mode 100644 docs/reference/experimental/sync/search_index.md create mode 100644 docs/reference/experimental/sync/search_management.md create mode 100644 synapseclient/models/search_dsl.py diff --git a/docs/reference/experimental/async/search_index.md b/docs/reference/experimental/async/search_index.md new file mode 100644 index 000000000..c60960604 --- /dev/null +++ b/docs/reference/experimental/async/search_index.md @@ -0,0 +1,30 @@ +# SearchIndex + +## API reference + +::: synapseclient.models.SearchIndex + options: + inherited_members: true + members: + - store_async + - get_async + - delete_async + - query_async + - autocomplete_async + - get_permissions_async + - get_acl_async + - set_permissions_async + - delete_permissions_async + - list_acl_async + +## Supporting types + +::: synapseclient.models.SearchQuery +::: synapseclient.models.SearchQueryPart +::: synapseclient.models.SearchIndexQuery +::: synapseclient.models.SearchAutocompleteRequest +::: synapseclient.models.SearchHit +::: synapseclient.models.SearchFieldValue +::: synapseclient.models.SearchHighlight +::: synapseclient.models.SearchIndexState +::: synapseclient.models.SearchIndexStatus diff --git a/docs/reference/experimental/async/search_management.md b/docs/reference/experimental/async/search_management.md new file mode 100644 index 000000000..4358942f6 --- /dev/null +++ b/docs/reference/experimental/async/search_management.md @@ -0,0 +1,46 @@ +# Search Configuration + +Analyzer, synonym, and configuration resources that control how a +[SearchIndex](search_index.md) builds its OpenSearch index. + +## API reference + +::: synapseclient.models.SearchConfiguration + options: + inherited_members: true + members: + - store_async + - get_async + - list_async + +::: synapseclient.models.TextAnalyzer + options: + inherited_members: true + members: + - store_async + - get_async + - list_async + +::: synapseclient.models.SynonymSet + options: + inherited_members: true + members: + - store_async + - get_async + - list_async + +::: synapseclient.models.ColumnAnalyzerOverride + options: + inherited_members: true + members: + - store_async + - get_async + - list_async + +::: synapseclient.models.SearchConfigBinding + options: + inherited_members: true + members: + - store_async + - get_async + - delete_async diff --git a/docs/reference/experimental/sync/search_index.md b/docs/reference/experimental/sync/search_index.md new file mode 100644 index 000000000..3f04985a1 --- /dev/null +++ b/docs/reference/experimental/sync/search_index.md @@ -0,0 +1,39 @@ +[](){ #searchindex-reference-sync } +# SearchIndex + +## API reference + +::: synapseclient.models.SearchIndex + options: + inherited_members: true + members: + - store + - get + - delete + - query + - autocomplete + - get_permissions + - get_acl + - set_permissions + - delete_permissions + - list_acl + +## Supporting types + +::: synapseclient.models.SearchQuery +::: synapseclient.models.SearchQueryPart +::: synapseclient.models.SearchIndexQuery +::: synapseclient.models.SearchAutocompleteRequest +::: synapseclient.models.SearchHit +::: synapseclient.models.SearchFieldValue +::: synapseclient.models.SearchHighlight +::: synapseclient.models.SearchIndexState +::: synapseclient.models.SearchIndexStatus + +## OpenSearch query DSL + +::: synapseclient.models.search_dsl + options: + show_root_heading: false + show_if_no_docstring: true + members_order: source diff --git a/docs/reference/experimental/sync/search_management.md b/docs/reference/experimental/sync/search_management.md new file mode 100644 index 000000000..43cd677c3 --- /dev/null +++ b/docs/reference/experimental/sync/search_management.md @@ -0,0 +1,51 @@ +[](){ #search-management-reference-sync } +# Search Configuration + +Analyzer, synonym, and configuration resources that control how a +[SearchIndex][searchindex-reference-sync] builds its OpenSearch index. + +## API reference + +::: synapseclient.models.SearchConfiguration + options: + inherited_members: true + members: + - store + - get + - list + +::: synapseclient.models.TextAnalyzer + options: + inherited_members: true + members: + - store + - get + - list + +::: synapseclient.models.SynonymSet + options: + inherited_members: true + members: + - store + - get + - list + +::: synapseclient.models.ColumnAnalyzerOverride + options: + inherited_members: true + members: + - store + - get + - list + +::: synapseclient.models.SearchConfigBinding + options: + inherited_members: true + members: + - store + - get + - delete + +## Supporting types + +::: synapseclient.models.ColumnAnalyzerOverrideEntry diff --git a/mkdocs.yml b/mkdocs.yml index 4783e355e..60aab2d64 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,6 +86,8 @@ nav: - Dataset Collection: reference/experimental/sync/dataset_collection.md - EntityView: reference/experimental/sync/entityview.md - MaterializedView: reference/experimental/sync/materializedview.md + - SearchIndex: reference/experimental/sync/search_index.md + - Search Configuration: reference/experimental/sync/search_management.md - SubmissionView: reference/experimental/sync/submissionview.md - Activity: reference/experimental/sync/activity.md - Team: reference/experimental/sync/team.md @@ -117,6 +119,8 @@ nav: - Dataset Collection: reference/experimental/async/dataset_collection.md - EntityView: reference/experimental/async/entityview.md - MaterializedView: reference/experimental/async/materializedview.md + - SearchIndex: reference/experimental/async/search_index.md + - Search Configuration: reference/experimental/async/search_management.md - SubmissionView: reference/experimental/async/submissionview.md - Activity: reference/experimental/async/activity.md - Team: reference/experimental/async/team.md diff --git a/synapseclient/models/mixins/__init__.py b/synapseclient/models/mixins/__init__.py index 2e64a1280..0f5984d60 100644 --- a/synapseclient/models/mixins/__init__.py +++ b/synapseclient/models/mixins/__init__.py @@ -2,7 +2,10 @@ from synapseclient.models.mixins.access_control import AccessControllable from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator -from synapseclient.models.mixins.enum_coercion import EnumCoercionMixin +from synapseclient.models.mixins.enum_coercion import ( + EnumCoercionMixin, + ForwardCompatibleStrEnum, +) from synapseclient.models.mixins.form import ( FormChangeRequest, FormData, @@ -30,6 +33,7 @@ __all__ = [ "AccessControllable", "EnumCoercionMixin", + "ForwardCompatibleStrEnum", "ProjectSettingsMixin", "StorableContainer", "StorageLocationConfigurable", diff --git a/synapseclient/models/mixins/enum_coercion.py b/synapseclient/models/mixins/enum_coercion.py index 77edf6b56..afd227638 100644 --- a/synapseclient/models/mixins/enum_coercion.py +++ b/synapseclient/models/mixins/enum_coercion.py @@ -1,6 +1,7 @@ -"""Mixin for automatic enum coercion in dataclasses.""" +"""Mixins and base classes for enum handling in dataclasses.""" -from typing import Any, ClassVar, Dict +from enum import Enum +from typing import Any, ClassVar, Dict, Optional class EnumCoercionMixin: @@ -31,3 +32,31 @@ def __setattr__(self, name: str, value: Any) -> None: ): value = enum_cls(value) super().__setattr__(name, value) + + +class ForwardCompatibleStrEnum(str, Enum): + """A string enum that accepts values it does not declare. + + Use this for enums whose values are chosen by the Synapse backend. An + undeclared value is kept as a new member rather than raising ``ValueError``, + so a backend that gains a value cannot break deserialization. Comparisons + against the raw string and against declared members both work, and repeated + lookups of the same undeclared value return the same member. + + Example:: + + class MyState(ForwardCompatibleStrEnum): + ACTIVE = "ACTIVE" + + MyState("ACTIVE") is MyState.ACTIVE # True + MyState("BRAND_NEW") == "BRAND_NEW" # True, no exception + """ + + @classmethod + def _missing_(cls, value: object) -> Optional["ForwardCompatibleStrEnum"]: + if not isinstance(value, str): + return None + pseudo_member = str.__new__(cls, value) + pseudo_member._name_ = value + pseudo_member._value_ = value + return cls._value2member_map_.setdefault(value, pseudo_member) diff --git a/synapseclient/models/mixins/table_components.py b/synapseclient/models/mixins/table_components.py index cf2f4dd40..44591714d 100644 --- a/synapseclient/models/mixins/table_components.py +++ b/synapseclient/models/mixins/table_components.py @@ -85,7 +85,7 @@ "DatasetCollection", "SubmissionView", ] -CLASSES_WITH_READ_ONLY_SCHEMA = ["MaterializedView", "VirtualTable", "SearchIndex"] +CLASSES_WITH_READ_ONLY_SCHEMA = ["MaterializedView", "VirtualTable"] PANDAS_TABLE_TYPE = { "floating": "DOUBLE", diff --git a/synapseclient/models/organization.py b/synapseclient/models/organization.py index ad1d5f195..e8ca28eb7 100644 --- a/synapseclient/models/organization.py +++ b/synapseclient/models/organization.py @@ -574,7 +574,7 @@ def fill_from_dict(self, response: dict[str, Any]) -> "Organization": @deprecated( - version="5.0.0", + version="4.14.0", reason="SchemaOrganization has been renamed to Organization. " "This alias will be removed in a future release; use Organization instead.", ) @@ -1439,7 +1439,7 @@ def list_organizations( @deprecated( - version="5.0.0", + version="4.14.0", reason="Renamed to list_organizations. " "This alias will be removed in a future release; use list_organizations instead.", ) diff --git a/synapseclient/models/protocols/search_index_protocol.py b/synapseclient/models/protocols/search_index_protocol.py index 6c8e2e68f..ecc65e210 100644 --- a/synapseclient/models/protocols/search_index_protocol.py +++ b/synapseclient/models/protocols/search_index_protocol.py @@ -1,14 +1,20 @@ """Protocol for the specific methods of this class that have synchronous counterparts generated at runtime.""" -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol +from typing import TYPE_CHECKING, List, Optional, Protocol from typing_extensions import Self from synapseclient import Synapse +from synapseclient.models.search_dsl import Query, SourceFilter if TYPE_CHECKING: - from synapseclient.models.search_management import SearchHit + from synapseclient.models.search_management import ( + SearchHit, + SearchIndexQuery, + SearchQuery, + SearchQueryPart, + ) class SearchIndexSynchronousProtocol(Protocol): @@ -18,7 +24,6 @@ def store( self, dry_run: bool = False, *, - job_timeout: int = 600, synapse_client: Optional[Synapse] = None, ) -> "Self": """Store metadata about a SearchIndex including the annotations. Creates @@ -28,8 +33,6 @@ def store( Arguments: dry_run: If True, will not actually store the SearchIndex but will log to the console what would be created or updated. - job_timeout: The maximum amount of time to wait for the index-build job - to complete before raising an error. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -121,23 +124,88 @@ def delete(self, *, synapse_client: Optional[Synapse] = None) -> None: """ return None + def query( + self, + search_query: "SearchQuery", + response_parts: Optional[List["SearchQueryPart"]] = None, + *, + job_timeout: int = 600, + synapse_client: Optional[Synapse] = None, + ) -> "SearchIndexQuery": + """Query this search index. Unlike a SQL-backed Table, a SearchIndex is + queried with the + [OpenSearch Query DSL](https://docs.opensearch.org/latest/query-dsl/) + carried by a [SearchQuery][synapseclient.models.SearchQuery] — not with + Synapse SQL. See [Query][synapseclient.models.search_dsl.Query] for the + supported clause kinds. + + Arguments: + search_query: The OpenSearch + [`_search`](https://docs.opensearch.org/latest/api-reference/search-apis/search/) + body to execute against this index. + response_parts: Additional response parts to request beyond the + default hits, such as the total hit count or the select columns. + job_timeout: The maximum amount of time to wait for the query job to + complete before raising a `SynapseTimeoutError`. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The completed + [SearchIndexQuery][synapseclient.models.SearchIndexQuery], carrying + the `hits` and any requested response parts. + + Raises: + ValueError: If the `id` attribute has not been set. + + Example: Query an index for documents mentioning "alzheimer". +   + + ```python + from synapseclient import Synapse + from synapseclient.models import SearchIndex, SearchQuery, SearchQueryPart + + syn = Synapse() + syn.login() + + results = SearchIndex(id="syn12345").query( + search_query=SearchQuery( + query={"match": {"title": {"query": "alzheimer"}}}, + size=10, + ), + response_parts=[SearchQueryPart.TOTAL_HITS], + ) + print(results.total_hits) + for hit in results.hits: + print(hit.row_id, hit.fields) + ``` + """ + from synapseclient.models.search_management import SearchIndexQuery + + return SearchIndexQuery() + def autocomplete( self, - query: Dict[str, Any], - source: Optional[Dict[str, Any]] = None, + query: Query, + source: Optional[SourceFilter] = None, *, synapse_client: Optional[Synapse] = None, ) -> List["SearchHit"]: """Run a synchronous autocomplete search against this index. The - autocomplete endpoint allowlists only prefix-style queries (`prefix`, - `match_phrase_prefix`, or `match_bool_prefix`) and caps results at 8. + autocomplete endpoint allowlists only prefix-style queries + ([`prefix`](https://docs.opensearch.org/latest/query-dsl/term/prefix/), + [`match_phrase_prefix`](https://docs.opensearch.org/latest/query-dsl/full-text/match-phrase-prefix/), + or [`match_bool_prefix`](https://docs.opensearch.org/latest/query-dsl/full-text/match-bool-prefix/)) + and caps results at 8. Arguments: - query: The top-level OpenSearch Query DSL clause; restricted - server-side to `prefix`, `match_phrase_prefix`, or + query: The top-level [OpenSearch Query DSL](https://docs.opensearch.org/latest/query-dsl/) + clause -- see [Query][synapseclient.models.search_dsl.Query]; + restricted server-side to `prefix`, `match_phrase_prefix`, or `match_bool_prefix`. - source: Optional source filter selecting which columns are returned - on each hit. + source: Optional [source filter](https://docs.opensearch.org/latest/search-plugins/searching-data/retrieve-specific-fields/) + selecting which columns are returned on each hit. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. diff --git a/synapseclient/models/search_dsl.py b/synapseclient/models/search_dsl.py new file mode 100644 index 000000000..96e132d8b --- /dev/null +++ b/synapseclient/models/search_dsl.py @@ -0,0 +1,1205 @@ +"""Typed shapes for the OpenSearch query DSL accepted by Synapse. + +These `TypedDict` types mirror the server-side schema for a SearchIndex query +one-for-one, so a dict literal written inline against a +[SearchQuery][synapseclient.models.SearchQuery] slot is checked by your IDE +against the exact shape the Synapse REST API expects. + +Every type is `total=False`: all keys are optional to the type checker, and the +per-field docstrings mark which ones the server requires. A `TypedDict` is a +plain `dict` at runtime, so these annotations add no validation and no overhead, +and a raw dict is always an acceptable value. + +Start at [Query][synapseclient.models.search_dsl.Query] for the query clause +kinds and [Aggregation][synapseclient.models.search_dsl.Aggregation] for the +aggregation kinds. Both link out to the relevant +[OpenSearch query DSL](https://docs.opensearch.org/latest/query-dsl/) reference +for each clause. + +Background reading: + +- [OpenSearch query DSL](https://docs.opensearch.org/latest/query-dsl/) +- [Query vs. filter context](https://docs.opensearch.org/latest/query-dsl/query-filter-context/) +- [Term-level vs. full-text queries](https://docs.opensearch.org/latest/query-dsl/term-vs-full-text/) +- [Aggregations](https://docs.opensearch.org/latest/aggregations/) +- [The `_search` API](https://docs.opensearch.org/latest/api-reference/search-apis/search/) +""" + +from typing import Any, Dict, List, TypedDict, Union + +ScalarValue = Union[str, int, float, bool] +"""A single column value. Which of these is valid depends on the target +column's type in the SearchIndex schema.""" + + +AnalyzerRef = TypedDict("AnalyzerRef", {"$ref": str}, total=False) +"""A reference to a saved [TextAnalyzer][synapseclient.models.TextAnalyzer] or +[SynonymSet][synapseclient.models.SynonymSet] by its qualified name +`{organizationName}-{name}`, written as `{"$ref": "my.org-stemmed_english"}`.""" + + +class ClauseScoringOptions(TypedDict, total=False): + """Scoring options shared by the OpenSearch query clauses: a relevance + `boost` and a `_name` label.""" + + boost: float + """Optional. Multiplier for the relevance score of this clause. + Default `1.0`.""" + + _name: str + """Optional. Label echoed back in matched-queries metadata.""" + + +class FuzzyMatchOptions(TypedDict, total=False): + """Fuzzy-matching parameters shared by the analyzed full-text match clauses + (`match`, `match_bool_prefix`, `multi_match`).""" + + fuzziness: Union[int, str] + """Optional. Allowed [edit distance](https://docs.opensearch.org/latest/query-dsl/term/fuzzy/): + an integer or `AUTO`.""" + + fuzzy_rewrite: str + """Optional. How the fuzzy query is rewritten internally.""" + + fuzzy_transpositions: bool + """Optional. Whether to count transpositions (ab -> ba) as a single edit. + Default `true`.""" + + prefix_length: int + """Optional. Number of leading characters left unchanged when fuzzy + matching.""" + + +class MinimumShouldMatchOption(TypedDict, total=False): + """The minimum-should-match option shared by the analyzed full-text match + clauses.""" + + minimum_should_match: Union[int, str] + """Optional. [Minimum number of terms](https://docs.opensearch.org/latest/query-dsl/minimum-should-match/) + a document must match. An integer or a percentage / formula string.""" + + +class ZeroTermsQueryOption(TypedDict, total=False): + """The zero-terms-query behavior shared by the analyzed full-text match + clauses.""" + + zero_terms_query: str + """Optional. Behavior when the analyzer removes all tokens: `none` + (default) or `all`.""" + + +class MatchFieldOptions( + ClauseScoringOptions, + FuzzyMatchOptions, + MinimumShouldMatchOption, + ZeroTermsQueryOption, + total=False, +): + """Per-field options for a [`match`](https://docs.opensearch.org/latest/query-dsl/full-text/match/) + full-text clause. Carried as the value of the field-keyed `match` map (the + map key is the column name).""" + + query: ScalarValue + """Required. The text (or scalar value on a non-text column) to match. A + string, number, or boolean depending on the target column type.""" + + operator: str + """Optional. Boolean logic used to combine the analyzed query terms: `or` + (default) or `and`.""" + + analyzer: str + """Optional. Analyzer used to tokenize the query text. Defaults to the + field's search analyzer.""" + + max_expansions: int + """Optional. Maximum number of terms the fuzzy expansion will generate.""" + + cutoff_frequency: float + """Optional. Term-frequency threshold above which terms are treated as + low-importance.""" + + auto_generate_synonyms_phrase_query: bool + """Optional. Whether to auto-generate phrase queries for multi-term + synonyms. Default `true`.""" + + lenient: bool + """Optional. When `true`, format-based errors (e.g. a text value against a + numeric field) are ignored.""" + + +class MatchPhraseFieldOptions(ClauseScoringOptions, ZeroTermsQueryOption, total=False): + """Per-field options for a [`match_phrase`](https://docs.opensearch.org/latest/query-dsl/full-text/match-phrase/) + clause. Carried as the value of the field-keyed `match_phrase` map (the map + key is the column name).""" + + query: ScalarValue + """Required. The phrase to match. A string (or scalar value on a non-text + column).""" + + analyzer: str + """Optional. Analyzer used to tokenize the phrase. Defaults to the field's + search analyzer.""" + + slop: int + """Optional. Number of positions allowed between matching terms. Default + `0` (exact phrase).""" + + +class MatchPhrasePrefixFieldOptions( + ClauseScoringOptions, ZeroTermsQueryOption, total=False +): + """Per-field options for a [`match_phrase_prefix`](https://docs.opensearch.org/latest/query-dsl/full-text/match-phrase-prefix/) + clause. Carried as the value of the field-keyed `match_phrase_prefix` map + (the map key is the column name).""" + + query: ScalarValue + """Required. The phrase whose last term is treated as a prefix. A + string.""" + + analyzer: str + """Optional. Analyzer used to tokenize the phrase. Defaults to the field's + search analyzer.""" + + slop: int + """Optional. Number of positions allowed between matching terms. Default + `0`.""" + + max_expansions: int + """Optional. Maximum number of terms the last (prefix) term expands into. + Default `50`.""" + + +class MatchBoolPrefixFieldOptions( + ClauseScoringOptions, FuzzyMatchOptions, MinimumShouldMatchOption, total=False +): + """Per-field options for a [`match_bool_prefix`](https://docs.opensearch.org/latest/query-dsl/full-text/match-bool-prefix/) + clause. Carried as the value of the field-keyed `match_bool_prefix` map + (the map key is the column name).""" + + query: ScalarValue + """Required. The text whose terms are matched, with the final term treated + as a prefix. A string.""" + + operator: str + """Optional. Boolean logic used to combine the analyzed terms: `or` + (default) or `and`.""" + + analyzer: str + """Optional. Analyzer used to tokenize the query text. Defaults to the + field's search analyzer.""" + + max_expansions: int + """Optional. Maximum number of terms the final (prefix) term expands into. + Default `50`.""" + + +class TermFieldOptions(ClauseScoringOptions, total=False): + """Per-field options for a [`term`](https://docs.opensearch.org/latest/query-dsl/term/term/) + term-level clause (exact, non-analyzed match). Carried as the value of the + field-keyed `term` map (the map key is the column name).""" + + value: ScalarValue + """Required. The exact value to match. A string, number, boolean, or date + depending on the target column type.""" + + case_insensitive: bool + """Optional. When `true`, matches the value regardless of case. Default + `false`.""" + + +class RangeFieldOptions(ClauseScoringOptions, total=False): + """Per-field options for a [`range`](https://docs.opensearch.org/latest/query-dsl/term/range/) + term-level clause. Carried as the value of the field-keyed `range` map (the + map key is the column name).""" + + gte: ScalarValue + """Optional. Greater-than-or-equal-to bound. A number or date string, per + the target column type.""" + + gt: ScalarValue + """Optional. Greater-than bound.""" + + lte: ScalarValue + """Optional. Less-than-or-equal-to bound.""" + + lt: ScalarValue + """Optional. Less-than bound.""" + + format: str + """Optional. Date format used to parse the bound values on a date + column.""" + + relation: str + """Optional. How the range relates to range-typed field values: + `INTERSECTS` (default), `CONTAINS`, or `WITHIN`.""" + + time_zone: str + """Optional. UTC offset or IANA zone used to interpret date bounds.""" + + +class PrefixFieldOptions(ClauseScoringOptions, total=False): + """Per-field options for a [`prefix`](https://docs.opensearch.org/latest/query-dsl/term/prefix/) + term-level clause. Carried as the value of the field-keyed `prefix` map + (the map key is the column name). A leading `*` or `?` in `value` is + rejected (it forces a full index scan).""" + + value: ScalarValue + """Required. The prefix the indexed term must start with. A string (or + scalar value on a non-text column).""" + + case_insensitive: bool + """Optional. When `true`, matches the prefix regardless of case. Default + `false`.""" + + rewrite: str + """Optional. How the multi-term query is rewritten internally.""" + + +class WildcardFieldOptions(ClauseScoringOptions, total=False): + """Per-field options for a [`wildcard`](https://docs.opensearch.org/latest/query-dsl/term/wildcard/) + term-level clause. Carried as the value of the field-keyed `wildcard` map + (the map key is the column name). A leading `*` or `?` in the pattern is + rejected (it forces a full index scan).""" + + value: ScalarValue + """Optional. The wildcard pattern (`*` matches any sequence, `?` matches a + single character). A string. Either `value` or `wildcard` supplies the + pattern.""" + + wildcard: ScalarValue + """Optional. Alias for `value` -- the wildcard pattern. A string.""" + + case_insensitive: bool + """Optional. When `true`, matches the pattern regardless of case. Default + `false`.""" + + rewrite: str + """Optional. How the multi-term query is rewritten internally.""" + + +class FuzzyFieldOptions(ClauseScoringOptions, total=False): + """Per-field options for a [`fuzzy`](https://docs.opensearch.org/latest/query-dsl/term/fuzzy/) + term-level clause. Carried as the value of the field-keyed `fuzzy` map (the + map key is the column name).""" + + value: ScalarValue + """Required. The term to match within the allowed edit distance. A string + (or scalar value on a non-text column).""" + + fuzziness: Union[int, str] + """Optional. Allowed [edit distance](https://docs.opensearch.org/latest/query-dsl/term/fuzzy/): + an integer or `AUTO`.""" + + max_expansions: int + """Optional. Maximum number of terms the fuzzy expansion will generate. + Default `50`.""" + + prefix_length: int + """Optional. Number of leading characters left unchanged when fuzzy + matching.""" + + transpositions: bool + """Optional. Whether to count transpositions (ab -> ba) as a single edit. + Default `true`.""" + + rewrite: str + """Optional. How the multi-term query is rewritten internally.""" + + +class ExistsQuery(ClauseScoringOptions, total=False): + """An [`exists`](https://docs.opensearch.org/latest/query-dsl/term/exists/) + term-level clause. Matches documents that have any non-null value for the + given column.""" + + field: str + """Required. The column that must have a value.""" + + +class MultiMatchQuery( + ClauseScoringOptions, + FuzzyMatchOptions, + MinimumShouldMatchOption, + ZeroTermsQueryOption, + total=False, +): + """A [`multi_match`](https://docs.opensearch.org/latest/query-dsl/full-text/multi-match/) + full-text clause -- a `match` run across several columns at once.""" + + query: ScalarValue + """Required. The text to match across the listed columns. A string.""" + + fields: List[str] + """Required. The columns to search. Each entry may carry a `^boost` suffix + (e.g. `title^2`).""" + + type: str + """Optional. How the per-field matches are combined: `best_fields` + (default), `most_fields`, `cross_fields`, `phrase`, `phrase_prefix`, or + `bool_prefix`.""" + + operator: str + """Optional. Boolean logic used to combine the analyzed terms: `or` + (default) or `and`.""" + + tie_breaker: float + """Optional. Weight (0-1) applied to non-best field scores in + `best_fields` / `cross_fields`.""" + + analyzer: str + """Optional. Analyzer used to tokenize the query text. Defaults to each + field's search analyzer.""" + + max_expansions: int + """Optional. Maximum number of terms a fuzzy / prefix expansion will + generate. Default `50`.""" + + slop: int + """Optional. Number of positions allowed between matching terms for the + phrase types.""" + + cutoff_frequency: float + """Optional. Term-frequency threshold above which terms are treated as + low-importance.""" + + auto_generate_synonyms_phrase_query: bool + """Optional. Whether to auto-generate phrase queries for multi-term + synonyms. Default `true`.""" + + lenient: bool + """Optional. When `true`, format-based errors are ignored.""" + + +class SimpleQueryStringQuery( + ClauseScoringOptions, MinimumShouldMatchOption, total=False +): + """A [`simple_query_string`](https://docs.opensearch.org/latest/query-dsl/full-text/simple-query-string/) + full-text clause -- a compact mini-DSL (`+`, `|`, `-`, `"`, `*`, `()`) + parsed leniently across the listed columns.""" + + query: str + """Required. The simple-query-string expression.""" + + fields: List[str] + """Optional. The columns to search. Each entry may carry a `^boost` + suffix. Defaults to the index's default search fields.""" + + default_operator: str + """Optional. Boolean logic used between terms when no explicit operator is + given: `or` (default) or `and`.""" + + flags: str + """Optional. Pipe-delimited list of enabled syntax features (e.g. + `AND|OR|PREFIX`), or `ALL` / `NONE`.""" + + analyzer: str + """Optional. Analyzer used to tokenize the query text. Defaults to each + field's search analyzer.""" + + analyze_wildcard: bool + """Optional. Whether to analyze wildcard terms. Default `false`. A leading + wildcard with this enabled is rejected (it forces a full index scan).""" + + auto_generate_synonyms_phrase_query: bool + """Optional. Whether to auto-generate phrase queries for multi-term + synonyms. Default `true`.""" + + fuzzy_max_expansions: int + """Optional. Maximum number of terms a fuzzy expansion will generate. + Default `50`.""" + + fuzzy_prefix_length: int + """Optional. Number of leading characters left unchanged when fuzzy + matching.""" + + fuzzy_transpositions: bool + """Optional. Whether to count transpositions (ab -> ba) as a single edit. + Default `true`.""" + + lenient: bool + """Optional. When `true`, format-based errors are ignored.""" + + quote_field_suffix: str + """Optional. Suffix appended to field names for quoted (exact-phrase) + portions of the query.""" + + +class MatchAllQuery(ClauseScoringOptions, total=False): + """A [`match_all`](https://docs.opensearch.org/latest/query-dsl/match-all/) + clause. Matches every document. Use `{"match_all": {}}` to match all + documents.""" + + +class BoolQuery(ClauseScoringOptions, total=False): + """A [`bool`](https://docs.opensearch.org/latest/query-dsl/compound/bool/) + compound clause -- combines sub-clauses with boolean logic.""" + + must: List["Query"] + """Sub-clauses that must all match (scored). Logical AND.""" + + should: List["Query"] + """Sub-clauses that should match (scored). See `minimum_should_match`.""" + + must_not: List["Query"] + """Sub-clauses that must not match (filter context, not scored).""" + + filter: List["Query"] + """Sub-clauses that must all match in + [filter context](https://docs.opensearch.org/latest/query-dsl/query-filter-context/) + (not scored).""" + + minimum_should_match: Union[int, str] + """Optional. How many `should` clauses must match. An integer or a + [percentage / formula string](https://docs.opensearch.org/latest/query-dsl/minimum-should-match/).""" + + adjust_pure_negative: bool + """Optional. Whether to automatically add a `match_all` when only negative + clauses are present. Default `true`.""" + + +class DisMaxQuery(ClauseScoringOptions, total=False): + """A [`dis_max`](https://docs.opensearch.org/latest/query-dsl/compound/disjunction-max/) + compound clause. A document matches if any sub-clause matches; its score is + the best single sub-clause score plus `tie_breaker` times the rest.""" + + queries: List["Query"] + """Required. The candidate clauses.""" + + tie_breaker: float + """Optional. Weight (0-1) applied to the scores of the non-best matching + clauses. Default `0.0`.""" + + +class ConstantScoreQuery(ClauseScoringOptions, total=False): + """A [`constant_score`](https://docs.opensearch.org/latest/query-dsl/compound/constant-score/) + compound clause. Wraps a filter and assigns every matching document the + same score (`boost`).""" + + filter: "Query" + """Required. The clause evaluated in + [filter context](https://docs.opensearch.org/latest/query-dsl/query-filter-context/).""" + + +class BoostingQuery(ClauseScoringOptions, total=False): + """A [`boosting`](https://docs.opensearch.org/latest/query-dsl/compound/boosting/) + compound clause. Returns documents matching `positive`, demoting those that + also match `negative` by `negative_boost`.""" + + positive: "Query" + """Required. The clause documents must match.""" + + negative: "Query" + """Required. The clause whose matches are demoted.""" + + negative_boost: float + """Required. Multiplier (0-1) applied to the score of documents that also + match `negative`.""" + + +class Query(TypedDict, total=False): + """A single [OpenSearch query DSL](https://docs.opensearch.org/latest/query-dsl/) + clause. Exactly one of the keys below may be set -- the set key names the + clause kind. See [query vs. filter context](https://docs.opensearch.org/latest/query-dsl/query-filter-context/) + and [term-level vs. full-text queries](https://docs.opensearch.org/latest/query-dsl/term-vs-full-text/). + + The field-keyed leaf clauses (`match`, `term`, `range`, ...) are maps whose + key is the column name and whose value is the per-field options object -- + only the long form is accepted (e.g. `{"match": {"title": {"query": "x"}}}`, + not the `{"match": {"title": "x"}}` shorthand). The compound clauses + (`bool`, `dis_max`, ...) nest further Query DSL clauses recursively. + """ + + match: Dict[str, MatchFieldOptions] + """A [`match`](https://docs.opensearch.org/latest/query-dsl/full-text/match/) + full-text clause. Map of column name to its match options.""" + + match_phrase: Dict[str, MatchPhraseFieldOptions] + """A [`match_phrase`](https://docs.opensearch.org/latest/query-dsl/full-text/match-phrase/) + clause. Map of column name to its phrase options.""" + + match_phrase_prefix: Dict[str, MatchPhrasePrefixFieldOptions] + """A [`match_phrase_prefix`](https://docs.opensearch.org/latest/query-dsl/full-text/match-phrase-prefix/) + clause. Map of column name to its options.""" + + match_bool_prefix: Dict[str, MatchBoolPrefixFieldOptions] + """A [`match_bool_prefix`](https://docs.opensearch.org/latest/query-dsl/full-text/match-bool-prefix/) + clause. Map of column name to its options.""" + + term: Dict[str, TermFieldOptions] + """A [`term`](https://docs.opensearch.org/latest/query-dsl/term/term/) + term-level clause. Map of column name to its term options.""" + + range: Dict[str, RangeFieldOptions] + """A [`range`](https://docs.opensearch.org/latest/query-dsl/term/range/) + term-level clause. Map of column name to its range bounds.""" + + prefix: Dict[str, PrefixFieldOptions] + """A [`prefix`](https://docs.opensearch.org/latest/query-dsl/term/prefix/) + term-level clause. Map of column name to its prefix options.""" + + wildcard: Dict[str, WildcardFieldOptions] + """A [`wildcard`](https://docs.opensearch.org/latest/query-dsl/term/wildcard/) + term-level clause. Map of column name to its wildcard options.""" + + fuzzy: Dict[str, FuzzyFieldOptions] + """A [`fuzzy`](https://docs.opensearch.org/latest/query-dsl/term/fuzzy/) + term-level clause. Map of column name to its fuzzy options.""" + + terms: Dict[str, Any] + """A [`terms`](https://docs.opensearch.org/latest/query-dsl/term/terms/) + term-level clause (matches any of several exact values). Field-keyed: + `{"terms": {"": [v1, v2], "boost": 1.0}}`. The cross-index + `terms`-lookup form is rejected. Untyped because the column name is itself a + key alongside the fixed option keys, which a `TypedDict` cannot express; the + same allowlist is enforced server-side.""" + + exists: ExistsQuery + """An [`exists`](https://docs.opensearch.org/latest/query-dsl/term/exists/) + term-level clause.""" + + multi_match: MultiMatchQuery + """A [`multi_match`](https://docs.opensearch.org/latest/query-dsl/full-text/multi-match/) + full-text clause.""" + + simple_query_string: SimpleQueryStringQuery + """A [`simple_query_string`](https://docs.opensearch.org/latest/query-dsl/full-text/simple-query-string/) + full-text clause.""" + + match_all: MatchAllQuery + """A [`match_all`](https://docs.opensearch.org/latest/query-dsl/match-all/) + clause.""" + + bool: BoolQuery + """A [`bool`](https://docs.opensearch.org/latest/query-dsl/compound/bool/) + compound clause -- combines sub-clauses with boolean logic.""" + + dis_max: DisMaxQuery + """A [`dis_max`](https://docs.opensearch.org/latest/query-dsl/compound/disjunction-max/) + compound clause.""" + + constant_score: ConstantScoreQuery + """A [`constant_score`](https://docs.opensearch.org/latest/query-dsl/compound/constant-score/) + compound clause.""" + + boosting: BoostingQuery + """A [`boosting`](https://docs.opensearch.org/latest/query-dsl/compound/boosting/) + compound clause.""" + + +class ExtendedBounds(TypedDict, total=False): + """Min/max bounds that force a [`histogram`](https://docs.opensearch.org/latest/aggregations/bucket/histogram/) + or [`date_histogram`](https://docs.opensearch.org/latest/aggregations/bucket/date-histogram/) + to emit buckets across the full range (used as `extended_bounds` or + `hard_bounds`). Bounding the range is what caps the bucket count.""" + + min: ScalarValue + """Lower bound. A number, or a date / date-math string on a + `date_histogram`.""" + + max: ScalarValue + """Upper bound. A number, or a date / date-math string on a + `date_histogram`.""" + + +class HistogramBoundsOptions(TypedDict, total=False): + """The extended/hard bounds options shared by the `histogram` and + `date_histogram` aggregations.""" + + extended_bounds: ExtendedBounds + """Optional. Forces buckets to span at least this min/max range.""" + + hard_bounds: ExtendedBounds + """Optional. Restricts buckets to this min/max range (values outside are + dropped).""" + + +class KeyedBucketOption(TypedDict, total=False): + """The keyed-output option shared by the bucketing aggregations.""" + + keyed: bool + """Optional. Whether to return buckets as a keyed object rather than an + array.""" + + +class MetricAggregation(TypedDict, total=False): + """Common options for the single-value numeric metric aggregations (`avg`, + `max`, `min`, `sum`).""" + + field: str + """Required. The numeric column to aggregate.""" + + format: str + """Optional. Format applied to the result value.""" + + +class MissingValueOption(TypedDict, total=False): + """The missing-value substitution option shared by the metric + aggregations.""" + + missing: ScalarValue + """Optional. Value substituted for documents missing `field`.""" + + +BucketOrder = Union[Dict[str, str], List[Dict[str, str]]] +"""Bucket sort order for a bucketing aggregation -- a `{metric: "asc|desc"}` +object or an array of them, applied in order.""" + + +class TermsAggregation(TypedDict, total=False): + """A [`terms`](https://docs.opensearch.org/latest/aggregations/bucket/terms/) + bucket aggregation -- one bucket per distinct value of `field`.""" + + field: str + """Required. The column to bucket by.""" + + size: int + """Optional. Maximum number of buckets to return. Capped server-side.""" + + shard_size: int + """Optional. Number of candidate buckets collected per shard before the + final reduce. Capped server-side.""" + + min_doc_count: int + """Optional. Minimum document count for a bucket to be returned. Default + `1`.""" + + shard_min_doc_count: int + """Optional. Per-shard minimum document count before a bucket is + considered.""" + + show_term_doc_count_error: bool + """Optional. Whether to return the per-bucket document-count error + bound.""" + + order: BucketOrder + """Optional. Bucket sort order -- a `{metric: "asc|desc"}` object or an + array of them.""" + + include: Union[str, List[ScalarValue]] + """Optional. Terms to include -- a regex string or an array of exact + values.""" + + exclude: Union[str, List[ScalarValue]] + """Optional. Terms to exclude -- a regex string or an array of exact + values.""" + + missing: ScalarValue + """Optional. Bucket value assigned to documents missing `field`.""" + + collect_mode: str + """Optional. `breadth_first` or `depth_first` sub-aggregation collection + strategy.""" + + execution_hint: str + """Optional. Internal execution strategy hint (`map` / + `global_ordinals`).""" + + format: str + """Optional. Format applied to the bucket key in the response.""" + + value_type: str + """Optional. Explicit value type for the field when it cannot be + inferred.""" + + +class HistogramAggregation(KeyedBucketOption, HistogramBoundsOptions, total=False): + """A [`histogram`](https://docs.opensearch.org/latest/aggregations/bucket/histogram/) + bucket aggregation over a numeric column. Must specify `extended_bounds` or + `hard_bounds` so the bucket count is bounded.""" + + field: str + """Required. The numeric column to bucket.""" + + interval: float + """Required. Bucket width. Must be positive.""" + + min_doc_count: int + """Optional. Minimum document count for a bucket to be returned.""" + + offset: float + """Optional. Shifts bucket boundaries by this amount.""" + + order: BucketOrder + """Optional. Bucket sort order -- a `{metric: "asc|desc"}` object or an + array of them.""" + + missing: ScalarValue + """Optional. Bucket value assigned to documents missing `field`.""" + + format: str + """Optional. Format applied to the bucket key in the response.""" + + +class DateHistogramAggregation(KeyedBucketOption, HistogramBoundsOptions, total=False): + """A [`date_histogram`](https://docs.opensearch.org/latest/aggregations/bucket/date-histogram/) + bucket aggregation over a date column. Must specify `extended_bounds` or + `hard_bounds` so the bucket count is bounded.""" + + field: str + """Required. The date column to bucket.""" + + calendar_interval: str + """Optional. Calendar-aware interval (e.g. `month`, `year`). Mutually + exclusive with `fixed_interval`.""" + + fixed_interval: str + """Optional. Fixed-duration interval (e.g. `30d`, `12h`). Mutually + exclusive with `calendar_interval`.""" + + interval: str + """Optional. Legacy interval (use `calendar_interval` / `fixed_interval` + instead).""" + + min_doc_count: int + """Optional. Minimum document count for a bucket to be returned.""" + + offset: str + """Optional. Shifts bucket boundaries by this duration.""" + + time_zone: str + """Optional. UTC offset or IANA zone used to compute bucket boundaries.""" + + order: BucketOrder + """Optional. Bucket sort order -- a `{metric: "asc|desc"}` object or an + array of them.""" + + missing: ScalarValue + """Optional. Bucket value assigned to documents missing `field`.""" + + format: str + """Optional. Date format applied to the bucket key in the response.""" + + +class RangeAggregation(KeyedBucketOption, total=False): + """A [`range`](https://docs.opensearch.org/latest/aggregations/bucket/range/) + bucket aggregation -- one bucket per caller-defined numeric range.""" + + field: str + """Required. The numeric column to bucket.""" + + ranges: List[Dict[str, Any]] + """Required. The [bucket ranges](https://docs.opensearch.org/latest/aggregations/bucket/range/), + each `{"from": , "to": , "key": "