Repository: opensearch-project/opensearch-py
Pull request #798 (2024-08-15 14:48:44)

Fix integration tests with unreleased versions of OpenSearch.
### Description

Use a different JDK on 2.x and main.

### Issues Resolved
_List any issues this PR will resolve, e.g. Closes [...]._ 

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin).



Patch:
diff --git a/.github/workflows/integration-unreleased.yml b/.github/workflows/integration-unreleased.yml
index 2d5b5b95..7f29e7c2 100644
--- a/.github/workflows/integration-unreleased.yml
+++ b/.github/workflows/integration-unreleased.yml
@@ -8,14 +8,17 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        opensearch_ref: [ '1.x', '2.x', 'main' ]
-        
+        entry:
+          - { branch: "1.x", java-version: "11" }
+          - { branch: "2.x", java-version: "17" }
+          - { branch: "main", java-version: "17" }
+
     steps:
       - name: Checkout OpenSearch
         uses: actions/checkout@v4
         with:
           repository: opensearch-project/opensearch
-          ref: ${{ matrix.opensearch_ref }}
+          ref: ${{ matrix.entry.branch }}
           path: opensearch
 
       - name: Get OpenSearch branch top
@@ -32,8 +35,8 @@ jobs:
 
       - uses: actions/setup-java@v4
         with:
-          distribution: 'temurin' # See 'Supported distributions' for available options
-          java-version: '11'
+          distribution: "temurin"
+          java-version: ${{ matrix.entry.java-version }}
 
       - name: Assemble OpenSearch
         if: steps.cache-restore.outputs.cache-hit != 'true'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e108c52..39f10e00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 
 ## [Unreleased]
 ### Added
