storage_kit/core/query/mod.rs
1//! Retained portable query requests and responses.
2
3mod common;
4mod scroll;
5mod text;
6mod vector;
7
8use std::{fmt, ops::Deref};
9
10use serde::{Deserialize, Serialize};
11
12use crate::DataRecord;
13
14use super::ApplyDefaultLocator;
15
16pub use common::{
17 Projection, QueryScope, ScopedQuery, TextQueryOptions, VectorQueryOptions, VectorSearchParams,
18};
19pub use scroll::ScrollRequest;
20pub use text::TextSearchRequest;
21pub use vector::NearestRequest;
22
23/// Opaque continuation token.
24///
25/// A token belongs to the backend and operation that returned it. Return it
26/// unchanged to the same query or scan with the same applicable scope. Do not
27/// interpret it as a universally numeric offset or use it to infer a snapshot.
28/// Constructing or deserializing this value does not validate backend acceptance.
29///
30/// # Examples
31///
32/// ```
33/// use storage_kit::database::StorageLocator;
34/// use storage_kit::database::data::{PageToken, ScanRequest};
35/// let token = PageToken::new("opaque-cursor");
36/// let request = ScanRequest::new(StorageLocator::new("documents")).page_token(token.clone());
37/// assert_eq!(request.page_token, Some(token));
38/// ```
39#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
40#[serde(transparent)]
41pub struct PageToken(String);
42
43impl PageToken {
44 /// Creates a token from the returned continuation value.
45 ///
46 /// # Examples
47 ///
48 /// ```
49 /// use storage_kit::database::data::PageToken;
50 /// let token = PageToken::new("opaque-cursor");
51 /// assert_eq!(token.as_str(), "opaque-cursor");
52 /// ```
53 #[must_use]
54 pub fn new(value: impl Into<String>) -> Self {
55 Self(value.into())
56 }
57
58 /// Borrows the continuation value.
59 ///
60 /// # Examples
61 ///
62 /// ```
63 /// use storage_kit::database::data::PageToken;
64 /// let token = PageToken::new("opaque-cursor");
65 /// assert_eq!(token.as_str(), "opaque-cursor");
66 /// ```
67 #[must_use]
68 pub fn as_str(&self) -> &str {
69 &self.0
70 }
71
72 /// Consumes the token.
73 ///
74 /// # Examples
75 ///
76 /// ```
77 /// use storage_kit::database::data::PageToken;
78 /// let token = PageToken::new("opaque-cursor");
79 /// let saved = token.into_string();
80 /// assert_eq!(PageToken::new(saved).as_str(), "opaque-cursor");
81 /// ```
82 #[must_use]
83 pub fn into_string(self) -> String {
84 self.0
85 }
86
87 pub(crate) fn effective_offset(
88 page_token: Option<&str>,
89 offset: Option<usize>,
90 ) -> Result<Option<usize>, crate::StorageError> {
91 match page_token {
92 Some(token) => token.parse::<usize>().map(Some).map_err(|error| {
93 crate::StorageError::InvalidConfig(format!(
94 "invalid numeric page token `{token}`: {error}"
95 ))
96 }),
97 None => Ok(offset),
98 }
99 }
100
101 #[must_use]
102 pub(crate) fn next_offset(
103 limit: Option<usize>,
104 effective_offset: Option<usize>,
105 count: usize,
106 ) -> Option<Self> {
107 let limit = limit?;
108 if count < limit {
109 None
110 } else {
111 Some((effective_offset.unwrap_or(0) + count).to_string().into())
112 }
113 }
114}
115
116impl AsRef<str> for PageToken {
117 fn as_ref(&self) -> &str {
118 self.as_str()
119 }
120}
121
122impl Deref for PageToken {
123 type Target = str;
124
125 fn deref(&self) -> &Self::Target {
126 self.as_str()
127 }
128}
129
130impl fmt::Display for PageToken {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.write_str(self.as_str())
133 }
134}
135
136impl From<String> for PageToken {
137 fn from(value: String) -> Self {
138 Self(value)
139 }
140}
141
142impl From<&str> for PageToken {
143 fn from(value: &str) -> Self {
144 Self(value.to_owned())
145 }
146}
147
148impl From<&String> for PageToken {
149 fn from(value: &String) -> Self {
150 Self(value.clone())
151 }
152}
153
154impl From<PageToken> for String {
155 fn from(value: PageToken) -> Self {
156 value.into_string()
157 }
158}
159
160/// Meaning of a returned score.
161///
162/// Scores retain their reported semantics; distance, similarity, and text
163/// relevance are not interchangeable scales. [`ScoredRecord::raw_score`] may
164/// be absent even when a record is returned.
165///
166/// # Examples
167///
168/// ```
169/// use storage_kit::database::data::DataRecord;
170/// use storage_kit::database::query::{ScoreKind, ScoredRecord};
171/// let hit = ScoredRecord { record: DataRecord::default(), raw_score: Some(0.25),
172/// score_kind: ScoreKind::Distance };
173/// assert!(matches!(hit.score_kind, ScoreKind::Distance));
174/// ```
175#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
176pub enum ScoreKind {
177 /// Larger is more similar.
178 Similarity,
179 /// Smaller is closer.
180 Distance,
181 /// Text relevance.
182 Relevance,
183 /// The implementation cannot expose stronger portable semantics.
184 Unknown,
185}
186
187/// One ranked record.
188///
189/// A missing raw score remains `None`; consumers must not substitute zero
190/// or infer a score from fields in the returned record.
191///
192/// # Examples
193///
194/// ```
195/// use storage_kit::database::data::DataRecord;
196/// use storage_kit::database::query::{ScoreKind, ScoredRecord};
197/// let hit = ScoredRecord { record: DataRecord::default(), raw_score: None,
198/// score_kind: ScoreKind::Unknown };
199/// assert!(hit.raw_score.is_none());
200/// ```
201#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
202pub struct ScoredRecord {
203 /// Returned record.
204 pub record: DataRecord,
205 /// Implementation-reported score.
206 pub raw_score: Option<f64>,
207 /// Score semantics.
208 pub score_kind: ScoreKind,
209}
210
211/// Ranked hits and optional continuation.
212///
213/// Some operations produce unranked records with no raw scores. Return a
214/// continuation unchanged to the same applicable operation; it does not promise
215/// a stable historical view across separate requests.
216///
217/// # Examples
218///
219/// ```
220/// use storage_kit::database::data::{DataRecord, PageToken};
221/// use storage_kit::database::query::{ScoreKind, ScoredRecord, QueryResponse};
222/// let response = QueryResponse {
223/// hits: vec![ScoredRecord { record: DataRecord::default(), raw_score: None,
224/// score_kind: ScoreKind::Unknown }],
225/// next_page: Some(PageToken::new("opaque-cursor")),
226/// };
227/// assert_eq!(response.hits.len(), 1);
228/// assert_eq!(response.next_page.as_deref(), Some("opaque-cursor"));
229/// ```
230#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
231pub struct QueryResponse {
232 /// Ranked or unranked records.
233 pub hits: Vec<ScoredRecord>,
234 /// Continuation token.
235 pub next_page: Option<PageToken>,
236}
237
238impl QueryResponse {
239 pub(crate) fn from_hits(hits: Vec<ScoredRecord>, next_page: Option<PageToken>) -> Self {
240 Self { hits, next_page }
241 }
242}
243
244/// Retained cross-backend query operations.
245///
246/// # Examples
247///
248/// ```
249/// use storage_kit::database::StorageLocator;
250/// use storage_kit::database::query::{ScopedQuery, QueryRequest, TextSearchRequest};
251/// let request = QueryRequest::Text(TextSearchRequest {
252/// query: ScopedQuery::text(StorageLocator::new("documents")).text_field("body"),
253/// text: "rust".to_owned(),
254/// });
255/// assert!(matches!(request, QueryRequest::Text(_)));
256/// ```
257#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
258pub enum QueryRequest {
259 /// Nearest-neighbor search using an explicit dense vector.
260 Nearest(NearestRequest),
261 /// Full-text search.
262 Text(TextSearchRequest),
263 /// Ordered or cursor-based scan through the query surface.
264 Scroll(ScrollRequest),
265}
266
267impl ApplyDefaultLocator for QueryRequest {
268 fn apply_default_locator(self, default: &crate::StorageLocator) -> Self {
269 match self {
270 Self::Nearest(request) => Self::Nearest(request.apply_default_locator(default)),
271 Self::Text(request) => Self::Text(request.apply_default_locator(default)),
272 Self::Scroll(request) => Self::Scroll(request.apply_default_locator(default)),
273 }
274 }
275}