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