1use crate::{
4 Collection, CollectionConfig, Database, DeleteSelector, Error, GetRequest, Point, PointId,
5 Query, Record, Result, ScoredPoint, WriteResult,
6};
7use git2::ErrorCode;
8use std::fs;
9use std::path::Path;
10use std::thread;
11
12const WRITE_ATTEMPTS: usize = 8;
13
14pub fn open(path: impl AsRef<Path>) -> Result<Store> {
20 Store::open(path)
21}
22
23#[derive(Clone, Debug)]
25pub struct Store {
26 database: Database,
27}
28
29#[derive(Clone, Debug)]
31pub struct CollectionHandle {
32 database: Database,
33 name: String,
34}
35
36impl Store {
37 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
39 let path = path.as_ref();
40 let database = match Database::open(path) {
41 Ok(database) => database,
42 Err(_) if !path.exists() => Database::init_bare(path)?,
43 Err(_) if path.is_dir() && directory_is_empty(path)? => Database::init_bare(path)?,
44 Err(open_error) if path.is_dir() => {
45 return Err(Error::Invalid(format!(
46 "database path {} is a nonempty directory but not a Git repository: {open_error}",
47 path.display()
48 )))
49 }
50 Err(open_error) => return Err(open_error),
51 };
52 Ok(Self { database })
53 }
54
55 pub fn collection(&self, name: impl Into<String>) -> CollectionHandle {
60 CollectionHandle {
61 database: self.database.clone(),
62 name: name.into(),
63 }
64 }
65
66 pub fn list_collections(&self) -> Result<Vec<String>> {
68 self.database.list_collections()
69 }
70
71 pub fn advanced(&self) -> &Database {
73 &self.database
74 }
75}
76
77impl CollectionHandle {
78 pub fn upsert(&self, points: impl IntoIterator<Item = Point>) -> Result<WriteResult> {
83 self.upsert_with_vector_space(points, None)
84 }
85
86 pub(crate) fn upsert_with_vector_space(
87 &self,
88 points: impl IntoIterator<Item = Point>,
89 vector_space: Option<&str>,
90 ) -> Result<WriteResult> {
91 let points: Vec<Point> = points.into_iter().collect();
92 let dimension = inferred_dimension(&points)?;
93
94 for _ in 0..WRITE_ATTEMPTS {
95 let collection = match self.database.collection(&self.name) {
96 Ok(collection) => collection,
97 Err(Error::CollectionNotFound(_)) => {
98 let mut config = CollectionConfig::new(dimension);
99 config.vector_space = vector_space.map(str::to_owned);
100 match self.database.create_collection(&self.name, config) {
101 Ok(collection) => collection,
102 Err(Error::CollectionExists(_)) => {
103 thread::yield_now();
104 continue;
105 }
106 Err(Error::Git(error)) if retryable_ref_error(&error) => {
107 thread::yield_now();
108 continue;
109 }
110 Err(error) => return Err(error),
111 }
112 }
113 Err(error) => return Err(error),
114 };
115
116 if let Some(vector_space) = vector_space {
117 let actual = collection.info()?.config.vector_space;
118 if actual.as_deref() != Some(vector_space) {
119 return Err(Error::Invalid(format!(
120 "collection {:?} uses vector space {:?}, expected {:?}",
121 self.name, actual, vector_space
122 )));
123 }
124 }
125
126 match collection.upsert(points.clone()) {
127 Ok(result) => return Ok(result),
128 Err(Error::StaleRoot { .. }) => thread::yield_now(),
129 Err(Error::Git(error)) if retryable_ref_error(&error) => thread::yield_now(),
130 Err(error) => return Err(error),
131 }
132 }
133
134 Err(Error::Invalid(format!(
135 "collection {:?} changed repeatedly while applying the first write; retry the upsert",
136 self.name
137 )))
138 }
139
140 pub fn search(
146 &self,
147 vector: impl IntoIterator<Item = f32>,
148 limit: usize,
149 ) -> Result<Vec<ScoredPoint>> {
150 Ok(self
151 .advanced()?
152 .query(Query::new(vector, limit).with_payload())?
153 .points)
154 }
155
156 pub fn get_ids(&self, ids: impl IntoIterator<Item = PointId>) -> Result<Vec<Record>> {
158 Ok(self
159 .advanced()?
160 .get(GetRequest {
161 ids: ids.into_iter().collect(),
162 with_payload: true,
163 ..GetRequest::default()
164 })?
165 .points)
166 }
167
168 pub fn delete_ids(&self, ids: impl IntoIterator<Item = PointId>) -> Result<WriteResult> {
170 self.advanced()?.delete(DeleteSelector {
171 ids: ids.into_iter().collect(),
172 ..DeleteSelector::default()
173 })
174 }
175
176 pub fn count(&self) -> Result<usize> {
178 Ok(self.advanced()?.count(None)?.count)
179 }
180
181 pub fn peek(&self, limit: usize) -> Result<Vec<Record>> {
183 Ok(self
184 .advanced()?
185 .get(GetRequest {
186 limit: Some(limit),
187 with_payload: true,
188 ..GetRequest::default()
189 })?
190 .points)
191 }
192
193 pub fn advanced(&self) -> Result<Collection> {
195 self.database.collection(&self.name)
196 }
197}
198
199fn directory_is_empty(path: &Path) -> Result<bool> {
200 Ok(fs::read_dir(path)?.next().is_none())
201}
202
203fn inferred_dimension(points: &[Point]) -> Result<usize> {
204 let Some(first) = points.first() else {
205 return Err(Error::Invalid("upsert batch must not be empty".into()));
206 };
207 let dimension = first.vector.len();
208 if dimension == 0 {
209 return Err(Error::Invalid("point vector must not be empty".into()));
210 }
211 if let Some(point) = points.iter().find(|point| point.vector.len() != dimension) {
212 return Err(Error::Invalid(format!(
213 "point {} has dimension {}, expected {dimension}",
214 point.id,
215 point.vector.len()
216 )));
217 }
218 Ok(dimension)
219}
220
221fn retryable_ref_error(error: &git2::Error) -> bool {
222 matches!(
223 error.code(),
224 ErrorCode::Exists | ErrorCode::Locked | ErrorCode::Modified
225 )
226}