Skip to main content

velesdb_core/
point.rs

1//! Point data structure representing a vector with metadata.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value as JsonValue;
7
8use crate::sparse_index::{SparseVector, DEFAULT_SPARSE_INDEX_NAME};
9
10/// A point in the vector database.
11///
12/// A point consists of:
13/// - A unique identifier
14/// - A dense vector (embedding)
15/// - Optional payload (metadata)
16/// - Optional named sparse vectors (e.g., SPLADE, BM25 term weights)
17#[derive(Debug, Clone, Serialize)]
18pub struct Point {
19    /// Unique identifier for the point.
20    pub id: u64,
21
22    /// The dense vector embedding.
23    pub vector: Vec<f32>,
24
25    /// Optional JSON payload containing metadata.
26    #[serde(default)]
27    pub payload: Option<JsonValue>,
28
29    /// Optional named sparse vectors for hybrid dense+sparse search.
30    ///
31    /// Keys are sparse vector names (e.g., `""` for default, `"title"`, `"body"`).
32    /// Enables multi-model support (BGE-M3, SPLADE title+body).
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub sparse_vectors: Option<BTreeMap<String, SparseVector>>,
35}
36
37/// Custom deserializer that accepts both:
38/// - `"sparse_vectors": {"name": {...}}` (new named map format)
39/// - `"sparse_vector": {...}` (old single format, wraps in map under `""` key)
40impl<'de> Deserialize<'de> for Point {
41    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
42    where
43        D: serde::Deserializer<'de>,
44    {
45        #[derive(Deserialize)]
46        struct PointHelper {
47            id: u64,
48            vector: Vec<f32>,
49            #[serde(default)]
50            payload: Option<JsonValue>,
51            #[serde(default)]
52            sparse_vectors: Option<BTreeMap<String, SparseVector>>,
53            /// Old single-vector field for backward compat.
54            #[serde(default)]
55            sparse_vector: Option<SparseVector>,
56        }
57
58        let helper = PointHelper::deserialize(deserializer)?;
59
60        // Prefer new `sparse_vectors` field; fall back to old `sparse_vector`.
61        let sparse_vectors = if helper.sparse_vectors.is_some() {
62            helper.sparse_vectors
63        } else {
64            helper.sparse_vector.map(|sv| {
65                let mut map = BTreeMap::new();
66                // Use the canonical constant to avoid magic empty-string literals.
67                map.insert(DEFAULT_SPARSE_INDEX_NAME.to_string(), sv);
68                map
69            })
70        };
71
72        Ok(Point {
73            id: helper.id,
74            vector: helper.vector,
75            payload: helper.payload,
76            sparse_vectors,
77        })
78    }
79}
80
81impl Point {
82    /// Creates a new point with the given ID, vector, and optional payload.
83    ///
84    /// # Arguments
85    ///
86    /// * `id` - Unique identifier
87    /// * `vector` - Vector embedding
88    /// * `payload` - Optional metadata
89    #[must_use]
90    pub fn new(id: u64, vector: Vec<f32>, payload: Option<JsonValue>) -> Self {
91        Self {
92            id,
93            vector,
94            payload,
95            sparse_vectors: None,
96        }
97    }
98
99    /// Creates a new point without payload.
100    #[must_use]
101    pub fn without_payload(id: u64, vector: Vec<f32>) -> Self {
102        Self::new(id, vector, None)
103    }
104
105    /// Creates a metadata-only point (no vector, only payload).
106    ///
107    /// Used for metadata-only collections that don't store vectors.
108    ///
109    /// # Arguments
110    ///
111    /// * `id` - Unique identifier
112    /// * `payload` - Metadata (JSON value)
113    #[must_use]
114    pub fn metadata_only(id: u64, payload: JsonValue) -> Self {
115        Self {
116            id,
117            vector: Vec::new(), // Empty vector
118            payload: Some(payload),
119            sparse_vectors: None,
120        }
121    }
122
123    /// Creates a point with both dense and named sparse vectors.
124    ///
125    /// # Arguments
126    ///
127    /// * `id` - Unique identifier
128    /// * `vector` - Dense vector embedding
129    /// * `payload` - Optional metadata
130    /// * `sparse_vectors` - Optional named sparse vectors
131    #[must_use]
132    pub fn with_sparse(
133        id: u64,
134        vector: Vec<f32>,
135        payload: Option<JsonValue>,
136        sparse_vectors: Option<BTreeMap<String, SparseVector>>,
137    ) -> Self {
138        Self {
139            id,
140            vector,
141            payload,
142            sparse_vectors,
143        }
144    }
145
146    /// Creates a sparse-only point (no dense vector).
147    ///
148    /// # Arguments
149    ///
150    /// * `id` - Unique identifier
151    /// * `sparse_vector` - The sparse vector (stored under the default `""` name)
152    /// * `payload` - Optional metadata
153    #[must_use]
154    pub fn sparse_only(id: u64, sparse_vector: SparseVector, payload: Option<JsonValue>) -> Self {
155        let mut map = BTreeMap::new();
156        // Use the canonical constant to avoid magic empty-string literals.
157        map.insert(DEFAULT_SPARSE_INDEX_NAME.to_string(), sparse_vector);
158        Self {
159            id,
160            vector: Vec::new(),
161            payload,
162            sparse_vectors: Some(map),
163        }
164    }
165
166    /// Returns `true` if this point has any sparse vectors.
167    #[must_use]
168    pub fn has_sparse_vectors(&self) -> bool {
169        self.sparse_vectors.as_ref().is_some_and(|m| !m.is_empty())
170    }
171
172    /// Returns the dimension of the vector.
173    #[must_use]
174    pub fn dimension(&self) -> usize {
175        self.vector.len()
176    }
177
178    /// Returns true if this point has no vector (metadata-only).
179    #[must_use]
180    pub fn is_metadata_only(&self) -> bool {
181        self.vector.is_empty()
182    }
183}
184
185/// A search result containing a point and its similarity score.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct SearchResult {
188    /// The matching point.
189    pub point: Point,
190
191    /// Similarity score (interpretation depends on the distance metric).
192    pub score: f32,
193}
194
195impl SearchResult {
196    /// Creates a new search result.
197    #[must_use]
198    pub const fn new(point: Point, score: f32) -> Self {
199        Self { point, score }
200    }
201}