Repository navigation
feat(chroma): expose tunable top_k retrieval limit - #1493
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds configurable ChangesChroma top_k retrieval tuning
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Chat
participant Chroma
participant CohereRerank
participant Prompt
participant LLMOpenAI
Chat->>Chroma: question embedding and top_k retrieval
Chroma-->>CohereRerank: candidate documents
CohereRerank-->>Prompt: reranked documents
Prompt->>LLMOpenAI: question and context prompt
LLMOpenAI-->>Chat: formatted answers
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@nodes/src/nodes/chroma/services.json`:
- Line 119: The score assertion in the Chroma service test is too loose and
could hide regressions in score computation. Tighten the expectation in the
services.json test case that checks the `[0].score` result so it asserts the
expected near-0.5 value with minimal floating-point tolerance, rather than using
a broader lower bound. Keep the change localized to the score check in the
Chroma query scenario so the test continues to validate identical-embedding
cosine scoring accurately.
In `@nodes/src/nodes/core/services.common.vector.json`:
- Around line 65-70: The `vector.top_k` schema currently uses a generic
`number`, which allows fractional values that later fail in `chroma.py`’s
`_coerceTopK` runtime coercion. Update the `vector.top_k` definition in
`services.common.vector.json` to accept only integers (while keeping the
existing minimum and description) so validation matches the `_coerceTopK`
contract and prevents invalid fractional inputs from reaching `chroma.py`.
In `@nodes/src/nodes/rerank_cohere/README.md`:
- Around line 53-55: The fenced diagram in the rerank_cohere README is missing a
language tag, which triggers markdownlint MD040. Update the diagram fence to use
a text language identifier for the block so the markdown linter recognizes it,
keeping the existing diagram content unchanged.
In `@nodes/test/chroma/test_retrieval_tuning.py`:
- Around line 229-257: Add a regression test for the store.get path to verify a
configured top_k does not change its limit or behavior. Use the existing
_make_store, MagicMock collectionObj, and doesCollectionExist setup alongside
the get method on the store, and assert that get continues to use the docfilter
limit or default behavior rather than being overridden by top_k, unlike
searchSemantic and searchKeyword.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 60ce6aa5-4c76-4313-a068-82311003d32d
📒 Files selected for processing (7)
examples/rag-rerank-pipeline.pipenodes/src/nodes/chroma/README.mdnodes/src/nodes/chroma/chroma.pynodes/src/nodes/chroma/services.jsonnodes/src/nodes/core/services.common.vector.jsonnodes/src/nodes/rerank_cohere/README.mdnodes/test/chroma/test_retrieval_tuning.py
c32f7f3 to
4390b7a
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (2)
nodes/src/nodes/core/services.common.vector.json (1)
65-70: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winConstrain
vector.top_kto integers.
"type": "number"still permits fractional input (e.g.2.5), but_coerceTopKinchroma.pyraisesValueErrorfor non-integer floats, surfacing as an unhandled error duringStore.__init__. This was previously flagged as addressed, yet the type is stillnumber.🔧 Proposed fix
"vector.top_k": { - "type": "number", + "type": "integer", "title": "Top K", "minimum": 1, "description": "Maximum number of candidate documents to retrieve before score filtering. Leave unset to use the pipeline default (25). Raise it to improve recall for precise, number-dense queries." },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@nodes/src/nodes/core/services.common.vector.json` around lines 65 - 70, The `vector.top_k` schema in `services.common.vector.json` still allows fractional values because it is declared as `number`, which can reach `_coerceTopK` in `chroma.py` and fail during `Store.__init__`. Update the `vector.top_k` property to accept only integers (for example by changing the schema type used for this setting) so the UI/config validation prevents non-integer input before it reaches `Store.__init__` and `_coerceTopK`.nodes/src/nodes/chroma/services.json (1)
119-119: 🎯 Functional Correctness | 🟡 Minor | 💤 Low valueLoosened score assertion may mask a scoring regression.
Identical doc/query embeddings yield an exact cosine score of ~0.5, so
> 0.49is wider than floating-point noise requires and could hide a real shift in score computation. Prefer asserting the value near 0.5 with a minimal tolerance.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@nodes/src/nodes/chroma/services.json` at line 119, The score assertion in the Chroma service test is too loose and could hide regressions in score computation. Update the check around the "[0].score" assertion to verify the score is near 0.5 with a small tolerance instead of using a broad greaterThan threshold, so the test still passes for floating-point noise but fails on real scoring shifts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@nodes/src/nodes/chroma/services.json`:
- Line 119: The score assertion in the Chroma service test is too loose and
could hide regressions in score computation. Update the check around the
"[0].score" assertion to verify the score is near 0.5 with a small tolerance
instead of using a broad greaterThan threshold, so the test still passes for
floating-point noise but fails on real scoring shifts.
In `@nodes/src/nodes/core/services.common.vector.json`:
- Around line 65-70: The `vector.top_k` schema in `services.common.vector.json`
still allows fractional values because it is declared as `number`, which can
reach `_coerceTopK` in `chroma.py` and fail during `Store.__init__`. Update the
`vector.top_k` property to accept only integers (for example by changing the
schema type used for this setting) so the UI/config validation prevents
non-integer input before it reaches `Store.__init__` and `_coerceTopK`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 354d3a3a-298a-4a92-989c-2d2eb69a9f50
📒 Files selected for processing (7)
examples/rag-rerank-pipeline.pipenodes/src/nodes/chroma/README.mdnodes/src/nodes/chroma/chroma.pynodes/src/nodes/chroma/services.jsonnodes/src/nodes/core/services.common.vector.jsonnodes/src/nodes/rerank_cohere/README.mdnodes/test/chroma/test_retrieval_tuning.py
There was a problem hiding this comment.
Review
Verdict: Approve. The two knobs land where the issue says they were missing, the floor removal is verified safe (arguably a fix in itself), and the change is carefully scoped to the similarity-search paths only.
Verified against the code (not just the diff)
- The 0.20 floor was near-dead code — and harmful where it wasn't. With real Chroma outputs, the cosine branch maps distances
[0,2]to scores[0.5,1.5]and the sigmoid branch mapsd ≥ 0(l2) to[0.5,1)— neither can produce a score below 0.20, so for cosine/l2 the floor never fired. The one case that can go below 0.5 is inner-product (negative distances), where the sigmoid gives the best matches the lowest scores — meaning the old floor was silently dropping the strongest IP matches. Removing it is a fix there, not just a cleanup. (The inverted IP/cosine score orientation itself is pre-existing and untouched — worth a separate look someday, but correctly out of scope here.) - Data lane stays gated:
DocumentStoreBase._addDoc(store.py:288) dropsdoc.score < self.threshold_search, andthreshold_searchnow actually reads the newly-exposedscorefield. The claim that Chroma'sconfig.get('score', 0.5)was frozen checks out —vector.scorewas absent from both chroma profile property lists. get()exclusion is right: it serves full-table/document rehydration; truncating it withtop_kwould corrupt whole-object fetches. The comment documents this at the site.- Assertion-op fix verified:
nodes/test/framework/expectations.pyimplementsgreaterThan/lessThanand silently ignores unknown keys — so the oldgreaterThanOrEqualreally was a no-op assertion, andgreaterThan: 0.49restores it. _coerceTopKrejects bools (int subclass), non-integral floats, and< 1— validation at the config boundary, with tests for each shape.
Non-blocking observations
- The README sentence "…and the
searchtool's ownscore_thresholdon the tool path" slightly oversells:_vectordb_score_threshold()readsIGlobal.score_threshold, which Chroma's IGlobal never sets andservices.jsondoesn't expose — so the tool-path gate defaults to 0.0 (off). Harmless in practice given the score ranges above, but if you want the tool path genuinely tunable, exposing that knob would be a small follow-up. top_kalso overrides the limit onsearchKeyword, which supports offset paging — a paginating caller would gettop_k-sized pages. Documented behavior, just noting the interaction._effectiveLimitreturnsdocFilter.limituntouched whentop_kis unset, so existing pipelines are bit-for-bit unchanged. Confirmed.
CI: builds, ruff, gitleaks, CodeRabbit all green.
4390b7a to
b30b3f6
Compare
🤖 Internal: Discord sync markerAuto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete. |
|
@joshuadarron heads-up: this PR changed materially after your approval, so it needs a fresh look — sorry for the churn, but we caught a real problem before it shipped. What changed: rebuilt on current Why: Chroma returns distances (lower = more similar; cosine distance = 1 − similarity, range [0, 2]), but What stayed: the Locally: 39/39 chroma tests green on the rebuilt branch, ruff clean. The PR description has the full details. |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
nodes/src/nodes/rerank_cohere/README.md (1)
62-64: 📐 Maintainability & Code Quality | 🟡 Minor | 💤 Low valueAdd a language tag to the fenced diagram.
Markdownlint flags this block (MD040).
textis enough here.♻️ Proposed fix
-``` +```text … → embedding → store ──questions──▶ rerank_cohere ──documents──▶ prompt → llm → response</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@nodes/src/nodes/rerank_cohere/README.mdaround lines 62 - 64, Add thetext
language tag to the fenced diagram in the README while preserving the diagram
content unchanged.</details> <!-- cr-comment:v1:074bcad3ca6579eca5b81fca --> _Source: Linters/SAST tools_ </blockquote></details> <details> <summary>nodes/src/nodes/core/services.common.vector.json (1)</summary><blockquote> `65-70`: _🎯 Functional Correctness_ | _🟡 Minor_ | _⚡ Quick win_ **Consider constraining `vector.top_k` to integers.** The schema type is `number`, allowing fractional input (e.g. `2.5`), but `nodes/src/nodes/chroma/chroma.py`'s `_coerceTopK` (per upstream contract snippet) raises `ValueError` for non-integer floats. A value that passes schema validation could still fail at runtime with an unhandled coercion error. <details> <summary>🔧 Proposed fix</summary> ```diff "vector.top_k": { - "type": "number", + "type": "integer", "title": "Top K", "minimum": 1, "description": "Maximum number of candidate documents to retrieve before score filtering. Leave unset to use the pipeline default (25). Raise it to improve recall for precise, number-dense queries." },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@nodes/src/nodes/core/services.common.vector.json` around lines 65 - 70, Update the vector.top_k schema definition to reject fractional values by requiring integer numbers while preserving its existing minimum and default behavior. Use the top_k property in the vector service schema; do not change the runtime coercion logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@nodes/src/nodes/chroma/chroma.py`:
- Line 74: Remove the unused self.threshold_search assignment from the Chroma
node initialization, leaving the surrounding configuration handling unchanged.
---
Duplicate comments:
In `@nodes/src/nodes/core/services.common.vector.json`:
- Around line 65-70: Update the vector.top_k schema definition to reject
fractional values by requiring integer numbers while preserving its existing
minimum and default behavior. Use the top_k property in the vector service
schema; do not change the runtime coercion logic.
In `@nodes/src/nodes/rerank_cohere/README.md`:
- Around line 62-64: Add the `text` language tag to the fenced diagram in the
README while preserving the diagram content unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7e2f0396-6e4f-4c6a-973f-99443a783124
📒 Files selected for processing (7)
examples/rag-rerank-pipeline.pipenodes/src/nodes/chroma/README.mdnodes/src/nodes/chroma/chroma.pynodes/src/nodes/chroma/services.jsonnodes/src/nodes/core/services.common.vector.jsonnodes/src/nodes/rerank_cohere/README.mdnodes/test/chroma/test_retrieval_tuning.py
|
@coderabbitai review |
✅ Action performedReview finished.
|
Chroma had no knob to widen the retrieval candidate pool (issue #1411): the data lane always fetched the DocFilter default (25). Add an optional per-node top_k that overrides the limit for searchSemantic/searchKeyword only; get() deliberately keeps the caller limit since it also serves full-table and full-document rehydration. Unset top_k preserves prior behavior exactly. Also fixes the node contract test score assertion: the framework implements only greaterThan, so the previous greaterThanOrEqual assertion never ran. The score-threshold exposure originally in this branch was deliberately descoped: Chroma returns distances (lower = more similar), but _convertToDocs maps larger distances to larger scores, so a user-facing minimum-score threshold would be semantically reversed. Score normalization needs its own design (cosine vs L2 vs inner product) and is tracked separately. Part of #1411 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pytest.approx lazily probes numpy.bool_, and other node test files in the shared CI worker leave a stubbed numpy module in sys.modules, turning that probe into "isinstance() arg 2 must be a type". A plain tolerance compare is immune to the leaked module state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Schema said "number", which lets AJV/editor validation accept fractional values like 2.5 that _coerceTopK then rejects at node init. "integer" catches this at validation time. Found by an independent model review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
760b0a8 to
ce6d37e
Compare
asclearuc
left a comment
There was a problem hiding this comment.
Thanks for this, @kgarg2468 - really clean, well-scoped work. Approving. The top_k knob lands exactly where retrieval was un-tunable, the search-only / get()-excluded boundary is the right call and is documented at the call site, and unset top_k keeps existing pipelines bit-for-bit unchanged; the 10-case test file and green CI back it up.
I left two optional inline nits (reusing/justifying the shared config_int parser, and an optional upper bound on top_k). Two body notes: I agree with CodeRabbit's already-discussed Minor point that the loosened score assertion in services.json:119 could tighten from > 0.49 to > 0.499 (purely optional - your "framework no-op" explanation is correct). And one quick check before merge: please confirm builder docs:build is clean now that vector.top_k is in the profile property lists, since the generated PARAMS table in store_chroma/README.md does not list it (though vector.collection/vector.apikey are also absent, so this may just be expected generator behavior). Nothing here blocks.
joshuadarron
left a comment
There was a problem hiding this comment.
Fix the merge conflicts.
Conflict in nodes/src/nodes/store_chroma/chroma.py: both sides added new methods at the same location. Kept both — _coerceTopK/_effectiveLimit from this branch, _getServerVersion/_enforceServerVersion from develop's Chroma server-version floor.
Dismissing as stale: the merge conflicts this review asked for were resolved by merging develop into the branch (77b0172), and GitHub now reports the PR as mergeable.
|
Both approvals were dismissed by the develop merge in 77b0172, which is why this is sitting at On @asclearuc's remaining pre-merge question (whether One thing to watch on the way in: #1768 rewrites the same two lines of |
Resolves the expected conflict in nodes/src/nodes/store_chroma/services.json: develop (#1768) added chroma.tenant / chroma.database / chroma.ssl to chroma.cloud.properties while this branch added vector.top_k. Both arrays are kept — vector.top_k sits after vector.collection to mirror the ordering already used in chroma.local.properties. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sertion Review follow-ups on #1493. - Cap `top_k` at 1000 (`Store.MAX_TOP_K`) and mirror it as `maximum` on the `vector.top_k` schema. Previously only the lower bound was enforced, so a fat-fingered `top_k: 10000000` flowed straight into Chroma's `n_results` / `limit`. 1000 leaves 40x headroom over the data-lane default of 25 and 10x over the `chroma.search` tool's own cap, so real reranker fan-in is unaffected. Out-of-range is rejected at startup rather than clamped, so a mistyped value cannot silently change how many documents are retrieved. - Document why `_coerceTopK` does not reuse `ai.common.utils.config_int`: that helper never returns None (so "unset" could not fall back to `docFilter.limit`), treats `<= 0` as unspecified, and clamps instead of raising. The comment keeps a future reader from "simplifying" it back and changing the semantics. - Tighten the contract-test score check from `greaterThan: 0.49` to a two-sided `greaterThan: 0.499` + `lessThan: 0.501`. Identical embeddings give an exact cosine score of 0.5; the old one-sided bound tolerated drift down to 0.491 and any drift upward at all. `nodes/test/framework/ expectations.py` evaluates both matchers on a single property entry. - New `nodes/test/store_chroma/test_services_top_k.py` pins the schema to the runtime contract: `vector.top_k` stays an integer, its bounds match `Store.MAX_TOP_K`, and both Chroma profiles list it. That last check guards the `chroma.cloud` / `chroma.local` properties arrays, which are a recurring merge-conflict site (#1768 rewrites the same lines). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed Merge conflictExactly the collision @nihalnihalani flagged: "properties": ["vector.cloud.host", "vector.cloud.port", "vector.apikey", "chroma.tenant", "chroma.database", "vector.collection", "vector.top_k", "chroma.serverName", "chroma.ssl"]That was the only conflict; Review fixes@asclearuc — upper bound. Capped: @asclearuc — @asclearuc + CodeRabbit — score assertion. Now two-sided, @asclearuc — TestingRan against a local env emulating the engine runtime (real
New tests
Plus in Not blocking, but worth knowingBoth approvals were dismissed by the |
asclearuc
left a comment
There was a problem hiding this comment.
Thanks @kgarg2468 — the rebuilt branch answers everything from the last round. MAX_TOP_K is mirrored in the schema and pinned by a test, the config_int decision is written down in the docstring, the score assertion is two-sided instead of a silently-ignored no-op, and CodeRabbit's Major finding (a regression test for get()) is covered by test_get_ignores_top_k_and_uses_docfilter_limit. Both of my earlier nits are resolved.
Request changes
Three non-blocking comments are inline. One more finding cannot be an inline comment, because it points at an unchanged line (and CodeRabbit already has a thread on the changed line next to it), so it is here:
MUST fix — the new score assertion pins what looks like an inverted relevance scale.
nodes/src/nodes/store_chroma/services.json:119 now asserts that a document whose embedding is identical to the query scores 0.5. That is a perfect match. On a 0..1 relevance scale it should score 1.0.
The cause is chroma.py:419, unchanged by this PR:
if self.similarity == 'cosine':
score = (results['distances'][i] + 1) / 2Chroma returns a distance for hnsw:space: cosine — 1 - cosine_similarity, range 0..2. Qdrant's point.score is a similarity in -1..1, and nodes/src/nodes/store_qdrant/qdrant.py:307 uses the same (x + 1) / 2 line, correctly, there. Applied to a distance it reverses the order:
| vectors | cosine distance | score today | score expected |
|---|---|---|---|
| identical | 0 | 0.5 | 1.0 |
| orthogonal | 1 | 1.0 | 0.5 |
| opposite | 2 | 1.5 | 0.0 |
DocumentStoreBase._addDoc (packages/ai/src/ai/common/store/document_store.py:295) then drops every doc below threshold_search, so raising the Retrieval Score knob on a Chroma node discards the best matches and keeps the worst. The l2 branch on the next line has the same shape (d=0 gives 0.5, large d approaches 1.0), which is also why the score < 0.20 noise floor can never fire on the cosine path.
Fixing the formula is outside the scope of a top_k PR, and I am not asking you to widen it. But the new two-sided band makes 0.5 the expected value for a perfect match, so a later fix will now fail this test. Please either fix it here, or open an issue and add a one-line comment next to the assertion saying it pins current behaviour and linking the issue.
One verification command that settles the scale, if you want it before filing anything:
python -c "import chromadb; c=chromadb.Client().create_collection('t', metadata={'hnsw:space':'cosine'}); c.add(ids=['a'], embeddings=[[1.0,0.0]]); print(c.query(query_embeddings=[[1.0,0.0]], n_results=1)['distances'])"…ad score Addresses @asclearuc's CHANGES_REQUESTED on #1493. - MERGE BLOCKER: `vector.top_k` was accidentally a *required* field. The schema compiler treats a missing `optional` flag as required (engLib/store/services/services.cpp:480 "Optional not specified, it is required", appended to `required` at :1293), and `top_k` has no `default` to satisfy it. Adds `"optional": true`, with a regression test. - `_coerceTopK` rejected every string, so `"top_k": "${ROCKETRIDE_TOP_K}"` killed the node at startup — in a node whose own README documents that form for `port`. It now mirrors `_coercePort`: integer strings parse, and a still-unresolved `${...}` placeholder logs and returns None so retrieval falls back to the caller's limit. Falling back to *absence of an override* is right here, not a hardcoded 25. Explicitly malformed values (bool, fractional, '5.0', '1e2', out of range) still raise rather than clamp. Schema type becomes ["integer", "string"], matching the port fields. - The score assertion is now marked a compatibility pin, not a correctness expectation, and links issue #2236. Chroma returns a *distance*, so the unchanged `(distance + 1) / 2` conversion scores a perfect match 0.5 and an opposite vector 1.5 — inverted, and outside the [0,1] range `vector.score` declares. Verified against chromadb 1.5.9: cosine distances 0.0/1.0/2.0 for identical/orthogonal/opposite. Fixing it would turn the currently dormant `threshold_search` gate (cosine scores are always >= 0.5 today, so nothing is ever dropped) into an active filter, silently cutting recall inside a PR meant to improve it. Left byte-identical to develop and tracked instead. - The example and README told users to "raise" top_k to 20, but the data-lane DocFilter.limit default is 25 (client-python/schema/doc_filter.py:96), so that was a 20% *reduction*. Example now uses 50, and the prose says top_k overrides in both directions rather than only widening. - `vector.top_k` lives in the shared vector catalogue but only Chroma implements it; the description now says so, so the next store to list it does not inherit a field that silently does nothing. Tests: store_chroma 91 -> 114, all seven vector stores 165 -> 188 passed, ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed The MUST-fix: you're right, and I verified it empiricallyI ran it against
Your table was exactly right. All three metrics are affected, not just cosine — Taking your pin-and-file option, and here's the argument for it beyond scope discipline.
So the "obvious" one-line fix silently cuts recall — inside a PR whose entire purpose is improving recall. It has to land with a decision on the default threshold and the floor. Filed as #2236 with the reproduction, the full measured table, the migration analysis, and a note that One deliberate deviation: I put the comment above Merge blocker found while acting on your nit —
|
|
CI is green on `b0356ab5`: 17 passing, 4 skipped, only `discord-notify` (a notification job, not a gate) outstanding. One note on the red Build / Windows Server 2022 in the run history, so it does not cost anyone a second look. It failed on two tests that this PR does not touch: A startup timeout, not an assertion. Both passed on macOS and Ubuntu at the identical commit, and @asclearuc this is ready for another look whenever you have time. The scoring-inversion pin is the one judgement call I would most like you to sanity-check — reasoning is in the thread above, and the fix is tracked in #2236. |
# Conflicts: # nodes/src/nodes/store_chroma/README.md
Merging develop brought the documentation revamp (#2139) and its node README validator, which rejects any non-canonical `##` heading and any hand-written field table. Re-home the Chroma top_k guidance to fit: - store_chroma: drop the Fields table and the `## Retrieval tuning` section; add a `### Top K` subsection under `## Configuration` in develop's layout. - rerank_cohere: move the `## Wiring` block under `## Notes`. Its example called `top_k: 20` a "higher" pool, but the data-lane default is 25, so it narrowed retrieval. Recommend 50 and say plainly that only Chroma exposes Top K. - core: add `vector.top_k` to the hand-written common-vector table and regenerate the generated block via nodes:docs-generate. The regeneration also emits the gcp and microsoft tables, which were added to the catalogue after the block was last generated. Verified: validate-node-readme.py --all passes; docs:test, docs:build and docs:check pass; store_chroma 114 passed; all store_* suites 188 passed, 1 pre-existing skip. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Pushed What #2139 changed for this PRThe documentation revamp added
Untouched: Verified locally |
asclearuc
left a comment
There was a problem hiding this comment.
Thanks @kgarg2468 — adadf97f is a clean landing. The retrieval code is byte-for-byte what I approved last round; this push only merges develop and re-homes the documentation to the README schema that #2139 introduced. The reranker guidance now recommends 50 instead of 20, which is the right side of the caller's 25 default, and I checked that the gcp and microsoft tables you picked up sit inside the ROCKETRIDE:GENERATED:PARAMS block, so they are regenerated output and not a hand edit. Merge base is one day old and every check is green.
Approve
One non-blocking should fix is inline, on a single sentence in the Top K section. Two CodeRabbit threads are still open on this head and both are false positives — please decline them rather than act on them:
- #discussion_r3539638838 (Major) asks for a regression test proving
get()ignorestop_k. It exists:test_get_ignores_top_k_and_uses_docfilter_limitatnodes/test/store_chroma/test_retrieval_tuning.py:252. - #discussion_r3611550904 (Minor) asks you to delete
self.threshold_searchatnodes/src/nodes/store_chroma/chroma.py:115as dead code. It is read by_addDocinsideDocumentStoreBase._queryDocumentsatpackages/ai/src/ai/common/store/document_store.py:295, so removing it would disable the Retrieval Score knob. It is also an unchanged line in this PR.
Brings in rocketride-org#1612 (`rocketride otel`), rocketride-org#1489 (context_optimizer node) and rocketride-org#1493 (chroma retrieval tuning). Four files conflicted; every develop addition and every `eval` addition is kept. Resolved files and the rule applied: - CHANGELOG.md — both sides appended a `**client-python**:` entry right after the `**cli**:` line. Kept both, `eval` (rocketride-org#1581) before `otel` (rocketride-org#1612): within a scope group the description and the PR number order the same way. develop's `**nodes**: context_optimizer (rocketride-org#1489)` line auto-merged untouched. - docs/public/product/connect/cli.mdx — both sides rewrote the same "Python vs TypeScript CLI" callout to name their own command as "the one exception" that is Python-only; concatenating would have claimed one exception twice, so the sentence now names both, keeping develop's otel-bridge link and its closing clause about the monitor stream. The command table and the two `###` sections auto-merged, each entry at the position its own side chose. - packages/client-python/src/rocketride/cli/commands/__init__.py — both sides appended a `Modules:` docstring line and an import after `.deploy`. Kept both with `eval` first: the import block is alphabetical (`.eval` < `.otel`) and the docstring keeps `eval` beside its sibling `validate`. `__all__` auto-merged with `run_eval` and `run_otel` already in place. - packages/client-python/src/rocketride/cli/main.py — four "both sides inserted here" hunks, resolved as `eval` then `otel` in each: the docstring command surface, the `setup_parser` blocks (the conflict bracketed a shared closing paren, so eval's last `add_argument(` got its own `)`), the `_dispatch` imports, and the `_dispatch` chain. `_add_connection_args` keeps develop's `*, json_arg: bool = True` signature; `eval` calls it bare and so keeps the shared `--json [FILE]`, `otel` keeps `json_arg=False`. pyproject.toml auto-merged: develop's `otel` extra and the `rocketride[otel]` entries in `dev`/`test` sit beside this PR's evals template package-data line. Verified: `rocketride --help` lists every command exactly once (validate, eval, otel, store, ...); client-python suite on the engine interpreter after `depends(tests/requirements.txt)` gives 456 passed, 1 skipped, 1 failed, 21 errors against the 306 passed / 1 failed / 21 errors baseline at 2ef11d4 — the +150 passed / +1 skipped are exactly develop's five `test_otel_*` modules, and the failure and errors are the same environment-only residue (host env; no engine on 127.0.0.1:5565) in modules byte-identical to the pre-merge tree. ruff check and ruff format --check clean on both touched .py files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in rocketride-org#1612 (`rocketride otel`), rocketride-org#1489 (context_optimizer node) and rocketride-org#1493 (chroma retrieval tuning). Five files conflicted because rocketride-org#1612 adds a CLI subcommand at the same anchors this branch uses for `diff`; every develop addition and every `diff` addition is kept. Resolved files and the rule applied: - CHANGELOG.md — the Unreleased "Added" list is sorted by scope, so the two `**client-python**` entries sit together in command order: `diff` (rocketride-org#1607) then `otel` (rocketride-org#1612). rocketride-org#1489's context_optimizer line auto-merged in place. - docs/public/product/connect/cli.mdx — both sides rewrote the same sentence of the "Python vs TypeScript CLI" note, each claiming "one exception" for its own command. Rewritten once for both: two Python-only exceptions, `diff` and `otel`, keeping develop's OpenTelemetry-bridge link and its monitor-stream clause. The Commands table and the `### diff` / `### otel` sections auto-merged at their own anchors, giving one consistent order (validate, diff, list, otel, store). The "Connecting" note still reads "every command except `diff`", which stays true: `otel` does take --uri/--apikey. - packages/client-python/pyproject.toml — both sides appended to the `test` extra. Merged into ONE `test` list (two `test = [...]` keys would make the file invalid TOML) carrying develop's `rocketride[otel]` and this branch's `cmarkgfm` oracle, with develop's new `otel` extra and its `dev` entry kept verbatim. Verified by loading the file with tomllib. - packages/client-python/src/rocketride/cli/commands/__init__.py — imports and `__all__` are alphabetical (`.diff` before `.otel`); the "Modules:" docstring list is insertion-ordered, so develop's line stays put and `diff` follows it. - packages/client-python/src/rocketride/cli/main.py — four hunks at the same three anchors. Command-surface docstring, subparser block and `_dispatch` branch: develop's `otel` stays where develop put it and `diff` follows; the function-local imports stay alphabetical. Develop's widened `_add_connection_args(parser, *, json_arg=True)` is kept as-is — `diff` takes no connection args at all, and `otel --help` still shows --uri/--apikey with no --json. Verified on the merged tree: 356 passed / 1 failed / 2 skipped / 21 errors (client-python via the bundled engine), the same environment-only residue as the pre-merge baseline — the 21 test_deploy/test_tool fixtures that need a live engine and test_client_env_loading's host-env test — with +150 passed and +1 collector-gated skip from develop's new otel suites. `rocketride --help` lists all 12 commands exactly once; ruff check/format clean; scripts/validate-client-docs.py and scripts/verify-action-pins.mjs both pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Chroma returns a distance for every hnsw:space (lower is more similar), but store_chroma converted it with a function that increased with distance: a perfect cosine match scored 0.5, an unrelated vector 1.0 and the opposite vector 1.5. Ordering by score was reversed, cosine scores escaped the [0, 1] range vector.score declares, l2 was nearly flat (0.500 to 0.601 across a squared distance of 41) and a strong ip match (negative distance) scored 0.49 and was dropped by the default 0.5 gate. Each space now gets the affine map that turns its distance into (cos + 1) / 2 for unit-norm embeddings, clamped to [0, 1]: cosine and ip use 1 - d / 2, l2 (squared) uses 1 - d / 4. The three spaces therefore agree on the score of a pair, a retrieval-score threshold means the same thing in each, and the scale matches store_qdrant's cosine rescale. The hardcoded 0.20 floor becomes a named MIN_RELEVANCE_SCORE equal to Qdrant's, and numpy is no longer needed. The default retrieval score stays 0.5. On the old scale it accepted everything; on the corrected scale it removes hits whose cosine similarity is negative, which real embeddings almost never produce for a related chunk, so recall for existing pipelines is preserved while the reversed ordering is fixed. The services.json contract pin (0.499 < score < 0.501, left in place by rocketride-org#1493 pending this fix) now expects 1.0, the README documents the conversion and the migration note, and new unit tests pin the measured table, monotonicity, the [0, 1] clamp, cross-space agreement on unit vectors and the floor, plus a check against a real chromadb client when one is installed. Fixes rocketride-org#2236
Summary
top_k) retrieval limit to the Chroma node — there was previously no knob to widen the candidate pool (the data lane always fetched the DocFilter default of 25), which is the single biggest lever for recall on precise, number-dense queries (field report I-4/I-8).top_kapplies to similarity search only (searchSemantic/searchKeyword);get()deliberately keeps the caller's limit because it also serves full-table/full-document rehydration, which must never be truncated. Unsettop_k⇒ prior behavior exactly.rerank_coherewiring section, a "Retrieval tuning" section in the Chroma README, and a runnableexamples/rag-rerank-pipeline.pipewiringchroma (top_k: 20) → rerank_cohere (top_n: 5) → llm— retrieve generously, rerank down.greaterThan(nodes/test/framework/expectations.py), so the previousgreaterThanOrEqual: 0.5assertion was silently ignored; it's nowgreaterThan: 0.49and actually runs.The earlier revision of this PR also exposed the
scorethreshold and removed the hardcoded 0.20 floor in_convertToDocs. That part was removed on purpose: Chroma returns distances (lower = more similar; cosine distance = 1 − cosine similarity, range [0, 2]), but_convertToDocsmaps larger distances to larger scores ((distance + 1) / 2for cosine, and a sigmoid increasing in distance otherwise). A user-facing "minimum score" threshold on top of that is semantically reversed — it would preferentially drop the most similar documents. Proper score normalization needs its own design per metric (cosine / L2 / inner product) and will be proposed separately. The floor and score handling in this diff are byte-identical todevelop.How
Store.top_kconfig field (validated by_coerceTopK: positive whole numbers only, bool rejected, unset/blank ⇒ None)._effectiveLimit(docFilter): configuredtop_kwins, else the caller'sdocFilter.limit.vector.top_kadded toservices.common.vector.jsonand to Chroma'slocal/cloudprofiles.vector.scoreis not exposed.Type
feat
Testing
nodes/test/chroma/test_retrieval_tuning.py(10 cases:_coerceTopKvalidation,_effectiveLimitprecedence/fallback,top_koverride in semantic + keyword search,get()ignorestop_k, and a regression test that the 0.20 floor still applies)nodes/test/chroma/suite 39 passed (includes fix(nodes): friendlier chroma port + splitter-profile config (RR-1416) #1496'stest_coerce_port.py; branch is rebuilt on currentdevelop);ruff check+ruff format --checkclean./builder testpasses — runs in CI (node-source edits force a full engine recompile locally)Checklist
${ROCKETRIDE_*}placeholders)top_k⇒ prior behaviorLinked Issue
Part of #1411 (score-threshold + hybrid retrieval remain open; score normalization to be designed separately)