Skip to content

[NA] [SDK] feat: allow dataset item insertion without deduplication - #8113

Merged
alexkuzmik merged 1 commit into
mainfrom
aliaksandrk/NA-dataset-insert-without-deduplication
Sep 2, 2026
Merged

[NA] [SDK] feat: allow dataset item insertion without deduplication#8113
alexkuzmik merged 1 commit into
mainfrom
aliaksandrk/NA-dataset-insert-without-deduplication

Conversation

@alexkuzmik

Copy link
Copy Markdown
Collaborator

Details

Adds a deduplication flag to every Python SDK method that writes dataset items. With deduplication=False the whole dedup path is bypassed — the existing items are not downloaded from the backend, and no content hashes are computed or compared — so inserts into large datasets stop paying for a full item download.

  • Flag added on Dataset.insert / update / insert_from_json / read_jsonl_from_file / insert_from_pandas, the internal __internal_api__insert_items_as_dataclasses__, and the equivalent TestSuite methods.
  • Skipping dedup leaves the local hash cache marked out-of-sync, so a later deduplication=True insert re-syncs from the backend instead of silently creating real duplicates.
  • Dataset.insert's num_threads default goes from 1 to 4. The internal dataclass method stays at 1 because it has no backend-version gate, and fanning out parallel batches against older backends races on a shared batch_group_id.
  • Since parallel upload is now the default, the backend-version probe that gates it is cached per Dataset — otherwise a loop of inserts would pay one extra round trip per call.

Change checklist

  • User facing
  • Documentation update

Issues

  • Resolves #
  • OPIK-

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: Implementation, tests and documentation for this change.
  • Human verification: Reviewed by the author; docstring wording and the scope of the num_threads default change were revised on review feedback.

Testing

Commands run:

  • pytest tests/unit in sdks/python — 5125 passed, 2 skipped.
  • pre-commit run --files <changed files> — ruff, ruff-format and mypy pass.

Scenarios covered by new unit tests:

  • Identical items are both sent when deduplication=False, and dropped when it is on.
  • No item download happens when dedup is disabled on a dataset fetched from the backend.
  • An insert with dedup disabled leaves the hash cache stale, and the next deduplicated insert re-syncs.
  • update with unchanged content is still sent when dedup is disabled.
  • insert() with no num_threads uploads batches concurrently, and the version gate is probed once per dataset across repeated inserts.
  • The deduplication flag is forwarded from TestSuite.insert to the dataset.

The concurrency test was mutation-checked by temporarily restoring the old default of 1 — it fails on the barrier timeout, confirming it is not passing vacuously.

Not run: e2e/library-integration suites, which need a live backend.

Documentation

manage_datasets.mdx gains a "Disabling deduplication" section and a note about the new default thread count.

Add a `deduplication` flag to every Python SDK method that writes dataset
items. With `deduplication=False` the whole dedup path is bypassed: the
existing items are not downloaded from the backend and no content hashes are
computed or compared, which is significantly faster on large datasets.

Also raise the default `num_threads` for `Dataset.insert` from 1 to 4, and
cache the backend-version probe that gates parallel upload so a loop of
inserts does not pay a round trip per call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alexkuzmik
alexkuzmik requested review from a team as code owners September 2, 2026 09:40
@github-actions github-actions Bot added documentation Improvements or additions to documentation python Pull requests that update Python code tests Including test files, or tests related like configuration. Python SDK 🟠 size/L labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
☕ spotless — java backend Format Java code 6.01s
⚓ helm-docs Regenerate Helm chart README 4.28s
🛡️ semgrep — java backend sql Block SQL injection-prone string formatting 2.62s
🐍 mypy — python sdk Static type check 1.51s
🐍 fix end of files — python sdk Ensure files end in a newline 0.03s
🐍 trim trailing whitespace — python sdk Strip trailing whitespace 0.03s
🐍 ruff-format — python sdk Format Python code (ruff) 0.01s
🐍 ruff — python sdk Lint + autofix Python (ruff) 0.01s
Total (8 ran) 14.50s
⏭️ 36 skipped (no matching files changed)
Hook Description Result
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌿 Preview your docs: https://opik-preview-01a0617e-ae89-74ac-9238-e61dfd49d608.docs.buildwithfern.com/docs/opik

No broken links found

Unverified links (timeout / rate-limited / server error — not failing the check)

