├── .code-samples.meilisearch.yaml
├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── dependabot.yml
├── release-draft-template.yml
├── scripts
│ └── check-release.sh
└── workflows
│ ├── pre-release-tests.yml
│ ├── publish.yml
│ ├── release-drafter.yml
│ └── tests.yml
├── .gitignore
├── .yamllint.yml
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── README.md
├── README.tpl
├── bors.toml
├── docker-compose.yml
├── examples
├── cli-app-with-awc
│ ├── Cargo.toml
│ ├── assets
│ │ └── clothes.json
│ └── src
│ │ └── main.rs
├── cli-app
│ ├── Cargo.toml
│ ├── assets
│ │ └── clothes.json
│ └── src
│ │ └── main.rs
├── settings.rs
├── web_app
│ ├── Cargo.toml
│ ├── README.md
│ ├── pkg
│ │ └── index.html
│ └── src
│ │ ├── document.rs
│ │ └── lib.rs
└── web_app_graphql
│ ├── .env.example.txt
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── README.md
│ ├── diesel.toml
│ ├── migrations
│ ├── .keep
│ ├── 00000000000000_diesel_initial_setup
│ │ ├── down.sql
│ │ └── up.sql
│ └── 2023-12-20-105441_users
│ │ ├── down.sql
│ │ └── up.sql
│ └── src
│ ├── app_env_vars.rs
│ ├── errors
│ └── mod.rs
│ ├── graphql_schema
│ ├── mod.rs
│ └── users
│ │ ├── mod.rs
│ │ ├── mutation
│ │ ├── add_user.rs
│ │ └── mod.rs
│ │ └── query
│ │ ├── get_users.rs
│ │ ├── mod.rs
│ │ └── search.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── models.rs
│ └── schema.rs
├── meilisearch-index-setting-macro
├── Cargo.toml
└── src
│ └── lib.rs
├── meilisearch-test-macro
├── Cargo.toml
├── README.md
└── src
│ └── lib.rs
├── scripts
├── check-readme.sh
├── update-readme.sh
└── update_macro_versions.sh
└── src
├── client.rs
├── documents.rs
├── dumps.rs
├── errors.rs
├── features.rs
├── indexes.rs
├── key.rs
├── lib.rs
├── request.rs
├── reqwest.rs
├── search.rs
├── settings.rs
├── snapshots.rs
├── task_info.rs
├── tasks.rs
├── tenant_tokens.rs
└── utils.rs
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | end_of_line = lf
9 | indent_style = space
10 |
11 | [*.rs]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.{yml,yaml}]
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report 🐞
3 | about: Create a report to help us improve.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 | **Description**
12 | Description of what the bug is about.
13 |
14 | **Expected behavior**
15 | What you expected to happen.
16 |
17 | **Current behavior**
18 | What happened.
19 |
20 | **Screenshots or Logs**
21 | If applicable, add screenshots or logs to help explain your problem.
22 |
23 | **Environment (please complete the following information):**
24 | - OS: [e.g. Debian GNU/Linux]
25 | - Meilisearch version: [e.g. v.0.20.0]
26 | - meilisearch-rust version: [e.g v0.20.1]
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Support questions & other
4 | url: https://discord.meilisearch.com/
5 | about: Support is not handled here but on our Discord
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request & Enhancement 💡
3 | about: Suggest a new idea for the project.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 | **Description**
12 | Brief explanation of the feature.
13 |
14 | **Basic example**
15 | If the proposal involves something new or a change, include a basic example. How would you use the feature? In which context?
16 |
17 | **Other**
18 | Any other things you want to add.
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | labels:
8 | - 'skip-changelog'
9 | - 'dependencies'
10 | rebase-strategy: disabled
11 |
12 | - package-ecosystem: cargo
13 | directory: "/"
14 | schedule:
15 | interval: "monthly"
16 | time: "04:00"
17 | open-pull-requests-limit: 10
18 | labels:
19 | - skip-changelog
20 | - dependencies
21 | rebase-strategy: disabled
22 |
--------------------------------------------------------------------------------
/.github/release-draft-template.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION 🦀'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | exclude-labels:
4 | - 'skip-changelog'
5 | version-resolver:
6 | minor:
7 | labels:
8 | - 'breaking-change'
9 | default: patch
10 | categories:
11 | - title: '⚠️ Breaking changes'
12 | label: 'breaking-change'
13 | - title: '🚀 Enhancements'
14 | label: 'enhancement'
15 | - title: '🐛 Bug Fixes'
16 | label: 'bug'
17 | - title: '🔒 Security'
18 | label: 'security'
19 | - title: '⚙️ Maintenance/misc'
20 | label:
21 | - 'maintenance'
22 | - 'documentation'
23 | template: |
24 | $CHANGES
25 |
26 | Thanks again to $CONTRIBUTORS! 🎉
27 | no-changes-template: 'Changes are coming soon 😎'
28 | sort-direction: 'ascending'
29 | replacers:
30 | - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g'
31 | replace: ''
32 | - search: '/(?:and )?@dependabot(?:\[bot\])?,?/g'
33 | replace: ''
34 | - search: '/(?:and )?@bors(?:\[bot\])?,?/g'
35 | replace: ''
36 | - search: '/(?:and )?@meili-bot(?:\[bot\])?,?/g'
37 | replace: ''
38 | - search: '/(?:and )?@meili-bors(?:\[bot\])?,?/g'
39 | replace: ''
40 |
--------------------------------------------------------------------------------
/.github/scripts/check-release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Checking if current tag matches the package version
4 | current_tag=$(echo "$GITHUB_REF" | cut -d '/' -f 3 | sed -r 's/^v//')
5 |
6 | file1='Cargo.toml'
7 | file2='README.tpl'
8 | file3='.code-samples.meilisearch.yaml'
9 | file4='README.md'
10 | file5='./meilisearch-index-setting-macro/Cargo.toml'
11 |
12 | file_tag1=$(grep '^version = ' $file1 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ')
13 | file_tag_1_1=$(grep '{ path = "meilisearch-index-setting-macro", version =' $file1 | grep -Eo '[0-9]+.[0-9]+.[0-9]+')
14 | file_tag2=$(grep 'meilisearch-sdk = ' $file2 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ')
15 | file_tag3=$(grep 'meilisearch-sdk = ' $file3 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ')
16 | file_tag4=$(grep 'meilisearch-sdk = ' $file4 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ')
17 | file_tag5=$(grep '^version = ' $file5 | grep -Eo '[0-9]+.[0-9]+.[0-9]+')
18 |
19 | if [ "$current_tag" != "$file_tag1" ] ||
20 | [ "$current_tag" != "$file_tag_1_1" ] ||
21 | [ "$current_tag" != "$file_tag2" ] ||
22 | [ "$current_tag" != "$file_tag3" ] ||
23 | [ "$current_tag" != "$file_tag4" ] ||
24 | [ "$current_tag" != "$file_tag5" ] \
25 | ; then
26 | echo "Error: the current tag does not match the version in package file(s)."
27 | echo "$file1: found $file_tag1 - expected $current_tag"
28 | echo "$file1: found $file_tag_1_1 - expected $current_tag"
29 | echo "$file2: found $file_tag2 - expected $current_tag"
30 | echo "$file3: found $file_tag3 - expected $current_tag"
31 | echo "$file4: found $file_tag4 - expected $current_tag"
32 | echo "$file5: found $file_tag5 - expected $current_tag"
33 | exit 1
34 | fi
35 |
36 | echo 'OK'
37 | exit 0
38 |
--------------------------------------------------------------------------------
/.github/workflows/pre-release-tests.yml:
--------------------------------------------------------------------------------
1 | # Testing the code base against the Meilisearch pre-releases
2 | name: Pre-Release Tests
3 |
4 | # Will only run for PRs and pushes to bump-meilisearch-v*
5 | on:
6 | push:
7 | branches: bump-meilisearch-v*
8 | pull_request:
9 | branches: bump-meilisearch-v*
10 |
11 | env:
12 | CARGO_TERM_COLOR: always
13 |
14 | jobs:
15 | integration_tests:
16 | name: integration-tests-against-rc
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Build
21 | run: cargo build --verbose
22 | - name: Get the latest Meilisearch RC
23 | run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV
24 | - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker
25 | run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics
26 | - name: Run tests
27 | run: cargo test --verbose -- --test-threads=1
28 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to crates.io
2 | on:
3 | push:
4 | tags:
5 | - v*
6 |
7 | jobs:
8 | publish:
9 | name: Rust project
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: hecrj/setup-rust-action@master
14 | with:
15 | rust-version: stable
16 | - name: Check release validity
17 | run: sh .github/scripts/check-release.sh
18 | - name: Build meilisearch crate
19 | run: cargo build --release
20 | - name: Build meilisearch crate
21 | run: |-
22 | cd meilisearch-index-setting-macro
23 | cargo build --release
24 | - name: Login
25 | run: cargo login ${{ secrets.CRATES_TOKEN }}
26 | - name: Publish meilisearch-index-setting-macro crate to crates.io
27 | run: |-
28 | cd meilisearch-index-setting-macro
29 | cargo publish
30 | - name: Publish meilisearch crate to crates.io
31 | run: cargo publish
32 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | update_release_draft:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: release-drafter/release-drafter@v6
13 | with:
14 | config-name: release-draft-template.yml
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN }}
17 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | pull_request:
5 | push:
6 | # trying and staging branches are for BORS config
7 | branches:
8 | - trying
9 | - staging
10 | - main
11 |
12 | env:
13 | CARGO_TERM_COLOR: always
14 |
15 | jobs:
16 | integration_tests:
17 | # Will not run if the event is a PR to bump-meilisearch-v* (so a pre-release PR)
18 | # Will still run for each push to bump-meilisearch-v*
19 | if: github.event_name != 'pull_request' || !startsWith(github.base_ref, 'bump-meilisearch-v')
20 | runs-on: ubuntu-latest
21 | name: integration-tests
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Build
25 | run: cargo build --verbose
26 | - name: Meilisearch (latest version) setup with Docker
27 | run: docker run -d -p 7700:7700 getmeili/meilisearch:latest meilisearch --no-analytics --master-key=masterKey
28 | - name: Run tests
29 | run: cargo test --verbose
30 | - name: Cargo check
31 | uses: actions-rs/cargo@v1
32 | with:
33 | command: check
34 | args: --workspace --all-targets --all
35 | - name: Cargo check no default features
36 | uses: actions-rs/cargo@v1
37 | with:
38 | command: check
39 | args: --workspace --all-targets --all --no-default-features
40 |
41 | linter:
42 | name: clippy-check
43 | runs-on: ubuntu-latest
44 | steps:
45 | - uses: actions/checkout@v4
46 | - name: Install clippy
47 | run: rustup component add clippy
48 | - name: Run linter (clippy)
49 | # Will fail when encountering warnings
50 | run: cargo clippy -- -D warnings
51 |
52 | formatter:
53 | name: rust-format
54 | runs-on: ubuntu-latest
55 | steps:
56 | - uses: actions/checkout@v4
57 | - name: Run formatter
58 | run: cargo fmt --all -- --check
59 |
60 | readme_check:
61 | name: readme-check
62 | runs-on: ubuntu-latest
63 | steps:
64 | - uses: actions/checkout@v4
65 | - name: Check the README.md file is up-to-date
66 | run: sh scripts/check-readme.sh
67 |
68 | wasm_build:
69 | name: wasm-build
70 | runs-on: ubuntu-latest
71 | steps:
72 | - uses: actions/checkout@v4
73 | - name: Build
74 | run: |
75 | rustup target add wasm32-unknown-unknown
76 | cargo check -p web_app --target wasm32-unknown-unknown
77 | yaml-lint:
78 | name: Yaml linting check
79 | runs-on: ubuntu-latest
80 | steps:
81 | - uses: actions/checkout@v4
82 | - name: Yaml lint check
83 | uses: ibiqlik/action-yamllint@v3
84 | with:
85 | config_file: .yamllint.yml
86 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 | examples/web_app/target/*
4 | .vscode
5 |
--------------------------------------------------------------------------------
/.yamllint.yml:
--------------------------------------------------------------------------------
1 | extends: default
2 | ignore: |
3 | node_modules
4 | rules:
5 | comments-indentation: disable
6 | line-length: disable
7 | document-start: disable
8 | brackets: disable
9 | truthy: disable
10 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | First of all, thank you for contributing to Meilisearch! The goal of this document is to provide everything you need to know in order to contribute to Meilisearch and its different integrations.
4 |
5 | - [Assumptions](#assumptions)
6 | - [How to Contribute](#how-to-contribute)
7 | - [Development Workflow](#development-workflow)
8 | - [Git Guidelines](#git-guidelines)
9 | - [Release Process (for internal team only)](#release-process-for-internal-team-only)
10 |
11 |
12 | ## Assumptions
13 |
14 | 1. **You're familiar with [GitHub](https://github.com) and the [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests)(PR) workflow.**
15 | 2. **You've read the Meilisearch [documentation](https://www.meilisearch.com/docs) and the [README](/README.md).**
16 | 3. **You know about the [Meilisearch community](https://discord.com/invite/meilisearch). Please use this for help.**
17 |
18 | ## How to Contribute
19 |
20 | 1. Make sure that the contribution you want to make is explained or detailed in a GitHub issue! Find an [existing issue](https://github.com/meilisearch/meilisearch-rust/issues/) or [open a new one](https://github.com/meilisearch/meilisearch-rust/issues/new).
21 | 2. Once done, [fork the meilisearch-rust repository](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) in your own GitHub account. Ask a maintainer if you want your issue to be checked before making a PR.
22 | 3. [Create a new Git branch](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository).
23 | 4. Review the [Development Workflow](#development-workflow) section that describes the steps to maintain the repository.
24 | 5. Make the changes on your branch.
25 | 6. [Submit the branch as a PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) pointing to the `main` branch of the main meilisearch-rust repository. A maintainer should comment and/or review your Pull Request within a few days. Although depending on the circumstances, it may take longer.
26 | We do not enforce a naming convention for the PRs, but **please use something descriptive of your changes**, having in mind that the title of your PR will be automatically added to the next [release changelog](https://github.com/meilisearch/meilisearch-rust/releases/).
27 |
28 | ## Development Workflow
29 |
30 | You can set up your local environment natively or using `docker`, check out the [`docker-compose.yml`](/docker-compose.yml).
31 |
32 | Example of running all the checks with docker:
33 | ```bash
34 | docker-compose run --rm package bash -c "cargo test"
35 | ```
36 |
37 | To install dependencies:
38 |
39 | ```bash
40 | cargo build --release
41 | ```
42 |
43 | To ensure the same dependency versions in all environments, for example the CI, update the dependencies by running: `cargo update`.
44 |
45 | ### Tests
46 |
47 | To run the tests, run:
48 |
49 | ```bash
50 | # Tests
51 | curl -L https://install.meilisearch.com | sh # download Meilisearch
52 | ./meilisearch --master-key=masterKey --no-analytics # run Meilisearch
53 | cargo test
54 | ```
55 |
56 | There are two kinds of tests, documentation tests and unit tests.
57 | If you need to write or read the unit tests you should consider reading this
58 | [readme](meilisearch-test-macro/README.md) about our custom testing macro.
59 |
60 | Also, the WASM example compilation should be checked:
61 |
62 | ```bash
63 | rustup target add wasm32-unknown-unknown
64 | cargo check -p web_app --target wasm32-unknown-unknown
65 | ```
66 |
67 | Each PR should pass the tests to be accepted.
68 |
69 | ### Clippy
70 |
71 | Each PR should pass [`clippy`](https://github.com/rust-lang/rust-clippy) (the linter) to be accepted.
72 |
73 | ```bash
74 | cargo clippy -- -D warnings
75 | ```
76 |
77 | If you don't have `clippy` installed on your machine yet, run:
78 |
79 | ```bash
80 | rustup update
81 | rustup component add clippy
82 | ```
83 |
84 | ⚠️ Also, if you have installed `clippy` a long time ago, you might need to update it:
85 |
86 | ```bash
87 | rustup update
88 | ```
89 |
90 | ### Fmt
91 |
92 | Each PR should pass the format test to be accepted.
93 |
94 | Run the following to fix the formatting errors:
95 |
96 | ```
97 | cargo fmt
98 | ```
99 |
100 | and the following to test if the formatting is correct:
101 | ```
102 | cargo fmt --all -- --check
103 | ```
104 |
105 | ### Update the README
106 |
107 | The README is generated. Please do not update manually the `README.md` file.
108 |
109 | Instead, update the `README.tpl` and `src/lib.rs` files, and run:
110 |
111 | ```sh
112 | sh scripts/update-readme.sh
113 | ```
114 |
115 | Then, push the changed files.
116 |
117 | You can check the current `README.md` is up-to-date by running:
118 |
119 | ```sh
120 | sh scripts/check-readme.sh
121 | # To see the diff
122 | sh scripts/check-readme.sh --diff
123 | ```
124 |
125 | If it's not, the CI will fail on your PR.
126 |
127 | ### Yaml lint
128 |
129 | To check if your `yaml` files are correctly formatted, you need to [install yamllint](https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint) and then run `yamllint .`
130 |
131 | ## Git Guidelines
132 |
133 | ### Git Branches
134 |
135 | All changes must be made in a branch and submitted as PR.
136 | We do not enforce any branch naming style, but please use something descriptive of your changes.
137 |
138 | ### Git Commits
139 |
140 | As minimal requirements, your commit message should:
141 | - be capitalized
142 | - not finished by a dot or any other punctuation character (!,?)
143 | - start with a verb so that we can read your commit message this way: "This commit will ...", where "..." is the commit message.
144 | e.g.: "Fix the home page button" or "Add more tests for create_index method"
145 |
146 | We don't follow any other convention, but if you want to use one, we recommend [this one](https://chris.beams.io/posts/git-commit/).
147 |
148 | ### GitHub Pull Requests
149 |
150 | Some notes on GitHub PRs:
151 |
152 | - [Convert your PR as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) if your changes are a work in progress: no one will review it until you pass your PR as ready for review.
153 | The draft PR can be very useful if you want to show that you are working on something and make your work visible.
154 | - The branch related to the PR must be **up-to-date with `main`** before merging. Fortunately, this project [integrates a bot](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md) to automatically enforce this requirement without the PR author having to do it manually.
155 | - All PRs must be reviewed and approved by at least one maintainer.
156 | - The PR title should be accurate and descriptive of the changes. The title of the PR will be indeed automatically added to the next [release changelogs](https://github.com/meilisearch/meilisearch-rust/releases/).
157 |
158 | ## Release Process (for the internal team only)
159 |
160 | Meilisearch tools follow the [Semantic Versioning Convention](https://semver.org/).
161 |
162 | ### Automation to Rebase and Merge the PRs
163 |
164 | This project integrates a bot that helps us manage pull requests merging.
165 | _[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md)._
166 |
167 | ### Automated Changelogs
168 |
169 | This project integrates a tool to create automated changelogs.
170 | _[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/release-drafter.md)._
171 |
172 | ### How to Publish the Release
173 |
174 | ⚠️ Before doing anything, make sure you get through the guide about [Releasing an Integration](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md).
175 |
176 | Make a PR modifying the file [`Cargo.toml`](/Cargo.toml):
177 |
178 | ```toml
179 | version = "X.X.X"
180 | ```
181 |
182 | the [`README.tpl`](/README.tpl):
183 |
184 | ```rust
185 | //! meilisearch-sdk = "X.X.X"
186 | ```
187 |
188 | and the [code-samples file](/.code-samples.meilisearch.yaml):
189 |
190 | ```yml
191 | meilisearch-sdk = "X.X.X"
192 | ```
193 |
194 | with the right version.
195 |
196 |
197 | After the changes on `Cargo.toml`, run the following command:
198 |
199 | ```
200 | sh scripts/update_macro_versions.sh
201 | ```
202 |
203 | After the changes on `lib.rs`, run the following command:
204 |
205 | ```bash
206 | sh scripts/update-readme.sh
207 | ```
208 |
209 | Once the changes are merged on `main`, you can publish the current draft release via the [GitHub interface](https://github.com/meilisearch/meilisearch-rust/releases): on this page, click on `Edit` (related to the draft release) > update the description (be sure you apply [these recommendations](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md#writting-the-release-description)) > when you are ready, click on `Publish release`.
210 |
211 | GitHub Actions will be triggered and push the package to [crates.io](https://crates.io/crates/meilisearch-sdk).
212 |
213 |
214 |
215 | Thank you again for reading this through. We cannot wait to begin to work with you if you make your way through this contributing guide ❤️
216 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "meilisearch-sdk"
3 | version = "0.28.0"
4 | authors = ["Mubelotix "]
5 | edition = "2018"
6 | description = "Rust wrapper for the Meilisearch API. Meilisearch is a powerful, fast, open-source, easy to use and deploy search engine."
7 | license = "MIT"
8 | readme = "README.md"
9 | repository = "https://github.com/meilisearch/meilisearch-sdk"
10 | resolver = "2"
11 |
12 | [workspace]
13 | members = ["examples/*"]
14 |
15 | [dependencies]
16 | async-trait = "0.1.51"
17 | iso8601 = "0.6.1"
18 | log = "0.4"
19 | serde = { version = "1.0", features = ["derive"] }
20 | serde_json = "1.0"
21 | time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] }
22 | yaup = "0.3.1"
23 | either = { version = "1.8.0", features = ["serde"] }
24 | thiserror = "1.0.37"
25 | meilisearch-index-setting-macro = { path = "meilisearch-index-setting-macro", version = "0.28.0" }
26 | pin-project-lite = { version = "0.2.13", optional = true }
27 | reqwest = { version = "0.12.3", optional = true, default-features = false, features = ["rustls-tls", "http2", "stream"] }
28 | bytes = { version = "1.6", optional = true }
29 | uuid = { version = "1.1.2", features = ["v4"] }
30 | futures-io = "0.3.30"
31 | futures = "0.3"
32 |
33 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
34 | jsonwebtoken = { version = "9", default-features = false }
35 |
36 | [target.'cfg(target_arch = "wasm32")'.dependencies]
37 | uuid = { version = "1.8.0", default-features = false, features = ["v4", "js"] }
38 | web-sys = "0.3"
39 | wasm-bindgen-futures = "0.4"
40 |
41 | [features]
42 | default = ["reqwest"]
43 | reqwest = ["dep:reqwest", "pin-project-lite", "bytes"]
44 | futures-unsend = []
45 |
46 | [dev-dependencies]
47 | futures-await-test = "0.3"
48 | futures = "0.3"
49 | mockito = "1.0.0"
50 | meilisearch-test-macro = { path = "meilisearch-test-macro" }
51 | tokio = { version = "1", features = ["rt", "macros"] }
52 |
53 | # The following dependencies are required for examples
54 | wasm-bindgen = "0.2"
55 | wasm-bindgen-futures = "0.4"
56 | yew = "0.21"
57 | lazy_static = "1.4"
58 | web-sys = "0.3"
59 | console_error_panic_hook = "0.1"
60 | big_s = "1.0.2"
61 | insta = "1.38.0"
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2025 Meili SAS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Meilisearch Rust SDK
10 |
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ⚡ The Meilisearch API client written for Rust 🦀
30 |
31 | **Meilisearch Rust** is the Meilisearch API client for Rust developers.
32 |
33 | **Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch)
34 |
35 | ## Table of Contents
36 |
37 | - [📖 Documentation](#-documentation)
38 | - [🔧 Installation](#-installation)
39 | - [🚀 Getting started](#-getting-started)
40 | - [🌐 Running in the Browser with WASM](#-running-in-the-browser-with-wasm)
41 | - [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch)
42 | - [⚙️ Contributing](#️-contributing)
43 |
44 | ## 📖 Documentation
45 |
46 | This readme contains all the documentation you need to start using this Meilisearch SDK.
47 |
48 | For general information on how to use Meilisearch—such as our API reference, tutorials, guides, and in-depth articles—refer to our [main documentation website](https://www.meilisearch.com/docs).
49 |
50 | ## 🔧 Installation
51 |
52 | To use `meilisearch-sdk`, add this to your `Cargo.toml`:
53 |
54 | ```toml
55 | [dependencies]
56 | meilisearch-sdk = "0.28.0"
57 | ```
58 |
59 | The following optional dependencies may also be useful:
60 |
61 | ```toml
62 | futures = "0.3" # To be able to block on async functions if you are not using an async runtime
63 | serde = { version = "1.0", features = ["derive"] }
64 | ```
65 |
66 | This crate is `async` but you can choose to use an async runtime like [tokio](https://crates.io/crates/tokio) or just [block on futures](https://docs.rs/futures/latest/futures/executor/fn.block_on.html).
67 | You can enable the `sync` feature to make most structs `Sync`. It may be a bit slower.
68 |
69 | Using this crate is possible without [serde](https://crates.io/crates/serde), but a lot of features require serde.
70 |
71 | ### Run Meilisearch
72 |
73 | ⚡️ **Launch, scale, and streamline in minutes with Meilisearch Cloud**—no maintenance, no commitment, cancel anytime. [Try it free now](https://cloud.meilisearch.com/login?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust).
74 |
75 | 🪨 Prefer to self-host? [Download and deploy](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust) our fast, open-source search engine on your own infrastructure.
76 |
77 | ## 🚀 Getting started
78 |
79 | #### Add Documents
80 |
81 | ```rust
82 | use meilisearch_sdk::client::*;
83 | use serde::{Serialize, Deserialize};
84 | use futures::executor::block_on;
85 |
86 | #[derive(Serialize, Deserialize, Debug)]
87 | struct Movie {
88 | id: usize,
89 | title: String,
90 | genres: Vec,
91 | }
92 |
93 |
94 | #[tokio::main(flavor = "current_thread")]
95 | async fn main() {
96 | // Create a client (without sending any request so that can't fail)
97 | let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
98 |
99 | // An index is where the documents are stored.
100 | let movies = client.index("movies");
101 |
102 | // Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
103 | movies.add_documents(&[
104 | Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] },
105 | Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] },
106 | Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] },
107 | Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] },
108 | Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] },
109 | Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] },
110 | ], Some("id")).await.unwrap();
111 | }
112 | ```
113 |
114 | With the `uid`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-task).
115 |
116 | #### Basic Search
117 |
118 | ```rust
119 | // Meilisearch is typo-tolerant:
120 | println!("{:?}", client.index("movies_2").search().with_query("caorl").execute::().await.unwrap().hits);
121 | ```
122 |
123 | Output:
124 | ```
125 | [Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"] }]
126 | ```
127 |
128 | Json output:
129 | ```json
130 | {
131 | "hits": [{
132 | "id": 1,
133 | "title": "Carol",
134 | "genres": ["Romance", "Drama"]
135 | }],
136 | "offset": 0,
137 | "limit": 10,
138 | "processingTimeMs": 1,
139 | "query": "caorl"
140 | }
141 | ```
142 |
143 | #### Custom Search
144 |
145 | ```rust
146 | let search_result = client.index("movies_3")
147 | .search()
148 | .with_query("phil")
149 | .with_attributes_to_highlight(Selectors::Some(&["*"]))
150 | .execute::()
151 | .await
152 | .unwrap();
153 | println!("{:?}", search_result.hits);
154 | ```
155 |
156 | Json output:
157 | ```json
158 | {
159 | "hits": [
160 | {
161 | "id": 6,
162 | "title": "Philadelphia",
163 | "_formatted": {
164 | "id": 6,
165 | "title": "Phil adelphia",
166 | "genre": ["Drama"]
167 | }
168 | }
169 | ],
170 | "offset": 0,
171 | "limit": 20,
172 | "processingTimeMs": 0,
173 | "query": "phil"
174 | }
175 | ```
176 |
177 | #### Custom Search With Filters
178 |
179 | If you want to enable filtering, you must add your attributes to the `filterableAttributes`
180 | index setting.
181 |
182 | ```rust
183 | let filterable_attributes = [
184 | "id",
185 | "genres",
186 | ];
187 | client.index("movies_4").set_filterable_attributes(&filterable_attributes).await.unwrap();
188 | ```
189 |
190 | You only need to perform this operation once.
191 |
192 | Note that Meilisearch will rebuild your index whenever you update `filterableAttributes`. Depending on the size of your dataset, this might take time. You can track the process using the [tasks](https://www.meilisearch.com/docs/reference/api/tasks#get-task).
193 |
194 | Then, you can perform the search:
195 |
196 | ```rust
197 | let search_result = client.index("movies_5")
198 | .search()
199 | .with_query("wonder")
200 | .with_filter("id > 1 AND genres = Action")
201 | .execute::()
202 | .await
203 | .unwrap();
204 | println!("{:?}", search_result.hits);
205 | ```
206 |
207 | Json output:
208 | ```json
209 | {
210 | "hits": [
211 | {
212 | "id": 2,
213 | "title": "Wonder Woman",
214 | "genres": ["Action", "Adventure"]
215 | }
216 | ],
217 | "offset": 0,
218 | "limit": 20,
219 | "estimatedTotalHits": 1,
220 | "processingTimeMs": 0,
221 | "query": "wonder"
222 | }
223 | ```
224 |
225 | #### Customize the `HttpClient`
226 |
227 | By default, the SDK uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) to make http calls.
228 | The SDK lets you customize the http client by implementing the `HttpClient` trait yourself and
229 | initializing the `Client` with the `new_with_client` method.
230 | You may be interested by the `futures-unsend` feature which lets you specify a non-Send http client.
231 |
232 | #### Wasm support
233 |
234 | The SDK supports wasm through reqwest. You'll need to enable the `futures-unsend` feature while importing it, though.
235 |
236 | ## 🌐 Running in the Browser with WASM
237 |
238 | This crate fully supports WASM.
239 |
240 | The only difference between the WASM and the native version is that the native version has one more variant (`Error::Http`) in the Error enum. That should not matter so much but we could add this variant in WASM too.
241 |
242 | However, making a program intended to run in a web browser requires a **very** different design than a CLI program. To see an example of a simple Rust web app using Meilisearch, see the [our demo](./examples/web_app).
243 |
244 | WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extension).
245 |
246 | ## 🤖 Compatibility with Meilisearch
247 |
248 | This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-rust/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info.
249 |
250 | ## ⚙️ Contributing
251 |
252 | Any new contribution is more than welcome in this project!
253 |
254 | If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions!
255 |
256 |
257 |
258 | **Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository.
259 |
--------------------------------------------------------------------------------
/README.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Meilisearch Rust SDK
10 |
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ⚡ The Meilisearch API client written for Rust 🦀
30 |
31 | **Meilisearch Rust** is the Meilisearch API client for Rust developers.
32 |
33 | **Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch)
34 |
35 | ## Table of Contents
36 |
37 | - [📖 Documentation](#-documentation)
38 | - [🔧 Installation](#-installation)
39 | - [🚀 Getting started](#-getting-started)
40 | - [🌐 Running in the Browser with WASM](#-running-in-the-browser-with-wasm)
41 | - [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch)
42 | - [⚙️ Contributing](#️-contributing)
43 |
44 | ## 📖 Documentation
45 |
46 | This readme contains all the documentation you need to start using this Meilisearch SDK.
47 |
48 | For general information on how to use Meilisearch—such as our API reference, tutorials, guides, and in-depth articles—refer to our [main documentation website](https://www.meilisearch.com/docs).
49 |
50 | ## 🔧 Installation
51 |
52 | To use `meilisearch-sdk`, add this to your `Cargo.toml`:
53 |
54 | ```toml
55 | [dependencies]
56 | meilisearch-sdk = "0.28.0"
57 | ```
58 |
59 | The following optional dependencies may also be useful:
60 |
61 | ```toml
62 | futures = "0.3" # To be able to block on async functions if you are not using an async runtime
63 | serde = { version = "1.0", features = ["derive"] }
64 | ```
65 |
66 | This crate is `async` but you can choose to use an async runtime like [tokio](https://crates.io/crates/tokio) or just [block on futures](https://docs.rs/futures/latest/futures/executor/fn.block_on.html).
67 | You can enable the `sync` feature to make most structs `Sync`. It may be a bit slower.
68 |
69 | Using this crate is possible without [serde](https://crates.io/crates/serde), but a lot of features require serde.
70 |
71 | ### Run Meilisearch
72 |
73 | ⚡️ **Launch, scale, and streamline in minutes with Meilisearch Cloud**—no maintenance, no commitment, cancel anytime. [Try it free now](https://cloud.meilisearch.com/login?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust).
74 |
75 | 🪨 Prefer to self-host? [Download and deploy](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust) our fast, open-source search engine on your own infrastructure.
76 |
77 | {{readme}}
78 |
79 | ## 🌐 Running in the Browser with WASM
80 |
81 | This crate fully supports WASM.
82 |
83 | The only difference between the WASM and the native version is that the native version has one more variant (`Error::Http`) in the Error enum. That should not matter so much but we could add this variant in WASM too.
84 |
85 | However, making a program intended to run in a web browser requires a **very** different design than a CLI program. To see an example of a simple Rust web app using Meilisearch, see the [our demo](./examples/web_app).
86 |
87 | WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extension).
88 |
89 | ## 🤖 Compatibility with Meilisearch
90 |
91 | This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-rust/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info.
92 |
93 | ## ⚙️ Contributing
94 |
95 | Any new contribution is more than welcome in this project!
96 |
97 | If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions!
98 |
99 |
100 |
101 | **Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository.
102 |
--------------------------------------------------------------------------------
/bors.toml:
--------------------------------------------------------------------------------
1 | status = [
2 | 'integration-tests',
3 | 'clippy-check',
4 | 'rust-format',
5 | 'readme-check',
6 | 'wasm-build',
7 | 'Yaml linting check'
8 | ]
9 | # 1 hour timeout
10 | timeout-sec = 3600
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | # remove this line if you don't need a volume to map your dependencies
4 | # Check how to cache the build
5 | volumes:
6 | cargo:
7 |
8 | services:
9 | package:
10 | image: rust:1
11 | tty: true
12 | stdin_open: true
13 | working_dir: /home/package
14 | environment:
15 | - MEILISEARCH_URL=http://meilisearch:7700
16 | - CARGO_HOME=/vendor/cargo
17 | depends_on:
18 | - meilisearch
19 | links:
20 | - meilisearch
21 | volumes:
22 | - ./:/home/package
23 | - cargo:/vendor/cargo
24 |
25 | meilisearch:
26 | image: getmeili/meilisearch:latest
27 | ports:
28 | - "7700"
29 | environment:
30 | - MEILI_MASTER_KEY=masterKey
31 | - MEILI_NO_ANALYTICS=true
32 |
--------------------------------------------------------------------------------
/examples/cli-app-with-awc/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "cli-app-with-awc"
3 | version = "0.0.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | meilisearch-sdk = { path = "../..", default-features = false, features = ["futures-unsend"] }
11 | futures = "0.3"
12 | serde = { version = "1.0", features = ["derive"] }
13 | serde_json = "1.0"
14 | lazy_static = "1.4.0"
15 | awc = "3.4"
16 | async-trait = "0.1.51"
17 | tokio = { version = "1.27.0", features = ["full"] }
18 | yaup = "0.3.0"
19 | tokio-util = { version = "0.7.10", features = ["full"] }
20 | actix-rt = "2.9.0"
21 | anyhow = "1.0.82"
22 |
--------------------------------------------------------------------------------
/examples/cli-app-with-awc/assets/clothes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "seaon": "winter",
5 | "article": "sweater",
6 | "cost": 63.40,
7 | "size":"L",
8 | "pattern":"striped"
9 | },
10 | {
11 | "id": 2,
12 | "seaon": "spring",
13 | "article": "sweat pants",
14 | "cost": 18.00,
15 | "size":"XXXL",
16 | "pattern":"floral"
17 | },
18 | {
19 | "id": 3,
20 | "seaon": "fall",
21 | "article": "t-shirt",
22 | "cost": 1634.90,
23 | "size":"M",
24 | "pattern":"solid black"
25 | },
26 | {
27 | "id": 4,
28 | "seaon": "summer",
29 | "article": "tank top",
30 | "cost": 3.40,
31 | "size":"L",
32 | "pattern":"diagonal"
33 | },
34 | {
35 | "id": 5,
36 | "seaon": "winter",
37 | "article": "jeans",
38 | "cost": 4.20,
39 | "size":"XL",
40 | "pattern":"striped"
41 | },
42 | {
43 | "id": 6,
44 | "seaon": "spring",
45 | "article": "sun dress",
46 | "cost": 12634.56,
47 | "size":"L",
48 | "pattern":"floral"
49 | },
50 | {
51 | "id": 7,
52 | "seaon": "fall",
53 | "article": "sweatshirt",
54 | "cost": 90.80,
55 | "size":"M",
56 | "pattern":"checker"
57 | },
58 | {
59 | "id": 8,
60 | "seaon": "summer",
61 | "article": "shorts",
62 | "cost": 16.34,
63 | "size":"XS",
64 | "pattern":"solid beige"
65 | },
66 | {
67 | "id": 9,
68 | "seaon": "winter",
69 | "article": "jacket",
70 | "cost": 634,
71 | "size":"L",
72 | "pattern":"camo"
73 | }
74 | ]
--------------------------------------------------------------------------------
/examples/cli-app-with-awc/src/main.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use meilisearch_sdk::errors::Error;
3 | use meilisearch_sdk::request::{parse_response, HttpClient, Method};
4 | use meilisearch_sdk::{client::*, settings::Settings};
5 | use serde::de::DeserializeOwned;
6 | use serde::{Deserialize, Serialize};
7 | use std::fmt;
8 | use std::io::stdin;
9 |
10 | #[derive(Debug, Clone)]
11 | pub struct AwcClient {
12 | api_key: Option,
13 | }
14 |
15 | impl AwcClient {
16 | pub fn new(api_key: Option<&str>) -> Result {
17 | Ok(AwcClient {
18 | api_key: api_key.map(|key| key.to_string()),
19 | })
20 | }
21 | }
22 |
23 | #[async_trait(?Send)]
24 | impl HttpClient for AwcClient {
25 | async fn stream_request<
26 | Query: Serialize + Send + Sync,
27 | Body: futures::AsyncRead + Send + Sync + 'static,
28 | Output: DeserializeOwned + 'static,
29 | >(
30 | &self,
31 | url: &str,
32 | method: Method,
33 | content_type: &str,
34 | expected_status_code: u16,
35 | ) -> Result {
36 | let mut builder = awc::ClientBuilder::new();
37 | if let Some(ref api_key) = self.api_key {
38 | builder = builder.bearer_auth(api_key);
39 | }
40 | builder = builder.add_default_header(("User-Agent", "Rust client with Awc"));
41 | let client = builder.finish();
42 |
43 | let query = method.query();
44 | let query = yaup::to_string(query)?;
45 |
46 | let url = if query.is_empty() {
47 | url.to_string()
48 | } else {
49 | format!("{url}?{query}")
50 | };
51 |
52 | let url = add_query_parameters(&url, method.query())?;
53 | let request = client.request(verb(&method), &url);
54 |
55 | let mut response = if let Some(body) = method.into_body() {
56 | let reader = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body);
57 | let stream = tokio_util::io::ReaderStream::new(reader);
58 | request
59 | .content_type(content_type)
60 | .send_stream(stream)
61 | .await
62 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))?
63 | } else {
64 | request
65 | .send()
66 | .await
67 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))?
68 | };
69 |
70 | let status = response.status().as_u16();
71 | let mut body = String::from_utf8(
72 | response
73 | .body()
74 | .await
75 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))?
76 | .to_vec(),
77 | )
78 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))?;
79 |
80 | if body.is_empty() {
81 | body = "null".to_string();
82 | }
83 |
84 | parse_response(status, expected_status_code, &body, url.to_string())
85 | }
86 | }
87 |
88 | #[actix_rt::main]
89 | async fn main() {
90 | let http_client = AwcClient::new(Some("masterKey")).unwrap();
91 | let client = Client::new_with_client("http://localhost:7700", Some("masterKey"), http_client);
92 |
93 | // build the index
94 | build_index(&client).await;
95 |
96 | // enter in search queries or quit
97 | loop {
98 | println!("Enter a search query or type \"q\" or \"quit\" to quit:");
99 | let mut input_string = String::new();
100 | stdin()
101 | .read_line(&mut input_string)
102 | .expect("Failed to read line");
103 | match input_string.trim() {
104 | "quit" | "q" | "" => {
105 | println!("exiting...");
106 | break;
107 | }
108 | _ => {
109 | search(&client, input_string.trim()).await;
110 | }
111 | }
112 | }
113 | // get rid of the index at the end, doing this only so users don't have the index without knowing
114 | let _ = client.delete_index("clothes").await.unwrap();
115 | }
116 |
117 | async fn search(client: &Client, query: &str) {
118 | // make the search query, which excutes and serializes hits into the
119 | // ClothesDisplay struct
120 | let query_results = client
121 | .index("clothes")
122 | .search()
123 | .with_query(query)
124 | .execute::()
125 | .await
126 | .unwrap()
127 | .hits;
128 |
129 | // display the query results
130 | if query_results.is_empty() {
131 | println!("no results...");
132 | } else {
133 | for clothes in query_results {
134 | let display = clothes.result;
135 | println!("{}", format_args!("{}", display));
136 | }
137 | }
138 | }
139 |
140 | async fn build_index(client: &Client) {
141 | // reading and parsing the file
142 | let content = include_str!("../assets/clothes.json");
143 |
144 | // serialize the string to clothes objects
145 | let clothes: Vec = serde_json::from_str(content).unwrap();
146 |
147 | //create displayed attributes
148 | let displayed_attributes = ["article", "cost", "size", "pattern"];
149 |
150 | // Create ranking rules
151 | let ranking_rules = ["words", "typo", "attribute", "exactness", "cost:asc"];
152 |
153 | //create searchable attributes
154 | let searchable_attributes = ["seaon", "article", "size", "pattern"];
155 |
156 | // create the synonyms hashmap
157 | let mut synonyms = std::collections::HashMap::new();
158 | synonyms.insert("sweater", vec!["cardigan", "long-sleeve"]);
159 | synonyms.insert("sweat pants", vec!["joggers", "gym pants"]);
160 | synonyms.insert("t-shirt", vec!["tees", "tshirt"]);
161 |
162 | //create the settings struct
163 | let settings = Settings::new()
164 | .with_ranking_rules(ranking_rules)
165 | .with_searchable_attributes(searchable_attributes)
166 | .with_displayed_attributes(displayed_attributes)
167 | .with_synonyms(synonyms);
168 |
169 | //add the settings to the index
170 | let result = client
171 | .index("clothes")
172 | .set_settings(&settings)
173 | .await
174 | .unwrap()
175 | .wait_for_completion(client, None, None)
176 | .await
177 | .unwrap();
178 |
179 | if result.is_failure() {
180 | panic!(
181 | "Encountered an error while setting settings for index: {:?}",
182 | result.unwrap_failure()
183 | );
184 | }
185 |
186 | // add the documents
187 | let result = client
188 | .index("clothes")
189 | .add_or_update(&clothes, Some("id"))
190 | .await
191 | .unwrap()
192 | .wait_for_completion(client, None, None)
193 | .await
194 | .unwrap();
195 |
196 | if result.is_failure() {
197 | panic!(
198 | "Encountered an error while sending the documents: {:?}",
199 | result.unwrap_failure()
200 | );
201 | }
202 | }
203 |
204 | /// Base search object.
205 | #[derive(Serialize, Deserialize, Debug)]
206 | pub struct Clothes {
207 | id: usize,
208 | seaon: String,
209 | article: String,
210 | cost: f32,
211 | size: String,
212 | pattern: String,
213 | }
214 |
215 | /// Search results get serialized to this struct
216 | #[derive(Serialize, Deserialize, Debug)]
217 | pub struct ClothesDisplay {
218 | article: String,
219 | cost: f32,
220 | size: String,
221 | pattern: String,
222 | }
223 |
224 | impl fmt::Display for ClothesDisplay {
225 | // This trait requires `fmt` with this exact signature.
226 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
227 | // Write strictly the first element into the supplied output
228 | // stream: `f`. Returns `fmt::Result` which indicates whether the
229 | // operation succeeded or failed. Note that `write!` uses syntax which
230 | // is very similar to `println!`.
231 | write!(
232 | f,
233 | "result\n article: {},\n price: {},\n size: {},\n pattern: {}\n",
234 | self.article, self.cost, self.size, self.pattern
235 | )
236 | }
237 | }
238 |
239 | fn add_query_parameters(url: &str, query: &Query) -> Result {
240 | let query = yaup::to_string(query)?;
241 |
242 | if query.is_empty() {
243 | Ok(url.to_string())
244 | } else {
245 | Ok(format!("{url}?{query}"))
246 | }
247 | }
248 |
249 | fn verb(method: &Method) -> awc::http::Method {
250 | match method {
251 | Method::Get { .. } => awc::http::Method::GET,
252 | Method::Delete { .. } => awc::http::Method::DELETE,
253 | Method::Post { .. } => awc::http::Method::POST,
254 | Method::Put { .. } => awc::http::Method::PUT,
255 | Method::Patch { .. } => awc::http::Method::PATCH,
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/examples/cli-app/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "cli-app"
3 | version = "0.0.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | meilisearch-sdk = {path="../.."}
11 | futures = "0.3"
12 | serde = { version="1.0", features = ["derive"] }
13 | serde_json = "1.0"
14 | lazy_static = "1.4.0"
15 | yaup = "0.3.0"
16 |
--------------------------------------------------------------------------------
/examples/cli-app/assets/clothes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "season": "winter",
5 | "article": "sweater",
6 | "cost": 63.40,
7 | "size":"L",
8 | "pattern":"striped"
9 | },
10 | {
11 | "id": 2,
12 | "season": "spring",
13 | "article": "sweat pants",
14 | "cost": 18.00,
15 | "size":"XXXL",
16 | "pattern":"floral"
17 | },
18 | {
19 | "id": 3,
20 | "season": "fall",
21 | "article": "t-shirt",
22 | "cost": 1634.90,
23 | "size":"M",
24 | "pattern":"solid black"
25 | },
26 | {
27 | "id": 4,
28 | "season": "summer",
29 | "article": "tank top",
30 | "cost": 3.40,
31 | "size":"L",
32 | "pattern":"diagonal"
33 | },
34 | {
35 | "id": 5,
36 | "season": "winter",
37 | "article": "jeans",
38 | "cost": 4.20,
39 | "size":"XL",
40 | "pattern":"striped"
41 | },
42 | {
43 | "id": 6,
44 | "season": "spring",
45 | "article": "sun dress",
46 | "cost": 12634.56,
47 | "size":"L",
48 | "pattern":"floral"
49 | },
50 | {
51 | "id": 7,
52 | "season": "fall",
53 | "article": "sweatshirt",
54 | "cost": 90.80,
55 | "size":"M",
56 | "pattern":"checker"
57 | },
58 | {
59 | "id": 8,
60 | "season": "summer",
61 | "article": "shorts",
62 | "cost": 16.34,
63 | "size":"XS",
64 | "pattern":"solid beige"
65 | },
66 | {
67 | "id": 9,
68 | "season": "winter",
69 | "article": "jacket",
70 | "cost": 634,
71 | "size":"L",
72 | "pattern":"camo"
73 | }
74 | ]
75 |
--------------------------------------------------------------------------------
/examples/cli-app/src/main.rs:
--------------------------------------------------------------------------------
1 | use futures::executor::block_on;
2 | use lazy_static::lazy_static;
3 | use meilisearch_sdk::client::Client;
4 | use meilisearch_sdk::settings::Settings;
5 | use serde::{Deserialize, Serialize};
6 | use std::fmt;
7 | use std::io::stdin;
8 |
9 | // instantiate the client. load it once
10 | lazy_static! {
11 | static ref CLIENT: Client = Client::new("http://localhost:7700", Some("masterKey")).unwrap();
12 | }
13 |
14 | fn main() {
15 | block_on(async move {
16 | // build the index
17 | build_index().await;
18 |
19 | // enter in search queries or quit
20 | loop {
21 | println!("Enter a search query or type \"q\" or \"quit\" to quit:");
22 | let mut input_string = String::new();
23 | stdin()
24 | .read_line(&mut input_string)
25 | .expect("Failed to read line");
26 | match input_string.trim() {
27 | "quit" | "q" | "" => {
28 | println!("exiting...");
29 | break;
30 | }
31 | _ => {
32 | search(input_string.trim()).await;
33 | }
34 | }
35 | }
36 | // get rid of the index at the end, doing this only so users don't have the index without knowing
37 | let _ = CLIENT.delete_index("clothes").await.unwrap();
38 | })
39 | }
40 |
41 | async fn search(query: &str) {
42 | // make the search query, which executes and serializes hits into the
43 | // ClothesDisplay struct
44 | let query_results = CLIENT
45 | .index("clothes")
46 | .search()
47 | .with_query(query)
48 | .execute::()
49 | .await
50 | .unwrap()
51 | .hits;
52 |
53 | // display the query results
54 | if query_results.is_empty() {
55 | println!("no results...");
56 | } else {
57 | for clothes in query_results {
58 | let display = clothes.result;
59 | println!("{}", format_args!("{}", display));
60 | }
61 | }
62 | }
63 |
64 | async fn build_index() {
65 | // reading and parsing the file
66 | let content = include_str!("../assets/clothes.json");
67 |
68 | // serialize the string to clothes objects
69 | let clothes: Vec = serde_json::from_str(content).unwrap();
70 |
71 | // create displayed attributes
72 | let displayed_attributes = ["article", "cost", "size", "pattern"];
73 |
74 | // Create ranking rules
75 | let ranking_rules = ["words", "typo", "attribute", "exactness", "cost:asc"];
76 |
77 | // create searchable attributes
78 | let searchable_attributes = ["season", "article", "size", "pattern"];
79 |
80 | // create the synonyms hashmap
81 | let mut synonyms = std::collections::HashMap::new();
82 | synonyms.insert("sweater", vec!["cardigan", "long-sleeve"]);
83 | synonyms.insert("sweat pants", vec!["joggers", "gym pants"]);
84 | synonyms.insert("t-shirt", vec!["tees", "tshirt"]);
85 |
86 | // create the settings struct
87 | let settings = Settings::new()
88 | .with_ranking_rules(ranking_rules)
89 | .with_searchable_attributes(searchable_attributes)
90 | .with_displayed_attributes(displayed_attributes)
91 | .with_synonyms(synonyms);
92 |
93 | // add the settings to the index
94 | let result = CLIENT
95 | .index("clothes")
96 | .set_settings(&settings)
97 | .await
98 | .unwrap()
99 | .wait_for_completion(&CLIENT, None, None)
100 | .await
101 | .unwrap();
102 |
103 | if result.is_failure() {
104 | panic!(
105 | "Encountered an error while setting settings for index: {:?}",
106 | result.unwrap_failure()
107 | );
108 | }
109 |
110 | // add the documents
111 | let result = CLIENT
112 | .index("clothes")
113 | .add_or_update(&clothes, Some("id"))
114 | .await
115 | .unwrap()
116 | .wait_for_completion(&CLIENT, None, None)
117 | .await
118 | .unwrap();
119 |
120 | if result.is_failure() {
121 | panic!(
122 | "Encountered an error while sending the documents: {:?}",
123 | result.unwrap_failure()
124 | );
125 | }
126 | }
127 |
128 | /// Base search object.
129 | #[derive(Serialize, Deserialize, Debug)]
130 | pub struct Clothes {
131 | id: usize,
132 | season: String,
133 | article: String,
134 | cost: f32,
135 | size: String,
136 | pattern: String,
137 | }
138 |
139 | /// Search results get serialized to this struct
140 | #[derive(Serialize, Deserialize, Debug)]
141 | pub struct ClothesDisplay {
142 | article: String,
143 | cost: f32,
144 | size: String,
145 | pattern: String,
146 | }
147 |
148 | impl fmt::Display for ClothesDisplay {
149 | // This trait requires `fmt` with this exact signature.
150 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151 | // Write strictly the first element into the supplied output
152 | // stream: `f`. Returns `fmt::Result` which indicates whether the
153 | // operation succeeded or failed. Note that `write!` uses syntax which
154 | // is very similar to `println!`.
155 | write!(
156 | f,
157 | "result\n article: {},\n price: {},\n size: {},\n pattern: {}\n",
158 | self.article, self.cost, self.size, self.pattern
159 | )
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/examples/settings.rs:
--------------------------------------------------------------------------------
1 | use meilisearch_sdk::{client::Client, indexes::Index, settings::Settings};
2 |
3 | // we need an async runtime
4 | #[tokio::main(flavor = "current_thread")]
5 | async fn main() {
6 | let client: Client = Client::new("http://localhost:7700", Some("masterKey")).unwrap();
7 |
8 | // We try to create an index called `movies` with a primary_key of `movie_id`.
9 | let my_index: Index = client
10 | .create_index("movies", Some("movie_id"))
11 | .await
12 | .expect("Could not join the remote server.")
13 | // The creation of indexes is asynchronous. But for the sake of the example so we will
14 | // wait until the update is entirely processed.
15 | .wait_for_completion(&client, None, None)
16 | .await
17 | .expect("Could not join the remote server.")
18 | // If the creation was successful we can generate an `Index` out of it.
19 | .try_make_index(&client)
20 | // This error comes from meilisearch itself.
21 | .expect("An error happened with the index creation.");
22 |
23 | // And now we can update the settings!
24 | // You can read more about the available options here: https://www.meilisearch.com/docs/learn/configuration/settings#index-settings
25 | let settings: Settings = Settings::new()
26 | .with_searchable_attributes(["name", "title"])
27 | .with_filterable_attributes(["created_at"]);
28 |
29 | // Updating the settings is also an asynchronous operation.
30 | let task = my_index
31 | .set_settings(&settings)
32 | .await
33 | .expect("Could not join the remote server.")
34 | // And here we wait for the operation to execute entirely so we can check any error happened.
35 | .wait_for_completion(&client, None, None)
36 | .await
37 | .expect("Could not join the remote server.");
38 |
39 | // We check if the task failed.
40 | assert!(
41 | !task.is_failure(),
42 | "Could not update the settings. {}",
43 | task.unwrap_failure().error_message
44 | );
45 |
46 | // And finally we delete the `Index`.
47 | my_index
48 | .delete()
49 | .await
50 | .expect("Could not join the remote server.")
51 | .wait_for_completion(&client, None, None)
52 | .await
53 | .expect("Could not join the remote server.");
54 |
55 | // We check if the task failed.
56 | assert!(
57 | !task.is_failure(),
58 | "Could not delete the index. {}",
59 | task.unwrap_failure().error_message
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/examples/web_app/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "web_app"
3 | version = "0.0.0"
4 | authors = ["Mubelotix "]
5 | edition = "2018"
6 | publish = false
7 |
8 | [lib]
9 | crate-type = ["cdylib", "rlib"]
10 |
11 | [dependencies]
12 | serde_json = "1.0"
13 | wasm-bindgen = "0.2"
14 | wasm-bindgen-futures = "0.4.18"
15 | yew = {version="0.21", features = ["csr"]}
16 | meilisearch-sdk = { path="../..", features = ["futures-unsend"] }
17 | lazy_static = "1.4"
18 | serde = {version="1.0", features=["derive"]}
19 | web-sys = "0.3"
20 | console_error_panic_hook = "0.1"
21 |
--------------------------------------------------------------------------------
/examples/web_app/README.md:
--------------------------------------------------------------------------------
1 | # Build your front-end page in Rust with WebAssembly
2 |
3 | > **Note**
4 | > It is not possible to run Meilisearch in the browser without a server. This demo uses the Rust SDK in a browser using WASM, and communicates with a Meilisearch instance that is running on a remote server.
5 |
6 | This example is a clone of [crates.meilisearch.com](https://crates.meilisearch.com), but the front-end is written in Rust!
7 | The Rust source files are compiled into WebAssembly and so can be readable by the browsers.
8 |
9 | ## Checking
10 |
11 | If you only want to check if this example compiles, you can run:
12 |
13 | ```console
14 | cargo build
15 | ```
16 |
17 | ## Building
18 |
19 | To build this example, you need [wasm-pack](https://github.com/rustwasm/wasm-pack).\
20 | You can install `wasm-pack` with this command:
21 | ```console
22 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
23 | ```
24 |
25 | ```console
26 | wasm-pack build . --target=web --no-typescript
27 | ```
28 |
29 | The compiled files will be stored in the `examples/web_app/pkg` folder.
30 |
31 | ## Using
32 |
33 | Theoretically, you could just open the `examples/web_app/pkg/index.html` file but due to browsers' security restrictions, you need a web server. For example:
34 |
35 | ```console
36 | python3 -m http.server 8080
37 | ```
38 |
39 | And then go to the `http://localhost:8080/` URL in your browser.
40 |
--------------------------------------------------------------------------------
/examples/web_app/src/document.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use serde_json::{Map, Value};
3 | use yew::prelude::*;
4 |
5 | #[derive(Debug, Serialize, Deserialize)]
6 | pub struct Crate {
7 | name: String,
8 | downloads: Option,
9 | description: String,
10 | keywords: Vec,
11 | categories: Vec,
12 | readme: String,
13 | version: String,
14 | }
15 |
16 | fn get_readable_download_count(this: &Map) -> String {
17 | if let Some(downloads) = this["downloads"].as_f64() {
18 | if downloads < 1000.0 {
19 | downloads.to_string()
20 | } else if downloads < 1000000.0 {
21 | format!("{:.1}k", downloads / 1000.0)
22 | } else {
23 | format!("{:.1}M", downloads / 1000000.0)
24 | }
25 | } else {
26 | String::from("?")
27 | }
28 | }
29 |
30 | pub fn display(this: &Map) -> Html {
31 | let mut url = format!(
32 | "https://lib.rs/crates/{}",
33 | this["name"].as_str().unwrap_or_default()
34 | );
35 | url = url.replace("", "");
36 | url = url.replace(" ", "");
37 |
38 | html! {
39 |
40 |
41 |
42 | {
43 | // This field is formatted so we don't want Yew to escape the HTML tags
44 | unescaped_html(this["name"].as_str().unwrap_or_default())
45 | }
46 |
47 |
{unescaped_html(this["description"].as_str().unwrap_or_default())}
48 |
49 |
50 |
51 | {"v"}
52 | {&this["version"].as_str().unwrap_or_default()}
53 |
54 |
55 | {get_readable_download_count(this)}
56 |
57 | {for this["keywords"].as_array().unwrap().iter().map(|keyword|
58 | html! {
59 |
60 | {"#"}
61 | {keyword.as_str().unwrap_or_default()}
62 |
63 | }
64 | )}
65 |
66 |
67 |
68 | }
69 | }
70 |
71 | use web_sys::Node;
72 | use yew::virtual_dom::VNode;
73 |
74 | /// Creates an element from raw HTML
75 | fn unescaped_html(html: &str) -> VNode {
76 | let element = web_sys::window()
77 | .unwrap()
78 | .document()
79 | .unwrap()
80 | .create_element("div")
81 | .unwrap();
82 | element.set_inner_html(html);
83 |
84 | VNode::VRef(Node::from(element))
85 | }
86 |
--------------------------------------------------------------------------------
/examples/web_app/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![recursion_limit = "512"]
2 | use lazy_static::lazy_static;
3 | use meilisearch_sdk::client::Client;
4 | use meilisearch_sdk::indexes::Index;
5 | use meilisearch_sdk::search::{SearchResults, Selectors::All};
6 | use serde_json::{Map, Value};
7 | use std::rc::Rc;
8 | use wasm_bindgen::prelude::*;
9 | use wasm_bindgen_futures::spawn_local;
10 | use yew::prelude::*;
11 | use yew::{html::Scope, Html};
12 |
13 | mod document;
14 | use crate::document::{display, Crate};
15 |
16 | lazy_static! {
17 | static ref CLIENT: Client = Client::new("http://localhost:7700", Some("masterKey")).unwrap();
18 | }
19 |
20 | struct Model {
21 | index: Rc,
22 | results: Vec>,
23 | processing_time_ms: usize,
24 |
25 | // These two fields are used to avoid rollbacks by giving an ID to each request
26 | latest_sent_request_id: usize,
27 | displayed_request_id: usize,
28 | }
29 |
30 | enum Msg {
31 | /// An event sent to update the results with a query
32 | Input(String),
33 | /// The event sent to display new results once they are received
34 | Update {
35 | results: Vec>,
36 | processing_time_ms: usize,
37 | request_id: usize,
38 | },
39 | }
40 |
41 | impl Component for Model {
42 | type Message = Msg;
43 | type Properties = ();
44 | fn create(_ctx: &Context) -> Model {
45 | Model {
46 | // The index method avoids checking the existence of the index.
47 | // It won't make any HTTP request so the function is not async so it's easier to use.
48 | // Use only if you are sure that the index exists.
49 | index: Rc::new(CLIENT.index("crates")),
50 | results: Vec::new(),
51 | processing_time_ms: 0,
52 |
53 | latest_sent_request_id: 0,
54 | displayed_request_id: 0,
55 | }
56 | }
57 |
58 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool {
59 | match msg {
60 | // Sent when the value of the text input changed (so we have to make a new request)
61 | Msg::Input(value) => {
62 | let index = Rc::clone(&self.index);
63 | let link = ctx.link().clone();
64 | self.latest_sent_request_id += 1;
65 | let request_id = self.latest_sent_request_id;
66 | // Spawn a task loading results
67 | spawn_local(async move {
68 | // Load the results
69 | let fresh_results: SearchResults = index
70 | .search()
71 | .with_query(&value)
72 | .with_attributes_to_highlight(All)
73 | .execute()
74 | .await
75 | .expect("Failed to execute query");
76 |
77 | let mut fresh_formatted_results = Vec::new();
78 | for result in fresh_results.hits {
79 | fresh_formatted_results.push(result.formatted_result.unwrap());
80 | }
81 |
82 | // We send a new event with the up-to-date data so that we can update the results and display them.
83 | link.send_message(Msg::Update {
84 | results: fresh_formatted_results,
85 | processing_time_ms: fresh_results.processing_time_ms,
86 | request_id,
87 | });
88 | });
89 | false
90 | }
91 |
92 | // Sent when new results are received
93 | Msg::Update {
94 | results,
95 | processing_time_ms,
96 | request_id,
97 | } => {
98 | if request_id >= self.latest_sent_request_id {
99 | self.results = results;
100 | self.processing_time_ms = processing_time_ms;
101 | self.displayed_request_id = request_id;
102 | true
103 | } else {
104 | // We are already displaying more up-to-date results.
105 | // This request is too late so we cannot display these results to avoid rollbacks.
106 | false
107 | }
108 | }
109 | }
110 | }
111 |
112 | fn changed(&mut self, _ctx: &Context, _old_props: &Self::Properties) -> bool {
113 | false
114 | }
115 |
116 | fn view(&self, ctx: &Context) -> Html {
117 | html! {
118 | <>
119 |
120 | {header_content(self.processing_time_ms, ctx.link())}
121 |
122 |
123 |
124 |
125 | {
126 | // Display the results
127 | for self.results.iter().map(display)
128 | }
129 |
130 |
131 |
132 |
137 | >
138 | }
139 | }
140 | }
141 |
142 | fn header_content(processing_time_ms: usize, link: &Scope) -> Html {
143 | html! {
144 |
145 |
{"Meili crates browser 2000"}
146 |
147 | {"This search bar is provided by "}{"Meili"} {", it is a demonstration of our instant search engine."}
148 | {"If you want to take a look at the project source code, it's your lucky day as it is "}{"available on github"} {"."}
149 | {"We wrote a blog post about "}{"how we made this search engine available for you"} {"."}
150 | {"What you are currently using is not the original front end, but a clone using "}{"the Meilisearch Rust SDK"} {" and "}{"Yew"} {". The code is available "}{"here"} {"."}
151 | {"The whole design was taken from "}{"lib.rs"} {" because we love it."}
152 | {"We pull new crates and crates update every "}{"10 minutes"} {" from "}{"docs.rs"} {" and all the downloads count "}{"every day at 3:30 PM UTC"} {" from "}{"crates.io"} {". Currently we have something like "}{" 31 729 crates"} {"."}
153 | {"Have fun using it "} {" "}
154 |
155 |
164 |
165 |
166 | {"Sorted by relevance"}
167 |
168 |
169 |
170 | }
171 | }
172 |
173 | // The main() function of wasm
174 | #[wasm_bindgen(start)]
175 | pub fn run_app() {
176 | console_error_panic_hook::set_once();
177 | yew::Renderer::::new().render();
178 | }
179 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/.env.example.txt:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://[username]:[password]@localhost/[database_name]
2 | MEILISEARCH_HOST=http://localhost:7700
3 | MEILISEARCH_API_KEY=[your-master-key]
4 | MIGRATIONS_DIR_PATH=migrations
--------------------------------------------------------------------------------
/examples/web_app_graphql/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
4 | .env
--------------------------------------------------------------------------------
/examples/web_app_graphql/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "meilisearch-ex"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["Eugene Korir "]
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | actix-cors = "0.7.0"
11 | actix-web = "4.4.0"
12 | async-graphql = "6.0.11"
13 | async-graphql-actix-web = "6.0.11"
14 | diesel = { version = "2.1.4", features = ["postgres"] }
15 | diesel-async = { version = "0.5.0", features = ["postgres", "deadpool"] }
16 | diesel_migrations = "2.1.0"
17 | dotenvy = "0.15.7"
18 | env_logger = "0.11.3"
19 | envy = "0.4.2"
20 | futures = "0.3.29"
21 | log = "0.4.20"
22 | meilisearch-sdk = "0.24.3"
23 | serde = { version = "1.0.192", features = ["derive"] }
24 | serde_json = "1.0.108"
25 | thiserror = "1.0.51"
26 | validator = { version = "0.18.1", features = ["derive"] }
27 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/README.md:
--------------------------------------------------------------------------------
1 | # Meilisearch example with graphql using `diesel`, `async_graphql` and `postgres`
2 |
3 | ## Contents
4 |
5 | Setting up a graphql server using `async_graphql` and `actix-web`
6 |
7 | Using `diesel` to query the database
8 |
9 | Using `meilisearch-sdk` to search for records that match a given criteria
10 |
11 | ## Running the example
12 |
13 | The meilisearch server needs to be running. You can run it by the command below
14 |
15 | ```bash
16 | meilisearch --master-key
17 | ```
18 |
19 | Then you can run the application by simply running
20 |
21 | ```bash
22 | cargo run --release
23 | ```
24 |
25 | The above command will display a link to your running instance and you can simply proceed by clicking the link or navigating to your browser.
26 |
27 | ### Running the resolvers
28 |
29 | On your browser, you will see a graphql playground in which you can use to run some queries
30 |
31 | You can use the `searchUsers` query as follows:
32 |
33 | ```gpl
34 | query {
35 | users{
36 | search(queryString: "Eugene"){
37 | lastName
38 | firstName
39 | email
40 | }
41 | }
42 | }
43 | ```
44 |
45 | ### Errors
46 |
47 | Incase you run into the following error:
48 |
49 | ```bash
50 | = note: ld: library not found for -lpq
51 | clang: error: linker command failed with exit code 1 (use -v to see invocation)
52 | ```
53 |
54 | Run:
55 |
56 | ```bash
57 | sudo apt install libpq-dev
58 | ```
59 |
60 | This should fix the error
61 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/diesel.toml:
--------------------------------------------------------------------------------
1 | # For documentation on how to configure this file,
2 | # see https://diesel.rs/guides/configuring-diesel-cli
3 |
4 | [print_schema]
5 | file = "src/schema.rs"
6 | custom_type_derives = ["diesel::query_builder::QueryId"]
7 |
8 | [migrations_directory]
9 | dir = "migrations"
10 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/migrations/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/meilisearch-rust/90a153c88112cddefc1936d3bef4763907211c2b/examples/web_app_graphql/migrations/.keep
--------------------------------------------------------------------------------
/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/down.sql:
--------------------------------------------------------------------------------
1 | -- This file was automatically created by Diesel to setup helper functions
2 | -- and other internal bookkeeping. This file is safe to edit, any future
3 | -- changes will be added to existing projects as new migrations.
4 |
5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
6 | DROP FUNCTION IF EXISTS diesel_set_updated_at();
7 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/up.sql:
--------------------------------------------------------------------------------
1 | -- This file was automatically created by Diesel to setup helper functions
2 | -- and other internal bookkeeping. This file is safe to edit, any future
3 | -- changes will be added to existing projects as new migrations.
4 |
5 |
6 |
7 |
8 | -- Sets up a trigger for the given table to automatically set a column called
9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included
10 | -- in the modified columns)
11 | --
12 | -- # Example
13 | --
14 | -- ```sql
15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
16 | --
17 | -- SELECT diesel_manage_updated_at('users');
18 | -- ```
19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
20 | BEGIN
21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
23 | END;
24 | $$ LANGUAGE plpgsql;
25 |
26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
27 | BEGIN
28 | IF (
29 | NEW IS DISTINCT FROM OLD AND
30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
31 | ) THEN
32 | NEW.updated_at := current_timestamp;
33 | END IF;
34 | RETURN NEW;
35 | END;
36 | $$ LANGUAGE plpgsql;
37 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/migrations/2023-12-20-105441_users/down.sql:
--------------------------------------------------------------------------------
1 | -- This file should undo anything in `up.sql`
2 | drop table if exists users;
--------------------------------------------------------------------------------
/examples/web_app_graphql/migrations/2023-12-20-105441_users/up.sql:
--------------------------------------------------------------------------------
1 | -- Your SQL goes here
2 | create table if not exists users(
3 | id serial primary key,
4 | first_name varchar not null,
5 | last_name varchar not null,
6 | email varchar not null unique
7 | );
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/app_env_vars.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | //Environment variables required for the app to run
4 | #[derive(Deserialize, Debug, Clone)]
5 | pub struct AppEnvVars {
6 | pub meilisearch_api_key: String,
7 | pub meilisearch_host: String,
8 | pub database_url: String,
9 | pub migrations_dir_path: String,
10 | }
11 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/errors/mod.rs:
--------------------------------------------------------------------------------
1 | use diesel::ConnectionError;
2 | use diesel_async::pooled_connection::deadpool::{BuildError, PoolError};
3 | use diesel_migrations::MigrationError;
4 | use serde_json::Error as SerdeError;
5 | use thiserror::Error;
6 |
7 | #[derive(Debug, Error)]
8 | pub enum ApplicationError {
9 | #[error("Missing environment variable")]
10 | Envy(#[from] envy::Error),
11 | #[error("Input/Output error")]
12 | Io(#[from] std::io::Error),
13 | #[error("Database error")]
14 | Diesel(#[from] diesel::result::Error),
15 | #[error("Deadpool build error")]
16 | DeadpoolBuild(#[from] BuildError),
17 | #[error("Migration error")]
18 | Migration(#[from] MigrationError),
19 | #[error("Connection error")]
20 | DieselConnection(#[from] ConnectionError),
21 | #[error("Pool Error")]
22 | Pool(#[from] PoolError),
23 | #[error("Serde json error")]
24 | SerDe(#[from] SerdeError),
25 | }
26 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/graphql_schema/mod.rs:
--------------------------------------------------------------------------------
1 | use async_graphql::SimpleObject;
2 | pub mod users;
3 |
4 | use users::mutation::UsersMut;
5 | use users::query::UsersQuery;
6 |
7 | #[derive(Default, SimpleObject)]
8 | pub struct Query {
9 | users: UsersQuery,
10 | }
11 |
12 | #[derive(Default, SimpleObject)]
13 | pub struct Mutation {
14 | users: UsersMut,
15 | }
16 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/graphql_schema/users/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod mutation;
2 | pub mod query;
3 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/graphql_schema/users/mutation/add_user.rs:
--------------------------------------------------------------------------------
1 | use async_graphql::{Context, InputObject, Object, Result};
2 | use diesel_async::RunQueryDsl;
3 | use validator::Validate;
4 |
5 | use crate::{
6 | models::{NewUser, User},
7 | validate_input, GraphQlData,
8 | };
9 |
10 | #[derive(Default)]
11 | pub struct AddUser;
12 |
13 | #[derive(InputObject, Validate)]
14 | pub struct IAddUser {
15 | #[validate(length(min = 1))]
16 | pub first_name: String,
17 | #[validate(length(min = 1))]
18 | pub last_name: String,
19 | #[validate(email)]
20 | pub email: String,
21 | }
22 |
23 | #[Object]
24 | impl AddUser {
25 | ///Resolver for creating a new user and storing that data in the database
26 | ///
27 | /// The mutation can be run as follows
28 | /// ```gpl
29 | /// mutation AddUser{
30 | /// users {
31 | /// signup(input: {firstName: "",lastName: "",email: ""}){
32 | /// id
33 | /// firstName
34 | /// lastName
35 | /// email
36 | /// }
37 | /// }
38 | /// }
39 | pub async fn signup(&self, ctx: &Context<'_>, input: IAddUser) -> Result {
40 | validate_input(&input)?;
41 |
42 | use crate::schema::users::dsl::users;
43 |
44 | let GraphQlData { pool, .. } = ctx.data().map_err(|e| {
45 | log::error!("Failed to get app data: {:?}", e);
46 | e
47 | })?;
48 |
49 | let mut connection = pool.get().await?;
50 |
51 | let value = NewUser {
52 | first_name: input.first_name,
53 | last_name: input.last_name,
54 | email: input.email,
55 | };
56 |
57 | let result = diesel::insert_into(users)
58 | .values(&value)
59 | .get_result::(&mut connection)
60 | .await
61 | .map_err(|e| {
62 | log::error!("Could not create new user: {:#?}", e);
63 | e
64 | })?;
65 |
66 | Ok(result)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/graphql_schema/users/mutation/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod add_user;
2 |
3 | use add_user::AddUser;
4 | use async_graphql::MergedObject;
5 |
6 | //Combines user queries into one struct
7 | #[derive(Default, MergedObject)]
8 | pub struct UsersMut(pub AddUser);
9 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/graphql_schema/users/query/get_users.rs:
--------------------------------------------------------------------------------
1 | use async_graphql::{Context, Object, Result};
2 | use diesel_async::RunQueryDsl;
3 |
4 | use crate::{models::User, GraphQlData};
5 |
6 | #[derive(Default)]
7 | pub struct GetUsers;
8 |
9 | #[Object]
10 | impl GetUsers {
11 | //Resolver for querying the database for user records
12 | pub async fn get_users(&self, ctx: &Context<'_>) -> Result> {
13 | use crate::schema::users::dsl::users;
14 |
15 | let GraphQlData { pool, .. } = ctx.data().map_err(|e| {
16 | log::error!("Failed to get app data: {:?}", e);
17 | e
18 | })?;
19 |
20 | let mut connection = pool.get().await?;
21 |
22 | let list_users = users.load::(&mut connection).await?;
23 |
24 | Ok(list_users)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/graphql_schema/users/query/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod get_users;
2 | pub mod search;
3 |
4 | use async_graphql::MergedObject;
5 | use get_users::GetUsers;
6 | use search::SearchUsers;
7 |
8 | //Combines user queries into one struct
9 | #[derive(Default, MergedObject)]
10 | pub struct UsersQuery(pub GetUsers, pub SearchUsers);
11 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/graphql_schema/users/query/search.rs:
--------------------------------------------------------------------------------
1 | use async_graphql::{Context, Object, Result};
2 | use diesel_async::RunQueryDsl;
3 | use meilisearch_sdk::search::{SearchQuery, SearchResults};
4 |
5 | use crate::{models::User, GraphQlData};
6 |
7 | #[derive(Default)]
8 | pub struct SearchUsers;
9 |
10 | #[Object]
11 | impl SearchUsers {
12 | async fn search(&self, ctx: &Context<'_>, query_string: String) -> Result> {
13 | use crate::schema::users::dsl::users;
14 |
15 | let GraphQlData { pool, client } = ctx.data().map_err(|e| {
16 | log::error!("Failed to get app data: {:?}", e);
17 | e
18 | })?;
19 |
20 | let mut connection = pool.get().await?;
21 |
22 | let list_users = users.load::(&mut connection).await?;
23 |
24 | match client.get_index("users").await {
25 | //If getting the index is successful, we add documents to it
26 | Ok(index) => {
27 | index.add_documents(&list_users, Some("id")).await?;
28 | }
29 |
30 | //If getting the index fails, we create it and then add documents to the new index
31 | Err(_) => {
32 | let task = client.create_index("users", Some("id")).await?;
33 | let task = task.wait_for_completion(client, None, None).await?;
34 | let index = task.try_make_index(client).unwrap();
35 |
36 | index.add_documents(&list_users, Some("id")).await?;
37 | }
38 | }
39 |
40 | let index = client.get_index("users").await?;
41 |
42 | //We build the query
43 | let query = SearchQuery::new(&index).with_query(&query_string).build();
44 |
45 | let results: SearchResults = index.execute_query(&query).await?;
46 |
47 | //Tranform the results into a type that implements OutputType
48 | //Required for return types to implement this trait
49 | let search_results: Vec = results
50 | .hits
51 | .into_iter()
52 | .map(|hit| User {
53 | id: hit.result.id,
54 | email: hit.result.email,
55 | first_name: hit.result.first_name,
56 | last_name: hit.result.last_name,
57 | })
58 | .collect();
59 |
60 | Ok(search_results)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod app_env_vars;
2 | pub mod errors;
3 | mod graphql_schema;
4 | mod models;
5 | mod schema;
6 |
7 | use actix_web::{web, HttpResponse, Result};
8 | use app_env_vars::AppEnvVars;
9 | use async_graphql::{
10 | http::GraphiQLSource, EmptySubscription, Error, Result as GraphqlResult, Schema,
11 | };
12 | use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
13 | use diesel_async::{
14 | pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager},
15 | AsyncPgConnection,
16 | };
17 | use errors::ApplicationError;
18 | use graphql_schema::{Mutation, Query};
19 | use meilisearch_sdk::Client as SearchClient;
20 | use validator::Validate;
21 |
22 | pub type ApplicationSchema = Schema;
23 |
24 | /// Represents application data passed to graphql resolvers
25 | pub struct GraphQlData {
26 | pub pool: Pool,
27 | pub client: SearchClient,
28 | }
29 |
30 | pub async fn index_graphiql() -> Result {
31 | Ok(HttpResponse::Ok()
32 | .content_type("text/html; charset=utf-8")
33 | .body(GraphiQLSource::build().endpoint("/").finish()))
34 | }
35 |
36 | pub async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse {
37 | let req_inner = req.into_inner();
38 |
39 | schema.execute(req_inner).await.into()
40 | }
41 |
42 | /// We build the graphql schema and any data required to be passed to all resolvers
43 | pub fn build_schema(app_env_vars: &AppEnvVars) -> Result {
44 | let client = SearchClient::new(
45 | &app_env_vars.meilisearch_host,
46 | Some(&app_env_vars.meilisearch_api_key),
47 | );
48 |
49 | let config = AsyncDieselConnectionManager::::new(&app_env_vars.database_url);
50 | let pool = Pool::builder(config).build()?;
51 |
52 | let schema_data = GraphQlData { pool, client };
53 |
54 | Ok(
55 | Schema::build(Query::default(), Mutation::default(), EmptySubscription)
56 | .data(schema_data)
57 | .finish(),
58 | )
59 | }
60 |
61 | /// Helper function for returning an error if inputs do not match the set conditions
62 | pub fn validate_input(input: &T) -> GraphqlResult<()> {
63 | if let Err(e) = input.validate() {
64 | log::error!("Validation error: {}", e);
65 | let err = serde_json::to_string(&e).unwrap();
66 | let err = Error::from(err);
67 | return Err(err);
68 | }
69 | Ok(())
70 | }
71 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/main.rs:
--------------------------------------------------------------------------------
1 | use actix_cors::Cors;
2 | use actix_web::middleware::Logger;
3 | use actix_web::web;
4 | use actix_web::{guard, App, HttpServer};
5 | use diesel::migration::MigrationSource;
6 | use diesel::{Connection, PgConnection};
7 | use diesel_migrations::FileBasedMigrations;
8 | use meilisearch_ex::{
9 | app_env_vars::AppEnvVars, build_schema, errors::ApplicationError, index, index_graphiql,
10 | };
11 |
12 | #[actix_web::main]
13 | async fn main() -> Result<(), ApplicationError> {
14 | let _ = dotenvy::dotenv();
15 |
16 | let app_env_vars = envy::from_env::()?;
17 |
18 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
19 |
20 | //Run migrations on app start
21 | let mut db_connection = PgConnection::establish(&app_env_vars.database_url)?;
22 | let mut migrations = FileBasedMigrations::from_path(&app_env_vars.migrations_dir_path)?
23 | .migrations()
24 | .unwrap();
25 |
26 | migrations.sort_by_key(|m| m.name().to_string());
27 |
28 | for migration in migrations {
29 | migration.run(&mut db_connection).unwrap();
30 | }
31 |
32 | let schema = build_schema(&app_env_vars)?;
33 |
34 | println!("GraphiQL IDE: http://localhost:8081");
35 |
36 | HttpServer::new(move || {
37 | App::new()
38 | .wrap(Logger::default())
39 | .wrap(
40 | Cors::default()
41 | .allow_any_origin()
42 | .allow_any_method()
43 | .allow_any_header()
44 | .max_age(3600)
45 | .supports_credentials(),
46 | )
47 | //Add schema to application `Data` extractor
48 | .app_data(web::Data::new(schema.clone()))
49 | .service(web::resource("/").guard(guard::Post()).to(index))
50 | .service(web::resource("/").guard(guard::Get()).to(index_graphiql))
51 | })
52 | .bind("0.0.0.0:8081")?
53 | .run()
54 | .await?;
55 |
56 | Ok(())
57 | }
58 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/models.rs:
--------------------------------------------------------------------------------
1 | use async_graphql::SimpleObject;
2 | use diesel::{prelude::Insertable, Queryable, Selectable};
3 | use serde::{Deserialize, Serialize};
4 |
5 | use crate::schema::users;
6 |
7 | //Struct that corresponds to our database structure for users table
8 | #[derive(SimpleObject, Deserialize, Serialize, Queryable, Selectable, Debug)]
9 | #[diesel(table_name = users)]
10 | pub struct User {
11 | pub id: i32,
12 | pub first_name: String,
13 | pub last_name: String,
14 | pub email: String,
15 | }
16 |
17 | #[derive(Insertable, Debug)]
18 | #[diesel(table_name = users)]
19 | pub struct NewUser {
20 | pub first_name: String,
21 | pub last_name: String,
22 | pub email: String,
23 | }
24 |
--------------------------------------------------------------------------------
/examples/web_app_graphql/src/schema.rs:
--------------------------------------------------------------------------------
1 | // @generated automatically by Diesel CLI.
2 |
3 | diesel::table! {
4 | users (id) {
5 | id -> Int4,
6 | first_name -> Varchar,
7 | last_name -> Varchar,
8 | email -> Varchar,
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/meilisearch-index-setting-macro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "meilisearch-index-setting-macro"
3 | version = "0.28.0"
4 | description = "Helper tool to generate settings of a Meilisearch index"
5 | edition = "2021"
6 | license = "MIT"
7 | repository = "https://github.com/meilisearch/meilisearch-rust"
8 |
9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10 |
11 | [lib]
12 | proc-macro = true
13 |
14 | [dependencies]
15 | syn = { version = "2.0.48", features = ["extra-traits"] }
16 | quote = "1.0.21"
17 | proc-macro2 = "1.0.46"
18 | convert_case = "0.6.0"
19 | structmeta = "0.3"
20 |
--------------------------------------------------------------------------------
/meilisearch-index-setting-macro/src/lib.rs:
--------------------------------------------------------------------------------
1 | use convert_case::{Case, Casing};
2 | use proc_macro2::Ident;
3 | use quote::quote;
4 | use structmeta::{Flag, StructMeta};
5 | use syn::{parse_macro_input, spanned::Spanned};
6 |
7 | #[derive(Clone, StructMeta, Default)]
8 | struct FieldAttrs {
9 | primary_key: Flag,
10 | displayed: Flag,
11 | searchable: Flag,
12 | distinct: Flag,
13 | filterable: Flag,
14 | sortable: Flag,
15 | }
16 |
17 | #[proc_macro_derive(IndexConfig, attributes(index_config))]
18 | pub fn generate_index_settings(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
19 | let ast = parse_macro_input!(input as syn::DeriveInput);
20 |
21 | let fields: &syn::Fields = match ast.data {
22 | syn::Data::Struct(ref data) => &data.fields,
23 | _ => {
24 | return proc_macro::TokenStream::from(
25 | syn::Error::new(ast.ident.span(), "Applicable only to struct").to_compile_error(),
26 | );
27 | }
28 | };
29 |
30 | let struct_ident = &ast.ident;
31 |
32 | let index_config_implementation = get_index_config_implementation(struct_ident, fields);
33 | proc_macro::TokenStream::from(quote! {
34 | #index_config_implementation
35 | })
36 | }
37 |
38 | fn get_index_config_implementation(
39 | struct_ident: &Ident,
40 | fields: &syn::Fields,
41 | ) -> proc_macro2::TokenStream {
42 | let mut primary_key_attribute = String::new();
43 | let mut distinct_key_attribute = String::new();
44 | let mut displayed_attributes = vec![];
45 | let mut searchable_attributes = vec![];
46 | let mut filterable_attributes = vec![];
47 | let mut sortable_attributes = vec![];
48 |
49 | let index_name = struct_ident
50 | .to_string()
51 | .from_case(Case::UpperCamel)
52 | .to_case(Case::Snake);
53 |
54 | let mut primary_key_found = false;
55 | let mut distinct_found = false;
56 |
57 | for field in fields {
58 | let attrs = field
59 | .attrs
60 | .iter()
61 | .filter(|attr| attr.path().is_ident("index_config"))
62 | .map(|attr| attr.parse_args::().unwrap())
63 | .collect::>()
64 | .first()
65 | .cloned()
66 | .unwrap_or_default();
67 |
68 | // Check if the primary key field is unique
69 | if attrs.primary_key.value() {
70 | if primary_key_found {
71 | return syn::Error::new(
72 | field.span(),
73 | "Only one field can be marked as primary key",
74 | )
75 | .to_compile_error();
76 | }
77 | primary_key_attribute = field.ident.clone().unwrap().to_string();
78 | primary_key_found = true;
79 | }
80 |
81 | // Check if the distinct field is unique
82 | if attrs.distinct.value() {
83 | if distinct_found {
84 | return syn::Error::new(field.span(), "Only one field can be marked as distinct")
85 | .to_compile_error();
86 | }
87 | distinct_key_attribute = field.ident.clone().unwrap().to_string();
88 | distinct_found = true;
89 | }
90 |
91 | if attrs.displayed.value() {
92 | displayed_attributes.push(field.ident.clone().unwrap().to_string());
93 | }
94 |
95 | if attrs.searchable.value() {
96 | searchable_attributes.push(field.ident.clone().unwrap().to_string());
97 | }
98 |
99 | if attrs.filterable.value() {
100 | filterable_attributes.push(field.ident.clone().unwrap().to_string());
101 | }
102 |
103 | if attrs.sortable.value() {
104 | sortable_attributes.push(field.ident.clone().unwrap().to_string());
105 | }
106 | }
107 |
108 | let primary_key_token: proc_macro2::TokenStream = if primary_key_attribute.is_empty() {
109 | quote! {
110 | ::std::option::Option::None
111 | }
112 | } else {
113 | quote! {
114 | ::std::option::Option::Some(#primary_key_attribute)
115 | }
116 | };
117 |
118 | let display_attr_tokens =
119 | get_settings_token_for_list(&displayed_attributes, "with_displayed_attributes");
120 | let sortable_attr_tokens =
121 | get_settings_token_for_list(&sortable_attributes, "with_sortable_attributes");
122 | let filterable_attr_tokens =
123 | get_settings_token_for_list(&filterable_attributes, "with_filterable_attributes");
124 | let searchable_attr_tokens =
125 | get_settings_token_for_list(&searchable_attributes, "with_searchable_attributes");
126 | let distinct_attr_token = get_settings_token_for_string_for_some_string(
127 | &distinct_key_attribute,
128 | "with_distinct_attribute",
129 | );
130 |
131 | quote! {
132 | #[::meilisearch_sdk::macro_helper::async_trait(?Send)]
133 | impl ::meilisearch_sdk::documents::IndexConfig for #struct_ident {
134 | const INDEX_STR: &'static str = #index_name;
135 |
136 | fn generate_settings() -> ::meilisearch_sdk::settings::Settings {
137 | ::meilisearch_sdk::settings::Settings::new()
138 | #display_attr_tokens
139 | #sortable_attr_tokens
140 | #filterable_attr_tokens
141 | #searchable_attr_tokens
142 | #distinct_attr_token
143 | }
144 |
145 | async fn generate_index(client: &::meilisearch_sdk::client::Client) -> std::result::Result<::meilisearch_sdk::indexes::Index, ::meilisearch_sdk::tasks::Task> {
146 | return client.create_index(#index_name, #primary_key_token)
147 | .await.unwrap()
148 | .wait_for_completion(&client, ::std::option::Option::None, ::std::option::Option::None)
149 | .await.unwrap()
150 | .try_make_index(&client);
151 | }
152 | }
153 | }
154 | }
155 |
156 | fn get_settings_token_for_list(
157 | field_name_list: &[String],
158 | method_name: &str,
159 | ) -> proc_macro2::TokenStream {
160 | let string_attributes = field_name_list.iter().map(|attr| {
161 | quote! {
162 | #attr
163 | }
164 | });
165 | let method_ident = Ident::new(method_name, proc_macro2::Span::call_site());
166 |
167 | if field_name_list.is_empty() {
168 | quote! {
169 | .#method_ident(::std::iter::empty::<&str>())
170 | }
171 | } else {
172 | quote! {
173 | .#method_ident([#(#string_attributes),*])
174 | }
175 | }
176 | }
177 |
178 | fn get_settings_token_for_string_for_some_string(
179 | field_name: &String,
180 | method_name: &str,
181 | ) -> proc_macro2::TokenStream {
182 | let method_ident = Ident::new(method_name, proc_macro2::Span::call_site());
183 |
184 | if field_name.is_empty() {
185 | proc_macro2::TokenStream::new()
186 | } else {
187 | quote! {
188 | .#method_ident(::std::option::Option::Some(#field_name))
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/meilisearch-test-macro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "meilisearch-test-macro"
3 | version = "0.0.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [lib]
8 | proc-macro = true
9 |
10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11 |
12 | [dependencies]
13 | proc-macro2 = "1.0.0"
14 | quote = "1.0.0"
15 | syn = { version = "2.0.48", features = ["clone-impls", "full", "parsing", "printing", "proc-macro"], default-features = false }
16 |
--------------------------------------------------------------------------------
/meilisearch-test-macro/README.md:
--------------------------------------------------------------------------------
1 | # Meilisearch test macro
2 |
3 | This crate defines the `meilisearch_test` macro.
4 |
5 | Since the code is a little bit harsh to read, here is a complete explanation of how to use it.
6 | The macro aims to ease the writing of tests by:
7 |
8 | 1. Reducing the amount of code you need to write and maintain for each test.
9 | 2. Ensuring All your indexes as a unique name so they can all run in parallel.
10 | 3. Ensuring you never forget to delete your index if you need one.
11 |
12 | Before explaining its usage, we're going to see a simple test _before_ this macro:
13 |
14 | ```rust
15 | #[async_test]
16 | async fn test_get_tasks() -> Result<(), Error> {
17 | let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);
18 |
19 | let index = client
20 | .create_index("test_get_tasks", None)
21 | .await?
22 | .wait_for_completion(&client, None, None)
23 | .await?
24 | .try_make_index(&client)
25 | .unwrap();
26 |
27 | let tasks = index.get_tasks().await?;
28 | // The only task is the creation of the index
29 | assert_eq!(status.results.len(), 1);
30 |
31 | index.delete()
32 | .await?
33 | .wait_for_completion(&client, None, None)
34 | .await?;
35 | Ok(())
36 | }
37 | ```
38 |
39 | I have multiple problems with this test:
40 |
41 | - `let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);`: This line is always the same in every test.
42 | And if you make a typo on the http addr or the master key, you'll have an error.
43 | - `let index = client.create_index("test_get_tasks", None)...`: Each test needs to have an unique name.
44 | This means we currently need to write the name of the test everywhere; it's not practical.
45 | - There are 11 lines dedicated to the creation and deletion of the index; this is once again something that'll never change
46 | whatever the test is. But, if you ever forget to delete the index at the end, you'll get in some trouble to re-run
47 | the tests.
48 |
49 | ---
50 |
51 | With this macro, all these problems are solved. See a rewrite of this test:
52 |
53 | ```rust
54 | #[meilisearch_test]
55 | async fn test_get_tasks(index: Index, client: Client) -> Result<(), Error> {
56 | let tasks = index.get_tasks().await?;
57 | // The only task is the creation of the index
58 | assert_eq!(status.results.len(), 1);
59 | }
60 | ```
61 |
62 | So now you're probably seeing what happened. By using an index and a client in the parameter of
63 | the test, the macro automatically did the same thing we've seen before.
64 | There are a few rules, though:
65 |
66 | 1. The macro only handles three types of arguments:
67 |
68 | - `String`: It returns the name of the test.
69 | - `Client`: It creates a client like that: `Client::new("http://localhost:7700", "masterKey")`.
70 | - `Index`: It creates and deletes an index, as we've seen before.
71 |
72 | 2. You only get what you asked for. That means if you don't ask for an index, no index will be created in meilisearch.
73 | So, if you are testing the creation of indexes, you can ask for a `Client` and a `String` and then create it yourself.
74 | The index won't be present in meilisearch.
75 | 3. You can put your parameters in the order you want it won't change anything.
76 | 4. Everything you use **must** be in scope directly. If you're using an `Index`, you must write `Index` in the parameters,
77 | not `meilisearch_rust::Index` or `crate::Index`.
78 | 5. And I think that's all, use and abuse it 🎉
79 |
--------------------------------------------------------------------------------
/meilisearch-test-macro/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![recursion_limit = "4096"]
2 |
3 | extern crate proc_macro;
4 |
5 | use proc_macro::TokenStream;
6 | use proc_macro2::Span;
7 | use quote::quote;
8 | use syn::{
9 | parse_macro_input, parse_quote, Expr, FnArg, Ident, Item, PatType, Path, Stmt, Type, TypePath,
10 | Visibility,
11 | };
12 |
13 | #[proc_macro_attribute]
14 | pub fn meilisearch_test(params: TokenStream, input: TokenStream) -> TokenStream {
15 | assert!(
16 | params.is_empty(),
17 | "the #[async_test] attribute currently does not take parameters"
18 | );
19 |
20 | let mut inner = parse_macro_input!(input as Item);
21 | let mut outer = inner.clone();
22 | if let (&mut Item::Fn(ref mut inner_fn), &mut Item::Fn(ref mut outer_fn)) =
23 | (&mut inner, &mut outer)
24 | {
25 | #[derive(Debug, PartialEq, Eq)]
26 | enum Param {
27 | Client,
28 | Index,
29 | String,
30 | }
31 |
32 | inner_fn.sig.ident = Ident::new(
33 | &("_inner_meilisearch_test_macro_".to_string() + &inner_fn.sig.ident.to_string()),
34 | Span::call_site(),
35 | );
36 | let inner_ident = &inner_fn.sig.ident;
37 | inner_fn.vis = Visibility::Inherited;
38 | inner_fn.attrs.clear();
39 | assert!(
40 | outer_fn.sig.asyncness.take().is_some(),
41 | "#[meilisearch_test] can only be applied to async functions"
42 | );
43 |
44 | let mut params = Vec::new();
45 |
46 | let parameters = &inner_fn.sig.inputs;
47 | for param in parameters {
48 | match param {
49 | FnArg::Typed(PatType { ty, .. }) => match &**ty {
50 | Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "String" => {
51 | params.push(Param::String);
52 | }
53 | Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "Index" => {
54 | params.push(Param::Index);
55 | }
56 | Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "Client" => {
57 | params.push(Param::Client);
58 | }
59 | // TODO: throw this error while pointing to the specific token
60 | ty => panic!(
61 | "#[meilisearch_test] can only receive Client, Index or String as parameters but received {ty:?}"
62 | ),
63 | },
64 | // TODO: throw this error while pointing to the specific token
65 | // Used `self` as a parameter
66 | FnArg::Receiver(_) => panic!(
67 | "#[meilisearch_test] can only receive Client, Index or String as parameters"
68 | ),
69 | }
70 | }
71 |
72 | // if a `Client` or an `Index` was asked for the test we must create a meilisearch `Client`.
73 | let use_client = params
74 | .iter()
75 | .any(|param| matches!(param, Param::Client | Param::Index));
76 | // if a `String` or an `Index` was asked then we need to extract the name of the test function.
77 | let use_name = params
78 | .iter()
79 | .any(|param| matches!(param, Param::String | Param::Index));
80 | let use_index = params.contains(&Param::Index);
81 |
82 | // Now we are going to build the body of the outer function
83 | let mut outer_block: Vec = Vec::new();
84 |
85 | // First we need to check if a client will be used and create it if it’s the case
86 | if use_client {
87 | outer_block.push(parse_quote!(
88 | let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
89 | ));
90 | outer_block.push(parse_quote!(
91 | let meilisearch_api_key = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
92 | ));
93 | outer_block.push(parse_quote!(
94 | let client = Client::new(meilisearch_url, Some(meilisearch_api_key)).unwrap();
95 | ));
96 | }
97 |
98 | // Now we do the same for the index name
99 | if use_name {
100 | let fn_name = &outer_fn.sig.ident;
101 | // the name we're going to return is the complete path to the function i.e., something like that;
102 | // `indexes::tests::test_fetch_info` but since the `::` are not allowed by meilisearch as an index
103 | // name we're going to rename that to `indexes-tests-test_fetch_info`.
104 | outer_block.push(parse_quote!(
105 | let name = format!("{}::{}", std::module_path!(), stringify!(#fn_name)).replace("::", "-");
106 | ));
107 | }
108 |
109 | // And finally if an index was asked, we delete it, and we (re)create it and wait until meilisearch confirm its creation.
110 | if use_index {
111 | outer_block.push(parse_quote!({
112 | let res = client
113 | .delete_index(&name)
114 | .await
115 | .expect("Network issue while sending the delete index task")
116 | .wait_for_completion(&client, None, None)
117 | .await
118 | .expect("Network issue while waiting for the index deletion");
119 | if res.is_failure() {
120 | let error = res.unwrap_failure();
121 | assert_eq!(
122 | error.error_code,
123 | crate::errors::ErrorCode::IndexNotFound,
124 | "{:?}",
125 | error
126 | );
127 | }
128 | }));
129 |
130 | outer_block.push(parse_quote!(
131 | let index = client
132 | .create_index(&name, None)
133 | .await
134 | .expect("Network issue while sending the create index task")
135 | .wait_for_completion(&client, None, None)
136 | .await
137 | .expect("Network issue while waiting for the index creation")
138 | .try_make_index(&client)
139 | .expect("Could not create the index out of the create index task");
140 | ));
141 | }
142 |
143 | // Create a list of params separated by comma with the name we defined previously.
144 | let params: Vec = params
145 | .into_iter()
146 | .map(|param| match param {
147 | Param::Client => parse_quote!(client),
148 | Param::Index => parse_quote!(index),
149 | Param::String => parse_quote!(name),
150 | })
151 | .collect();
152 |
153 | // Now we can call the user code with our parameters :tada:
154 | outer_block.push(parse_quote!(
155 | let result = #inner_ident(#(#params.clone()),*).await;
156 | ));
157 |
158 | // And right before the end, if an index was created and the tests successfully executed we delete it.
159 | if use_index {
160 | outer_block.push(parse_quote!(
161 | index
162 | .delete()
163 | .await
164 | .expect("Network issue while sending the last delete index task");
165 | // we early exit the test here and let meilisearch handle the deletion asynchronously
166 | ));
167 | }
168 |
169 | // Finally, for the great finish we just return the result the user gave us.
170 | outer_block.push(parse_quote!(return result;));
171 |
172 | outer_fn.sig.inputs.clear();
173 | outer_fn.sig.asyncness = inner_fn.sig.asyncness;
174 | outer_fn.attrs.push(parse_quote!(#[tokio::test]));
175 | outer_fn.block.stmts = outer_block;
176 | } else {
177 | panic!("#[meilisearch_test] can only be applied to async functions")
178 | }
179 | quote!(
180 | #inner
181 | #outer
182 | )
183 | .into()
184 | }
185 |
--------------------------------------------------------------------------------
/scripts/check-readme.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Checking that cargo is installed
4 | command -v cargo > /dev/null 2>&1
5 | if [ "$?" -ne 0 ]; then
6 | echo 'You must install cargo to make this script working.'
7 | echo 'See https://doc.rust-lang.org/cargo/getting-started/installation.html'
8 | exit 1
9 | fi
10 |
11 | # Installing cargo-readme if it's not installed yet
12 | cargo install cargo-readme
13 |
14 | # Comparing the generated README and the current one
15 | current_readme="README.md"
16 | generated_readme="README.md_tmp"
17 | cargo readme > "$generated_readme"
18 |
19 | # Exiting with the right message
20 | echo ''
21 | diff "$current_readme" "$generated_readme" > /dev/null 2>&1
22 | if [ "$?" = 0 ]; then
23 | echo "OK"
24 | rm -f "$generated_readme"
25 | exit 0
26 | else
27 | echo "The current README.md is not up-to-date with the template."
28 |
29 | # Displaying the diff if the --diff flag is activated
30 | if [ "$1" = '--diff' ]; then
31 | echo 'Diff found:'
32 | diff "$current_readme" "$generated_readme"
33 | else
34 | echo 'To see the diff, run:'
35 | echo ' $ sh scripts/check-readme.sh --diff'
36 | echo 'To update the README, run:'
37 | echo ' $ sh scripts/update-readme.sh'
38 | fi
39 |
40 | rm -f "$generated_readme"
41 | exit 1
42 | fi
43 |
--------------------------------------------------------------------------------
/scripts/update-readme.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Checking that cargo is installed
4 | command -v cargo > /dev/null 2>&1
5 | if [ "$?" -ne 0 ]; then
6 | echo 'You must install cargo to make this script working.'
7 | echo 'See https://doc.rust-lang.org/cargo/getting-started/installation.html'
8 | exit
9 | fi
10 |
11 | # Installing cargo-readme if it's not installed yet
12 | cargo install cargo-readme
13 |
14 | # Generating the README.md file
15 | cargo readme > README.md
16 |
--------------------------------------------------------------------------------
/scripts/update_macro_versions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | new_version=$(grep '^version = ' Cargo.toml)
3 |
4 | # Updates the versions in meilisearch-rust and meilisearch-index-setting-macro of the latter, with the latest meilisearch-rust version.
5 |
6 | old_index_macro_version=$(grep '^version = ' ./meilisearch-index-setting-macro/Cargo.toml)
7 | old_macro_in_sdk_version=$(grep '{ path = "meilisearch-index-setting-macro", version =' ./Cargo.toml)
8 |
9 | sed -i '' -e "s/^$old_index_macro_version/$new_version/g" './meilisearch-index-setting-macro/Cargo.toml'
10 | sed -i '' -e "s/$old_macro_in_sdk_version/meilisearch-index-setting-macro = { path = \"meilisearch-index-setting-macro\", $new_version }/g" './Cargo.toml'
11 |
--------------------------------------------------------------------------------
/src/documents.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use serde::{de::DeserializeOwned, Deserialize, Serialize};
3 |
4 | /// Derive the [`IndexConfig`] trait.
5 | ///
6 | /// ## Field attribute
7 | /// Use the `#[index_config(..)]` field attribute to generate the correct settings
8 | /// for each field. The available parameters are:
9 | /// - `primary_key` (can only be used once)
10 | /// - `distinct` (can only be used once)
11 | /// - `searchable`
12 | /// - `displayed`
13 | /// - `filterable`
14 | /// - `sortable`
15 | ///
16 | /// ## Index name
17 | /// The name of the index will be the name of the struct converted to snake case.
18 | ///
19 | /// ## Sample usage:
20 | /// ```
21 | /// use serde::{Serialize, Deserialize};
22 | /// use meilisearch_sdk::documents::IndexConfig;
23 | /// use meilisearch_sdk::settings::Settings;
24 | /// use meilisearch_sdk::indexes::Index;
25 | /// use meilisearch_sdk::client::Client;
26 | ///
27 | /// #[derive(Serialize, Deserialize, IndexConfig)]
28 | /// struct Movie {
29 | /// #[index_config(primary_key)]
30 | /// movie_id: u64,
31 | /// #[index_config(displayed, searchable)]
32 | /// title: String,
33 | /// #[index_config(displayed)]
34 | /// description: String,
35 | /// #[index_config(filterable, sortable, displayed)]
36 | /// release_date: String,
37 | /// #[index_config(filterable, displayed)]
38 | /// genres: Vec,
39 | /// }
40 | ///
41 | /// async fn usage(client: Client) {
42 | /// // Default settings with the distinct, searchable, displayed, filterable, and sortable fields set correctly.
43 | /// let settings: Settings = Movie::generate_settings();
44 | /// // Index created with the name `movie` and the primary key set to `movie_id`
45 | /// let index: Index = Movie::generate_index(&client).await.unwrap();
46 | /// }
47 | /// ```
48 | pub use meilisearch_index_setting_macro::IndexConfig;
49 |
50 | use crate::client::Client;
51 | use crate::request::HttpClient;
52 | use crate::settings::Settings;
53 | use crate::task_info::TaskInfo;
54 | use crate::tasks::Task;
55 | use crate::{errors::Error, indexes::Index};
56 |
57 | #[async_trait(?Send)]
58 | pub trait IndexConfig {
59 | const INDEX_STR: &'static str;
60 |
61 | #[must_use]
62 | fn index(client: &Client) -> Index {
63 | client.index(Self::INDEX_STR)
64 | }
65 | fn generate_settings() -> Settings;
66 | async fn generate_index(client: &Client) -> Result, Task>;
67 | }
68 |
69 | #[derive(Debug, Clone, Deserialize)]
70 | pub struct DocumentsResults {
71 | pub results: Vec,
72 | pub limit: u32,
73 | pub offset: u32,
74 | pub total: u32,
75 | }
76 |
77 | #[derive(Debug, Clone, Serialize)]
78 | pub struct DocumentQuery<'a, Http: HttpClient> {
79 | #[serde(skip_serializing)]
80 | pub index: &'a Index,
81 |
82 | /// The fields that should appear in the documents. By default, all of the fields are present.
83 | #[serde(skip_serializing_if = "Option::is_none")]
84 | pub fields: Option>,
85 | }
86 |
87 | impl<'a, Http: HttpClient> DocumentQuery<'a, Http> {
88 | #[must_use]
89 | pub fn new(index: &Index) -> DocumentQuery {
90 | DocumentQuery {
91 | index,
92 | fields: None,
93 | }
94 | }
95 |
96 | /// Specify the fields to return in the document.
97 | ///
98 | /// # Example
99 | ///
100 | /// ```
101 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
102 | /// #
103 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
104 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
105 | /// #
106 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
107 | /// let index = client.index("document_query_with_fields");
108 | /// let mut document_query = DocumentQuery::new(&index);
109 | ///
110 | /// document_query.with_fields(["title"]);
111 | /// ```
112 | pub fn with_fields(
113 | &mut self,
114 | fields: impl IntoIterator- ,
115 | ) -> &mut DocumentQuery<'a, Http> {
116 | self.fields = Some(fields.into_iter().collect());
117 | self
118 | }
119 |
120 | /// Execute the get document query.
121 | ///
122 | /// # Example
123 | ///
124 | /// ```
125 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
126 | /// # use serde::{Deserialize, Serialize};
127 | /// #
128 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
129 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
130 | /// #
131 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
132 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
133 | /// #[derive(Debug, Serialize, Deserialize, PartialEq)]
134 | /// struct MyObject {
135 | /// id: String,
136 | /// kind: String,
137 | /// }
138 | ///
139 | /// #[derive(Debug, Serialize, Deserialize, PartialEq)]
140 | /// struct MyObjectReduced {
141 | /// id: String,
142 | /// }
143 | /// # let index = client.index("document_query_execute");
144 | /// # index.add_or_replace(&[MyObject{id:"1".to_string(), kind:String::from("a kind")},MyObject{id:"2".to_string(), kind:String::from("some kind")}], None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
145 | ///
146 | /// let document = DocumentQuery::new(&index).with_fields(["id"])
147 | /// .execute::
("1")
148 | /// .await
149 | /// .unwrap();
150 | ///
151 | /// assert_eq!(
152 | /// document,
153 | /// MyObjectReduced { id: "1".to_string() }
154 | /// );
155 | /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
156 | /// # });
157 | pub async fn execute(
158 | &self,
159 | document_id: &str,
160 | ) -> Result {
161 | self.index.get_document_with::(document_id, self).await
162 | }
163 | }
164 |
165 | #[derive(Debug, Clone, Serialize)]
166 | pub struct DocumentsQuery<'a, Http: HttpClient> {
167 | #[serde(skip_serializing)]
168 | pub index: &'a Index,
169 |
170 | /// The number of documents to skip.
171 | ///
172 | /// If the value of the parameter `offset` is `n`, the `n` first documents will not be returned.
173 | /// This is helpful for pagination.
174 | ///
175 | /// Example: If you want to skip the first document, set offset to `1`.
176 | #[serde(skip_serializing_if = "Option::is_none")]
177 | pub offset: Option,
178 |
179 | /// The maximum number of documents returned.
180 | /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response.
181 | /// This is helpful for pagination.
182 | ///
183 | /// Example: If you don't want to get more than two documents, set limit to `2`.
184 | ///
185 | /// **Default: `20`**
186 | #[serde(skip_serializing_if = "Option::is_none")]
187 | pub limit: Option,
188 |
189 | /// The fields that should appear in the documents. By default, all of the fields are present.
190 | #[serde(skip_serializing_if = "Option::is_none")]
191 | pub fields: Option>,
192 |
193 | /// Filters to apply.
194 | ///
195 | /// Available since v1.2 of Meilisearch
196 | /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax.
197 | #[serde(skip_serializing_if = "Option::is_none")]
198 | pub filter: Option<&'a str>,
199 | }
200 |
201 | impl<'a, Http: HttpClient> DocumentsQuery<'a, Http> {
202 | #[must_use]
203 | pub fn new(index: &Index) -> DocumentsQuery {
204 | DocumentsQuery {
205 | index,
206 | offset: None,
207 | limit: None,
208 | fields: None,
209 | filter: None,
210 | }
211 | }
212 |
213 | /// Specify the offset.
214 | ///
215 | /// # Example
216 | ///
217 | /// ```
218 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
219 | /// #
220 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
221 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
222 | /// #
223 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
224 | /// let index = client.index("my_index");
225 | ///
226 | /// let mut documents_query = DocumentsQuery::new(&index).with_offset(1);
227 | /// ```
228 | pub fn with_offset(&mut self, offset: usize) -> &mut DocumentsQuery<'a, Http> {
229 | self.offset = Some(offset);
230 | self
231 | }
232 |
233 | /// Specify the limit.
234 | ///
235 | /// # Example
236 | ///
237 | /// ```
238 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
239 | /// #
240 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
241 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
242 | /// #
243 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
244 | /// let index = client.index("my_index");
245 | ///
246 | /// let mut documents_query = DocumentsQuery::new(&index);
247 | ///
248 | /// documents_query.with_limit(1);
249 | /// ```
250 | pub fn with_limit(&mut self, limit: usize) -> &mut DocumentsQuery<'a, Http> {
251 | self.limit = Some(limit);
252 | self
253 | }
254 |
255 | /// Specify the fields to return in the documents.
256 | ///
257 | /// # Example
258 | ///
259 | /// ```
260 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
261 | /// #
262 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
263 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
264 | /// #
265 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
266 | /// let index = client.index("my_index");
267 | ///
268 | /// let mut documents_query = DocumentsQuery::new(&index);
269 | ///
270 | /// documents_query.with_fields(["title"]);
271 | /// ```
272 | pub fn with_fields(
273 | &mut self,
274 | fields: impl IntoIterator- ,
275 | ) -> &mut DocumentsQuery<'a, Http> {
276 | self.fields = Some(fields.into_iter().collect());
277 | self
278 | }
279 |
280 | pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentsQuery<'a, Http> {
281 | self.filter = Some(filter);
282 | self
283 | }
284 |
285 | /// Execute the get documents query.
286 | ///
287 | /// # Example
288 | ///
289 | /// ```
290 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
291 | /// # use serde::{Deserialize, Serialize};
292 | /// #
293 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
294 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
295 | /// #
296 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
297 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
298 | /// # let index = client.create_index("documents_query_execute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
299 | /// #[derive(Debug, Serialize, Deserialize, PartialEq)]
300 | /// struct MyObject {
301 | /// id: Option
,
302 | /// kind: String,
303 | /// }
304 | /// let index = client.index("documents_query_execute");
305 | ///
306 | /// let document = DocumentsQuery::new(&index)
307 | /// .with_offset(1)
308 | /// .execute::()
309 | /// .await
310 | /// .unwrap();
311 | ///
312 | /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
313 | /// # });
314 | /// ```
315 | pub async fn execute(
316 | &self,
317 | ) -> Result, Error> {
318 | self.index.get_documents_with::(self).await
319 | }
320 | }
321 |
322 | #[derive(Debug, Clone, Serialize)]
323 | pub struct DocumentDeletionQuery<'a, Http: HttpClient> {
324 | #[serde(skip_serializing)]
325 | pub index: &'a Index,
326 |
327 | /// Filters to apply.
328 | ///
329 | /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax.
330 | pub filter: Option<&'a str>,
331 | }
332 |
333 | impl<'a, Http: HttpClient> DocumentDeletionQuery<'a, Http> {
334 | #[must_use]
335 | pub fn new(index: &Index) -> DocumentDeletionQuery {
336 | DocumentDeletionQuery {
337 | index,
338 | filter: None,
339 | }
340 | }
341 |
342 | pub fn with_filter<'b>(
343 | &'b mut self,
344 | filter: &'a str,
345 | ) -> &'b mut DocumentDeletionQuery<'a, Http> {
346 | self.filter = Some(filter);
347 | self
348 | }
349 |
350 | pub async fn execute(&self) -> Result {
351 | self.index.delete_documents_with(self).await
352 | }
353 | }
354 |
355 | #[cfg(test)]
356 | mod tests {
357 | use super::*;
358 | use crate::{client::Client, errors::*, indexes::*};
359 | use meilisearch_test_macro::meilisearch_test;
360 | use serde::{Deserialize, Serialize};
361 |
362 | #[derive(Debug, Serialize, Deserialize, PartialEq)]
363 | struct MyObject {
364 | id: Option,
365 | kind: String,
366 | }
367 |
368 | #[allow(unused)]
369 | #[derive(IndexConfig)]
370 | struct MovieClips {
371 | #[index_config(primary_key)]
372 | movie_id: u64,
373 | #[index_config(distinct)]
374 | owner: String,
375 | #[index_config(displayed, searchable)]
376 | title: String,
377 | #[index_config(displayed)]
378 | description: String,
379 | #[index_config(filterable, sortable, displayed)]
380 | release_date: String,
381 | #[index_config(filterable, displayed)]
382 | genres: Vec,
383 | }
384 |
385 | #[allow(unused)]
386 | #[derive(IndexConfig)]
387 | struct VideoClips {
388 | video_id: u64,
389 | }
390 |
391 | async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
392 | let t0 = index
393 | .add_documents(
394 | &[
395 | MyObject {
396 | id: Some(0),
397 | kind: "text".into(),
398 | },
399 | MyObject {
400 | id: Some(1),
401 | kind: "text".into(),
402 | },
403 | MyObject {
404 | id: Some(2),
405 | kind: "title".into(),
406 | },
407 | MyObject {
408 | id: Some(3),
409 | kind: "title".into(),
410 | },
411 | ],
412 | None,
413 | )
414 | .await?;
415 |
416 | t0.wait_for_completion(client, None, None).await?;
417 |
418 | Ok(())
419 | }
420 |
421 | #[meilisearch_test]
422 | async fn test_get_documents_with_execute(client: Client, index: Index) -> Result<(), Error> {
423 | setup_test_index(&client, &index).await?;
424 | let documents = DocumentsQuery::new(&index)
425 | .with_limit(1)
426 | .with_offset(1)
427 | .with_fields(["kind"])
428 | .execute::()
429 | .await
430 | .unwrap();
431 |
432 | assert_eq!(documents.limit, 1);
433 | assert_eq!(documents.offset, 1);
434 | assert_eq!(documents.results.len(), 1);
435 |
436 | Ok(())
437 | }
438 |
439 | #[meilisearch_test]
440 | async fn test_delete_documents_with(client: Client, index: Index) -> Result<(), Error> {
441 | setup_test_index(&client, &index).await?;
442 | index
443 | .set_filterable_attributes(["id"])
444 | .await?
445 | .wait_for_completion(&client, None, None)
446 | .await?;
447 |
448 | let mut query = DocumentDeletionQuery::new(&index);
449 | query.with_filter("id = 1");
450 | index
451 | .delete_documents_with(&query)
452 | .await?
453 | .wait_for_completion(&client, None, None)
454 | .await?;
455 | let document_result = index.get_document::("1").await;
456 |
457 | match document_result {
458 | Ok(_) => panic!("The test was expecting no documents to be returned but got one."),
459 | Err(e) => match e {
460 | Error::Meilisearch(err) => {
461 | assert_eq!(err.error_code, ErrorCode::DocumentNotFound);
462 | }
463 | _ => panic!("The error was expected to be a Meilisearch error, but it was not."),
464 | },
465 | }
466 |
467 | Ok(())
468 | }
469 |
470 | #[meilisearch_test]
471 | async fn test_delete_documents_with_filter_not_filterable(
472 | client: Client,
473 | index: Index,
474 | ) -> Result<(), Error> {
475 | setup_test_index(&client, &index).await?;
476 |
477 | let mut query = DocumentDeletionQuery::new(&index);
478 | query.with_filter("id = 1");
479 | let error = index
480 | .delete_documents_with(&query)
481 | .await?
482 | .wait_for_completion(&client, None, None)
483 | .await?;
484 |
485 | let error = error.unwrap_failure();
486 |
487 | assert!(matches!(
488 | error,
489 | MeilisearchError {
490 | error_code: ErrorCode::InvalidDocumentFilter,
491 | error_type: ErrorType::InvalidRequest,
492 | ..
493 | }
494 | ));
495 |
496 | Ok(())
497 | }
498 |
499 | #[meilisearch_test]
500 | async fn test_get_documents_with_only_one_param(
501 | client: Client,
502 | index: Index,
503 | ) -> Result<(), Error> {
504 | setup_test_index(&client, &index).await?;
505 | // let documents = index.get_documents(None, None, None).await.unwrap();
506 | let documents = DocumentsQuery::new(&index)
507 | .with_limit(1)
508 | .execute::()
509 | .await
510 | .unwrap();
511 |
512 | assert_eq!(documents.limit, 1);
513 | assert_eq!(documents.offset, 0);
514 | assert_eq!(documents.results.len(), 1);
515 |
516 | Ok(())
517 | }
518 |
519 | #[meilisearch_test]
520 | async fn test_get_documents_with_filter(client: Client, index: Index) -> Result<(), Error> {
521 | setup_test_index(&client, &index).await?;
522 |
523 | index
524 | .set_filterable_attributes(["id"])
525 | .await
526 | .unwrap()
527 | .wait_for_completion(&client, None, None)
528 | .await
529 | .unwrap();
530 |
531 | let documents = DocumentsQuery::new(&index)
532 | .with_filter("id = 1")
533 | .execute::()
534 | .await?;
535 |
536 | assert_eq!(documents.results.len(), 1);
537 |
538 | Ok(())
539 | }
540 |
541 | #[meilisearch_test]
542 | async fn test_get_documents_with_error_hint() -> Result<(), Error> {
543 | let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
544 | let client = Client::new(format!("{meilisearch_url}/hello"), Some("masterKey")).unwrap();
545 | let index = client.index("test_get_documents_with_filter_wrong_ms_version");
546 |
547 | let documents = DocumentsQuery::new(&index)
548 | .with_filter("id = 1")
549 | .execute::()
550 | .await;
551 |
552 | let error = documents.unwrap_err();
553 |
554 | let message = Some("Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.".to_string());
555 | let url = format!(
556 | "{meilisearch_url}/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch"
557 | );
558 | let status_code = 404;
559 | let displayed_error = format!("MeilisearchCommunicationError: The server responded with a 404. Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.\nurl: {meilisearch_url}/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch");
560 |
561 | match &error {
562 | Error::MeilisearchCommunication(error) => {
563 | assert_eq!(error.status_code, status_code);
564 | assert_eq!(error.message, message);
565 | assert_eq!(error.url, url);
566 | }
567 | _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."),
568 | };
569 | assert_eq!(format!("{error}"), displayed_error);
570 |
571 | Ok(())
572 | }
573 |
574 | #[meilisearch_test]
575 | async fn test_get_documents_with_error_hint_meilisearch_api_error(
576 | index: Index,
577 | client: Client,
578 | ) -> Result<(), Error> {
579 | setup_test_index(&client, &index).await?;
580 |
581 | let error = DocumentsQuery::new(&index)
582 | .with_filter("id = 1")
583 | .execute::()
584 | .await
585 | .unwrap_err();
586 |
587 | let message = "Attribute `id` is not filterable. This index does not have configured filterable attributes.
588 | 1:3 id = 1
589 | Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.".to_string();
590 | let displayed_error = "Meilisearch invalid_request: invalid_document_filter: Attribute `id` is not filterable. This index does not have configured filterable attributes.
591 | 1:3 id = 1
592 | Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.. https://docs.meilisearch.com/errors#invalid_document_filter";
593 |
594 | match &error {
595 | Error::Meilisearch(error) => {
596 | assert_eq!(error.error_message, message);
597 | }
598 | _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."),
599 | };
600 | assert_eq!(format!("{error}"), displayed_error);
601 |
602 | Ok(())
603 | }
604 |
605 | #[meilisearch_test]
606 | async fn test_get_documents_with_invalid_filter(
607 | client: Client,
608 | index: Index,
609 | ) -> Result<(), Error> {
610 | setup_test_index(&client, &index).await?;
611 |
612 | // Does not work because `id` is not filterable
613 | let error = DocumentsQuery::new(&index)
614 | .with_filter("id = 1")
615 | .execute::()
616 | .await
617 | .unwrap_err();
618 |
619 | assert!(matches!(
620 | error,
621 | Error::Meilisearch(MeilisearchError {
622 | error_code: ErrorCode::InvalidDocumentFilter,
623 | error_type: ErrorType::InvalidRequest,
624 | ..
625 | })
626 | ));
627 |
628 | Ok(())
629 | }
630 |
631 | #[meilisearch_test]
632 | async fn test_settings_generated_by_macro(client: Client, index: Index) -> Result<(), Error> {
633 | setup_test_index(&client, &index).await?;
634 |
635 | let movie_settings: Settings = MovieClips::generate_settings();
636 | let video_settings: Settings = VideoClips::generate_settings();
637 |
638 | assert_eq!(movie_settings.searchable_attributes.unwrap(), ["title"]);
639 | assert!(video_settings.searchable_attributes.unwrap().is_empty());
640 |
641 | assert_eq!(
642 | movie_settings.displayed_attributes.unwrap(),
643 | ["title", "description", "release_date", "genres"]
644 | );
645 | assert!(video_settings.displayed_attributes.unwrap().is_empty());
646 |
647 | assert_eq!(
648 | movie_settings.filterable_attributes.unwrap(),
649 | ["release_date", "genres"]
650 | );
651 | assert!(video_settings.filterable_attributes.unwrap().is_empty());
652 |
653 | assert_eq!(
654 | movie_settings.sortable_attributes.unwrap(),
655 | ["release_date"]
656 | );
657 | assert!(video_settings.sortable_attributes.unwrap().is_empty());
658 |
659 | Ok(())
660 | }
661 |
662 | #[meilisearch_test]
663 | async fn test_generate_index(client: Client) -> Result<(), Error> {
664 | let index: Index = MovieClips::generate_index(&client).await.unwrap();
665 |
666 | assert_eq!(index.uid, "movie_clips");
667 |
668 | index
669 | .delete()
670 | .await?
671 | .wait_for_completion(&client, None, None)
672 | .await?;
673 |
674 | Ok(())
675 | }
676 | #[derive(Serialize, Deserialize, IndexConfig)]
677 | struct Movie {
678 | #[index_config(primary_key)]
679 | movie_id: u64,
680 | #[index_config(displayed, searchable)]
681 | title: String,
682 | #[index_config(displayed)]
683 | description: String,
684 | #[index_config(filterable, sortable, displayed)]
685 | release_date: String,
686 | #[index_config(filterable, displayed)]
687 | genres: Vec,
688 | }
689 | }
690 |
--------------------------------------------------------------------------------
/src/dumps.rs:
--------------------------------------------------------------------------------
1 | //! The `dumps` module allows the creation of database dumps.
2 | //!
3 | //! - Dumps are `.dump` files that can be used to launch Meilisearch.
4 | //!
5 | //! - Dumps are compatible between Meilisearch versions.
6 | //!
7 | //! - Creating a dump is also referred to as exporting it, whereas launching Meilisearch with a dump is referred to as importing it.
8 | //!
9 | //! - During a [dump export](crate::client::Client::create_dump), all [indexes](crate::indexes::Index) of the current instance are exported—together with their documents and settings—and saved as a single `.dump` file.
10 | //!
11 | //! - During a dump import, all indexes contained in the indicated `.dump` file are imported along with their associated documents and [settings](crate::settings::Settings).
12 | //! Any existing [index](crate::indexes::Index) with the same uid as an index in the dump file will be overwritten.
13 | //!
14 | //! - Dump imports are [performed at launch](https://www.meilisearch.com/docs/learn/configuration/instance_options#import-dump) using an option.
15 | //!
16 | //! # Example
17 | //!
18 | //! ```
19 | //! # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*};
20 | //! # use futures_await_test::async_test;
21 | //! # use std::{thread::sleep, time::Duration};
22 | //! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
23 | //! #
24 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
25 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
26 | //! #
27 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
28 | //!
29 | //! // Create a dump
30 | //! let task_info = client.create_dump().await.unwrap();
31 | //! assert!(matches!(
32 | //! task_info,
33 | //! TaskInfo {
34 | //! update_type: TaskType::DumpCreation { .. },
35 | //! ..
36 | //! }
37 | //! ));
38 | //! # });
39 | //! ```
40 |
41 | use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo};
42 |
43 | /// Dump related methods.
44 | /// See the [dumps](crate::dumps) module.
45 | impl Client {
46 | /// Triggers a dump creation process.
47 | ///
48 | /// Once the process is complete, a dump is created in the [dumps directory](https://www.meilisearch.com/docs/learn/configuration/instance_options#dump-directory).
49 | /// If the dumps directory does not exist yet, it will be created.
50 | ///
51 | /// # Example
52 | ///
53 | /// ```
54 | /// # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*};
55 | /// # use futures_await_test::async_test;
56 | /// # use std::{thread::sleep, time::Duration};
57 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
58 | /// #
59 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
60 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
61 | /// #
62 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
63 | /// #
64 | /// let task_info = client.create_dump().await.unwrap();
65 | ///
66 | /// assert!(matches!(
67 | /// task_info,
68 | /// TaskInfo {
69 | /// update_type: TaskType::DumpCreation { .. },
70 | /// ..
71 | /// }
72 | /// ));
73 | /// # });
74 | /// ```
75 | pub async fn create_dump(&self) -> Result {
76 | self.http_client
77 | .request::<(), (), TaskInfo>(
78 | &format!("{}/dumps", self.host),
79 | Method::Post {
80 | query: (),
81 | body: (),
82 | },
83 | 202,
84 | )
85 | .await
86 | }
87 | }
88 |
89 | /// Alias for [`create_dump`](Client::create_dump).
90 | pub async fn create_dump(client: &Client) -> Result {
91 | client.create_dump().await
92 | }
93 |
94 | #[cfg(test)]
95 | mod tests {
96 | use super::*;
97 | use crate::{client::*, tasks::*};
98 | use meilisearch_test_macro::meilisearch_test;
99 | use std::time::Duration;
100 |
101 | #[meilisearch_test]
102 | async fn test_dumps_success_creation(client: Client) -> Result<(), Error> {
103 | let task = client
104 | .create_dump()
105 | .await?
106 | .wait_for_completion(
107 | &client,
108 | Some(Duration::from_millis(1)),
109 | Some(Duration::from_millis(6000)),
110 | )
111 | .await?;
112 |
113 | assert!(matches!(task, Task::Succeeded { .. }));
114 | Ok(())
115 | }
116 |
117 | #[meilisearch_test]
118 | async fn test_dumps_correct_update_type(client: Client) -> Result<(), Error> {
119 | let task_info = client.create_dump().await.unwrap();
120 |
121 | assert!(matches!(
122 | task_info,
123 | TaskInfo {
124 | update_type: TaskType::DumpCreation { .. },
125 | ..
126 | }
127 | ));
128 | Ok(())
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/errors.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use thiserror::Error;
3 |
4 | /// An enum representing the errors that can occur.
5 |
6 | #[derive(Debug, Error)]
7 | #[non_exhaustive]
8 | pub enum Error {
9 | /// The exhaustive list of Meilisearch errors:
10 | ///
11 | /// Also check out:
12 | #[error(transparent)]
13 | Meilisearch(#[from] MeilisearchError),
14 |
15 | #[error(transparent)]
16 | MeilisearchCommunication(#[from] MeilisearchCommunicationError),
17 | /// The Meilisearch server returned an invalid JSON for a request.
18 | #[error("Error parsing response JSON: {}", .0)]
19 | ParseError(#[from] serde_json::Error),
20 |
21 | /// A timeout happened while waiting for an update to complete.
22 | #[error("A task did not succeed in time.")]
23 | Timeout,
24 | /// This Meilisearch SDK generated an invalid request (which was not sent).
25 | ///
26 | /// It probably comes from an invalid API key resulting in an invalid HTTP header.
27 | #[error("Unable to generate a valid HTTP request. It probably comes from an invalid API key.")]
28 | InvalidRequest,
29 |
30 | /// Can't call this method without setting an api key in the client.
31 | #[error("You need to provide an api key to use the `{0}` method.")]
32 | CantUseWithoutApiKey(String),
33 | /// It is not possible to generate a tenant token with an invalid api key.
34 | ///
35 | /// Empty strings or with less than 8 characters are considered invalid.
36 | #[error("The provided api_key is invalid.")]
37 | TenantTokensInvalidApiKey,
38 | /// It is not possible to generate an already expired tenant token.
39 | #[error("The provided expires_at is already expired.")]
40 | TenantTokensExpiredSignature,
41 |
42 | /// When jsonwebtoken cannot generate the token successfully.
43 | #[cfg(not(target_arch = "wasm32"))]
44 | #[error("Impossible to generate the token, jsonwebtoken encountered an error: {}", .0)]
45 | InvalidTenantToken(#[from] jsonwebtoken::errors::Error),
46 |
47 | /// The http client encountered an error.
48 | #[cfg(feature = "reqwest")]
49 | #[error("HTTP request failed: {}", .0)]
50 | HttpError(#[from] reqwest::Error),
51 |
52 | /// The library formatting the query parameters encountered an error.
53 | #[error("Internal Error: could not parse the query parameters: {}", .0)]
54 | Yaup(#[from] yaup::Error),
55 |
56 | /// The library validating the format of an uuid.
57 | #[cfg(not(target_arch = "wasm32"))]
58 | #[error("The uid of the token has bit an uuid4 format: {}", .0)]
59 | Uuid(#[from] uuid::Error),
60 |
61 | /// Error thrown in case the version of the Uuid is not v4.
62 | #[error("The uid provided to the token is not of version uuidv4")]
63 | InvalidUuid4Version,
64 |
65 | #[error(transparent)]
66 | Other(Box),
67 | }
68 |
69 | #[derive(Debug, Clone, Deserialize, Error)]
70 | #[serde(rename_all = "camelCase")]
71 | pub struct MeilisearchCommunicationError {
72 | pub status_code: u16,
73 | pub message: Option,
74 | pub url: String,
75 | }
76 |
77 | impl std::fmt::Display for MeilisearchCommunicationError {
78 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 | write!(
80 | f,
81 | "MeilisearchCommunicationError: The server responded with a {}.",
82 | self.status_code
83 | )?;
84 | if let Some(message) = &self.message {
85 | write!(f, " {message}")?;
86 | }
87 | write!(f, "\nurl: {}", self.url)?;
88 | Ok(())
89 | }
90 | }
91 |
92 | #[derive(Debug, Clone, Deserialize, Error)]
93 | #[serde(rename_all = "camelCase")]
94 | #[error("Meilisearch {}: {}: {}. {}", .error_type, .error_code, .error_message, .error_link)]
95 | pub struct MeilisearchError {
96 | /// The human readable error message
97 | #[serde(rename = "message")]
98 | pub error_message: String,
99 | /// The error code of the error. Officially documented at
100 | /// .
101 | #[serde(rename = "code")]
102 | pub error_code: ErrorCode,
103 | /// The type of error (invalid request, internal error, or authentication error)
104 | #[serde(rename = "type")]
105 | pub error_type: ErrorType,
106 | /// A link to the Meilisearch documentation for an error.
107 | #[serde(rename = "link")]
108 | pub error_link: String,
109 | }
110 |
111 | /// The type of error that was encountered.
112 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113 | #[serde(rename_all = "snake_case")]
114 | #[non_exhaustive]
115 | pub enum ErrorType {
116 | /// The submitted request was invalid.
117 | InvalidRequest,
118 | /// The Meilisearch instance encountered an internal error.
119 | Internal,
120 | /// Authentication was either incorrect or missing.
121 | Auth,
122 |
123 | /// That's unexpected. Please open a GitHub issue after ensuring you are
124 | /// using the supported version of the Meilisearch server.
125 | #[serde(other)]
126 | Unknown,
127 | }
128 |
129 | impl std::fmt::Display for ErrorType {
130 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
131 | write!(
132 | fmt,
133 | "{}",
134 | // this can't fail
135 | serde_json::to_value(self).unwrap().as_str().unwrap()
136 | )
137 | }
138 | }
139 |
140 | /// The error code.
141 | ///
142 | /// Officially documented at .
143 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144 | #[serde(rename_all = "snake_case")]
145 | #[non_exhaustive]
146 | pub enum ErrorCode {
147 | IndexCreationFailed,
148 | IndexAlreadyExists,
149 | IndexNotFound,
150 | InvalidIndexUid,
151 | InvalidState,
152 | PrimaryKeyInferenceFailed,
153 | IndexPrimaryKeyAlreadyPresent,
154 | InvalidStoreFile,
155 | MaxFieldsLimitExceeded,
156 | MissingDocumentId,
157 | InvalidDocumentId,
158 | BadParameter,
159 | BadRequest,
160 | DatabaseSizeLimitReached,
161 | DocumentNotFound,
162 | InternalError,
163 | InvalidApiKey,
164 | MissingAuthorizationHeader,
165 | TaskNotFound,
166 | DumpNotFound,
167 | MissingMasterKey,
168 | NoSpaceLeftOnDevice,
169 | PayloadTooLarge,
170 | UnretrievableDocument,
171 | SearchError,
172 | UnsupportedMediaType,
173 | DumpAlreadyProcessing,
174 | DumpProcessFailed,
175 | MissingContentType,
176 | MalformedPayload,
177 | InvalidContentType,
178 | MissingPayload,
179 | InvalidApiKeyDescription,
180 | InvalidApiKeyActions,
181 | InvalidApiKeyIndexes,
182 | InvalidApiKeyExpiresAt,
183 | ApiKeyNotFound,
184 | MissingTaskFilters,
185 | MissingIndexUid,
186 | InvalidIndexOffset,
187 | InvalidIndexLimit,
188 | InvalidIndexPrimaryKey,
189 | InvalidDocumentFilter,
190 | MissingDocumentFilter,
191 | InvalidDocumentFields,
192 | InvalidDocumentLimit,
193 | InvalidDocumentOffset,
194 | InvalidDocumentGeoField,
195 | InvalidSearchQ,
196 | InvalidSearchOffset,
197 | InvalidSearchLimit,
198 | InvalidSearchPage,
199 | InvalidSearchHitsPerPage,
200 | InvalidSearchAttributesToRetrieve,
201 | InvalidSearchAttributesToCrop,
202 | InvalidSearchCropLength,
203 | InvalidSearchAttributesToHighlight,
204 | InvalidSearchShowMatchesPosition,
205 | InvalidSearchFilter,
206 | InvalidSearchSort,
207 | InvalidSearchFacets,
208 | InvalidSearchHighlightPreTag,
209 | InvalidSearchHighlightPostTag,
210 | InvalidSearchCropMarker,
211 | InvalidSearchMatchingStrategy,
212 | ImmutableApiKeyUid,
213 | ImmutableApiKeyActions,
214 | ImmutableApiKeyIndexes,
215 | ImmutableExpiresAt,
216 | ImmutableCreatedAt,
217 | ImmutableUpdatedAt,
218 | InvalidSwapDuplicateIndexFound,
219 | InvalidSwapIndexes,
220 | MissingSwapIndexes,
221 | InvalidTaskTypes,
222 | InvalidTaskUids,
223 | InvalidTaskStatuses,
224 | InvalidTaskLimit,
225 | InvalidTaskFrom,
226 | InvalidTaskCanceledBy,
227 | InvalidTaskFilters,
228 | TooManyOpenFiles,
229 | IoError,
230 | InvalidTaskIndexUids,
231 | ImmutableIndexUid,
232 | ImmutableIndexCreatedAt,
233 | ImmutableIndexUpdatedAt,
234 | InvalidSettingsDisplayedAttributes,
235 | InvalidSettingsSearchableAttributes,
236 | InvalidSettingsFilterableAttributes,
237 | InvalidSettingsSortableAttributes,
238 | InvalidSettingsRankingRules,
239 | InvalidSettingsStopWords,
240 | InvalidSettingsSynonyms,
241 | InvalidSettingsDistinctAttributes,
242 | InvalidSettingsTypoTolerance,
243 | InvalidSettingsFaceting,
244 | InvalidSettingsDictionary,
245 | InvalidSettingsPagination,
246 | InvalidTaskBeforeEnqueuedAt,
247 | InvalidTaskAfterEnqueuedAt,
248 | InvalidTaskBeforeStartedAt,
249 | InvalidTaskAfterStartedAt,
250 | InvalidTaskBeforeFinishedAt,
251 | InvalidTaskAfterFinishedAt,
252 | MissingApiKeyActions,
253 | MissingApiKeyIndexes,
254 | MissingApiKeyExpiresAt,
255 | InvalidApiKeyLimit,
256 | InvalidApiKeyOffset,
257 |
258 | /// That's unexpected. Please open a GitHub issue after ensuring you are
259 | /// using the supported version of the Meilisearch server.
260 | #[serde(other)]
261 | Unknown,
262 | }
263 |
264 | pub const MEILISEARCH_VERSION_HINT: &str = "Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method";
265 |
266 | impl std::fmt::Display for ErrorCode {
267 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
268 | write!(
269 | fmt,
270 | "{}",
271 | // this can't fail
272 | serde_json::to_value(self).unwrap().as_str().unwrap()
273 | )
274 | }
275 | }
276 |
277 | #[cfg(test)]
278 | mod test {
279 | use super::*;
280 |
281 | use jsonwebtoken::errors::ErrorKind::InvalidToken;
282 | use meilisearch_test_macro::meilisearch_test;
283 | use uuid::Uuid;
284 |
285 | #[meilisearch_test]
286 | async fn test_meilisearch_error() {
287 | let error: MeilisearchError = serde_json::from_str(
288 | r#"
289 | {
290 | "message": "The cool error message.",
291 | "code": "index_creation_failed",
292 | "type": "internal",
293 | "link": "https://the best link ever"
294 | }"#,
295 | )
296 | .unwrap();
297 |
298 | assert_eq!(error.error_message, "The cool error message.");
299 | assert_eq!(error.error_code, ErrorCode::IndexCreationFailed);
300 | assert_eq!(error.error_type, ErrorType::Internal);
301 | assert_eq!(error.error_link, "https://the best link ever");
302 |
303 | let error: MeilisearchError = serde_json::from_str(
304 | r#"
305 | {
306 | "message": "",
307 | "code": "An unknown error",
308 | "type": "An unknown type",
309 | "link": ""
310 | }"#,
311 | )
312 | .unwrap();
313 |
314 | assert_eq!(error.error_code, ErrorCode::Unknown);
315 | assert_eq!(error.error_type, ErrorType::Unknown);
316 | }
317 |
318 | #[meilisearch_test]
319 | async fn test_error_message_parsing() {
320 | let error: MeilisearchError = serde_json::from_str(
321 | r#"
322 | {
323 | "message": "The cool error message.",
324 | "code": "index_creation_failed",
325 | "type": "internal",
326 | "link": "https://the best link ever"
327 | }"#,
328 | )
329 | .unwrap();
330 |
331 | assert_eq!(error.to_string(), "Meilisearch internal: index_creation_failed: The cool error message.. https://the best link ever");
332 |
333 | let error: MeilisearchCommunicationError = MeilisearchCommunicationError {
334 | status_code: 404,
335 | message: Some("Hint: something.".to_string()),
336 | url: "http://localhost:7700/something".to_string(),
337 | };
338 |
339 | assert_eq!(
340 | error.to_string(),
341 | "MeilisearchCommunicationError: The server responded with a 404. Hint: something.\nurl: http://localhost:7700/something"
342 | );
343 |
344 | let error: MeilisearchCommunicationError = MeilisearchCommunicationError {
345 | status_code: 404,
346 | message: None,
347 | url: "http://localhost:7700/something".to_string(),
348 | };
349 |
350 | assert_eq!(
351 | error.to_string(),
352 | "MeilisearchCommunicationError: The server responded with a 404.\nurl: http://localhost:7700/something"
353 | );
354 |
355 | let error = Error::Timeout;
356 | assert_eq!(error.to_string(), "A task did not succeed in time.");
357 |
358 | let error = Error::InvalidRequest;
359 | assert_eq!(
360 | error.to_string(),
361 | "Unable to generate a valid HTTP request. It probably comes from an invalid API key."
362 | );
363 |
364 | let error = Error::TenantTokensInvalidApiKey;
365 | assert_eq!(error.to_string(), "The provided api_key is invalid.");
366 |
367 | let error = Error::TenantTokensExpiredSignature;
368 | assert_eq!(
369 | error.to_string(),
370 | "The provided expires_at is already expired."
371 | );
372 |
373 | let error = Error::InvalidUuid4Version;
374 | assert_eq!(
375 | error.to_string(),
376 | "The uid provided to the token is not of version uuidv4"
377 | );
378 |
379 | let error = Error::Uuid(Uuid::parse_str("67e55044").unwrap_err());
380 | assert_eq!(error.to_string(), "The uid of the token has bit an uuid4 format: invalid length: expected length 32 for simple format, found 8");
381 |
382 | let data = r#"
383 | {
384 | "name": "John Doe"
385 | "age": 43,
386 | }"#;
387 |
388 | let error = Error::ParseError(serde_json::from_str::(data).unwrap_err());
389 | assert_eq!(
390 | error.to_string(),
391 | "Error parsing response JSON: invalid type: map, expected a string at line 2 column 8"
392 | );
393 |
394 | let error = Error::HttpError(
395 | reqwest::Client::new()
396 | .execute(reqwest::Request::new(
397 | reqwest::Method::POST,
398 | // there will never be a `meilisearch.gouv.fr` addr since these domain name are controlled by the state of france
399 | reqwest::Url::parse("https://meilisearch.gouv.fr").unwrap(),
400 | ))
401 | .await
402 | .unwrap_err(),
403 | );
404 | assert_eq!(
405 | error.to_string(),
406 | "HTTP request failed: error sending request for url (https://meilisearch.gouv.fr/)"
407 | );
408 |
409 | let error = Error::InvalidTenantToken(jsonwebtoken::errors::Error::from(InvalidToken));
410 | assert_eq!(
411 | error.to_string(),
412 | "Impossible to generate the token, jsonwebtoken encountered an error: InvalidToken"
413 | );
414 |
415 | let error = Error::Yaup(yaup::Error::Custom("Test yaup error".to_string()));
416 | assert_eq!(
417 | error.to_string(),
418 | "Internal Error: could not parse the query parameters: Test yaup error"
419 | );
420 | }
421 | }
422 |
--------------------------------------------------------------------------------
/src/features.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | client::Client,
3 | errors::Error,
4 | request::{HttpClient, Method},
5 | };
6 | use serde::{Deserialize, Serialize};
7 |
8 | /// Struct representing the experimental features result from the API.
9 | #[derive(Clone, Debug, Deserialize)]
10 | #[serde(rename_all = "camelCase")]
11 | pub struct ExperimentalFeaturesResult {
12 | pub metrics: bool,
13 | pub logs_route: bool,
14 | pub contains_filter: bool,
15 | pub network: bool,
16 | pub edit_documents_by_function: bool,
17 | }
18 |
19 | /// Struct representing the experimental features request.
20 | ///
21 | /// You can build this struct using the builder pattern.
22 | ///
23 | /// # Example
24 | ///
25 | /// ```
26 | /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures};
27 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
28 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
29 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
30 | /// let mut features = ExperimentalFeatures::new(&client);
31 | /// ```
32 | #[derive(Debug, Serialize)]
33 | #[serde(rename_all = "camelCase")]
34 | pub struct ExperimentalFeatures<'a, Http: HttpClient> {
35 | #[serde(skip_serializing)]
36 | client: &'a Client,
37 |
38 | #[serde(skip_serializing_if = "Option::is_none")]
39 | pub metrics: Option,
40 | #[serde(skip_serializing_if = "Option::is_none")]
41 | pub contains_filter: Option,
42 | #[serde(skip_serializing_if = "Option::is_none")]
43 | pub logs_route: Option,
44 | #[serde(skip_serializing_if = "Option::is_none")]
45 | pub network: Option,
46 | #[serde(skip_serializing_if = "Option::is_none")]
47 | pub edit_documents_by_function: Option,
48 | }
49 |
50 | impl<'a, Http: HttpClient> ExperimentalFeatures<'a, Http> {
51 | #[must_use]
52 | pub fn new(client: &'a Client) -> Self {
53 | ExperimentalFeatures {
54 | client,
55 | metrics: None,
56 | logs_route: None,
57 | network: None,
58 | contains_filter: None,
59 | edit_documents_by_function: None,
60 | }
61 | }
62 |
63 | /// Get all the experimental features
64 | ///
65 | /// # Example
66 | ///
67 | /// ```
68 | /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures};
69 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
70 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
71 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
72 | /// tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
73 | /// let features = ExperimentalFeatures::new(&client);
74 | /// features.get().await.unwrap();
75 | /// });
76 | /// ```
77 | pub async fn get(&self) -> Result {
78 | self.client
79 | .http_client
80 | .request::<(), (), ExperimentalFeaturesResult>(
81 | &format!("{}/experimental-features", self.client.host),
82 | Method::Get { query: () },
83 | 200,
84 | )
85 | .await
86 | }
87 |
88 | /// Update the experimental features
89 | ///
90 | /// # Example
91 | ///
92 | /// ```
93 | /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures};
94 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
95 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
96 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
97 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
98 | /// let features = ExperimentalFeatures::new(&client);
99 | /// features.update().await.unwrap();
100 | /// # });
101 | /// ```
102 | pub async fn update(&self) -> Result {
103 | self.client
104 | .http_client
105 | .request::<(), &Self, ExperimentalFeaturesResult>(
106 | &format!("{}/experimental-features", self.client.host),
107 | Method::Patch {
108 | query: (),
109 | body: self,
110 | },
111 | 200,
112 | )
113 | .await
114 | }
115 |
116 | pub fn set_metrics(&mut self, metrics: bool) -> &mut Self {
117 | self.metrics = Some(metrics);
118 | self
119 | }
120 |
121 | pub fn set_logs_route(&mut self, logs_route: bool) -> &mut Self {
122 | self.logs_route = Some(logs_route);
123 | self
124 | }
125 |
126 | pub fn set_contains_filter(&mut self, contains_filter: bool) -> &mut Self {
127 | self.contains_filter = Some(contains_filter);
128 | self
129 | }
130 |
131 | pub fn set_edit_documents_by_function(
132 | &mut self,
133 | edit_documents_by_function: bool,
134 | ) -> &mut Self {
135 | self.edit_documents_by_function = Some(edit_documents_by_function);
136 | self
137 | }
138 |
139 | pub fn set_network(&mut self, network: bool) -> &mut Self {
140 | self.network = Some(network);
141 | self
142 | }
143 | }
144 |
145 | #[cfg(test)]
146 | mod tests {
147 | use super::*;
148 | use meilisearch_test_macro::meilisearch_test;
149 |
150 | #[meilisearch_test]
151 | async fn test_experimental_features_set_metrics(client: Client) {
152 | let mut features = ExperimentalFeatures::new(&client);
153 | features.set_metrics(true);
154 | let _ = features.update().await.unwrap();
155 |
156 | let res = features.get().await.unwrap();
157 | assert!(res.metrics)
158 | }
159 |
160 | #[meilisearch_test]
161 | async fn test_experimental_features_set_logs_route(client: Client) {
162 | let mut features = ExperimentalFeatures::new(&client);
163 | features.set_logs_route(true);
164 | let _ = features.update().await.unwrap();
165 |
166 | let res = features.get().await.unwrap();
167 | assert!(res.logs_route)
168 | }
169 |
170 | #[meilisearch_test]
171 | async fn test_experimental_features_set_contains_filter(client: Client) {
172 | let mut features = ExperimentalFeatures::new(&client);
173 | features.set_contains_filter(true);
174 | let _ = features.update().await.unwrap();
175 |
176 | let res = features.get().await.unwrap();
177 | assert!(res.contains_filter)
178 | }
179 |
180 | #[meilisearch_test]
181 | async fn test_experimental_features_set_network(client: Client) {
182 | let mut features = ExperimentalFeatures::new(&client);
183 | features.set_network(true);
184 | let _ = features.update().await.unwrap();
185 |
186 | let res = features.get().await.unwrap();
187 | assert!(res.network)
188 | }
189 |
190 | #[meilisearch_test]
191 | async fn test_experimental_features_set_edit_documents_by_function(client: Client) {
192 | let mut features = ExperimentalFeatures::new(&client);
193 | features.set_edit_documents_by_function(true);
194 | let _ = features.update().await.unwrap();
195 |
196 | let res = features.get().await.unwrap();
197 | assert!(res.edit_documents_by_function)
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! # 🚀 Getting started
2 | //!
3 | //! ### Add Documents
4 | //!
5 | //! ```
6 | //! use meilisearch_sdk::client::*;
7 | //! use serde::{Serialize, Deserialize};
8 | //! use futures::executor::block_on;
9 | //!
10 | //! #[derive(Serialize, Deserialize, Debug)]
11 | //! struct Movie {
12 | //! id: usize,
13 | //! title: String,
14 | //! genres: Vec,
15 | //! }
16 | //!
17 | //!
18 | //! #[tokio::main(flavor = "current_thread")]
19 | //! async fn main() {
20 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
21 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
22 | //! // Create a client (without sending any request so that can't fail)
23 | //! let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
24 | //!
25 | //! # let index = client.create_index("movies", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
26 | //! // An index is where the documents are stored.
27 | //! let movies = client.index("movies");
28 | //!
29 | //! // Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
30 | //! movies.add_documents(&[
31 | //! Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] },
32 | //! Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] },
33 | //! Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] },
34 | //! Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] },
35 | //! Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] },
36 | //! Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] },
37 | //! ], Some("id")).await.unwrap();
38 | //! # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
39 | //! }
40 | //! ```
41 | //!
42 | //! With the `uid`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-task).
43 | //!
44 | //! ### Basic Search
45 | //!
46 | //! ```
47 | //! # use meilisearch_sdk::client::*;
48 | //! # use serde::{Serialize, Deserialize};
49 | //! # #[derive(Serialize, Deserialize, Debug)]
50 | //! # struct Movie {
51 | //! # id: usize,
52 | //! # title: String,
53 | //! # genres: Vec,
54 | //! # }
55 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
56 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
57 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
58 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
59 | //! # let movies = client.create_index("movies_2", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
60 | //! // Meilisearch is typo-tolerant:
61 | //! println!("{:?}", client.index("movies_2").search().with_query("caorl").execute::().await.unwrap().hits);
62 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
63 | //! # })}
64 | //! ```
65 | //!
66 | //! Output:
67 | //! ```text
68 | //! [Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"] }]
69 | //! ```
70 | //!
71 | //! Json output:
72 | //! ```json
73 | //! {
74 | //! "hits": [{
75 | //! "id": 1,
76 | //! "title": "Carol",
77 | //! "genres": ["Romance", "Drama"]
78 | //! }],
79 | //! "offset": 0,
80 | //! "limit": 10,
81 | //! "processingTimeMs": 1,
82 | //! "query": "caorl"
83 | //! }
84 | //! ```
85 | //!
86 | //! ### Custom Search
87 | //!
88 | //! ```
89 | //! # use meilisearch_sdk::{client::*, search::*};
90 | //! # use serde::{Serialize, Deserialize};
91 | //! # #[derive(Serialize, Deserialize, Debug)]
92 | //! # struct Movie {
93 | //! # id: usize,
94 | //! # title: String,
95 | //! # genres: Vec,
96 | //! # }
97 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
98 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
99 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
100 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
101 | //! # let movies = client.create_index("movies_3", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
102 | //! let search_result = client.index("movies_3")
103 | //! .search()
104 | //! .with_query("phil")
105 | //! .with_attributes_to_highlight(Selectors::Some(&["*"]))
106 | //! .execute::()
107 | //! .await
108 | //! .unwrap();
109 | //! println!("{:?}", search_result.hits);
110 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
111 | //! # })}
112 | //! ```
113 | //!
114 | //! Json output:
115 | //! ```json
116 | //! {
117 | //! "hits": [
118 | //! {
119 | //! "id": 6,
120 | //! "title": "Philadelphia",
121 | //! "_formatted": {
122 | //! "id": 6,
123 | //! "title": "Phil adelphia",
124 | //! "genre": ["Drama"]
125 | //! }
126 | //! }
127 | //! ],
128 | //! "offset": 0,
129 | //! "limit": 20,
130 | //! "processingTimeMs": 0,
131 | //! "query": "phil"
132 | //! }
133 | //! ```
134 | //!
135 | //! ### Custom Search With Filters
136 | //!
137 | //! If you want to enable filtering, you must add your attributes to the `filterableAttributes`
138 | //! index setting.
139 | //!
140 | //! ```
141 | //! # use meilisearch_sdk::{client::*};
142 | //! # use serde::{Serialize, Deserialize};
143 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
144 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
145 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
146 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
147 | //! # let movies = client.create_index("movies_4", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
148 | //! let filterable_attributes = [
149 | //! "id",
150 | //! "genres",
151 | //! ];
152 | //! client.index("movies_4").set_filterable_attributes(&filterable_attributes).await.unwrap();
153 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
154 | //! # })}
155 | //! ```
156 | //!
157 | //! You only need to perform this operation once.
158 | //!
159 | //! Note that Meilisearch will rebuild your index whenever you update `filterableAttributes`. Depending on the size of your dataset, this might take time. You can track the process using the [tasks](https://www.meilisearch.com/docs/reference/api/tasks#get-task).
160 | //!
161 | //! Then, you can perform the search:
162 | //!
163 | //! ```
164 | //! # use meilisearch_sdk::{client::*, search::*};
165 | //! # use serde::{Serialize, Deserialize};
166 | //! # #[derive(Serialize, Deserialize, Debug)]
167 | //! # struct Movie {
168 | //! # id: usize,
169 | //! # title: String,
170 | //! # genres: Vec,
171 | //! # }
172 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
173 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
174 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
175 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
176 | //! # let movies = client.create_index("movies_5", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
177 | //! # let filterable_attributes = [
178 | //! # "id",
179 | //! # "genres"
180 | //! # ];
181 | //! # movies.set_filterable_attributes(&filterable_attributes).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
182 | //! # movies.add_documents(&[
183 | //! # Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] },
184 | //! # Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] },
185 | //! # Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] },
186 | //! # Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] },
187 | //! # Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] },
188 | //! # Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] },
189 | //! # ], Some("id")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
190 | //! let search_result = client.index("movies_5")
191 | //! .search()
192 | //! .with_query("wonder")
193 | //! .with_filter("id > 1 AND genres = Action")
194 | //! .execute::()
195 | //! .await
196 | //! .unwrap();
197 | //! println!("{:?}", search_result.hits);
198 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
199 | //! # })}
200 | //! ```
201 | //!
202 | //! Json output:
203 | //! ```json
204 | //! {
205 | //! "hits": [
206 | //! {
207 | //! "id": 2,
208 | //! "title": "Wonder Woman",
209 | //! "genres": ["Action", "Adventure"]
210 | //! }
211 | //! ],
212 | //! "offset": 0,
213 | //! "limit": 20,
214 | //! "estimatedTotalHits": 1,
215 | //! "processingTimeMs": 0,
216 | //! "query": "wonder"
217 | //! }
218 | //! ```
219 | //!
220 | //! ### Customize the `HttpClient`
221 | //!
222 | //! By default, the SDK uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) to make http calls.
223 | //! The SDK lets you customize the http client by implementing the `HttpClient` trait yourself and
224 | //! initializing the `Client` with the `new_with_client` method.
225 | //! You may be interested by the `futures-unsend` feature which lets you specify a non-Send http client.
226 | //!
227 | //! ### Wasm support
228 | //!
229 | //! The SDK supports wasm through reqwest. You'll need to enable the `futures-unsend` feature while importing it, though.
230 | #![warn(clippy::all)]
231 | #![allow(clippy::needless_doctest_main)]
232 |
233 | /// Module containing the [`Client`](client::Client) struct.
234 | pub mod client;
235 | /// Module representing the [documents] structures.
236 | pub mod documents;
237 | /// Module containing the [dumps] trait.
238 | pub mod dumps;
239 | /// Module containing the [`errors::Error`] struct.
240 | pub mod errors;
241 | /// Module related to runtime and instance features.
242 | pub mod features;
243 | /// Module containing the Index struct.
244 | pub mod indexes;
245 | /// Module containing the [`Key`](key::Key) struct.
246 | pub mod key;
247 | pub mod request;
248 | /// Module related to search queries and results.
249 | pub mod search;
250 | /// Module containing [`Settings`](settings::Settings).
251 | pub mod settings;
252 | /// Module containing the [snapshots](snapshots::create_snapshot)-feature.
253 | pub mod snapshots;
254 | /// Module representing the [`TaskInfo`](task_info::TaskInfo)s.
255 | pub mod task_info;
256 | /// Module representing the [`Task`](tasks::Task)s.
257 | pub mod tasks;
258 | /// Module that generates tenant tokens.
259 | #[cfg(not(target_arch = "wasm32"))]
260 | mod tenant_tokens;
261 | /// Module containing utilizes functions.
262 | mod utils;
263 |
264 | #[cfg(feature = "reqwest")]
265 | pub mod reqwest;
266 |
267 | #[cfg(feature = "reqwest")]
268 | pub type DefaultHttpClient = reqwest::ReqwestClient;
269 |
270 | #[cfg(not(feature = "reqwest"))]
271 | pub type DefaultHttpClient = std::convert::Infallible;
272 |
273 | #[cfg(test)]
274 | /// Support for the `IndexConfig` derive proc macro in the crate's tests.
275 | extern crate self as meilisearch_sdk;
276 | /// Can't assume that the user of proc_macro will have access to `async_trait` crate. So exporting the `async-trait` crate from `meilisearch_sdk` in a hidden module.
277 | #[doc(hidden)]
278 | pub mod macro_helper {
279 | pub use async_trait::async_trait;
280 | }
281 |
--------------------------------------------------------------------------------
/src/request.rs:
--------------------------------------------------------------------------------
1 | use std::convert::Infallible;
2 |
3 | use async_trait::async_trait;
4 | use log::{error, trace, warn};
5 | use serde::{de::DeserializeOwned, Serialize};
6 | use serde_json::{from_str, to_vec};
7 |
8 | use crate::errors::{Error, MeilisearchCommunicationError, MeilisearchError};
9 |
10 | #[derive(Debug)]
11 | pub enum Method {
12 | Get { query: Q },
13 | Post { query: Q, body: B },
14 | Patch { query: Q, body: B },
15 | Put { query: Q, body: B },
16 | Delete { query: Q },
17 | }
18 |
19 | impl Method {
20 | pub fn map_body(self, f: impl Fn(B) -> B2) -> Method {
21 | match self {
22 | Method::Get { query } => Method::Get { query },
23 | Method::Delete { query } => Method::Delete { query },
24 | Method::Post { query, body } => Method::Post {
25 | query,
26 | body: f(body),
27 | },
28 | Method::Patch { query, body } => Method::Patch {
29 | query,
30 | body: f(body),
31 | },
32 | Method::Put { query, body } => Method::Put {
33 | query,
34 | body: f(body),
35 | },
36 | }
37 | }
38 |
39 | pub fn query(&self) -> &Q {
40 | match self {
41 | Method::Get { query } => query,
42 | Method::Delete { query } => query,
43 | Method::Post { query, .. } => query,
44 | Method::Put { query, .. } => query,
45 | Method::Patch { query, .. } => query,
46 | }
47 | }
48 |
49 | pub fn body(&self) -> Option<&B> {
50 | match self {
51 | Method::Get { query: _ } | Method::Delete { query: _ } => None,
52 | Method::Post { body, query: _ } => Some(body),
53 | Method::Put { body, query: _ } => Some(body),
54 | Method::Patch { body, query: _ } => Some(body),
55 | }
56 | }
57 |
58 | pub fn into_body(self) -> Option {
59 | match self {
60 | Method::Get { query: _ } | Method::Delete { query: _ } => None,
61 | Method::Post { body, query: _ } => Some(body),
62 | Method::Put { body, query: _ } => Some(body),
63 | Method::Patch { body, query: _ } => Some(body),
64 | }
65 | }
66 | }
67 |
68 | #[cfg_attr(feature = "futures-unsend", async_trait(?Send))]
69 | #[cfg_attr(not(feature = "futures-unsend"), async_trait)]
70 | pub trait HttpClient: Clone + Send + Sync {
71 | async fn request(
72 | &self,
73 | url: &str,
74 | method: Method,
75 | expected_status_code: u16,
76 | ) -> Result
77 | where
78 | Query: Serialize + Send + Sync,
79 | Body: Serialize + Send + Sync,
80 | Output: DeserializeOwned + 'static + Send,
81 | {
82 | use futures::io::Cursor;
83 |
84 | self.stream_request(
85 | url,
86 | method.map_body(|body| Cursor::new(to_vec(&body).unwrap())),
87 | "application/json",
88 | expected_status_code,
89 | )
90 | .await
91 | }
92 |
93 | async fn stream_request<
94 | Query: Serialize + Send + Sync,
95 | Body: futures_io::AsyncRead + Send + Sync + 'static,
96 | Output: DeserializeOwned + 'static,
97 | >(
98 | &self,
99 | url: &str,
100 | method: Method,
101 | content_type: &str,
102 | expected_status_code: u16,
103 | ) -> Result;
104 | }
105 |
106 | pub fn parse_response(
107 | status_code: u16,
108 | expected_status_code: u16,
109 | body: &str,
110 | url: String,
111 | ) -> Result {
112 | if status_code == expected_status_code {
113 | return match from_str::(body) {
114 | Ok(output) => {
115 | trace!("Request succeed");
116 | Ok(output)
117 | }
118 | Err(e) => {
119 | error!("Request succeeded but failed to parse response");
120 | Err(Error::ParseError(e))
121 | }
122 | };
123 | }
124 |
125 | warn!(
126 | "Expected response code {}, got {}",
127 | expected_status_code, status_code
128 | );
129 |
130 | match from_str::(body) {
131 | Ok(e) => Err(Error::from(e)),
132 | Err(e) => {
133 | if status_code >= 400 {
134 | return Err(Error::MeilisearchCommunication(
135 | MeilisearchCommunicationError {
136 | status_code,
137 | message: None,
138 | url,
139 | },
140 | ));
141 | }
142 | Err(Error::ParseError(e))
143 | }
144 | }
145 | }
146 |
147 | #[cfg_attr(feature = "futures-unsend", async_trait(?Send))]
148 | #[cfg_attr(not(feature = "futures-unsend"), async_trait)]
149 | impl HttpClient for Infallible {
150 | async fn request(
151 | &self,
152 | _url: &str,
153 | _method: Method,
154 | _expected_status_code: u16,
155 | ) -> Result
156 | where
157 | Query: Serialize + Send + Sync,
158 | Body: Serialize + Send + Sync,
159 | Output: DeserializeOwned + 'static + Send,
160 | {
161 | unreachable!()
162 | }
163 |
164 | async fn stream_request<
165 | Query: Serialize + Send + Sync,
166 | Body: futures_io::AsyncRead + Send + Sync + 'static,
167 | Output: DeserializeOwned + 'static,
168 | >(
169 | &self,
170 | _url: &str,
171 | _method: Method,
172 | _content_type: &str,
173 | _expected_status_code: u16,
174 | ) -> Result {
175 | unreachable!()
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/reqwest.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | pin::Pin,
3 | task::{Context, Poll},
4 | };
5 |
6 | use async_trait::async_trait;
7 | use bytes::{Bytes, BytesMut};
8 | use futures::{AsyncRead, Stream};
9 | use pin_project_lite::pin_project;
10 | use serde::{de::DeserializeOwned, Serialize};
11 |
12 | use crate::{
13 | errors::Error,
14 | request::{parse_response, HttpClient, Method},
15 | };
16 |
17 | #[derive(Debug, Clone, Default)]
18 | pub struct ReqwestClient {
19 | client: reqwest::Client,
20 | }
21 |
22 | impl ReqwestClient {
23 | pub fn new(api_key: Option<&str>) -> Result {
24 | use reqwest::{header, ClientBuilder};
25 |
26 | let builder = ClientBuilder::new();
27 | let mut headers = header::HeaderMap::new();
28 | #[cfg(not(target_arch = "wasm32"))]
29 | headers.insert(
30 | header::USER_AGENT,
31 | header::HeaderValue::from_str(&qualified_version()).unwrap(),
32 | );
33 | #[cfg(target_arch = "wasm32")]
34 | headers.insert(
35 | header::HeaderName::from_static("x-meilisearch-client"),
36 | header::HeaderValue::from_str(&qualified_version()).unwrap(),
37 | );
38 |
39 | if let Some(api_key) = api_key {
40 | headers.insert(
41 | header::AUTHORIZATION,
42 | header::HeaderValue::from_str(&format!("Bearer {api_key}")).unwrap(),
43 | );
44 | }
45 |
46 | let builder = builder.default_headers(headers);
47 | let client = builder.build()?;
48 |
49 | Ok(ReqwestClient { client })
50 | }
51 | }
52 |
53 | #[cfg_attr(feature = "futures-unsend", async_trait(?Send))]
54 | #[cfg_attr(not(feature = "futures-unsend"), async_trait)]
55 | impl HttpClient for ReqwestClient {
56 | async fn stream_request<
57 | Query: Serialize + Send + Sync,
58 | Body: futures_io::AsyncRead + Send + Sync + 'static,
59 | Output: DeserializeOwned + 'static,
60 | >(
61 | &self,
62 | url: &str,
63 | method: Method,
64 | content_type: &str,
65 | expected_status_code: u16,
66 | ) -> Result {
67 | use reqwest::header;
68 |
69 | let query = method.query();
70 | let query = yaup::to_string(query)?;
71 |
72 | let url = if query.is_empty() {
73 | url.to_string()
74 | } else {
75 | format!("{url}{query}")
76 | };
77 |
78 | let mut request = self.client.request(verb(&method), &url);
79 |
80 | if let Some(body) = method.into_body() {
81 | // TODO: Currently reqwest doesn't support streaming data in wasm so we need to collect everything in RAM
82 | #[cfg(not(target_arch = "wasm32"))]
83 | {
84 | let stream = ReaderStream::new(body);
85 | let body = reqwest::Body::wrap_stream(stream);
86 |
87 | request = request
88 | .header(header::CONTENT_TYPE, content_type)
89 | .body(body);
90 | }
91 | #[cfg(target_arch = "wasm32")]
92 | {
93 | use futures::{pin_mut, AsyncReadExt};
94 |
95 | let mut buf = Vec::new();
96 | pin_mut!(body);
97 | body.read_to_end(&mut buf)
98 | .await
99 | .map_err(|err| Error::Other(Box::new(err)))?;
100 | request = request.header(header::CONTENT_TYPE, content_type).body(buf);
101 | }
102 | }
103 |
104 | let response = self.client.execute(request.build()?).await?;
105 | let status = response.status().as_u16();
106 | let mut body = response.text().await?;
107 |
108 | if body.is_empty() {
109 | body = "null".to_string();
110 | }
111 |
112 | parse_response(status, expected_status_code, &body, url.to_string())
113 | }
114 | }
115 |
116 | fn verb(method: &Method) -> reqwest::Method {
117 | match method {
118 | Method::Get { .. } => reqwest::Method::GET,
119 | Method::Delete { .. } => reqwest::Method::DELETE,
120 | Method::Post { .. } => reqwest::Method::POST,
121 | Method::Put { .. } => reqwest::Method::PUT,
122 | Method::Patch { .. } => reqwest::Method::PATCH,
123 | }
124 | }
125 |
126 | pub fn qualified_version() -> String {
127 | const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
128 |
129 | format!("Meilisearch Rust (v{})", VERSION.unwrap_or("unknown"))
130 | }
131 |
132 | pin_project! {
133 | #[derive(Debug)]
134 | pub struct ReaderStream {
135 | #[pin]
136 | reader: R,
137 | buf: BytesMut,
138 | capacity: usize,
139 | }
140 | }
141 |
142 | impl ReaderStream {
143 | pub fn new(reader: R) -> Self {
144 | Self {
145 | reader,
146 | buf: BytesMut::new(),
147 | // 8KiB of capacity, the default capacity used by `BufReader` in the std
148 | capacity: 8 * 1024 * 1024,
149 | }
150 | }
151 | }
152 |
153 | impl Stream for ReaderStream {
154 | type Item = std::io::Result;
155 |
156 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
157 | let this = self.as_mut().project();
158 |
159 | if this.buf.capacity() == 0 {
160 | this.buf.resize(*this.capacity, 0);
161 | }
162 |
163 | match AsyncRead::poll_read(this.reader, cx, this.buf) {
164 | Poll::Pending => Poll::Pending,
165 | Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))),
166 | Poll::Ready(Ok(0)) => Poll::Ready(None),
167 | Poll::Ready(Ok(i)) => {
168 | let chunk = this.buf.split_to(i);
169 | Poll::Ready(Some(Ok(chunk.freeze())))
170 | }
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/snapshots.rs:
--------------------------------------------------------------------------------
1 | //! The `snapshots` module allows the creation of database snapshots.
2 | //!
3 | //! - snapshots are `.snapshots` files that can be used to launch Meilisearch.
4 | //!
5 | //! - snapshots are not compatible between Meilisearch versions.
6 | //!
7 | //! # Example
8 | //!
9 | //! ```
10 | //! # use meilisearch_sdk::{client::*, errors::*, snapshots::*, snapshots::*, task_info::*, tasks::*};
11 | //! # use futures_await_test::async_test;
12 | //! # use std::{thread::sleep, time::Duration};
13 | //! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
14 | //! #
15 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
16 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
17 | //! #
18 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
19 | //!
20 | //! // Create a snapshot
21 | //! let task_info = client.create_snapshot().await.unwrap();
22 | //! assert!(matches!(
23 | //! task_info,
24 | //! TaskInfo {
25 | //! update_type: TaskType::SnapshotCreation { .. },
26 | //! ..
27 | //! }
28 | //! ));
29 | //! # });
30 | //! ```
31 |
32 | use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo};
33 |
34 | /// Snapshots related methods.
35 | /// See the [snapshots](crate::snapshots) module.
36 | impl Client {
37 | /// Triggers a snapshots creation process.
38 | ///
39 | /// Once the process is complete, a snapshots is created in the [snapshots directory].
40 | /// If the snapshots directory does not exist yet, it will be created.
41 | ///
42 | /// # Example
43 | ///
44 | /// ```
45 | /// # use meilisearch_sdk::{client::*, errors::*, snapshots::*, snapshots::*, task_info::*, tasks::*};
46 | /// # use futures_await_test::async_test;
47 | /// # use std::{thread::sleep, time::Duration};
48 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
49 | /// #
50 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
51 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
52 | /// #
53 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
54 | /// #
55 | /// let task_info = client.create_snapshot().await.unwrap();
56 | ///
57 | /// assert!(matches!(
58 | /// task_info,
59 | /// TaskInfo {
60 | /// update_type: TaskType::SnapshotCreation { .. },
61 | /// ..
62 | /// }
63 | /// ));
64 | /// # });
65 | /// ```
66 | pub async fn create_snapshot(&self) -> Result {
67 | self.http_client
68 | .request::<(), (), TaskInfo>(
69 | &format!("{}/snapshots", self.host),
70 | Method::Post {
71 | query: (),
72 | body: (),
73 | },
74 | 202,
75 | )
76 | .await
77 | }
78 | }
79 |
80 | /// Alias for [`create_snapshot`](Client::create_snapshot).
81 | pub async fn create_snapshot(client: &Client) -> Result {
82 | client.create_snapshot().await
83 | }
84 |
85 | #[cfg(test)]
86 | mod tests {
87 | use super::*;
88 | use crate::{client::*, tasks::*};
89 | use meilisearch_test_macro::meilisearch_test;
90 |
91 | #[meilisearch_test]
92 | async fn test_snapshot_success_creation(client: Client) -> Result<(), Error> {
93 | let task = client
94 | .create_snapshot()
95 | .await?
96 | .wait_for_completion(&client, None, None)
97 | .await?;
98 |
99 | assert!(matches!(task, Task::Succeeded { .. }));
100 | Ok(())
101 | }
102 |
103 | #[meilisearch_test]
104 | async fn test_snapshot_correct_update_type(client: Client) -> Result<(), Error> {
105 | let task_info = client.create_snapshot().await.unwrap();
106 |
107 | assert!(matches!(
108 | task_info,
109 | TaskInfo {
110 | update_type: TaskType::SnapshotCreation { .. },
111 | ..
112 | }
113 | ));
114 | Ok(())
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/task_info.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 | use std::time::Duration;
3 | use time::OffsetDateTime;
4 |
5 | use crate::{client::Client, errors::Error, request::HttpClient, tasks::*};
6 |
7 | #[derive(Debug, Clone, Deserialize)]
8 | #[serde(rename_all = "camelCase")]
9 | pub struct TaskInfo {
10 | #[serde(with = "time::serde::rfc3339")]
11 | pub enqueued_at: OffsetDateTime,
12 | pub index_uid: Option,
13 | pub status: String,
14 | #[serde(flatten)]
15 | pub update_type: TaskType,
16 | pub task_uid: u32,
17 | }
18 |
19 | impl AsRef for TaskInfo {
20 | fn as_ref(&self) -> &u32 {
21 | &self.task_uid
22 | }
23 | }
24 |
25 | impl TaskInfo {
26 | #[must_use]
27 | pub fn get_task_uid(&self) -> u32 {
28 | self.task_uid
29 | }
30 |
31 | /// Wait until Meilisearch processes a task provided by [`TaskInfo`], and get its status.
32 | ///
33 | /// `interval` = The frequency at which the server should be polled. **Default = 50ms**
34 | ///
35 | /// `timeout` = The maximum time to wait for processing to complete. **Default = 5000ms**
36 | ///
37 | /// If the waited time exceeds `timeout` then an [`Error::Timeout`] will be returned.
38 | ///
39 | /// See also [`Client::wait_for_task`, `Index::wait_for_task`].
40 | ///
41 | /// # Example
42 | ///
43 | /// ```
44 | /// # use meilisearch_sdk::{client::*, indexes::*, tasks::*};
45 | /// # use serde::{Serialize, Deserialize};
46 | /// #
47 | /// # #[derive(Debug, Serialize, Deserialize, PartialEq)]
48 | /// # struct Document {
49 | /// # id: usize,
50 | /// # value: String,
51 | /// # kind: String,
52 | /// # }
53 | /// #
54 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
55 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
56 | /// #
57 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
58 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
59 | /// let movies = client.index("movies_wait_for_completion");
60 | ///
61 | /// let status = movies.add_documents(&[
62 | /// Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() },
63 | /// Document { id: 1, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() },
64 | /// ], None)
65 | /// .await
66 | /// .unwrap()
67 | /// .wait_for_completion(&client, None, None)
68 | /// .await
69 | /// .unwrap();
70 | ///
71 | /// assert!(matches!(status, Task::Succeeded { .. }));
72 | /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
73 | /// # });
74 | /// ```
75 | pub async fn wait_for_completion(
76 | self,
77 | client: &Client,
78 | interval: Option,
79 | timeout: Option,
80 | ) -> Result {
81 | client.wait_for_task(self, interval, timeout).await
82 | }
83 | }
84 |
85 | #[cfg(test)]
86 | mod test {
87 | use super::*;
88 | use crate::{
89 | client::*,
90 | errors::{ErrorCode, ErrorType},
91 | indexes::Index,
92 | };
93 | use big_s::S;
94 | use meilisearch_test_macro::meilisearch_test;
95 | use serde::{Deserialize, Serialize};
96 | use std::time::Duration;
97 |
98 | #[derive(Debug, Serialize, Deserialize, PartialEq)]
99 | struct Document {
100 | id: usize,
101 | value: String,
102 | kind: String,
103 | }
104 |
105 | #[test]
106 | fn test_deserialize_task_info() {
107 | let datetime = OffsetDateTime::parse(
108 | "2022-02-03T13:02:38.369634Z",
109 | &time::format_description::well_known::Rfc3339,
110 | )
111 | .unwrap();
112 |
113 | let task_info: TaskInfo = serde_json::from_str(
114 | r#"
115 | {
116 | "enqueuedAt": "2022-02-03T13:02:38.369634Z",
117 | "indexUid": "meili",
118 | "status": "enqueued",
119 | "type": "documentAdditionOrUpdate",
120 | "taskUid": 12
121 | }"#,
122 | )
123 | .unwrap();
124 |
125 | assert!(matches!(
126 | task_info,
127 | TaskInfo {
128 | enqueued_at,
129 | index_uid: Some(index_uid),
130 | task_uid: 12,
131 | update_type: TaskType::DocumentAdditionOrUpdate { details: None },
132 | status,
133 | }
134 | if enqueued_at == datetime && index_uid == "meili" && status == "enqueued"));
135 | }
136 |
137 | #[meilisearch_test]
138 | async fn test_wait_for_task_with_args(client: Client, movies: Index) -> Result<(), Error> {
139 | let task_info = movies
140 | .add_documents(
141 | &[
142 | Document {
143 | id: 0,
144 | kind: "title".into(),
145 | value: S("The Social Network"),
146 | },
147 | Document {
148 | id: 1,
149 | kind: "title".into(),
150 | value: S("Harry Potter and the Sorcerer's Stone"),
151 | },
152 | ],
153 | None,
154 | )
155 | .await?;
156 |
157 | let task = client
158 | .get_task(task_info)
159 | .await?
160 | .wait_for_completion(
161 | &client,
162 | Some(Duration::from_millis(1)),
163 | Some(Duration::from_millis(6000)),
164 | )
165 | .await?;
166 |
167 | assert!(matches!(task, Task::Succeeded { .. }));
168 | Ok(())
169 | }
170 |
171 | #[meilisearch_test]
172 | async fn test_failing_task(client: Client, index: Index) -> Result<(), Error> {
173 | let task_info = client.create_index(index.uid, None).await.unwrap();
174 | let task = client.wait_for_task(task_info, None, None).await?;
175 |
176 | let error = task.unwrap_failure();
177 | assert_eq!(error.error_code, ErrorCode::IndexAlreadyExists);
178 | assert_eq!(error.error_type, ErrorType::InvalidRequest);
179 | Ok(())
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/tenant_tokens.rs:
--------------------------------------------------------------------------------
1 | use crate::errors::Error;
2 | use jsonwebtoken::{encode, EncodingKey, Header};
3 | use serde::{Deserialize, Serialize};
4 | use serde_json::Value;
5 | use time::OffsetDateTime;
6 | #[cfg(not(target_arch = "wasm32"))]
7 | use uuid::Uuid;
8 |
9 | #[derive(Debug, Serialize, Deserialize)]
10 | #[serde(rename_all = "camelCase")]
11 | struct TenantTokenClaim {
12 | api_key_uid: String,
13 | search_rules: Value,
14 | #[serde(with = "time::serde::timestamp::option")]
15 | exp: Option,
16 | }
17 |
18 | pub fn generate_tenant_token(
19 | api_key_uid: String,
20 | search_rules: Value,
21 | api_key: impl AsRef,
22 | expires_at: Option,
23 | ) -> Result {
24 | // Validate uuid format
25 | let uid = Uuid::try_parse(&api_key_uid)?;
26 |
27 | // Validate uuid version
28 | if uid.get_version_num() != 4 {
29 | return Err(Error::InvalidUuid4Version);
30 | }
31 |
32 | if expires_at.is_some_and(|expires_at| OffsetDateTime::now_utc() > expires_at) {
33 | return Err(Error::TenantTokensExpiredSignature);
34 | }
35 |
36 | let claims = TenantTokenClaim {
37 | api_key_uid,
38 | exp: expires_at,
39 | search_rules,
40 | };
41 |
42 | let token = encode(
43 | &Header::default(),
44 | &claims,
45 | &EncodingKey::from_secret(api_key.as_ref().as_bytes()),
46 | );
47 |
48 | Ok(token?)
49 | }
50 |
51 | #[cfg(test)]
52 | mod tests {
53 | use crate::tenant_tokens::*;
54 | use big_s::S;
55 | use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
56 | use serde_json::json;
57 | use std::collections::HashSet;
58 |
59 | const SEARCH_RULES: [&str; 1] = ["*"];
60 | const VALID_KEY: &str = "a19b6ec84ee31324efa560cd1f7e6939";
61 |
62 | fn build_validation() -> Validation {
63 | let mut validation = Validation::new(Algorithm::HS256);
64 | validation.validate_exp = false;
65 | validation.required_spec_claims = HashSet::new();
66 |
67 | validation
68 | }
69 |
70 | #[test]
71 | fn test_generate_token_with_given_key() {
72 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4");
73 | let token =
74 | generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, None).unwrap();
75 |
76 | let valid_key = decode::(
77 | &token,
78 | &DecodingKey::from_secret(VALID_KEY.as_ref()),
79 | &build_validation(),
80 | );
81 | let invalid_key = decode::(
82 | &token,
83 | &DecodingKey::from_secret("not-the-same-key".as_ref()),
84 | &build_validation(),
85 | );
86 |
87 | assert!(valid_key.is_ok());
88 | assert!(invalid_key.is_err());
89 | }
90 |
91 | #[test]
92 | fn test_generate_token_without_uid() {
93 | let api_key_uid = S("");
94 | let key = S("");
95 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None);
96 |
97 | assert!(token.is_err());
98 | }
99 |
100 | #[test]
101 | fn test_generate_token_with_expiration() {
102 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4");
103 | let exp = OffsetDateTime::now_utc() + time::Duration::HOUR;
104 | let token =
105 | generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp)).unwrap();
106 |
107 | let decoded = decode::(
108 | &token,
109 | &DecodingKey::from_secret(VALID_KEY.as_ref()),
110 | &Validation::new(Algorithm::HS256),
111 | );
112 |
113 | assert!(decoded.is_ok());
114 | }
115 |
116 | #[test]
117 | fn test_generate_token_with_expires_at_in_the_past() {
118 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4");
119 | let exp = OffsetDateTime::now_utc() - time::Duration::HOUR;
120 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp));
121 |
122 | assert!(token.is_err());
123 | }
124 |
125 | #[test]
126 | fn test_generate_token_contains_claims() {
127 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4");
128 | let token =
129 | generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), VALID_KEY, None)
130 | .unwrap();
131 |
132 | let decoded = decode::(
133 | &token,
134 | &DecodingKey::from_secret(VALID_KEY.as_ref()),
135 | &build_validation(),
136 | )
137 | .expect("Cannot decode the token");
138 |
139 | assert_eq!(decoded.claims.api_key_uid, api_key_uid);
140 | assert_eq!(decoded.claims.search_rules, json!(SEARCH_RULES));
141 | }
142 |
143 | #[test]
144 | fn test_generate_token_with_multi_byte_chars() {
145 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4");
146 | let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09";
147 | let token =
148 | generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None).unwrap();
149 |
150 | let decoded = decode::(
151 | &token,
152 | &DecodingKey::from_secret(key.as_ref()),
153 | &build_validation(),
154 | )
155 | .expect("Cannot decode the token");
156 |
157 | assert_eq!(decoded.claims.api_key_uid, api_key_uid);
158 | }
159 |
160 | #[test]
161 | fn test_generate_token_with_wrongly_formatted_uid() {
162 | let api_key_uid = S("xxx");
163 | let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09";
164 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None);
165 |
166 | assert!(token.is_err());
167 | }
168 |
169 | #[test]
170 | fn test_generate_token_with_wrong_uid_version() {
171 | let api_key_uid = S("6a11eb96-2485-11ed-861d-0242ac120002");
172 | let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09";
173 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None);
174 |
175 | assert!(token.is_err());
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | #[cfg(not(target_arch = "wasm32"))]
4 | pub(crate) async fn async_sleep(interval: Duration) {
5 | let (sender, receiver) = futures::channel::oneshot::channel::<()>();
6 | std::thread::spawn(move || {
7 | std::thread::sleep(interval);
8 | let _ = sender.send(());
9 | });
10 | let _ = receiver.await;
11 | }
12 |
13 | #[cfg(target_arch = "wasm32")]
14 | pub(crate) async fn async_sleep(interval: Duration) {
15 | use std::convert::TryInto;
16 | use wasm_bindgen_futures::JsFuture;
17 |
18 | JsFuture::from(web_sys::js_sys::Promise::new(&mut |yes, _| {
19 | web_sys::window()
20 | .unwrap()
21 | .set_timeout_with_callback_and_timeout_and_arguments_0(
22 | &yes,
23 | interval.as_millis().try_into().unwrap(),
24 | )
25 | .unwrap();
26 | }))
27 | .await
28 | .unwrap();
29 | }
30 |
31 | #[cfg(test)]
32 | mod test {
33 | use super::*;
34 | use meilisearch_test_macro::meilisearch_test;
35 |
36 | #[meilisearch_test]
37 | async fn test_async_sleep() {
38 | let sleep_duration = Duration::from_millis(10);
39 | let now = std::time::Instant::now();
40 |
41 | async_sleep(sleep_duration).await;
42 |
43 | assert!(now.elapsed() >= sleep_duration);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------