Compare commits

..

1 Commits

Author SHA1 Message Date
Quentin de Quelen
62a1de043d Add verbose OpenAPI schema descriptions for documentation
Add detailed doc comments to API types and route parameters to improve
the generated OpenAPI specification. These descriptions will be used
by the documentation team to generate comprehensive API documentation.

All doc comments respect the 80 character line limit using multi-line format.
2025-12-20 11:46:19 +01:00
41 changed files with 1567 additions and 1591 deletions

View File

@@ -89,8 +89,8 @@ jobs:
asset_name: meilisearch-${{ matrix.edition-suffix }}${{ matrix.asset_name }}
tag: ${{ github.ref }}
publish-openapi-files:
name: Publish OpenAPI files
publish-openapi-file:
name: Publish OpenAPI file
needs: check-version
runs-on: ubuntu-latest
steps:
@@ -101,26 +101,16 @@ jobs:
with:
toolchain: stable
override: true
- name: Generate OpenAPI files
- name: Generate OpenAPI file
run: |
cd crates/openapi-generator
cargo run --release -- --pretty --debug --output ../../meilisearch-openapi.json
cargo run --release -- --pretty --debug --with-mintlify-code-samples --output ../../meilisearch-openapi-mintlify.json
- name: Upload OpenAPI file to Release
cargo run --release -- --pretty --output ../../meilisearch.json
- name: Upload OpenAPI to Release
# No need to upload for dry run (cron or workflow_dispatch)
if: github.event_name == 'release'
uses: svenstaro/upload-release-action@2.11.2
with:
repo_token: ${{ secrets.MEILI_BOT_GH_PAT }}
file: ./meilisearch-openapi.json
file: ./meilisearch.json
asset_name: meilisearch-openapi.json
tag: ${{ github.ref }}
- name: Upload Mintlify OpenAPI file to Release
# No need to upload for dry run (cron or workflow_dispatch)
if: github.event_name == 'release'
uses: svenstaro/upload-release-action@2.11.2
with:
repo_token: ${{ secrets.MEILI_BOT_GH_PAT }}
file: ./meilisearch-openapi-mintlify.json
asset_name: meilisearch-openapi-mintlify.json
tag: ${{ github.ref }}

3
.gitignore vendored
View File

@@ -29,6 +29,3 @@ crates/meilisearch/db.snapshot
# Fuzzcheck data for the facet indexing fuzz test
crates/milli/fuzz/update::facet::incremental::fuzz::fuzz/
# OpenAPI generator
**/meilisearch-openapi.json

View File

@@ -117,7 +117,7 @@ With swagger:
With the internal crate:
```bash
cd crates/openapi-generator
cargo run --release -- --pretty
cargo run --release -- --pretty --output meilisearch.json
```
### Logging

977
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,14 @@ time = { version = "0.3.44", features = [
] }
tokio = "1.48"
urlencoding = "2.1.3"
utoipa = { version = "5.4.0", features = ["macros"] }
utoipa = { version = "5.4.0", features = [
"macros",
"non_strict_integers",
"preserve_order",
"uuid",
"time",
"openapi_extensions",
] }
uuid = { version = "1.18.1", features = ["serde", "v4"] }
[dev-dependencies]

View File

@@ -7,30 +7,71 @@ use crate::batches::{Batch, BatchId, BatchStats, EmbedderStatsView};
use crate::task_view::DetailsView;
use crate::tasks::serialize_duration;
/// Represents a batch of tasks that were processed together.
///
/// Meilisearch groups compatible tasks into batches for efficient processing.
/// For example, multiple document additions to the same index may be batched
/// together. Use this view to monitor batch progress and performance.
#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct BatchView {
/// The unique sequential identifier assigned to this batch. Batch UIDs
/// are assigned in order of creation and can be used to retrieve specific
/// batch information or correlate tasks that were processed together.
pub uid: BatchId,
/// Real-time progress information for the batch if it's currently being
/// processed. Contains details about which step is executing and the
/// percentage of completion. This is `null` for completed batches.
#[schema(value_type = Option<ProgressView>)]
pub progress: Option<ProgressView>,
/// Aggregated details from all tasks in this batch. For example, if the
/// batch contains multiple document addition tasks, this will show the
/// total number of documents received and indexed across all tasks.
pub details: DetailsView,
/// Statistical information about the batch, including the number of tasks
/// by status, the types of tasks included, and the indexes affected.
/// Useful for understanding the composition and outcome of the batch.
pub stats: BatchStatsView,
/// The total time spent processing this batch, formatted as an ISO-8601
/// duration (e.g., `PT2.5S` for 2.5 seconds). This is `null` for batches
/// that haven't finished processing yet.
#[serde(serialize_with = "serialize_duration", default)]
pub duration: Option<Duration>,
/// The timestamp when Meilisearch began processing this batch, formatted
/// as an RFC 3339 date-time string. All batches have a start time as it's
/// set when processing begins.
#[serde(with = "time::serde::rfc3339", default)]
pub started_at: OffsetDateTime,
/// The timestamp when this batch finished processing, formatted as an
/// RFC 3339 date-time string. This is `null` for batches that are still
/// being processed.
#[serde(with = "time::serde::rfc3339::option", default)]
pub finished_at: Option<OffsetDateTime>,
/// Explains why the batch was finalized and stopped accepting more tasks.
/// Common reasons include reaching the maximum batch size, encountering
/// incompatible tasks, or processing being explicitly triggered.
#[serde(default = "meilisearch_types::batches::default_stop_reason")]
pub batch_strategy: String,
}
/// Provides comprehensive statistics about a batch's execution.
///
/// Includes task counts, status breakdowns, and AI embedder usage. This
/// information is useful for monitoring system performance and understanding
/// batch composition.
#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct BatchStatsView {
/// Core batch statistics including the total number of tasks, counts by
/// status (succeeded, failed, canceled), task types included, and which
/// indexes were affected by this batch.
#[serde(flatten)]
pub stats: BatchStats,
/// Statistics about AI embedder API requests made during batch processing.
/// Includes total requests, successful/failed counts, and response times.
/// Only present when the batch involved vector embedding operations.
#[serde(skip_serializing_if = "EmbedderStatsView::skip_serializing", default)]
pub embedder_requests: EmbedderStatsView,
}

View File

@@ -72,28 +72,40 @@ pub struct BatchEnqueuedAt {
pub oldest: OffsetDateTime,
}
/// Statistics for a batch of tasks
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct BatchStats {
/// Total number of tasks in the batch
pub total_nb_tasks: BatchId,
/// Count of tasks by status
pub status: BTreeMap<Status, u32>,
/// Count of tasks by type
pub types: BTreeMap<Kind, u32>,
/// Count of tasks by index UID
pub index_uids: BTreeMap<String, u32>,
/// Detailed progress trace information
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
pub progress_trace: serde_json::Map<String, serde_json::Value>,
/// Write channel congestion metrics
#[serde(default, skip_serializing_if = "Option::is_none")]
pub write_channel_congestion: Option<serde_json::Map<String, serde_json::Value>>,
/// Internal database size information
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
pub internal_database_sizes: serde_json::Map<String, serde_json::Value>,
}
/// Statistics for embedder requests
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct EmbedderStatsView {
/// Total number of embedder requests
pub total: usize,
/// Number of failed embedder requests
pub failed: usize,
/// Last error message from the embedder
#[serde(skip_serializing_if = "Option::is_none", default)]
pub last_error: Option<String>,
}

View File

