Skip to main content

flusso_query/
lib.rs

1//! `flusso-query` — a typed query client for a flusso-maintained search index.
2//!
3//! Targets OpenSearch and Elasticsearch 7.x, which share the `_search` query DSL
4//! this crate emits; any future backend divergence is handled on the [`Client`],
5//! not by separate crates.
6//!
7//! This is the **runtime** layer described in the
8//! [querying guide](https://alias2k.github.io/flusso/guides/querying.html):
9//! the transport, the field-handle / [`Query`] / [`Search`] builder, and the
10//! typed [`SearchResponse`]. It is generic over the caller's document type `T`.
11//!
12//! # What this first cut covers
13//!
14//! - [`Client`] transport over OpenSearch (`connect`, `basic_auth`, search,
15//!   msearch, get).
16//! - Field handles ([`Keyword`], [`Text`], [`Bool`], [`Number`], [`Date`],
17//!   [`Nested`], [`Binary`], [`Json`]) with operators that build a [`Query`].
18//! - [`Query`] composition (`and` / `or` / `not`) and the [`Search`] bool-clause
19//!   builder (`query` / `filter` / `must_not` / `should`, plus `sort` / `from` /
20//!   `size` / `raw`). A [`Search`] is a plain client-free value — build it
21//!   anywhere, store and reuse it; a [`Client`] appears only at the terminals:
22//!   [`Search::send`] (a typed page), [`Search::ids`] (a page of bare document
23//!   ids, `_source: false`), or [`Search::count`] (just the number of matches,
24//!   via `_count`).
25//! - Several searches in one round-trip: [`Client::msearch`] (a tuple of
26//!   `&Search<T>`, mixed document types, one typed response per slot) and
27//!   [`Client::msearch_all`] (a slice of one type).
28//! - Combined (blended) search: [`FlussoMultiDocument`] — a caller-owned enum
29//!   with one variant per document type — and its [`MultiSearch`] builder. One
30//!   query across all the union's indexes, one relevance-ranked result list,
31//!   each hit decoded into the variant matching its physical `_index`.
32//! - Typed [`SearchResponse`] / [`Hit`].
33//!
34//! Also covered: optional filters (`Option<Q>` is a [`Query`]); object/to-one-join
35//! handles ([`Object`]); shaping returned nested arrays ([`Search::filter_nested`]
36//! with [`Nested::matching`], via `inner_hits`); and scope-tagged queries —
37//! [`Query`]`<S>` carries the scope `S` it was built in ([`Root`] for the document
38//! root and flattened objects, the element type for a `nested` array), so a nested
39//! query must be lifted through [`Nested::any`]/[`Nested::all`] before it can join a
40//! root query; the compiler enforces it.
41//!
42//! # Not yet built
43//!
44//! - The `#[derive(FlussoDocument)]` and `#[derive(FlussoMultiDocument)]`
45//!   proc-macros live in `flusso-query-derive` (the `derive` feature). Without
46//!   them, document structs + handles and union impls are written by hand —
47//!   exactly the calls this crate exposes (see the integration tests).
48//! - `filter_nested`'s `keep_source()` opt-out (it always replaces the array in
49//!   `source` today) and a typed `hit.nested(handle)` accessor.
50//!
51//! # Example (hand-written until the derive lands)
52//!
53//! ```no_run
54//! use flusso_query::{Client, Keyword, Number, Nested, kind};
55//!
56//! #[derive(serde::Deserialize)]
57//! struct User {
58//!     email: String,
59//!     #[serde(rename = "orderCount")]
60//!     order_count: i64,
61//! }
62//!
63//! impl User {
64//!     fn email() -> Keyword { Keyword::at("email") }
65//!     fn order_count() -> Number<kind::Long> { Number::at("orderCount") }
66//!     fn query() -> flusso_query::Search<User> {
67//!         flusso_query::Search::new("users", "xxxxxx")
68//!     }
69//! }
70//!
71//! # async fn run() -> flusso_query::Result<()> {
72//! // A query is a plain value — no client involved while building it.
73//! let busy = User::query()
74//!     .filter(User::email().eq("ada@example.com"))
75//!     .filter(User::order_count().gte(5))
76//!     .size(20);
77//!
78//! // The client appears once, when it's time to run.
79//! let client = Client::connect("https://localhost:9200")?;
80//! let page = busy.send(&client).await?;
81//! println!("{} matches", page.total);
82//! # Ok(())
83//! # }
84//! ```
85
86// The combined-search e2e (`tests/combined_search.rs`) pulls dev-deps the unit
87// tests don't touch (the OpenSearch sink, testcontainers, tokio); they look
88// unused to the `--test`-compiled lib.
89#![cfg_attr(test, allow(unused_crate_dependencies))]
90
91mod client;
92mod error;
93mod handles;
94mod msearch;
95mod multi;
96mod path;
97mod query;
98mod search;
99
100#[cfg(test)]
101mod tests;
102
103pub use client::Client;
104pub use error::{Error, Result};
105pub use handles::{
106    Binary, Bool, BoostMode, BoostingQuery, CombinedFieldsQuery, ConstantScoreQuery, Date, DateMap,
107    DisMaxQuery, Distance, DistanceFeatureQuery, DistanceType, DistanceUnit, EqQuery, FlussoMap,
108    FlussoValue, FunctionScoreQuery, Fuzziness, FuzzyQuery, Geo, GeoDistanceQuery, GeoPoint,
109    IdsQuery, Json, Keyword, KeywordMap, MapKey, MapKeySort, MapSearch, MatchQuery, MaybeOrderBy,
110    MinimumShouldMatch, Missing, MoreLikeThisQuery, MultiMatchQuery, MultiMatchType, Nested,
111    NestedProjection, NestedQuery, NestedScoreMode, NoSubfields, Number, NumberMap, NumericType,
112    Object, Operator, OrderBy, PrefixQuery, QueryStringQuery, RangeQuery, RangeRelation,
113    RankFeatureQuery, RegexpQuery, ScoreMode, ScriptQuery, ScriptScoreQuery, ScriptSortType,
114    SimpleQueryStringQuery, Sort, SortBuilder, SortMode, SortOrder, Sortable, TermQuery,
115    TermsQuery, Text, TextMap, ValidationMethod, WildcardQuery, WithSubfields, ZeroTermsQuery,
116    boosting, combined_fields, constant_score, dis_max, distance_feature, function_score, ids,
117    kind, more_like_this, multi_match, query_string, rank_feature, script, script_score,
118    simple_query_string,
119};
120pub use msearch::MsearchBundle;
121pub use multi::{FlussoMultiDocument, MultiSearch};
122pub use path::{Segment, SegmentKind, nested_boundaries};
123pub use query::{AsQuery, Query, Root};
124pub use search::{FlussoDocument, FlussoIndex, Highlight, Hit, Search, SearchResponse, ShardStats};
125
126/// `#[derive(FlussoDocument)]` — generates the typed query surface for a
127/// hand-written document struct (its field handles) and implements the
128/// [`FlussoDocument`](trait@FlussoDocument) trait (`INDEX`/`SCHEMA_HASH` +
129/// `search`/`get`). See the
130/// [querying guide](https://alias2k.github.io/flusso/guides/querying.html).
131/// Enabled by the `derive` feature.
132#[cfg(feature = "derive")]
133pub use flusso_query_derive::FlussoDocument;
134
135/// `#[derive(FlussoValue)]` — implements [`trait@FlussoValue`] for an enum or newtype
136/// wrapper, so it may stand in for a field of the chosen kind (`#[flusso(keyword)]`
137/// — the default — `#[flusso(text)]`, `#[flusso(number)]`, or `#[flusso(date)]`)
138/// in a [`FlussoDocument`] struct. Enabled by the `derive` feature.
139#[cfg(feature = "derive")]
140pub use flusso_query_derive::FlussoValue;
141
142/// `#[derive(FlussoMap)]` — implements [`trait@FlussoMap`] for a newtype wrapper
143/// over a `map` field, so it may stand in for a `map` of the chosen value kind
144/// (`#[flusso(keyword)]` — the default — `#[flusso(text)]`, `#[flusso(number)]`,
145/// or `#[flusso(date)]`) in a [`FlussoDocument`] struct. A bare
146/// `HashMap<String, V>` needs no derive. Enabled by the `derive` feature.
147#[cfg(feature = "derive")]
148pub use flusso_query_derive::FlussoMap;
149
150/// `#[derive(FlussoMultiDocument)]` — implements [`trait@FlussoMultiDocument`]
151/// for an enum with one single-field variant per document type (the
152/// combined-search union): the generated impl lists every variant's index and
153/// decodes each hit into the variant matching its physical `_index`. Enabled
154/// by the `derive` feature.
155#[cfg(feature = "derive")]
156pub use flusso_query_derive::FlussoMultiDocument;
157
158// The multi-document derive's generated code deserializes variant payloads;
159// routing it through this re-export keeps it on this crate's `serde_json`.
160// Hidden: not API.
161#[doc(hidden)]
162pub use serde_json as __serde_json;
163
164/// `rust_decimal::Decimal`, re-exported for `decimal` fields. Enabled by the
165/// `decimal` feature.
166#[cfg(feature = "decimal")]
167pub use rust_decimal::Decimal;
168
169/// `chrono`, re-exported for `date`/`timestamp` fields. Enabled by the `chrono`
170/// feature.
171#[cfg(feature = "chrono")]
172pub use chrono;
173
174/// `uuid`, re-exported for `keyword` id / foreign-key fields. With this feature
175/// a `uuid::Uuid` field needs no `#[flusso(skip)]` and `id().eq(some_uuid)`
176/// works without `.to_string()`. Enabled by the `uuid` feature.
177#[cfg(feature = "uuid")]
178pub use uuid;