Repository: qdrant/qdrant
File: lib/api/src/rest/schema.rs (Rust)

Code before:
    use super::*;

    #[test]
    fn test_document_options_should_deserialize_into_common() {
        let json = Bm25Config {
            tokenizer: TokenizerType::Word,
            ..Default::default()
        };
        let valid_bm25_config = serde_json::to_string(&json).unwrap();
        let options: DocumentOptions = serde_json::from_str(&valid_bm25_config).unwrap();
        // Bm25 option is used only for schema, actual deserialization will happen in specialized code
        assert!(matches!(options, DocumentOptions::Common(_)));
    }
}

impl DocumentOptions {
    pub fn into_options(self) -> HashMap<String, Value> {
        match self {
            DocumentOptions::Common(options) => options,
            DocumentOptions::Bm25(bm25) => bm25.to_options(),
        }
    }
}

impl Hash for DocumentOptions {
    fn hash<H: Hasher>(&self, state: &mut H) {
        let options = match self {
            DocumentOptions::Common(options) => Cow::Borrowed(options),
            DocumentOptions::Bm25(bm25) => Cow::Owned(bm25.to_options()),
        };

        // Order of keys in the map should not affect the hash
        let mut keys: Vec<_> = options.keys().collect();
        keys.sort();
        for key in keys {
            key.hash(state);
            options.get(key).unwrap().hash(state);
        }
    }
}

/// WARN: Work-in-progress, unimplemented
///
/// Text document for embedding. Requires inference infrastructure, unimplemented.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema, Hash, Validate)]
pub struct Document {
    /// Text of the document.
    /// This field will be used as input for the embedding model.
    #[schemars(example = "document_text_example")]
    pub text: String,
    /// Name of the model used to generate the vector.

Review comment (bug):
Can we maybe just collect the options into a `BTreeMap` instead? I think this custom hashing approach might cause hard to detect bugs in future.
Alternatively we could just replace the initial `HashMap` with a `BTreeMap`, the APIs should be the same.

Code after:
        };
        let valid_bm25_config = serde_json::to_string(&json).unwrap();
        let options: DocumentOptions = serde_json::from_str(&valid_bm25_config).unwrap();
        // Bm25 option is used only for schema, actual deserialization will happen in specialized code
        assert!(matches!(options, DocumentOptions::Common(_)));
    }
}

impl DocumentOptions {
    pub fn into_options(self) -> HashMap<String, Value> {
        match self {
            DocumentOptions::Common(options) => options,
            DocumentOptions::Bm25(bm25) => bm25.to_options(),
        }
    }
}

impl Hash for DocumentOptions {
    fn hash<H: Hasher>(&self, state: &mut H) {
        let options = match self {
            DocumentOptions::Common(options) => Cow::Borrowed(options),
            DocumentOptions::Bm25(bm25) => Cow::Owned(bm25.to_options()),
        };

        // Order of keys in the map should not affect the hash
        let mut keys: Vec<_> = options.keys().collect();
        keys.sort();
        for key in keys {
            key.hash(state);
            options.get(key).unwrap().hash(state);
        }
    }
}

/// WARN: Work-in-progress, unimplemented
///
/// Text document for embedding. Requires inference infrastructure, unimplemented.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema, Hash, Validate)]
pub struct Document {
    /// Text of the document.
    /// This field will be used as input for the embedding model.
    #[schemars(example = "document_text_example")]
    pub text: String,
    /// Name of the model used to generate the vector.
    /// List of available models depends on a provider.
    #[validate(length(min = 1))]
    #[schemars(length(min = 1), example = "model_example")]
    pub model: String,
    /// Additional options for the model, will be passed to the inference service as-is.
    /// See model cards for available options.
    #[serde(default, skip_serializing_if = "Option::is_none")]