use crate::error::LaserError;
use crate::laser::Laser;
use crate::query::{
AGDX_QUERY_CODE, AggCall, AggFunc, Aggregate, CmpOp, Consistency, Dir, Filter, KeyMatch,
MAX_PAGE_SIZE, QUERY_OP_VERSION, Query, QueryEnvelope, QueryError, QueryReply, QueryResult,
RawSql, Row, Sort, TextQuery, VECTOR_FIELD, Value, VectorQuery, Window,
};
use crate::stream::Decoder;
use crate::types::ConversationId;
use laser_wire::framing::encode_named;
use serde::de::DeserializeOwned;
impl Laser {
pub fn query<'a>(&'a self, index: &'a str) -> QueryRequest<'a> {
QueryRequest::new(self, index)
}
pub async fn execute_query(&self, query: Query) -> Result<QueryResult, LaserError> {
let capabilities = self.capabilities().await;
if !capabilities.query.available {
return Err(LaserError::unsupported(
"query",
"the query surface is not served by this deployment",
));
}
if !capabilities.serves_consistency(query.consistency) {
return Err(LaserError::unsupported_feature(
"query",
"consistency",
format!(
"consistency level {:?} is not served by this deployment",
query.consistency
),
));
}
if query.text.is_some() && !capabilities.query.keyword {
return Err(LaserError::unsupported_feature(
"query",
"keyword",
"lexical text search is not advertised by this deployment",
));
}
if let Some(versions) = capabilities.versions
&& versions.query != QUERY_OP_VERSION
{
return Err(QueryError::Version {
expected: versions.query,
got: QUERY_OP_VERSION,
}
.into());
}
if query.limit > MAX_PAGE_SIZE {
return Err(QueryError::TooLarge {
what: "limit".to_owned(),
size: query.limit,
cap: MAX_PAGE_SIZE,
}
.into());
}
if let Some(vector) = &query.vector
&& vector.top_k > MAX_PAGE_SIZE
{
return Err(QueryError::TooLarge {
what: "top_k".to_owned(),
size: vector.top_k,
cap: MAX_PAGE_SIZE,
}
.into());
}
let request = QueryEnvelope::new(query);
let payload = encode_named(&request)
.map_err(|error| LaserError::Codec(format!("encode request: {error}")))?;
let payload = self
.send_raw_with_response(AGDX_QUERY_CODE, payload)
.await?;
match crate::error::decode_managed_reply::<QueryReply>(&payload)? {
QueryReply::Ok(result) => Ok(result),
QueryReply::Err(error) => Err(error.into()),
_ => Err(LaserError::Protocol(
"query: unknown reply variant".to_owned(),
)),
}
}
}
#[must_use = "call .fetch().await (or a fetch_* variant) to run the query"]
pub struct QueryRequest<'a> {
laser: &'a Laser,
query: Query,
max_rows: Option<usize>,
}
impl<'a> QueryRequest<'a> {
fn new(laser: &'a Laser, index: &'a str) -> Self {
Self {
laser,
query: Query::builder().index(index).build(),
max_rows: None,
}
}
pub fn where_eq(mut self, field: impl Into<String>, value: impl Into<String>) -> Self {
self.query.by_key.push(KeyMatch::new(field, value));
self
}
pub fn conversation(self, conversation: ConversationId) -> Self {
self.where_eq(
laser_wire::headers::CONVERSATION_FIELD,
conversation.to_string(),
)
}
pub fn fork(mut self, fork_id: impl Into<String>) -> Self {
self.query.fork = Some(fork_id.into());
self
}
pub fn filter_eq(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.predicate(field, CmpOp::Eq, value)
}
pub fn filter_ne(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.predicate(field, CmpOp::Ne, value)
}
pub fn filter_gt(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.predicate(field, CmpOp::Gt, value)
}
pub fn filter_gte(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.predicate(field, CmpOp::Gte, value)
}
pub fn filter_lt(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.predicate(field, CmpOp::Lt, value)
}
pub fn filter_lte(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.predicate(field, CmpOp::Lte, value)
}
pub fn filter_in<T: Into<Value>>(
self,
field: impl Into<String>,
values: impl IntoIterator<Item = T>,
) -> Self {
let list = Value::List(values.into_iter().map(Into::into).collect());
self.predicate(field, CmpOp::In, list)
}
pub fn filter_contains(self, field: impl Into<String>, value: impl Into<String>) -> Self {
self.predicate(field, CmpOp::Contains, Value::Str(value.into()))
}
pub fn filter_prefix(self, field: impl Into<String>, value: impl Into<String>) -> Self {
self.predicate(field, CmpOp::Prefix, Value::Str(value.into()))
}
pub fn message_type(mut self, value: impl Into<String>) -> Self {
self.query.message_type = Some(value.into());
self
}
pub fn time_range(mut self, start: u64, end: u64) -> Self {
self.query.time_range = Some((start, end));
self
}
pub fn order_asc(mut self, field: impl Into<String>) -> Self {
self.query.order.push(Sort {
field: field.into(),
dir: Dir::Asc,
});
self
}
pub fn order_desc(mut self, field: impl Into<String>) -> Self {
self.query.order.push(Sort {
field: field.into(),
dir: Dir::Desc,
});
self
}
pub fn limit(mut self, n: usize) -> Self {
self.query.limit = n;
self
}
pub fn offset(mut self, n: usize) -> Self {
self.query.offset = n;
self
}
pub fn with_payload(mut self) -> Self {
self.query.select.payload = true;
self
}
pub fn with_total(mut self) -> Self {
self.query.want_total = true;
self
}
pub fn read_your_writes(mut self) -> Self {
self.query.consistency = Consistency::ReadYourWrites;
self
}
pub fn consistency(mut self, level: Consistency) -> Self {
self.query.consistency = level;
self
}
pub fn text(mut self, query: impl Into<String>) -> Self {
self.query.text = Some(TextQuery {
field: None,
query: query.into(),
});
self
}
pub fn text_in(mut self, field: impl Into<String>, query: impl Into<String>) -> Self {
self.query.text = Some(TextQuery {
field: Some(field.into()),
query: query.into(),
});
self
}
pub fn select_fields<S: Into<String>>(mut self, fields: impl IntoIterator<Item = S>) -> Self {
self.query.select.fields = fields.into_iter().map(Into::into).collect();
self
}
pub fn count(self) -> Self {
self.push_agg(agg_call(AggFunc::Count, None, None, "count"))
}
pub fn sum(self, field: impl Into<String>) -> Self {
self.push_agg(agg_call(AggFunc::Sum, Some(field.into()), None, "sum"))
}
pub fn avg(self, field: impl Into<String>) -> Self {
self.push_agg(agg_call(AggFunc::Avg, Some(field.into()), None, "avg"))
}
pub fn min(self, field: impl Into<String>) -> Self {
self.push_agg(agg_call(AggFunc::Min, Some(field.into()), None, "min"))
}
pub fn max(self, field: impl Into<String>) -> Self {
self.push_agg(agg_call(AggFunc::Max, Some(field.into()), None, "max"))
}
pub fn count_distinct(self, field: impl Into<String>) -> Self {
self.push_agg(agg_call(
AggFunc::CountDistinct,
Some(field.into()),
None,
"count_distinct",
))
}
pub fn stddev(self, field: impl Into<String>) -> Self {
self.push_agg(agg_call(
AggFunc::StdDev,
Some(field.into()),
None,
"stddev",
))
}
pub fn percentile(self, field: impl Into<String>, fraction: f64) -> Self {
self.push_agg(agg_call(
AggFunc::Percentile,
Some(field.into()),
Some(fraction),
"percentile",
))
}
pub fn agg_as(
self,
func: AggFunc,
field: Option<String>,
fraction: Option<f64>,
alias: impl Into<String>,
) -> Self {
let alias = alias.into();
self.push_agg(AggCall {
func,
field,
arg: fraction,
alias,
})
}
pub fn group_by<S: Into<String>>(mut self, fields: impl IntoIterator<Item = S>) -> Self {
let group_by: Vec<String> = fields.into_iter().map(Into::into).collect();
match self.query.aggregate.as_mut() {
Some(aggregate) => aggregate.group_by = group_by,
None => {
self.query.aggregate = Some(Aggregate {
group_by,
funcs: Vec::new(),
window: None,
});
}
}
self
}
pub fn window(mut self, field: impl Into<String>, every_micros: u64) -> Self {
let window = Some(Window {
field: field.into(),
every_micros,
});
match self.query.aggregate.as_mut() {
Some(aggregate) => aggregate.window = window,
None => {
self.query.aggregate = Some(Aggregate {
group_by: Vec::new(),
funcs: Vec::new(),
window,
});
}
}
self
}
pub fn having(mut self, filter: Filter) -> Self {
self.query.having = Some(filter);
self
}
pub fn distinct(mut self) -> Self {
self.query.distinct = true;
self
}
pub fn raw_sql(mut self, sql: impl Into<String>) -> Self {
self.query.raw_sql = Some(RawSql {
sql: sql.into(),
params: Vec::new(),
});
self
}
pub fn raw_sql_with<V: Into<Value>>(
mut self,
sql: impl Into<String>,
params: impl IntoIterator<Item = V>,
) -> Self {
self.query.raw_sql = Some(RawSql {
sql: sql.into(),
params: params.into_iter().map(Into::into).collect(),
});
self
}
pub fn nearest(self, embedding: Vec<f32>, top_k: usize) -> Self {
self.nearest_in(VECTOR_FIELD, embedding, top_k)
}
pub fn nearest_in(
mut self,
field: impl Into<String>,
embedding: Vec<f32>,
top_k: usize,
) -> Self {
self.query.vector = Some(VectorQuery {
field: field.into(),
embedding,
top_k,
});
self
}
pub async fn fetch(self) -> Result<QueryResult, LaserError> {
self.laser.execute_query(self.query).await
}
pub async fn fetch_typed<T: DeserializeOwned>(mut self) -> Result<Vec<T>, LaserError> {
self.query.select.payload = true;
let result = self.laser.execute_query(self.query).await?;
result
.rows
.iter()
.map(|row| row.decode_json::<T>().map_err(LaserError::from))
.collect()
}
pub async fn fetch_one<T: DeserializeOwned>(mut self) -> Result<Option<T>, LaserError> {
self.query.select.payload = true;
self.query.limit = 1;
let result = self.laser.execute_query(self.query).await?;
match result.rows.first() {
Some(row) => row.decode_json::<T>().map(Some).map_err(LaserError::from),
None => Ok(None),
}
}
pub async fn fetch_typed_with<C, T>(mut self) -> Result<Vec<T>, LaserError>
where
C: Decoder<T>,
{
self.query.select.payload = true;
let result = self.laser.execute_query(self.query).await?;
result
.rows
.iter()
.map(|row| row.decode_with::<C, T>().map_err(LaserError::from))
.collect()
}
pub async fn fetch_one_with<C, T>(mut self) -> Result<Option<T>, LaserError>
where
C: Decoder<T>,
{
self.query.select.payload = true;
self.query.limit = 1;
let result = self.laser.execute_query(self.query).await?;
match result.rows.first() {
Some(row) => row
.decode_with::<C, T>()
.map(Some)
.map_err(LaserError::from),
None => Ok(None),
}
}
fn page_stream(mut self) -> QueryStream<'a> {
if self.query.limit == 0 {
self.query.limit = crate::query::DEFAULT_STREAM_PAGE_SIZE;
}
let single_page = self.query.aggregate.is_some() || self.query.vector.is_some();
QueryStream::new(self.laser, self.query, single_page)
}
fn typed_page_stream<T: DeserializeOwned>(mut self) -> TypedQueryStream<'a, T> {
self.query.select.payload = true;
if self.query.limit == 0 {
self.query.limit = crate::query::DEFAULT_STREAM_PAGE_SIZE;
}
let single_page = self.query.aggregate.is_some() || self.query.vector.is_some();
TypedQueryStream::new(self.laser, self.query, single_page)
}
pub fn max_rows(mut self, n: usize) -> Self {
self.max_rows = Some(n);
self
}
pub fn rows(self) -> Result<QueryRows<'a>, LaserError> {
let Some(max_rows) = self.max_rows else {
return Err(LaserError::Invalid(
"rows() needs an explicit ceiling: chain .max_rows(n) first".to_owned(),
));
};
Ok(QueryRows {
stream: self.page_stream(),
remaining: max_rows,
})
}
pub fn rows_typed<T: DeserializeOwned>(self) -> Result<TypedQueryRows<'a, T>, LaserError> {
let Some(max_rows) = self.max_rows else {
return Err(LaserError::Invalid(
"rows_typed() needs an explicit ceiling: chain .max_rows(n) first".to_owned(),
));
};
Ok(TypedQueryRows {
stream: self.typed_page_stream(),
remaining: max_rows,
})
}
pub async fn fetch_all(self) -> Result<Vec<Row>, LaserError> {
let mut stream = self.page_stream();
let mut rows = Vec::new();
while let Some(row) = stream.next().await? {
rows.push(row);
}
Ok(rows)
}
pub async fn fetch_all_typed<T: DeserializeOwned>(self) -> Result<Vec<T>, LaserError> {
let mut stream = self.typed_page_stream::<T>();
let mut rows = Vec::new();
while let Some(row) = stream.next().await? {
rows.push(row);
}
Ok(rows)
}
pub fn into_query(self) -> Query {
self.query
}
fn predicate(self, field: impl Into<String>, op: CmpOp, value: impl Into<Value>) -> Self {
self.and_filter(Filter::pred(field, op, value))
}
pub fn filter(self, filter: Filter) -> Self {
self.and_filter(filter)
}
fn and_filter(mut self, filter: Filter) -> Self {
self.query.filter = Some(match self.query.filter.take() {
None => filter,
Some(Filter::All(mut existing)) => {
existing.push(filter);
Filter::All(existing)
}
Some(other) => Filter::All(vec![other, filter]),
});
self
}
fn push_agg(mut self, call: AggCall) -> Self {
match self.query.aggregate.as_mut() {
Some(aggregate) => aggregate.funcs.push(call),
None => {
self.query.aggregate = Some(Aggregate {
group_by: Vec::new(),
funcs: vec![call],
window: None,
});
}
}
self
}
}
fn agg_call(func: AggFunc, field: Option<String>, arg: Option<f64>, alias: &str) -> AggCall {
AggCall {
func,
field,
arg,
alias: alias.to_owned(),
}
}
impl<'a> From<QueryRequest<'a>> for Query {
fn from(request: QueryRequest<'a>) -> Self {
request.query
}
}
struct QueryStream<'a> {
laser: &'a Laser,
query: Query,
finished: bool,
single_page: bool,
buffer: std::vec::IntoIter<Row>,
}
impl<'a> QueryStream<'a> {
fn new(laser: &'a Laser, query: Query, single_page: bool) -> Self {
Self {
laser,
query,
finished: false,
single_page,
buffer: Vec::new().into_iter(),
}
}
async fn next(&mut self) -> Result<Option<Row>, LaserError> {
if let Some(row) = self.buffer.next() {
return Ok(Some(row));
}
if self.finished {
return Ok(None);
}
let page = self.laser.execute_query(self.query.clone()).await?;
let fetched = page.rows.len();
self.finished = self.single_page || fetched == 0 || !page.page.has_more;
self.query.offset = self.query.offset.saturating_add(fetched);
self.buffer = page.rows.into_iter();
Ok(self.buffer.next())
}
}
pub struct QueryRows<'a> {
stream: QueryStream<'a>,
remaining: usize,
}
impl QueryRows<'_> {
pub async fn next(&mut self) -> Result<Option<Row>, LaserError> {
if self.remaining == 0 {
return Ok(None);
}
let row = self.stream.next().await?;
if row.is_some() {
self.remaining -= 1;
}
Ok(row)
}
}
struct TypedQueryStream<'a, T> {
inner: QueryStream<'a>,
_marker: std::marker::PhantomData<fn() -> T>,
}
impl<'a, T: DeserializeOwned> TypedQueryStream<'a, T> {
fn new(laser: &'a Laser, query: Query, single_page: bool) -> Self {
Self {
inner: QueryStream::new(laser, query, single_page),
_marker: std::marker::PhantomData,
}
}
async fn next(&mut self) -> Result<Option<T>, LaserError> {
match self.inner.next().await? {
Some(row) => row.decode_json::<T>().map(Some).map_err(LaserError::from),
None => Ok(None),
}
}
}
pub struct TypedQueryRows<'a, T> {
stream: TypedQueryStream<'a, T>,
remaining: usize,
}
impl<T: DeserializeOwned> TypedQueryRows<'_, T> {
pub async fn next(&mut self) -> Result<Option<T>, LaserError> {
if self.remaining == 0 {
return Ok(None);
}
let row = self.stream.next().await?;
if row.is_some() {
self.remaining -= 1;
}
Ok(row)
}
}