@@ -45,19 +45,27 @@ pub struct CreateApiKey {
#[schema(example = "Indexing Products API key")]
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
pub name: Option<String>,
/// A uuid v4 to identify the API Key. If not specified, it's generated by Meilisearch.
/// A uuid v4 to identify the API Key. If not specified, it's generated
/// by Meilisearch.
#[schema(value_type = Uuid, example = json!(null))]
#[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, try_from(&String) = Uuid::from_str -> uuid::Error)]
pub uid: KeyId,
/// A list of actions permitted for the key. `["*"]` for all actions. The `*` character can be used as a wildcard when located at the last position. e.g. `documents.*` to authorize access on all documents endpoints.
/// A list of actions permitted for the key. `["*"]` for all actions. The
/// `*` character can be used as a wildcard when located at the last
/// position. e.g. `documents.*` to authorize access on all documents
/// endpoints.
#[schema(example = json!(["documents.add"]))]
#[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)]
pub actions: Vec<Action>,
/// A list of accessible indexes permitted for the key. `["*"]` for all indexes. The `*` character can be used as a wildcard when located at the last position. e.g. `products_*` to allow access to all indexes whose names start with `products_`.
/// A list of accessible indexes permitted for the key. `["*"]` for all
/// indexes. The `*` character can be used as a wildcard when located at
/// the last position. e.g. `products_*` to allow access to all indexes
/// whose names start with `products_`.
#[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)]
#[schema(value_type = Vec<String>, example = json!(["products"]))]
pub indexes: Vec<IndexUidPattern>,
/// Represent the expiration date and time as RFC 3339 format. `null` equals to no expiration time.
/// Represent the expiration date and time as RFC 3339 format. `null`
/// equals to no expiration time.
#[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, try_from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)]
pub expires_at: Option<OffsetDateTime>,
}
@@ -99,13 +107,21 @@ fn deny_immutable_fields_api_key(
}
}
/// Request body for updating an existing API key. Only the name and
/// description can be modified - other properties like actions, indexes,
/// and expiration are immutable after creation.
#[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)]
#[schema(rename_all = "camelCase")]
pub struct PatchApiKey {
/// A new description for the API key. Pass `null` to remove the existing
/// description. Useful for documenting the purpose or usage of the key.
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
#[schema(value_type = Option<String>, example = "This key is used to update documents in the products index")]
pub description: Setting<String>,
/// A new human-readable name for the API key. Pass `null` to remove the
/// existing name. Use this to identify keys by purpose, such as
/// "Production Search Key" or "CI/CD Indexing Key".
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
#[schema(value_type = Option<String>, example = "Indexing Products API key")]
pub name: Setting<String>,

View File

@@ -3,11 +3,21 @@ use milli::{AttributePatterns, LocalizedAttributesRule};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
/// Defines a rule for associating specific locales (languages) with
/// attributes. This allows Meilisearch to use language-specific tokenization
/// and processing for matched attributes, improving search quality for
/// multilingual content.
#[derive(Debug, Clone, PartialEq, Eq, Deserr, Serialize, Deserialize, ToSchema)]
#[deserr(rename_all = camelCase)]
#[serde(rename_all = "camelCase")]
pub struct LocalizedAttributesRuleView {
/// Patterns to match attribute names. Use `*` as a wildcard to match any
/// characters. For example, `["title_*", "description"]` matches
/// `title_en`, `title_fr`, and `description`.
pub attribute_patterns: AttributePatterns,
/// The list of locales (languages) to apply to matching attributes. When
/// these attributes are indexed, Meilisearch will use language-specific
/// tokenization rules. Examples: `["en", "fr"]` or `["jpn", "zho"]`.
pub locales: Vec<Locale>,
}

View File

@@ -74,64 +74,98 @@ fn validate_min_word_size_for_typo_setting<E: DeserializeError>(
Ok(s)
}
/// Configures the minimum word length required before typos are allowed.
///
/// This helps prevent matching very short words with typos, which can lead
/// to irrelevant results.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError<InvalidSettingsTypoTolerance>)]
pub struct MinWordSizeTyposSetting {
/// The minimum word length required to accept one typo. Words shorter
/// than this value must match exactly. For example, if set to `5`, the
/// word "apple" (5 letters) can have one typo, but "app" (3 letters)
/// cannot. Defaults to `5`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<u8>, example = json!(5))]
pub one_typo: Setting<u8>,
/// The minimum word length required to accept two typos. Words shorter
/// than this value can have at most one typo. For example, if set to `9`,
/// the word "computing" (9 letters) can have two typos. Must be greater
/// than or equal to `oneTypo`. Defaults to `9`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<u8>, example = json!(9))]
pub two_typos: Setting<u8>,
}
/// Configuration for typo tolerance in search queries.
///
/// Typo tolerance allows Meilisearch to match documents even when search
/// terms contain spelling mistakes.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrJsonError<InvalidSettingsTypoTolerance>>)]
pub struct TypoSettings {
/// When `true`, enables typo tolerance for search queries. When `false`,
/// only exact matches are returned. Defaults to `true`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<bool>, example = json!(true))]
pub enabled: Setting<bool>,
/// Configures the minimum word length before typos are allowed. Contains
/// `oneTypo` (min length for 1 typo) and `twoTypos` (min length for 2
/// typos) settings.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
#[schema(value_type = Option<MinWordSizeTyposSetting>, example = json!({ "oneTypo": 5, "twoTypo": 9 }))]
pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>,
/// A list of words for which typo tolerance should be disabled. Use this
/// for brand names, technical terms, or other words that must be matched
/// exactly. Example: `["iPhone", "macOS"]`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<BTreeSet<String>>, example = json!(["iPhone", "phone"]))]
pub disable_on_words: Setting<BTreeSet<String>>,
/// A list of attributes for which typo tolerance should be disabled.
/// Searches in these attributes will only return exact matches. Useful
/// for fields like product codes or IDs.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<BTreeSet<String>>, example = json!(["uuid", "url"]))]
pub disable_on_attributes: Setting<BTreeSet<String>>,
/// When `true`, disables typo tolerance on numeric tokens. This prevents
/// numbers like `123` from matching `132`. Defaults to `false`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<bool>, example = json!(true))]
pub disable_on_numbers: Setting<bool>,
}
/// Faceting configuration settings
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct FacetingSettings {
/// Maximum number of facet values returned for each facet
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<usize>, example = json!(10))]
pub max_values_per_facet: Setting<usize>,
/// How facet values should be sorted (by count or alphabetically)
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<BTreeMap<String, FacetValuesSort>>, example = json!({ "genre": FacetValuesSort::Count }))]
pub sort_facet_values_by: Setting<BTreeMap<String, FacetValuesSort>>,
}
/// Pagination configuration settings
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct PaginationSettings {
/// Maximum number of hits that can be returned
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<usize>, example = json!(250))]
@@ -157,12 +191,17 @@ impl MergeWithError<milli::CriterionError> for DeserrJsonError<InvalidSettingsRa
#[serde(transparent)]
/// "Technical" type that is required due to utoipa.
///
/// We did not find a way to implement [`utoipa::ToSchema`] for the [`Setting`] enum,
/// but most types can use the `value_type` macro parameter to workaround that issue.
/// We did not find a way to implement [`utoipa::ToSchema`] for the
/// [`Setting`] enum, but most types can use the `value_type` macro parameter
/// to workaround that issue.
///
/// However that type is used in the settings route, including through the macro that auto-generate
/// all the settings route, so we can't remap the `value_type`.
/// However that type is used in the settings route, including through the
/// macro that auto-generate all the settings route, so we can't remap the
/// `value_type`.
pub struct SettingEmbeddingSettings {
/// Configuration for this embedder. Includes the source (openAi,
/// huggingFace, ollama, rest, userProvided), model settings, API
/// credentials, and document template for generating embeddings.
#[schema(inline, value_type = Option<crate::milli::vector::settings::EmbeddingSettings>)]
pub inner: Setting<crate::milli::vector::settings::EmbeddingSettings>,
}
@@ -185,9 +224,10 @@ impl<E: DeserializeError> Deserr<E> for SettingEmbeddingSettings {
}
}
/// Holds all the settings for an index. `T` can either be `Checked` if they represents settings
/// whose validity is guaranteed, or `Unchecked` if they need to be validated. In the later case, a
/// call to `check` will return a `Settings<Checked>` from a `Settings<Unchecked>`.
/// Holds all the settings for an index. `T` can either be `Checked` if
/// they represents settings whose validity is guaranteed, or `Unchecked` if
/// they need to be validated. In the later case, a call to `check` will
/// return a `Settings<Checked>` from a `Settings<Unchecked>`.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Deserr, ToSchema)]
#[serde(
deny_unknown_fields,
@@ -203,13 +243,16 @@ pub struct Settings<T> {
#[schema(value_type = Option<Vec<String>>, example = json!(["id", "title", "description", "url"]))]
pub displayed_attributes: WildcardSetting,
/// Fields in which to search for matching query words sorted by order of importance.
/// Fields in which to search for matching query words sorted by order of
/// importance.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["title", "description"]))]
pub searchable_attributes: WildcardSetting,
/// Attributes to use for faceting and filtering. See [Filtering and Faceted Search](https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters).
/// Attributes to use for faceting and filtering.
/// See [Filtering and Faceted
/// Search](https://meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters).
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
#[schema(value_type = Option<Vec<FilterableAttributesRule>>, example = json!(["release_date", "genre"]))]
@@ -221,8 +264,9 @@ pub struct Settings<T> {
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date"]))]
pub sortable_attributes: Setting<BTreeSet<String>>,
/// List of ranking rules sorted by order of importance. The order is customizable.
/// [A list of ordered built-in ranking rules](https://www.meilisearch.com/docs/learn/relevancy/relevancy).
/// List of ranking rules sorted by order of importance. The order is
/// customizable. [A list of ordered built-in ranking
/// rules](https://www.meilisearch.com/docs/learn/relevancy/relevancy).
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsRankingRules>)]
#[schema(value_type = Option<Vec<String>>, example = json!([RankingRuleView::Words, RankingRuleView::Typo, RankingRuleView::Proximity, RankingRuleView::Attribute, RankingRuleView::Exactness]))]
@@ -252,13 +296,15 @@ pub struct Settings<T> {
#[schema(value_type = Option<Vec<String>>, example = json!(["iPhone pro"]))]
pub dictionary: Setting<BTreeSet<String>>,
/// List of associated words treated similarly. A word associated to an array of word as synonyms.
/// List of associated words treated similarly. A word associated to an
/// array of word as synonyms.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsSynonyms>)]
#[schema(value_type = Option<BTreeMap<String, Vec<String>>>, example = json!({ "he": ["she", "they", "them"], "phone": ["iPhone", "android"]}))]
pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
/// Search returns documents with distinct (different) values of the given field.
/// Search returns documents with distinct (different) values of the given
/// field.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)]
#[schema(value_type = Option<String>, example = json!("sku"))]
@@ -270,19 +316,24 @@ pub struct Settings<T> {
#[schema(value_type = Option<String>, example = json!(ProximityPrecisionView::ByAttribute))]
pub proximity_precision: Setting<ProximityPrecisionView>,
/// Customize typo tolerance feature.
/// Typo tolerance settings for controlling how Meilisearch handles
/// spelling mistakes in search queries. Configure minimum word lengths,
/// disable on specific words or attributes.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
#[schema(value_type = Option<TypoSettings>, example = json!({ "enabled": true, "disableOnAttributes": ["title"]}))]
pub typo_tolerance: Setting<TypoSettings>,
/// Faceting settings.
/// Faceting settings for controlling facet behavior. Configure maximum
/// facet values returned and sorting order for facet values.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)]
#[schema(value_type = Option<FacetingSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
pub faceting: Setting<FacetingSettings>,
/// Pagination settings.
/// Pagination settings for controlling the maximum number of results
/// that can be returned. Set `maxTotalHits` to limit how far users can
/// paginate into results.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)]
#[schema(value_type = Option<PaginationSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
@@ -300,27 +351,41 @@ pub struct Settings<T> {
#[schema(value_type = Option<u64>, example = json!(50))]
pub search_cutoff_ms: Setting<u64>,
/// Rules for associating locales (languages) with specific attributes.
/// This enables language-specific tokenization for multilingual content,
/// improving search quality for non-English text.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsLocalizedAttributes>)]
#[schema(value_type = Option<Vec<LocalizedAttributesRuleView>>, example = json!(50))]
pub localized_attributes: Setting<Vec<LocalizedAttributesRuleView>>,
/// When `true`, enables facet search which allows users to search within
/// facet values. When `false`, only the first `maxValuesPerFacet` values
/// are returned. Defaults to `true`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsFacetSearch>)]
#[schema(value_type = Option<bool>, example = json!(true))]
pub facet_search: Setting<bool>,
/// Controls prefix search behavior. `indexingTime` enables prefix search
/// by building a prefix database at indexing time. `disabled` turns off
/// prefix search for faster indexing. Defaults to `indexingTime`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsPrefixSearch>)]
#[schema(value_type = Option<PrefixSearchSettings>, example = json!("Hemlo"))]
pub prefix_search: Setting<PrefixSearchSettings>,
/// Customize the chat prompting.
/// Chat settings for AI-powered search. Configure the index description,
/// document template for rendering, and search parameters used when the
/// LLM queries this index.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsIndexChat>)]
#[schema(value_type = Option<ChatSettings>)]
pub chat: Setting<ChatSettings>,
/// Backend storage for vector embeddings. `memory` stores vectors in
/// memory for fastest performance. `database` stores vectors on disk to
/// reduce memory usage at the cost of speed.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsVectorStore>)]
#[schema(value_type = Option<VectorStoreBackend>)]
@@ -1009,7 +1074,8 @@ pub fn settings(
#[deserr(try_from(&String) = FromStr::from_str -> CriterionError)]
pub enum RankingRuleView {
/// Sorted by decreasing number of matched query terms.
/// Query words at the front of an attribute is considered better than if it was at the back.
/// Query words at the front of an attribute is considered better than if
/// it was at the back.
Words,
/// Sorted by increasing number of typos.
Typo,
@@ -1018,8 +1084,9 @@ pub enum RankingRuleView {
/// Documents with quey words contained in more important
/// attributes are considered better.
Attribute,
/// Dynamically sort at query time the documents. None, one or multiple Asc/Desc sortable
/// attributes can be used in place of this criterion at query time.
/// Dynamically sort at query time the documents. None, one or multiple
/// Asc/Desc sortable attributes can be used in place of this criterion at
/// query time.
Sort,
/// Sorted by the similarity of the matched words with the query words.
Exactness,

View File

@@ -14,48 +14,89 @@ use crate::tasks::{
serialize_duration, Details, DetailsExportIndexSettings, IndexSwap, Kind, Status, Task, TaskId,
};
/// Represents the current state and details of an asynchronous task.
///
/// Tasks are created when you perform operations like adding documents,
/// updating settings, or creating indexes. Use this view to monitor task
/// progress and check for errors.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct TaskView {
/// The unique sequential identifier of the task.
/// The unique sequential identifier assigned to this task. Task UIDs are
/// assigned in order of creation and can be used to retrieve specific
/// task information or track task dependencies.
#[schema(value_type = u32, example = 4312)]
pub uid: TaskId,
/// The unique identifier of the index where this task is operated.
/// The unique identifier of the batch that processed this task. Multiple
/// tasks may share the same batch UID if they were processed together
/// for efficiency. This is `null` for tasks that haven't been processed.
#[schema(value_type = Option<u32>, example = json!("movies"))]
pub batch_uid: Option<BatchId>,
/// The unique identifier of the index this task operates on. This is
/// `null` for global tasks like `dumpCreation` or `taskDeletion` that
/// don't target a specific index.
#[serde(default)]
pub index_uid: Option<String>,
/// The current processing status of the task. Possible values are:
/// `enqueued` (waiting), `processing` (executing), `succeeded`,
/// `failed`, or `canceled`.
pub status: Status,
/// The type of the task.
/// The type of operation this task performs. Examples include
/// `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`,
/// `indexCreation`, `indexDeletion`, `dumpCreation`, etc.
#[serde(rename = "type")]
pub kind: Kind,
/// The uid of the task that performed the taskCancelation if the task has been canceled.
/// If this task was canceled, this field contains the UID of the
/// `taskCancelation` task that canceled it. This is `null` for tasks
/// that were not canceled.
#[schema(value_type = Option<u32>, example = json!(4326))]
pub canceled_by: Option<TaskId>,
/// Contains type-specific information about the task, such as the number
/// of documents processed, settings that were applied, or filters that
/// were used. The structure varies depending on the task type.
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<DetailsView>)]
pub details: Option<DetailsView>,
/// If the task failed, this field contains detailed error information
/// including an error message, error code, error type, and a link to
/// documentation. This is `null` for tasks that succeeded or are still
/// processing.
#[schema(value_type = Option<ResponseError>)]
pub error: Option<ResponseError>,
/// Total elasped time the engine was in processing state expressed as a `ISO-8601` duration format.
/// The total time spent processing this task, formatted as an ISO-8601
/// duration (e.g., `PT0.5S` for 0.5 seconds). This is `null` for tasks
/// that haven't finished processing yet.
#[schema(value_type = Option<String>, example = json!(null))]
#[serde(serialize_with = "serialize_duration", default)]
pub duration: Option<Duration>,
/// An `RFC 3339` format for date/time/duration.
/// The timestamp when this task was added to the queue, formatted as an
/// RFC 3339 date-time string. All tasks have an enqueued timestamp as
/// it's set when the task is created.
#[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))]
#[serde(with = "time::serde::rfc3339")]
pub enqueued_at: OffsetDateTime,
/// An `RFC 3339` format for date/time/duration.
/// The timestamp when Meilisearch began processing this task, formatted
/// as an RFC 3339 date-time string. This is `null` for tasks that are
/// still in the queue waiting to be processed.
#[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))]
#[serde(with = "time::serde::rfc3339::option", default)]
pub started_at: Option<OffsetDateTime>,
/// An `RFC 3339` format for date/time/duration.
/// The timestamp when this task finished processing (whether successfully
/// or with an error), formatted as an RFC 3339 date-time string. This is
/// `null` for tasks that haven't finished yet.
#[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))]
#[serde(with = "time::serde::rfc3339::option", default)]
pub finished_at: Option<OffsetDateTime>,
/// Network topology information for distributed deployments. Contains
/// details about which nodes are involved in processing this task. This
/// is only present when running Meilisearch in a distributed config.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<DbTaskNetwork>)]
pub network: Option<DbTaskNetwork>,
/// Custom metadata string that was attached to this task when it was
/// created. This can be used to associate tasks with external systems,
/// track task origins, or add any application-specific information.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_metadata: Option<String>,
}
@@ -81,79 +122,147 @@ impl TaskView {
}
}
/// Contains type-specific details about a task's execution.
///
/// The fields present depend on the task type. For example, document addition
/// tasks will have `receivedDocuments` and `indexedDocuments`, while settings
/// update tasks will have the applied settings.
#[derive(Default, Debug, PartialEq, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct DetailsView {
/// Number of documents received for documentAdditionOrUpdate task.
/// The number of documents that were sent in the request payload for a
/// `documentAdditionOrUpdate` task. This count is determined before any
/// processing occurs.
#[serde(skip_serializing_if = "Option::is_none")]
pub received_documents: Option<u64>,
/// Number of documents finally indexed for documentAdditionOrUpdate task or a documentAdditionOrUpdate batch of tasks.
/// The number of documents that were successfully indexed after
/// processing a `documentAdditionOrUpdate` task. This may differ from
/// `receivedDocuments` if some documents were invalid or duplicates.
/// The inner `null` indicates the task is still processing.
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_documents: Option<Option<u64>>,
/// Number of documents edited for editDocumentByFunction task.
/// The number of documents that were modified by an `documentEdition`
/// task using a RHAI function. The inner `null` indicates the task is
/// still processing.
#[serde(skip_serializing_if = "Option::is_none")]
pub edited_documents: Option<Option<u64>>,
/// Value for the primaryKey field encountered if any for indexCreation or indexUpdate task.
/// The primary key attribute set for the index. For `indexCreation`
/// tasks, this is the primary key that was specified. For `indexUpdate`
/// tasks, this shows the new primary key if it was changed. The inner
/// `null` means no primary key was specified and Meilisearch will infer
/// it from documents.
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_key: Option<Option<String>>,
/// Number of provided document ids for the documentDeletion task.
/// The number of document IDs that were provided in a `documentDeletion`
/// request. This is the count before processing - the actual number
/// deleted may be lower if some IDs didn't exist.
#[serde(skip_serializing_if = "Option::is_none")]
pub provided_ids: Option<usize>,
/// Number of documents finally deleted for documentDeletion and indexDeletion tasks.
/// The number of documents that were actually removed from the index for
/// `documentDeletion`, `documentDeletionByFilter`, or `indexDeletion`
/// tasks. The inner `null` indicates the task is still processing.
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted_documents: Option<Option<u64>>,
/// Number of tasks that match the request for taskCancelation or taskDeletion tasks.
/// The number of tasks that matched the filter criteria for a
/// `taskCancelation` or `taskDeletion` request. This is determined when
/// the request is received, before any cancellation or deletion occurs.
#[serde(skip_serializing_if = "Option::is_none")]
pub matched_tasks: Option<u64>,
/// Number of tasks canceled for taskCancelation.
/// The number of tasks that were successfully canceled by a
/// `taskCancelation` task. This may be less than `matchedTasks` if some
/// tasks completed before they could be canceled. The inner `null`
/// indicates the task is still processing.
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_tasks: Option<Option<u64>>,
/// Number of tasks deleted for taskDeletion.
/// The number of tasks that were successfully deleted by a `taskDeletion`
/// task. The inner `null` indicates the task is still processing.
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted_tasks: Option<Option<u64>>,
/// Original filter query for taskCancelation or taskDeletion tasks.
/// The original filter query string that was used to select tasks for a
/// `taskCancelation` or `taskDeletion` operation. Useful for
/// understanding which tasks were targeted.
#[serde(skip_serializing_if = "Option::is_none")]
pub original_filter: Option<Option<String>>,
/// Identifier generated for the dump for dumpCreation task.
/// The unique identifier assigned to the dump file created by a
/// `dumpCreation` task. Use this UID to locate the dump file in the
/// dumps directory. The inner `null` indicates the task is still
/// processing or failed before generating a UID.
#[serde(skip_serializing_if = "Option::is_none")]
pub dump_uid: Option<Option<String>>,
/// The context object that was provided to the RHAI function for a
/// `documentEdition` task. This object contains data that the function
/// can access during document processing.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Option<Object>>,
/// The RHAI function code that was executed for a `documentEdition`
/// task. This function is applied to each document matching the filter.
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<String>,
/// [Learn more about the settings in this guide](https://www.meilisearch.com/docs/reference/api/settings).
/// The complete settings object that was applied by a `settingsUpdate`
/// task. Only the settings that were modified are included in this
/// object.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub settings: Option<Box<Settings<Unchecked>>>,
/// The list of index swap operations that were performed by an
/// `indexSwap` task. Each swap specifies two indexes that exchanged
/// their contents.
#[serde(skip_serializing_if = "Option::is_none")]
pub swaps: Option<Vec<IndexSwap>>,
/// The Meilisearch version before a database upgrade was performed.
/// Formatted as `vX.Y.Z`.
#[serde(skip_serializing_if = "Option::is_none")]
pub upgrade_from: Option<String>,
/// The Meilisearch version after a database upgrade was completed.
/// Formatted as `vX.Y.Z`.
#[serde(skip_serializing_if = "Option::is_none")]
pub upgrade_to: Option<String>,
// exporting
/// The destination URL where data is being exported for an `export`
/// task. This is the endpoint that receives the exported index data.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// The API key used for authentication when exporting data to a remote
/// Meilisearch instance. This value is partially masked for security.
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
/// The maximum payload size configured for an `export` task, formatted
/// as a human-readable string (e.g., `100 MB`). This limits the size of
/// each batch of documents sent during export.
#[serde(skip_serializing_if = "Option::is_none")]
pub payload_size: Option<String>,
/// A map of index patterns to their export settings for an `export`
/// task. The keys are index patterns (which may include wildcards) and
/// the values contain the specific export configuration for matching
/// indexes.
#[serde(skip_serializing_if = "Option::is_none")]
pub indexes: Option<BTreeMap<String, DetailsExportIndexSettings>>,
// index rename
/// The original unique identifier of the index before an `indexRename`
/// operation. This is the name the index had before it was renamed.
#[serde(skip_serializing_if = "Option::is_none")]
pub old_index_uid: Option<String>,
/// The new unique identifier assigned to the index after an `indexRename`
/// operation. This is the name the index has after being renamed.
#[serde(skip_serializing_if = "Option::is_none")]
pub new_index_uid: Option<String>,
// index compaction
/// The size of the index before an `indexCompaction` task was performed,
/// formatted as a human-readable string (e.g., `1.5 GB`). Compare with
/// `postCompactionSize` to see how much space was reclaimed.
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_compaction_size: Option<String>,
/// The size of the index after an `indexCompaction` task completed,
/// formatted as a human-readable string (e.g., `1.2 GB`). This should
/// be smaller than or equal to `preCompactionSize`.
#[serde(skip_serializing_if = "Option::is_none")]
pub post_compaction_size: Option<String>,
// network topology change
/// The number of documents that were redistributed during a
/// `networkTopologyChange` task in a distributed deployment. This
/// occurs when the cluster configuration changes.
#[serde(skip_serializing_if = "Option::is_none")]
pub moved_documents: Option<u64>,
/// A human-readable message providing additional information about the
/// task, such as status updates or explanatory text about what occurred
/// during processing.
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
@@ -199,7 +308,8 @@ impl DetailsView {
(None | Some(None), Some(Some(doc))) | (Some(Some(doc)), None | Some(None)) => {
Some(Some(doc.to_string()))
}
// In the case we receive multiple primary keys (which shouldn't happens) we only return the first one encountered.
// In the case we receive multiple primary keys (which shouldn't happens)
// we only return the first one encountered.
(Some(Some(left)), Some(Some(_right))) => Some(Some(left.to_string())),
},
provided_ids: match (self.provided_ids, other.provided_ids) {
@@ -241,8 +351,8 @@ impl DetailsView {
(None, Some(None)) | (Some(None), None) | (Some(None), Some(None)) => Some(None),
(None | Some(None), Some(Some(filter)))
| (Some(Some(filter)), None | Some(None)) => Some(Some(filter.to_string())),
// In this case, we cannot really merge both filters or return an array so we're going to return
// all the conditions one after the other.
// In this case, we cannot really merge both filters or return an array
// so we're going to return all the conditions one after the other.
(Some(Some(left)), Some(Some(right))) => Some(Some(format!("{left}&{right}"))),
},
dump_uid: match (&self.dump_uid, &other.dump_uid) {
@@ -250,8 +360,9 @@ impl DetailsView {
(None, Some(None)) | (Some(None), None) | (Some(None), Some(None)) => Some(None),
(None | Some(None), Some(Some(dump_uid)))
| (Some(Some(dump_uid)), None | Some(None)) => Some(Some(dump_uid.to_string())),
// We should never be able to batch multiple dumps at the same time. So we return
// the first one we encounter but that shouldn't be an issue anyway.
// We should never be able to batch multiple dumps at the same time.
// So we return the first one we encounter but that shouldn't be an
// issue anyway.
(Some(Some(left)), Some(Some(_right))) => Some(Some(left.to_string())),
},
context: match (&self.context, &other.context) {
@@ -260,15 +371,17 @@ impl DetailsView {
(None | Some(None), Some(Some(ctx))) | (Some(Some(ctx)), None | Some(None)) => {
Some(Some(ctx.clone()))
}
// We should never be able to batch multiple documents edited at the same time. So we return
// the first one we encounter but that shouldn't be an issue anyway.
// We should never be able to batch multiple documents edited at the
// same time. So we return the first one we encounter but that
// shouldn't be an issue anyway.
(Some(Some(left)), Some(Some(_right))) => Some(Some(left.clone())),
},
function: match (&self.function, &other.function) {
(None, None) => None,
(None, Some(fun)) | (Some(fun), None) => Some(fun.to_string()),
// We should never be able to batch multiple documents edited at the same time. So we return
// the first one we encounter but that shouldn't be an issue anyway.
// We should never be able to batch multiple documents edited at the
// same time. So we return the first one we encounter but that
// shouldn't be an issue anyway.
(Some(left), Some(_right)) => Some(left.to_string()),
},
settings: match (self.settings.clone(), other.settings.clone()) {
@@ -291,28 +404,32 @@ impl DetailsView {
(None, None) => None,
(None, Some(url)) | (Some(url), None) => Some(url),
// We should never be able to batch multiple exports at the same time.
// So we return the first one we encounter but that shouldn't be an issue anyway.
// So we return the first one we encounter but that shouldn't be an
// issue anyway.
(Some(left), Some(_right)) => Some(left),
},
api_key: match (self.api_key.clone(), other.api_key.clone()) {
(None, None) => None,
(None, Some(key)) | (Some(key), None) => Some(key),
// We should never be able to batch multiple exports at the same time.
// So we return the first one we encounter but that shouldn't be an issue anyway.
// So we return the first one we encounter but that shouldn't be an
// issue anyway.
(Some(left), Some(_right)) => Some(left),
},
payload_size: match (self.payload_size.clone(), other.payload_size.clone()) {
(None, None) => None,
(None, Some(size)) | (Some(size), None) => Some(size),
// We should never be able to batch multiple exports at the same time.
// So we return the first one we encounter but that shouldn't be an issue anyway.
// So we return the first one we encounter but that shouldn't be an
// issue anyway.
(Some(left), Some(_right)) => Some(left),
},
indexes: match (self.indexes.clone(), other.indexes.clone()) {
(None, None) => None,
(None, Some(indexes)) | (Some(indexes), None) => Some(indexes),
// We should never be able to batch multiple exports at the same time.
// So we return the first one we encounter but that shouldn't be an issue anyway.
// So we return the first one we encounter but that shouldn't be an
// issue anyway.
(Some(left), Some(_right)) => Some(left),
},
// We want the earliest version
@@ -345,7 +462,8 @@ impl DetailsView {
) {
(None, None) => None,
(None, Some(size)) | (Some(size), None) => Some(size),
// We should never be able to batch multiple compactions at the same time.
// We should never be able to batch multiple compactions at the
// same time.
(Some(left), Some(_right)) => Some(left),
},
post_compaction_size: match (
@@ -354,7 +472,8 @@ impl DetailsView {
) {
(None, None) => None,
(None, Some(size)) | (Some(size), None) => Some(size),
// We should never be able to batch multiple compactions at the same time.
// We should never be able to batch multiple compactions at the
// same time.
(Some(left), Some(_right)) => Some(left),
},
}

View File

@@ -187,18 +187,24 @@ pub enum KindWithContent {
NetworkTopologyChange(network::NetworkTopologyChange),
}
/// Index swap operation
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct IndexSwap {
/// Pair of index UIDs to swap
pub indexes: (String, String),
/// Whether this is a rename operation
#[serde(default)]
pub rename: bool,
}
/// Export settings for an index
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportIndexSettings {
/// Filter expression to select documents for export
pub filter: Option<Value>,
/// Whether to override settings on the destination index
pub override_settings: bool,
}

View File

@@ -96,11 +96,20 @@ impl From<TaskNetwork> for DbTaskNetwork {
}
}
/// Information about the origin of a task in a distributed Meilisearch
/// deployment. This tracks where a task was originally created before being
/// replicated to other nodes.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Origin {
/// The name of the remote Meilisearch instance where this task originated.
/// This corresponds to a remote defined in the network configuration.
pub remote_name: String,
/// The unique task identifier on the originating remote. This allows
/// tracking the same task across different nodes in the network.
pub task_uid: u32,
/// The version of the network topology when this task was created. Used to
/// ensure consistent task routing during network topology changes.
#[serde(default)]
pub network_version: Uuid,
}
@@ -124,17 +133,27 @@ pub struct ImportMetadata {
pub index_count: u64,
/// Key unique to this (network_change, index, host, key).
///
/// In practice, an internal document id of one of the documents to import.
/// In practice, an internal document id of one of the documents to
/// import.
pub task_key: Option<DocumentId>,
/// Total number of documents to import for this index from this host.
pub total_index_documents: u64,
}
/// Represents a task that was replicated to a remote Meilisearch instance.
/// Contains either the remote task UID on success, or an error if
/// replication failed.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RemoteTask {
/// The unique task identifier assigned by the remote Meilisearch instance.
/// Present when the task was successfully replicated to the remote.
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<u32>)]
task_uid: Option<TaskId>,
/// Error details if the task failed to replicate to this remote. Contains
/// the error message, code, and type from the remote instance.
#[schema(value_type = Option<ResponseError>)]
error: Option<ResponseError>,
}
@@ -149,12 +168,14 @@ impl From<Result<TaskId, ResponseError>> for RemoteTask {
/// Contains the full state of a network topology change.
///
/// A network topology change task is unique in that it can be processed in multiple different batches, as its resolution
/// depends on various document additions tasks being processed.
/// A network topology change task is unique in that it can be processed in
/// multiple different batches, as its resolution depends on various document
/// additions tasks being processed.
///
/// A network topology task has 4 states:
///
/// 1. Processing any task that was meant for an earlier version of the network. This is necessary to know that we have the right version of
/// 1. Processing any task that was meant for an earlier version of the
/// network. This is necessary to know that we have the right version of
/// documents.
/// 2. Sending all documents that must be moved to other remotes.
/// 3. Processing any task coming from the remotes.
@@ -422,9 +443,10 @@ impl InRemote {
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum ImportState {
/// Initially Meilisearch doesn't know how many documents it should expect from a remote.
/// Any task from each remote contains the information of how many indexes will be imported,
/// and the number of documents to import for the index of the task.
/// Initially Meilisearch doesn't know how many documents it should expect
/// from a remote. Any task from each remote contains the information of
/// how many indexes will be imported, and the number of documents to
/// import for the index of the task.
#[default]
WaitingForInitialTask,
Ongoing {

View File

@@ -114,9 +114,14 @@ pub async fn create_api_key(
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct ListApiKeys {
/// Number of API keys to skip in the response. Use together with `limit`
/// for pagination through large sets of keys. For example, to get keys
/// 21-40, set `offset=20` and `limit=20`. Defaults to `0`.
#[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
#[param(value_type = usize, default = 0)]
pub offset: Param<usize>,
/// Maximum number of API keys to return in a single response. Use together
/// with `offset` for pagination. Defaults to `20`.
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
#[param(value_type = usize, default = PAGINATION_DEFAULT_LIMIT_FN)]
pub limit: Param<usize>,
@@ -264,8 +269,9 @@ pub async fn get_api_key(
/// Update a Key
///
/// Update the name and description of an API key.
/// Updates to keys are partial. This means you should provide only the fields you intend to update, as any fields not present in the payload will remain unchanged.
/// Update the name and description of an API key. Updates to keys are partial.
/// This means you should provide only the fields you intend to update, as any
/// fields not present in the payload will remain unchanged.
#[utoipa::path(
patch,
path = "/{uidOrKey}",
@@ -379,29 +385,51 @@ pub struct AuthParam {
key: String,
}
/// Represents an API key used for authenticating requests to Meilisearch.
/// Each key has specific permissions defined by its actions and can be scoped
/// to particular indexes. Keys provide fine-grained access control for your
/// Meilisearch instance.
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub(super) struct KeyView {
/// The name of the API Key if any
/// A human-readable name for the API key. Use this to identify the purpose
/// of each key, such as "Frontend Search Key" or "Admin Key for CI/CD".
/// This is optional and can be `null`.
name: Option<String>,
/// The description of the API Key if any
/// A longer description explaining the purpose or usage of this API key.
/// Useful for documenting why the key was created and how it should be
/// used. This is optional and can be `null`.
description: Option<String>,
/// The actual API Key you can send to Meilisearch
/// The actual API key string to use in the `Authorization: Bearer <key>`
/// header when making requests to Meilisearch. Keep this value secret and
/// never expose it in client-side code.
key: String,
/// The `Uuid` specified while creating the key or autogenerated by Meilisearch.
/// The unique identifier (UUID) for this API key. Use this to update or
/// delete the key. The UID remains constant even if the key's name or
/// description changes.
uid: Uuid,
/// The actions accessible with this key.
/// The list of actions (permissions) this key is allowed to perform.
/// Examples include `documents.add`, `search`, `indexes.create`,
/// `settings.update`, etc. Use `*` to grant all permissions.
actions: Vec<Action>,
/// The indexes accessible with this key.
/// The list of index UIDs this key can access. Use `*` to grant access to
/// all indexes, including future ones. Patterns are also supported, e.g.,
/// `movies_*` matches any index starting with "movies_".
indexes: Vec<String>,
/// The expiration date of the key. Once this timestamp is exceeded the key is not deleted but cannot be used anymore.
/// The expiration date and time of the key in RFC 3339 format. After this
/// time, the key will no longer be valid for authentication. Set to `null`
/// for keys that never expire.
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
expires_at: Option<OffsetDateTime>,
/// The date of creation of this API Key.
/// The date and time when this API key was created, formatted as an
/// RFC 3339 date-time string. This is automatically set by Meilisearch
/// and cannot be modified.
#[schema(read_only)]
#[serde(serialize_with = "time::serde::rfc3339::serialize")]
created_at: OffsetDateTime,
/// The date of the last update made on this key.
/// The date and time when this API key was last modified, formatted as an
/// RFC 3339 date-time string. This is automatically updated by Meilisearch
/// when the key's name or description changes.
#[schema(read_only)]
#[serde(serialize_with = "time::serde::rfc3339::serialize")]
updated_at: OffsetDateTime,

View File

@@ -105,20 +105,28 @@ async fn get_batch(
}
}
/// Response containing a paginated list of batches
#[derive(Debug, Serialize, ToSchema)]
pub struct AllBatches {
/// Array of batch objects
results: Vec<BatchView>,
/// Total number of batches
total: u64,
/// Maximum number of batches returned
limit: u32,
/// The first batch uid returned
from: Option<u32>,
/// Value to send in from to fetch the next slice of results
next: Option<u32>,
}
/// Get batches
///
/// List all batches, regardless of index. The batch objects are contained in the results array.
/// Batches are always returned in descending order of uid. This means that by default, the most recently created batch objects appear first.
/// Batch results are paginated and can be filtered with query parameters.
/// List all batches, regardless of index. The batch objects are contained in
/// the results array. Batches are always returned in descending order of uid.
/// This means that by default, the most recently created batch objects appear
/// first. Batch results are paginated and can be filtered with query
/// parameters.
#[utoipa::path(
get,
path = "",

View File

@@ -30,14 +30,16 @@ mod utils;
/// This function is used to report on what meilisearch is
/// doing which must be used on the frontend to report progress.
const MEILI_SEARCH_PROGRESS_NAME: &str = "_meiliSearchProgress";
/// The function name to append a conversation message in the user conversation.
/// This function is used to append a conversation message in the user conversation.
/// The function name to append a conversation message in the user
/// conversation. This function is used to append a conversation message in
/// the user conversation.
/// This must be used on the frontend to keep context of what happened on the
/// Meilisearch-side and keep good context for follow up questions.
const MEILI_APPEND_CONVERSATION_MESSAGE_NAME: &str = "_meiliAppendConversationMessage";
/// The function name to report sources to the frontend.
/// This function is used to report sources to the frontend.
/// The call id is associated to the one used by the search progress function.
/// The call id is associated to the one used by the search progress
/// function.
const MEILI_SEARCH_SOURCES_NAME: &str = "_meiliSearchSources";
/// The *internal* function name to provide to the LLM to search in indexes.
/// This function must not leak to the user as the LLM will call it and the
@@ -94,7 +96,8 @@ pub async fn delete_chat(
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct ListChats {
/// The number of chat workspaces to skip before starting to retrieve anything
/// The number of chat workspaces to skip before starting to retrieve
/// anything
#[param(value_type = Option<usize>, default, example = 100)]
#[deserr(default, error = DeserrQueryParamError<InvalidIndexOffset>)]
pub offset: Param<usize>,
@@ -110,10 +113,11 @@ impl ListChats {
}
}
/// A chat workspace containing conversation data
#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ChatWorkspaceView {
/// Unique identifier for the index
/// Unique identifier for the chat workspace
pub uid: String,
}

View File

@@ -177,53 +177,67 @@ async fn reset_settings(
}
}
/// Settings for a chat workspace
#[derive(Debug, Clone, Deserialize, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct ChatWorkspaceSettings {
/// LLM provider to use for chat completions
#[serde(default)]
#[deserr(default)]
#[schema(value_type = Option<ChatCompletionSource>)]
pub source: Setting<ChatCompletionSource>,
/// Organization ID for the LLM provider
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionOrgId>)]
#[schema(value_type = Option<String>, example = json!("dcba4321..."))]
pub org_id: Setting<String>,
/// Project ID for the LLM provider
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionProjectId>)]
#[schema(value_type = Option<String>, example = json!("4321dcba..."))]
pub project_id: Setting<String>,
/// API version for the LLM provider
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionApiVersion>)]
#[schema(value_type = Option<String>, example = json!("2024-02-01"))]
pub api_version: Setting<String>,
/// Deployment ID for Azure OpenAI
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionDeploymentId>)]
#[schema(value_type = Option<String>, example = json!("1234abcd..."))]
pub deployment_id: Setting<String>,
/// Base URL for the LLM API
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionBaseApi>)]
#[schema(value_type = Option<String>, example = json!("https://api.mistral.ai/v1"))]
pub base_url: Setting<String>,
/// API key for authentication with the LLM provider
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionApiKey>)]
#[schema(value_type = Option<String>, example = json!("abcd1234..."))]
pub api_key: Setting<String>,
/// Custom prompts for chat completions
#[serde(default)]
#[deserr(default)]
#[schema(inline, value_type = Option<ChatPrompts>)]
pub prompts: Setting<ChatPrompts>,
}
/// LLM provider for chat completions
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub enum ChatCompletionSource {
/// OpenAI API
#[default]
OpenAi,
/// Mistral AI API
Mistral,
/// Azure OpenAI Service
AzureOpenAi,
/// vLLM compatible API
VLlm,
}
@@ -239,23 +253,28 @@ impl From<ChatCompletionSource> for DbChatCompletionSource {
}
}
/// Custom prompts for chat completions
#[derive(Debug, Clone, Deserialize, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct ChatPrompts {
/// System prompt for the LLM
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionSystemPrompt>)]
#[schema(value_type = Option<String>, example = json!("You are a helpful assistant..."))]
pub system: Setting<String>,
/// Description of the search function for the LLM
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionSearchDescriptionPrompt>)]
#[schema(value_type = Option<String>, example = json!("This is the search function..."))]
pub search_description: Setting<String>,
/// Description of the query parameter for search
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionSearchQueryParamPrompt>)]
#[schema(value_type = Option<String>, example = json!("This is query parameter..."))]
pub search_q_param: Setting<String>,
/// Description of the filter parameter for search
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidChatCompletionSearchFilterParamPrompt>)]
#[schema(value_type = Option<String>, example = json!("This is filter parameter..."))]

