Skip to main content

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}