From b95e6a4bfbbe4976c7e531b86faa781f9ac24103 Mon Sep 17 00:00:00 2001 From: yashb1708 Date: Wed, 25 Feb 2026 15:42:50 -0500 Subject: [PATCH] feat: add exact_match parameter to search method Fixes TAV-5096 Co-Authored-By: Claude Opus 4.6 --- README.md | 15 +++++++++++ setup.py | 2 +- tavily/async_tavily.py | 4 +++ tavily/tavily.py | 4 +++ tests/test_search.py | 59 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 81 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 506026d..68c8773 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,21 @@ response = tavily_client.search("Who is Leo Messi?") print(response) ``` +### Using exact match to find specific names or phrases + +```python +from tavily import TavilyClient + +client = TavilyClient(api_key="tvly-YOUR_API_KEY") + +# Use exact_match=True to only return results containing the exact phrase(s) inside quotes +response = client.search( + query='"John Smith" CEO Acme Corp', + exact_match=True +) +print(response) +``` + This is equivalent to directly querying our REST API. ### Generating context for a RAG Application diff --git a/setup.py b/setup.py index 9f65b52..fd317e7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='tavily-python', - version='0.7.21', + version='0.7.22', url='https://github.com/tavily-ai/tavily-python', author='Tavily AI', author_email='support@tavily.com', diff --git a/tavily/async_tavily.py b/tavily/async_tavily.py index 80dce2b..b52640e 100644 --- a/tavily/async_tavily.py +++ b/tavily/async_tavily.py @@ -88,6 +88,7 @@ async def _search( auto_parameters: bool = None, include_favicon: bool = None, include_usage: bool = None, + exact_match: bool = None, **kwargs, ) -> dict: """ @@ -111,6 +112,7 @@ async def _search( "auto_parameters": auto_parameters, "include_favicon": include_favicon, "include_usage": include_usage, + "exact_match": exact_match, } data = {k: v for k, v in data.items() if v is not None} @@ -164,6 +166,7 @@ async def search(self, auto_parameters: bool = None, include_favicon: bool = None, include_usage: bool = None, + exact_match: bool = None, **kwargs, # Accept custom arguments ) -> dict: """ @@ -188,6 +191,7 @@ async def search(self, auto_parameters=auto_parameters, include_favicon=include_favicon, include_usage=include_usage, + exact_match=exact_match, **kwargs, ) diff --git a/tavily/tavily.py b/tavily/tavily.py index 194d209..b8e6674 100644 --- a/tavily/tavily.py +++ b/tavily/tavily.py @@ -71,6 +71,7 @@ def _search(self, auto_parameters: bool = None, include_favicon: bool = None, include_usage: bool = None, + exact_match: bool = None, **kwargs ) -> dict: """ @@ -95,6 +96,7 @@ def _search(self, "auto_parameters": auto_parameters, "include_favicon": include_favicon, "include_usage": include_usage, + "exact_match": exact_match, } data = {k: v for k, v in data.items() if v is not None} @@ -151,6 +153,7 @@ def search(self, auto_parameters: bool = None, include_favicon: bool = None, include_usage: bool = None, + exact_match: bool = None, **kwargs, # Accept custom arguments ) -> dict: """ @@ -175,6 +178,7 @@ def search(self, auto_parameters=auto_parameters, include_favicon=include_favicon, include_usage=include_usage, + exact_match=exact_match, **kwargs) response_dict.setdefault("results", []) return response_dict diff --git a/tests/test_search.py b/tests/test_search.py index bcf09d0..20d55ec 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -32,7 +32,7 @@ def validate_specific(request, response): assert request.headers["Authorization"] == "Bearer tvly-test" assert request.headers["X-Client-Source"] == "tavily-python" assert request.timeout == 10 - + request_json = request.json() for key, value in { "query": "What is Tavily?", @@ -44,7 +44,8 @@ def validate_specific(request, response): "exclude_domains": ["example.com"], "include_answer": "advanced", "include_raw_content": True, - "include_images": True + "include_images": True, + "exact_match": True }.items(): assert request_json.get(key) == value @@ -69,6 +70,7 @@ def test_sync_search_specific(sync_interceptor, sync_client): include_answer="advanced", include_raw_content=True, include_images=True, + exact_match=True, timeout=10 ) @@ -94,8 +96,61 @@ def test_async_search_specific(async_interceptor, async_client): include_answer="advanced", include_raw_content=True, include_images=True, + exact_match=True, timeout=10 )) request = async_interceptor.get_request() validate_specific(request, response) + +def test_sync_search_exact_match_not_sent_by_default(sync_interceptor, sync_client): + sync_interceptor.set_response(200, json=dummy_response) + sync_client.search("What is Tavily?") + request = sync_interceptor.get_request() + assert "exact_match" not in request.json() + +def test_sync_search_exact_match_true(sync_interceptor, sync_client): + sync_interceptor.set_response(200, json=dummy_response) + sync_client.search("What is Tavily?", exact_match=True) + request = sync_interceptor.get_request() + assert request.json()["exact_match"] is True + +def test_sync_search_exact_match_false(sync_interceptor, sync_client): + sync_interceptor.set_response(200, json=dummy_response) + sync_client.search("What is Tavily?", exact_match=False) + request = sync_interceptor.get_request() + assert request.json()["exact_match"] is False + +def test_async_search_exact_match_not_sent_by_default(async_interceptor, async_client): + async_interceptor.set_response(200, json=dummy_response) + asyncio.run(async_client.search("What is Tavily?")) + request = async_interceptor.get_request() + assert "exact_match" not in request.json() + +def test_async_search_exact_match_true(async_interceptor, async_client): + async_interceptor.set_response(200, json=dummy_response) + asyncio.run(async_client.search("What is Tavily?", exact_match=True)) + request = async_interceptor.get_request() + assert request.json()["exact_match"] is True + +def test_async_search_exact_match_false(async_interceptor, async_client): + async_interceptor.set_response(200, json=dummy_response) + asyncio.run(async_client.search("What is Tavily?", exact_match=False)) + request = async_interceptor.get_request() + assert request.json()["exact_match"] is False + +def test_sync_search_exact_match_query_quotes_escaped_in_payload(sync_interceptor, sync_client): + sync_interceptor.set_response(200, json=dummy_response) + sync_client.search('"John Smith" CEO Acme Corp', exact_match=True) + request = sync_interceptor.get_request() + # The raw JSON payload should have escaped quotes for the quoted phrase + assert r'\"John Smith\"' in request.body + # But the parsed query should preserve the original quotes + assert request.json()["query"] == '"John Smith" CEO Acme Corp' + +def test_async_search_exact_match_query_quotes_escaped_in_payload(async_interceptor, async_client): + async_interceptor.set_response(200, json=dummy_response) + asyncio.run(async_client.search('"John Smith" CEO Acme Corp', exact_match=True)) + request = async_interceptor.get_request() + assert r'\"John Smith\"' in request.body + assert request.json()["query"] == '"John Smith" CEO Acme Corp'