Skip to content

Conversation

@jairad26
Copy link
Contributor

@jairad26 jairad26 commented Oct 30, 2025

Description of changes

Summarize the changes made by this PR.

  • Improvements & Bug fixes
    • This PR is to clean up how collections are created:
    1. when collection configuration was hnsw default and schema was any default, it blindly converted config -> schema. instead, this should use the knn index to build the correct default schema, and only take the embedding function from config
    2. when converting config -> schema, it writes #document as the source key for both defaults and #embedding vector indexes. Instead, it should only write #document as the source key for #embedding
    3. On the distributed modify path, the json mapping for the boolean type in go does not match the rust type
  • New functionality
    • ...

Test plan

How are these changes tested?
added unit tests for all 8 default cases (config hnsw or spann default, schema hnsw or spann default, default_knn_index), test thta #document does not populate for defaults, and embedding functions do.

  • [ x] Tests pass locally with pytest for python, yarn test for js, cargo test for rust

Migration plan

Are there any migrations, or any forwards/backwards compatibility changes needed in order to make sure this change deploys reliably?

Observability plan

What is the plan to instrument and monitor this change?

Documentation Changes

Are all docstrings for user-facing APIs updated if required? Do we need to make documentation changes in the docs section?

Copy link
Contributor Author

jairad26 commented Oct 30, 2025

@github-actions
Copy link

Reviewer Checklist

Please leverage this checklist to ensure your code review is thorough before approving

Testing, Bugs, Errors, Logs, Documentation

  • Can you think of any use case in which the code does not behave as intended? Have they been tested?
  • Can you think of any inputs or external events that could break the code? Is user input validated and safe? Have they been tested?
  • If appropriate, are there adequate property based tests?
  • If appropriate, are there adequate unit tests?
  • Should any logging, debugging, tracing information be added or removed?
  • Are error messages user-friendly?
  • Have all documentation changes needed been made?
  • Have all non-obvious changes been commented?

System Compatibility

  • Are there any potential impacts on other parts of the system or backward compatibility?
  • Does this change intersect with any items on our roadmap, and if so, is there a plan for fitting them together?

Quality

  • Is this code of a unexpectedly high quality (Readability, Modularity, Intuitiveness)

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from 4bb47ac to 53142f3 Compare October 30, 2025 17:50
@jairad26 jairad26 changed the title [BUG] schema: build default with config ef & knn_index, remove #document population in defaults [BUG] schema: build default with config ef & default_knn_index, remove #document population in defaults Oct 30, 2025
@blacksmith-sh

This comment has been minimized.

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch 4 times, most recently from b3975e9 to 83d8fb6 Compare October 30, 2025 19:10
@blacksmith-sh

This comment has been minimized.

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch 2 times, most recently from b428f20 to 8f2ee14 Compare October 30, 2025 21:37
@blacksmith-sh

This comment has been minimized.

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch 2 times, most recently from 9ab2efd to 2f1e7bc Compare October 31, 2025 16:59
@blacksmith-sh

This comment has been minimized.

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch 5 times, most recently from 7ad31cf to dcb63e8 Compare November 1, 2025 01:08
@jairad26 jairad26 changed the base branch from main to graphite-base/5775 November 3, 2025 18:47
@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from dcb63e8 to 3cd61f8 Compare November 3, 2025 18:47
@jairad26 jairad26 changed the base branch from graphite-base/5775 to jai/fix-is-default-schema-check November 3, 2025 18:47
@jairad26 jairad26 changed the base branch from jai/fix-is-default-schema-check to graphite-base/5775 November 3, 2025 20:22
@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from 3cd61f8 to 1c0b019 Compare November 3, 2025 20:22
@jairad26 jairad26 changed the base branch from graphite-base/5775 to main November 3, 2025 20:23
@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from 1c0b019 to 463f83b Compare November 3, 2025 20:52
@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from 061ee06 to fe04c8f Compare November 6, 2025 18:00
// for both defaults and #embedding key
let mut new_schema = Schema::new_default(default_knn_index);

if collection_config.embedding_function.is_some() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Using and_then is more idiomatic rust here

Copy link
Contributor

Choose a reason for hiding this comment

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

also this should be method instead of inlining here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

im not sure what this means