View File

@@ -109,23 +109,28 @@ async fn export(
Ok(HttpResponse::Ok().json(task))
}
/// Request body for exporting data to a remote Meilisearch instance
#[derive(Debug, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct Export {
/// URL of the destination Meilisearch instance
#[schema(value_type = Option<String>, example = json!("https://ms-1234.heaven.meilisearch.com"))]
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidExportUrl>)]
pub url: String,
/// API key for authenticating with the destination instance
#[schema(value_type = Option<String>, example = json!("1234abcd"))]
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidExportApiKey>)]
pub api_key: Option<String>,
/// Maximum payload size per request
#[schema(value_type = Option<String>, example = json!("24MiB"))]
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidExportPayloadSize>)]
pub payload_size: Option<ByteWithDeserr>,
/// Index patterns to export with their settings
#[schema(value_type = Option<BTreeMap<String, ExportIndexSettings>>, example = json!({ "*": { "filter": null } }))]
#[deserr(default)]
#[serde(default)]
@@ -167,15 +172,18 @@ where
}
}
/// Export settings for a specific index
#[derive(Debug, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct ExportIndexSettings {
/// Filter expression to select which documents to export
#[schema(value_type = Option<String>, example = json!("genres = action"))]
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidExportIndexFilter>)]
pub filter: Option<Value>,
/// Whether to override settings on the destination index
#[schema(value_type = Option<bool>, example = json!(true))]
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidExportIndexOverrideSettings>)]

View File

@@ -38,7 +38,9 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
/// Get all experimental features
///
/// Get a list of all experimental features that can be activated via the /experimental-features route and whether or not they are currently activated.
/// Get a list of all experimental features that can be activated via the
/// /experimental-features route and whether or not they are currently
/// activated.
#[utoipa::path(
get,
path = "",
@@ -81,29 +83,40 @@ async fn get_features(
HttpResponse::Ok().json(features)
}
/// Experimental features that can be toggled at runtime
#[derive(Debug, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct RuntimeTogglableFeatures {
/// Enable the /metrics endpoint for Prometheus metrics
#[deserr(default)]
pub metrics: Option<bool>,
/// Enable the /logs route for log configuration
#[deserr(default)]
pub logs_route: Option<bool>,
/// Enable document editing via JavaScript functions
#[deserr(default)]
pub edit_documents_by_function: Option<bool>,
/// Enable the CONTAINS filter operator
#[deserr(default)]
pub contains_filter: Option<bool>,
/// Enable network features for distributed search
#[deserr(default)]
pub network: Option<bool>,
/// Enable the route to get documents from tasks
#[deserr(default)]
pub get_task_documents_route: Option<bool>,
/// Enable composite embedders for multi-source embeddings
#[deserr(default)]
pub composite_embedders: Option<bool>,
/// Enable chat completion capabilities
#[deserr(default)]
pub chat_completions: Option<bool>,
/// Enable multimodal search with images and other media
#[deserr(default)]
pub multimodal: Option<bool>,
/// Enable vector store settings configuration
#[deserr(default)]
pub vector_store_setting: Option<bool>,
}

View File

@@ -119,10 +119,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
#[into_params(rename_all = "camelCase", parameter_in = Query)]
#[schema(rename_all = "camelCase")]
pub struct GetDocument {
/// Comma-separated list of document attributes to include in the
/// response. Use `*` to retrieve all attributes. By default, all
/// attributes listed in the `displayedAttributes` setting are returned.
/// Example: `title,description,price`.
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentFields>)]
#[param(value_type = Option<Vec<String>>)]
#[schema(value_type = Option<Vec<String>>)]
fields: OptionStarOrList<String>,
/// When `true`, includes the vector embeddings in the response for this
/// document. This is useful when you need to inspect or export vector
/// data. Note that this can significantly increase response size if the
/// document has multiple embedders configured. Defaults to `false`.
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentRetrieveVectors>)]
#[param(value_type = Option<bool>)]
#[schema(value_type = Option<bool>)]
@@ -388,50 +396,102 @@ pub async fn delete_document(
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct BrowseQueryGet {
/// Number of documents to skip in the response. Use this parameter
/// together with `limit` to paginate through large document sets. For
/// example, to get documents 21-40, set `offset=20` and `limit=20`.
/// Defaults to `0`.
#[param(default, value_type = Option<usize>)]
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentOffset>)]
offset: Param<usize>,
/// Maximum number of documents to return in a single response. Use
/// together with `offset` for pagination. Defaults to `20`.
#[param(default, value_type = Option<usize>)]
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidDocumentLimit>)]
limit: Param<usize>,
/// Comma-separated list of document attributes to include in the
/// response. Use `*` to retrieve all attributes. By default, all
/// attributes are returned. Example: `title,description,price`.
#[param(default, value_type = Option<Vec<String>>)]
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentFields>)]
fields: OptionStarOrList<String>,
/// When `true`, includes vector embeddings in the response for documents
/// that have them. This is useful when you need to inspect or export
/// vector data. Defaults to `false`.
#[param(default, value_type = Option<bool>)]
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentRetrieveVectors>)]
retrieve_vectors: Param<bool>,
/// Comma-separated list of document IDs to retrieve. Only documents with
/// matching IDs will be returned. If not specified, all documents
/// matching other criteria are returned.
#[param(default, value_type = Option<Vec<String>>)]
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentIds>)]
ids: Option<CS<String>>,
/// Filter expression to select which documents to return. Uses the same
/// syntax as search filters. Only documents matching the filter will be
/// included in the response. Example: `genres = action AND rating > 4`.
#[param(default, value_type = Option<String>, example = "popularity > 1000")]
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentFilter>)]
filter: Option<String>,
/// Attribute(s) to sort the documents by. Format: `attribute:asc` or
/// `attribute:desc`. Multiple sort criteria can be comma-separated.
/// Example: `price:asc,rating:desc`.
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentSort>)]
sort: Option<String>,
}
/// Request body for browsing and retrieving documents from an index. Use
/// this to fetch documents with optional filtering, sorting, and pagination.
/// This is useful for displaying document lists, exporting data, or
/// inspecting index contents.
#[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct BrowseQuery {
/// Number of documents to skip in the response. Use together with `limit`
/// for pagination through large document sets. For example, to get
/// documents 151-170, set `offset=150` and `limit=20`. Defaults to `0`.
#[schema(default, example = 150)]
#[deserr(default, error = DeserrJsonError<InvalidDocumentOffset>)]
offset: usize,
/// Maximum number of documents to return in a single response. Use
/// together with `offset` for pagination. Higher values return more
/// results but may increase response time and memory usage. Defaults to
/// `20`.
#[schema(default = 20, example = 1)]
#[deserr(default = PAGINATION_DEFAULT_LIMIT, error = DeserrJsonError<InvalidDocumentLimit>)]
limit: usize,
/// Array of document attributes to include in the response. If not
/// specified, all attributes listed in the `displayedAttributes` setting
/// are returned. Use this to reduce response size by only requesting the
/// fields you need. Example: `["title", "description", "price"]`.
#[schema(example = json!(["title, description"]))]
#[deserr(default, error = DeserrJsonError<InvalidDocumentFields>)]
fields: Option<Vec<String>>,
/// When `true`, includes the vector embeddings in the response for
/// documents that have them. This is useful when you need to inspect or
/// export vector data. Note that this can significantly increase response
/// size. Defaults to `false`.
#[schema(default, example = true)]
#[deserr(default, error = DeserrJsonError<InvalidDocumentRetrieveVectors>)]
retrieve_vectors: bool,
/// Array of specific document IDs to retrieve. Only documents with
/// matching primary key values will be returned. If not specified, all
/// documents matching other criteria are returned. This is useful for
/// fetching specific known documents.
#[schema(value_type = Option<Vec<String>>, example = json!(["cody", "finn", "brandy", "gambit"]))]
#[deserr(default, error = DeserrJsonError<InvalidDocumentIds>)]
ids: Option<Vec<serde_json::Value>>,
/// Filter expression to select which documents to return. Uses the same
/// syntax as search filters. Only documents matching the filter will be
/// included in the response. Example: `"genres = action AND rating > 4"`
/// or as an array `[["genres = action"], "rating > 4"]`.
#[schema(default, value_type = Option<Value>, example = "popularity > 1000")]
#[deserr(default, error = DeserrJsonError<InvalidDocumentFilter>)]
filter: Option<Value>,
/// Array of attributes to sort the documents by. Each entry should be in
/// the format `attribute:direction` where direction is either `asc`
/// (ascending) or `desc` (descending). Example: `["price:asc",
/// "rating:desc"]` sorts by price ascending, then by rating descending.
#[schema(default, value_type = Option<Vec<String>>, example = json!(["title:asc", "rating:desc"]))]
#[deserr(default, error = DeserrJsonError<InvalidDocumentSort>)]
sort: Option<Vec<String>>,
@@ -682,8 +742,10 @@ fn documents_by_query(
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(parameter_in = Query, rename_all = "camelCase")]
pub struct UpdateDocumentsQuery {
/// The primary key of the documents. primaryKey is optional. If you want to set the primary key of your index through this route,
/// it only has to be done the first time you add documents to the index. After which it will be ignored if given.
/// The primary key of the documents. primaryKey is optional. If you want
/// to set the primary key of your index through this route, it only has
/// to be done the first time you add documents to the index. After which
/// it will be ignored if given.
#[param(example = "id")]
#[deserr(default, error = DeserrQueryParamError<InvalidIndexPrimaryKey>)]
pub primary_key: Option<String>,
@@ -692,13 +754,20 @@ pub struct UpdateDocumentsQuery {
#[deserr(default, try_from(char) = from_char_csv_delimiter -> DeserrQueryParamError<InvalidDocumentCsvDelimiter>, error = DeserrQueryParamError<InvalidDocumentCsvDelimiter>)]
pub csv_delimiter: Option<u8>,
/// A string that can be used to identify and filter tasks. This metadata
/// is stored with the task and returned in task responses. Useful for
/// tracking tasks from external systems or associating tasks with
/// specific operations in your application.
#[param(example = "custom")]
#[deserr(default, error = DeserrQueryParamError<InvalidIndexCustomMetadata>)]
pub custom_metadata: Option<String>,
/// When set to `true`, only updates existing documents and skips creating
/// new ones. Documents that don't already exist in the index will be
/// ignored. This is useful for partial updates where you only want to
/// modify existing records without adding new ones.
#[param(example = "true")]
#[deserr(default, try_from(&String) = from_string_skip_creation -> DeserrQueryParamError<InvalidSkipCreation>, error = DeserrQueryParamError<InvalidSkipCreation>)]
/// Only update documents if they already exist.
pub skip_creation: Option<bool>,
}
@@ -706,6 +775,10 @@ pub struct UpdateDocumentsQuery {
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(parameter_in = Query, rename_all = "camelCase")]
pub struct CustomMetadataQuery {
/// A string that can be used to identify and filter tasks. This metadata
/// is stored with the task and returned in task responses. Useful for
/// tracking tasks from external systems or associating tasks with
/// specific operations in your application.
#[param(example = "custom")]
#[deserr(default, error = DeserrQueryParamError<InvalidIndexCustomMetadata>)]
pub custom_metadata: Option<String>,
@@ -778,17 +851,22 @@ impl<Method: AggregateMethod> Aggregate for DocumentsAggregator<Method> {
///
/// Add a list of documents or replace them if they already exist.
///
/// If you send an already existing document (same id) the whole existing document will be overwritten by the new document. Fields previously in the document not present in the new document are removed.
/// If you send an already existing document (same id) the whole existing
/// document will be overwritten by the new document. Fields previously in the
/// document not present in the new document are removed.
///
/// For a partial update of the document see Add or update documents route.
/// > info
/// > If the provided index does not exist, it will be created.
/// > info
/// > Use the reserved `_geo` object to add geo coordinates to a document. `_geo` is an object made of `lat` and `lng` field.
/// > Use the reserved `_geo` object to add geo coordinates to a document.
/// > `_geo` is an object made of `lat` and `lng` field.
/// >
/// > When the vectorStore feature is enabled you can use the reserved `_vectors` field in your documents.
/// > It can accept an array of floats, multiple arrays of floats in an outer array or an object.
/// > This object accepts keys corresponding to the different embedders defined your index settings.
/// > When the vectorStore feature is enabled you can use the reserved
/// > `_vectors` field in your documents. It can accept an array of floats,
/// > multiple arrays of floats in an outer array or an object. This object
/// > accepts keys corresponding to the different embedders defined your index
/// > settings.
#[utoipa::path(
post,
path = "{indexUid}/documents",
@@ -883,16 +961,22 @@ pub async fn replace_documents(
/// Add or update documents
///
/// Add a list of documents or update them if they already exist.
/// If you send an already existing document (same id) the old document will be only partially updated according to the fields of the new document. Thus, any fields not present in the new document are kept and remained unchanged.
/// If you send an already existing document (same id) the old document will
/// be only partially updated according to the fields of the new document.
/// Thus, any fields not present in the new document are kept and remained
/// unchanged.
/// To completely overwrite a document, see Add or replace documents route.
/// > info
/// > If the provided index does not exist, it will be created.
/// > info
/// > Use the reserved `_geo` object to add geo coordinates to a document. `_geo` is an object made of `lat` and `lng` field.
/// > Use the reserved `_geo` object to add geo coordinates to a document.
/// > `_geo` is an object made of `lat` and `lng` field.
/// >
/// > When the vectorStore feature is enabled you can use the reserved `_vectors` field in your documents.
/// > It can accept an array of floats, multiple arrays of floats in an outer array or an object.
/// > This object accepts keys corresponding to the different embedders defined your index settings.
/// > When the vectorStore feature is enabled you can use the reserved
/// > `_vectors` field in your documents. It can accept an array of floats,
/// > multiple arrays of floats in an outer array or an object. This object
/// > accepts keys corresponding to the different embedders defined your index
/// > settings.
#[utoipa::path(
put,
path = "{indexUid}/documents",
@@ -1295,10 +1379,12 @@ pub async fn delete_documents_batch(
Ok(HttpResponse::Accepted().json(task))
}
/// Request body for deleting documents by filter
#[derive(Debug, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct DocumentDeletionByFilter {
/// Filter expression to match documents for deletion
#[deserr(error = DeserrJsonError<InvalidDocumentFilter>, missing_field_error = DeserrJsonError::missing_document_filter)]
filter: Value,
}
@@ -1409,16 +1495,17 @@ pub async fn delete_documents_by_filter(
Ok(HttpResponse::Accepted().json(task))
}
/// Request body for editing documents using a JavaScript function
#[derive(Debug, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct DocumentEditionByFunction {
/// A string containing a RHAI function.
/// Filter expression to select which documents to edit
#[deserr(default, error = DeserrJsonError<InvalidDocumentFilter>)]
pub filter: Option<Value>,
/// A string containing a filter expression.
/// Data to make available for the editing function
#[deserr(default, error = DeserrJsonError<InvalidDocumentEditionContext>)]
pub context: Option<Value>,
/// An object with data Meilisearch should make available for the editing function.
/// RHAI function to apply to each document
#[deserr(error = DeserrJsonError<InvalidDocumentEditionFunctionFilter>, missing_field_error = DeserrJsonError::missing_document_edition_function)]
pub function: String,
}
@@ -1453,7 +1540,8 @@ impl Aggregate for EditDocumentsByFunctionAggregator {
/// Edit documents by function.
///
/// Use a [RHAI function](https://rhai.rs/book/engine/hello-world.html) to edit one or more documents directly in Meilisearch.
/// Use a [RHAI function](https://rhai.rs/book/engine/hello-world.html) to
/// edit one or more documents directly in Meilisearch.
#[utoipa::path(
post,
path = "{indexUid}/documents/edit",

View File

@@ -45,31 +45,51 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
// # Important
//
// Intentionally don't use `deny_unknown_fields` to ignore search parameters sent by user
/// Request body for searching facet values
#[derive(Debug, Clone, Default, PartialEq, deserr::Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase)]
pub struct FacetSearchQuery {
/// Query string to search for facet values
#[deserr(default, error = DeserrJsonError<InvalidFacetSearchQuery>)]
pub facet_query: Option<String>,
/// Name of the facet to search
#[deserr(error = DeserrJsonError<InvalidFacetSearchFacetName>, missing_field_error = DeserrJsonError::missing_facet_search_facet_name)]
pub facet_name: String,
/// Query string to filter documents before facet search
#[deserr(default, error = DeserrJsonError<InvalidSearchQ>)]
pub q: Option<String>,
/// Custom query vector for semantic search
#[deserr(default, error = DeserrJsonError<InvalidSearchVector>)]
pub vector: Option<Vec<f32>>,
/// Multimodal content for AI-powered search
#[deserr(default, error = DeserrJsonError<InvalidSearchMedia>)]
pub media: Option<Value>,
/// Hybrid search configuration that combines keyword search with semantic
/// (vector) search. Set `semanticRatio` to balance between keyword
/// matching (0.0) and semantic similarity (1.0). Requires an embedder to
/// be configured in the index settings.
#[deserr(default, error = DeserrJsonError<InvalidSearchHybridQuery>)]
#[schema(value_type = Option<HybridQuery>)]
pub hybrid: Option<HybridQuery>,
/// Filter expression to apply before facet search
#[deserr(default, error = DeserrJsonError<InvalidSearchFilter>)]
pub filter: Option<Value>,
/// Strategy used to match query terms
#[deserr(default, error = DeserrJsonError<InvalidSearchMatchingStrategy>, default)]
pub matching_strategy: MatchingStrategy,
/// Restrict search to specified attributes
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToSearchOn>, default)]
pub attributes_to_search_on: Option<Vec<String>>,
/// Minimum ranking score threshold (0.0 to 1.0) that documents must
/// achieve to be considered when computing facet counts. Documents with
/// scores below this threshold are excluded from facet value counts.
#[deserr(default, error = DeserrJsonError<InvalidSearchRankingScoreThreshold>, default)]
#[schema(value_type = Option<f64>)]
pub ranking_score_threshold: Option<RankingScoreThreshold>,
/// Languages to use for query processing
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>, default)]
pub locales: Option<Vec<Locale>>,
/// Return exhaustive facet count instead of an estimate
#[deserr(default, error = DeserrJsonError<InvalidFacetSearchExhaustiveFacetCount>, default)]
pub exhaustive_facet_count: Option<bool>,
}

View File

@@ -51,7 +51,7 @@ mod similar_analytics;
(path = "/", api = settings::SettingsApi),
(path = "/", api = compact::CompactApi),
),
paths(list_indexes, create_index, get_index, update_index, delete_index, get_index_stats),
paths(list_indexes, create_index, get_index, update_index, delete_index, get_index_stats, compact::compact),
tags(
(
name = "Indexes",
@@ -86,18 +86,19 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
);
}
/// An index containing searchable documents
#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct IndexView {
/// Unique identifier for the index
/// Unique identifier for the index. Once created, it cannot be changed
pub uid: String,
/// An `RFC 3339` format for date/time/duration.
/// Creation date of the index, represented in RFC 3339 format
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
/// An `RFC 3339` format for date/time/duration.
/// Latest date of index update, represented in RFC 3339 format
#[serde(with = "time::serde::rfc3339")]
pub updated_at: OffsetDateTime,
/// Custom primaryKey for documents
/// Primary key of the index
pub primary_key: Option<String>,
}
@@ -193,15 +194,16 @@ pub async fn list_indexes(
Ok(HttpResponse::Ok().json(ret))
}
/// Request body for creating a new index
#[derive(Deserr, Serialize, Debug, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct IndexCreateRequest {
/// The name of the index
/// Unique identifier for the index
#[schema(example = "movies")]
#[deserr(error = DeserrJsonError<InvalidIndexUid>, missing_field_error = DeserrJsonError::missing_index_uid)]
uid: IndexUid,
/// The primary key of the index
/// Primary key of the index
#[schema(example = "id")]
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
primary_key: Option<String>,
@@ -395,14 +397,15 @@ impl Aggregate for IndexUpdatedAggregate {
}
}
/// Request body for updating an existing index
#[derive(Deserr, Serialize, Debug, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)]
#[schema(rename_all = "camelCase")]
pub struct UpdateIndexRequest {
/// The new primary key of the index
/// New primary key of the index
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
primary_key: Option<String>,
/// The new uid of the index (for renaming)
/// New uid for the index (for renaming)
#[deserr(default, error = DeserrJsonError<InvalidIndexUid>)]
uid: Option<String>,
}
@@ -410,7 +413,8 @@ pub struct UpdateIndexRequest {
/// Update index
///
/// Update the `primaryKey` of an index.
/// Return an error if the index doesn't exists yet or if it contains documents.
/// Return an error if the index doesn't exists yet or if it contains
/// documents.
#[utoipa::path(
patch,
path = "/{indexUid}",
@@ -576,7 +580,8 @@ pub struct IndexStats {
/// Number of embedded documents in the index
#[serde(skip_serializing_if = "Option::is_none")]
pub number_of_embedded_documents: Option<u64>,
/// Association of every field name with the number of times it occurs in the documents.
/// Association of every field name with the number of times it occurs in
/// the documents.
#[schema(value_type = HashMap<String, u64>)]
pub field_distribution: FieldDistribution,
}

View File

@@ -59,81 +59,158 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct SearchQueryGet {
/// The search query string. Meilisearch will return documents that match
/// this query. Supports prefix search (words matching the beginning of
/// the query) and typo tolerance. Leave empty to match all documents.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchQ>)]
q: Option<String>,
/// A vector of floating-point numbers for semantic/vector search. The
/// dimensions must match the embedder configuration. When provided,
/// documents are ranked by vector similarity. Can be combined with `q`
/// for hybrid search.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchVector>)]
#[param(value_type = Vec<f32>, explode = false)]
vector: Option<CS<f32>>,
/// Number of search results to skip. Use together with `limit` for
/// pagination. For example, to get results 21-40, set `offset=20` and
/// `limit=20`. Defaults to `0`. Cannot be used with `page`/`hitsPerPage`.
#[deserr(default = Param(DEFAULT_SEARCH_OFFSET()), error = DeserrQueryParamError<InvalidSearchOffset>)]
#[param(value_type = usize, default = DEFAULT_SEARCH_OFFSET)]
offset: Param<usize>,
/// Maximum number of search results to return. Use together with `offset`
/// for pagination. Defaults to `20`. Cannot be used with
/// `page`/`hitsPerPage`.
#[deserr(default = Param(DEFAULT_SEARCH_LIMIT()), error = DeserrQueryParamError<InvalidSearchLimit>)]
#[param(value_type = usize, default = DEFAULT_SEARCH_LIMIT)]
limit: Param<usize>,
/// Request a specific page of results (1-indexed). Use together with
/// `hitsPerPage` for page-based pagination. Cannot be used with
/// `offset`/`limit`.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchPage>)]
#[param(value_type = Option<usize>)]
page: Option<Param<usize>>,
/// Number of results per page when using page-based pagination. Use
/// together with `page`. Cannot be used with `offset`/`limit`.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchHitsPerPage>)]
#[param(value_type = Option<usize>)]
hits_per_page: Option<Param<usize>>,
/// Comma-separated list of attributes to include in the returned
/// documents. Use `*` to return all attributes. By default, returns
/// attributes from the `displayedAttributes` setting.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToRetrieve>)]
#[param(value_type = Vec<String>, explode = false)]
attributes_to_retrieve: Option<CS<String>>,
/// When `true`, includes vector embeddings in the response for documents
/// that have them. Defaults to `false`.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchRetrieveVectors>)]
#[param(value_type = bool, default)]
retrieve_vectors: Param<bool>,
/// Comma-separated list of attributes whose values should be cropped to
/// fit within `cropLength`. Useful for displaying long text attributes
/// in search results. Format: `attribute` or `attribute:length`.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToCrop>)]
#[param(value_type = Vec<String>, explode = false)]
attributes_to_crop: Option<CS<String>>,
/// Maximum number of words to keep when cropping attribute values. The
/// cropped text will be centered around the matching terms. Defaults to
/// `10`.
#[deserr(default = Param(DEFAULT_CROP_LENGTH()), error = DeserrQueryParamError<InvalidSearchCropLength>)]
#[param(value_type = usize, default = DEFAULT_CROP_LENGTH)]
crop_length: Param<usize>,
/// Comma-separated list of attributes whose matching terms should be
/// highlighted with `highlightPreTag` and `highlightPostTag`. Use `*` to
/// highlight all searchable attributes.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToHighlight>)]
#[param(value_type = Vec<String>, explode = false)]
attributes_to_highlight: Option<CS<String>>,
/// Filter expression to narrow down search results. Uses SQL-like syntax.
/// Example: `genres = action AND rating > 4`. Only attributes in
/// `filterableAttributes` can be used.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchFilter>)]
filter: Option<String>,
/// Comma-separated list of attributes to sort by. Format: `attribute:asc`
/// or `attribute:desc`. Only attributes in `sortableAttributes` can be
/// used. Custom ranking rules can also affect sort order.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchSort>)]
sort: Option<String>,
/// Attribute used to ensure only one document with each unique value is
/// returned. Useful for deduplication. Only attributes in
/// `filterableAttributes` can be used.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchDistinct>)]
distinct: Option<String>,
/// When `true`, returns the position (start and length) of each matched
/// term in the original document attributes. Useful for custom
/// highlighting implementations.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchShowMatchesPosition>)]
#[param(value_type = bool)]
show_matches_position: Param<bool>,
/// When `true`, includes a `_rankingScore` field (0.0 to 1.0) in each
/// document indicating how well it matches the query. Higher scores mean
/// better matches.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchShowRankingScore>)]
#[param(value_type = bool)]
show_ranking_score: Param<bool>,
/// When `true`, includes a `_rankingScoreDetails` object showing the
/// contribution of each ranking rule to the final score. Useful for
/// debugging relevancy.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchShowRankingScoreDetails>)]
#[param(value_type = bool)]
show_ranking_score_details: Param<bool>,
/// Comma-separated list of attributes for which to return facet
/// distribution (value counts). Only attributes in `filterableAttributes`
/// can be used. Returns the count of documents matching each facet value.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchFacets>)]
#[param(value_type = Vec<String>, explode = false)]
facets: Option<CS<String>>,
/// HTML tag or string to insert before highlighted matching terms.
/// Defaults to `<em>`.
#[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPreTag>)]
#[param(default = DEFAULT_HIGHLIGHT_PRE_TAG)]
highlight_pre_tag: String,
/// HTML tag or string to insert after highlighted matching terms.
/// Defaults to `</em>`.
#[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPostTag>)]
#[param(default = DEFAULT_HIGHLIGHT_POST_TAG)]
highlight_post_tag: String,
/// String used to indicate truncated content when cropping. Defaults to
/// `…` (ellipsis).
#[deserr(default = DEFAULT_CROP_MARKER(), error = DeserrQueryParamError<InvalidSearchCropMarker>)]
#[param(default = DEFAULT_CROP_MARKER)]
crop_marker: String,
/// Strategy for matching query terms. `last` (default): all terms must
/// match, removing terms from the end if needed. `all`: all terms must
/// match exactly. `frequency`: prioritizes matching frequent terms.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchMatchingStrategy>)]
matching_strategy: MatchingStrategy,
/// Comma-separated list of attributes to search in. By default, searches
/// all `searchableAttributes`. Use this to restrict search to specific
/// fields for better performance or relevance.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToSearchOn>)]
#[param(value_type = Vec<String>, explode = false)]
pub attributes_to_search_on: Option<CS<String>>,
/// Name of the embedder to use for hybrid/semantic search. Must match an
/// embedder configured in the index settings.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchEmbedder>)]
pub hybrid_embedder: Option<String>,
/// Balance between keyword search (0.0) and semantic/vector search (1.0)
/// in hybrid search. A value of 0.5 gives equal weight to both. Defaults
/// to `0.5`.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchSemanticRatio>)]
#[param(value_type = f32)]
pub hybrid_semantic_ratio: Option<SemanticRatioGet>,
/// Minimum ranking score (0.0 to 1.0) a document must have to be
/// included in results. Documents with lower scores are excluded. Useful
/// for filtering out poor matches.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchRankingScoreThreshold>)]
#[param(value_type = f32)]
pub ranking_score_threshold: Option<RankingScoreThresholdGet>,
/// Comma-separated list of language locales to use for tokenization and
/// processing. Useful for multilingual content. Example: `en,fr,de`.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchLocales>)]
#[param(value_type = Vec<Locale>, explode = false)]
pub locales: Option<CS<Locale>>,
/// User-specific context for personalized search results. The format
/// depends on your personalization configuration.
#[deserr(default, error = DeserrQueryParamError<InvalidSearchPersonalizeUserContext>)]
pub personalize_user_context: Option<String>,
}

View File

@@ -252,32 +252,71 @@ async fn similar(
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(parameter_in = Query)]
pub struct SimilarQueryGet {
/// The unique identifier (primary key value) of the target document.
/// Meilisearch will find and return documents that are semantically
/// similar to this document based on their vector embeddings. This is a
/// required parameter.
#[deserr(error = DeserrQueryParamError<InvalidSimilarId>)]
#[param(value_type = String)]
id: Param<String>,
/// Number of similar documents to skip in the response. Use together with
/// `limit` for pagination through large result sets. For example, to get
/// similar documents 21-40, set `offset=20` and `limit=20`. Defaults to
/// `0`.
#[deserr(default = Param(DEFAULT_SEARCH_OFFSET()), error = DeserrQueryParamError<InvalidSimilarOffset>)]
#[param(value_type = usize, default = DEFAULT_SEARCH_OFFSET)]
offset: Param<usize>,
/// Maximum number of similar documents to return in a single response. Use
/// together with `offset` for pagination. Higher values return more
/// results but may increase response time. Defaults to `20`.
#[deserr(default = Param(DEFAULT_SEARCH_LIMIT()), error = DeserrQueryParamError<InvalidSimilarLimit>)]
#[param(value_type = usize, default = DEFAULT_SEARCH_LIMIT)]
limit: Param<usize>,
/// Comma-separated list of document attributes to include in the response.
/// Use `*` to retrieve all attributes. By default, all attributes listed
/// in the `displayedAttributes` setting are returned. Example:
/// `title,description,price`.
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarAttributesToRetrieve>)]
#[param(value_type = Vec<String>)]
attributes_to_retrieve: Option<CS<String>>,
/// When `true`, includes the vector embeddings for each returned document.
/// Useful for debugging or when you need to inspect the vector data. Note
/// that this can significantly increase response size. Defaults to
/// `false`.
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarRetrieveVectors>)]
#[param(value_type = bool, default)]
retrieve_vectors: Param<bool>,
/// Filter expression to narrow down which documents can be returned as
/// similar. Uses the same syntax as search filters. Only documents
/// matching this filter will be considered when finding similar documents.
/// Example: `genres = action AND year > 2000`.
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarFilter>)]
filter: Option<String>,
/// When `true`, includes a global `_rankingScore` field in each document
/// showing how similar it is to the target document. The score is a value
/// between 0 and 1, where higher values indicate greater similarity.
/// Defaults to `false`.
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarShowRankingScore>)]
#[param(value_type = bool, default)]
show_ranking_score: Param<bool>,
/// When `true`, includes a detailed `_rankingScoreDetails` object in each
/// document breaking down how the similarity score was calculated. Useful
/// for debugging and understanding why certain documents are considered
/// more similar. Defaults to `false`.
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarShowRankingScoreDetails>)]
#[param(value_type = bool, default)]
show_ranking_score_details: Param<bool>,
/// Minimum ranking score threshold (between 0.0 and 1.0) that documents
/// must meet to be included in results. Documents with a similarity score
/// below this threshold will be excluded. Useful for ensuring only highly
/// similar documents are returned.
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarRankingScoreThreshold>, default)]
#[param(value_type = Option<f32>)]
pub ranking_score_threshold: Option<RankingScoreThresholdGet>,
/// The name of the embedder to use for finding similar documents. This
/// must match one of the embedders configured in your index settings. The
/// embedder determines how document similarity is calculated based on
/// vector embeddings.
#[deserr(error = DeserrQueryParamError<InvalidSimilarEmbedder>)]
pub embedder: String,
}

View File

@@ -47,24 +47,27 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(web::resource("stderr").route(web::post().to(SeqHandler(update_stderr_target))));
}
/// Format for log output
#[derive(Debug, Default, Clone, Copy, Deserr, Serialize, PartialEq, Eq, ToSchema)]
#[deserr(rename_all = camelCase)]
#[schema(rename_all = "camelCase")]
pub enum LogMode {
/// Output the logs in a human readable form.
/// Output the logs in a human readable form
#[default]
Human,
/// Output the logs in json.
/// Output the logs in JSON format
Json,
/// Output the logs in the firefox profiler format. They can then be loaded and visualized at https://profiler.firefox.com/
/// Output the logs in Firefox profiler format for visualization
Profile,
}
/// Simple wrapper around the `Targets` from `tracing_subscriber` to implement `MergeWithError` on it.
/// Simple wrapper around the `Targets` from `tracing_subscriber` to
/// implement `MergeWithError` on it.
#[derive(Clone, Debug)]
struct MyTargets(Targets);
/// Simple wrapper around the `ParseError` from `tracing_subscriber` to implement `MergeWithError` on it.
/// Simple wrapper around the `ParseError` from `tracing_subscriber` to
/// implement `MergeWithError` on it.
#[derive(Debug, thiserror::Error)]
enum MyParseError {
#[error(transparent)]
@@ -101,24 +104,26 @@ impl MergeWithError<MyParseError> for DeserrJsonError<BadRequest> {
}
}
/// Request body for streaming logs
#[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields, validate = validate_get_logs -> DeserrJsonError<InvalidSettingsTypoTolerance>)]
#[schema(rename_all = "camelCase")]
pub struct GetLogs {
/// Lets you specify which parts of the code you want to inspect and is formatted like that: code_part=log_level,code_part=log_level
/// - If the `code_part` is missing, then the `log_level` will be applied to everything.
/// - If the `log_level` is missing, then the `code_part` will be selected in `info` log level.
/// Log targets to filter. Format: code_part=log_level (e.g.,
/// milli=trace,actix_web=off)
#[deserr(default = "info".parse().unwrap(), try_from(&String) = MyTargets::from_str -> DeserrJsonError<BadRequest>)]
#[schema(value_type = String, default = "info", example = json!("milli=trace,index_scheduler,actix_web=off"))]
target: MyTargets,
/// Lets you customize the format of the logs.
/// Output format for log entries. `human` provides readable text output,
/// `json` provides structured JSON for parsing, and `profile` outputs
/// Firefox profiler format for performance visualization.
#[deserr(default, error = DeserrJsonError<BadRequest>)]
#[schema(default = LogMode::default)]
mode: LogMode,
/// A boolean to indicate if you want to profile the memory as well. This is only useful while using the `profile` mode.
/// Be cautious, though; it slows down the engine a lot.
/// Enable memory profiling (only useful with profile mode, significantly
/// slows down the engine)
#[deserr(default = false, error = DeserrJsonError<BadRequest>)]
#[schema(default = false)]
profile_memory: bool,
@@ -157,7 +162,8 @@ impl Write for LogWriter {
}
struct HandleGuard {
/// We need to keep an handle on the logs to make it available again when the streamer is dropped
/// We need to keep an handle on the logs to make it available again when
/// the streamer is dropped
logs: Arc<LogRouteHandle>,
}
@@ -278,11 +284,14 @@ fn entry_stream(
/// Retrieve logs
///
/// Stream logs over HTTP. The format of the logs depends on the configuration specified in the payload.
/// The logs are sent as multi-part, and the stream never stops, so make sure your clients correctly handle that.
/// To make the server stop sending you logs, you can call the `DELETE /logs/stream` route.
/// Stream logs over HTTP. The format of the logs depends on the
/// configuration specified in the payload. The logs are sent as multi-part,
/// and the stream never stops, so make sure your clients correctly handle
/// that. To make the server stop sending you logs, you can call the `DELETE
/// /logs/stream` route.
///
/// There can only be one listener at a timeand an error will be returned if you call this route while it's being used by another client.
/// There can only be one listener at a timeand an error will be returned if
/// you call this route while it's being used by another client.
#[utoipa::path(
post,
path = "/stream",
@@ -350,7 +359,8 @@ pub async fn get_logs(
/// Stop retrieving logs
///
/// Call this route to make the engine stops sending logs through the `POST /logs/stream` route.
/// Call this route to make the engine stops sending logs through the `POST
/// /logs/stream` route.
#[utoipa::path(
delete,
path = "/stream",
@@ -381,12 +391,12 @@ pub async fn cancel_logs(
Ok(HttpResponse::NoContent().finish())
}
/// Request body for updating stderr log configuration
#[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct UpdateStderrLogs {
/// Lets you specify which parts of the code you want to inspect and is formatted like that: code_part=log_level,code_part=log_level
/// - If the `code_part` is missing, then the `log_level` will be applied to everything.
/// - If the `log_level` is missing, then the `code_part` will be selected in `info` log level.
/// Log targets to filter. Format: code_part=log_level (e.g.,
/// milli=trace,actix_web=off)
#[deserr(default = "info".parse().unwrap(), try_from(&String) = MyTargets::from_str -> DeserrJsonError<BadRequest>)]
#[schema(value_type = String, default = "info", example = json!("milli=trace,index_scheduler,actix_web=off"))]
target: MyTargets,
@@ -394,7 +404,8 @@ pub struct UpdateStderrLogs {
/// Update target of the console logs
///
/// This route lets you specify at runtime the level of the console logs outputted on stderr.
/// This route lets you specify at runtime the level of the console logs
/// outputted on stderr.
#[utoipa::path(
post,
path = "/stderr",

View File

@@ -189,7 +189,8 @@ pub fn is_dry_run(req: &HttpRequest, opt: &Opt) -> Result<bool, ResponseError> {
/// Parse the `Meili-Include-Metadata` header from an HTTP request.
///
/// Returns `true` if the header is present and set to "true" or "1" (case-insensitive).
/// Returns `true` if the header is present and set to "true" or "1"
/// (case-insensitive).
/// Returns `false` if the header is not present or has any other value.
pub fn parse_include_metadata_header(req: &HttpRequest) -> bool {
req.headers()
@@ -199,25 +200,30 @@ pub fn parse_include_metadata_header(req: &HttpRequest) -> bool {
.unwrap_or(false)
}
/// A summarized view of a task, returned when a task is enqueued
#[derive(Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SummarizedTaskView {
/// The task unique identifier.
/// Unique sequential identifier of the task
#[schema(value_type = u32)]
pub task_uid: TaskId,
/// The index affected by this task. May be `null` if the task is not linked to any index.
/// Unique identifier of the targeted index. Null for global tasks
pub index_uid: Option<String>,
/// The status of the task.
/// Status of the task. Possible values are enqueued, processing,
/// succeeded, failed, and canceled
pub status: Status,
/// The type of the task.
/// Type of operation performed by the task
#[serde(rename = "type")]
pub kind: Kind,
/// The date on which the task was enqueued.
/// Date and time when the task was enqueued
#[serde(
serialize_with = "time::serde::rfc3339::serialize",
deserialize_with = "time::serde::rfc3339::deserialize"
)]
pub enqueued_at: OffsetDateTime,
/// Custom metadata string that was attached to this task when it was
/// created. This can be used to associate tasks with external systems or
/// add application-specific information.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_metadata: Option<String>,
}
@@ -240,13 +246,18 @@ pub struct Pagination {
pub limit: usize,
}
/// Paginated response wrapper
#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct PaginationView<T> {
/// Array of items for the current page
pub results: Vec<T>,
/// Number of items skipped
pub offset: usize,
/// Maximum number of items returned
pub limit: usize,
/// Total number of items matching the query
pub total: usize,
}
@@ -264,7 +275,8 @@ impl Pagination {
self.format_with(total, content)
}
/// Given an iterator and the total number of elements, returns the selected section.
/// Given an iterator and the total number of elements, returns the
/// selected section.
pub fn auto_paginate_unsized<T>(
self,
total: usize,
@@ -277,7 +289,8 @@ impl Pagination {
self.format_with(total, content)
}
/// Given the data already paginated + the total number of elements, it stores
/// Given the data already paginated + the total number of elements, it
/// stores
/// everything in a [PaginationResult].
pub fn format_with<T>(self, total: usize, results: Vec<T>) -> PaginationView<T>
where
@@ -398,17 +411,19 @@ pub async fn running() -> HttpResponse {
HttpResponse::Ok().json(serde_json::json!({ "status": "Meilisearch is running" }))
}
/// Global statistics for the Meilisearch instance
#[derive(Serialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Stats {
/// The disk space used by the database, in bytes.
/// Total disk space used by the database in bytes
pub database_size: u64,
/// The size of the database, in bytes.
/// Actual size of the data in the database in bytes
pub used_database_size: u64,
/// The date of the last update in the RFC 3339 formats. Can be `null` if no update has ever been processed.
/// Date of the last update in RFC 3339 format. Null if no update has been
/// processed
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
pub last_update: Option<OffsetDateTime>,
/// The stats of every individual index your API key lets you access.
/// Statistics for each index
#[schema(value_type = HashMap<String, indexes::IndexStats>)]
pub indexes: BTreeMap<String, indexes::IndexStats>,
}
@@ -573,7 +588,8 @@ enum HealthStatus {
/// Get Health
///
/// The health check endpoint enables you to periodically test the health of your Meilisearch instance.
/// The health check endpoint enables you to periodically test the health of
/// your Meilisearch instance.
#[utoipa::path(
get,
path = "/health",

View File

@@ -41,14 +41,17 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("").route(web::post().to(SeqHandler(multi_search_with_post))));
}
/// Response containing results from multiple search queries
#[derive(Serialize, ToSchema)]
pub struct SearchResults {
/// Array of search results for each query
results: Vec<SearchResultWithIndex>,
}
/// Perform a multi-search
///
/// Bundle multiple search queries in a single API request. Use this endpoint to search through multiple indexes at once.
/// Bundle multiple search queries in a single API request. Use this endpoint
/// to search through multiple indexes at once.
#[utoipa::path(
post,
request_body = FederatedSearch,
@@ -358,7 +361,8 @@ pub async fn multi_search_with_post(
/// Local `Result` extension trait to avoid `map_err` boilerplate.
trait WithIndex {
type T;
/// convert the error type inside of the `Result` to a `ResponseError`, and return a couple of it + the usize.
/// convert the error type inside of the `Result` to a `ResponseError`, and
/// return a couple of it + the usize.
fn with_index(self, index: usize) -> Result<Self::T, (ResponseError, usize)>;
}

View File

@@ -94,11 +94,13 @@ async fn get_network(
Ok(HttpResponse::Ok().json(network))
}
/// Configuration for a remote Meilisearch instance
#[derive(Clone, Debug, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError<InvalidNetworkRemotes>, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct Remote {
/// URL of the remote instance
#[schema(value_type = Option<String>, example = json!({
"ms-0": Remote { url: Setting::Set("http://localhost:7700".into()), search_api_key: Setting::Reset, write_api_key: Setting::Reset },
"ms-1": Remote { url: Setting::Set("http://localhost:7701".into()), search_api_key: Setting::Set("foo".into()), write_api_key: Setting::Set("bar".into()) },
@@ -107,21 +109,25 @@ pub struct Remote {
#[deserr(default, error = DeserrJsonError<InvalidNetworkUrl>)]
#[serde(default)]
pub url: Setting<String>,
/// API key for search operations on this remote
#[schema(value_type = Option<String>, example = json!("XWnBI8QHUc-4IlqbKPLUDuhftNq19mQtjc6JvmivzJU"))]
#[deserr(default, error = DeserrJsonError<InvalidNetworkSearchApiKey>)]
#[serde(default)]
pub search_api_key: Setting<String>,
/// API key for write operations on this remote
#[schema(value_type = Option<String>, example = json!("XWnBI8QHUc-4IlqbKPLUDuhftNq19mQtjc6JvmivzJU"))]
#[deserr(default, error = DeserrJsonError<InvalidNetworkWriteApiKey>)]
#[serde(default)]
pub write_api_key: Setting<String>,
}
/// Network topology configuration for distributed Meilisearch
#[derive(Clone, Debug, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct Network {
/// Map of remote instance names to their configurations
#[schema(value_type = Option<BTreeMap<String, Remote>>, example = json!({
"ms-00": {
"url": "http://localhost:7700"
@@ -133,14 +139,17 @@ pub struct Network {
#[deserr(default, error = DeserrJsonError<InvalidNetworkRemotes>)]
#[serde(default)]
pub remotes: Setting<BTreeMap<String, Option<Remote>>>,
/// Name of this instance in the network
#[schema(value_type = Option<String>, example = json!("ms-00"), rename = "self")]
#[serde(default, rename = "self")]
#[deserr(default, rename = "self", error = DeserrJsonError<InvalidNetworkSelf>)]
pub local: Setting<String>,
/// Name of the leader instance in the network
#[schema(value_type = Option<String>, example = json!("ms-00"))]
#[serde(default)]
#[deserr(default, error = DeserrJsonError<InvalidNetworkLeader>)]
pub leader: Setting<String>,
/// Previous remote configurations (for rollback)
#[schema(value_type = Option<BTreeMap<String, Remote>>, example = json!({
"ms-00": {
"url": "http://localhost:7700"

View File

@@ -28,13 +28,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("").route(web::post().to(SeqHandler(swap_indexes))));
}
/// Request body for swapping two indexes
#[derive(Deserr, Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct SwapIndexesPayload {
/// Array of the two indexUids to be swapped
/// Array of the two index UIDs to be swapped
#[deserr(error = DeserrJsonError<InvalidSwapIndexes>, missing_field_error = DeserrJsonError::missing_swap_indexes)]
indexes: Vec<IndexUid>,
/// If set to true, instead of swapping the left and right indexes it'll change the name of the first index to the second
/// If true, rename the first index to the second instead of swapping
#[deserr(default, error = DeserrJsonError<InvalidSwapRename>)]
rename: bool,
}
@@ -64,9 +65,12 @@ impl Aggregate for IndexSwappedAnalytics {
/// Swap indexes
///
/// Swap the documents, settings, and task history of two or more indexes. You can only swap indexes in pairs. However, a single request can swap as many index pairs as you wish.
/// Swapping indexes is an atomic transaction: either all indexes are successfully swapped, or none are.
/// Swapping indexA and indexB will also replace every mention of indexA by indexB and vice-versa in the task history. enqueued tasks are left unmodified.
/// Swap the documents, settings, and task history of two or more indexes.
/// You can only swap indexes in pairs. However, a single request can swap as
/// many index pairs as you wish. Swapping indexes is an atomic transaction:
/// either all indexes are successfully swapped, or none are. Swapping indexA
/// and indexB will also replace every mention of indexA by indexB and
/// vice-versa in the task history. enqueued tasks are left unmodified.
#[utoipa::path(
post,
path = "",

View File

@@ -71,53 +71,74 @@ pub struct TasksFilterQuery {
#[param(required = false, value_type = Option<bool>, example = true)]
pub reverse: Option<Param<bool>>,
/// Permits to filter tasks by their batch uid. By default, when the `batchUids` query parameter is not set, all task uids are returned. It's possible to specify several batch uids by separating them with the `,` character.
/// Permits to filter tasks by their batch uid. By default, when the
/// `batchUids` query parameter is not set, all task uids are returned.
/// It's possible to specify several batch uids by separating them with
/// the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidBatchUids>)]
#[param(required = false, value_type = Option<u32>, example = 12421)]
pub batch_uids: OptionStarOrList<BatchId>,
/// Permits to filter tasks by their uid. By default, when the uids query parameter is not set, all task uids are returned. It's possible to specify several uids by separating them with the `,` character.
/// Permits to filter tasks by their uid. By default, when the uids query
/// parameter is not set, all task uids are returned. It's possible to
/// specify several uids by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
pub uids: OptionStarOrList<u32>,
/// Permits to filter tasks using the uid of the task that canceled them. It's possible to specify several task uids by separating them with the `,` character.
/// Permits to filter tasks using the uid of the task that canceled them.
/// It's possible to specify several task uids by separating them with
/// the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([374, "*"]))]
pub canceled_by: OptionStarOrList<u32>,
/// Permits to filter tasks by their related type. By default, when `types` query parameter is not set, all task types are returned. It's possible to specify several types by separating them with the `,` character.
/// Permits to filter tasks by their related type. By default, when `types`
/// query parameter is not set, all task types are returned. It's possible
/// to specify several types by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)]
#[param(required = false, value_type = Option<Vec<String>>, example = json!([Kind::DocumentAdditionOrUpdate, "*"]))]
pub types: OptionStarOrList<Kind>,
/// Permits to filter tasks by their status. By default, when `statuses` query parameter is not set, all task statuses are returned. It's possible to specify several statuses by separating them with the `,` character.
/// Permits to filter tasks by their status. By default, when `statuses`
/// query parameter is not set, all task statuses are returned. It's
/// possible to specify several statuses by separating them with the `,`
/// character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)]
#[param(required = false, value_type = Option<Vec<Status>>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, Status::Enqueued, Status::Processing, "*"]))]
pub statuses: OptionStarOrList<Status>,
/// Permits to filter tasks by their related index. By default, when `indexUids` query parameter is not set, the tasks of all the indexes are returned. It is possible to specify several indexes by separating them with the `,` character.
/// Permits to filter tasks by their related index. By default, when
/// `indexUids` query parameter is not set, the tasks of all the indexes
/// are returned. It is possible to specify several indexes by separating
/// them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)]
#[param(required = false, value_type = Option<Vec<String>>, example = json!(["movies", "theater", "*"]))]
pub index_uids: OptionStarOrList<IndexUid>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued after the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks
/// enqueued after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued before the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks
/// enqueued before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started after the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their startedAt time. Matches tasks
/// started after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started before the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their startedAt time. Matches tasks
/// started before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished after the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their finishedAt time. Matches tasks
/// finished after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_finished_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished before the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their finishedAt time. Matches tasks
/// finished before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_finished_at: OptionStarOr<OffsetDateTime>,
@@ -171,7 +192,9 @@ impl TaskDeletionOrCancelationQuery {
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct TaskDeletionOrCancelationQuery {
/// Permits to filter tasks by their uid. By default, when the `uids` query parameter is not set, all task uids are returned. It's possible to specify several uids by separating them with the `,` character.
/// Permits to filter tasks by their uid. By default, when the `uids` query
/// parameter is not set, all task uids are returned. It's possible to
/// specify several uids by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
pub uids: OptionStarOrList<u32>,
@@ -179,44 +202,60 @@ pub struct TaskDeletionOrCancelationQuery {
#[deserr(default, error = DeserrQueryParamError<InvalidBatchUids>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
pub batch_uids: OptionStarOrList<BatchId>,
/// Permits to filter tasks using the uid of the task that canceled them. It's possible to specify several task uids by separating them with the `,` character.
/// Permits to filter tasks using the uid of the task that canceled them.
/// It's possible to specify several task uids by separating them with
/// the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([374, "*"]))]
pub canceled_by: OptionStarOrList<u32>,
/// Permits to filter tasks by their related type. By default, when `types` query parameter is not set, all task types are returned. It's possible to specify several types by separating them with the `,` character.
/// Permits to filter tasks by their related type. By default, when `types`
/// query parameter is not set, all task types are returned. It's possible
/// to specify several types by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)]
#[param(required = false, value_type = Option<Vec<Kind>>, example = json!([Kind::DocumentDeletion, "*"]))]
pub types: OptionStarOrList<Kind>,
/// Permits to filter tasks by their status. By default, when `statuses` query parameter is not set, all task statuses are returned. It's possible to specify several statuses by separating them with the `,` character.
/// Permits to filter tasks by their status. By default, when `statuses`
/// query parameter is not set, all task statuses are returned. It's
/// possible to specify several statuses by separating them with the `,`
/// character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)]
#[param(required = false, value_type = Option<Vec<Status>>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, "*"]))]
pub statuses: OptionStarOrList<Status>,
/// Permits to filter tasks by their related index. By default, when `indexUids` query parameter is not set, the tasks of all the indexes are returned. It is possible to specify several indexes by separating them with the `,` character.
/// Permits to filter tasks by their related index. By default, when
/// `indexUids` query parameter is not set, the tasks of all the indexes
/// are returned. It is possible to specify several indexes by separating
/// them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)]
#[param(required = false, value_type = Option<Vec<String>>, example = json!(["movies", "theater", "*"]))]
pub index_uids: OptionStarOrList<IndexUid>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued after the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks
/// enqueued after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued before the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks
/// enqueued before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started after the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their startedAt time. Matches tasks
/// started after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started before the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their startedAt time. Matches tasks
/// started before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished after the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their finishedAt time. Matches tasks
/// finished after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_finished_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished before the given date. Supports RFC 3339 date format.
/// Permits to filter tasks based on their finishedAt time. Matches tasks
/// finished before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_finished_at: OptionStarOr<OffsetDateTime>,
@@ -488,17 +527,18 @@ async fn delete_tasks(
Ok(HttpResponse::Ok().json(task))
}
/// Response containing a paginated list of tasks
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct AllTasks {
/// The list of tasks that matched the filter.
/// Array of task objects matching the query
pub results: Vec<TaskView>,
/// Total number of browsable results using offset/limit parameters for the given resource.
/// Total number of tasks matching the query
pub total: u64,
/// Limit given for the query. If limit is not provided as a query parameter, this parameter displays the default limit value.
/// Maximum number of tasks returned
pub limit: u32,
/// The first task uid returned.
/// The first task uid returned
pub from: Option<u32>,
/// Represents the value to send in from to fetch the next slice of the results. The first item for the next slice starts at this exact number. When the returned value is null, it means that all the data have been browsed in the given order.
/// Value to send in from to fetch the next slice of results. Null when all data has been browsed
pub next: Option<u32>,
}

View File

@@ -56,15 +56,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
);
}
/// Configuration for a webhook endpoint
#[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_webhook)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub(super) struct WebhookSettings {
/// URL endpoint to call when tasks complete
#[schema(value_type = Option<String>, example = "https://your.site/on-tasks-completed")]
#[deserr(default, error = DeserrJsonError<InvalidWebhookUrl>)]
#[serde(default)]
url: Setting<String>,
/// HTTP headers to include in webhook requests
#[schema(value_type = Option<BTreeMap<String, String>>, example = json!({"Authorization":"Bearer a-secret-token"}))]
#[deserr(default, error = DeserrJsonError<InvalidWebhookHeaders>)]
#[serde(default)]
@@ -87,12 +90,16 @@ fn deny_immutable_fields_webhook(
}
}
/// A webhook with metadata and redacted authorization headers
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub(super) struct WebhookWithMetadataRedactedAuthorization {
/// Unique identifier of the webhook
uuid: Uuid,
/// Whether the webhook can be edited
is_editable: bool,
/// Webhook settings
#[schema(value_type = WebhookSettings)]
#[serde(flatten)]
webhook: Webhook,
@@ -105,15 +112,16 @@ impl WebhookWithMetadataRedactedAuthorization {
}
}
/// Response containing a list of all registered webhooks
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub(super) struct WebhookResults {
/// Array of all webhooks configured in this Meilisearch instance. Each
/// webhook includes its UUID, URL, headers (with authorization values
/// redacted), and editability status.
results: Vec<WebhookWithMetadataRedactedAuthorization>,
}
/// List webhooks
///
/// Get the list of all registered webhooks.
#[utoipa::path(
get,
path = "",
@@ -299,9 +307,6 @@ fn check_changed(uuid: Uuid, webhook: &Webhook) -> Result<(), WebhooksError> {
Ok(())
}
/// Get a webhook
///
/// Get a single webhook by its UUID.
#[utoipa::path(
get,
path = "/{uuid}",
@@ -337,9 +342,6 @@ async fn get_webhook(
Ok(HttpResponse::Ok().json(webhook))
}
/// Create a webhook
///
/// Create a new webhook to receive task notifications.
#[utoipa::path(
post,
path = "",
@@ -398,9 +400,6 @@ async fn post_webhook(
Ok(HttpResponse::Created().json(response))
}
/// Update a webhook
///
/// Update an existing webhook's URL or headers.
#[utoipa::path(
patch,
path = "/{uuid}",
@@ -453,9 +452,6 @@ async fn patch_webhook(
Ok(HttpResponse::Ok().json(response))
}
/// Delete a webhook
///
/// Delete an existing webhook by its UUID.
#[utoipa::path(
delete,
path = "/{uuid}",

View File

@@ -32,17 +32,21 @@ pub const WEIGHTED_RANKING_SCORE: &str = "weightedRankingScore";
pub const WEIGHTED_SCORE_VALUES: &str = "weightedScoreValues";
pub const FEDERATION_REMOTE: &str = "remote";
/// Options for federated multi-search queries
#[derive(Debug, Default, Clone, PartialEq, Serialize, deserr::Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
pub struct FederationOptions {
/// Weight to apply to results from this query (default: 1.0)
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchWeight>)]
#[schema(value_type = f64)]
pub weight: Weight,
/// Remote server to send this query to
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchRemote>)]
pub remote: Option<String>,
/// Position of this query in the list of queries
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchQueryPosition>)]
pub query_position: Option<usize>,
}
@@ -77,67 +81,105 @@ impl std::ops::Deref for Weight {
}
}
/// Configuration for federated multi-search
#[derive(Debug, Clone, deserr::Deserr, Serialize, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub struct Federation {
/// Maximum number of results to return across all queries
#[deserr(default = super::super::DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError<InvalidSearchLimit>)]
pub limit: usize,
/// Number of results to skip
#[deserr(default = super::super::DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError<InvalidSearchOffset>)]
pub offset: usize,
/// Facets to retrieve per index
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchFacetsByIndex>)]
pub facets_by_index: BTreeMap<IndexUid, Option<Vec<String>>>,
/// Options for merging facets from multiple indexes
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchMergeFacets>)]
#[schema(value_type = Option<MergeFacets>)]
pub merge_facets: Option<MergeFacets>,
}
/// Options for merging facets from multiple indexes in federated search.
/// When multiple indexes are queried, this controls how their facet values
/// are combined into a single facet distribution.
#[derive(Copy, Clone, Debug, deserr::Deserr, Serialize, Default, ToSchema)]
#[deserr(error = DeserrJsonError<InvalidMultiSearchMergeFacets>, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub struct MergeFacets {
/// The maximum number of facet values to return for each facet after
/// merging. Values from all indexes are combined and sorted before
/// truncation. If not specified, uses the default limit from the index
/// settings.
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchMaxValuesPerFacet>)]
pub max_values_per_facet: Option<usize>,
}
/// Request body for federated multi-search across multiple indexes. This
/// allows you to execute multiple search queries in a single request and
/// optionally combine their results into a unified response. Use this for
/// cross-index search scenarios or to reduce network round-trips.
#[derive(Debug, deserr::Deserr, Serialize, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub struct FederatedSearch {
/// An array of search queries to execute. Each query can target a
/// different index and have its own parameters. When `federation` is
/// `null`, results are returned separately for each query. When
/// `federation` is set, results are merged.
pub queries: Vec<SearchQueryWithIndex>,
/// Configuration for combining results from multiple queries into a
/// single response. When set, results are merged and ranked together.
/// When `null`, each query's results are returned separately in an
/// array.
#[deserr(default)]
#[schema(value_type = Option<Federation>)]
pub federation: Option<Federation>,
}
/// Response from a federated multi-search query
#[derive(Serialize, Deserialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct FederatedSearchResult {
/// Combined search results from all queries
pub hits: Vec<SearchHit>,
/// Total processing time in milliseconds
pub processing_time_ms: u128,
/// Pagination information
#[serde(flatten)]
pub hits_info: HitsInfo,
/// Vector representations used for each query
#[serde(default, skip_serializing_if = "Option::is_none")]
pub query_vectors: Option<BTreeMap<usize, Embedding>>,
/// Number of results from semantic search
#[serde(default, skip_serializing_if = "Option::is_none")]
pub semantic_hit_count: Option<u32>,
/// Merged facet distribution across all indexes
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<BTreeMap<String, BTreeMap<String, u64>>>)]
pub facet_distribution: Option<BTreeMap<String, IndexMap<String, u64>>>,
/// Merged facet statistics across all indexes
#[serde(default, skip_serializing_if = "Option::is_none")]
pub facet_stats: Option<BTreeMap<String, FacetStats>>,
/// Facets grouped by index
#[serde(default, skip_serializing_if = "FederatedFacets::is_empty")]
pub facets_by_index: FederatedFacets,
/// Unique identifier for the request
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_uid: Option<Uuid>,
/// Metadata for each query
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Vec<SearchMetadata>>,
/// Errors from remote servers
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_errors: Option<BTreeMap<String, ResponseError>>,

View File

@@ -59,9 +59,17 @@ pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
pub const DEFAULT_SEMANTIC_RATIO: fn() -> SemanticRatio = || SemanticRatio(0.5);
pub const INCLUDE_METADATA_HEADER: &str = "Meili-Include-Metadata";
/// Configuration for personalized search results.
///
/// When enabled, search results are tailored based on user context,
/// providing different rankings and results for different user profiles.
#[derive(Clone, Default, PartialEq, Deserr, ToSchema, Debug)]
#[deserr(error = DeserrJsonError<InvalidSearchPersonalize>, rename_all = camelCase, deny_unknown_fields)]
pub struct Personalize {
/// A string describing the user context for personalization. This is
/// passed to the embedder to generate user-specific vectors that
/// influence search ranking. Example: user preferences, browsing
/// history, or demographic information.
#[deserr(error = DeserrJsonError<InvalidSearchPersonalizeUserContext>)]
pub user_context: String,
}
@@ -69,67 +77,106 @@ pub struct Personalize {
#[derive(Clone, Default, PartialEq, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct SearchQuery {
/// Query string
#[deserr(default, error = DeserrJsonError<InvalidSearchQ>)]
pub q: Option<String>,
/// Search using a custom query vector
#[deserr(default, error = DeserrJsonError<InvalidSearchVector>)]
pub vector: Option<Vec<f32>>,
/// Perform AI-powered search queries with multimodal content
#[deserr(default, error = DeserrJsonError<InvalidSearchMedia>)]
pub media: Option<serde_json::Value>,
/// Hybrid search configuration combining keyword and semantic search.
/// Set `semanticRatio` to balance between keyword matching (0.0) and
/// semantic similarity (1.0). Requires an embedder to be configured.
#[deserr(default, error = DeserrJsonError<InvalidSearchHybridQuery>)]
#[schema(value_type = Option<HybridQuery>)]
pub hybrid: Option<HybridQuery>,
/// Number of documents to skip
#[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError<InvalidSearchOffset>)]
#[schema(default = DEFAULT_SEARCH_OFFSET)]
pub offset: usize,
/// Maximum number of documents returned
#[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError<InvalidSearchLimit>)]
#[schema(default = DEFAULT_SEARCH_LIMIT)]
pub limit: usize,
/// Request a specific page of results
#[deserr(default, error = DeserrJsonError<InvalidSearchPage>)]
pub page: Option<usize>,
/// Maximum number of documents returned for a page
#[deserr(default, error = DeserrJsonError<InvalidSearchHitsPerPage>)]
pub hits_per_page: Option<usize>,
/// Attributes to display in the returned documents
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToRetrieve>)]
pub attributes_to_retrieve: Option<BTreeSet<String>>,
/// Return document and query vector data
#[deserr(default, error = DeserrJsonError<InvalidSearchRetrieveVectors>)]
pub retrieve_vectors: bool,
/// Attributes whose values have to be cropped
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToCrop>)]
pub attributes_to_crop: Option<Vec<String>>,
/// Maximum length of cropped value in words
#[deserr(error = DeserrJsonError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH())]
#[schema(default = DEFAULT_CROP_LENGTH)]
pub crop_length: usize,
/// Highlight matching terms contained in an attribute
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToHighlight>)]
pub attributes_to_highlight: Option<HashSet<String>>,
/// Return matching terms location
#[deserr(default, error = DeserrJsonError<InvalidSearchShowMatchesPosition>)]
pub show_matches_position: bool,
/// Display the global ranking score of a document
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScore>)]
pub show_ranking_score: bool,
/// Adds a detailed global ranking score field
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScoreDetails>)]
pub show_ranking_score_details: bool,
/// Filter queries by an attribute's value
#[deserr(default, error = DeserrJsonError<InvalidSearchFilter>)]
pub filter: Option<Value>,
/// Sort search results by an attribute's value
#[deserr(default, error = DeserrJsonError<InvalidSearchSort>)]
pub sort: Option<Vec<String>>,
/// Restrict search to documents with unique values of specified
/// attribute
#[deserr(default, error = DeserrJsonError<InvalidSearchDistinct>)]
pub distinct: Option<String>,
/// Display the count of matches per facet
#[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)]
pub facets: Option<Vec<String>>,
/// String inserted at the start of a highlighted term
#[deserr(error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
#[schema(default = DEFAULT_HIGHLIGHT_PRE_TAG)]
pub highlight_pre_tag: String,
/// String inserted at the end of a highlighted term
#[deserr(error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
#[schema(default = DEFAULT_HIGHLIGHT_POST_TAG)]
pub highlight_post_tag: String,
/// String marking crop boundaries
#[deserr(error = DeserrJsonError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())]
#[schema(default = DEFAULT_CROP_MARKER)]
pub crop_marker: String,
/// Strategy used to match query terms within documents
#[deserr(default, error = DeserrJsonError<InvalidSearchMatchingStrategy>)]
pub matching_strategy: MatchingStrategy,
/// Restrict search to the specified attributes
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToSearchOn>)]
pub attributes_to_search_on: Option<Vec<String>>,
/// Minimum ranking score threshold (0.0 to 1.0) that documents must
/// achieve to be included in results. Documents with scores below this
/// threshold are excluded. Useful for filtering out low-relevance
/// results.
#[deserr(default, error = DeserrJsonError<InvalidSearchRankingScoreThreshold>)]
#[schema(value_type = Option<f64>)]
pub ranking_score_threshold: Option<RankingScoreThreshold>,
/// Explicitly specify languages used in a query
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>)]
pub locales: Option<Vec<Locale>>,
/// Enables personalized search results based on user context. When
/// provided, the search uses AI to tailor results to the user's
/// profile, preferences, or behavior described in `userContext`.
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalize>, default)]
#[schema(value_type = Option<Personalize>)]
pub personalize: Option<Personalize>,
}
@@ -357,14 +404,17 @@ impl fmt::Debug for SearchQuery {
}
}
/// Hybrid search configuration for combining keyword and semantic search
#[derive(Debug, Clone, Default, PartialEq, Deserr, ToSchema, Serialize)]
#[deserr(error = DeserrJsonError<InvalidSearchHybridQuery>, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
pub struct HybridQuery {
/// Balance between keyword search (0.0) and semantic search (1.0)
#[deserr(default, error = DeserrJsonError<InvalidSearchSemanticRatio>)]
#[schema(default, value_type = f32)]
#[serde(default)]
pub semantic_ratio: SemanticRatio,
/// Name of the embedder to use for semantic search
#[deserr(error = DeserrJsonError<InvalidSearchEmbedder>)]
pub embedder: String,
}
@@ -502,67 +552,102 @@ impl SearchQuery {
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct SearchQueryWithIndex {
/// Index unique identifier
#[deserr(error = DeserrJsonError<InvalidIndexUid>, missing_field_error = DeserrJsonError::missing_index_uid)]
pub index_uid: IndexUid,
/// Query string
#[deserr(default, error = DeserrJsonError<InvalidSearchQ>)]
pub q: Option<String>,
/// Search using a custom query vector
#[deserr(default, error = DeserrJsonError<InvalidSearchVector>)]
pub vector: Option<Vec<f32>>,
/// Perform AI-powered search queries with multimodal content
#[deserr(default, error = DeserrJsonError<InvalidSearchMedia>)]
pub media: Option<serde_json::Value>,
/// Hybrid search configuration combining keyword and semantic search.
/// Set `semanticRatio` to balance between keyword matching (0.0) and
/// semantic similarity (1.0). Requires an embedder to be configured.
#[deserr(default, error = DeserrJsonError<InvalidSearchHybridQuery>)]
#[schema(value_type = Option<HybridQuery>)]
pub hybrid: Option<HybridQuery>,
/// Number of documents to skip
#[deserr(default, error = DeserrJsonError<InvalidSearchOffset>)]
pub offset: Option<usize>,
/// Maximum number of documents returned
#[deserr(default, error = DeserrJsonError<InvalidSearchLimit>)]
pub limit: Option<usize>,
/// Request a specific page of results
#[deserr(default, error = DeserrJsonError<InvalidSearchPage>)]
pub page: Option<usize>,
/// Maximum number of documents returned for a page
#[deserr(default, error = DeserrJsonError<InvalidSearchHitsPerPage>)]
pub hits_per_page: Option<usize>,
/// Attributes to display in the returned documents
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToRetrieve>)]
pub attributes_to_retrieve: Option<BTreeSet<String>>,
/// Return document and query vector data
#[deserr(default, error = DeserrJsonError<InvalidSearchRetrieveVectors>)]
pub retrieve_vectors: bool,
/// Attributes whose values have to be cropped
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToCrop>)]
pub attributes_to_crop: Option<Vec<String>>,
/// Maximum length of cropped value in words
#[deserr(default, error = DeserrJsonError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH())]
pub crop_length: usize,
/// Highlight matching terms contained in an attribute
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToHighlight>)]
pub attributes_to_highlight: Option<HashSet<String>>,
/// Display the global ranking score of a document
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScore>, default)]
pub show_ranking_score: bool,
/// Adds a detailed global ranking score field
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScoreDetails>, default)]
pub show_ranking_score_details: bool,
/// Return matching terms location
#[deserr(default, error = DeserrJsonError<InvalidSearchShowMatchesPosition>, default)]
pub show_matches_position: bool,
/// Filter queries by an attribute's value
#[deserr(default, error = DeserrJsonError<InvalidSearchFilter>)]
pub filter: Option<Value>,
/// Sort search results by an attribute's value
#[deserr(default, error = DeserrJsonError<InvalidSearchSort>)]
pub sort: Option<Vec<String>>,
/// Restrict search to documents with unique values of specified
/// attribute
#[deserr(default, error = DeserrJsonError<InvalidSearchDistinct>)]
pub distinct: Option<String>,
/// Display the count of matches per facet
#[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)]
pub facets: Option<Vec<String>>,
/// String inserted at the start of a highlighted term
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
pub highlight_pre_tag: String,
/// String inserted at the end of a highlighted term
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
pub highlight_post_tag: String,
/// String marking crop boundaries
#[deserr(default, error = DeserrJsonError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())]
pub crop_marker: String,
/// Strategy used to match query terms within documents
#[deserr(default, error = DeserrJsonError<InvalidSearchMatchingStrategy>, default)]
pub matching_strategy: MatchingStrategy,
/// Restrict search to the specified attributes
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToSearchOn>, default)]
pub attributes_to_search_on: Option<Vec<String>>,
/// Exclude results below the specified ranking score
#[deserr(default, error = DeserrJsonError<InvalidSearchRankingScoreThreshold>, default)]
#[schema(value_type = Option<f64>)]
pub ranking_score_threshold: Option<RankingScoreThreshold>,
/// Languages to use for query tokenization
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>, default)]
pub locales: Option<Vec<Locale>>,
/// Personalize search results
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalize>, default)]
#[serde(skip)]
pub personalize: Option<Personalize>,
/// Federation options for multi-index search
#[deserr(default)]
#[schema(value_type = Option<FederationOptions>)]
pub federation_options: Option<FederationOptions>,
}
@@ -731,28 +816,39 @@ impl SearchQueryWithIndex {
}
}
/// Request body for similar document search
#[derive(Debug, Clone, PartialEq, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct SimilarQuery {
/// Document ID to find similar documents for
#[deserr(error = DeserrJsonError<InvalidSimilarId>)]
#[schema(value_type = String)]
pub id: serde_json::Value,
/// Number of documents to skip
#[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError<InvalidSimilarOffset>)]
pub offset: usize,
/// Maximum number of documents returned
#[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError<InvalidSimilarLimit>)]
pub limit: usize,
/// Filter queries by an attribute's value
#[deserr(default, error = DeserrJsonError<InvalidSimilarFilter>)]
pub filter: Option<Value>,
/// Name of the embedder to use for semantic similarity
#[deserr(error = DeserrJsonError<InvalidSimilarEmbedder>)]
pub embedder: String,
/// Attributes to display in the returned documents
#[deserr(default, error = DeserrJsonError<InvalidSimilarAttributesToRetrieve>)]
pub attributes_to_retrieve: Option<BTreeSet<String>>,
/// Return document vector data
#[deserr(default, error = DeserrJsonError<InvalidSimilarRetrieveVectors>)]
pub retrieve_vectors: bool,
/// Display the global ranking score of a document
#[deserr(default, error = DeserrJsonError<InvalidSimilarShowRankingScore>, default)]
pub show_ranking_score: bool,
/// Adds a detailed global ranking score field
#[deserr(default, error = DeserrJsonError<InvalidSimilarShowRankingScoreDetails>, default)]
pub show_ranking_score_details: bool,
/// Excludes results with low ranking scores
#[deserr(default, error = DeserrJsonError<InvalidSimilarRankingScoreThreshold>, default)]
#[schema(value_type = f64)]
pub ranking_score_threshold: Option<RankingScoreThresholdSimilar>,
@@ -789,6 +885,7 @@ impl TryFrom<Value> for ExternalDocumentId {
}
}
/// Strategy used to match query terms within documents
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Deserr, ToSchema, Serialize)]
#[deserr(rename_all = camelCase)]
#[serde(rename_all = "camelCase")]
@@ -825,11 +922,13 @@ impl From<index::MatchingStrategy> for MatchingStrategy {
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserr)]
#[deserr(rename_all = camelCase)]
pub enum FacetValuesSort {
/// Facet values are sorted in alphabetical order, ascending from A to Z.
/// Facet values are sorted in alphabetical order, ascending from A to
/// Z.
#[default]
Alpha,
/// Facet values are sorted by decreasing count.
/// The count is the number of records containing this facet value in the results of the query.
/// The count is the number of records containing this facet value in
/// the results of the query.
Count,
}
@@ -842,55 +941,79 @@ impl From<FacetValuesSort> for OrderBy {
}
}
/// A single search result hit
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
pub struct SearchHit {
/// The document data
#[serde(flatten)]
#[schema(additional_properties, inline, value_type = HashMap<String, Value>)]
pub document: Document,
/// The formatted document with highlighted and cropped attributes
#[serde(default, rename = "_formatted", skip_serializing_if = "Document::is_empty")]
#[schema(additional_properties, value_type = HashMap<String, Value>)]
pub formatted: Document,
/// Location of matching terms in the document
#[serde(default, rename = "_matchesPosition", skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<BTreeMap<String, Vec<MatchBounds>>>)]
pub matches_position: Option<MatchesPosition>,
/// Global ranking score of the document
#[serde(default, rename = "_rankingScore", skip_serializing_if = "Option::is_none")]
pub ranking_score: Option<f64>,
/// Detailed breakdown of the ranking score
#[serde(default, rename = "_rankingScoreDetails", skip_serializing_if = "Option::is_none")]
pub ranking_score_details: Option<serde_json::Map<String, serde_json::Value>>,
}
/// Metadata about a search query
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct SearchMetadata {
/// Unique identifier for the query
pub query_uid: Uuid,
/// Identifier of the queried index
pub index_uid: String,
/// Primary key of the queried index
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_key: Option<String>,
/// Remote server that processed the query
#[serde(skip_serializing_if = "Option::is_none")]
pub remote: Option<String>,
}
/// Search response containing matching documents and metadata
#[derive(Serialize, Clone, PartialEq, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct SearchResult {
/// Results of the query
pub hits: Vec<SearchHit>,
/// Query originating the response
pub query: String,
/// Vector representation of the query
#[serde(skip_serializing_if = "Option::is_none")]
pub query_vector: Option<Vec<f32>>,
/// Processing time of the query in milliseconds
pub processing_time_ms: u128,
/// Pagination information for the search results
#[serde(flatten)]
pub hits_info: HitsInfo,
/// Distribution of the given facets
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<BTreeMap<String, Value>>)]
pub facet_distribution: Option<BTreeMap<String, IndexMap<String, u64>>>,
/// The numeric min and max values per facet
#[serde(skip_serializing_if = "Option::is_none")]
pub facet_stats: Option<BTreeMap<String, FacetStats>>,
/// A UUID v7 identifying the search request
#[serde(skip_serializing_if = "Option::is_none")]
pub request_uid: Option<Uuid>,
/// Metadata about the search query
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<SearchMetadata>,
/// Exhaustive number of semantic search matches (only present in
/// AI-powered searches)
#[serde(skip_serializing_if = "Option::is_none")]
pub semantic_hit_count: Option<u32>,
@@ -953,39 +1076,69 @@ impl fmt::Debug for SearchResult {
}
}
/// Response containing similar documents
#[derive(Serialize, Debug, Clone, PartialEq, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SimilarResult {
/// Results of the query
pub hits: Vec<SearchHit>,
/// Document ID that was used as reference
pub id: String,
/// Processing time of the query in milliseconds
pub processing_time_ms: u128,
/// Pagination information
#[serde(flatten)]
pub hits_info: HitsInfo,
}
/// Search result with index identifier for multi-search responses
#[derive(Serialize, Debug, Clone, PartialEq, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct SearchResultWithIndex {
/// Identifier of the queried index
pub index_uid: String,
/// Search results for this index
#[serde(flatten)]
pub result: SearchResult,
}
/// Pagination information for search results
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
#[serde(untagged)]
pub enum HitsInfo {
/// Finite pagination with exact counts
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
Pagination { hits_per_page: usize, page: usize, total_pages: usize, total_hits: usize },
Pagination {
/// Number of results on each page
hits_per_page: usize,
/// Current search results page
page: usize,
/// Exhaustive total number of search result pages
total_pages: usize,
/// Exhaustive total number of matches
total_hits: usize,
},
/// Offset-based pagination with estimated counts
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
OffsetLimit { limit: usize, offset: usize, estimated_total_hits: usize },
OffsetLimit {
/// Number of documents to take
limit: usize,
/// Number of documents skipped
offset: usize,
/// Estimated total number of matches
estimated_total_hits: usize,
},
}
/// The numeric min and max values for a facet
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
pub struct FacetStats {
/// Minimum value of the numeric facet
pub min: f64,
/// Maximum value of the numeric facet
pub max: f64,
}
@@ -1319,10 +1472,13 @@ pub fn perform_search(
Ok((result, time_budget))
}
/// Computed facet data from a search
#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
pub struct ComputedFacets {
/// Count of documents for each facet value
#[schema(value_type = BTreeMap<String, BTreeMap<String, u64>>)]
pub distribution: BTreeMap<String, IndexMap<String, u64>>,
/// Numeric statistics for each facet
pub stats: BTreeMap<String, FacetStats>,
}
@@ -1422,11 +1578,14 @@ struct AttributesFormat {
pub enum RetrieveVectors {
/// Remove the `_vectors` field
///
/// this is the behavior when the vectorStore feature is enabled, and `retrieveVectors` is `false`
/// this is the behavior when the vectorStore feature is enabled, and
/// `retrieveVectors` is `false`
Hide,
/// Retrieve vectors from the DB and merge them into the `_vectors` field
/// Retrieve vectors from the DB and merge them into the `_vectors`
/// field
///
/// this is the behavior when the vectorStore feature is enabled, and `retrieveVectors` is `true`
/// this is the behavior when the vectorStore feature is enabled, and
/// `retrieveVectors` is `true`
Retrieve,
}

View File

@@ -106,6 +106,7 @@ enum-iterator = "2.3.0"
bbqueue = { git = "https://github.com/meilisearch/bbqueue" }
flume = { version = "0.11.1", default-features = false }
utoipa = { version = "5.4.0", features = [
"macros",
"non_strict_integers",
"preserve_order",
"uuid",

View File

@@ -4,10 +4,17 @@ use utoipa::ToSchema;
use crate::is_faceted_by;
/// A collection of patterns used to match attribute names. Patterns can
/// include wildcards (`*`) for flexible matching. For example, `title`
/// matches exactly, `overview_*` matches any attribute starting with
/// `overview_`, and `*_date` matches any attribute ending with `_date`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[repr(transparent)]
#[serde(transparent)]
pub struct AttributePatterns {
/// An array of attribute name patterns. Each pattern can be an exact
/// attribute name, or include wildcards (`*`) at the start, end, or
/// both. Examples: `["title", "description_*", "*_date", "*content*"]`.
#[schema(example = json!(["title", "overview_*", "release_date"]))]
pub patterns: Vec<String>,
}
@@ -28,7 +35,8 @@ impl From<Vec<String>> for AttributePatterns {
}
impl AttributePatterns {
/// Match a string against the attribute patterns using the match_pattern function.
/// Match a string against the attribute patterns using the
/// match_pattern function.
pub fn match_str(&self, str: &str) -> PatternMatch {
let mut pattern_match = PatternMatch::NoMatch;
for pattern in &self.patterns {
@@ -84,8 +92,10 @@ pub fn match_pattern(pattern: &str, str: &str) -> PatternMatch {
/// Match a field against a pattern using the legacy behavior.
///
/// A field matches a pattern if it is a parent of the pattern or if it is the pattern itself.
/// This behavior is used to match the sortable attributes, the searchable attributes and the filterable attributes rules `Field`.
/// A field matches a pattern if it is a parent of the pattern or if it is
/// the pattern itself. This behavior is used to match the sortable
/// attributes, the searchable attributes and the filterable attributes
/// rules `Field`.
///
/// # Arguments
///

View File

@@ -28,8 +28,9 @@ impl FilterableAttributesRule {
/// Check if the rule is a geo field.
///
/// prefer using `index.is_geo_enabled`, `index.is_geo_filtering_enabled` or `index.is_geo_sorting_enabled`
/// to check if the geo feature is enabled.
/// prefer using `index.is_geo_enabled`, `index.is_geo_filtering_enabled`
/// or `index.is_geo_sorting_enabled` to check if the geo feature is
/// enabled.
pub fn has_geo(&self) -> bool {
matches!(self, FilterableAttributesRule::Field(field_name) if field_name == RESERVED_GEO_FIELD_NAME)
}
@@ -49,11 +50,20 @@ impl FilterableAttributesRule {
}
}
/// Defines a set of attribute patterns with specific filtering and faceting
/// features. This allows fine-grained control over which operations are
/// allowed on matched attributes.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct FilterableAttributesPatterns {
/// Patterns to match attribute names. Use `*` as a wildcard to match any
/// characters. For example, `["price_*", "stock"]` matches `price_usd`,
/// `price_eur`, and `stock`.
pub attribute_patterns: AttributePatterns,
/// The filtering and faceting features enabled for attributes matching
/// these patterns. If not specified, defaults to equality filtering
/// enabled.
#[serde(default)]
#[deserr(default)]
pub features: FilterableAttributesFeatures,
@@ -69,24 +79,34 @@ impl FilterableAttributesPatterns {
}
}
/// Controls which filtering and faceting operations are enabled for matching
/// attributes. This allows restricting certain operations on specific fields
/// for security or performance reasons.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
#[derive(Default)]
pub struct FilterableAttributesFeatures {
/// When `true`, allows searching within facet values for matching
/// attributes. This enables the facet search feature which lets users
/// search for specific facet values. Defaults to `false`.
#[serde(default)]
#[deserr(default)]
facet_search: bool,
/// Controls which filter operators are allowed for matching attributes.
/// See `FilterFeatures` for available options.
#[serde(default)]
#[deserr(default)]
filter: FilterFeatures,
}
impl FilterableAttributesFeatures {
/// Create a new `FilterableAttributesFeatures` with the legacy default features.
/// Create a new `FilterableAttributesFeatures` with the legacy default
/// features.
///
/// This is the default behavior for `FilterableAttributesRule::Field`.
/// This will set the facet search to true and activate all the filter operators.
/// This will set the facet search to true and activate all the filter
/// operators.
pub fn legacy_default() -> Self {
Self { facet_search: true, filter: FilterFeatures::legacy_default() }
}
@@ -150,13 +170,22 @@ impl<E: DeserializeError> Deserr<E> for FilterableAttributesRule {
}
}
/// Controls which filter operators are allowed for an attribute. This
/// provides fine-grained control over filtering capabilities.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct FilterFeatures {
/// When `true`, enables equality operators: `=`, `!=`, and `IN`. These
/// allow filtering for exact matches or membership in a set of values.
/// Also enables `IS EMPTY`, `IS NULL`, and `EXISTS` operators. Defaults
/// to `true`.
#[serde(default = "default_true")]
#[deserr(default = true)]
equality: bool,
/// When `true`, enables comparison operators: `<`, `>`, `<=`, `>=`, and
/// `TO` (range). These allow filtering based on numeric or string
/// comparisons. Defaults to `false`.
#[serde(default)]
#[deserr(default)]
comparison: bool,
@@ -243,12 +272,15 @@ impl Default for FilterFeatures {
/// Match a field against a set of filterable attributes rules.
///
/// This function will return the set of patterns that match the given filter.
/// This function will return the set of patterns that match the given
/// filter.
///
/// # Arguments
///
/// * `filterable_attributes` - The set of filterable attributes rules to match against.
/// * `filter` - The filter function to apply to the filterable attributes rules.
/// * `filterable_attributes` - The set of filterable attributes rules to
/// match against.
/// * `filter` - The filter function to apply to the filterable attributes
/// rules.
pub fn filtered_matching_patterns<'patterns>(
filterable_attributes: &'patterns [FilterableAttributesRule],
filter: &impl Fn(FilterableAttributesFeatures) -> bool,
@@ -280,11 +312,13 @@ pub fn filtered_matching_patterns<'patterns>(
/// # Arguments
///
/// * `field_name` - The field name to match against.
/// * `filterable_attributes` - The set of filterable attributes rules to match against.
/// * `filterable_attributes` - The set of filterable attributes rules to
/// match against.
///
/// # Returns
///
/// * `Some((rule_index, features))` - The features of the matching rule and the index of the rule in the `filterable_attributes` array.
/// * `Some((rule_index, features))` - The features of the matching rule and
/// the index of the rule in the `filterable_attributes` array.
/// * `None` - No matching rule was found.
pub fn matching_features(
field_name: &str,
@@ -298,7 +332,8 @@ pub fn matching_features(
None
}
/// Match a field against a set of filterable, facet searchable fields, distinct field, sortable fields, and asc_desc fields.
/// Match a field against a set of filterable, facet searchable fields,
/// distinct field, sortable fields, and asc_desc fields.
pub fn match_faceted_field(
field_name: &str,
filterable_fields: &[FilterableAttributesRule],

View File

@@ -121,13 +121,16 @@ fn push_steps_durations(
}
/// This trait lets you use the AtomicSubStep defined right below.
/// The name must be a const that never changed but that can't be enforced by the type system because it make the trait non object-safe.
/// By forcing the Default trait + the &'static str we make it harder to miss-use the trait.
/// The name must be a const that never changed but that can't be enforced
/// by the type system because it make the trait non object-safe. By forcing
/// the Default trait + the &'static str we make it harder to miss-use the
/// trait.
pub trait NamedStep: 'static + Send + Sync + Default {
fn name(&self) -> &'static str;
}
/// Structure to quickly define steps that need very quick, lockless updating of their current step.
/// Structure to quickly define steps that need very quick, lockless
/// updating of their current step.
/// You can use this struct if:
/// - The name of the step doesn't change
/// - The total number of steps doesn't change
@@ -223,25 +226,46 @@ make_enum_progress! {
}
}
/// Real-time progress information for a batch or task that is currently
/// being processed. Use this to display progress bars or status updates to
/// users.
#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct ProgressView {
/// A hierarchical list of processing steps currently being executed.
/// Steps are listed from outermost to innermost, with each step
/// representing a more granular operation within its parent step.
pub steps: Vec<ProgressStepView>,
/// The overall completion percentage of the operation (0.0 to 100.0).
/// This is calculated by combining the progress of all nested steps,
/// weighted by their relative importance.
pub percentage: f32,
}
/// Information about a single processing step within a batch or task. Each
/// step has a name, current progress, and total items to process.
#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct ProgressStepView {
/// A human-readable name describing what this processing step is doing.
/// Examples include "indexing documents", "computing embeddings",
/// "building word cache", etc.
pub current_step: Cow<'static, str>,
/// The number of items that have been processed so far in this step.
/// Compare with `total` to calculate the percentage complete for this
/// specific step.
pub finished: u32,
/// The total number of items to process in this step. When `finished`
/// equals `total`, this step is complete and processing moves to the
/// next step.
pub total: u32,
}
/// Used when the name can change but it's still the same step.
/// To avoid conflicts on the `TypeId`, create a unique type every time you use this step:
/// To avoid conflicts on the `TypeId`, create a unique type every time you
/// use this step:
/// ```text
/// enum UpgradeVersion {}
///

View File

@@ -102,16 +102,27 @@ impl FormatOptions {
}
}
/// Represents the position of a matching term in a document field. Used to
/// indicate where query terms were found within attribute values, enabling
/// features like highlighting and match position display.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
pub struct MatchBounds {
/// The byte offset where the match begins within the attribute value.
/// This is a zero-indexed position from the start of the string.
pub start: usize,
/// The length in bytes of the matched text. Combined with `start`, this
/// defines the exact substring that matched the query term.
pub length: usize,
/// Byte indices of individual matched characters when the match spans
/// multiple positions (e.g., for prefix matches). This is `null` for
/// simple contiguous matches.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub indices: Option<Vec<usize>>,
}
/// Structure used to analyze a string, compute words that match,
/// and format the source string, returning a highlighted and cropped sub-string.
/// and format the source string, returning a highlighted and cropped
/// sub-string.
pub struct Matcher<'t, 'tokenizer, 'b, 'lang> {
text: &'t str,
matching_words: &'b MatchingWords,
@@ -126,8 +137,9 @@ pub struct Matcher<'t, 'tokenizer, 'b, 'lang> {
impl<'t> Matcher<'t, '_, '_, '_> {
/// Iterates over tokens and save any of them that matches the query.
fn compute_matches(&mut self) -> &mut Self {
/// some words are counted as matches only if they are close together and in the good order,
/// compute_partial_match peek into next words to validate if the match is complete.
/// some words are counted as matches only if they are close together
/// and in the good order, compute_partial_match peek into next words
/// to validate if the match is complete.
fn compute_partial_match<'a>(
mut partial: PartialMatch<'a>,
first_token_position: usize,

View File

@@ -10,10 +10,18 @@ use crate::index::{self, ChatConfig, MatchingStrategy, RankingScoreThreshold, Se
use crate::prompt::{default_max_bytes, PromptData};
use crate::update::Setting;
/// Configuration settings for AI-powered chat and search functionality.
///
/// These settings control how documents are presented to the LLM and what
/// search parameters are used when the LLM queries the index.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(error = JsonError, deny_unknown_fields, rename_all = camelCase)]
pub struct ChatSettings {
/// A description of this index that helps the LLM understand its contents
/// and purpose. This description is provided to the LLM to help it decide
/// when and how to query this index.
/// Example: "Contains product catalog with prices and descriptions".
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<String>)]
@@ -28,13 +36,18 @@ pub struct ChatSettings {
#[schema(value_type = Option<String>)]
pub document_template: Setting<String>,
/// Rendered texts are truncated to this size. Defaults to 400.
/// Maximum size in bytes for the rendered document text. Texts longer than
/// this limit are truncated. This prevents very large documents from
/// consuming too much context in the LLM conversation.
/// Defaults to `400` bytes.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<usize>)]
pub document_template_max_bytes: Setting<usize>,
/// The search parameters to use for the LLM.
/// Default search parameters used when the LLM queries this index.
/// These settings control how search results are retrieved and ranked.
/// If not specified, standard search defaults are used.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<ChatSearchParams>)]
@@ -83,54 +96,91 @@ impl From<ChatConfig> for ChatSettings {
}
}
/// Search parameters that control how the LLM queries this index.
///
/// These settings are applied automatically when the chat system
/// performs searches.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(error = JsonError, deny_unknown_fields, rename_all = camelCase)]
pub struct ChatSearchParams {
/// Configuration for hybrid search combining keyword and semantic search.
/// Set the `semanticRatio` to balance between keyword matching (0.0) and
/// semantic similarity (1.0). Requires an embedder to be configured.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<HybridQuery>)]
pub hybrid: Setting<HybridQuery>,
/// Maximum number of documents to return when the LLM queries this index.
/// Higher values provide more context to the LLM but may increase
/// response time and token usage.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<usize>)]
pub limit: Setting<usize>,
/// Sort criteria for ordering search results before presenting to the LLM.
/// Each entry should be in the format `attribute:asc` or `attribute:desc`.
/// Example: `["price:asc", "rating:desc"]`.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<Vec<String>>)]
pub sort: Setting<Vec<String>>,
/// The attribute used for deduplicating results. When set, only one
/// document per unique value of this attribute is returned. Useful for
/// avoiding duplicate content in LLM responses.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<String>)]
pub distinct: Setting<String>,
/// Strategy for matching query terms. `last` (default) matches all words
/// and returns documents matching at least the last word. `all` requires
/// all words to match. `frequency` prioritizes less frequent words.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<MatchingStrategy>)]
pub matching_strategy: Setting<MatchingStrategy>,
/// Restricts the search to only the specified attributes. If not set, all
/// searchable attributes are searched.
/// Example: `["title", "description"]` searches only these two fields.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<Vec<String>>)]
pub attributes_to_search_on: Setting<Vec<String>>,
/// Minimum ranking score (0.0 to 1.0) that documents must achieve to be
/// included in results. Documents below this threshold are excluded.
/// Useful for filtering out low-relevance results.
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
#[schema(value_type = Option<RankingScoreThreshold>)]
pub ranking_score_threshold: Setting<RankingScoreThreshold>,
}
/// Configuration for hybrid search combining keyword and semantic search.
///
/// This allows searches that understand both exact words and conceptual
/// meaning.
#[derive(Debug, Clone, Default, Deserr, ToSchema, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[deserr(error = JsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct HybridQuery {
/// Controls the balance between keyword search and semantic search.
/// A value of `0.0` uses only keyword search, `1.0` uses only semantic
/// search, and `0.5` (the default) gives equal weight to both.
/// Use lower values for exact term matching and higher values for
/// conceptual similarity.
#[deserr(default)]
#[serde(default)]
#[schema(default, value_type = f32)]
pub semantic_ratio: SemanticRatio,
/// The name of the embedder configuration to use for generating query
/// vectors. This must match one of the embedders defined in the index's
/// `embedders` settings.
#[schema(value_type = String)]
pub embedder: String,
}

View File

@@ -10,5 +10,3 @@ serde_json = "1.0"
clap = { version = "4.5.52", features = ["derive"] }
anyhow = "1.0.100"
utoipa = "5.4.0"
reqwest = { version = "0.12", features = ["blocking"] }
regex = "1.10"

View File

@@ -1,54 +1,21 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::{Context, Result};
use anyhow::Result;
use clap::Parser;
use meilisearch::routes::MeilisearchApi;
use serde_json::{json, Value};
use utoipa::OpenApi;
const HTTP_METHODS: &[&str] = &["get", "post", "put", "patch", "delete"];
/// Language used in the documentation repository (contains the key mapping)
const DOCS_LANG: &str = "cURL";
/// Mapping of repository URLs to language names.
/// The "cURL" entry is special: it contains the key mapping used to resolve sample IDs for all SDKs.
const CODE_SAMPLES: &[(&str, &str)] = &[
("https://raw.githubusercontent.com/meilisearch/documentation/refs/heads/main/.code-samples.meilisearch.yaml", "cURL"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-dotnet/refs/heads/main/.code-samples.meilisearch.yaml", "C#"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-dart/refs/heads/main/.code-samples.meilisearch.yaml", "Dart"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-go/refs/heads/main/.code-samples.meilisearch.yaml", "Go"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-java/refs/heads/main/.code-samples.meilisearch.yaml", "Java"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-js/refs/heads/main/.code-samples.meilisearch.yaml", "JS"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-php/refs/heads/main/.code-samples.meilisearch.yaml", "PHP"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-python/refs/heads/main/.code-samples.meilisearch.yaml", "Python"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-ruby/refs/heads/main/.code-samples.meilisearch.yaml", "Ruby"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-rust/refs/heads/main/.code-samples.meilisearch.yaml", "Rust"),
("https://raw.githubusercontent.com/meilisearch/meilisearch-swift/refs/heads/main/.code-samples.meilisearch.yaml", "Swift"),
];
#[derive(Parser)]
#[command(name = "openapi-generator")]
#[command(about = "Generate OpenAPI specification for Meilisearch")]
struct Cli {
/// Output file path (default: meilisearch-openapi.json)
/// Output file path (default: meilisearch.json)
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
/// Pretty print the JSON output
#[arg(short, long)]
pretty: bool,
/// Include Mintlify code samples from SDK repositories
#[arg(long)]
with_mintlify_code_samples: bool,
/// Debug mode: display the mapping table and code samples
#[arg(long)]
debug: bool,
}
fn main() -> Result<()> {
@@ -57,26 +24,14 @@ fn main() -> Result<()> {
// Generate the OpenAPI specification
let openapi = MeilisearchApi::openapi();
// Convert to serde_json::Value for modification
let mut openapi_value: Value = serde_json::to_value(&openapi)?;
// Fetch and add code samples if enabled
if cli.with_mintlify_code_samples {
let code_samples = fetch_all_code_samples(cli.debug)?;
add_code_samples_to_openapi(&mut openapi_value, &code_samples, cli.debug)?;
}
// Clean up null descriptions in tags
clean_null_descriptions(&mut openapi_value);
// Determine output path
let output_path = cli.output.unwrap_or_else(|| PathBuf::from("meilisearch-openapi.json"));
let output_path = cli.output.unwrap_or_else(|| PathBuf::from("meilisearch.json"));
// Serialize to JSON
let json = if cli.pretty {
serde_json::to_string_pretty(&openapi_value)?
serde_json::to_string_pretty(&openapi)?
} else {
serde_json::to_string(&openapi_value)?
serde_json::to_string(&openapi)?
};
// Write to file
@@ -86,608 +41,3 @@ fn main() -> Result<()> {
Ok(())
}
/// Code sample for a specific language
#[derive(Debug, Clone)]
struct CodeSample {
lang: String,
source: String,
}
/// Fetch and parse code samples from all repositories
/// Returns a map from OpenAPI key (e.g., "get_indexes") to a list of code samples for different languages
fn fetch_all_code_samples(debug: bool) -> Result<HashMap<String, Vec<CodeSample>>> {
// First, fetch the documentation file to get the OpenAPI key -> code sample ID mapping
let (docs_url, _) = CODE_SAMPLES
.iter()
.find(|(_, lang)| *lang == DOCS_LANG)
.context("Documentation source not found in CODE_SAMPLES")?;
let docs_content = reqwest::blocking::get(*docs_url)
.context("Failed to fetch documentation code samples")?
.text()
.context("Failed to read documentation code samples response")?;
// Build mapping from OpenAPI key to code sample ID (only first match per key)
let openapi_key_to_sample_id = build_openapi_key_mapping(&docs_content);
// Build final result
let mut all_samples: HashMap<String, Vec<CodeSample>> = HashMap::new();
// Loop through all CODE_SAMPLES files
for (url, lang) in CODE_SAMPLES {
// Fetch content (reuse docs_content for documentation)
let content: Cow<'_, str> = if *lang == DOCS_LANG {
Cow::Borrowed(&docs_content)
} else {
match reqwest::blocking::get(*url).and_then(|r| r.text()) {
Ok(text) => Cow::Owned(text),
Err(e) => {
eprintln!("Warning: Failed to fetch code samples for {}: {}", lang, e);
continue;
}
}
};
// Parse all code samples from this file
let sample_id_to_code = parse_code_samples_from_file(&content);
// Add to result using the mapping
for (openapi_key, sample_id) in &openapi_key_to_sample_id {
if let Some(source) = sample_id_to_code.get(sample_id) {
all_samples.entry(openapi_key.clone()).or_default().push(CodeSample {
lang: lang.to_string(),
source: source.clone(),
});
}
}
}
// Debug mode: display mapping table and code samples
if debug {
println!("\n=== OpenAPI Key to Sample ID Mapping ===\n");
let mut keys: Vec<_> = openapi_key_to_sample_id.keys().collect();
keys.sort();
for key in keys {
println!(" {} -> {}", key, openapi_key_to_sample_id[key]);
}
println!("\n=== Code Samples ===\n");
let mut sample_keys: Vec<_> = all_samples.keys().collect();
sample_keys.sort();
for key in sample_keys {
let samples = &all_samples[key];
let langs: Vec<_> = samples.iter().map(|s| s.lang.as_str()).collect();
println!(" {} -> {}", key, langs.join(", "));
}
println!();
}
Ok(all_samples)
}
/// Build a mapping from OpenAPI key to code sample ID from the documentation file.
///
/// The OpenAPI key is found on a line starting with `# ` (hash + space), containing a single word
/// that starts with an HTTP method followed by an underscore (e.g., `# get_indexes`).
/// The code sample ID is the first word of the next line.
/// Only keeps the first code sample ID per OpenAPI key.
///
/// Example input:
/// ```yaml
/// # get_indexes
/// get_indexes_1: |-
/// curl \
/// -X GET 'MEILISEARCH_URL/indexes'
/// get_indexes_2: |-
/// curl \
/// -X GET 'MEILISEARCH_URL/indexes?limit=5'
/// # post_indexes
/// create_indexes_1: |-
/// curl \
/// -X POST 'MEILISEARCH_URL/indexes'
/// ```
///
/// This produces: {"get_indexes": "get_indexes_1", "post_indexes": "create_indexes_1"}
fn build_openapi_key_mapping(content: &str) -> HashMap<String, String> {
let mut mapping: HashMap<String, String> = HashMap::new();
let lines: Vec<&str> = content.lines().collect();
for i in 0..lines.len() {
let line = lines[i];
// Check if line starts with "# " and contains exactly one word
let Some(rest) = line.strip_prefix("# ") else {
continue;
};
let word = rest.trim();
// Must be a single word (no spaces)
if word.contains(' ') {
continue;
}
// Must start with an HTTP method followed by underscore
let starts_with_http_method =
HTTP_METHODS.iter().any(|method| word.starts_with(&format!("{}_", method)));
if !starts_with_http_method {
continue;
}
let openapi_key = word.to_string();
// Only keep first match per key
if mapping.contains_key(&openapi_key) {
continue;
}
// Get the code sample ID from the next line (first word before `:`)
if i + 1 < lines.len() {
let next_line = lines[i + 1];
if let Some(sample_id) = next_line.split(':').next() {
let sample_id = sample_id.trim();
if !sample_id.is_empty() {
mapping.insert(openapi_key, sample_id.to_string());
}
}
}
}
mapping
}
/// Parse all code samples from a file.
///
/// A code sample ID is found when a line contains `: |-`.
/// The code sample value is everything between `: |-` and:
/// - The next code sample (next line containing `: |-`)
/// - OR a line starting with `#` at column 0 (indented `#` is part of the code sample)
/// - OR the end of file
///
/// Example input:
/// ```yaml
/// get_indexes_1: |-
/// client.getIndexes()
/// # I write something
/// # COMMENT TO IGNORE
/// get_indexes_2: |-
/// client.getIndexes({ limit: 3 })
/// ```
///
/// This produces:
/// - get_indexes_1 -> "client.getIndexes()\n# I write something"
/// - get_indexes_2 -> "client.getIndexes({ limit: 3 })"
fn parse_code_samples_from_file(content: &str) -> HashMap<String, String> {
let mut samples: HashMap<String, String> = HashMap::new();
let mut current_sample_id: Option<String> = None;
let mut current_lines: Vec<String> = Vec::new();
let mut base_indent: Option<usize> = None;
for line in content.lines() {
// Check if this line starts a new code sample (contains `: |-`)
if line.contains(": |-") {
// Save previous sample if exists
if let Some(sample_id) = current_sample_id.take() {
let value = current_lines.join("\n").trim_end().to_string();
samples.insert(sample_id, value);
}
current_lines.clear();
base_indent = None;
// Extract sample ID (first word before `:`)
if let Some(id) = line.split(':').next() {
current_sample_id = Some(id.trim().to_string());
}
continue;
}
// Check if this line ends the current code sample (line starts with `#` at column 0)
// Indented `#` (spaces or tabs) is part of the code sample
if line.starts_with('#') {
// Save current sample and reset
if let Some(sample_id) = current_sample_id.take() {
let value = current_lines.join("\n").trim_end().to_string();
samples.insert(sample_id, value);
}
current_lines.clear();
base_indent = None;
continue;
}
// If we're in a code sample, add this line to the value
if current_sample_id.is_some() {
// Handle empty lines
if line.trim().is_empty() {
if !current_lines.is_empty() {
current_lines.push(String::new());
}
continue;
}
// Calculate indentation and strip base indent
let indent = line.len() - line.trim_start().len();
let base = *base_indent.get_or_insert(indent);
// Remove base indentation
let dedented = line.get(base..).unwrap_or_else(|| line.trim_start());
current_lines.push(dedented.to_string());
}
}
// Don't forget the last sample
if let Some(sample_id) = current_sample_id {
let value = current_lines.join("\n").trim_end().to_string();
samples.insert(sample_id, value);
}
samples
}
/// Convert an OpenAPI path to a code sample key
/// Path: /indexes/{index_uid}/documents/{document_id}
/// Method: GET
/// Key: get_indexes_indexUid_documents_documentId
fn path_to_key(path: &str, method: &str) -> String {
let method_lower = method.to_lowercase();
// Remove leading slash and convert path
let path_part = path
.trim_start_matches('/')
.split('/')
.map(|segment| {
if segment.starts_with('{') && segment.ends_with('}') {
// Convert {param_name} to camelCase
let param = &segment[1..segment.len() - 1];
to_camel_case(param)
} else {
// Keep path segments as-is, but replace hyphens with underscores
segment.replace('-', "_")
}
})
.collect::<Vec<_>>()
.join("_");
if path_part.is_empty() {
method_lower
} else {
format!("{}_{}", method_lower, path_part)
}
}
/// Convert snake_case to camelCase
fn to_camel_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = false;
for (i, c) in s.chars().enumerate() {
match c {
'_' => capitalize_next = true,
_ if capitalize_next => {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
}
_ if i == 0 => result.push(c.to_ascii_lowercase()),
_ => result.push(c),
}
}
result
}
/// Add code samples to the OpenAPI specification
fn add_code_samples_to_openapi(
openapi: &mut Value,
code_samples: &HashMap<String, Vec<CodeSample>>,
debug: bool,
) -> Result<()> {
let paths = openapi
.get_mut("paths")
.and_then(|p| p.as_object_mut())
.context("OpenAPI spec missing 'paths' object")?;
let mut routes_with_samples: Vec<String> = Vec::new();
let mut routes_without_samples: Vec<String> = Vec::new();
// Collect all routes first for sorted debug output
let mut all_routes: Vec<(String, String, String)> = Vec::new(); // (path, method, key)
for (path, path_item) in paths.iter_mut() {
let Some(path_item) = path_item.as_object_mut() else {
continue;
};
for method in HTTP_METHODS {
let Some(operation) = path_item.get_mut(*method) else {
continue;
};
let key = path_to_key(path, method);
all_routes.push((path.clone(), method.to_string(), key.clone()));
if let Some(samples) = code_samples.get(&key) {
routes_with_samples.push(key);
// Create x-codeSamples array according to Redocly spec
// Sort by language name for consistent output
let mut sorted_samples = samples.clone();
sorted_samples.sort_by(|a, b| a.lang.cmp(&b.lang));
let code_sample_array: Vec<Value> = sorted_samples
.iter()
.map(|sample| {
json!({
"lang": sample.lang,
"source": sample.source
})
})
.collect();
if let Some(op) = operation.as_object_mut() {
op.insert("x-codeSamples".to_string(), json!(code_sample_array));
}
} else {
routes_without_samples.push(key);
}
}
}
// Debug output
if debug {
routes_without_samples.sort();
if !routes_without_samples.is_empty() {
println!("=== Routes without code samples ===\n");
for key in &routes_without_samples {
println!(" {}", key);
}
}
let total = all_routes.len();
let with_samples = routes_with_samples.len();
let without_samples = routes_without_samples.len();
let percentage = if total > 0 { (with_samples as f64 / total as f64) * 100.0 } else { 0.0 };
println!("\n=== Summary ===\n");
println!(" Total routes: {}", total);
println!(" With code samples: {} ({:.1}%)", with_samples, percentage);
println!(" Missing code samples: {} ({:.1}%)\n", without_samples, 100.0 - percentage);
}
Ok(())
}
/// Clean up null descriptions in tags to make Mintlify work
/// Removes any "description" fields with null values (both JSON null and "null" string)
/// from the tags array and all nested objects
fn clean_null_descriptions(openapi: &mut Value) {
if let Some(tags) = openapi.get_mut("tags").and_then(|t| t.as_array_mut()) {
for tag in tags.iter_mut() {
remove_null_descriptions_recursive(tag);
}
}
}
/// Recursively remove all "description" fields that are null or "null" string
fn remove_null_descriptions_recursive(value: &mut Value) {
if let Some(obj) = value.as_object_mut() {
// Check and remove description if it's null or "null" string
if let Some(desc) = obj.get("description") {
if desc.is_null() || (desc.is_string() && desc.as_str() == Some("null")) {
obj.remove("description");
}
}
// Recursively process all nested objects
for (_, v) in obj.iter_mut() {
remove_null_descriptions_recursive(v);
}
} else if let Some(arr) = value.as_array_mut() {
// Recursively process arrays
for item in arr.iter_mut() {
remove_null_descriptions_recursive(item);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_to_key() {
assert_eq!(path_to_key("/indexes", "GET"), "get_indexes");
assert_eq!(path_to_key("/indexes/{index_uid}", "GET"), "get_indexes_indexUid");
assert_eq!(
path_to_key("/indexes/{index_uid}/documents", "POST"),
"post_indexes_indexUid_documents"
);
assert_eq!(
path_to_key("/indexes/{index_uid}/documents/{document_id}", "GET"),
"get_indexes_indexUid_documents_documentId"
);
assert_eq!(
path_to_key("/indexes/{index_uid}/settings/stop-words", "GET"),
"get_indexes_indexUid_settings_stop_words"
);
}
#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("index_uid"), "indexUid");
assert_eq!(to_camel_case("document_id"), "documentId");
assert_eq!(to_camel_case("task_uid"), "taskUid");
}
#[test]
fn test_build_openapi_key_mapping() {
let yaml = r#"
# get_indexes
get_indexes_1: |-
curl \
-X GET 'MEILISEARCH_URL/indexes'
get_indexes_2: |-
curl \
-X GET 'MEILISEARCH_URL/indexes?limit=5'
# post_indexes
create_indexes_1: |-
curl \
-X POST 'MEILISEARCH_URL/indexes'
# get_version
get_version_1: |-
curl \
-X GET 'MEILISEARCH_URL/version'
# COMMENT WITHOUT KEY - SHOULD BE IGNORED
## COMMENT WITHOUT KEY - SHOULD BE IGNORED
unrelated_sample_without_comment: |-
curl \
-X GET 'MEILISEARCH_URL/something'
"#;
let mapping = build_openapi_key_mapping(yaml);
// Should have 3 OpenAPI keys
assert_eq!(mapping.len(), 3);
assert!(mapping.contains_key("get_indexes"));
assert!(mapping.contains_key("post_indexes"));
assert!(mapping.contains_key("get_version"));
// Only keeps the first code sample ID per OpenAPI key
assert_eq!(mapping["get_indexes"], "get_indexes_1");
assert_eq!(mapping["post_indexes"], "create_indexes_1");
assert_eq!(mapping["get_version"], "get_version_1");
// Comments with multiple words or ## should be ignored and not create keys
assert!(!mapping.contains_key("COMMENT"));
assert!(!mapping.contains_key("##"));
}
#[test]
fn test_parse_code_samples_from_file() {
let yaml = r#"
get_indexes_1: |-
client.getIndexes()
# I write something
# COMMENT TO IGNORE
get_indexes_2: |-
client.getIndexes({ limit: 3 })
update_document: |-
// Code with blank line
updateDoc(doc)
// End
delete_document_1: |-
client.deleteDocument(1)
no_newline_at_end: |-
client.update({ id: 1 })
key_with_empty_sample: |-
# This should produce an empty string for the sample
complex_block: |-
// Some code
Indented line
# Indented comment
Last line
"#;
let samples = parse_code_samples_from_file(yaml);
assert_eq!(samples.len(), 7);
assert!(samples.contains_key("get_indexes_1"));
assert!(samples.contains_key("get_indexes_2"));
assert!(samples.contains_key("update_document"));
assert!(samples.contains_key("delete_document_1"));
assert!(samples.contains_key("no_newline_at_end"));
assert!(samples.contains_key("key_with_empty_sample"));
assert!(samples.contains_key("complex_block"));
// get_indexes_1 includes indented comment
assert_eq!(samples["get_indexes_1"], "client.getIndexes()\n# I write something");
// get_indexes_2 is a single line
assert_eq!(samples["get_indexes_2"], "client.getIndexes({ limit: 3 })");
// update_document contains a blank line and some code
assert_eq!(
samples["update_document"],
"// Code with blank line\n\nupdateDoc(doc)\n// End"
);
// delete_document_1
assert_eq!(samples["delete_document_1"], "client.deleteDocument(1)");
// no_newline_at_end, explicitly just one line
assert_eq!(samples["no_newline_at_end"], "client.update({ id: 1 })");
// key_with_empty_sample should be empty string
assert_eq!(samples["key_with_empty_sample"], "");
// complex_block preserves indentation and comments
assert_eq!(
samples["complex_block"],
"// Some code\n Indented line\n # Indented comment\nLast line"
);
}
#[test]
fn test_clean_null_descriptions() {
let mut openapi = json!({
"tags": [
{
"name": "Test1",
"description": "null"
},
{
"name": "Test2",
"description": null
},
{
"name": "Test3",
"description": "Valid description"
},
{
"name": "Test4",
"description": "null",
"externalDocs": {
"url": "https://example.com",
"description": null
}
},
{
"name": "Test5",
"externalDocs": {
"url": "https://example.com",
"description": "null"
}
}
]
});
clean_null_descriptions(&mut openapi);
let tags = openapi["tags"].as_array().unwrap();
// Test1: description "null" should be removed
assert!(!tags[0].as_object().unwrap().contains_key("description"));
// Test2: description null should be removed
assert!(!tags[1].as_object().unwrap().contains_key("description"));
// Test3: valid description should remain
assert_eq!(tags[2]["description"], "Valid description");
// Test4: both tag description and externalDocs description should be removed
assert!(!tags[3].as_object().unwrap().contains_key("description"));
assert!(!tags[3]["externalDocs"]
.as_object()
.unwrap()
.contains_key("description"));
assert_eq!(tags[3]["externalDocs"]["url"], "https://example.com");
// Test5: externalDocs description "null" should be removed
assert!(!tags[4]["externalDocs"]
.as_object()
.unwrap()
.contains_key("description"));
assert_eq!(tags[4]["externalDocs"]["url"], "https://example.com");
}
}