Skip to content

Commit fb2201b

Browse files
committed
updated tests
1 parent 5b4aa97 commit fb2201b

7 files changed

Lines changed: 109 additions & 60 deletions

File tree

sdks/python/sdk/src/moss/client/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass, field
3+
from dataclasses import dataclass
44
from typing import Any, Dict, List, Optional
55

66

sdks/python/sdk/src/moss/client/moss_client.py

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
)
2323

2424
from ..rerankers import get_reranker
25-
from .models import QueryOptions
25+
from .models import QueryOptions, RerankOptions
2626

2727
logger = logging.getLogger(__name__)
2828

@@ -191,24 +191,35 @@ async def query(
191191
Otherwise, falls back to the cloud query API.
192192
193193
Args:
194-
options: Query options (top_k, alpha, embedding, filter). Example filter:
195-
QueryOptions(filter={"$and": [
196-
{"field": "city", "condition": {"$eq": "NYC"}},
197-
{"field": "price", "condition": {"$lt": "50"}},
198-
]})
194+
options: Query options (top_k, alpha, embedding, filter, rerank).
195+
Reranking is applied client-side after retrieval and works on
196+
both the local and cloud paths. Example filter:
197+
QueryOptions(filter={"$and": [
198+
{"field": "city", "condition": {"$eq": "NYC"}},
199+
{"field": "price", "condition": {"$lt": "50"}},
200+
]})
199201
"""
200202
is_loaded = await asyncio.to_thread(self._manager.has_index, name)
201203

202204
if is_loaded:
203-
return await self._query_local(name, query, options)
205+
result = await self._query_local(name, query, options)
206+
else:
207+
if getattr(options, "filter", None) is not None:
208+
logger.warning(
209+
"Metadata filter ignored: filtering is only supported for locally loaded indexes. "
210+
"Call load_index('%s') first.",
211+
name,
212+
)
213+
result = await self._query_cloud(name, query, options)
204214

205-
if getattr(options, "filter", None) is not None:
206-
logger.warning(
207-
"Metadata filter ignored: filtering is only supported for locally loaded indexes. "
208-
"Call load_index('%s') first.",
209-
name,
210-
)
211-
return await self._query_cloud(name, query, options)
215+
rerank = getattr(options, "rerank", None)
216+
if rerank:
217+
top_k = getattr(options, "top_k", None)
218+
if top_k is None:
219+
top_k = 5
220+
result = await self._apply_rerank(query, result, rerank, top_k)
221+
222+
return result
212223

213224
# -- Internal ---------------------------------------------------
214225

@@ -218,7 +229,9 @@ async def _query_local(
218229
query: str,
219230
options: Optional[QueryOptions],
220231
) -> SearchResult:
221-
top_k = getattr(options, "top_k", None) or 5
232+
top_k = getattr(options, "top_k", None)
233+
if top_k is None:
234+
top_k = 5
222235
alpha = getattr(options, "alpha", None)
223236
if alpha is None:
224237
alpha = 0.8
@@ -229,7 +242,7 @@ async def _query_local(
229242
fetch_k = top_k * 4 if rerank else top_k
230243

231244
if query_embedding is not None:
232-
result = await asyncio.to_thread(
245+
return await asyncio.to_thread(
233246
self._manager.query,
234247
name,
235248
query,
@@ -238,41 +251,46 @@ async def _query_local(
238251
alpha,
239252
filter,
240253
)
241-
else:
242-
try:
243-
result = await asyncio.to_thread(
244-
self._manager.query_text,
245-
name,
246-
query,
247-
fetch_k,
248-
alpha,
249-
filter,
250-
)
251-
except RuntimeError as e:
252-
if "requires explicit query embeddings" in str(e):
253-
raise ValueError(
254-
"This index uses custom embeddings. "
255-
"Query embeddings must be provided via QueryOptions.embedding."
256-
) from e
257-
raise
258254

259-
if rerank:
260-
if rerank._instance is None:
261-
rerank._instance = get_reranker(
262-
rerank.provider, **rerank.init_kwargs
263-
)
264-
final_n = rerank.top_n or top_k
265-
reranked_docs = await rerank._instance.rerank(
266-
query, result.docs, top_k=final_n
267-
)
268-
result = SearchResult(
269-
docs=reranked_docs,
270-
query=result.query,
271-
index_name=result.index_name,
272-
time_taken_ms=result.time_taken_ms,
255+
try:
256+
return await asyncio.to_thread(
257+
self._manager.query_text,
258+
name,
259+
query,
260+
fetch_k,
261+
alpha,
262+
filter,
273263
)
264+
except RuntimeError as e:
265+
if "requires explicit query embeddings" in str(e):
266+
raise ValueError(
267+
"This index uses custom embeddings. "
268+
"Query embeddings must be provided via QueryOptions.embedding."
269+
) from e
270+
raise
274271

275-
return result
272+
@staticmethod
273+
async def _apply_rerank(
274+
query: str,
275+
result: SearchResult,
276+
rerank_opts: RerankOptions,
277+
default_top_k: Optional[int],
278+
) -> SearchResult:
279+
"""Rerank search results. Works on both local and cloud paths."""
280+
if rerank_opts._instance is None:
281+
rerank_opts._instance = get_reranker(
282+
rerank_opts.provider, **rerank_opts.init_kwargs
283+
)
284+
final_n = rerank_opts.top_n or default_top_k
285+
reranked_docs = await rerank_opts._instance.rerank(
286+
query, result.docs, top_k=final_n
287+
)
288+
return SearchResult(
289+
docs=reranked_docs,
290+
query=result.query,
291+
index_name=result.index_name,
292+
time_taken_ms=result.time_taken_ms,
293+
)
276294

277295
async def _query_cloud(
278296
self,
@@ -281,15 +299,19 @@ async def _query_cloud(
281299
options: Optional[QueryOptions],
282300
) -> SearchResult:
283301
"""Fallback: query via the cloud API when the index is not loaded locally."""
284-
top_k = getattr(options, "top_k", None) or 10
302+
top_k = getattr(options, "top_k", None)
303+
if top_k is None:
304+
top_k = 5
305+
rerank = getattr(options, "rerank", None)
306+
fetch_k = top_k * 4 if rerank else top_k
285307
query_embedding = getattr(options, "embedding", None)
286308

287309
request_body: Dict[str, Any] = {
288310
"query": query,
289311
"indexName": name,
290312
"projectId": self._project_id,
291313
"projectKey": self._project_key,
292-
"topK": top_k,
314+
"topK": fetch_k,
293315
}
294316
if query_embedding is not None:
295317
request_body["queryEmbedding"] = list(query_embedding)

sdks/python/sdk/src/moss/rerankers/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
_REGISTRY: Dict[str, Type[Reranker]] = {}
88

9+
_MISSING_PROVIDERS: Dict[str, str] = {}
10+
911

1012
def register_reranker(name: str, cls: Type[Reranker]) -> None:
1113
"""Register a reranker class under a provider name.
@@ -24,8 +26,17 @@ def get_reranker(name: str, **kwargs: Any) -> Reranker:
2426
"""Instantiate a reranker by provider name.
2527
2628
Raises:
27-
ValueError: If the provider name is not registered.
29+
ImportError: If the provider is built-in but its optional dependency
30+
isn't installed (e.g. "cohere" without `pip install cohere`).
31+
ValueError: If the provider name is not registered and not a known
32+
built-in.
2833
"""
34+
if name in _MISSING_PROVIDERS:
35+
package = _MISSING_PROVIDERS[name]
36+
raise ImportError(
37+
f"The '{name}' reranker requires the '{package}' package. "
38+
f"Install it with: pip install {package}"
39+
)
2940
if name not in _REGISTRY:
3041
available = list(_REGISTRY) or ["(none registered)"]
3142
raise ValueError(
@@ -43,4 +54,5 @@ def get_reranker(name: str, **kwargs: Any) -> Reranker:
4354

4455
register_reranker("cohere", CohereReranker)
4556
except ImportError:
46-
pass
57+
# `pip install cohere` to enable the Cohere reranker.
58+
_MISSING_PROVIDERS["cohere"] = "cohere"

sdks/python/sdk/tests/test_client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ async def test_query_with_custom_embedding(self, client):
354354
opts.alpha = 0.9
355355
opts.embedding = [0.1, 0.2, 0.3]
356356
opts.filter = None
357+
opts.rerank = None
357358

358359
result = await client.query("idx", "search text", opts)
359360

@@ -377,6 +378,7 @@ async def test_query_defaults_top_k_and_alpha(self, client):
377378
opts.alpha = None
378379
opts.embedding = [0.5]
379380
opts.filter = None
381+
opts.rerank = None
380382

381383
await client.query("idx", "q", opts)
382384

@@ -394,6 +396,8 @@ async def test_query_raises_for_custom_model_without_embedding(self, client):
394396
opts.embedding = None
395397
opts.top_k = 5
396398
opts.alpha = 0.8
399+
opts.filter = None
400+
opts.rerank = None
397401

398402
with pytest.raises(ValueError, match="custom embeddings"):
399403
await client.query("idx", "q", opts)
@@ -418,6 +422,7 @@ async def test_query_passes_filter_to_manager(self, client):
418422
opts.top_k = 5
419423
opts.alpha = 0.8
420424
opts.embedding = [0.1]
425+
opts.rerank = None
421426

422427
metadata_filter = {"field": "city", "condition": {"$eq": "NYC"}}
423428
opts.filter = metadata_filter
@@ -443,6 +448,7 @@ async def test_query_passes_none_filter_when_omitted(self, client):
443448
opts.alpha = 0.8
444449
opts.embedding = [0.1]
445450
opts.filter = None
451+
opts.rerank = None
446452

447453
await client.query("idx", "q", opts)
448454

@@ -464,6 +470,7 @@ async def test_query_passes_complex_and_filter(self, client):
464470
opts.top_k = 10
465471
opts.alpha = 0.8
466472
opts.embedding = [0.5]
473+
opts.rerank = None
467474

468475
metadata_filter = {
469476
"$and": [
@@ -561,6 +568,8 @@ async def test_uses_local_when_index_loaded(self, client):
561568
opts.top_k = 5
562569
opts.alpha = 0.8
563570
opts.embedding = [0.1]
571+
opts.filter = None
572+
opts.rerank = None
564573

565574
result = await client.query("idx", "q", opts)
566575

sdks/python/sdk/tests/test_client_extended.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ async def test_cloud_fallback_with_custom_embedding(self, unloaded_client):
100100
opts.top_k = 5
101101
opts.embedding = [0.1, 0.2, 0.3]
102102
opts.filter = None
103+
opts.rerank = None
103104

104105
result = await unloaded_client.query("idx", "test query", opts)
105106

@@ -263,6 +264,7 @@ async def test_query_with_only_top_k(self, client):
263264
opts.alpha = None
264265
opts.embedding = None
265266
opts.filter = None
267+
opts.rerank = None
266268

267269
await client.query("idx", "test", opts)
268270

@@ -278,6 +280,7 @@ async def test_query_with_only_alpha(self, client):
278280
opts.alpha = 0.5
279281
opts.embedding = None
280282
opts.filter = None
283+
opts.rerank = None
281284

282285
await client.query("idx", "test", opts)
283286

@@ -293,6 +296,7 @@ async def test_query_alpha_zero_keyword_only(self, client):
293296
opts.alpha = 0
294297
opts.embedding = None
295298
opts.filter = None
299+
opts.rerank = None
296300

297301
await client.query("idx", "test", opts)
298302

@@ -308,6 +312,7 @@ async def test_query_alpha_one_semantic_only(self, client):
308312
opts.alpha = 1
309313
opts.embedding = None
310314
opts.filter = None
315+
opts.rerank = None
311316

312317
await client.query("idx", "test", opts)
313318

@@ -333,6 +338,7 @@ async def test_filter_warning_logged_when_unloaded(self, unloaded_client, caplog
333338

334339
opts = MagicMock()
335340
opts.filter = {"field": "city", "condition": {"$eq": "NYC"}}
341+
opts.rerank = None
336342

337343
with caplog.at_level("WARNING"):
338344
await unloaded_client.query("idx", "test", opts)

sdks/python/sdk/tests/test_rerankers.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ def test_multiple_kwargs_forwarded(self):
7373
opts = RerankOptions(
7474
provider="cohere", api_key="k", model="rerank-v3.5", top_n=3
7575
)
76-
self.assertEqual(
77-
opts.init_kwargs, {"api_key": "k", "model": "rerank-v3.5"}
78-
)
76+
self.assertEqual(opts.init_kwargs, {"api_key": "k", "model": "rerank-v3.5"})
7977

8078

8179
class TestCohereRerankerProtocol(unittest.TestCase):
@@ -132,7 +130,9 @@ async def test_rerank_calls_cohere_sdk(self):
132130

133131
mock_result_1 = type("Result", (), {"index": 1, "relevance_score": 0.95})()
134132
mock_result_2 = type("Result", (), {"index": 0, "relevance_score": 0.72})()
135-
mock_response = type("Response", (), {"results": [mock_result_1, mock_result_2]})()
133+
mock_response = type(
134+
"Response", (), {"results": [mock_result_1, mock_result_2]}
135+
)()
136136
reranker._client.rerank = AsyncMock(return_value=mock_response)
137137

138138
docs = [
@@ -177,9 +177,7 @@ async def test_rerank_with_top_k(self):
177177
async def test_rerank_sdk_error(self):
178178
reranker = CohereReranker(api_key="bad-key")
179179
reranker._client = AsyncMock()
180-
reranker._client.rerank = AsyncMock(
181-
side_effect=Exception("Unauthorized")
182-
)
180+
reranker._client.rerank = AsyncMock(side_effect=Exception("Unauthorized"))
183181

184182
docs = [QueryResultDocumentInfo(id="d1", text="doc", score=0.5)]
185183

sdks/python/sdk/tests/test_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async def test_query_with_filter_option(self, client):
2020
opts.alpha = 0.8
2121
opts.embedding = None
2222
opts.filter = {"field": "city", "condition": {"$eq": "NYC"}}
23+
opts.rerank = None
2324

2425
await client.query("idx", "test", opts)
2526

@@ -38,6 +39,7 @@ async def test_query_with_custom_embedding_option(self, client):
3839
opts.alpha = 0.5
3940
opts.embedding = embedding
4041
opts.filter = None
42+
opts.rerank = None
4143

4244
await client.query("idx", "test", opts)
4345

0 commit comments

Comments
 (0)