https://aistudio.google.com/apikey (401)
↳ on page: /docs/opik/development/optimization-runs/optimization/configure_models
https://console.cloud.google.com/iam-admin/iam (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/roles (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/serviceaccounts (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.mistral.ai/api-keys/ (timeout)
↳ on page: /docs/opik/integrations/mistral
https://console.x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok
https://docs.predibase.com/integrations/comet (403)
↳ on page: /docs/opik/integrations/predibase
https://docs.ragas.io/en/latest/concepts/metrics/available_metrics/ (timeout)
↳ on page: /docs/opik/integrations/ragas
https://en.wikipedia.org/wiki/ROUGE_(metric) (429)
↳ on page: /docs/opik/evaluation/metrics/heuristic_metrics
https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/observability/telemetry-with-console?tabs=Powershell-CreateFile%2CEnvironmentFile&pivots=programming-language-python (timeout)
↳ on page: /docs/opik/integrations/semantic-kernel
https://learn.microsoft.com/en-us/semantic-kernel/overview/ (timeout)
↳ on page: /docs/opik/integrations/semantic-kernel
https://portal.azure.com/ (403)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok


📌 Results for commit b2d5c39

@CometActions

Copy link
Copy Markdown
Collaborator

This change looks worth a test.

deduplication=False creates a new user-visible outcome: the same content inserted twice now becomes two dataset items with distinct generated ids, both rendered on the items page and both counted by the version counters. Nothing in the e2e estate asserts SDK dedup at all — dataset-version-repeated-item-id.spec.ts deliberately seeds through the backend client because Dataset.insert() dedups, and dataset-crud-smoke.spec.ts's sdk-round-trip adds its item through the UI. Your unit tests pin the call shape against a mocked rest client; what no test covers is what the backend actually stores and the page shows, and the deduplication=False -> _hashes_synced = False -> re-sync on the next deduplicated insert sequence. This is reachable pre-merge: the SDK bridge resolves opik from this repo, so it only needs deduplication threaded through /datasets/insert-items.

Would target datasets.sdk-round-trip, datasets.view-items.

What it would check
  1. Insert the same item content twice with deduplication=False through the SDK bridge; confirm two items with distinct ids are stored and both render on the dataset items page
  2. Insert the same content again on the same dataset with the default (deduplication=True) and confirm it collapses to no new item — so the flag is the only difference
  3. After a deduplication=False insert, insert an overlapping item WITH dedup on and confirm the forced hash re-sync drops it (the _hashes_synced = False path)
  4. Check the Version history tab after a non-dedup insert of N duplicate entries: items_added and "Item count" should count all N
  5. Repeat step 1 through TestSuite.insert(..., deduplication=False) and confirm the suite items page shows both

Deploying a test environment for this PR and exploring it — results will follow in a comment.

Also already tested. The num_threads 1 -> 4 default change lands on every existing caller of insert()/update(), but the semantics it exposes are covered: tests_end_to_end/e2e/tests/datasets/dataset-version-counters.spec.ts asserts a parallel multi-batch insert (num_threads=8) cuts one version with the same items_total/added/modified and the same stored items as the sequential path, and it asserts on the result rather than the transport. One follow-up on our side rather than yours: our SDK bridge pins num_threads=1 (e2e/services/opik-sdk-driver/src/opik_sdk_driver/schemas.py), so nothing currently exercises the new default — we should follow it to 4.

also touches Python SDK

Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

@CometActions CometActions added the test-environment Deploy Opik adhoc environment label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🔄 Test environment deployment process has started

Phase 1: Deploying base version 2.2.47-6508 (from main branch) if environment doesn't exist
Phase 2: Building new images from PR branch aliaksandrk/NA-dataset-insert-without-deduplication
Phase 3: Will deploy newly built version after build completes

You can monitor the progress here.

