This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 444da2a1 fix(lumina): store the canonical native metric name in index 
metadata (#895)
444da2a1 is described below

commit 444da2a168fa8220bdea6fdccb8500a8a3608f37
Author: jackylee <[email protected]>
AuthorDate: Tue Sep 22 14:15:17 2026 +0800

    fix(lumina): store the canonical native metric name in index metadata (#895)
---
 crates/paimon/src/lumina/mod.rs | 84 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 83 insertions(+), 1 deletion(-)

diff --git a/crates/paimon/src/lumina/mod.rs b/crates/paimon/src/lumina/mod.rs
index 56796e2c..2302fd9f 100644
--- a/crates/paimon/src/lumina/mod.rs
+++ b/crates/paimon/src/lumina/mod.rs
@@ -135,7 +135,8 @@ impl LuminaVectorIndexOptions {
             .cloned()
             .unwrap_or_else(|| "diskann".to_string());
 
-        let lumina_options = build_lumina_options(paimon_options, dimension)?;
+        let mut lumina_options = build_lumina_options(paimon_options, 
dimension)?;
+        canonicalize_metric(&mut lumina_options);
 
         Ok(Self {
             dimension,
@@ -150,6 +151,26 @@ impl LuminaVectorIndexOptions {
     }
 }
 
+/// Rewrite `distance.metric` to the spelling `from_lumina_name` accepts.
+///
+/// The option parser also takes the enum names, via `from_string` above, but
+/// `LuminaIndexMeta::metric` is exact-match, so the configured spelling must 
not
+/// reach the committed metadata. An unrecognized value is left alone, keeping 
its
+/// existing error surface.
+fn canonicalize_metric(native_options: &mut HashMap<String, String>) {
+    let Some(configured) = native_options.get(KEY_DISTANCE_METRIC) else {
+        return;
+    };
+    let parsed = LuminaVectorMetric::from_lumina_name(configured)
+        .or_else(|_| LuminaVectorMetric::from_string(configured));
+    if let Ok(metric) = parsed {
+        native_options.insert(
+            KEY_DISTANCE_METRIC.to_string(),
+            metric.lumina_name().to_string(),
+        );
+    }
+}
+
 fn validate_encoding_metric(encoding: &str, metric: LuminaVectorMetric) -> 
crate::Result<()> {
     if encoding.eq_ignore_ascii_case("pq") && metric == 
LuminaVectorMetric::Cosine {
         return Err(crate::Error::DataInvalid {
@@ -226,6 +247,9 @@ pub fn strip_lumina_options(paimon_options: 
&HashMap<String, String>) -> HashMap
             result.insert(native_key.to_string(), value.to_string());
         }
     }
+    // `LuminaVectorReader::ensure_loaded` overlays this map on top of the 
index
+    // metadata, so a table option spelled `L2` would put the enum name back.
+    canonicalize_metric(&mut result);
     result
 }
 
@@ -494,4 +518,62 @@ mod tests {
         assert_eq!(lumina_opts.get("encoding.pq.m").unwrap(), "64");
         assert_eq!(lumina_opts.get("search.parallel_number").unwrap(), "5");
     }
+
+    #[test]
+    fn test_enum_form_metric_is_canonicalized_in_native_options() {
+        // The native-spelling rows pin that canonicalization is a no-op for 
them.
+        let cases = [
+            ("L2", "l2"),
+            ("COSINE", "cosine"),
+            ("INNER_PRODUCT", "inner_product"),
+            ("l2", "l2"),
+            ("cosine", "cosine"),
+            ("inner_product", "inner_product"),
+        ];
+        for (configured, native) in cases {
+            let paimon_options = HashMap::from([
+                ("lumina.index.dimension".to_string(), "4".to_string()),
+                // `cosine` is rejected with the default `pq` encoding.
+                ("lumina.encoding.type".to_string(), "rawf32".to_string()),
+                ("lumina.distance.metric".to_string(), configured.to_string()),
+            ]);
+            let options = 
LuminaVectorIndexOptions::new(&paimon_options).unwrap();
+            let native_options = options.to_lumina_options();
+            assert_eq!(
+                native_options.get(KEY_DISTANCE_METRIC).map(String::as_str),
+                Some(native),
+                "configured: {configured}"
+            );
+            let metric = LuminaIndexMeta::new(native_options)
+                .metric()
+                .unwrap_or_else(|e| panic!("configured {configured}: {e}"));
+            assert_eq!(metric.lumina_name(), native, "configured: 
{configured}");
+        }
+    }
+
+    #[test]
+    fn test_stripped_search_options_use_the_canonical_metric_name() {
+        let stripped = strip_lumina_options(&HashMap::from([(
+            "lumina.distance.metric".to_string(),
+            "L2".to_string(),
+        )]));
+        assert_eq!(
+            stripped.get(KEY_DISTANCE_METRIC).map(String::as_str),
+            Some("l2")
+        );
+    }
+
+    /// An unknown metric keeps its existing error surface rather than being
+    /// defaulted here. Passes without the fix too.
+    #[test]
+    fn test_stripped_search_options_pass_an_unknown_metric_through() {
+        let stripped = strip_lumina_options(&HashMap::from([(
+            "lumina.distance.metric".to_string(),
+            "hamming".to_string(),
+        )]));
+        assert_eq!(
+            stripped.get(KEY_DISTANCE_METRIC).map(String::as_str),
+            Some("hamming")
+        );
+    }
 }

Reply via email to