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}