Skip to main content

ecp_core/search/
info.rs

1use std::path::PathBuf;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use zarrs::array::{Array, ArrayCreateError};
6use zarrs::filesystem::FilesystemStore;
7use zarrs::storage::ReadableListableStorage;
8
9use crate::error::{EcpError, Result, ResultExt};
10use crate::format::{FORMAT_VERSION, V1_ARRAYS};
11use crate::metric::Metric;
12
13/// Opens the array at `path` in `store`, or returns `None` if there is no
14/// such array.
15fn open_optional(
16    store: &ReadableListableStorage,
17    path: &str,
18) -> Result<Option<Array<dyn zarrs::storage::ReadableListableStorageTraits>>> {
19    match Array::open(store.clone(), path) {
20        Ok(array) => Ok(Some(array)),
21        Err(ArrayCreateError::MissingMetadata) => Ok(None),
22        Err(other) => Err(EcpError::Store(format!("failed to open {path}: {other}"))),
23    }
24}
25
26/// Opens the array at `info/{name}` in `store`. A missing array means the
27/// store isn't a valid index, so this maps to [`EcpError::Corrupt`] rather
28/// than [`EcpError::NotFound`].
29fn open_info_field(
30    store: &ReadableListableStorage,
31    name: &str,
32) -> Result<Array<dyn zarrs::storage::ReadableListableStorageTraits>> {
33    open_optional(store, &format!("/info/{name}"))?
34        .ok_or_else(|| EcpError::Corrupt(format!("not a valid index: /info/{name} is missing")))
35}
36
37/// Reads `info/format_version` and refuses a version this build does not read.
38/// `None` means the field is missing from an index that has every other array
39/// a version 1 index holds. If others are missing too, the error names them all.
40pub(super) fn read_format_version(store: &ReadableListableStorage) -> Result<Option<u32>> {
41    let Some(array) = open_optional(store, "/info/format_version")? else {
42        let mut missing = vec!["/info/format_version"];
43        for path in V1_ARRAYS {
44            if open_optional(store, path)?.is_none() {
45                missing.push(path);
46            }
47        }
48        if missing.len() > 1 {
49            return Err(EcpError::Corrupt(format!(
50                "not a valid index, missing: {}",
51                missing.join(", ")
52            )));
53        }
54        return Ok(None);
55    };
56
57    let version = array
58        .retrieve_array_subset::<Vec<u32>>(&array.subset_all())
59        .store_err("failed to retrieve info/format_version")?[0];
60    if version != FORMAT_VERSION {
61        return Err(EcpError::UnsupportedVersion(format!(
62            "index format version {version} is not supported, this build reads version {FORMAT_VERSION}"
63        )));
64    }
65    Ok(Some(version))
66}
67
68/// Reads `info/levels`, `info/metric` and `info/is_normalized`, which is
69/// `None` when the index does not record it.
70pub(super) fn read_info_fields(
71    store: &ReadableListableStorage,
72) -> Result<(u32, Metric, Option<bool>)> {
73    let levels_array = open_info_field(store, "levels")?;
74    let levels: u32 = levels_array
75        .retrieve_array_subset::<Vec<u32>>(&levels_array.subset_all())
76        .store_err("failed to retrieve info/levels")?[0];
77
78    let metric_array = open_info_field(store, "metric")?;
79    let metric_str = metric_array
80        .retrieve_array_subset::<Vec<String>>(&metric_array.subset_all())
81        .store_err("failed to retrieve info/metric")?
82        .remove(0);
83    let metric = Metric::from_str(&metric_str)
84        .map_err(|e| EcpError::Corrupt(format!("info/metric holds an unrecognized metric: {e}")))?;
85
86    let is_normalized = match open_optional(store, "/info/is_normalized")? {
87        Some(array) => Some(
88            array
89                .retrieve_array_subset::<Vec<bool>>(&array.subset_all())
90                .store_err("failed to retrieve info/is_normalized")?[0],
91        ),
92        None => None,
93    };
94
95    Ok((levels, metric, is_normalized))
96}
97
98/// Reads the `u32` scalar at `info/{name}`.
99pub(super) fn read_info_u32(store: &ReadableListableStorage, name: &str) -> Result<u32> {
100    let array = open_info_field(store, name)?;
101    Ok(array
102        .retrieve_array_subset::<Vec<u32>>(&array.subset_all())
103        .store_err_with(|| format!("failed to retrieve info/{name}"))?[0])
104}
105
106/// An index's `info/*` fields and representative count, read without
107/// loading the tree.
108pub struct IndexInfo {
109    /// The version of the on-disk format. An index missing the field but
110    /// otherwise complete reads as the current version.
111    pub format_version: u32,
112    /// Node levels below the root; the last one holds the leaves.
113    pub levels: u32,
114    pub metric: Metric,
115    /// Whether every stored embedding is unit-length, or `None` if the index
116    /// does not say.
117    pub is_normalized: Option<bool>,
118    /// Items stored in the index.
119    pub total_items: u32,
120    /// The id `insert` gives the next item. Equal to `total_items` unless a
121    /// crash stopped an insert between taking ids and writing the items.
122    pub next_item_id: u32,
123    /// How many representatives the build picked, one per leaf node. Read
124    /// from `/rep_item_ids`'s length.
125    pub total_representatives: u32,
126}
127
128impl IndexInfo {
129    /// Loads an index's info fields from `index_path`.
130    pub fn load(index_path: PathBuf) -> Result<Self> {
131        if !index_path.exists() {
132            return Err(EcpError::NotFound(format!(
133                "index directory {} does not exist",
134                index_path.display()
135            )));
136        }
137        let store: ReadableListableStorage =
138            Arc::new(FilesystemStore::new(&index_path).store_err("failed to open store")?);
139        Self::load_from_store(store)
140    }
141
142    /// Reads the info fields from `store` instead of a path. Otherwise the same as `load`.
143    /// Note: This function is only split out from `load` for unit tests.
144    fn load_from_store(store: ReadableListableStorage) -> Result<Self> {
145        let format_version = read_format_version(&store)?.unwrap_or(FORMAT_VERSION);
146        let (levels, metric, is_normalized) = read_info_fields(&store)?;
147        let total_items = read_info_u32(&store, "total_items")?;
148        let next_item_id = read_info_u32(&store, "next_item_id")?;
149
150        let rep_ids_array = Array::open(store.clone(), "/rep_item_ids").map_err(|e| match e {
151            ArrayCreateError::MissingMetadata => {
152                EcpError::Corrupt("not a valid index: /rep_item_ids is missing".to_string())
153            }
154            other => EcpError::Store(format!("failed to open rep_item_ids: {other}")),
155        })?;
156        let total_representatives = rep_ids_array.shape()[0] as u32;
157
158        Ok(IndexInfo {
159            format_version,
160            levels,
161            metric,
162            is_normalized,
163            total_items,
164            next_item_id,
165            total_representatives,
166        })
167    }
168}
169
170#[cfg(test)]
171#[path = "utests/info.rs"]
172mod tests;