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

Code before:
/// Ether general-purpose options or BM25-specific options.
/// BM25-specific will only take effect if the `qdrant/bm25` is specified as a model.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(untagged, rename_all = "snake_case")]
pub enum DocumentOptions {
    // This option should go first
    Common(HashMap<String, Value>),
    // This should never be deserialized into, but we keep it for schema generation
    Bm25(Bm25Config),
}

#[cfg(test)]
mod tests {
    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);
        }

Review comment (suggestion):
If we're never going to serialize the bm25 variant, I'd suggest to make this clear by using `unreachable!("BM25 variant never gets created");` so we will notice when we accidentally create it.
The same below

Code after:
    // This should never be deserialized into, but we keep it for schema generation
    Bm25(Bm25Config),
}

#[cfg(test)]
mod tests {
    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)]