Skip to main content

qdrant_edge/segment/index/
payload_config.rs

1use std::collections::HashMap;
2use std::ops::{Deref, DerefMut};
3use std::path::{Path, PathBuf};
4
5use crate::common::fs::{atomic_save_json, read_json};
6use crate::common::universal_io::{OkNotFound, UniversalReadFs, read_json_via};
7use serde::{Deserialize, Serialize};
8
9use crate::segment::common::operation_error::OperationResult;
10use crate::segment::types::{PayloadFieldSchema, PayloadKeyType};
11
12pub const PAYLOAD_INDEX_CONFIG_FILE: &str = "config.json";
13
14/// Keeps information of which field should be index
15#[derive(Debug, Default, Deserialize, Serialize, Clone)]
16pub struct PayloadConfig {
17    /// Mapping of payload index schemas and types
18    #[serde(flatten)]
19    pub indices: PayloadIndices,
20}
21
22impl PayloadConfig {
23    pub fn get_config_path(path: &Path) -> PathBuf {
24        path.join(PAYLOAD_INDEX_CONFIG_FILE)
25    }
26
27    pub fn load(path: &Path) -> OperationResult<Self> {
28        Ok(read_json(path)?)
29    }
30
31    /// Read-only mirror of [`load`](Self::load) over the universal filesystem
32    /// `fs`. Returns `Ok(None)` when the config file is absent.
33    pub fn load_universal<Fs: UniversalReadFs>(
34        fs: &Fs,
35        path: &Path,
36    ) -> OperationResult<Option<Self>> {
37        Ok(read_json_via(fs, path).ok_not_found()?)
38    }
39
40    pub fn save(&self, path: &Path) -> OperationResult<()> {
41        Ok(atomic_save_json(path, self)?)
42    }
43}
44
45/// Map of indexed fields with their schema and type
46///
47/// Virtual structure, serialized and deserialized through `PayloadIndicesStorage`.
48#[derive(Debug, Default, Deserialize, Serialize, Clone)]
49#[serde(from = "PayloadIndicesStorage", into = "PayloadIndicesStorage")]
50pub struct PayloadIndices {
51    fields: HashMap<PayloadKeyType, PayloadFieldSchemaWithIndexType>,
52}
53
54impl PayloadIndices {
55    pub fn to_schemas(&self) -> HashMap<PayloadKeyType, PayloadFieldSchema> {
56        self.fields
57            .iter()
58            .map(|(field, index)| (field.clone(), index.schema.clone()))
59            .collect()
60    }
61}
62
63impl Deref for PayloadIndices {
64    type Target = HashMap<PayloadKeyType, PayloadFieldSchemaWithIndexType>;
65    fn deref(&self) -> &Self::Target {
66        &self.fields
67    }
68}
69
70impl DerefMut for PayloadIndices {
71    fn deref_mut(&mut self) -> &mut Self::Target {
72        &mut self.fields
73    }
74}
75
76/// Storage helper for `PayloadIndices`
77///
78/// This type is used for serialization and deserialization of the payload indices. It is
79/// compatible with the old format of payload indices, which only stored the indexed fields.
80#[derive(Deserialize, Serialize)]
81pub struct PayloadIndicesStorage {
82    /// Map of indexed fields and their schema
83    pub indexed_fields: HashMap<PayloadKeyType, PayloadFieldSchema>,
84
85    /// Map of indexed fields and their explicit index types
86    ///
87    /// If empty for a field, no explicit payload index type mappings have been stored yet.
88    /// Then use `schemas` to determine the index types with a best effort approach.
89    ///
90    /// Added since Qdrant 1.15
91    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
92    pub indexed_types: HashMap<PayloadKeyType, Vec<FullPayloadIndexType>>,
93}
94
95impl From<PayloadIndicesStorage> for PayloadIndices {
96    fn from(mut storage: PayloadIndicesStorage) -> Self {
97        let fields = storage
98            .indexed_fields
99            .into_iter()
100            .map(|(field, schema)| {
101                let index_types = storage.indexed_types.remove(&field).unwrap_or_default();
102                (
103                    field,
104                    PayloadFieldSchemaWithIndexType::new(schema, index_types),
105                )
106            })
107            .collect::<HashMap<_, _>>();
108        Self { fields }
109    }
110}
111
112impl From<PayloadIndices> for PayloadIndicesStorage {
113    fn from(storage: PayloadIndices) -> Self {
114        let (indexed_fields, indexed_types) = storage.fields.into_iter().fold(
115            (HashMap::new(), HashMap::new()),
116            |(mut fields, mut types), (field, schema)| {
117                fields.insert(field.clone(), schema.schema);
118                if !schema.types.is_empty() {
119                    types.insert(field, schema.types);
120                }
121                (fields, types)
122            },
123        );
124        Self {
125            indexed_fields,
126            indexed_types,
127        }
128    }
129}
130
131#[derive(Debug, Clone, PartialEq)]
132pub struct PayloadFieldSchemaWithIndexType {
133    pub schema: PayloadFieldSchema,
134    pub types: Vec<FullPayloadIndexType>,
135}
136
137impl PayloadFieldSchemaWithIndexType {
138    pub fn new(schema: PayloadFieldSchema, types: Vec<FullPayloadIndexType>) -> Self {
139        Self { schema, types }
140    }
141}
142
143#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, strum::EnumIter)]
144#[serde(rename_all = "snake_case")]
145pub enum PayloadIndexType {
146    IntIndex,
147    DatetimeIndex,
148    IntMapIndex,
149    KeywordIndex,
150    FloatIndex,
151    GeoIndex,
152    FullTextIndex,
153    BoolIndex,
154    UuidIndex,
155    UuidMapIndex,
156    NullIndex,
157}
158
159#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
160pub struct FullPayloadIndexType {
161    pub index_type: PayloadIndexType,
162    pub mutability: IndexMutability,
163    pub storage_type: StorageType,
164}
165
166impl FullPayloadIndexType {
167    pub fn mutability(&self) -> &IndexMutability {
168        &self.mutability
169    }
170}
171
172#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)]
173#[serde(rename_all = "snake_case")]
174pub enum IndexMutability {
175    /// Supports insertions, updates, and deletions
176    Mutable,
177    /// Only supports deletions
178    Immutable,
179}
180
181#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)]
182#[serde(rename_all = "snake_case")]
183pub enum StorageType {
184    Gridstore,
185    Mmap { is_on_disk: bool },
186}
187
188#[cfg(test)]
189mod test {
190    use std::str::FromStr;
191
192    use serde_json::Value;
193
194    use super::*;
195    use crate::segment::json_path::JsonPath;
196
197    #[test]
198    fn test_storage_compatibility() {
199        // Check that old format can be parsed with new format.
200
201        let old = r#"{"indexed_fields":{"c":{"type":"integer","lookup":true,"range":false,"is_principal":false,"on_disk":false}}}"#;
202        let payload_config: PayloadConfig = serde_json::from_str(old).unwrap();
203
204        let old_value: Value = serde_json::from_str(old).unwrap();
205        let old_schema = old_value
206            .as_object()
207            .unwrap()
208            .get("indexed_fields")
209            .unwrap()
210            .get("c")
211            .unwrap()
212            .clone();
213
214        let old_config: PayloadFieldSchema = serde_json::from_value(old_schema).unwrap();
215        assert_eq!(
216            payload_config
217                .indices
218                .get(&JsonPath::from_str("c").unwrap())
219                .unwrap()
220                .schema,
221            old_config
222        );
223    }
224}