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}