use crate::{
client::{PreparedCommand, prepare_command},
commands::{GeoUnit, SortOrder},
resp::{
RefBulkString, Response, Value, cmd, count_args, serialize_byte_buf_option, serialize_flag,
serialize_slice_with_arg_count,
},
};
use serde::{
Deserialize, Deserializer, Serialize,
de::{self, DeserializeSeed, Visitor, value::MapAccessDeserializer},
ser::SerializeSeq,
};
use smallvec::SmallVec;
use std::{collections::HashMap, fmt};
pub trait SearchCommands<'a>: Sized {
#[must_use]
fn ft_aggregate(
self,
index: impl Serialize,
query: impl Serialize,
options: FtAggregateOptions,
) -> PreparedCommand<'a, Self, FtAggregateResult> {
prepare_command(
self,
cmd("FT.AGGREGATE")
.arg(index)
.arg(query)
.arg(options)
.readonly(),
)
}
#[must_use]
fn ft_aliasadd(
self,
alias: impl Serialize,
index: impl Serialize,
) -> PreparedCommand<'a, Self, ()> {
prepare_command(self, cmd("FT.ALIASADD").arg(alias).arg(index))
}
#[must_use]
fn ft_aliasdel(self, alias: impl Serialize) -> PreparedCommand<'a, Self, ()> {
prepare_command(self, cmd("FT.ALIASDEL").arg(alias))
}
#[must_use]
fn ft_aliasupdate(
self,
alias: impl Serialize,
index: impl Serialize,
) -> PreparedCommand<'a, Self, ()> {
prepare_command(self, cmd("FT.ALIASUPDATE").arg(alias).arg(index))
}
#[must_use]
fn ft_alter(
self,
index: impl Serialize,
skip_initial_scan: bool,
attribute: FtFieldSchema,
) -> PreparedCommand<'a, Self, ()> {
prepare_command(
self,
cmd("FT.ALTER")
.arg(index)
.arg_if(skip_initial_scan, "SKIPINITIALSCAN")
.arg("SCHEMA")
.arg("ADD")
.arg(attribute),
)
}
#[must_use]
fn ft_config_get<R: Response>(self, option: impl Serialize) -> PreparedCommand<'a, Self, R> {
prepare_command(self, cmd("FT.CONFIG").arg("GET").arg(option).readonly())
}
#[must_use]
fn ft_config_set(
self,
option: impl Serialize,
value: impl Serialize,
) -> PreparedCommand<'a, Self, ()> {
prepare_command(
self,
cmd("FT.CONFIG")
.arg("SET")
.arg(option)
.arg(value)
.readonly(),
)
}
#[must_use]
fn ft_create(
self,
index: impl Serialize,
options: FtCreateOptions,
) -> PreparedCommand<'a, Self, ()> {
prepare_command(self, cmd("FT.CREATE").arg(index).arg(options))
}
#[must_use]
fn ft_cursor_del(self, index: impl Serialize, cursor_id: u64) -> PreparedCommand<'a, Self, ()> {
prepare_command(
self,
cmd("FT.CURSOR")
.arg("DEL")
.arg(index)
.arg(cursor_id)
.readonly(),
)
}
#[must_use]
fn ft_cursor_read(
self,
index: impl Serialize,
cursor_id: u64,
) -> PreparedCommand<'a, Self, FtAggregateResult> {
prepare_command(
self,
cmd("FT.CURSOR")
.arg("READ")
.arg(index)
.arg(cursor_id)
.readonly(),
)
}
#[must_use]
fn ft_dictadd(
self,
dict: impl Serialize,
terms: impl Serialize,
) -> PreparedCommand<'a, Self, usize> {
prepare_command(self, cmd("FT.DICTADD").arg(dict).arg(terms))
}
#[must_use]
fn ft_dictdel(
self,
dict: impl Serialize,
terms: impl Serialize,
) -> PreparedCommand<'a, Self, usize> {
prepare_command(self, cmd("FT.DICTDEL").arg(dict).arg(terms))
}
#[must_use]
fn ft_dictdump<R: Response>(self, dict: impl Serialize) -> PreparedCommand<'a, Self, R> {
prepare_command(self, cmd("FT.DICTDUMP").arg(dict).readonly())
}
#[must_use]
fn ft_dropindex(self, index: impl Serialize, dd: bool) -> PreparedCommand<'a, Self, ()> {
prepare_command(self, cmd("FT.DROPINDEX").arg(index).arg_if(dd, "DD"))
}
#[must_use]
fn ft_explain<R: Response>(
self,
index: impl Serialize,
query: impl Serialize,
dialect_version: Option<u64>,
) -> PreparedCommand<'a, Self, R> {
prepare_command(
self,
cmd("FT.EXPLAIN")
.arg(index)
.arg(query)
.arg(dialect_version)
.readonly(),
)
}
#[must_use]
fn ft_explaincli(
self,
index: impl Serialize,
query: impl Serialize,
dialect_version: Option<u64>,
) -> PreparedCommand<'a, Self, Value> {
prepare_command(
self,
cmd("FT.EXPLAINCLI")
.arg(index)
.arg(query)
.arg(dialect_version)
.readonly(),
)
}
#[must_use]
fn ft_info(self, index: impl Serialize) -> PreparedCommand<'a, Self, FtInfoResult> {
prepare_command(self, cmd("FT.INFO").arg(index).readonly())
}
#[must_use]
fn ft_list<R: Response>(self) -> PreparedCommand<'a, Self, R> {
prepare_command(self, cmd("FT._LIST").readonly())
}
#[must_use]
fn ft_hybrid<R: Response>(
self,
index: impl Serialize,
search: FtHybridSearch,
vsim: FtHybridVsim,
options: FtHybridOptions,
) -> PreparedCommand<'a, Self, R> {
let mut command = cmd("FT.HYBRID").arg(index);
command = command
.arg("SEARCH")
.arg(search.query)
.arg_counted("SCORER", search.scorer);
if let Some(name) = search.yield_score_as {
command = command.arg("YIELD_SCORE_AS").arg(name);
}
command = command.arg("VSIM").arg(vsim.field).arg(vsim.vector);
match vsim.query {
Some(FtHybridVectorQuery::Knn {
k,
ef_runtime,
shard_k_ratio,
}) => {
command = command.arg_counted(
"KNN",
(
("K", k),
ef_runtime.map(|e| ("EF_RUNTIME", e)),
shard_k_ratio.map(|r| ("SHARD_K_RATIO", r)),
),
);
}
Some(FtHybridVectorQuery::Range { radius, epsilon }) => {
command = command.arg_counted(
"RANGE",
(("RADIUS", radius), epsilon.map(|e| ("EPSILON", e))),
);
}
None => {}
}
if let Some(filter) = vsim.filter {
command = command.arg("FILTER").arg(filter);
}
if let Some(name) = vsim.yield_score_as {
command = command.arg("YIELD_SCORE_AS").arg(name);
}
match options.combine {
Some(FtHybridCombine::Rrf { constant, window }) => {
command = command.arg_counted(
("COMBINE", "RRF"),
(
constant.map(|c| ("CONSTANT", c)),
window.map(|w| ("WINDOW", w)),
),
);
}
Some(FtHybridCombine::Linear {
alpha,
beta,
window,
}) => {
command = command.arg_counted(
("COMBINE", "LINEAR"),
(
("ALPHA", alpha),
("BETA", beta),
window.map(|w| ("WINDOW", w)),
),
);
}
None => {}
}
match options.load {
FtHybridLoad::All => command = command.arg("LOAD").arg("*"),
FtHybridLoad::Fields(fields) => command = command.arg_counted("LOAD", fields),
FtHybridLoad::None => {}
}
if let Some(groupby) = options.groupby {
command = command.arg("GROUPBY").arg(groupby);
}
for (expr, name) in options.apply {
command = command.arg("APPLY").arg(expr).arg("AS").arg(name);
}
if let Some(filter) = options.filter {
command = command.arg("FILTER").arg(filter);
}
if options.nosort {
command = command.arg("NOSORT");
} else if let Some((field, order)) = options.sortby {
command = command.arg_counted("SORTBY", (field, order));
}
if let Some((offset, num)) = options.limit {
command = command.arg("LIMIT").arg(offset).arg(num);
}
if let Some(format) = options.format {
command = command.arg("FORMAT").arg(format);
}
command = command.arg_counted("PARAMS", ByteParams(&options.params));
if let Some(timeout) = options.timeout {
command = command.arg("TIMEOUT").arg(timeout);
}
prepare_command(self, command)
}
#[must_use]
fn ft_profile_search(
self,
index: impl Serialize,
limited: bool,
query: impl Serialize,
) -> PreparedCommand<'a, Self, Value> {
prepare_command(
self,
cmd("FT.PROFILE")
.arg(index)
.arg("SEARCH")
.arg_if(limited, "LIMITED")
.arg("QUERY")
.arg(query)
.readonly(),
)
}
#[must_use]
fn ft_profile_aggregate(
self,
index: impl Serialize,
limited: bool,
query: impl Serialize,
) -> PreparedCommand<'a, Self, Value> {
prepare_command(
self,
cmd("FT.PROFILE")
.arg(index)
.arg("AGGREGATE")
.arg_if(limited, "LIMITED")
.arg("QUERY")
.arg(query)
.readonly(),
)
}
#[must_use]
fn ft_search(
self,
index: impl Serialize,
query: impl Serialize,
options: FtSearchOptions,
) -> PreparedCommand<'a, Self, FtSearchResult> {
prepare_command(
self,
cmd("FT.SEARCH")
.arg(index)
.arg(query)
.arg(options)
.readonly(),
)
}
#[must_use]
fn ft_spellcheck(
self,
index: impl Serialize,
query: impl Serialize,
options: FtSpellCheckOptions,
) -> PreparedCommand<'a, Self, FtSpellCheckResult> {
prepare_command(
self,
cmd("FT.SPELLCHECK")
.arg(index)
.arg(query)
.arg(options)
.readonly(),
)
}
#[must_use]
fn ft_syndump<R: Response>(self, index: impl Serialize) -> PreparedCommand<'a, Self, R> {
prepare_command(self, cmd("FT.SYNDUMP").arg(index).readonly())
}
#[must_use]
fn ft_synupdate(
self,
index: impl Serialize,
synonym_group_id: impl Serialize,
skip_initial_scan: bool,
terms: impl Serialize,
) -> PreparedCommand<'a, Self, ()> {
prepare_command(
self,
cmd("FT.SYNUPDATE")
.arg(index)
.arg(synonym_group_id)
.arg_if(skip_initial_scan, "SKIPINITIALSCAN")
.arg(terms),
)
}
#[must_use]
fn ft_tagvals<R: Response>(
self,
index: impl Serialize,
field_name: impl Serialize,
) -> PreparedCommand<'a, Self, R> {
prepare_command(
self,
cmd("FT.TAGVALS").arg(index).arg(field_name).readonly(),
)
}
#[must_use]
fn ft_sugadd(
self,
key: impl Serialize,
string: impl Serialize,
score: f64,
options: FtSugAddOptions,
) -> PreparedCommand<'a, Self, usize> {
prepare_command(
self,
cmd("FT.SUGADD")
.key(key)
.arg(string)
.arg(score)
.arg(options),
)
}
#[must_use]
fn ft_sugdel(
self,
key: impl Serialize,
string: impl Serialize,
) -> PreparedCommand<'a, Self, bool> {
prepare_command(self, cmd("FT.SUGDEL").key(key).arg(string))
}
#[must_use]
fn ft_sugget<R: Response>(
self,
key: impl Serialize,
prefix: impl Serialize,
options: FtSugGetOptions,
) -> PreparedCommand<'a, Self, R> {
prepare_command(
self,
cmd("FT.SUGGET")
.key(key)
.arg(prefix)
.arg(options)
.readonly(),
)
}
#[must_use]
fn ft_suglen(self, key: impl Serialize) -> PreparedCommand<'a, Self, usize> {
prepare_command(self, cmd("FT.SUGLEN").key(key).readonly())
}
}
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtVectorType {
Float64,
Float32,
}
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtVectorDistanceMetric {
L2,
IP,
Cosine,
}
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub struct FtFlatVectorFieldAttributes {
pub r#type: FtVectorType,
pub dim: usize,
pub distance_metric: FtVectorDistanceMetric,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_cap: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_size: Option<usize>,
}
impl FtFlatVectorFieldAttributes {
pub fn new(ty: FtVectorType, dim: usize, distance_metric: FtVectorDistanceMetric) -> Self {
Self {
r#type: ty,
dim,
distance_metric,
initial_cap: None,
block_size: None,
}
}
pub fn initial_cap(self, initial_cap: usize) -> Self {
Self {
initial_cap: Some(initial_cap),
..self
}
}
pub fn block_size(self, block_size: usize) -> Self {
Self {
block_size: Some(block_size),
..self
}
}
}
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub struct FtHnswVectorFieldAttributes {
pub r#type: FtVectorType,
pub dim: usize,
pub distance_metric: FtVectorDistanceMetric,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_cap: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub m: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ef_construction: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ef_runtime: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub epsilon: Option<f64>,
}
impl FtHnswVectorFieldAttributes {
pub fn new(ty: FtVectorType, dim: usize, distance_metric: FtVectorDistanceMetric) -> Self {
Self {
r#type: ty,
dim,
distance_metric,
initial_cap: None,
m: None,
ef_construction: None,
ef_runtime: None,
epsilon: None,
}
}
pub fn initial_cap(self, initial_cap: usize) -> Self {
Self {
initial_cap: Some(initial_cap),
..self
}
}
pub fn m(self, m: usize) -> Self {
Self { m: Some(m), ..self }
}
pub fn ef_construction(self, ef_construction: usize) -> Self {
Self {
ef_construction: Some(ef_construction),
..self
}
}
pub fn ef_runtime(self, ef_runtime: usize) -> Self {
Self {
ef_runtime: Some(ef_runtime),
..self
}
}
pub fn epsilon(self, epsilon: f64) -> Self {
Self {
epsilon: Some(epsilon),
..self
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FtVectorFieldAlgorithm {
Flat(FtFlatVectorFieldAttributes),
HNSW(FtHnswVectorFieldAttributes),
}
impl Serialize for FtVectorFieldAlgorithm {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(None)?;
match self {
FtVectorFieldAlgorithm::Flat(attributes) => {
seq.serialize_element("FLAT")?;
seq.serialize_element(&count_args(attributes)?)?;
seq.serialize_element(attributes)?;
}
FtVectorFieldAlgorithm::HNSW(attributes) => {
seq.serialize_element("HNSW")?;
seq.serialize_element(&count_args(attributes)?)?;
seq.serialize_element(attributes)?;
}
}
seq.end()
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtFieldType {
#[default]
Text,
Tag,
Numeric,
Geo,
Geoshape(#[serde(skip_deserializing)] Option<FtGeoShapeCoordSystem>),
Vector(#[serde(skip_deserializing)] Option<FtVectorFieldAlgorithm>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtGeoShapeCoordSystem {
Flat,
Spherical,
}
#[derive(Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub enum FtPhoneticMatcher {
#[serde(rename = "dm:en")]
DmEn,
#[serde(rename = "dm:fr")]
DmFr,
#[serde(rename = "dm:pt")]
DmPt,
#[serde(rename = "dm:es")]
DmEs,
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtFieldSchema<'a> {
#[serde(rename = "")]
identifier: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
r#as: Option<&'a str>,
#[serde(rename = "")]
field_type: FtFieldType,
#[serde(skip_serializing_if = "Option::is_none")]
phonetic: Option<FtPhoneticMatcher>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
nostem: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
sortable: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
unf: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
noindex: bool,
#[serde(skip_serializing_if = "Option::is_none")]
weight: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
separator: Option<char>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
casesensitive: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withsuffixtrie: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
indexmissing: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
indexempty: bool,
}
impl<'a> FtFieldSchema<'a> {
#[must_use]
pub fn identifier(identifier: &'a str) -> Self {
Self {
identifier,
..Default::default()
}
}
#[must_use]
pub fn as_attribute(mut self, as_attribute: &'a str) -> Self {
self.r#as = Some(as_attribute);
self
}
#[must_use]
pub fn field_type(mut self, field_type: FtFieldType) -> Self {
self.field_type = field_type;
self
}
#[must_use]
pub fn sortable(mut self) -> Self {
self.sortable = true;
self
}
#[must_use]
pub fn unf(mut self) -> Self {
self.unf = true;
self
}
#[must_use]
pub fn nostem(mut self) -> Self {
self.nostem = true;
self
}
#[must_use]
pub fn noindex(mut self) -> Self {
self.noindex = true;
self
}
#[must_use]
pub fn phonetic(mut self, matcher: FtPhoneticMatcher) -> Self {
self.phonetic = Some(matcher);
self
}
#[must_use]
pub fn weight(mut self, weight: f64) -> Self {
self.weight = Some(weight);
self
}
#[must_use]
pub fn separator(mut self, sep: char) -> Self {
self.separator = Some(sep);
self
}
#[must_use]
pub fn case_sensitive(mut self) -> Self {
self.casesensitive = true;
self
}
#[must_use]
pub fn with_suffix_trie(mut self) -> Self {
self.withsuffixtrie = true;
self
}
#[must_use]
pub fn index_missing(mut self) -> Self {
self.indexmissing = true;
self
}
#[must_use]
pub fn index_empty(mut self) -> Self {
self.indexempty = true;
self
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtIndexDataType {
Hash,
Json,
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtCreateOptions<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
on: Option<FtIndexDataType>,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
prefix: SmallVec<[&'a str; 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
filter: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<FtLanguage>,
#[serde(skip_serializing_if = "Option::is_none")]
language_field: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
score_field: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
payload_field: Option<&'a str>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
maxtextfields: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
nooffsets: bool,
#[serde(skip_serializing_if = "Option::is_none")]
temporary: Option<u64>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
nohl: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
nofields: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
nofreqs: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
skipinitialscan: bool,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
stopwords: SmallVec<[&'a str; 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
indexall: Option<FtIndexAll>,
#[serde(skip_serializing_if = "SmallVec::is_empty")]
schema: SmallVec<[FtFieldSchema<'a>; 10]>,
}
impl<'a> FtCreateOptions<'a> {
#[must_use]
pub fn on(mut self, data_type: FtIndexDataType) -> Self {
self.on = Some(data_type);
self
}
#[must_use]
pub fn prefix(mut self, prefix: &'a str) -> Self {
self.prefix.push(prefix);
self
}
#[must_use]
pub fn filter(mut self, filter: &'a str) -> Self {
self.filter = Some(filter);
self
}
#[must_use]
pub fn language(mut self, default_lang: FtLanguage) -> Self {
self.language = Some(default_lang);
self
}
#[must_use]
pub fn language_field(mut self, lang_attribute: &'a str) -> Self {
self.language_field = Some(lang_attribute);
self
}
#[must_use]
pub fn score(mut self, default_score: f64) -> Self {
self.score = Some(default_score);
self
}
#[must_use]
pub fn score_field(mut self, score_attribute: &'a str) -> Self {
self.score_field = Some(score_attribute);
self
}
#[must_use]
pub fn payload_field(mut self, payload_attribute: &'a str) -> Self {
self.payload_field = Some(payload_attribute);
self
}
#[must_use]
pub fn max_text_fields(mut self) -> Self {
self.maxtextfields = true;
self
}
#[must_use]
pub fn no_offsets(mut self) -> Self {
self.nooffsets = true;
self
}
#[must_use]
pub fn temporary(mut self, expiration_sec: u64) -> Self {
self.temporary = Some(expiration_sec);
self
}
#[must_use]
pub fn nohl(mut self) -> Self {
self.nohl = true;
self
}
#[must_use]
pub fn nofields(mut self) -> Self {
self.nofields = true;
self
}
#[must_use]
pub fn nofreqs(mut self) -> Self {
self.nofreqs = true;
self
}
#[must_use]
pub fn skip_initial_scan(mut self) -> Self {
self.skipinitialscan = true;
self
}
#[must_use]
pub fn stop_word(mut self, stop_word: &'a str) -> Self {
self.stopwords.push(stop_word);
self
}
#[must_use]
pub fn index_all(mut self, index_all: FtIndexAll) -> Self {
self.indexall = Some(index_all);
self
}
pub fn schema(mut self, schema: FtFieldSchema<'a>) -> Self {
self.schema.push(schema);
self
}
}
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtIndexAll {
Enable,
Disable,
}
#[derive(Default)]
pub struct FtHybridSearch<'a> {
query: &'a str,
scorer: SmallVec<[&'a str; 4]>,
yield_score_as: Option<&'a str>,
}
impl<'a> FtHybridSearch<'a> {
#[must_use]
pub fn new(query: &'a str) -> Self {
Self {
query,
..Default::default()
}
}
#[must_use]
pub fn scorer(mut self, tokens: impl IntoIterator<Item = &'a str>) -> Self {
self.scorer = tokens.into_iter().collect();
self
}
#[must_use]
pub fn yield_score_as(mut self, name: &'a str) -> Self {
self.yield_score_as = Some(name);
self
}
}
#[non_exhaustive]
pub enum FtHybridVectorQuery {
Knn {
k: u32,
ef_runtime: Option<u32>,
shard_k_ratio: Option<f64>,
},
Range { radius: f64, epsilon: Option<f64> },
}
pub struct FtHybridVsim<'a> {
field: &'a str,
vector: &'a str,
query: Option<FtHybridVectorQuery>,
filter: Option<&'a str>,
yield_score_as: Option<&'a str>,
}
impl<'a> FtHybridVsim<'a> {
#[must_use]
pub fn new(field: &'a str, vector: &'a str) -> Self {
Self {
field,
vector,
query: None,
filter: None,
yield_score_as: None,
}
}
#[must_use]
pub fn query(mut self, query: FtHybridVectorQuery) -> Self {
self.query = Some(query);
self
}
#[must_use]
pub fn filter(mut self, filter: &'a str) -> Self {
self.filter = Some(filter);
self
}
#[must_use]
pub fn yield_score_as(mut self, name: &'a str) -> Self {
self.yield_score_as = Some(name);
self
}
}
#[non_exhaustive]
pub enum FtHybridCombine {
Rrf {
constant: Option<f64>,
window: Option<u32>,
},
Linear {
alpha: f64,
beta: f64,
window: Option<u32>,
},
}
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtHybridFormat {
String,
Expand,
}
#[derive(Default)]
#[non_exhaustive]
pub enum FtHybridLoad<'a> {
#[default]
None,
All,
Fields(SmallVec<[&'a str; 4]>),
}
#[derive(Default)]
pub struct FtHybridOptions<'a> {
combine: Option<FtHybridCombine>,
limit: Option<(u32, u32)>,
sortby: Option<(&'a str, SortOrder)>,
nosort: bool,
load: FtHybridLoad<'a>,
groupby: Option<FtGroupBy<'a>>,
apply: SmallVec<[(&'a str, &'a str); 2]>,
filter: Option<&'a str>,
format: Option<FtHybridFormat>,
params: SmallVec<[(&'a str, &'a [u8]); 2]>,
timeout: Option<u64>,
}
struct ByteParams<'a>(&'a [(&'a str, &'a [u8])]);
impl Serialize for ByteParams<'_> {
#[expect(
clippy::arithmetic_side_effects,
reason = "the declared element count is derived from the length of a \
collection already held in memory, so the product cannot \
leave `usize`."
)]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.len() * 2))?;
for (name, value) in self.0 {
seq.serialize_element(name)?;
seq.serialize_element(&RefBulkString::new(value))?;
}
seq.end()
}
}
impl<'a> FtHybridOptions<'a> {
#[must_use]
pub fn combine(mut self, combine: FtHybridCombine) -> Self {
self.combine = Some(combine);
self
}
#[must_use]
pub fn limit(mut self, offset: u32, num: u32) -> Self {
self.limit = Some((offset, num));
self
}
#[must_use]
pub fn sortby(mut self, field: &'a str, order: SortOrder) -> Self {
self.sortby = Some((field, order));
self
}
#[must_use]
pub fn nosort(mut self) -> Self {
self.nosort = true;
self
}
#[must_use]
pub fn load(mut self, fields: impl IntoIterator<Item = &'a str>) -> Self {
self.load = FtHybridLoad::Fields(fields.into_iter().collect());
self
}
#[must_use]
pub fn load_all(mut self) -> Self {
self.load = FtHybridLoad::All;
self
}
#[must_use]
pub fn groupby(mut self, groupby: FtGroupBy<'a>) -> Self {
self.groupby = Some(groupby);
self
}
#[must_use]
pub fn apply(mut self, expr: &'a str, name: &'a str) -> Self {
self.apply.push((expr, name));
self
}
#[must_use]
pub fn filter(mut self, filter: &'a str) -> Self {
self.filter = Some(filter);
self
}
#[must_use]
pub fn format(mut self, format: FtHybridFormat) -> Self {
self.format = Some(format);
self
}
#[must_use]
pub fn param(mut self, name: &'a str, value: &'a [u8]) -> Self {
self.params.push((name, value));
self
}
#[must_use]
pub fn timeout(mut self, milliseconds: u64) -> Self {
self.timeout = Some(milliseconds);
self
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtAggregateOptions<'a> {
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
verbatim: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
addscores: bool,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
load: SmallVec<[FtAttribute<'a>; 10]>,
#[serde(rename = "LOAD", skip_serializing_if = "Option::is_none")]
load_all: Option<&'static str>,
#[serde(rename = "", skip_serializing_if = "SmallVec::is_empty")]
expressions: SmallVec<[FtAggregateExpression<'a>; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<(u32, u32)>,
#[serde(skip_serializing_if = "Option::is_none")]
filter: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
withcursor: Option<FtWithCursorOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
params: SmallVec<[(&'a str, &'a str); 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
scorer: Option<FtScorerOptions<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
dialect: Option<u64>,
}
impl<'a> FtAggregateOptions<'a> {
#[must_use]
pub fn verbatim(mut self) -> Self {
self.verbatim = true;
self
}
#[must_use]
pub fn load(mut self, attribute: FtAttribute<'a>) -> Self {
self.load.push(attribute);
self
}
#[must_use]
pub fn load_all(mut self) -> Self {
self.load_all = Some("*");
self
}
#[must_use]
pub fn groupby(mut self, options: FtGroupBy<'a>) -> Self {
self.expressions
.push(FtAggregateExpression::GroupBy(options));
self
}
#[must_use]
pub fn sortby(mut self, options: FtSortBy<'a>) -> Self {
self.expressions
.push(FtAggregateExpression::SortBy(options));
self
}
#[must_use]
pub fn apply(mut self, expr: &'a str, as_name: &'a str) -> Self {
self.expressions
.push(FtAggregateExpression::Apply(FtApplyOptions::new(
expr, as_name,
)));
self
}
#[must_use]
pub fn limit(mut self, offset: u32, num: u32) -> Self {
self.limit = Some((offset, num));
self
}
#[must_use]
pub fn filter<E, N>(mut self, expr: &'a str) -> Self {
self.filter = Some(expr);
self
}
#[must_use]
pub fn withcursor(mut self, options: FtWithCursorOptions) -> Self {
self.withcursor = Some(options);
self
}
#[must_use]
pub fn timeout(mut self, milliseconds: u64) -> Self {
self.timeout = Some(milliseconds);
self
}
#[must_use]
pub fn param(mut self, name: &'a str, value: &'a str) -> Self {
self.params.push((name, value));
self
}
#[must_use]
pub fn scorer(mut self, options: FtScorerOptions<'a>) -> Self {
self.scorer = Some(options);
self
}
#[must_use]
pub fn add_scores(mut self) -> Self {
self.addscores = true;
self
}
#[must_use]
pub fn dialect(mut self, dialect_version: u64) -> Self {
self.dialect = Some(dialect_version);
self
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
enum FtAggregateExpression<'a> {
GroupBy(FtGroupBy<'a>),
SortBy(FtSortBy<'a>),
Apply(FtApplyOptions<'a>),
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtAttribute<'a> {
#[serde(rename = "")]
identifier: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
r#as: Option<&'a str>,
}
impl<'a> FtAttribute<'a> {
#[must_use]
pub fn new(identifier: &'a str) -> Self {
Self {
identifier,
..Default::default()
}
}
#[must_use]
pub fn r#as(mut self, property: &'a str) -> Self {
self.r#as = Some(property);
self
}
}
#[derive(Default, Serialize)]
pub struct FtGroupBy<'a> {
#[serde(rename = "", serialize_with = "serialize_slice_with_arg_count")]
properties: SmallVec<[&'a str; 10]>,
#[serde(rename = "", skip_serializing_if = "SmallVec::is_empty")]
reducers: SmallVec<[FtReduceOptions<'a>; 2]>,
}
impl<'a> FtGroupBy<'a> {
pub fn property(mut self, property: &'a str) -> Self {
self.properties.push(property);
self
}
pub fn reduce(mut self, reducer: FtReducer<'a>) -> Self {
self.reducers.push(FtReduceOptions { reduce: reducer });
self
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct FtReduceOptions<'a> {
reduce: FtReducer<'a>,
}
impl<'a> From<FtReducer<'a>> for FtReduceOptions<'a> {
fn from(reduce: FtReducer<'a>) -> Self {
Self { reduce }
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtReducer<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
count_distinct: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
count_distinctish: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
sum: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
min: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
max: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
avg: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
stddev: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
quantile: Option<(u32, &'a str, f64)>,
#[serde(skip_serializing_if = "Option::is_none")]
tolist: Option<(u32, &'a str)>,
#[serde(skip_serializing_if = "Option::is_none")]
first_value: Option<FtFirstValue<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
random_sample: Option<(u32, &'a str, u32)>,
#[serde(skip_serializing_if = "Option::is_none")]
r#as: Option<&'a str>,
}
#[derive(Serialize)]
struct FtFirstValue<'a> {
#[serde(rename = "")]
count: u32,
#[serde(rename = "")]
property: &'a str,
#[serde(rename = "BY", skip_serializing_if = "Option::is_none")]
by_property: Option<&'a str>,
#[serde(rename = "", skip_serializing_if = "Option::is_none")]
order: Option<SortOrder>,
}
impl<'a> FtReducer<'a> {
#[must_use]
pub fn count() -> Self {
Self {
count: Some(0),
..Default::default()
}
}
pub fn count_distinct(property: &'a str) -> Self {
Self {
count_distinct: Some((1, property)),
..Default::default()
}
}
pub fn count_distinctish(property: &'a str) -> Self {
Self {
count_distinctish: Some((1, property)),
..Default::default()
}
}
pub fn sum(property: &'a str) -> Self {
Self {
sum: Some((1, property)),
..Default::default()
}
}
pub fn min(property: &'a str) -> Self {
Self {
min: Some((1, property)),
..Default::default()
}
}
pub fn max(property: &'a str) -> Self {
Self {
max: Some((1, property)),
..Default::default()
}
}
pub fn avg(property: &'a str) -> Self {
Self {
avg: Some((1, property)),
..Default::default()
}
}
pub fn stddev(property: &'a str) -> Self {
Self {
stddev: Some((1, property)),
..Default::default()
}
}
pub fn quantile(property: &'a str, quantile: f64) -> Self {
Self {
quantile: Some((2, property, quantile)),
..Default::default()
}
}
pub fn tolist(property: &'a str) -> Self {
Self {
tolist: Some((1, property)),
..Default::default()
}
}
pub fn first_value(property: &'a str) -> Self {
Self {
first_value: Some(FtFirstValue {
count: 1,
property,
by_property: None,
order: None,
}),
..Default::default()
}
}
pub fn first_value_by(property: &'a str, by_property: &'a str) -> Self {
Self {
first_value: Some(FtFirstValue {
count: 3,
property,
by_property: Some(by_property),
order: None,
}),
..Default::default()
}
}
pub fn first_value_by_order(property: &'a str, by_property: &'a str, order: SortOrder) -> Self {
Self {
first_value: Some(FtFirstValue {
count: 4,
property,
by_property: Some(by_property),
order: Some(order),
}),
..Default::default()
}
}
pub fn random_sample(property: &'a str, sample_size: u32) -> Self {
Self {
random_sample: Some((2, property, sample_size)),
..Default::default()
}
}
pub fn as_name(mut self, name: &'a str) -> Self {
self.r#as = Some(name);
self
}
}
#[derive(Serialize)]
pub struct FtSortByProperty<'a>(&'a str, SortOrder);
impl<'a> FtSortByProperty<'a> {
pub fn new(property: &'a str) -> Self {
Self(property, SortOrder::Asc)
}
pub fn asc(mut self) -> Self {
self.1 = SortOrder::Asc;
self
}
pub fn desc(mut self) -> Self {
self.1 = SortOrder::Desc;
self
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtSortBy<'a> {
#[serde(
rename = "",
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
properties: SmallVec<[&'a str; 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
max: Option<u32>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withcount: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withoutcount: bool,
}
impl<'a> FtSortBy<'a> {
pub fn property(mut self, property: FtSortByProperty<'a>) -> Self {
self.properties.push(property.0);
match property.1 {
SortOrder::Asc => self.properties.push("ASC"),
SortOrder::Desc => self.properties.push("DESC"),
}
self
}
pub fn max(mut self, num: u32) -> Self {
self.max = Some(num);
self
}
pub fn with_count(mut self) -> Self {
self.withcount = true;
self
}
pub fn without_count(mut self) -> Self {
self.withoutcount = true;
self
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct FtApplyOptions<'a> {
#[serde(rename = "")]
expression: &'a str,
r#as: &'a str,
}
impl<'a> FtApplyOptions<'a> {
#[must_use]
pub(crate) fn new(expression: &'a str, as_name: &'a str) -> Self {
Self {
expression,
r#as: as_name,
}
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtWithCursorOptions {
#[serde(skip_serializing_if = "Option::is_none")]
count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
maxidle: Option<u64>,
}
impl FtWithCursorOptions {
pub fn count(mut self, read_size: u32) -> FtWithCursorOptions {
self.count = Some(read_size);
self
}
pub fn maxidle(mut self, idle_time_ms: u64) -> FtWithCursorOptions {
self.maxidle = Some(idle_time_ms);
self
}
}
#[derive(Serialize)]
#[non_exhaustive]
pub enum FtScorerOptions<'a> {
#[serde(rename = "TFIDF")]
TfIdf,
#[serde(rename = "TFIDF.DOCNORM")]
TfIdfDocNorm,
#[serde(rename = "BM25STD")]
Bm25Std,
#[serde(rename = "BM25STD.NORM")]
Bm25StdNorm,
#[serde(rename = "BM25STD.TANH")]
Bm25StdTanh {
#[serde(rename = "BM25STD_TANH_FACTOR")]
factor: f64,
},
#[serde(rename = "DISMAX")]
DisMax,
#[serde(rename = "DISMAX")]
DOCSCORE,
#[serde(rename = "HAMMING")]
Hamming,
#[serde(rename = "")]
Custom(&'a str),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct FtAggregateResult {
pub attributes: Vec<String>,
pub format: String,
pub results: Vec<FtSearchResultRow>,
pub total_results: usize,
pub warning: Vec<String>,
pub cursor_id: Option<u64>,
}
impl<'de> Deserialize<'de> for FtAggregateResult {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FtAggregateResultVisitor;
impl<'de> Visitor<'de> for FtAggregateResultVisitor {
type Value = FtAggregateResult;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("FtAggregateResult")
}
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let Some(result) = seq.next_element::<FtSearchResult>()? else {
return Err(de::Error::invalid_length(0, &"2 elements in sequence"));
};
let Some(cursor) = seq.next_element::<u64>()? else {
return Err(de::Error::invalid_length(0, &"2 elements in sequence"));
};
Ok(FtAggregateResult {
attributes: result.attributes,
format: result.format,
results: result.results,
total_results: result.total_results,
warning: result.warning,
cursor_id: Some(cursor),
})
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let result = FtSearchResult::deserialize(MapAccessDeserializer::new(map))?;
Ok(FtAggregateResult {
attributes: result.attributes,
format: result.format,
results: result.results,
total_results: result.total_results,
warning: result.warning,
cursor_id: None,
})
}
}
deserializer.deserialize_any(FtAggregateResultVisitor)
}
}
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct FtSearchResult {
pub attributes: Vec<String>,
pub format: String,
pub results: Vec<FtSearchResultRow>,
pub total_results: usize,
pub warning: Vec<String>,
}
#[derive(Debug, Default, Deserialize)]
#[non_exhaustive]
pub struct FtSearchResultRow {
#[serde(default)]
pub id: String,
#[serde(default)]
pub score: FtScore,
#[serde(default)]
pub payload: String,
#[serde(default)]
pub sortkey: String,
pub values: Vec<(String, FtAttributeValue)>,
#[serde(default)]
pub extra_attributes: Vec<(String, FtAttributeValue)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FtAttributeValue {
Text(String),
Array(Vec<String>),
}
impl FtAttributeValue {
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Text(text) => Some(text),
Self::Array(_) => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[String]> {
match self {
Self::Text(_) => None,
Self::Array(elements) => Some(elements),
}
}
}
impl From<String> for FtAttributeValue {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<Vec<String>> for FtAttributeValue {
fn from(elements: Vec<String>) -> Self {
Self::Array(elements)
}
}
impl PartialEq<str> for FtAttributeValue {
fn eq(&self, other: &str) -> bool {
self.as_str() == Some(other)
}
}
impl PartialEq<&str> for FtAttributeValue {
fn eq(&self, other: &&str) -> bool {
self.as_str() == Some(*other)
}
}
impl PartialEq<String> for FtAttributeValue {
fn eq(&self, other: &String) -> bool {
self.as_str() == Some(other.as_str())
}
}
impl PartialEq<[String]> for FtAttributeValue {
fn eq(&self, other: &[String]) -> bool {
self.as_array() == Some(other)
}
}
impl PartialEq<FtAttributeValue> for str {
fn eq(&self, other: &FtAttributeValue) -> bool {
other == self
}
}
impl PartialEq<FtAttributeValue> for &str {
fn eq(&self, other: &FtAttributeValue) -> bool {
other == self
}
}
impl PartialEq<FtAttributeValue> for String {
fn eq(&self, other: &FtAttributeValue) -> bool {
other == self
}
}
impl PartialEq<FtAttributeValue> for [String] {
fn eq(&self, other: &FtAttributeValue) -> bool {
other == self
}
}
impl<'de> Deserialize<'de> for FtAttributeValue {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FtAttributeValueVisitor;
impl<'de> Visitor<'de> for FtAttributeValueVisitor {
type Value = FtAttributeValue;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an attribute value: a string, or an array of strings")
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(FtAttributeValue::Text(value.to_owned()))
}
fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(FtAttributeValue::Text(value))
}
fn visit_bytes<E>(self, value: &[u8]) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(FtAttributeValue::Text(
String::from_utf8_lossy(value).into_owned(),
))
}
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut elements = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(element) = seq.next_element::<String>()? {
elements.push(element);
}
Ok(FtAttributeValue::Array(elements))
}
}
deserializer.deserialize_any(FtAttributeValueVisitor)
}
}
#[derive(Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct FtScore {
pub value: f64,
pub explanation: Vec<String>,
}
impl<'de> Deserialize<'de> for FtScore {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FtScoreVisitor;
fn collect_explanation(value: &Value, lines: &mut Vec<String>) {
match value {
Value::SimpleString(line) => lines.push(line.clone()),
Value::BulkString(line) => {
lines.push(String::from_utf8_lossy(line).into_owned());
}
Value::Array(values) => {
for value in values {
collect_explanation(value, lines);
}
}
_ => (),
}
}
impl<'de> Visitor<'de> for FtScoreVisitor {
type Value = FtScore;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a score, or a score followed by its breakdown")
}
fn visit_f64<E>(self, value: f64) -> std::result::Result<FtScore, E> {
Ok(FtScore {
value,
explanation: Vec::new(),
})
}
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<FtScore, A::Error>
where
A: de::SeqAccess<'de>,
{
let value = seq
.next_element::<f64>()?
.ok_or_else(|| de::Error::custom("missing score"))?;
let mut explanation = Vec::new();
while let Some(element) = seq.next_element::<Value>()? {
collect_explanation(&element, &mut explanation);
}
Ok(FtScore { value, explanation })
}
}
deserializer.deserialize_any(FtScoreVisitor)
}
}
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct FtInfoResult {
pub index_name: String,
pub index_options: Vec<String>,
pub index_definition: FtIndexDefinition,
pub attributes: Vec<FtIndexAttribute>,
pub num_docs: usize,
pub max_doc_id: u64,
pub num_terms: usize,
pub num_records: usize,
pub inverted_sz_mb: f64,
pub vector_index_sz_mb: f64,
pub total_inverted_index_blocks: usize,
pub offset_vectors_sz_mb: f64,
pub doc_table_size_mb: f64,
pub sortable_values_size_mb: f64,
pub key_table_size_mb: f64,
pub tag_overhead_sz_mb: f64,
pub text_overhead_sz_mb: f64,
pub total_index_memory_sz_mb: f64,
pub geoshapes_sz_mb: f64,
pub records_per_doc_avg: f64,
pub bytes_per_record_avg: f64,
pub offsets_per_term_avg: f64,
pub offset_bits_per_record_avg: f64,
pub hash_indexing_failures: usize,
pub total_indexing_time: f64,
pub indexing: bool,
pub percent_indexed: f64,
pub number_of_uses: usize,
pub cleaning: bool,
#[serde(default)]
pub gc_stats: Option<FtGcStats>,
#[serde(default)]
pub cursor_stats: Option<FtCursorStats>,
#[serde(default)]
pub stopwords_list: Vec<String>,
pub dialect_stats: HashMap<String, usize>,
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct FtIndexAttribute {
pub identifier: String,
pub attribute: String,
pub field_type: FtFieldType,
pub weight: f64,
pub sortable: bool,
pub unf: bool,
pub no_stem: bool,
pub no_index: bool,
pub phonetic: Option<FtPhoneticMatcher>,
pub separator: Option<char>,
pub case_sensitive: bool,
pub with_suffixe_trie: bool,
pub algorithm: Option<String>,
pub data_type: Option<String>,
pub dim: Option<usize>,
pub distance_metric: Option<String>,
}
impl<'de> Deserialize<'de> for FtIndexAttribute {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct FlagsSeed<'a> {
attribute: &'a mut FtIndexAttribute,
}
impl<'a> FlagsSeed<'a> {
pub(crate) fn new(attribute: &'a mut FtIndexAttribute) -> Self {
Self { attribute }
}
}
impl<'de, 'a> de::DeserializeSeed<'de> for FlagsSeed<'a> {
type Value = ();
fn deserialize<D>(self, deserializer: D) -> Result<(), D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(self)
}
}
impl<'de, 'a> Visitor<'de> for FlagsSeed<'a> {
type Value = ();
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence of flags")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
while let Some(flag) = seq.next_element::<&str>()? {
match flag {
"SORTABLE" => self.attribute.sortable = true,
"UNF" => self.attribute.unf = true,
"NOSTEM" => self.attribute.no_stem = true,
"NOINDEX" => self.attribute.no_index = true,
"CASESENSITIVE" => self.attribute.case_sensitive = true,
"WITHSUFFIXTRIE" => self.attribute.with_suffixe_trie = true,
_ => (),
}
}
Ok(())
}
}
struct FtIndexAttributeVisitor;
impl<'de> Visitor<'de> for FtIndexAttributeVisitor {
type Value = FtIndexAttribute;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("FtIndexAttribute")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut attribute = FtIndexAttribute::default();
while let Some(field_name) = map.next_key::<&str>()? {
match field_name {
"identifier" => {
attribute.identifier = map.next_value::<String>()?;
}
"attribute" => {
attribute.attribute = map.next_value::<String>()?;
}
"type" => {
attribute.field_type = map.next_value::<FtFieldType>()?;
}
"WEIGHT" => {
attribute.weight = map.next_value::<f64>()?;
}
"flags" => {
map.next_value_seed(FlagsSeed::new(&mut attribute))?;
}
"SEPARATOR" => attribute.separator = Some(map.next_value::<char>()?),
"PHONETIC" => {
attribute.phonetic = Some(map.next_value::<FtPhoneticMatcher>()?)
}
"algorithm" => attribute.algorithm = Some(map.next_value::<String>()?),
"data_type" => attribute.data_type = Some(map.next_value::<String>()?),
"dim" => attribute.dim = Some(map.next_value::<usize>()?),
"distance_metric" => {
attribute.distance_metric = Some(map.next_value::<String>()?)
}
_ => {
map.next_value::<de::IgnoredAny>()?;
}
}
}
Ok(attribute)
}
}
deserializer.deserialize_map(FtIndexAttributeVisitor)
}
}
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct FtGcStats {
pub bytes_collected: usize,
pub total_ms_run: usize,
pub total_cycles: usize,
pub average_cycle_time_ms: f64,
pub last_run_time_ms: usize,
pub gc_numeric_trees_missed: usize,
pub gc_blocks_denied: usize,
}
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct FtCursorStats {
pub global_idle: usize,
pub global_total: usize,
pub index_capacity: usize,
pub index_total: usize,
}
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct FtIndexDefinition {
pub key_type: FtIndexDataType,
pub prefixes: Vec<String>,
#[serde(default)]
pub filter: String,
#[serde(default)]
pub default_language: String,
#[serde(default)]
pub language_field: String,
pub default_score: f64,
#[serde(default)]
pub score_field: String,
#[serde(default)]
pub payload_field: String,
#[serde(default)]
pub indexes_all: String,
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtSearchOptions<'a> {
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
nocontent: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
verbatim: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
nostopwords: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withscores: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withpayloads: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withsortkeys: bool,
#[serde(rename = "", skip_serializing_if = "SmallVec::is_empty")]
filter: SmallVec<[FtFilterOptions<'a>; 10]>,
#[serde(rename = "", skip_serializing_if = "SmallVec::is_empty")]
geofilter: SmallVec<[FtGeoFilterOptions<'a>; 10]>,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
inkeys: SmallVec<[&'a str; 10]>,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
infields: SmallVec<[&'a str; 10]>,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
r#return: SmallVec<[FtAttribute<'a>; 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
summarize: Option<FtSearchSummarizeOptions<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
highlight: Option<FtSearchHighlightOptions<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
slop: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
inorder: bool,
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<FtLanguage>,
#[serde(skip_serializing_if = "Option::is_none")]
expander: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
scorer: Option<FtScorerOptions<'a>>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
explainscore: bool,
#[serde(skip_serializing_if = "Option::is_none")]
payload: Option<&'a [u8]>,
#[serde(skip_serializing_if = "Option::is_none")]
sortby: Option<(&'a str, SortOrder)>,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withcount: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withoutcount: bool,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<(u32, u32)>,
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
params: SmallVec<[(&'a str, &'a str); 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
dialect: Option<u64>,
}
impl<'a> FtSearchOptions<'a> {
#[must_use]
pub fn nocontent(mut self) -> Self {
self.nocontent = true;
self
}
#[must_use]
pub fn verbatim(mut self) -> Self {
self.verbatim = true;
self
}
#[must_use]
pub fn withscores(mut self) -> Self {
self.withscores = true;
self
}
#[must_use]
pub fn withpayloads(mut self) -> Self {
self.withpayloads = true;
self
}
#[must_use]
pub fn withsortkeys(mut self) -> Self {
self.withsortkeys = true;
self
}
#[must_use]
pub fn filter(mut self, numeric_field: &'a str, min: &'a str, max: &'a str) -> Self {
self.filter.push(FtFilterOptions {
filter: (numeric_field, min, max),
});
self
}
#[must_use]
pub fn geo_filter(
mut self,
geo_field: &'a str,
longitude: f64,
latitude: f64,
radius: f64,
unit: GeoUnit,
) -> Self {
self.geofilter.push(FtGeoFilterOptions {
geofilter: (geo_field, longitude, latitude, radius, unit),
});
self
}
#[must_use]
pub fn inkey(mut self, key: &'a str) -> Self {
self.inkeys.push(key);
self
}
#[must_use]
pub fn infields(mut self, field: &'a str) -> Self {
self.infields.push(field);
self
}
#[must_use]
pub fn _return(mut self, attribute: FtAttribute<'a>) -> Self {
self.r#return.push(attribute);
self
}
#[must_use]
pub fn summarize(mut self, options: FtSearchSummarizeOptions<'a>) -> Self {
self.summarize = Some(options);
self
}
#[must_use]
pub fn highlight(mut self, options: FtSearchHighlightOptions<'a>) -> Self {
self.highlight = Some(options);
self
}
#[must_use]
pub fn slop(mut self, slop: u32) -> Self {
self.slop = Some(slop);
self
}
#[must_use]
pub fn timeout(mut self, milliseconds: u64) -> Self {
self.timeout = Some(milliseconds);
self
}
#[must_use]
pub fn inorder(mut self) -> Self {
self.inorder = true;
self
}
#[must_use]
pub fn language(mut self, language: FtLanguage) -> Self {
self.language = Some(language);
self
}
#[must_use]
pub fn expander(mut self, expander: &'a str) -> Self {
self.expander = Some(expander);
self
}
#[must_use]
pub fn scorer(mut self, options: FtScorerOptions<'a>) -> Self {
self.scorer = Some(options);
self
}
#[must_use]
pub fn explainscore(mut self) -> Self {
self.explainscore = true;
self
}
#[must_use]
pub fn payload(mut self, payload: &'a [u8]) -> Self {
self.payload = Some(payload);
self
}
#[must_use]
pub fn sortby(mut self, attribute: &'a str, order: SortOrder, with_count: bool) -> Self {
self.sortby = Some((attribute, order));
if with_count {
self.withcount = true;
self.withoutcount = false;
} else {
self.withcount = false;
self.withoutcount = true;
}
self
}
#[must_use]
pub fn limit(mut self, offset: u32, num: u32) -> Self {
self.limit = Some((offset, num));
self
}
#[must_use]
pub fn param(mut self, name: &'a str, value: &'a str) -> Self {
self.params.push((name, value));
self
}
#[must_use]
pub fn dialect(mut self, dialect_version: u64) -> Self {
self.dialect = Some(dialect_version);
self
}
}
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct FtFilterOptions<'a> {
filter: (&'a str, &'a str, &'a str),
}
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct FtGeoFilterOptions<'a> {
geofilter: (&'a str, f64, f64, f64, GeoUnit),
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtSearchSummarizeOptions<'a> {
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
fields: SmallVec<[&'a str; 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
frags: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
len: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
separator: Option<&'a str>,
}
impl<'a> FtSearchSummarizeOptions<'a> {
#[must_use]
pub fn field(mut self, field: &'a str) -> Self {
self.fields.push(field);
self
}
#[must_use]
pub fn frags(mut self, num_frags: u32) -> Self {
self.frags = Some(num_frags);
self
}
#[must_use]
pub fn len(mut self, frag_len: u32) -> Self {
self.len = Some(frag_len);
self
}
#[must_use]
pub fn separator(mut self, separator: &'a str) -> Self {
self.separator = Some(separator);
self
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtSearchHighlightOptions<'a> {
#[serde(
skip_serializing_if = "SmallVec::is_empty",
serialize_with = "serialize_slice_with_arg_count"
)]
fields: SmallVec<[&'a str; 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
tags: Option<(&'a str, &'a str)>,
}
impl<'a> FtSearchHighlightOptions<'a> {
#[must_use]
pub fn fields(mut self, field: &'a str) -> Self {
self.fields.push(field);
self
}
#[must_use]
pub fn tags(mut self, open_tag: &'a str, close_tag: &'a str) -> Self {
self.tags = Some((open_tag, close_tag));
self
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum FtLanguage {
Arabic,
Armenian,
Basque,
Catalan,
Chinese,
Danish,
Dutch,
#[default]
English,
Finnish,
French,
German,
Greek,
Hungarian,
Indonesian,
Irish,
Italian,
Lithuanian,
Nepali,
Norwegian,
Portuguese,
Romanian,
Russian,
Serbian,
Spanish,
Swedish,
Tamil,
Turkish,
Yiddish,
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtSpellCheckOptions<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
distance: Option<u64>,
#[serde(rename = "", skip_serializing_if = "SmallVec::is_empty")]
terms: SmallVec<[FtSpellCheckTermsOption<'a>; 10]>,
#[serde(skip_serializing_if = "Option::is_none")]
dialect: Option<u64>,
}
impl<'a> FtSpellCheckOptions<'a> {
#[must_use]
pub fn distance(mut self, distance: u64) -> Self {
self.distance = Some(distance);
self
}
#[must_use]
pub fn terms(mut self, term_type: FtTermType, dictionary: &'a str) -> Self {
self.terms.push(FtSpellCheckTermsOption {
terms: (term_type, dictionary),
});
self
}
#[must_use]
pub fn dialect(mut self, dialect_version: u64) -> Self {
self.dialect = Some(dialect_version);
self
}
}
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct FtSpellCheckTermsOption<'a> {
terms: (FtTermType, &'a str),
}
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum FtTermType {
Include,
Exclude,
}
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct FtSpellCheckResult {
#[serde(rename = "results", deserialize_with = "deserialize_misspelled_terms")]
pub misspelled_terms: Vec<FtMisspelledTerm>,
}
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct FtMisspelledTerm {
pub misspelled_term: String,
pub suggestions: Vec<(String, f64)>,
}
fn deserialize_misspelled_terms<'de, D>(deserializer: D) -> Result<Vec<FtMisspelledTerm>, D::Error>
where
D: Deserializer<'de>,
{
struct SuggestionSeed;
impl<'de> DeserializeSeed<'de> for SuggestionSeed {
type Value = (String, f64);
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = (String, f64);
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a (String, f64)")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let Some(suggestion) = map.next_entry()? else {
return Err(de::Error::custom("Cannot parse misspelled terms"));
};
Ok(suggestion)
}
}
deserializer.deserialize_map(Visitor)
}
}
struct SuggestionsSeed;
impl<'de> DeserializeSeed<'de> for SuggestionsSeed {
type Value = Vec<(String, f64)>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Vec<(String, f64)>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a Vec<(String, f64)>")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut suggestions = Vec::with_capacity(seq.size_hint().unwrap_or_default());
while let Some(suggestion) = seq.next_element_seed(SuggestionSeed)? {
suggestions.push(suggestion);
}
Ok(suggestions)
}
}
deserializer.deserialize_seq(Visitor)
}
}
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Vec<FtMisspelledTerm>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an array of FtMisspelledTerm")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut result = Vec::with_capacity(map.size_hint().unwrap_or_default());
while let Some(misspelled_term) = map.next_key()? {
let suggestions = map.next_value_seed(SuggestionsSeed)?;
result.push(FtMisspelledTerm {
misspelled_term,
suggestions,
});
}
Ok(result)
}
}
deserializer.deserialize_map(Visitor)
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtSugAddOptions<'a> {
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
incr: bool,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_byte_buf_option"
)]
payload: Option<&'a [u8]>,
}
impl<'a> FtSugAddOptions<'a> {
#[must_use]
pub fn incr(mut self) -> Self {
self.incr = true;
self
}
#[must_use]
pub fn payload(mut self, payload: &'a [u8]) -> Self {
self.payload = Some(payload);
self
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct FtSugGetOptions {
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
fuzzy: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withscores: bool,
#[serde(
skip_serializing_if = "std::ops::Not::not",
serialize_with = "serialize_flag"
)]
withpayloads: bool,
#[serde(skip_serializing_if = "Option::is_none")]
max: Option<u32>,
}
impl FtSugGetOptions {
#[must_use]
pub fn fuzzy(mut self) -> Self {
self.fuzzy = true;
self
}
#[must_use]
pub fn withscores(mut self) -> Self {
self.withscores = true;
self
}
#[must_use]
pub fn withpayloads(mut self) -> Self {
self.withpayloads = true;
self
}
#[must_use]
pub fn max(mut self, num: u32) -> Self {
self.max = Some(num);
self
}
}