Repository: opensearch-project/opensearch-py (python)

Issue:
[BUG] `TypeError` error when using a `.filter(...)` on a `Search` object.
### What is the bug?
Get a `TypeError` error when using a `.filter("terms", ...)` on a `Search` object.

### How can one reproduce the bug?
Having a str instead of an int for `Bool.filter` or `Bool.must` attributes when reach the `Bool.__and__` method.
This case can happen when using string based percent (eg: `"50%"`) instead of pure integer (eg: `50`) for `minimum_should_match`.

### What is the expected behavior?
Accept `50`, `"50"` or `"50%"` as valid values.

### Do you have any additional context?
A [TODO statement](https://github.com/opensearch-project/opensearch-py/blob/main/opensearchpy/helpers/query.py#L222) is present in the code for this specific case.
This TODO statement date back at least from the [opensearch-dsl 1.0](https://github.com/opensearch-project/opensearch-dsl-py/blob/1.0/opensearch_dsl/query.py#L224).


Pull request:
Allow the use of percent string in Bool.__and__ method
### Description
Allow the use of `50`, `"50"` or `"50%"` as valid value for `qx._min_should_match`/`min_should_match` in the `Bool.__and__` method.

Previously, if for any reason the `qx._min_should_match` was a string, a TypeError error was raise by the `if len(qx.should) <= min_should_match:`.

Still raise a ValueError if the given value is an invalid integer representation.

### Issues Resolved
Closes #779

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/CHANGELOG.md b/CHANGELOG.md
index 03e1b768..d8b86560 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 - Removed redundant dependency on mock and upgrade Python syntax ([#785](https://github.com/opensearch-project/opensearch-py/pull/785))
 ### Fixed
 - Fixed Search helper to ensure proper retention of the _collapse attribute in chained operations. ([#771](https://github.com/opensearch-project/opensearch-py/pull/771))
+- Fixed the use of `minimum_should_match` with `Bool` to allow the use of string-based value (percent string, combination). ([#780](https://github.com/opensearch-project/opensearch-py/pull/780))
 ### Updated APIs
 - Updated opensearch-py APIs to reflect [opensearch-api-specification@0453dbe](https://github.com/opensearch-project/opensearch-api-specification/commit/0453dbe35080bf31eebac1d75f8022667d019e9f)
 - Updated opensearch-py APIs to reflect [opensearch-api-specification@0b033a9](https://github.com/opensearch-project/opensearch-api-specification/commit/0b033a92cac4cb20ec3fb51350c139afc753b089)
diff --git a/opensearchpy/helpers/query.py b/opensearchpy/helpers/query.py
index c7878f60..0da2eeb3 100644
--- a/opensearchpy/helpers/query.py
+++ b/opensearchpy/helpers/query.py
@@ -219,10 +219,12 @@ class Bool(Query):
                 del q._params["minimum_should_match"]
 
             for qx in (self, other):
-                # TODO: percentages will fail here
                 min_should_match = qx._min_should_match
                 # all subqueries are required
-                if len(qx.should) <= min_should_match:
+                if (
+                    isinstance(min_should_match, int)
+                    and len(qx.should) <= min_should_match
+                ):
                     q.must.extend(qx.should)
                 # not all of them are required, use it and remember min_should_match
                 elif not q.should:


Test patch:
diff --git a/test_opensearchpy/test_helpers/test_query.py b/test_opensearchpy/test_helpers/test_query.py
index 7fb2f3a0..3acde60b 100644
--- a/test_opensearchpy/test_helpers/test_query.py
+++ b/test_opensearchpy/test_helpers/test_query.py
@@ -23,9 +23,9 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
-
 from typing import Any
 
+import pytest
 from pytest import raises
 
 from opensearchpy.helpers import function, query
@@ -564,3 +564,58 @@ def test_script_score() -> None:
     assert isinstance(q.query, query.MatchAll)
     assert q.script == {"source": "...", "params": {}}
     assert q.to_dict() == d
+
+
+@pytest.mark.parametrize(  # type: ignore[misc]
+    "minimum_should_match",
+    [1, -1, "1", "-1", "50%", "-50%", "1<50%"],
+)
+def test_bool_with_minimum_should_match_as_int_or_string(
+    minimum_should_match: str,
+) -> None:
+
+    q1 = query.Bool(
+        minimum_should_match=minimum_should_match,
+        should=[
+            query.Q("term", field="aa1"),
+            query.Q("term", field="aa2"),
+        ],
+    )
+
+    q2 = query.Bool(
+        minimum_should_match=minimum_should_match,
+        should=[
+            query.Q("term", field="bb1"),
+            query.Q("term", field="bb2"),
+        ],
+    )
+
+    q3 = q1 & q2
+
+    d1 = {
+        "bool": {
+            "minimum_should_match": minimum_should_match,  # input type is preserved
+            "should": [
+                {"term": {"field": "aa1"}},
+                {"term": {"field": "aa2"}},
+            ],
+        }
+    }
+
+    d2 = {
+        "bool": {
+            "minimum_should_match": minimum_should_match,  # input type is preserved
+            "should": [
+                {"term": {"field": "bb1"}},
+                {"term": {"field": "bb2"}},
+            ],
+        }
+    }
+
+    d3 = {
+        "bool": {**d1["bool"], "must": [d2]},
+    }
+
+    assert q1.to_dict() == d1
+    assert q2.to_dict() == d2
+    assert q3.to_dict() == d3