Comment on lines +614 to +625
@functools.cached_property
def _parallel_insert_supported(self) -> bool:
"""Whether the backend tolerates concurrent batches sharing a batch_group_id.

Older backends race on concurrent batches that share a batch_group_id,
so parallelism is only safe from
Older backends race on them, so parallelism is only safe from
``constants.MIN_BACKEND_VERSION_FOR_PARALLEL_INSERT`` onwards. When the
version cannot be determined at all — unreachable endpoint, non-semver
build string — we fall back to sequential rather than risk the race.
build string — we report unsupported rather than risk the race.

Cached because the backend cannot change version mid-session, and
parallel upload is the default: probing per ``insert`` would add a
round trip to every call in a loop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_parallel_insert_supported caches a failed self._rest_client.version() probe as False, so later insert() calls remain single-worker after the backend recovers — should we cache only successful checks and re-probe after failures?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/api_objects/dataset/dataset.py` around lines 614-625, replace the
`@functools.cached_property` decorator on `_parallel_insert_supported` with manual
caching that stores the result only after successfully determining backend support
(unsupported/invalid version can be cached as `False`). On a transient exception from
`self._rest_client.version()` (e.g., unreachable backend), return `False` without
caching, so subsequent `insert()` calls can re-probe and recover parallel insert once
the backend becomes available again.

Comment on lines 638 to +641
constants.MIN_BACKEND_VERSION_FOR_PARALLEL_INSERT,
exc_info=True,
)
return 1
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transient probe disables parallel uploads

Failures from self._rest_client.version() become False and are cached by @functools.cached_property, so one transient probe failure forces later insert() calls on that Dataset to set num_threads to 1 permanently — should we cache only successful version/support results?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/api_objects/dataset/dataset.py` around lines 638-641, fix the
`_parallel_insert_supported` probe so transient failures from
`self._rest_client.version()` are not permanently cached as `False`. Replace the blanket
`cached_property` behavior with explicit caching that stores the support result only
after a successful version lookup and comparison; on exceptions, return `False` for that
insert without populating the cache, allowing later inserts to retry the probe.

Comment on lines +721 to +722
if deduplication:
items_to_send = self._deduplicate(items)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Listed suites bypass backend deduplication

Datasets returned by rest_operations.get_test_suites() retain _hashes_synced=True, so _deduplicate() skips __internal_api__sync_hashes__, treats the empty local hash set as authoritative, and resubmits existing items — should we clear the flag before insertion?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
sdks/python/src/opik/api_objects/dataset/dataset.py around lines 721-722, the
deduplication path assumes datasets fetched from the backend have their hashes already
synced if `_hashes_synced=True`, but test suite datasets created by
`rest_operations.get_test_suites()` have `_hashes_synced=True` with empty `_hashes`,
causing duplicate items to be submitted to the backend. Ensure that the `_deduplicate()`
method or the datasets from `get_test_suites()` maintain a consistent invariant: either
reset `_hashes_synced=False` for test suite datasets before deduplication, or modify
`_deduplicate()` to always sync hashes for datasets fetched from the backend regardless
of the initial `_hashes_synced` state.

Comment on lines +724 to +727
# Nothing was hashed, so the local cache no longer describes the
# backend; force a re-sync before the next deduplicated insert.
items_to_send = items
self._hashes_synced = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concurrent bypass creates duplicates

Marking _hashes_synced stale before the upload does not synchronize with the bypassed REST batch, so a concurrent deduplicating insert can miss in-flight content and persist a duplicate item ID — should we add per-Dataset synchronization around the state transition and upload, or make snapshots wait for overlapping bypass writes?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/api_objects/dataset/dataset.py` around lines 724-727, update
`__internal_api__insert_items_as_dataclasses__` so setting `_hashes_synced = False`
cannot race with a concurrent deduplicating insert. Add a per-Dataset synchronization
mechanism that covers hash syncing/deduplication and the REST batch upload, ensuring
deduplicating inserts wait for any bypassed upload to finish and that the cache is
invalidated consistently afterward. Add or update concurrency tests to verify that
overlapping `deduplication=False` and deduplicating inserts cannot persist the same
content under different IDs.

Comment on lines +744 to +749
def insert(
self,
items: Sequence[Dict[str, Any]],
num_threads: int = 4,
deduplication: bool = True,
) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deduplication accepts non-bool truthy/falsy values, so "false" enables deduplication while 0 or None disables hash synchronization, causing inserts to be duplicated or skipped — should we reject non-bool values with a clear ValueError/TypeError before probing, hashing, or uploading, including in update, read_json, read_jsonl_from_file, and insert_from_pandas?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/api_objects/dataset/dataset.py`, update `Dataset.insert` (around
lines 744-749) to validate that `deduplication` is strictly a boolean before probing
backend support, hashing, or uploading, raising a clear `ValueError` or `TypeError` for
values such as strings, integers, or `None` instead of relying on truthiness. Apply the
same runtime validation contract to the forwarding methods `update`, `read_json`,
`read_jsonl_from_file`, and `insert_from_pandas` (around lines 816-834, 933-956,
960-980, and 984-1006), using a shared helper if appropriate, while preserving the
documented default of `True`.

Comment on lines 750 to +759
"""
Insert new items into the dataset. A new dataset version will be created.

