mirror of
https://github.com/quickwit-oss/tantivy.git
synced 2025-12-31 22:42:55 +00:00
Compare commits
2 Commits
0.1
...
python-bin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7c579f5c9 | ||
|
|
f2e546bdff |
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: fulmicoton
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
@@ -47,7 +47,6 @@ matrix:
|
|||||||
before_install:
|
before_install:
|
||||||
- set -e
|
- set -e
|
||||||
- rustup self update
|
- rustup self update
|
||||||
- rustup component add rustfmt
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- sh ci/install.sh
|
- sh ci/install.sh
|
||||||
@@ -61,7 +60,6 @@ before_script:
|
|||||||
|
|
||||||
script:
|
script:
|
||||||
- bash ci/script.sh
|
- bash ci/script.sh
|
||||||
- cargo fmt --all -- --check
|
|
||||||
|
|
||||||
before_deploy:
|
before_deploy:
|
||||||
- sh ci/before_deploy.sh
|
- sh ci/before_deploy.sh
|
||||||
|
|||||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -2,29 +2,6 @@ Tantivy 0.11.0
|
|||||||
=====================
|
=====================
|
||||||
|
|
||||||
- Added f64 field. Internally reuse u64 code the same way i64 does (@fdb-hiroshima)
|
- Added f64 field. Internally reuse u64 code the same way i64 does (@fdb-hiroshima)
|
||||||
- Various bugfixes in the query parser.
|
|
||||||
- Better handling of hyphens in query parser. (#609)
|
|
||||||
- Better handling of whitespaces.
|
|
||||||
- Closes #498 - add support for Elastic-style unbounded range queries for alphanumeric types eg. "title:>hello", "weight:>=70.5", "height:<200" (@petr-tik)
|
|
||||||
- API change around `Box<BoxableTokenizer>`. See detail in #629
|
|
||||||
- Avoid rebuilding Regex automaton whenever a regex query is reused. #639 (@brainlock)
|
|
||||||
- Add footer with some metadata to index files. #605 (@fdb-hiroshima)
|
|
||||||
- TopDocs collector: ensure stable sorting on equal score. #671 (@brainlock)
|
|
||||||
- Added handling of pre-tokenized text fields (#642), which will enable users to
|
|
||||||
load tokens created outside tantivy. See usage in examples/pre_tokenized_text. (@kkoziara)
|
|
||||||
- Fix crash when committing multiple times with deleted documents. #681 (@brainlock)
|
|
||||||
|
|
||||||
## How to update?
|
|
||||||
|
|
||||||
- `Box<dyn BoxableTokenizer>` has been replaced by a `BoxedTokenizer` struct.
|
|
||||||
- Regex are now compiled when the `RegexQuery` instance is built. As a result, it can now return
|
|
||||||
an error and handling the `Result` is required.
|
|
||||||
|
|
||||||
|
|
||||||
Tantivy 0.10.2
|
|
||||||
=====================
|
|
||||||
|
|
||||||
- Closes #656. Solving memory leak.
|
|
||||||
|
|
||||||
Tantivy 0.10.1
|
Tantivy 0.10.1
|
||||||
=====================
|
=====================
|
||||||
|
|||||||
33
Cargo.toml
33
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tantivy"
|
name = "tantivy"
|
||||||
version = "0.11.0"
|
version = "0.10.1"
|
||||||
authors = ["Paul Masurel <paul.masurel@gmail.com>"]
|
authors = ["Paul Masurel <paul.masurel@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
categories = ["database-implementations", "data-structures"]
|
categories = ["database-implementations", "data-structures"]
|
||||||
@@ -13,11 +13,10 @@ keywords = ["search", "information", "retrieval"]
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.11.0"
|
base64 = "0.10.0"
|
||||||
byteorder = "1.0"
|
byteorder = "1.0"
|
||||||
crc32fast = "1.2.0"
|
once_cell = "0.2"
|
||||||
once_cell = "1.0"
|
regex = "1.0"
|
||||||
regex ={version = "1.3.0", default-features = false, features = ["std"]}
|
|
||||||
tantivy-fst = "0.1"
|
tantivy-fst = "0.1"
|
||||||
memmap = {version = "0.7", optional=true}
|
memmap = {version = "0.7", optional=true}
|
||||||
lz4 = {version="1.20", optional=true}
|
lz4 = {version="1.20", optional=true}
|
||||||
@@ -25,6 +24,8 @@ snap = {version="0.2"}
|
|||||||
atomicwrites = {version="0.2.2", optional=true}
|
atomicwrites = {version="0.2.2", optional=true}
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
combine = ">=3.6.0,<4.0.0"
|
||||||
|
tempdir = "0.3"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@@ -34,15 +35,14 @@ itertools = "0.8"
|
|||||||
levenshtein_automata = {version="0.1", features=["fst_automaton"]}
|
levenshtein_automata = {version="0.1", features=["fst_automaton"]}
|
||||||
notify = {version="4", optional=true}
|
notify = {version="4", optional=true}
|
||||||
bit-set = "0.5"
|
bit-set = "0.5"
|
||||||
uuid = { version = "0.8", features = ["v4", "serde"] }
|
uuid = { version = "0.7.2", features = ["v4", "serde"] }
|
||||||
crossbeam = "0.7"
|
crossbeam = "0.5"
|
||||||
futures = "0.1"
|
futures = "0.1"
|
||||||
futures-cpupool = "0.1"
|
futures-cpupool = "0.1"
|
||||||
owning_ref = "0.4"
|
owning_ref = "0.4"
|
||||||
stable_deref_trait = "1.0.0"
|
stable_deref_trait = "1.0.0"
|
||||||
rust-stemmers = "1.1"
|
rust-stemmers = "1.1"
|
||||||
downcast-rs = { version="1.0" }
|
downcast-rs = { version="1.0" }
|
||||||
tantivy-query-grammar = { path="./query-grammar" }
|
|
||||||
bitpacking = {version="0.8", default-features = false, features=["bitpacker4x"]}
|
bitpacking = {version="0.8", default-features = false, features=["bitpacker4x"]}
|
||||||
census = "0.2"
|
census = "0.2"
|
||||||
fnv = "1.0.6"
|
fnv = "1.0.6"
|
||||||
@@ -50,10 +50,10 @@ owned-read = "0.4"
|
|||||||
failure = "0.1"
|
failure = "0.1"
|
||||||
htmlescape = "0.3.1"
|
htmlescape = "0.3.1"
|
||||||
fail = "0.3"
|
fail = "0.3"
|
||||||
|
scoped-pool = "1.0"
|
||||||
murmurhash32 = "0.2"
|
murmurhash32 = "0.2"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
smallvec = "1.0"
|
smallvec = "0.6"
|
||||||
rayon = "1"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winapi = "0.3"
|
winapi = "0.3"
|
||||||
@@ -64,10 +64,6 @@ maplit = "1"
|
|||||||
matches = "0.1.8"
|
matches = "0.1.8"
|
||||||
time = "0.1.42"
|
time = "0.1.42"
|
||||||
|
|
||||||
[dev-dependencies.fail]
|
|
||||||
version = "0.3"
|
|
||||||
features = ["failpoints"]
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
debug = false
|
debug = false
|
||||||
@@ -85,12 +81,13 @@ failpoints = ["fail/failpoints"]
|
|||||||
unstable = [] # useful for benches.
|
unstable = [] # useful for benches.
|
||||||
wasm-bindgen = ["uuid/wasm-bindgen"]
|
wasm-bindgen = ["uuid/wasm-bindgen"]
|
||||||
|
|
||||||
[workspace]
|
|
||||||
members = ["query-grammar"]
|
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
travis-ci = { repository = "tantivy-search/tantivy" }
|
travis-ci = { repository = "tantivy-search/tantivy" }
|
||||||
|
|
||||||
|
[dev-dependencies.fail]
|
||||||
|
features = ["failpoints"]
|
||||||
|
|
||||||
|
|
||||||
# Following the "fail" crate best practises, we isolate
|
# Following the "fail" crate best practises, we isolate
|
||||||
# tests that define specific behavior in fail check points
|
# tests that define specific behavior in fail check points
|
||||||
# in a different binary.
|
# in a different binary.
|
||||||
@@ -101,4 +98,4 @@ travis-ci = { repository = "tantivy-search/tantivy" }
|
|||||||
[[test]]
|
[[test]]
|
||||||
name = "failpoints"
|
name = "failpoints"
|
||||||
path = "tests/failpoints/mod.rs"
|
path = "tests/failpoints/mod.rs"
|
||||||
required-features = ["fail/failpoints"]
|
required-features = ["fail/failpoints"]
|
||||||
3
Makefile
3
Makefile
@@ -1,3 +0,0 @@
|
|||||||
test:
|
|
||||||
echo "Run test only... No examples."
|
|
||||||
cargo test --tests --lib
|
|
||||||
57
README.md
57
README.md
@@ -21,9 +21,9 @@
|
|||||||
[](https://www.patreon.com/fulmicoton)
|
[](https://www.patreon.com/fulmicoton)
|
||||||
|
|
||||||
|
|
||||||
**Tantivy** is a **full text search engine library** written in Rust.
|
**Tantivy** is a **full text search engine library** written in rust.
|
||||||
|
|
||||||
It is closer to [Apache Lucene](https://lucene.apache.org/) than to [Elasticsearch](https://www.elastic.co/products/elasticsearch) or [Apache Solr](https://lucene.apache.org/solr/) in the sense it is not
|
It is closer to [Apache Lucene](https://lucene.apache.org/) than to [Elasticsearch](https://www.elastic.co/products/elasticsearch) and [Apache Solr](https://lucene.apache.org/solr/) in the sense it is not
|
||||||
an off-the-shelf search engine server, but rather a crate that can be used
|
an off-the-shelf search engine server, but rather a crate that can be used
|
||||||
to build such a search engine.
|
to build such a search engine.
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Tantivy is, in fact, strongly inspired by Lucene's design.
|
|||||||
|
|
||||||
# Benchmark
|
# Benchmark
|
||||||
|
|
||||||
Tantivy is typically faster than Lucene, but the results depend on
|
Tantivy is typically faster than Lucene, but the results will depend on
|
||||||
the nature of the queries in your workload.
|
the nature of the queries in your workload.
|
||||||
|
|
||||||
The following [benchmark](https://tantivy-search.github.io/bench/) break downs
|
The following [benchmark](https://tantivy-search.github.io/bench/) break downs
|
||||||
@@ -40,19 +40,19 @@ performance for different type of queries / collection.
|
|||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Full-text search
|
- Full-text search
|
||||||
- Configurable tokenizer (stemming available for 17 Latin languages with third party support for Chinese ([tantivy-jieba](https://crates.io/crates/tantivy-jieba) and [cang-jie](https://crates.io/crates/cang-jie)) and [Japanese](https://crates.io/crates/tantivy-tokenizer-tiny-segmenter))
|
- Configurable tokenizer. (stemming available for 17 latin languages. Third party support for Chinese ([tantivy-jieba](https://crates.io/crates/tantivy-jieba) and [cang-jie](https://crates.io/crates/cang-jie)) and [Japanese](https://crates.io/crates/tantivy-tokenizer-tiny-segmenter)
|
||||||
- Fast (check out the :racehorse: :sparkles: [benchmark](https://tantivy-search.github.io/bench/) :sparkles: :racehorse:)
|
- Fast (check out the :racehorse: :sparkles: [benchmark](https://tantivy-search.github.io/bench/) :sparkles: :racehorse:)
|
||||||
- Tiny startup time (<10ms), perfect for command line tools
|
- Tiny startup time (<10ms), perfect for command line tools
|
||||||
- BM25 scoring (the same as Lucene)
|
- BM25 scoring (the same as lucene)
|
||||||
- Natural query language (e.g. `(michael AND jackson) OR "king of pop"`)
|
- Natural query language `(michael AND jackson) OR "king of pop"`
|
||||||
- Phrase queries search (e.g. `"michael jackson"`)
|
- Phrase queries search (`"michael jackson"`)
|
||||||
- Incremental indexing
|
- Incremental indexing
|
||||||
- Multithreaded indexing (indexing English Wikipedia takes < 3 minutes on my desktop)
|
- Multithreaded indexing (indexing English Wikipedia takes < 3 minutes on my desktop)
|
||||||
- Mmap directory
|
- Mmap directory
|
||||||
- SIMD integer compression when the platform/CPU includes the SSE2 instruction set
|
- SIMD integer compression when the platform/CPU includes the SSE2 instruction set.
|
||||||
- Single valued and multivalued u64, i64, and f64 fast fields (equivalent of doc values in Lucene)
|
- Single valued and multivalued u64, i64 and f64 fast fields (equivalent of doc values in Lucene)
|
||||||
- `&[u8]` fast fields
|
- `&[u8]` fast fields
|
||||||
- Text, i64, u64, f64, dates, and hierarchical facet fields
|
- Text, i64, u64, f64, dates and hierarchical facet fields
|
||||||
- LZ4 compressed document store
|
- LZ4 compressed document store
|
||||||
- Range queries
|
- Range queries
|
||||||
- Faceted search
|
- Faceted search
|
||||||
@@ -61,42 +61,43 @@ performance for different type of queries / collection.
|
|||||||
|
|
||||||
# Non-features
|
# Non-features
|
||||||
|
|
||||||
- Distributed search is out of the scope of Tantivy. That being said, Tantivy is a
|
- Distributed search is out of the scope of tantivy. That being said, tantivy is meant as a
|
||||||
library upon which one could build a distributed search. Serializable/mergeable collector state for instance,
|
library upon which one could build a distributed search. Serializable/mergeable collector state for instance,
|
||||||
are within the scope of Tantivy.
|
are within the scope of tantivy.
|
||||||
|
|
||||||
# Supported OS and compiler
|
# Supported OS and compiler
|
||||||
|
|
||||||
Tantivy works on stable Rust (>= 1.27) and supports Linux, MacOS, and Windows.
|
Tantivy works on stable rust (>= 1.27) and supports Linux, MacOS and Windows.
|
||||||
|
|
||||||
# Getting started
|
# Getting started
|
||||||
|
|
||||||
- [Tantivy's simple search example](https://tantivy-search.github.io/examples/basic_search.html)
|
- [tantivy's simple search example](https://tantivy-search.github.io/examples/basic_search.html)
|
||||||
- [tantivy-cli and its tutorial](https://github.com/tantivy-search/tantivy-cli) - `tantivy-cli` is an actual command line interface that makes it easy for you to create a search engine,
|
- [tantivy-cli and its tutorial](https://github.com/tantivy-search/tantivy-cli).
|
||||||
index documents, and search via the CLI or a small server with a REST API.
|
`tantivy-cli` is an actual command line interface that makes it easy for you to create a search engine,
|
||||||
It walks you through getting a wikipedia search engine up and running in a few minutes.
|
index documents and search via the CLI or a small server with a REST API.
|
||||||
- [Reference doc for the last released version](https://docs.rs/tantivy/)
|
It will walk you through getting a wikipedia search engine up and running in a few minutes.
|
||||||
|
- [reference doc for the last released version](https://docs.rs/tantivy/)
|
||||||
|
|
||||||
# How can I support this project?
|
# How can I support this project?
|
||||||
|
|
||||||
There are many ways to support this project.
|
There are many ways to support this project.
|
||||||
|
|
||||||
- Use Tantivy and tell us about your experience on [Gitter](https://gitter.im/tantivy-search/tantivy) or by email (paul.masurel@gmail.com)
|
- Use tantivy and tell us about your experience on [gitter](https://gitter.im/tantivy-search/tantivy) or by email (paul.masurel@gmail.com)
|
||||||
- Report bugs
|
- Report bugs
|
||||||
- Write a blog post
|
- Write a blog post
|
||||||
- Help with documentation by asking questions or submitting PRs
|
- Help with documentation by asking questions or submitting PRs
|
||||||
- Contribute code (you can join [our Gitter](https://gitter.im/tantivy-search/tantivy))
|
- Contribute code (you can join [our gitter](https://gitter.im/tantivy-search/tantivy) )
|
||||||
- Talk about Tantivy around you
|
- Talk about tantivy around you
|
||||||
- Drop a word on on [](https://saythanks.io/to/fulmicoton) or even [](https://www.patreon.com/fulmicoton)
|
- Drop a word on on [](https://saythanks.io/to/fulmicoton) or even [](https://www.patreon.com/fulmicoton)
|
||||||
|
|
||||||
# Contributing code
|
# Contributing code
|
||||||
|
|
||||||
We use the GitHub Pull Request workflow: reference a GitHub ticket and/or include a comprehensive commit message when opening a PR.
|
We use the GitHub Pull Request workflow - reference a GitHub ticket and/or include a comprehensive commit message when opening a PR.
|
||||||
|
|
||||||
## Clone and build locally
|
## Clone and build locally
|
||||||
|
|
||||||
Tantivy compiles on stable Rust but requires `Rust >= 1.27`.
|
Tantivy compiles on stable rust but requires `Rust >= 1.27`.
|
||||||
To check out and run tests, you can simply run:
|
To check out and run tests, you can simply run :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/tantivy-search/tantivy.git
|
git clone https://github.com/tantivy-search/tantivy.git
|
||||||
@@ -107,7 +108,7 @@ To check out and run tests, you can simply run:
|
|||||||
## Run tests
|
## Run tests
|
||||||
|
|
||||||
Some tests will not run with just `cargo test` because of `fail-rs`.
|
Some tests will not run with just `cargo test` because of `fail-rs`.
|
||||||
To run the tests exhaustively, run `./run-tests.sh`.
|
To run the tests exhaustively, run `./run-tests.sh`
|
||||||
|
|
||||||
## Debug
|
## Debug
|
||||||
|
|
||||||
@@ -115,13 +116,13 @@ You might find it useful to step through the programme with a debugger.
|
|||||||
|
|
||||||
### A failing test
|
### A failing test
|
||||||
|
|
||||||
Make sure you haven't run `cargo clean` after the most recent `cargo test` or `cargo build` to guarantee that the `target/` directory exists. Use this bash script to find the name of the most recent debug build of Tantivy and run it under `rust-gdb`:
|
Make sure you haven't run `cargo clean` after the most recent `cargo test` or `cargo build` to guarantee that `target/` dir exists. Use this bash script to find the most name of the most recent debug build of tantivy and run it under rust-gdb.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
find target/debug/ -maxdepth 1 -executable -type f -name "tantivy*" -printf '%TY-%Tm-%Td %TT %p\n' | sort -r | cut -d " " -f 3 | xargs -I RECENT_DBG_TANTIVY rust-gdb RECENT_DBG_TANTIVY
|
find target/debug/ -maxdepth 1 -executable -type f -name "tantivy*" -printf '%TY-%Tm-%Td %TT %p\n' | sort -r | cut -d " " -f 3 | xargs -I RECENT_DBG_TANTIVY rust-gdb RECENT_DBG_TANTIVY
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that you are in `rust-gdb`, you can set breakpoints on lines and methods that match your source code and run the debug executable with flags that you normally pass to `cargo test` like this:
|
Now that you are in rust-gdb, you can set breakpoints on lines and methods that match your source-code and run the debug executable with flags that you normally pass to `cargo test` to like this
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$gdb run --test-threads 1 --test $NAME_OF_TEST
|
$gdb run --test-threads 1 --test $NAME_OF_TEST
|
||||||
@@ -129,7 +130,7 @@ $gdb run --test-threads 1 --test $NAME_OF_TEST
|
|||||||
|
|
||||||
### An example
|
### An example
|
||||||
|
|
||||||
By default, `rustc` compiles everything in the `examples/` directory in debug mode. This makes it easy for you to make examples to reproduce bugs:
|
By default, rustc compiles everything in the `examples/` dir in debug mode. This makes it easy for you to make examples to reproduce bugs.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rust-gdb target/debug/examples/$EXAMPLE_NAME
|
rust-gdb target/debug/examples/$EXAMPLE_NAME
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ set -ex
|
|||||||
main() {
|
main() {
|
||||||
if [ ! -z $CODECOV ]; then
|
if [ ! -z $CODECOV ]; then
|
||||||
echo "Codecov"
|
echo "Codecov"
|
||||||
cargo build --verbose && cargo coverage --verbose --all && bash <(curl -s https://codecov.io/bash) -s target/kcov
|
cargo build --verbose && cargo coverage --verbose && bash <(curl -s https://codecov.io/bash) -s target/kcov
|
||||||
else
|
else
|
||||||
echo "Build"
|
echo "Build"
|
||||||
cross build --target $TARGET
|
cross build --target $TARGET
|
||||||
@@ -15,8 +15,7 @@ main() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
echo "Test"
|
echo "Test"
|
||||||
cross test --target $TARGET --no-default-features --features mmap
|
cross test --target $TARGET --no-default-features --features mmap -- --test-threads 1
|
||||||
cross test --target $TARGET --no-default-features --features mmap query-grammar
|
|
||||||
fi
|
fi
|
||||||
for example in $(ls examples/*.rs)
|
for example in $(ls examples/*.rs)
|
||||||
do
|
do
|
||||||
|
|||||||
@@ -5,23 +5,26 @@
|
|||||||
//
|
//
|
||||||
// We will :
|
// We will :
|
||||||
// - define our schema
|
// - define our schema
|
||||||
// - create an index in a directory
|
// = create an index in a directory
|
||||||
// - index a few documents into our index
|
// - index few documents in our index
|
||||||
// - search for the best document matching a basic query
|
// - search for the best document matchings "sea whale"
|
||||||
// - retrieve the best document's original content.
|
// - retrieve the best document original content.
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::TopDocs;
|
use tantivy::collector::TopDocs;
|
||||||
use tantivy::query::QueryParser;
|
use tantivy::query::QueryParser;
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::{doc, Index, ReloadPolicy};
|
use tantivy::Index;
|
||||||
use tempfile::TempDir;
|
use tantivy::ReloadPolicy;
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
fn main() -> tantivy::Result<()> {
|
||||||
// Let's create a temporary directory for the
|
// Let's create a temporary directory for the
|
||||||
// sake of this example
|
// sake of this example
|
||||||
let index_path = TempDir::new()?;
|
let index_path = TempDir::new("tantivy_example_dir")?;
|
||||||
|
|
||||||
// # Defining the schema
|
// # Defining the schema
|
||||||
//
|
//
|
||||||
@@ -30,7 +33,7 @@ fn main() -> tantivy::Result<()> {
|
|||||||
// and for each field, its type and "the way it should
|
// and for each field, its type and "the way it should
|
||||||
// be indexed".
|
// be indexed".
|
||||||
|
|
||||||
// First we need to define a schema ...
|
// first we need to define a schema ...
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
|
|
||||||
// Our first field is title.
|
// Our first field is title.
|
||||||
@@ -45,7 +48,7 @@ fn main() -> tantivy::Result<()> {
|
|||||||
//
|
//
|
||||||
// `STORED` means that the field will also be saved
|
// `STORED` means that the field will also be saved
|
||||||
// in a compressed, row-oriented key-value store.
|
// in a compressed, row-oriented key-value store.
|
||||||
// This store is useful for reconstructing the
|
// This store is useful to reconstruct the
|
||||||
// documents that were selected during the search phase.
|
// documents that were selected during the search phase.
|
||||||
schema_builder.add_text_field("title", TEXT | STORED);
|
schema_builder.add_text_field("title", TEXT | STORED);
|
||||||
|
|
||||||
@@ -54,7 +57,8 @@ fn main() -> tantivy::Result<()> {
|
|||||||
// need to be able to be able to retrieve it
|
// need to be able to be able to retrieve it
|
||||||
// for our application.
|
// for our application.
|
||||||
//
|
//
|
||||||
// We can make our index lighter by omitting the `STORED` flag.
|
// We can make our index lighter and
|
||||||
|
// by omitting `STORED` flag.
|
||||||
schema_builder.add_text_field("body", TEXT);
|
schema_builder.add_text_field("body", TEXT);
|
||||||
|
|
||||||
let schema = schema_builder.build();
|
let schema = schema_builder.build();
|
||||||
@@ -67,7 +71,7 @@ fn main() -> tantivy::Result<()> {
|
|||||||
// with our schema in the directory.
|
// with our schema in the directory.
|
||||||
let index = Index::create_in_dir(&index_path, schema.clone())?;
|
let index = Index::create_in_dir(&index_path, schema.clone())?;
|
||||||
|
|
||||||
// To insert a document we will need an index writer.
|
// To insert document we need an index writer.
|
||||||
// There must be only one writer at a time.
|
// There must be only one writer at a time.
|
||||||
// This single `IndexWriter` is already
|
// This single `IndexWriter` is already
|
||||||
// multithreaded.
|
// multithreaded.
|
||||||
@@ -145,8 +149,8 @@ fn main() -> tantivy::Result<()> {
|
|||||||
// At this point our documents are not searchable.
|
// At this point our documents are not searchable.
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// We need to call `.commit()` explicitly to force the
|
// We need to call .commit() explicitly to force the
|
||||||
// `index_writer` to finish processing the documents in the queue,
|
// index_writer to finish processing the documents in the queue,
|
||||||
// flush the current index to the disk, and advertise
|
// flush the current index to the disk, and advertise
|
||||||
// the existence of new documents.
|
// the existence of new documents.
|
||||||
//
|
//
|
||||||
@@ -158,14 +162,14 @@ fn main() -> tantivy::Result<()> {
|
|||||||
// persistently indexed.
|
// persistently indexed.
|
||||||
//
|
//
|
||||||
// In the scenario of a crash or a power failure,
|
// In the scenario of a crash or a power failure,
|
||||||
// tantivy behaves as if it has rolled back to its last
|
// tantivy behaves as if has rolled back to its last
|
||||||
// commit.
|
// commit.
|
||||||
|
|
||||||
// # Searching
|
// # Searching
|
||||||
//
|
//
|
||||||
// ### Searcher
|
// ### Searcher
|
||||||
//
|
//
|
||||||
// A reader is required first in order to search an index.
|
// A reader is required to get search the index.
|
||||||
// It acts as a `Searcher` pool that reloads itself,
|
// It acts as a `Searcher` pool that reloads itself,
|
||||||
// depending on a `ReloadPolicy`.
|
// depending on a `ReloadPolicy`.
|
||||||
//
|
//
|
||||||
@@ -181,7 +185,7 @@ fn main() -> tantivy::Result<()> {
|
|||||||
|
|
||||||
// We now need to acquire a searcher.
|
// We now need to acquire a searcher.
|
||||||
//
|
//
|
||||||
// A searcher points to a snapshotted, immutable version of the index.
|
// A searcher points to snapshotted, immutable version of the index.
|
||||||
//
|
//
|
||||||
// Some search experience might require more than
|
// Some search experience might require more than
|
||||||
// one query. Using the same searcher ensures that all of these queries will run on the
|
// one query. Using the same searcher ensures that all of these queries will run on the
|
||||||
@@ -201,7 +205,7 @@ fn main() -> tantivy::Result<()> {
|
|||||||
// in both title and body.
|
// in both title and body.
|
||||||
let query_parser = QueryParser::for_index(&index, vec![title, body]);
|
let query_parser = QueryParser::for_index(&index, vec![title, body]);
|
||||||
|
|
||||||
// `QueryParser` may fail if the query is not in the right
|
// QueryParser may fail if the query is not in the right
|
||||||
// format. For user facing applications, this can be a problem.
|
// format. For user facing applications, this can be a problem.
|
||||||
// A ticket has been opened regarding this problem.
|
// A ticket has been opened regarding this problem.
|
||||||
let query = query_parser.parse_query("sea whale")?;
|
let query = query_parser.parse_query("sea whale")?;
|
||||||
@@ -217,7 +221,7 @@ fn main() -> tantivy::Result<()> {
|
|||||||
//
|
//
|
||||||
// We are not interested in all of the documents but
|
// We are not interested in all of the documents but
|
||||||
// only in the top 10. Keeping track of our top 10 best documents
|
// only in the top 10. Keeping track of our top 10 best documents
|
||||||
// is the role of the `TopDocs` collector.
|
// is the role of the TopDocs.
|
||||||
|
|
||||||
// We can now perform our query.
|
// We can now perform our query.
|
||||||
let top_docs = searcher.search(&query, &TopDocs::with_limit(10))?;
|
let top_docs = searcher.search(&query, &TopDocs::with_limit(10))?;
|
||||||
|
|||||||
@@ -9,12 +9,15 @@
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::{Collector, SegmentCollector};
|
use tantivy::collector::{Collector, SegmentCollector};
|
||||||
use tantivy::fastfield::FastFieldReader;
|
use tantivy::fastfield::FastFieldReader;
|
||||||
use tantivy::query::QueryParser;
|
use tantivy::query::QueryParser;
|
||||||
use tantivy::schema::Field;
|
use tantivy::schema::Field;
|
||||||
use tantivy::schema::{Schema, FAST, INDEXED, TEXT};
|
use tantivy::schema::{Schema, FAST, INDEXED, TEXT};
|
||||||
use tantivy::{doc, Index, SegmentReader, TantivyError};
|
use tantivy::SegmentReader;
|
||||||
|
use tantivy::{Index, TantivyError};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Stats {
|
struct Stats {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
//
|
//
|
||||||
// In this example, we'll see how to define a tokenizer pipeline
|
// In this example, we'll see how to define a tokenizer pipeline
|
||||||
// by aligning a bunch of `TokenFilter`.
|
// by aligning a bunch of `TokenFilter`.
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::TopDocs;
|
use tantivy::collector::TopDocs;
|
||||||
use tantivy::query::QueryParser;
|
use tantivy::query::QueryParser;
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::tokenizer::NgramTokenizer;
|
use tantivy::tokenizer::NgramTokenizer;
|
||||||
use tantivy::{doc, Index};
|
use tantivy::Index;
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
fn main() -> tantivy::Result<()> {
|
||||||
// # Defining the schema
|
// # Defining the schema
|
||||||
|
|||||||
@@ -8,10 +8,13 @@
|
|||||||
//
|
//
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::TopDocs;
|
use tantivy::collector::TopDocs;
|
||||||
use tantivy::query::TermQuery;
|
use tantivy::query::TermQuery;
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::{doc, Index, IndexReader};
|
use tantivy::Index;
|
||||||
|
use tantivy::IndexReader;
|
||||||
|
|
||||||
// A simple helper function to fetch a single document
|
// A simple helper function to fetch a single document
|
||||||
// given its id from our index.
|
// given its id from our index.
|
||||||
|
|||||||
@@ -12,16 +12,17 @@
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::FacetCollector;
|
use tantivy::collector::FacetCollector;
|
||||||
use tantivy::query::AllQuery;
|
use tantivy::query::AllQuery;
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::{doc, Index};
|
use tantivy::Index;
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
fn main() -> tantivy::Result<()> {
|
||||||
// Let's create a temporary directory for the
|
// Let's create a temporary directory for the
|
||||||
// sake of this example
|
// sake of this example
|
||||||
let index_path = TempDir::new()?;
|
let index_path = TempDir::new("tantivy_facet_example_dir")?;
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
|
|
||||||
schema_builder.add_text_field("name", TEXT | STORED);
|
schema_builder.add_text_field("name", TEXT | STORED);
|
||||||
@@ -73,3 +74,5 @@ fn main() -> tantivy::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
//
|
//
|
||||||
// Below is an example of creating an indexed integer field in your schema
|
// Below is an example of creating an indexed integer field in your schema
|
||||||
// You can use RangeQuery to get a Count of all occurrences in a given range.
|
// You can use RangeQuery to get a Count of all occurrences in a given range.
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::Count;
|
use tantivy::collector::Count;
|
||||||
use tantivy::query::RangeQuery;
|
use tantivy::query::RangeQuery;
|
||||||
use tantivy::schema::{Schema, INDEXED};
|
use tantivy::schema::{Schema, INDEXED};
|
||||||
use tantivy::{doc, Index, Result};
|
use tantivy::Index;
|
||||||
|
use tantivy::Result;
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
// For the sake of simplicity, this schema will only have 1 field
|
// For the sake of simplicity, this schema will only have 1 field
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::{doc, DocId, DocSet, Index, Postings};
|
use tantivy::Index;
|
||||||
|
use tantivy::{DocId, DocSet, Postings};
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
fn main() -> tantivy::Result<()> {
|
||||||
// We first create a schema for the sake of the
|
// We first create a schema for the sake of the
|
||||||
|
|||||||
@@ -25,11 +25,14 @@
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tantivy::schema::{Schema, STORED, TEXT};
|
use tantivy::schema::{Schema, STORED, TEXT};
|
||||||
use tantivy::{doc, Index, IndexWriter, Opstamp};
|
use tantivy::Opstamp;
|
||||||
|
use tantivy::{Index, IndexWriter};
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
fn main() -> tantivy::Result<()> {
|
||||||
// # Defining the schema
|
// # Defining the schema
|
||||||
@@ -46,9 +49,10 @@ fn main() -> tantivy::Result<()> {
|
|||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
// we index 100 times the document... for the sake of the example.
|
// we index 100 times the document... for the sake of the example.
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
let opstamp = index_writer_clone_1
|
let opstamp = {
|
||||||
.read().unwrap() //< A read lock is sufficient here.
|
// A read lock is sufficient here.
|
||||||
.add_document(
|
let index_writer_rlock = index_writer_clone_1.read().unwrap();
|
||||||
|
index_writer_rlock.add_document(
|
||||||
doc!(
|
doc!(
|
||||||
title => "Of Mice and Men",
|
title => "Of Mice and Men",
|
||||||
body => "A few miles south of Soledad, the Salinas River drops in close to the hillside \
|
body => "A few miles south of Soledad, the Salinas River drops in close to the hillside \
|
||||||
@@ -59,7 +63,8 @@ fn main() -> tantivy::Result<()> {
|
|||||||
fresh and green with every spring, carrying in their lower leaf junctures the \
|
fresh and green with every spring, carrying in their lower leaf junctures the \
|
||||||
debris of the winter’s flooding; and sycamores with mottled, white, recumbent \
|
debris of the winter’s flooding; and sycamores with mottled, white, recumbent \
|
||||||
limbs and branches that arch over the pool"
|
limbs and branches that arch over the pool"
|
||||||
));
|
))
|
||||||
|
};
|
||||||
println!("add doc {} from thread 1 - opstamp {}", i, opstamp);
|
println!("add doc {} from thread 1 - opstamp {}", i, opstamp);
|
||||||
thread::sleep(Duration::from_millis(20));
|
thread::sleep(Duration::from_millis(20));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
// # Pre-tokenized text example
|
|
||||||
//
|
|
||||||
// This example shows how to use pre-tokenized text. Sometimes yout might
|
|
||||||
// want to index and search through text which is already split into
|
|
||||||
// tokens by some external tool.
|
|
||||||
//
|
|
||||||
// In this example we will:
|
|
||||||
// - use tantivy tokenizer to create tokens and load them directly into tantivy,
|
|
||||||
// - import tokenized text straight from json,
|
|
||||||
// - perform a search on documents with pre-tokenized text
|
|
||||||
|
|
||||||
use tantivy::tokenizer::{PreTokenizedString, SimpleTokenizer, Token, TokenStream, Tokenizer};
|
|
||||||
|
|
||||||
use tantivy::collector::{Count, TopDocs};
|
|
||||||
use tantivy::query::TermQuery;
|
|
||||||
use tantivy::schema::*;
|
|
||||||
use tantivy::{doc, Index, ReloadPolicy};
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
fn pre_tokenize_text(text: &str) -> Vec<Token> {
|
|
||||||
let mut token_stream = SimpleTokenizer.token_stream(text);
|
|
||||||
let mut tokens = vec![];
|
|
||||||
while token_stream.advance() {
|
|
||||||
tokens.push(token_stream.token().clone());
|
|
||||||
}
|
|
||||||
tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
|
||||||
let index_path = TempDir::new()?;
|
|
||||||
|
|
||||||
let mut schema_builder = Schema::builder();
|
|
||||||
|
|
||||||
schema_builder.add_text_field("title", TEXT | STORED);
|
|
||||||
schema_builder.add_text_field("body", TEXT);
|
|
||||||
|
|
||||||
let schema = schema_builder.build();
|
|
||||||
|
|
||||||
let index = Index::create_in_dir(&index_path, schema.clone())?;
|
|
||||||
|
|
||||||
let mut index_writer = index.writer(50_000_000)?;
|
|
||||||
|
|
||||||
// We can create a document manually, by setting the fields
|
|
||||||
// one by one in a Document object.
|
|
||||||
let title = schema.get_field("title").unwrap();
|
|
||||||
let body = schema.get_field("body").unwrap();
|
|
||||||
|
|
||||||
let title_text = "The Old Man and the Sea";
|
|
||||||
let body_text = "He was an old man who fished alone in a skiff in the Gulf Stream";
|
|
||||||
|
|
||||||
// Content of our first document
|
|
||||||
// We create `PreTokenizedString` which contains original text and vector of tokens
|
|
||||||
let title_tok = PreTokenizedString {
|
|
||||||
text: String::from(title_text),
|
|
||||||
tokens: pre_tokenize_text(title_text),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Original text: \"{}\" and tokens: {:?}",
|
|
||||||
title_tok.text, title_tok.tokens
|
|
||||||
);
|
|
||||||
|
|
||||||
let body_tok = PreTokenizedString {
|
|
||||||
text: String::from(body_text),
|
|
||||||
tokens: pre_tokenize_text(body_text),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Now lets create a document and add our `PreTokenizedString` using
|
|
||||||
// `add_pre_tokenized_text` method of `Document`
|
|
||||||
let mut old_man_doc = Document::default();
|
|
||||||
old_man_doc.add_pre_tokenized_text(title, &title_tok);
|
|
||||||
old_man_doc.add_pre_tokenized_text(body, &body_tok);
|
|
||||||
|
|
||||||
// ... now let's just add it to the IndexWriter
|
|
||||||
index_writer.add_document(old_man_doc);
|
|
||||||
|
|
||||||
// Pretokenized text can also be fed as JSON
|
|
||||||
let short_man_json = r#"{
|
|
||||||
"title":[{
|
|
||||||
"text":"The Old Man",
|
|
||||||
"tokens":[
|
|
||||||
{"offset_from":0,"offset_to":3,"position":0,"text":"The","position_length":1},
|
|
||||||
{"offset_from":4,"offset_to":7,"position":1,"text":"Old","position_length":1},
|
|
||||||
{"offset_from":8,"offset_to":11,"position":2,"text":"Man","position_length":1}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let short_man_doc = schema.parse_document(&short_man_json)?;
|
|
||||||
|
|
||||||
index_writer.add_document(short_man_doc);
|
|
||||||
|
|
||||||
// Let's commit changes
|
|
||||||
index_writer.commit()?;
|
|
||||||
|
|
||||||
// ... and now is the time to query our index
|
|
||||||
|
|
||||||
let reader = index
|
|
||||||
.reader_builder()
|
|
||||||
.reload_policy(ReloadPolicy::OnCommit)
|
|
||||||
.try_into()?;
|
|
||||||
|
|
||||||
let searcher = reader.searcher();
|
|
||||||
|
|
||||||
// We want to get documents with token "Man", we will use TermQuery to do it
|
|
||||||
// Using PreTokenizedString means the tokens are stored as is avoiding stemming
|
|
||||||
// and lowercasing, which preserves full words in their original form
|
|
||||||
let query = TermQuery::new(
|
|
||||||
Term::from_field_text(title, "Man"),
|
|
||||||
IndexRecordOption::Basic,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (top_docs, count) = searcher
|
|
||||||
.search(&query, &(TopDocs::with_limit(2), Count))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(count, 2);
|
|
||||||
|
|
||||||
for (_score, doc_address) in top_docs {
|
|
||||||
let retrieved_doc = searcher.doc(doc_address)?;
|
|
||||||
println!("Document: {}", schema.to_json(&retrieved_doc));
|
|
||||||
}
|
|
||||||
|
|
||||||
// In contrary to the previous query, when we search for the "man" term we
|
|
||||||
// should get no results, as it's not one of the indexed tokens. SimpleTokenizer
|
|
||||||
// only splits text on whitespace / punctuation.
|
|
||||||
|
|
||||||
let query = TermQuery::new(
|
|
||||||
Term::from_field_text(title, "man"),
|
|
||||||
IndexRecordOption::Basic,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (_top_docs, count) = searcher
|
|
||||||
.search(&query, &(TopDocs::with_limit(2), Count))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(count, 0);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -7,16 +7,19 @@
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::TopDocs;
|
use tantivy::collector::TopDocs;
|
||||||
use tantivy::query::QueryParser;
|
use tantivy::query::QueryParser;
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::{doc, Index, Snippet, SnippetGenerator};
|
use tantivy::Index;
|
||||||
use tempfile::TempDir;
|
use tantivy::{Snippet, SnippetGenerator};
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
fn main() -> tantivy::Result<()> {
|
||||||
// Let's create a temporary directory for the
|
// Let's create a temporary directory for the
|
||||||
// sake of this example
|
// sake of this example
|
||||||
let index_path = TempDir::new()?;
|
let index_path = TempDir::new("tantivy_example_dir")?;
|
||||||
|
|
||||||
// # Defining the schema
|
// # Defining the schema
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Importing tantivy...
|
// Importing tantivy...
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tantivy;
|
||||||
use tantivy::collector::TopDocs;
|
use tantivy::collector::TopDocs;
|
||||||
use tantivy::query::QueryParser;
|
use tantivy::query::QueryParser;
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::tokenizer::*;
|
use tantivy::tokenizer::*;
|
||||||
use tantivy::{doc, Index};
|
use tantivy::Index;
|
||||||
|
|
||||||
fn main() -> tantivy::Result<()> {
|
fn main() -> tantivy::Result<()> {
|
||||||
// this example assumes you understand the content in `basic_search`
|
// this example assumes you understand the content in `basic_search`
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "tantivy-query-grammar"
|
|
||||||
version = "0.11.0"
|
|
||||||
authors = ["Paul Masurel <paul.masurel@gmail.com>"]
|
|
||||||
license = "MIT"
|
|
||||||
categories = ["database-implementations", "data-structures"]
|
|
||||||
description = """Search engine library"""
|
|
||||||
documentation = "https://tantivy-search.github.io/tantivy/tantivy/index.html"
|
|
||||||
homepage = "https://github.com/tantivy-search/tantivy"
|
|
||||||
repository = "https://github.com/tantivy-search/tantivy"
|
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["search", "information", "retrieval"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
combine = ">=3.6.0,<4.0.0"
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#![recursion_limit = "100"]
|
|
||||||
|
|
||||||
mod occur;
|
|
||||||
mod query_grammar;
|
|
||||||
mod user_input_ast;
|
|
||||||
use combine::parser::Parser;
|
|
||||||
|
|
||||||
pub use crate::occur::Occur;
|
|
||||||
use crate::query_grammar::parse_to_ast;
|
|
||||||
pub use crate::user_input_ast::{UserInputAST, UserInputBound, UserInputLeaf, UserInputLiteral};
|
|
||||||
|
|
||||||
pub struct Error;
|
|
||||||
|
|
||||||
pub fn parse_query(query: &str) -> Result<UserInputAST, Error> {
|
|
||||||
let (user_input_ast, _remaining) = parse_to_ast().parse(query).map_err(|_| Error)?;
|
|
||||||
Ok(user_input_ast)
|
|
||||||
}
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
use super::user_input_ast::*;
|
|
||||||
use crate::Occur;
|
|
||||||
use combine::char::*;
|
|
||||||
use combine::error::StreamError;
|
|
||||||
use combine::stream::StreamErrorFor;
|
|
||||||
use combine::*;
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
fn field[I]()(I) -> String
|
|
||||||
where [I: Stream<Item = char>] {
|
|
||||||
(
|
|
||||||
letter(),
|
|
||||||
many(satisfy(|c: char| c.is_alphanumeric() || c == '_')),
|
|
||||||
).skip(char(':')).map(|(s1, s2): (char, String)| format!("{}{}", s1, s2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
fn word[I]()(I) -> String
|
|
||||||
where [I: Stream<Item = char>] {
|
|
||||||
(
|
|
||||||
satisfy(|c: char| !c.is_whitespace() && !['-', '`', ':', '{', '}', '"', '[', ']', '(',')'].contains(&c) ),
|
|
||||||
many(satisfy(|c: char| !c.is_whitespace() && ![':', '{', '}', '"', '[', ']', '(',')'].contains(&c)))
|
|
||||||
)
|
|
||||||
.map(|(s1, s2): (char, String)| format!("{}{}", s1, s2))
|
|
||||||
.and_then(|s: String|
|
|
||||||
match s.as_str() {
|
|
||||||
"OR" => Err(StreamErrorFor::<I>::unexpected_static_message("OR")),
|
|
||||||
"AND" => Err(StreamErrorFor::<I>::unexpected_static_message("AND")),
|
|
||||||
"NOT" => Err(StreamErrorFor::<I>::unexpected_static_message("NOT")),
|
|
||||||
_ => Ok(s)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
fn literal[I]()(I) -> UserInputLeaf
|
|
||||||
where [I: Stream<Item = char>]
|
|
||||||
{
|
|
||||||
let term_val = || {
|
|
||||||
let phrase = char('"').with(many1(satisfy(|c| c != '"'))).skip(char('"'));
|
|
||||||
phrase.or(word())
|
|
||||||
};
|
|
||||||
let term_val_with_field = negative_number().or(term_val());
|
|
||||||
let term_query =
|
|
||||||
(field(), term_val_with_field)
|
|
||||||
.map(|(field_name, phrase)| UserInputLiteral {
|
|
||||||
field_name: Some(field_name),
|
|
||||||
phrase,
|
|
||||||
});
|
|
||||||
let term_default_field = term_val().map(|phrase| UserInputLiteral {
|
|
||||||
field_name: None,
|
|
||||||
phrase,
|
|
||||||
});
|
|
||||||
attempt(term_query)
|
|
||||||
.or(term_default_field)
|
|
||||||
.map(UserInputLeaf::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
fn negative_number[I]()(I) -> String
|
|
||||||
where [I: Stream<Item = char>]
|
|
||||||
{
|
|
||||||
(char('-'), many1(satisfy(char::is_numeric)),
|
|
||||||
optional((char('.'), many1(satisfy(char::is_numeric)))))
|
|
||||||
.map(|(s1, s2, s3): (char, String, Option<(char, String)>)| {
|
|
||||||
if let Some(('.', s3)) = s3 {
|
|
||||||
format!("{}{}.{}", s1, s2, s3)
|
|
||||||
} else {
|
|
||||||
format!("{}{}", s1, s2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
fn spaces1[I]()(I) -> ()
|
|
||||||
where [I: Stream<Item = char>] {
|
|
||||||
skip_many1(space())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
/// Function that parses a range out of a Stream
|
|
||||||
/// Supports ranges like:
|
|
||||||
/// [5 TO 10], {5 TO 10}, [* TO 10], [10 TO *], {10 TO *], >5, <=10
|
|
||||||
/// [a TO *], [a TO c], [abc TO bcd}
|
|
||||||
fn range[I]()(I) -> UserInputLeaf
|
|
||||||
where [I: Stream<Item = char>] {
|
|
||||||
let range_term_val = || {
|
|
||||||
word().or(negative_number()).or(char('*').with(value("*".to_string())))
|
|
||||||
};
|
|
||||||
|
|
||||||
// check for unbounded range in the form of <5, <=10, >5, >=5
|
|
||||||
let elastic_unbounded_range = (choice([attempt(string(">=")),
|
|
||||||
attempt(string("<=")),
|
|
||||||
attempt(string("<")),
|
|
||||||
attempt(string(">"))])
|
|
||||||
.skip(spaces()),
|
|
||||||
range_term_val()).
|
|
||||||
map(|(comparison_sign, bound): (&str, String)|
|
|
||||||
match comparison_sign {
|
|
||||||
">=" => (UserInputBound::Inclusive(bound), UserInputBound::Unbounded),
|
|
||||||
"<=" => (UserInputBound::Unbounded, UserInputBound::Inclusive(bound)),
|
|
||||||
"<" => (UserInputBound::Unbounded, UserInputBound::Exclusive(bound)),
|
|
||||||
">" => (UserInputBound::Exclusive(bound), UserInputBound::Unbounded),
|
|
||||||
// default case
|
|
||||||
_ => (UserInputBound::Unbounded, UserInputBound::Unbounded)
|
|
||||||
});
|
|
||||||
let lower_bound = (one_of("{[".chars()), range_term_val())
|
|
||||||
.map(|(boundary_char, lower_bound): (char, String)|
|
|
||||||
if lower_bound == "*" {
|
|
||||||
UserInputBound::Unbounded
|
|
||||||
} else if boundary_char == '{' {
|
|
||||||
UserInputBound::Exclusive(lower_bound)
|
|
||||||
} else {
|
|
||||||
UserInputBound::Inclusive(lower_bound)
|
|
||||||
});
|
|
||||||
let upper_bound = (range_term_val(), one_of("}]".chars()))
|
|
||||||
.map(|(higher_bound, boundary_char): (String, char)|
|
|
||||||
if higher_bound == "*" {
|
|
||||||
UserInputBound::Unbounded
|
|
||||||
} else if boundary_char == '}' {
|
|
||||||
UserInputBound::Exclusive(higher_bound)
|
|
||||||
} else {
|
|
||||||
UserInputBound::Inclusive(higher_bound)
|
|
||||||
});
|
|
||||||
// return only lower and upper
|
|
||||||
let lower_to_upper = (lower_bound.
|
|
||||||
skip((spaces(),
|
|
||||||
string("TO"),
|
|
||||||
spaces())),
|
|
||||||
upper_bound);
|
|
||||||
|
|
||||||
(optional(field()).skip(spaces()),
|
|
||||||
// try elastic first, if it matches, the range is unbounded
|
|
||||||
attempt(elastic_unbounded_range).or(lower_to_upper))
|
|
||||||
.map(|(field, (lower, upper))|
|
|
||||||
// Construct the leaf from extracted field (optional)
|
|
||||||
// and bounds
|
|
||||||
UserInputLeaf::Range {
|
|
||||||
field,
|
|
||||||
lower,
|
|
||||||
upper
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn negate(expr: UserInputAST) -> UserInputAST {
|
|
||||||
expr.unary(Occur::MustNot)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn must(expr: UserInputAST) -> UserInputAST {
|
|
||||||
expr.unary(Occur::Must)
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
fn leaf[I]()(I) -> UserInputAST
|
|
||||||
where [I: Stream<Item = char>] {
|
|
||||||
char('-').with(leaf()).map(negate)
|
|
||||||
.or(char('+').with(leaf()).map(must))
|
|
||||||
.or(char('(').with(ast()).skip(char(')')))
|
|
||||||
.or(char('*').map(|_| UserInputAST::from(UserInputLeaf::All)))
|
|
||||||
.or(attempt(string("NOT").skip(spaces1()).with(leaf()).map(negate)))
|
|
||||||
.or(attempt(range().map(UserInputAST::from)))
|
|
||||||
.or(literal().map(UserInputAST::from))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
enum BinaryOperand {
|
|
||||||
Or,
|
|
||||||
And,
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
fn binary_operand[I]()(I) -> BinaryOperand
|
|
||||||
where [I: Stream<Item = char>]
|
|
||||||
{
|
|
||||||
string("AND").with(value(BinaryOperand::And))
|
|
||||||
.or(string("OR").with(value(BinaryOperand::Or)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn aggregate_binary_expressions(
|
|
||||||
left: UserInputAST,
|
|
||||||
others: Vec<(BinaryOperand, UserInputAST)>,
|
|
||||||
) -> UserInputAST {
|
|
||||||
let mut dnf: Vec<Vec<UserInputAST>> = vec![vec![left]];
|
|
||||||
for (operator, operand_ast) in others {
|
|
||||||
match operator {
|
|
||||||
BinaryOperand::And => {
|
|
||||||
if let Some(last) = dnf.last_mut() {
|
|
||||||
last.push(operand_ast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BinaryOperand::Or => {
|
|
||||||
dnf.push(vec![operand_ast]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dnf.len() == 1 {
|
|
||||||
UserInputAST::and(dnf.into_iter().next().unwrap()) //< safe
|
|
||||||
} else {
|
|
||||||
let conjunctions = dnf.into_iter().map(UserInputAST::and).collect();
|
|
||||||
UserInputAST::or(conjunctions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
pub fn ast[I]()(I) -> UserInputAST
|
|
||||||
where [I: Stream<Item = char>]
|
|
||||||
{
|
|
||||||
let operand_leaf = (binary_operand().skip(spaces()), leaf().skip(spaces()));
|
|
||||||
let boolean_expr = (leaf().skip(spaces().silent()), many1(operand_leaf)).map(
|
|
||||||
|(left, right)| aggregate_binary_expressions(left,right));
|
|
||||||
let whitespace_separated_leaves = many1(leaf().skip(spaces().silent()))
|
|
||||||
.map(|subqueries: Vec<UserInputAST>|
|
|
||||||
if subqueries.len() == 1 {
|
|
||||||
subqueries.into_iter().next().unwrap()
|
|
||||||
} else {
|
|
||||||
UserInputAST::Clause(subqueries.into_iter().collect())
|
|
||||||
});
|
|
||||||
let expr = attempt(boolean_expr).or(whitespace_separated_leaves);
|
|
||||||
spaces().with(expr).skip(spaces())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser! {
|
|
||||||
pub fn parse_to_ast[I]()(I) -> UserInputAST
|
|
||||||
where [I: Stream<Item = char>]
|
|
||||||
{
|
|
||||||
spaces().with(optional(ast()).skip(eof())).map(|opt_ast| opt_ast.unwrap_or_else(UserInputAST::empty_query))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn test_parse_query_to_ast_helper(query: &str, expected: &str) {
|
|
||||||
let query = parse_to_ast().parse(query).unwrap().0;
|
|
||||||
let query_str = format!("{:?}", query);
|
|
||||||
assert_eq!(query_str, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_is_parse_err(query: &str) {
|
|
||||||
assert!(parse_to_ast().parse(query).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_empty_to_ast() {
|
|
||||||
test_parse_query_to_ast_helper("", "<emptyclause>");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_query_to_ast_hyphen() {
|
|
||||||
test_parse_query_to_ast_helper("\"www-form-encoded\"", "\"www-form-encoded\"");
|
|
||||||
test_parse_query_to_ast_helper("www-form-encoded", "\"www-form-encoded\"");
|
|
||||||
test_parse_query_to_ast_helper("www-form-encoded", "\"www-form-encoded\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_query_to_ast_not_op() {
|
|
||||||
assert_eq!(
|
|
||||||
format!("{:?}", parse_to_ast().parse("NOT")),
|
|
||||||
"Err(UnexpectedParse)"
|
|
||||||
);
|
|
||||||
test_parse_query_to_ast_helper("NOTa", "\"NOTa\"");
|
|
||||||
test_parse_query_to_ast_helper("NOT a", "-(\"a\")");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_query_to_ast_binary_op() {
|
|
||||||
test_parse_query_to_ast_helper("a AND b", "(+(\"a\") +(\"b\"))");
|
|
||||||
test_parse_query_to_ast_helper("a OR b", "(?(\"a\") ?(\"b\"))");
|
|
||||||
test_parse_query_to_ast_helper("a OR b AND c", "(?(\"a\") ?((+(\"b\") +(\"c\"))))");
|
|
||||||
test_parse_query_to_ast_helper("a AND b AND c", "(+(\"a\") +(\"b\") +(\"c\"))");
|
|
||||||
assert_eq!(
|
|
||||||
format!("{:?}", parse_to_ast().parse("a OR b aaa")),
|
|
||||||
"Err(UnexpectedParse)"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!("{:?}", parse_to_ast().parse("a AND b aaa")),
|
|
||||||
"Err(UnexpectedParse)"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!("{:?}", parse_to_ast().parse("aaa a OR b ")),
|
|
||||||
"Err(UnexpectedParse)"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!("{:?}", parse_to_ast().parse("aaa ccc a OR b ")),
|
|
||||||
"Err(UnexpectedParse)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_elastic_query_ranges() {
|
|
||||||
test_parse_query_to_ast_helper("title: >a", "title:{\"a\" TO \"*\"}");
|
|
||||||
test_parse_query_to_ast_helper("title:>=a", "title:[\"a\" TO \"*\"}");
|
|
||||||
test_parse_query_to_ast_helper("title: <a", "title:{\"*\" TO \"a\"}");
|
|
||||||
test_parse_query_to_ast_helper("title:<=a", "title:{\"*\" TO \"a\"]");
|
|
||||||
test_parse_query_to_ast_helper("title:<=bsd", "title:{\"*\" TO \"bsd\"]");
|
|
||||||
|
|
||||||
test_parse_query_to_ast_helper("weight: >70", "weight:{\"70\" TO \"*\"}");
|
|
||||||
test_parse_query_to_ast_helper("weight:>=70", "weight:[\"70\" TO \"*\"}");
|
|
||||||
test_parse_query_to_ast_helper("weight: <70", "weight:{\"*\" TO \"70\"}");
|
|
||||||
test_parse_query_to_ast_helper("weight:<=70", "weight:{\"*\" TO \"70\"]");
|
|
||||||
test_parse_query_to_ast_helper("weight: >60.7", "weight:{\"60.7\" TO \"*\"}");
|
|
||||||
|
|
||||||
test_parse_query_to_ast_helper("weight: <= 70", "weight:{\"*\" TO \"70\"]");
|
|
||||||
|
|
||||||
test_parse_query_to_ast_helper("weight: <= 70.5", "weight:{\"*\" TO \"70.5\"]");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_range_parser() {
|
|
||||||
// testing the range() parser separately
|
|
||||||
let res = range().parse("title: <hello").unwrap().0;
|
|
||||||
let expected = UserInputLeaf::Range {
|
|
||||||
field: Some("title".to_string()),
|
|
||||||
lower: UserInputBound::Unbounded,
|
|
||||||
upper: UserInputBound::Exclusive("hello".to_string()),
|
|
||||||
};
|
|
||||||
let res2 = range().parse("title:{* TO hello}").unwrap().0;
|
|
||||||
assert_eq!(res, expected);
|
|
||||||
assert_eq!(res2, expected);
|
|
||||||
let expected_weight = UserInputLeaf::Range {
|
|
||||||
field: Some("weight".to_string()),
|
|
||||||
lower: UserInputBound::Inclusive("71.2".to_string()),
|
|
||||||
upper: UserInputBound::Unbounded,
|
|
||||||
};
|
|
||||||
|
|
||||||
let res3 = range().parse("weight: >=71.2").unwrap().0;
|
|
||||||
let res4 = range().parse("weight:[71.2 TO *}").unwrap().0;
|
|
||||||
assert_eq!(res3, expected_weight);
|
|
||||||
assert_eq!(res4, expected_weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_query_to_triming_spaces() {
|
|
||||||
test_parse_query_to_ast_helper(" abc", "\"abc\"");
|
|
||||||
test_parse_query_to_ast_helper("abc ", "\"abc\"");
|
|
||||||
test_parse_query_to_ast_helper("( a OR abc)", "(?(\"a\") ?(\"abc\"))");
|
|
||||||
test_parse_query_to_ast_helper("(a OR abc)", "(?(\"a\") ?(\"abc\"))");
|
|
||||||
test_parse_query_to_ast_helper("(a OR abc)", "(?(\"a\") ?(\"abc\"))");
|
|
||||||
test_parse_query_to_ast_helper("a OR abc ", "(?(\"a\") ?(\"abc\"))");
|
|
||||||
test_parse_query_to_ast_helper("(a OR abc )", "(?(\"a\") ?(\"abc\"))");
|
|
||||||
test_parse_query_to_ast_helper("(a OR abc) ", "(?(\"a\") ?(\"abc\"))");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_query_to_ast() {
|
|
||||||
test_parse_query_to_ast_helper("abc", "\"abc\"");
|
|
||||||
test_parse_query_to_ast_helper("a b", "(\"a\" \"b\")");
|
|
||||||
test_parse_query_to_ast_helper("+(a b)", "+((\"a\" \"b\"))");
|
|
||||||
test_parse_query_to_ast_helper("+d", "+(\"d\")");
|
|
||||||
test_parse_query_to_ast_helper("+(a b) +d", "(+((\"a\" \"b\")) +(\"d\"))");
|
|
||||||
test_parse_query_to_ast_helper("(+a +b) d", "((+(\"a\") +(\"b\")) \"d\")");
|
|
||||||
test_parse_query_to_ast_helper("(+a)", "+(\"a\")");
|
|
||||||
test_parse_query_to_ast_helper("(+a +b)", "(+(\"a\") +(\"b\"))");
|
|
||||||
test_parse_query_to_ast_helper("abc:toto", "abc:\"toto\"");
|
|
||||||
test_parse_query_to_ast_helper("abc:1.1", "abc:\"1.1\"");
|
|
||||||
test_parse_query_to_ast_helper("+abc:toto", "+(abc:\"toto\")");
|
|
||||||
test_parse_query_to_ast_helper("(+abc:toto -titi)", "(+(abc:\"toto\") -(\"titi\"))");
|
|
||||||
test_parse_query_to_ast_helper("-abc:toto", "-(abc:\"toto\")");
|
|
||||||
test_parse_query_to_ast_helper("abc:a b", "(abc:\"a\" \"b\")");
|
|
||||||
test_parse_query_to_ast_helper("abc:\"a b\"", "abc:\"a b\"");
|
|
||||||
test_parse_query_to_ast_helper("foo:[1 TO 5]", "foo:[\"1\" TO \"5\"]");
|
|
||||||
test_parse_query_to_ast_helper("[1 TO 5]", "[\"1\" TO \"5\"]");
|
|
||||||
test_parse_query_to_ast_helper("foo:{a TO z}", "foo:{\"a\" TO \"z\"}");
|
|
||||||
test_parse_query_to_ast_helper("foo:[1 TO toto}", "foo:[\"1\" TO \"toto\"}");
|
|
||||||
test_parse_query_to_ast_helper("foo:[* TO toto}", "foo:{\"*\" TO \"toto\"}");
|
|
||||||
test_parse_query_to_ast_helper("foo:[1 TO *}", "foo:[\"1\" TO \"*\"}");
|
|
||||||
test_parse_query_to_ast_helper("foo:[1.1 TO *}", "foo:[\"1.1\" TO \"*\"}");
|
|
||||||
test_is_parse_err("abc + ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,12 @@ use crate::SegmentReader;
|
|||||||
/// documents match the query.
|
/// documents match the query.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
/// use tantivy::schema::{Schema, TEXT};
|
||||||
|
/// use tantivy::{Index, Result};
|
||||||
/// use tantivy::collector::Count;
|
/// use tantivy::collector::Count;
|
||||||
/// use tantivy::query::QueryParser;
|
/// use tantivy::query::QueryParser;
|
||||||
/// use tantivy::schema::{Schema, TEXT};
|
|
||||||
/// use tantivy::{doc, Index, Result};
|
|
||||||
///
|
///
|
||||||
/// # fn main() { example().unwrap(); }
|
/// # fn main() { example().unwrap(); }
|
||||||
/// fn example() -> Result<()> {
|
/// fn example() -> Result<()> {
|
||||||
@@ -123,4 +125,5 @@ mod tests {
|
|||||||
assert_eq!(count_collector.harvest(), 2);
|
assert_eq!(count_collector.harvest(), 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,10 +81,12 @@ fn facet_depth(facet_bytes: &[u8]) -> usize {
|
|||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
/// use tantivy::schema::{Facet, Schema, TEXT};
|
||||||
|
/// use tantivy::{Index, Result};
|
||||||
/// use tantivy::collector::FacetCollector;
|
/// use tantivy::collector::FacetCollector;
|
||||||
/// use tantivy::query::AllQuery;
|
/// use tantivy::query::AllQuery;
|
||||||
/// use tantivy::schema::{Facet, Schema, TEXT};
|
|
||||||
/// use tantivy::{doc, Index, Result};
|
|
||||||
///
|
///
|
||||||
/// # fn main() { example().unwrap(); }
|
/// # fn main() { example().unwrap(); }
|
||||||
/// fn example() -> Result<()> {
|
/// fn example() -> Result<()> {
|
||||||
@@ -515,7 +517,7 @@ mod tests {
|
|||||||
#[should_panic(expected = "Tried to add a facet which is a descendant of \
|
#[should_panic(expected = "Tried to add a facet which is a descendant of \
|
||||||
an already added facet.")]
|
an already added facet.")]
|
||||||
fn test_misused_facet_collector() {
|
fn test_misused_facet_collector() {
|
||||||
let mut facet_collector = FacetCollector::for_field(Field::from_field_id(0));
|
let mut facet_collector = FacetCollector::for_field(Field(0));
|
||||||
facet_collector.add_facet(Facet::from("/country"));
|
facet_collector.add_facet(Facet::from("/country"));
|
||||||
facet_collector.add_facet(Facet::from("/country/europe"));
|
facet_collector.add_facet(Facet::from("/country/europe"));
|
||||||
}
|
}
|
||||||
@@ -546,7 +548,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_non_used_facet_collector() {
|
fn test_non_used_facet_collector() {
|
||||||
let mut facet_collector = FacetCollector::for_field(Field::from_field_id(0));
|
let mut facet_collector = FacetCollector::for_field(Field(0));
|
||||||
facet_collector.add_facet(Facet::from("/country"));
|
facet_collector.add_facet(Facet::from("/country"));
|
||||||
facet_collector.add_facet(Facet::from("/countryeurope"));
|
facet_collector.add_facet(Facet::from("/countryeurope"));
|
||||||
}
|
}
|
||||||
@@ -599,18 +601,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "unstable"))]
|
#[cfg(all(test, feature = "unstable"))]
|
||||||
mod bench {
|
mod bench {
|
||||||
|
|
||||||
use crate::collector::FacetCollector;
|
use collector::FacetCollector;
|
||||||
use crate::query::AllQuery;
|
use query::AllQuery;
|
||||||
use crate::schema::{Facet, Schema};
|
use rand::{thread_rng, Rng};
|
||||||
use crate::Index;
|
use schema::Facet;
|
||||||
use rand::seq::SliceRandom;
|
use schema::Schema;
|
||||||
use rand::thread_rng;
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
use Index;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_facet_collector(b: &mut Bencher) {
|
fn bench_facet_collector(b: &mut Bencher) {
|
||||||
@@ -627,7 +630,7 @@ mod bench {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 40425 docs
|
// 40425 docs
|
||||||
docs[..].shuffle(&mut thread_rng());
|
thread_rng().shuffle(&mut docs[..]);
|
||||||
|
|
||||||
let mut index_writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
|
let mut index_writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
|
||||||
for doc in docs {
|
for doc in docs {
|
||||||
@@ -636,7 +639,7 @@ mod bench {
|
|||||||
index_writer.commit().unwrap();
|
index_writer.commit().unwrap();
|
||||||
let reader = index.reader().unwrap();
|
let reader = index.reader().unwrap();
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let searcher = reader.searcher();
|
let searcher = index.searcher();
|
||||||
let facet_collector = FacetCollector::for_field(facet_field);
|
let facet_collector = FacetCollector::for_field(facet_field);
|
||||||
searcher.search(&AllQuery, &facet_collector).unwrap();
|
searcher.search(&AllQuery, &facet_collector).unwrap();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ The resulting `Fruit` will then be a typed tuple with each collector's original
|
|||||||
in their respective position.
|
in their respective position.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
# extern crate tantivy;
|
||||||
# use tantivy::schema::*;
|
# use tantivy::schema::*;
|
||||||
# use tantivy::*;
|
# use tantivy::*;
|
||||||
# use tantivy::query::*;
|
# use tantivy::query::*;
|
||||||
|
|||||||
@@ -105,10 +105,12 @@ impl<TFruit: Fruit> FruitHandle<TFruit> {
|
|||||||
/// [Combining several collectors section of the collector documentation](./index.html#combining-several-collectors).
|
/// [Combining several collectors section of the collector documentation](./index.html#combining-several-collectors).
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
/// use tantivy::schema::{Schema, TEXT};
|
||||||
|
/// use tantivy::{Index, Result};
|
||||||
/// use tantivy::collector::{Count, TopDocs, MultiCollector};
|
/// use tantivy::collector::{Count, TopDocs, MultiCollector};
|
||||||
/// use tantivy::query::QueryParser;
|
/// use tantivy::query::QueryParser;
|
||||||
/// use tantivy::schema::{Schema, TEXT};
|
|
||||||
/// use tantivy::{doc, Index, Result};
|
|
||||||
///
|
///
|
||||||
/// # fn main() { example().unwrap(); }
|
/// # fn main() { example().unwrap(); }
|
||||||
/// fn example() -> Result<()> {
|
/// fn example() -> Result<()> {
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ use std::collections::BinaryHeap;
|
|||||||
/// It has a custom implementation of `PartialOrd` that reverses the order. This is because the
|
/// It has a custom implementation of `PartialOrd` that reverses the order. This is because the
|
||||||
/// default Rust heap is a max heap, whereas a min heap is needed.
|
/// default Rust heap is a max heap, whereas a min heap is needed.
|
||||||
///
|
///
|
||||||
/// Additionally, it guarantees stable sorting: in case of a tie on the feature, the document
|
|
||||||
/// address is used.
|
|
||||||
///
|
|
||||||
/// WARNING: equality is not what you would expect here.
|
/// WARNING: equality is not what you would expect here.
|
||||||
/// Two elements are equal if their feature is equal, and regardless of whether `doc`
|
/// Two elements are equal if their feature is equal, and regardless of whether `doc`
|
||||||
/// is equal. This should be perfectly fine for this usage, but let's make sure this
|
/// is equal. This should be perfectly fine for this usage, but let's make sure this
|
||||||
@@ -24,37 +21,29 @@ struct ComparableDoc<T, D> {
|
|||||||
doc: D,
|
doc: D,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialOrd, D: PartialOrd> PartialOrd for ComparableDoc<T, D> {
|
impl<T: PartialOrd, D> PartialOrd for ComparableDoc<T, D> {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialOrd, D: PartialOrd> Ord for ComparableDoc<T, D> {
|
impl<T: PartialOrd, D> Ord for ComparableDoc<T, D> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
// Reversed to make BinaryHeap work as a min-heap
|
other
|
||||||
let by_feature = other
|
|
||||||
.feature
|
.feature
|
||||||
.partial_cmp(&self.feature)
|
.partial_cmp(&self.feature)
|
||||||
.unwrap_or(Ordering::Equal);
|
.unwrap_or_else(|| Ordering::Equal)
|
||||||
|
|
||||||
let lazy_by_doc_address = || self.doc.partial_cmp(&other.doc).unwrap_or(Ordering::Equal);
|
|
||||||
|
|
||||||
// In case of a tie on the feature, we sort by ascending
|
|
||||||
// `DocAddress` in order to ensure a stable sorting of the
|
|
||||||
// documents.
|
|
||||||
by_feature.then_with(lazy_by_doc_address)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialOrd, D: PartialOrd> PartialEq for ComparableDoc<T, D> {
|
impl<T: PartialOrd, D> PartialEq for ComparableDoc<T, D> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.cmp(other) == Ordering::Equal
|
self.cmp(other) == Ordering::Equal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialOrd, D: PartialOrd> Eq for ComparableDoc<T, D> {}
|
impl<T: PartialOrd, D> Eq for ComparableDoc<T, D> {}
|
||||||
|
|
||||||
pub(crate) struct TopCollector<T> {
|
pub(crate) struct TopCollector<T> {
|
||||||
limit: usize,
|
limit: usize,
|
||||||
@@ -225,94 +214,4 @@ mod tests {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_top_segment_collector_stable_ordering_for_equal_feature() {
|
|
||||||
// given that the documents are collected in ascending doc id order,
|
|
||||||
// when harvesting we have to guarantee stable sorting in case of a tie
|
|
||||||
// on the score
|
|
||||||
let doc_ids_collection = [4, 5, 6];
|
|
||||||
let score = 3.14;
|
|
||||||
|
|
||||||
let mut top_collector_limit_2 = TopSegmentCollector::new(0, 2);
|
|
||||||
for id in &doc_ids_collection {
|
|
||||||
top_collector_limit_2.collect(*id, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut top_collector_limit_3 = TopSegmentCollector::new(0, 3);
|
|
||||||
for id in &doc_ids_collection {
|
|
||||||
top_collector_limit_3.collect(*id, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
top_collector_limit_2.harvest(),
|
|
||||||
top_collector_limit_3.harvest()[..2].to_vec(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "unstable"))]
|
|
||||||
mod bench {
|
|
||||||
use super::TopSegmentCollector;
|
|
||||||
use test::Bencher;
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn bench_top_segment_collector_collect_not_at_capacity(b: &mut Bencher) {
|
|
||||||
let mut top_collector = TopSegmentCollector::new(0, 400);
|
|
||||||
|
|
||||||
b.iter(|| {
|
|
||||||
for i in 0..100 {
|
|
||||||
top_collector.collect(i, 0.8);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn bench_top_segment_collector_collect_at_capacity(b: &mut Bencher) {
|
|
||||||
let mut top_collector = TopSegmentCollector::new(0, 100);
|
|
||||||
|
|
||||||
for i in 0..100 {
|
|
||||||
top_collector.collect(i, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
b.iter(|| {
|
|
||||||
for i in 0..100 {
|
|
||||||
top_collector.collect(i, 0.8);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn bench_top_segment_collector_collect_and_harvest_many_ties(b: &mut Bencher) {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut top_collector = TopSegmentCollector::new(0, 100);
|
|
||||||
|
|
||||||
for i in 0..100 {
|
|
||||||
top_collector.collect(i, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// it would be nice to be able to do the setup N times but still
|
|
||||||
// measure only harvest(). We can't since harvest() consumes
|
|
||||||
// the top_collector.
|
|
||||||
top_collector.harvest()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn bench_top_segment_collector_collect_and_harvest_no_tie(b: &mut Bencher) {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut top_collector = TopSegmentCollector::new(0, 100);
|
|
||||||
let mut score = 1.0;
|
|
||||||
|
|
||||||
for i in 0..100 {
|
|
||||||
score += 1.0;
|
|
||||||
top_collector.collect(i, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
// it would be nice to be able to do the setup N times but still
|
|
||||||
// measure only harvest(). We can't since harvest() consumes
|
|
||||||
// the top_collector.
|
|
||||||
top_collector.harvest()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ use crate::SegmentLocalId;
|
|||||||
use crate::SegmentReader;
|
use crate::SegmentReader;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
/// The `TopDocs` collector keeps track of the top `K` documents
|
/// The Top Score Collector keeps track of the K documents
|
||||||
/// sorted by their score.
|
/// sorted by their score.
|
||||||
///
|
///
|
||||||
/// The implementation is based on a `BinaryHeap`.
|
/// The implementation is based on a `BinaryHeap`.
|
||||||
/// The theorical complexity for collecting the top `K` out of `n` documents
|
/// The theorical complexity for collecting the top `K` out of `n` documents
|
||||||
/// is `O(n log K)`.
|
/// is `O(n log K)`.
|
||||||
///
|
///
|
||||||
/// This collector guarantees a stable sorting in case of a tie on the
|
|
||||||
/// document score. As such, it is suitable to implement pagination.
|
|
||||||
///
|
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
/// use tantivy::DocAddress;
|
||||||
|
/// use tantivy::schema::{Schema, TEXT};
|
||||||
|
/// use tantivy::{Index, Result};
|
||||||
/// use tantivy::collector::TopDocs;
|
/// use tantivy::collector::TopDocs;
|
||||||
/// use tantivy::query::QueryParser;
|
/// use tantivy::query::QueryParser;
|
||||||
/// use tantivy::schema::{Schema, TEXT};
|
|
||||||
/// use tantivy::{doc, DocAddress, Index, Result};
|
|
||||||
///
|
///
|
||||||
/// # fn main() { example().unwrap(); }
|
/// # fn main() { example().unwrap(); }
|
||||||
/// fn example() -> Result<()> {
|
/// fn example() -> Result<()> {
|
||||||
@@ -87,8 +87,10 @@ impl TopDocs {
|
|||||||
/// Set top-K to rank documents by a given fast field.
|
/// Set top-K to rank documents by a given fast field.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
/// # use tantivy::schema::{Schema, FAST, TEXT};
|
/// # use tantivy::schema::{Schema, FAST, TEXT};
|
||||||
/// # use tantivy::{doc, Index, Result, DocAddress};
|
/// # use tantivy::{Index, Result, DocAddress};
|
||||||
/// # use tantivy::query::{Query, QueryParser};
|
/// # use tantivy::query::{Query, QueryParser};
|
||||||
/// use tantivy::Searcher;
|
/// use tantivy::Searcher;
|
||||||
/// use tantivy::collector::TopDocs;
|
/// use tantivy::collector::TopDocs;
|
||||||
@@ -126,7 +128,7 @@ impl TopDocs {
|
|||||||
/// ///
|
/// ///
|
||||||
/// /// `field` is required to be a FAST field.
|
/// /// `field` is required to be a FAST field.
|
||||||
/// fn docs_sorted_by_rating(searcher: &Searcher,
|
/// fn docs_sorted_by_rating(searcher: &Searcher,
|
||||||
/// query: &dyn Query,
|
/// query: &Query,
|
||||||
/// sort_by_field: Field)
|
/// sort_by_field: Field)
|
||||||
/// -> Result<Vec<(u64, DocAddress)>> {
|
/// -> Result<Vec<(u64, DocAddress)>> {
|
||||||
///
|
///
|
||||||
@@ -195,8 +197,10 @@ impl TopDocs {
|
|||||||
/// learning-to-rank model over various features
|
/// learning-to-rank model over various features
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
/// # use tantivy::schema::{Schema, FAST, TEXT};
|
/// # use tantivy::schema::{Schema, FAST, TEXT};
|
||||||
/// # use tantivy::{doc, Index, DocAddress, DocId, Score};
|
/// # use tantivy::{Index, DocAddress, DocId, Score};
|
||||||
/// # use tantivy::query::QueryParser;
|
/// # use tantivy::query::QueryParser;
|
||||||
/// use tantivy::SegmentReader;
|
/// use tantivy::SegmentReader;
|
||||||
/// use tantivy::collector::TopDocs;
|
/// use tantivy::collector::TopDocs;
|
||||||
@@ -298,8 +302,10 @@ impl TopDocs {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// # #[macro_use]
|
||||||
|
/// # extern crate tantivy;
|
||||||
/// # use tantivy::schema::{Schema, FAST, TEXT};
|
/// # use tantivy::schema::{Schema, FAST, TEXT};
|
||||||
/// # use tantivy::{doc, Index, DocAddress, DocId};
|
/// # use tantivy::{Index, DocAddress, DocId};
|
||||||
/// # use tantivy::query::QueryParser;
|
/// # use tantivy::query::QueryParser;
|
||||||
/// use tantivy::SegmentReader;
|
/// use tantivy::SegmentReader;
|
||||||
/// use tantivy::collector::TopDocs;
|
/// use tantivy::collector::TopDocs;
|
||||||
@@ -431,13 +437,12 @@ impl SegmentCollector for TopScoreSegmentCollector {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::TopDocs;
|
use super::TopDocs;
|
||||||
use crate::collector::Collector;
|
use crate::collector::Collector;
|
||||||
use crate::query::{AllQuery, Query, QueryParser};
|
use crate::query::{Query, QueryParser};
|
||||||
use crate::schema::{Field, Schema, FAST, STORED, TEXT};
|
use crate::schema::{Field, Schema, FAST, STORED, TEXT};
|
||||||
use crate::DocAddress;
|
use crate::DocAddress;
|
||||||
use crate::Index;
|
use crate::Index;
|
||||||
use crate::IndexWriter;
|
use crate::IndexWriter;
|
||||||
use crate::Score;
|
use crate::Score;
|
||||||
use itertools::Itertools;
|
|
||||||
|
|
||||||
fn make_index() -> Index {
|
fn make_index() -> Index {
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
@@ -498,29 +503,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_top_collector_stable_sorting() {
|
|
||||||
let index = make_index();
|
|
||||||
|
|
||||||
// using AllQuery to get a constant score
|
|
||||||
let searcher = index.reader().unwrap().searcher();
|
|
||||||
|
|
||||||
let page_1 = searcher.search(&AllQuery, &TopDocs::with_limit(2)).unwrap();
|
|
||||||
|
|
||||||
let page_2 = searcher.search(&AllQuery, &TopDocs::with_limit(3)).unwrap();
|
|
||||||
|
|
||||||
// precondition for the test to be meaningful: we did get documents
|
|
||||||
// with the same score
|
|
||||||
assert!(page_1.iter().map(|result| result.0).all_equal());
|
|
||||||
assert!(page_2.iter().map(|result| result.0).all_equal());
|
|
||||||
|
|
||||||
// sanity check since we're relying on make_index()
|
|
||||||
assert_eq!(page_1.len(), 2);
|
|
||||||
assert_eq!(page_2.len(), 3);
|
|
||||||
|
|
||||||
assert_eq!(page_1, &page_2[..page_1.len()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn test_top_0() {
|
fn test_top_0() {
|
||||||
@@ -578,7 +560,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
let searcher = index.reader().unwrap().searcher();
|
let searcher = index.reader().unwrap().searcher();
|
||||||
let top_collector = TopDocs::with_limit(4).order_by_u64_field(Field::from_field_id(2));
|
let top_collector = TopDocs::with_limit(4).order_by_u64_field(Field(2));
|
||||||
let segment_reader = searcher.segment_reader(0u32);
|
let segment_reader = searcher.segment_reader(0u32);
|
||||||
top_collector
|
top_collector
|
||||||
.for_segment(0, segment_reader)
|
.for_segment(0, segment_reader)
|
||||||
@@ -609,7 +591,7 @@ mod tests {
|
|||||||
query_field: Field,
|
query_field: Field,
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
mut doc_adder: impl FnMut(&mut IndexWriter) -> (),
|
mut doc_adder: impl FnMut(&mut IndexWriter) -> (),
|
||||||
) -> (Index, Box<dyn Query>) {
|
) -> (Index, Box<Query>) {
|
||||||
let index = Index::create_in_ram(schema);
|
let index = Index::create_in_ram(schema);
|
||||||
|
|
||||||
let mut index_writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
|
let mut index_writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
|
||||||
@@ -619,4 +601,5 @@ mod tests {
|
|||||||
let query = query_parser.parse_query(query).unwrap();
|
let query = query_parser.parse_query(query).unwrap();
|
||||||
(index, query)
|
(index, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::common::BinarySerializable;
|
|||||||
use crate::common::CountingWriter;
|
use crate::common::CountingWriter;
|
||||||
use crate::common::VInt;
|
use crate::common::VInt;
|
||||||
use crate::directory::ReadOnlySource;
|
use crate::directory::ReadOnlySource;
|
||||||
use crate::directory::{TerminatingWrite, WritePtr};
|
use crate::directory::WritePtr;
|
||||||
use crate::schema::Field;
|
use crate::schema::Field;
|
||||||
use crate::space_usage::FieldUsage;
|
use crate::space_usage::FieldUsage;
|
||||||
use crate::space_usage::PerFieldSpaceUsage;
|
use crate::space_usage::PerFieldSpaceUsage;
|
||||||
@@ -42,7 +42,7 @@ pub struct CompositeWrite<W = WritePtr> {
|
|||||||
offsets: HashMap<FileAddr, u64>,
|
offsets: HashMap<FileAddr, u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: TerminatingWrite + Write> CompositeWrite<W> {
|
impl<W: Write> CompositeWrite<W> {
|
||||||
/// Crate a new API writer that writes a composite file
|
/// Crate a new API writer that writes a composite file
|
||||||
/// in a given write.
|
/// in a given write.
|
||||||
pub fn wrap(w: W) -> CompositeWrite<W> {
|
pub fn wrap(w: W) -> CompositeWrite<W> {
|
||||||
@@ -91,7 +91,8 @@ impl<W: TerminatingWrite + Write> CompositeWrite<W> {
|
|||||||
|
|
||||||
let footer_len = (self.write.written_bytes() - footer_offset) as u32;
|
let footer_len = (self.write.written_bytes() - footer_offset) as u32;
|
||||||
footer_len.serialize(&mut self.write)?;
|
footer_len.serialize(&mut self.write)?;
|
||||||
self.write.terminate()
|
self.write.flush()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,13 +200,13 @@ mod test {
|
|||||||
let w = directory.open_write(path).unwrap();
|
let w = directory.open_write(path).unwrap();
|
||||||
let mut composite_write = CompositeWrite::wrap(w);
|
let mut composite_write = CompositeWrite::wrap(w);
|
||||||
{
|
{
|
||||||
let mut write_0 = composite_write.for_field(Field::from_field_id(0u32));
|
let mut write_0 = composite_write.for_field(Field(0u32));
|
||||||
VInt(32431123u64).serialize(&mut write_0).unwrap();
|
VInt(32431123u64).serialize(&mut write_0).unwrap();
|
||||||
write_0.flush().unwrap();
|
write_0.flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut write_4 = composite_write.for_field(Field::from_field_id(4u32));
|
let mut write_4 = composite_write.for_field(Field(4u32));
|
||||||
VInt(2).serialize(&mut write_4).unwrap();
|
VInt(2).serialize(&mut write_4).unwrap();
|
||||||
write_4.flush().unwrap();
|
write_4.flush().unwrap();
|
||||||
}
|
}
|
||||||
@@ -215,18 +216,14 @@ mod test {
|
|||||||
let r = directory.open_read(path).unwrap();
|
let r = directory.open_read(path).unwrap();
|
||||||
let composite_file = CompositeFile::open(&r).unwrap();
|
let composite_file = CompositeFile::open(&r).unwrap();
|
||||||
{
|
{
|
||||||
let file0 = composite_file
|
let file0 = composite_file.open_read(Field(0u32)).unwrap();
|
||||||
.open_read(Field::from_field_id(0u32))
|
|
||||||
.unwrap();
|
|
||||||
let mut file0_buf = file0.as_slice();
|
let mut file0_buf = file0.as_slice();
|
||||||
let payload_0 = VInt::deserialize(&mut file0_buf).unwrap().0;
|
let payload_0 = VInt::deserialize(&mut file0_buf).unwrap().0;
|
||||||
assert_eq!(file0_buf.len(), 0);
|
assert_eq!(file0_buf.len(), 0);
|
||||||
assert_eq!(payload_0, 32431123u64);
|
assert_eq!(payload_0, 32431123u64);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let file4 = composite_file
|
let file4 = composite_file.open_read(Field(4u32)).unwrap();
|
||||||
.open_read(Field::from_field_id(4u32))
|
|
||||||
.unwrap();
|
|
||||||
let mut file4_buf = file4.as_slice();
|
let mut file4_buf = file4.as_slice();
|
||||||
let payload_4 = VInt::deserialize(&mut file4_buf).unwrap().0;
|
let payload_4 = VInt::deserialize(&mut file4_buf).unwrap().0;
|
||||||
assert_eq!(file4_buf.len(), 0);
|
assert_eq!(file4_buf.len(), 0);
|
||||||
@@ -234,4 +231,5 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use crate::directory::AntiCallToken;
|
|
||||||
use crate::directory::TerminatingWrite;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
@@ -44,13 +42,6 @@ impl<W: Write> Write for CountingWriter<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: TerminatingWrite> TerminatingWrite for CountingWriter<W> {
|
|
||||||
fn terminate_ref(&mut self, token: AntiCallToken) -> io::Result<()> {
|
|
||||||
self.flush()?;
|
|
||||||
self.underlying.terminate_ref(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
|
|||||||
@@ -124,24 +124,26 @@ pub fn f64_to_u64(val: f64) -> u64 {
|
|||||||
/// Reverse the mapping given by [`i64_to_u64`](./fn.i64_to_u64.html).
|
/// Reverse the mapping given by [`i64_to_u64`](./fn.i64_to_u64.html).
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn u64_to_f64(val: u64) -> f64 {
|
pub fn u64_to_f64(val: u64) -> f64 {
|
||||||
f64::from_bits(if val & HIGHEST_BIT != 0 {
|
f64::from_bits(
|
||||||
val ^ HIGHEST_BIT
|
if val & HIGHEST_BIT != 0 {
|
||||||
} else {
|
val ^ HIGHEST_BIT
|
||||||
!val
|
} else {
|
||||||
})
|
!val
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod test {
|
pub(crate) mod test {
|
||||||
|
|
||||||
pub use super::serialize::test::fixed_size_test;
|
pub use super::serialize::test::fixed_size_test;
|
||||||
use super::{compute_num_bits, f64_to_u64, i64_to_u64, u64_to_f64, u64_to_i64};
|
use super::{compute_num_bits, i64_to_u64, u64_to_i64, f64_to_u64, u64_to_f64};
|
||||||
use std::f64;
|
use std::f64;
|
||||||
|
|
||||||
fn test_i64_converter_helper(val: i64) {
|
fn test_i64_converter_helper(val: i64) {
|
||||||
assert_eq!(u64_to_i64(i64_to_u64(val)), val);
|
assert_eq!(u64_to_i64(i64_to_u64(val)), val);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_f64_converter_helper(val: f64) {
|
fn test_f64_converter_helper(val: f64) {
|
||||||
assert_eq!(u64_to_f64(f64_to_u64(val)), val);
|
assert_eq!(u64_to_f64(f64_to_u64(val)), val);
|
||||||
}
|
}
|
||||||
@@ -170,8 +172,7 @@ pub(crate) mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_f64_order() {
|
fn test_f64_order() {
|
||||||
assert!(!(f64_to_u64(f64::NEG_INFINITY)..f64_to_u64(f64::INFINITY))
|
assert!(!(f64_to_u64(f64::NEG_INFINITY)..f64_to_u64(f64::INFINITY)).contains(&f64_to_u64(f64::NAN))); //nan is not a number
|
||||||
.contains(&f64_to_u64(f64::NAN))); //nan is not a number
|
|
||||||
assert!(f64_to_u64(1.5) > f64_to_u64(1.0)); //same exponent, different mantissa
|
assert!(f64_to_u64(1.5) > f64_to_u64(1.0)); //same exponent, different mantissa
|
||||||
assert!(f64_to_u64(2.0) > f64_to_u64(1.0)); //same mantissa, different exponent
|
assert!(f64_to_u64(2.0) > f64_to_u64(1.0)); //same mantissa, different exponent
|
||||||
assert!(f64_to_u64(2.0) > f64_to_u64(1.5)); //different exponent and mantissa
|
assert!(f64_to_u64(2.0) > f64_to_u64(1.5)); //different exponent and mantissa
|
||||||
|
|||||||
@@ -199,7 +199,10 @@ pub mod test {
|
|||||||
fn test_serialize_string() {
|
fn test_serialize_string() {
|
||||||
assert_eq!(serialize_test(String::from("")), 1);
|
assert_eq!(serialize_test(String::from("")), 1);
|
||||||
assert_eq!(serialize_test(String::from("ぽよぽよ")), 1 + 3 * 4);
|
assert_eq!(serialize_test(String::from("ぽよぽよ")), 1 + 3 * 4);
|
||||||
assert_eq!(serialize_test(String::from("富士さん見える。")), 1 + 3 * 8);
|
assert_eq!(
|
||||||
|
serialize_test(String::from("富士さん見える。")),
|
||||||
|
1 + 3 * 8
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::Result;
|
use crate::Result;
|
||||||
use crossbeam::channel;
|
use crossbeam::channel;
|
||||||
use rayon::{ThreadPool, ThreadPoolBuilder};
|
use scoped_pool::{Pool, ThreadConfig};
|
||||||
|
|
||||||
/// Search executor whether search request are single thread or multithread.
|
/// Search executor whether search request are single thread or multithread.
|
||||||
///
|
///
|
||||||
@@ -11,7 +11,7 @@ use rayon::{ThreadPool, ThreadPoolBuilder};
|
|||||||
/// used by the client. Second, we may stop using rayon in the future.
|
/// used by the client. Second, we may stop using rayon in the future.
|
||||||
pub enum Executor {
|
pub enum Executor {
|
||||||
SingleThread,
|
SingleThread,
|
||||||
ThreadPool(ThreadPool),
|
ThreadPool(Pool),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Executor {
|
impl Executor {
|
||||||
@@ -21,12 +21,10 @@ impl Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates an Executor that dispatches the tasks in a thread pool.
|
// Creates an Executor that dispatches the tasks in a thread pool.
|
||||||
pub fn multi_thread(num_threads: usize, prefix: &'static str) -> Result<Executor> {
|
pub fn multi_thread(num_threads: usize, prefix: &'static str) -> Executor {
|
||||||
let pool = ThreadPoolBuilder::new()
|
let thread_config = ThreadConfig::new().prefix(prefix);
|
||||||
.num_threads(num_threads)
|
let pool = Pool::with_thread_config(num_threads, thread_config);
|
||||||
.thread_name(move |num| format!("{}{}", prefix, num))
|
Executor::ThreadPool(pool)
|
||||||
.build()?;
|
|
||||||
Ok(Executor::ThreadPool(pool))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform a map in the thread pool.
|
// Perform a map in the thread pool.
|
||||||
@@ -50,9 +48,9 @@ impl Executor {
|
|||||||
let num_fruits = args_with_indices.len();
|
let num_fruits = args_with_indices.len();
|
||||||
let fruit_receiver = {
|
let fruit_receiver = {
|
||||||
let (fruit_sender, fruit_receiver) = channel::unbounded();
|
let (fruit_sender, fruit_receiver) = channel::unbounded();
|
||||||
pool.scope(|scope| {
|
pool.scoped(|scope| {
|
||||||
for arg_with_idx in args_with_indices {
|
for arg_with_idx in args_with_indices {
|
||||||
scope.spawn(|_| {
|
scope.execute(|| {
|
||||||
let (idx, arg) = arg_with_idx;
|
let (idx, arg) = arg_with_idx;
|
||||||
let fruit = f(arg);
|
let fruit = f(arg);
|
||||||
if let Err(err) = fruit_sender.send((idx, fruit)) {
|
if let Err(err) = fruit_sender.send((idx, fruit)) {
|
||||||
@@ -105,7 +103,6 @@ mod tests {
|
|||||||
#[should_panic] //< unfortunately the panic message is not propagated
|
#[should_panic] //< unfortunately the panic message is not propagated
|
||||||
fn test_panic_propagates_multi_thread() {
|
fn test_panic_propagates_multi_thread() {
|
||||||
let _result: Vec<usize> = Executor::multi_thread(1, "search-test")
|
let _result: Vec<usize> = Executor::multi_thread(1, "search-test")
|
||||||
.unwrap()
|
|
||||||
.map(
|
.map(
|
||||||
|_| {
|
|_| {
|
||||||
panic!("panic should propagate");
|
panic!("panic should propagate");
|
||||||
@@ -129,7 +126,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_map_multithread() {
|
fn test_map_multithread() {
|
||||||
let result: Vec<usize> = Executor::multi_thread(3, "search-test")
|
let result: Vec<usize> = Executor::multi_thread(3, "search-test")
|
||||||
.unwrap()
|
|
||||||
.map(|i| Ok(i * 2), 0..10)
|
.map(|i| Ok(i * 2), 0..10)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result.len(), 10);
|
assert_eq!(result.len(), 10);
|
||||||
|
|||||||
@@ -26,10 +26,9 @@ use crate::IndexWriter;
|
|||||||
use crate::Result;
|
use crate::Result;
|
||||||
use num_cpus;
|
use num_cpus;
|
||||||
use std::borrow::BorrowMut;
|
use std::borrow::BorrowMut;
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
#[cfg(feature = "mmap")]
|
#[cfg(feature = "mmap")]
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn load_metas(directory: &dyn Directory, inventory: &SegmentMetaInventory) -> Result<IndexMeta> {
|
fn load_metas(directory: &dyn Directory, inventory: &SegmentMetaInventory) -> Result<IndexMeta> {
|
||||||
@@ -73,16 +72,15 @@ impl Index {
|
|||||||
|
|
||||||
/// Replace the default single thread search executor pool
|
/// Replace the default single thread search executor pool
|
||||||
/// by a thread pool with a given number of threads.
|
/// by a thread pool with a given number of threads.
|
||||||
pub fn set_multithread_executor(&mut self, num_threads: usize) -> Result<()> {
|
pub fn set_multithread_executor(&mut self, num_threads: usize) {
|
||||||
self.executor = Arc::new(Executor::multi_thread(num_threads, "thrd-tantivy-search-")?);
|
self.executor = Arc::new(Executor::multi_thread(num_threads, "thrd-tantivy-search-"));
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the default single thread search executor pool
|
/// Replace the default single thread search executor pool
|
||||||
/// by a thread pool with a given number of threads.
|
/// by a thread pool with a given number of threads.
|
||||||
pub fn set_default_multithread_executor(&mut self) -> Result<()> {
|
pub fn set_default_multithread_executor(&mut self) {
|
||||||
let default_num_threads = num_cpus::get();
|
let default_num_threads = num_cpus::get();
|
||||||
self.set_multithread_executor(default_num_threads)
|
self.set_multithread_executor(default_num_threads);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new index using the `RAMDirectory`.
|
/// Creates a new index using the `RAMDirectory`.
|
||||||
@@ -175,11 +173,11 @@ impl Index {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to access the tokenizer associated to a specific field.
|
/// Helper to access the tokenizer associated to a specific field.
|
||||||
pub fn tokenizer_for_field(&self, field: Field) -> Result<BoxedTokenizer> {
|
pub fn tokenizer_for_field(&self, field: Field) -> Result<Box<dyn BoxedTokenizer>> {
|
||||||
let field_entry = self.schema.get_field_entry(field);
|
let field_entry = self.schema.get_field_entry(field);
|
||||||
let field_type = field_entry.field_type();
|
let field_type = field_entry.field_type();
|
||||||
let tokenizer_manager: &TokenizerManager = self.tokenizers();
|
let tokenizer_manager: &TokenizerManager = self.tokenizers();
|
||||||
let tokenizer_name_opt: Option<BoxedTokenizer> = match field_type {
|
let tokenizer_name_opt: Option<Box<dyn BoxedTokenizer>> = match field_type {
|
||||||
FieldType::Str(text_options) => text_options
|
FieldType::Str(text_options) => text_options
|
||||||
.get_indexing_options()
|
.get_indexing_options()
|
||||||
.map(|text_indexing_options| text_indexing_options.tokenizer().to_string())
|
.map(|text_indexing_options| text_indexing_options.tokenizer().to_string())
|
||||||
@@ -218,22 +216,8 @@ impl Index {
|
|||||||
Index::open(mmap_directory)
|
Index::open(mmap_directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list of the segment metas tracked by the index.
|
pub(crate) fn inventory(&self) -> &SegmentMetaInventory {
|
||||||
///
|
&self.inventory
|
||||||
/// Such segments can of course be part of the index,
|
|
||||||
/// but also they could be segments being currently built or in the middle of a merge
|
|
||||||
/// operation.
|
|
||||||
pub fn list_all_segment_metas(&self) -> Vec<SegmentMeta> {
|
|
||||||
self.inventory.all()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new segment_meta (Advanced user only).
|
|
||||||
///
|
|
||||||
/// As long as the `SegmentMeta` lives, the files associated with the
|
|
||||||
/// `SegmentMeta` are guaranteed to not be garbage collected, regardless of
|
|
||||||
/// whether the segment is recorded as part of the index or not.
|
|
||||||
pub fn new_segment_meta(&self, segment_id: SegmentId, max_doc: u32) -> SegmentMeta {
|
|
||||||
self.inventory.new_segment_meta(segment_id, max_doc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the index using the provided directory
|
/// Open the index using the provided directory
|
||||||
@@ -370,11 +354,6 @@ impl Index {
|
|||||||
.map(SegmentMeta::id)
|
.map(SegmentMeta::id)
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the set of corrupted files
|
|
||||||
pub fn validate_checksum(&self) -> Result<HashSet<PathBuf>> {
|
|
||||||
self.directory.list_damaged().map_err(Into::into)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Index {
|
impl fmt::Debug for Index {
|
||||||
@@ -480,13 +459,13 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tempfile::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_index_on_commit_reload_policy_mmap() {
|
fn test_index_on_commit_reload_policy_mmap() {
|
||||||
let schema = throw_away_schema();
|
let schema = throw_away_schema();
|
||||||
let field = schema.get_field("num_likes").unwrap();
|
let field = schema.get_field("num_likes").unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new("index").unwrap();
|
||||||
let tempdir_path = PathBuf::from(tempdir.path());
|
let tempdir_path = PathBuf::from(tempdir.path());
|
||||||
let index = Index::create_in_dir(&tempdir_path, schema).unwrap();
|
let index = Index::create_in_dir(&tempdir_path, schema).unwrap();
|
||||||
let mut writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
|
let mut writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
|
||||||
@@ -525,7 +504,7 @@ mod tests {
|
|||||||
fn test_index_on_commit_reload_policy_different_directories() {
|
fn test_index_on_commit_reload_policy_different_directories() {
|
||||||
let schema = throw_away_schema();
|
let schema = throw_away_schema();
|
||||||
let field = schema.get_field("num_likes").unwrap();
|
let field = schema.get_field("num_likes").unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new("index").unwrap();
|
||||||
let tempdir_path = PathBuf::from(tempdir.path());
|
let tempdir_path = PathBuf::from(tempdir.path());
|
||||||
let write_index = Index::create_in_dir(&tempdir_path, schema).unwrap();
|
let write_index = Index::create_in_dir(&tempdir_path, schema).unwrap();
|
||||||
let read_index = Index::open_in_dir(&tempdir_path).unwrap();
|
let read_index = Index::open_in_dir(&tempdir_path).unwrap();
|
||||||
@@ -602,4 +581,5 @@ mod tests {
|
|||||||
assert_eq!(searcher.num_docs(), 8_000);
|
assert_eq!(searcher.num_docs(), 8_000);
|
||||||
assert!(mem_right_after_merge_finished < mem_right_after_commit);
|
assert!(mem_right_after_merge_finished < mem_right_after_commit);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ impl SegmentMetaInventory {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub fn new_segment_meta(&self, segment_id: SegmentId, max_doc: u32) -> SegmentMeta {
|
pub fn new_segment_meta(&self, segment_id: SegmentId, max_doc: u32) -> SegmentMeta {
|
||||||
let inner = InnerSegmentMeta {
|
let inner = InnerSegmentMeta {
|
||||||
segment_id,
|
segment_id,
|
||||||
@@ -150,21 +151,6 @@ impl SegmentMeta {
|
|||||||
self.num_deleted_docs() > 0
|
self.num_deleted_docs() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the max_doc value from the `SegmentMeta`.
|
|
||||||
///
|
|
||||||
/// This method is only used when updating `max_doc` from 0
|
|
||||||
/// as we finalize a fresh new segment.
|
|
||||||
pub(crate) fn with_max_doc(self, max_doc: u32) -> SegmentMeta {
|
|
||||||
assert_eq!(self.tracked.max_doc, 0);
|
|
||||||
assert!(self.tracked.deletes.is_none());
|
|
||||||
let tracked = self.tracked.map(move |inner_meta| InnerSegmentMeta {
|
|
||||||
segment_id: inner_meta.segment_id,
|
|
||||||
max_doc,
|
|
||||||
deletes: None,
|
|
||||||
});
|
|
||||||
SegmentMeta { tracked }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn with_delete_meta(self, num_deleted_docs: u32, opstamp: Opstamp) -> SegmentMeta {
|
pub fn with_delete_meta(self, num_deleted_docs: u32, opstamp: Opstamp) -> SegmentMeta {
|
||||||
let delete_meta = DeleteMeta {
|
let delete_meta = DeleteMeta {
|
||||||
|
|||||||
@@ -50,17 +50,6 @@ impl Segment {
|
|||||||
&self.meta
|
&self.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the max_doc value from the `SegmentMeta`.
|
|
||||||
///
|
|
||||||
/// This method is only used when updating `max_doc` from 0
|
|
||||||
/// as we finalize a fresh new segment.
|
|
||||||
pub(crate) fn with_max_doc(self, max_doc: u32) -> Segment {
|
|
||||||
Segment {
|
|
||||||
index: self.index,
|
|
||||||
meta: self.meta.with_max_doc(max_doc),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn with_delete_meta(self, num_deleted_docs: u32, opstamp: Opstamp) -> Segment {
|
pub fn with_delete_meta(self, num_deleted_docs: u32, opstamp: Opstamp) -> Segment {
|
||||||
Segment {
|
Segment {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::error::Error;
|
|
||||||
use std::str::FromStr;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::sync::atomic;
|
use std::sync::atomic;
|
||||||
|
|
||||||
@@ -54,51 +52,15 @@ impl SegmentId {
|
|||||||
/// and the rest is random.
|
/// and the rest is random.
|
||||||
///
|
///
|
||||||
/// Picking the first 8 chars is ok to identify
|
/// Picking the first 8 chars is ok to identify
|
||||||
/// segments in a display message (e.g. a5c4dfcb).
|
/// segments in a display message.
|
||||||
pub fn short_uuid_string(&self) -> String {
|
pub fn short_uuid_string(&self) -> String {
|
||||||
(&self.0.to_simple_ref().to_string()[..8]).to_string()
|
(&self.0.to_simple_ref().to_string()[..8]).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a segment uuid string.
|
/// Returns a segment uuid string.
|
||||||
///
|
|
||||||
/// It consists in 32 lowercase hexadecimal chars
|
|
||||||
/// (e.g. a5c4dfcbdfe645089129e308e26d5523)
|
|
||||||
pub fn uuid_string(&self) -> String {
|
pub fn uuid_string(&self) -> String {
|
||||||
self.0.to_simple_ref().to_string()
|
self.0.to_simple_ref().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a `SegmentId` string from the full uuid string.
|
|
||||||
///
|
|
||||||
/// E.g. "a5c4dfcbdfe645089129e308e26d5523"
|
|
||||||
pub fn from_uuid_string(uuid_string: &str) -> Result<SegmentId, SegmentIdParseError> {
|
|
||||||
FromStr::from_str(uuid_string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error type used when parsing a `SegmentId` from a string fails.
|
|
||||||
pub struct SegmentIdParseError(uuid::Error);
|
|
||||||
|
|
||||||
impl Error for SegmentIdParseError {}
|
|
||||||
|
|
||||||
impl fmt::Debug for SegmentIdParseError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for SegmentIdParseError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for SegmentId {
|
|
||||||
type Err = SegmentIdParseError;
|
|
||||||
|
|
||||||
fn from_str(uuid_string: &str) -> Result<Self, SegmentIdParseError> {
|
|
||||||
let uuid = Uuid::parse_str(uuid_string).map_err(SegmentIdParseError)?;
|
|
||||||
Ok(SegmentId(uuid))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for SegmentId {
|
impl fmt::Debug for SegmentId {
|
||||||
@@ -118,18 +80,3 @@ impl Ord for SegmentId {
|
|||||||
self.0.as_bytes().cmp(other.0.as_bytes())
|
self.0.as_bytes().cmp(other.0.as_bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::SegmentId;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_to_uuid_string() {
|
|
||||||
let full_uuid = "a5c4dfcbdfe645089129e308e26d5523";
|
|
||||||
let segment_id = SegmentId::from_uuid_string(full_uuid).unwrap();
|
|
||||||
assert_eq!(segment_id.uuid_string(), full_uuid);
|
|
||||||
assert_eq!(segment_id.short_uuid_string(), "a5c4dfcb");
|
|
||||||
// one extra char
|
|
||||||
assert!(SegmentId::from_uuid_string("a5c4dfcbdfe645089129e308e26d5523b").is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ impl RetryPolicy {
|
|||||||
///
|
///
|
||||||
/// It is transparently associated to a lock file, that gets deleted
|
/// It is transparently associated to a lock file, that gets deleted
|
||||||
/// on `Drop.` The lock is released automatically on `Drop`.
|
/// on `Drop.` The lock is released automatically on `Drop`.
|
||||||
pub struct DirectoryLock(Box<dyn Send + Sync + 'static>);
|
pub struct DirectoryLock(Box<dyn Drop + Send + Sync + 'static>);
|
||||||
|
|
||||||
struct DirectoryLockGuard {
|
struct DirectoryLockGuard {
|
||||||
directory: Box<dyn Directory>,
|
directory: Box<dyn Directory>,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> From<Box<T>> for DirectoryLock {
|
impl<T: Drop + Send + Sync + 'static> From<Box<T>> for DirectoryLock {
|
||||||
fn from(underlying: Box<T>) -> Self {
|
fn from(underlying: Box<T>) -> Self {
|
||||||
DirectoryLock(underlying)
|
DirectoryLock(underlying)
|
||||||
}
|
}
|
||||||
@@ -118,8 +118,6 @@ pub trait Directory: DirectoryClone + fmt::Debug + Send + Sync + 'static {
|
|||||||
///
|
///
|
||||||
/// Specifically, subsequent writes or flushes should
|
/// Specifically, subsequent writes or flushes should
|
||||||
/// have no effect on the returned `ReadOnlySource` object.
|
/// have no effect on the returned `ReadOnlySource` object.
|
||||||
///
|
|
||||||
/// You should only use this to read files create with [`open_write`]
|
|
||||||
fn open_read(&self, path: &Path) -> result::Result<ReadOnlySource, OpenReadError>;
|
fn open_read(&self, path: &Path) -> result::Result<ReadOnlySource, OpenReadError>;
|
||||||
|
|
||||||
/// Removes a file
|
/// Removes a file
|
||||||
@@ -159,8 +157,6 @@ pub trait Directory: DirectoryClone + fmt::Debug + Send + Sync + 'static {
|
|||||||
/// atomic_write.
|
/// atomic_write.
|
||||||
///
|
///
|
||||||
/// This should only be used for small files.
|
/// This should only be used for small files.
|
||||||
///
|
|
||||||
/// You should only use this to read files create with [`atomic_write`]
|
|
||||||
fn atomic_read(&self, path: &Path) -> Result<Vec<u8>, OpenReadError>;
|
fn atomic_read(&self, path: &Path) -> Result<Vec<u8>, OpenReadError>;
|
||||||
|
|
||||||
/// Atomically replace the content of a file with data.
|
/// Atomically replace the content of a file with data.
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
use crate::directory::read_only_source::ReadOnlySource;
|
|
||||||
use crate::directory::{AntiCallToken, TerminatingWrite};
|
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
|
||||||
use crc32fast::Hasher;
|
|
||||||
use std::io;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
const COMMON_FOOTER_SIZE: usize = 4 * 5;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Footer {
|
|
||||||
pub tantivy_version: (u32, u32, u32),
|
|
||||||
pub meta: String,
|
|
||||||
pub versioned_footer: VersionedFooter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Footer {
|
|
||||||
pub fn new(versioned_footer: VersionedFooter) -> Self {
|
|
||||||
let tantivy_version = (
|
|
||||||
env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
|
|
||||||
env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
|
|
||||||
env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
|
|
||||||
);
|
|
||||||
Footer {
|
|
||||||
tantivy_version,
|
|
||||||
meta: format!(
|
|
||||||
"tantivy {}.{}.{}, index v{}",
|
|
||||||
tantivy_version.0,
|
|
||||||
tantivy_version.1,
|
|
||||||
tantivy_version.2,
|
|
||||||
versioned_footer.version()
|
|
||||||
),
|
|
||||||
versioned_footer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
let mut res = self.versioned_footer.to_bytes();
|
|
||||||
res.extend_from_slice(self.meta.as_bytes());
|
|
||||||
let len = res.len();
|
|
||||||
res.resize(len + COMMON_FOOTER_SIZE, 0);
|
|
||||||
let mut common_footer = &mut res[len..];
|
|
||||||
LittleEndian::write_u32(&mut common_footer, self.meta.len() as u32);
|
|
||||||
LittleEndian::write_u32(&mut common_footer[4..], self.tantivy_version.0);
|
|
||||||
LittleEndian::write_u32(&mut common_footer[8..], self.tantivy_version.1);
|
|
||||||
LittleEndian::write_u32(&mut common_footer[12..], self.tantivy_version.2);
|
|
||||||
LittleEndian::write_u32(&mut common_footer[16..], (len + COMMON_FOOTER_SIZE) as u32);
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(data: &[u8]) -> Result<Self, io::Error> {
|
|
||||||
let len = data.len();
|
|
||||||
if len < COMMON_FOOTER_SIZE + 4 {
|
|
||||||
// 4 bytes for index version, stored in versioned footer
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
format!("File corrupted. The footer len must be over 24, while the entire file len is {}", len)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = LittleEndian::read_u32(&data[len - 4..]) as usize;
|
|
||||||
if len < size as usize {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
format!(
|
|
||||||
"File corrupted. The footer len is {}, while the entire file len is {}",
|
|
||||||
size, len
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let footer = &data[len - size as usize..];
|
|
||||||
let meta_len = LittleEndian::read_u32(&footer[size - 20..]) as usize;
|
|
||||||
let tantivy_major = LittleEndian::read_u32(&footer[size - 16..]);
|
|
||||||
let tantivy_minor = LittleEndian::read_u32(&footer[size - 12..]);
|
|
||||||
let tantivy_patch = LittleEndian::read_u32(&footer[size - 8..]);
|
|
||||||
Ok(Footer {
|
|
||||||
tantivy_version: (tantivy_major, tantivy_minor, tantivy_patch),
|
|
||||||
meta: String::from_utf8_lossy(&footer[size - meta_len - 20..size - 20]).into_owned(),
|
|
||||||
versioned_footer: VersionedFooter::from_bytes(&footer[..size - meta_len - 20])?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_footer(source: ReadOnlySource) -> Result<(Footer, ReadOnlySource), io::Error> {
|
|
||||||
let footer = Footer::from_bytes(source.as_slice())?;
|
|
||||||
let reader = source.slice_to(source.as_slice().len() - footer.size());
|
|
||||||
Ok((footer, reader))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> usize {
|
|
||||||
self.versioned_footer.size() as usize + self.meta.len() + 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum VersionedFooter {
|
|
||||||
UnknownVersion { version: u32, size: u32 },
|
|
||||||
V0(u32), // crc
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VersionedFooter {
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
VersionedFooter::V0(crc) => {
|
|
||||||
let mut res = vec![0; 8];
|
|
||||||
LittleEndian::write_u32(&mut res, 0);
|
|
||||||
LittleEndian::write_u32(&mut res[4..], *crc);
|
|
||||||
res
|
|
||||||
}
|
|
||||||
VersionedFooter::UnknownVersion { .. } => {
|
|
||||||
panic!("Unsupported index should never get serialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(footer: &[u8]) -> Result<Self, io::Error> {
|
|
||||||
assert!(footer.len() >= 4);
|
|
||||||
let version = LittleEndian::read_u32(footer);
|
|
||||||
match version {
|
|
||||||
0 => {
|
|
||||||
if footer.len() == 8 {
|
|
||||||
Ok(VersionedFooter::V0(LittleEndian::read_u32(&footer[4..])))
|
|
||||||
} else {
|
|
||||||
Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
format!(
|
|
||||||
"File corrupted. The versioned footer len is {}, while it should be 8",
|
|
||||||
footer.len()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
version => Ok(VersionedFooter::UnknownVersion {
|
|
||||||
version,
|
|
||||||
size: footer.len() as u32,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> u32 {
|
|
||||||
match self {
|
|
||||||
VersionedFooter::V0(_) => 8,
|
|
||||||
VersionedFooter::UnknownVersion { size, .. } => *size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn version(&self) -> u32 {
|
|
||||||
match self {
|
|
||||||
VersionedFooter::V0(_) => 0,
|
|
||||||
VersionedFooter::UnknownVersion { version, .. } => *version,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn crc(&self) -> Option<u32> {
|
|
||||||
match self {
|
|
||||||
VersionedFooter::V0(crc) => Some(*crc),
|
|
||||||
VersionedFooter::UnknownVersion { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct FooterProxy<W: TerminatingWrite> {
|
|
||||||
/// always Some except after terminate call
|
|
||||||
hasher: Option<Hasher>,
|
|
||||||
/// always Some except after terminate call
|
|
||||||
writer: Option<W>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: TerminatingWrite> FooterProxy<W> {
|
|
||||||
pub fn new(writer: W) -> Self {
|
|
||||||
FooterProxy {
|
|
||||||
hasher: Some(Hasher::new()),
|
|
||||||
writer: Some(writer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: TerminatingWrite> Write for FooterProxy<W> {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
let count = self.writer.as_mut().unwrap().write(buf)?;
|
|
||||||
self.hasher.as_mut().unwrap().update(&buf[..count]);
|
|
||||||
Ok(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.writer.as_mut().unwrap().flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: TerminatingWrite> TerminatingWrite for FooterProxy<W> {
|
|
||||||
fn terminate_ref(&mut self, _: AntiCallToken) -> io::Result<()> {
|
|
||||||
let crc = self.hasher.take().unwrap().finalize();
|
|
||||||
|
|
||||||
let footer = Footer::new(VersionedFooter::V0(crc)).to_bytes();
|
|
||||||
let mut writer = self.writer.take().unwrap();
|
|
||||||
writer.write_all(&footer)?;
|
|
||||||
writer.terminate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::directory::footer::{Footer, VersionedFooter};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_deserialize_footer() {
|
|
||||||
let crc = 123456;
|
|
||||||
let footer = Footer::new(VersionedFooter::V0(crc));
|
|
||||||
let footer_bytes = footer.to_bytes();
|
|
||||||
|
|
||||||
assert_eq!(Footer::from_bytes(&footer_bytes).unwrap(), footer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::core::MANAGED_FILEPATH;
|
use crate::core::MANAGED_FILEPATH;
|
||||||
use crate::directory::error::{DeleteError, IOError, LockError, OpenReadError, OpenWriteError};
|
use crate::directory::error::{DeleteError, IOError, LockError, OpenReadError, OpenWriteError};
|
||||||
use crate::directory::footer::{Footer, FooterProxy};
|
|
||||||
use crate::directory::DirectoryLock;
|
use crate::directory::DirectoryLock;
|
||||||
use crate::directory::Lock;
|
use crate::directory::Lock;
|
||||||
use crate::directory::META_LOCK;
|
use crate::directory::META_LOCK;
|
||||||
@@ -9,7 +8,6 @@ use crate::directory::{WatchCallback, WatchHandle};
|
|||||||
use crate::error::DataCorruption;
|
use crate::error::DataCorruption;
|
||||||
use crate::Directory;
|
use crate::Directory;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use crc32fast::Hasher;
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -209,59 +207,17 @@ impl ManagedDirectory {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify checksum of a managed file
|
|
||||||
pub fn validate_checksum(&self, path: &Path) -> result::Result<bool, OpenReadError> {
|
|
||||||
let reader = self.directory.open_read(path)?;
|
|
||||||
let (footer, data) = Footer::extract_footer(reader)
|
|
||||||
.map_err(|err| IOError::with_path(path.to_path_buf(), err))?;
|
|
||||||
let mut hasher = Hasher::new();
|
|
||||||
hasher.update(data.as_slice());
|
|
||||||
let crc = hasher.finalize();
|
|
||||||
Ok(footer
|
|
||||||
.versioned_footer
|
|
||||||
.crc()
|
|
||||||
.map(|v| v == crc)
|
|
||||||
.unwrap_or(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List files for which checksum does not match content
|
|
||||||
pub fn list_damaged(&self) -> result::Result<HashSet<PathBuf>, OpenReadError> {
|
|
||||||
let mut hashset = HashSet::new();
|
|
||||||
let managed_paths = self
|
|
||||||
.meta_informations
|
|
||||||
.read()
|
|
||||||
.expect("Managed directory rlock poisoned in list damaged.")
|
|
||||||
.managed_paths
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
for path in managed_paths.into_iter() {
|
|
||||||
if !self.validate_checksum(&path)? {
|
|
||||||
hashset.insert(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(hashset)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Directory for ManagedDirectory {
|
impl Directory for ManagedDirectory {
|
||||||
fn open_read(&self, path: &Path) -> result::Result<ReadOnlySource, OpenReadError> {
|
fn open_read(&self, path: &Path) -> result::Result<ReadOnlySource, OpenReadError> {
|
||||||
let read_only_source = self.directory.open_read(path)?;
|
self.directory.open_read(path)
|
||||||
let (_footer, reader) = Footer::extract_footer(read_only_source)
|
|
||||||
.map_err(|err| IOError::with_path(path.to_path_buf(), err))?;
|
|
||||||
Ok(reader)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_write(&mut self, path: &Path) -> result::Result<WritePtr, OpenWriteError> {
|
fn open_write(&mut self, path: &Path) -> result::Result<WritePtr, OpenWriteError> {
|
||||||
self.register_file_as_managed(path)
|
self.register_file_as_managed(path)
|
||||||
.map_err(|e| IOError::with_path(path.to_owned(), e))?;
|
.map_err(|e| IOError::with_path(path.to_owned(), e))?;
|
||||||
Ok(io::BufWriter::new(Box::new(FooterProxy::new(
|
self.directory.open_write(path)
|
||||||
self.directory
|
|
||||||
.open_write(path)?
|
|
||||||
.into_inner()
|
|
||||||
.map_err(|_| ())
|
|
||||||
.expect("buffer should be empty"),
|
|
||||||
))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn atomic_write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
fn atomic_write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||||
@@ -303,16 +259,15 @@ impl Clone for ManagedDirectory {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_mmap_specific {
|
mod tests_mmap_specific {
|
||||||
|
|
||||||
use crate::directory::{Directory, ManagedDirectory, MmapDirectory, TerminatingWrite};
|
use crate::directory::{Directory, ManagedDirectory, MmapDirectory};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs::OpenOptions;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tempfile::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_managed_directory() {
|
fn test_managed_directory() {
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new("tantivy-test").unwrap();
|
||||||
let tempdir_path = PathBuf::from(tempdir.path());
|
let tempdir_path = PathBuf::from(tempdir.path());
|
||||||
|
|
||||||
let test_path1: &'static Path = Path::new("some_path_for_test");
|
let test_path1: &'static Path = Path::new("some_path_for_test");
|
||||||
@@ -320,14 +275,15 @@ mod tests_mmap_specific {
|
|||||||
{
|
{
|
||||||
let mmap_directory = MmapDirectory::open(&tempdir_path).unwrap();
|
let mmap_directory = MmapDirectory::open(&tempdir_path).unwrap();
|
||||||
let mut managed_directory = ManagedDirectory::wrap(mmap_directory).unwrap();
|
let mut managed_directory = ManagedDirectory::wrap(mmap_directory).unwrap();
|
||||||
let write_file = managed_directory.open_write(test_path1).unwrap();
|
let mut write_file = managed_directory.open_write(test_path1).unwrap();
|
||||||
write_file.terminate().unwrap();
|
write_file.flush().unwrap();
|
||||||
managed_directory
|
managed_directory
|
||||||
.atomic_write(test_path2, &[0u8, 1u8])
|
.atomic_write(test_path2, &[0u8, 1u8])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(managed_directory.exists(test_path1));
|
assert!(managed_directory.exists(test_path1));
|
||||||
assert!(managed_directory.exists(test_path2));
|
assert!(managed_directory.exists(test_path2));
|
||||||
let living_files: HashSet<PathBuf> = [test_path1.to_owned()].iter().cloned().collect();
|
let living_files: HashSet<PathBuf> =
|
||||||
|
[test_path1.to_owned()].into_iter().cloned().collect();
|
||||||
managed_directory.garbage_collect(|| living_files);
|
managed_directory.garbage_collect(|| living_files);
|
||||||
assert!(managed_directory.exists(test_path1));
|
assert!(managed_directory.exists(test_path1));
|
||||||
assert!(!managed_directory.exists(test_path2));
|
assert!(!managed_directory.exists(test_path2));
|
||||||
@@ -348,15 +304,15 @@ mod tests_mmap_specific {
|
|||||||
fn test_managed_directory_gc_while_mmapped() {
|
fn test_managed_directory_gc_while_mmapped() {
|
||||||
let test_path1: &'static Path = Path::new("some_path_for_test");
|
let test_path1: &'static Path = Path::new("some_path_for_test");
|
||||||
|
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new("index").unwrap();
|
||||||
let tempdir_path = PathBuf::from(tempdir.path());
|
let tempdir_path = PathBuf::from(tempdir.path());
|
||||||
let living_files = HashSet::new();
|
let living_files = HashSet::new();
|
||||||
|
|
||||||
let mmap_directory = MmapDirectory::open(&tempdir_path).unwrap();
|
let mmap_directory = MmapDirectory::open(&tempdir_path).unwrap();
|
||||||
let mut managed_directory = ManagedDirectory::wrap(mmap_directory).unwrap();
|
let mut managed_directory = ManagedDirectory::wrap(mmap_directory).unwrap();
|
||||||
let mut write = managed_directory.open_write(test_path1).unwrap();
|
managed_directory
|
||||||
write.write_all(&[0u8, 1u8]).unwrap();
|
.atomic_write(test_path1, &vec![0u8, 1u8])
|
||||||
write.terminate().unwrap();
|
.unwrap();
|
||||||
assert!(managed_directory.exists(test_path1));
|
assert!(managed_directory.exists(test_path1));
|
||||||
|
|
||||||
let _mmap_read = managed_directory.open_read(test_path1).unwrap();
|
let _mmap_read = managed_directory.open_read(test_path1).unwrap();
|
||||||
@@ -375,38 +331,4 @@ mod tests_mmap_specific {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_checksum() {
|
|
||||||
let test_path1: &'static Path = Path::new("some_path_for_test");
|
|
||||||
let test_path2: &'static Path = Path::new("other_test_path");
|
|
||||||
|
|
||||||
let tempdir = TempDir::new().unwrap();
|
|
||||||
let tempdir_path = PathBuf::from(tempdir.path());
|
|
||||||
|
|
||||||
let mmap_directory = MmapDirectory::open(&tempdir_path).unwrap();
|
|
||||||
let mut managed_directory = ManagedDirectory::wrap(mmap_directory).unwrap();
|
|
||||||
let mut write = managed_directory.open_write(test_path1).unwrap();
|
|
||||||
write.write_all(&[0u8, 1u8]).unwrap();
|
|
||||||
write.terminate().unwrap();
|
|
||||||
|
|
||||||
let mut write = managed_directory.open_write(test_path2).unwrap();
|
|
||||||
write.write_all(&[3u8, 4u8, 5u8]).unwrap();
|
|
||||||
write.terminate().unwrap();
|
|
||||||
|
|
||||||
assert!(managed_directory.list_damaged().unwrap().is_empty());
|
|
||||||
|
|
||||||
let mut corrupted_path = tempdir_path.clone();
|
|
||||||
corrupted_path.push(test_path2);
|
|
||||||
let mut file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.open(&corrupted_path)
|
|
||||||
.unwrap();
|
|
||||||
file.write_all(&[255u8]).unwrap();
|
|
||||||
file.flush().unwrap();
|
|
||||||
drop(file);
|
|
||||||
|
|
||||||
let damaged = managed_directory.list_damaged().unwrap();
|
|
||||||
assert_eq!(damaged.len(), 1);
|
|
||||||
assert!(damaged.contains(test_path2));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use crate::directory::error::{
|
|||||||
DeleteError, IOError, OpenDirectoryError, OpenReadError, OpenWriteError,
|
DeleteError, IOError, OpenDirectoryError, OpenReadError, OpenWriteError,
|
||||||
};
|
};
|
||||||
use crate::directory::read_only_source::BoxedData;
|
use crate::directory::read_only_source::BoxedData;
|
||||||
use crate::directory::AntiCallToken;
|
|
||||||
use crate::directory::Directory;
|
use crate::directory::Directory;
|
||||||
use crate::directory::DirectoryLock;
|
use crate::directory::DirectoryLock;
|
||||||
use crate::directory::Lock;
|
use crate::directory::Lock;
|
||||||
@@ -19,7 +18,7 @@ use crate::directory::ReadOnlySource;
|
|||||||
use crate::directory::WatchCallback;
|
use crate::directory::WatchCallback;
|
||||||
use crate::directory::WatchCallbackList;
|
use crate::directory::WatchCallbackList;
|
||||||
use crate::directory::WatchHandle;
|
use crate::directory::WatchHandle;
|
||||||
use crate::directory::{TerminatingWrite, WritePtr};
|
use crate::directory::WritePtr;
|
||||||
use atomicwrites;
|
use atomicwrites;
|
||||||
use memmap::Mmap;
|
use memmap::Mmap;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -37,7 +36,7 @@ use std::sync::Mutex;
|
|||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::sync::Weak;
|
use std::sync::Weak;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use tempfile::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
/// Create a default io error given a string.
|
/// Create a default io error given a string.
|
||||||
pub(crate) fn make_io_err(msg: String) -> io::Error {
|
pub(crate) fn make_io_err(msg: String) -> io::Error {
|
||||||
@@ -142,28 +141,42 @@ impl MmapCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WatcherWrapper {
|
struct InnerWatcherWrapper {
|
||||||
_watcher: Mutex<notify::RecommendedWatcher>,
|
_watcher: Mutex<notify::RecommendedWatcher>,
|
||||||
watcher_router: Arc<WatchCallbackList>,
|
watcher_router: WatchCallbackList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InnerWatcherWrapper {
|
||||||
|
pub fn new(path: &Path) -> Result<(Self, Receiver<notify::RawEvent>), notify::Error> {
|
||||||
|
let (tx, watcher_recv): (Sender<RawEvent>, Receiver<RawEvent>) = channel();
|
||||||
|
// We need to initialize the
|
||||||
|
let mut watcher = notify::raw_watcher(tx)?;
|
||||||
|
watcher.watch(path, RecursiveMode::Recursive)?;
|
||||||
|
let inner = InnerWatcherWrapper {
|
||||||
|
_watcher: Mutex::new(watcher),
|
||||||
|
watcher_router: Default::default(),
|
||||||
|
};
|
||||||
|
Ok((inner, watcher_recv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct WatcherWrapper {
|
||||||
|
inner: Arc<InnerWatcherWrapper>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WatcherWrapper {
|
impl WatcherWrapper {
|
||||||
pub fn new(path: &Path) -> Result<Self, OpenDirectoryError> {
|
pub fn new(path: &Path) -> Result<Self, OpenDirectoryError> {
|
||||||
let (tx, watcher_recv): (Sender<RawEvent>, Receiver<RawEvent>) = channel();
|
let (inner, watcher_recv) = InnerWatcherWrapper::new(path).map_err(|err| match err {
|
||||||
// We need to initialize the
|
notify::Error::PathNotFound => OpenDirectoryError::DoesNotExist(path.to_owned()),
|
||||||
let watcher = notify::raw_watcher(tx)
|
_ => {
|
||||||
.and_then(|mut watcher| {
|
panic!("Unknown error while starting watching directory {:?}", path);
|
||||||
watcher.watch(path, RecursiveMode::Recursive)?;
|
}
|
||||||
Ok(watcher)
|
})?;
|
||||||
})
|
let watcher_wrapper = WatcherWrapper {
|
||||||
.map_err(|err| match err {
|
inner: Arc::new(inner),
|
||||||
notify::Error::PathNotFound => OpenDirectoryError::DoesNotExist(path.to_owned()),
|
};
|
||||||
_ => {
|
let watcher_wrapper_clone = watcher_wrapper.clone();
|
||||||
panic!("Unknown error while starting watching directory {:?}", path);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
let watcher_router: Arc<WatchCallbackList> = Default::default();
|
|
||||||
let watcher_router_clone = watcher_router.clone();
|
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("meta-file-watch-thread".to_string())
|
.name("meta-file-watch-thread".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
@@ -174,7 +187,7 @@ impl WatcherWrapper {
|
|||||||
// We might want to be more accurate than this at one point.
|
// We might want to be more accurate than this at one point.
|
||||||
if let Some(filename) = changed_path.file_name() {
|
if let Some(filename) = changed_path.file_name() {
|
||||||
if filename == *META_FILEPATH {
|
if filename == *META_FILEPATH {
|
||||||
watcher_router_clone.broadcast();
|
watcher_wrapper_clone.inner.watcher_router.broadcast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,15 +200,13 @@ impl WatcherWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})?;
|
})
|
||||||
Ok(WatcherWrapper {
|
.expect("Failed to spawn thread to watch meta.json");
|
||||||
_watcher: Mutex::new(watcher),
|
Ok(watcher_wrapper)
|
||||||
watcher_router,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn watch(&mut self, watch_callback: WatchCallback) -> WatchHandle {
|
pub fn watch(&mut self, watch_callback: WatchCallback) -> WatchHandle {
|
||||||
self.watcher_router.subscribe(watch_callback)
|
self.inner.watcher_router.subscribe(watch_callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +265,7 @@ impl MmapDirectoryInner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(watch_wrapper) = self.watcher.write().unwrap().as_mut() {
|
if let Some(watch_wrapper) = self.watcher.write().unwrap().as_mut() {
|
||||||
Ok(watch_wrapper.watch(watch_callback))
|
return Ok(watch_wrapper.watch(watch_callback));
|
||||||
} else {
|
} else {
|
||||||
unreachable!("At this point, watch wrapper is supposed to be initialized");
|
unreachable!("At this point, watch wrapper is supposed to be initialized");
|
||||||
}
|
}
|
||||||
@@ -283,7 +294,7 @@ impl MmapDirectory {
|
|||||||
/// This is mostly useful to test the MmapDirectory itself.
|
/// This is mostly useful to test the MmapDirectory itself.
|
||||||
/// For your unit tests, prefer the RAMDirectory.
|
/// For your unit tests, prefer the RAMDirectory.
|
||||||
pub fn create_from_tempdir() -> Result<MmapDirectory, OpenDirectoryError> {
|
pub fn create_from_tempdir() -> Result<MmapDirectory, OpenDirectoryError> {
|
||||||
let tempdir = TempDir::new().map_err(OpenDirectoryError::IoError)?;
|
let tempdir = TempDir::new("index").map_err(OpenDirectoryError::IoError)?;
|
||||||
let tempdir_path = PathBuf::from(tempdir.path());
|
let tempdir_path = PathBuf::from(tempdir.path());
|
||||||
MmapDirectory::new(tempdir_path, Some(tempdir))
|
MmapDirectory::new(tempdir_path, Some(tempdir))
|
||||||
}
|
}
|
||||||
@@ -401,12 +412,6 @@ impl Seek for SafeFileWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminatingWrite for SafeFileWriter {
|
|
||||||
fn terminate_ref(&mut self, _: AntiCallToken) -> io::Result<()> {
|
|
||||||
self.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Directory for MmapDirectory {
|
impl Directory for MmapDirectory {
|
||||||
fn open_read(&self, path: &Path) -> result::Result<ReadOnlySource, OpenReadError> {
|
fn open_read(&self, path: &Path) -> result::Result<ReadOnlySource, OpenReadError> {
|
||||||
debug!("Open Read {:?}", path);
|
debug!("Open Read {:?}", path);
|
||||||
@@ -637,7 +642,7 @@ mod tests {
|
|||||||
fn test_watch_wrapper() {
|
fn test_watch_wrapper() {
|
||||||
let counter: Arc<AtomicUsize> = Default::default();
|
let counter: Arc<AtomicUsize> = Default::default();
|
||||||
let counter_clone = counter.clone();
|
let counter_clone = counter.clone();
|
||||||
let tmp_dir = tempfile::TempDir::new().unwrap();
|
let tmp_dir: TempDir = tempdir::TempDir::new("test_watch_wrapper").unwrap();
|
||||||
let tmp_dirpath = tmp_dir.path().to_owned();
|
let tmp_dirpath = tmp_dir.path().to_owned();
|
||||||
let mut watch_wrapper = WatcherWrapper::new(&tmp_dirpath).unwrap();
|
let mut watch_wrapper = WatcherWrapper::new(&tmp_dirpath).unwrap();
|
||||||
let tmp_file = tmp_dirpath.join("coucou");
|
let tmp_file = tmp_dirpath.join("coucou");
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ mod mmap_directory;
|
|||||||
|
|
||||||
mod directory;
|
mod directory;
|
||||||
mod directory_lock;
|
mod directory_lock;
|
||||||
mod footer;
|
|
||||||
mod managed_directory;
|
mod managed_directory;
|
||||||
mod ram_directory;
|
mod ram_directory;
|
||||||
mod read_only_source;
|
mod read_only_source;
|
||||||
@@ -25,49 +24,18 @@ pub use self::ram_directory::RAMDirectory;
|
|||||||
pub use self::read_only_source::ReadOnlySource;
|
pub use self::read_only_source::ReadOnlySource;
|
||||||
pub(crate) use self::watch_event_router::WatchCallbackList;
|
pub(crate) use self::watch_event_router::WatchCallbackList;
|
||||||
pub use self::watch_event_router::{WatchCallback, WatchHandle};
|
pub use self::watch_event_router::{WatchCallback, WatchHandle};
|
||||||
use std::io::{self, BufWriter, Write};
|
use std::io::{BufWriter, Write};
|
||||||
|
|
||||||
#[cfg(feature = "mmap")]
|
#[cfg(feature = "mmap")]
|
||||||
pub use self::mmap_directory::MmapDirectory;
|
pub use self::mmap_directory::MmapDirectory;
|
||||||
|
|
||||||
pub use self::managed_directory::ManagedDirectory;
|
pub use self::managed_directory::ManagedDirectory;
|
||||||
|
|
||||||
/// Struct used to prevent from calling [`terminate_ref`](trait.TerminatingWrite#method.terminate_ref) directly
|
|
||||||
pub struct AntiCallToken(());
|
|
||||||
|
|
||||||
/// Trait used to indicate when no more write need to be done on a writer
|
|
||||||
pub trait TerminatingWrite: Write {
|
|
||||||
/// Indicate that the writer will no longer be used. Internally call terminate_ref.
|
|
||||||
fn terminate(mut self) -> io::Result<()>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.terminate_ref(AntiCallToken(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// You should implement this function to define custom behavior.
|
|
||||||
/// This function should flush any buffer it may hold.
|
|
||||||
fn terminate_ref(&mut self, _: AntiCallToken) -> io::Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: TerminatingWrite + ?Sized> TerminatingWrite for Box<W> {
|
|
||||||
fn terminate_ref(&mut self, token: AntiCallToken) -> io::Result<()> {
|
|
||||||
self.as_mut().terminate_ref(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: TerminatingWrite> TerminatingWrite for BufWriter<W> {
|
|
||||||
fn terminate_ref(&mut self, a: AntiCallToken) -> io::Result<()> {
|
|
||||||
self.flush()?;
|
|
||||||
self.get_mut().terminate_ref(a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write object for Directory.
|
/// Write object for Directory.
|
||||||
///
|
///
|
||||||
/// `WritePtr` are required to implement both Write
|
/// `WritePtr` are required to implement both Write
|
||||||
/// and Seek.
|
/// and Seek.
|
||||||
pub type WritePtr = BufWriter<Box<dyn TerminatingWrite>>;
|
pub type WritePtr = BufWriter<Box<dyn Write>>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::core::META_FILEPATH;
|
use crate::core::META_FILEPATH;
|
||||||
use crate::directory::error::{DeleteError, OpenReadError, OpenWriteError};
|
use crate::directory::error::{DeleteError, OpenReadError, OpenWriteError};
|
||||||
use crate::directory::AntiCallToken;
|
|
||||||
use crate::directory::WatchCallbackList;
|
use crate::directory::WatchCallbackList;
|
||||||
|
use crate::directory::WritePtr;
|
||||||
use crate::directory::{Directory, ReadOnlySource, WatchCallback, WatchHandle};
|
use crate::directory::{Directory, ReadOnlySource, WatchCallback, WatchHandle};
|
||||||
use crate::directory::{TerminatingWrite, WritePtr};
|
|
||||||
use fail::fail_point;
|
use fail::fail_point;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -72,12 +71,6 @@ impl Write for VecWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminatingWrite for VecWriter {
|
|
||||||
fn terminate_ref(&mut self, _: AntiCallToken) -> io::Result<()> {
|
|
||||||
self.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct InnerDirectory {
|
struct InnerDirectory {
|
||||||
fs: HashMap<PathBuf, ReadOnlySource>,
|
fs: HashMap<PathBuf, ReadOnlySource>,
|
||||||
@@ -184,7 +177,7 @@ impl Directory for RAMDirectory {
|
|||||||
fn atomic_write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
fn atomic_write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||||
fail_point!("RAMDirectory::atomic_write", |msg| Err(io::Error::new(
|
fail_point!("RAMDirectory::atomic_write", |msg| Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
msg.unwrap_or_else(|| "Undefined".to_string())
|
msg.unwrap_or("Undefined".to_string())
|
||||||
)));
|
)));
|
||||||
let path_buf = PathBuf::from(path);
|
let path_buf = PathBuf::from(path);
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ fn test_watch(directory: &mut dyn Directory) {
|
|||||||
assert!(directory
|
assert!(directory
|
||||||
.atomic_write(Path::new("meta.json"), b"random_test_data_2")
|
.atomic_write(Path::new("meta.json"), b"random_test_data_2")
|
||||||
.is_ok());
|
.is_ok());
|
||||||
for _ in 0..1_000 {
|
for _ in 0..100 {
|
||||||
if counter.load(Ordering::SeqCst) > i {
|
if counter.load(Ordering::SeqCst) > i {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,4 +152,5 @@ mod tests {
|
|||||||
thread::sleep(Duration::from_millis(WAIT_TIME));
|
thread::sleep(Duration::from_millis(WAIT_TIME));
|
||||||
assert_eq!(2, counter.load(Ordering::SeqCst));
|
assert_eq!(2, counter.load(Ordering::SeqCst));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,9 +170,3 @@ impl From<serde_json::Error> for TantivyError {
|
|||||||
TantivyError::IOError(io_err.into())
|
TantivyError::IOError(io_err.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<rayon::ThreadPoolBuildError> for TantivyError {
|
|
||||||
fn from(error: rayon::ThreadPoolBuildError) -> TantivyError {
|
|
||||||
TantivyError::SystemError(error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,14 +10,11 @@ use std::io::Write;
|
|||||||
/// Write a delete `BitSet`
|
/// Write a delete `BitSet`
|
||||||
///
|
///
|
||||||
/// where `delete_bitset` is the set of deleted `DocId`.
|
/// where `delete_bitset` is the set of deleted `DocId`.
|
||||||
pub fn write_delete_bitset(
|
pub fn write_delete_bitset(delete_bitset: &BitSet, writer: &mut WritePtr) -> io::Result<()> {
|
||||||
delete_bitset: &BitSet,
|
let max_doc = delete_bitset.capacity();
|
||||||
max_doc: u32,
|
|
||||||
writer: &mut WritePtr,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let mut byte = 0u8;
|
let mut byte = 0u8;
|
||||||
let mut shift = 0u8;
|
let mut shift = 0u8;
|
||||||
for doc in 0..(max_doc as usize) {
|
for doc in 0..max_doc {
|
||||||
if delete_bitset.contains(doc) {
|
if delete_bitset.contains(doc) {
|
||||||
byte |= 1 << shift;
|
byte |= 1 << shift;
|
||||||
}
|
}
|
||||||
@@ -89,17 +86,18 @@ mod tests {
|
|||||||
use bit_set::BitSet;
|
use bit_set::BitSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn test_delete_bitset_helper(bitset: &BitSet, max_doc: u32) {
|
fn test_delete_bitset_helper(bitset: &BitSet) {
|
||||||
let test_path = PathBuf::from("test");
|
let test_path = PathBuf::from("test");
|
||||||
let mut directory = RAMDirectory::create();
|
let mut directory = RAMDirectory::create();
|
||||||
{
|
{
|
||||||
let mut writer = directory.open_write(&*test_path).unwrap();
|
let mut writer = directory.open_write(&*test_path).unwrap();
|
||||||
write_delete_bitset(bitset, max_doc, &mut writer).unwrap();
|
write_delete_bitset(bitset, &mut writer).unwrap();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let source = directory.open_read(&test_path).unwrap();
|
let source = directory.open_read(&test_path).unwrap();
|
||||||
let delete_bitset = DeleteBitSet::open(source);
|
let delete_bitset = DeleteBitSet::open(source);
|
||||||
for doc in 0..max_doc as usize {
|
let n = bitset.capacity();
|
||||||
|
for doc in 0..n {
|
||||||
assert_eq!(bitset.contains(doc), delete_bitset.is_deleted(doc as DocId));
|
assert_eq!(bitset.contains(doc), delete_bitset.is_deleted(doc as DocId));
|
||||||
}
|
}
|
||||||
assert_eq!(delete_bitset.len(), bitset.len());
|
assert_eq!(delete_bitset.len(), bitset.len());
|
||||||
@@ -112,7 +110,7 @@ mod tests {
|
|||||||
let mut bitset = BitSet::with_capacity(10);
|
let mut bitset = BitSet::with_capacity(10);
|
||||||
bitset.insert(1);
|
bitset.insert(1);
|
||||||
bitset.insert(9);
|
bitset.insert(9);
|
||||||
test_delete_bitset_helper(&bitset, 10);
|
test_delete_bitset_helper(&bitset);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let mut bitset = BitSet::with_capacity(8);
|
let mut bitset = BitSet::with_capacity(8);
|
||||||
@@ -121,7 +119,7 @@ mod tests {
|
|||||||
bitset.insert(3);
|
bitset.insert(3);
|
||||||
bitset.insert(5);
|
bitset.insert(5);
|
||||||
bitset.insert(7);
|
bitset.insert(7);
|
||||||
test_delete_bitset_helper(&bitset, 8);
|
test_delete_bitset_helper(&bitset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "unstable"))]
|
#[cfg(all(test, feature = "unstable"))]
|
||||||
@@ -436,9 +437,9 @@ mod bench {
|
|||||||
use super::tests::FIELD;
|
use super::tests::FIELD;
|
||||||
use super::tests::{generate_permutation, SCHEMA};
|
use super::tests::{generate_permutation, SCHEMA};
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::common::CompositeFile;
|
use common::CompositeFile;
|
||||||
use crate::directory::{Directory, RAMDirectory, WritePtr};
|
use directory::{Directory, RAMDirectory, WritePtr};
|
||||||
use crate::fastfield::FastFieldReader;
|
use fastfield::FastFieldReader;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use test::{self, Bencher};
|
use test::{self, Bencher};
|
||||||
@@ -536,4 +537,5 @@ mod bench {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use crate::postings::UnorderedTermId;
|
|||||||
use crate::schema::{Document, Field};
|
use crate::schema::{Document, Field};
|
||||||
use crate::termdict::TermOrdinal;
|
use crate::termdict::TermOrdinal;
|
||||||
use crate::DocId;
|
use crate::DocId;
|
||||||
use fnv::FnvHashMap;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
/// Writer for multi-valued (as in, more than one value per document)
|
/// Writer for multi-valued (as in, more than one value per document)
|
||||||
@@ -102,7 +102,7 @@ impl MultiValueIntFastFieldWriter {
|
|||||||
pub fn serialize(
|
pub fn serialize(
|
||||||
&self,
|
&self,
|
||||||
serializer: &mut FastFieldSerializer,
|
serializer: &mut FastFieldSerializer,
|
||||||
mapping_opt: Option<&FnvHashMap<UnorderedTermId, TermOrdinal>>,
|
mapping_opt: Option<&HashMap<UnorderedTermId, TermOrdinal>>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
{
|
{
|
||||||
// writing the offset index
|
// writing the offset index
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ impl FastFieldReaders {
|
|||||||
fast_bytes: Default::default(),
|
fast_bytes: Default::default(),
|
||||||
fast_fields_composite: fast_fields_composite.clone(),
|
fast_fields_composite: fast_fields_composite.clone(),
|
||||||
};
|
};
|
||||||
for (field, field_entry) in schema.fields() {
|
for (field_id, field_entry) in schema.fields().iter().enumerate() {
|
||||||
|
let field = Field(field_id as u32);
|
||||||
let field_type = field_entry.field_type();
|
let field_type = field_entry.field_type();
|
||||||
if field_type == &FieldType::Bytes {
|
if field_type == &FieldType::Bytes {
|
||||||
let idx_reader = fast_fields_composite
|
let idx_reader = fast_fields_composite
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use crate::fastfield::{BytesFastFieldWriter, FastFieldSerializer};
|
|||||||
use crate::postings::UnorderedTermId;
|
use crate::postings::UnorderedTermId;
|
||||||
use crate::schema::{Cardinality, Document, Field, FieldType, Schema};
|
use crate::schema::{Cardinality, Document, Field, FieldType, Schema};
|
||||||
use crate::termdict::TermOrdinal;
|
use crate::termdict::TermOrdinal;
|
||||||
use fnv::FnvHashMap;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
@@ -24,16 +23,15 @@ impl FastFieldsWriter {
|
|||||||
let mut multi_values_writers = Vec::new();
|
let mut multi_values_writers = Vec::new();
|
||||||
let mut bytes_value_writers = Vec::new();
|
let mut bytes_value_writers = Vec::new();
|
||||||
|
|
||||||
for (field, field_entry) in schema.fields() {
|
for (field_id, field_entry) in schema.fields().iter().enumerate() {
|
||||||
|
let field = Field(field_id as u32);
|
||||||
let default_value = match *field_entry.field_type() {
|
let default_value = match *field_entry.field_type() {
|
||||||
FieldType::I64(_) => common::i64_to_u64(0i64),
|
FieldType::I64(_) => common::i64_to_u64(0i64),
|
||||||
FieldType::F64(_) => common::f64_to_u64(0.0f64),
|
FieldType::F64(_) => common::f64_to_u64(0.0f64),
|
||||||
_ => 0u64,
|
_ => 0u64,
|
||||||
};
|
};
|
||||||
match *field_entry.field_type() {
|
match *field_entry.field_type() {
|
||||||
FieldType::I64(ref int_options)
|
FieldType::I64(ref int_options) | FieldType::U64(ref int_options) | FieldType::F64(ref int_options) => {
|
||||||
| FieldType::U64(ref int_options)
|
|
||||||
| FieldType::F64(ref int_options) => {
|
|
||||||
match int_options.get_fastfield_cardinality() {
|
match int_options.get_fastfield_cardinality() {
|
||||||
Some(Cardinality::SingleValue) => {
|
Some(Cardinality::SingleValue) => {
|
||||||
let mut fast_field_writer = IntFastFieldWriter::new(field);
|
let mut fast_field_writer = IntFastFieldWriter::new(field);
|
||||||
@@ -116,7 +114,7 @@ impl FastFieldsWriter {
|
|||||||
pub fn serialize(
|
pub fn serialize(
|
||||||
&self,
|
&self,
|
||||||
serializer: &mut FastFieldSerializer,
|
serializer: &mut FastFieldSerializer,
|
||||||
mapping: &HashMap<Field, FnvHashMap<UnorderedTermId, TermOrdinal>>,
|
mapping: &HashMap<Field, HashMap<UnorderedTermId, TermOrdinal>>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
for field_writer in &self.single_value_writers {
|
for field_writer in &self.single_value_writers {
|
||||||
field_writer.serialize(serializer)?;
|
field_writer.serialize(serializer)?;
|
||||||
|
|||||||
@@ -22,14 +22,11 @@ impl FieldNormsWriter {
|
|||||||
pub(crate) fn fields_with_fieldnorm(schema: &Schema) -> Vec<Field> {
|
pub(crate) fn fields_with_fieldnorm(schema: &Schema) -> Vec<Field> {
|
||||||
schema
|
schema
|
||||||
.fields()
|
.fields()
|
||||||
.filter_map(|(field, field_entry)| {
|
.iter()
|
||||||
if field_entry.is_indexed() {
|
.enumerate()
|
||||||
Some(field)
|
.filter(|&(_, field_entry)| field_entry.is_indexed())
|
||||||
} else {
|
.map(|(field, _)| Field(field as u32))
|
||||||
None
|
.collect::<Vec<Field>>()
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize with state for tracking the field norm fields
|
/// Initialize with state for tracking the field norm fields
|
||||||
@@ -38,7 +35,7 @@ impl FieldNormsWriter {
|
|||||||
let fields = FieldNormsWriter::fields_with_fieldnorm(schema);
|
let fields = FieldNormsWriter::fields_with_fieldnorm(schema);
|
||||||
let max_field = fields
|
let max_field = fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(Field::field_id)
|
.map(|field| field.0)
|
||||||
.max()
|
.max()
|
||||||
.map(|max_field_id| max_field_id as usize + 1)
|
.map(|max_field_id| max_field_id as usize + 1)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@@ -53,8 +50,8 @@ impl FieldNormsWriter {
|
|||||||
///
|
///
|
||||||
/// Will extend with 0-bytes for documents that have not been seen.
|
/// Will extend with 0-bytes for documents that have not been seen.
|
||||||
pub fn fill_up_to_max_doc(&mut self, max_doc: DocId) {
|
pub fn fill_up_to_max_doc(&mut self, max_doc: DocId) {
|
||||||
for field in self.fields.iter() {
|
for &field in self.fields.iter() {
|
||||||
self.fieldnorms_buffer[field.field_id() as usize].resize(max_doc as usize, 0u8);
|
self.fieldnorms_buffer[field.0 as usize].resize(max_doc as usize, 0u8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +64,7 @@ impl FieldNormsWriter {
|
|||||||
/// * field - the field being set
|
/// * field - the field being set
|
||||||
/// * fieldnorm - the number of terms present in document `doc` in field `field`
|
/// * fieldnorm - the number of terms present in document `doc` in field `field`
|
||||||
pub fn record(&mut self, doc: DocId, field: Field, fieldnorm: u32) {
|
pub fn record(&mut self, doc: DocId, field: Field, fieldnorm: u32) {
|
||||||
let fieldnorm_buffer: &mut Vec<u8> = &mut self.fieldnorms_buffer[field.field_id() as usize];
|
let fieldnorm_buffer: &mut Vec<u8> = &mut self.fieldnorms_buffer[field.0 as usize];
|
||||||
assert!(
|
assert!(
|
||||||
fieldnorm_buffer.len() <= doc as usize,
|
fieldnorm_buffer.len() <= doc as usize,
|
||||||
"Cannot register a given fieldnorm twice"
|
"Cannot register a given fieldnorm twice"
|
||||||
@@ -80,7 +77,7 @@ impl FieldNormsWriter {
|
|||||||
/// Serialize the seen fieldnorm values to the serializer for all fields.
|
/// Serialize the seen fieldnorm values to the serializer for all fields.
|
||||||
pub fn serialize(&self, fieldnorms_serializer: &mut FieldNormsSerializer) -> io::Result<()> {
|
pub fn serialize(&self, fieldnorms_serializer: &mut FieldNormsSerializer) -> io::Result<()> {
|
||||||
for &field in self.fields.iter() {
|
for &field in self.fields.iter() {
|
||||||
let fieldnorm_values: &[u8] = &self.fieldnorms_buffer[field.field_id() as usize][..];
|
let fieldnorm_values: &[u8] = &self.fieldnorms_buffer[field.0 as usize][..];
|
||||||
fieldnorms_serializer.serialize_field(field, fieldnorm_values)?;
|
fieldnorms_serializer.serialize_field(field, fieldnorm_values)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ mod tests {
|
|||||||
let delete_queue = DeleteQueue::new();
|
let delete_queue = DeleteQueue::new();
|
||||||
|
|
||||||
let make_op = |i: usize| {
|
let make_op = |i: usize| {
|
||||||
let field = Field::from_field_id(1u32);
|
let field = Field(1u32);
|
||||||
DeleteOperation {
|
DeleteOperation {
|
||||||
opstamp: i as u64,
|
opstamp: i as u64,
|
||||||
term: Term::from_field_u64(field, i as u64),
|
term: Term::from_field_u64(field, i as u64),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use crate::core::SegmentId;
|
|||||||
use crate::core::SegmentMeta;
|
use crate::core::SegmentMeta;
|
||||||
use crate::core::SegmentReader;
|
use crate::core::SegmentReader;
|
||||||
use crate::directory::DirectoryLock;
|
use crate::directory::DirectoryLock;
|
||||||
use crate::directory::TerminatingWrite;
|
|
||||||
use crate::docset::DocSet;
|
use crate::docset::DocSet;
|
||||||
use crate::error::TantivyError;
|
use crate::error::TantivyError;
|
||||||
use crate::fastfield::write_delete_bitset;
|
use crate::fastfield::write_delete_bitset;
|
||||||
@@ -148,6 +147,7 @@ pub(crate) fn advance_deletes(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let delete_cursor = segment_entry.delete_cursor();
|
let delete_cursor = segment_entry.delete_cursor();
|
||||||
|
|
||||||
compute_deleted_bitset(
|
compute_deleted_bitset(
|
||||||
&mut delete_bitset,
|
&mut delete_bitset,
|
||||||
&segment_reader,
|
&segment_reader,
|
||||||
@@ -167,8 +167,7 @@ pub(crate) fn advance_deletes(
|
|||||||
if num_deleted_docs > 0 {
|
if num_deleted_docs > 0 {
|
||||||
segment = segment.with_delete_meta(num_deleted_docs as u32, target_opstamp);
|
segment = segment.with_delete_meta(num_deleted_docs as u32, target_opstamp);
|
||||||
let mut delete_file = segment.open_write(SegmentComponent::DELETE)?;
|
let mut delete_file = segment.open_write(SegmentComponent::DELETE)?;
|
||||||
write_delete_bitset(&delete_bitset, max_doc, &mut delete_file)?;
|
write_delete_bitset(&delete_bitset, &mut delete_file)?;
|
||||||
delete_file.terminate()?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
segment_entry.set_meta(segment.meta().clone());
|
segment_entry.set_meta(segment.meta().clone());
|
||||||
@@ -177,13 +176,13 @@ pub(crate) fn advance_deletes(
|
|||||||
|
|
||||||
fn index_documents(
|
fn index_documents(
|
||||||
memory_budget: usize,
|
memory_budget: usize,
|
||||||
segment: Segment,
|
segment: &Segment,
|
||||||
grouped_document_iterator: &mut dyn Iterator<Item = OperationGroup>,
|
grouped_document_iterator: &mut dyn Iterator<Item = OperationGroup>,
|
||||||
segment_updater: &mut SegmentUpdater,
|
segment_updater: &mut SegmentUpdater,
|
||||||
mut delete_cursor: DeleteCursor,
|
mut delete_cursor: DeleteCursor,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let schema = segment.schema();
|
let schema = segment.schema();
|
||||||
|
let segment_id = segment.id();
|
||||||
let mut segment_writer = SegmentWriter::for_segment(memory_budget, segment.clone(), &schema)?;
|
let mut segment_writer = SegmentWriter::for_segment(memory_budget, segment.clone(), &schema)?;
|
||||||
for document_group in grouped_document_iterator {
|
for document_group in grouped_document_iterator {
|
||||||
for doc in document_group {
|
for doc in document_group {
|
||||||
@@ -203,32 +202,25 @@ fn index_documents(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_doc = segment_writer.max_doc();
|
let num_docs = segment_writer.max_doc();
|
||||||
|
|
||||||
// this is ensured by the call to peek before starting
|
// this is ensured by the call to peek before starting
|
||||||
// the worker thread.
|
// the worker thread.
|
||||||
assert!(max_doc > 0);
|
assert!(num_docs > 0);
|
||||||
|
|
||||||
let doc_opstamps: Vec<Opstamp> = segment_writer.finalize()?;
|
let doc_opstamps: Vec<Opstamp> = segment_writer.finalize()?;
|
||||||
|
let segment_meta = segment
|
||||||
let segment_with_max_doc = segment.with_max_doc(max_doc);
|
.index()
|
||||||
|
.inventory()
|
||||||
|
.new_segment_meta(segment_id, num_docs);
|
||||||
|
|
||||||
let last_docstamp: Opstamp = *(doc_opstamps.last().unwrap());
|
let last_docstamp: Opstamp = *(doc_opstamps.last().unwrap());
|
||||||
|
|
||||||
let delete_bitset_opt = apply_deletes(
|
let delete_bitset_opt =
|
||||||
&segment_with_max_doc,
|
apply_deletes(&segment, &mut delete_cursor, &doc_opstamps, last_docstamp)?;
|
||||||
&mut delete_cursor,
|
|
||||||
&doc_opstamps,
|
|
||||||
last_docstamp,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let segment_entry = SegmentEntry::new(
|
let segment_entry = SegmentEntry::new(segment_meta, delete_cursor, delete_bitset_opt);
|
||||||
segment_with_max_doc.meta().clone(),
|
Ok(segment_updater.add_segment(segment_entry))
|
||||||
delete_cursor,
|
|
||||||
delete_bitset_opt,
|
|
||||||
);
|
|
||||||
segment_updater.add_segment(segment_entry);
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_deletes(
|
fn apply_deletes(
|
||||||
@@ -244,9 +236,7 @@ fn apply_deletes(
|
|||||||
}
|
}
|
||||||
let segment_reader = SegmentReader::open(segment)?;
|
let segment_reader = SegmentReader::open(segment)?;
|
||||||
let doc_to_opstamps = DocToOpstampMapping::from(doc_opstamps);
|
let doc_to_opstamps = DocToOpstampMapping::from(doc_opstamps);
|
||||||
|
let mut deleted_bitset = BitSet::with_capacity(segment_reader.max_doc() as usize);
|
||||||
let max_doc = segment.meta().max_doc();
|
|
||||||
let mut deleted_bitset = BitSet::with_capacity(max_doc as usize);
|
|
||||||
let may_have_deletes = compute_deleted_bitset(
|
let may_have_deletes = compute_deleted_bitset(
|
||||||
&mut deleted_bitset,
|
&mut deleted_bitset,
|
||||||
&segment_reader,
|
&segment_reader,
|
||||||
@@ -418,7 +408,7 @@ impl IndexWriter {
|
|||||||
let segment = index.new_segment();
|
let segment = index.new_segment();
|
||||||
index_documents(
|
index_documents(
|
||||||
mem_budget,
|
mem_budget,
|
||||||
segment,
|
&segment,
|
||||||
&mut document_iterator,
|
&mut document_iterator,
|
||||||
&mut segment_updater,
|
&mut segment_updater,
|
||||||
delete_cursor.clone(),
|
delete_cursor.clone(),
|
||||||
@@ -460,10 +450,12 @@ impl IndexWriter {
|
|||||||
/// by clearing and resubmitting necessary documents
|
/// by clearing and resubmitting necessary documents
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use tantivy::collector::TopDocs;
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
/// use tantivy::query::QueryParser;
|
/// use tantivy::query::QueryParser;
|
||||||
|
/// use tantivy::collector::TopDocs;
|
||||||
/// use tantivy::schema::*;
|
/// use tantivy::schema::*;
|
||||||
/// use tantivy::{doc, Index};
|
/// use tantivy::Index;
|
||||||
///
|
///
|
||||||
/// fn main() -> tantivy::Result<()> {
|
/// fn main() -> tantivy::Result<()> {
|
||||||
/// let mut schema_builder = Schema::builder();
|
/// let mut schema_builder = Schema::builder();
|
||||||
@@ -769,6 +761,7 @@ mod tests {
|
|||||||
use crate::Index;
|
use crate::Index;
|
||||||
use crate::ReloadPolicy;
|
use crate::ReloadPolicy;
|
||||||
use crate::Term;
|
use crate::Term;
|
||||||
|
use fail;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_operations_group() {
|
fn test_operations_group() {
|
||||||
@@ -1190,4 +1183,5 @@ mod tests {
|
|||||||
assert!(clear_again.is_ok());
|
assert!(clear_again.is_ok());
|
||||||
assert!(commit_again.is_ok());
|
assert!(commit_again.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ impl IndexMerger {
|
|||||||
fast_field_serializer: &mut FastFieldSerializer,
|
fast_field_serializer: &mut FastFieldSerializer,
|
||||||
mut term_ord_mappings: HashMap<Field, TermOrdinalMapping>,
|
mut term_ord_mappings: HashMap<Field, TermOrdinalMapping>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for (field, field_entry) in self.schema.fields() {
|
for (field_id, field_entry) in self.schema.fields().iter().enumerate() {
|
||||||
|
let field = Field(field_id as u32);
|
||||||
let field_type = field_entry.field_type();
|
let field_type = field_entry.field_type();
|
||||||
match *field_type {
|
match *field_type {
|
||||||
FieldType::HierarchicalFacet => {
|
FieldType::HierarchicalFacet => {
|
||||||
@@ -648,12 +649,15 @@ impl IndexMerger {
|
|||||||
serializer: &mut InvertedIndexSerializer,
|
serializer: &mut InvertedIndexSerializer,
|
||||||
) -> Result<HashMap<Field, TermOrdinalMapping>> {
|
) -> Result<HashMap<Field, TermOrdinalMapping>> {
|
||||||
let mut term_ordinal_mappings = HashMap::new();
|
let mut term_ordinal_mappings = HashMap::new();
|
||||||
for (field, field_entry) in self.schema.fields() {
|
for (field_ord, field_entry) in self.schema.fields().iter().enumerate() {
|
||||||
if field_entry.is_indexed() {
|
if field_entry.is_indexed() {
|
||||||
if let Some(term_ordinal_mapping) =
|
let indexed_field = Field(field_ord as u32);
|
||||||
self.write_postings_for_field(field, field_entry.field_type(), serializer)?
|
if let Some(term_ordinal_mapping) = self.write_postings_for_field(
|
||||||
{
|
indexed_field,
|
||||||
term_ordinal_mappings.insert(field, term_ordinal_mapping);
|
field_entry.field_type(),
|
||||||
|
serializer,
|
||||||
|
)? {
|
||||||
|
term_ordinal_mappings.insert(indexed_field, term_ordinal_mapping);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,25 +28,3 @@ pub use self::segment_writer::SegmentWriter;
|
|||||||
|
|
||||||
/// Alias for the default merge policy, which is the `LogMergePolicy`.
|
/// Alias for the default merge policy, which is the `LogMergePolicy`.
|
||||||
pub type DefaultMergePolicy = LogMergePolicy;
|
pub type DefaultMergePolicy = LogMergePolicy;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::schema::{self, Schema};
|
|
||||||
use crate::{Index, Term};
|
|
||||||
#[test]
|
|
||||||
fn test_advance_delete_bug() {
|
|
||||||
let mut schema_builder = Schema::builder();
|
|
||||||
let text_field = schema_builder.add_text_field("text", schema::TEXT);
|
|
||||||
let index = Index::create_from_tempdir(schema_builder.build()).unwrap();
|
|
||||||
let mut index_writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
|
|
||||||
// there must be one deleted document in the segment
|
|
||||||
index_writer.add_document(doc!(text_field=>"b"));
|
|
||||||
index_writer.delete_term(Term::from_field_text(text_field, "b"));
|
|
||||||
// we need enough data to trigger the bug (at least 32 documents)
|
|
||||||
for _ in 0..32 {
|
|
||||||
index_writer.add_document(doc!(text_field=>"c"));
|
|
||||||
}
|
|
||||||
index_writer.commit().unwrap();
|
|
||||||
index_writer.commit().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -134,4 +134,5 @@ mod tests {
|
|||||||
}
|
}
|
||||||
assert_eq!(segment_ids(&segment_register), vec![segment_id_merged]);
|
assert_eq!(segment_ids(&segment_register), vec![segment_id_merged]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,9 @@ fn perform_merge(
|
|||||||
|
|
||||||
let num_docs = merger.write(segment_serializer)?;
|
let num_docs = merger.write(segment_serializer)?;
|
||||||
|
|
||||||
let segment_meta = index.new_segment_meta(merged_segment.id(), num_docs);
|
let segment_meta = index
|
||||||
|
.inventory()
|
||||||
|
.new_segment_meta(merged_segment.id(), num_docs);
|
||||||
|
|
||||||
let after_merge_segment_entry = SegmentEntry::new(segment_meta.clone(), delete_cursor, None);
|
let after_merge_segment_entry = SegmentEntry::new(segment_meta.clone(), delete_cursor, None);
|
||||||
Ok(after_merge_segment_entry)
|
Ok(after_merge_segment_entry)
|
||||||
@@ -199,12 +201,14 @@ impl SegmentUpdater {
|
|||||||
self.0.pool.spawn_fn(move || Ok(f(me_clone)))
|
self.0.pool.spawn_fn(move || Ok(f(me_clone)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_segment(&self, segment_entry: SegmentEntry) {
|
pub fn add_segment(&self, segment_entry: SegmentEntry) -> bool {
|
||||||
self.run_async(|segment_updater| {
|
self.run_async(|segment_updater| {
|
||||||
segment_updater.0.segment_manager.add_segment(segment_entry);
|
segment_updater.0.segment_manager.add_segment(segment_entry);
|
||||||
segment_updater.consider_merge_options();
|
segment_updater.consider_merge_options();
|
||||||
|
true
|
||||||
})
|
})
|
||||||
.forget();
|
.forget();
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Orders `SegmentManager` to remove all segments
|
/// Orders `SegmentManager` to remove all segments
|
||||||
@@ -278,7 +282,7 @@ impl SegmentUpdater {
|
|||||||
fn list_files(&self) -> HashSet<PathBuf> {
|
fn list_files(&self) -> HashSet<PathBuf> {
|
||||||
let mut files = HashSet::new();
|
let mut files = HashSet::new();
|
||||||
files.insert(META_FILEPATH.to_path_buf());
|
files.insert(META_FILEPATH.to_path_buf());
|
||||||
for segment_meta in self.0.index.list_all_segment_metas() {
|
for segment_meta in self.0.index.inventory().all() {
|
||||||
files.extend(segment_meta.list_files());
|
files.extend(segment_meta.list_files());
|
||||||
}
|
}
|
||||||
files
|
files
|
||||||
|
|||||||
@@ -6,15 +6,14 @@ use crate::fieldnorm::FieldNormsWriter;
|
|||||||
use crate::indexer::segment_serializer::SegmentSerializer;
|
use crate::indexer::segment_serializer::SegmentSerializer;
|
||||||
use crate::postings::compute_table_size;
|
use crate::postings::compute_table_size;
|
||||||
use crate::postings::MultiFieldPostingsWriter;
|
use crate::postings::MultiFieldPostingsWriter;
|
||||||
|
use crate::schema::FieldEntry;
|
||||||
use crate::schema::FieldType;
|
use crate::schema::FieldType;
|
||||||
use crate::schema::Schema;
|
use crate::schema::Schema;
|
||||||
use crate::schema::Term;
|
use crate::schema::Term;
|
||||||
use crate::schema::Value;
|
use crate::schema::Value;
|
||||||
use crate::schema::{Field, FieldEntry};
|
|
||||||
use crate::tokenizer::BoxedTokenizer;
|
use crate::tokenizer::BoxedTokenizer;
|
||||||
use crate::tokenizer::FacetTokenizer;
|
use crate::tokenizer::FacetTokenizer;
|
||||||
use crate::tokenizer::PreTokenizedStream;
|
use crate::tokenizer::{TokenStream, Tokenizer};
|
||||||
use crate::tokenizer::{TokenStream, TokenStreamChain, Tokenizer};
|
|
||||||
use crate::DocId;
|
use crate::DocId;
|
||||||
use crate::Opstamp;
|
use crate::Opstamp;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
@@ -50,7 +49,7 @@ pub struct SegmentWriter {
|
|||||||
fast_field_writers: FastFieldsWriter,
|
fast_field_writers: FastFieldsWriter,
|
||||||
fieldnorms_writer: FieldNormsWriter,
|
fieldnorms_writer: FieldNormsWriter,
|
||||||
doc_opstamps: Vec<Opstamp>,
|
doc_opstamps: Vec<Opstamp>,
|
||||||
tokenizers: Vec<Option<BoxedTokenizer>>,
|
tokenizers: Vec<Option<Box<dyn BoxedTokenizer>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SegmentWriter {
|
impl SegmentWriter {
|
||||||
@@ -71,10 +70,12 @@ impl SegmentWriter {
|
|||||||
let table_num_bits = initial_table_size(memory_budget)?;
|
let table_num_bits = initial_table_size(memory_budget)?;
|
||||||
let segment_serializer = SegmentSerializer::for_segment(&mut segment)?;
|
let segment_serializer = SegmentSerializer::for_segment(&mut segment)?;
|
||||||
let multifield_postings = MultiFieldPostingsWriter::new(schema, table_num_bits);
|
let multifield_postings = MultiFieldPostingsWriter::new(schema, table_num_bits);
|
||||||
let tokenizers = schema
|
let tokenizers =
|
||||||
.fields()
|
schema
|
||||||
.map(
|
.fields()
|
||||||
|(_, field_entry): (Field, &FieldEntry)| match field_entry.field_type() {
|
.iter()
|
||||||
|
.map(FieldEntry::field_type)
|
||||||
|
.map(|field_type| match *field_type {
|
||||||
FieldType::Str(ref text_options) => text_options
|
FieldType::Str(ref text_options) => text_options
|
||||||
.get_indexing_options()
|
.get_indexing_options()
|
||||||
.and_then(|text_index_option| {
|
.and_then(|text_index_option| {
|
||||||
@@ -82,9 +83,8 @@ impl SegmentWriter {
|
|||||||
segment.index().tokenizers().get(tokenizer_name)
|
segment.index().tokenizers().get(tokenizer_name)
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
})
|
||||||
)
|
.collect();
|
||||||
.collect();
|
|
||||||
Ok(SegmentWriter {
|
Ok(SegmentWriter {
|
||||||
max_doc: 0,
|
max_doc: 0,
|
||||||
multifield_postings,
|
multifield_postings,
|
||||||
@@ -159,44 +159,26 @@ impl SegmentWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FieldType::Str(_) => {
|
FieldType::Str(_) => {
|
||||||
let mut token_streams: Vec<Box<dyn TokenStream>> = vec![];
|
let num_tokens = if let Some(ref mut tokenizer) =
|
||||||
let mut offsets = vec![];
|
self.tokenizers[field.0 as usize]
|
||||||
let mut total_offset = 0;
|
{
|
||||||
|
let texts: Vec<&str> = field_values
|
||||||
for field_value in field_values {
|
.iter()
|
||||||
match field_value.value() {
|
.flat_map(|field_value| match *field_value.value() {
|
||||||
Value::PreTokStr(tok_str) => {
|
Value::Str(ref text) => Some(text.as_str()),
|
||||||
offsets.push(total_offset);
|
_ => None,
|
||||||
if let Some(last_token) = tok_str.tokens.last() {
|
})
|
||||||
total_offset += last_token.offset_to;
|
.collect();
|
||||||
}
|
if texts.is_empty() {
|
||||||
|
0
|
||||||
token_streams
|
} else {
|
||||||
.push(Box::new(PreTokenizedStream::from(tok_str.clone())));
|
let mut token_stream = tokenizer.token_stream_texts(&texts[..]);
|
||||||
}
|
self.multifield_postings
|
||||||
Value::Str(ref text) => {
|
.index_text(doc_id, field, &mut token_stream)
|
||||||
if let Some(ref mut tokenizer) =
|
|
||||||
self.tokenizers[field.field_id() as usize]
|
|
||||||
{
|
|
||||||
offsets.push(total_offset);
|
|
||||||
total_offset += text.len();
|
|
||||||
|
|
||||||
token_streams.push(tokenizer.token_stream(text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let num_tokens = if token_streams.is_empty() {
|
|
||||||
0
|
|
||||||
} else {
|
} else {
|
||||||
let mut token_stream: Box<dyn TokenStream> =
|
0
|
||||||
Box::new(TokenStreamChain::new(offsets, token_streams));
|
|
||||||
self.multifield_postings
|
|
||||||
.index_text(doc_id, field, &mut token_stream)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.fieldnorms_writer.record(doc_id, field, num_tokens);
|
self.fieldnorms_writer.record(doc_id, field, num_tokens);
|
||||||
}
|
}
|
||||||
FieldType::U64(ref int_option) => {
|
FieldType::U64(ref int_option) => {
|
||||||
@@ -314,4 +296,5 @@ mod tests {
|
|||||||
assert_eq!(initial_table_size(10_000_000).unwrap(), 17);
|
assert_eq!(initial_table_size(10_000_000).unwrap(), 17);
|
||||||
assert_eq!(initial_table_size(1_000_000_000).unwrap(), 19);
|
assert_eq!(initial_table_size(1_000_000_000).unwrap(), 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/lib.rs
32
src/lib.rs
@@ -3,6 +3,7 @@
|
|||||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::module_inception))]
|
#![cfg_attr(feature = "cargo-clippy", allow(clippy::module_inception))]
|
||||||
#![doc(test(attr(allow(unused_variables), deny(warnings))))]
|
#![doc(test(attr(allow(unused_variables), deny(warnings))))]
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
#![recursion_limit = "80"]
|
||||||
|
|
||||||
//! # `tantivy`
|
//! # `tantivy`
|
||||||
//!
|
//!
|
||||||
@@ -10,17 +11,26 @@
|
|||||||
//! Think `Lucene`, but in Rust.
|
//! Think `Lucene`, but in Rust.
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
|
|
||||||
|
//! # extern crate tempdir;
|
||||||
|
//! #
|
||||||
|
//! #[macro_use]
|
||||||
|
//! extern crate tantivy;
|
||||||
|
//!
|
||||||
|
//! // ...
|
||||||
|
//!
|
||||||
//! # use std::path::Path;
|
//! # use std::path::Path;
|
||||||
//! # use tempfile::TempDir;
|
//! # use tempdir::TempDir;
|
||||||
|
//! # use tantivy::Index;
|
||||||
|
//! # use tantivy::schema::*;
|
||||||
|
//! # use tantivy::{Score, DocAddress};
|
||||||
//! # use tantivy::collector::TopDocs;
|
//! # use tantivy::collector::TopDocs;
|
||||||
//! # use tantivy::query::QueryParser;
|
//! # use tantivy::query::QueryParser;
|
||||||
//! # use tantivy::schema::*;
|
|
||||||
//! # use tantivy::{doc, DocAddress, Index, Score};
|
|
||||||
//! #
|
//! #
|
||||||
//! # fn main() {
|
//! # fn main() {
|
||||||
//! # // Let's create a temporary directory for the
|
//! # // Let's create a temporary directory for the
|
||||||
//! # // sake of this example
|
//! # // sake of this example
|
||||||
//! # if let Ok(dir) = TempDir::new() {
|
//! # if let Ok(dir) = TempDir::new("tantivy_example_dir") {
|
||||||
//! # run_example(dir.path()).unwrap();
|
//! # run_example(dir.path()).unwrap();
|
||||||
//! # dir.close().unwrap();
|
//! # dir.close().unwrap();
|
||||||
//! # }
|
//! # }
|
||||||
@@ -212,13 +222,15 @@ pub type Score = f32;
|
|||||||
pub type SegmentLocalId = u32;
|
pub type SegmentLocalId = u32;
|
||||||
|
|
||||||
impl DocAddress {
|
impl DocAddress {
|
||||||
/// Return the segment ordinal id that identifies the segment
|
/// Return the segment ordinal.
|
||||||
/// hosting the document in the `Searcher` it is called from.
|
/// The segment ordinal is an id identifying the segment
|
||||||
|
/// hosting the document. It is only meaningful, in the context
|
||||||
|
/// of a searcher.
|
||||||
pub fn segment_ord(self) -> SegmentLocalId {
|
pub fn segment_ord(self) -> SegmentLocalId {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the segment-local `DocId`
|
/// Return the segment local `DocId`
|
||||||
pub fn doc(self) -> DocId {
|
pub fn doc(self) -> DocId {
|
||||||
self.1
|
self.1
|
||||||
}
|
}
|
||||||
@@ -227,11 +239,11 @@ impl DocAddress {
|
|||||||
/// `DocAddress` contains all the necessary information
|
/// `DocAddress` contains all the necessary information
|
||||||
/// to identify a document given a `Searcher` object.
|
/// to identify a document given a `Searcher` object.
|
||||||
///
|
///
|
||||||
/// It consists of an id identifying its segment, and
|
/// It consists in an id identifying its segment, and
|
||||||
/// a segment-local `DocId`.
|
/// its segment-local `DocId`.
|
||||||
///
|
///
|
||||||
/// The id used for the segment is actually an ordinal
|
/// The id used for the segment is actually an ordinal
|
||||||
/// in the list of `Segment`s held by a `Searcher`.
|
/// in the list of segment hold by a `Searcher`.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct DocAddress(pub SegmentLocalId, pub DocId);
|
pub struct DocAddress(pub SegmentLocalId, pub DocId);
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,11 @@
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
///
|
||||||
/// use tantivy::schema::{Schema, TEXT, FAST};
|
/// use tantivy::schema::{Schema, TEXT, FAST};
|
||||||
/// use tantivy::doc;
|
|
||||||
///
|
///
|
||||||
/// //...
|
/// //...
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -274,15 +274,13 @@ pub mod tests {
|
|||||||
mod bench {
|
mod bench {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::Rng;
|
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
|
use rand::{Rng, XorShiftRng};
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
fn generate_array_with_seed(n: usize, ratio: f64, seed_val: u8) -> Vec<u32> {
|
fn generate_array_with_seed(n: usize, ratio: f64, seed_val: u8) -> Vec<u32> {
|
||||||
let mut seed: [u8; 32] = [0; 32];
|
let seed: &[u8; 16] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, seed_val];
|
||||||
seed[31] = seed_val;
|
let mut rng: XorShiftRng = XorShiftRng::from_seed(*seed);
|
||||||
let mut rng = StdRng::from_seed(seed);
|
|
||||||
(0u32..).filter(|_| rng.gen_bool(ratio)).take(n).collect()
|
(0u32..).filter(|_| rng.gen_bool(ratio)).take(n).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -356,9 +356,9 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_skip_next() {
|
fn test_skip_next() {
|
||||||
let term_0 = Term::from_field_u64(Field::from_field_id(0), 0);
|
let term_0 = Term::from_field_u64(Field(0), 0);
|
||||||
let term_1 = Term::from_field_u64(Field::from_field_id(0), 1);
|
let term_1 = Term::from_field_u64(Field(0), 1);
|
||||||
let term_2 = Term::from_field_u64(Field::from_field_id(0), 2);
|
let term_2 = Term::from_field_u64(Field(0), 2);
|
||||||
|
|
||||||
let num_docs = 300u32;
|
let num_docs = 300u32;
|
||||||
|
|
||||||
@@ -511,19 +511,19 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub static TERM_A: Lazy<Term> = Lazy::new(|| {
|
pub static TERM_A: Lazy<Term> = Lazy::new(|| {
|
||||||
let field = Field::from_field_id(0);
|
let field = Field(0);
|
||||||
Term::from_field_text(field, "a")
|
Term::from_field_text(field, "a")
|
||||||
});
|
});
|
||||||
pub static TERM_B: Lazy<Term> = Lazy::new(|| {
|
pub static TERM_B: Lazy<Term> = Lazy::new(|| {
|
||||||
let field = Field::from_field_id(0);
|
let field = Field(0);
|
||||||
Term::from_field_text(field, "b")
|
Term::from_field_text(field, "b")
|
||||||
});
|
});
|
||||||
pub static TERM_C: Lazy<Term> = Lazy::new(|| {
|
pub static TERM_C: Lazy<Term> = Lazy::new(|| {
|
||||||
let field = Field::from_field_id(0);
|
let field = Field(0);
|
||||||
Term::from_field_text(field, "c")
|
Term::from_field_text(field, "c")
|
||||||
});
|
});
|
||||||
pub static TERM_D: Lazy<Term> = Lazy::new(|| {
|
pub static TERM_D: Lazy<Term> = Lazy::new(|| {
|
||||||
let field = Field::from_field_id(0);
|
let field = Field(0);
|
||||||
Term::from_field_text(field, "d")
|
Term::from_field_text(field, "d")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -622,23 +622,23 @@ pub mod tests {
|
|||||||
assert!(!postings_unopt.advance());
|
assert!(!postings_unopt.advance());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "unstable"))]
|
#[cfg(all(test, feature = "unstable"))]
|
||||||
mod bench {
|
mod bench {
|
||||||
|
|
||||||
use super::tests::*;
|
use super::tests::*;
|
||||||
use crate::docset::SkipResult;
|
use docset::SkipResult;
|
||||||
use crate::query::Intersection;
|
use query::Intersection;
|
||||||
use crate::schema::IndexRecordOption;
|
use schema::IndexRecordOption;
|
||||||
use crate::tests;
|
|
||||||
use crate::DocSet;
|
|
||||||
use test::{self, Bencher};
|
use test::{self, Bencher};
|
||||||
|
use tests;
|
||||||
|
use DocSet;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_segment_postings(b: &mut Bencher) {
|
fn bench_segment_postings(b: &mut Bencher) {
|
||||||
let reader = INDEX.reader().unwrap();
|
let searcher = INDEX.searcher();
|
||||||
let searcher = reader.searcher();
|
|
||||||
let segment_reader = searcher.segment_reader(0);
|
let segment_reader = searcher.segment_reader(0);
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
@@ -652,8 +652,7 @@ mod bench {
|
|||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_segment_intersection(b: &mut Bencher) {
|
fn bench_segment_intersection(b: &mut Bencher) {
|
||||||
let reader = INDEX.reader().unwrap();
|
let searcher = INDEX.searcher();
|
||||||
let searcher = reader.searcher();
|
|
||||||
let segment_reader = searcher.segment_reader(0);
|
let segment_reader = searcher.segment_reader(0);
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let segment_postings_a = segment_reader
|
let segment_postings_a = segment_reader
|
||||||
@@ -683,8 +682,7 @@ mod bench {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bench_skip_next(p: f64, b: &mut Bencher) {
|
fn bench_skip_next(p: f64, b: &mut Bencher) {
|
||||||
let reader = INDEX.reader().unwrap();
|
let searcher = INDEX.searcher();
|
||||||
let searcher = reader.searcher();
|
|
||||||
let segment_reader = searcher.segment_reader(0);
|
let segment_reader = searcher.segment_reader(0);
|
||||||
let docs = tests::sample(segment_reader.num_docs(), p);
|
let docs = tests::sample(segment_reader.num_docs(), p);
|
||||||
|
|
||||||
@@ -739,8 +737,7 @@ mod bench {
|
|||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_iterate_segment_postings(b: &mut Bencher) {
|
fn bench_iterate_segment_postings(b: &mut Bencher) {
|
||||||
let reader = INDEX.reader().unwrap();
|
let searcher = INDEX.searcher();
|
||||||
let searcher = reader.searcher();
|
|
||||||
let segment_reader = searcher.segment_reader(0);
|
let segment_reader = searcher.segment_reader(0);
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let n: u32 = test::black_box(17);
|
let n: u32 = test::black_box(17);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use crate::tokenizer::TokenStream;
|
|||||||
use crate::tokenizer::{Token, MAX_TOKEN_LEN};
|
use crate::tokenizer::{Token, MAX_TOKEN_LEN};
|
||||||
use crate::DocId;
|
use crate::DocId;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use fnv::FnvHashMap;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -61,12 +60,12 @@ fn make_field_partition(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(key, _, _)| Term::wrap(key).field())
|
.map(|(key, _, _)| Term::wrap(key).field())
|
||||||
.enumerate();
|
.enumerate();
|
||||||
let mut prev_field_opt = None;
|
let mut prev_field = Field(u32::max_value());
|
||||||
let mut fields = vec![];
|
let mut fields = vec![];
|
||||||
let mut offsets = vec![];
|
let mut offsets = vec![];
|
||||||
for (offset, field) in term_offsets_it {
|
for (offset, field) in term_offsets_it {
|
||||||
if Some(field) != prev_field_opt {
|
if field != prev_field {
|
||||||
prev_field_opt = Some(field);
|
prev_field = field;
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
offsets.push(offset);
|
offsets.push(offset);
|
||||||
}
|
}
|
||||||
@@ -86,7 +85,8 @@ impl MultiFieldPostingsWriter {
|
|||||||
let term_index = TermHashMap::new(table_bits);
|
let term_index = TermHashMap::new(table_bits);
|
||||||
let per_field_postings_writers: Vec<_> = schema
|
let per_field_postings_writers: Vec<_> = schema
|
||||||
.fields()
|
.fields()
|
||||||
.map(|(_, field_entry)| posting_from_field_entry(field_entry))
|
.iter()
|
||||||
|
.map(|field_entry| posting_from_field_entry(field_entry))
|
||||||
.collect();
|
.collect();
|
||||||
MultiFieldPostingsWriter {
|
MultiFieldPostingsWriter {
|
||||||
heap: MemoryArena::new(),
|
heap: MemoryArena::new(),
|
||||||
@@ -106,8 +106,7 @@ impl MultiFieldPostingsWriter {
|
|||||||
field: Field,
|
field: Field,
|
||||||
token_stream: &mut dyn TokenStream,
|
token_stream: &mut dyn TokenStream,
|
||||||
) -> u32 {
|
) -> u32 {
|
||||||
let postings_writer =
|
let postings_writer = self.per_field_postings_writers[field.0 as usize].deref_mut();
|
||||||
self.per_field_postings_writers[field.field_id() as usize].deref_mut();
|
|
||||||
postings_writer.index_text(
|
postings_writer.index_text(
|
||||||
&mut self.term_index,
|
&mut self.term_index,
|
||||||
doc,
|
doc,
|
||||||
@@ -118,8 +117,7 @@ impl MultiFieldPostingsWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe(&mut self, doc: DocId, term: &Term) -> UnorderedTermId {
|
pub fn subscribe(&mut self, doc: DocId, term: &Term) -> UnorderedTermId {
|
||||||
let postings_writer =
|
let postings_writer = self.per_field_postings_writers[term.field().0 as usize].deref_mut();
|
||||||
self.per_field_postings_writers[term.field().field_id() as usize].deref_mut();
|
|
||||||
postings_writer.subscribe(&mut self.term_index, doc, 0u32, term, &mut self.heap)
|
postings_writer.subscribe(&mut self.term_index, doc, 0u32, term, &mut self.heap)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,12 +127,12 @@ impl MultiFieldPostingsWriter {
|
|||||||
pub fn serialize(
|
pub fn serialize(
|
||||||
&self,
|
&self,
|
||||||
serializer: &mut InvertedIndexSerializer,
|
serializer: &mut InvertedIndexSerializer,
|
||||||
) -> Result<HashMap<Field, FnvHashMap<UnorderedTermId, TermOrdinal>>> {
|
) -> Result<HashMap<Field, HashMap<UnorderedTermId, TermOrdinal>>> {
|
||||||
let mut term_offsets: Vec<(&[u8], Addr, UnorderedTermId)> =
|
let mut term_offsets: Vec<(&[u8], Addr, UnorderedTermId)> =
|
||||||
self.term_index.iter().collect();
|
self.term_index.iter().collect();
|
||||||
term_offsets.sort_unstable_by_key(|&(k, _, _)| k);
|
term_offsets.sort_unstable_by_key(|&(k, _, _)| k);
|
||||||
|
|
||||||
let mut unordered_term_mappings: HashMap<Field, FnvHashMap<UnorderedTermId, TermOrdinal>> =
|
let mut unordered_term_mappings: HashMap<Field, HashMap<UnorderedTermId, TermOrdinal>> =
|
||||||
HashMap::new();
|
HashMap::new();
|
||||||
|
|
||||||
let field_offsets = make_field_partition(&term_offsets);
|
let field_offsets = make_field_partition(&term_offsets);
|
||||||
@@ -149,7 +147,7 @@ impl MultiFieldPostingsWriter {
|
|||||||
let unordered_term_ids = term_offsets[start..stop]
|
let unordered_term_ids = term_offsets[start..stop]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(_, _, bucket)| bucket);
|
.map(|&(_, _, bucket)| bucket);
|
||||||
let mapping: FnvHashMap<UnorderedTermId, TermOrdinal> = unordered_term_ids
|
let mapping: HashMap<UnorderedTermId, TermOrdinal> = unordered_term_ids
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(term_ord, unord_term_id)| {
|
.map(|(term_ord, unord_term_id)| {
|
||||||
(unord_term_id as UnorderedTermId, term_ord as TermOrdinal)
|
(unord_term_id as UnorderedTermId, term_ord as TermOrdinal)
|
||||||
@@ -161,7 +159,7 @@ impl MultiFieldPostingsWriter {
|
|||||||
FieldType::Bytes => {}
|
FieldType::Bytes => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let postings_writer = &self.per_field_postings_writers[field.field_id() as usize];
|
let postings_writer = &self.per_field_postings_writers[field.0 as usize];
|
||||||
let mut field_serializer =
|
let mut field_serializer =
|
||||||
serializer.new_field(field, postings_writer.total_num_tokens())?;
|
serializer.new_field(field, postings_writer.total_num_tokens())?;
|
||||||
postings_writer.serialize(
|
postings_writer.serialize(
|
||||||
|
|||||||
@@ -141,7 +141,10 @@ impl<'a> FieldSerializer<'a> {
|
|||||||
FieldType::Str(ref text_options) => {
|
FieldType::Str(ref text_options) => {
|
||||||
if let Some(text_indexing_options) = text_options.get_indexing_options() {
|
if let Some(text_indexing_options) = text_options.get_indexing_options() {
|
||||||
let index_option = text_indexing_options.index_option();
|
let index_option = text_indexing_options.index_option();
|
||||||
(index_option.has_freq(), index_option.has_positions())
|
(
|
||||||
|
index_option.is_termfreq_enabled(),
|
||||||
|
index_option.is_position_enabled(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(false, false)
|
(false, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,7 +310,6 @@ mod bench {
|
|||||||
use super::super::MemoryArena;
|
use super::super::MemoryArena;
|
||||||
use super::ExpUnrolledLinkedList;
|
use super::ExpUnrolledLinkedList;
|
||||||
use byteorder::{NativeEndian, WriteBytesExt};
|
use byteorder::{NativeEndian, WriteBytesExt};
|
||||||
use std::iter;
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
const NUM_STACK: usize = 10_000;
|
const NUM_STACK: usize = 10_000;
|
||||||
@@ -336,10 +335,11 @@ mod bench {
|
|||||||
fn bench_push_stack(bench: &mut Bencher) {
|
fn bench_push_stack(bench: &mut Bencher) {
|
||||||
bench.iter(|| {
|
bench.iter(|| {
|
||||||
let mut heap = MemoryArena::new();
|
let mut heap = MemoryArena::new();
|
||||||
let mut stacks: Vec<ExpUnrolledLinkedList> =
|
let mut stacks = Vec::with_capacity(100);
|
||||||
iter::repeat_with(ExpUnrolledLinkedList::new)
|
for _ in 0..NUM_STACK {
|
||||||
.take(NUM_STACK)
|
let mut stack = ExpUnrolledLinkedList::new();
|
||||||
.collect();
|
stacks.push(stack);
|
||||||
|
}
|
||||||
for s in 0..NUM_STACK {
|
for s in 0..NUM_STACK {
|
||||||
for i in 0u32..STACK_SIZE {
|
for i in 0u32..STACK_SIZE {
|
||||||
let t = s * 392017 % NUM_STACK;
|
let t = s * 392017 % NUM_STACK;
|
||||||
|
|||||||
@@ -130,4 +130,5 @@ mod tests {
|
|||||||
assert!(!scorer.advance());
|
assert!(!scorer.advance());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ use crate::termdict::{TermDictionary, TermStreamer};
|
|||||||
use crate::DocId;
|
use crate::DocId;
|
||||||
use crate::TantivyError;
|
use crate::TantivyError;
|
||||||
use crate::{Result, SkipResult};
|
use crate::{Result, SkipResult};
|
||||||
use std::sync::Arc;
|
|
||||||
use tantivy_fst::Automaton;
|
use tantivy_fst::Automaton;
|
||||||
|
|
||||||
/// A weight struct for Fuzzy Term and Regex Queries
|
/// A weight struct for Fuzzy Term and Regex Queries
|
||||||
pub struct AutomatonWeight<A> {
|
pub struct AutomatonWeight<A>
|
||||||
|
where
|
||||||
|
A: Automaton + Send + Sync + 'static,
|
||||||
|
{
|
||||||
field: Field,
|
field: Field,
|
||||||
automaton: Arc<A>,
|
automaton: A,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A> AutomatonWeight<A>
|
impl<A> AutomatonWeight<A>
|
||||||
@@ -22,16 +24,12 @@ where
|
|||||||
A: Automaton + Send + Sync + 'static,
|
A: Automaton + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
/// Create a new AutomationWeight
|
/// Create a new AutomationWeight
|
||||||
pub fn new<IntoArcA: Into<Arc<A>>>(field: Field, automaton: IntoArcA) -> AutomatonWeight<A> {
|
pub fn new(field: Field, automaton: A) -> AutomatonWeight<A> {
|
||||||
AutomatonWeight {
|
AutomatonWeight { field, automaton }
|
||||||
field,
|
|
||||||
automaton: automaton.into(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn automaton_stream<'a>(&'a self, term_dict: &'a TermDictionary) -> TermStreamer<'a, &'a A> {
|
fn automaton_stream<'a>(&'a self, term_dict: &'a TermDictionary) -> TermStreamer<'a, &'a A> {
|
||||||
let automaton: &A = &*self.automaton;
|
let term_stream_builder = term_dict.search(&self.automaton);
|
||||||
let term_stream_builder = term_dict.search(automaton);
|
|
||||||
term_stream_builder.into_stream()
|
term_stream_builder.into_stream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ mod tests {
|
|||||||
assert!(!docset.advance());
|
assert!(!docset.advance());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "unstable"))]
|
#[cfg(all(test, feature = "unstable"))]
|
||||||
@@ -223,12 +224,13 @@ mod bench {
|
|||||||
|
|
||||||
use super::BitSet;
|
use super::BitSet;
|
||||||
use super::BitSetDocSet;
|
use super::BitSetDocSet;
|
||||||
use crate::test;
|
use test;
|
||||||
use crate::tests;
|
use tests;
|
||||||
use crate::DocSet;
|
use DocSet;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_bitset_1pct_insert(b: &mut test::Bencher) {
|
fn bench_bitset_1pct_insert(b: &mut test::Bencher) {
|
||||||
|
use tests;
|
||||||
let els = tests::generate_nonunique_unsorted(1_000_000u32, 10_000);
|
let els = tests::generate_nonunique_unsorted(1_000_000u32, 10_000);
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let mut bitset = BitSet::with_max_value(1_000_000);
|
let mut bitset = BitSet::with_max_value(1_000_000);
|
||||||
@@ -240,6 +242,7 @@ mod bench {
|
|||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_bitset_1pct_clone(b: &mut test::Bencher) {
|
fn bench_bitset_1pct_clone(b: &mut test::Bencher) {
|
||||||
|
use tests;
|
||||||
let els = tests::generate_nonunique_unsorted(1_000_000u32, 10_000);
|
let els = tests::generate_nonunique_unsorted(1_000_000u32, 10_000);
|
||||||
let mut bitset = BitSet::with_max_value(1_000_000);
|
let mut bitset = BitSet::with_max_value(1_000_000);
|
||||||
for el in els {
|
for el in els {
|
||||||
|
|||||||
@@ -137,4 +137,5 @@ mod tests {
|
|||||||
fn test_idf() {
|
fn test_idf() {
|
||||||
assert_nearly_equals(idf(1, 2), 0.6931472);
|
assert_nearly_equals(idf(1, 2), 0.6931472);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ use crate::Result;
|
|||||||
use crate::Searcher;
|
use crate::Searcher;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
/// The boolean query returns a set of documents
|
/// The boolean query combines a set of queries
|
||||||
/// that matches the Boolean combination of constituent subqueries.
|
|
||||||
///
|
///
|
||||||
/// The documents matched by the boolean query are
|
/// The documents matched by the boolean query are
|
||||||
/// those which
|
/// those which
|
||||||
@@ -20,113 +19,6 @@ use std::collections::BTreeSet;
|
|||||||
/// `MustNot` occurence.
|
/// `MustNot` occurence.
|
||||||
/// * match at least one of the subqueries that is not
|
/// * match at least one of the subqueries that is not
|
||||||
/// a `MustNot` occurence.
|
/// a `MustNot` occurence.
|
||||||
///
|
|
||||||
///
|
|
||||||
/// You can combine other query types and their `Occur`ances into one `BooleanQuery`
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
///use tantivy::collector::Count;
|
|
||||||
///use tantivy::doc;
|
|
||||||
///use tantivy::query::{BooleanQuery, Occur, PhraseQuery, Query, TermQuery};
|
|
||||||
///use tantivy::schema::{IndexRecordOption, Schema, TEXT};
|
|
||||||
///use tantivy::Term;
|
|
||||||
///use tantivy::{Index, Result};
|
|
||||||
///
|
|
||||||
///fn main() -> Result<()> {
|
|
||||||
/// let mut schema_builder = Schema::builder();
|
|
||||||
/// let title = schema_builder.add_text_field("title", TEXT);
|
|
||||||
/// let body = schema_builder.add_text_field("body", TEXT);
|
|
||||||
/// let schema = schema_builder.build();
|
|
||||||
/// let index = Index::create_in_ram(schema);
|
|
||||||
/// {
|
|
||||||
/// let mut index_writer = index.writer(3_000_000)?;
|
|
||||||
/// index_writer.add_document(doc!(
|
|
||||||
/// title => "The Name of the Wind",
|
|
||||||
/// ));
|
|
||||||
/// index_writer.add_document(doc!(
|
|
||||||
/// title => "The Diary of Muadib",
|
|
||||||
/// ));
|
|
||||||
/// index_writer.add_document(doc!(
|
|
||||||
/// title => "A Dairy Cow",
|
|
||||||
/// body => "hidden",
|
|
||||||
/// ));
|
|
||||||
/// index_writer.add_document(doc!(
|
|
||||||
/// title => "A Dairy Cow",
|
|
||||||
/// body => "found",
|
|
||||||
/// ));
|
|
||||||
/// index_writer.add_document(doc!(
|
|
||||||
/// title => "The Diary of a Young Girl",
|
|
||||||
/// ));
|
|
||||||
/// index_writer.commit().unwrap();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let reader = index.reader()?;
|
|
||||||
/// let searcher = reader.searcher();
|
|
||||||
///
|
|
||||||
/// // Make TermQuery's for "girl" and "diary" in the title
|
|
||||||
/// let girl_term_query: Box<dyn Query> = Box::new(TermQuery::new(
|
|
||||||
/// Term::from_field_text(title, "girl"),
|
|
||||||
/// IndexRecordOption::Basic,
|
|
||||||
/// ));
|
|
||||||
/// let diary_term_query: Box<dyn Query> = Box::new(TermQuery::new(
|
|
||||||
/// Term::from_field_text(title, "diary"),
|
|
||||||
/// IndexRecordOption::Basic,
|
|
||||||
/// ));
|
|
||||||
/// // A TermQuery with "found" in the body
|
|
||||||
/// let body_term_query: Box<dyn Query> = Box::new(TermQuery::new(
|
|
||||||
/// Term::from_field_text(body, "found"),
|
|
||||||
/// IndexRecordOption::Basic,
|
|
||||||
/// ));
|
|
||||||
/// // TermQuery "diary" must and "girl" must not be present
|
|
||||||
/// let queries_with_occurs1 = vec![
|
|
||||||
/// (Occur::Must, diary_term_query.box_clone()),
|
|
||||||
/// (Occur::MustNot, girl_term_query),
|
|
||||||
/// ];
|
|
||||||
/// // Make a BooleanQuery equivalent to
|
|
||||||
/// // title:+diary title:-girl
|
|
||||||
/// let diary_must_and_girl_mustnot = BooleanQuery::from(queries_with_occurs1);
|
|
||||||
/// let count1 = searcher.search(&diary_must_and_girl_mustnot, &Count)?;
|
|
||||||
/// assert_eq!(count1, 1);
|
|
||||||
///
|
|
||||||
/// // TermQuery for "cow" in the title
|
|
||||||
/// let cow_term_query: Box<dyn Query> = Box::new(TermQuery::new(
|
|
||||||
/// Term::from_field_text(title, "cow"),
|
|
||||||
/// IndexRecordOption::Basic,
|
|
||||||
/// ));
|
|
||||||
/// // "title:diary OR title:cow"
|
|
||||||
/// let title_diary_or_cow = BooleanQuery::from(vec![
|
|
||||||
/// (Occur::Should, diary_term_query.box_clone()),
|
|
||||||
/// (Occur::Should, cow_term_query),
|
|
||||||
/// ]);
|
|
||||||
/// let count2 = searcher.search(&title_diary_or_cow, &Count)?;
|
|
||||||
/// assert_eq!(count2, 4);
|
|
||||||
///
|
|
||||||
/// // Make a `PhraseQuery` from a vector of `Term`s
|
|
||||||
/// let phrase_query: Box<dyn Query> = Box::new(PhraseQuery::new(vec![
|
|
||||||
/// Term::from_field_text(title, "dairy"),
|
|
||||||
/// Term::from_field_text(title, "cow"),
|
|
||||||
/// ]));
|
|
||||||
/// // You can combine subqueries of different types into 1 BooleanQuery:
|
|
||||||
/// // `TermQuery` and `PhraseQuery`
|
|
||||||
/// // "title:diary OR "dairy cow"
|
|
||||||
/// let term_of_phrase_query = BooleanQuery::from(vec![
|
|
||||||
/// (Occur::Should, diary_term_query.box_clone()),
|
|
||||||
/// (Occur::Should, phrase_query.box_clone()),
|
|
||||||
/// ]);
|
|
||||||
/// let count3 = searcher.search(&term_of_phrase_query, &Count)?;
|
|
||||||
/// assert_eq!(count3, 4);
|
|
||||||
///
|
|
||||||
/// // You can nest one BooleanQuery inside another
|
|
||||||
/// // body:found AND ("title:diary OR "dairy cow")
|
|
||||||
/// let nested_query = BooleanQuery::from(vec![
|
|
||||||
/// (Occur::Must, body_term_query),
|
|
||||||
/// (Occur::Must, Box::new(term_of_phrase_query))
|
|
||||||
/// ]);
|
|
||||||
/// let count4 = searcher.search(&nested_query, &Count)?;
|
|
||||||
/// assert_eq!(count4, 1);
|
|
||||||
/// Ok(())
|
|
||||||
///}
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BooleanQuery {
|
pub struct BooleanQuery {
|
||||||
subqueries: Vec<(Occur, Box<dyn Query>)>,
|
subqueries: Vec<(Occur, Box<dyn Query>)>,
|
||||||
|
|||||||
@@ -247,7 +247,9 @@ mod tests {
|
|||||||
let reader = index.reader().unwrap();
|
let reader = index.reader().unwrap();
|
||||||
let searcher = reader.searcher();
|
let searcher = reader.searcher();
|
||||||
let query_parser = QueryParser::for_index(&index, vec![title, text]);
|
let query_parser = QueryParser::for_index(&index, vec![title, text]);
|
||||||
let query = query_parser.parse_query("Оксана Лифенко").unwrap();
|
let query = query_parser
|
||||||
|
.parse_query("Оксана Лифенко")
|
||||||
|
.unwrap();
|
||||||
let weight = query.weight(&searcher, true).unwrap();
|
let weight = query.weight(&searcher, true).unwrap();
|
||||||
let mut scorer = weight.scorer(searcher.segment_reader(0u32)).unwrap();
|
let mut scorer = weight.scorer(searcher.segment_reader(0u32)).unwrap();
|
||||||
scorer.advance();
|
scorer.advance();
|
||||||
|
|||||||
@@ -175,4 +175,5 @@ mod tests {
|
|||||||
sample_skip,
|
sample_skip,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::error::TantivyError::InvalidArgument;
|
|
||||||
use crate::query::{AutomatonWeight, Query, Weight};
|
use crate::query::{AutomatonWeight, Query, Weight};
|
||||||
use crate::schema::Term;
|
use crate::schema::Term;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
@@ -6,16 +5,11 @@ use crate::Searcher;
|
|||||||
use levenshtein_automata::{LevenshteinAutomatonBuilder, DFA};
|
use levenshtein_automata::{LevenshteinAutomatonBuilder, DFA};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
/// A range of Levenshtein distances that we will build DFAs for our terms
|
|
||||||
/// The computation is exponential, so best keep it to low single digits
|
|
||||||
const VALID_LEVENSHTEIN_DISTANCE_RANGE: Range<u8> = (0..3);
|
|
||||||
|
|
||||||
static LEV_BUILDER: Lazy<HashMap<(u8, bool), LevenshteinAutomatonBuilder>> = Lazy::new(|| {
|
static LEV_BUILDER: Lazy<HashMap<(u8, bool), LevenshteinAutomatonBuilder>> = Lazy::new(|| {
|
||||||
let mut lev_builder_cache = HashMap::new();
|
let mut lev_builder_cache = HashMap::new();
|
||||||
// TODO make population lazy on a `(distance, val)` basis
|
// TODO make population lazy on a `(distance, val)` basis
|
||||||
for distance in VALID_LEVENSHTEIN_DISTANCE_RANGE {
|
for distance in 0..3 {
|
||||||
for &transposition in &[false, true] {
|
for &transposition in &[false, true] {
|
||||||
let lev_automaton_builder = LevenshteinAutomatonBuilder::new(distance, transposition);
|
let lev_automaton_builder = LevenshteinAutomatonBuilder::new(distance, transposition);
|
||||||
lev_builder_cache.insert((distance, transposition), lev_automaton_builder);
|
lev_builder_cache.insert((distance, transposition), lev_automaton_builder);
|
||||||
@@ -28,10 +22,12 @@ static LEV_BUILDER: Lazy<HashMap<(u8, bool), LevenshteinAutomatonBuilder>> = Laz
|
|||||||
/// containing a specific term that is within
|
/// containing a specific term that is within
|
||||||
/// Levenshtein distance
|
/// Levenshtein distance
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
/// use tantivy::schema::{Schema, TEXT};
|
||||||
|
/// use tantivy::{Index, Result, Term};
|
||||||
/// use tantivy::collector::{Count, TopDocs};
|
/// use tantivy::collector::{Count, TopDocs};
|
||||||
/// use tantivy::query::FuzzyTermQuery;
|
/// use tantivy::query::FuzzyTermQuery;
|
||||||
/// use tantivy::schema::{Schema, TEXT};
|
|
||||||
/// use tantivy::{doc, Index, Result, Term};
|
|
||||||
///
|
///
|
||||||
/// # fn main() { example().unwrap(); }
|
/// # fn main() { example().unwrap(); }
|
||||||
/// fn example() -> Result<()> {
|
/// fn example() -> Result<()> {
|
||||||
@@ -104,18 +100,10 @@ impl FuzzyTermQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn specialized_weight(&self) -> Result<AutomatonWeight<DFA>> {
|
fn specialized_weight(&self) -> Result<AutomatonWeight<DFA>> {
|
||||||
// LEV_BUILDER is a HashMap, whose `get` method returns an Option
|
let automaton = LEV_BUILDER.get(&(self.distance, false))
|
||||||
match LEV_BUILDER.get(&(self.distance, false)) {
|
.unwrap() // TODO return an error
|
||||||
// Unwrap the option and build the Ok(AutomatonWeight)
|
.build_dfa(self.term.text());
|
||||||
Some(automaton_builder) => {
|
Ok(AutomatonWeight::new(self.term.field(), automaton))
|
||||||
let automaton = automaton_builder.build_dfa(self.term.text());
|
|
||||||
Ok(AutomatonWeight::new(self.term.field(), automaton))
|
|
||||||
}
|
|
||||||
None => Err(InvalidArgument(format!(
|
|
||||||
"Levenshtein distance of {} is not allowed. Choose a value in the {:?} range",
|
|
||||||
self.distance, VALID_LEVENSHTEIN_DISTANCE_RANGE
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ pub fn intersect_scorers(mut scorers: Vec<Box<dyn Scorer>>) -> Box<dyn Scorer> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `DocSet` that iterate through the intersection of two or more `DocSet`s.
|
/// Creates a `DocSet` that iterator through the intersection of two `DocSet`s.
|
||||||
pub struct Intersection<TDocSet: DocSet, TOtherDocSet: DocSet = Box<dyn Scorer>> {
|
pub struct Intersection<TDocSet: DocSet, TOtherDocSet: DocSet = Box<dyn Scorer>> {
|
||||||
left: TDocSet,
|
left: TDocSet,
|
||||||
right: TDocSet,
|
right: TDocSet,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use Score;
|
|||||||
use SkipResult;
|
use SkipResult;
|
||||||
|
|
||||||
|
|
||||||
/// Creates a `DocSet` that iterate through the intersection of two `DocSet`s.
|
/// Creates a `DocSet` that iterator through the intersection of two `DocSet`s.
|
||||||
pub struct IntersectionTwoTerms<TDocSet> {
|
pub struct IntersectionTwoTerms<TDocSet> {
|
||||||
left: TDocSet,
|
left: TDocSet,
|
||||||
right: TDocSet
|
right: TDocSet
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod exclude;
|
|||||||
mod explanation;
|
mod explanation;
|
||||||
mod fuzzy_query;
|
mod fuzzy_query;
|
||||||
mod intersection;
|
mod intersection;
|
||||||
|
mod occur;
|
||||||
mod phrase_query;
|
mod phrase_query;
|
||||||
mod query;
|
mod query;
|
||||||
mod query_parser;
|
mod query_parser;
|
||||||
@@ -42,6 +43,7 @@ pub use self::exclude::Exclude;
|
|||||||
pub use self::explanation::Explanation;
|
pub use self::explanation::Explanation;
|
||||||
pub use self::fuzzy_query::FuzzyTermQuery;
|
pub use self::fuzzy_query::FuzzyTermQuery;
|
||||||
pub use self::intersection::intersect_scorers;
|
pub use self::intersection::intersect_scorers;
|
||||||
|
pub use self::occur::Occur;
|
||||||
pub use self::phrase_query::PhraseQuery;
|
pub use self::phrase_query::PhraseQuery;
|
||||||
pub use self::query::Query;
|
pub use self::query::Query;
|
||||||
pub use self::query_parser::QueryParser;
|
pub use self::query_parser::QueryParser;
|
||||||
@@ -53,7 +55,6 @@ pub use self::scorer::ConstScorer;
|
|||||||
pub use self::scorer::Scorer;
|
pub use self::scorer::Scorer;
|
||||||
pub use self::term_query::TermQuery;
|
pub use self::term_query::TermQuery;
|
||||||
pub use self::weight::Weight;
|
pub use self::weight::Weight;
|
||||||
pub use tantivy_query_grammar::Occur;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
/// Defines whether a term in a query must be present,
|
/// Defines whether a term in a query must be present,
|
||||||
/// should be present or must be not present.
|
/// should be present or must not be present.
|
||||||
#[derive(Debug, Clone, Hash, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Hash, Copy, Eq, PartialEq)]
|
||||||
pub enum Occur {
|
pub enum Occur {
|
||||||
/// For a given document to be considered for scoring,
|
/// For a given document to be considered for scoring,
|
||||||
@@ -21,38 +18,32 @@ impl Occur {
|
|||||||
/// - `Should` => '?',
|
/// - `Should` => '?',
|
||||||
/// - `Must` => '+'
|
/// - `Must` => '+'
|
||||||
/// - `Not` => '-'
|
/// - `Not` => '-'
|
||||||
fn to_char(self) -> char {
|
pub fn to_char(self) -> char {
|
||||||
match self {
|
match self {
|
||||||
Occur::Should => '?',
|
Occur::Should => '?',
|
||||||
Occur::Must => '+',
|
Occur::Must => '+',
|
||||||
Occur::MustNot => '-',
|
Occur::MustNot => '-',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Compose two occur values.
|
/// Compose two occur values.
|
||||||
pub fn compose(left: Occur, right: Occur) -> Occur {
|
pub fn compose_occur(left: Occur, right: Occur) -> Occur {
|
||||||
match left {
|
match left {
|
||||||
Occur::Should => right,
|
Occur::Should => right,
|
||||||
Occur::Must => {
|
Occur::Must => {
|
||||||
if right == Occur::MustNot {
|
if right == Occur::MustNot {
|
||||||
Occur::MustNot
|
Occur::MustNot
|
||||||
} else {
|
} else {
|
||||||
Occur::Must
|
Occur::Must
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Occur::MustNot => {
|
}
|
||||||
if right == Occur::MustNot {
|
Occur::MustNot => {
|
||||||
Occur::Must
|
if right == Occur::MustNot {
|
||||||
} else {
|
Occur::Must
|
||||||
Occur::MustNot
|
} else {
|
||||||
}
|
Occur::MustNot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Occur {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_char(self.to_char())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,7 @@ impl PhraseQuery {
|
|||||||
PhraseQuery::new_with_offset(terms_with_offset)
|
PhraseQuery::new_with_offset(terms_with_offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new `PhraseQuery` given a list of terms and their offsets.
|
/// Creates a new `PhraseQuery` given a list of terms and there offsets.
|
||||||
///
|
///
|
||||||
/// Can be used to provide custom offset for each term.
|
/// Can be used to provide custom offset for each term.
|
||||||
pub fn new_with_offset(mut terms: Vec<(usize, Term)>) -> PhraseQuery {
|
pub fn new_with_offset(mut terms: Vec<(usize, Term)>) -> PhraseQuery {
|
||||||
@@ -73,7 +73,7 @@ impl PhraseQuery {
|
|||||||
.collect::<Vec<Term>>()
|
.collect::<Vec<Term>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `PhraseWeight` for the given phrase query given a specific `searcher`.
|
/// Returns the `PhraseWeight` for the given phrase query given a specific `searcher`.
|
||||||
///
|
///
|
||||||
/// This function is the same as `.weight(...)` except it returns
|
/// This function is the same as `.weight(...)` except it returns
|
||||||
/// a specialized type `PhraseWeight` instead of a Boxed trait.
|
/// a specialized type `PhraseWeight` instead of a Boxed trait.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub enum LogicalLiteral {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum LogicalAST {
|
pub enum LogicalAST {
|
||||||
Clause(Vec<(Occur, LogicalAST)>),
|
Clause(Vec<(Occur, LogicalAST)>),
|
||||||
Leaf(Box<LogicalLiteral>),
|
Leaf(Box<LogicalLiteral>),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
mod query_grammar;
|
||||||
mod query_parser;
|
mod query_parser;
|
||||||
|
mod user_input_ast;
|
||||||
|
|
||||||
pub mod logical_ast;
|
pub mod logical_ast;
|
||||||
pub use self::query_parser::QueryParser;
|
pub use self::query_parser::QueryParser;
|
||||||
|
|||||||
284
src/query/query_parser/query_grammar.rs
Normal file
284
src/query/query_parser/query_grammar.rs
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
use super::query_grammar;
|
||||||
|
use super::user_input_ast::*;
|
||||||
|
use crate::query::occur::Occur;
|
||||||
|
use crate::query::query_parser::user_input_ast::UserInputBound;
|
||||||
|
use combine::char::*;
|
||||||
|
use combine::error::StreamError;
|
||||||
|
use combine::stream::StreamErrorFor;
|
||||||
|
use combine::*;
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn field[I]()(I) -> String
|
||||||
|
where [I: Stream<Item = char>] {
|
||||||
|
(
|
||||||
|
letter(),
|
||||||
|
many(satisfy(|c: char| c.is_alphanumeric() || c == '_')),
|
||||||
|
).map(|(s1, s2): (char, String)| format!("{}{}", s1, s2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn word[I]()(I) -> String
|
||||||
|
where [I: Stream<Item = char>] {
|
||||||
|
many1(satisfy(|c: char| c.is_alphanumeric() || c=='.'))
|
||||||
|
.and_then(|s: String| {
|
||||||
|
match s.as_str() {
|
||||||
|
"OR" => Err(StreamErrorFor::<I>::unexpected_static_message("OR")),
|
||||||
|
"AND" => Err(StreamErrorFor::<I>::unexpected_static_message("AND")),
|
||||||
|
"NOT" => Err(StreamErrorFor::<I>::unexpected_static_message("NOT")),
|
||||||
|
_ => Ok(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn literal[I]()(I) -> UserInputLeaf
|
||||||
|
where [I: Stream<Item = char>]
|
||||||
|
{
|
||||||
|
let term_val = || {
|
||||||
|
let phrase = (char('"'), many1(satisfy(|c| c != '"')), char('"')).map(|(_, s, _)| s);
|
||||||
|
phrase.or(word())
|
||||||
|
};
|
||||||
|
let term_val_with_field = negative_number().or(term_val());
|
||||||
|
let term_query =
|
||||||
|
(field(), char(':'), term_val_with_field).map(|(field_name, _, phrase)| UserInputLiteral {
|
||||||
|
field_name: Some(field_name),
|
||||||
|
phrase,
|
||||||
|
});
|
||||||
|
let term_default_field = term_val().map(|phrase| UserInputLiteral {
|
||||||
|
field_name: None,
|
||||||
|
phrase,
|
||||||
|
});
|
||||||
|
attempt(term_query)
|
||||||
|
.or(term_default_field)
|
||||||
|
.map(UserInputLeaf::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn negative_number[I]()(I) -> String
|
||||||
|
where [I: Stream<Item = char>]
|
||||||
|
{
|
||||||
|
(char('-'), many1(satisfy(char::is_numeric)))
|
||||||
|
.map(|(s1, s2): (char, String)| format!("{}{}", s1, s2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn spaces1[I]()(I) -> ()
|
||||||
|
where [I: Stream<Item = char>] {
|
||||||
|
skip_many1(space())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn range[I]()(I) -> UserInputLeaf
|
||||||
|
where [I: Stream<Item = char>] {
|
||||||
|
let term_val = || {
|
||||||
|
word().or(negative_number()).or(char('*').map(|_| "*".to_string()))
|
||||||
|
};
|
||||||
|
let lower_bound = {
|
||||||
|
let excl = (char('{'), term_val()).map(|(_, w)| UserInputBound::Exclusive(w));
|
||||||
|
let incl = (char('['), term_val()).map(|(_, w)| UserInputBound::Inclusive(w));
|
||||||
|
attempt(excl).or(incl)
|
||||||
|
};
|
||||||
|
let upper_bound = {
|
||||||
|
let excl = (term_val(), char('}')).map(|(w, _)| UserInputBound::Exclusive(w));
|
||||||
|
let incl = (term_val(), char(']')).map(|(w, _)| UserInputBound::Inclusive(w));
|
||||||
|
attempt(excl).or(incl)
|
||||||
|
};
|
||||||
|
(
|
||||||
|
optional((field(), char(':')).map(|x| x.0)),
|
||||||
|
lower_bound,
|
||||||
|
spaces(),
|
||||||
|
string("TO"),
|
||||||
|
spaces(),
|
||||||
|
upper_bound,
|
||||||
|
).map(|(field, lower, _, _, _, upper)| UserInputLeaf::Range {
|
||||||
|
field,
|
||||||
|
lower,
|
||||||
|
upper
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn leaf[I]()(I) -> UserInputAST
|
||||||
|
where [I: Stream<Item = char>] {
|
||||||
|
(char('-'), leaf()).map(|(_, expr)| expr.unary(Occur::MustNot) )
|
||||||
|
.or((char('+'), leaf()).map(|(_, expr)| expr.unary(Occur::Must) ))
|
||||||
|
.or((char('('), parse_to_ast(), char(')')).map(|(_, expr, _)| expr))
|
||||||
|
.or(char('*').map(|_| UserInputAST::from(UserInputLeaf::All) ))
|
||||||
|
.or(attempt(
|
||||||
|
(string("NOT"), spaces1(), leaf()).map(|(_, _, expr)| expr.unary(Occur::MustNot))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.or(attempt(
|
||||||
|
range().map(UserInputAST::from)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.or(literal().map(|leaf| UserInputAST::Leaf(Box::new(leaf))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BinaryOperand {
|
||||||
|
Or,
|
||||||
|
And,
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
fn binary_operand[I]()(I) -> BinaryOperand
|
||||||
|
where [I: Stream<Item = char>] {
|
||||||
|
(spaces1(),
|
||||||
|
(
|
||||||
|
string("AND").map(|_| BinaryOperand::And)
|
||||||
|
.or(string("OR").map(|_| BinaryOperand::Or))
|
||||||
|
),
|
||||||
|
spaces1()).map(|(_, op,_)| op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Element {
|
||||||
|
SingleEl(UserInputAST),
|
||||||
|
NormalDisjunctive(Vec<Vec<UserInputAST>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element {
|
||||||
|
pub fn into_dnf(self) -> Vec<Vec<UserInputAST>> {
|
||||||
|
match self {
|
||||||
|
Element::NormalDisjunctive(conjunctions) => conjunctions,
|
||||||
|
Element::SingleEl(el) => vec![vec![el]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser! {
|
||||||
|
pub fn parse_to_ast[I]()(I) -> UserInputAST
|
||||||
|
where [I: Stream<Item = char>]
|
||||||
|
{
|
||||||
|
(
|
||||||
|
attempt(
|
||||||
|
chainl1(
|
||||||
|
leaf().map(Element::SingleEl),
|
||||||
|
binary_operand().map(|op: BinaryOperand|
|
||||||
|
move |left: Element, right: Element| {
|
||||||
|
let mut dnf = left.into_dnf();
|
||||||
|
if let Element::SingleEl(el) = right {
|
||||||
|
match op {
|
||||||
|
BinaryOperand::And => {
|
||||||
|
if let Some(last) = dnf.last_mut() {
|
||||||
|
last.push(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BinaryOperand::Or => {
|
||||||
|
dnf.push(vec!(el));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!("Please report.")
|
||||||
|
}
|
||||||
|
Element::NormalDisjunctive(dnf)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(query_grammar::Element::into_dnf)
|
||||||
|
.map(|fnd| {
|
||||||
|
if fnd.len() == 1 {
|
||||||
|
UserInputAST::and(fnd.into_iter().next().unwrap()) //< safe
|
||||||
|
} else {
|
||||||
|
let conjunctions = fnd
|
||||||
|
.into_iter()
|
||||||
|
.map(UserInputAST::and)
|
||||||
|
.collect();
|
||||||
|
UserInputAST::or(conjunctions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.or(
|
||||||
|
sep_by(leaf(), spaces())
|
||||||
|
.map(|subqueries: Vec<UserInputAST>| {
|
||||||
|
if subqueries.len() == 1 {
|
||||||
|
subqueries.into_iter().next().unwrap()
|
||||||
|
} else {
|
||||||
|
UserInputAST::Clause(subqueries.into_iter().collect())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_parse_query_to_ast_helper(query: &str, expected: &str) {
|
||||||
|
let query = parse_to_ast().parse(query).unwrap().0;
|
||||||
|
let query_str = format!("{:?}", query);
|
||||||
|
assert_eq!(query_str, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_is_parse_err(query: &str) {
|
||||||
|
assert!(parse_to_ast().parse(query).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_query_to_ast_not_op() {
|
||||||
|
assert_eq!(
|
||||||
|
format!("{:?}", parse_to_ast().parse("NOT")),
|
||||||
|
"Err(UnexpectedParse)"
|
||||||
|
);
|
||||||
|
test_parse_query_to_ast_helper("NOTa", "\"NOTa\"");
|
||||||
|
test_parse_query_to_ast_helper("NOT a", "-(\"a\")");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_query_to_ast_binary_op() {
|
||||||
|
test_parse_query_to_ast_helper("a AND b", "(+(\"a\") +(\"b\"))");
|
||||||
|
test_parse_query_to_ast_helper("a OR b", "(?(\"a\") ?(\"b\"))");
|
||||||
|
test_parse_query_to_ast_helper("a OR b AND c", "(?(\"a\") ?((+(\"b\") +(\"c\"))))");
|
||||||
|
test_parse_query_to_ast_helper("a AND b AND c", "(+(\"a\") +(\"b\") +(\"c\"))");
|
||||||
|
assert_eq!(
|
||||||
|
format!("{:?}", parse_to_ast().parse("a OR b aaa")),
|
||||||
|
"Err(UnexpectedParse)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!("{:?}", parse_to_ast().parse("a AND b aaa")),
|
||||||
|
"Err(UnexpectedParse)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!("{:?}", parse_to_ast().parse("aaa a OR b ")),
|
||||||
|
"Err(UnexpectedParse)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!("{:?}", parse_to_ast().parse("aaa ccc a OR b ")),
|
||||||
|
"Err(UnexpectedParse)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_query_to_ast() {
|
||||||
|
test_parse_query_to_ast_helper("+(a b) +d", "(+((\"a\" \"b\")) +(\"d\"))");
|
||||||
|
test_parse_query_to_ast_helper("(+a +b) d", "((+(\"a\") +(\"b\")) \"d\")");
|
||||||
|
test_parse_query_to_ast_helper("(+a)", "+(\"a\")");
|
||||||
|
test_parse_query_to_ast_helper("(+a +b)", "(+(\"a\") +(\"b\"))");
|
||||||
|
test_parse_query_to_ast_helper("abc:toto", "abc:\"toto\"");
|
||||||
|
test_parse_query_to_ast_helper("abc:1.1", "abc:\"1.1\"");
|
||||||
|
test_parse_query_to_ast_helper("+abc:toto", "+(abc:\"toto\")");
|
||||||
|
test_parse_query_to_ast_helper("(+abc:toto -titi)", "(+(abc:\"toto\") -(\"titi\"))");
|
||||||
|
test_parse_query_to_ast_helper("-abc:toto", "-(abc:\"toto\")");
|
||||||
|
test_parse_query_to_ast_helper("abc:a b", "(abc:\"a\" \"b\")");
|
||||||
|
test_parse_query_to_ast_helper("abc:\"a b\"", "abc:\"a b\"");
|
||||||
|
test_parse_query_to_ast_helper("foo:[1 TO 5]", "foo:[\"1\" TO \"5\"]");
|
||||||
|
test_parse_query_to_ast_helper("[1 TO 5]", "[\"1\" TO \"5\"]");
|
||||||
|
test_parse_query_to_ast_helper("foo:{a TO z}", "foo:{\"a\" TO \"z\"}");
|
||||||
|
test_parse_query_to_ast_helper("foo:[1 TO toto}", "foo:[\"1\" TO \"toto\"}");
|
||||||
|
test_parse_query_to_ast_helper("foo:[* TO toto}", "foo:[\"*\" TO \"toto\"}");
|
||||||
|
test_parse_query_to_ast_helper("foo:[1 TO *}", "foo:[\"1\" TO \"*\"}");
|
||||||
|
test_parse_query_to_ast_helper("foo:[1.1 TO *}", "foo:[\"1.1\" TO \"*\"}");
|
||||||
|
test_is_parse_err("abc + ");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
use super::logical_ast::*;
|
use super::logical_ast::*;
|
||||||
|
use super::query_grammar::parse_to_ast;
|
||||||
|
use super::user_input_ast::*;
|
||||||
use crate::core::Index;
|
use crate::core::Index;
|
||||||
|
use crate::query::occur::compose_occur;
|
||||||
|
use crate::query::query_parser::logical_ast::LogicalAST;
|
||||||
use crate::query::AllQuery;
|
use crate::query::AllQuery;
|
||||||
use crate::query::BooleanQuery;
|
use crate::query::BooleanQuery;
|
||||||
use crate::query::EmptyQuery;
|
use crate::query::EmptyQuery;
|
||||||
@@ -12,11 +16,11 @@ use crate::schema::IndexRecordOption;
|
|||||||
use crate::schema::{Field, Schema};
|
use crate::schema::{Field, Schema};
|
||||||
use crate::schema::{FieldType, Term};
|
use crate::schema::{FieldType, Term};
|
||||||
use crate::tokenizer::TokenizerManager;
|
use crate::tokenizer::TokenizerManager;
|
||||||
|
use combine::Parser;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::num::{ParseFloatError, ParseIntError};
|
use std::num::{ParseFloatError, ParseIntError};
|
||||||
use std::ops::Bound;
|
use std::ops::Bound;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tantivy_query_grammar::{UserInputAST, UserInputBound, UserInputLeaf};
|
|
||||||
|
|
||||||
/// Possible error that may happen when parsing a query.
|
/// Possible error that may happen when parsing a query.
|
||||||
#[derive(Debug, PartialEq, Eq, Fail)]
|
#[derive(Debug, PartialEq, Eq, Fail)]
|
||||||
@@ -218,8 +222,9 @@ impl QueryParser {
|
|||||||
|
|
||||||
/// Parse the user query into an AST.
|
/// Parse the user query into an AST.
|
||||||
fn parse_query_to_logical_ast(&self, query: &str) -> Result<LogicalAST, QueryParserError> {
|
fn parse_query_to_logical_ast(&self, query: &str) -> Result<LogicalAST, QueryParserError> {
|
||||||
let user_input_ast =
|
let (user_input_ast, _remaining) = parse_to_ast()
|
||||||
tantivy_query_grammar::parse_query(query).map_err(|_| QueryParserError::SyntaxError)?;
|
.parse(query)
|
||||||
|
.map_err(|_| QueryParserError::SyntaxError)?;
|
||||||
self.compute_logical_ast(user_input_ast)
|
self.compute_logical_ast(user_input_ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +369,6 @@ impl QueryParser {
|
|||||||
match *bound {
|
match *bound {
|
||||||
UserInputBound::Inclusive(_) => Ok(Bound::Included(term)),
|
UserInputBound::Inclusive(_) => Ok(Bound::Included(term)),
|
||||||
UserInputBound::Exclusive(_) => Ok(Bound::Excluded(term)),
|
UserInputBound::Exclusive(_) => Ok(Bound::Excluded(term)),
|
||||||
UserInputBound::Unbounded => Ok(Bound::Unbounded),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +398,7 @@ impl QueryParser {
|
|||||||
let mut logical_sub_queries: Vec<(Occur, LogicalAST)> = Vec::new();
|
let mut logical_sub_queries: Vec<(Occur, LogicalAST)> = Vec::new();
|
||||||
for sub_query in sub_queries {
|
for sub_query in sub_queries {
|
||||||
let (occur, sub_ast) = self.compute_logical_ast_with_occur(sub_query)?;
|
let (occur, sub_ast) = self.compute_logical_ast_with_occur(sub_query)?;
|
||||||
let new_occur = Occur::compose(default_occur, occur);
|
let new_occur = compose_occur(default_occur, occur);
|
||||||
logical_sub_queries.push((new_occur, sub_ast));
|
logical_sub_queries.push((new_occur, sub_ast));
|
||||||
}
|
}
|
||||||
Ok((Occur::Should, LogicalAST::Clause(logical_sub_queries)))
|
Ok((Occur::Should, LogicalAST::Clause(logical_sub_queries)))
|
||||||
@@ -402,7 +406,7 @@ impl QueryParser {
|
|||||||
UserInputAST::Unary(left_occur, subquery) => {
|
UserInputAST::Unary(left_occur, subquery) => {
|
||||||
let (right_occur, logical_sub_queries) =
|
let (right_occur, logical_sub_queries) =
|
||||||
self.compute_logical_ast_with_occur(*subquery)?;
|
self.compute_logical_ast_with_occur(*subquery)?;
|
||||||
Ok((Occur::compose(left_occur, right_occur), logical_sub_queries))
|
Ok((compose_occur(left_occur, right_occur), logical_sub_queries))
|
||||||
}
|
}
|
||||||
UserInputAST::Leaf(leaf) => {
|
UserInputAST::Leaf(leaf) => {
|
||||||
let result_ast = self.compute_logical_ast_from_leaf(*leaf)?;
|
let result_ast = self.compute_logical_ast_from_leaf(*leaf)?;
|
||||||
@@ -624,7 +628,7 @@ mod test {
|
|||||||
pub fn test_parse_query_untokenized() {
|
pub fn test_parse_query_untokenized() {
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"nottokenized:\"wordone wordtwo\"",
|
"nottokenized:\"wordone wordtwo\"",
|
||||||
"Term(field=7,bytes=[119, 111, 114, 100, 111, 110, \
|
"Term([0, 0, 0, 7, 119, 111, 114, 100, 111, 110, \
|
||||||
101, 32, 119, 111, 114, 100, 116, 119, 111])",
|
101, 32, 119, 111, 114, 100, 116, 119, 111])",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -668,46 +672,40 @@ mod test {
|
|||||||
.is_ok());
|
.is_ok());
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"unsigned:2324",
|
"unsigned:2324",
|
||||||
"Term(field=3,bytes=[0, 0, 0, 0, 0, 0, 9, 20])",
|
"Term([0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 9, 20])",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"signed:-2324",
|
"signed:-2324",
|
||||||
&format!(
|
&format!("{:?}", Term::from_field_i64(Field(2u32), -2324)),
|
||||||
"{:?}",
|
|
||||||
Term::from_field_i64(Field::from_field_id(2u32), -2324)
|
|
||||||
),
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"float:2.5",
|
"float:2.5",
|
||||||
&format!(
|
&format!("{:?}", Term::from_field_f64(Field(10u32), 2.5)),
|
||||||
"{:?}",
|
|
||||||
Term::from_field_f64(Field::from_field_id(10u32), 2.5)
|
|
||||||
),
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_parse_query_to_ast_single_term() {
|
pub fn test_parse_query_to_ast_disjunction() {
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:toto",
|
"title:toto",
|
||||||
"Term(field=0,bytes=[116, 111, 116, 111])",
|
"Term([0, 0, 0, 0, 116, 111, 116, 111])",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"+title:toto",
|
"+title:toto",
|
||||||
"Term(field=0,bytes=[116, 111, 116, 111])",
|
"Term([0, 0, 0, 0, 116, 111, 116, 111])",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"+title:toto -titi",
|
"+title:toto -titi",
|
||||||
"(+Term(field=0,bytes=[116, 111, 116, 111]) \
|
"(+Term([0, 0, 0, 0, 116, 111, 116, 111]) \
|
||||||
-(Term(field=0,bytes=[116, 105, 116, 105]) \
|
-(Term([0, 0, 0, 0, 116, 105, 116, 105]) \
|
||||||
Term(field=1,bytes=[116, 105, 116, 105])))",
|
Term([0, 0, 0, 1, 116, 105, 116, 105])))",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -716,67 +714,49 @@ mod test {
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
QueryParserError::AllButQueryForbidden
|
QueryParserError::AllButQueryForbidden
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_parse_query_to_ast_two_terms() {
|
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:a b",
|
"title:a b",
|
||||||
"(Term(field=0,bytes=[97]) (Term(field=0,bytes=[98]) Term(field=1,bytes=[98])))",
|
"(Term([0, 0, 0, 0, 97]) (Term([0, 0, 0, 0, 98]) \
|
||||||
|
Term([0, 0, 0, 1, 98])))",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:\"a b\"",
|
"title:\"a b\"",
|
||||||
"\"[(0, Term(field=0,bytes=[97])), \
|
"\"[(0, Term([0, 0, 0, 0, 97])), \
|
||||||
(1, Term(field=0,bytes=[98]))]\"",
|
(1, Term([0, 0, 0, 0, 98]))]\"",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_parse_query_to_ast_ranges() {
|
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:[a TO b]",
|
"title:[a TO b]",
|
||||||
"(Included(Term(field=0,bytes=[97])) TO Included(Term(field=0,bytes=[98])))",
|
"(Included(Term([0, 0, 0, 0, 97])) TO \
|
||||||
|
Included(Term([0, 0, 0, 0, 98])))",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"[a TO b]",
|
"[a TO b]",
|
||||||
"((Included(Term(field=0,bytes=[97])) TO \
|
"((Included(Term([0, 0, 0, 0, 97])) TO \
|
||||||
Included(Term(field=0,bytes=[98]))) \
|
Included(Term([0, 0, 0, 0, 98]))) \
|
||||||
(Included(Term(field=1,bytes=[97])) TO \
|
(Included(Term([0, 0, 0, 1, 97])) TO \
|
||||||
Included(Term(field=1,bytes=[98]))))",
|
Included(Term([0, 0, 0, 1, 98]))))",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:{titi TO toto}",
|
"title:{titi TO toto}",
|
||||||
"(Excluded(Term(field=0,bytes=[116, 105, 116, 105])) TO \
|
"(Excluded(Term([0, 0, 0, 0, 116, 105, 116, 105])) TO \
|
||||||
Excluded(Term(field=0,bytes=[116, 111, 116, 111])))",
|
Excluded(Term([0, 0, 0, 0, 116, 111, 116, 111])))",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:{* TO toto}",
|
"title:{* TO toto}",
|
||||||
"(Unbounded TO Excluded(Term(field=0,bytes=[116, 111, 116, 111])))",
|
"(Unbounded TO \
|
||||||
|
Excluded(Term([0, 0, 0, 0, 116, 111, 116, 111])))",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:{titi TO *}",
|
"title:{titi TO *}",
|
||||||
"(Excluded(Term(field=0,bytes=[116, 105, 116, 105])) TO Unbounded)",
|
"(Excluded(Term([0, 0, 0, 0, 116, 105, 116, 105])) TO Unbounded)",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
|
||||||
"signed:{-5 TO 3}",
|
|
||||||
"(Excluded(Term(field=2,bytes=[127, 255, 255, 255, 255, 255, 255, 251])) TO \
|
|
||||||
Excluded(Term(field=2,bytes=[128, 0, 0, 0, 0, 0, 0, 3])))",
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
test_parse_query_to_logical_ast_helper(
|
|
||||||
"float:{-1.5 TO 1.5}",
|
|
||||||
"(Excluded(Term(field=10,bytes=[64, 7, 255, 255, 255, 255, 255, 255])) TO \
|
|
||||||
Excluded(Term(field=10,bytes=[191, 248, 0, 0, 0, 0, 0, 0])))",
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
test_parse_query_to_logical_ast_helper("*", "*", false);
|
test_parse_query_to_logical_ast_helper("*", "*", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,19 +858,19 @@ mod test {
|
|||||||
pub fn test_parse_query_to_ast_conjunction() {
|
pub fn test_parse_query_to_ast_conjunction() {
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:toto",
|
"title:toto",
|
||||||
"Term(field=0,bytes=[116, 111, 116, 111])",
|
"Term([0, 0, 0, 0, 116, 111, 116, 111])",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"+title:toto",
|
"+title:toto",
|
||||||
"Term(field=0,bytes=[116, 111, 116, 111])",
|
"Term([0, 0, 0, 0, 116, 111, 116, 111])",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"+title:toto -titi",
|
"+title:toto -titi",
|
||||||
"(+Term(field=0,bytes=[116, 111, 116, 111]) \
|
"(+Term([0, 0, 0, 0, 116, 111, 116, 111]) \
|
||||||
-(Term(field=0,bytes=[116, 105, 116, 105]) \
|
-(Term([0, 0, 0, 0, 116, 105, 116, 105]) \
|
||||||
Term(field=1,bytes=[116, 105, 116, 105])))",
|
Term([0, 0, 0, 1, 116, 105, 116, 105])))",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -901,25 +881,16 @@ mod test {
|
|||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:a b",
|
"title:a b",
|
||||||
"(+Term(field=0,bytes=[97]) \
|
"(+Term([0, 0, 0, 0, 97]) \
|
||||||
+(Term(field=0,bytes=[98]) \
|
+(Term([0, 0, 0, 0, 98]) \
|
||||||
Term(field=1,bytes=[98])))",
|
Term([0, 0, 0, 1, 98])))",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
test_parse_query_to_logical_ast_helper(
|
test_parse_query_to_logical_ast_helper(
|
||||||
"title:\"a b\"",
|
"title:\"a b\"",
|
||||||
"\"[(0, Term(field=0,bytes=[97])), \
|
"\"[(0, Term([0, 0, 0, 0, 97])), \
|
||||||
(1, Term(field=0,bytes=[98]))]\"",
|
(1, Term([0, 0, 0, 0, 98]))]\"",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_query_parser_hyphen() {
|
|
||||||
test_parse_query_to_logical_ast_helper(
|
|
||||||
"title:www-form-encoded",
|
|
||||||
"\"[(0, Term(field=0,bytes=[119, 119, 119])), (1, Term(field=0,bytes=[102, 111, 114, 109])), (2, Term(field=0,bytes=[101, 110, 99, 111, 100, 101, 100]))]\"",
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/query/query_parser/stemmer.rs
Normal file
44
src/query/query_parser/stemmer.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use stemmer;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct StemmerTokenStream<TailTokenStream>
|
||||||
|
where TailTokenStream: TokenStream {
|
||||||
|
tail: TailTokenStream,
|
||||||
|
stemmer: Arc<stemmer::Stemmer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<TailTokenStream> TokenStream for StemmerTokenStream<TailTokenStream>
|
||||||
|
where TailTokenStream: TokenStream {
|
||||||
|
|
||||||
|
fn token(&self) -> &Token {
|
||||||
|
self.tail.token()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_mut(&mut self) -> &mut Token {
|
||||||
|
self.tail.token_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(&mut self) -> bool {
|
||||||
|
if self.tail.advance() {
|
||||||
|
// self.tail.token_mut().term.make_ascii_lowercase();
|
||||||
|
let new_str = self.stemmer.stem_str(&self.token().term);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<TailTokenStream> StemmerTokenStream<TailTokenStream>
|
||||||
|
where TailTokenStream: TokenStream {
|
||||||
|
|
||||||
|
fn wrap(stemmer: Arc<stemmer::Stemmer>, tail: TailTokenStream) -> StemmerTokenStream<TailTokenStream> {
|
||||||
|
StemmerTokenStream {
|
||||||
|
tail,
|
||||||
|
stemmer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
use crate::Occur;
|
use crate::query::Occur;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum UserInputLeaf {
|
pub enum UserInputLeaf {
|
||||||
Literal(UserInputLiteral),
|
Literal(UserInputLiteral),
|
||||||
All,
|
All,
|
||||||
@@ -36,7 +35,6 @@ impl Debug for UserInputLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub struct UserInputLiteral {
|
pub struct UserInputLiteral {
|
||||||
pub field_name: Option<String>,
|
pub field_name: Option<String>,
|
||||||
pub phrase: String,
|
pub phrase: String,
|
||||||
@@ -51,11 +49,9 @@ impl fmt::Debug for UserInputLiteral {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum UserInputBound {
|
pub enum UserInputBound {
|
||||||
Inclusive(String),
|
Inclusive(String),
|
||||||
Exclusive(String),
|
Exclusive(String),
|
||||||
Unbounded,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserInputBound {
|
impl UserInputBound {
|
||||||
@@ -63,7 +59,6 @@ impl UserInputBound {
|
|||||||
match *self {
|
match *self {
|
||||||
UserInputBound::Inclusive(ref word) => write!(formatter, "[\"{}\"", word),
|
UserInputBound::Inclusive(ref word) => write!(formatter, "[\"{}\"", word),
|
||||||
UserInputBound::Exclusive(ref word) => write!(formatter, "{{\"{}\"", word),
|
UserInputBound::Exclusive(ref word) => write!(formatter, "{{\"{}\"", word),
|
||||||
UserInputBound::Unbounded => write!(formatter, "{{\"*\""),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +66,6 @@ impl UserInputBound {
|
|||||||
match *self {
|
match *self {
|
||||||
UserInputBound::Inclusive(ref word) => write!(formatter, "\"{}\"]", word),
|
UserInputBound::Inclusive(ref word) => write!(formatter, "\"{}\"]", word),
|
||||||
UserInputBound::Exclusive(ref word) => write!(formatter, "\"{}\"}}", word),
|
UserInputBound::Exclusive(ref word) => write!(formatter, "\"{}\"}}", word),
|
||||||
UserInputBound::Unbounded => write!(formatter, "\"*\"}}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +73,6 @@ impl UserInputBound {
|
|||||||
match *self {
|
match *self {
|
||||||
UserInputBound::Inclusive(ref contents) => contents,
|
UserInputBound::Inclusive(ref contents) => contents,
|
||||||
UserInputBound::Exclusive(ref contents) => contents,
|
UserInputBound::Exclusive(ref contents) => contents,
|
||||||
UserInputBound::Unbounded => &"*",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,6 +80,9 @@ impl UserInputBound {
|
|||||||
pub enum UserInputAST {
|
pub enum UserInputAST {
|
||||||
Clause(Vec<UserInputAST>),
|
Clause(Vec<UserInputAST>),
|
||||||
Unary(Occur, Box<UserInputAST>),
|
Unary(Occur, Box<UserInputAST>),
|
||||||
|
// Not(Box<UserInputAST>),
|
||||||
|
// Should(Box<UserInputAST>),
|
||||||
|
// Must(Box<UserInputAST>),
|
||||||
Leaf(Box<UserInputLeaf>),
|
Leaf(Box<UserInputLeaf>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +92,7 @@ impl UserInputAST {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn compose(occur: Occur, asts: Vec<UserInputAST>) -> UserInputAST {
|
fn compose(occur: Occur, asts: Vec<UserInputAST>) -> UserInputAST {
|
||||||
assert_ne!(occur, Occur::MustNot);
|
assert!(occur != Occur::MustNot);
|
||||||
assert!(!asts.is_empty());
|
assert!(!asts.is_empty());
|
||||||
if asts.len() == 1 {
|
if asts.len() == 1 {
|
||||||
asts.into_iter().next().unwrap() //< safe
|
asts.into_iter().next().unwrap() //< safe
|
||||||
@@ -109,10 +105,6 @@ impl UserInputAST {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn empty_query() -> UserInputAST {
|
|
||||||
UserInputAST::Clause(Vec::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn and(asts: Vec<UserInputAST>) -> UserInputAST {
|
pub fn and(asts: Vec<UserInputAST>) -> UserInputAST {
|
||||||
UserInputAST::compose(Occur::Must, asts)
|
UserInputAST::compose(Occur::Must, asts)
|
||||||
}
|
}
|
||||||
@@ -122,6 +114,42 @@ impl UserInputAST {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl UserInputAST {
|
||||||
|
|
||||||
|
fn compose_occur(self, occur: Occur) -> UserInputAST {
|
||||||
|
match self {
|
||||||
|
UserInputAST::Not(other) => {
|
||||||
|
let new_occur = compose_occur(Occur::MustNot, occur);
|
||||||
|
other.simplify()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simplify(self) -> UserInputAST {
|
||||||
|
match self {
|
||||||
|
UserInputAST::Clause(els) => {
|
||||||
|
if els.len() == 1 {
|
||||||
|
return els.into_iter().next().unwrap();
|
||||||
|
} else {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserInputAST::Not(els) => {
|
||||||
|
if els.len() == 1 {
|
||||||
|
return els.into_iter().next().unwrap();
|
||||||
|
} else {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
impl From<UserInputLiteral> for UserInputLeaf {
|
impl From<UserInputLiteral> for UserInputLeaf {
|
||||||
fn from(literal: UserInputLiteral) -> UserInputLeaf {
|
fn from(literal: UserInputLiteral) -> UserInputLeaf {
|
||||||
UserInputLeaf::Literal(literal)
|
UserInputLeaf::Literal(literal)
|
||||||
@@ -151,7 +179,7 @@ impl fmt::Debug for UserInputAST {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
UserInputAST::Unary(ref occur, ref subquery) => {
|
UserInputAST::Unary(ref occur, ref subquery) => {
|
||||||
write!(formatter, "{}({:?})", occur, subquery)
|
write!(formatter, "{}({:?})", occur.to_char(), subquery)
|
||||||
}
|
}
|
||||||
UserInputAST::Leaf(ref subquery) => write!(formatter, "{:?}", subquery),
|
UserInputAST::Leaf(ref subquery) => write!(formatter, "{:?}", subquery),
|
||||||
}
|
}
|
||||||
@@ -38,10 +38,14 @@ fn map_bound<TFrom, TTo, Transform: Fn(&TFrom) -> TTo>(
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use tantivy::collector::Count;
|
///
|
||||||
/// # use tantivy::query::RangeQuery;
|
/// # #[macro_use]
|
||||||
|
/// # extern crate tantivy;
|
||||||
|
/// # use tantivy::Index;
|
||||||
/// # use tantivy::schema::{Schema, INDEXED};
|
/// # use tantivy::schema::{Schema, INDEXED};
|
||||||
/// # use tantivy::{doc, Index, Result};
|
/// # use tantivy::collector::Count;
|
||||||
|
/// # use tantivy::Result;
|
||||||
|
/// # use tantivy::query::RangeQuery;
|
||||||
/// #
|
/// #
|
||||||
/// # fn run() -> Result<()> {
|
/// # fn run() -> Result<()> {
|
||||||
/// # let mut schema_builder = Schema::builder();
|
/// # let mut schema_builder = Schema::builder();
|
||||||
@@ -334,33 +338,39 @@ mod tests {
|
|||||||
use crate::collector::Count;
|
use crate::collector::Count;
|
||||||
use crate::schema::{Document, Field, Schema, INDEXED};
|
use crate::schema::{Document, Field, Schema, INDEXED};
|
||||||
use crate::Index;
|
use crate::Index;
|
||||||
|
use crate::Result;
|
||||||
use std::collections::Bound;
|
use std::collections::Bound;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_range_query_simple() {
|
fn test_range_query_simple() {
|
||||||
let mut schema_builder = Schema::builder();
|
fn run() -> Result<()> {
|
||||||
let year_field = schema_builder.add_u64_field("year", INDEXED);
|
let mut schema_builder = Schema::builder();
|
||||||
let schema = schema_builder.build();
|
let year_field = schema_builder.add_u64_field("year", INDEXED);
|
||||||
|
let schema = schema_builder.build();
|
||||||
|
|
||||||
let index = Index::create_in_ram(schema);
|
let index = Index::create_in_ram(schema);
|
||||||
{
|
{
|
||||||
let mut index_writer = index.writer_with_num_threads(1, 6_000_000).unwrap();
|
let mut index_writer = index.writer_with_num_threads(1, 6_000_000).unwrap();
|
||||||
for year in 1950u64..2017u64 {
|
for year in 1950u64..2017u64 {
|
||||||
let num_docs_within_year = 10 + (year - 1950) * (year - 1950);
|
let num_docs_within_year = 10 + (year - 1950) * (year - 1950);
|
||||||
for _ in 0..num_docs_within_year {
|
for _ in 0..num_docs_within_year {
|
||||||
index_writer.add_document(doc!(year_field => year));
|
index_writer.add_document(doc!(year_field => year));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
index_writer.commit().unwrap();
|
||||||
}
|
}
|
||||||
index_writer.commit().unwrap();
|
let reader = index.reader().unwrap();
|
||||||
|
let searcher = reader.searcher();
|
||||||
|
|
||||||
|
let docs_in_the_sixties = RangeQuery::new_u64(year_field, 1960u64..1970u64);
|
||||||
|
|
||||||
|
// ... or `1960..=1969` if inclusive range is enabled.
|
||||||
|
let count = searcher.search(&docs_in_the_sixties, &Count)?;
|
||||||
|
assert_eq!(count, 2285);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
let reader = index.reader().unwrap();
|
|
||||||
let searcher = reader.searcher();
|
|
||||||
|
|
||||||
let docs_in_the_sixties = RangeQuery::new_u64(year_field, 1960u64..1970u64);
|
run().unwrap();
|
||||||
|
|
||||||
// ... or `1960..=1969` if inclusive range is enabled.
|
|
||||||
let count = searcher.search(&docs_in_the_sixties, &Count).unwrap();
|
|
||||||
assert_eq!(count, 2285);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -450,10 +460,7 @@ mod tests {
|
|||||||
let count_multiples =
|
let count_multiples =
|
||||||
|range_query: RangeQuery| searcher.search(&range_query, &Count).unwrap();
|
|range_query: RangeQuery| searcher.search(&range_query, &Count).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(count_multiples(RangeQuery::new_f64(float_field, 10.0..11.0)), 9);
|
||||||
count_multiples(RangeQuery::new_f64(float_field, 10.0..11.0)),
|
|
||||||
9
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
count_multiples(RangeQuery::new_f64_bounds(
|
count_multiples(RangeQuery::new_f64_bounds(
|
||||||
float_field,
|
float_field,
|
||||||
@@ -479,4 +486,5 @@ mod tests {
|
|||||||
91
|
91
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,22 @@ use crate::schema::Field;
|
|||||||
use crate::Result;
|
use crate::Result;
|
||||||
use crate::Searcher;
|
use crate::Searcher;
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
use std::sync::Arc;
|
|
||||||
use tantivy_fst::Regex;
|
use tantivy_fst::Regex;
|
||||||
|
|
||||||
/// A Regex Query matches all of the documents
|
// A Regex Query matches all of the documents
|
||||||
/// containing a specific term that matches
|
/// containing a specific term that matches
|
||||||
/// a regex pattern.
|
/// a regex pattern
|
||||||
|
/// A Fuzzy Query matches all of the documents
|
||||||
|
/// containing a specific term that is within
|
||||||
|
/// Levenshtein distance
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
/// use tantivy::schema::{Schema, TEXT};
|
||||||
|
/// use tantivy::{Index, Result, Term};
|
||||||
/// use tantivy::collector::Count;
|
/// use tantivy::collector::Count;
|
||||||
/// use tantivy::query::RegexQuery;
|
/// use tantivy::query::RegexQuery;
|
||||||
/// use tantivy::schema::{Schema, TEXT};
|
|
||||||
/// use tantivy::{doc, Index, Result, Term};
|
|
||||||
///
|
///
|
||||||
/// # fn main() { example().unwrap(); }
|
/// # fn main() { example().unwrap(); }
|
||||||
/// fn example() -> Result<()> {
|
/// fn example() -> Result<()> {
|
||||||
@@ -44,7 +48,7 @@ use tantivy_fst::Regex;
|
|||||||
/// let searcher = reader.searcher();
|
/// let searcher = reader.searcher();
|
||||||
///
|
///
|
||||||
/// let term = Term::from_field_text(title, "Diary");
|
/// let term = Term::from_field_text(title, "Diary");
|
||||||
/// let query = RegexQuery::from_pattern("d[ai]{2}ry", title)?;
|
/// let query = RegexQuery::new("d[ai]{2}ry".to_string(), title);
|
||||||
/// let count = searcher.search(&query, &Count)?;
|
/// let count = searcher.search(&query, &Count)?;
|
||||||
/// assert_eq!(count, 3);
|
/// assert_eq!(count, 3);
|
||||||
/// Ok(())
|
/// Ok(())
|
||||||
@@ -52,34 +56,30 @@ use tantivy_fst::Regex;
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RegexQuery {
|
pub struct RegexQuery {
|
||||||
regex: Arc<Regex>,
|
regex_pattern: String,
|
||||||
field: Field,
|
field: Field,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegexQuery {
|
impl RegexQuery {
|
||||||
/// Creates a new RegexQuery from a given pattern
|
/// Creates a new Fuzzy Query
|
||||||
pub fn from_pattern(regex_pattern: &str, field: Field) -> Result<Self> {
|
pub fn new(regex_pattern: String, field: Field) -> RegexQuery {
|
||||||
let regex = Regex::new(®ex_pattern)
|
|
||||||
.map_err(|_| TantivyError::InvalidArgument(regex_pattern.to_string()))?;
|
|
||||||
Ok(RegexQuery::from_regex(regex, field))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new RegexQuery from a fully built Regex
|
|
||||||
pub fn from_regex<T: Into<Arc<Regex>>>(regex: T, field: Field) -> Self {
|
|
||||||
RegexQuery {
|
RegexQuery {
|
||||||
regex: regex.into(),
|
regex_pattern,
|
||||||
field,
|
field,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn specialized_weight(&self) -> AutomatonWeight<Regex> {
|
fn specialized_weight(&self) -> Result<AutomatonWeight<Regex>> {
|
||||||
AutomatonWeight::new(self.field, self.regex.clone())
|
let automaton = Regex::new(&self.regex_pattern)
|
||||||
|
.map_err(|_| TantivyError::InvalidArgument(self.regex_pattern.clone()))?;
|
||||||
|
|
||||||
|
Ok(AutomatonWeight::new(self.field, automaton))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Query for RegexQuery {
|
impl Query for RegexQuery {
|
||||||
fn weight(&self, _searcher: &Searcher, _scoring_enabled: bool) -> Result<Box<dyn Weight>> {
|
fn weight(&self, _searcher: &Searcher, _scoring_enabled: bool) -> Result<Box<dyn Weight>> {
|
||||||
Ok(Box::new(self.specialized_weight()))
|
Ok(Box::new(self.specialized_weight()?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +87,13 @@ impl Query for RegexQuery {
|
|||||||
mod test {
|
mod test {
|
||||||
use super::RegexQuery;
|
use super::RegexQuery;
|
||||||
use crate::collector::TopDocs;
|
use crate::collector::TopDocs;
|
||||||
|
use crate::schema::Schema;
|
||||||
use crate::schema::TEXT;
|
use crate::schema::TEXT;
|
||||||
use crate::schema::{Field, Schema};
|
|
||||||
use crate::tests::assert_nearly_equals;
|
use crate::tests::assert_nearly_equals;
|
||||||
use crate::{Index, IndexReader};
|
use crate::Index;
|
||||||
use std::sync::Arc;
|
|
||||||
use tantivy_fst::Regex;
|
|
||||||
|
|
||||||
fn build_test_index() -> (IndexReader, Field) {
|
#[test]
|
||||||
|
pub fn test_regex_query() {
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
let country_field = schema_builder.add_text_field("country", TEXT);
|
let country_field = schema_builder.add_text_field("country", TEXT);
|
||||||
let schema = schema_builder.build();
|
let schema = schema_builder.build();
|
||||||
@@ -110,65 +109,20 @@ mod test {
|
|||||||
index_writer.commit().unwrap();
|
index_writer.commit().unwrap();
|
||||||
}
|
}
|
||||||
let reader = index.reader().unwrap();
|
let reader = index.reader().unwrap();
|
||||||
|
|
||||||
(reader, country_field)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_regex_query(
|
|
||||||
query_matching_one: RegexQuery,
|
|
||||||
query_matching_zero: RegexQuery,
|
|
||||||
reader: IndexReader,
|
|
||||||
) {
|
|
||||||
let searcher = reader.searcher();
|
let searcher = reader.searcher();
|
||||||
{
|
{
|
||||||
|
let regex_query = RegexQuery::new("jap[ao]n".to_string(), country_field);
|
||||||
let scored_docs = searcher
|
let scored_docs = searcher
|
||||||
.search(&query_matching_one, &TopDocs::with_limit(2))
|
.search(®ex_query, &TopDocs::with_limit(2))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(scored_docs.len(), 1, "Expected only 1 document");
|
assert_eq!(scored_docs.len(), 1, "Expected only 1 document");
|
||||||
let (score, _) = scored_docs[0];
|
let (score, _) = scored_docs[0];
|
||||||
assert_nearly_equals(1f32, score);
|
assert_nearly_equals(1f32, score);
|
||||||
}
|
}
|
||||||
|
let regex_query = RegexQuery::new("jap[A-Z]n".to_string(), country_field);
|
||||||
let top_docs = searcher
|
let top_docs = searcher
|
||||||
.search(&query_matching_zero, &TopDocs::with_limit(2))
|
.search(®ex_query, &TopDocs::with_limit(2))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(top_docs.is_empty(), "Expected ZERO document");
|
assert!(top_docs.is_empty(), "Expected ZERO document");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_regex_query() {
|
|
||||||
let (reader, field) = build_test_index();
|
|
||||||
|
|
||||||
let matching_one = RegexQuery::from_pattern("jap[ao]n", field).unwrap();
|
|
||||||
let matching_zero = RegexQuery::from_pattern("jap[A-Z]n", field).unwrap();
|
|
||||||
|
|
||||||
verify_regex_query(matching_one, matching_zero, reader);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_construct_from_regex() {
|
|
||||||
let (reader, field) = build_test_index();
|
|
||||||
|
|
||||||
let matching_one = RegexQuery::from_regex(Regex::new("jap[ao]n").unwrap(), field);
|
|
||||||
let matching_zero = RegexQuery::from_regex(Regex::new("jap[A-Z]n").unwrap(), field);
|
|
||||||
|
|
||||||
verify_regex_query(matching_one, matching_zero, reader);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_construct_from_reused_regex() {
|
|
||||||
let r1 = Arc::new(Regex::new("jap[ao]n").unwrap());
|
|
||||||
let r2 = Arc::new(Regex::new("jap[A-Z]n").unwrap());
|
|
||||||
|
|
||||||
let (reader, field) = build_test_index();
|
|
||||||
|
|
||||||
let matching_one = RegexQuery::from_regex(r1.clone(), field);
|
|
||||||
let matching_zero = RegexQuery::from_regex(r2.clone(), field);
|
|
||||||
|
|
||||||
verify_regex_query(matching_one, matching_zero, reader.clone());
|
|
||||||
|
|
||||||
let matching_one = RegexQuery::from_regex(r1.clone(), field);
|
|
||||||
let matching_zero = RegexQuery::from_regex(r2.clone(), field);
|
|
||||||
|
|
||||||
verify_regex_query(matching_one, matching_zero, reader.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,4 +190,5 @@ mod tests {
|
|||||||
skip_docs,
|
skip_docs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ mod tests {
|
|||||||
use crate::collector::TopDocs;
|
use crate::collector::TopDocs;
|
||||||
use crate::docset::DocSet;
|
use crate::docset::DocSet;
|
||||||
use crate::query::{Query, QueryParser, Scorer, TermQuery};
|
use crate::query::{Query, QueryParser, Scorer, TermQuery};
|
||||||
use crate::schema::{Field, IndexRecordOption, Schema, STRING, TEXT};
|
use crate::schema::{IndexRecordOption, Schema, STRING, TEXT};
|
||||||
use crate::tests::assert_nearly_equals;
|
use crate::tests::assert_nearly_equals;
|
||||||
use crate::Index;
|
use crate::Index;
|
||||||
use crate::Term;
|
use crate::Term;
|
||||||
@@ -114,16 +114,4 @@ mod tests {
|
|||||||
let reader = index.reader().unwrap();
|
let reader = index.reader().unwrap();
|
||||||
assert_eq!(term_query.count(&*reader.searcher()).unwrap(), 1);
|
assert_eq!(term_query.count(&*reader.searcher()).unwrap(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_term_query_debug() {
|
|
||||||
let term_query = TermQuery::new(
|
|
||||||
Term::from_field_text(Field::from_field_id(1), "hello"),
|
|
||||||
IndexRecordOption::WithFreqs,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!("{:?}", term_query),
|
|
||||||
"TermQuery(Term(field=1,bytes=[104, 101, 108, 108, 111]))"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use crate::Result;
|
|||||||
use crate::Searcher;
|
use crate::Searcher;
|
||||||
use crate::Term;
|
use crate::Term;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// A Term query matches all of the documents
|
/// A Term query matches all of the documents
|
||||||
/// containing a specific term.
|
/// containing a specific term.
|
||||||
@@ -20,10 +19,12 @@ use std::fmt;
|
|||||||
/// * `field norm` - number of tokens in the field.
|
/// * `field norm` - number of tokens in the field.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate tantivy;
|
||||||
|
/// use tantivy::schema::{Schema, TEXT, IndexRecordOption};
|
||||||
|
/// use tantivy::{Index, Result, Term};
|
||||||
/// use tantivy::collector::{Count, TopDocs};
|
/// use tantivy::collector::{Count, TopDocs};
|
||||||
/// use tantivy::query::TermQuery;
|
/// use tantivy::query::TermQuery;
|
||||||
/// use tantivy::schema::{Schema, TEXT, IndexRecordOption};
|
|
||||||
/// use tantivy::{doc, Index, Result, Term};
|
|
||||||
///
|
///
|
||||||
/// # fn main() { example().unwrap(); }
|
/// # fn main() { example().unwrap(); }
|
||||||
/// fn example() -> Result<()> {
|
/// fn example() -> Result<()> {
|
||||||
@@ -60,18 +61,12 @@ use std::fmt;
|
|||||||
/// Ok(())
|
/// Ok(())
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TermQuery {
|
pub struct TermQuery {
|
||||||
term: Term,
|
term: Term,
|
||||||
index_record_option: IndexRecordOption,
|
index_record_option: IndexRecordOption,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for TermQuery {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "TermQuery({:?})", self.term)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TermQuery {
|
impl TermQuery {
|
||||||
/// Creates a new term query.
|
/// Creates a new term query.
|
||||||
pub fn new(term: Term, segment_postings_options: IndexRecordOption) -> TermQuery {
|
pub fn new(term: Term, segment_postings_options: IndexRecordOption) -> TermQuery {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `DocSet` that iterate through the union of two or more `DocSet`s.
|
/// Creates a `DocSet` that iterator through the intersection of two `DocSet`s.
|
||||||
pub struct Union<TScorer, TScoreCombiner = DoNothingCombiner> {
|
pub struct Union<TScorer, TScoreCombiner = DoNothingCombiner> {
|
||||||
docsets: Vec<TScorer>,
|
docsets: Vec<TScorer>,
|
||||||
bitsets: Box<[TinySet; HORIZON_NUM_TINYBITSETS]>,
|
bitsets: Box<[TinySet; HORIZON_NUM_TINYBITSETS]>,
|
||||||
@@ -409,17 +409,20 @@ mod tests {
|
|||||||
vec![1, 2, 3, 7, 8, 9, 99, 100, 101, 500, 20000],
|
vec![1, 2, 3, 7, 8, 9, 99, 100, 101, 500, 20000],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "unstable"))]
|
#[cfg(all(test, feature = "unstable"))]
|
||||||
mod bench {
|
mod bench {
|
||||||
|
|
||||||
use crate::query::score_combiner::DoNothingCombiner;
|
use query::score_combiner::DoNothingCombiner;
|
||||||
use crate::query::{ConstScorer, Union, VecDocSet};
|
use query::ConstScorer;
|
||||||
use crate::tests;
|
use query::Union;
|
||||||
use crate::DocId;
|
use query::VecDocSet;
|
||||||
use crate::DocSet;
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
use tests;
|
||||||
|
use DocId;
|
||||||
|
use DocSet;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_union_3_high(bench: &mut Bencher) {
|
fn bench_union_3_high(bench: &mut Bencher) {
|
||||||
|
|||||||
@@ -82,4 +82,5 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
assert_eq!(postings.fill_buffer(&mut buffer[..]), 9);
|
assert_eq!(postings.fill_buffer(&mut buffer[..]), 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::common::BinarySerializable;
|
use crate::common::BinarySerializable;
|
||||||
use crate::common::VInt;
|
use crate::common::VInt;
|
||||||
use crate::tokenizer::PreTokenizedString;
|
|
||||||
use crate::DateTime;
|
use crate::DateTime;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
@@ -30,8 +29,8 @@ impl From<Vec<FieldValue>> for Document {
|
|||||||
impl PartialEq for Document {
|
impl PartialEq for Document {
|
||||||
fn eq(&self, other: &Document) -> bool {
|
fn eq(&self, other: &Document) -> bool {
|
||||||
// super slow, but only here for tests
|
// super slow, but only here for tests
|
||||||
let mut self_field_values: Vec<&_> = self.field_values.iter().collect();
|
let mut self_field_values = self.field_values.clone();
|
||||||
let mut other_field_values: Vec<&_> = other.field_values.iter().collect();
|
let mut other_field_values = other.field_values.clone();
|
||||||
self_field_values.sort();
|
self_field_values.sort();
|
||||||
other_field_values.sort();
|
other_field_values.sort();
|
||||||
self_field_values.eq(&other_field_values)
|
self_field_values.eq(&other_field_values)
|
||||||
@@ -79,16 +78,6 @@ impl Document {
|
|||||||
self.add(FieldValue::new(field, value));
|
self.add(FieldValue::new(field, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a pre-tokenized text field.
|
|
||||||
pub fn add_pre_tokenized_text(
|
|
||||||
&mut self,
|
|
||||||
field: Field,
|
|
||||||
pre_tokenized_text: &PreTokenizedString,
|
|
||||||
) {
|
|
||||||
let value = Value::PreTokStr(pre_tokenized_text.clone());
|
|
||||||
self.add(FieldValue::new(field, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a u64 field
|
/// Add a u64 field
|
||||||
pub fn add_u64(&mut self, field: Field, value: u64) {
|
pub fn add_u64(&mut self, field: Field, value: u64) {
|
||||||
self.add(FieldValue::new(field, Value::U64(value)));
|
self.add(FieldValue::new(field, Value::U64(value)));
|
||||||
@@ -189,4 +178,5 @@ mod tests {
|
|||||||
doc.add_text(text_field, "My title");
|
doc.add_text(text_field, "My title");
|
||||||
assert_eq!(doc.field_values().len(), 1);
|
assert_eq!(doc.field_values().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ impl Facet {
|
|||||||
|
|
||||||
/// Extract path from the `Facet`.
|
/// Extract path from the `Facet`.
|
||||||
pub fn to_path(&self) -> Vec<&str> {
|
pub fn to_path(&self) -> Vec<&str> {
|
||||||
self.encoded_str().split(|c| c == FACET_SEP_CHAR).collect()
|
self.encoded_str()
|
||||||
|
.split(|c| c == FACET_SEP_CHAR)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,14 @@ use std::io;
|
|||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
/// `Field` is represented by an unsigned 32-bit integer type
|
/// `Field` is actually a `u8` identifying a `Field`
|
||||||
/// The schema holds the mapping between field names and `Field` objects.
|
/// The schema is in charge of holding mapping between field names
|
||||||
|
/// to `Field` objects.
|
||||||
|
///
|
||||||
|
/// Because the field id is a `u8`, tantivy can only have at most `255` fields.
|
||||||
|
/// Value 255 is reserved.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
|
||||||
pub struct Field(u32);
|
pub struct Field(pub u32);
|
||||||
|
|
||||||
impl Field {
|
|
||||||
/// Create a new field object for the given FieldId.
|
|
||||||
pub fn from_field_id(field_id: u32) -> Field {
|
|
||||||
Field(field_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a u32 identifying uniquely a field within a schema.
|
|
||||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
|
||||||
pub fn field_id(&self) -> u32 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BinarySerializable for Field {
|
impl BinarySerializable for Field {
|
||||||
fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
|
|||||||
@@ -108,9 +108,7 @@ impl FieldEntry {
|
|||||||
/// Returns true iff the field is a int (signed or unsigned) fast field
|
/// Returns true iff the field is a int (signed or unsigned) fast field
|
||||||
pub fn is_int_fast(&self) -> bool {
|
pub fn is_int_fast(&self) -> bool {
|
||||||
match self.field_type {
|
match self.field_type {
|
||||||
FieldType::U64(ref options)
|
FieldType::U64(ref options) | FieldType::I64(ref options) | FieldType::F64(ref options) => options.is_fast(),
|
||||||
| FieldType::I64(ref options)
|
|
||||||
| FieldType::F64(ref options) => options.is_fast(),
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use base64::decode;
|
use base64::decode;
|
||||||
|
|
||||||
|
use crate::schema::{IntOptions, TextOptions};
|
||||||
|
|
||||||
use crate::schema::Facet;
|
use crate::schema::Facet;
|
||||||
use crate::schema::IndexRecordOption;
|
use crate::schema::IndexRecordOption;
|
||||||
use crate::schema::TextFieldIndexing;
|
use crate::schema::TextFieldIndexing;
|
||||||
use crate::schema::Value;
|
use crate::schema::Value;
|
||||||
use crate::schema::{IntOptions, TextOptions};
|
|
||||||
use crate::tokenizer::PreTokenizedString;
|
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
/// Possible error that may occur while parsing a field value
|
/// Possible error that may occur while parsing a field value
|
||||||
/// At this point the JSON is known to be valid.
|
/// At this point the JSON is known to be valid.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug)]
|
||||||
pub enum ValueParsingError {
|
pub enum ValueParsingError {
|
||||||
/// Encountered a numerical value that overflows or underflow its integer type.
|
/// Encountered a numerical value that overflows or underflow its integer type.
|
||||||
OverflowError(String),
|
OverflowError(String),
|
||||||
@@ -83,9 +83,9 @@ impl FieldType {
|
|||||||
pub fn is_indexed(&self) -> bool {
|
pub fn is_indexed(&self) -> bool {
|
||||||
match *self {
|
match *self {
|
||||||
FieldType::Str(ref text_options) => text_options.get_indexing_options().is_some(),
|
FieldType::Str(ref text_options) => text_options.get_indexing_options().is_some(),
|
||||||
FieldType::U64(ref int_options)
|
FieldType::U64(ref int_options) | FieldType::I64(ref int_options) | FieldType::F64(ref int_options) => {
|
||||||
| FieldType::I64(ref int_options)
|
int_options.is_indexed()
|
||||||
| FieldType::F64(ref int_options) => int_options.is_indexed(),
|
}
|
||||||
FieldType::Date(ref date_options) => date_options.is_indexed(),
|
FieldType::Date(ref date_options) => date_options.is_indexed(),
|
||||||
FieldType::HierarchicalFacet => true,
|
FieldType::HierarchicalFacet => true,
|
||||||
FieldType::Bytes => false,
|
FieldType::Bytes => false,
|
||||||
@@ -125,12 +125,9 @@ impl FieldType {
|
|||||||
match *json {
|
match *json {
|
||||||
JsonValue::String(ref field_text) => match *self {
|
JsonValue::String(ref field_text) => match *self {
|
||||||
FieldType::Str(_) => Ok(Value::Str(field_text.clone())),
|
FieldType::Str(_) => Ok(Value::Str(field_text.clone())),
|
||||||
FieldType::U64(_) | FieldType::I64(_) | FieldType::F64(_) | FieldType::Date(_) => {
|
FieldType::U64(_) | FieldType::I64(_) | FieldType::F64(_) | FieldType::Date(_) => Err(
|
||||||
Err(ValueParsingError::TypeError(format!(
|
ValueParsingError::TypeError(format!("Expected an integer, got {:?}", json)),
|
||||||
"Expected an integer, got {:?}",
|
),
|
||||||
json
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
FieldType::HierarchicalFacet => Ok(Value::Facet(Facet::from(field_text))),
|
FieldType::HierarchicalFacet => Ok(Value::Facet(Facet::from(field_text))),
|
||||||
FieldType::Bytes => decode(field_text).map(Value::Bytes).map_err(|_| {
|
FieldType::Bytes => decode(field_text).map(Value::Bytes).map_err(|_| {
|
||||||
ValueParsingError::InvalidBase64(format!(
|
ValueParsingError::InvalidBase64(format!(
|
||||||
@@ -155,7 +152,7 @@ impl FieldType {
|
|||||||
let msg = format!("Expected a u64 int, got {:?}", json);
|
let msg = format!("Expected a u64 int, got {:?}", json);
|
||||||
Err(ValueParsingError::OverflowError(msg))
|
Err(ValueParsingError::OverflowError(msg))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
FieldType::F64(_) => {
|
FieldType::F64(_) => {
|
||||||
if let Some(field_val_f64) = field_val_num.as_f64() {
|
if let Some(field_val_f64) = field_val_num.as_f64() {
|
||||||
Ok(Value::F64(field_val_f64))
|
Ok(Value::F64(field_val_f64))
|
||||||
@@ -169,28 +166,6 @@ impl FieldType {
|
|||||||
Err(ValueParsingError::TypeError(msg))
|
Err(ValueParsingError::TypeError(msg))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
JsonValue::Object(_) => match *self {
|
|
||||||
FieldType::Str(_) => {
|
|
||||||
if let Ok(tok_str_val) =
|
|
||||||
serde_json::from_value::<PreTokenizedString>(json.clone())
|
|
||||||
{
|
|
||||||
Ok(Value::PreTokStr(tok_str_val))
|
|
||||||
} else {
|
|
||||||
let msg = format!(
|
|
||||||
"Json value {:?} cannot be translated to PreTokenizedString.",
|
|
||||||
json
|
|
||||||
);
|
|
||||||
Err(ValueParsingError::TypeError(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let msg = format!(
|
|
||||||
"Json value not supported error {:?}. Expected {:?}",
|
|
||||||
json, self
|
|
||||||
);
|
|
||||||
Err(ValueParsingError::TypeError(msg))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
_ => {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"Json value not supported error {:?}. Expected {:?}",
|
"Json value not supported error {:?}. Expected {:?}",
|
||||||
@@ -206,9 +181,7 @@ impl FieldType {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::FieldType;
|
use super::FieldType;
|
||||||
use crate::schema::field_type::ValueParsingError;
|
use crate::schema::field_type::ValueParsingError;
|
||||||
use crate::schema::TextOptions;
|
|
||||||
use crate::schema::Value;
|
use crate::schema::Value;
|
||||||
use crate::tokenizer::{PreTokenizedString, Token};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bytes_value_from_json() {
|
fn test_bytes_value_from_json() {
|
||||||
@@ -229,71 +202,4 @@ mod tests {
|
|||||||
_ => panic!("Expected parse failure for invalid base64"),
|
_ => panic!("Expected parse failure for invalid base64"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pre_tok_str_value_from_json() {
|
|
||||||
let pre_tokenized_string_json = r#"{
|
|
||||||
"text": "The Old Man",
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"offset_from": 0,
|
|
||||||
"offset_to": 3,
|
|
||||||
"position": 0,
|
|
||||||
"text": "The",
|
|
||||||
"position_length": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"offset_from": 4,
|
|
||||||
"offset_to": 7,
|
|
||||||
"position": 1,
|
|
||||||
"text": "Old",
|
|
||||||
"position_length": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"offset_from": 8,
|
|
||||||
"offset_to": 11,
|
|
||||||
"position": 2,
|
|
||||||
"text": "Man",
|
|
||||||
"position_length": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let expected_value = Value::PreTokStr(PreTokenizedString {
|
|
||||||
text: String::from("The Old Man"),
|
|
||||||
tokens: vec![
|
|
||||||
Token {
|
|
||||||
offset_from: 0,
|
|
||||||
offset_to: 3,
|
|
||||||
position: 0,
|
|
||||||
text: String::from("The"),
|
|
||||||
position_length: 1,
|
|
||||||
},
|
|
||||||
Token {
|
|
||||||
offset_from: 4,
|
|
||||||
offset_to: 7,
|
|
||||||
position: 1,
|
|
||||||
text: String::from("Old"),
|
|
||||||
position_length: 1,
|
|
||||||
},
|
|
||||||
Token {
|
|
||||||
offset_from: 8,
|
|
||||||
offset_to: 11,
|
|
||||||
position: 2,
|
|
||||||
text: String::from("Man"),
|
|
||||||
position_length: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
let deserialized_value = FieldType::Str(TextOptions::default())
|
|
||||||
.value_from_json(&serde_json::from_str(pre_tokenized_string_json).unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(deserialized_value, expected_value);
|
|
||||||
|
|
||||||
let serialized_value_json = serde_json::to_string_pretty(&expected_value).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(serialized_value_json, pre_tokenized_string_json);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,22 @@ pub enum IndexRecordOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IndexRecordOption {
|
impl IndexRecordOption {
|
||||||
|
/// Returns true iff the term frequency will be encoded.
|
||||||
|
pub fn is_termfreq_enabled(self) -> bool {
|
||||||
|
match self {
|
||||||
|
IndexRecordOption::WithFreqsAndPositions | IndexRecordOption::WithFreqs => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true iff the term positions within the document are stored as well.
|
||||||
|
pub fn is_position_enabled(self) -> bool {
|
||||||
|
match self {
|
||||||
|
IndexRecordOption::WithFreqsAndPositions => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true iff this option includes encoding
|
/// Returns true iff this option includes encoding
|
||||||
/// term frequencies.
|
/// term frequencies.
|
||||||
pub fn has_freq(self) -> bool {
|
pub fn has_freq(self) -> bool {
|
||||||
|
|||||||
@@ -174,4 +174,5 @@ mod tests {
|
|||||||
assert!(!is_valid_field_name("シャボン玉"));
|
assert!(!is_valid_field_name("シャボン玉"));
|
||||||
assert!(is_valid_field_name("my_text_field"));
|
assert!(is_valid_field_name("my_text_field"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ impl SchemaBuilder {
|
|||||||
|
|
||||||
/// Adds a field entry to the schema in build.
|
/// Adds a field entry to the schema in build.
|
||||||
fn add_field(&mut self, field_entry: FieldEntry) -> Field {
|
fn add_field(&mut self, field_entry: FieldEntry) -> Field {
|
||||||
let field = Field::from_field_id(self.fields.len() as u32);
|
let field = Field(self.fields.len() as u32);
|
||||||
let field_name = field_entry.name().to_string();
|
let field_name = field_entry.name().to_string();
|
||||||
self.fields.push(field_entry);
|
self.fields.push(field_entry);
|
||||||
self.fields_map.insert(field_name, field);
|
self.fields_map.insert(field_name, field);
|
||||||
@@ -223,7 +223,7 @@ pub struct Schema(Arc<InnerSchema>);
|
|||||||
impl Schema {
|
impl Schema {
|
||||||
/// Return the `FieldEntry` associated to a `Field`.
|
/// Return the `FieldEntry` associated to a `Field`.
|
||||||
pub fn get_field_entry(&self, field: Field) -> &FieldEntry {
|
pub fn get_field_entry(&self, field: Field) -> &FieldEntry {
|
||||||
&self.0.fields[field.field_id() as usize]
|
&self.0.fields[field.0 as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the field name for a given `Field`.
|
/// Return the field name for a given `Field`.
|
||||||
@@ -232,12 +232,8 @@ impl Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the list of all the `Field`s.
|
/// Return the list of all the `Field`s.
|
||||||
pub fn fields(&self) -> impl Iterator<Item = (Field, &FieldEntry)> {
|
pub fn fields(&self) -> &[FieldEntry] {
|
||||||
self.0
|
&self.0.fields
|
||||||
.fields
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(field_id, field_entry)| (Field::from_field_id(field_id as u32), field_entry))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new builder.
|
/// Creates a new builder.
|
||||||
@@ -250,25 +246,6 @@ impl Schema {
|
|||||||
self.0.fields_map.get(field_name).cloned()
|
self.0.fields_map.get(field_name).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a named document off the doc.
|
|
||||||
pub fn convert_named_doc(
|
|
||||||
&self,
|
|
||||||
named_doc: NamedFieldDocument,
|
|
||||||
) -> Result<Document, DocParsingError> {
|
|
||||||
let mut document = Document::new();
|
|
||||||
for (field_name, values) in named_doc.0 {
|
|
||||||
if let Some(field) = self.get_field(&field_name) {
|
|
||||||
for value in values {
|
|
||||||
let field_value = FieldValue::new(field, value);
|
|
||||||
document.add(field_value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(DocParsingError::NoSuchFieldInSchema(field_name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(document)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a named document off the doc.
|
/// Create a named document off the doc.
|
||||||
pub fn to_named_doc(&self, doc: &Document) -> NamedFieldDocument {
|
pub fn to_named_doc(&self, doc: &Document) -> NamedFieldDocument {
|
||||||
let mut field_map = BTreeMap::new();
|
let mut field_map = BTreeMap::new();
|
||||||
@@ -284,6 +261,24 @@ impl Schema {
|
|||||||
NamedFieldDocument(field_map)
|
NamedFieldDocument(field_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a named doc into a document.
|
||||||
|
pub fn from_named_doc(
|
||||||
|
&self,
|
||||||
|
named_doc: NamedFieldDocument,
|
||||||
|
) -> Result<Document, DocParsingError> {
|
||||||
|
let mut doc = Document::default();
|
||||||
|
for (field_name, field_values) in named_doc.0 {
|
||||||
|
if let Some(field) = self.get_field(&field_name) {
|
||||||
|
for field_value in field_values {
|
||||||
|
doc.add(FieldValue::new(field, field_value));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(DocParsingError::NoSuchFieldInSchema(field_name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(doc)
|
||||||
|
}
|
||||||
|
|
||||||
/// Encode the schema in JSON.
|
/// Encode the schema in JSON.
|
||||||
///
|
///
|
||||||
/// Encoding a document cannot fail.
|
/// Encoding a document cannot fail.
|
||||||
@@ -302,29 +297,30 @@ impl Schema {
|
|||||||
};
|
};
|
||||||
DocParsingError::NotJSON(doc_json_sample)
|
DocParsingError::NotJSON(doc_json_sample)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut doc = Document::default();
|
let mut doc = Document::default();
|
||||||
for (field_name, json_value) in json_obj.iter() {
|
for (field_name, json_value) in json_obj.iter() {
|
||||||
let field = self
|
match self.get_field(field_name) {
|
||||||
.get_field(field_name)
|
Some(field) => {
|
||||||
.ok_or_else(|| DocParsingError::NoSuchFieldInSchema(field_name.clone()))?;
|
let field_entry = self.get_field_entry(field);
|
||||||
let field_entry = self.get_field_entry(field);
|
let field_type = field_entry.field_type();
|
||||||
let field_type = field_entry.field_type();
|
match *json_value {
|
||||||
match *json_value {
|
JsonValue::Array(ref json_items) => {
|
||||||
JsonValue::Array(ref json_items) => {
|
for json_item in json_items {
|
||||||
for json_item in json_items {
|
let value = field_type.value_from_json(json_item).map_err(|e| {
|
||||||
let value = field_type
|
DocParsingError::ValueError(field_name.clone(), e)
|
||||||
.value_from_json(json_item)
|
})?;
|
||||||
.map_err(|e| DocParsingError::ValueError(field_name.clone(), e))?;
|
doc.add(FieldValue::new(field, value));
|
||||||
doc.add(FieldValue::new(field, value));
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let value = field_type
|
||||||
|
.value_from_json(json_value)
|
||||||
|
.map_err(|e| DocParsingError::ValueError(field_name.clone(), e))?;
|
||||||
|
doc.add(FieldValue::new(field, value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
None => return Err(DocParsingError::NoSuchFieldInSchema(field_name.clone())),
|
||||||
let value = field_type
|
|
||||||
.value_from_json(json_value)
|
|
||||||
.map_err(|e| DocParsingError::ValueError(field_name.clone(), e))?;
|
|
||||||
doc.add(FieldValue::new(field, value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(doc)
|
Ok(doc)
|
||||||
@@ -381,7 +377,7 @@ impl<'de> Deserialize<'de> for Schema {
|
|||||||
|
|
||||||
/// Error that may happen when deserializing
|
/// Error that may happen when deserializing
|
||||||
/// a document from JSON.
|
/// a document from JSON.
|
||||||
#[derive(Debug, Fail, PartialEq)]
|
#[derive(Debug, Fail)]
|
||||||
pub enum DocParsingError {
|
pub enum DocParsingError {
|
||||||
/// The payload given is not valid JSON.
|
/// The payload given is not valid JSON.
|
||||||
#[fail(display = "The provided string is not valid JSON")]
|
#[fail(display = "The provided string is not valid JSON")]
|
||||||
@@ -390,10 +386,7 @@ pub enum DocParsingError {
|
|||||||
#[fail(display = "The field '{:?}' could not be parsed: {:?}", _0, _1)]
|
#[fail(display = "The field '{:?}' could not be parsed: {:?}", _0, _1)]
|
||||||
ValueError(String, ValueParsingError),
|
ValueError(String, ValueParsingError),
|
||||||
/// The json-document contains a field that is not declared in the schema.
|
/// The json-document contains a field that is not declared in the schema.
|
||||||
#[fail(
|
#[fail(display = "The json-document contains an unknown field: {:?}", _0)]
|
||||||
display = "The document contains a field that is not declared in the schema: {:?}",
|
|
||||||
_0
|
|
||||||
)]
|
|
||||||
NoSuchFieldInSchema(String),
|
NoSuchFieldInSchema(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +398,6 @@ mod tests {
|
|||||||
use crate::schema::*;
|
use crate::schema::*;
|
||||||
use matches::{assert_matches, matches};
|
use matches::{assert_matches, matches};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn is_indexed_test() {
|
pub fn is_indexed_test() {
|
||||||
@@ -489,32 +481,13 @@ mod tests {
|
|||||||
|
|
||||||
let schema: Schema = serde_json::from_str(expected).unwrap();
|
let schema: Schema = serde_json::from_str(expected).unwrap();
|
||||||
|
|
||||||
let mut fields = schema.fields();
|
let mut fields = schema.fields().iter();
|
||||||
{
|
|
||||||
let (field, field_entry) = fields.next().unwrap();
|
assert_eq!("title", fields.next().unwrap().name());
|
||||||
assert_eq!("title", field_entry.name());
|
assert_eq!("author", fields.next().unwrap().name());
|
||||||
assert_eq!(0, field.field_id());
|
assert_eq!("count", fields.next().unwrap().name());
|
||||||
}
|
assert_eq!("popularity", fields.next().unwrap().name());
|
||||||
{
|
assert_eq!("score", fields.next().unwrap().name());
|
||||||
let (field, field_entry) = fields.next().unwrap();
|
|
||||||
assert_eq!("author", field_entry.name());
|
|
||||||
assert_eq!(1, field.field_id());
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let (field, field_entry) = fields.next().unwrap();
|
|
||||||
assert_eq!("count", field_entry.name());
|
|
||||||
assert_eq!(2, field.field_id());
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let (field, field_entry) = fields.next().unwrap();
|
|
||||||
assert_eq!("popularity", field_entry.name());
|
|
||||||
assert_eq!(3, field.field_id());
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let (field, field_entry) = fields.next().unwrap();
|
|
||||||
assert_eq!("score", field_entry.name());
|
|
||||||
assert_eq!(4, field.field_id());
|
|
||||||
}
|
|
||||||
assert!(fields.next().is_none());
|
assert!(fields.next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,54 +512,6 @@ mod tests {
|
|||||||
assert_eq!(doc, doc_serdeser);
|
assert_eq!(doc, doc_serdeser);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_document_from_nameddoc() {
|
|
||||||
let mut schema_builder = Schema::builder();
|
|
||||||
let title = schema_builder.add_text_field("title", TEXT);
|
|
||||||
let val = schema_builder.add_i64_field("val", INDEXED);
|
|
||||||
let schema = schema_builder.build();
|
|
||||||
let mut named_doc_map = BTreeMap::default();
|
|
||||||
named_doc_map.insert(
|
|
||||||
"title".to_string(),
|
|
||||||
vec![Value::from("title1"), Value::from("title2")],
|
|
||||||
);
|
|
||||||
named_doc_map.insert(
|
|
||||||
"val".to_string(),
|
|
||||||
vec![Value::from(14u64), Value::from(-1i64)],
|
|
||||||
);
|
|
||||||
let doc = schema
|
|
||||||
.convert_named_doc(NamedFieldDocument(named_doc_map))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
doc.get_all(title),
|
|
||||||
vec![
|
|
||||||
&Value::from("title1".to_string()),
|
|
||||||
&Value::from("title2".to_string())
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
doc.get_all(val),
|
|
||||||
vec![&Value::from(14u64), &Value::from(-1i64)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_document_from_nameddoc_error() {
|
|
||||||
let schema = Schema::builder().build();
|
|
||||||
let mut named_doc_map = BTreeMap::default();
|
|
||||||
named_doc_map.insert(
|
|
||||||
"title".to_string(),
|
|
||||||
vec![Value::from("title1"), Value::from("title2")],
|
|
||||||
);
|
|
||||||
let err = schema
|
|
||||||
.convert_named_doc(NamedFieldDocument(named_doc_map))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
err,
|
|
||||||
DocParsingError::NoSuchFieldInSchema("title".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_parse_document() {
|
pub fn test_parse_document() {
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ impl Term {
|
|||||||
/// Builds a term given a field, and a i64-value
|
/// Builds a term given a field, and a i64-value
|
||||||
///
|
///
|
||||||
/// Assuming the term has a field id of 1, and a i64 value of 3234,
|
/// Assuming the term has a field id of 1, and a i64 value of 3234,
|
||||||
/// the Term will have 12 bytes.
|
/// the Term will have 8 bytes.
|
||||||
///
|
///
|
||||||
/// The first four byte are dedicated to storing the field id as a u64.
|
/// The first four byte are dedicated to storing the field id as a u64.
|
||||||
/// The 8 following bytes are encoding the u64 value.
|
/// The 4 following bytes are encoding the u64 value.
|
||||||
pub fn from_field_i64(field: Field, val: i64) -> Term {
|
pub fn from_field_i64(field: Field, val: i64) -> Term {
|
||||||
let val_u64: u64 = common::i64_to_u64(val);
|
let val_u64: u64 = common::i64_to_u64(val);
|
||||||
Term::from_field_u64(field, val_u64)
|
Term::from_field_u64(field, val_u64)
|
||||||
@@ -33,11 +33,11 @@ impl Term {
|
|||||||
|
|
||||||
/// Builds a term given a field, and a f64-value
|
/// Builds a term given a field, and a f64-value
|
||||||
///
|
///
|
||||||
/// Assuming the term has a field id of 1, and a f64 value of 1.5,
|
/// Assuming the term has a field id of 1, and a u64 value of 3234,
|
||||||
/// the Term will have 12 bytes.
|
/// the Term will have 8 bytes. <= this is wrong
|
||||||
///
|
///
|
||||||
/// The first four byte are dedicated to storing the field id as a u64.
|
/// The first four byte are dedicated to storing the field id as a u64.
|
||||||
/// The 8 following bytes are encoding the f64 as a u64 value.
|
/// The 4 following bytes are encoding the u64 value.
|
||||||
pub fn from_field_f64(field: Field, val: f64) -> Term {
|
pub fn from_field_f64(field: Field, val: f64) -> Term {
|
||||||
let val_u64: u64 = common::f64_to_u64(val);
|
let val_u64: u64 = common::f64_to_u64(val);
|
||||||
Term::from_field_u64(field, val_u64)
|
Term::from_field_u64(field, val_u64)
|
||||||
@@ -46,10 +46,10 @@ impl Term {
|
|||||||
/// Builds a term given a field, and a DateTime value
|
/// Builds a term given a field, and a DateTime value
|
||||||
///
|
///
|
||||||
/// Assuming the term has a field id of 1, and a timestamp i64 value of 3234,
|
/// Assuming the term has a field id of 1, and a timestamp i64 value of 3234,
|
||||||
/// the Term will have 12 bytes.
|
/// the Term will have 8 bytes.
|
||||||
///
|
///
|
||||||
/// The first four byte are dedicated to storing the field id as a u64.
|
/// The first four byte are dedicated to storing the field id as a u64.
|
||||||
/// The 8 following bytes are encoding the DateTime as i64 timestamp value.
|
/// The 4 following bytes are encoding the DateTime as i64 timestamp value.
|
||||||
pub fn from_field_date(field: Field, val: &DateTime) -> Term {
|
pub fn from_field_date(field: Field, val: &DateTime) -> Term {
|
||||||
let val_timestamp = val.timestamp();
|
let val_timestamp = val.timestamp();
|
||||||
Term::from_field_i64(field, val_timestamp)
|
Term::from_field_i64(field, val_timestamp)
|
||||||
@@ -82,10 +82,10 @@ impl Term {
|
|||||||
/// Builds a term given a field, and a u64-value
|
/// Builds a term given a field, and a u64-value
|
||||||
///
|
///
|
||||||
/// Assuming the term has a field id of 1, and a u64 value of 3234,
|
/// Assuming the term has a field id of 1, and a u64 value of 3234,
|
||||||
/// the Term will have 12 bytes.
|
/// the Term will have 8 bytes.
|
||||||
///
|
///
|
||||||
/// The first four byte are dedicated to storing the field id as a u64.
|
/// The first four byte are dedicated to storing the field id as a u64.
|
||||||
/// The 8 following bytes are encoding the u64 value.
|
/// The 4 following bytes are encoding the u64 value.
|
||||||
pub fn from_field_u64(field: Field, val: u64) -> Term {
|
pub fn from_field_u64(field: Field, val: u64) -> Term {
|
||||||
let mut term = Term(vec![0u8; INT_TERM_LEN]);
|
let mut term = Term(vec![0u8; INT_TERM_LEN]);
|
||||||
term.set_field(field);
|
term.set_field(field);
|
||||||
@@ -105,7 +105,7 @@ impl Term {
|
|||||||
if self.0.len() < 4 {
|
if self.0.len() < 4 {
|
||||||
self.0.resize(4, 0u8);
|
self.0.resize(4, 0u8);
|
||||||
}
|
}
|
||||||
BigEndian::write_u32(&mut self.0[0..4], field.field_id());
|
BigEndian::write_u32(&mut self.0[0..4], field.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a u64 value in the term.
|
/// Sets a u64 value in the term.
|
||||||
@@ -157,7 +157,7 @@ where
|
|||||||
|
|
||||||
/// Returns the field.
|
/// Returns the field.
|
||||||
pub fn field(&self) -> Field {
|
pub fn field(&self) -> Field {
|
||||||
Field::from_field_id(BigEndian::read_u32(&self.0.as_ref()[..4]))
|
Field(BigEndian::read_u32(&self.0.as_ref()[..4]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `u64` value stored in a term.
|
/// Returns the `u64` value stored in a term.
|
||||||
@@ -182,7 +182,7 @@ where
|
|||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// ... or returns an invalid value
|
/// ... or returns an invalid value
|
||||||
/// if the term is not a `f64` field.
|
/// if the term is not a `i64` field.
|
||||||
pub fn get_f64(&self) -> f64 {
|
pub fn get_f64(&self) -> f64 {
|
||||||
common::u64_to_f64(BigEndian::read_u64(&self.0.as_ref()[4..]))
|
common::u64_to_f64(BigEndian::read_u64(&self.0.as_ref()[4..]))
|
||||||
}
|
}
|
||||||
@@ -224,12 +224,7 @@ where
|
|||||||
|
|
||||||
impl fmt::Debug for Term {
|
impl fmt::Debug for Term {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(f, "Term({:?})", &self.0[..])
|
||||||
f,
|
|
||||||
"Term(field={},bytes={:?})",
|
|
||||||
self.field().field_id(),
|
|
||||||
self.value_bytes()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user