+- Added support for the `multi_terms` bucket aggregation ([#797](https://github.com/opensearch-project/opensearch-py/pull/797))
 ### Changed
 - Removed deprecated `numpy.float_` and update NumPy/Pandas imports ([#762](https://github.com/opensearch-project/opensearch-py/pull/762))
 - Removed workaround for [aiohttp#1769](https://github.com/aio-libs/aiohttp/issues/1769) ([#794](https://github.com/opensearch-project/opensearch-py/pull/794))
@@ -191,6 +192,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 - Improved CI performance of integration with unreleased OpenSearch ([#318](https://github.com/opensearch-project/opensearch-py/pull/318))
 - Added k-NN guide and samples ([#449](https://github.com/opensearch-project/opensearch-py/pull/449))
 - Added the ability to run tests matching a pattern to `.ci/run-tests` ([#454](https://github.com/opensearch-project/opensearch-py/pull/454))
+- Added a guide for taking snapshots ([#486](https://github.com/opensearch-project/opensearch-py/pull/429))
 ### Changed
 - Moved security from `plugins` to `clients` ([#442](https://github.com/opensearch-project/opensearch-py/pull/442))
 - Updated Security Client APIs ([#450](https://github.com/opensearch-project/opensearch-py/pull/450))
diff --git a/USER_GUIDE.md b/USER_GUIDE.md
index 78aa6656..5f38b535 100644
--- a/USER_GUIDE.md
+++ b/USER_GUIDE.md
@@ -153,6 +153,7 @@ print(response)
 - [Search](guides/search.md)
 - [Point in Time](guides/point_in_time.md)
 - [Using a Proxy](guides/proxy.md)
+- [Working with Snapshots](guides/snapshot.md)
 - [Index Templates](guides/index_template.md)
 - [Advanced Index Actions](guides/advanced_index_actions.md)
 - [Making Raw JSON REST Requests](guides/json.md)
diff --git a/guides/snapshot.md b/guides/snapshot.md
new file mode 100644
index 00000000..0630fc94
--- /dev/null
+++ b/guides/snapshot.md
@@ -0,0 +1,145 @@
+# Table of Contents
+- [Snapshot Actions](#snapshot-actions)
+  - [Setup](#setup)
+  - [API Actions](#api-actions)
+    - [Create Snapshot Repository](#create-snapshot-repository)
+    - [Create Snapshot](#create-snapshot)
+    - [Verify Snapshot Repository](#verify-snapshot-repository)
+    - [Delete Snapshot](#delete-snapshot)
+    - [Restore Snapshot](#restore-snapshot)
+    - [Get Snapshot Status](#get-snapshot-status)
+    - [Clone Snapshot](#clone-snapshot)
+    - [Get Snapshot](#get-snapshot)
+    - [Get Repository](#get-repository)
+    - [Repository Analyze](#repository-analyze)
+  - [Cleanup](#cleanup)
+
+# Snapshot Actions
+In this guide, we will look at some snapshot actions that allow you to manage and work with snapshots of your indices.
+
+A complete working sample for this guide can be found in [samples/snapshot](../samples/snapshot).
+
+## Setup
+Let's create a client instance, and an index named `movies`:
+```python
+from opensearchpy import OpenSearch
+
+host = 'localhost'
+port = 9200
+auth = ('admin', 'admin') # For testing only. Don't store credentials in code.
+
+client = OpenSearch(
+    hosts = [{'host': host, 'port': port}],
+    http_auth = auth,
+    use_ssl = True,
+    verify_certs = False,
+    ssl_show_warn = False
+)
+
+print(client.info())  # Check server info and make sure the client is connected
+client.indices.create(index='movies')
+```
+## API Actions
+### Create Snapshot Repository
+Before taking a snapshot, you need to create a snapshot repository to store the snapshots. You can use the `create_repository` API action for this purpose. The following example creates a snapshot repository named `my_repository`:
+
+```python
+repo_body = {
+    "type": "fs",  # Replace 'fs' with the appropriate repository type
+    "settings": {
+        "location": "/path/to/repo", 
+    }
+}
+
+# Create the snapshot repository and capture the response
+response = client.snapshot.create_repository(repository='my_repository', body=repo_body)
+
+# Print the response to see the result
+print(response)
+```
+
+### Create Snapshot
+To create a snapshot of an index, you can use the `create` method from the `snapshot` API. The following example creates a snapshot named `my_snapshot` for the movies index:
+
+```python
+client.snapshot.create(repository='my_repository', snapshot='my_snapshot', body={"indices": "movies"})
+```
+
+### Verify Snapshot Repository
+The `verify_repository` API action allows you to verify a snapshot repository. Verifying a repository ensures that it is accessible and operational, but it does not validate the integrity of the snapshots stored within the repository. The following example verifies `my_repository`:
+
+```python
+response = client.snapshot.verify_repository(repository='my_repository')
+
+# Print the HTTP status code
+print("HTTP Status Code:", response.status_code)
+
+# Print the response content
+print("Response Content:", response.content)
+```
+
+### Delete Snapshot
+To delete a specific snapshot, use the `delete` API action:
+
+```python
+client.snapshot.delete(repository='my_repository', snapshot='my_snapshot')
+```
+### Restore Snapshot
+To restore a snapshot and recreate the indices, mappings, and data, you can use the `restore` method. The following example restores the `my_snapshot` snapshot:
+
+```python
+response = client.snapshot.restore(repository='my_repository', snapshot='my_snapshot')
+```
+
+### Get Snapshot Status
+To check the status of a snapshot, you can use the `status` method.
+
+```python
+response = client.snapshot.status(repository='my_repository', snapshot='my_snapshot')
+```
+
+### Clone Snapshot
+You can clone an existing snapshot to create a new snapshot with the same contents. The `clone` operation allows you to create multiple copies of a snapshot, which can be useful for backup retention or creating snapshots for different purposes. The following example clones a snapshot named `my_snapshot` to create a new snapshot named `my_snapshot_clone`:
+
+```python
+client.snapshot.clone(
+    repository='my_repository',
+    snapshot='my_snapshot',
+    target_snapshot='my_snapshot_clone'
+)
+```
+## Get Snapshot
+To retrieve information about a specific snapshot, you can use the `get` API action. It provides metadata such as the snapshot's status, indices included in the snapshot, and the timestamp when the snapshot was taken. The following example retrieves information about the `my_snapshot`:
+
+```python
+response = client.snapshot.get(
+    repository='my_repository',
+    snapshot='my_snapshot'
+)
+
+# Print the response to see the result
+print(response)
+```
+
+## Get Repository
+To retrieve information about a snapshot repository, you can use the `get_repository` API action. It provides details about the configured repository, including its type and settings. The following example retrieves information about the `my_repository`:
+
+```python
+response = client.snapshot.get_repository(repository='my_repository')
+```
+
+## Repository Analyze
+The `repository_analyze` API action allows you to analyze a snapshot repository for correctness and performance. It checks for any inconsistencies or corruption in the repository. The following example performs a repository analysis on `my_repository`:
+
+```python
+response = client.snapshot.repository_analyze(repository='my_repository')
+```
+
+## Cleanup
+
+Finally, let's delete the `movies` index and clean up all the snapshots and the repository:
+```python
+client.indices.delete(index='movies')
+client.snapshot.delete(repository='my_repository', snapshot='my_snapshot')
+client.snapshot.delete_repository(repository='my_repository')
+```
\ No newline at end of file
diff --git a/opensearchpy/helpers/aggs.py b/opensearchpy/helpers/aggs.py
index 0133287f..b3be3614 100644
--- a/opensearchpy/helpers/aggs.py
+++ b/opensearchpy/helpers/aggs.py
@@ -308,6 +308,10 @@ class VariableWidthHistogram(Bucket):
         return FieldBucketData(self, search, data)
 
 
+class MultiTerms(Bucket):
+    name = "multi_terms"
+
+
 # metric aggregations
 class TopHits(Agg):
     name = "top_hits"
diff --git a/samples/snapshot/Dockerfile b/samples/snapshot/Dockerfile
new file mode 100644
index 00000000..b2f22e3b
--- /dev/null
+++ b/samples/snapshot/Dockerfile
@@ -0,0 +1,9 @@
+FROM opensearchproject/opensearch:2.11.0
+
+ARG OPENSEARCH_HOME=/usr/share/opensearch
+ARG UID=1000
+ARG GID=1000
+
+RUN echo 'path.repo: ["/usr/share/opensearch/backups"]' >> $OPENSEARCH_HOME/config/opensearch.yml
+RUN mkdir -p $OPENSEARCH_HOME/backups 
+RUN chown -Rv $UID:$GID $OPENSEARCH_HOME/backups
diff --git a/samples/snapshot/README.md b/samples/snapshot/README.md
new file mode 100644
index 00000000..ff947425
--- /dev/null
+++ b/samples/snapshot/README.md
@@ -0,0 +1,8 @@
+Run this sample as follows.
+
+```
+cd samples
+docker run --rm -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" -it $(docker build -q snapshot )
+poetry install
+poetry run python snapshot/snapshot_sample.py 
+```
diff --git a/samples/snapshot/snapshot_sample.py b/samples/snapshot/snapshot_sample.py
new file mode 100644
index 00000000..ac512ed0
--- /dev/null
+++ b/samples/snapshot/snapshot_sample.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+
+# SPDX-License-Identifier: Apache-2.0
+#
+# The OpenSearch Contributors require contributions made to
+# this file be licensed under the Apache-2.0 license or a
+# compatible open source license.
+#
+# Modifications Copyright OpenSearch Contributors. See
+# GitHub history for details.
+
+import os
+import tempfile
+
+from opensearchpy import OpenSearch
+
+# connect to OpenSearch
+
+HOST = "localhost"
+PORT = 9200
+auth = (
+    "admin",
+    os.getenv("OPENSEARCH_PASSWORD", "admin"),
+)  # For testing only. Don't store credentials in code.
+
+client = OpenSearch(
+    hosts=[{"host": HOST, "port": PORT}],
+    http_auth=auth,
+    use_ssl=True,
+    verify_certs=False,
+    ssl_show_warn=False,
+)
+
+# Create an index
+
+INDEX_NAME = "test-snapshot"
+client.indices.create(index=INDEX_NAME)
+
+# Create a temporary directory for the snapshot repository
+temp_repo = tempfile.TemporaryDirectory()
+TEMP_REPO_LOCATION = "/usr/share/opensearch/backups"
+
+# Define the repository body with the temporary location
+repo_body = {
+    "type": "fs",  # Replace 'fs' with the appropriate repository type
+    "settings": {
+        "location": TEMP_REPO_LOCATION,  # Replace with the desired repository location
+    },
+}
+
+REPOSITORY_NAME = "my_repository"
+response = client.snapshot.create_repository(repository=REPOSITORY_NAME, body=repo_body)
+
+print(response)
+
+# Create a snapshot
+
+SNAPSHOT_NAME = "my_snapshot"
+response = client.snapshot.create(
+    repository=REPOSITORY_NAME, snapshot=SNAPSHOT_NAME, body={"indices": INDEX_NAME}
+)
+
+print(response)
+
+# Get Snapshot Information
+
+snapshot_info = client.snapshot.get(repository=REPOSITORY_NAME, snapshot=SNAPSHOT_NAME)
+
+print(snapshot_info)
+
+# Clean up - Delete Snapshot and Repository
+
+client.snapshot.delete(repository=REPOSITORY_NAME, snapshot=SNAPSHOT_NAME)
+client.snapshot.delete_repository(repository=REPOSITORY_NAME)
+
+# Clean up - Delete Index
+
+client.indices.delete(index=INDEX_NAME)


Test patch:
diff --git a/test_opensearchpy/test_helpers/test_aggs.py b/test_opensearchpy/test_helpers/test_aggs.py
index 006edbe4..467c64a3 100644
--- a/test_opensearchpy/test_helpers/test_aggs.py
+++ b/test_opensearchpy/test_helpers/test_aggs.py
@@ -309,6 +309,18 @@ def test_variable_width_histogram_aggregation() -> None:
     assert {"variable_width_histogram": {"buckets": 2, "field": "price"}} == a.to_dict()
 
 
+def test_multi_terms_aggregation() -> None:
+    a = aggs.MultiTerms(terms=[{"field": "tags"}, {"field": "author.row"}])
+    assert {
+        "multi_terms": {
+            "terms": [
+                {"field": "tags"},
+                {"field": "author.row"},
+            ]
+        }
+    } == a.to_dict()
+
+
 def test_median_absolute_deviation_aggregation() -> None:
     a = aggs.MedianAbsoluteDeviation(field="rating")