space: Some(hnsw_config.space.clone()),
embedding_function: collection_config.embedding_function.clone(),
source_key: Some(DOCUMENT_KEY.to_string()), // Default source key
source_key: None,
Copy link
Contributor

Choose a reason for hiding this comment

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

Note to self: will need to migrate existing collections for uniformity

) -> Result<Schema, SchemaError> {
// Start with a default schema structure
let mut schema = Schema::new_default(KnnIndex::Spann); // Default to HNSW, will be overridden
let mut schema = Schema::new_default(default_knn_index);
Copy link
Contributor

Choose a reason for hiding this comment

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

why does it matter what default you pass here since it is overridden below by config anyways?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it doesn't but the code feels cleaner like this instead of creating a spann always

if let Some(embedding_types) = schema.keys.get_mut(EMBEDDING_KEY) {
if let Some(float_list) = &mut embedding_types.float_list {
if let Some(vector_index) = &mut float_list.vector_index {
let mut vector_config = vector_config.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

don't need to clone vector_config here

)
.map_err(CollectionsWithSegmentsProviderError::InvalidSchema)?;
collection_and_segments_sysdb.collection.schema = Some(reconciled_schema);
if collection_and_segments_sysdb.collection.schema.is_none() {
Copy link
Contributor

Choose a reason for hiding this comment

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

have we tested that schema is None and not {} for older collections?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes

.map_err(CollectionsWithSegmentsProviderError::InvalidSchema)?;
collection_and_segments_sysdb.collection.schema = Some(reconciled_schema);
if collection_and_segments_sysdb.collection.schema.is_none() {
collection_and_segments_sysdb.collection.schema = Some(
Copy link
Contributor

@sanketkedia sanketkedia Nov 7, 2025

Choose a reason for hiding this comment

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

If we are always passing schema down to the reader then should we update the reader in distributed_hnsw.rs to use schema instead of collection config (with fallback to legacy metadata). Similar to local_hnsw.rs. Makes things uniform and easier to understand. (I understand that the current code is also correct, this is just a code design nit)

)
.map_err(SpannSegmentWriterError::InvalidSchema)?;
let schema = if let Some(schema) = collection.schema.as_ref() {
schema.clone()
Copy link
Contributor

Choose a reason for hiding this comment

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

why clone here?

Copy link
Contributor Author

@jairad26 jairad26 Nov 7, 2025

Choose a reason for hiding this comment

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

because i cant do collection.schema directly, i have to borrow the schema with as_ref

the other alternative would be to borrow collection.schema directly. this works, just want to confirm this is safe?

        let schema = if let Some(schema) = &collection.schema {
            schema
        } else {
            &Schema::convert_collection_config_to_schema(&collection.config, KnnIndex::Spann)
                .map_err(SpannSegmentWriterError::InvalidSchema)?
        };

/// The read path needs to tolerate collections that only have a configuration persisted.
/// This helper hydrates `schema` from the stored configuration when needed, or regenerates
/// the configuration from the existing schema to keep both representations consistent.
pub fn reconcile_schema_for_read(&mut self, knn_index: KnnIndex) -> Result<(), SchemaError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

why read path needs to take a default_knn config. That seems like unnecessary because the knn index type has already been decided and persisted

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from fe04c8f to db3b74e Compare November 7, 2025 19:26
@blacksmith-sh

This comment has been minimized.

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from db3b74e to f8cfb53 Compare November 10, 2025 17:10
Int *IntValueType `json:"int,omitempty"`
Float *FloatValueType `json:"float,omitempty"`
Boolean *BoolValueType `json:"boolean,omitempty"`
Boolean *BoolValueType `json:"bool,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

[CriticalError]

The JSON tag has been changed from "boolean" to "bool" in the Go struct and corresponding test JSON strings. However, there's a critical compatibility issue to consider:

  1. Breaking Change Impact: This JSON field name change will break compatibility with existing stored data or API clients that expect the "boolean" field name.

  2. Migration Strategy: If this change is intentional to match Rust's naming convention, you'll need to ensure:

    • Database migration to rename existing boolean fields to bool
    • API versioning to handle both field names during transition
    • Client SDK updates across all languages

If this is purely for Go-Rust JSON compatibility (as mentioned in the PR description), consider:

// Option 1: Support both during transition
Boolean *BoolValueType `json:"bool,omitempty" legacy:"boolean,omitempty"`

// Option 2: Custom marshaling to handle both names

Could you clarify the migration plan for this breaking change?

Context for Agents
[**CriticalError**]

The JSON tag has been changed from `"boolean"` to `"bool"` in the Go struct and corresponding test JSON strings. However, there's a critical compatibility issue to consider:

1. **Breaking Change Impact**: This JSON field name change will break compatibility with existing stored data or API clients that expect the `"boolean"` field name.

2. **Migration Strategy**: If this change is intentional to match Rust's naming convention, you'll need to ensure:
   - Database migration to rename existing `boolean` fields to `bool`
   - API versioning to handle both field names during transition
   - Client SDK updates across all languages

If this is purely for Go-Rust JSON compatibility (as mentioned in the PR description), consider:
```go
// Option 1: Support both during transition
Boolean *BoolValueType `json:"bool,omitempty" legacy:"boolean,omitempty"`

// Option 2: Custom marshaling to handle both names
```

Could you clarify the migration plan for this breaking change?

File: go/pkg/sysdb/coordinator/model/collection_configuration.go
Line: 252

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from f8cfb53 to 2b76659 Compare November 10, 2025 17:37
collection_and_segments_sysdb.collection.schema = Some(reconciled_schema);
if collection_and_segments_sysdb.collection.schema.is_none() {
collection_and_segments_sysdb.collection.schema = Some(
Schema::convert_collection_config_to_schema(
Copy link
Contributor

Choose a reason for hiding this comment

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

[BestPractice]

Critical schema handling gap: When collection.schema is None in the reader path, this code converts config to schema but doesn't persist the result. The conversion is done in-memory only, meaning:

  1. Inconsistent state: The reader gets the converted schema, but the database still has schema = None
  2. Performance impact: Schema conversion happens on every read operation instead of once
  3. Potential race conditions: Multiple readers could be converting the same config simultaneously

Consider adding a mechanism to persist the converted schema back to the database after conversion, or implement a write-through cache to avoid repeated conversions.

// After conversion, consider persisting:
if was_none_before_conversion {
    // Persist the converted schema to avoid future conversions
    self.persist_schema_if_needed(collection_id, &converted_schema).await?;
}
Context for Agents
[**BestPractice**]

Critical schema handling gap: When `collection.schema` is `None` in the reader path, this code converts config to schema but doesn't persist the result. The conversion is done in-memory only, meaning:

1. **Inconsistent state**: The reader gets the converted schema, but the database still has `schema = None`
2. **Performance impact**: Schema conversion happens on every read operation instead of once
3. **Potential race conditions**: Multiple readers could be converting the same config simultaneously

Consider adding a mechanism to persist the converted schema back to the database after conversion, or implement a write-through cache to avoid repeated conversions.

```rust
// After conversion, consider persisting:
if was_none_before_conversion {
    // Persist the converted schema to avoid future conversions
    self.persist_schema_if_needed(collection_id, &converted_schema).await?;
}
```

File: rust/frontend/src/get_collection_with_segments_provider.rs
Line: 190

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from 2b76659 to e0d238f Compare November 10, 2025 18:04
@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch 2 times, most recently from 5fafa38 to 7c682b1 Compare November 10, 2025 23:47
.collection
.reconcile_schema_with_config(KnnIndex::Hnsw)?;
if !schema_previously_persisted {
collection_and_segments.collection.schema = Some(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Minor change - for local we init the schema here if None but for distributed we init in the writer? Would be good to make the behavior uniform across the two


impl Collection {
/// Reconcile the collection schema and configuration, ensuring both are consistent.
pub fn reconcile_schema_with_config(&mut self, knn_index: KnnIndex) -> Result<(), SchemaError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this method used anywhere now?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can remove this method. Only the in-memory frontend uses it but that should also be changed?

@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from 7c682b1 to 8671ea1 Compare November 12, 2025 23:56
@jairad26 jairad26 force-pushed the jai/fix-default-path-reconcile branch from 8671ea1 to c129192 Compare November 13, 2025 00:15
@jairad26 jairad26 merged commit 3d9843b into main Nov 13, 2025
62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants