Compare commits

...

105 Commits

Author SHA1 Message Date
1387a211d2 Merge #3053
3053: Upgrade alpine 3.14 to 3.16 r=Kerollmops a=curquiza

Otherwise CI is failing https://github.com/meilisearch/meilisearch/actions/runs/3470576605/jobs/5799173168

Co-authored-by: curquiza <clementine@meilisearch.com>
2022-11-15 13:56:45 +00:00
661b345ad9 Upgrade alpine 3.14 to 3.16 2022-11-15 14:54:18 +01:00
0f0d1dccf0 Merge #3047
3047: Fix soft deleted bug settings r=curquiza a=Kerollmops

This PR fixes https://github.com/meilisearch/meilisearch/issues/3021 and fixes https://github.com/meilisearch/meilisearch/issues/2945 and is released as version 0.29.2.

Co-authored-by: Kerollmops <clement@meilisearch.com>
2022-11-15 11:08:47 +00:00
0331fc7c71 Make clippy happy 2022-11-15 12:07:00 +01:00
5cfcdbb55a Bump the version to v0.29.2 2022-11-14 17:39:10 +01:00
c77c3a90a0 Use milli v0.33.5 2022-11-14 17:39:09 +01:00
3ebd88c03b Revert "Comment cache steps in jobs"
This reverts commit f513ac1233.
2022-10-10 14:46:54 +02:00
c958097e99 Merge #2862
2862: Use Ubuntu 18.04 for all CI tasks that previously used Ubuntu 20.04 r=curquiza a=loiclec

This is to prevent linking with a version of glibc that is too recent.

With meilisearch v0.29.0 we inadvertently bumped the minimum supported glibc version to 2.29, which means it couldn't be run from Debian 10 (for example) anymore. By using Ubuntu 18.04, which uses glibc 2.27, we restore support for older Linux distros.

Fixes #2850

Co-authored-by: Loïc Lecrenier <loic@meilisearch.com>
Co-authored-by: Clémentine Urquizar <clementine@meilisearch.com>
2022-10-10 14:42:18 +02:00
f513ac1233 Comment cache steps in jobs 2022-10-10 14:25:24 +02:00
97c202db51 Update version for next release (v0.29.1) 2022-10-10 14:25:24 +02:00
a5e23aa6e4 Use Ubuntu 18.04 for all CI tasks that previously used Ubuntu 20.04
This is to prevent linking with a version of glibc that is too recent.

With meilisearch v0.29.0 we inadvertently bumped the minimum supported
glibc version to 2.29, which means it couldn't be run from Debian 10
(for example) anymore. By using Ubuntu 18.04, which uses glibc 2.27, we
restore support for older Linux distros.
2022-10-10 14:25:18 +02:00
fa315352da Merge #2770
2770: Update milli 0.33.4 r=Kerollmops a=curquiza

Fixes https://github.com/meilisearch/meilisearch/issues/2764

Co-authored-by: Clémentine Urquizar <clementine@meilisearch.com>
2022-09-13 16:07:06 +00:00
268d59ccb1 Update milli version to v0.33.4 2022-09-13 18:01:09 +02:00
5901d4e407 Merge #2768
2768: Update patch versions to remove CVE r=Kerollmops a=curquiza

Trying to fix CVE we have with [synchronoise](https://github.com/QuietMisdreavus/synchronoise) crate

Co-authored-by: Clémentine Urquizar <clementine@meilisearch.com>
2022-09-12 12:47:59 +00:00
aabd67a9fa Update patch version to remove CVE 2022-09-12 14:36:45 +02:00
3fd6af25f9 Merge #2759
2759: Bump milli to 0.33.3 r=Kerollmops a=Kerollmops

This PR fixes #2743.

Co-authored-by: Kerollmops <clement@meilisearch.com>
2022-09-07 20:55:08 +00:00
441492f1c8 Bump milli to v0.33.3 2022-09-07 18:23:49 +02:00
92b0c51bfe Merge #2755
2755: Update mini-dashboard to v0.2.2 r=Kerollmops a=mdubus

# Pull Request

## What does this PR do?
Fixes #2716

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?

Thank you so much for contributing to Meilisearch!


Co-authored-by: Morgane Dubus <30866152+mdubus@users.noreply.github.com>
2022-09-07 15:53:04 +00:00
b3ffcb2d97 Merge #2758
2758: Update ubuntu-18.04 to 20.04 r=Kerollmops a=curquiza

Trying to avoid CI failure by updating ubuntu machines
Commit already available on main, so for v0.30.0
https://github.com/meilisearch/meilisearch/pull/2719

Co-authored-by: Clémentine Urquizar <clementine@meilisearch.com>
2022-09-07 15:27:19 +00:00
5cbd047989 Update ubuntu-18.04 to 20.04 2022-09-07 17:24:35 +02:00
07f45251e9 Update mini-dashboard to v0.2.2 2022-09-07 11:09:12 +02:00
37dc6537c3 Fix api keys bugs (#2734)
* Add some tests

* Disallow index creation when API key doesn't havec explicitelly the right on the creating index

* Fix lazy index creation with `indexes.*` action
2022-09-06 15:13:09 +02:00
4e37427de8 Merge #2732
2732: Update milli v0.33.2 r=Kerollmops a=ManyTheFish

closes #2722


⚠️ : merging into [release-v0.29.0](https://github.com/meilisearch/meilisearch/tree/release-v0.29.0)

Co-authored-by: ManyTheFish <many@meilisearch.com>
2022-09-01 11:18:11 +00:00
50434d35d0 Update milli v0.33.2 2022-09-01 13:15:05 +02:00
e315547ffc Merge #2724
2724: Make the document addition done log to appear once indexing is over r=curquiza a=evpeople

# Pull Request

## What does this PR do?
Fixes #2703 
<!-- Please link the issue you're trying to fix with this PR, if none then please create an issue first. -->

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?

Thank you so much for contributing to Meilisearch!


Co-authored-by: evpeople <hangcaihui@gmail.com>
2022-08-31 13:13:36 +00:00
833ade80a6 cargo update 2022-08-31 13:58:53 +08:00
f117c90c46 remove the intermediate addition variable 2022-08-30 21:49:34 +08:00
1131400694 Merge #2717
2717: Disable LTO due to compilation failures on some platforms r=curquiza a=loiclec

Meilisearch fails to compile on aarch64 Linux due to a linker error ( https://github.com/meilisearch/meilisearch/runs/8072616457?check_suite_focus=true ). This is probably caused by link-time-optimisation (LTO). Since it is not possible to modify a profile based on the target triple, this PR deactivates LTO completely for all platforms.
In the future, we might want to create different custom profiles, such as:
```toml
[profile.release-lto]
inherits = "release"
lto = "thin"
```
and compile Meilisearch using `cargo build --profile release-lto` on the platforms that can support it.


Co-authored-by: Loïc Lecrenier <loic@meilisearch.com>
2022-08-29 16:04:34 +00:00
9a789daa58 Disable LTO due to compilation failures on some platforms
(aarch64 linux)
2022-08-29 17:21:08 +02:00
0826aa35e2 Merge #2713
2713: Move prometheus behind a feature flag r=Kerollmops a=irevoire

We decided we wanted to continue working on this feature before making it public.

Co-authored-by: Tamo <tamo@meilisearch.com>
2022-08-29 12:38:42 +00:00
6aa3ad6b6c move prometheus behind a feature flag 2022-08-29 14:36:59 +02:00
b774adfbf7 The "document addition done"
appear once the indexation is over now.
2022-08-27 21:50:10 +08:00
47a1aa69f0 Merge #2702
2702: Add link to the main image r=curquiza a=brunoocasali

I have wrapped the image with a `<a>` link, and it seems to be working fine, WDYT?

Co-authored-by: Bruno Casali <brunoocasali@gmail.com>
Co-authored-by: Clémentine Urquizar - curqui <clementine@meilisearch.com>
2022-08-24 16:54:08 +00:00
b3d89da74d Update README.md 2022-08-24 18:52:34 +02:00
0798170a64 Add link to the main image 2022-08-24 13:47:09 -03:00
446dfccc8c Merge #2504 #2697
2504: New README 🌟 r=curquiza a=curquiza

⚠️ Please do not only look at the Markdown but also how the GitHub renders the README 😇 

👉 👉 [Rendered](https://github.com/meilisearch/meilisearch/blob/new-readme/README.md) 👈 👈

2697: Accept an environment variable to enable the metrics route r=ManyTheFish a=Kerollmops

With the PR Meilisearch is able to accept the `MEILI_ENABLE_METRICS_ROUTE` environment variable to enable the newly introduces metrics route.

Co-authored-by: Clémentine Urquizar <clementine@meilisearch.com>
Co-authored-by: Clément Renault <clement@meilisearch.com>
2022-08-24 15:41:44 +00:00
43175cfb78 New README version
Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update according to guigui reuqest

Add demo link

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: CaroFG <48251481+CaroFG@users.noreply.github.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Update README.md

Co-authored-by: gui machiavelli <hey@guimachiavelli.com>

Put sentence in bold
2022-08-24 17:16:29 +02:00
a1bb49c351 Merge #2696
2696: Add the new `metrics.get` and `metrics.all` actions rights r=Kerollmops a=Kerollmops

Follow the specification and add the new `metrics.get` and `metrics.all` actions, making the `/metrics` route only accessible with those rights.

Co-authored-by: Clément Renault <clement@meilisearch.com>
2022-08-24 15:08:53 +00:00
bebd76064a Add test for the rights of /metrics route 2022-08-24 17:03:43 +02:00
f0b2ac6efb metrics.all must define metrics.get 2022-08-24 17:03:30 +02:00
08d86e33ca Accept an env variable to enable the metrics route 2022-08-24 16:39:56 +02:00
2c2efc7ab6 Remove the hand written numbers of the actions rights 2022-08-24 16:33:12 +02:00
381df43be4 Change the metrics route API access rights 2022-08-24 16:28:33 +02:00
f87ebfe477 Merge #2692
2692: Slight changes for prometheus metrics r=Kerollmops a=gmourier

# Pull Request

## What does this PR do?

- Replace "MeiliSearch" with "Meilisearch"
- Brings some consistency between rust identifier and exposed metrics names
- Add suffix describing unit, in plural form. e.g `MEILISEARCH_DB_SIZE_BYTES` (https://prometheus.io/docs/practices/naming/#metric-names)
- Update dashboard.json

Co-authored-by: Guillaume Mourier <guillaume@meilisearch.com>
2022-08-24 10:12:24 +00:00
c445334070 Merge #2636
2636: Upgrade milli to v0.33.0 r=Kerollmops a=ManyTheFish

# Summary
- Update milli to v0.33.0
- Classify the new InvalidLmdbOpenOptions error as an Internal error
- Update filter error check in tests
- Introduce Terms Matching Policies

fixes #2479
fixes #2484
fixes #2486
fixes #2516
fixes #2578
fixes #2580
fixes #2583
fixes #2600
fixes #2640
fixes #2672
fixes #2679
fixes #2686

# Terms Matching Policies
This PR allows end users to customize matching term policies

## Todo

- [x] Update the API to return the number of pages and allow users to directly choose a page instead of computing an offset
- [x] Change generation of the query tree depending on the chosen settings https://github.com/meilisearch/milli/pull/598

## Small Documentation

### Default search query

**request**:
```sh
curl \
  -X POST 'http://localhost:7700/indexes/movies/search' \
  -H 'Content-Type: application/json' \
  --data-binary '{ "q": "doctor of tokio" }'
```

**result**:
```json
{
  "hits":[...],
  "estimatedTotalHits":32,
  "query":"doctor of tokio",
  "limit":20,
  "offset":0,
  "processingTimeMs":7
}
```

The default behavior doesn't change with the current Meilisearch behavior:
If we don't have enough documents to fit the requested limit, we remove the query words from the last to the first typed word.

## Search query with `optionalWords` parameter

**request**:
```sh
curl \
  -X POST 'http://localhost:7700/indexes/movies/search' \
  -H 'Content-Type: application/json' \
  --data-binary '{ "q": "doctor of tokio", "matchingStrategy": "all"}'
```

**result**:
```json
{
  "hits":[...],
  "estimatedTotalHits":1,
  "query":"doctor of tokio",
  "limit":20,
  "offset":0,
  "processingTimeMs":7
}
```

### allowed `matchingStrategy` values

#### `last`
The default behavior, If we don't have enough documents to fit the requested limit, we remove the query words from the last to the first typed word.

#### `all`
No word will be removed, If we don't have enough documents to fit the requested limit, we return the number of documents we found.

### In charge of the feature

Core: `@ManyTheFish` & `@curquiza`  
Docs: TBD
Integration: `@bidoubiwa` 


Co-authored-by: ManyTheFish <many@meilisearch.com>
Co-authored-by: Many the fish <many@meilisearch.com>
2022-08-23 16:21:00 +00:00
651a22b1ed Enhance enum documentation
Co-authored-by: Clément Renault <clement@meilisearch.com>
2022-08-23 18:11:20 +02:00
ff59ae56f4 cargo fmt 2022-08-23 17:17:02 +02:00
b2577aac52 Add suffix describing the unit when needed; Replace MeiliSearch by Meilisearch; Precised some metrics name 2022-08-23 17:09:27 +02:00
c9bb111ef3 Implement all and last matching strategy 2022-08-23 17:07:43 +02:00
e2af8dccb8 Fix filter tests 2022-08-23 16:39:39 +02:00
5e206ee84b Classify InvalidLmdbOpenOptions as an Internal error 2022-08-23 16:39:39 +02:00
aff4b64265 Update dependencies 2022-08-23 16:39:39 +02:00
0a2ef0037f Merge #2689 #2690
2689: Use mimalloc as the global allocator r=Kerollmops a=loiclec

milli has switched its global allocator to mimalloc already, and we have seen some performance gains as a result. Furthermore, we can use mimalloc as the global allocator on all platforms whereas jemalloc was only activated on Linux. 

This PR brings mimalloc to Meilisearch as well. 

2690: Add LTO and codegen-units=1 to release compile options r=Kerollmops a=loiclec

This PR brings Meilisearch's release compile options in line with milli (see https://github.com/meilisearch/milli/pull/606 ). 

Adding LTO and codegen=units=1 will make compile times longer, but they also speed up the final binary significantly.

Co-authored-by: Loïc Lecrenier <loic@meilisearch.com>
2022-08-23 12:05:02 +00:00
40ae26478a Merge #2691
2691: Update version for next release (v0.29.0) in Cargo.toml files r=Kerollmops a=curquiza



Co-authored-by: Clémentine Urquizar <clementine@meilisearch.com>
2022-08-23 11:41:22 +00:00
6fe3f285ce Update version for next release (v0.29.0) 2022-08-23 13:39:56 +02:00
72f8adaa70 Add LTO and codegen-units=1 to release compile options 2022-08-23 13:03:57 +02:00
e659c08ac4 Use mimalloc as the global allocator 2022-08-23 12:58:10 +02:00
ea365126b4 Merge #2657
2657: prometheus and grafana dashboards implemented r=irevoire a=pavo-tusker

Implemented Basic Prometheus Metrics and Grafana Dashboard using this [Prometheus Crate](https://crates.io/crates/prometheus) [#496](https://github.com/meilisearch/product/issues/496)
![Screenshot from 2022-08-04 19-59-06](https://user-images.githubusercontent.com/43550760/182880420-71ec8591-a2cb-4fd5-b1c5-911a6dcbdaf9.png)
![Screenshot from 2022-08-04 19-58-56](https://user-images.githubusercontent.com/43550760/182880433-11727814-e230-44dd-89c9-fec3baa47b11.png)
![Screenshot from 2022-08-04 19-58-40](https://user-images.githubusercontent.com/43550760/182880436-73312a68-4f20-49f0-80e9-5e344f96db6f.png)


Co-authored-by: mohandasspat <mohan.s@pavo-tusker.com>
Co-authored-by: Pavo-Tusker <43550760+pavo-tusker@users.noreply.github.com>
Co-authored-by: Tamo <tamo@meilisearch.com>
2022-08-22 11:15:42 +00:00
a277cc9a18 Merge branch 'metrics/prometheus-setup' of https://github.com/pavo-tusker/meilisearch into metrics/prometheus-setup 2022-08-22 13:34:37 +05:30
a37c7ba1bb clippy & cargo fixed 2022-08-22 13:34:19 +05:30
ef1d6b1694 clippy & cargo fixed 2022-08-22 13:27:26 +05:30
099abefc6d Merge branch 'main' into metrics/prometheus-setup 2022-08-22 09:56:15 +02:00
a05101af4d clippy & fmt fixed 2022-08-22 13:21:22 +05:30
109540011a conflict fixes 2022-08-22 13:21:22 +05:30
2f92169e48 clippy issue in metrics fixed 2022-08-22 13:21:22 +05:30
a58b00d8f1 Update meilisearch-http/src/option.rs
Co-authored-by: Tamo <irevoire@protonmail.ch>
2022-08-22 13:21:22 +05:30
2b8f3c26ec Changed prometheus metrics feature as optional 2022-08-22 13:21:22 +05:30
0b6ca73790 review fixes 2022-08-22 13:21:22 +05:30
1f1482e97c Update meilisearch-http/src/routes/mod.rs
Co-authored-by: Tamo <irevoire@protonmail.ch>
2022-08-22 13:21:22 +05:30
25fecf9360 clippy & rustfmt fixed 2022-08-22 13:21:22 +05:30
4bee0565e8 prometheus and grafana dashboards implemented 2022-08-22 13:21:22 +05:30
d5da063666 clippy & fmt fixed 2022-08-22 10:52:09 +05:30
43bb5176a9 conflict fixes 2022-08-22 10:30:07 +05:30
a0734c991c Merge #2674
2674: Add analytics on the stats routes r=ManyTheFish a=irevoire

# Pull Request

## What does this PR do?
Implements https://github.com/meilisearch/specifications/pull/169

## PR checklist
Please check if your PR fulfills the following requirements:
- [ ] Does this PR fix an existing issue?
- [ ] Have you read the contributing guidelines?
- [ ] Have you made sure that the title is accurate and descriptive of the changes?

Thank you so much for contributing to Meilisearch!


Co-authored-by: Irevoire <tamo@meilisearch.com>
2022-08-18 14:19:56 +00:00
cb29d7d124 Merge #2678
2678: Accept either an array of documents or a single document r=irevoire a=Kerollmops

# Pull Request

## What does this PR do?
Fixes #2671

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?


Co-authored-by: Clément Renault <clement@meilisearch.com>
2022-08-18 14:00:01 +00:00
e32d5ef2b3 Fix the test with an uncomprehensible user error message 2022-08-18 14:37:44 +02:00
ee69ede1ce Merge #2677
2677: Hide the batch_uid field from the tasks route r=Kerollmops a=Kerollmops

# Pull Request

## What does this PR do?

Fixes #2676

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?


Co-authored-by: Clément Renault <clement@meilisearch.com>
2022-08-18 10:01:09 +00:00
9b2036ac05 Accept either an array of documents or a single document 2022-08-18 11:55:14 +02:00
5c543f9d94 Add a test for single document upload 2022-08-18 11:33:22 +02:00
0c03ed3c1e Hide the batch_uid field from the tasks route 2022-08-18 11:15:21 +02:00
54a0b47c2b clippy issue in metrics fixed 2022-08-17 21:08:28 +05:30
947fb5c956 Update meilisearch-http/src/option.rs
Co-authored-by: Tamo <irevoire@protonmail.ch>
2022-08-17 20:57:07 +05:30
cd18459484 Changed prometheus metrics feature as optional 2022-08-17 20:56:15 +05:30
225d9936ed review fixes 2022-08-17 20:55:29 +05:30
93daa4c464 Update meilisearch-http/src/routes/mod.rs
Co-authored-by: Tamo <irevoire@protonmail.ch>
2022-08-17 20:55:29 +05:30
d08c77706c clippy & rustfmt fixed 2022-08-17 20:55:29 +05:30
de58ccd4ba prometheus and grafana dashboards implemented 2022-08-17 20:54:39 +05:30
62240b7e19 add analytics on the stats routes 2022-08-17 16:12:26 +02:00
22874ce300 Merge #2664
2664: 🐞 fix: Support https in print_launch_resume r=irevoire a=evpeople

fix #2660

# Pull Request

## What does this PR do?
Fixes #2660 
<!-- Please link the issue you're trying to fix with this PR, if none then please create an issue first. -->

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?

Thank you so much for contributing to Meilisearch!


Co-authored-by: evpeople <hangcaihui@gmail.com>
2022-08-16 14:40:20 +00:00
b5f91b91c3 Merge #2523
2523: Improve the tasks error reporting when processed in batches r=irevoire a=Kerollmops

This fixes #2478 by changing the behavior of the task handler when there is an error in a batch of document addition or update.

What changes is that when there is a user error in a task in a batch we now report this task as failed with the right error message but we continue to process the other tasks. A user error can be when a geo field is invalid, a document id is invalid, or missing.

fixes #2582, #2478

Co-authored-by: Kerollmops <clement@meilisearch.com>
Co-authored-by: ManyTheFish <many@meilisearch.com>
2022-08-16 14:15:30 +00:00
b6e6a08f7d Fix CI test 2022-08-16 15:14:01 +02:00
8198bb9da2 Merge #2665
2665: 📎 makes clippy happy r=Kerollmops a=irevoire



Co-authored-by: Irevoire <tamo@meilisearch.com>
2022-08-16 12:01:56 +00:00
68a7d6bc61 reformat 2022-08-12 15:11:01 +02:00
83e20027fd 📎 makes clippy happy 2022-08-12 14:18:27 +02:00
f21a4d61da 🌈 style(http/main.rs): 2022-08-12 16:16:23 +08:00
12538d5a44 🐞 fix: Support https when print_lanuch_resume
fix #2660
2022-08-11 21:29:18 +08:00
ae174c2cca Fix task serialization 2022-08-11 13:35:35 +02:00
e6b806e0cf Merge #2662
2662: Fix(cli): Clamp databases max size to a multiple of system page size r=Kerollmops a=ManyTheFish

fix #2659


Co-authored-by: ManyTheFish <many@meilisearch.com>
2022-08-11 08:49:24 +00:00
cf955a77db Fix(cli): Clamp databases max size to a multiple of system page size
fix #2659
2022-08-11 10:44:47 +02:00
3a48de136e Add autobatching test 2022-08-10 17:02:29 +02:00
e6f03f82df Fix clippy warnings 2022-07-28 15:56:22 +02:00
58d2aad309 Change binary option and add env var support 2022-07-28 15:13:49 +02:00
e3426d5b7a Improve the tasks error reporting 2022-07-28 15:12:54 +02:00
73d4869e5e Make the changes to plug the new DocumentsBatch system 2022-07-28 14:45:33 +02:00
fe32097964 Update milli v0.32 2022-07-28 14:45:10 +02:00
63 changed files with 2472 additions and 1066 deletions

View File

@ -4,6 +4,10 @@ First, thank you for contributing to Meilisearch! The goal of this document is t
Remember that there are many ways to contribute other than writing code: writing [tutorials or blog posts](https://github.com/meilisearch/awesome-meilisearch), improving [the documentation](https://github.com/meilisearch/documentation), submitting [bug reports](https://github.com/meilisearch/meilisearch/issues/new?assignees=&labels=&template=bug_report.md&title=) and [feature requests](https://github.com/meilisearch/product/discussions/categories/feedback-feature-proposal)... Remember that there are many ways to contribute other than writing code: writing [tutorials or blog posts](https://github.com/meilisearch/awesome-meilisearch), improving [the documentation](https://github.com/meilisearch/documentation), submitting [bug reports](https://github.com/meilisearch/meilisearch/issues/new?assignees=&labels=&template=bug_report.md&title=) and [feature requests](https://github.com/meilisearch/product/discussions/categories/feedback-feature-proposal)...
The code in this repository is only concerned with managing multiple indexes, handling the update store, and exposing an HTTP API. Search and indexation are the domain of our core engine, [`milli`](https://github.com/meilisearch/milli), while tokenization is handled by [our `charabia` library](https://github.com/meilisearch/charabia/).
If Meilisearch does not offer optimized support for your language, please consider contributing to `charabia` by following the [CONTRIBUTING.md file](https://github.com/meilisearch/charabia/blob/main/CONTRIBUTING.md) and integrating your intended normalizer/segmenter.
## Table of Contents ## Table of Contents
- [Assumptions](#assumptions) - [Assumptions](#assumptions)

1024
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,9 @@ members = [
"permissive-json-pointer", "permissive-json-pointer",
] ]
[profile.release]
codegen-units = 1
[profile.dev.package.flate2] [profile.dev.package.flate2]
opt-level = 3 opt-level = 3

View File

@ -1,5 +1,5 @@
# Compile # Compile
FROM rust:alpine3.14 AS compiler FROM rust:alpine3.16 AS compiler
RUN apk add -q --update-cache --no-cache build-base openssl-dev RUN apk add -q --update-cache --no-cache build-base openssl-dev
@ -19,7 +19,7 @@ RUN set -eux; \
cargo build --release cargo build --release
# Run # Run
FROM alpine:3.14 FROM alpine:3.16
ENV MEILI_HTTP_ADDR 0.0.0.0:7700 ENV MEILI_HTTP_ADDR 0.0.0.0:7700
ENV MEILI_SERVER_PROVIDER docker ENV MEILI_SERVER_PROVIDER docker

212
README.md
View File

@ -1,205 +1,103 @@
<p align="center"> <p align="center">
<img src="assets/logo.svg" alt="Meilisearch" width="200" height="200" /> <img src="assets/meilisearch-logo-light.svg?sanitize=true#gh-light-mode-only">
<img src="assets/meilisearch-logo-dark.svg?sanitize=true#gh-dark-mode-only">
</p> </p>
<h1 align="center">Meilisearch</h1>
<h4 align="center"> <h4 align="center">
<a href="https://www.meilisearch.com">Website</a> | <a href="https://www.meilisearch.com">Website</a> |
<a href="https://roadmap.meilisearch.com/tabs/1-under-consideration">Roadmap</a> | <a href="https://roadmap.meilisearch.com/tabs/1-under-consideration">Roadmap</a> |
<a href="https://blog.meilisearch.com">Blog</a> | <a href="https://blog.meilisearch.com">Blog</a> |
<a href="https://fr.linkedin.com/company/meilisearch">LinkedIn</a> |
<a href="https://twitter.com/meilisearch">Twitter</a> |
<a href="https://docs.meilisearch.com">Documentation</a> | <a href="https://docs.meilisearch.com">Documentation</a> |
<a href="https://docs.meilisearch.com/faq/">FAQ</a> <a href="https://docs.meilisearch.com/faq/">FAQ</a> |
<a href="https://slack.meilisearch.com">Slack</a>
</h4> </h4>
<p align="center"> <p align="center">
<a href="https://github.com/meilisearch/meilisearch/actions"><img src="https://github.com/meilisearch/meilisearch/workflows/Cargo%20test/badge.svg" alt="Build Status"></a> <a href="https://github.com/meilisearch/meilisearch/actions"><img src="https://github.com/meilisearch/meilisearch/workflows/Cargo%20test/badge.svg" alt="Build Status"></a>
<a href="https://deps.rs/repo/github/meilisearch/meilisearch"><img src="https://deps.rs/repo/github/meilisearch/meilisearch/status.svg" alt="Dependency status"></a> <a href="https://deps.rs/repo/github/meilisearch/meilisearch"><img src="https://deps.rs/repo/github/meilisearch/meilisearch/status.svg" alt="Dependency status"></a>
<a href="https://github.com/meilisearch/meilisearch/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-informational" alt="License"></a> <a href="https://github.com/meilisearch/meilisearch/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-informational" alt="License"></a>
<a href="https://slack.meilisearch.com"><img src="https://img.shields.io/badge/slack-meilisearch-blue.svg?logo=slack" alt="Slack"></a>
<a href="https://github.com/meilisearch/meilisearch/discussions" alt="Discussions"><img src="https://img.shields.io/badge/github-discussions-red" /></a>
<a href="https://app.bors.tech/repositories/26457"><img src="https://bors.tech/images/badge_small.svg" alt="Bors enabled"></a> <a href="https://app.bors.tech/repositories/26457"><img src="https://bors.tech/images/badge_small.svg" alt="Bors enabled"></a>
</p> </p>
<p align="center">Lightning Fast, Ultra Relevant, and Typo-Tolerant Search Engine 🔍</p> <p align="center">A lightning-fast search engine that fits effortlessly into your apps, websites, and workflow 🔍</p>
**Meilisearch** is a powerful, fast, open-source, easy to use and deploy search engine. Both searching and indexing are highly customizable. Features such as typo-tolerance, filters, and synonyms are provided out-of-the-box. Meilisearch helps you shape a delightful search experience in a snap, offering features that work out-of-the-box to speed up your workflow.
For more information about features go to [our documentation](https://docs.meilisearch.com/).
<p align="center"> <p align="center" name="demo">
<img src="assets/trumen-fast.gif" alt="Web interface gif" /> <a href="https://where2watch.meilisearch.com/#gh-light-mode-only" target="_blank">
<img src="assets/demo-light.gif#gh-light-mode-only" alt="A bright colored application for finding movies screening near the user">
</a>
<a href="https://where2watch.meilisearch.com/#gh-dark-mode-only" target="_blank">
<img src="assets/demo-dark.gif#gh-dark-mode-only" alt="A dark colored application for finding movies screening near the user">
</a>
</p> </p>
🔥 [**Try it!**](https://where2watch.meilisearch.com/) 🔥
## ✨ Features ## ✨ Features
* Search-as-you-type experience (answers < 50 milliseconds)
* Full-text search
* Typo tolerant (understands typos and misspelling)
* Faceted search and filters
* Supports hanzi (Chinese characters)
* Supports synonyms
* Easy to install, deploy, and maintain
* Whole documents are returned
* Highly customizable
* RESTful API
## Getting started - **Search-as-you-type:** find search results in less than 50 milliseconds
- **[Typo tolerance](https://docs.meilisearch.com/learn/getting_started/customizing_relevancy.html#typo-tolerance):** get relevant matches even when queries contain typos and misspellings
- **[Filtering and faceted search](https://docs.meilisearch.com/learn/advanced/filtering_and_faceted_search.html):** enhance your user's search experience with custom filters and build a faceted search interface in a few lines of code
- **[Sorting](https://docs.meilisearch.com/learn/advanced/sorting.html):** sort results based on price, date, or pretty much anything else your users need
- **[Synonym support](https://docs.meilisearch.com/learn/getting_started/customizing_relevancy.html#synonyms):** configure synonyms to include more relevant content in your search results
- **[Geosearch](https://docs.meilisearch.com/learn/advanced/geosearch.html):** filter and sort documents based on geographic data
- **[Extensive language support](https://docs.meilisearch.com/learn/what_is_meilisearch/language.html):** search datasets in any language, with optimized support for Chinese, Japanese, Hebrew, and languages using the Latin alphabet
- **[Security management](https://docs.meilisearch.com/learn/security/master_api_keys.html):** control which users can access what data with API keys that allow fine-grained permissions handling
- **[Multi-Tenancy](https://docs.meilisearch.com/learn/security/tenant_tokens.html):** personalize search results for any number of application tenants
- **Highly Customizable:** customize Meilisearch to your specific needs or use our out-of-the-box and hassle-free presets
- **[RESTful API](https://docs.meilisearch.com/reference/api/overview.html):** integrate Meilisearch in your technical stack with our plugins and SDKs
- **Easy to install, deploy, and maintain**
### Deploy the Server ## 📖 Documentation
#### Homebrew (Mac OS) You can consult Meilisearch's documentation at [https://docs.meilisearch.com](https://docs.meilisearch.com/).
```bash ## 🚀 Getting started
brew update && brew install meilisearch
meilisearch
```
#### Docker For basic instructions on how to set up Meilisearch, add documents to an index, and search for documents, take a look at our [Quick Start](https://docs.meilisearch.com/learn/getting_started/quick_start.html) guide.
```bash You may also want to check out [Meilisearch 101](https://docs.meilisearch.com/learn/getting_started/filtering_and_sorting.html) for an introduction to some of Meilisearch's most popular features.
docker run -p 7700:7700 -v "$(pwd)/meili_data:/meili_data" getmeili/meilisearch
```
#### Announcing a cloud-hosted Meilisearch ## ☁️ Meilisearch cloud
Join the closed beta by filling out this [form](https://meilisearch.typeform.com/to/VI2cI2rv). Join the closed beta for Meilisearch cloud by filling out [this form](https://meilisearch.typeform.com/to/VI2cI2rv).
#### Try Meilisearch in our Sandbox ## 🧰 SDKs & integration tools
Create a Meilisearch instance in [Meilisearch Sandbox](https://sandbox.meilisearch.com/). This instance is free, and will be active for 48 hours. Install one of our SDKs in your project for seamless integration between Meilisearch and your favorite language or framework!
#### Run on Digital Ocean Take a look at the complete [Meilisearch integration list](https://docs.meilisearch.com/learn/what_is_meilisearch/sdks.html).
[![DigitalOcean Marketplace](assets/do-btn-blue.svg)](https://marketplace.digitalocean.com/apps/meilisearch?action=deploy&refcode=7c67bd97e101) ![Logos belonging to different languages and frameworks supported by Meilisearch, including React, Ruby on Rails, Go, Rust, and PHP](assets/integrations.png)
#### Deploy on Platform.sh ## ⚙️ Advanced usage
<a href="https://console.platform.sh/projects/create-project?template=https://raw.githubusercontent.com/platformsh/template-builder/master/templates/meilisearch/.platform.template.yaml&utm_content=meilisearch&utm_source=github&utm_medium=button&utm_campaign=deploy_on_platform"> Experienced users will want to keep our [API Reference](https://docs.meilisearch.com/reference/api) close at hand.
<img src="https://platform.sh/images/deploy/lg-blue.svg" alt="Deploy on Platform.sh" width="180px" />
</a>
#### APT (Debian & Ubuntu) We also offer a wide range of dedicated guides to all Meilisearch features, such as [filtering](https://docs.meilisearch.com/learn/advanced/filtering_and_faceted_search.html), [sorting](https://docs.meilisearch.com/learn/advanced/sorting.html), [geosearch](https://docs.meilisearch.com/learn/advanced/geosearch.html), [API keys](https://docs.meilisearch.com/learn/security/master_api_keys.html), and [tenant tokens](https://docs.meilisearch.com/learn/security/tenant_tokens.html).
```bash Finally, for more in-depth information, refer to our articles explaining fundamental Meilisearch concepts such as [documents](https://docs.meilisearch.com/learn/core_concepts/documents.html) and [indexes](https://docs.meilisearch.com/learn/core_concepts/indexes.html).
echo "deb [trusted=yes] https://apt.fury.io/meilisearch/ /" > /etc/apt/sources.list.d/fury.list
apt update && apt install meilisearch-http
meilisearch
```
#### Download the binary (Linux & Mac OS) ## 📊 Telemetry
```bash Meilisearch collects **anonymized** data from users to help us improve our product. You can [deactivate this](https://docs.meilisearch.com/learn/what_is_meilisearch/telemetry.html#how-to-disable-data-collection) whenever you want.
curl -L https://install.meilisearch.com | sh
./meilisearch
```
#### Compile and run it from sources To request deletion of collected data, please write to us at [privacy@meilisearch.com](mailto:privacy@meilisearch.com). Don't forget to include your `Instance UID` in the message, as this helps us quickly find and delete your data.
If you have the latest stable Rust toolchain installed on your local system, clone the repository and change it to your working directory. If you want to know more about the kind of data we collect and what we use it for, check the [telemetry section](https://docs.meilisearch.com/learn/what_is_meilisearch/telemetry.html) of our documentation.
```bash ## 📫 Get in touch!
git clone https://github.com/meilisearch/meilisearch.git
cd meilisearch
cargo run --release
```
### Create an Index and Upload Some Documents Meilisearch is a search engine created by [Meili](https://www.welcometothejungle.com/en/companies/meilisearch), a software development company based in France and with team members all over the world. Want to know more about us? [Check out our blog!](https://blog.meilisearch.com/)
Let's create an index! If you need a sample dataset, use [this movie database](https://www.notion.so/meilisearch/A-movies-dataset-to-test-Meili-1cbf7c9cfa4247249c40edfa22d7ca87#b5ae399b81834705ba5420ac70358a65). You can also find it in the `datasets/` directory. 🗞 [Subscribe to our newsletter](https://meilisearch.us2.list-manage.com/subscribe?u=27870f7b71c908a8b359599fb&id=79582d828e) if you don't want to miss any updates! We promise we won't clutter your mailbox: we only send one edition every two months.
```bash 💌 Want to make a suggestion or give feedback? Here are some of the channels where you can reach us:
curl -L https://docs.meilisearch.com/movies.json -o movies.json
```
Now, you're ready to index some data. - For feature requests, please visit our [product repository](https://github.com/meilisearch/product/discussions)
- Found a bug? Open an [issue](https://github.com/meilisearch/meilisearch/issues)!
- Want to be part of our Slack community? [Join us!](https://slack.meilisearch.com/)
- For everything else, please check [this page listing some of the other places where you can find us](https://docs.meilisearch.com/learn/what_is_meilisearch/contact.html)
```bash Thank you for your support!
curl -i -X POST 'http://127.0.0.1:7700/indexes/movies/documents' \
--header 'content-type: application/json' \
--data-binary @movies.json
```
### Search for Documents
#### In command line
The search engine is now aware of your documents and can serve those via an HTTP server.
The [`jq` command-line tool](https://stedolan.github.io/jq/) can greatly help you read the server responses.
```bash
curl 'http://127.0.0.1:7700/indexes/movies/search?q=botman+robin&limit=2' | jq
```
```json
{
"hits": [
{
"id": "415",
"title": "Batman & Robin",
"poster": "https://image.tmdb.org/t/p/w1280/79AYCcxw3kSKbhGpx1LiqaCAbwo.jpg",
"overview": "Along with crime-fighting partner Robin and new recruit Batgirl, Batman battles the dual threat of frosty genius Mr. Freeze and homicidal horticulturalist Poison Ivy. Freeze plans to put Gotham City on ice, while Ivy tries to drive a wedge between the dynamic duo.",
"release_date": 866768400
},
{
"id": "411736",
"title": "Batman: Return of the Caped Crusaders",
"poster": "https://image.tmdb.org/t/p/w1280/GW3IyMW5Xgl0cgCN8wu96IlNpD.jpg",
"overview": "Adam West and Burt Ward returns to their iconic roles of Batman and Robin. Featuring the voices of Adam West, Burt Ward, and Julie Newmar, the film sees the superheroes going up against classic villains like The Joker, The Riddler, The Penguin and Catwoman, both in Gotham City… and in space.",
"release_date": 1475888400
}
],
"nbHits": 8,
"exhaustiveNbHits": false,
"query": "botman robin",
"limit": 2,
"offset": 0,
"processingTimeMs": 2
}
```
#### Use the Web Interface
We also deliver an **out-of-the-box [web interface](https://github.com/meilisearch/mini-dashboard)** in which you can test Meilisearch interactively.
You can access the web interface in your web browser at the root of the server. The default URL is [http://127.0.0.1:7700](http://127.0.0.1:7700). All you need to do is open your web browser and enter Meilisearchs address to visit it. This will lead you to a web page with a search bar that will allow you to search in the selected index.
| [See the gif above](#demo)
## Documentation
Now that your Meilisearch server is up and running, you can learn more about how to tune your search engine in [the documentation](https://docs.meilisearch.com).
## Contributing
Hey! We're glad you're thinking about contributing to Meilisearch! Feel free to pick an [issue labeled as `good first issue`](https://github.com/meilisearch/meilisearch/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), and to ask any question you need. Some points might not be clear and we are available to help you!
Also, we recommend following the [CONTRIBUTING](./CONTRIBUTING.md) to create your PR.
## Core engine and tokenizer
The code in this repository is only concerned with managing multiple indexes, handling the update store, and exposing an HTTP API.
Search and indexation are the domain of our core engine, [`milli`](https://github.com/meilisearch/milli), while tokenization is handled by [our `tokenizer` library](https://github.com/meilisearch/tokenizer/).
## Telemetry
Meilisearch collects anonymous data regarding general usage.
This helps us better understand developers' usage of Meilisearch features.
To find out more on what information we're retrieving, please see our documentation on [Telemetry](https://docs.meilisearch.com/learn/what_is_meilisearch/telemetry.html).
This program is optional, you can disable these analytics by using the `MEILI_NO_ANALYTICS` env variable.
## Feature request
The feature requests are not managed in this repository. Please visit our [dedicated repository](https://github.com/meilisearch/product) to see our work about the Meilisearch product.
If you have a feature request or any feedback about an existing feature, please open [a discussion](https://github.com/meilisearch/product/discussions).
Also, feel free to participate in the current discussions, we are looking forward to reading your comments.
## 💌 Contact
Please visit [this page](https://docs.meilisearch.com/learn/what_is_meilisearch/contact.html#contact-us).
Meilisearch is developed by [Meili](https://www.meilisearch.com), a young company. To know more about us, you can [read our blog](https://blog.meilisearch.com). Any suggestion or feedback is highly appreciated. Thank you for your support!

BIN
assets/demo-dark.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
assets/demo-light.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
assets/integrations.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

View File

@ -0,0 +1,30 @@
<svg width="495" height="74" viewBox="0 0 495 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M181.842 42.5349C181.842 37.6137 184.201 34.715 188.718 34.715C192.965 34.715 194.381 37.7486 194.381 41.6585V62.6238H203.953V40.5799C203.953 32.3556 199.639 26.4907 191.145 26.4907C186.089 26.4907 182.516 28.0412 179.415 31.4792C177.393 28.3782 173.955 26.4907 169.168 26.4907C164.112 26.4907 160.607 28.5805 158.989 31.614V27.2996H150.158V62.6238H159.731V42.3326C159.731 37.6137 162.157 34.715 166.607 34.715C170.854 34.715 172.269 37.7486 172.269 41.6585V62.6238H181.842V42.5349Z" fill="white"/>
<path d="M243.245 47.7256C243.245 47.7256 243.379 46.4448 243.379 44.8943C243.379 34.4454 236.301 26.4907 225.852 26.4907C215.403 26.4907 208.123 34.4454 208.123 44.8943C208.123 55.7477 215.471 63.4327 225.92 63.4327C234.077 63.4327 240.548 58.5116 242.638 51.3659H232.998C231.852 53.9276 229.088 55.2084 226.189 55.2084C221.403 55.2084 218.302 52.5793 217.628 47.7256H243.245ZM225.785 34.1757C230.234 34.1757 233.133 36.8722 233.807 40.8495H217.763C218.572 36.8048 221.403 34.1757 225.785 34.1757Z" fill="white"/>
<path d="M244.791 35.524H249.038V62.6238H258.61V27.2996H244.791V35.524ZM253.824 22.7156C257.195 22.7156 259.622 20.3561 259.622 16.9855C259.622 13.6149 257.195 11.188 253.824 11.188C250.454 11.188 248.027 13.6149 248.027 16.9855C248.027 20.3561 250.454 22.7156 253.824 22.7156Z" fill="white"/>
<path d="M278.432 54.3995C278.163 54.3995 277.758 54.4669 277.152 54.4669C274.994 54.4669 274.725 53.4557 274.725 51.9726V12.0644H265.152V52.6467C265.152 59.6576 267.849 62.7586 275.466 62.7586C276.747 62.7586 277.96 62.6238 278.432 62.5564V54.3995Z" fill="white"/>
<path d="M279.521 35.524H283.768V62.6238H293.341V27.2996H279.521V35.524ZM288.555 22.7156C291.925 22.7156 294.352 20.3561 294.352 16.9855C294.352 13.6149 291.925 11.188 288.555 11.188C285.184 11.188 282.757 13.6149 282.757 16.9855C282.757 20.3561 285.184 22.7156 288.555 22.7156Z" fill="white"/>
<path d="M312.557 62.9937C321.86 62.9937 326.242 58.0726 326.242 52.8819C326.242 38.4556 305.007 46.4777 305.007 36.9725C305.007 33.8716 307.636 31.2425 312.962 31.2425C318.422 31.2425 320.984 34.2086 321.388 37.9163H326.175C325.77 33.2648 322.602 27.0629 313.097 27.0629C304.94 27.0629 300.356 31.9166 300.356 37.1748C300.356 51.264 321.591 43.1745 321.591 53.0167C321.591 56.4547 318.355 58.8142 312.557 58.8142C306.625 58.8142 303.659 55.848 303.322 51.4662H298.468C298.873 57.4659 302.648 62.9937 312.557 62.9937Z" fill="white"/>
<path d="M364.256 46.4103C364.256 46.4103 364.324 45.3317 364.324 44.5901C364.324 34.8827 358.054 27.0629 347.808 27.0629C337.494 27.0629 330.955 35.4894 330.955 44.9946C330.955 54.6346 337.022 62.9937 347.875 62.9937C356.032 62.9937 361.695 58.0052 363.717 51.4662H358.729C357.245 55.6458 353.201 58.6794 347.943 58.6794C340.729 58.6794 336.213 53.3538 335.741 46.4103H364.256ZM347.808 31.3773C354.549 31.3773 358.931 35.8939 359.538 42.5004H335.876C336.685 36.1636 341.134 31.3773 347.808 31.3773Z" fill="white"/>
<path d="M394.037 45.871V49.1068C394.037 54.9717 389.79 59.0164 381.634 59.0164C376.578 59.0164 373.814 56.9266 373.814 52.41C373.814 50.118 374.892 48.3652 376.578 47.4215C378.33 46.4777 380.69 45.871 394.037 45.871ZM381.094 62.9937C387.027 62.9937 391.813 61.1062 394.24 57.1963V62.1848H398.824V39.7364C398.824 32.1188 394.442 27.0629 384.532 27.0629C375.027 27.0629 370.848 31.8492 369.971 37.9837H374.623C375.566 33.13 379.274 31.1751 384.33 31.1751C390.802 31.1751 394.037 33.8716 394.037 39.669V41.8936C383.184 41.8936 378.667 42.0959 375.297 43.4441C371.387 44.9946 369.095 48.4327 369.095 52.5448C369.095 58.5445 372.937 62.9937 381.094 62.9937Z" fill="white"/>
<path d="M424.991 27.6022C424.991 27.6022 424.182 27.5348 423.845 27.5348C417.509 27.5348 414.138 30.838 412.857 33.1974V27.8718H408.273V62.1848H413.059V42.7026C413.059 35.5569 417.441 32.0514 423.306 32.0514C424.182 32.0514 424.991 32.1188 424.991 32.1188V27.6022Z" fill="white"/>
<path d="M425.809 45.062C425.809 54.4324 432.28 62.9937 442.729 62.9937C452.032 62.9937 457.425 56.7918 458.773 49.9831H453.92C452.504 55.3087 448.594 58.6794 442.729 58.6794C435.516 58.6794 430.662 52.9493 430.662 45.062C430.662 37.1073 435.516 31.3773 442.729 31.3773C448.594 31.3773 452.504 34.7479 453.92 40.0735H458.773C457.425 33.2648 452.032 27.0629 442.729 27.0629C432.28 27.0629 425.809 35.6243 425.809 45.062Z" fill="white"/>
<path d="M470.041 11.6254H465.255V62.1848H470.041V41.8936C470.041 34.8827 474.558 31.2425 480.355 31.2425C486.49 31.2425 489.389 35.0176 489.389 41.2195V62.1848H494.175V40.2757C494.175 32.6581 489.658 27.0629 481.164 27.0629C474.76 27.0629 471.255 30.5683 470.041 32.6581V11.6254Z" fill="white"/>
<path d="M0.825012 73.993L24.0688 14.5224C27.3443 6.14179 35.4223 0.625977 44.4203 0.625977H58.4336L35.1899 60.0966C31.9143 68.4772 23.8363 73.993 14.8384 73.993H0.825012Z" fill="url(#paint0_linear_0_3)"/>
<path d="M34.9246 73.9932L58.1684 14.5226C61.444 6.14197 69.5219 0.626152 78.5199 0.626152H92.5333L69.2895 60.0968C66.014 68.4774 57.936 73.9932 48.938 73.9932H34.9246Z" fill="url(#paint1_linear_0_3)"/>
<path d="M69.0262 73.9932L92.27 14.5226C95.5456 6.14197 103.624 0.626152 112.622 0.626152H126.635L103.391 60.0968C100.116 68.4774 92.0376 73.9932 83.0396 73.9932H69.0262Z" fill="url(#paint2_linear_0_3)"/>
<defs>
<linearGradient id="paint0_linear_0_3" x1="126.635" y1="-4.97799" x2="0.825008" y2="66.0978" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5CAA"/>
<stop offset="1" stop-color="#FF4E62"/>
</linearGradient>
<linearGradient id="paint1_linear_0_3" x1="126.635" y1="-4.97799" x2="0.825008" y2="66.0978" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5CAA"/>
<stop offset="1" stop-color="#FF4E62"/>
</linearGradient>
<linearGradient id="paint2_linear_0_3" x1="126.635" y1="-4.97799" x2="0.825008" y2="66.0978" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5CAA"/>
<stop offset="1" stop-color="#FF4E62"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,30 @@
<svg width="495" height="74" viewBox="0 0 495 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M181.84 42.5347C181.84 37.6136 184.199 34.7149 188.716 34.7149C192.963 34.7149 194.378 37.7484 194.378 41.6584V62.6237H203.951V40.5798C203.951 32.3554 199.637 26.4906 191.143 26.4906C186.087 26.4906 182.514 28.041 179.413 31.4791C177.39 28.3781 173.952 26.4906 169.166 26.4906C164.11 26.4906 160.605 28.5804 158.987 31.6139V27.2995H150.156V62.6237H159.728V42.3325C159.728 37.6136 162.155 34.7149 166.604 34.7149C170.851 34.7149 172.267 37.7484 172.267 41.6584V62.6237H181.84V42.5347Z" fill="#21004B"/>
<path d="M243.242 47.7255C243.242 47.7255 243.377 46.4447 243.377 44.8942C243.377 34.4452 236.299 26.4906 225.85 26.4906C215.401 26.4906 208.12 34.4452 208.12 44.8942C208.12 55.7476 215.468 63.4326 225.917 63.4326C234.074 63.4326 240.546 58.5115 242.636 51.3658H232.996C231.85 53.9274 229.086 55.2083 226.187 55.2083C221.401 55.2083 218.3 52.5792 217.626 47.7255H243.242ZM225.783 34.1756C230.232 34.1756 233.131 36.8721 233.805 40.8494H217.76C218.569 36.8047 221.401 34.1756 225.783 34.1756Z" fill="#21004B"/>
<path d="M244.789 35.5238H249.036V62.6237H258.608V27.2995H244.789V35.5238ZM253.822 22.7155C257.193 22.7155 259.619 20.356 259.619 16.9854C259.619 13.6148 257.193 11.1879 253.822 11.1879C250.451 11.1879 248.024 13.6148 248.024 16.9854C248.024 20.356 250.451 22.7155 253.822 22.7155Z" fill="#21004B"/>
<path d="M278.43 54.3993C278.16 54.3993 277.756 54.4667 277.149 54.4667C274.992 54.4667 274.722 53.4556 274.722 51.9725V12.0643H265.15V52.6466C265.15 59.6575 267.846 62.7585 275.464 62.7585C276.745 62.7585 277.958 62.6237 278.43 62.5562V54.3993Z" fill="#21004B"/>
<path d="M279.519 35.5238H283.766V62.6237H293.339V27.2995H279.519V35.5238ZM288.553 22.7155C291.923 22.7155 294.35 20.356 294.35 16.9854C294.35 13.6148 291.923 11.1879 288.553 11.1879C285.182 11.1879 282.755 13.6148 282.755 16.9854C282.755 20.356 285.182 22.7155 288.553 22.7155Z" fill="#21004B"/>
<path d="M312.557 62.9939C321.86 62.9939 326.242 58.0728 326.242 52.882C326.242 38.4557 305.007 46.4778 305.007 36.9726C305.007 33.8717 307.636 31.2426 312.962 31.2426C318.422 31.2426 320.984 34.2087 321.388 37.9164H326.175C325.77 33.265 322.602 27.063 313.097 27.063C304.94 27.063 300.356 31.9167 300.356 37.1749C300.356 51.2641 321.591 43.1746 321.591 53.0168C321.591 56.4548 318.355 58.8143 312.557 58.8143C306.625 58.8143 303.659 55.8481 303.322 51.4663H298.468C298.872 57.466 302.648 62.9939 312.557 62.9939Z" fill="#21004B"/>
<path d="M364.256 46.4104C364.256 46.4104 364.324 45.3318 364.324 44.5903C364.324 34.8829 358.054 27.063 347.808 27.063C337.494 27.063 330.955 35.4896 330.955 44.9947C330.955 54.6347 337.022 62.9939 347.875 62.9939C356.032 62.9939 361.695 58.0053 363.717 51.4663H358.728C357.245 55.6459 353.201 58.6795 347.942 58.6795C340.729 58.6795 336.213 53.3539 335.741 46.4104H364.256ZM347.808 31.3774C354.549 31.3774 358.931 35.894 359.537 42.5005H335.876C336.685 36.1637 341.134 31.3774 347.808 31.3774Z" fill="#21004B"/>
<path d="M394.037 45.8711V49.1069C394.037 54.9718 389.79 59.0165 381.633 59.0165C376.578 59.0165 373.814 56.9267 373.814 52.4101C373.814 50.1181 374.892 48.3654 376.578 47.4216C378.33 46.4778 380.69 45.8711 394.037 45.8711ZM381.094 62.9939C387.026 62.9939 391.813 61.1063 394.24 57.1964V62.1849H398.824V39.7366C398.824 32.1189 394.442 27.063 384.532 27.063C375.027 27.063 370.847 31.8493 369.971 37.9838H374.623C375.566 33.1301 379.274 31.1752 384.33 31.1752C390.802 31.1752 394.037 33.8717 394.037 39.6691V41.8938C383.184 41.8938 378.667 42.096 375.297 43.4442C371.387 44.9947 369.095 48.4328 369.095 52.5449C369.095 58.5446 372.937 62.9939 381.094 62.9939Z" fill="#21004B"/>
<path d="M424.991 27.6023C424.991 27.6023 424.182 27.5349 423.845 27.5349C417.508 27.5349 414.138 30.8381 412.857 33.1975V27.872H408.273V62.1849H413.059V42.7027C413.059 35.557 417.441 32.0515 423.306 32.0515C424.182 32.0515 424.991 32.1189 424.991 32.1189V27.6023Z" fill="#21004B"/>
<path d="M425.809 45.0621C425.809 54.4325 432.28 62.9939 442.729 62.9939C452.032 62.9939 457.425 56.7919 458.773 49.9832H453.92C452.504 55.3088 448.594 58.6795 442.729 58.6795C435.516 58.6795 430.662 52.9494 430.662 45.0621C430.662 37.1075 435.516 31.3774 442.729 31.3774C448.594 31.3774 452.504 34.748 453.92 40.0736H458.773C457.425 33.265 452.032 27.063 442.729 27.063C432.28 27.063 425.809 35.6244 425.809 45.0621Z" fill="#21004B"/>
<path d="M470.041 11.6255H465.255V62.1849H470.041V41.8938C470.041 34.8829 474.558 31.2426 480.355 31.2426C486.49 31.2426 489.389 35.0177 489.389 41.2196V62.1849H494.175V40.2759C494.175 32.6582 489.658 27.063 481.164 27.063C474.76 27.063 471.255 30.5685 470.041 32.6582V11.6255Z" fill="#21004B"/>
<path d="M0.824951 73.993L24.0688 14.5224C27.3443 6.14179 35.4223 0.625977 44.4202 0.625977H58.4336L35.1898 60.0966C31.9143 68.4772 23.8363 73.993 14.8383 73.993H0.824951Z" fill="url(#paint0_linear_0_15)"/>
<path d="M34.9246 73.9932L58.1684 14.5226C61.4439 6.14197 69.5219 0.626152 78.5199 0.626152H92.5332L69.2894 60.0968C66.0139 68.4774 57.9359 73.9932 48.9379 73.9932H34.9246Z" fill="url(#paint1_linear_0_15)"/>
<path d="M69.0262 73.9932L92.27 14.5226C95.5455 6.14197 103.623 0.626152 112.621 0.626152H126.635L103.391 60.0968C100.115 68.4774 92.0375 73.9932 83.0395 73.9932H69.0262Z" fill="url(#paint2_linear_0_15)"/>
<defs>
<linearGradient id="paint0_linear_0_15" x1="126.635" y1="-4.97799" x2="0.824952" y2="66.0978" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5CAA"/>
<stop offset="1" stop-color="#FF4E62"/>
</linearGradient>
<linearGradient id="paint1_linear_0_15" x1="126.635" y1="-4.97799" x2="0.824952" y2="66.0978" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5CAA"/>
<stop offset="1" stop-color="#FF4E62"/>
</linearGradient>
<linearGradient id="paint2_linear_0_15" x1="126.635" y1="-4.97799" x2="0.824952" y2="66.0978" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5CAA"/>
<stop offset="1" stop-color="#FF4E62"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
[package] [package]
name = "meilisearch-auth" name = "meilisearch-auth"
version = "0.28.1" version = "0.29.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
enum-iterator = "0.7.0" enum-iterator = "0.7.0"
hmac = "0.12.1" hmac = "0.12.1"
meilisearch-types = { path = "../meilisearch-types" } meilisearch-types = { path = "../meilisearch-types" }
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.31.2" } milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.33.5" }
rand = "0.8.4" rand = "0.8.4"
serde = { version = "1.0.136", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] }
serde_json = { version = "1.0.79", features = ["preserve_order"] } serde_json = { version = "1.0.85", features = ["preserve_order"] }
sha2 = "0.10.2" sha2 = "0.10.2"
thiserror = "1.0.30" thiserror = "1.0.30"
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] } time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] }

View File

@ -6,59 +6,63 @@ use std::hash::Hash;
#[repr(u8)] #[repr(u8)]
pub enum Action { pub enum Action {
#[serde(rename = "*")] #[serde(rename = "*")]
All = actions::ALL, All = 0,
#[serde(rename = "search")] #[serde(rename = "search")]
Search = actions::SEARCH, Search,
#[serde(rename = "documents.*")] #[serde(rename = "documents.*")]
DocumentsAll = actions::DOCUMENTS_ALL, DocumentsAll,
#[serde(rename = "documents.add")] #[serde(rename = "documents.add")]
DocumentsAdd = actions::DOCUMENTS_ADD, DocumentsAdd,
#[serde(rename = "documents.get")] #[serde(rename = "documents.get")]
DocumentsGet = actions::DOCUMENTS_GET, DocumentsGet,
#[serde(rename = "documents.delete")] #[serde(rename = "documents.delete")]
DocumentsDelete = actions::DOCUMENTS_DELETE, DocumentsDelete,
#[serde(rename = "indexes.*")] #[serde(rename = "indexes.*")]
IndexesAll = actions::INDEXES_ALL, IndexesAll,
#[serde(rename = "indexes.create")] #[serde(rename = "indexes.create")]
IndexesAdd = actions::INDEXES_CREATE, IndexesAdd,
#[serde(rename = "indexes.get")] #[serde(rename = "indexes.get")]
IndexesGet = actions::INDEXES_GET, IndexesGet,
#[serde(rename = "indexes.update")] #[serde(rename = "indexes.update")]
IndexesUpdate = actions::INDEXES_UPDATE, IndexesUpdate,
#[serde(rename = "indexes.delete")] #[serde(rename = "indexes.delete")]
IndexesDelete = actions::INDEXES_DELETE, IndexesDelete,
#[serde(rename = "tasks.*")] #[serde(rename = "tasks.*")]
TasksAll = actions::TASKS_ALL, TasksAll,
#[serde(rename = "tasks.get")] #[serde(rename = "tasks.get")]
TasksGet = actions::TASKS_GET, TasksGet,
#[serde(rename = "settings.*")] #[serde(rename = "settings.*")]
SettingsAll = actions::SETTINGS_ALL, SettingsAll,
#[serde(rename = "settings.get")] #[serde(rename = "settings.get")]
SettingsGet = actions::SETTINGS_GET, SettingsGet,
#[serde(rename = "settings.update")] #[serde(rename = "settings.update")]
SettingsUpdate = actions::SETTINGS_UPDATE, SettingsUpdate,
#[serde(rename = "stats.*")] #[serde(rename = "stats.*")]
StatsAll = actions::STATS_ALL, StatsAll,
#[serde(rename = "stats.get")] #[serde(rename = "stats.get")]
StatsGet = actions::STATS_GET, StatsGet,
#[serde(rename = "metrics.*")]
MetricsAll,
#[serde(rename = "metrics.get")]
MetricsGet,
#[serde(rename = "dumps.*")] #[serde(rename = "dumps.*")]
DumpsAll = actions::DUMPS_ALL, DumpsAll,
#[serde(rename = "dumps.create")] #[serde(rename = "dumps.create")]
DumpsCreate = actions::DUMPS_CREATE, DumpsCreate,
#[serde(rename = "version")] #[serde(rename = "version")]
Version = actions::VERSION, Version,
#[serde(rename = "keys.create")] #[serde(rename = "keys.create")]
KeysAdd = actions::KEYS_CREATE, KeysAdd,
#[serde(rename = "keys.get")] #[serde(rename = "keys.get")]
KeysGet = actions::KEYS_GET, KeysGet,
#[serde(rename = "keys.update")] #[serde(rename = "keys.update")]
KeysUpdate = actions::KEYS_UPDATE, KeysUpdate,
#[serde(rename = "keys.delete")] #[serde(rename = "keys.delete")]
KeysDelete = actions::KEYS_DELETE, KeysDelete,
} }
impl Action { impl Action {
pub fn from_repr(repr: u8) -> Option<Self> { pub const fn from_repr(repr: u8) -> Option<Self> {
use actions::*; use actions::*;
match repr { match repr {
ALL => Some(Self::All), ALL => Some(Self::All),
@ -79,6 +83,8 @@ impl Action {
SETTINGS_UPDATE => Some(Self::SettingsUpdate), SETTINGS_UPDATE => Some(Self::SettingsUpdate),
STATS_ALL => Some(Self::StatsAll), STATS_ALL => Some(Self::StatsAll),
STATS_GET => Some(Self::StatsGet), STATS_GET => Some(Self::StatsGet),
METRICS_ALL => Some(Self::MetricsAll),
METRICS_GET => Some(Self::MetricsGet),
DUMPS_ALL => Some(Self::DumpsAll), DUMPS_ALL => Some(Self::DumpsAll),
DUMPS_CREATE => Some(Self::DumpsCreate), DUMPS_CREATE => Some(Self::DumpsCreate),
VERSION => Some(Self::Version), VERSION => Some(Self::Version),
@ -90,62 +96,39 @@ impl Action {
} }
} }
pub fn repr(&self) -> u8 { pub const fn repr(&self) -> u8 {
use actions::*; *self as u8
match self {
Self::All => ALL,
Self::Search => SEARCH,
Self::DocumentsAll => DOCUMENTS_ALL,
Self::DocumentsAdd => DOCUMENTS_ADD,
Self::DocumentsGet => DOCUMENTS_GET,
Self::DocumentsDelete => DOCUMENTS_DELETE,
Self::IndexesAll => INDEXES_ALL,
Self::IndexesAdd => INDEXES_CREATE,
Self::IndexesGet => INDEXES_GET,
Self::IndexesUpdate => INDEXES_UPDATE,
Self::IndexesDelete => INDEXES_DELETE,
Self::TasksAll => TASKS_ALL,
Self::TasksGet => TASKS_GET,
Self::SettingsAll => SETTINGS_ALL,
Self::SettingsGet => SETTINGS_GET,
Self::SettingsUpdate => SETTINGS_UPDATE,
Self::StatsAll => STATS_ALL,
Self::StatsGet => STATS_GET,
Self::DumpsAll => DUMPS_ALL,
Self::DumpsCreate => DUMPS_CREATE,
Self::Version => VERSION,
Self::KeysAdd => KEYS_CREATE,
Self::KeysGet => KEYS_GET,
Self::KeysUpdate => KEYS_UPDATE,
Self::KeysDelete => KEYS_DELETE,
}
} }
} }
pub mod actions { pub mod actions {
pub(crate) const ALL: u8 = 0; use super::Action::*;
pub const SEARCH: u8 = 1;
pub const DOCUMENTS_ALL: u8 = 2; pub(crate) const ALL: u8 = All.repr();
pub const DOCUMENTS_ADD: u8 = 3; pub const SEARCH: u8 = Search.repr();
pub const DOCUMENTS_GET: u8 = 4; pub const DOCUMENTS_ALL: u8 = DocumentsAll.repr();
pub const DOCUMENTS_DELETE: u8 = 5; pub const DOCUMENTS_ADD: u8 = DocumentsAdd.repr();
pub const INDEXES_ALL: u8 = 6; pub const DOCUMENTS_GET: u8 = DocumentsGet.repr();
pub const INDEXES_CREATE: u8 = 7; pub const DOCUMENTS_DELETE: u8 = DocumentsDelete.repr();
pub const INDEXES_GET: u8 = 8; pub const INDEXES_ALL: u8 = IndexesAll.repr();
pub const INDEXES_UPDATE: u8 = 9; pub const INDEXES_CREATE: u8 = IndexesAdd.repr();
pub const INDEXES_DELETE: u8 = 10; pub const INDEXES_GET: u8 = IndexesGet.repr();
pub const TASKS_ALL: u8 = 11; pub const INDEXES_UPDATE: u8 = IndexesUpdate.repr();
pub const TASKS_GET: u8 = 12; pub const INDEXES_DELETE: u8 = IndexesDelete.repr();
pub const SETTINGS_ALL: u8 = 13; pub const TASKS_ALL: u8 = TasksAll.repr();
pub const SETTINGS_GET: u8 = 14; pub const TASKS_GET: u8 = TasksGet.repr();
pub const SETTINGS_UPDATE: u8 = 15; pub const SETTINGS_ALL: u8 = SettingsAll.repr();
pub const STATS_ALL: u8 = 16; pub const SETTINGS_GET: u8 = SettingsGet.repr();
pub const STATS_GET: u8 = 17; pub const SETTINGS_UPDATE: u8 = SettingsUpdate.repr();
pub const DUMPS_ALL: u8 = 18; pub const STATS_ALL: u8 = StatsAll.repr();
pub const DUMPS_CREATE: u8 = 19; pub const STATS_GET: u8 = StatsGet.repr();
pub const VERSION: u8 = 20; pub const METRICS_ALL: u8 = MetricsAll.repr();
pub const KEYS_CREATE: u8 = 21; pub const METRICS_GET: u8 = MetricsGet.repr();
pub const KEYS_GET: u8 = 22; pub const DUMPS_ALL: u8 = DumpsAll.repr();
pub const KEYS_UPDATE: u8 = 23; pub const DUMPS_CREATE: u8 = DumpsCreate.repr();
pub const KEYS_DELETE: u8 = 24; pub const VERSION: u8 = Version.repr();
pub const KEYS_CREATE: u8 = KeysAdd.repr();
pub const KEYS_GET: u8 = KeysGet.repr();
pub const KEYS_UPDATE: u8 = KeysUpdate.repr();
pub const KEYS_DELETE: u8 = KeysDelete.repr();
} }

View File

@ -110,10 +110,7 @@ impl AuthController {
filters.search_rules = search_rules; filters.search_rules = search_rules;
} }
filters.allow_index_creation = key filters.allow_index_creation = self.is_key_authorized(uid, Action::IndexesAdd, None)?;
.actions
.iter()
.any(|&action| action == Action::IndexesAdd || action == Action::All);
Ok(filters) Ok(filters)
} }

View File

@ -126,6 +126,9 @@ impl HeedAuthStore {
Action::StatsAll => { Action::StatsAll => {
actions.insert(Action::StatsGet); actions.insert(Action::StatsGet);
} }
Action::MetricsAll => {
actions.insert(Action::MetricsGet);
}
other => { other => {
actions.insert(*other); actions.insert(*other);
} }

View File

@ -4,14 +4,14 @@ description = "Meilisearch HTTP server"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "meilisearch-http" name = "meilisearch-http"
version = "0.28.1" version = "0.29.2"
[[bin]] [[bin]]
name = "meilisearch" name = "meilisearch"
path = "src/main.rs" path = "src/main.rs"
[build-dependencies] [build-dependencies]
anyhow = { version = "1.0.56", optional = true } anyhow = { version = "1.0.62", optional = true }
cargo_toml = { version = "0.11.4", optional = true } cargo_toml = { version = "0.11.4", optional = true }
hex = { version = "0.4.3", optional = true } hex = { version = "0.4.3", optional = true }
reqwest = { version = "0.11.9", features = ["blocking", "rustls-tls"], default-features = false, optional = true } reqwest = { version = "0.11.9", features = ["blocking", "rustls-tls"], default-features = false, optional = true }
@ -25,7 +25,7 @@ zip = { version = "0.5.13", optional = true }
actix-cors = "0.6.1" actix-cors = "0.6.1"
actix-web = { version = "4.0.1", default-features = false, features = ["macros", "compress-brotli", "compress-gzip", "cookies", "rustls"] } actix-web = { version = "4.0.1", default-features = false, features = ["macros", "compress-brotli", "compress-gzip", "cookies", "rustls"] }
actix-web-static-files = { git = "https://github.com/kilork/actix-web-static-files.git", rev = "2d3b6160", optional = true } actix-web-static-files = { git = "https://github.com/kilork/actix-web-static-files.git", rev = "2d3b6160", optional = true }
anyhow = { version = "1.0.56", features = ["backtrace"] } anyhow = { version = "1.0.62", features = ["backtrace"] }
async-stream = "0.3.3" async-stream = "0.3.3"
async-trait = "0.1.52" async-trait = "0.1.52"
bstr = "0.2.17" bstr = "0.2.17"
@ -47,6 +47,7 @@ log = "0.4.14"
meilisearch-auth = { path = "../meilisearch-auth" } meilisearch-auth = { path = "../meilisearch-auth" }
meilisearch-types = { path = "../meilisearch-types" } meilisearch-types = { path = "../meilisearch-types" }
meilisearch-lib = { path = "../meilisearch-lib" } meilisearch-lib = { path = "../meilisearch-lib" }
mimalloc = { version = "0.1.29", default-features = false }
mime = "0.3.16" mime = "0.3.16"
num_cpus = "1.13.1" num_cpus = "1.13.1"
obkv = "0.2.0" obkv = "0.2.0"
@ -63,7 +64,7 @@ rustls-pemfile = "0.3.0"
segment = { version = "0.2.0", optional = true } segment = { version = "0.2.0", optional = true }
serde = { version = "1.0.136", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] }
serde-cs = "0.2.3" serde-cs = "0.2.3"
serde_json = { version = "1.0.79", features = ["preserve_order"] } serde_json = { version = "1.0.85", features = ["preserve_order"] }
sha2 = "0.10.2" sha2 = "0.10.2"
siphasher = "0.3.10" siphasher = "0.3.10"
slice-group-by = "0.3.0" slice-group-by = "0.3.0"
@ -77,6 +78,8 @@ tokio = { version = "1.17.0", features = ["full"] }
tokio-stream = "0.1.8" tokio-stream = "0.1.8"
uuid = { version = "1.1.2", features = ["serde", "v4"] } uuid = { version = "1.1.2", features = ["serde", "v4"] }
walkdir = "2.3.2" walkdir = "2.3.2"
prometheus = { version = "0.13.0", features = ["process"], optional = true }
lazy_static = "1.4.0"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7.0" actix-rt = "2.7.0"
@ -88,6 +91,7 @@ yaup = "0.2.0"
[features] [features]
default = ["analytics", "mini-dashboard"] default = ["analytics", "mini-dashboard"]
metrics = ["prometheus"]
analytics = ["segment"] analytics = ["segment"]
mini-dashboard = [ mini-dashboard = [
"actix-web-static-files", "actix-web-static-files",
@ -101,9 +105,6 @@ mini-dashboard = [
"zip", "zip",
] ]
[target.'cfg(target_os = "linux")'.dependencies]
tikv-jemallocator = "0.4.3"
[package.metadata.mini-dashboard] [package.metadata.mini-dashboard]
assets-url = "https://github.com/meilisearch/mini-dashboard/releases/download/v0.2.1/build.zip" assets-url = "https://github.com/meilisearch/mini-dashboard/releases/download/v0.2.2/build.zip"
sha1 = "05a02ff13c3982091884a3f81d28bf53e72607b2" sha1 = "c69feffc6b590e38a46981a85c47f48905d4082a"

View File

@ -366,6 +366,9 @@ pub struct SearchAggregator {
// The maximum number of terms in a q request // The maximum number of terms in a q request
max_terms_number: usize, max_terms_number: usize,
// everytime a search is done, we increment the counter linked to the used settings
matching_strategy: HashMap<String, usize>,
// pagination // pagination
max_limit: usize, max_limit: usize,
max_offset: usize, max_offset: usize,
@ -423,6 +426,9 @@ impl SearchAggregator {
ret.max_terms_number = q.split_whitespace().count(); ret.max_terms_number = q.split_whitespace().count();
} }
ret.matching_strategy
.insert(format!("{:?}", query.matching_strategy), 1);
ret.max_limit = query.limit; ret.max_limit = query.limit;
ret.max_offset = query.offset.unwrap_or_default(); ret.max_offset = query.offset.unwrap_or_default();
@ -476,6 +482,11 @@ impl SearchAggregator {
} }
// q // q
self.max_terms_number = self.max_terms_number.max(other.max_terms_number); self.max_terms_number = self.max_terms_number.max(other.max_terms_number);
for (key, value) in other.matching_strategy.into_iter() {
let matching_strategy = self.matching_strategy.entry(key).or_insert(0);
*matching_strategy = matching_strategy.saturating_add(value);
}
// pagination // pagination
self.max_limit = self.max_limit.max(other.max_limit); self.max_limit = self.max_limit.max(other.max_limit);
self.max_offset = self.max_offset.max(other.max_offset); self.max_offset = self.max_offset.max(other.max_offset);
@ -517,6 +528,7 @@ impl SearchAggregator {
}, },
"q": { "q": {
"max_terms_number": self.max_terms_number, "max_terms_number": self.max_terms_number,
"most_used_matching_strategy": self.matching_strategy.iter().max_by_key(|(_, v)| *v).map(|(k, _)| json!(k)).unwrap_or_else(|| json!(null)),
}, },
"pagination": { "pagination": {
"max_limit": self.max_limit, "max_limit": self.max_limit,

View File

@ -5,7 +5,7 @@ use std::ops::Deref;
use std::pin::Pin; use std::pin::Pin;
use actix_web::FromRequest; use actix_web::FromRequest;
use error::AuthenticationError; pub use error::AuthenticationError;
use futures::future::err; use futures::future::err;
use futures::Future; use futures::Future;
use meilisearch_auth::{AuthController, AuthFilter}; use meilisearch_auth::{AuthController, AuthFilter};

View File

@ -1,17 +0,0 @@
use meilisearch_lib::heed::Env;
use walkdir::WalkDir;
pub trait EnvSizer {
fn size(&self) -> u64;
}
impl EnvSizer for Env {
fn size(&self) -> u64 {
WalkDir::new(self.path())
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len())
}
}

View File

@ -1,3 +0,0 @@
mod env;
pub use env::EnvSizer;

View File

@ -5,10 +5,14 @@ pub mod analytics;
pub mod task; pub mod task;
#[macro_use] #[macro_use]
pub mod extractors; pub mod extractors;
pub mod helpers;
pub mod option; pub mod option;
pub mod routes; pub mod routes;
#[cfg(feature = "metrics")]
pub mod metrics;
#[cfg(feature = "metrics")]
pub mod route_metrics;
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{atomic::AtomicBool, Arc};
use std::time::Duration; use std::time::Duration;
@ -30,9 +34,9 @@ pub static AUTOBATCHING_ENABLED: AtomicBool = AtomicBool::new(false);
pub fn setup_meilisearch(opt: &Opt) -> anyhow::Result<MeiliSearch> { pub fn setup_meilisearch(opt: &Opt) -> anyhow::Result<MeiliSearch> {
let mut meilisearch = MeiliSearch::builder(); let mut meilisearch = MeiliSearch::builder();
// enable autobatching? // disable autobatching?
AUTOBATCHING_ENABLED.store( AUTOBATCHING_ENABLED.store(
opt.scheduler_options.enable_auto_batching, !opt.scheduler_options.disable_auto_batching,
std::sync::atomic::Ordering::Relaxed, std::sync::atomic::Ordering::Relaxed,
); );
@ -141,22 +145,40 @@ pub fn dashboard(config: &mut web::ServiceConfig, _enable_frontend: bool) {
config.service(web::resource("/").route(web::get().to(routes::running))); config.service(web::resource("/").route(web::get().to(routes::running)));
} }
#[cfg(feature = "metrics")]
pub fn configure_metrics_route(config: &mut web::ServiceConfig, enable_metrics_route: bool) {
if enable_metrics_route {
config.service(
web::resource("/metrics").route(web::get().to(crate::route_metrics::get_metrics)),
);
}
}
#[macro_export] #[macro_export]
macro_rules! create_app { macro_rules! create_app {
($data:expr, $auth:expr, $enable_frontend:expr, $opt:expr, $analytics:expr) => {{ ($data:expr, $auth:expr, $enable_frontend:expr, $opt:expr, $analytics:expr) => {{
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::dev::Service;
use actix_web::middleware::Condition;
use actix_web::middleware::TrailingSlash; use actix_web::middleware::TrailingSlash;
use actix_web::App; use actix_web::App;
use actix_web::{middleware, web}; use actix_web::{middleware, web};
use meilisearch_http::error::MeilisearchHttpError; use meilisearch_http::error::MeilisearchHttpError;
use meilisearch_http::routes; use meilisearch_http::routes;
use meilisearch_http::{configure_data, dashboard}; use meilisearch_http::{configure_data, dashboard};
#[cfg(feature = "metrics")]
use meilisearch_http::{configure_metrics_route, metrics, route_metrics};
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
App::new() let app = App::new()
.configure(|s| configure_data(s, $data.clone(), $auth.clone(), &$opt, $analytics)) .configure(|s| configure_data(s, $data.clone(), $auth.clone(), &$opt, $analytics))
.configure(routes::configure) .configure(routes::configure)
.configure(|s| dashboard(s, $enable_frontend)) .configure(|s| dashboard(s, $enable_frontend));
#[cfg(feature = "metrics")]
let app = app.configure(|s| configure_metrics_route(s, $opt.enable_metrics_route));
let app = app
.wrap( .wrap(
Cors::default() Cors::default()
.send_wildcard() .send_wildcard()
@ -169,6 +191,14 @@ macro_rules! create_app {
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::NormalizePath::new( .wrap(middleware::NormalizePath::new(
middleware::TrailingSlash::Trim, middleware::TrailingSlash::Trim,
)) ));
#[cfg(feature = "metrics")]
let app = app.wrap(Condition::new(
$opt.enable_metrics_route,
route_metrics::RouteMetrics,
));
app
}}; }};
} }

View File

@ -10,9 +10,8 @@ use meilisearch_http::analytics::Analytics;
use meilisearch_http::{create_app, setup_meilisearch, Opt}; use meilisearch_http::{create_app, setup_meilisearch, Opt};
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
#[cfg(target_os = "linux")]
#[global_allocator] #[global_allocator]
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
/// does all the setup before meilisearch is launched /// does all the setup before meilisearch is launched
fn setup(opt: &Opt) -> anyhow::Result<()> { fn setup(opt: &Opt) -> anyhow::Result<()> {
@ -101,7 +100,11 @@ async fn run_http(
pub fn print_launch_resume(opt: &Opt, user: &str) { pub fn print_launch_resume(opt: &Opt, user: &str) {
let commit_sha = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"); let commit_sha = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown");
let commit_date = option_env!("VERGEN_GIT_COMMIT_TIMESTAMP").unwrap_or("unknown"); let commit_date = option_env!("VERGEN_GIT_COMMIT_TIMESTAMP").unwrap_or("unknown");
let protocol = if opt.ssl_cert_path.is_some() && opt.ssl_key_path.is_some() {
"https"
} else {
"http"
};
let ascii_name = r#" let ascii_name = r#"
888b d888 d8b 888 d8b 888 888b d888 d8b 888 d8b 888
8888b d8888 Y8P 888 Y8P 888 8888b d8888 Y8P 888 Y8P 888
@ -116,7 +119,7 @@ pub fn print_launch_resume(opt: &Opt, user: &str) {
eprintln!("{}", ascii_name); eprintln!("{}", ascii_name);
eprintln!("Database path:\t\t{:?}", opt.db_path); eprintln!("Database path:\t\t{:?}", opt.db_path);
eprintln!("Server listening on:\t\"http://{}\"", opt.http_addr); eprintln!("Server listening on:\t\"{}://{}\"", protocol, opt.http_addr);
eprintln!("Environment:\t\t{:?}", opt.env); eprintln!("Environment:\t\t{:?}", opt.env);
eprintln!("Commit SHA:\t\t{:?}", commit_sha.to_string()); eprintln!("Commit SHA:\t\t{:?}", commit_sha.to_string());
eprintln!("Commit date:\t\t{:?}", commit_date.to_string()); eprintln!("Commit date:\t\t{:?}", commit_date.to_string());

View File

@ -0,0 +1,42 @@
use lazy_static::lazy_static;
use prometheus::{
opts, register_histogram_vec, register_int_counter_vec, register_int_gauge,
register_int_gauge_vec,
};
use prometheus::{HistogramVec, IntCounterVec, IntGauge, IntGaugeVec};
const HTTP_RESPONSE_TIME_CUSTOM_BUCKETS: &[f64; 14] = &[
0.0005, 0.0008, 0.00085, 0.0009, 0.00095, 0.001, 0.00105, 0.0011, 0.00115, 0.0012, 0.0015,
0.002, 0.003, 1.0,
];
lazy_static! {
pub static ref HTTP_REQUESTS_TOTAL: IntCounterVec = register_int_counter_vec!(
opts!("http_requests_total", "HTTP requests total"),
&["method", "path"]
)
.expect("Can't create a metric");
pub static ref MEILISEARCH_DB_SIZE_BYTES: IntGauge = register_int_gauge!(opts!(
"meilisearch_db_size_bytes",
"Meilisearch Db Size In Bytes"
))
.expect("Can't create a metric");
pub static ref MEILISEARCH_INDEX_COUNT: IntGauge =
register_int_gauge!(opts!("meilisearch_index_count", "Meilisearch Index Count"))
.expect("Can't create a metric");
pub static ref MEILISEARCH_INDEX_DOCS_COUNT: IntGaugeVec = register_int_gauge_vec!(
opts!(
"meilisearch_index_docs_count",
"Meilisearch Index Docs Count"
),
&["index"]
)
.expect("Can't create a metric");
pub static ref HTTP_RESPONSE_TIME_SECONDS: HistogramVec = register_histogram_vec!(
"http_response_time_seconds",
"HTTP response times",
&["method", "path"],
HTTP_RESPONSE_TIME_CUSTOM_BUCKETS.to_vec()
)
.expect("Can't create a metric");
}

View File

@ -146,6 +146,11 @@ pub struct Opt {
#[clap(long, env = "MEILI_LOG_LEVEL", default_value = "info")] #[clap(long, env = "MEILI_LOG_LEVEL", default_value = "info")]
pub log_level: String, pub log_level: String,
/// Enables Prometheus metrics and /metrics route.
#[cfg(feature = "metrics")]
#[clap(long, env = "MEILI_ENABLE_METRICS_ROUTE")]
pub enable_metrics_route: bool,
#[serde(flatten)] #[serde(flatten)]
#[clap(flatten)] #[clap(flatten)]
pub indexer_options: IndexerOpts, pub indexer_options: IndexerOpts,

View File

@ -0,0 +1,112 @@
use std::future::{ready, Ready};
use actix_web::http::header;
use actix_web::HttpResponse;
use actix_web::{
dev::{self, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::LocalBoxFuture;
use meilisearch_auth::actions;
use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use prometheus::HistogramTimer;
use prometheus::{Encoder, TextEncoder};
use crate::extractors::authentication::policies::ActionPolicy;
use crate::extractors::authentication::GuardedData;
pub async fn get_metrics(
meilisearch: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, MeiliSearch>,
) -> Result<HttpResponse, ResponseError> {
let search_rules = &meilisearch.filters().search_rules;
let response = meilisearch.get_all_stats(search_rules).await?;
crate::metrics::MEILISEARCH_DB_SIZE_BYTES.set(response.database_size as i64);
crate::metrics::MEILISEARCH_INDEX_COUNT.set(response.indexes.len() as i64);
for (index, value) in response.indexes.iter() {
crate::metrics::MEILISEARCH_INDEX_DOCS_COUNT
.with_label_values(&[index])
.set(value.number_of_documents as i64);
}
let encoder = TextEncoder::new();
let mut buffer = vec![];
encoder
.encode(&prometheus::gather(), &mut buffer)
.expect("Failed to encode metrics");
let response = String::from_utf8(buffer).expect("Failed to convert bytes to string");
Ok(HttpResponse::Ok()
.insert_header(header::ContentType(mime::TEXT_PLAIN))
.body(response))
}
pub struct RouteMetrics;
// Middleware factory is `Transform` trait from actix-service crate
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for RouteMetrics
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = RouteMetricsMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(RouteMetricsMiddleware { service }))
}
}
pub struct RouteMetricsMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for RouteMetricsMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
dev::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let mut histogram_timer: Option<HistogramTimer> = None;
let request_path = req.path();
let is_registered_resource = req.resource_map().has_resource(request_path);
if is_registered_resource {
let request_method = req.method().to_string();
histogram_timer = Some(
crate::metrics::HTTP_RESPONSE_TIME_SECONDS
.with_label_values(&[&request_method, request_path])
.start_timer(),
);
crate::metrics::HTTP_REQUESTS_TOTAL
.with_label_values(&[&request_method, request_path])
.inc();
}
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
if let Some(histogram_timer) = histogram_timer {
histogram_timer.observe_duration();
};
Ok(res)
})
}
}

View File

@ -8,7 +8,7 @@ use serde_json::json;
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::analytics::Analytics; use crate::analytics::Analytics;
use crate::extractors::authentication::{policies::*, GuardedData}; use crate::extractors::authentication::{policies::*, AuthenticationError, GuardedData};
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::task::SummarizedTaskView; use crate::task::SummarizedTaskView;
@ -74,16 +74,21 @@ pub async fn create_index(
primary_key, uid, .. primary_key, uid, ..
} = body.into_inner(); } = body.into_inner();
analytics.publish( let allow_index_creation = meilisearch.filters().search_rules.is_index_authorized(&uid);
"Index Created".to_string(), if allow_index_creation {
json!({ "primary_key": primary_key }), analytics.publish(
Some(&req), "Index Created".to_string(),
); json!({ "primary_key": primary_key }),
Some(&req),
);
let update = Update::CreateIndex { primary_key }; let update = Update::CreateIndex { primary_key };
let task: SummarizedTaskView = meilisearch.register_update(uid, update).await?.into(); let task: SummarizedTaskView = meilisearch.register_update(uid, update).await?.into();
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} else {
Err(AuthenticationError::InvalidToken.into())
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -158,7 +163,14 @@ pub async fn delete_index(
pub async fn get_index_stats( pub async fn get_index_stats(
meilisearch: GuardedData<ActionPolicy<{ actions::STATS_GET }>, MeiliSearch>, meilisearch: GuardedData<ActionPolicy<{ actions::STATS_GET }>, MeiliSearch>,
path: web::Path<String>, path: web::Path<String>,
req: HttpRequest,
analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
analytics.publish(
"Stats Seen".to_string(),
json!({ "per_index_uid": true }),
Some(&req),
);
let response = meilisearch.get_index_stats(path.into_inner()).await?; let response = meilisearch.get_index_stats(path.into_inner()).await?;
debug!("returns: {:?}", response); debug!("returns: {:?}", response);

View File

@ -2,8 +2,8 @@ use actix_web::{web, HttpRequest, HttpResponse};
use log::debug; use log::debug;
use meilisearch_auth::IndexSearchRules; use meilisearch_auth::IndexSearchRules;
use meilisearch_lib::index::{ use meilisearch_lib::index::{
SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER,
DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
}; };
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
@ -45,6 +45,8 @@ pub struct SearchQueryGet {
highlight_post_tag: String, highlight_post_tag: String,
#[serde(default = "DEFAULT_CROP_MARKER")] #[serde(default = "DEFAULT_CROP_MARKER")]
crop_marker: String, crop_marker: String,
#[serde(default)]
matching_strategy: MatchingStrategy,
} }
impl From<SearchQueryGet> for SearchQuery { impl From<SearchQueryGet> for SearchQuery {
@ -76,6 +78,7 @@ impl From<SearchQueryGet> for SearchQuery {
highlight_pre_tag: other.highlight_pre_tag, highlight_pre_tag: other.highlight_pre_tag,
highlight_post_tag: other.highlight_post_tag, highlight_post_tag: other.highlight_post_tag,
crop_marker: other.crop_marker, crop_marker: other.crop_marker,
matching_strategy: other.matching_strategy,
} }
} }
} }

View File

@ -1,7 +1,8 @@
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json;
use time::OffsetDateTime; use time::OffsetDateTime;
use meilisearch_lib::index::{Settings, Unchecked}; use meilisearch_lib::index::{Settings, Unchecked};
@ -9,6 +10,7 @@ use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::star_or::StarOr; use meilisearch_types::star_or::StarOr;
use crate::analytics::Analytics;
use crate::extractors::authentication::{policies::*, GuardedData}; use crate::extractors::authentication::{policies::*, GuardedData};
mod api_key; mod api_key;
@ -231,7 +233,14 @@ pub async fn running() -> HttpResponse {
async fn get_stats( async fn get_stats(
meilisearch: GuardedData<ActionPolicy<{ actions::STATS_GET }>, MeiliSearch>, meilisearch: GuardedData<ActionPolicy<{ actions::STATS_GET }>, MeiliSearch>,
req: HttpRequest,
analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
analytics.publish(
"Stats Seen".to_string(),
json!({ "per_index_uid": false }),
Some(&req),
);
let search_rules = &meilisearch.filters().search_rules; let search_rules = &meilisearch.filters().search_rules;
let response = meilisearch.get_all_stats(search_rules).await?; let response = meilisearch.get_all_stats(search_rules).await?;

View File

@ -119,7 +119,7 @@ async fn get_tasks(
// Then we complete the task filter with other potential status and types filters. // Then we complete the task filter with other potential status and types filters.
let filters = if type_.is_some() || status.is_some() { let filters = if type_.is_some() || status.is_some() {
let mut filters = indexes_filters.unwrap_or_default(); let mut filters = indexes_filters.unwrap_or_default();
filters.filter_fn(move |task| { filters.filter_fn(Box::new(move |task| {
let matches_type = match &type_ { let matches_type = match &type_ {
Some(types) => types Some(types) => types
.iter() .iter()
@ -135,7 +135,7 @@ async fn get_tasks(
}; };
matches_type && matches_status matches_type && matches_status
}); }));
Some(filters) Some(filters)
} else { } else {
indexes_filters indexes_filters

View File

@ -4,7 +4,6 @@ use std::str::FromStr;
use std::write; use std::write;
use meilisearch_lib::index::{Settings, Unchecked}; use meilisearch_lib::index::{Settings, Unchecked};
use meilisearch_lib::tasks::batch::BatchId;
use meilisearch_lib::tasks::task::{ use meilisearch_lib::tasks::task::{
DocumentDeletion, Task, TaskContent, TaskEvent, TaskId, TaskResult, DocumentDeletion, Task, TaskContent, TaskEvent, TaskId, TaskResult,
}; };
@ -12,8 +11,6 @@ use meilisearch_types::error::ResponseError;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use crate::AUTOBATCHING_ENABLED;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum TaskType { pub enum TaskType {
@ -230,8 +227,6 @@ pub struct TaskView {
started_at: Option<OffsetDateTime>, started_at: Option<OffsetDateTime>,
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")] #[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
finished_at: Option<OffsetDateTime>, finished_at: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
batch_uid: Option<Option<BatchId>>,
} }
impl From<Task> for TaskView { impl From<Task> for TaskView {
@ -380,16 +375,6 @@ impl From<Task> for TaskView {
let duration = finished_at.zip(started_at).map(|(tf, ts)| (tf - ts)); let duration = finished_at.zip(started_at).map(|(tf, ts)| (tf - ts));
let batch_uid = if AUTOBATCHING_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
let id = events.iter().find_map(|e| match e {
TaskEvent::Batched { batch_id, .. } => Some(*batch_id),
_ => None,
});
Some(id)
} else {
None
};
Self { Self {
uid: id, uid: id,
index_uid, index_uid,
@ -401,7 +386,6 @@ impl From<Task> for TaskView {
enqueued_at, enqueued_at,
started_at, started_at,
finished_at, finished_at,
batch_uid,
} }
} }
} }

View File

@ -8,7 +8,7 @@ use time::{Duration, OffsetDateTime};
pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'static str>>> = pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'static str>>> =
Lazy::new(|| { Lazy::new(|| {
hashmap! { let mut authorizations = hashmap! {
("POST", "/indexes/products/search") => hashset!{"search", "*"}, ("POST", "/indexes/products/search") => hashset!{"search", "*"},
("GET", "/indexes/products/search") => hashset!{"search", "*"}, ("GET", "/indexes/products/search") => hashset!{"search", "*"},
("POST", "/indexes/products/documents") => hashset!{"documents.add", "documents.*", "*"}, ("POST", "/indexes/products/documents") => hashset!{"documents.add", "documents.*", "*"},
@ -52,7 +52,16 @@ pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'
("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"}, ("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"},
("POST", "/keys") => hashset!{"keys.create", "*"}, ("POST", "/keys") => hashset!{"keys.create", "*"},
("GET", "/keys") => hashset!{"keys.get", "*"}, ("GET", "/keys") => hashset!{"keys.get", "*"},
};
if cfg!(feature = "metrics") {
authorizations.insert(
("GET", "/metrics"),
hashset! {"metrics.get", "metrics.*", "*"},
);
} }
authorizations
}); });
pub static ALL_ACTIONS: Lazy<HashSet<&'static str>> = Lazy::new(|| { pub static ALL_ACTIONS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
@ -90,7 +99,7 @@ async fn error_access_expired_key() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
// wait until the key is expired. // wait until the key is expired.
thread::sleep(time::Duration::new(1, 0)); thread::sleep(time::Duration::new(1, 0));
@ -126,7 +135,7 @@ async fn error_access_unauthorized_index() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
for (method, route) in AUTHORIZATIONS for (method, route) in AUTHORIZATIONS
.keys() .keys()
@ -166,7 +175,7 @@ async fn error_access_unauthorized_action() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.dummy_request(method, route).await; let (response, code) = server.dummy_request(method, route).await;
assert_eq!( assert_eq!(
@ -221,7 +230,7 @@ async fn access_authorized_restricted_index() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.dummy_request(method, route).await; let (response, code) = server.dummy_request(method, route).await;
@ -259,7 +268,7 @@ async fn access_authorized_no_index_restriction() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.dummy_request(method, route).await; let (response, code) = server.dummy_request(method, route).await;
@ -304,7 +313,7 @@ async fn access_authorized_stats_restricted_index() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.stats().await; let (response, code) = server.stats().await;
assert_eq!(200, code, "{:?}", &response); assert_eq!(200, code, "{:?}", &response);
@ -344,7 +353,7 @@ async fn access_authorized_stats_no_index_restriction() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.stats().await; let (response, code) = server.stats().await;
assert_eq!(200, code, "{:?}", &response); assert_eq!(200, code, "{:?}", &response);
@ -384,7 +393,7 @@ async fn list_authorized_indexes_restricted_index() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.list_indexes(None, None).await; let (response, code) = server.list_indexes(None, None).await;
assert_eq!(200, code, "{:?}", &response); assert_eq!(200, code, "{:?}", &response);
@ -425,7 +434,7 @@ async fn list_authorized_indexes_no_index_restriction() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.list_indexes(None, None).await; let (response, code) = server.list_indexes(None, None).await;
assert_eq!(200, code, "{:?}", &response); assert_eq!(200, code, "{:?}", &response);
@ -465,7 +474,7 @@ async fn list_authorized_tasks_restricted_index() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.service.get("/tasks").await; let (response, code) = server.service.get("/tasks").await;
assert_eq!(200, code, "{:?}", &response); assert_eq!(200, code, "{:?}", &response);
@ -505,7 +514,7 @@ async fn list_authorized_tasks_no_index_restriction() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let (response, code) = server.service.get("/tasks").await; let (response, code) = server.service.get("/tasks").await;
assert_eq!(200, code, "{:?}", &response); assert_eq!(200, code, "{:?}", &response);
@ -536,7 +545,7 @@ async fn error_creating_index_without_action() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
let expected_error = json!({ let expected_error = json!({
"message": "Index `test` not found.", "message": "Index `test` not found.",
@ -588,11 +597,91 @@ async fn error_creating_index_without_action() {
#[actix_rt::test] #[actix_rt::test]
async fn lazy_create_index() { async fn lazy_create_index() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;
// create key with access on all indexes.
let contents = vec![
json!({
"indexes": ["*"],
"actions": ["*"],
"expiresAt": "2050-11-13T00:00:00Z"
}),
json!({
"indexes": ["*"],
"actions": ["indexes.*", "documents.*", "settings.*", "tasks.*"],
"expiresAt": "2050-11-13T00:00:00Z"
}),
json!({
"indexes": ["*"],
"actions": ["indexes.create", "documents.add", "settings.update", "tasks.get"],
"expiresAt": "2050-11-13T00:00:00Z"
}),
];
for content in contents {
server.use_api_key("MASTER_KEY");
let (response, code) = server.add_api_key(content).await;
assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string());
// use created key.
let key = response["key"].as_str().unwrap();
server.use_api_key(key);
// try to create a index via add documents route
let index = server.index("test");
let documents = json!([
{
"id": 1,
"content": "foo",
}
]);
let (response, code) = index.add_documents(documents, None).await;
assert_eq!(202, code, "{:?}", &response);
let task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(200, code, "{:?}", &response);
assert_eq!(response["status"], "succeeded");
// try to create a index via add settings route
let index = server.index("test1");
let settings = json!({ "distinctAttribute": "test"});
let (response, code) = index.update_settings(settings).await;
assert_eq!(202, code, "{:?}", &response);
let task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(200, code, "{:?}", &response);
assert_eq!(response["status"], "succeeded");
// try to create a index via add specialized settings route
let index = server.index("test2");
let (response, code) = index.update_distinct_attribute(json!("test")).await;
assert_eq!(202, code, "{:?}", &response);
let task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(200, code, "{:?}", &response);
assert_eq!(response["status"], "succeeded");
}
}
#[actix_rt::test]
async fn error_creating_index_without_index() {
let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY"); server.use_api_key("MASTER_KEY");
// create key with access on all indexes. // create key with access on all indexes.
let content = json!({ let content = json!({
"indexes": ["*"], "indexes": ["unexpected"],
"actions": ["*"], "actions": ["*"],
"expiresAt": "2050-11-13T00:00:00Z" "expiresAt": "2050-11-13T00:00:00Z"
}); });
@ -603,7 +692,7 @@ async fn lazy_create_index() {
// use created key. // use created key.
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(key);
// try to create a index via add documents route // try to create a index via add documents route
let index = server.index("test"); let index = server.index("test");
@ -615,38 +704,21 @@ async fn lazy_create_index() {
]); ]);
let (response, code) = index.add_documents(documents, None).await; let (response, code) = index.add_documents(documents, None).await;
assert_eq!(202, code, "{:?}", &response); assert_eq!(403, code, "{:?}", &response);
let task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(200, code, "{:?}", &response);
assert_eq!(response["status"], "succeeded");
// try to create a index via add settings route // try to create a index via add settings route
let index = server.index("test1"); let index = server.index("test1");
let settings = json!({ "distinctAttribute": "test"}); let settings = json!({ "distinctAttribute": "test"});
let (response, code) = index.update_settings(settings).await; let (response, code) = index.update_settings(settings).await;
assert_eq!(202, code, "{:?}", &response); assert_eq!(403, code, "{:?}", &response);
let task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(200, code, "{:?}", &response);
assert_eq!(response["status"], "succeeded");
// try to create a index via add specialized settings route // try to create a index via add specialized settings route
let index = server.index("test2"); let index = server.index("test2");
let (response, code) = index.update_distinct_attribute(json!("test")).await; let (response, code) = index.update_distinct_attribute(json!("test")).await;
assert_eq!(202, code, "{:?}", &response); assert_eq!(403, code, "{:?}", &response);
let task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await; // try to create a index via create index route
let index = server.index("test3");
let (response, code) = index.get_task(task_id).await; let (response, code) = index.create(None).await;
assert_eq!(200, code, "{:?}", &response); assert_eq!(403, code, "{:?}", &response);
assert_eq!(response["status"], "succeeded");
} }

View File

@ -19,7 +19,7 @@ async fn error_api_key_bad_content_types() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -91,7 +91,7 @@ async fn error_api_key_empty_content_types() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -163,7 +163,7 @@ async fn error_api_key_missing_content_types() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -227,7 +227,7 @@ async fn error_api_key_empty_payload() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -283,7 +283,7 @@ async fn error_api_key_malformed_payload() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;

View File

@ -473,7 +473,7 @@ async fn error_access_forbidden_routes() {
"searchRules" => json!(["*"]), "searchRules" => json!(["*"]),
"exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp())
}; };
let web_token = generate_tenant_token(&uid, &key, tenant_token); let web_token = generate_tenant_token(uid, key, tenant_token);
server.use_api_key(&web_token); server.use_api_key(&web_token);
for ((method, route), actions) in AUTHORIZATIONS.iter() { for ((method, route), actions) in AUTHORIZATIONS.iter() {
@ -509,7 +509,7 @@ async fn error_access_expired_parent_key() {
"searchRules" => json!(["*"]), "searchRules" => json!(["*"]),
"exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp())
}; };
let web_token = generate_tenant_token(&uid, &key, tenant_token); let web_token = generate_tenant_token(uid, key, tenant_token);
server.use_api_key(&web_token); server.use_api_key(&web_token);
// test search request while parent_key is not expired // test search request while parent_key is not expired
@ -552,7 +552,7 @@ async fn error_access_modified_token() {
"searchRules" => json!(["products"]), "searchRules" => json!(["products"]),
"exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp())
}; };
let web_token = generate_tenant_token(&uid, &key, tenant_token); let web_token = generate_tenant_token(uid, key, tenant_token);
server.use_api_key(&web_token); server.use_api_key(&web_token);
// test search request while web_token is valid // test search request while web_token is valid
@ -567,7 +567,7 @@ async fn error_access_modified_token() {
"exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp())
}; };
let alt = generate_tenant_token(&uid, &key, tenant_token); let alt = generate_tenant_token(uid, key, tenant_token);
let altered_token = [ let altered_token = [
web_token.split('.').next().unwrap(), web_token.split('.').next().unwrap(),
alt.split('.').nth(1).unwrap(), alt.split('.').nth(1).unwrap(),

View File

@ -162,6 +162,8 @@ pub fn default_settings(dir: impl AsRef<Path>) -> Opt {
max_indexing_memory: MaxMemory::unlimited(), max_indexing_memory: MaxMemory::unlimited(),
..Parser::parse_from(None as Option<&str>) ..Parser::parse_from(None as Option<&str>)
}, },
#[cfg(feature = "metrics")]
enable_metrics_route: true,
..Parser::parse_from(None as Option<&str>) ..Parser::parse_from(None as Option<&str>)
} }
} }

View File

@ -18,7 +18,7 @@ impl Service {
&self.meilisearch, &self.meilisearch,
&self.auth, &self.auth,
true, true,
&self.options, self.options,
analytics::MockAnalytics::new(&self.options).0 analytics::MockAnalytics::new(&self.options).0
)) ))
.await; .await;
@ -46,7 +46,7 @@ impl Service {
&self.meilisearch, &self.meilisearch,
&self.auth, &self.auth,
true, true,
&self.options, self.options,
analytics::MockAnalytics::new(&self.options).0 analytics::MockAnalytics::new(&self.options).0
)) ))
.await; .await;
@ -72,7 +72,7 @@ impl Service {
&self.meilisearch, &self.meilisearch,
&self.auth, &self.auth,
true, true,
&self.options, self.options,
analytics::MockAnalytics::new(&self.options).0 analytics::MockAnalytics::new(&self.options).0
)) ))
.await; .await;
@ -95,7 +95,7 @@ impl Service {
&self.meilisearch, &self.meilisearch,
&self.auth, &self.auth,
true, true,
&self.options, self.options,
analytics::MockAnalytics::new(&self.options).0 analytics::MockAnalytics::new(&self.options).0
)) ))
.await; .await;
@ -118,7 +118,7 @@ impl Service {
&self.meilisearch, &self.meilisearch,
&self.auth, &self.auth,
true, true,
&self.options, self.options,
analytics::MockAnalytics::new(&self.options).0 analytics::MockAnalytics::new(&self.options).0
)) ))
.await; .await;
@ -141,7 +141,7 @@ impl Service {
&self.meilisearch, &self.meilisearch,
&self.auth, &self.auth,
true, true,
&self.options, self.options,
analytics::MockAnalytics::new(&self.options).0 analytics::MockAnalytics::new(&self.options).0
)) ))
.await; .await;

View File

@ -63,7 +63,7 @@ async fn error_json_bad_content_type() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -146,7 +146,7 @@ async fn extract_actual_content_type() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;

View File

@ -1,5 +1,6 @@
use crate::common::{GetAllDocumentsOptions, Server}; use crate::common::{GetAllDocumentsOptions, Server};
use actix_web::test; use actix_web::test;
use meilisearch_http::{analytics, create_app}; use meilisearch_http::{analytics, create_app};
use serde_json::{json, Value}; use serde_json::{json, Value};
use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use time::{format_description::well_known::Rfc3339, OffsetDateTime};
@ -20,7 +21,52 @@ async fn add_documents_test_json_content_types() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0
))
.await;
// post
let req = test::TestRequest::post()
.uri("/indexes/dog/documents")
.set_payload(document.to_string())
.insert_header(("content-type", "application/json"))
.to_request();
let res = test::call_service(&app, req).await;
let status_code = res.status();
let body = test::read_body(res).await;
let response: Value = serde_json::from_slice(&body).unwrap_or_default();
assert_eq!(status_code, 202);
assert_eq!(response["taskUid"], 0);
// put
let req = test::TestRequest::put()
.uri("/indexes/dog/documents")
.set_payload(document.to_string())
.insert_header(("content-type", "application/json"))
.to_request();
let res = test::call_service(&app, req).await;
let status_code = res.status();
let body = test::read_body(res).await;
let response: Value = serde_json::from_slice(&body).unwrap_or_default();
assert_eq!(status_code, 202);
assert_eq!(response["taskUid"], 1);
}
/// Here we try to send a single document instead of an array with a single document inside.
#[actix_rt::test]
async fn add_single_document_test_json_content_types() {
let document = json!({
"id": 1,
"content": "Bouvier Bernois",
});
// this is a what is expected and should work
let server = Server::new().await;
let app = test::init_service(create_app!(
&server.service.meilisearch,
&server.service.auth,
true,
server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -66,7 +112,7 @@ async fn error_add_documents_test_bad_content_types() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -134,7 +180,7 @@ async fn error_add_documents_test_no_content_type() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -194,7 +240,7 @@ async fn error_add_malformed_csv_documents() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -256,7 +302,7 @@ async fn error_add_malformed_json_documents() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -326,7 +372,7 @@ async fn error_add_malformed_json_documents() {
assert_eq!( assert_eq!(
response["message"], response["message"],
json!( json!(
r#"The `json` payload provided is malformed. `Couldn't serialize document value: invalid type: string "0123456789012345678901234567...890123456789", expected a documents, or a sequence of documents. at line 1 column 102`."# r#"The `json` payload provided is malformed. `Couldn't serialize document value: data did not match any variant of untagged enum Either`."#
) )
); );
assert_eq!(response["code"], json!("malformed_payload")); assert_eq!(response["code"], json!("malformed_payload"));
@ -349,9 +395,7 @@ async fn error_add_malformed_json_documents() {
assert_eq!(status_code, 400); assert_eq!(status_code, 400);
assert_eq!( assert_eq!(
response["message"], response["message"],
json!( json!("The `json` payload provided is malformed. `Couldn't serialize document value: data did not match any variant of untagged enum Either`.")
r#"The `json` payload provided is malformed. `Couldn't serialize document value: invalid type: string "0123456789012345678901234567...90123456789m", expected a documents, or a sequence of documents. at line 1 column 103`."#
)
); );
assert_eq!(response["code"], json!("malformed_payload")); assert_eq!(response["code"], json!("malformed_payload"));
assert_eq!(response["type"], json!("invalid_request")); assert_eq!(response["type"], json!("invalid_request"));
@ -370,7 +414,7 @@ async fn error_add_malformed_ndjson_documents() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -388,7 +432,7 @@ async fn error_add_malformed_ndjson_documents() {
assert_eq!( assert_eq!(
response["message"], response["message"],
json!( json!(
r#"The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 1 column 2`."# r#"The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`."#
) )
); );
assert_eq!(response["code"], json!("malformed_payload")); assert_eq!(response["code"], json!("malformed_payload"));
@ -411,9 +455,7 @@ async fn error_add_malformed_ndjson_documents() {
assert_eq!(status_code, 400); assert_eq!(status_code, 400);
assert_eq!( assert_eq!(
response["message"], response["message"],
json!( json!("The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`.")
r#"The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 1 column 2`."#
)
); );
assert_eq!(response["code"], json!("malformed_payload")); assert_eq!(response["code"], json!("malformed_payload"));
assert_eq!(response["type"], json!("invalid_request")); assert_eq!(response["type"], json!("invalid_request"));
@ -432,7 +474,7 @@ async fn error_add_missing_payload_csv_documents() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -484,7 +526,7 @@ async fn error_add_missing_payload_json_documents() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -536,7 +578,7 @@ async fn error_add_missing_payload_ndjson_documents() {
&server.service.meilisearch, &server.service.meilisearch,
&server.service.auth, &server.service.auth,
true, true,
&server.service.options, server.service.options,
analytics::MockAnalytics::new(&server.service.options).0 analytics::MockAnalytics::new(&server.service.options).0
)) ))
.await; .await;
@ -1020,7 +1062,7 @@ async fn add_documents_invalid_geo_field() {
index.wait_task(2).await; index.wait_task(2).await;
let (response, code) = index.get_task(2).await; let (response, code) = index.get_task(2).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response["status"], "succeeded"); assert_eq!(response["status"], "failed");
} }
#[actix_rt::test] #[actix_rt::test]
@ -1099,3 +1141,62 @@ async fn add_documents_with_primary_key_twice() {
let (response, _code) = index.get_task(1).await; let (response, _code) = index.get_task(1).await;
assert_eq!(response["status"], "succeeded"); assert_eq!(response["status"], "succeeded");
} }
#[actix_rt::test]
async fn batch_several_documents_addition() {
let server = Server::new().await;
let index = server.index("test");
let mut documents: Vec<_> = (0..150usize)
.into_iter()
.map(|id| {
json!(
{
"id": id,
"title": "foo",
"desc": "bar"
}
)
})
.collect();
documents[100] = json!({"title": "error", "desc": "error"});
// enqueue batch of documents
let mut waiter = Vec::new();
for chunk in documents.chunks(30) {
waiter.push(index.add_documents(json!(chunk), Some("id")));
}
// wait first batch of documents to finish
futures::future::join_all(waiter).await;
index.wait_task(4).await;
// run a second completely failing batch
documents[40] = json!({"title": "error", "desc": "error"});
documents[70] = json!({"title": "error", "desc": "error"});
documents[130] = json!({"title": "error", "desc": "error"});
let mut waiter = Vec::new();
for chunk in documents.chunks(30) {
waiter.push(index.add_documents(json!(chunk), Some("id")));
}
// wait second batch of documents to finish
futures::future::join_all(waiter).await;
index.wait_task(9).await;
let (response, _code) = index.filtered_tasks(&[], &["failed"]).await;
// Check if only the 6th task failed
println!("{}", &response);
assert_eq!(response["results"].as_array().unwrap().len(), 5);
// Check if there are exactly 120 documents (150 - 30) in the index;
let (response, code) = index
.get_all_documents(GetAllDocumentsOptions {
limit: Some(200),
..Default::default()
})
.await;
assert_eq!(code, 200, "failed with `{}`", response);
assert_eq!(response["results"].as_array().unwrap().len(), 120);
}

View File

@ -74,7 +74,7 @@ async fn filter_invalid_syntax_object() {
index.wait_task(1).await; index.wait_task(1).await;
let expected_response = json!({ let expected_response = json!({
"message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `TO` or `_geoRadius` at `title & Glass`.\n1:14 title & Glass", "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `TO`, `EXISTS`, `NOT EXISTS`, or `_geoRadius` at `title & Glass`.\n1:14 title & Glass",
"code": "invalid_filter", "code": "invalid_filter",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_filter" "link": "https://docs.meilisearch.com/errors#invalid_filter"
@ -101,7 +101,7 @@ async fn filter_invalid_syntax_array() {
index.wait_task(1).await; index.wait_task(1).await;
let expected_response = json!({ let expected_response = json!({
"message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `TO` or `_geoRadius` at `title & Glass`.\n1:14 title & Glass", "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `TO`, `EXISTS`, `NOT EXISTS`, or `_geoRadius` at `title & Glass`.\n1:14 title & Glass",
"code": "invalid_filter", "code": "invalid_filter",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_filter" "link": "https://docs.meilisearch.com/errors#invalid_filter"

View File

@ -708,9 +708,7 @@ async fn faceting_max_values_per_facet() {
}), }),
|response, code| { |response, code| {
assert_eq!(code, 200, "{}", response); assert_eq!(code, 200, "{}", response);
let numbers = dbg!(&response)["facetDistribution"]["number"] let numbers = &response["facetDistribution"]["number"].as_object().unwrap();
.as_object()
.unwrap();
assert_eq!(numbers.len(), 10_000); assert_eq!(numbers.len(), 10_000);
}, },
) )

View File

@ -1,11 +1,11 @@
[package] [package]
name = "meilisearch-lib" name = "meilisearch-lib"
version = "0.28.1" version = "0.29.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
actix-web = { version = "4.0.1", default-features = false } actix-web = { version = "4.0.1", default-features = false }
anyhow = { version = "1.0.56", features = ["backtrace"] } anyhow = { version = "1.0.62", features = ["backtrace"] }
async-stream = "0.3.3" async-stream = "0.3.3"
async-trait = "0.1.52" async-trait = "0.1.52"
atomic_refcell = "0.1.8" atomic_refcell = "0.1.8"
@ -15,7 +15,7 @@ clap = { version = "3.1.6", features = ["derive", "env"] }
crossbeam-channel = "0.5.2" crossbeam-channel = "0.5.2"
csv = "1.1.6" csv = "1.1.6"
derivative = "2.2.0" derivative = "2.2.0"
either = "1.6.1" either = { version = "1.6.1", features = ["serde"] }
flate2 = "1.0.22" flate2 = "1.0.22"
fs_extra = "1.2.0" fs_extra = "1.2.0"
fst = "0.4.7" fst = "0.4.7"
@ -28,11 +28,12 @@ lazy_static = "1.4.0"
log = "0.4.14" log = "0.4.14"
meilisearch-auth = { path = "../meilisearch-auth" } meilisearch-auth = { path = "../meilisearch-auth" }
meilisearch-types = { path = "../meilisearch-types" } meilisearch-types = { path = "../meilisearch-types" }
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.31.2" } milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.33.5" }
mime = "0.3.16" mime = "0.3.16"
num_cpus = "1.13.1" num_cpus = "1.13.1"
obkv = "0.2.0" obkv = "0.2.0"
once_cell = "1.10.0" once_cell = "1.10.0"
page_size = "0.4.2"
parking_lot = "0.12.0" parking_lot = "0.12.0"
permissive-json-pointer = { path = "../permissive-json-pointer" } permissive-json-pointer = { path = "../permissive-json-pointer" }
rand = "0.8.5" rand = "0.8.5"
@ -42,7 +43,7 @@ reqwest = { version = "0.11.9", features = ["json", "rustls-tls"], default-featu
roaring = "0.9.0" roaring = "0.9.0"
rustls = "0.20.4" rustls = "0.20.4"
serde = { version = "1.0.136", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] }
serde_json = { version = "1.0.79", features = ["preserve_order"] } serde_json = { version = "1.0.85", features = ["preserve_order"] }
siphasher = "0.3.10" siphasher = "0.3.10"
slice-group-by = "0.3.0" slice-group-by = "0.3.0"
sysinfo = "0.23.5" sysinfo = "0.23.5"

View File

@ -1,10 +1,13 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use std::fmt::{self, Debug, Display}; use std::fmt::{self, Debug, Display};
use std::io::{self, BufRead, BufReader, BufWriter, Cursor, Read, Seek, Write}; use std::io::{self, BufReader, Read, Seek, Write};
use either::Either;
use meilisearch_types::error::{Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::internal_error; use meilisearch_types::internal_error;
use milli::documents::DocumentBatchBuilder; use milli::documents::{DocumentsBatchBuilder, Error};
use milli::Object;
use serde::Deserialize;
type Result<T> = std::result::Result<T, DocumentFormatError>; type Result<T> = std::result::Result<T, DocumentFormatError>;
@ -18,9 +21,9 @@ pub enum PayloadType {
impl fmt::Display for PayloadType { impl fmt::Display for PayloadType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
PayloadType::Ndjson => write!(f, "ndjson"), PayloadType::Ndjson => f.write_str("ndjson"),
PayloadType::Json => write!(f, "json"), PayloadType::Json => f.write_str("json"),
PayloadType::Csv => write!(f, "csv"), PayloadType::Csv => f.write_str("csv"),
} }
} }
} }
@ -28,7 +31,7 @@ impl fmt::Display for PayloadType {
#[derive(Debug)] #[derive(Debug)]
pub enum DocumentFormatError { pub enum DocumentFormatError {
Internal(Box<dyn std::error::Error + Send + Sync + 'static>), Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
MalformedPayload(Box<milli::documents::Error>, PayloadType), MalformedPayload(Error, PayloadType),
} }
impl Display for DocumentFormatError { impl Display for DocumentFormatError {
@ -36,7 +39,7 @@ impl Display for DocumentFormatError {
match self { match self {
Self::Internal(e) => write!(f, "An internal error has occurred: `{}`.", e), Self::Internal(e) => write!(f, "An internal error has occurred: `{}`.", e),
Self::MalformedPayload(me, b) => match me.borrow() { Self::MalformedPayload(me, b) => match me.borrow() {
milli::documents::Error::JsonError(se) => { Error::Json(se) => {
// https://github.com/meilisearch/meilisearch/issues/2107 // https://github.com/meilisearch/meilisearch/issues/2107
// The user input maybe insanely long. We need to truncate it. // The user input maybe insanely long. We need to truncate it.
let mut serde_msg = se.to_string(); let mut serde_msg = se.to_string();
@ -59,11 +62,11 @@ impl Display for DocumentFormatError {
impl std::error::Error for DocumentFormatError {} impl std::error::Error for DocumentFormatError {}
impl From<(PayloadType, milli::documents::Error)> for DocumentFormatError { impl From<(PayloadType, Error)> for DocumentFormatError {
fn from((ty, error): (PayloadType, milli::documents::Error)) -> Self { fn from((ty, error): (PayloadType, Error)) -> Self {
match error { match error {
milli::documents::Error::Io(e) => Self::Internal(Box::new(e)), Error::Io(e) => Self::Internal(Box::new(e)),
e => Self::MalformedPayload(Box::new(e), ty), e => Self::MalformedPayload(e, ty),
} }
} }
} }
@ -79,51 +82,74 @@ impl ErrorCode for DocumentFormatError {
internal_error!(DocumentFormatError: io::Error); internal_error!(DocumentFormatError: io::Error);
/// reads csv from input and write an obkv batch to writer. /// Reads CSV from input and write an obkv batch to writer.
pub fn read_csv(input: impl Read, writer: impl Write + Seek) -> Result<usize> { pub fn read_csv(input: impl Read, writer: impl Write + Seek) -> Result<usize> {
let writer = BufWriter::new(writer); let mut builder = DocumentsBatchBuilder::new(writer);
let builder =
DocumentBatchBuilder::from_csv(input, writer).map_err(|e| (PayloadType::Csv, e))?;
let count = builder.finish().map_err(|e| (PayloadType::Csv, e))?; let csv = csv::Reader::from_reader(input);
builder.append_csv(csv).map_err(|e| (PayloadType::Csv, e))?;
Ok(count) let count = builder.documents_count();
let _ = builder
.into_inner()
.map_err(Into::into)
.map_err(DocumentFormatError::Internal)?;
Ok(count as usize)
} }
/// reads jsonl from input and write an obkv batch to writer. /// Reads JSON Lines from input and write an obkv batch to writer.
pub fn read_ndjson(input: impl Read, writer: impl Write + Seek) -> Result<usize> { pub fn read_ndjson(input: impl Read, writer: impl Write + Seek) -> Result<usize> {
let mut reader = BufReader::new(input); let mut builder = DocumentsBatchBuilder::new(writer);
let writer = BufWriter::new(writer); let reader = BufReader::new(input);
let mut builder = DocumentBatchBuilder::new(writer).map_err(|e| (PayloadType::Ndjson, e))?; for result in serde_json::Deserializer::from_reader(reader).into_iter() {
let mut buf = String::new(); let object = result
.map_err(Error::Json)
while reader.read_line(&mut buf)? > 0 {
// skip empty lines
if buf == "\n" {
buf.clear();
continue;
}
builder
.extend_from_json(Cursor::new(&buf.as_bytes()))
.map_err(|e| (PayloadType::Ndjson, e))?; .map_err(|e| (PayloadType::Ndjson, e))?;
buf.clear(); builder
.append_json_object(&object)
.map_err(Into::into)
.map_err(DocumentFormatError::Internal)?;
} }
let count = builder.finish().map_err(|e| (PayloadType::Ndjson, e))?; let count = builder.documents_count();
let _ = builder
.into_inner()
.map_err(Into::into)
.map_err(DocumentFormatError::Internal)?;
Ok(count) Ok(count as usize)
} }
/// reads json from input and write an obkv batch to writer. /// Reads JSON from input and write an obkv batch to writer.
pub fn read_json(input: impl Read, writer: impl Write + Seek) -> Result<usize> { pub fn read_json(input: impl Read, writer: impl Write + Seek) -> Result<usize> {
let writer = BufWriter::new(writer); let mut builder = DocumentsBatchBuilder::new(writer);
let mut builder = DocumentBatchBuilder::new(writer).map_err(|e| (PayloadType::Json, e))?; let reader = BufReader::new(input);
builder
.extend_from_json(input) #[derive(Deserialize, Debug)]
#[serde(transparent)]
struct ArrayOrSingleObject {
#[serde(with = "either::serde_untagged")]
inner: Either<Vec<Object>, Object>,
}
let content: ArrayOrSingleObject = serde_json::from_reader(reader)
.map_err(Error::Json)
.map_err(|e| (PayloadType::Json, e))?; .map_err(|e| (PayloadType::Json, e))?;
let count = builder.finish().map_err(|e| (PayloadType::Json, e))?; for object in content.inner.map_right(|o| vec![o]).into_inner() {
builder
.append_json_object(&object)
.map_err(Into::into)
.map_err(DocumentFormatError::Internal)?;
}
Ok(count) let count = builder.documents_count();
let _ = builder
.into_inner()
.map_err(Into::into)
.map_err(DocumentFormatError::Internal)?;
Ok(count as usize)
} }

View File

@ -11,7 +11,7 @@ pub enum DumpError {
#[error("An internal error has occurred. `{0}`.")] #[error("An internal error has occurred. `{0}`.")]
Internal(Box<dyn std::error::Error + Send + Sync + 'static>), Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("{0}")] #[error("{0}")]
IndexResolver(#[from] IndexResolverError), IndexResolver(Box<IndexResolverError>),
} }
internal_error!( internal_error!(
@ -26,6 +26,12 @@ internal_error!(
TaskError TaskError
); );
impl From<IndexResolverError> for DumpError {
fn from(e: IndexResolverError) -> Self {
Self::IndexResolver(Box::new(e))
}
}
impl ErrorCode for DumpError { impl ErrorCode for DumpError {
fn error_code(&self) -> Code { fn error_code(&self) -> Code {
match self { match self {

View File

@ -143,7 +143,7 @@ impl MetadataVersion {
} }
} }
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum DumpStatus { pub enum DumpStatus {
Done, Done,

View File

@ -24,7 +24,9 @@ impl ErrorCode for MilliError<'_> {
match error { match error {
// TODO: wait for spec for new error codes. // TODO: wait for spec for new error codes.
UserError::SerdeJson(_) UserError::SerdeJson(_)
| UserError::InvalidLmdbOpenOptions
| UserError::DocumentLimitReached | UserError::DocumentLimitReached
| UserError::AccessingSoftDeletedDocument { .. }
| UserError::UnknownInternalDocumentId { .. } => Code::Internal, | UserError::UnknownInternalDocumentId { .. } => Code::Internal,
UserError::InvalidStoreFile => Code::InvalidStore, UserError::InvalidStoreFile => Code::InvalidStore,
UserError::NoSpaceLeftOnDevice => Code::NoSpaceLeftOnDevice, UserError::NoSpaceLeftOnDevice => Code::NoSpaceLeftOnDevice,
@ -32,7 +34,9 @@ impl ErrorCode for MilliError<'_> {
UserError::AttributeLimitReached => Code::MaxFieldsLimitExceeded, UserError::AttributeLimitReached => Code::MaxFieldsLimitExceeded,
UserError::InvalidFilter(_) => Code::Filter, UserError::InvalidFilter(_) => Code::Filter,
UserError::MissingDocumentId { .. } => Code::MissingDocumentId, UserError::MissingDocumentId { .. } => Code::MissingDocumentId,
UserError::InvalidDocumentId { .. } => Code::InvalidDocumentId, UserError::InvalidDocumentId { .. } | UserError::TooManyDocumentIds { .. } => {
Code::InvalidDocumentId
}
UserError::MissingPrimaryKey => Code::MissingPrimaryKey, UserError::MissingPrimaryKey => Code::MissingPrimaryKey,
UserError::PrimaryKeyCannotBeChanged(_) => Code::PrimaryKeyAlreadyPresent, UserError::PrimaryKeyCannotBeChanged(_) => Code::PrimaryKeyAlreadyPresent,
UserError::SortRankingRuleMissing => Code::Sort, UserError::SortRankingRuleMissing => Code::Sort,

View File

@ -4,7 +4,7 @@ use std::path::Path;
use anyhow::Context; use anyhow::Context;
use indexmap::IndexMap; use indexmap::IndexMap;
use milli::documents::DocumentBatchReader; use milli::documents::DocumentsBatchReader;
use milli::heed::{EnvOpenOptions, RoTxn}; use milli::heed::{EnvOpenOptions, RoTxn};
use milli::update::{IndexDocumentsConfig, IndexerConfig}; use milli::update::{IndexDocumentsConfig, IndexerConfig};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -135,19 +135,20 @@ impl Index {
if !empty { if !empty {
tmp_doc_file.seek(SeekFrom::Start(0))?; tmp_doc_file.seek(SeekFrom::Start(0))?;
let documents_reader = DocumentBatchReader::from_reader(tmp_doc_file)?; let documents_reader = DocumentsBatchReader::from_reader(tmp_doc_file)?;
//If the document file is empty, we don't perform the document addition, to prevent //If the document file is empty, we don't perform the document addition, to prevent
//a primary key error to be thrown. //a primary key error to be thrown.
let config = IndexDocumentsConfig::default(); let config = IndexDocumentsConfig::default();
let mut builder = milli::update::IndexDocuments::new( let builder = milli::update::IndexDocuments::new(
&mut txn, &mut txn,
&index, &index,
indexer_config, indexer_config,
config, config,
|_| (), |_| (),
)?; )?;
builder.add_documents(documents_reader)?; let (builder, user_error) = builder.add_documents(documents_reader)?;
user_error?;
builder.execute()?; builder.execute()?;
} }

View File

@ -40,6 +40,12 @@ impl ErrorCode for IndexError {
} }
} }
impl From<milli::UserError> for IndexError {
fn from(error: milli::UserError) -> IndexError {
IndexError::Milli(error.into())
}
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum FacetError { pub enum FacetError {
#[error("Invalid syntax for the filter parameter: `expected {}, found: {1}`.", .0.join(", "))] #[error("Invalid syntax for the filter parameter: `expected {}, found: {1}`.", .0.join(", "))]

View File

@ -4,7 +4,6 @@ use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use walkdir::WalkDir;
use fst::IntoStreamer; use fst::IntoStreamer;
use milli::heed::{CompactionOption, EnvOpenOptions, RoTxn}; use milli::heed::{CompactionOption, EnvOpenOptions, RoTxn};
@ -14,6 +13,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
use walkdir::WalkDir;
use crate::index::search::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use crate::index::search::DEFAULT_PAGINATION_MAX_TOTAL_HITS;
@ -245,11 +245,8 @@ impl Index {
let fields_ids_map = self.fields_ids_map(&txn)?; let fields_ids_map = self.fields_ids_map(&txn)?;
let all_fields: Vec<_> = fields_ids_map.iter().map(|(id, _)| id).collect(); let all_fields: Vec<_> = fields_ids_map.iter().map(|(id, _)| id).collect();
let iter = self.all_documents(&txn)?.skip(offset).take(limit);
let mut documents = Vec::new(); let mut documents = Vec::new();
for entry in self.all_documents(&txn)?.skip(offset).take(limit) {
for entry in iter {
let (_id, obkv) = entry?; let (_id, obkv) = entry?;
let document = obkv_to_json(&all_fields, &fields_ids_map, obkv)?; let document = obkv_to_json(&all_fields, &fields_ids_map, obkv)?;
let document = match &attributes_to_retrieve { let document = match &attributes_to_retrieve {
@ -302,7 +299,7 @@ impl Index {
} }
pub fn size(&self) -> u64 { pub fn size(&self) -> u64 {
WalkDir::new(self.inner.path()) WalkDir::new(self.path())
.into_iter() .into_iter()
.filter_map(|entry| entry.ok()) .filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok()) .filter_map(|entry| entry.metadata().ok())

View File

@ -1,5 +1,5 @@
pub use search::{ pub use search::{
SearchQuery, SearchResult, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, MatchingStrategy, SearchQuery, SearchResult, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER,
DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
}; };
pub use updates::{apply_settings_to_builder, Checked, Facets, Settings, Unchecked}; pub use updates::{apply_settings_to_builder, Checked, Facets, Settings, Unchecked};
@ -24,12 +24,12 @@ pub use test::MockIndex as Index;
/// code for unit testing, in places where an index would normally be used. /// code for unit testing, in places where an index would normally be used.
#[cfg(test)] #[cfg(test)]
pub mod test { pub mod test {
use std::path::Path; use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use milli::update::IndexerConfig; use milli::update::{
use milli::update::{DocumentAdditionResult, DocumentDeletionResult, IndexDocumentsMethod}; DocumentAdditionResult, DocumentDeletionResult, IndexDocumentsMethod, IndexerConfig,
};
use nelson::Mocker; use nelson::Mocker;
use uuid::Uuid; use uuid::Uuid;
@ -162,7 +162,7 @@ pub mod test {
primary_key: Option<String>, primary_key: Option<String>,
file_store: UpdateFileStore, file_store: UpdateFileStore,
contents: impl Iterator<Item = Uuid>, contents: impl Iterator<Item = Uuid>,
) -> Result<DocumentAdditionResult> { ) -> Result<Vec<Result<DocumentAdditionResult>>> {
match self { match self {
MockIndex::Real(index) => { MockIndex::Real(index) => {
index.update_documents(method, primary_key, file_store, contents) index.update_documents(method, primary_key, file_store, contents)

View File

@ -7,7 +7,7 @@ use either::Either;
use milli::tokenizer::TokenizerBuilder; use milli::tokenizer::TokenizerBuilder;
use milli::{ use milli::{
AscDesc, FieldId, FieldsIdsMap, Filter, FormatOptions, MatchBounds, MatcherBuilder, SortError, AscDesc, FieldId, FieldsIdsMap, Filter, FormatOptions, MatchBounds, MatcherBuilder, SortError,
DEFAULT_VALUES_PER_FACET, TermsMatchingStrategy, DEFAULT_VALUES_PER_FACET,
}; };
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -31,7 +31,7 @@ pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
/// will be able to return in one search call. /// will be able to return in one search call.
pub const DEFAULT_PAGINATION_MAX_TOTAL_HITS: usize = 1000; pub const DEFAULT_PAGINATION_MAX_TOTAL_HITS: usize = 1000;
#[derive(Deserialize, Debug, Clone, PartialEq)] #[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SearchQuery { pub struct SearchQuery {
pub q: Option<String>, pub q: Option<String>,
@ -55,6 +55,32 @@ pub struct SearchQuery {
pub highlight_post_tag: String, pub highlight_post_tag: String,
#[serde(default = "DEFAULT_CROP_MARKER")] #[serde(default = "DEFAULT_CROP_MARKER")]
pub crop_marker: String, pub crop_marker: String,
#[serde(default)]
pub matching_strategy: MatchingStrategy,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum MatchingStrategy {
/// Remove query words from last to first
Last,
/// All query words are mandatory
All,
}
impl Default for MatchingStrategy {
fn default() -> Self {
Self::Last
}
}
impl From<MatchingStrategy> for TermsMatchingStrategy {
fn from(other: MatchingStrategy) -> Self {
match other {
MatchingStrategy::Last => Self::Last,
MatchingStrategy::All => Self::All,
}
}
} }
#[derive(Debug, Clone, Serialize, PartialEq)] #[derive(Debug, Clone, Serialize, PartialEq)]
@ -91,6 +117,8 @@ impl Index {
search.query(query); search.query(query);
} }
search.terms_matching_strategy(query.matching_strategy.into());
let max_total_hits = self let max_total_hits = self
.pagination_max_total_hits(&rtxn)? .pagination_max_total_hits(&rtxn)?
.unwrap_or(DEFAULT_PAGINATION_MAX_TOTAL_HITS); .unwrap_or(DEFAULT_PAGINATION_MAX_TOTAL_HITS);

View File

@ -3,7 +3,7 @@ use std::marker::PhantomData;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use log::{debug, info, trace}; use log::{debug, info, trace};
use milli::documents::DocumentBatchReader; use milli::documents::DocumentsBatchReader;
use milli::update::{ use milli::update::{
DocumentAdditionResult, DocumentDeletionResult, IndexDocumentsConfig, IndexDocumentsMethod, DocumentAdditionResult, DocumentDeletionResult, IndexDocumentsConfig, IndexDocumentsMethod,
Setting, Setting,
@ -11,7 +11,7 @@ use milli::update::{
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use uuid::Uuid; use uuid::Uuid;
use super::error::Result; use super::error::{IndexError, Result};
use super::index::{Index, IndexMeta}; use super::index::{Index, IndexMeta};
use crate::update_file_store::UpdateFileStore; use crate::update_file_store::UpdateFileStore;
@ -31,10 +31,10 @@ where
.serialize(s) .serialize(s)
} }
#[derive(Clone, Default, Debug, Serialize, PartialEq)] #[derive(Clone, Default, Debug, Serialize, PartialEq, Eq)]
pub struct Checked; pub struct Checked;
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Unchecked; pub struct Unchecked;
#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(proptest_derive::Arbitrary))]
@ -299,7 +299,7 @@ impl Index {
primary_key: Option<String>, primary_key: Option<String>,
file_store: UpdateFileStore, file_store: UpdateFileStore,
contents: impl IntoIterator<Item = Uuid>, contents: impl IntoIterator<Item = Uuid>,
) -> Result<DocumentAdditionResult> { ) -> Result<Vec<Result<DocumentAdditionResult>>> {
trace!("performing document addition"); trace!("performing document addition");
let mut txn = self.write_txn()?; let mut txn = self.write_txn()?;
@ -323,19 +323,31 @@ impl Index {
indexing_callback, indexing_callback,
)?; )?;
let mut results = Vec::new();
for content_uuid in contents.into_iter() { for content_uuid in contents.into_iter() {
let content_file = file_store.get_update(content_uuid)?; let content_file = file_store.get_update(content_uuid)?;
let reader = DocumentBatchReader::from_reader(content_file)?; let reader = DocumentsBatchReader::from_reader(content_file)?;
builder.add_documents(reader)?; let (new_builder, user_result) = builder.add_documents(reader)?;
builder = new_builder;
let user_result = match user_result {
Ok(count) => Ok(DocumentAdditionResult {
indexed_documents: count,
number_of_documents: count,
}),
Err(e) => Err(IndexError::from(e)),
};
results.push(user_result);
} }
let addition = builder.execute()?; if results.iter().any(Result::is_ok) {
let addition = builder.execute()?;
txn.commit()?;
info!("document addition done: {:?}", addition);
}
txn.commit()?; Ok(results)
info!("document addition done: {:?}", addition);
Ok(addition)
} }
pub fn update_settings(&self, settings: &Settings<Checked>) -> Result<()> { pub fn update_settings(&self, settings: &Settings<Checked>) -> Result<()> {

View File

@ -275,11 +275,13 @@ impl IndexControllerBuilder {
/// Set the index controller builder's max update store size. /// Set the index controller builder's max update store size.
pub fn set_max_task_store_size(&mut self, max_update_store_size: usize) -> &mut Self { pub fn set_max_task_store_size(&mut self, max_update_store_size: usize) -> &mut Self {
let max_update_store_size = clamp_to_page_size(max_update_store_size);
self.max_task_store_size.replace(max_update_store_size); self.max_task_store_size.replace(max_update_store_size);
self self
} }
pub fn set_max_index_size(&mut self, size: usize) -> &mut Self { pub fn set_max_index_size(&mut self, size: usize) -> &mut Self {
let size = clamp_to_page_size(size);
self.max_index_size.replace(size); self.max_index_size.replace(size);
self self
} }
@ -645,6 +647,11 @@ pub async fn get_arc_ownership_blocking<T>(mut item: Arc<T>) -> T {
} }
} }
// Clamp the provided value to be a multiple of system page size.
fn clamp_to_page_size(size: usize) -> usize {
size / page_size::get() * page_size::get()
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use futures::future::ok; use futures::future::ok;
@ -697,6 +704,7 @@ mod test {
highlight_pre_tag: DEFAULT_HIGHLIGHT_PRE_TAG(), highlight_pre_tag: DEFAULT_HIGHLIGHT_PRE_TAG(),
highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(), highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(),
crop_marker: DEFAULT_CROP_MARKER(), crop_marker: DEFAULT_CROP_MARKER(),
matching_strategy: Default::default(),
}; };
let result = SearchResult { let result = SearchResult {

View File

@ -150,25 +150,34 @@ mod real {
}) })
.await; .await;
let event = match result { match result {
Ok(Ok(result)) => TaskEvent::Succeeded { Ok(Ok(results)) => {
timestamp: OffsetDateTime::now_utc(), for (task, result) in tasks.iter_mut().zip(results) {
result: TaskResult::DocumentAddition { let event = match result {
indexed_documents: result.indexed_documents, Ok(addition) => {
}, TaskEvent::succeeded(TaskResult::DocumentAddition {
}, indexed_documents: addition.indexed_documents,
Ok(Err(e)) => TaskEvent::Failed { })
timestamp: OffsetDateTime::now_utc(), }
error: e.into(), Err(error) => {
}, TaskEvent::failed(IndexResolverError::from(error))
Err(e) => TaskEvent::Failed { }
timestamp: OffsetDateTime::now_utc(), };
error: IndexResolverError::from(e).into(), task.events.push(event);
}, }
}; }
Ok(Err(e)) => {
for task in tasks.iter_mut() { let event = TaskEvent::failed(e);
task.events.push(event.clone()); for task in tasks.iter_mut() {
task.events.push(event.clone());
}
}
Err(e) => {
let event = TaskEvent::failed(IndexResolverError::from(e));
for task in tasks.iter_mut() {
task.events.push(event.clone());
}
}
} }
} }
_ => panic!("invalid batch!"), _ => panic!("invalid batch!"),

View File

@ -41,27 +41,10 @@ pub struct IndexerOpts {
#[derive(Debug, Clone, Parser, Default, Serialize)] #[derive(Debug, Clone, Parser, Default, Serialize)]
pub struct SchedulerConfig { pub struct SchedulerConfig {
/// enable the autobatching experimental feature /// The engine will disable task auto-batching,
#[clap(long, hide = true)] /// and will sequencialy compute each task one by one.
pub enable_auto_batching: bool, #[clap(long, env = "DISABLE_AUTO_BATCHING")]
pub disable_auto_batching: bool,
// The maximum number of updates of the same type that can be batched together.
// If unspecified, this is unlimited. A value of 0 is interpreted as 1.
#[clap(long, requires = "enable-auto-batching", hide = true)]
pub max_batch_size: Option<usize>,
// The maximum number of documents in a document batch. Since batches must contain at least one
// update for the scheduler to make progress, the number of documents in a batch will be at
// least the number of documents of its first update.
#[clap(long, requires = "enable-auto-batching", hide = true)]
pub max_documents_per_batch: Option<usize>,
/// Debounce duration in seconds
///
/// When a new task is enqueued, the scheduler waits for `debounce_duration_sec` seconds for new updates before
/// starting to process a batch of updates.
#[clap(long, requires = "enable-auto-batching", hide = true)]
pub debounce_duration_sec: Option<u64>,
} }
impl TryFrom<&IndexerOpts> for IndexerConfig { impl TryFrom<&IndexerOpts> for IndexerConfig {

View File

@ -3,7 +3,6 @@ use std::collections::{hash_map::Entry, BinaryHeap, HashMap, VecDeque};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::slice; use std::slice;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use atomic_refcell::AtomicRefCell; use atomic_refcell::AtomicRefCell;
use milli::update::IndexDocumentsMethod; use milli::update::IndexDocumentsMethod;
@ -214,7 +213,7 @@ impl TaskQueue {
let head = self.queue.pop()?; let head = self.queue.pop()?;
let result = { let result = {
let mut ref_head = head.borrow_mut(); let mut ref_head = head.borrow_mut();
f(&mut *ref_head) f(&mut ref_head)
}; };
if !head.borrow().tasks.is_empty() { if !head.borrow().tasks.is_empty() {
// After being mutated, the head is reinserted to the correct position. // After being mutated, the head is reinserted to the correct position.
@ -248,17 +247,10 @@ impl Scheduler {
pub fn new( pub fn new(
store: TaskStore, store: TaskStore,
performers: Vec<Arc<dyn BatchHandler + Sync + Send + 'static>>, performers: Vec<Arc<dyn BatchHandler + Sync + Send + 'static>>,
mut config: SchedulerConfig, config: SchedulerConfig,
) -> Result<Arc<RwLock<Self>>> { ) -> Result<Arc<RwLock<Self>>> {
let (notifier, rcv) = watch::channel(()); let (notifier, rcv) = watch::channel(());
let debounce_time = config.debounce_duration_sec;
// Disable autobatching
if !config.enable_auto_batching {
config.max_batch_size = Some(1);
}
let this = Self { let this = Self {
snapshots: VecDeque::new(), snapshots: VecDeque::new(),
tasks: TaskQueue::default(), tasks: TaskQueue::default(),
@ -275,12 +267,7 @@ impl Scheduler {
let this = Arc::new(RwLock::new(this)); let this = Arc::new(RwLock::new(this));
let update_loop = UpdateLoop::new( let update_loop = UpdateLoop::new(this.clone(), performers, rcv);
this.clone(),
performers,
debounce_time.filter(|&v| v > 0).map(Duration::from_secs),
rcv,
);
tokio::task::spawn_local(update_loop.run()); tokio::task::spawn_local(update_loop.run());
@ -416,7 +403,7 @@ impl Scheduler {
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Processing { pub enum Processing {
DocumentAdditions(Vec<TaskId>), DocumentAdditions(Vec<TaskId>),
IndexUpdate(TaskId), IndexUpdate(TaskId),
@ -497,27 +484,17 @@ fn make_batch(tasks: &mut TaskQueue, config: &SchedulerConfig) -> Processing {
match list.peek() { match list.peek() {
Some(pending) if pending.kind == kind => { Some(pending) if pending.kind == kind => {
// We always need to process at least one task for the scheduler to make progress. // We always need to process at least one task for the scheduler to make progress.
if task_list.len() >= config.max_batch_size.unwrap_or(usize::MAX).max(1) if config.disable_auto_batching && !task_list.is_empty() {
{
break; break;
} }
let pending = list.pop().unwrap(); let pending = list.pop().unwrap();
task_list.push(pending.id); task_list.push(pending.id);
// We add the number of documents to the count if we are scheduling document additions and // We add the number of documents to the count if we are scheduling document additions.
// stop adding if we already have enough.
//
// We check that bound only after adding the current task to the batch, so that a batch contains at least one task.
match pending.kind { match pending.kind {
TaskType::DocumentUpdate { number } TaskType::DocumentUpdate { number }
| TaskType::DocumentAddition { number } => { | TaskType::DocumentAddition { number } => {
doc_count += number; doc_count += number;
if doc_count
>= config.max_documents_per_batch.unwrap_or(usize::MAX)
{
break;
}
} }
_ => (), _ => (),
} }

View File

@ -128,7 +128,7 @@ impl Task {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub enum DocumentDeletion { pub enum DocumentDeletion {
Clear, Clear,

View File

@ -22,11 +22,13 @@ pub use store::test::MockStore as Store;
#[cfg(not(test))] #[cfg(not(test))]
pub use store::Store; pub use store::Store;
type FilterFn = Box<dyn Fn(&Task) -> bool + Sync + Send + 'static>;
/// Defines constraints to be applied when querying for Tasks from the store. /// Defines constraints to be applied when querying for Tasks from the store.
#[derive(Default)] #[derive(Default)]
pub struct TaskFilter { pub struct TaskFilter {
indexes: Option<HashSet<String>>, indexes: Option<HashSet<String>>,
filter_fn: Option<Box<dyn Fn(&Task) -> bool + Sync + Send + 'static>>, filter_fn: Option<FilterFn>,
} }
impl TaskFilter { impl TaskFilter {
@ -51,8 +53,8 @@ impl TaskFilter {
.insert(index); .insert(index);
} }
pub fn filter_fn(&mut self, f: impl Fn(&Task) -> bool + Sync + Send + 'static) { pub fn filter_fn(&mut self, f: FilterFn) {
self.filter_fn.replace(Box::new(f)); self.filter_fn.replace(f);
} }
} }
@ -115,7 +117,7 @@ impl TaskStore {
match filter { match filter {
Some(filter) => filter Some(filter) => filter
.pass(&task) .pass(&task)
.then(|| task) .then_some(task)
.ok_or(TaskError::UnexistingTask(id)), .ok_or(TaskError::UnexistingTask(id)),
None => Ok(task), None => Ok(task),
} }

View File

@ -372,9 +372,6 @@ pub mod test {
let tasks = store.list_tasks(&txn, None, Some(filter), None).unwrap(); let tasks = store.list_tasks(&txn, None, Some(filter), None).unwrap();
assert_eq!(tasks.len(), 1); assert_eq!(tasks.len(), 1);
assert_eq!( assert_eq!(tasks.first().as_ref().unwrap().index_uid().unwrap(), "test");
&*tasks.first().as_ref().unwrap().index_uid().unwrap(),
"test"
);
} }
} }

View File

@ -1,9 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::sync::{watch, RwLock}; use tokio::sync::{watch, RwLock};
use tokio::time::interval_at;
use super::batch::Batch; use super::batch::Batch;
use super::error::Result; use super::error::Result;
@ -17,20 +15,17 @@ pub struct UpdateLoop {
performers: Vec<Arc<dyn BatchHandler + Send + Sync + 'static>>, performers: Vec<Arc<dyn BatchHandler + Send + Sync + 'static>>,
notifier: Option<watch::Receiver<()>>, notifier: Option<watch::Receiver<()>>,
debounce_duration: Option<Duration>,
} }
impl UpdateLoop { impl UpdateLoop {
pub fn new( pub fn new(
scheduler: Arc<RwLock<Scheduler>>, scheduler: Arc<RwLock<Scheduler>>,
performers: Vec<Arc<dyn BatchHandler + Send + Sync + 'static>>, performers: Vec<Arc<dyn BatchHandler + Send + Sync + 'static>>,
debuf_duration: Option<Duration>,
notifier: watch::Receiver<()>, notifier: watch::Receiver<()>,
) -> Self { ) -> Self {
Self { Self {
scheduler, scheduler,
performers, performers,
debounce_duration: debuf_duration,
notifier: Some(notifier), notifier: Some(notifier),
} }
} }
@ -43,11 +38,6 @@ impl UpdateLoop {
break; break;
} }
if let Some(t) = self.debounce_duration {
let mut interval = interval_at(tokio::time::Instant::now() + t, t);
interval.tick().await;
};
if let Err(e) = self.process_next_batch().await { if let Err(e) = self.process_next_batch().await {
log::error!("an error occurred while processing an update batch: {}", e); log::error!("an error occurred while processing an update batch: {}", e);
} }

View File

@ -3,7 +3,7 @@ use std::io::{self, BufReader, BufWriter, Write};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use milli::documents::DocumentBatchReader; use milli::documents::DocumentsBatchReader;
use serde_json::Map; use serde_json::Map;
use tempfile::{NamedTempFile, PersistError}; use tempfile::{NamedTempFile, PersistError};
use uuid::Uuid; use uuid::Uuid;
@ -44,7 +44,8 @@ into_update_store_error!(
PersistError, PersistError,
io::Error, io::Error,
serde_json::Error, serde_json::Error,
milli::documents::Error milli::documents::Error,
milli::documents::DocumentsBatchCursorError
); );
impl UpdateFile { impl UpdateFile {
@ -149,12 +150,13 @@ mod store {
let update_file = File::open(update_file_path)?; let update_file = File::open(update_file_path)?;
let mut dst_file = NamedTempFile::new_in(&dump_path)?; let mut dst_file = NamedTempFile::new_in(&dump_path)?;
let mut document_reader = DocumentBatchReader::from_reader(update_file)?; let (mut document_cursor, index) =
DocumentsBatchReader::from_reader(update_file)?.into_cursor_and_fields_index();
let mut document_buffer = Map::new(); let mut document_buffer = Map::new();
// TODO: we need to find a way to do this more efficiently. (create a custom serializer // TODO: we need to find a way to do this more efficiently. (create a custom serializer
// for jsonl for example...) // for jsonl for example...)
while let Some((index, document)) = document_reader.next_document_with_index()? { while let Some(document) = document_cursor.next_document()? {
for (field_id, content) in document.iter() { for (field_id, content) in document.iter() {
if let Some(field_name) = index.name(field_id) { if let Some(field_name) = index.name(field_id) {
let content = serde_json::from_slice(content)?; let content = serde_json::from_slice(content)?;

View File

@ -1,6 +1,6 @@
[package] [package]
name = "meilisearch-types" name = "meilisearch-types"
version = "0.28.1" version = "0.29.2"
authors = ["marin <postma.marin@protonmail.com>"] authors = ["marin <postma.marin@protonmail.com>"]
edition = "2021" edition = "2021"

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
/// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400
/// bytes long /// bytes long
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] #[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
pub struct IndexUid( pub struct IndexUid(
#[cfg_attr(feature = "test-traits", proptest(regex("[a-zA-Z0-9_-]{1,400}")))] String, #[cfg_attr(feature = "test-traits", proptest(regex("[a-zA-Z0-9_-]{1,400}")))] String,

View File

@ -1,6 +1,6 @@
[package] [package]
name = "permissive-json-pointer" name = "permissive-json-pointer"
version = "0.28.1" version = "0.29.2"
edition = "2021" edition = "2021"
description = "A permissive json pointer" description = "A permissive json pointer"
readme = "README.md" readme = "README.md"

View File

@ -49,7 +49,7 @@ fn contained_in(selector: &str, key: &str) -> bool {
/// map_leaf_values( /// map_leaf_values(
/// value.as_object_mut().unwrap(), /// value.as_object_mut().unwrap(),
/// ["jean.race.name"], /// ["jean.race.name"],
/// |key, value| match (value, dbg!(key)) { /// |key, value| match (value, key) {
/// (Value::String(name), "jean.race.name") => *name = "patou".to_string(), /// (Value::String(name), "jean.race.name") => *name = "patou".to_string(),
/// _ => unreachable!(), /// _ => unreachable!(),
/// }, /// },
@ -729,7 +729,7 @@ mod tests {
map_leaf_values( map_leaf_values(
value.as_object_mut().unwrap(), value.as_object_mut().unwrap(),
["jean.race.name"], ["jean.race.name"],
|key, value| match (value, dbg!(key)) { |key, value| match (value, key) {
(Value::String(name), "jean.race.name") => *name = S("patou"), (Value::String(name), "jean.race.name") => *name = S("patou"),
_ => unreachable!(), _ => unreachable!(),
}, },