Skip to content

Commit 85de2df

Browse files
alexkuzmikclaude
andcommitted
feat(dataset): let get_items choose its thread count
get_items() read at a fixed 4 threads, so reaching the faster end of the range meant flattening stream_items() by hand. Accept num_threads and forward it. Appended to the signature, so existing positional callers are unaffected; validation is inherited from stream_items(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b3a9153 commit 85de2df

3 files changed

Lines changed: 99 additions & 15 deletions

File tree

apps/opik-documentation/documentation/fern/docs-v2/evaluation/advanced/manage_datasets.mdx

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -415,33 +415,58 @@ You can download a dataset from Opik using the `get_dataset` method:
415415
```
416416
</CodeBlocks>
417417

418-
### Downloading large datasets
418+
### Downloading large datasets faster
419419

420-
`get_items()` returns the whole dataset as a single list, so the call does not return until every
421-
item has been downloaded and the full result is held in memory.
420+
Dataset items are fetched a page at a time, and those pages are downloaded concurrently. Raise
421+
`num_threads` to speed up a large read — the default is 4, and 8 is usually worth trying on
422+
datasets of tens of thousands of items:
423+
424+
```python title="Python" language="python"
425+
from opik import Opik
426+
427+
client = Opik()
428+
dataset = client.get_dataset(name="My dataset")
422429

423-
`stream_items()` reads the same items in chunks instead, yielding each chunk as soon as it
424-
arrives. Use it when you want to start processing before the download finishes, or when the
425-
dataset is too large to hold in memory all at once:
430+
# The whole dataset as one list, downloaded over 8 threads
431+
items = dataset.get_items(num_threads=8)
432+
```
433+
434+
The thread count never changes the result, only how quickly it arrives.
435+
436+
`get_items()` returns the whole dataset as a single list, so the call does not return until every
437+
item has been downloaded and the full result is held in memory. `stream_items()` reads the same
438+
items in chunks instead, yielding each chunk as soon as it arrives. Use it when you want to start
439+
processing before the download finishes, or when the dataset is too large to hold in memory all at
440+
once:
426441

427442
```python title="Python" language="python"
428443
from opik import Opik
429444

430445
client = Opik()
431446
dataset = client.get_dataset(name="My dataset")
432447

433-
for chunk in dataset.stream_items(num_threads=8):
434-
process(chunk) # chunk is a list of dicts, exactly as get_items() returns them
448+
# One chunk at a time, instead of the whole dataset at once
449+
for chunk in dataset.stream_items(chunk_size=5000, num_threads=8):
450+
process(chunk) # a list of dicts, exactly as get_items() returns them
435451
```
436452

437-
It accepts the same `filter_string` as `get_items()`, and `nb_samples` to stop after a given
438-
number of items. Chunks arrive in dataset order; only the last one may be shorter than
439-
`chunk_size`.
453+
Chunks arrive in dataset order; only the last one may be shorter than `chunk_size`. Both methods
454+
accept the same `filter_string`, and `nb_samples` to stop after a given number of items:
455+
456+
```python title="Python" language="python"
457+
for chunk in dataset.stream_items(
458+
num_threads=8,
459+
filter_string='data.category = "geography"',
460+
nb_samples=10_000,
461+
):
462+
process(chunk)
463+
```
440464

441465
<Note>
442-
Both methods fetch pages concurrently — `get_items()` is built on `stream_items()` — so raising
443-
`num_threads` speeds up either read. The default of 4 is a safe starting point; 8 is usually
444-
worth trying on datasets of tens of thousands of items.
466+
`chunk_size` controls how many items each request fetches (default 2000). Fetching a chunk costs
467+
a fixed overhead whatever its size, so lowering it makes the whole read slower — raise it to
468+
trade memory for speed. Lower it only when individual items are large, since up to
469+
`2 * num_threads` chunks are held in memory at once.
445470
</Note>
446471

447472
## Filtering datasets programmatically

sdks/python/src/opik/api_objects/dataset/dataset.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,20 @@ def get_items(
121121
self,
122122
nb_samples: Optional[int] = None,
123123
filter_string: Optional[str] = None,
124+
num_threads: int = constants.DATASET_ITEMS_READ_NUM_THREADS,
124125
) -> List[Dict[str, Any]]:
125126
"""
126127
Retrieve dataset items as a list of dictionaries.
127128
128129
Args:
129130
nb_samples: Maximum number of items to retrieve. If not set, all items are returned.
131+
num_threads: Number of item pages fetched concurrently. Must be a
132+
positive integer, defaults to 4; pass ``1`` to fetch
133+
sequentially. Raising it speeds up large reads at the cost of
134+
more load on the backend. Capped at
135+
``constants.DATASET_ITEMS_READ_MAX_THREADS``. Use
136+
:meth:`stream_items` instead when the dataset is too large to
137+
hold in memory all at once.
130138
filter_string: Optional OQL filter string to filter dataset items.
131139
Supports filtering by tags, data fields, metadata, etc.
132140
@@ -144,11 +152,17 @@ def get_items(
144152
145153
Returns:
146154
A list of dictionaries representing the dataset items.
155+
156+
Raises:
157+
ValueError: If ``num_threads`` is not a positive integer, or
158+
``nb_samples`` is not a positive integer.
147159
"""
148160
return [
149161
item
150162
for chunk in self.stream_items(
151-
filter_string=filter_string, nb_samples=nb_samples
163+
filter_string=filter_string,
164+
nb_samples=nb_samples,
165+
num_threads=num_threads,
152166
)
153167
for item in chunk
154168
]

sdks/python/tests/unit/api_objects/dataset/test_stream_item_chunks.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,48 @@ def test_get_items__empty_dataset__returns_empty_list():
385385
dataset = _build_dataset(endpoint)
386386

387387
assert dataset.get_items() == []
388+
389+
390+
def test_get_items__num_threads__forwarded_to_the_reader():
391+
chunk = constants.DATASET_STREAM_BATCH_SIZE
392+
endpoint = FakeItemsEndpoint(_rest_items(chunk * 4), delay_seconds=0.05)
393+
dataset = _build_dataset(endpoint)
394+
395+
items = dataset.get_items(num_threads=4)
396+
397+
assert len(items) == chunk * 4
398+
assert endpoint.max_in_flight > 1
399+
400+
401+
def test_get_items__num_threads_one__fetches_pages_sequentially():
402+
chunk = constants.DATASET_STREAM_BATCH_SIZE
403+
endpoint = FakeItemsEndpoint(_rest_items(chunk * 3), delay_seconds=0.05)
404+
dataset = _build_dataset(endpoint)
405+
406+
items = dataset.get_items(num_threads=1)
407+
408+
assert len(items) == chunk * 3
409+
assert endpoint.max_in_flight == 1
410+
411+
412+
def test_get_items__thread_count_does_not_change_the_result():
413+
endpoint = FakeItemsEndpoint(_rest_items(constants.DATASET_STREAM_BATCH_SIZE * 3))
414+
dataset = _build_dataset(endpoint)
415+
416+
assert dataset.get_items(num_threads=1) == dataset.get_items(num_threads=8)
417+
418+
419+
@pytest.mark.parametrize("num_threads", [0, -1, True, "4"])
420+
def test_get_items__invalid_num_threads__raises_value_error(num_threads):
421+
dataset = _build_dataset(endpoint=None)
422+
423+
with pytest.raises(ValueError):
424+
dataset.get_items(num_threads=num_threads)
425+
426+
427+
def test_get_items__positional_args_unchanged__nb_samples_still_first():
428+
"""num_threads was appended, so existing positional callers are unaffected."""
429+
endpoint = FakeItemsEndpoint(_rest_items(50))
430+
dataset = _build_dataset(endpoint)
431+
432+
assert len(dataset.get_items(10)) == 10

0 commit comments

Comments
 (0)