Args:
items: List of dicts (which will be converted to dataset items)
to add to the dataset.
deduplication: Whether to skip items whose content already exists
in the dataset. Pass ``False`` to insert every item as-is
without any duplicate checking, which is significantly faster
on large datasets.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hidden cache resynchronization behavior

The public insert docs describe deduplication=False only as skipping duplicate checks, so callers may miss that it marks the local hash cache out of sync and triggers a backend resync on the next deduplicated insert — should we document this behavior?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`sdks/python/src/opik/api_objects/dataset/dataset.py` around lines 750-759, update the
`Dataset.insert` docstring’s `deduplication` description. Explicitly state that
disabling deduplication marks the local deduplication/hash cache as out of sync, and
that the next insert with deduplication enabled performs a backend hash
resynchronization, adding the resulting network cost and cache behavior for callers.

@CometActions

Copy link
Copy Markdown
Collaborator

Test environment is now available!

To configure additional Environment variables for your environment, run [Deploy Opik AdHoc Environment workflow] (https://github.com/comet-ml/comet-deployment/actions/workflows/deploy_opik_adhoc_env.yaml)

Access Information

The deployment has completed successfully and the version has been verified.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Python SDK E2E Tests Results (Python 3.14)

296 tests   287 ✅  4m 51s ⏱️
  1 suites    8 💤
  1 files      1 ❌

For more details on these failures, see this check.

Results for commit c684b65.

@alexkuzmik
alexkuzmik merged commit 976d1ca into main Sep 2, 2026
158 of 160 checks passed
@alexkuzmik
alexkuzmik deleted the aliaksandrk/NA-dataset-insert-without-deduplication branch September 2, 2026 10:01
alexkuzmik added a commit that referenced this pull request Sep 2, 2026
…on (#8115)

* [NA] [SDK] fix: address review comments on dataset insert deduplication

Follow-up to #8113.

- `_parallel_insert_supported` cached a probe that failed to reach the backend
  as `False`, which pinned the dataset to sequential uploads for the rest of the
  session. Only a conclusive answer is cached now; an unreachable backend is
  re-probed on the next insert, while a version that cannot be parsed still
  caches since it will not change.
- Test suites listed by `get_test_suites` kept `_hashes_synced=True` with an
  empty hash set, so the first deduplicated insert compared against nothing and
  resubmitted items the suite already held. They now start unsynced, matching
  `get_datasets` and `get_test_suite`.
- Reject a non-bool `deduplication`, so a truthy string or `None` cannot
  silently select the wrong duplicate-checking behaviour.
- Note the resync cost of `deduplication=False` in the `insert` docstring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(dataset): gate worker count in the insert funnel and test dedup by behaviour

Addresses the review comments on the follow-up.

- The parallel-upload version gate moves from `insert` into
  `__internal_api__insert_items_as_dataclasses__`, so a direct caller passing
  `num_threads > 1` can no longer reach the thread pool against a backend that
  races on batches sharing a batch_group_id.
- The probe-recovery test now spies on `_send_batches` and asserts the worker
  count that actually reached the upload, instead of only counting probes.
- The listed-suite regression test now inserts an item the suite already holds
  and asserts it is left out of the REST batch, rather than inspecting the
  private `_hashes_synced` flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(dataset): validate num_threads at the insert funnel and assert real uploads

- The funnel compared `num_threads > 1` before checking its type, so a direct
  caller passing a non-orderable value got a `TypeError` from the gate rather
  than the `ValueError` naming the argument. It now validates both parameters
  it consumes.
- The sequential-gate test spied on `_send_batches` without calling through, so
  it only covered argument plumbing. It now calls through and asserts every
  item still reached the backend exactly once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(dataset): document the ValueError raised by insert's argument checks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation Python SDK python Pull requests that update Python code 🟠 size/L test-environment Deploy Opik adhoc environment tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants