mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-07-19 04:50:37 +00:00
Compare commits
140 Commits
Author | SHA1 | Date | |
---|---|---|---|
fcc154da1c | |||
00d1200704 | |||
7cc096e0a2 | |||
58eaf78dc4 | |||
3be2281483 | |||
cc06d96993 | |||
93c7e700bc | |||
97c6757fc7 | |||
276d3f8e22 | |||
4869a88ae2 | |||
ae88bc31bc | |||
8aed1d96c5 | |||
c93949474c | |||
8cf19f1c6b | |||
a82ecb3cef | |||
04c2b37d82 | |||
ab3e8d6537 | |||
fd185a5e6b | |||
d9678f0040 | |||
840217b111 | |||
9605a2cd88 | |||
0f86ccc035 | |||
b3b73e2276 | |||
f241c999ad | |||
d4d2a2303a | |||
c8832409ad | |||
98f76aa952 | |||
4236632af6 | |||
e2c98244ec | |||
c1cf67c008 | |||
4abea919b2 | |||
d60aa722c0 | |||
055368acd8 | |||
7f2e5d091a | |||
c69ae8154f | |||
cd95b243bb | |||
1f1cb1f501 | |||
530738cfe9 | |||
878dd6912e | |||
5f0f699f37 | |||
ca13900699 | |||
cc97889b37 | |||
45ded0498b | |||
d01a3944c1 | |||
a0caf0d6d7 | |||
e22debb994 | |||
1b8df0ed8b | |||
3286a5213c | |||
394976d330 | |||
b95acbece0 | |||
c94f4dff71 | |||
e6465f4ea1 | |||
2b3c91aabd | |||
e97e13ce9f | |||
39e2b73718 | |||
a90facaa41 | |||
5527457655 | |||
076e781810 | |||
750d336018 | |||
e8251ad45b | |||
963ca1e2c7 | |||
12a6c7d54d | |||
2d0fc3f9d3 | |||
e554784527 | |||
2cb43fa638 | |||
66d5309a51 | |||
7eeedec7eb | |||
4b798c71ae | |||
685016bfec | |||
d30e5f6231 | |||
e854d67a55 | |||
23a89732a5 | |||
3a1f41ebdb | |||
f873761a27 | |||
ebf620c7f9 | |||
8b92bc3421 | |||
70a5aa61e9 | |||
a76169042f | |||
c9c3cfcee9 | |||
2e60ac5359 | |||
2dd7751e09 | |||
26bdabcdec | |||
fc8c7ed77e | |||
521c96354f | |||
9788779894 | |||
9b965764ab | |||
9a5a543311 | |||
b18fb868e8 | |||
c734af55c0 | |||
810b328ad2 | |||
0a8039d8d8 | |||
e51704c09a | |||
623a9012d5 | |||
b9a185634f | |||
b46889b5f0 | |||
ef9a0c07db | |||
3a6f3947c9 | |||
5c5f41d755 | |||
6803a8fad0 | |||
8e4b362e4d | |||
acb5e624c6 | |||
a98949ff1d | |||
f355280250 | |||
cee8d6a8d9 | |||
27326ea069 | |||
7bbe5aca5b | |||
1c4afe6d0f | |||
2d8f9a9849 | |||
3f41681b18 | |||
64791815fa | |||
8a36571a74 | |||
d18e775bec | |||
78381f1818 | |||
7f33a01ae1 | |||
d07d14d33a | |||
540d7886ab | |||
5a5d10af52 | |||
f95d077ef8 | |||
05dd99936f | |||
c086625773 | |||
dc17bebf4a | |||
026464b2e4 | |||
bd42158a70 | |||
df066f4321 | |||
69832e8c70 | |||
95eb6ad09a | |||
f3fc0bed45 | |||
5dd6b697b9 | |||
b7d170c7d1 | |||
7541172d12 | |||
85bf5d113c | |||
89fd397903 | |||
d8392f2f18 | |||
36b74f0efe | |||
68c0a36b00 | |||
a127b72a74 | |||
5782fb9e52 | |||
20319f7974 | |||
c4087e2ec2 | |||
b1d1f2f627 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
target
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
11
.github/workflows/README.md
vendored
Normal file
11
.github/workflows/README.md
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# GitHub actions workflow for MeiliDB
|
||||
|
||||
> **Note:**
|
||||
|
||||
> - We do not use [cache](https://github.com/actions/cache) yet but we could use it to speed up CI
|
||||
|
||||
## Workflow
|
||||
|
||||
- On each pull request, we are triggering `cargo test`.
|
||||
- On each commit on master, we are building the latest docker image.
|
||||
- On each tag, we are building the tagged docker image and the binaries for MacOS & Ubuntu.
|
36
.github/workflows/publish-binaries.yml
vendored
Normal file
36
.github/workflows/publish-binaries.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Publish binaries to GitHub release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish for ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact_name: meilisearch
|
||||
asset_name: meilisearch-linux-amd64
|
||||
- os: macos-latest
|
||||
artifact_name: meilisearch
|
||||
asset_name: meilisearch-macos-amd64
|
||||
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@master
|
||||
with:
|
||||
rust-version: stable
|
||||
- uses: actions/checkout@v1
|
||||
- name: Build
|
||||
run: cargo build --release --locked
|
||||
- name: Upload binaries to release
|
||||
uses: svenstaro/upload-release-action@v1-release
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: target/release/${{ matrix.artifact_name }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
19
.github/workflows/publish-docker-latest.yml
vendored
Normal file
19
.github/workflows/publish-docker-latest.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
name: Publish latest image to Docker Hub
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
with:
|
||||
name: getmeili/meilisearch
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
20
.github/workflows/publish-docker-tag.yml
vendored
Normal file
20
.github/workflows/publish-docker-tag.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
name: Publish tagged image to Docker Hub
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
with:
|
||||
name: getmeili/meilisearch
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
tags: true
|
20
.github/workflows/test.yml
vendored
Normal file
20
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
on: [pull_request]
|
||||
|
||||
name: Cargo test
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: MeiliSearch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
**/*.csv
|
||||
**/*.json_lines
|
||||
**/*.rs.bk
|
||||
|
2780
Cargo.lock
generated
Normal file
2780
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,10 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"meilidb-core",
|
||||
"meilidb-http",
|
||||
"meilidb-schema",
|
||||
"meilidb-tokenizer",
|
||||
"meilisearch-core",
|
||||
"meilisearch-http",
|
||||
"meilisearch-schema",
|
||||
"meilisearch-tokenizer",
|
||||
"meilisearch-types",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
# Compile
|
||||
FROM alpine:3.10 AS compiler
|
||||
|
||||
RUN apk update --quiet
|
||||
RUN apk add curl
|
||||
RUN apk add build-base
|
||||
RUN apk add libressl-dev
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
|
||||
WORKDIR /meilisearch
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV RUSTFLAGS="-C target-feature=-crt-static"
|
||||
|
||||
RUN $HOME/.cargo/bin/cargo build --release
|
||||
|
||||
# Run
|
||||
FROM alpine:3.10
|
||||
|
||||
RUN apk update --quiet
|
||||
RUN apk add libressl
|
||||
RUN apk add build-base
|
||||
|
||||
COPY --from=compiler /meilisearch/target/release/meilisearch .
|
||||
|
||||
ENV MEILI_HTTP_ADDR 0.0.0.0:8080
|
||||
CMD ./meilisearch
|
26
LICENSE
26
LICENSE
@ -1,13 +1,21 @@
|
||||
“Commons Clause” License Condition v1.0
|
||||
MIT License
|
||||
|
||||
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
Software: MeiliDB
|
||||
|
||||
License: MIT
|
||||
|
||||
Licensor: MEILI SAS
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
136
README.md
136
README.md
@ -1,41 +1,117 @@
|
||||
# MeiliDB
|
||||
# MeiliSearch
|
||||
|
||||
[](https://dev.azure.com/thomas0884/thomas/_build/latest?definitionId=1&branchName=master)
|
||||
[](https://deps.rs/repo/github/meilisearch/MeiliDB)
|
||||
[](https://github.com/meilisearch/MeiliSearch/actions)
|
||||
[](https://deps.rs/repo/github/meilisearch/MeiliSearch)
|
||||
[](https://commonsclause.com/)
|
||||
|
||||
A _full-text search database_ based on the fast [LMDB key-value store](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database).
|
||||
Ultra relevant and instant full-text search API.
|
||||
|
||||
MeiliSearch is a powerful, fast, open-source, easy to use and deploy search engine. The search and indexation are fully customizable and handles features like typo-tolerance, filters, and ranking.
|
||||
|
||||
## Features
|
||||
|
||||
- Provides [6 default ranking criteria](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/criterion/mod.rs#L107-L113) used to [bucket sort](https://en.wikipedia.org/wiki/Bucket_sort) documents
|
||||
- Accepts [custom criteria](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/criterion/mod.rs#L24-L33) and can apply them in any custom order
|
||||
- Support [ranged queries](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/query_builder.rs#L283), useful for paginating results
|
||||
- Can [distinct](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/query_builder.rs#L265-L270) and [filter](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/query_builder.rs#L246-L259) returned documents based on context defined rules
|
||||
- Searches for [concatenated](https://github.com/meilisearch/MeiliDB/pull/164) and [splitted query words](https://github.com/meilisearch/MeiliDB/pull/232) to improve the search quality.
|
||||
- Can store complete documents or only [user schema specified fields](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-schema/src/lib.rs#L265-L279)
|
||||
- The [default tokenizer](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-tokenizer/src/lib.rs) can index latin and kanji based languages
|
||||
- Returns [the matching text areas](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/lib.rs#L66-L88), useful to highlight matched words in results
|
||||
- Accepts query time search config like the [searchable attributes](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/query_builder.rs#L272-L275)
|
||||
- Supports [runtime incremental indexing](https://github.com/meilisearch/MeiliDB/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilidb-core/src/store/mod.rs#L143-L173)
|
||||
- Provides [6 default ranking criteria](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/criterion/mod.rs#L107-L113) used to [bucket sort](https://en.wikipedia.org/wiki/Bucket_sort) documents
|
||||
- Accepts [custom criteria](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/criterion/mod.rs#L24-L33) and can apply them in any custom order
|
||||
- Support [ranged queries](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/query_builder.rs#L283), useful for paginating results
|
||||
- Can [distinct](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/query_builder.rs#L265-L270) and [filter](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/query_builder.rs#L246-L259) returned documents based on context defined rules
|
||||
- Searches for [concatenated](https://github.com/meilisearch/MeiliSearch/pull/164) and [splitted query words](https://github.com/meilisearch/MeiliSearch/pull/232) to improve the search quality.
|
||||
- Can store complete documents or only [user schema specified fields](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-schema/src/lib.rs#L265-L279)
|
||||
- The [default tokenizer](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-tokenizer/src/lib.rs) can index latin and kanji based languages
|
||||
- Returns [the matching text areas](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/lib.rs#L66-L88), useful to highlight matched words in results
|
||||
- Accepts query time search config like the [searchable attributes](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/query_builder.rs#L272-L275)
|
||||
- Supports [runtime incremental indexing](https://github.com/meilisearch/MeiliSearch/blob/dc5c42821e1340e96cb90a3da472264624a26326/meilisearch-core/src/store/mod.rs#L143-L173)
|
||||
|
||||
|
||||
|
||||
It uses [LMDB](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database) as the internal key-value store. The key-value store allows us to handle updates and queries with small memory and CPU overheads. The whole ranking system is [data oriented](https://github.com/meilisearch/MeiliDB/issues/82) and provides great performances.
|
||||
It uses [LMDB](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database) as the internal key-value store. The key-value store allows us to handle updates and queries with small memory and CPU overheads. The whole ranking system is [data oriented](https://github.com/meilisearch/MeiliSearch/issues/82) and provides great performances.
|
||||
|
||||
You can [read the deep dive](deep-dive.md) if you want more information on the engine, it describes the whole process of generating updates and handling queries or you can take a look at the [typos and ranking rules](typos-ranking-rules.md) if you want to know the default rules used to sort the documents.
|
||||
|
||||
We will be proud if you submit issues and pull requests. You can help to grow this project and start contributing by checking [issues tagged "good-first-issue"](https://github.com/meilisearch/MeiliDB/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). It is a good start!
|
||||
We will be glad if you submit issues and pull requests. You can help to grow this project and start contributing by checking [issues tagged "good-first-issue"](https://github.com/meilisearch/MeiliSearch/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). It is a good start!
|
||||
|
||||
The project is only a library yet. It means that there is no binary provided yet. To get started, you can check the examples wich are made to work with the data located in the `datasets/` folder.
|
||||
[](https://crates.meilisearch.com)
|
||||
|
||||
MeiliDB will be a binary in a near future so you will be able to use it as a database out-of-the-box. We should be able to query it using HTTP. This is our current goal, [see the milestones](https://github.com/meilisearch/MeiliDB/milestones). In the end, the binary will be a bunch of network protocols and wrappers around the library - which will also be published on [crates.io](https://crates.io). Both the binary and the library will follow the same update cycle.
|
||||
> Meili helps the Rust community find crates on [crates.meilisearch.com](https://crates.meilisearch.com)
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
You can deploy your own instant, relevant and typo-tolerant MeiliSearch search engine by yourself too.
|
||||
Something similar to the demo above can be achieve by following these little three steps first.
|
||||
You will need to create your own web front display to make it pretty though.
|
||||
|
||||
### Deploy the Server
|
||||
|
||||
If you have not installed Rust and its package manager `cargo` yet, go to [the installation page](https://www.rust-lang.org/tools/install).<br/>
|
||||
You can deploy the server on your own machine, it will listen to HTTP requests on the 8080 port by default.
|
||||
|
||||
```bash
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
For more logs during the execution, run:
|
||||
```bash
|
||||
RUST_LOG=info cargo run --release
|
||||
```
|
||||
|
||||
### Create an Index and Upload Some Documents
|
||||
|
||||
MeiliSearch can serve multiple indexes, with different kinds of documents,
|
||||
therefore, it is required to create the index before sending documents to it.
|
||||
|
||||
```bash
|
||||
curl -i -X POST 'http://127.0.0.1:8080/indexes' --data '{ "name": "Movies", "uid": "movies" }'
|
||||
```
|
||||
|
||||
Now that the server knows about our brand new index, we can send it data.
|
||||
We provided you a little dataset, it is available in the `datasets/` directory.
|
||||
|
||||
```bash
|
||||
curl -i -X POST 'http://127.0.0.1:8080/indexes/movies/documents' \
|
||||
--header 'content-type: application/json' \
|
||||
--data @datasets/movies/movies.json
|
||||
```
|
||||
|
||||
### Search for Documents
|
||||
|
||||
The search engine is now aware of our documents and can serve those via our HTTP server again.
|
||||
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:8080/indexes/movies/search?q=botman'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": "29751",
|
||||
"title": "Batman Unmasked: The Psychology of the Dark Knight",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/jjHu128XLARc2k4cJrblAvZe0HE.jpg",
|
||||
"overview": "Delve into the world of Batman and the vigilante justice tha",
|
||||
"release_date": "2008-07-15"
|
||||
},
|
||||
{
|
||||
"id": "471474",
|
||||
"title": "Batman: Gotham by Gaslight",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/7souLi5zqQCnpZVghaXv0Wowi0y.jpg",
|
||||
"overview": "ve Victorian Age Gotham City, Batman begins his war on crime",
|
||||
"release_date": "2018-01-12"
|
||||
}
|
||||
],
|
||||
"offset": 0,
|
||||
"limit": 2,
|
||||
"processingTimeMs": 1,
|
||||
"query": "botman"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Performances
|
||||
|
||||
With a database composed of _100 353_ documents with _352_ attributes each and _3_ of them indexed.
|
||||
With a dataset composed of _100 353_ documents with _352_ attributes each and _3_ of them indexed.
|
||||
So more than _300 000_ fields indexed for _35 million_ stored we can handle more than _2.8k req/sec_ with an average response time of _9 ms_ on an Intel i7-7700 (8) @ 4.2GHz.
|
||||
|
||||
Requests are made using [wrk](https://github.com/wg/wrk) and scripted to simulate real users queries.
|
||||
@ -51,20 +127,24 @@ Requests/sec: 2806.46
|
||||
Transfer/sec: 759.17KB
|
||||
```
|
||||
|
||||
We also indexed a dataset containing something like _12 millions_ cities names in _24 minutes_ on a machine with _8 cores_, _64 GB of RAM_ and a _300 GB NMVe_ SSD.<br/>
|
||||
The resulting database was _16 GB_ and search results were between _30 ms_ and _4 seconds_ for short prefix queries.
|
||||
|
||||
### Notes
|
||||
|
||||
With Rust 1.32 the allocator has been [changed to use the system allocator](https://blog.rust-lang.org/2019/01/17/Rust-1.32.0.html#jemalloc-is-removed-by-default).
|
||||
We have seen much better performances when [using jemalloc as the global allocator](https://github.com/alexcrichton/jemallocator#documentation).
|
||||
|
||||
## Usage and examples
|
||||
## Usage and Examples
|
||||
|
||||
Currently MeiliDB do not provide an http server but you can run the example binary.
|
||||
MeiliSearch also provides an example binary that is mostly used for features testing.
|
||||
Notice that the example binary is faster to index data as it does read direct CSV files and not JSON HTTP payloads.
|
||||
|
||||
The _index_ subcommand has been made to create an index and inject documents into it. Using the command line below, the index will be named _movies_ and the _19 700_ movies of the `datasets/` will be injected in MeiliDB.
|
||||
The _index_ subcommand has been made to create an index and inject documents into it. Using the command line below, the index will be named _movies_ and the _19 700_ movies of the `datasets/` will be injected in MeiliSearch.
|
||||
|
||||
```bash
|
||||
cargo run --release --example from_file -- \
|
||||
index example.mdb datasets/movies/data.csv \
|
||||
index example.mdb datasets/movies/movies.csv \
|
||||
--schema datasets/movies/schema.toml
|
||||
```
|
||||
|
||||
@ -72,8 +152,14 @@ Once the first command is done, you can query the freshly created _movies_ index
|
||||
|
||||
```bash
|
||||
cargo run --release --example from_file -- \
|
||||
search example.mdb
|
||||
--number 4 \
|
||||
search example.mdb \
|
||||
--number-results 4 \
|
||||
--filter '!adult' \
|
||||
id popularity adult original_title
|
||||
```
|
||||
|
||||
### Analytic Events
|
||||
|
||||
We send events to our Amplitude instance to be aware of the number of people who use MeiliSearch.<br/>
|
||||
We only send the platform on which the server runs once by day. No other information is sent.<br/>
|
||||
If you do not want us to send events, you can disable these analytics by using the `MEILI_NO_ANALYTICS` env variable.
|
||||
|
@ -1,52 +0,0 @@
|
||||
---
|
||||
trigger:
|
||||
branches:
|
||||
include: [ master ]
|
||||
|
||||
pr: [ master ]
|
||||
|
||||
jobs:
|
||||
- job: test
|
||||
pool:
|
||||
vmImage: 'Ubuntu 16.04'
|
||||
container: tpayet/chiquitita:latest
|
||||
steps:
|
||||
- script: |
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly
|
||||
$HOME/.cargo/bin/rustup component add rustfmt
|
||||
displayName: 'Install rustc and components'
|
||||
- script: |
|
||||
$HOME/.cargo/bin/cargo check
|
||||
displayName: 'Check MeiliDB'
|
||||
- script: |
|
||||
$HOME/.cargo/bin/cargo test
|
||||
displayName: 'Test MeiliDB'
|
||||
- script: |
|
||||
$HOME/.cargo/bin/cargo fmt --all -- --check
|
||||
displayName: 'Fmt MeiliDB'
|
||||
|
||||
- job: build
|
||||
dependsOn:
|
||||
- test
|
||||
condition: succeeded()
|
||||
pool:
|
||||
vmImage: 'Ubuntu 16.04'
|
||||
container: tpayet/chiquitita:latest
|
||||
steps:
|
||||
- script: |
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly
|
||||
$HOME/.cargo/bin/rustup component add rustfmt
|
||||
displayName: 'Install rustc and components'
|
||||
- script: |
|
||||
$HOME/.cargo/bin/cargo build --release
|
||||
displayName: 'Build MeiliDB'
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: '$(System.DefaultWorkingDirectory)/target/release/libmeilidb.rlib'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
displayName: 'Copy build'
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
artifactName: libmeilidb.rlib
|
||||
displayName: 'Upload artifacts'
|
||||
|
@ -1 +1 @@
|
||||
_datas in movies.csv are from https://www.themoviedb.org/_
|
||||
_datas in movies.csv are from https://www.themoviedb.org/_
|
||||
|
19655
datasets/movies/movies.json
Normal file
19655
datasets/movies/movies.json
Normal file
File diff suppressed because it is too large
Load Diff
36
deep-dive.md
36
deep-dive.md
@ -1,8 +1,8 @@
|
||||
# A deep dive in MeiliDB
|
||||
# A deep dive in MeiliSearch
|
||||
|
||||
On the 15 of May 2019.
|
||||
|
||||
MeiliDB is a full text search engine based on a final state transducer named [fst](https://github.com/BurntSushi/fst) and a key-value store named [sled](https://github.com/spacejam/sled). The goal of a search engine is to store data and to respond to queries as accurate and fast as possible. To achieve this it must save the matching words in an [inverted index](https://en.wikipedia.org/wiki/Inverted_index).
|
||||
MeiliSearch is a full text search engine based on a final state transducer named [fst](https://github.com/BurntSushi/fst) and a key-value store named [sled](https://github.com/spacejam/sled). The goal of a search engine is to store data and to respond to queries as accurate and fast as possible. To achieve this it must save the matching words in an [inverted index](https://en.wikipedia.org/wiki/Inverted_index).
|
||||
|
||||
<!-- MarkdownTOC autolink="true" -->
|
||||
|
||||
@ -22,7 +22,7 @@ MeiliDB is a full text search engine based on a final state transducer named [fs
|
||||
|
||||
## Where is the data stored?
|
||||
|
||||
MeiliDB is entirely backed by a key-value store like any good database (i.e. Postgres, MySQL). This brings a great flexibility in the way documents can be stored and updates handled along time.
|
||||
MeiliSearch is entirely backed by a key-value store like any good database (i.e. Postgres, MySQL). This brings a great flexibility in the way documents can be stored and updates handled along time.
|
||||
|
||||
[sled will brings some](https://github.com/spacejam/sled/tree/434533332a3f485e6d2e467023be0a0b55d3a1af#plans) of the [A.C.I.D. properties](https://en.wikipedia.org/wiki/ACID_(computer_science)) to help us be sure the saved data is consistent.
|
||||
|
||||
@ -34,7 +34,7 @@ It contain the inverted word index, the schema and the documents fields.
|
||||
|
||||
### The inverted word index
|
||||
|
||||
[The inverted word index](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-data/src/database/words_index.rs) is a sled Tree dedicated to store and give access to all documents that contains a specific word. The information stored under the word is simply a big ordered array of where in the document the word has been found. In other word, a big list of [`DocIndex`](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-core/src/lib.rs#L35-L51).
|
||||
[The inverted word index](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-data/src/database/words_index.rs) is a sled Tree dedicated to store and give access to all documents that contains a specific word. The information stored under the word is simply a big ordered array of where in the document the word has been found. In other word, a big list of [`DocIndex`](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-core/src/lib.rs#L35-L51).
|
||||
|
||||
#### A final state transducer
|
||||
|
||||
@ -42,27 +42,27 @@ _...also abbreviated fst_
|
||||
|
||||
This is the first entry point of the engine, you can read more about how it work with the beautiful blog post of @BurntSushi, [Index 1,600,000,000 Keys with Automata and Rust](https://blog.burntsushi.net/transducers/).
|
||||
|
||||
To make it short it is a powerful way to store all the words that are present in the indexed documents. You construct it by giving it all the words you want to index. When you want to search in it you can provide any automaton you want, in MeiliDB [a custom levenshtein automaton](https://github.com/tantivy-search/levenshtein-automata/) is used.
|
||||
To make it short it is a powerful way to store all the words that are present in the indexed documents. You construct it by giving it all the words you want to index. When you want to search in it you can provide any automaton you want, in MeiliSearch [a custom levenshtein automaton](https://github.com/tantivy-search/levenshtein-automata/) is used.
|
||||
|
||||
#### Document indexes
|
||||
|
||||
The `fst` will only return the words that match with the search automaton but the goal of the search engine is to retrieve all matches in all the documents when a query is made. You want it to return some sort of position in an attribute in a document, an information about where the given word matched.
|
||||
|
||||
To make it possible we retrieve all of the `DocIndex` corresponding to all the matching words in the fst, we use the [`WordsIndex`](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-data/src/database/words_index.rs#L11-L21) Tree to get the `DocIndexes` corresponding the words.
|
||||
To make it possible we retrieve all of the `DocIndex` corresponding to all the matching words in the fst, we use the [`WordsIndex`](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-data/src/database/words_index.rs#L11-L21) Tree to get the `DocIndexes` corresponding the words.
|
||||
|
||||
### The schema
|
||||
|
||||
The schema is a data structure that represents which documents attributes should be stored and which should be indexed. It is stored under a the [`MainIndex`](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-data/src/database/main_index.rs#L12) Tree and given to MeiliDB only at the creation of an index.
|
||||
The schema is a data structure that represents which documents attributes should be stored and which should be indexed. It is stored under a the [`MainIndex`](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-data/src/database/main_index.rs#L12) Tree and given to MeiliSearch only at the creation of an index.
|
||||
|
||||
Each document attribute is associated to a unique 16 bit number named [`SchemaAttr`](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-data/src/schema.rs#L186).
|
||||
Each document attribute is associated to a unique 16 bit number named [`SchemaAttr`](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-data/src/schema.rs#L186).
|
||||
|
||||
In the future, this schema type could be given along with updates, the database could be able to handled a new schema and reindex the database according to the new one.
|
||||
|
||||
### Document attributes
|
||||
|
||||
When the engine handle a query the result that the requester want is a document, not only the [`Matches`](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-core/src/lib.rs#L62-L88) associated to it, fields of the original document must be returned too.
|
||||
When the engine handle a query the result that the requester want is a document, not only the [`Matches`](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-core/src/lib.rs#L62-L88) associated to it, fields of the original document must be returned too.
|
||||
|
||||
So MeiliDB again uses the power of the underlying key-value store and save the documents attributes marked as _STORE_ in the schema. The dedicated Tree for this information is the [`DocumentsIndex`](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-data/src/database/documents_index.rs#L11).
|
||||
So MeiliSearch again uses the power of the underlying key-value store and save the documents attributes marked as _STORE_ in the schema. The dedicated Tree for this information is the [`DocumentsIndex`](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-data/src/database/documents_index.rs#L11).
|
||||
|
||||
When a document field is saved in the key-value store its value is binary encoded using [message pack](https://github.com/3Hren/msgpack-rust), so a document must be serializable using serde.
|
||||
|
||||
@ -70,26 +70,26 @@ When a document field is saved in the key-value store its value is binary encode
|
||||
|
||||
## How is a request processed?
|
||||
|
||||
Now that we have our inverted index we are able to return results based on a query. In the MeiliDB universe a query is a simple string containing words.
|
||||
Now that we have our inverted index we are able to return results based on a query. In the MeiliSearch universe a query is a simple string containing words.
|
||||
|
||||
### Query lexemes
|
||||
|
||||
The first step to be able to call the underlying structures is to split the query in words, for that we use a [custom tokenizer](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-tokenizer/src/lib.rs#L82-L84). Note that a tokenizer is specialized for a human language, this is the hard part.
|
||||
The first step to be able to call the underlying structures is to split the query in words, for that we use a [custom tokenizer](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-tokenizer/src/lib.rs#L82-L84). Note that a tokenizer is specialized for a human language, this is the hard part.
|
||||
|
||||
### Automatons and query index
|
||||
|
||||
So to query the fst we need an automaton, in MeiliDB we use a [levenshtein automaton](https://en.wikipedia.org/wiki/Levenshtein_automaton), this automaton is constructed using a string and a maximum distance. According to the [Algolia's blog post](https://blog.algolia.com/inside-the-algolia-engine-part-3-query-processing/#algolia%e2%80%99s-way-of-searching-for-alternatives) we [created the DFAs](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-core/src/automaton.rs#L59-L78) with different settings.
|
||||
So to query the fst we need an automaton, in MeiliSearch we use a [levenshtein automaton](https://en.wikipedia.org/wiki/Levenshtein_automaton), this automaton is constructed using a string and a maximum distance. According to the [Algolia's blog post](https://blog.algolia.com/inside-the-algolia-engine-part-3-query-processing/#algolia%e2%80%99s-way-of-searching-for-alternatives) we [created the DFAs](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-core/src/automaton.rs#L59-L78) with different settings.
|
||||
|
||||
Thanks to the power of the fst library [it is possible to union multiple automatons](https://docs.rs/fst/0.3.2/fst/map/struct.OpBuilder.html#method.union) on the same fst set. The `Stream` is able to return all the matching words. We use these words to find the whole list of `DocIndexes` associated.
|
||||
|
||||
With all these informations it is possible [to reconstruct a list of all the `DocIndexes` associated](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-core/src/query_builder.rs#L103-L130) with the words queried.
|
||||
With all these informations it is possible [to reconstruct a list of all the `DocIndexes` associated](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-core/src/query_builder.rs#L103-L130) with the words queried.
|
||||
|
||||
### Sort by criteria
|
||||
|
||||
Now that we are able to get a big list of [DocIndexes](https://github.com/Kerollmops/MeiliDB/blob/550dc1e99224e386516877450320f694947332d4/src/lib.rs#L21-L36) it is not enough to sort them by criteria, we need more informations like the levenshtein distance or the fact that a query word match exactly the word stored in the fst. So [we stuff it a little bit](https://github.com/Kerollmops/MeiliDB/blob/550dc1e99224e386516877450320f694947332d4/src/rank/query_builder.rs#L86-L93), and aggregate all these [Matches](https://github.com/Kerollmops/MeiliDB/blob/550dc1e99224e386516877450320f694947332d4/src/lib.rs#L47-L74) for each document. This way it will be easy to sort a simple vector of document using a bunch of functions.
|
||||
Now that we are able to get a big list of [DocIndexes](https://github.com/Kerollmops/MeiliSearch/blob/550dc1e99224e386516877450320f694947332d4/src/lib.rs#L21-L36) it is not enough to sort them by criteria, we need more informations like the levenshtein distance or the fact that a query word match exactly the word stored in the fst. So [we stuff it a little bit](https://github.com/Kerollmops/MeiliSearch/blob/550dc1e99224e386516877450320f694947332d4/src/rank/query_builder.rs#L86-L93), and aggregate all these [Matches](https://github.com/Kerollmops/MeiliSearch/blob/550dc1e99224e386516877450320f694947332d4/src/lib.rs#L47-L74) for each document. This way it will be easy to sort a simple vector of document using a bunch of functions.
|
||||
|
||||
With this big list of documents and associated matches [we are able to sort only the part of the slice that we want](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-core/src/query_builder.rs#L160-L188) using bucket sorting. [Each criterion](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-core/src/criterion/mod.rs#L95-L101) is evaluated on each subslice without copy, thanks to [GroupByMut](https://docs.rs/slice-group-by/0.2.4/slice_group_by/) which, I hope [will soon be merged](https://github.com/rust-lang/rfcs/pull/2477).
|
||||
With this big list of documents and associated matches [we are able to sort only the part of the slice that we want](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-core/src/query_builder.rs#L160-L188) using bucket sorting. [Each criterion](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-core/src/criterion/mod.rs#L95-L101) is evaluated on each subslice without copy, thanks to [GroupByMut](https://docs.rs/slice-group-by/0.2.4/slice_group_by/) which, I hope [will soon be merged](https://github.com/rust-lang/rfcs/pull/2477).
|
||||
|
||||
Note that it is possible to customize the criteria used by using the `QueryBuilder::with_criteria` constructor, this way you can implement some custom ranking based on the document attributes using the appropriate structure and the [`document` method](https://github.com/meilisearch/MeiliDB/blob/3db823de002243004612e36a19b4578d800dab97/meilidb-data/src/database/index.rs#L86).
|
||||
Note that it is possible to customize the criteria used by using the `QueryBuilder::with_criteria` constructor, this way you can implement some custom ranking based on the document attributes using the appropriate structure and the [`document` method](https://github.com/meilisearch/MeiliSearch/blob/3db823de002243004612e36a19b4578d800dab97/meilisearch-data/src/database/index.rs#L86).
|
||||
|
||||
At this point, MeiliDB work is over 🎉
|
||||
At this point, MeiliSearch work is over 🎉
|
||||
|
@ -1,362 +0,0 @@
|
||||
use std::collections::hash_map::{Entry, HashMap};
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::{fs, thread};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use heed::types::{Str, Unit};
|
||||
use heed::{CompactionOption, Result as ZResult};
|
||||
use log::debug;
|
||||
|
||||
use crate::{store, update, Index, MResult};
|
||||
|
||||
pub type BoxUpdateFn = Box<dyn Fn(update::ProcessedUpdateResult) + Send + Sync + 'static>;
|
||||
type ArcSwapFn = arc_swap::ArcSwapOption<BoxUpdateFn>;
|
||||
|
||||
pub struct Database {
|
||||
pub env: heed::Env,
|
||||
common_store: heed::PolyDatabase,
|
||||
indexes_store: heed::Database<Str, Unit>,
|
||||
indexes: RwLock<HashMap<String, (Index, Arc<ArcSwapFn>, thread::JoinHandle<()>)>>,
|
||||
}
|
||||
|
||||
macro_rules! r#break_try {
|
||||
($expr:expr, $msg:tt) => {
|
||||
match $expr {
|
||||
core::result::Result::Ok(val) => val,
|
||||
core::result::Result::Err(err) => {
|
||||
log::error!(concat!($msg, ": {}"), err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn update_awaiter(receiver: Receiver<()>, env: heed::Env, update_fn: Arc<ArcSwapFn>, index: Index) {
|
||||
for () in receiver {
|
||||
// consume all updates in order (oldest first)
|
||||
loop {
|
||||
// instantiate a main/parent transaction
|
||||
let mut writer = break_try!(env.write_txn(), "LMDB write transaction begin failed");
|
||||
|
||||
// retrieve the update that needs to be processed
|
||||
let result = index.updates.pop_front(&mut writer);
|
||||
let (update_id, update) = match break_try!(result, "pop front update failed") {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
debug!("no more updates");
|
||||
writer.abort();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// instantiate a nested transaction
|
||||
let result = env.nested_write_txn(&mut writer);
|
||||
let mut nested_writer = break_try!(result, "LMDB nested write transaction failed");
|
||||
|
||||
// try to apply the update to the database using the nested transaction
|
||||
let result = update::update_task(&mut nested_writer, index.clone(), update_id, update);
|
||||
let status = break_try!(result, "update task failed");
|
||||
|
||||
// commit the nested transaction if the update was successful, abort it otherwise
|
||||
if status.result.is_ok() {
|
||||
break_try!(nested_writer.commit(), "commit nested transaction failed");
|
||||
} else {
|
||||
nested_writer.abort()
|
||||
}
|
||||
|
||||
// write the result of the update in the updates-results store
|
||||
let updates_results = index.updates_results;
|
||||
let result = updates_results.put_update_result(&mut writer, update_id, &status);
|
||||
|
||||
// always commit the main/parent transaction, even if the update was unsuccessful
|
||||
break_try!(result, "update result store commit failed");
|
||||
break_try!(writer.commit(), "update parent transaction failed");
|
||||
|
||||
// call the user callback when the update and the result are written consistently
|
||||
if let Some(ref callback) = *update_fn.load() {
|
||||
(callback)(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn open_or_create(path: impl AsRef<Path>) -> MResult<Database> {
|
||||
fs::create_dir_all(path.as_ref())?;
|
||||
|
||||
let env = heed::EnvOpenOptions::new()
|
||||
.map_size(10 * 1024 * 1024 * 1024) // 10GB
|
||||
.max_dbs(3000)
|
||||
.open(path)?;
|
||||
|
||||
let common_store = env.create_poly_database(Some("common"))?;
|
||||
let indexes_store = env.create_database::<Str, Unit>(Some("indexes"))?;
|
||||
|
||||
// list all indexes that needs to be opened
|
||||
let mut must_open = Vec::new();
|
||||
let reader = env.read_txn()?;
|
||||
for result in indexes_store.iter(&reader)? {
|
||||
let (index_name, _) = result?;
|
||||
must_open.push(index_name.to_owned());
|
||||
}
|
||||
|
||||
reader.abort();
|
||||
|
||||
// open the previously aggregated indexes
|
||||
let mut indexes = HashMap::new();
|
||||
for index_name in must_open {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(100);
|
||||
let index = match store::open(&env, &index_name, sender.clone())? {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
log::warn!(
|
||||
"the index {} doesn't exist or has not all the databases",
|
||||
index_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let update_fn = Arc::new(ArcSwapFn::empty());
|
||||
|
||||
let env_clone = env.clone();
|
||||
let index_clone = index.clone();
|
||||
let update_fn_clone = update_fn.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
update_awaiter(receiver, env_clone, update_fn_clone, index_clone)
|
||||
});
|
||||
|
||||
// send an update notification to make sure that
|
||||
// possible pre-boot updates are consumed
|
||||
sender.send(()).unwrap();
|
||||
|
||||
let result = indexes.insert(index_name, (index, update_fn, handle));
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"The index should not have been already open"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Database {
|
||||
env,
|
||||
common_store,
|
||||
indexes_store,
|
||||
indexes: RwLock::new(indexes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_index(&self, name: impl AsRef<str>) -> Option<Index> {
|
||||
let indexes_lock = self.indexes.read().unwrap();
|
||||
match indexes_lock.get(name.as_ref()) {
|
||||
Some((index, ..)) => Some(index.clone()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_index(&self, name: impl AsRef<str>) -> MResult<Index> {
|
||||
let name = name.as_ref();
|
||||
let mut indexes_lock = self.indexes.write().unwrap();
|
||||
|
||||
match indexes_lock.entry(name.to_owned()) {
|
||||
Entry::Occupied(_) => Err(crate::Error::IndexAlreadyExists),
|
||||
Entry::Vacant(entry) => {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(100);
|
||||
let index = store::create(&self.env, name, sender)?;
|
||||
|
||||
let mut writer = self.env.write_txn()?;
|
||||
self.indexes_store.put(&mut writer, name, &())?;
|
||||
|
||||
let env_clone = self.env.clone();
|
||||
let index_clone = index.clone();
|
||||
|
||||
let no_update_fn = Arc::new(ArcSwapFn::empty());
|
||||
let no_update_fn_clone = no_update_fn.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
update_awaiter(receiver, env_clone, no_update_fn_clone, index_clone)
|
||||
});
|
||||
|
||||
writer.commit()?;
|
||||
entry.insert((index.clone(), no_update_fn, handle));
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_update_callback(&self, name: impl AsRef<str>, update_fn: BoxUpdateFn) -> bool {
|
||||
let indexes_lock = self.indexes.read().unwrap();
|
||||
match indexes_lock.get(name.as_ref()) {
|
||||
Some((_, current_update_fn, _)) => {
|
||||
let update_fn = Some(Arc::new(update_fn));
|
||||
current_update_fn.swap(update_fn);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unset_update_callback(&self, name: impl AsRef<str>) -> bool {
|
||||
let indexes_lock = self.indexes.read().unwrap();
|
||||
match indexes_lock.get(name.as_ref()) {
|
||||
Some((_, current_update_fn, _)) => {
|
||||
current_update_fn.swap(None);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_and_compact_to_path<P: AsRef<Path>>(&self, path: P) -> ZResult<File> {
|
||||
self.env.copy_to_path(path, CompactionOption::Enabled)
|
||||
}
|
||||
|
||||
pub fn indexes_names(&self) -> MResult<Vec<String>> {
|
||||
let indexes = self.indexes.read().unwrap();
|
||||
Ok(indexes.keys().cloned().collect())
|
||||
}
|
||||
|
||||
pub fn common_store(&self) -> heed::PolyDatabase {
|
||||
self.common_store
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::update::{ProcessedUpdateResult, UpdateStatus};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn valid_updates() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let database = Database::open_or_create(dir.path()).unwrap();
|
||||
let env = &database.env;
|
||||
|
||||
let (sender, receiver) = mpsc::sync_channel(100);
|
||||
let update_fn = move |update: ProcessedUpdateResult| sender.send(update.update_id).unwrap();
|
||||
let index = database.create_index("test").unwrap();
|
||||
|
||||
let done = database.set_update_callback("test", Box::new(update_fn));
|
||||
assert!(done, "could not set the index update function");
|
||||
|
||||
let schema = {
|
||||
let data = r#"
|
||||
identifier = "id"
|
||||
|
||||
[attributes."name"]
|
||||
displayed = true
|
||||
indexed = true
|
||||
|
||||
[attributes."description"]
|
||||
displayed = true
|
||||
indexed = true
|
||||
"#;
|
||||
toml::from_str(data).unwrap()
|
||||
};
|
||||
|
||||
let mut writer = env.write_txn().unwrap();
|
||||
let _update_id = index.schema_update(&mut writer, schema).unwrap();
|
||||
|
||||
// don't forget to commit...
|
||||
writer.commit().unwrap();
|
||||
|
||||
let mut additions = index.documents_addition();
|
||||
|
||||
let doc1 = serde_json::json!({
|
||||
"id": 123,
|
||||
"name": "Marvin",
|
||||
"description": "My name is Marvin",
|
||||
});
|
||||
|
||||
let doc2 = serde_json::json!({
|
||||
"id": 234,
|
||||
"name": "Kevin",
|
||||
"description": "My name is Kevin",
|
||||
});
|
||||
|
||||
additions.update_document(doc1);
|
||||
additions.update_document(doc2);
|
||||
|
||||
let mut writer = env.write_txn().unwrap();
|
||||
let update_id = additions.finalize(&mut writer).unwrap();
|
||||
|
||||
// don't forget to commit...
|
||||
writer.commit().unwrap();
|
||||
|
||||
// block until the transaction is processed
|
||||
let _ = receiver.into_iter().find(|id| *id == update_id);
|
||||
|
||||
let reader = env.read_txn().unwrap();
|
||||
let result = index.update_status(&reader, update_id).unwrap();
|
||||
assert_matches!(result, UpdateStatus::Processed(status) if status.result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_updates() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let database = Database::open_or_create(dir.path()).unwrap();
|
||||
let env = &database.env;
|
||||
|
||||
let (sender, receiver) = mpsc::sync_channel(100);
|
||||
let update_fn = move |update: ProcessedUpdateResult| sender.send(update.update_id).unwrap();
|
||||
let index = database.create_index("test").unwrap();
|
||||
|
||||
let done = database.set_update_callback("test", Box::new(update_fn));
|
||||
assert!(done, "could not set the index update function");
|
||||
|
||||
let schema = {
|
||||
let data = r#"
|
||||
identifier = "id"
|
||||
|
||||
[attributes."name"]
|
||||
displayed = true
|
||||
indexed = true
|
||||
|
||||
[attributes."description"]
|
||||
displayed = true
|
||||
indexed = true
|
||||
"#;
|
||||
toml::from_str(data).unwrap()
|
||||
};
|
||||
|
||||
let mut writer = env.write_txn().unwrap();
|
||||
let _update_id = index.schema_update(&mut writer, schema).unwrap();
|
||||
|
||||
// don't forget to commit...
|
||||
writer.commit().unwrap();
|
||||
|
||||
let mut additions = index.documents_addition();
|
||||
|
||||
let doc1 = serde_json::json!({
|
||||
"id": 123,
|
||||
"name": "Marvin",
|
||||
"description": "My name is Marvin",
|
||||
});
|
||||
|
||||
let doc2 = serde_json::json!({
|
||||
"name": "Kevin",
|
||||
"description": "My name is Kevin",
|
||||
});
|
||||
|
||||
additions.update_document(doc1);
|
||||
additions.update_document(doc2);
|
||||
|
||||
let mut writer = env.write_txn().unwrap();
|
||||
let update_id = additions.finalize(&mut writer).unwrap();
|
||||
|
||||
// don't forget to commit...
|
||||
writer.commit().unwrap();
|
||||
|
||||
// block until the transaction is processed
|
||||
let _ = receiver.into_iter().find(|id| *id == update_id);
|
||||
|
||||
let reader = env.read_txn().unwrap();
|
||||
let result = index.update_status(&reader, update_id).unwrap();
|
||||
assert_matches!(result, UpdateStatus::Processed(status) if status.result.is_err());
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use heed::types::{SerdeBincode, Str};
|
||||
use log::*;
|
||||
use meilidb_core::{Database, MResult};
|
||||
use sysinfo::Pid;
|
||||
|
||||
use crate::option::Opt;
|
||||
use crate::routes::index::index_update_callback;
|
||||
|
||||
pub type FreqsMap = HashMap<String, usize>;
|
||||
type SerdeFreqsMap = SerdeBincode<FreqsMap>;
|
||||
type SerdeDatetime = SerdeBincode<DateTime<Utc>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Data {
|
||||
inner: Arc<DataInner>,
|
||||
}
|
||||
|
||||
impl Deref for Data {
|
||||
type Target = DataInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DataInner {
|
||||
pub db: Arc<Database>,
|
||||
pub db_path: String,
|
||||
pub admin_token: Option<String>,
|
||||
pub server_pid: Pid,
|
||||
pub accept_updates: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl DataInner {
|
||||
pub fn is_indexing(&self, reader: &heed::RoTxn, index: &str) -> MResult<Option<bool>> {
|
||||
match self.db.open_index(&index) {
|
||||
Some(index) => index.current_update_id(&reader).map(|u| Some(u.is_some())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_update(
|
||||
&self,
|
||||
reader: &heed::RoTxn,
|
||||
index_name: &str,
|
||||
) -> MResult<Option<DateTime<Utc>>> {
|
||||
let key = format!("last-update-{}", index_name);
|
||||
match self
|
||||
.db
|
||||
.common_store()
|
||||
.get::<Str, SerdeDatetime>(&reader, &key)?
|
||||
{
|
||||
Some(datetime) => Ok(Some(datetime)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_last_update(&self, writer: &mut heed::RwTxn, index_name: &str) -> MResult<()> {
|
||||
let key = format!("last-update-{}", index_name);
|
||||
self.db
|
||||
.common_store()
|
||||
.put::<Str, SerdeDatetime>(writer, &key, &Utc::now())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn last_backup(&self, reader: &heed::RoTxn) -> MResult<Option<DateTime<Utc>>> {
|
||||
match self
|
||||
.db
|
||||
.common_store()
|
||||
.get::<Str, SerdeDatetime>(&reader, "last-backup")?
|
||||
{
|
||||
Some(datetime) => Ok(Some(datetime)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_last_backup(&self, writer: &mut heed::RwTxn) -> MResult<()> {
|
||||
self.db
|
||||
.common_store()
|
||||
.put::<Str, SerdeDatetime>(writer, "last-backup", &Utc::now())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fields_frequency(
|
||||
&self,
|
||||
reader: &heed::RoTxn,
|
||||
index_name: &str,
|
||||
) -> MResult<Option<FreqsMap>> {
|
||||
let key = format!("fields-frequency-{}", index_name);
|
||||
match self
|
||||
.db
|
||||
.common_store()
|
||||
.get::<Str, SerdeFreqsMap>(&reader, &key)?
|
||||
{
|
||||
Some(freqs) => Ok(Some(freqs)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_stats(&self, writer: &mut heed::RwTxn, index_name: &str) -> MResult<()> {
|
||||
let index = match self.db.open_index(&index_name) {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
error!("Impossible to retrieve index {}", index_name);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let schema = match index.main.schema(&writer)? {
|
||||
Some(schema) => schema,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let all_documents_fields = index
|
||||
.documents_fields_counts
|
||||
.all_documents_fields_counts(&writer)?;
|
||||
|
||||
// count fields frequencies
|
||||
let mut fields_frequency = HashMap::<_, usize>::new();
|
||||
for result in all_documents_fields {
|
||||
let (_, attr, _) = result?;
|
||||
*fields_frequency.entry(attr).or_default() += 1;
|
||||
}
|
||||
|
||||
// convert attributes to their names
|
||||
let frequency: HashMap<_, _> = fields_frequency
|
||||
.into_iter()
|
||||
.map(|(a, c)| (schema.attribute_name(a).to_owned(), c))
|
||||
.collect();
|
||||
|
||||
let key = format!("fields-frequency-{}", index_name);
|
||||
self.db
|
||||
.common_store()
|
||||
.put::<Str, SerdeFreqsMap>(writer, &key, &frequency)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_accept_updates(&self) {
|
||||
self.accept_updates.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn accept_updates(&self) -> bool {
|
||||
self.accept_updates.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn new(opt: Opt) -> Data {
|
||||
let db_path = opt.database_path.clone();
|
||||
let admin_token = opt.admin_token.clone();
|
||||
let server_pid = sysinfo::get_current_pid().unwrap();
|
||||
|
||||
let db = Arc::new(Database::open_or_create(opt.database_path.clone()).unwrap());
|
||||
let accept_updates = Arc::new(AtomicBool::new(true));
|
||||
|
||||
let inner_data = DataInner {
|
||||
db: db.clone(),
|
||||
db_path,
|
||||
admin_token,
|
||||
server_pid,
|
||||
accept_updates,
|
||||
};
|
||||
|
||||
let data = Data {
|
||||
inner: Arc::new(inner_data),
|
||||
};
|
||||
|
||||
for index_name in db.indexes_names().unwrap() {
|
||||
let callback_context = data.clone();
|
||||
let callback_name = index_name.clone();
|
||||
db.set_update_callback(
|
||||
index_name,
|
||||
Box::new(move |status| {
|
||||
index_update_callback(&callback_name, &callback_context, status);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
pub mod meilidb;
|
||||
pub mod tide;
|
@ -1,56 +0,0 @@
|
||||
use envconfig::Envconfig;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(Debug, Clone, StructOpt, Envconfig)]
|
||||
struct Vars {
|
||||
/// The destination where the database must be created.
|
||||
#[structopt(long)]
|
||||
#[envconfig(from = "MEILI_DATABASE_PATH")]
|
||||
pub database_path: Option<String>,
|
||||
|
||||
/// The addr on which the http server will listen.
|
||||
#[structopt(long)]
|
||||
#[envconfig(from = "MEILI_HTTP_ADDR")]
|
||||
pub http_addr: Option<String>,
|
||||
|
||||
#[structopt(long)]
|
||||
#[envconfig(from = "MEILI_ADMIN_TOKEN")]
|
||||
pub admin_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Opt {
|
||||
pub database_path: String,
|
||||
pub http_addr: String,
|
||||
pub admin_token: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Opt {
|
||||
fn default() -> Self {
|
||||
Opt {
|
||||
database_path: String::from("/tmp/meilidb"),
|
||||
http_addr: String::from("127.0.0.1:8080"),
|
||||
admin_token: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
pub fn new() -> Self {
|
||||
let default = Self::default();
|
||||
let args = Vars::from_args();
|
||||
let env = Vars::init().unwrap();
|
||||
|
||||
Self {
|
||||
database_path: env
|
||||
.database_path
|
||||
.or(args.database_path)
|
||||
.unwrap_or(default.database_path),
|
||||
http_addr: env
|
||||
.http_addr
|
||||
.or(args.http_addr)
|
||||
.unwrap_or(default.http_addr),
|
||||
admin_token: env.admin_token.or(args.admin_token).or(default.admin_token),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
use http::StatusCode;
|
||||
use meilidb_core::{ProcessedUpdateResult, UpdateStatus};
|
||||
use meilidb_schema::Schema;
|
||||
use serde_json::json;
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::schema::SchemaBody;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::routes::document::IndexUpdateResponse;
|
||||
use crate::Data;
|
||||
|
||||
pub async fn list_indexes(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
let list = ctx
|
||||
.state()
|
||||
.db
|
||||
.indexes_names()
|
||||
.map_err(ResponseError::internal)?;
|
||||
Ok(tide::response::json(list))
|
||||
}
|
||||
|
||||
pub async fn get_index_schema(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let schema = index
|
||||
.main
|
||||
.schema(&reader)
|
||||
.map_err(ResponseError::create_index)?;
|
||||
|
||||
match schema {
|
||||
Some(schema) => {
|
||||
let schema = SchemaBody::from(schema);
|
||||
Ok(tide::response::json(schema))
|
||||
}
|
||||
None => Ok(
|
||||
tide::response::json(json!({ "message": "missing index schema" }))
|
||||
.with_status(StatusCode::NOT_FOUND)
|
||||
.into_response(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let index_name = ctx.url_param("index")?;
|
||||
|
||||
let body = ctx.body_bytes().await.map_err(ResponseError::bad_request)?;
|
||||
let schema: Option<Schema> = if body.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::from_slice::<SchemaBody>(&body)
|
||||
.map_err(ResponseError::bad_request)
|
||||
.map(|s| Some(s.into()))?
|
||||
};
|
||||
|
||||
let db = &ctx.state().db;
|
||||
|
||||
let created_index = match db.create_index(&index_name) {
|
||||
Ok(index) => index,
|
||||
Err(meilidb_core::Error::IndexAlreadyExists) => db.open_index(&index_name).ok_or(
|
||||
ResponseError::internal("index not found but must have been found"),
|
||||
)?,
|
||||
Err(e) => return Err(ResponseError::create_index(e)),
|
||||
};
|
||||
|
||||
let callback_context = ctx.state().clone();
|
||||
let callback_name = index_name.clone();
|
||||
db.set_update_callback(
|
||||
&index_name,
|
||||
Box::new(move |status| {
|
||||
index_update_callback(&callback_name, &callback_context, status);
|
||||
}),
|
||||
);
|
||||
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
match schema {
|
||||
Some(schema) => {
|
||||
let update_id = created_index
|
||||
.schema_update(&mut writer, schema.clone())
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::CREATED)
|
||||
.into_response())
|
||||
}
|
||||
None => Ok(Response::new(tide::Body::empty())
|
||||
.with_status(StatusCode::NO_CONTENT)
|
||||
.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_schema(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let index_name = ctx.url_param("index")?;
|
||||
|
||||
let schema = ctx
|
||||
.body_json::<SchemaBody>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let index = db
|
||||
.open_index(&index_name)
|
||||
.ok_or(ResponseError::index_not_found(index_name))?;
|
||||
|
||||
let schema: meilidb_schema::Schema = schema.into();
|
||||
let update_id = index
|
||||
.schema_update(&mut writer, schema.clone())
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn get_update_status(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let update_id = ctx
|
||||
.param::<u64>("update_id")
|
||||
.map_err(|e| ResponseError::bad_parameter("update_id", e))?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let status = index
|
||||
.update_status(&reader, update_id)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = match status {
|
||||
UpdateStatus::Enqueued(data) => {
|
||||
tide::response::json(json!({ "status": "enqueued", "data": data }))
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response()
|
||||
}
|
||||
UpdateStatus::Processed(data) => {
|
||||
tide::response::json(json!({ "status": "processed", "data": data }))
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response()
|
||||
}
|
||||
UpdateStatus::Unknown => tide::response::json(json!({ "message": "unknown update id" }))
|
||||
.with_status(StatusCode::NOT_FOUND)
|
||||
.into_response(),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get_all_updates_status(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let all_status = index
|
||||
.all_updates_status(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = tide::response::json(all_status)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response();
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn delete_index(ctx: Context<Data>) -> SResult<StatusCode> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
let _index_name = ctx.url_param("index")?;
|
||||
let _index = ctx.index()?;
|
||||
|
||||
// ctx.state()
|
||||
// .db
|
||||
// .delete_index(&index_name)
|
||||
// .map_err(ResponseError::internal)?;
|
||||
|
||||
Ok(StatusCode::NOT_IMPLEMENTED)
|
||||
}
|
||||
|
||||
pub fn index_update_callback(index_name: &str, data: &Data, _status: ProcessedUpdateResult) {
|
||||
let env = &data.db.env;
|
||||
let mut writer = env.write_txn().unwrap();
|
||||
|
||||
data.compute_stats(&mut writer, &index_name).unwrap();
|
||||
data.set_last_update(&mut writer, &index_name).unwrap();
|
||||
|
||||
writer.commit().unwrap();
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "meilidb-core"
|
||||
version = "0.6.2"
|
||||
name = "meilisearch-core"
|
||||
version = "0.8.0"
|
||||
authors = ["Kerollmops <clement@meilisearch.com>"]
|
||||
edition = "2018"
|
||||
|
||||
@ -8,32 +8,27 @@ edition = "2018"
|
||||
arc-swap = "0.4.3"
|
||||
bincode = "1.1.4"
|
||||
byteorder = "1.3.2"
|
||||
crossbeam-channel = "0.3.9"
|
||||
chrono = { version = "0.4.9", features = ["serde"] }
|
||||
crossbeam-channel = "0.4.0"
|
||||
deunicode = "1.0.0"
|
||||
env_logger = "0.7.0"
|
||||
fst = { version = "0.3.5", default-features = false }
|
||||
hashbrown = { version = "0.6.0", features = ["serde"] }
|
||||
heed = "0.5.0"
|
||||
levenshtein_automata = { version = "0.1.1", features = ["fst_automaton"] }
|
||||
log = "0.4.8"
|
||||
meilidb-schema = { path = "../meilidb-schema", version = "0.6.0" }
|
||||
meilidb-tokenizer = { path = "../meilidb-tokenizer", version = "0.6.0" }
|
||||
meilisearch-schema = { path = "../meilisearch-schema", version = "0.8.0" }
|
||||
meilisearch-tokenizer = { path = "../meilisearch-tokenizer", version = "0.8.0" }
|
||||
meilisearch-types = { path = "../meilisearch-types", version = "0.8.0" }
|
||||
once_cell = "1.2.0"
|
||||
ordered-float = { version = "1.0.2", features = ["serde"] }
|
||||
sdset = "0.3.3"
|
||||
serde = { version = "1.0.101", features = ["derive"] }
|
||||
serde_json = "1.0.41"
|
||||
siphasher = "0.3.0"
|
||||
siphasher = "0.3.1"
|
||||
slice-group-by = "0.2.6"
|
||||
zerocopy = "0.2.8"
|
||||
|
||||
[dependencies.levenshtein_automata]
|
||||
git = "https://github.com/Kerollmops/levenshtein-automata.git"
|
||||
branch = "arc-byte-slice"
|
||||
features = ["fst_automaton"]
|
||||
|
||||
[dependencies.fst]
|
||||
git = "https://github.com/Kerollmops/fst.git"
|
||||
branch = "arc-byte-slice"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.3"
|
||||
csv = "1.0.7"
|
@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
|
||||
|
||||
use meilidb_core::{Database, Highlight, ProcessedUpdateResult};
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_core::{Database, Highlight, ProcessedUpdateResult};
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct IndexCommand {
|
||||
@ -22,7 +22,7 @@ struct IndexCommand {
|
||||
database_path: PathBuf,
|
||||
|
||||
#[structopt(long, default_value = "default")]
|
||||
index_name: String,
|
||||
index_uid: String,
|
||||
|
||||
/// The csv file to index.
|
||||
#[structopt(parse(from_os_str))]
|
||||
@ -46,7 +46,7 @@ struct SearchCommand {
|
||||
database_path: PathBuf,
|
||||
|
||||
#[structopt(long, default_value = "default")]
|
||||
index_name: String,
|
||||
index_uid: String,
|
||||
|
||||
/// Timeout after which the search will return results.
|
||||
#[structopt(long)]
|
||||
@ -76,7 +76,7 @@ struct ShowUpdatesCommand {
|
||||
database_path: PathBuf,
|
||||
|
||||
#[structopt(long, default_value = "default")]
|
||||
index_name: String,
|
||||
index_uid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
@ -104,14 +104,14 @@ fn index_command(command: IndexCommand, database: Database) -> Result<(), Box<dy
|
||||
let start = Instant::now();
|
||||
|
||||
let (sender, receiver) = mpsc::sync_channel(100);
|
||||
let update_fn = move |update: ProcessedUpdateResult| sender.send(update.update_id).unwrap();
|
||||
let index = match database.open_index(&command.index_name) {
|
||||
let update_fn =
|
||||
move |_name: &str, update: ProcessedUpdateResult| sender.send(update.update_id).unwrap();
|
||||
let index = match database.open_index(&command.index_uid) {
|
||||
Some(index) => index,
|
||||
None => database.create_index(&command.index_name).unwrap(),
|
||||
None => database.create_index(&command.index_uid).unwrap(),
|
||||
};
|
||||
|
||||
let done = database.set_update_callback(&command.index_name, Box::new(update_fn));
|
||||
assert!(done, "could not set the index update function");
|
||||
database.set_update_callback(Box::new(update_fn));
|
||||
|
||||
let env = &database.env;
|
||||
|
||||
@ -124,7 +124,7 @@ fn index_command(command: IndexCommand, database: Database) -> Result<(), Box<dy
|
||||
match index.main.schema(&writer)? {
|
||||
Some(current_schema) => {
|
||||
if current_schema != schema {
|
||||
return Err(meilidb_core::Error::SchemaDiffer.into());
|
||||
return Err(meilisearch_core::Error::SchemaDiffer.into());
|
||||
}
|
||||
writer.abort();
|
||||
}
|
||||
@ -195,8 +195,9 @@ fn index_command(command: IndexCommand, database: Database) -> Result<(), Box<dy
|
||||
);
|
||||
|
||||
if let Some(path) = command.compact_to_path {
|
||||
fs::create_dir_all(&path)?;
|
||||
let start = Instant::now();
|
||||
let _file = database.copy_and_compact_to_path(&path)?;
|
||||
let _file = database.copy_and_compact_to_path(path.join("data.mdb"))?;
|
||||
println!(
|
||||
"database compacted in {:.2?} at: {:?}",
|
||||
start.elapsed(),
|
||||
@ -217,7 +218,11 @@ fn display_highlights(text: &str, ranges: &[usize]) -> io::Result<()> {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if highlighted {
|
||||
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
||||
stdout.set_color(
|
||||
ColorSpec::new()
|
||||
.set_fg(Some(Color::Yellow))
|
||||
.set_underline(true),
|
||||
)?;
|
||||
}
|
||||
write!(&mut stdout, "{}", &text[start..end])?;
|
||||
stdout.reset()?;
|
||||
@ -313,14 +318,14 @@ fn crop_text(
|
||||
fn search_command(command: SearchCommand, database: Database) -> Result<(), Box<dyn Error>> {
|
||||
let env = &database.env;
|
||||
let index = database
|
||||
.open_index(&command.index_name)
|
||||
.open_index(&command.index_uid)
|
||||
.expect("Could not find index");
|
||||
|
||||
let reader = env.read_txn().unwrap();
|
||||
let schema = index.main.schema(&reader)?;
|
||||
reader.abort();
|
||||
|
||||
let schema = schema.ok_or(meilidb_core::Error::SchemaMissing)?;
|
||||
let schema = schema.ok_or(meilisearch_core::Error::SchemaMissing)?;
|
||||
|
||||
let fields = command.displayed_fields.iter().map(String::as_str);
|
||||
let fields = HashSet::from_iter(fields);
|
||||
@ -441,7 +446,7 @@ fn show_updates_command(
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let env = &database.env;
|
||||
let index = database
|
||||
.open_index(&command.index_name)
|
||||
.open_index(&command.index_uid)
|
||||
.expect("Could not find index");
|
||||
|
||||
let reader = env.read_txn().unwrap();
|
@ -6,7 +6,7 @@ use std::{cmp, vec};
|
||||
|
||||
use fst::{IntoStreamer, Streamer};
|
||||
use levenshtein_automata::DFA;
|
||||
use meilidb_tokenizer::{is_cjk, split_query_string};
|
||||
use meilisearch_tokenizer::{is_cjk, split_query_string};
|
||||
|
||||
use crate::error::MResult;
|
||||
use crate::store;
|
@ -1,6 +1,6 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
use sdset::Set;
|
||||
use slice_group_by::GroupBy;
|
||||
|
@ -4,7 +4,7 @@ use std::fmt;
|
||||
|
||||
use crate::criterion::Criterion;
|
||||
use crate::{RankedMap, RawDocument};
|
||||
use meilidb_schema::{Schema, SchemaAttr};
|
||||
use meilisearch_schema::{Schema, SchemaAttr};
|
||||
|
||||
/// An helper struct that permit to sort documents by
|
||||
/// some of their stored attributes.
|
||||
@ -23,7 +23,7 @@ use meilidb_schema::{Schema, SchemaAttr};
|
||||
///
|
||||
/// ```ignore
|
||||
/// use serde_derive::Deserialize;
|
||||
/// use meilidb::rank::criterion::*;
|
||||
/// use meilisearch::rank::criterion::*;
|
||||
///
|
||||
/// let custom_ranking = SortByAttr::lower_is_better(&ranked_map, &schema, "published_at")?;
|
||||
///
|
1011
meilisearch-core/src/database.rs
Normal file
1011
meilisearch-core/src/database.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -97,7 +97,7 @@ pub enum UnsupportedOperation {
|
||||
SchemaAlreadyExists,
|
||||
CannotUpdateSchemaIdentifier,
|
||||
CannotReorderSchemaAttribute,
|
||||
CannotIntroduceNewSchemaAttribute,
|
||||
CanOnlyIntroduceNewSchemaAttributesAtEnd,
|
||||
CannotRemoveSchemaAttribute,
|
||||
}
|
||||
|
||||
@ -108,8 +108,8 @@ impl fmt::Display for UnsupportedOperation {
|
||||
SchemaAlreadyExists => write!(f, "Cannot update index which already have a schema"),
|
||||
CannotUpdateSchemaIdentifier => write!(f, "Cannot update the identifier of a schema"),
|
||||
CannotReorderSchemaAttribute => write!(f, "Cannot reorder the attributes of a schema"),
|
||||
CannotIntroduceNewSchemaAttribute => {
|
||||
write!(f, "Cannot introduce new attributes in a schema")
|
||||
CanOnlyIntroduceNewSchemaAttributesAtEnd => {
|
||||
write!(f, "Can only introduce new attributes at end of a schema")
|
||||
}
|
||||
CannotRemoveSchemaAttribute => write!(f, "Cannot remove attributes from a schema"),
|
||||
}
|
@ -25,79 +25,7 @@ pub use self::ranked_map::RankedMap;
|
||||
pub use self::raw_document::RawDocument;
|
||||
pub use self::store::Index;
|
||||
pub use self::update::{EnqueuedUpdateResult, ProcessedUpdateResult, UpdateStatus, UpdateType};
|
||||
|
||||
use ::serde::{Deserialize, Serialize};
|
||||
use zerocopy::{AsBytes, FromBytes};
|
||||
|
||||
/// Represent an internally generated document unique identifier.
|
||||
///
|
||||
/// It is used to inform the database the document you want to deserialize.
|
||||
/// Helpful for custom ranking.
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
AsBytes,
|
||||
FromBytes,
|
||||
)]
|
||||
#[repr(C)]
|
||||
pub struct DocumentId(pub u64);
|
||||
|
||||
/// This structure represent the position of a word
|
||||
/// in a document and its attributes.
|
||||
///
|
||||
/// This is stored in the map, generated at index time,
|
||||
/// extracted and interpreted at search time.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, AsBytes, FromBytes)]
|
||||
#[repr(C)]
|
||||
pub struct DocIndex {
|
||||
/// The document identifier where the word was found.
|
||||
pub document_id: DocumentId,
|
||||
|
||||
/// The attribute in the document where the word was found
|
||||
/// along with the index in it.
|
||||
pub attribute: u16,
|
||||
pub word_index: u16,
|
||||
|
||||
/// The position in bytes where the word was found
|
||||
/// along with the length of it.
|
||||
///
|
||||
/// It informs on the original word area in the text indexed
|
||||
/// without needing to run the tokenizer again.
|
||||
pub char_index: u16,
|
||||
pub char_length: u16,
|
||||
}
|
||||
|
||||
/// This structure represent a matching word with informations
|
||||
/// on the location of the word in the document.
|
||||
///
|
||||
/// The order of the field is important because it defines
|
||||
/// the way these structures are ordered between themselves.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Highlight {
|
||||
/// The attribute in the document where the word was found
|
||||
/// along with the index in it.
|
||||
pub attribute: u16,
|
||||
|
||||
/// The position in bytes where the word was found.
|
||||
///
|
||||
/// It informs on the original word area in the text indexed
|
||||
/// without needing to run the tokenizer again.
|
||||
pub char_index: u16,
|
||||
|
||||
/// The length in bytes of the found word.
|
||||
///
|
||||
/// It informs on the original word area in the text indexed
|
||||
/// without needing to run the tokenizer again.
|
||||
pub char_length: u16,
|
||||
}
|
||||
pub use meilisearch_types::{DocIndex, DocumentId, Highlight};
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
@ -1,3 +1,4 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::num::{ParseFloatError, ParseIntError};
|
||||
use std::str::FromStr;
|
||||
@ -5,7 +6,7 @@ use std::str::FromStr;
|
||||
use ordered_float::OrderedFloat;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash)]
|
||||
pub enum Number {
|
||||
Unsigned(u64),
|
||||
Signed(i64),
|
||||
@ -39,6 +40,50 @@ impl FromStr for Number {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Number {
|
||||
fn eq(&self, other: &Number) -> bool {
|
||||
self.cmp(other) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Number {}
|
||||
|
||||
impl PartialOrd for Number {
|
||||
fn partial_cmp(&self, other: &Number) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Number {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
use Number::{Float, Signed, Unsigned};
|
||||
|
||||
match (*self, *other) {
|
||||
(Unsigned(a), Unsigned(b)) => a.cmp(&b),
|
||||
(Unsigned(a), Signed(b)) => {
|
||||
if b < 0 {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
a.cmp(&(b as u64))
|
||||
}
|
||||
}
|
||||
(Unsigned(a), Float(b)) => (OrderedFloat(a as f64)).cmp(&b),
|
||||
(Signed(a), Unsigned(b)) => {
|
||||
if a < 0 {
|
||||
Ordering::Less
|
||||
} else {
|
||||
(a as u64).cmp(&b)
|
||||
}
|
||||
}
|
||||
(Signed(a), Signed(b)) => a.cmp(&b),
|
||||
(Signed(a), Float(b)) => OrderedFloat(a as f64).cmp(&b),
|
||||
(Float(a), Unsigned(b)) => a.cmp(&OrderedFloat(b as f64)),
|
||||
(Float(a), Signed(b)) => a.cmp(&OrderedFloat(b as f64)),
|
||||
(Float(a), Float(b)) => a.cmp(&b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ParseNumberError {
|
||||
uint_error: ParseIntError,
|
@ -1,9 +1,9 @@
|
||||
use hashbrown::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::mem;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{cmp, mem};
|
||||
|
||||
use fst::{IntoStreamer, Streamer};
|
||||
use sdset::SetBuf;
|
||||
@ -178,7 +178,7 @@ fn fetch_raw_documents(
|
||||
let distance = dfa.eval(input).to_u8();
|
||||
let is_exact = *is_exact && distance == 0 && input.len() == *query_len;
|
||||
|
||||
let covered_area = if query.len() > input.len() {
|
||||
let covered_area = if *query_len > input.len() {
|
||||
input.len()
|
||||
} else {
|
||||
prefix_damerau_levenshtein(query.as_bytes(), input).1
|
||||
@ -202,10 +202,13 @@ fn fetch_raw_documents(
|
||||
is_exact,
|
||||
};
|
||||
|
||||
let covered_area = u16::try_from(covered_area).unwrap_or(u16::max_value());
|
||||
let covered_area = cmp::min(covered_area, di.char_length);
|
||||
|
||||
let highlight = Highlight {
|
||||
attribute: di.attribute,
|
||||
char_index: di.char_index,
|
||||
char_length: u16::try_from(covered_area).unwrap_or(u16::max_value()),
|
||||
char_length: covered_area,
|
||||
};
|
||||
|
||||
tmp_matches.push((di.document_id, id, match_, highlight));
|
||||
@ -692,7 +695,7 @@ mod tests {
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use fst::{IntoStreamer, Set};
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
use sdset::SetBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@ -1669,7 +1672,7 @@ mod tests {
|
||||
#[test]
|
||||
fn deunicoded_synonyms() {
|
||||
let mut store = TempDatabase::from_iter(vec![
|
||||
("telephone", &[doc_index(0, 0)][..]), // meilidb indexes the unidecoded
|
||||
("telephone", &[doc_index(0, 0)][..]), // meilisearch indexes the unidecoded
|
||||
("téléphone", &[doc_index(0, 0)][..]), // and the original words on the same DocIndex
|
||||
("iphone", &[doc_index(1, 0)][..]),
|
||||
]);
|
@ -1,7 +1,7 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{DocumentId, Number};
|
@ -1,7 +1,7 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
use sdset::SetBuf;
|
||||
use slice_group_by::GroupBy;
|
||||
|
@ -3,10 +3,12 @@ use std::convert::TryFrom;
|
||||
|
||||
use crate::{DocIndex, DocumentId};
|
||||
use deunicode::deunicode_with_tofu;
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilidb_tokenizer::{is_cjk, SeqTokenizer, Token, Tokenizer};
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
use meilisearch_tokenizer::{is_cjk, SeqTokenizer, Token, Tokenizer};
|
||||
use sdset::SetBuf;
|
||||
|
||||
const WORD_LENGTH_LIMIT: usize = 80;
|
||||
|
||||
type Word = Vec<u8>; // TODO make it be a SmallVec
|
||||
|
||||
pub struct RawIndexer {
|
||||
@ -128,35 +130,30 @@ fn index_token(
|
||||
match token_to_docindex(id, attr, token) {
|
||||
Some(docindex) => {
|
||||
let word = Vec::from(token.word);
|
||||
words_doc_indexes
|
||||
.entry(word.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(docindex);
|
||||
docs_words.entry(id).or_insert_with(Vec::new).push(word);
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
|
||||
if !lower.contains(is_cjk) {
|
||||
let unidecoded = deunicode_with_tofu(&lower, "");
|
||||
if unidecoded != lower && !unidecoded.is_empty() {
|
||||
let token = Token {
|
||||
word: &unidecoded,
|
||||
..token
|
||||
};
|
||||
if word.len() <= WORD_LENGTH_LIMIT {
|
||||
words_doc_indexes
|
||||
.entry(word.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(docindex);
|
||||
docs_words.entry(id).or_insert_with(Vec::new).push(word);
|
||||
|
||||
match token_to_docindex(id, attr, token) {
|
||||
Some(docindex) => {
|
||||
let word = Vec::from(token.word);
|
||||
words_doc_indexes
|
||||
.entry(word.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(docindex);
|
||||
docs_words.entry(id).or_insert_with(Vec::new).push(word);
|
||||
if !lower.contains(is_cjk) {
|
||||
let unidecoded = deunicode_with_tofu(&lower, "");
|
||||
if unidecoded != lower && !unidecoded.is_empty() {
|
||||
let word = Vec::from(unidecoded);
|
||||
if word.len() <= WORD_LENGTH_LIMIT {
|
||||
words_doc_indexes
|
||||
.entry(word.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(docindex);
|
||||
docs_words.entry(id).or_insert_with(Vec::new).push(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
use std::io::Cursor;
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use meilidb_schema::{Schema, SchemaAttr};
|
||||
use meilisearch_schema::{Schema, SchemaAttr};
|
||||
use serde::{de, forward_to_deserialize_any};
|
||||
use serde_json::de::IoRead as SerdeJsonIoRead;
|
||||
use serde_json::Deserializer as SerdeJsonDeserializer;
|
||||
@ -63,13 +63,14 @@ impl<'de, 'a, 'b> de::Deserializer<'de> for &'b mut Deserializer<'a> {
|
||||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
self.deserialize_map(visitor)
|
||||
self.deserialize_option(visitor)
|
||||
}
|
||||
|
||||
forward_to_deserialize_any! {
|
||||
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
|
||||
bytes byte_buf option unit unit_struct newtype_struct seq tuple
|
||||
tuple_struct struct enum identifier ignored_any
|
||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
self.deserialize_map(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
@ -104,16 +105,29 @@ impl<'de, 'a, 'b> de::Deserializer<'de> for &'b mut Deserializer<'a> {
|
||||
}
|
||||
});
|
||||
|
||||
let map_deserializer = de::value::MapDeserializer::new(iter);
|
||||
let result = visitor
|
||||
.visit_map(map_deserializer)
|
||||
.map_err(DeserializerError::from);
|
||||
let mut iter = iter.peekable();
|
||||
|
||||
let result = match iter.peek() {
|
||||
Some(_) => {
|
||||
let map_deserializer = de::value::MapDeserializer::new(iter);
|
||||
visitor
|
||||
.visit_some(map_deserializer)
|
||||
.map_err(DeserializerError::from)
|
||||
}
|
||||
None => visitor.visit_none(),
|
||||
};
|
||||
|
||||
match error.take() {
|
||||
Some(error) => Err(error.into()),
|
||||
None => result,
|
||||
}
|
||||
}
|
||||
|
||||
forward_to_deserialize_any! {
|
||||
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
|
||||
bytes byte_buf unit unit_struct newtype_struct seq tuple
|
||||
tuple_struct struct enum identifier ignored_any
|
||||
}
|
||||
}
|
||||
|
||||
struct Value(SerdeJsonDeserializer<SerdeJsonIoRead<Cursor<Vec<u8>>>>);
|
@ -1,4 +1,4 @@
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
use serde::ser;
|
||||
use serde::Serialize;
|
||||
|
@ -1,4 +1,4 @@
|
||||
use meilidb_schema::{Schema, SchemaAttr, SchemaProps};
|
||||
use meilisearch_schema::{Schema, SchemaAttr, SchemaProps};
|
||||
use serde::ser;
|
||||
|
||||
use crate::raw_indexer::RawIndexer;
|
@ -39,7 +39,7 @@ impl DocsWords {
|
||||
match self.docs_words.get(reader, &document_id)? {
|
||||
Some(bytes) => {
|
||||
let len = bytes.len();
|
||||
let bytes = Arc::from(bytes);
|
||||
let bytes = Arc::new(bytes.to_owned());
|
||||
let fst = fst::raw::Fst::from_shared_bytes(bytes, 0, len).unwrap();
|
||||
Ok(Some(fst::Set::from(fst)))
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use heed::types::{ByteSlice, OwnedType};
|
||||
use heed::Result as ZResult;
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
|
||||
use super::DocumentAttrKey;
|
||||
use crate::DocumentId;
|
@ -2,7 +2,7 @@ use super::DocumentAttrKey;
|
||||
use crate::DocumentId;
|
||||
use heed::types::OwnedType;
|
||||
use heed::Result as ZResult;
|
||||
use meilidb_schema::SchemaAttr;
|
||||
use meilisearch_schema::SchemaAttr;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct DocumentsFieldsCounts {
|
@ -1,23 +1,66 @@
|
||||
use crate::RankedMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use heed::types::{ByteSlice, OwnedType, SerdeBincode, Str};
|
||||
use heed::Result as ZResult;
|
||||
use meilidb_schema::Schema;
|
||||
use meilisearch_schema::Schema;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
const CREATED_AT_KEY: &str = "created-at";
|
||||
const CUSTOMS_KEY: &str = "customs-key";
|
||||
const FIELDS_FREQUENCY_KEY: &str = "fields-frequency";
|
||||
const NAME_KEY: &str = "name";
|
||||
const NUMBER_OF_DOCUMENTS_KEY: &str = "number-of-documents";
|
||||
const RANKED_MAP_KEY: &str = "ranked-map";
|
||||
const SCHEMA_KEY: &str = "schema";
|
||||
const SYNONYMS_KEY: &str = "synonyms";
|
||||
const STOP_WORDS_KEY: &str = "stop-words";
|
||||
const SYNONYMS_KEY: &str = "synonyms";
|
||||
const UPDATED_AT_KEY: &str = "updated-at";
|
||||
const WORDS_KEY: &str = "words";
|
||||
|
||||
pub type FreqsMap = HashMap<String, usize>;
|
||||
type SerdeFreqsMap = SerdeBincode<FreqsMap>;
|
||||
type SerdeDatetime = SerdeBincode<DateTime<Utc>>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Main {
|
||||
pub(crate) main: heed::PolyDatabase,
|
||||
}
|
||||
|
||||
impl Main {
|
||||
pub fn clear(self, writer: &mut heed::RwTxn) -> ZResult<()> {
|
||||
self.main.clear(writer)
|
||||
}
|
||||
|
||||
pub fn put_name(self, writer: &mut heed::RwTxn, name: &str) -> ZResult<()> {
|
||||
self.main.put::<Str, Str>(writer, NAME_KEY, name)
|
||||
}
|
||||
|
||||
pub fn name(self, reader: &heed::RoTxn) -> ZResult<Option<String>> {
|
||||
Ok(self
|
||||
.main
|
||||
.get::<Str, Str>(reader, NAME_KEY)?
|
||||
.map(|name| name.to_owned()))
|
||||
}
|
||||
|
||||
pub fn put_created_at(self, writer: &mut heed::RwTxn) -> ZResult<()> {
|
||||
self.main
|
||||
.put::<Str, SerdeDatetime>(writer, CREATED_AT_KEY, &Utc::now())
|
||||
}
|
||||
|
||||
pub fn created_at(self, reader: &heed::RoTxn) -> ZResult<Option<DateTime<Utc>>> {
|
||||
self.main.get::<Str, SerdeDatetime>(reader, CREATED_AT_KEY)
|
||||
}
|
||||
|
||||
pub fn put_updated_at(self, writer: &mut heed::RwTxn) -> ZResult<()> {
|
||||
self.main
|
||||
.put::<Str, SerdeDatetime>(writer, UPDATED_AT_KEY, &Utc::now())
|
||||
}
|
||||
|
||||
pub fn updated_at(self, reader: &heed::RoTxn) -> ZResult<Option<DateTime<Utc>>> {
|
||||
self.main.get::<Str, SerdeDatetime>(reader, UPDATED_AT_KEY)
|
||||
}
|
||||
|
||||
pub fn put_words_fst(self, writer: &mut heed::RwTxn, fst: &fst::Set) -> ZResult<()> {
|
||||
let bytes = fst.as_fst().as_bytes();
|
||||
self.main.put::<Str, ByteSlice>(writer, WORDS_KEY, bytes)
|
||||
@ -27,7 +70,7 @@ impl Main {
|
||||
match self.main.get::<Str, ByteSlice>(reader, WORDS_KEY)? {
|
||||
Some(bytes) => {
|
||||
let len = bytes.len();
|
||||
let bytes = Arc::from(bytes);
|
||||
let bytes = Arc::new(bytes.to_owned());
|
||||
let fst = fst::raw::Fst::from_shared_bytes(bytes, 0, len).unwrap();
|
||||
Ok(Some(fst::Set::from(fst)))
|
||||
}
|
||||
@ -64,7 +107,7 @@ impl Main {
|
||||
match self.main.get::<Str, ByteSlice>(reader, SYNONYMS_KEY)? {
|
||||
Some(bytes) => {
|
||||
let len = bytes.len();
|
||||
let bytes = Arc::from(bytes);
|
||||
let bytes = Arc::new(bytes.to_owned());
|
||||
let fst = fst::raw::Fst::from_shared_bytes(bytes, 0, len).unwrap();
|
||||
Ok(Some(fst::Set::from(fst)))
|
||||
}
|
||||
@ -82,7 +125,7 @@ impl Main {
|
||||
match self.main.get::<Str, ByteSlice>(reader, STOP_WORDS_KEY)? {
|
||||
Some(bytes) => {
|
||||
let len = bytes.len();
|
||||
let bytes = Arc::from(bytes);
|
||||
let bytes = Arc::new(bytes.to_owned());
|
||||
let fst = fst::raw::Fst::from_shared_bytes(bytes, 0, len).unwrap();
|
||||
Ok(Some(fst::Set::from(fst)))
|
||||
}
|
||||
@ -110,6 +153,25 @@ impl Main {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_fields_frequency(
|
||||
self,
|
||||
writer: &mut heed::RwTxn,
|
||||
fields_frequency: &FreqsMap,
|
||||
) -> ZResult<()> {
|
||||
self.main
|
||||
.put::<Str, SerdeFreqsMap>(writer, FIELDS_FREQUENCY_KEY, fields_frequency)
|
||||
}
|
||||
|
||||
pub fn fields_frequency(&self, reader: &heed::RoTxn) -> ZResult<Option<FreqsMap>> {
|
||||
match self
|
||||
.main
|
||||
.get::<Str, SerdeFreqsMap>(reader, FIELDS_FREQUENCY_KEY)?
|
||||
{
|
||||
Some(freqs) => Ok(Some(freqs)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_customs(self, writer: &mut heed::RwTxn, customs: &[u8]) -> ZResult<()> {
|
||||
self.main
|
||||
.put::<Str, ByteSlice>(writer, CUSTOMS_KEY, customs)
|
@ -21,11 +21,12 @@ pub use self::updates_results::UpdatesResults;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use heed::Result as ZResult;
|
||||
use meilidb_schema::{Schema, SchemaAttr};
|
||||
use serde::de;
|
||||
use meilisearch_schema::{Schema, SchemaAttr};
|
||||
use serde::de::{self, Deserialize};
|
||||
use zerocopy::{AsBytes, FromBytes};
|
||||
|
||||
use crate::criterion::Criteria;
|
||||
use crate::database::{UpdateEvent, UpdateEventsEmitter};
|
||||
use crate::serde::Deserializer;
|
||||
use crate::{query_builder::QueryBuilder, update, DocumentId, Error, MResult};
|
||||
|
||||
@ -91,7 +92,7 @@ pub struct Index {
|
||||
|
||||
pub updates: Updates,
|
||||
pub updates_results: UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
pub(crate) updates_notifier: UpdateEventsEmitter,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
@ -120,9 +121,7 @@ impl Index {
|
||||
attributes: attributes.as_ref(),
|
||||
};
|
||||
|
||||
// TODO: currently we return an error if all document fields are missing,
|
||||
// returning None would have been better
|
||||
Ok(T::deserialize(&mut deserializer).map(Some)?)
|
||||
Ok(Option::<T>::deserialize(&mut deserializer)?)
|
||||
}
|
||||
|
||||
pub fn document_attribute<T: de::DeserializeOwned>(
|
||||
@ -141,12 +140,12 @@ impl Index {
|
||||
}
|
||||
|
||||
pub fn schema_update(&self, writer: &mut heed::RwTxn, schema: Schema) -> MResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
update::push_schema_update(writer, self.updates, self.updates_results, schema)
|
||||
}
|
||||
|
||||
pub fn customs_update(&self, writer: &mut heed::RwTxn, customs: Vec<u8>) -> ZResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
update::push_customs_update(writer, self.updates, self.updates_results, customs)
|
||||
}
|
||||
|
||||
@ -158,6 +157,14 @@ impl Index {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn documents_partial_addition<D>(&self) -> update::DocumentsAddition<D> {
|
||||
update::DocumentsAddition::new_partial(
|
||||
self.updates,
|
||||
self.updates_results,
|
||||
self.updates_notifier.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn documents_deletion(&self) -> update::DocumentsDeletion {
|
||||
update::DocumentsDeletion::new(
|
||||
self.updates,
|
||||
@ -167,7 +174,7 @@ impl Index {
|
||||
}
|
||||
|
||||
pub fn clear_all(&self, writer: &mut heed::RwTxn) -> MResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
update::push_clear_all(writer, self.updates, self.updates_results)
|
||||
}
|
||||
|
||||
@ -214,7 +221,7 @@ impl Index {
|
||||
&self,
|
||||
reader: &heed::RoTxn,
|
||||
update_id: u64,
|
||||
) -> MResult<update::UpdateStatus> {
|
||||
) -> MResult<Option<update::UpdateStatus>> {
|
||||
update::update_status(reader, self.updates, self.updates_results, update_id)
|
||||
}
|
||||
|
||||
@ -227,17 +234,19 @@ impl Index {
|
||||
updates.reserve(last_id as usize);
|
||||
|
||||
for id in 0..=last_id {
|
||||
let update = self.update_status(reader, id)?;
|
||||
updates.push(update);
|
||||
last_update_result_id = id;
|
||||
if let Some(update) = self.update_status(reader, id)? {
|
||||
updates.push(update);
|
||||
last_update_result_id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve all enqueued updates
|
||||
if let Some((last_id, _)) = self.updates.last_update_id(reader)? {
|
||||
for id in last_update_result_id + 1..last_id {
|
||||
let update = self.update_status(reader, id)?;
|
||||
updates.push(update);
|
||||
for id in last_update_result_id + 1..=last_id {
|
||||
if let Some(update) = self.update_status(reader, id)? {
|
||||
updates.push(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,7 +279,7 @@ impl Index {
|
||||
pub fn create(
|
||||
env: &heed::Env,
|
||||
name: &str,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> MResult<Index> {
|
||||
// create all the store names
|
||||
let main_name = main_name(name);
|
||||
@ -310,7 +319,7 @@ pub fn create(
|
||||
pub fn open(
|
||||
env: &heed::Env,
|
||||
name: &str,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> MResult<Option<Index>> {
|
||||
// create all the store names
|
||||
let main_name = main_name(name);
|
||||
@ -370,3 +379,16 @@ pub fn open(
|
||||
updates_notifier,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn clear(writer: &mut heed::RwTxn, index: &Index) -> MResult<()> {
|
||||
// clear all the stores
|
||||
index.main.clear(writer)?;
|
||||
index.postings_lists.clear(writer)?;
|
||||
index.documents_fields.clear(writer)?;
|
||||
index.documents_fields_counts.clear(writer)?;
|
||||
index.synonyms.clear(writer)?;
|
||||
index.docs_words.clear(writer)?;
|
||||
index.updates.clear(writer)?;
|
||||
index.updates_results.clear(writer)?;
|
||||
Ok(())
|
||||
}
|
@ -22,11 +22,15 @@ impl Synonyms {
|
||||
self.synonyms.delete(writer, word)
|
||||
}
|
||||
|
||||
pub fn clear(self, writer: &mut heed::RwTxn) -> ZResult<()> {
|
||||
self.synonyms.clear(writer)
|
||||
}
|
||||
|
||||
pub fn synonyms(self, reader: &heed::RoTxn, word: &[u8]) -> ZResult<Option<fst::Set>> {
|
||||
match self.synonyms.get(reader, word)? {
|
||||
Some(bytes) => {
|
||||
let len = bytes.len();
|
||||
let bytes = Arc::from(bytes);
|
||||
let bytes = Arc::new(bytes.to_owned());
|
||||
let fst = fst::raw::Fst::from_shared_bytes(bytes, 0, len).unwrap();
|
||||
Ok(Some(fst::Set::from(fst)))
|
||||
}
|
@ -52,4 +52,8 @@ impl Updates {
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(self, writer: &mut heed::RwTxn) -> ZResult<()> {
|
||||
self.updates.clear(writer)
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
use super::BEU64;
|
||||
use crate::update::ProcessedUpdateResult;
|
||||
use heed::types::{OwnedType, SerdeBincode};
|
||||
use heed::types::{OwnedType, SerdeJson};
|
||||
use heed::Result as ZResult;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct UpdatesResults {
|
||||
pub(crate) updates_results:
|
||||
heed::Database<OwnedType<BEU64>, SerdeBincode<ProcessedUpdateResult>>,
|
||||
pub(crate) updates_results: heed::Database<OwnedType<BEU64>, SerdeJson<ProcessedUpdateResult>>,
|
||||
}
|
||||
|
||||
impl UpdatesResults {
|
||||
@ -38,4 +37,8 @@ impl UpdatesResults {
|
||||
let update_id = BEU64::new(update_id);
|
||||
self.updates_results.get(reader, &update_id)
|
||||
}
|
||||
|
||||
pub fn clear(self, writer: &mut heed::RwTxn) -> ZResult<()> {
|
||||
self.updates_results.clear(writer)
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ pub fn push_clear_all(
|
||||
updates_results_store: store::UpdatesResults,
|
||||
) -> MResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
let update = Update::ClearAll;
|
||||
let update = Update::clear_all();
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -18,7 +18,7 @@ pub fn push_customs_update(
|
||||
) -> ZResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::Customs(customs);
|
||||
let update = Update::customs(customs);
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -2,10 +2,11 @@ use std::collections::HashMap;
|
||||
|
||||
use fst::{set::OpBuilder, SetBuilder};
|
||||
use sdset::{duo::Union, SetOperation};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::{UpdateEvent, UpdateEventsEmitter};
|
||||
use crate::raw_indexer::RawIndexer;
|
||||
use crate::serde::{extract_document_id, serialize_value, Serializer};
|
||||
use crate::serde::{extract_document_id, serialize_value, Deserializer, Serializer};
|
||||
use crate::store;
|
||||
use crate::update::{apply_documents_deletion, next_update_id, Update};
|
||||
use crate::{Error, MResult, RankedMap};
|
||||
@ -13,21 +14,37 @@ use crate::{Error, MResult, RankedMap};
|
||||
pub struct DocumentsAddition<D> {
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
documents: Vec<D>,
|
||||
is_partial: bool,
|
||||
}
|
||||
|
||||
impl<D> DocumentsAddition<D> {
|
||||
pub fn new(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> DocumentsAddition<D> {
|
||||
DocumentsAddition {
|
||||
updates_store,
|
||||
updates_results_store,
|
||||
updates_notifier,
|
||||
documents: Vec::new(),
|
||||
is_partial: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_partial(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> DocumentsAddition<D> {
|
||||
DocumentsAddition {
|
||||
updates_store,
|
||||
updates_results_store,
|
||||
updates_notifier,
|
||||
documents: Vec::new(),
|
||||
is_partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,12 +56,13 @@ impl<D> DocumentsAddition<D> {
|
||||
where
|
||||
D: serde::Serialize,
|
||||
{
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
let update_id = push_documents_addition(
|
||||
writer,
|
||||
self.updates_store,
|
||||
self.updates_results_store,
|
||||
self.documents,
|
||||
self.is_partial,
|
||||
)?;
|
||||
Ok(update_id)
|
||||
}
|
||||
@ -61,6 +79,7 @@ pub fn push_documents_addition<D: serde::Serialize>(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
addition: Vec<D>,
|
||||
is_partial: bool,
|
||||
) -> MResult<u64> {
|
||||
let mut values = Vec::with_capacity(addition.len());
|
||||
for add in addition {
|
||||
@ -71,7 +90,12 @@ pub fn push_documents_addition<D: serde::Serialize>(
|
||||
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::DocumentsAddition(values);
|
||||
let update = if is_partial {
|
||||
Update::documents_partial(values)
|
||||
} else {
|
||||
Update::documents_addition(values)
|
||||
};
|
||||
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
||||
@ -84,7 +108,7 @@ pub fn apply_documents_addition<'a, 'b>(
|
||||
documents_fields_counts_store: store::DocumentsFieldsCounts,
|
||||
postings_lists_store: store::PostingsLists,
|
||||
docs_words_store: store::DocsWords,
|
||||
addition: Vec<serde_json::Value>,
|
||||
addition: Vec<HashMap<String, serde_json::Value>>,
|
||||
) -> MResult<()> {
|
||||
let mut documents_additions = HashMap::new();
|
||||
|
||||
@ -156,6 +180,102 @@ pub fn apply_documents_addition<'a, 'b>(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn apply_documents_partial_addition<'a, 'b>(
|
||||
writer: &'a mut heed::RwTxn<'b>,
|
||||
main_store: store::Main,
|
||||
documents_fields_store: store::DocumentsFields,
|
||||
documents_fields_counts_store: store::DocumentsFieldsCounts,
|
||||
postings_lists_store: store::PostingsLists,
|
||||
docs_words_store: store::DocsWords,
|
||||
addition: Vec<HashMap<String, serde_json::Value>>,
|
||||
) -> MResult<()> {
|
||||
let mut documents_additions = HashMap::new();
|
||||
|
||||
let schema = match main_store.schema(writer)? {
|
||||
Some(schema) => schema,
|
||||
None => return Err(Error::SchemaMissing),
|
||||
};
|
||||
|
||||
let identifier = schema.identifier_name();
|
||||
|
||||
// 1. store documents ids for future deletion
|
||||
for mut document in addition {
|
||||
let document_id = match extract_document_id(identifier, &document)? {
|
||||
Some(id) => id,
|
||||
None => return Err(Error::MissingDocumentId),
|
||||
};
|
||||
|
||||
let mut deserializer = Deserializer {
|
||||
document_id,
|
||||
reader: writer,
|
||||
documents_fields: documents_fields_store,
|
||||
schema: &schema,
|
||||
attributes: None,
|
||||
};
|
||||
|
||||
// retrieve the old document and
|
||||
// update the new one with missing keys found in the old one
|
||||
let result = Option::<HashMap<String, serde_json::Value>>::deserialize(&mut deserializer)?;
|
||||
if let Some(old_document) = result {
|
||||
for (key, value) in old_document {
|
||||
document.entry(key).or_insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
documents_additions.insert(document_id, document);
|
||||
}
|
||||
|
||||
// 2. remove the documents posting lists
|
||||
let number_of_inserted_documents = documents_additions.len();
|
||||
let documents_ids = documents_additions.iter().map(|(id, _)| *id).collect();
|
||||
apply_documents_deletion(
|
||||
writer,
|
||||
main_store,
|
||||
documents_fields_store,
|
||||
documents_fields_counts_store,
|
||||
postings_lists_store,
|
||||
docs_words_store,
|
||||
documents_ids,
|
||||
)?;
|
||||
|
||||
let mut ranked_map = match main_store.ranked_map(writer)? {
|
||||
Some(ranked_map) => ranked_map,
|
||||
None => RankedMap::default(),
|
||||
};
|
||||
|
||||
let stop_words = match main_store.stop_words_fst(writer)? {
|
||||
Some(stop_words) => stop_words,
|
||||
None => fst::Set::default(),
|
||||
};
|
||||
|
||||
// 3. index the documents fields in the stores
|
||||
let mut indexer = RawIndexer::new(stop_words);
|
||||
|
||||
for (document_id, document) in documents_additions {
|
||||
let serializer = Serializer {
|
||||
txn: writer,
|
||||
schema: &schema,
|
||||
document_store: documents_fields_store,
|
||||
document_fields_counts: documents_fields_counts_store,
|
||||
indexer: &mut indexer,
|
||||
ranked_map: &mut ranked_map,
|
||||
document_id,
|
||||
};
|
||||
|
||||
document.serialize(serializer)?;
|
||||
}
|
||||
|
||||
write_documents_addition_index(
|
||||
writer,
|
||||
main_store,
|
||||
postings_lists_store,
|
||||
docs_words_store,
|
||||
&ranked_map,
|
||||
number_of_inserted_documents,
|
||||
indexer,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn reindex_all_documents(
|
||||
writer: &mut heed::RwTxn,
|
||||
main_store: store::Main,
|
@ -1,9 +1,10 @@
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
|
||||
use fst::{SetBuilder, Streamer};
|
||||
use meilidb_schema::Schema;
|
||||
use meilisearch_schema::Schema;
|
||||
use sdset::{duo::DifferenceByKey, SetBuf, SetOperation};
|
||||
|
||||
use crate::database::{UpdateEvent, UpdateEventsEmitter};
|
||||
use crate::serde::extract_document_id;
|
||||
use crate::store;
|
||||
use crate::update::{next_update_id, Update};
|
||||
@ -12,7 +13,7 @@ use crate::{DocumentId, Error, MResult, RankedMap};
|
||||
pub struct DocumentsDeletion {
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
documents: Vec<DocumentId>,
|
||||
}
|
||||
|
||||
@ -20,7 +21,7 @@ impl DocumentsDeletion {
|
||||
pub fn new(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> DocumentsDeletion {
|
||||
DocumentsDeletion {
|
||||
updates_store,
|
||||
@ -50,7 +51,7 @@ impl DocumentsDeletion {
|
||||
}
|
||||
|
||||
pub fn finalize(self, writer: &mut heed::RwTxn) -> MResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
let update_id = push_documents_deletion(
|
||||
writer,
|
||||
self.updates_store,
|
||||
@ -75,7 +76,7 @@ pub fn push_documents_deletion(
|
||||
) -> MResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::DocumentsDeletion(deletion);
|
||||
let update = Update::documents_deletion(deletion);
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -10,7 +10,9 @@ mod synonyms_deletion;
|
||||
|
||||
pub use self::clear_all::{apply_clear_all, push_clear_all};
|
||||
pub use self::customs_update::{apply_customs_update, push_customs_update};
|
||||
pub use self::documents_addition::{apply_documents_addition, DocumentsAddition};
|
||||
pub use self::documents_addition::{
|
||||
apply_documents_addition, apply_documents_partial_addition, DocumentsAddition,
|
||||
};
|
||||
pub use self::documents_deletion::{apply_documents_deletion, DocumentsDeletion};
|
||||
pub use self::schema_update::{apply_schema_update, push_schema_update};
|
||||
pub use self::stop_words_addition::{apply_stop_words_addition, StopWordsAddition};
|
||||
@ -19,22 +21,102 @@ pub use self::synonyms_addition::{apply_synonyms_addition, SynonymsAddition};
|
||||
pub use self::synonyms_deletion::{apply_synonyms_deletion, SynonymsDeletion};
|
||||
|
||||
use std::cmp;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::time::Instant;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use heed::Result as ZResult;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{store, DocumentId, MResult};
|
||||
use meilidb_schema::Schema;
|
||||
use meilisearch_schema::Schema;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Update {
|
||||
pub struct Update {
|
||||
data: UpdateData,
|
||||
enqueued_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
fn clear_all() -> Update {
|
||||
Update {
|
||||
data: UpdateData::ClearAll,
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn schema(data: Schema) -> Update {
|
||||
Update {
|
||||
data: UpdateData::Schema(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn customs(data: Vec<u8>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::Customs(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn documents_addition(data: Vec<HashMap<String, serde_json::Value>>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::DocumentsAddition(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn documents_partial(data: Vec<HashMap<String, serde_json::Value>>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::DocumentsPartial(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn documents_deletion(data: Vec<DocumentId>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::DocumentsDeletion(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn synonyms_addition(data: BTreeMap<String, Vec<String>>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::SynonymsAddition(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn synonyms_deletion(data: BTreeMap<String, Option<Vec<String>>>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::SynonymsDeletion(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_words_addition(data: BTreeSet<String>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::StopWordsAddition(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_words_deletion(data: BTreeSet<String>) -> Update {
|
||||
Update {
|
||||
data: UpdateData::StopWordsDeletion(data),
|
||||
enqueued_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum UpdateData {
|
||||
ClearAll,
|
||||
Schema(Schema),
|
||||
Customs(Vec<u8>),
|
||||
DocumentsAddition(Vec<serde_json::Value>),
|
||||
DocumentsAddition(Vec<HashMap<String, serde_json::Value>>),
|
||||
DocumentsPartial(Vec<HashMap<String, serde_json::Value>>),
|
||||
DocumentsDeletion(Vec<DocumentId>),
|
||||
SynonymsAddition(BTreeMap<String, Vec<String>>),
|
||||
SynonymsDeletion(BTreeMap<String, Option<Vec<String>>>),
|
||||
@ -42,30 +124,31 @@ pub enum Update {
|
||||
StopWordsDeletion(BTreeSet<String>),
|
||||
}
|
||||
|
||||
impl Update {
|
||||
impl UpdateData {
|
||||
pub fn update_type(&self) -> UpdateType {
|
||||
match self {
|
||||
Update::ClearAll => UpdateType::ClearAll,
|
||||
Update::Schema(schema) => UpdateType::Schema {
|
||||
schema: schema.clone(),
|
||||
},
|
||||
Update::Customs(_) => UpdateType::Customs,
|
||||
Update::DocumentsAddition(addition) => UpdateType::DocumentsAddition {
|
||||
UpdateData::ClearAll => UpdateType::ClearAll,
|
||||
UpdateData::Schema(_) => UpdateType::Schema,
|
||||
UpdateData::Customs(_) => UpdateType::Customs,
|
||||
UpdateData::DocumentsAddition(addition) => UpdateType::DocumentsAddition {
|
||||
number: addition.len(),
|
||||
},
|
||||
Update::DocumentsDeletion(deletion) => UpdateType::DocumentsDeletion {
|
||||
UpdateData::DocumentsPartial(addition) => UpdateType::DocumentsPartial {
|
||||
number: addition.len(),
|
||||
},
|
||||
UpdateData::DocumentsDeletion(deletion) => UpdateType::DocumentsDeletion {
|
||||
number: deletion.len(),
|
||||
},
|
||||
Update::SynonymsAddition(addition) => UpdateType::SynonymsAddition {
|
||||
UpdateData::SynonymsAddition(addition) => UpdateType::SynonymsAddition {
|
||||
number: addition.len(),
|
||||
},
|
||||
Update::SynonymsDeletion(deletion) => UpdateType::SynonymsDeletion {
|
||||
UpdateData::SynonymsDeletion(deletion) => UpdateType::SynonymsDeletion {
|
||||
number: deletion.len(),
|
||||
},
|
||||
Update::StopWordsAddition(addition) => UpdateType::StopWordsAddition {
|
||||
UpdateData::StopWordsAddition(addition) => UpdateType::StopWordsAddition {
|
||||
number: addition.len(),
|
||||
},
|
||||
Update::StopWordsDeletion(deletion) => UpdateType::StopWordsDeletion {
|
||||
UpdateData::StopWordsDeletion(deletion) => UpdateType::StopWordsDeletion {
|
||||
number: deletion.len(),
|
||||
},
|
||||
}
|
||||
@ -73,11 +156,13 @@ impl Update {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "name")]
|
||||
pub enum UpdateType {
|
||||
ClearAll,
|
||||
Schema { schema: Schema },
|
||||
Schema,
|
||||
Customs,
|
||||
DocumentsAddition { number: usize },
|
||||
DocumentsPartial { number: usize },
|
||||
DocumentsDeletion { number: usize },
|
||||
SynonymsAddition { number: usize },
|
||||
SynonymsDeletion { number: usize },
|
||||
@ -85,30 +170,36 @@ pub enum UpdateType {
|
||||
StopWordsDeletion { number: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DetailedDuration {
|
||||
pub main: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcessedUpdateResult {
|
||||
pub update_id: u64,
|
||||
#[serde(rename = "type")]
|
||||
pub update_type: UpdateType,
|
||||
pub result: Result<(), String>,
|
||||
pub detailed_duration: DetailedDuration,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
pub duration: f64, // in seconds
|
||||
pub enqueued_at: DateTime<Utc>,
|
||||
pub processed_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnqueuedUpdateResult {
|
||||
pub update_id: u64,
|
||||
pub update_type: UpdateType,
|
||||
pub enqueued_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "status")]
|
||||
pub enum UpdateStatus {
|
||||
Enqueued(EnqueuedUpdateResult),
|
||||
Processed(ProcessedUpdateResult),
|
||||
Unknown,
|
||||
Enqueued {
|
||||
#[serde(flatten)]
|
||||
content: EnqueuedUpdateResult,
|
||||
},
|
||||
Processed {
|
||||
#[serde(flatten)]
|
||||
content: ProcessedUpdateResult,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn update_status(
|
||||
@ -116,19 +207,19 @@ pub fn update_status(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
update_id: u64,
|
||||
) -> MResult<UpdateStatus> {
|
||||
) -> MResult<Option<UpdateStatus>> {
|
||||
match updates_results_store.update_result(reader, update_id)? {
|
||||
Some(result) => Ok(UpdateStatus::Processed(result)),
|
||||
None => {
|
||||
if let Some(update) = updates_store.get(reader, update_id)? {
|
||||
Ok(UpdateStatus::Enqueued(EnqueuedUpdateResult {
|
||||
Some(result) => Ok(Some(UpdateStatus::Processed { content: result })),
|
||||
None => match updates_store.get(reader, update_id)? {
|
||||
Some(update) => Ok(Some(UpdateStatus::Enqueued {
|
||||
content: EnqueuedUpdateResult {
|
||||
update_id,
|
||||
update_type: update.update_type(),
|
||||
}))
|
||||
} else {
|
||||
Ok(UpdateStatus::Unknown)
|
||||
}
|
||||
}
|
||||
update_type: update.data.update_type(),
|
||||
enqueued_at: update.enqueued_at,
|
||||
},
|
||||
})),
|
||||
None => Ok(None),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,8 +248,10 @@ pub fn update_task<'a, 'b>(
|
||||
) -> MResult<ProcessedUpdateResult> {
|
||||
debug!("Processing update number {}", update_id);
|
||||
|
||||
let (update_type, result, duration) = match update {
|
||||
Update::ClearAll => {
|
||||
let Update { enqueued_at, data } = update;
|
||||
|
||||
let (update_type, result, duration) = match data {
|
||||
UpdateData::ClearAll => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::ClearAll;
|
||||
@ -173,12 +266,10 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::Schema(schema) => {
|
||||
UpdateData::Schema(schema) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::Schema {
|
||||
schema: schema.clone(),
|
||||
};
|
||||
let update_type = UpdateType::Schema;
|
||||
let result = apply_schema_update(
|
||||
writer,
|
||||
&schema,
|
||||
@ -191,7 +282,7 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::Customs(customs) => {
|
||||
UpdateData::Customs(customs) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::Customs;
|
||||
@ -199,7 +290,7 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::DocumentsAddition(documents) => {
|
||||
UpdateData::DocumentsAddition(documents) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::DocumentsAddition {
|
||||
@ -218,7 +309,26 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::DocumentsDeletion(documents) => {
|
||||
UpdateData::DocumentsPartial(documents) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::DocumentsPartial {
|
||||
number: documents.len(),
|
||||
};
|
||||
|
||||
let result = apply_documents_partial_addition(
|
||||
writer,
|
||||
index.main,
|
||||
index.documents_fields,
|
||||
index.documents_fields_counts,
|
||||
index.postings_lists,
|
||||
index.docs_words,
|
||||
documents,
|
||||
);
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
UpdateData::DocumentsDeletion(documents) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::DocumentsDeletion {
|
||||
@ -237,7 +347,7 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::SynonymsAddition(synonyms) => {
|
||||
UpdateData::SynonymsAddition(synonyms) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::SynonymsAddition {
|
||||
@ -248,7 +358,7 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::SynonymsDeletion(synonyms) => {
|
||||
UpdateData::SynonymsDeletion(synonyms) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::SynonymsDeletion {
|
||||
@ -259,7 +369,7 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::StopWordsAddition(stop_words) => {
|
||||
UpdateData::StopWordsAddition(stop_words) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::StopWordsAddition {
|
||||
@ -271,7 +381,7 @@ pub fn update_task<'a, 'b>(
|
||||
|
||||
(update_type, result, start.elapsed())
|
||||
}
|
||||
Update::StopWordsDeletion(stop_words) => {
|
||||
UpdateData::StopWordsDeletion(stop_words) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let update_type = UpdateType::StopWordsDeletion {
|
||||
@ -297,12 +407,13 @@ pub fn update_task<'a, 'b>(
|
||||
update_id, update_type, result
|
||||
);
|
||||
|
||||
let detailed_duration = DetailedDuration { main: duration };
|
||||
let status = ProcessedUpdateResult {
|
||||
update_id,
|
||||
update_type,
|
||||
result: result.map_err(|e| e.to_string()),
|
||||
detailed_duration,
|
||||
error: result.map_err(|e| e.to_string()).err(),
|
||||
duration: duration.as_secs_f64(),
|
||||
enqueued_at,
|
||||
processed_at: Utc::now(),
|
||||
};
|
||||
|
||||
Ok(status)
|
@ -1,4 +1,4 @@
|
||||
use meilidb_schema::{Diff, Schema};
|
||||
use meilisearch_schema::{Diff, Schema};
|
||||
|
||||
use crate::update::documents_addition::reindex_all_documents;
|
||||
use crate::update::{next_update_id, Update};
|
||||
@ -14,14 +14,14 @@ pub fn apply_schema_update(
|
||||
docs_words_store: store::DocsWords,
|
||||
) -> MResult<()> {
|
||||
use UnsupportedOperation::{
|
||||
CannotIntroduceNewSchemaAttribute, CannotRemoveSchemaAttribute,
|
||||
CanOnlyIntroduceNewSchemaAttributesAtEnd, CannotRemoveSchemaAttribute,
|
||||
CannotReorderSchemaAttribute, CannotUpdateSchemaIdentifier,
|
||||
};
|
||||
|
||||
let mut need_full_reindexing = false;
|
||||
|
||||
if let Some(old_schema) = main_store.schema(writer)? {
|
||||
for diff in meilidb_schema::diff(&old_schema, new_schema) {
|
||||
for diff in meilisearch_schema::diff(&old_schema, new_schema) {
|
||||
match diff {
|
||||
Diff::IdentChange { .. } => return Err(CannotUpdateSchemaIdentifier.into()),
|
||||
Diff::AttrMove { .. } => return Err(CannotReorderSchemaAttribute.into()),
|
||||
@ -33,7 +33,12 @@ pub fn apply_schema_update(
|
||||
need_full_reindexing = true;
|
||||
}
|
||||
}
|
||||
Diff::NewAttr { .. } => return Err(CannotIntroduceNewSchemaAttribute.into()),
|
||||
Diff::NewAttr { pos, .. } => {
|
||||
// new attribute not at the end of the schema
|
||||
if pos < old_schema.number_of_attributes() {
|
||||
return Err(CanOnlyIntroduceNewSchemaAttributesAtEnd.into());
|
||||
}
|
||||
}
|
||||
Diff::RemovedAttr { .. } => return Err(CannotRemoveSchemaAttribute.into()),
|
||||
}
|
||||
}
|
||||
@ -63,7 +68,7 @@ pub fn push_schema_update(
|
||||
) -> MResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::Schema(schema);
|
||||
let update = Update::schema(schema);
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -3,13 +3,14 @@ use std::collections::BTreeSet;
|
||||
use fst::{set::OpBuilder, SetBuilder};
|
||||
|
||||
use crate::automaton::normalize_str;
|
||||
use crate::database::{UpdateEvent, UpdateEventsEmitter};
|
||||
use crate::update::{next_update_id, Update};
|
||||
use crate::{store, MResult};
|
||||
|
||||
pub struct StopWordsAddition {
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
stop_words: BTreeSet<String>,
|
||||
}
|
||||
|
||||
@ -17,7 +18,7 @@ impl StopWordsAddition {
|
||||
pub fn new(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> StopWordsAddition {
|
||||
StopWordsAddition {
|
||||
updates_store,
|
||||
@ -33,7 +34,7 @@ impl StopWordsAddition {
|
||||
}
|
||||
|
||||
pub fn finalize(self, writer: &mut heed::RwTxn) -> MResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
let update_id = push_stop_words_addition(
|
||||
writer,
|
||||
self.updates_store,
|
||||
@ -52,7 +53,7 @@ pub fn push_stop_words_addition(
|
||||
) -> MResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::StopWordsAddition(addition);
|
||||
let update = Update::stop_words_addition(addition);
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -3,6 +3,7 @@ use std::collections::BTreeSet;
|
||||
use fst::{set::OpBuilder, SetBuilder};
|
||||
|
||||
use crate::automaton::normalize_str;
|
||||
use crate::database::{UpdateEvent, UpdateEventsEmitter};
|
||||
use crate::update::documents_addition::reindex_all_documents;
|
||||
use crate::update::{next_update_id, Update};
|
||||
use crate::{store, MResult};
|
||||
@ -10,7 +11,7 @@ use crate::{store, MResult};
|
||||
pub struct StopWordsDeletion {
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
stop_words: BTreeSet<String>,
|
||||
}
|
||||
|
||||
@ -18,7 +19,7 @@ impl StopWordsDeletion {
|
||||
pub fn new(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> StopWordsDeletion {
|
||||
StopWordsDeletion {
|
||||
updates_store,
|
||||
@ -34,7 +35,7 @@ impl StopWordsDeletion {
|
||||
}
|
||||
|
||||
pub fn finalize(self, writer: &mut heed::RwTxn) -> MResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
let update_id = push_stop_words_deletion(
|
||||
writer,
|
||||
self.updates_store,
|
||||
@ -53,7 +54,7 @@ pub fn push_stop_words_deletion(
|
||||
) -> MResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::StopWordsDeletion(deletion);
|
||||
let update = Update::stop_words_deletion(deletion);
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -4,13 +4,14 @@ use fst::{set::OpBuilder, SetBuilder};
|
||||
use sdset::SetBuf;
|
||||
|
||||
use crate::automaton::normalize_str;
|
||||
use crate::database::{UpdateEvent, UpdateEventsEmitter};
|
||||
use crate::update::{next_update_id, Update};
|
||||
use crate::{store, MResult};
|
||||
|
||||
pub struct SynonymsAddition {
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
synonyms: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
@ -18,7 +19,7 @@ impl SynonymsAddition {
|
||||
pub fn new(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> SynonymsAddition {
|
||||
SynonymsAddition {
|
||||
updates_store,
|
||||
@ -43,7 +44,7 @@ impl SynonymsAddition {
|
||||
}
|
||||
|
||||
pub fn finalize(self, writer: &mut heed::RwTxn) -> MResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
let update_id = push_synonyms_addition(
|
||||
writer,
|
||||
self.updates_store,
|
||||
@ -62,7 +63,7 @@ pub fn push_synonyms_addition(
|
||||
) -> MResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::SynonymsAddition(addition);
|
||||
let update = Update::synonyms_addition(addition);
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -5,13 +5,14 @@ use fst::{set::OpBuilder, SetBuilder};
|
||||
use sdset::SetBuf;
|
||||
|
||||
use crate::automaton::normalize_str;
|
||||
use crate::database::{UpdateEvent, UpdateEventsEmitter};
|
||||
use crate::update::{next_update_id, Update};
|
||||
use crate::{store, MResult};
|
||||
|
||||
pub struct SynonymsDeletion {
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
synonyms: BTreeMap<String, Option<Vec<String>>>,
|
||||
}
|
||||
|
||||
@ -19,7 +20,7 @@ impl SynonymsDeletion {
|
||||
pub fn new(
|
||||
updates_store: store::Updates,
|
||||
updates_results_store: store::UpdatesResults,
|
||||
updates_notifier: crossbeam_channel::Sender<()>,
|
||||
updates_notifier: UpdateEventsEmitter,
|
||||
) -> SynonymsDeletion {
|
||||
SynonymsDeletion {
|
||||
updates_store,
|
||||
@ -50,7 +51,7 @@ impl SynonymsDeletion {
|
||||
}
|
||||
|
||||
pub fn finalize(self, writer: &mut heed::RwTxn) -> MResult<u64> {
|
||||
let _ = self.updates_notifier.send(());
|
||||
let _ = self.updates_notifier.send(UpdateEvent::NewUpdate);
|
||||
let update_id = push_synonyms_deletion(
|
||||
writer,
|
||||
self.updates_store,
|
||||
@ -69,7 +70,7 @@ pub fn push_synonyms_deletion(
|
||||
) -> MResult<u64> {
|
||||
let last_update_id = next_update_id(writer, updates_store, updates_results_store)?;
|
||||
|
||||
let update = Update::SynonymsDeletion(deletion);
|
||||
let update = Update::synonyms_deletion(deletion);
|
||||
updates_store.put_update(writer, last_update_id, &update)?;
|
||||
|
||||
Ok(last_update_id)
|
@ -1,39 +1,45 @@
|
||||
[package]
|
||||
name = "meilidb-http"
|
||||
version = "0.1.0"
|
||||
name = "meilisearch-http"
|
||||
version = "0.8.0"
|
||||
authors = [
|
||||
"Quentin de Quelen <quentin@dequelen.me>",
|
||||
"Clément Renault <clement@meilisearch.com>",
|
||||
]
|
||||
edition = "2018"
|
||||
|
||||
[[bin]]
|
||||
name = "meilisearch"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.2.0"
|
||||
chrono = { version = "0.4.9", features = ["serde"] }
|
||||
crossbeam-channel = "0.3.9"
|
||||
envconfig = "0.5.1"
|
||||
envconfig_derive = "0.5.1"
|
||||
crossbeam-channel = "0.4.0"
|
||||
env_logger = "0.7.1"
|
||||
heed = "0.5.0"
|
||||
http = "0.1.19"
|
||||
indexmap = { version = "1.3.0", features = ["serde-1"] }
|
||||
jemallocator = "0.3.2"
|
||||
isahc = "0.7.6"
|
||||
log = "0.4.8"
|
||||
main_error = "0.1.0"
|
||||
meilidb-core = { path = "../meilidb-core", version = "0.6.0" }
|
||||
meilidb-schema = { path = "../meilidb-schema", version = "0.6.0" }
|
||||
meilisearch-core = { path = "../meilisearch-core", version = "0.8.0" }
|
||||
meilisearch-schema = { path = "../meilisearch-schema", version = "0.8.0" }
|
||||
pretty-bytes = "0.2.2"
|
||||
rand = "0.7.2"
|
||||
rayon = "1.2.0"
|
||||
serde = { version = "1.0.101", features = ["derive"] }
|
||||
serde_json = { version = "1.0.41", features = ["preserve_order"] }
|
||||
serde_qs = "0.5.1"
|
||||
siphasher = "0.3.1"
|
||||
structopt = "0.3.3"
|
||||
sysinfo = "0.9.5"
|
||||
walkdir = "2.2.9"
|
||||
whoami = "0.6"
|
||||
|
||||
[dependencies.async-compression]
|
||||
default-features = false
|
||||
features = ["stream", "gzip", "zlib", "brotli", "zstd"]
|
||||
version = "0.1.0-alpha.7"
|
||||
version = "=0.1.0-alpha.7"
|
||||
|
||||
[dependencies.tide]
|
||||
git = "https://github.com/rustasync/tide"
|
||||
@ -53,3 +59,6 @@ rev = "e77709370bb24cf776fe6da902467c35131535b1"
|
||||
|
||||
[build-dependencies]
|
||||
vergen = "3.0.4"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
jemallocator = "0.3.2"
|
73
meilisearch-http/src/analytics.rs
Normal file
73
meilisearch-http/src/analytics.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use log::error;
|
||||
use serde::Serialize;
|
||||
use serde_qs as qs;
|
||||
use siphasher::sip::SipHasher;
|
||||
|
||||
const AMPLITUDE_API_KEY: &str = "f7fba398780e06d8fe6666a9be7e3d47";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Event<'a> {
|
||||
user_id: &'a str,
|
||||
event_type: &'a str,
|
||||
device_id: &'a str,
|
||||
time: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AmplitudeRequest<'a> {
|
||||
api_key: &'a str,
|
||||
event: &'a str,
|
||||
}
|
||||
|
||||
pub fn analytics_sender() {
|
||||
let username = whoami::username();
|
||||
let hostname = whoami::hostname();
|
||||
let platform = whoami::platform();
|
||||
|
||||
let uid = username + &hostname + &platform.to_string();
|
||||
|
||||
let mut hasher = SipHasher::new();
|
||||
uid.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
let uid = format!("{:X}", hash);
|
||||
let platform = platform.to_string();
|
||||
|
||||
loop {
|
||||
let n = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let user_id = &uid;
|
||||
let device_id = &platform;
|
||||
let time = n.as_secs();
|
||||
let event_type = "runtime_tick";
|
||||
|
||||
let event = Event {
|
||||
user_id,
|
||||
event_type,
|
||||
device_id,
|
||||
time,
|
||||
};
|
||||
let event = serde_json::to_string(&event).unwrap();
|
||||
|
||||
let request = AmplitudeRequest {
|
||||
api_key: AMPLITUDE_API_KEY,
|
||||
event: &event,
|
||||
};
|
||||
|
||||
let body = qs::to_string(&request).unwrap();
|
||||
match isahc::post("https://api.amplitude.com/httpapi", body) {
|
||||
Ok(response) => {
|
||||
if !response.status().is_success() {
|
||||
let body = response.into_body().text().unwrap();
|
||||
error!("Unsuccessful call to Amplitude: {}", body);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Error while sending a request to Amplitude: {}", e),
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_secs(86_400)) // one day
|
||||
}
|
||||
}
|
129
meilisearch-http/src/data.rs
Normal file
129
meilisearch-http/src/data.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use heed::types::{SerdeBincode, Str};
|
||||
use log::error;
|
||||
use meilisearch_core::{Database, Error as MError, MResult};
|
||||
use sysinfo::Pid;
|
||||
|
||||
use crate::option::Opt;
|
||||
use crate::routes::index::index_update_callback;
|
||||
|
||||
const LAST_UPDATE_KEY: &str = "last-update";
|
||||
|
||||
type SerdeDatetime = SerdeBincode<DateTime<Utc>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Data {
|
||||
inner: Arc<DataInner>,
|
||||
}
|
||||
|
||||
impl Deref for Data {
|
||||
type Target = DataInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DataInner {
|
||||
pub db: Arc<Database>,
|
||||
pub db_path: String,
|
||||
pub api_key: Option<String>,
|
||||
pub server_pid: Pid,
|
||||
}
|
||||
|
||||
impl DataInner {
|
||||
pub fn is_indexing(&self, reader: &heed::RoTxn, index: &str) -> MResult<Option<bool>> {
|
||||
match self.db.open_index(&index) {
|
||||
Some(index) => index.current_update_id(&reader).map(|u| Some(u.is_some())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_update(&self, reader: &heed::RoTxn) -> MResult<Option<DateTime<Utc>>> {
|
||||
match self
|
||||
.db
|
||||
.common_store()
|
||||
.get::<Str, SerdeDatetime>(reader, LAST_UPDATE_KEY)?
|
||||
{
|
||||
Some(datetime) => Ok(Some(datetime)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_last_update(&self, writer: &mut heed::RwTxn) -> MResult<()> {
|
||||
self.db
|
||||
.common_store()
|
||||
.put::<Str, SerdeDatetime>(writer, LAST_UPDATE_KEY, &Utc::now())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn compute_stats(&self, writer: &mut heed::RwTxn, index_uid: &str) -> MResult<()> {
|
||||
let index = match self.db.open_index(&index_uid) {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
error!("Impossible to retrieve index {}", index_uid);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let schema = match index.main.schema(&writer)? {
|
||||
Some(schema) => schema,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let all_documents_fields = index
|
||||
.documents_fields_counts
|
||||
.all_documents_fields_counts(&writer)?;
|
||||
|
||||
// count fields frequencies
|
||||
let mut fields_frequency = HashMap::<_, usize>::new();
|
||||
for result in all_documents_fields {
|
||||
let (_, attr, _) = result?;
|
||||
*fields_frequency.entry(attr).or_default() += 1;
|
||||
}
|
||||
|
||||
// convert attributes to their names
|
||||
let frequency: HashMap<_, _> = fields_frequency
|
||||
.into_iter()
|
||||
.map(|(a, c)| (schema.attribute_name(a).to_owned(), c))
|
||||
.collect();
|
||||
|
||||
index
|
||||
.main
|
||||
.put_fields_frequency(writer, &frequency)
|
||||
.map_err(MError::Zlmdb)
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn new(opt: Opt) -> Data {
|
||||
let db_path = opt.db_path.clone();
|
||||
let api_key = opt.api_key.clone();
|
||||
let server_pid = sysinfo::get_current_pid().unwrap();
|
||||
|
||||
let db = Arc::new(Database::open_or_create(opt.db_path.clone()).unwrap());
|
||||
|
||||
let inner_data = DataInner {
|
||||
db: db.clone(),
|
||||
db_path,
|
||||
api_key,
|
||||
server_pid,
|
||||
};
|
||||
|
||||
let data = Data {
|
||||
inner: Arc::new(inner_data),
|
||||
};
|
||||
|
||||
let callback_context = data.clone();
|
||||
db.set_update_callback(Box::new(move |index_uid, status| {
|
||||
index_update_callback(&index_uid, &callback_context, status);
|
||||
}));
|
||||
|
||||
data
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ pub enum ResponseError {
|
||||
DocumentNotFound(String),
|
||||
MissingHeader(String),
|
||||
BadParameter(String, String),
|
||||
OpenIndex(String),
|
||||
CreateIndex(String),
|
||||
Maintenance,
|
||||
}
|
||||
@ -54,6 +55,10 @@ impl ResponseError {
|
||||
ResponseError::BadParameter(name.to_string(), message.to_string())
|
||||
}
|
||||
|
||||
pub fn open_index(message: impl Display) -> ResponseError {
|
||||
ResponseError::OpenIndex(message.to_string())
|
||||
}
|
||||
|
||||
pub fn create_index(message: impl Display) -> ResponseError {
|
||||
ResponseError::CreateIndex(message.to_string())
|
||||
}
|
||||
@ -96,6 +101,10 @@ impl IntoResponse for ResponseError {
|
||||
format!("Impossible to create index; {}", err),
|
||||
StatusCode::BAD_REQUEST,
|
||||
),
|
||||
ResponseError::OpenIndex(err) => error(
|
||||
format!("Impossible to open index; {}", err),
|
||||
StatusCode::BAD_REQUEST,
|
||||
),
|
||||
ResponseError::Maintenance => error(
|
||||
String::from("Server is in maintenance, please try again later"),
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
@ -1,10 +1,10 @@
|
||||
use crate::routes::setting::{RankingOrdering, SettingBody};
|
||||
use indexmap::IndexMap;
|
||||
use log::*;
|
||||
use meilidb_core::criterion::*;
|
||||
use meilidb_core::Highlight;
|
||||
use meilidb_core::{Index, RankedMap};
|
||||
use meilidb_schema::{Schema, SchemaAttr};
|
||||
use log::error;
|
||||
use meilisearch_core::criterion::*;
|
||||
use meilisearch_core::Highlight;
|
||||
use meilisearch_core::{Index, RankedMap};
|
||||
use meilisearch_schema::{Schema, SchemaAttr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::cmp::Ordering;
|
||||
@ -57,8 +57,8 @@ impl fmt::Display for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<meilidb_core::Error> for Error {
|
||||
fn from(error: meilidb_core::Error) -> Self {
|
||||
impl From<meilisearch_core::Error> for Error {
|
||||
fn from(error: meilisearch_core::Error) -> Self {
|
||||
Error::Internal(error.to_string())
|
||||
}
|
||||
}
|
||||
@ -131,6 +131,12 @@ impl<'a> SearchBuilder<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_attribute_to_search_in(&mut self, value: String) -> &SearchBuilder {
|
||||
let attributes_to_search_in = self.attributes_to_search_in.get_or_insert(HashSet::new());
|
||||
attributes_to_search_in.insert(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attributes_to_highlight(&mut self, value: HashSet<String>) -> &SearchBuilder {
|
||||
self.attributes_to_highlight = Some(value);
|
||||
self
|
||||
@ -229,43 +235,35 @@ impl<'a> SearchBuilder<'a> {
|
||||
}
|
||||
fields = Some(set);
|
||||
}
|
||||
let mut document: IndexMap<String, Value> = self
|
||||
|
||||
let document: IndexMap<String, Value> = self
|
||||
.index
|
||||
.document(reader, fields.as_ref(), doc.id)
|
||||
.map_err(|e| Error::RetrieveDocument(doc.id.0, e.to_string()))?
|
||||
.ok_or(Error::DocumentNotFound(doc.id.0))?;
|
||||
|
||||
let mut formatted = document.clone();
|
||||
let mut matches = doc.highlights.clone();
|
||||
|
||||
// Crops fields if needed
|
||||
if let Some(fields) = self.attributes_to_crop.clone() {
|
||||
for (field, length) in fields {
|
||||
let _ = crop_document(&mut document, &mut matches, &schema, &field, length);
|
||||
}
|
||||
if let Some(fields) = &self.attributes_to_crop {
|
||||
crop_document(&mut formatted, &mut matches, &schema, fields);
|
||||
}
|
||||
|
||||
// Transform to readable matches
|
||||
let matches = calculate_matches(matches, self.attributes_to_retrieve.clone(), &schema);
|
||||
|
||||
if !self.matches {
|
||||
if let Some(attributes_to_highlight) = self.attributes_to_highlight.clone() {
|
||||
let highlights = calculate_highlights(
|
||||
document.clone(),
|
||||
matches.clone(),
|
||||
attributes_to_highlight,
|
||||
);
|
||||
for (key, value) in highlights {
|
||||
if let Some(content) = document.get_mut(&key) {
|
||||
*content = value;
|
||||
}
|
||||
}
|
||||
if let Some(attributes_to_highlight) = &self.attributes_to_highlight {
|
||||
formatted = calculate_highlights(&formatted, &matches, attributes_to_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
let matches_info = if self.matches { Some(matches) } else { None };
|
||||
|
||||
let hit = SearchHit {
|
||||
hit: document,
|
||||
document,
|
||||
formatted,
|
||||
matches_info,
|
||||
};
|
||||
|
||||
@ -382,7 +380,9 @@ pub type MatchesInfos = HashMap<String, Vec<MatchPosition>>;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchHit {
|
||||
#[serde(flatten)]
|
||||
pub hit: IndexMap<String, Value>,
|
||||
pub document: IndexMap<String, Value>,
|
||||
#[serde(rename = "_formatted", skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub formatted: IndexMap<String, Value>,
|
||||
#[serde(rename = "_matchesInfo", skip_serializing_if = "Option::is_none")]
|
||||
pub matches_info: Option<MatchesInfos>,
|
||||
}
|
||||
@ -425,32 +425,31 @@ fn crop_document(
|
||||
document: &mut IndexMap<String, Value>,
|
||||
matches: &mut Vec<Highlight>,
|
||||
schema: &Schema,
|
||||
field: &str,
|
||||
length: usize,
|
||||
) -> Result<(), Error> {
|
||||
fields: &HashMap<String, usize>,
|
||||
) {
|
||||
matches.sort_unstable_by_key(|m| (m.char_index, m.char_length));
|
||||
|
||||
let attribute = schema
|
||||
.attribute(field)
|
||||
.ok_or(Error::AttributeNotFoundOnSchema(field.to_string()))?;
|
||||
let selected_matches = matches
|
||||
.iter()
|
||||
.filter(|m| SchemaAttr::new(m.attribute) == attribute)
|
||||
.cloned();
|
||||
let original_text = match document.get(field) {
|
||||
Some(Value::String(text)) => text,
|
||||
Some(_) => return Err(Error::CropFieldWrongType(field.to_string())),
|
||||
None => return Err(Error::AttributeNotFoundOnDocument(field.to_string())),
|
||||
};
|
||||
let (cropped_text, cropped_matches) = crop_text(&original_text, selected_matches, length);
|
||||
for (field, length) in fields {
|
||||
let attribute = match schema.attribute(field) {
|
||||
Some(attribute) => attribute,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
document.insert(
|
||||
field.to_string(),
|
||||
serde_json::value::Value::String(cropped_text),
|
||||
);
|
||||
matches.retain(|m| SchemaAttr::new(m.attribute) != attribute);
|
||||
matches.extend_from_slice(&cropped_matches);
|
||||
Ok(())
|
||||
let selected_matches = matches
|
||||
.iter()
|
||||
.filter(|m| SchemaAttr::new(m.attribute) == attribute)
|
||||
.cloned();
|
||||
|
||||
if let Some(Value::String(ref mut original_text)) = document.get_mut(field) {
|
||||
let (cropped_text, cropped_matches) =
|
||||
crop_text(original_text, selected_matches, *length);
|
||||
|
||||
*original_text = cropped_text;
|
||||
|
||||
matches.retain(|m| SchemaAttr::new(m.attribute) != attribute);
|
||||
matches.extend_from_slice(&cropped_matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_matches(
|
||||
@ -490,13 +489,14 @@ fn calculate_matches(
|
||||
}
|
||||
|
||||
fn calculate_highlights(
|
||||
document: IndexMap<String, Value>,
|
||||
matches: MatchesInfos,
|
||||
attributes_to_highlight: HashSet<String>,
|
||||
) -> HighlightInfos {
|
||||
let mut highlight_result: HashMap<String, Value> = HashMap::new();
|
||||
document: &IndexMap<String, Value>,
|
||||
matches: &MatchesInfos,
|
||||
attributes_to_highlight: &HashSet<String>,
|
||||
) -> IndexMap<String, Value> {
|
||||
let mut highlight_result = IndexMap::new();
|
||||
|
||||
for (attribute, matches) in matches.iter() {
|
||||
if attributes_to_highlight.contains("*") || attributes_to_highlight.contains(attribute) {
|
||||
if attributes_to_highlight.contains(attribute) {
|
||||
if let Some(Value::String(value)) = document.get(attribute) {
|
||||
let value: Vec<_> = value.chars().collect();
|
||||
let mut highlighted_value = String::new();
|
||||
@ -521,6 +521,7 @@ fn calculate_highlights(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
highlight_result
|
||||
}
|
||||
|
||||
@ -537,9 +538,10 @@ mod tests {
|
||||
|
||||
let document: IndexMap<String, Value> = serde_json::from_str(data).unwrap();
|
||||
let mut attributes_to_highlight = HashSet::new();
|
||||
attributes_to_highlight.insert("*".to_string());
|
||||
attributes_to_highlight.insert("title".to_string());
|
||||
attributes_to_highlight.insert("description".to_string());
|
||||
|
||||
let mut matches: HashMap<String, Vec<MatchPosition>> = HashMap::new();
|
||||
let mut matches = HashMap::new();
|
||||
|
||||
let mut m = Vec::new();
|
||||
m.push(MatchPosition {
|
||||
@ -554,9 +556,9 @@ mod tests {
|
||||
length: 9,
|
||||
});
|
||||
matches.insert("description".to_string(), m);
|
||||
let result = super::calculate_highlights(document, matches, attributes_to_highlight);
|
||||
let result = super::calculate_highlights(&document, &matches, &attributes_to_highlight);
|
||||
|
||||
let mut result_expected = HashMap::new();
|
||||
let mut result_expected = IndexMap::new();
|
||||
result_expected.insert(
|
||||
"title".to_string(),
|
||||
Value::String("<em>Fondation</em> (Isaac ASIMOV)".to_string()),
|
2
meilisearch-http/src/helpers/mod.rs
Normal file
2
meilisearch-http/src/helpers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod meilisearch;
|
||||
pub mod tide;
|
@ -3,7 +3,7 @@ use crate::models::token::*;
|
||||
use crate::Data;
|
||||
use chrono::Utc;
|
||||
use heed::types::{SerdeBincode, Str};
|
||||
use meilidb_core::Index;
|
||||
use meilisearch_core::Index;
|
||||
use serde_json::Value;
|
||||
use tide::Context;
|
||||
|
||||
@ -17,13 +17,13 @@ pub trait ContextExt {
|
||||
|
||||
impl ContextExt for Context<Data> {
|
||||
fn is_allowed(&self, acl: ACL) -> SResult<()> {
|
||||
let admin_token = match &self.state().admin_token {
|
||||
Some(admin_token) => admin_token,
|
||||
let api_key = match &self.state().api_key {
|
||||
Some(api_key) => api_key,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let user_api_key = self.header("X-Meili-API-Key")?;
|
||||
if user_api_key == *admin_token {
|
||||
if user_api_key == *api_key {
|
||||
return Ok(());
|
||||
}
|
||||
let request_index: Option<String> = None; //self.param::<String>("index").ok();
|
||||
@ -38,9 +38,9 @@ impl ContextExt for Context<Data> {
|
||||
.common_store()
|
||||
.get::<Str, SerdeBincode<Token>>(&reader, &token_key)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::not_found(format!(
|
||||
"token key: {}",
|
||||
token_key
|
||||
.ok_or(ResponseError::invalid_token(format!(
|
||||
"Api key does not exist: {}",
|
||||
user_api_key
|
||||
)))?;
|
||||
|
||||
if token_config.revoked {
|
||||
@ -93,12 +93,12 @@ impl ContextExt for Context<Data> {
|
||||
}
|
||||
|
||||
fn index(&self) -> Result<Index, ResponseError> {
|
||||
let index_name = self.url_param("index")?;
|
||||
let index_uid = self.url_param("index")?;
|
||||
let index = self
|
||||
.state()
|
||||
.db
|
||||
.open_index(&index_name)
|
||||
.ok_or(ResponseError::index_not_found(index_name))?;
|
||||
.open_index(&index_uid)
|
||||
.ok_or(ResponseError::index_not_found(index_uid))?;
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ impl ContextExt for Context<Data> {
|
||||
let name = self
|
||||
.param::<Value>("identifier")
|
||||
.as_ref()
|
||||
.map(meilidb_core::serde::value_to_string)
|
||||
.map(meilisearch_core::serde::value_to_string)
|
||||
.map_err(|e| ResponseError::bad_parameter("identifier", e))?
|
||||
.ok_or(ResponseError::bad_parameter(
|
||||
"identifier",
|
@ -1,6 +1,3 @@
|
||||
#[macro_use]
|
||||
extern crate envconfig_derive;
|
||||
|
||||
pub mod data;
|
||||
pub mod error;
|
||||
pub mod helpers;
|
||||
@ -8,4 +5,4 @@ pub mod models;
|
||||
pub mod option;
|
||||
pub mod routes;
|
||||
|
||||
use self::data::Data;
|
||||
pub use self::data::Data;
|
@ -1,21 +1,39 @@
|
||||
use std::env::VarError::NotPresent;
|
||||
use std::{env, thread};
|
||||
|
||||
use http::header::HeaderValue;
|
||||
use log::info;
|
||||
use main_error::MainError;
|
||||
use structopt::StructOpt;
|
||||
use tide::middleware::{CorsMiddleware, CorsOrigin};
|
||||
use tide_log::RequestLogger;
|
||||
|
||||
use meilidb_http::data::Data;
|
||||
use meilidb_http::option::Opt;
|
||||
use meilidb_http::routes;
|
||||
use meilisearch_http::data::Data;
|
||||
use meilisearch_http::option::Opt;
|
||||
use meilisearch_http::routes;
|
||||
use meilisearch_http::routes::index::index_update_callback;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod analytics;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[global_allocator]
|
||||
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||
|
||||
pub fn main() -> Result<(), MainError> {
|
||||
let opt = Opt::new();
|
||||
env_logger::init();
|
||||
|
||||
let opt = Opt::from_args();
|
||||
let data = Data::new(opt.clone());
|
||||
|
||||
if env::var("MEILI_NO_ANALYTICS") == Err(NotPresent) {
|
||||
thread::spawn(|| analytics::analytics_sender());
|
||||
}
|
||||
|
||||
let data_cloned = data.clone();
|
||||
data.db.set_update_callback(Box::new(move |name, status| {
|
||||
index_update_callback(name, &data_cloned, status);
|
||||
}));
|
||||
|
||||
let mut app = tide::App::with_state(data);
|
||||
|
||||
app.middleware(
|
@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use meilidb_schema::{Schema, SchemaBuilder, SchemaProps};
|
||||
use meilisearch_schema::{Schema, SchemaBuilder, SchemaProps};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
20
meilisearch-http/src/option.rs
Normal file
20
meilisearch-http/src/option.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(Debug, Clone, StructOpt)]
|
||||
pub struct Opt {
|
||||
/// The destination where the database must be created.
|
||||
#[structopt(long, env = "MEILI_DB_PATH", default_value = "/tmp/meilisearch")]
|
||||
pub db_path: String,
|
||||
|
||||
/// The address on which the http server will listen.
|
||||
#[structopt(long, env = "MEILI_HTTP_ADDR", default_value = "127.0.0.1:8080")]
|
||||
pub http_addr: String,
|
||||
|
||||
/// The master key allowing you to do everything on the server.
|
||||
#[structopt(long, env = "MEILI_API_KEY")]
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Do not send analytics to Meili.
|
||||
#[structopt(long, env = "MEILI_NO_ANALYTICS")]
|
||||
pub no_analytics: bool,
|
||||
}
|
@ -19,7 +19,7 @@ pub async fn get_document(ctx: Context<Data>) -> SResult<Response> {
|
||||
let index = ctx.index()?;
|
||||
|
||||
let identifier = ctx.identifier()?;
|
||||
let document_id = meilidb_core::serde::compute_document_id(identifier.clone());
|
||||
let document_id = meilisearch_core::serde::compute_document_id(identifier.clone());
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
@ -45,13 +45,9 @@ pub struct IndexUpdateResponse {
|
||||
pub async fn delete_document(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
|
||||
if !ctx.state().accept_updates() {
|
||||
return Err(ResponseError::Maintenance);
|
||||
}
|
||||
|
||||
let index = ctx.index()?;
|
||||
let identifier = ctx.identifier()?;
|
||||
let document_id = meilidb_core::serde::compute_document_id(identifier.clone());
|
||||
let document_id = meilisearch_core::serde::compute_document_id(identifier.clone());
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
@ -78,7 +74,7 @@ struct BrowseQuery {
|
||||
attributes_to_retrieve: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn browse_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
pub async fn get_all_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
@ -118,19 +114,11 @@ pub async fn browse_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
if response_body.is_empty() {
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::NO_CONTENT)
|
||||
.into_response())
|
||||
} else {
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response())
|
||||
}
|
||||
Ok(tide::response::json(response_body))
|
||||
}
|
||||
|
||||
fn infered_schema(document: &IndexMap<String, Value>) -> Option<meilidb_schema::Schema> {
|
||||
use meilidb_schema::{SchemaBuilder, DISPLAYED, INDEXED};
|
||||
fn infered_schema(document: &IndexMap<String, Value>) -> Option<meilisearch_schema::Schema> {
|
||||
use meilisearch_schema::{SchemaBuilder, DISPLAYED, INDEXED};
|
||||
|
||||
let mut identifier = None;
|
||||
for key in document.keys() {
|
||||
@ -151,12 +139,9 @@ fn infered_schema(document: &IndexMap<String, Value>) -> Option<meilidb_schema::
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_or_update_multiple_documents(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
async fn update_multiple_documents(mut ctx: Context<Data>, is_partial: bool) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
|
||||
if !ctx.state().accept_updates() {
|
||||
return Err(ResponseError::Maintenance);
|
||||
}
|
||||
let data: Vec<IndexMap<String, Value>> =
|
||||
ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
let index = ctx.index()?;
|
||||
@ -179,7 +164,11 @@ pub async fn add_or_update_multiple_documents(mut ctx: Context<Data>) -> SResult
|
||||
}
|
||||
}
|
||||
|
||||
let mut document_addition = index.documents_addition();
|
||||
let mut document_addition = if is_partial {
|
||||
index.documents_partial_addition()
|
||||
} else {
|
||||
index.documents_addition()
|
||||
};
|
||||
|
||||
for document in data {
|
||||
document_addition.update_document(document);
|
||||
@ -197,11 +186,17 @@ pub async fn add_or_update_multiple_documents(mut ctx: Context<Data>) -> SResult
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn add_or_replace_multiple_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
update_multiple_documents(ctx, false).await
|
||||
}
|
||||
|
||||
pub async fn add_or_update_multiple_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
update_multiple_documents(ctx, true).await
|
||||
}
|
||||
|
||||
pub async fn delete_multiple_documents(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
if !ctx.state().accept_updates() {
|
||||
return Err(ResponseError::Maintenance);
|
||||
}
|
||||
|
||||
let data: Vec<Value> = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
@ -211,9 +206,9 @@ pub async fn delete_multiple_documents(mut ctx: Context<Data>) -> SResult<Respon
|
||||
let mut documents_deletion = index.documents_deletion();
|
||||
|
||||
for identifier in data {
|
||||
if let Some(identifier) = meilidb_core::serde::value_to_string(&identifier) {
|
||||
if let Some(identifier) = meilisearch_core::serde::value_to_string(&identifier) {
|
||||
documents_deletion
|
||||
.delete_document_by_id(meilidb_core::serde::compute_document_id(identifier));
|
||||
.delete_document_by_id(meilisearch_core::serde::compute_document_id(identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,9 +226,7 @@ pub async fn delete_multiple_documents(mut ctx: Context<Data>) -> SResult<Respon
|
||||
|
||||
pub async fn clear_all_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
if !ctx.state().accept_updates() {
|
||||
return Err(ResponseError::Maintenance);
|
||||
}
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
441
meilisearch-http/src/routes/index.rs
Normal file
441
meilisearch-http/src/routes/index.rs
Normal file
@ -0,0 +1,441 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use http::StatusCode;
|
||||
use log::error;
|
||||
use meilisearch_core::ProcessedUpdateResult;
|
||||
use meilisearch_schema::{Schema, SchemaBuilder};
|
||||
use rand::seq::SliceRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tide::querystring::ContextExt as QSContextExt;
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::schema::SchemaBody;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::routes::document::IndexUpdateResponse;
|
||||
use crate::Data;
|
||||
|
||||
fn generate_uid() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let sample = b"abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
sample
|
||||
.choose_multiple(&mut rng, 8)
|
||||
.map(|c| *c as char)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn list_indexes(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let indexes_uids = ctx.state().db.indexes_uids();
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut response_body = Vec::new();
|
||||
|
||||
for index_uid in indexes_uids {
|
||||
let index = ctx.state().db.open_index(&index_uid);
|
||||
|
||||
match index {
|
||||
Some(index) => {
|
||||
let name = index
|
||||
.main
|
||||
.name(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'name' not found"))?;
|
||||
let created_at = index
|
||||
.main
|
||||
.created_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'created_at' date not found"))?;
|
||||
let updated_at = index
|
||||
.main
|
||||
.updated_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'updated_at' date not found"))?;
|
||||
|
||||
let index_reponse = IndexResponse {
|
||||
name,
|
||||
uid: index_uid,
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
response_body.push(index_reponse);
|
||||
}
|
||||
None => error!(
|
||||
"Index {} is referenced in the indexes list but cannot be found",
|
||||
index_uid
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tide::response::json(response_body))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IndexResponse {
|
||||
name: String,
|
||||
uid: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn get_index(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let uid = ctx.url_param("index")?;
|
||||
let name = index
|
||||
.main
|
||||
.name(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'name' not found"))?;
|
||||
let created_at = index
|
||||
.main
|
||||
.created_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'created_at' date not found"))?;
|
||||
let updated_at = index
|
||||
.main
|
||||
.updated_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'updated_at' date not found"))?;
|
||||
|
||||
let response_body = IndexResponse {
|
||||
name,
|
||||
uid,
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response_body))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct IndexCreateRequest {
|
||||
name: String,
|
||||
uid: Option<String>,
|
||||
schema: Option<SchemaBody>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IndexCreateResponse {
|
||||
name: String,
|
||||
uid: String,
|
||||
schema: Option<SchemaBody>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
update_id: Option<u64>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn create_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let body = ctx
|
||||
.body_json::<IndexCreateRequest>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
|
||||
let uid = match body.uid {
|
||||
Some(uid) => uid,
|
||||
None => loop {
|
||||
let uid = generate_uid();
|
||||
if db.open_index(&uid).is_none() {
|
||||
break uid;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let created_index = match db.create_index(&uid) {
|
||||
Ok(index) => index,
|
||||
Err(e) => return Err(ResponseError::create_index(e)),
|
||||
};
|
||||
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
created_index
|
||||
.main
|
||||
.put_name(&mut writer, &body.name)
|
||||
.map_err(ResponseError::internal)?;
|
||||
created_index
|
||||
.main
|
||||
.put_created_at(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
created_index
|
||||
.main
|
||||
.put_updated_at(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let schema: Option<Schema> = body.schema.clone().map(Into::into);
|
||||
let mut response_update_id = None;
|
||||
if let Some(schema) = schema {
|
||||
let update_id = created_index
|
||||
.schema_update(&mut writer, schema)
|
||||
.map_err(ResponseError::internal)?;
|
||||
response_update_id = Some(update_id)
|
||||
}
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexCreateResponse {
|
||||
name: body.name,
|
||||
uid,
|
||||
schema: body.schema,
|
||||
update_id: response_update_id,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::CREATED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct UpdateIndexRequest {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateIndexResponse {
|
||||
name: String,
|
||||
uid: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn update_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let body = ctx
|
||||
.body_json::<UpdateIndexRequest>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
index
|
||||
.main
|
||||
.put_name(&mut writer, &body.name)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
index
|
||||
.main
|
||||
.put_updated_at(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let created_at = index
|
||||
.main
|
||||
.created_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'created_at' date not found"))?;
|
||||
let updated_at = index
|
||||
.main
|
||||
.updated_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'updated_at' date not found"))?;
|
||||
|
||||
let response_body = UpdateIndexResponse {
|
||||
name: body.name,
|
||||
uid: index_uid,
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct SchemaParams {
|
||||
raw: bool,
|
||||
}
|
||||
|
||||
pub async fn get_index_schema(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
// Tide doesn't support "no query param"
|
||||
let params: SchemaParams = ctx.url_query().unwrap_or_default();
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let schema = index
|
||||
.main
|
||||
.schema(&reader)
|
||||
.map_err(ResponseError::open_index)?;
|
||||
|
||||
match schema {
|
||||
Some(schema) => {
|
||||
if params.raw {
|
||||
Ok(tide::response::json(schema))
|
||||
} else {
|
||||
Ok(tide::response::json(SchemaBody::from(schema)))
|
||||
}
|
||||
}
|
||||
None => Err(ResponseError::not_found("missing index schema")),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_schema(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
|
||||
let params: SchemaParams = ctx.url_query().unwrap_or_default();
|
||||
|
||||
let schema = if params.raw {
|
||||
ctx.body_json::<SchemaBuilder>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?
|
||||
.build()
|
||||
} else {
|
||||
ctx.body_json::<SchemaBody>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?
|
||||
.into()
|
||||
};
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let index = db
|
||||
.open_index(&index_uid)
|
||||
.ok_or(ResponseError::index_not_found(index_uid))?;
|
||||
|
||||
let update_id = index
|
||||
.schema_update(&mut writer, schema.clone())
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn get_update_status(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let update_id = ctx
|
||||
.param::<u64>("update_id")
|
||||
.map_err(|e| ResponseError::bad_parameter("update_id", e))?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let status = index
|
||||
.update_status(&reader, update_id)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = match status {
|
||||
Some(status) => tide::response::json(status)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response(),
|
||||
None => tide::response::json(json!({ "message": "unknown update id" }))
|
||||
.with_status(StatusCode::NOT_FOUND)
|
||||
.into_response(),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get_all_updates_status(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let all_status = index
|
||||
.all_updates_status(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = tide::response::json(all_status)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response();
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn delete_index(ctx: Context<Data>) -> SResult<StatusCode> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
|
||||
let found = ctx
|
||||
.state()
|
||||
.db
|
||||
.delete_index(&index_uid)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
if found {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Ok(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index_update_callback(index_uid: &str, data: &Data, status: ProcessedUpdateResult) {
|
||||
if status.error.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(index) = data.db.open_index(&index_uid) {
|
||||
let env = &data.db.env;
|
||||
let mut writer = match env.write_txn() {
|
||||
Ok(writer) => writer,
|
||||
Err(e) => {
|
||||
error!("Impossible to get write_txn; {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = data.compute_stats(&mut writer, &index_uid) {
|
||||
error!("Impossible to compute stats; {}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = data.set_last_update(&mut writer) {
|
||||
error!("Impossible to update last_update; {}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = index.main.put_updated_at(&mut writer) {
|
||||
error!("Impossible to update updated_at; {}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = writer.commit() {
|
||||
error!("Impossible to get write_txn; {}", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -117,6 +117,8 @@ pub struct UpdatedRequest {
|
||||
description: Option<String>,
|
||||
acl: Option<Vec<ACL>>,
|
||||
indexes: Option<Vec<Wildcard>>,
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
revoked: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn update(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
@ -154,6 +156,14 @@ pub async fn update(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
token_config.indexes = indexes;
|
||||
}
|
||||
|
||||
if let Some(expires_at) = data.expires_at {
|
||||
token_config.expires_at = expires_at;
|
||||
}
|
||||
|
||||
if let Some(revoked) = data.revoked {
|
||||
token_config.revoked = revoked;
|
||||
}
|
||||
|
||||
token_config.updated_at = Utc::now();
|
||||
|
||||
common_store
|
||||
@ -163,7 +173,7 @@ pub async fn update(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
Ok(tide::response::json(token_config)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@ -185,5 +195,5 @@ pub async fn delete(ctx: Context<Data>) -> SResult<StatusCode> {
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
@ -13,7 +13,10 @@ pub mod synonym;
|
||||
pub fn load_routes(app: &mut tide::App<Data>) {
|
||||
app.at("").nest(|router| {
|
||||
router.at("/indexes").nest(|router| {
|
||||
router.at("/").get(index::list_indexes);
|
||||
router
|
||||
.at("/")
|
||||
.get(index::list_indexes)
|
||||
.post(index::create_index);
|
||||
|
||||
router.at("/search").post(search::search_multi_index);
|
||||
|
||||
@ -28,16 +31,21 @@ pub fn load_routes(app: &mut tide::App<Data>) {
|
||||
|
||||
router
|
||||
.at("/")
|
||||
.get(index::get_index_schema)
|
||||
.post(index::create_index)
|
||||
.put(index::update_schema)
|
||||
.get(index::get_index)
|
||||
.put(index::update_index)
|
||||
.delete(index::delete_index);
|
||||
|
||||
router
|
||||
.at("/schema")
|
||||
.get(index::get_index_schema)
|
||||
.put(index::update_schema);
|
||||
|
||||
router.at("/documents").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(document::browse_documents)
|
||||
.post(document::add_or_update_multiple_documents)
|
||||
.get(document::get_all_documents)
|
||||
.post(document::add_or_replace_multiple_documents)
|
||||
.put(document::add_or_update_multiple_documents)
|
||||
.delete(document::clear_all_documents);
|
||||
|
||||
router.at("/:identifier").nest(|router| {
|
||||
@ -52,8 +60,12 @@ pub fn load_routes(app: &mut tide::App<Data>) {
|
||||
.post(document::delete_multiple_documents);
|
||||
});
|
||||
|
||||
router.at("/synonym").nest(|router| {
|
||||
router.at("/").get(synonym::list).post(synonym::create);
|
||||
router.at("/synonyms").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(synonym::list)
|
||||
.post(synonym::create)
|
||||
.delete(synonym::clear);
|
||||
|
||||
router
|
||||
.at("/:synonym")
|
||||
@ -62,14 +74,13 @@ pub fn load_routes(app: &mut tide::App<Data>) {
|
||||
.delete(synonym::delete);
|
||||
|
||||
router.at("/batch").post(synonym::batch_write);
|
||||
router.at("/clear").post(synonym::clear);
|
||||
});
|
||||
|
||||
router.at("/stop-words").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(stop_words::list)
|
||||
.put(stop_words::add)
|
||||
.patch(stop_words::add)
|
||||
.delete(stop_words::delete);
|
||||
});
|
||||
|
@ -2,14 +2,14 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use meilidb_core::Index;
|
||||
use meilisearch_core::Index;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tide::querystring::ContextExt as QSContextExt;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::meilidb::{Error, IndexSearchExt, SearchHit};
|
||||
use crate::helpers::meilisearch::{Error, IndexSearchExt, SearchHit};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::Data;
|
||||
|
||||
@ -36,6 +36,12 @@ pub async fn search_with_url_query(ctx: Context<Data>) -> SResult<Response> {
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let schema = index
|
||||
.main
|
||||
.schema(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::open_index("No Schema found"))?;
|
||||
|
||||
let query: SearchQuery = ctx
|
||||
.url_query()
|
||||
.map_err(|_| ResponseError::bad_request("invalid query parameter"))?;
|
||||
@ -56,23 +62,36 @@ pub async fn search_with_url_query(ctx: Context<Data>) -> SResult<Response> {
|
||||
}
|
||||
if let Some(attributes_to_search_in) = query.attributes_to_search_in {
|
||||
for attr in attributes_to_search_in.split(',') {
|
||||
search_builder.add_retrievable_field(attr.to_string());
|
||||
search_builder.add_attribute_to_search_in(attr.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(attributes_to_crop) = query.attributes_to_crop {
|
||||
let crop_length = query.crop_length.unwrap_or(200);
|
||||
let attributes_to_crop = attributes_to_crop
|
||||
.split(',')
|
||||
.map(|r| (r.to_string(), crop_length))
|
||||
.collect();
|
||||
search_builder.attributes_to_crop(attributes_to_crop);
|
||||
if attributes_to_crop == "*" {
|
||||
let attributes_to_crop = schema
|
||||
.iter()
|
||||
.map(|(attr, ..)| (attr.to_string(), crop_length))
|
||||
.collect();
|
||||
search_builder.attributes_to_crop(attributes_to_crop);
|
||||
} else {
|
||||
let attributes_to_crop = attributes_to_crop
|
||||
.split(',')
|
||||
.map(|r| (r.to_string(), crop_length))
|
||||
.collect();
|
||||
search_builder.attributes_to_crop(attributes_to_crop);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(attributes_to_highlight) = query.attributes_to_highlight {
|
||||
let attributes_to_highlight = attributes_to_highlight
|
||||
.split(',')
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
let attributes_to_highlight = if attributes_to_highlight == "*" {
|
||||
schema.iter().map(|(attr, ..)| attr.to_string()).collect()
|
||||
} else {
|
||||
attributes_to_highlight
|
||||
.split(',')
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
};
|
||||
|
||||
search_builder.attributes_to_highlight(attributes_to_highlight);
|
||||
}
|
||||
|
||||
@ -136,13 +155,8 @@ pub async fn search_multi_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
|
||||
for index in index_list.clone() {
|
||||
if index == "*" {
|
||||
index_list = ctx
|
||||
.state()
|
||||
.db
|
||||
.indexes_names()
|
||||
.map_err(ResponseError::internal)?
|
||||
.into_iter()
|
||||
.collect();
|
||||
index_list = ctx.state().db.indexes_uids().into_iter().collect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,10 +176,10 @@ pub async fn search_multi_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
let par_body = body.clone();
|
||||
let responses_per_index: Vec<SResult<_>> = index_list
|
||||
.into_par_iter()
|
||||
.map(move |index_name| {
|
||||
.map(move |index_uid| {
|
||||
let index: Index = db
|
||||
.open_index(&index_name)
|
||||
.ok_or(ResponseError::index_not_found(&index_name))?;
|
||||
.open_index(&index_uid)
|
||||
.ok_or(ResponseError::index_not_found(&index_uid))?;
|
||||
|
||||
let mut search_builder = index.new_search(par_body.query.clone());
|
||||
|
||||
@ -188,7 +202,7 @@ pub async fn search_multi_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
search_builder.filters(filters);
|
||||
}
|
||||
if let Some(timeout_ms) = par_body.timeout_ms {
|
||||
search_builder.timeout(Duration::from_secs(timeout_ms));
|
||||
search_builder.timeout(Duration::from_millis(timeout_ms));
|
||||
}
|
||||
if let Some(matches) = par_body.matches {
|
||||
if matches {
|
||||
@ -202,7 +216,7 @@ pub async fn search_multi_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
let response = search_builder
|
||||
.search(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
Ok((index_name, response))
|
||||
Ok((index_uid, response))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -211,11 +225,11 @@ pub async fn search_multi_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
let mut max_query_time = 0;
|
||||
|
||||
for response in responses_per_index {
|
||||
if let Ok((index_name, response)) = response {
|
||||
if let Ok((index_uid, response)) = response {
|
||||
if response.processing_time_ms > max_query_time {
|
||||
max_query_time = response.processing_time_ms;
|
||||
}
|
||||
hits_map.insert(index_name, response.hits);
|
||||
hits_map.insert(index_uid, response.hits);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -14,7 +14,6 @@ use crate::Data;
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SettingBody {
|
||||
pub stop_words: Option<StopWords>,
|
||||
pub ranking_order: Option<RankingOrder>,
|
||||
pub distinct_field: Option<DistinctField>,
|
||||
pub ranking_rules: Option<RankingRules>,
|
||||
@ -27,7 +26,6 @@ pub enum RankingOrdering {
|
||||
Dsc,
|
||||
}
|
||||
|
||||
pub type StopWords = HashSet<String>;
|
||||
pub type RankingOrder = Vec<String>;
|
||||
pub type DistinctField = String;
|
||||
pub type RankingRules = HashMap<String, RankingOrdering>;
|
||||
@ -62,10 +60,6 @@ pub async fn update(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
None => SettingBody::default(),
|
||||
};
|
||||
|
||||
if let Some(stop_words) = settings.stop_words {
|
||||
current_settings.stop_words = Some(stop_words);
|
||||
}
|
||||
|
||||
if let Some(ranking_order) = settings.ranking_order {
|
||||
current_settings.ranking_order = Some(ranking_order);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::error;
|
||||
use pretty_bytes::converter::convert;
|
||||
use serde::Serialize;
|
||||
use sysinfo::{NetworkExt, Pid, ProcessExt, ProcessorExt, System, SystemExt};
|
||||
@ -17,13 +18,12 @@ use crate::Data;
|
||||
struct IndexStatsResponse {
|
||||
number_of_documents: u64,
|
||||
is_indexing: bool,
|
||||
last_update: Option<DateTime<Utc>>,
|
||||
fields_frequency: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
pub async fn index_stat(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
let index_name = ctx.url_param("index")?;
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
@ -34,27 +34,21 @@ pub async fn index_stat(ctx: Context<Data>) -> SResult<Response> {
|
||||
.number_of_documents(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let fields_frequency = ctx
|
||||
.state()
|
||||
.fields_frequency(&reader, &index_name)
|
||||
let fields_frequency = index
|
||||
.main
|
||||
.fields_frequency(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_indexing = ctx
|
||||
.state()
|
||||
.is_indexing(&reader, &index_name)
|
||||
.is_indexing(&reader, &index_uid)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::not_found("Index not found"))?;
|
||||
|
||||
let last_update = ctx
|
||||
.state()
|
||||
.last_update(&reader, &index_name)
|
||||
.map_err(ResponseError::internal)?;
|
||||
.ok_or(ResponseError::internal("'is_indexing' date not found"))?;
|
||||
|
||||
let response = IndexStatsResponse {
|
||||
number_of_documents,
|
||||
is_indexing,
|
||||
last_update,
|
||||
fields_frequency,
|
||||
};
|
||||
Ok(tide::response::json(response))
|
||||
@ -64,50 +58,53 @@ pub async fn index_stat(ctx: Context<Data>) -> SResult<Response> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StatsResult {
|
||||
database_size: u64,
|
||||
last_update: Option<DateTime<Utc>>,
|
||||
indexes: HashMap<String, IndexStatsResponse>,
|
||||
}
|
||||
|
||||
pub async fn get_stats(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
|
||||
let mut index_list = HashMap::new();
|
||||
|
||||
if let Ok(indexes_set) = ctx.state().db.indexes_names() {
|
||||
for index_name in indexes_set {
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let index = db.open_index(&index_name).unwrap();
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
let indexes_set = ctx.state().db.indexes_uids();
|
||||
for index_uid in indexes_set {
|
||||
let index = ctx.state().db.open_index(&index_uid);
|
||||
|
||||
let number_of_documents = index
|
||||
.main
|
||||
.number_of_documents(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
match index {
|
||||
Some(index) => {
|
||||
let number_of_documents = index
|
||||
.main
|
||||
.number_of_documents(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let fields_frequency = ctx
|
||||
.state()
|
||||
.fields_frequency(&reader, &index_name)
|
||||
.map_err(ResponseError::internal)?
|
||||
.unwrap_or_default();
|
||||
let fields_frequency = index
|
||||
.main
|
||||
.fields_frequency(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_indexing = ctx
|
||||
.state()
|
||||
.is_indexing(&reader, &index_name)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::not_found("Index not found"))?;
|
||||
let is_indexing = ctx
|
||||
.state()
|
||||
.is_indexing(&reader, &index_uid)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'is_indexing' date not found"))?;
|
||||
|
||||
let last_update = ctx
|
||||
.state()
|
||||
.last_update(&reader, &index_name)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = IndexStatsResponse {
|
||||
number_of_documents,
|
||||
is_indexing,
|
||||
last_update,
|
||||
fields_frequency,
|
||||
};
|
||||
index_list.insert(index_name, response);
|
||||
let response = IndexStatsResponse {
|
||||
number_of_documents,
|
||||
is_indexing,
|
||||
fields_frequency,
|
||||
};
|
||||
index_list.insert(index_uid, response);
|
||||
}
|
||||
None => error!(
|
||||
"Index {:?} is referenced in the indexes list but cannot be found",
|
||||
index_uid
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,8 +115,14 @@ pub async fn get_stats(ctx: Context<Data>) -> SResult<Response> {
|
||||
.filter(|metadata| metadata.is_file())
|
||||
.fold(0, |acc, m| acc + m.len());
|
||||
|
||||
let last_update = ctx
|
||||
.state()
|
||||
.last_update(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = StatsResult {
|
||||
database_size,
|
||||
last_update,
|
||||
indexes: index_list,
|
||||
};
|
||||
|
@ -115,7 +115,7 @@ pub async fn create(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::CREATED)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "meilidb-schema"
|
||||
version = "0.6.0"
|
||||
name = "meilisearch-schema"
|
||||
version = "0.8.0"
|
||||
authors = ["Kerollmops <renault.cle@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user