From b5be019236aa12495b470469b66de6bbf4ef4e9b Mon Sep 17 00:00:00 2001 From: sankalp nagaonkar Date: Wed, 22 Jul 2026 19:34:42 +0530 Subject: [PATCH 1/7] Keep SDK warning docs server-owned --- videodb/search.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/videodb/search.py b/videodb/search.py index 32cb102..857c4c8 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -18,13 +18,11 @@ _LEGACY_SEARCH_WARNING = ( "This search used legacy search because legacy parameters were provided. " - "Use legacy_search(...) to keep searching older indexes, or remove legacy parameters and create Search V2 indexes. " - "Docs: https://videodb-docs-indexing-search-v2.mintlify.app/api-reference/search-v2/legacy-search" + "Use legacy_search(...) to keep searching older indexes, or remove legacy parameters to use Search V2." ) _EXPLICIT_LEGACY_SEARCH_WARNING = ( "legacy_search() searches older spoken-word and scene indexes only. " - "Create Search V2 indexes to use search(), semantic_search(), query(), aggregate(), and ask(). " - "Docs: https://videodb-docs-indexing-search-v2.mintlify.app/pages/understand/indexing-pipelines/create-an-index" + "Create Search V2 indexes to use search(), semantic_search(), query(), aggregate(), and ask()." ) _LEGACY_SEARCH_WARNING_EMITTED = set() From fb3b651f82a2599ddf6d10c04479619e62231a50 Mon Sep 17 00:00:00 2001 From: sankalp nagaonkar Date: Wed, 22 Jul 2026 19:36:15 +0530 Subject: [PATCH 2/7] Remove internal Search V2 migration warning tests --- tests/test_search_v2_migration_warnings.py | 358 --------------------- 1 file changed, 358 deletions(-) delete mode 100644 tests/test_search_v2_migration_warnings.py diff --git a/tests/test_search_v2_migration_warnings.py b/tests/test_search_v2_migration_warnings.py deleted file mode 100644 index 2af8290..0000000 --- a/tests/test_search_v2_migration_warnings.py +++ /dev/null @@ -1,358 +0,0 @@ -import pytest - -import videodb.search as search_module -from videodb.collection import Collection -from videodb.exceptions import SearchError -from videodb.video import Video - - -WARNING = { - "code": "search_v2_migration_dual_read", - "message": "search() now uses Search V2 and may also check older indexes during migration.", - "docs": [{"label": "Search V2 search", "url": "https://example.test/search"}], -} -ASK_WARNING = { - "code": "ask_requires_search_v2_index", - "message": "ask() uses Search V2 indexes only.", - "docs": [{"label": "Ask", "url": "https://example.test/ask"}], -} -SEMANTIC_WARNING = { - "code": "semantic_search_v2_migration_dual_read", - "message": "semantic_search() may also check older indexes during migration.", - "docs": [{"label": "Semantic search", "url": "https://example.test/semantic"}], -} - - -class FakeConnection: - def __init__(self): - self.calls = [] - - def post(self, path, data=None, **kwargs): - self.calls.append((path, data or {})) - if path == "compile": - return { - "stream_url": "https://stream.example.test/compiled.m3u8", - "player_url": "https://player.example.test/watch?v=compiled", - } - if path.endswith("/ask"): - return {"answer": "I do not have enough information to answer.", "sources": [], "warnings": [ASK_WARNING]} - if path.endswith("/semantic-search"): - return {"results": _results("semantic"), "warnings": [SEMANTIC_WARNING]} - if path.endswith("/query"): - return {"results": _results("query"), "warnings": [WARNING]} - if path.endswith("/aggregate"): - return {"results": [{"label": "object", "count": 2}], "warnings": [WARNING]} - if path.endswith("/search/v2"): - if (data or {}).get("mode") == "deepsearch": - return {"response_type": "deepsearch", "results": _results("deepsearch"), "warnings": [WARNING]} - return {"response_type": "shots", "results": _results("v2"), "warnings": [WARNING]} - if path.endswith("/search"): - return {"results": _results("legacy")} - raise AssertionError(f"unexpected path: {path}") - - -class FakeCollectionConnection(FakeConnection): - pass - - -def _results(text): - return [ - { - "collection_id": "c1", - "video_id": "v1", - "length": 30, - "title": "video", - "docs": [{"start": 1, "end": 3, "text": text, "score": 0.8}], - } - ] - - -@pytest.fixture(autouse=True) -def reset_warning_state(): - search_module._LEGACY_SEARCH_WARNING_EMITTED.clear() - yield - search_module._LEGACY_SEARCH_WARNING_EMITTED.clear() - - -def test_search_v2_server_warnings_are_emitted_and_exposed(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2") as emitted: - response = video.search("find cars", filter={"legacy_key": "legacy_value"}) - - assert len(emitted) == 1 - assert conn.calls[-1] == ( - "video/v1/search/v2", - {"query": "find cars", "filter": {"legacy_key": "legacy_value"}}, - ) - assert response.warnings == [WARNING] - assert response.results.warnings == [WARNING] - assert response.shots[0].text == "v2" - - -def test_deepsearch_server_warnings_are_emitted_and_exposed_on_search_response(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.search("find cars", mode="deepsearch") - - assert conn.calls[-1] == ("video/v1/search/v2", {"query": "find cars", "mode": "deepsearch"}) - assert response.response_type == "deepsearch" - assert response.warnings == [WARNING] - assert response.results.warnings == [WARNING] - - -def test_ask_server_warnings_are_emitted_and_exposed_on_ask_response(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"ask\(\) uses Search V2 indexes only"): - response = video.ask("what happened?") - - assert conn.calls[-1] == ( - "video/v1/ask", - {"question": "what happened?", "top_k": 15, "mode": "default", "include_sources": False}, - ) - assert response.answer == "I do not have enough information to answer." - assert response.warnings == [ASK_WARNING] - - -def test_search_response_compile_delegates_for_shots(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.search("find cars") - - assert response.compile() == "https://stream.example.test/compiled.m3u8" - assert conn.calls[-1] == ( - "compile", - [{"video_id": "v1", "collection_id": "c1", "shots": [(1, 3)]}], - ) - assert response.results.stream_url == "https://stream.example.test/compiled.m3u8" - - -def test_search_response_play_delegates_for_shots(monkeypatch): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - played = [] - - monkeypatch.setattr(search_module, "play_stream", lambda url: played.append(url) or f"played:{url}") - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.search("find cars") - - assert response.play() == "played:https://stream.example.test/compiled.m3u8" - assert played == ["https://stream.example.test/compiled.m3u8"] - - -def test_search_response_embed_code_delegates_for_deepsearch(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.search("find cars", mode="deepsearch") - - html = response.get_embed_code(width="640", height=360, title="Result", allow_fullscreen=False) - assert response.response_type == "deepsearch" - assert 'src="https://player.example.test/embed?v=compiled"' in html - assert 'width="640"' in html - assert 'height="360"' in html - assert 'title="Result"' in html - assert "allowfullscreen" not in html - - -def test_search_response_media_helpers_raise_for_non_shot_response(): - response = search_module.SearchResponse( - FakeConnection(), - response_type="aggregate", - results=[{"label": "car", "count": 2}], - ) - - with pytest.raises(SearchError, match="does not contain shot results"): - response.compile() - with pytest.raises(SearchError, match="does not contain shot results"): - response.play() - with pytest.raises(SearchError, match="does not contain shot results"): - response.get_embed_code() - - -def test_semantic_search_accepts_plural_selectors_emits_and_exposes_warnings(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"semantic_search\(\) may also check older indexes"): - response = video.semantic_search("find cars", index_names=["scene"], index_ids=["idx-v2"]) - - assert conn.calls[-1] == ( - "video/v1/semantic-search", - { - "query": "find cars", - "index_names": ["scene"], - "index_ids": ["idx-v2"], - "top_k": 10, - "score_threshold": None, - "filter": None, - "return_fields": None, - }, - ) - assert response.warnings == [SEMANTIC_WARNING] - assert response.shots[0].text == "semantic" - - -def test_semantic_search_rejects_singular_selectors_in_sdk_signature(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.raises(TypeError, match="unexpected keyword argument 'index_name'"): - video.semantic_search("find cars", index_name="scene") - with pytest.raises(TypeError, match="unexpected keyword argument 'index_id'"): - video.semantic_search("find cars", index_id="idx-old") - assert conn.calls == [] - - -def test_query_supports_singular_index_name_emits_and_exposes_warnings(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.query(index_name="scene") - - assert conn.calls[-1] == ( - "video/v1/query", - { - "index_name": "scene", - "index_id": None, - "filter": None, - "limit": 100, - "return_fields": None, - "sort": None, - }, - ) - assert response.warnings == [WARNING] - assert response.shots[0].text == "query" - - -def test_query_supports_singular_index_id(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.query(index_id="idx-v2") - - assert conn.calls[-1][0] == "video/v1/query" - assert conn.calls[-1][1]["index_name"] is None - assert conn.calls[-1][1]["index_id"] == "idx-v2" - assert response.warnings == [WARNING] - - -def test_query_rejects_plural_selectors_in_sdk_signature(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.raises(TypeError, match="unexpected keyword argument 'index_ids'"): - video.query(index_ids=["idx-v2"]) - assert conn.calls == [] - - -def test_aggregate_keeps_singular_index_name_emits_and_returns_raw_warning_payload(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.aggregate(index_name="scene") - - assert conn.calls[-1] == ( - "video/v1/aggregate", - { - "index_name": "scene", - "index_id": None, - "filter": None, - "group_by": None, - "metric": "count", - "limit": 100, - "sort": None, - }, - ) - assert response["warnings"] == [WARNING] - - -def test_aggregate_keeps_singular_index_id(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"search\(\) now uses Search V2"): - response = video.aggregate(index_id="idx-v2") - - assert conn.calls[-1][0] == "video/v1/aggregate" - assert conn.calls[-1][1]["index_name"] is None - assert conn.calls[-1][1]["index_id"] == "idx-v2" - assert response["warnings"] == [WARNING] - - -def test_search_index_id_routes_video_to_legacy_with_python_warning_and_scene_index_id_mapping(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match="This search used legacy search because legacy parameters were provided") as emitted: - response = video.search("find cars", index_id="old-scene-index") - - assert len(emitted) == 1 - assert conn.calls[-1][0] == "video/v1/search" - assert conn.calls[-1][1]["scene_index_id"] == "old-scene-index" - assert response.warnings == [] - assert response.shots[0].text == "legacy" - - -def test_search_index_id_routes_collection_to_legacy_with_python_warning_and_scene_index_id_mapping(): - conn = FakeCollectionConnection() - collection = Collection(conn, id="c1") - - with pytest.warns(UserWarning, match="This search used legacy search because legacy parameters were provided"): - response = collection.search("find cars", index_id="old-scene-index") - - assert conn.calls[-1][0] == "collection/c1/search" - assert conn.calls[-1][1]["scene_index_id"] == "old-scene-index" - assert response.warnings == [] - - -def test_explicit_legacy_search_warns_once_and_uses_old_endpoint(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.warns(UserWarning, match=r"legacy_search\(\) searches older spoken-word and scene indexes only") as emitted: - video.legacy_search("find cars") - video.legacy_search("find people") - - assert len(emitted) == 1 - assert [call[0] for call in conn.calls] == ["video/v1/search", "video/v1/search"] - - -def test_search_rejects_mixed_legacy_and_v2_params_before_http(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.raises(ValueError, match="Cannot mix legacy search parameters with Search V2 parameters"): - video.search("find cars", index_id="old-scene-index", top_k=5) - assert conn.calls == [] - - -def test_search_rejects_index_selectors_before_http(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.raises(ValueError, match=r"search\(\) chooses indexes automatically"): - video.search("find cars", index_names=["scene"]) - with pytest.raises(ValueError, match=r"search\(\) chooses indexes automatically"): - video.search("find cars", index_ids=["idx-v2"]) - assert conn.calls == [] - - -def test_search_rejects_internal_deepsearch_config_before_http(): - conn = FakeConnection() - video = Video(conn, id="v1", collection_id="c1") - - with pytest.raises(ValueError, match=r"deepsearch_config is not a public search\(\) option"): - video.search("find cars", deepsearch_config={"internal": True}) - assert conn.calls == [] From 94ef25628ef0300752a152ef70421bdc5c134856 Mon Sep 17 00:00:00 2001 From: sankalp nagaonkar Date: Wed, 22 Jul 2026 19:40:04 +0530 Subject: [PATCH 3/7] Hide internal DeepSearch option from SDK errors --- videodb/collection.py | 2 +- videodb/video.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/videodb/collection.py b/videodb/collection.py index 6751cf6..1450a52 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -527,7 +527,7 @@ def search( if kwargs.get("deepsearch_config") is not None: raise ValueError( - "deepsearch_config is not a public search() option. Use mode='deepsearch', top_k, session_id, and return_fields for DeepSearch requests." + "Unsupported search option. Use mode='deepsearch', top_k, session_id, and return_fields for DeepSearch requests." ) if has_old and (has_new or has_unsupported): raise ValueError( diff --git a/videodb/video.py b/videodb/video.py index 6c8d124..3008a67 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -146,7 +146,7 @@ def search( if kwargs.get("deepsearch_config") is not None: raise ValueError( - "deepsearch_config is not a public search() option. Use mode='deepsearch', top_k, session_id, and return_fields for DeepSearch requests." + "Unsupported search option. Use mode='deepsearch', top_k, session_id, and return_fields for DeepSearch requests." ) if has_old and (has_new or has_unsupported): raise ValueError( From c5d387d630036c9a6db282486484519a95c4f809 Mon Sep 17 00:00:00 2001 From: sankalp nagaonkar Date: Wed, 22 Jul 2026 19:45:56 +0530 Subject: [PATCH 4/7] Use server-owned Search V2 warning copy --- videodb/collection.py | 9 --------- videodb/search.py | 22 +++------------------- videodb/video.py | 10 +--------- 3 files changed, 4 insertions(+), 37 deletions(-) diff --git a/videodb/collection.py b/videodb/collection.py index 1450a52..e5b8d6e 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -22,8 +22,6 @@ SearchFactory, SearchResponse, SearchResult, - warn_explicit_legacy_search_once, - warn_legacy_search_once, warn_response_warnings_once, ) @@ -525,10 +523,6 @@ def search( has_new = any(k in kwargs and kwargs[k] is not None for k in new_params) has_unsupported = any(k in kwargs and kwargs[k] is not None for k in unsupported_params) - if kwargs.get("deepsearch_config") is not None: - raise ValueError( - "Unsupported search option. Use mode='deepsearch', top_k, session_id, and return_fields for DeepSearch requests." - ) if has_old and (has_new or has_unsupported): raise ValueError( "Cannot mix legacy search parameters with Search V2 parameters. " @@ -541,7 +535,6 @@ def search( ) if has_old: - warn_legacy_search_once() return self.legacy_search(query=query, _skip_warning=True, **kwargs) return self._new_search(query=query, **kwargs) @@ -678,8 +671,6 @@ def legacy_search( :rtype: Union[:class:`videodb.search.SearchResult`, :class:`videodb.rtstream.RTStreamSearchResult`] """ - if not _skip_warning: - warn_explicit_legacy_search_once() if scene_index_id is None and index_id is not None: scene_index_id = index_id diff --git a/videodb/search.py b/videodb/search.py index 857c4c8..fac8682 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -16,32 +16,16 @@ from videodb.shot import Shot -_LEGACY_SEARCH_WARNING = ( - "This search used legacy search because legacy parameters were provided. " - "Use legacy_search(...) to keep searching older indexes, or remove legacy parameters to use Search V2." -) -_EXPLICIT_LEGACY_SEARCH_WARNING = ( - "legacy_search() searches older spoken-word and scene indexes only. " - "Create Search V2 indexes to use search(), semantic_search(), query(), aggregate(), and ask()." -) -_LEGACY_SEARCH_WARNING_EMITTED = set() +_RESPONSE_WARNING_EMITTED = set() def _warn_once(key: str, message: str, stacklevel: int = 4): - if key in _LEGACY_SEARCH_WARNING_EMITTED: + if key in _RESPONSE_WARNING_EMITTED: return - _LEGACY_SEARCH_WARNING_EMITTED.add(key) + _RESPONSE_WARNING_EMITTED.add(key) warnings.warn(message, UserWarning, stacklevel=stacklevel) -def warn_legacy_search_once(): - _warn_once("legacy_params", _LEGACY_SEARCH_WARNING) - - -def warn_explicit_legacy_search_once(): - _warn_once("legacy_search", _EXPLICIT_LEGACY_SEARCH_WARNING) - - def _warning_docs_text(warning: dict) -> str: docs = warning.get("docs") or [] parts = [] diff --git a/videodb/video.py b/videodb/video.py index 3008a67..ddd0ea4 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -20,8 +20,6 @@ SearchFactory, SearchResponse, SearchResult, - warn_explicit_legacy_search_once, - warn_legacy_search_once, warn_response_warnings_once, ) from videodb.shot import Shot @@ -144,10 +142,6 @@ def search( has_new = any(k in kwargs and kwargs[k] is not None for k in new_params) has_unsupported = any(k in kwargs and kwargs[k] is not None for k in unsupported_params) - if kwargs.get("deepsearch_config") is not None: - raise ValueError( - "Unsupported search option. Use mode='deepsearch', top_k, session_id, and return_fields for DeepSearch requests." - ) if has_old and (has_new or has_unsupported): raise ValueError( "Cannot mix legacy search parameters with Search V2 parameters. " @@ -160,7 +154,6 @@ def search( ) if has_old: - warn_legacy_search_once() return self.legacy_search(query=query, _skip_warning=True, **kwargs) return self._new_search(query=query, **kwargs) @@ -286,8 +279,7 @@ def legacy_search( :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` """ - if not kwargs.pop("_skip_warning", False): - warn_explicit_legacy_search_once() + kwargs.pop("_skip_warning", False) if kwargs.get("scene_index_id") is None and kwargs.get("index_id") is not None: kwargs["scene_index_id"] = kwargs.get("index_id") kwargs.pop("index_id", None) From 8c50ac6756a4923e02d78ea987a272f4a367e3ae Mon Sep 17 00:00:00 2001 From: sankalp nagaonkar Date: Wed, 22 Jul 2026 19:58:32 +0530 Subject: [PATCH 5/7] Route legacy search tuning options correctly --- tests/test_search_legacy_routing.py | 70 +++++++++++++++++++++++++++++ videodb/collection.py | 15 +++++++ videodb/video.py | 3 ++ 3 files changed, 88 insertions(+) create mode 100644 tests/test_search_legacy_routing.py diff --git a/tests/test_search_legacy_routing.py b/tests/test_search_legacy_routing.py new file mode 100644 index 0000000..8ccfe70 --- /dev/null +++ b/tests/test_search_legacy_routing.py @@ -0,0 +1,70 @@ +import warnings + +from videodb.collection import Collection +from videodb.video import Video +import videodb.search as search_module + + +class FakeConnection: + def __init__(self, response=None): + self.posts = [] + self.response = response or {"results": []} + + def post(self, path, data=None, **kwargs): + self.posts.append({"path": path, "data": data or {}, "kwargs": kwargs}) + return self.response + + +def setup_function(): + search_module._RESPONSE_WARNING_EMITTED.clear() + + +def test_video_search_routes_stitch_rerank_options_to_legacy_endpoint_without_local_warning(): + conn = FakeConnection() + video = Video(conn, id="video-1", collection_id="collection-1") + + with warnings.catch_warnings(record=True) as emitted: + warnings.simplefilter("always") + result = video.search( + "find moments", + stitch=False, + rerank=True, + rerank_params={"top_k": 3}, + ) + + assert len(emitted) == 0 + assert result.warnings == [] + assert len(conn.posts) == 1 + assert conn.posts[0]["path"] == "video/video-1/search" + assert conn.posts[0]["data"]["stitch"] is False + assert conn.posts[0]["data"]["rerank"] is True + assert conn.posts[0]["data"]["rerank_params"] == {"top_k": 3} + + +def test_collection_search_routes_stitch_rerank_options_to_legacy_endpoint(): + conn = FakeConnection() + collection = Collection(conn, id="collection-1") + + collection.search( + "find moments", + stitch=False, + rerank=True, + rerank_params={"rank_fields": ["text"]}, + ) + + assert len(conn.posts) == 1 + assert conn.posts[0]["path"] == "collection/collection-1/search" + assert conn.posts[0]["data"]["stitch"] is False + assert conn.posts[0]["data"]["rerank"] is True + assert conn.posts[0]["data"]["rerank_params"] == {"rank_fields": ["text"]} + + +def test_video_search_still_routes_to_v2_when_no_legacy_params_are_present(): + conn = FakeConnection({"response_type": "shots", "results": []}) + video = Video(conn, id="video-1", collection_id="collection-1") + + video.search("find moments") + + assert len(conn.posts) == 1 + assert conn.posts[0]["path"] == "video/video-1/search/v2" + assert conn.posts[0]["data"] == {"query": "find moments"} diff --git a/videodb/collection.py b/videodb/collection.py index e5b8d6e..f869872 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -490,6 +490,9 @@ def search( "algorithm", "sort_docs_on", "namespace", + "stitch", + "rerank", + "rerank_params", } new_params = { "top_k", @@ -651,6 +654,9 @@ def legacy_search( scene_index_id: Optional[str] = None, index_id: Optional[str] = None, algorithm: Optional[str] = None, + stitch: Optional[bool] = None, + rerank: Optional[bool] = None, + rerank_params: Optional[Dict[str, Any]] = None, _skip_warning: bool = False, ) -> Union[SearchResult, RTStreamSearchResult]: """Search for a query in the collection. @@ -709,6 +715,14 @@ def legacy_search( ] return RTStreamSearchResult(collection_id=self.id, shots=shots) + legacy_options = {} + if stitch is not None: + legacy_options["stitch"] = stitch + if rerank is not None: + legacy_options["rerank"] = rerank + if rerank_params is not None: + legacy_options["rerank_params"] = rerank_params + search = SearchFactory(self._connection).get_search(search_type) return search.search_inside_collection( collection_id=self.id, @@ -722,6 +736,7 @@ def legacy_search( filter=filter, scene_index_id=scene_index_id, algorithm=algorithm, + **legacy_options, ) def search_title(self, query) -> List[Video]: diff --git a/videodb/video.py b/videodb/video.py index ddd0ea4..a5e4b2d 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -112,6 +112,9 @@ def search( "algorithm", "sort_docs_on", "namespace", + "stitch", + "rerank", + "rerank_params", } new_params = { "top_k", From 14ad49de7452617faf644b84113b838387f9f163 Mon Sep 17 00:00:00 2001 From: sankalp nagaonkar Date: Wed, 22 Jul 2026 20:09:16 +0530 Subject: [PATCH 6/7] Document Search V2 SDK methods --- videodb/collection.py | 114 ++++++++++++++++++++++++++++++++++-------- videodb/video.py | 95 ++++++++++++++++++++++++++++++----- 2 files changed, 177 insertions(+), 32 deletions(-) diff --git a/videodb/collection.py b/videodb/collection.py index f869872..6414080 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -475,10 +475,21 @@ def search( config: Optional[Dict[str, Any]] = None, **kwargs, ) -> Union[SearchResponse, SearchResult, RTStreamSearchResult]: - """Search the collection. + """Search this collection using Search V2 by default. - New search is used by default. Calls that use legacy-shaped parameters are - routed to :meth:`legacy_search` with a warning. + Pass Search V2 options such as ``top_k``, ``mode``, ``return_fields``, + ``include_clip``, ``session_id``, or ``config`` for the new search API. + Calls with legacy options such as ``search_type``, ``index_type``, + ``result_threshold``, ``scene_index_id``, ``index_id``, ``stitch``, + ``rerank``, or ``rerank_params`` are routed to :meth:`legacy_search`. + Do not mix Search V2 and legacy options in one call. + + Server-provided warnings are exposed on ``response.warnings`` and are + emitted as Python ``UserWarning`` messages. + + :param str query: Natural-language search query. + :param dict config: Optional Search V2 request configuration. + :return: ``SearchResponse`` for Search V2, ``SearchResult`` for legacy, or ``RTStreamSearchResult`` for RTStream legacy search. """ old_params = { "search_type", @@ -557,6 +568,18 @@ def ask( mode: str = "default", include_sources: bool = False, ) -> AskResponse: + """Ask a question over this collection's Search V2 indexes. + + ``ask()`` is Search V2 only; it does not search legacy indexes. If the + server cannot answer from indexed content, warnings are available on + ``response.warnings``. + + :param str question: Question to answer from the collection. + :param int top_k: Maximum number of source shots to retrieve. + :param str mode: Search mode to use. + :param bool include_sources: Include source shots in the response. + :return: ``AskResponse`` with ``answer``, ``sources``, and ``warnings``. + """ ask_data = self._connection.post( path=f"{ApiPath.collection}/{self.id}/{ApiPath.ask}", data={ @@ -578,6 +601,21 @@ def semantic_search( return_fields: Optional[Union[List, Dict, str]] = None, index_ids: Optional[Union[List[str], str]] = None, ) -> SearchResult: + """Run direct Search V2 semantic retrieval for this collection. + + Use ``index_names`` or ``index_ids`` to target semantic indexes. Singular + legacy selectors such as ``index_name``/``index_id`` are not accepted by + this method. + + :param str query: Natural-language query. + :param index_names: Optional Search V2 semantic index name or names. + :param int top_k: Maximum number of shots to return. + :param float score_threshold: Optional minimum similarity score. + :param filter: Optional Search V2 filter. + :param return_fields: Optional metadata fields to include. + :param index_ids: Optional Search V2 index ID or IDs. + :return: ``SearchResult`` with shots and server-provided ``warnings``. + """ search_data = self._connection.post( path=f"{ApiPath.collection}/{self.id}/{ApiPath.semantic_search}", data={ @@ -601,6 +639,19 @@ def query( sort: Optional[Union[str, List[Tuple[str, str]]]] = None, index_id: Optional[str] = None, ) -> SearchResult: + """Run a structured Search V2 query for this collection. + + ``query()`` is V2-only and is intended for filtering, sorting, and + retrieving indexed records without natural-language planning. + + :param str index_name: Optional Search V2 index name. + :param filter: Optional Search V2 filter. + :param int limit: Maximum number of records to return. + :param return_fields: Optional fields to include in each result. + :param sort: Optional sort field or ``[(field, direction)]`` list. + :param str index_id: Optional Search V2 index ID. + :return: ``SearchResult`` with shots and server-provided ``warnings``. + """ query_data = self._connection.post( path=f"{ApiPath.collection}/{self.id}/{ApiPath.query}", data={ @@ -624,6 +675,20 @@ def aggregate( sort: Optional[Union[str, List[Tuple[str, str]]]] = None, index_id: Optional[str] = None, ) -> Union[Dict, List[Dict]]: + """Run a Search V2 aggregate over this collection's indexed records. + + Use this for counts, facets, and grouped metrics. ``aggregate()`` is + V2-only and returns the server aggregate payload directly. + + :param str index_name: Optional Search V2 index name. + :param filter: Optional Search V2 filter. + :param str group_by: Optional field to group by. + :param str metric: Aggregate metric, default ``"count"``. + :param int limit: Maximum number of aggregate rows. + :param sort: Optional sort field or ``[(field, direction)]`` list. + :param str index_id: Optional Search V2 index ID. + :return: Aggregate dict/list; dict responses may include ``warnings``. + """ aggregate_data = self._connection.post( path=f"{ApiPath.collection}/{self.id}/{ApiPath.aggregate}", data={ @@ -659,23 +724,32 @@ def legacy_search( rerank_params: Optional[Dict[str, Any]] = None, _skip_warning: bool = False, ) -> Union[SearchResult, RTStreamSearchResult]: - """Search for a query in the collection. - - :param str query: Query to search for - :param SearchType search_type: Type of search to perform (optional) - :param IndexType index_type: Type of index to search (optional) - :param int result_threshold: Number of results to return (optional) - :param float score_threshold: Threshold score for the search (optional) - :param float dynamic_score_percentage: Percentage of dynamic score to consider (optional) - :param list filter: Additional metadata filters (optional) - :param str sort_docs_on: Sort docs within each video by "score" or "start" (optional) - :param str namespace: Search namespace (optional, "rtstream" to search RTStreams) - :param str scene_index_id: Filter by specific scene index (optional) - :raise SearchError: If the search fails - :return: :class:`SearchResult ` or - :class:`RTStreamSearchResult ` object - :rtype: Union[:class:`videodb.search.SearchResult`, - :class:`videodb.rtstream.RTStreamSearchResult`] + """Search this collection using legacy spoken-word, scene, or RTStream indexes. + + Use this when you intentionally want older indexes. New applications + should prefer :meth:`search`, :meth:`semantic_search`, :meth:`query`, + :meth:`aggregate`, or :meth:`ask` for Search V2 indexes. Set + ``namespace="rtstream"`` to search legacy RTStream results. The + ``index_id`` keyword is accepted as an alias for ``scene_index_id``. + + Server-provided migration warnings are available on ``result.warnings``. + + :param str query: Query to search for. + :param SearchType search_type: Legacy search type. + :param IndexType index_type: Legacy index type. + :param int result_threshold: Number of results to return. + :param float score_threshold: Minimum score threshold. + :param float dynamic_score_percentage: Dynamic score percentage. + :param list filter: Legacy metadata filters. + :param str sort_docs_on: Sort docs within each video by ``"score"`` or ``"start"``. + :param str namespace: Search namespace; use ``"rtstream"`` for RTStream legacy search. + :param str scene_index_id: Filter by a specific scene index. + :param str index_id: Alias for ``scene_index_id``. + :param str algorithm: Legacy keyword search algorithm when applicable. + :param bool stitch: Whether legacy search should stitch adjacent results. + :param bool rerank: Whether legacy search should rerank results. + :param dict rerank_params: Legacy reranking parameters. + :return: ``SearchResult`` with warnings, or ``RTStreamSearchResult`` for RTStream legacy search. """ if scene_index_id is None and index_id is not None: scene_index_id = index_id diff --git a/videodb/video.py b/videodb/video.py index a5e4b2d..8fc624a 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -97,10 +97,21 @@ def search( config: Optional[Dict[str, Any]] = None, **kwargs, ) -> Union[SearchResponse, SearchResult]: - """Search this video. + """Search this video using Search V2 by default. - New search is used by default. Calls that use legacy-shaped parameters are - routed to :meth:`legacy_search` with a warning. + Pass Search V2 options such as ``top_k``, ``mode``, ``return_fields``, + ``include_clip``, ``session_id``, or ``config`` for the new search API. + Calls with legacy options such as ``search_type``, ``index_type``, + ``result_threshold``, ``scene_index_id``, ``index_id``, ``stitch``, + ``rerank``, or ``rerank_params`` are routed to :meth:`legacy_search`. + Do not mix Search V2 and legacy options in one call. + + Server-provided warnings are exposed on ``response.warnings`` and are + emitted as Python ``UserWarning`` messages. + + :param str query: Natural-language search query. + :param dict config: Optional Search V2 request configuration. + :return: ``SearchResponse`` for Search V2, or ``SearchResult`` for legacy-routed calls. """ old_params = { "search_type", @@ -176,6 +187,18 @@ def ask( mode: str = "default", include_sources: bool = False, ) -> AskResponse: + """Ask a question over this video's Search V2 indexes. + + ``ask()`` is Search V2 only; it does not search legacy indexes. If the + server cannot answer from indexed content, warnings are available on + ``response.warnings``. + + :param str question: Question to answer from the video. + :param int top_k: Maximum number of source shots to retrieve. + :param str mode: Search mode to use. + :param bool include_sources: Include source shots in the response. + :return: ``AskResponse`` with ``answer``, ``sources``, and ``warnings``. + """ ask_data = self._connection.post( path=f"{ApiPath.video}/{self.id}/{ApiPath.ask}", data={ @@ -197,6 +220,21 @@ def semantic_search( return_fields: Optional[Union[List, Dict, str]] = None, index_ids: Optional[Union[List[str], str]] = None, ) -> SearchResult: + """Run direct Search V2 semantic retrieval for this video. + + Use ``index_names`` or ``index_ids`` to target semantic indexes. Singular + legacy selectors such as ``index_name``/``index_id`` are not accepted by + this method. + + :param str query: Natural-language query. + :param index_names: Optional Search V2 semantic index name or names. + :param int top_k: Maximum number of shots to return. + :param float score_threshold: Optional minimum similarity score. + :param filter: Optional Search V2 filter. + :param return_fields: Optional metadata fields to include. + :param index_ids: Optional Search V2 index ID or IDs. + :return: ``SearchResult`` with shots and server-provided ``warnings``. + """ search_data = self._connection.post( path=f"{ApiPath.video}/{self.id}/{ApiPath.semantic_search}", data={ @@ -220,6 +258,19 @@ def query( sort: Optional[Union[str, List[Tuple[str, str]]]] = None, index_id: Optional[str] = None, ) -> SearchResult: + """Run a structured Search V2 query for this video. + + ``query()`` is V2-only and is intended for filtering, sorting, and + retrieving indexed records without natural-language planning. + + :param str index_name: Optional Search V2 index name. + :param filter: Optional Search V2 filter. + :param int limit: Maximum number of records to return. + :param return_fields: Optional fields to include in each result. + :param sort: Optional sort field or ``[(field, direction)]`` list. + :param str index_id: Optional Search V2 index ID. + :return: ``SearchResult`` with shots and server-provided ``warnings``. + """ query_data = self._connection.post( path=f"{ApiPath.video}/{self.id}/{ApiPath.query}", data={ @@ -243,6 +294,20 @@ def aggregate( sort: Optional[Union[str, List[Tuple[str, str]]]] = None, index_id: Optional[str] = None, ) -> Union[Dict, List[Dict]]: + """Run a Search V2 aggregate over this video's indexed records. + + Use this for counts, facets, and grouped metrics. ``aggregate()`` is + V2-only and returns the server aggregate payload directly. + + :param str index_name: Optional Search V2 index name. + :param filter: Optional Search V2 filter. + :param str group_by: Optional field to group by. + :param str metric: Aggregate metric, default ``"count"``. + :param int limit: Maximum number of aggregate rows. + :param sort: Optional sort field or ``[(field, direction)]`` list. + :param str index_id: Optional Search V2 index ID. + :return: Aggregate dict/list; dict responses may include ``warnings``. + """ aggregate_data = self._connection.post( path=f"{ApiPath.video}/{self.id}/{ApiPath.aggregate}", data={ @@ -270,17 +335,23 @@ def legacy_search( filter: List[Dict[str, Any]] = [], **kwargs, ) -> SearchResult: - """Search for a query in the video. + """Search this video using legacy spoken-word or scene indexes. + + Use this when you intentionally want older indexes. New applications + should prefer :meth:`search`, :meth:`semantic_search`, :meth:`query`, + :meth:`aggregate`, or :meth:`ask` for Search V2 indexes. The ``index_id`` + keyword is accepted as an alias for ``scene_index_id``. + + Server-provided migration warnings are available on ``result.warnings``. :param str query: Query to search for. - :param SearchType search_type: (optional) Type of search to perform :class:`SearchType ` object - :param IndexType index_type: (optional) Type of index to search :class:`IndexType ` object - :param int result_threshold: (optional) Number of results to return - :param float score_threshold: (optional) Threshold score for the search - :param float dynamic_score_percentage: (optional) Percentage of dynamic score to consider - :raise SearchError: If the search fails - :return: :class:`SearchResult ` object - :rtype: :class:`videodb.search.SearchResult` + :param SearchType search_type: Legacy search type. + :param IndexType index_type: Legacy index type. + :param int result_threshold: Number of results to return. + :param float score_threshold: Minimum score threshold. + :param float dynamic_score_percentage: Dynamic score percentage. + :param list filter: Legacy metadata filters. + :return: ``SearchResult`` with shots and server-provided ``warnings``. """ kwargs.pop("_skip_warning", False) if kwargs.get("scene_index_id") is None and kwargs.get("index_id") is not None: From 0ac1b4a3b6f545586cb2698010ccfc4efc690e55 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:20:39 +0530 Subject: [PATCH 7/7] fix: move constants to constants.py --- videodb/_constants.py | 5 +++++ videodb/index.py | 5 +---- videodb/understanding.py | 9 +++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/videodb/_constants.py b/videodb/_constants.py index c16eab6..cdfcb5c 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -147,6 +147,11 @@ class Status: in_progress = "in progress" +INDEX_TERMINAL_STATUSES = {"ready", "failed"} +UNDERSTANDING_TERMINAL_STATUSES = {"done", "failed"} +ANALYZER_TERMINAL_STATUSES = {"done", "failed", "skipped", "cancelled"} + + class MeetingStatus: initializing = "initializing" processing = "processing" diff --git a/videodb/index.py b/videodb/index.py index 731bb04..9c2d93b 100644 --- a/videodb/index.py +++ b/videodb/index.py @@ -2,10 +2,7 @@ from typing import List, Optional -from videodb._constants import ApiPath - - -INDEX_TERMINAL_STATUSES = {"ready", "failed"} +from videodb._constants import ApiPath, INDEX_TERMINAL_STATUSES class FieldSchema: diff --git a/videodb/understanding.py b/videodb/understanding.py index 150e071..208c4f0 100644 --- a/videodb/understanding.py +++ b/videodb/understanding.py @@ -1,12 +1,13 @@ import time from typing import Any, Dict, List, Optional -from videodb._constants import ApiPath +from videodb._constants import ( + ApiPath, + ANALYZER_TERMINAL_STATUSES, + UNDERSTANDING_TERMINAL_STATUSES, +) -UNDERSTANDING_TERMINAL_STATUSES = {"done", "failed"} -ANALYZER_TERMINAL_STATUSES = {"done", "failed", "skipped", "cancelled"} - ANALYZER_TYPE_ALIASES = { "spoken_words": "speech_transcription", }