diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a02ab59..b68dbdc61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v0.10.2 + - Add support for faceted search (#631) - Add support for configuring the lmdb map size (#646, #647) - Add exposed port for Dockerfile (#654) - Add sentry probe diff --git a/Cargo.lock b/Cargo.lock index 8be56eb68..4b718210a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,15 +138,16 @@ dependencies = [ [[package]] name = "actix-rt" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20066d9200ef8d441ac156c76dd36c3f1e9a15976c34e69ae97f7f570b331882" +checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227" dependencies = [ "actix-macros", "actix-threadpool", "copyless", "futures-channel", "futures-util", + "smallvec", "tokio", ] @@ -309,12 +310,9 @@ checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" [[package]] name = "ahash" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b909d1c126f78ace756fc337133356c499eebeefcce930fa5fb018823f2b2d" -dependencies = [ - "const-random", -] +checksum = "9c251dce3391a07b43218ca070203ecb8f9f520d35ab71312296a59dbceab154" [[package]] name = "aho-corasick" @@ -436,9 +434,9 @@ dependencies = [ [[package]] name = "backtrace-sys" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78848718ee1255a2485d1309ad9cdecfc2e7d0362dd11c6829364c6b35ae1bc7" +checksum = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399" dependencies = [ "cc", "libc", @@ -452,9 +450,9 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "base64" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5ca2cd0adc3f48f9e9ea5a6bbdf9ccc0bfade884847e484d452414c7ccffb3" +checksum = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" [[package]] name = "bincode" @@ -646,26 +644,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ab08c5bed92075075d5db5149887a477b2dc0318c40882a0dfbd34315ac6141" -[[package]] -name = "const-random" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" -dependencies = [ - "const-random-macro", - "proc-macro-hack", -] - -[[package]] -name = "const-random-macro" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" -dependencies = [ - "getrandom", - "proc-macro-hack", -] - [[package]] name = "copyless" version = "0.1.4" @@ -688,6 +666,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +[[package]] +name = "cow-utils" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79bb3adfaf5f75d24b01aee375f7555907840fa2800e5ec8fa3b9e2031830173" + [[package]] name = "crc32fast" version = "1.2.0" @@ -699,16 +683,16 @@ dependencies = [ [[package]] name = "criterion" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc755679c12bda8e5523a71e4d654b6bf2e14bd838dfc48cde6559a05caf7d1" +checksum = "63f696897c88b57f4ffe3c69d8e1a0613c7d0e6c4833363c8560fbde9c47b966" dependencies = [ "atty", "cast", "clap", "criterion-plot", "csv", - "itertools 0.8.2", + "itertools", "lazy_static", "num-traits", "oorandom", @@ -724,12 +708,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01e15e0ea58e8234f96146b1f91fa9d0e4dd7a38da93ff7a75d42c0b9d3a545" +checksum = "ddeaf7989f00f2e1d871a26a110f3ed713632feac17f65f03ca938c542618b60" dependencies = [ "cast", - "itertools 0.8.2", + "itertools", ] [[package]] @@ -926,9 +910,9 @@ dependencies = [ [[package]] name = "failure" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8529c2421efa3066a5cbd8063d2244603824daccb6936b079010bb2aa89464b" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" dependencies = [ "backtrace", "failure_derive", @@ -936,9 +920,9 @@ dependencies = [ [[package]] name = "failure_derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote", @@ -1153,9 +1137,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42" +checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" dependencies = [ "bytes 0.5.4", "fnv", @@ -1172,9 +1156,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479e9d9a1a3f8c489868a935b557ab5710e3e223836da2ecd52901d88935cb56" +checksum = "96282e96bfcd3da0d3aa9938bedf1e50df3269b6db08b4876d2da0bb1a0841cf" dependencies = [ "ahash", "autocfg", @@ -1432,9 +1416,9 @@ dependencies = [ [[package]] name = "ipconfig" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" dependencies = [ "socket2", "widestring", @@ -1442,15 +1426,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "itertools" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.9.0" @@ -1489,9 +1464,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055" +checksum = "fa5a448de267e7358beaf4a5d849518fe9a0c13fce7afd44b06e68550e5562a7" dependencies = [ "wasm-bindgen", ] @@ -1535,9 +1510,9 @@ checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" [[package]] name = "linked-hash-map" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "lmdb-rkv-sys" @@ -1617,17 +1592,19 @@ dependencies = [ "byteorder", "chrono", "compact_arena", + "cow-utils", "criterion", "crossbeam-channel", "csv", "deunicode", + "either", "env_logger", "fst", "hashbrown", "heed", "indexmap", "intervaltree", - "itertools 0.9.0", + "itertools", "jemallocator", "levenshtein_automata", "log", @@ -1708,6 +1685,7 @@ dependencies = [ "serde", "serde_json", "toml", + "zerocopy", ] [[package]] @@ -1768,9 +1746,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.6.21" +version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" +checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" dependencies = [ "cfg-if", "fuchsia-zircon", @@ -1787,9 +1765,9 @@ dependencies = [ [[package]] name = "mio-uds" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" dependencies = [ "iovec", "libc", @@ -1828,9 +1806,9 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" dependencies = [ "cfg-if", "libc", @@ -1906,9 +1884,9 @@ checksum = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b" [[package]] name = "oorandom" -version = "11.1.0" +version = "11.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405" +checksum = "94af325bc33c7f60191be4e2c984d48aaa21e2854f473b85398344b60c9b6358" [[package]] name = "opaque-debug" @@ -2058,18 +2036,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6a7f5eee6292c559c793430c55c00aea9d3b3d1905e855806ca4d7253426a2" +checksum = "82c3bfbfb5bb42f99498c7234bbd768c220eb0cea6818259d0d18a1aa3d2595d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8988430ce790d8682672117bc06dda364c0be32d3abd738234f19f3240bad99a" +checksum = "ccbf6449dcfb18562c015526b085b8df1aa3cdab180af8ec2ebd300a3bd28f63" dependencies = [ "proc-macro2", "quote", @@ -2078,9 +2056,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" +checksum = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" [[package]] name = "pin-utils" @@ -2096,9 +2074,9 @@ checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" [[package]] name = "plotters" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3bb8da247d27ae212529352020f3e5ee16e83c0c258061d27b08ab92675eeb" +checksum = "f9b1d9ca091d370ea3a78d5619145d1b59426ab0c9eedbad2514a4cee08bf389" dependencies = [ "js-sys", "num-traits", @@ -2162,9 +2140,9 @@ checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" [[package]] name = "proc-macro2" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" +checksum = "8872cf6f48eee44265156c111456a700ab3483686b3f96df4cf5481c89157319" dependencies = [ "unicode-xid", ] @@ -2186,9 +2164,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" +checksum = "4c1f4b0efa5fc5e8ceb705136bfee52cfdb6a4e3509f770b478cd6ed434232a7" dependencies = [ "proc-macro2", ] @@ -2398,13 +2376,13 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.12" +version = "0.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c" +checksum = "703516ae74571f24b465b4a1431e81e2ad51336cb0ded733a55a1aa3eccac196" dependencies = [ "cc", - "lazy_static", "libc", + "once_cell", "spin", "untrusted", "web-sys", @@ -2615,9 +2593,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" +checksum = "a7894c8ed05b7a3a279aeb79025fdec1d3158080b75b98a08faf2806bb799edd" dependencies = [ "indexmap", "itoa", @@ -2773,9 +2751,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410a7488c0a728c7ceb4ad59b9567eb4053d02e8cc7f5c0e0eeeb39518369213" +checksum = "e8e5aa70697bb26ee62214ae3288465ecec0000f05182f039b477001f08f5ae7" dependencies = [ "proc-macro2", "quote", @@ -2902,9 +2880,9 @@ dependencies = [ [[package]] name = "tokio" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d9c43f1bb96970e153bcbae39a65e249ccb942bd9d36dbdf086024920417c9c" +checksum = "05c1d570eb1a36f0345a5ce9c6c6e665b70b73d11236912c0b477616aeec47b1" dependencies = [ "bytes 0.5.4", "fnv", @@ -3113,9 +3091,9 @@ checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" [[package]] name = "untrusted" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" @@ -3123,7 +3101,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd754afd5f60388b4188210c3795392c5f2fd69a1cc947ec4505dbfee955b902" dependencies = [ - "base64 0.12.0", + "base64 0.12.1", "chunked_transfer", "lazy_static", "qstring", @@ -3200,9 +3178,9 @@ checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" [[package]] name = "vec_map" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "vergen" @@ -3261,9 +3239,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" -version = "0.2.60" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f" +checksum = "e3c7d40d09cdbf0f4895ae58cf57d92e1e57a9dd8ed2e8390514b54a47cc5551" dependencies = [ "cfg-if", "serde", @@ -3273,9 +3251,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.60" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd" +checksum = "c3972e137ebf830900db522d6c8fd74d1900dcfc733462e9a12e942b00b4ac94" dependencies = [ "bumpalo", "lazy_static", @@ -3288,9 +3266,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7add542ea1ac7fdaa9dc25e031a6af33b7d63376292bd24140c637d00d1c312a" +checksum = "8a369c5e1dfb7569e14d62af4da642a3cbc2f9a3652fe586e26ac22222aa4b04" dependencies = [ "cfg-if", "js-sys", @@ -3300,9 +3278,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.60" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4" +checksum = "2cd85aa2c579e8892442954685f0d801f9129de24fa2136b2c6a539c76b65776" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3310,9 +3288,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.60" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931" +checksum = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a" dependencies = [ "proc-macro2", "quote", @@ -3323,15 +3301,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.60" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639" +checksum = "a91c2916119c17a8e316507afaaa2dd94b47646048014bbdf6bef098c1bb58ad" [[package]] name = "web-sys" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb" +checksum = "8bc359e5dd3b46cb9687a051d50a2fdd228e4ba7cf6fcf861a5365c3d671a642" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/meilisearch-core/Cargo.toml b/meilisearch-core/Cargo.toml index e616c8328..00e8640d0 100644 --- a/meilisearch-core/Cargo.toml +++ b/meilisearch-core/Cargo.toml @@ -11,8 +11,10 @@ bincode = "1.2.1" byteorder = "1.3.4" chrono = { version = "0.4.11", features = ["serde"] } compact_arena = "0.4.0" +cow-utils = "0.1.2" crossbeam-channel = "0.4.2" deunicode = "1.1.0" +either = "1.5.3" env_logger = "0.7.1" fst = { version = "0.3.5", default-features = false } hashbrown = { version = "0.7.1", features = ["serde"] } diff --git a/meilisearch-core/src/bucket_sort.rs b/meilisearch-core/src/bucket_sort.rs index 23e7149a2..ec45f36d0 100644 --- a/meilisearch-core/src/bucket_sort.rs +++ b/meilisearch-core/src/bucket_sort.rs @@ -11,7 +11,7 @@ use std::fmt; use compact_arena::{SmallArena, Idx32, mk_arena}; use log::debug; use meilisearch_types::DocIndex; -use sdset::{Set, SetBuf, exponential_search}; +use sdset::{Set, SetBuf, exponential_search, SetOperation}; use slice_group_by::{GroupBy, GroupByMut}; use crate::error::Error; @@ -28,6 +28,7 @@ pub fn bucket_sort<'c, FI>( reader: &heed::RoTxn, query: &str, range: Range, + facets_docids: Option>, filter: Option, criteria: Criteria<'c>, searchable_attrs: Option, @@ -50,6 +51,7 @@ where reader, query, range, + facets_docids, filter, distinct, distinct_size, @@ -94,10 +96,17 @@ where let mut queries_kinds = HashMap::new(); recurs_operation(&mut queries_kinds, &operation); - let QueryResult { docids, queries } = traverse_query_tree(reader, &context, &operation)?; + let QueryResult { mut docids, queries } = traverse_query_tree(reader, &context, &operation)?; debug!("found {} documents", docids.len()); debug!("number of postings {:?}", queries.len()); + if let Some(facets_docids) = facets_docids { + let intersection = sdset::duo::OpBuilder::new(docids.as_ref(), facets_docids.as_set()) + .intersection() + .into_set_buf(); + docids = Cow::Owned(intersection); + } + let before = Instant::now(); mk_arena!(arena); let mut bare_matches = cleanup_bare_matches(&mut arena, &docids, queries); @@ -179,6 +188,7 @@ pub fn bucket_sort_with_distinct<'c, FI, FD>( reader: &heed::RoTxn, query: &str, range: Range, + facets_docids: Option>, filter: Option, distinct: FD, distinct_size: usize, @@ -225,10 +235,17 @@ where let mut queries_kinds = HashMap::new(); recurs_operation(&mut queries_kinds, &operation); - let QueryResult { docids, queries } = traverse_query_tree(reader, &context, &operation)?; + let QueryResult { mut docids, queries } = traverse_query_tree(reader, &context, &operation)?; debug!("found {} documents", docids.len()); debug!("number of postings {:?}", queries.len()); + if let Some(facets_docids) = facets_docids { + let intersection = sdset::duo::OpBuilder::new(docids.as_ref(), facets_docids.as_set()) + .intersection() + .into_set_buf(); + docids = Cow::Owned(intersection); + } + let before = Instant::now(); mk_arena!(arena); let mut bare_matches = cleanup_bare_matches(&mut arena, &docids, queries); diff --git a/meilisearch-core/src/error.rs b/meilisearch-core/src/error.rs index 110d6ab76..7990f691f 100644 --- a/meilisearch-core/src/error.rs +++ b/meilisearch-core/src/error.rs @@ -28,7 +28,8 @@ pub enum Error { Serializer(SerializerError), Deserializer(DeserializerError), UnsupportedOperation(UnsupportedOperation), - FilterParseError(PestError) + FilterParseError(PestError), + FacetError(FacetError), } impl From for Error { @@ -57,7 +58,13 @@ impl From> for Error { s.to_string() })) } -} +} + +impl From for Error { + fn from(error: FacetError) -> Error { + Error::FacetError(error) + } +} impl From for Error { fn from(error: meilisearch_schema::Error) -> Error { @@ -127,6 +134,7 @@ impl fmt::Display for Error { Deserializer(e) => write!(f, "deserializer error; {}", e), UnsupportedOperation(op) => write!(f, "unsupported operation; {}", op), FilterParseError(e) => write!(f, "error parsing filter; {}", e), + FacetError(e) => write!(f, "error processing facet filter: {}", e), } } } @@ -156,3 +164,40 @@ impl fmt::Display for UnsupportedOperation { } } } + +#[derive(Debug)] +pub enum FacetError { + EmptyArray, + ParsingError(String), + UnexpectedToken { expected: &'static [&'static str], found: String }, + InvalidFormat(String), + AttributeNotFound(String), + AttributeNotSet { expected: Vec, found: String }, + InvalidDocumentAttribute(String), +} + +impl FacetError { + pub fn unexpected_token(expected: &'static [&'static str], found: impl ToString) -> FacetError { + FacetError::UnexpectedToken{ expected, found: found.to_string() } + } + + pub fn attribute_not_set(expected: Vec, found: impl ToString) -> FacetError { + FacetError::AttributeNotSet{ expected, found: found.to_string() } + } +} + +impl fmt::Display for FacetError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use FacetError::*; + + match self { + EmptyArray => write!(f, "empty array in facet filter is unspecified behavior"), + ParsingError(msg) => write!(f, "parsing error: {}", msg), + UnexpectedToken { expected, found } => write!(f, "unexpected token {}, expected {}", found, expected.join("or")), + InvalidFormat(found) => write!(f, "invalid facet: {}, facets should be \"facetName:facetValue\"", found), + AttributeNotFound(attr) => write!(f, "unknown {:?} attribute", attr), + AttributeNotSet { found, expected } => write!(f, "`{}` is not set as a faceted attribute. available facet attributes: {}", found, expected.join(", ")), + InvalidDocumentAttribute(attr) => write!(f, "invalid document attribute {}, accepted types: String and [String]", attr), + } + } +} diff --git a/meilisearch-core/src/facets.rs b/meilisearch-core/src/facets.rs new file mode 100644 index 000000000..dc8654915 --- /dev/null +++ b/meilisearch-core/src/facets.rs @@ -0,0 +1,353 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::hash::Hash; +use std::ops::Deref; + +use cow_utils::CowUtils; +use either::Either; +use heed::types::{Str, OwnedType}; +use indexmap::IndexMap; +use serde_json::Value; + +use meilisearch_schema::{FieldId, Schema}; +use meilisearch_types::DocumentId; + +use crate::database::MainT; +use crate::error::{FacetError, Error}; +use crate::store::BEU16; + +/// Data structure used to represent a boolean expression in the form of nested arrays. +/// Values in the outer array are and-ed together, values in the inner arrays are or-ed together. +#[derive(Debug, PartialEq)] +pub struct FacetFilter(Vec, FacetKey>>); + +impl Deref for FacetFilter { + type Target = Vec, FacetKey>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FacetFilter { + pub fn from_str( + s: &str, + schema: &Schema, + attributes_for_faceting: &[FieldId], + ) -> Result { + + let parsed = serde_json::from_str::(s).map_err(|e| FacetError::ParsingError(e.to_string()))?; + let mut filter = Vec::new(); + match parsed { + Value::Array(and_exprs) => { + if and_exprs.is_empty() { + return Err(FacetError::EmptyArray); + } + for expr in and_exprs { + match expr { + Value::String(s) => { + let key = FacetKey::from_str( &s, schema, attributes_for_faceting)?; + filter.push(Either::Right(key)); + } + Value::Array(or_exprs) => { + if or_exprs.is_empty() { + return Err(FacetError::EmptyArray); + } + let mut inner = Vec::new(); + for expr in or_exprs { + match expr { + Value::String(s) => { + let key = FacetKey::from_str( &s, schema, attributes_for_faceting)?; + inner.push(key); + } + bad_value => return Err(FacetError::unexpected_token(&["String"], bad_value)), + } + } + filter.push(Either::Left(inner)); + } + bad_value => return Err(FacetError::unexpected_token(&["Array", "String"], bad_value)), + } + } + return Ok(Self(filter)); + } + bad_value => Err(FacetError::unexpected_token(&["Array"], bad_value)), + } + } +} + +#[derive(Debug, Eq, PartialEq, Hash)] +#[repr(C)] +pub struct FacetKey(FieldId, String); + +impl FacetKey { + pub fn new(field_id: FieldId, value: String) -> Self { + let value = match value.cow_to_lowercase() { + Cow::Borrowed(_) => value, + Cow::Owned(s) => s, + }; + Self(field_id, value) + } + + pub fn key(&self) -> FieldId { + self.0 + } + + pub fn value(&self) -> &str { + &self.1 + } + + // TODO improve parser + fn from_str( + s: &str, + schema: &Schema, + attributes_for_faceting: &[FieldId], + ) -> Result { + let mut split = s.splitn(2, ':'); + let key = split + .next() + .ok_or_else(|| FacetError::InvalidFormat(s.to_string()))? + .trim(); + let field_id = schema + .id(key) + .ok_or_else(|| FacetError::AttributeNotFound(key.to_string()))?; + + if !attributes_for_faceting.contains(&field_id) { + return Err(FacetError::attribute_not_set( + attributes_for_faceting + .iter() + .filter_map(|&id| schema.name(id)) + .map(str::to_string) + .collect::>(), + key)) + } + let value = split + .next() + .ok_or_else(|| FacetError::InvalidFormat(s.to_string()))? + .trim(); + // unquoting the string if need be: + let mut indices = value.char_indices(); + let value = match (indices.next(), indices.last()) { + (Some((s, '\'')), Some((e, '\''))) | + (Some((s, '\"')), Some((e, '\"'))) => value[s + 1..e].to_string(), + _ => value.to_string(), + }; + Ok(Self::new(field_id, value)) + } +} + +impl<'a> heed::BytesEncode<'a> for FacetKey { + type EItem = FacetKey; + + fn bytes_encode(item: &'a Self::EItem) -> Option> { + let mut buffer = Vec::with_capacity(2 + item.1.len()); + let id = BEU16::new(item.key().into()); + let id_bytes = OwnedType::bytes_encode(&id)?; + let value_bytes = Str::bytes_encode(item.value())?; + buffer.extend_from_slice(id_bytes.as_ref()); + buffer.extend_from_slice(value_bytes.as_ref()); + Some(Cow::Owned(buffer)) + } +} + +impl<'a> heed::BytesDecode<'a> for FacetKey { + type DItem = FacetKey; + + fn bytes_decode(bytes: &'a [u8]) -> Option { + let (id_bytes, value_bytes) = bytes.split_at(2); + let id = OwnedType::::bytes_decode(id_bytes)?; + let id = id.get().into(); + let string = Str::bytes_decode(&value_bytes)?; + Some(FacetKey(id, string.to_string())) + } +} + +pub fn add_to_facet_map( + facet_map: &mut HashMap>, + field_id: FieldId, + value: Value, + document_id: DocumentId, +) -> Result<(), FacetError> { + let value = match value { + Value::String(s) => s, + // ignore null + Value::Null => return Ok(()), + value => return Err(FacetError::InvalidDocumentAttribute(value.to_string())), + }; + let key = FacetKey::new(field_id, value); + facet_map.entry(key).or_insert_with(Vec::new).push(document_id); + Ok(()) +} + +pub fn facet_map_from_docids( + rtxn: &heed::RoTxn, + index: &crate::Index, + document_ids: &[DocumentId], + attributes_for_facetting: &[FieldId], +) -> Result>, Error> { + let mut facet_map = HashMap::new(); + for document_id in document_ids { + for result in index + .documents_fields + .document_fields(rtxn, *document_id)? + { + let (field_id, bytes) = result?; + if attributes_for_facetting.contains(&field_id) { + match serde_json::from_slice(bytes)? { + Value::Array(values) => { + for v in values { + add_to_facet_map(&mut facet_map, field_id, v, *document_id)?; + } + } + v => add_to_facet_map(&mut facet_map, field_id, v, *document_id)?, + }; + } + } + } + Ok(facet_map) +} + +pub fn facet_map_from_docs( + schema: &Schema, + documents: &HashMap>, + attributes_for_facetting: &[FieldId], +) -> Result>, Error> { + let mut facet_map = HashMap::new(); + let attributes_for_facetting = attributes_for_facetting + .iter() + .filter_map(|&id| schema.name(id).map(|name| (id, name))) + .collect::>(); + + for (id, document) in documents { + for (field_id, name) in &attributes_for_facetting { + if let Some(value) = document.get(*name) { + match value { + Value::Array(values) => { + for v in values { + add_to_facet_map(&mut facet_map, *field_id, v.clone(), *id)?; + } + } + v => add_to_facet_map(&mut facet_map, *field_id, v.clone(), *id)?, + } + } + } + } + Ok(facet_map) +} + +#[cfg(test)] +mod test { + use super::*; + use meilisearch_schema::Schema; + + #[test] + fn test_facet_key() { + let mut schema = Schema::new(); + let id = schema.insert_and_index("hello").unwrap(); + let facet_list = [schema.id("hello").unwrap()]; + assert_eq!( + FacetKey::from_str("hello:12", &schema, &facet_list).unwrap(), + FacetKey::new(id, "12".to_string()) + ); + assert_eq!( + FacetKey::from_str("hello:\"foo bar\"", &schema, &facet_list).unwrap(), + FacetKey::new(id, "foo bar".to_string()) + ); + assert_eq!( + FacetKey::from_str("hello:'foo bar'", &schema, &facet_list).unwrap(), + FacetKey::new(id, "foo bar".to_string()) + ); + // weird case + assert_eq!( + FacetKey::from_str("hello:blabla:machin", &schema, &facet_list).unwrap(), + FacetKey::new(id, "blabla:machin".to_string()) + ); + + assert_eq!( + FacetKey::from_str("hello:\"\"", &schema, &facet_list).unwrap(), + FacetKey::new(id, "".to_string()) + ); + + assert_eq!( + FacetKey::from_str("hello:'", &schema, &facet_list).unwrap(), + FacetKey::new(id, "'".to_string()) + ); + assert_eq!( + FacetKey::from_str("hello:''", &schema, &facet_list).unwrap(), + FacetKey::new(id, "".to_string()) + ); + assert!(FacetKey::from_str("hello", &schema, &facet_list).is_err()); + assert!(FacetKey::from_str("toto:12", &schema, &facet_list).is_err()); + } + + #[test] + fn test_parse_facet_array() { + use either::Either::{Left, Right}; + let mut schema = Schema::new(); + let _id = schema.insert_and_index("hello").unwrap(); + let facet_list = [schema.id("hello").unwrap()]; + assert_eq!( + FacetFilter::from_str("[[\"hello:12\"]]", &schema, &facet_list).unwrap(), + FacetFilter(vec![Left(vec![FacetKey(FieldId(0), "12".to_string())])]) + ); + assert_eq!( + FacetFilter::from_str("[\"hello:12\"]", &schema, &facet_list).unwrap(), + FacetFilter(vec![Right(FacetKey(FieldId(0), "12".to_string()))]) + ); + assert_eq!( + FacetFilter::from_str("[\"hello:12\", \"hello:13\"]", &schema, &facet_list).unwrap(), + FacetFilter(vec![ + Right(FacetKey(FieldId(0), "12".to_string())), + Right(FacetKey(FieldId(0), "13".to_string())) + ]) + ); + assert_eq!( + FacetFilter::from_str("[[\"hello:12\", \"hello:13\"]]", &schema, &facet_list).unwrap(), + FacetFilter(vec![Left(vec![ + FacetKey(FieldId(0), "12".to_string()), + FacetKey(FieldId(0), "13".to_string()) + ])]) + ); + assert_eq!( + FacetFilter::from_str( + "[[\"hello:12\", \"hello:13\"], \"hello:14\"]", + &schema, + &facet_list + ) + .unwrap(), + FacetFilter(vec![ + Left(vec![ + FacetKey(FieldId(0), "12".to_string()), + FacetKey(FieldId(0), "13".to_string()) + ]), + Right(FacetKey(FieldId(0), "14".to_string())) + ]) + ); + + // invalid array depths + assert!(FacetFilter::from_str( + "[[[\"hello:12\", \"hello:13\"], \"hello:14\"]]", + &schema, + &facet_list + ) + .is_err()); + assert!(FacetFilter::from_str( + "[[[\"hello:12\", \"hello:13\"]], \"hello:14\"]]", + &schema, + &facet_list + ) + .is_err()); + assert!(FacetFilter::from_str("\"hello:14\"", &schema, &facet_list).is_err()); + + // unexisting key + assert!(FacetFilter::from_str("[\"foo:12\"]", &schema, &facet_list).is_err()); + + // invalid facet key + assert!(FacetFilter::from_str("[\"foo=12\"]", &schema, &facet_list).is_err()); + assert!(FacetFilter::from_str("[\"foo12\"]", &schema, &facet_list).is_err()); + assert!(FacetFilter::from_str("[\"\"]", &schema, &facet_list).is_err()); + + // empty array error + assert!(FacetFilter::from_str("[]", &schema, &facet_list).is_err()); + assert!(FacetFilter::from_str("[\"hello:12\", []]", &schema, &facet_list).is_err()); + } +} diff --git a/meilisearch-core/src/lib.rs b/meilisearch-core/src/lib.rs index 87db276a0..179c7746c 100644 --- a/meilisearch-core/src/lib.rs +++ b/meilisearch-core/src/lib.rs @@ -19,14 +19,15 @@ mod ranked_map; mod raw_document; mod reordered_attrs; mod update; -pub mod settings; pub mod criterion; +pub mod facets; pub mod raw_indexer; +pub mod settings; pub mod serde; pub mod store; pub use self::database::{BoxUpdateFn, Database, DatabaseOptions, MainT, UpdateT}; -pub use self::error::{Error, HeedError, FstError, MResult, pest_error}; +pub use self::error::{Error, HeedError, FstError, MResult, pest_error, FacetError}; pub use self::filters::Filter; pub use self::number::{Number, ParseNumberError}; pub use self::ranked_map::RankedMap; diff --git a/meilisearch-core/src/query_builder.rs b/meilisearch-core/src/query_builder.rs index d95c19713..b3bbf21bf 100644 --- a/meilisearch-core/src/query_builder.rs +++ b/meilisearch-core/src/query_builder.rs @@ -1,66 +1,50 @@ -use std::ops::Range; +use std::borrow::Cow; +use std::ops::{Range, Deref}; use std::time::Duration; use crate::database::MainT; use crate::bucket_sort::{bucket_sort, bucket_sort_with_distinct}; use crate::{criterion::Criteria, Document, DocumentId}; use crate::{reordered_attrs::ReorderedAttrs, store, MResult}; +use crate::facets::FacetFilter; -pub struct QueryBuilder<'c, 'f, 'd> { +use either::Either; +use sdset::SetOperation; + +pub struct QueryBuilder<'c, 'f, 'd, 'fa, 'i> { criteria: Criteria<'c>, searchable_attrs: Option, filter: Option bool + 'f>>, distinct: Option<(Box Option + 'd>, usize)>, timeout: Option, - main_store: store::Main, - postings_lists_store: store::PostingsLists, - documents_fields_counts_store: store::DocumentsFieldsCounts, - synonyms_store: store::Synonyms, - prefix_documents_cache_store: store::PrefixDocumentsCache, - prefix_postings_lists_cache_store: store::PrefixPostingsListsCache, + index: &'i store::Index, + facets: Option<&'fa FacetFilter>, } -impl<'c, 'f, 'd> QueryBuilder<'c, 'f, 'd> { - pub fn new( - main: store::Main, - postings_lists: store::PostingsLists, - documents_fields_counts: store::DocumentsFieldsCounts, - synonyms: store::Synonyms, - prefix_documents_cache: store::PrefixDocumentsCache, - prefix_postings_lists_cache: store::PrefixPostingsListsCache, - ) -> QueryBuilder<'c, 'f, 'd> { +impl<'c, 'f, 'd, 'fa, 'i> QueryBuilder<'c, 'f, 'd, 'fa, 'i> { + pub fn new(index: &'i store::Index) -> Self { QueryBuilder::with_criteria( - main, - postings_lists, - documents_fields_counts, - synonyms, - prefix_documents_cache, - prefix_postings_lists_cache, + index, Criteria::default(), ) } + pub fn set_facets(&mut self, facets: Option<&'fa FacetFilter>) { + self.facets = facets; + } + pub fn with_criteria( - main: store::Main, - postings_lists: store::PostingsLists, - documents_fields_counts: store::DocumentsFieldsCounts, - synonyms: store::Synonyms, - prefix_documents_cache: store::PrefixDocumentsCache, - prefix_postings_lists_cache: store::PrefixPostingsListsCache, + index: &'i store::Index, criteria: Criteria<'c>, - ) -> QueryBuilder<'c, 'f, 'd> { + ) -> Self { QueryBuilder { criteria, searchable_attrs: None, filter: None, distinct: None, timeout: None, - main_store: main, - postings_lists_store: postings_lists, - documents_fields_counts_store: documents_fields_counts, - synonyms_store: synonyms, - prefix_documents_cache_store: prefix_documents_cache, - prefix_postings_lists_cache_store: prefix_postings_lists_cache, + index, + facets: None, } } @@ -93,36 +77,70 @@ impl<'c, 'f, 'd> QueryBuilder<'c, 'f, 'd> { query: &str, range: Range, ) -> MResult<(Vec, usize)> { + let facets_docids = match self.facets { + Some(facets) => { + let mut ands = Vec::with_capacity(facets.len()); + let mut ors = Vec::new(); + for f in facets.deref() { + match f { + Either::Left(keys) => { + ors.reserve(keys.len()); + for key in keys { + let docids = self.index.facets.facet_document_ids(reader, &key)?.unwrap_or_default(); + ors.push(docids); + } + let sets: Vec<_> = ors.iter().map(Cow::deref).collect(); + let or_result = sdset::multi::OpBuilder::from_vec(sets).union().into_set_buf(); + ands.push(Cow::Owned(or_result)); + ors.clear(); + } + Either::Right(key) =>{ + match self.index.facets.facet_document_ids(reader, &key)? { + Some(docids) => ands.push(docids), + // no candidates for search, early return. + None => return Ok((vec![], 0)), + } + } + }; + } + let ands: Vec<_> = ands.iter().map(Cow::deref).collect(); + Some(sdset::multi::OpBuilder::from_vec(ands).intersection().into_set_buf()) + } + None => None + }; + match self.distinct { Some((distinct, distinct_size)) => bucket_sort_with_distinct( reader, query, range, + facets_docids, self.filter, distinct, distinct_size, self.criteria, self.searchable_attrs, - self.main_store, - self.postings_lists_store, - self.documents_fields_counts_store, - self.synonyms_store, - self.prefix_documents_cache_store, - self.prefix_postings_lists_cache_store, + self.index.main, + self.index.postings_lists, + self.index.documents_fields_counts, + self.index.synonyms, + self.index.prefix_documents_cache, + self.index.prefix_postings_lists_cache, ), None => bucket_sort( reader, query, range, + facets_docids, self.filter, self.criteria, self.searchable_attrs, - self.main_store, - self.postings_lists_store, - self.documents_fields_counts_store, - self.synonyms_store, - self.prefix_documents_cache_store, - self.prefix_postings_lists_cache_store, + self.index.main, + self.index.postings_lists, + self.index.documents_fields_counts, + self.index.synonyms, + self.index.prefix_documents_cache, + self.index.prefix_postings_lists_cache, ), } } diff --git a/meilisearch-core/src/settings.rs b/meilisearch-core/src/settings.rs index b9ec0fa3e..05e6117f0 100644 --- a/meilisearch-core/src/settings.rs +++ b/meilisearch-core/src/settings.rs @@ -31,6 +31,8 @@ pub struct Settings { pub synonyms: Option>>>, #[serde(default, deserialize_with = "deserialize_some")] pub accept_new_fields: Option>, + #[serde(default, deserialize_with = "deserialize_some")] + pub attributes_for_faceting: Option>>, } // Any value that is present is considered Some value, including null. @@ -60,6 +62,7 @@ impl Settings { stop_words: settings.stop_words.into(), synonyms: settings.synonyms.into(), accept_new_fields: settings.accept_new_fields.into(), + attributes_for_faceting: settings.attributes_for_faceting.into(), }) } } @@ -166,6 +169,7 @@ pub struct SettingsUpdate { pub stop_words: UpdateState>, pub synonyms: UpdateState>>, pub accept_new_fields: UpdateState, + pub attributes_for_faceting: UpdateState>, } impl Default for SettingsUpdate { @@ -179,6 +183,7 @@ impl Default for SettingsUpdate { stop_words: UpdateState::Nothing, synonyms: UpdateState::Nothing, accept_new_fields: UpdateState::Nothing, + attributes_for_faceting: UpdateState::Nothing, } } } diff --git a/meilisearch-core/src/store/cow_set.rs b/meilisearch-core/src/store/cow_set.rs new file mode 100644 index 000000000..063f73198 --- /dev/null +++ b/meilisearch-core/src/store/cow_set.rs @@ -0,0 +1,32 @@ +use std::borrow::Cow; + +use heed::{types::CowSlice, BytesEncode, BytesDecode}; +use sdset::{Set, SetBuf}; +use zerocopy::{AsBytes, FromBytes}; + +pub struct CowSet(std::marker::PhantomData); + +impl<'a, T: 'a> BytesEncode<'a> for CowSet +where + T: AsBytes, +{ + type EItem = Set; + + fn bytes_encode(item: &'a Self::EItem) -> Option> { + CowSlice::bytes_encode(item.as_slice()) + } +} + +impl<'a, T: 'a> BytesDecode<'a> for CowSet +where + T: FromBytes + Copy, +{ + type DItem = Cow<'a, Set>; + + fn bytes_decode(bytes: &'a [u8]) -> Option { + match CowSlice::::bytes_decode(bytes)? { + Cow::Owned(vec) => Some(Cow::Owned(SetBuf::new_unchecked(vec))), + Cow::Borrowed(slice) => Some(Cow::Borrowed(Set::new_unchecked(slice))), + } + } +} diff --git a/meilisearch-core/src/store/facets.rs b/meilisearch-core/src/store/facets.rs new file mode 100644 index 000000000..c9811f765 --- /dev/null +++ b/meilisearch-core/src/store/facets.rs @@ -0,0 +1,53 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use heed::{RwTxn, RoTxn, Result as ZResult}; +use sdset::{SetBuf, Set, SetOperation}; + +use meilisearch_types::DocumentId; + +use crate::database::MainT; +use crate::facets::FacetKey; +use super::cow_set::CowSet; + +/// contains facet info +#[derive(Clone, Copy)] +pub struct Facets { + pub(crate) facets: heed::Database>, +} + +impl Facets { + // we use sdset::SetBuf to ensure the docids are sorted. + pub fn put_facet_document_ids(&self, writer: &mut RwTxn, facet_key: FacetKey, doc_ids: &Set) -> ZResult<()> { + self.facets.put(writer, &facet_key, doc_ids) + } + + pub fn facet_document_ids<'txn>(&self, reader: &'txn RoTxn, facet_key: &FacetKey) -> ZResult>>> { + self.facets.get(reader, &facet_key) + } + + /// updates the facets store, revmoving the documents from the facets provided in the + /// `facet_map` argument + pub fn remove(&self, writer: &mut RwTxn, facet_map: HashMap>) -> ZResult<()> { + for (key, document_ids) in facet_map { + if let Some(old) = self.facets.get(writer, &key)? { + let to_remove = SetBuf::from_dirty(document_ids); + let new = sdset::duo::OpBuilder::new(old.as_ref(), to_remove.as_set()).difference().into_set_buf(); + self.facets.put(writer, &key, new.as_set())?; + } + } + Ok(()) + } + + pub fn add(&self, writer: &mut RwTxn, facet_map: HashMap>) -> ZResult<()> { + for (key, document_ids) in facet_map { + let set = SetBuf::from_dirty(document_ids); + self.put_facet_document_ids(writer, key, set.as_set())?; + } + Ok(()) + } + + pub fn clear(self, writer: &mut heed::RwTxn) -> ZResult<()> { + self.facets.clear(writer) + } +} diff --git a/meilisearch-core/src/store/main.rs b/meilisearch-core/src/store/main.rs index a156cee4b..34a88afcd 100644 --- a/meilisearch-core/src/store/main.rs +++ b/meilisearch-core/src/store/main.rs @@ -1,16 +1,20 @@ +use std::borrow::Cow; use std::sync::Arc; use std::collections::HashMap; use chrono::{DateTime, Utc}; use heed::types::{ByteSlice, OwnedType, SerdeBincode, Str}; use heed::Result as ZResult; -use meilisearch_schema::Schema; +use meilisearch_schema::{FieldId, Schema}; +use sdset::Set; use crate::database::MainT; use crate::RankedMap; use crate::settings::RankingRule; +use super::cow_set::CowSet; const CREATED_AT_KEY: &str = "created-at"; +const ATTRIBUTES_FOR_FACETING: &str = "attributes-for-faceting"; const RANKING_RULES_KEY: &str = "ranking-rules"; const DISTINCT_ATTRIBUTE_KEY: &str = "distinct-attribute"; const STOP_WORDS_KEY: &str = "stop-words"; @@ -188,6 +192,18 @@ impl Main { } } + pub fn attributes_for_faceting<'txn>(&self, reader: &'txn heed::RoTxn) -> ZResult>>> { + self.main.get::<_, Str, CowSet>(reader, ATTRIBUTES_FOR_FACETING) + } + + pub fn put_attributes_for_faceting(self, writer: &mut heed::RwTxn, attributes: &Set) -> ZResult<()> { + self.main.put::<_, Str, CowSet>(writer, ATTRIBUTES_FOR_FACETING, attributes) + } + + pub fn delete_attributes_for_faceting(self, writer: &mut heed::RwTxn) -> ZResult { + self.main.delete::<_, Str>(writer, ATTRIBUTES_FOR_FACETING) + } + pub fn ranking_rules(&self, reader: &heed::RoTxn) -> ZResult>> { self.main.get::<_, Str, SerdeBincode>>(reader, RANKING_RULES_KEY) } diff --git a/meilisearch-core/src/store/mod.rs b/meilisearch-core/src/store/mod.rs index 48bcfcef8..c0df09809 100644 --- a/meilisearch-core/src/store/mod.rs +++ b/meilisearch-core/src/store/mod.rs @@ -1,3 +1,4 @@ +mod cow_set; mod docs_words; mod prefix_documents_cache; mod prefix_postings_lists_cache; @@ -8,8 +9,10 @@ mod postings_lists; mod synonyms; mod updates; mod updates_results; +mod facets; pub use self::docs_words::DocsWords; +pub use self::facets::Facets; pub use self::prefix_documents_cache::PrefixDocumentsCache; pub use self::prefix_postings_lists_cache::PrefixPostingsListsCache; pub use self::documents_fields::{DocumentFieldsIter, DocumentsFields}; @@ -42,7 +45,7 @@ use crate::settings::SettingsUpdate; use crate::{query_builder::QueryBuilder, update, DocIndex, DocumentId, Error, MResult}; type BEU64 = zerocopy::U64; -type BEU16 = zerocopy::U16; +pub type BEU16 = zerocopy::U16; #[derive(Debug, Copy, Clone, AsBytes, FromBytes)] #[repr(C)] @@ -197,12 +200,17 @@ fn updates_results_name(name: &str) -> String { format!("store-{}-updates-results", name) } +fn facets_name(name: &str) -> String { + format!("store-{}-facets", name) +} + #[derive(Clone)] pub struct Index { pub main: Main, pub postings_lists: PostingsLists, pub documents_fields: DocumentsFields, pub documents_fields_counts: DocumentsFieldsCounts, + pub facets: Facets, pub synonyms: Synonyms, pub docs_words: DocsWords, pub prefix_documents_cache: PrefixDocumentsCache, @@ -352,29 +360,14 @@ impl Index { } pub fn query_builder(&self) -> QueryBuilder { - QueryBuilder::new( - self.main, - self.postings_lists, - self.documents_fields_counts, - self.synonyms, - self.prefix_documents_cache, - self.prefix_postings_lists_cache, - ) + QueryBuilder::new(self) } - pub fn query_builder_with_criteria<'c, 'f, 'd>( - &self, + pub fn query_builder_with_criteria<'c, 'f, 'd, 'fa, 'i>( + &'i self, criteria: Criteria<'c>, - ) -> QueryBuilder<'c, 'f, 'd> { - QueryBuilder::with_criteria( - self.main, - self.postings_lists, - self.documents_fields_counts, - self.synonyms, - self.prefix_documents_cache, - self.prefix_postings_lists_cache, - criteria, - ) + ) -> QueryBuilder<'c, 'f, 'd, 'fa, 'i> { + QueryBuilder::with_criteria(self, criteria) } } @@ -395,12 +388,14 @@ pub fn create( let prefix_postings_lists_cache_name = prefix_postings_lists_cache_name(name); let updates_name = updates_name(name); let updates_results_name = updates_results_name(name); + let facets_name = facets_name(name); // open all the stores let main = env.create_poly_database(Some(&main_name))?; let postings_lists = env.create_database(Some(&postings_lists_name))?; let documents_fields = env.create_database(Some(&documents_fields_name))?; let documents_fields_counts = env.create_database(Some(&documents_fields_counts_name))?; + let facets = env.create_database(Some(&facets_name))?; let synonyms = env.create_database(Some(&synonyms_name))?; let docs_words = env.create_database(Some(&docs_words_name))?; let prefix_documents_cache = env.create_database(Some(&prefix_documents_cache_name))?; @@ -417,6 +412,8 @@ pub fn create( docs_words: DocsWords { docs_words }, prefix_postings_lists_cache: PrefixPostingsListsCache { prefix_postings_lists_cache }, prefix_documents_cache: PrefixDocumentsCache { prefix_documents_cache }, + facets: Facets { facets }, + updates: Updates { updates }, updates_results: UpdatesResults { updates_results }, updates_notifier, @@ -437,6 +434,7 @@ pub fn open( let synonyms_name = synonyms_name(name); let docs_words_name = docs_words_name(name); let prefix_documents_cache_name = prefix_documents_cache_name(name); + let facets_name = facets_name(name); let prefix_postings_lists_cache_name = prefix_postings_lists_cache_name(name); let updates_name = updates_name(name); let updates_results_name = updates_results_name(name); @@ -470,6 +468,10 @@ pub fn open( Some(prefix_documents_cache) => prefix_documents_cache, None => return Ok(None), }; + let facets = match env.open_database(Some(&facets_name))? { + Some(facets) => facets, + None => return Ok(None), + }; let prefix_postings_lists_cache = match env.open_database(Some(&prefix_postings_lists_cache_name))? { Some(prefix_postings_lists_cache) => prefix_postings_lists_cache, None => return Ok(None), @@ -491,6 +493,7 @@ pub fn open( synonyms: Synonyms { synonyms }, docs_words: DocsWords { docs_words }, prefix_documents_cache: PrefixDocumentsCache { prefix_documents_cache }, + facets: Facets { facets }, prefix_postings_lists_cache: PrefixPostingsListsCache { prefix_postings_lists_cache }, updates: Updates { updates }, updates_results: UpdatesResults { updates_results }, diff --git a/meilisearch-core/src/update/documents_addition.rs b/meilisearch-core/src/update/documents_addition.rs index 464e330ad..d8f1f53f1 100644 --- a/meilisearch-core/src/update/documents_addition.rs +++ b/meilisearch-core/src/update/documents_addition.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::database::{MainT, UpdateT}; use crate::database::{UpdateEvent, UpdateEventsEmitter}; +use crate::facets; use crate::raw_indexer::RawIndexer; use crate::serde::{extract_document_id, serialize_value_with_id, Deserializer, Serializer}; use crate::store; @@ -103,10 +104,11 @@ pub fn push_documents_addition( Ok(last_update_id) } -pub fn apply_documents_addition<'a, 'b>( +pub fn apply_addition<'a, 'b>( writer: &'a mut heed::RwTxn<'b, MainT>, index: &store::Index, addition: Vec>, + partial: bool ) -> MResult<()> { let mut documents_additions = HashMap::new(); @@ -118,12 +120,30 @@ pub fn apply_documents_addition<'a, 'b>( let primary_key = schema.primary_key().ok_or(Error::MissingPrimaryKey)?; // 1. store documents ids for future deletion - for document in addition { + for mut document in addition { let document_id = match extract_document_id(&primary_key, &document)? { Some(id) => id, None => return Err(Error::MissingDocumentId), }; + if partial { + let mut deserializer = Deserializer { + document_id, + reader: writer, + documents_fields: index.documents_fields, + schema: &schema, + fields: None, + }; + + // retrieve the old document and + // update the new one with missing keys found in the old one + let result = Option::>::deserialize(&mut deserializer)?; + if let Some(old_document) = result { + for (key, value) in old_document { + document.entry(key).or_insert(value); + } + } + } documents_additions.insert(document_id, document); } @@ -143,6 +163,11 @@ pub fn apply_documents_addition<'a, 'b>( }; // 3. index the documents fields in the stores + if let Some(attributes_for_facetting) = index.main.attributes_for_faceting(writer)? { + let facet_map = facets::facet_map_from_docs(&schema, &documents_additions, attributes_for_facetting.as_ref())?; + index.facets.add(writer, facet_map)?; + } + let mut indexer = RawIndexer::new(stop_words); for (document_id, document) in documents_additions { @@ -177,85 +202,15 @@ pub fn apply_documents_partial_addition<'a, 'b>( index: &store::Index, addition: Vec>, ) -> MResult<()> { - let mut documents_additions = HashMap::new(); + apply_addition(writer, index, addition, true) +} - let mut schema = match index.main.schema(writer)? { - Some(schema) => schema, - None => return Err(Error::SchemaMissing), - }; - - let primary_key = schema.primary_key().ok_or(Error::MissingPrimaryKey)?; - - // 1. store documents ids for future deletion - for mut document in addition { - let document_id = match extract_document_id(&primary_key, &document)? { - Some(id) => id, - None => return Err(Error::MissingDocumentId), - }; - - let mut deserializer = Deserializer { - document_id, - reader: writer, - documents_fields: index.documents_fields, - schema: &schema, - fields: None, - }; - - // retrieve the old document and - // update the new one with missing keys found in the old one - let result = Option::>::deserialize(&mut deserializer)?; - if let Some(old_document) = result { - for (key, value) in old_document { - document.entry(key).or_insert(value); - } - } - - documents_additions.insert(document_id, document); - } - - // 2. remove the documents posting lists - let number_of_inserted_documents = documents_additions.len(); - let documents_ids = documents_additions.iter().map(|(id, _)| *id).collect(); - apply_documents_deletion(writer, index, documents_ids)?; - - let mut ranked_map = match index.main.ranked_map(writer)? { - Some(ranked_map) => ranked_map, - None => RankedMap::default(), - }; - - let stop_words = match index.main.stop_words_fst(writer)? { - Some(stop_words) => stop_words, - None => fst::Set::default(), - }; - - // 3. index the documents fields in the stores - let mut indexer = RawIndexer::new(stop_words); - - for (document_id, document) in documents_additions { - let serializer = Serializer { - txn: writer, - schema: &mut schema, - document_store: index.documents_fields, - document_fields_counts: index.documents_fields_counts, - indexer: &mut indexer, - ranked_map: &mut ranked_map, - document_id, - }; - - document.serialize(serializer)?; - } - - write_documents_addition_index( - writer, - index, - &ranked_map, - number_of_inserted_documents, - indexer, - )?; - - index.main.put_schema(writer, &schema)?; - - Ok(()) +pub fn apply_documents_addition<'a, 'b>( + writer: &'a mut heed::RwTxn<'b, MainT>, + index: &store::Index, + addition: Vec>, +) -> MResult<()> { + apply_addition(writer, index, addition, false) } pub fn reindex_all_documents(writer: &mut heed::RwTxn, index: &store::Index) -> MResult<()> { @@ -277,6 +232,7 @@ pub fn reindex_all_documents(writer: &mut heed::RwTxn, index: &store::Ind index.main.put_words_fst(writer, &fst::Set::default())?; index.main.put_ranked_map(writer, &ranked_map)?; index.main.put_number_of_documents(writer, |_| 0)?; + index.facets.clear(writer)?; index.postings_lists.clear(writer)?; index.docs_words.clear(writer)?; @@ -289,6 +245,11 @@ pub fn reindex_all_documents(writer: &mut heed::RwTxn, index: &store::Ind let mut indexer = RawIndexer::new(stop_words); let mut ram_store = HashMap::new(); + if let Some(ref attributes_for_facetting) = index.main.attributes_for_faceting(writer)? { + let facet_map = facets::facet_map_from_docids(writer, &index, &documents_ids_to_reindex, &attributes_for_facetting)?; + index.facets.add(writer, facet_map)?; + } + // ^-- https://github.com/meilisearch/MeiliSearch/pull/631#issuecomment-626624470 --v for document_id in documents_ids_to_reindex { for result in index.documents_fields.document_fields(writer, document_id)? { let (field_id, bytes) = result?; diff --git a/meilisearch-core/src/update/documents_deletion.rs b/meilisearch-core/src/update/documents_deletion.rs index f28709ad9..30d563efb 100644 --- a/meilisearch-core/src/update/documents_deletion.rs +++ b/meilisearch-core/src/update/documents_deletion.rs @@ -6,6 +6,7 @@ use sdset::{duo::DifferenceByKey, SetBuf, SetOperation}; use crate::database::{MainT, UpdateT}; use crate::database::{UpdateEvent, UpdateEventsEmitter}; +use crate::facets; use crate::serde::extract_document_id; use crate::store; use crate::update::{next_update_id, compute_short_prefixes, Update}; @@ -88,8 +89,6 @@ pub fn apply_documents_deletion( index: &store::Index, deletion: Vec, ) -> MResult<()> { - let idset = SetBuf::from_dirty(deletion); - let schema = match index.main.schema(writer)? { Some(schema) => schema, None => return Err(Error::SchemaMissing), @@ -100,9 +99,16 @@ pub fn apply_documents_deletion( None => RankedMap::default(), }; + // facet filters deletion + if let Some(attributes_for_facetting) = index.main.attributes_for_faceting(writer)? { + let facet_map = facets::facet_map_from_docids(writer, &index, &deletion, &attributes_for_facetting)?; + index.facets.remove(writer, facet_map)?; + } + // collect the ranked attributes according to the schema let ranked_fields = schema.ranked(); + let idset = SetBuf::from_dirty(deletion); let mut words_document_ids = HashMap::new(); for id in idset { // remove all the ranked attributes from the ranked_map diff --git a/meilisearch-core/src/update/settings_update.rs b/meilisearch-core/src/update/settings_update.rs index 0ad2c904d..b4bf6c125 100644 --- a/meilisearch-core/src/update/settings_update.rs +++ b/meilisearch-core/src/update/settings_update.rs @@ -102,6 +102,18 @@ pub fn apply_settings_update( UpdateState::Nothing => (), } + match settings.attributes_for_faceting { + UpdateState::Update(attrs) => { + apply_attributes_for_faceting_update(writer, index, &mut schema, &attrs)?; + must_reindex = true; + }, + UpdateState::Clear => { + index.main.delete_attributes_for_faceting(writer)?; + index.facets.clear(writer)?; + }, + UpdateState::Nothing => (), + } + index.main.put_schema(writer, &schema)?; match settings.stop_words { @@ -131,6 +143,21 @@ pub fn apply_settings_update( Ok(()) } +fn apply_attributes_for_faceting_update( + writer: &mut heed::RwTxn, + index: &store::Index, + schema: &mut Schema, + attributes: &[String] + ) -> MResult<()> { + let mut attribute_ids = Vec::new(); + for name in attributes { + attribute_ids.push(schema.insert(name)?); + } + let attributes_for_faceting = SetBuf::from_dirty(attribute_ids); + index.main.put_attributes_for_faceting(writer, &attributes_for_faceting)?; + Ok(()) +} + pub fn apply_stop_words_update( writer: &mut heed::RwTxn, index: &store::Index, diff --git a/meilisearch-http/src/error.rs b/meilisearch-http/src/error.rs index 638726ece..e24a57bb8 100644 --- a/meilisearch-http/src/error.rs +++ b/meilisearch-http/src/error.rs @@ -23,6 +23,7 @@ pub enum ResponseError { FilterParsing(String), RetrieveDocument(u64, String), SearchDocuments(String), + FacetExpression(String), } impl ResponseError { @@ -106,6 +107,7 @@ impl fmt::Display for ResponseError { Self::OpenIndex(err) => write!(f, "Impossible to open index; {}", err), Self::RetrieveDocument(id, err) => write!(f, "impossible to retrieve the document with id: {}; {}", id, err), Self::SearchDocuments(err) => write!(f, "impossible to search documents; {}", err), + Self::FacetExpression(e) => write!(f, "error parsing facet filter expression: {}", e), } } } @@ -118,13 +120,14 @@ impl aweb::error::ResponseError for ResponseError { } fn status_code(&self) -> StatusCode { - match *self { + match *self { Self::BadParameter(_, _) | Self::BadRequest(_) | Self::CreateIndex(_) | Self::InvalidIndexUid | Self::OpenIndex(_) | Self::RetrieveDocument(_, _) + | Self::FacetExpression(_) | Self::SearchDocuments(_) | Self::FilterParsing(_) => StatusCode::BAD_REQUEST, Self::DocumentNotFound(_) @@ -151,6 +154,12 @@ impl From for ResponseError { } } +impl From for ResponseError { + fn from(error: meilisearch_core::FacetError) -> ResponseError { + ResponseError::FacetExpression(error.to_string()) + } +} + impl From for ResponseError { fn from(err: meilisearch_core::Error) -> ResponseError { use meilisearch_core::pest_error::LineColLocation::*; @@ -164,6 +173,7 @@ impl From for ResponseError { ResponseError::FilterParsing(message) }, + meilisearch_core::Error::FacetError(e) => ResponseError::FacetExpression(e.to_string()), _ => ResponseError::Internal(err.to_string()), } } diff --git a/meilisearch-http/src/helpers/meilisearch.rs b/meilisearch-http/src/helpers/meilisearch.rs index 6c8046826..94f496c07 100644 --- a/meilisearch-http/src/helpers/meilisearch.rs +++ b/meilisearch-http/src/helpers/meilisearch.rs @@ -6,6 +6,7 @@ use std::time::Instant; use indexmap::IndexMap; use log::error; use meilisearch_core::Filter; +use meilisearch_core::facets::FacetFilter; use meilisearch_core::criterion::*; use meilisearch_core::settings::RankingRule; use meilisearch_core::{Highlight, Index, MainT, RankedMap}; @@ -34,6 +35,7 @@ impl IndexSearchExt for Index { attributes_to_highlight: None, filters: None, matches: false, + facet_filters: None, } } } @@ -48,6 +50,7 @@ pub struct SearchBuilder<'a> { attributes_to_highlight: Option>, filters: Option, matches: bool, + facet_filters: Option, } impl<'a> SearchBuilder<'a> { @@ -82,6 +85,11 @@ impl<'a> SearchBuilder<'a> { self } + pub fn add_facet_filters(&mut self, filters: FacetFilter) -> &SearchBuilder { + self.facet_filters = Some(filters); + self + } + pub fn filters(&mut self, value: String) -> &SearchBuilder { self.filters = Some(value); self @@ -138,6 +146,8 @@ impl<'a> SearchBuilder<'a> { } } + query_builder.set_facets(self.facet_filters.as_ref()); + let start = Instant::now(); let result = query_builder.query(reader, &self.query, self.offset..(self.offset + self.limit)); let (docs, nb_hits) = result.map_err(ResponseError::search_documents)?; diff --git a/meilisearch-http/src/routes/search.rs b/meilisearch-http/src/routes/search.rs index 1189e79a8..846fc8904 100644 --- a/meilisearch-http/src/routes/search.rs +++ b/meilisearch-http/src/routes/search.rs @@ -12,6 +12,8 @@ use crate::helpers::Authentication; use crate::routes::IndexParam; use crate::Data; +use meilisearch_core::facets::FacetFilter; + pub fn services(cfg: &mut web::ServiceConfig) { cfg.service(search_with_url_query); } @@ -28,6 +30,7 @@ struct SearchQuery { attributes_to_highlight: Option, filters: Option, matches: Option, + facet_filters: Option, } #[get("/indexes/{index_uid}/search", wrap = "Authentication::Public")] @@ -81,6 +84,13 @@ async fn search_with_url_query( } } + if let Some(ref facet_filters) = params.facet_filters { + match index.main.attributes_for_faceting(&reader)? { + Some(ref attrs) => { search_builder.add_facet_filters(FacetFilter::from_str(facet_filters, &schema, attrs)?); }, + None => return Err(ResponseError::FacetExpression("can't filter on facets, as no facet is set".to_string())) + } + } + if let Some(attributes_to_crop) = ¶ms.attributes_to_crop { let default_length = params.crop_length.unwrap_or(200); let mut final_attributes: HashMap = HashMap::new(); diff --git a/meilisearch-http/src/routes/setting.rs b/meilisearch-http/src/routes/setting.rs index 262af8b84..96e6935e7 100644 --- a/meilisearch-http/src/routes/setting.rs +++ b/meilisearch-http/src/routes/setting.rs @@ -91,6 +91,17 @@ async fn get_all( let schema = index.main.schema(&reader)?; + let attributes_for_faceting = match (&schema, &index.main.attributes_for_faceting(&reader)?) { + (Some(schema), Some(attrs)) => { + Some(attrs + .iter() + .filter_map(|&id| schema .name(id)) + .map(str::to_string) + .collect()) + } + _ => None, + }; + let searchable_attributes = schema.clone().map(|s| { s.indexed_name() .iter() @@ -115,6 +126,7 @@ async fn get_all( stop_words: Some(Some(stop_words)), synonyms: Some(Some(synonyms)), accept_new_fields: Some(accept_new_fields), + attributes_for_faceting: Some(attributes_for_faceting), }; Ok(HttpResponse::Ok().json(settings)) @@ -140,6 +152,7 @@ async fn delete_all( stop_words: UpdateState::Clear, synonyms: UpdateState::Clear, accept_new_fields: UpdateState::Clear, + attributes_for_faceting: UpdateState::Clear, }; let update_id = index.settings_update(&mut writer, settings)?; diff --git a/meilisearch-http/tests/assets/test_set.json b/meilisearch-http/tests/assets/test_set.json new file mode 100644 index 000000000..f165fa1b3 --- /dev/null +++ b/meilisearch-http/tests/assets/test_set.json @@ -0,0 +1,1613 @@ +[ + { + "id": 0, + "isActive": false, + "balance": "$2,668.55", + "picture": "http://placehold.it/32x32", + "age": 36, + "color": "green", + "name": "Lucas Hess", + "gender": "male", + "email": "lucashess@chorizon.com", + "phone": "+1 (998) 478-2597", + "address": "412 Losee Terrace, Blairstown, Georgia, 2825", + "about": "Mollit ad in exercitation quis. Anim est ut consequat fugiat duis magna aliquip velit nisi. Commodo eiusmod est consequat proident consectetur aliqua enim fugiat. Aliqua adipisicing laboris elit proident enim veniam laboris mollit. Incididunt fugiat minim ad nostrud deserunt tempor in. Id irure officia labore qui est labore nulla nisi. Magna sit quis tempor esse consectetur amet labore duis aliqua consequat.\r\n", + "registered": "2016-06-21T09:30:25 -02:00", + "latitude": -44.174957, + "longitude": -145.725388, + "tags": [ + "bug", + "bug" + ] + }, + { + "id": 1, + "isActive": true, + "balance": "$1,706.13", + "picture": "http://placehold.it/32x32", + "age": 27, + "color": "green", + "name": "Cherry Orr", + "gender": "female", + "email": "cherryorr@chorizon.com", + "phone": "+1 (995) 479-3174", + "address": "442 Beverly Road, Ventress, New Mexico, 3361", + "about": "Exercitation officia mollit proident nostrud ea. Pariatur voluptate labore nostrud magna duis non elit et incididunt Lorem velit duis amet commodo. Irure in velit laboris pariatur. Do tempor ex deserunt duis minim amet.\r\n", + "registered": "2020-03-18T11:12:21 -01:00", + "latitude": -24.356932, + "longitude": 27.184808, + "tags": [ + "new issue", + "bug" + ] + }, + { + "id": 2, + "isActive": true, + "balance": "$2,467.47", + "picture": "http://placehold.it/32x32", + "age": 34, + "color": "blue", + "name": "Patricia Goff", + "gender": "female", + "email": "patriciagoff@chorizon.com", + "phone": "+1 (864) 463-2277", + "address": "866 Hornell Loop, Cresaptown, Ohio, 1700", + "about": "Non culpa duis dolore Lorem aliqua. Labore veniam laborum cupidatat nostrud ea exercitation. Esse nostrud sit veniam laborum minim ullamco nulla aliqua est cillum magna. Duis non esse excepteur veniam voluptate sunt cupidatat nostrud consequat sint adipisicing ut excepteur. Incididunt sit aliquip non id magna amet deserunt esse quis dolor.\r\n", + "registered": "2014-10-28T12:59:30 -01:00", + "latitude": -64.008555, + "longitude": 11.867098, + "tags": [ + "good first issue" + ] + }, + { + "id": 3, + "isActive": true, + "balance": "$3,344.40", + "picture": "http://placehold.it/32x32", + "age": 35, + "color": "blue", + "name": "Adeline Flynn", + "gender": "female", + "email": "adelineflynn@chorizon.com", + "phone": "+1 (994) 600-2840", + "address": "428 Paerdegat Avenue, Hollymead, Pennsylvania, 948", + "about": "Ex velit magna minim labore dolor id laborum incididunt. Proident dolor fugiat exercitation ad adipisicing amet dolore. Veniam nisi pariatur aute eu amet sint elit duis exercitation. Eu fugiat Lorem nostrud consequat aute sunt. Minim excepteur cillum laboris enim tempor adipisicing nulla reprehenderit ea velit Lorem qui in incididunt. Esse ipsum mollit deserunt ea exercitation ex aliqua anim magna cupidatat culpa.\r\n", + "registered": "2014-03-27T06:24:45 -01:00", + "latitude": -74.485173, + "longitude": -11.059859, + "tags": [ + "bug", + "good first issue", + "wontfix", + "new issue" + ] + }, + { + "id": 4, + "isActive": false, + "balance": "$2,575.78", + "picture": "http://placehold.it/32x32", + "age": 39, + "color": "green", + "name": "Mariana Pacheco", + "gender": "female", + "email": "marianapacheco@chorizon.com", + "phone": "+1 (820) 414-2223", + "address": "664 Rapelye Street, Faywood, California, 7320", + "about": "Sint cillum enim eu Lorem dolore. Est excepteur cillum consequat incididunt. Ut consectetur et do culpa eiusmod ex ut id proident aliqua. Sunt dolor anim minim labore incididunt deserunt enim velit sunt ut in velit. Nulla ipsum cillum qui est minim officia in occaecat exercitation Lorem sunt. Aliqua minim excepteur tempor incididunt dolore. Quis amet ullamco et proident aliqua magna consequat.\r\n", + "registered": "2015-09-02T03:23:35 -02:00", + "latitude": 75.763501, + "longitude": -78.777124, + "tags": [ + "new issue" + ] + }, + { + "id": 5, + "isActive": true, + "balance": "$3,793.09", + "picture": "http://placehold.it/32x32", + "age": 20, + "color": "green", + "name": "Warren Watson", + "gender": "male", + "email": "warrenwatson@chorizon.com", + "phone": "+1 (807) 583-2427", + "address": "671 Prince Street, Faxon, Connecticut, 4275", + "about": "Cillum incididunt mollit labore ipsum elit ea. Lorem labore consectetur nulla ea fugiat sint esse cillum ea commodo id qui. Sint cillum mollit dolore enim quis esse. Nisi labore duis dolor tempor laborum laboris ad minim pariatur in excepteur sit. Aliqua anim amet sunt ullamco labore amet culpa irure esse eiusmod deserunt consequat Lorem nostrud.\r\n", + "registered": "2017-06-04T06:02:17 -02:00", + "latitude": 29.979223, + "longitude": 25.358943, + "tags": [ + "wontfix", + "wontfix", + "wontfix" + ] + }, + { + "id": 6, + "isActive": true, + "balance": "$2,919.70", + "picture": "http://placehold.it/32x32", + "age": 20, + "color": "blue", + "name": "Shelia Berry", + "gender": "female", + "email": "sheliaberry@chorizon.com", + "phone": "+1 (853) 511-2651", + "address": "437 Forrest Street, Coventry, Illinois, 2056", + "about": "Id occaecat qui voluptate proident culpa cillum nisi reprehenderit. Pariatur nostrud proident adipisicing reprehenderit eiusmod qui minim proident aliqua id cupidatat laboris deserunt. Proident sint laboris sit mollit dolor qui incididunt quis veniam cillum cupidatat ad nostrud ut. Aliquip consequat eiusmod eiusmod irure tempor do incididunt id culpa laboris eiusmod.\r\n", + "registered": "2018-07-11T02:45:01 -02:00", + "latitude": 54.815991, + "longitude": -118.690609, + "tags": [ + "good first issue", + "bug", + "wontfix", + "new issue" + ] + }, + { + "id": 7, + "isActive": true, + "balance": "$1,349.50", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "green", + "name": "Chrystal Boyd", + "gender": "female", + "email": "chrystalboyd@chorizon.com", + "phone": "+1 (936) 563-2802", + "address": "670 Croton Loop, Sussex, Florida, 4692", + "about": "Consequat ex voluptate consectetur laborum nulla. Qui voluptate Lorem amet labore est esse sunt. Nulla cupidatat consequat quis incididunt exercitation aliquip reprehenderit ea ea adipisicing reprehenderit id consectetur quis. Exercitation est incididunt ullamco non proident consequat. Nisi veniam aliquip fugiat voluptate ex id aute duis ullamco magna ipsum ad laborum ipsum. Cupidatat velit dolore esse nisi.\r\n", + "registered": "2016-11-01T07:36:04 -01:00", + "latitude": -24.711933, + "longitude": 147.246705, + "tags": [] + }, + { + "id": 8, + "isActive": false, + "balance": "$3,999.56", + "picture": "http://placehold.it/32x32", + "age": 30, + "color": "brown", + "name": "Martin Porter", + "gender": "male", + "email": "martinporter@chorizon.com", + "phone": "+1 (895) 580-2304", + "address": "577 Regent Place, Aguila, Guam, 6554", + "about": "Nostrud nulla labore ex excepteur labore enim cillum pariatur in do Lorem eiusmod ullamco est. Labore aliquip id ut nisi commodo pariatur ea esse laboris. Incididunt eu dolor esse excepteur nulla minim proident non cillum nisi dolore incididunt ipsum tempor.\r\n", + "registered": "2014-09-20T02:08:30 -02:00", + "latitude": -88.344273, + "longitude": 37.964466, + "tags": [] + }, + { + "id": 9, + "isActive": true, + "balance": "$3,729.71", + "picture": "http://placehold.it/32x32", + "age": 26, + "color": "blue", + "name": "Kelli Mendez", + "gender": "female", + "email": "kellimendez@chorizon.com", + "phone": "+1 (936) 401-2236", + "address": "242 Caton Place, Grazierville, Alabama, 3968", + "about": "Consectetur occaecat dolore esse eiusmod enim ea aliqua eiusmod amet velit laborum. Velit quis consequat consectetur velit fugiat labore commodo amet do. Magna minim est ad commodo consequat fugiat. Laboris duis Lorem ipsum irure sit ipsum consequat tempor sit. Est ad nulla duis quis velit anim id nulla. Cupidatat ea esse laboris eu veniam cupidatat proident veniam quis.\r\n", + "registered": "2018-05-04T10:35:30 -02:00", + "latitude": 49.37551, + "longitude": 41.872323, + "tags": [ + "new issue", + "new issue" + ] + }, + { + "id": 10, + "isActive": false, + "balance": "$1,127.47", + "picture": "http://placehold.it/32x32", + "age": 27, + "color": "blue", + "name": "Maddox Johns", + "gender": "male", + "email": "maddoxjohns@chorizon.com", + "phone": "+1 (892) 470-2357", + "address": "756 Beard Street, Avalon, Louisiana, 114", + "about": "Voluptate et dolor magna do do. Id do enim ut nulla esse culpa fugiat excepteur quis. Nostrud ad aliquip aliqua qui esse ut consequat proident deserunt esse cupidatat do elit fugiat. Sint cillum aliquip cillum laboris laborum laboris ad aliquip enim reprehenderit cillum eu sint. Sint ut ad duis do culpa non eiusmod amet non ipsum commodo. Pariatur aliquip sit deserunt non. Ut consequat pariatur deserunt veniam est sit eiusmod officia aliquip commodo sunt in eu duis.\r\n", + "registered": "2016-04-22T06:41:25 -02:00", + "latitude": 66.640229, + "longitude": -17.222666, + "tags": [ + "new issue", + "good first issue", + "good first issue", + "new issue" + ] + }, + { + "id": 11, + "isActive": true, + "balance": "$1,351.43", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "green", + "name": "Evans Wagner", + "gender": "male", + "email": "evanswagner@chorizon.com", + "phone": "+1 (889) 496-2332", + "address": "118 Monaco Place, Lutsen, Delaware, 6209", + "about": "Sunt consectetur enim ipsum consectetur occaecat reprehenderit nulla pariatur. Cupidatat do exercitation tempor voluptate duis nostrud dolor consectetur. Excepteur aliquip Lorem voluptate cillum est. Nisi velit nulla nostrud ea id officia laboris et.\r\n", + "registered": "2016-10-27T01:26:31 -02:00", + "latitude": -77.673222, + "longitude": -142.657214, + "tags": [ + "good first issue", + "good first issue" + ] + }, + { + "id": 12, + "isActive": false, + "balance": "$3,394.96", + "picture": "http://placehold.it/32x32", + "age": 25, + "color": "blue", + "name": "Aida Kirby", + "gender": "female", + "email": "aidakirby@chorizon.com", + "phone": "+1 (942) 532-2325", + "address": "797 Engert Avenue, Wilsonia, Idaho, 6532", + "about": "Mollit aute esse Lorem do laboris anim reprehenderit excepteur. Ipsum culpa esse voluptate officia cupidatat minim. Velit officia proident nostrud sunt irure labore. Culpa ex commodo amet dolor amet voluptate Lorem ex esse commodo fugiat quis non. Ex est adipisicing veniam sunt dolore ut aliqua nisi ex sit. Esse voluptate esse anim id adipisicing enim aute ea exercitation tempor cillum.\r\n", + "registered": "2018-06-18T04:39:57 -02:00", + "latitude": -58.062041, + "longitude": 34.999254, + "tags": [ + "new issue", + "wontfix", + "bug", + "new issue" + ] + }, + { + "id": 13, + "isActive": true, + "balance": "$2,812.62", + "picture": "http://placehold.it/32x32", + "age": 40, + "color": "blue", + "name": "Nelda Burris", + "gender": "female", + "email": "neldaburris@chorizon.com", + "phone": "+1 (813) 600-2576", + "address": "160 Opal Court, Fowlerville, Tennessee, 2170", + "about": "Ipsum aliquip adipisicing elit magna. Veniam irure quis laborum laborum sint velit amet. Irure non eiusmod laborum fugiat qui quis Lorem culpa veniam commodo. Fugiat cupidatat dolore et consequat pariatur enim ex velit consequat deserunt quis. Deserunt et quis laborum cupidatat cillum minim cupidatat nisi do commodo commodo labore cupidatat ea. In excepteur sit nostrud nulla nostrud dolor sint. Et anim culpa aliquip laborum Lorem elit.\r\n", + "registered": "2015-08-15T12:39:53 -02:00", + "latitude": 66.6871, + "longitude": 179.549488, + "tags": [ + "wontfix" + ] + }, + { + "id": 14, + "isActive": true, + "balance": "$1,718.33", + "picture": "http://placehold.it/32x32", + "age": 35, + "color": "blue", + "name": "Jennifer Hart", + "gender": "female", + "email": "jenniferhart@chorizon.com", + "phone": "+1 (850) 537-2513", + "address": "124 Veranda Place, Nash, Utah, 985", + "about": "Amet amet voluptate in occaecat pariatur. Nulla ipsum esse quis qui in quis qui. Non est non nisi qui tempor commodo consequat fugiat. Sint eu ipsum aute anim anim. Ea nostrud excepteur exercitation consectetur Lorem.\r\n", + "registered": "2016-09-04T11:46:59 -02:00", + "latitude": -66.827751, + "longitude": 99.220079, + "tags": [ + "wontfix", + "bug", + "new issue", + "new issue" + ] + }, + { + "id": 15, + "isActive": false, + "balance": "$2,698.16", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "blue", + "name": "Aurelia Contreras", + "gender": "female", + "email": "aureliacontreras@chorizon.com", + "phone": "+1 (932) 442-3103", + "address": "655 Dwight Street, Grapeview, Palau, 8356", + "about": "Qui adipisicing consectetur aute veniam culpa ipsum. Occaecat occaecat ut mollit enim enim elit Lorem nostrud Lorem. Consequat laborum mollit nulla aute cillum sunt mollit commodo velit culpa. Pariatur pariatur velit nostrud tempor. In minim enim cillum exercitation in laboris labore ea sunt in incididunt fugiat.\r\n", + "registered": "2014-09-11T10:43:15 -02:00", + "latitude": -71.328973, + "longitude": 133.404895, + "tags": [ + "wontfix", + "bug", + "good first issue" + ] + }, + { + "id": 16, + "isActive": true, + "balance": "$3,303.25", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "brown", + "name": "Estella Bass", + "gender": "female", + "email": "estellabass@chorizon.com", + "phone": "+1 (825) 436-2909", + "address": "435 Rockwell Place, Garberville, Wisconsin, 2230", + "about": "Sit eiusmod mollit velit non. Qui ea in exercitation elit reprehenderit occaecat tempor minim officia. Culpa amet voluptate sit eiusmod pariatur.\r\n", + "registered": "2017-11-23T09:32:09 -01:00", + "latitude": 81.17014, + "longitude": -145.262693, + "tags": [ + "new issue" + ] + }, + { + "id": 17, + "isActive": false, + "balance": "$3,579.20", + "picture": "http://placehold.it/32x32", + "age": 25, + "color": "brown", + "name": "Ortega Brennan", + "gender": "male", + "email": "ortegabrennan@chorizon.com", + "phone": "+1 (906) 526-2287", + "address": "440 Berry Street, Rivera, Maine, 1849", + "about": "Veniam velit non laboris consectetur sit aliquip enim proident velit in ipsum reprehenderit reprehenderit. Dolor qui nulla adipisicing ad magna dolore do ut duis et aute est. Qui est elit cupidatat nostrud. Laboris voluptate reprehenderit minim sint exercitation cupidatat ipsum sint consectetur velit sunt et officia incididunt. Ut amet Lorem minim deserunt officia officia irure qui et Lorem deserunt culpa sit.\r\n", + "registered": "2016-03-31T02:17:13 -02:00", + "latitude": -68.407524, + "longitude": -113.642067, + "tags": [ + "new issue", + "wontfix" + ] + }, + { + "id": 18, + "isActive": false, + "balance": "$1,484.92", + "picture": "http://placehold.it/32x32", + "age": 39, + "color": "blue", + "name": "Leonard Tillman", + "gender": "male", + "email": "leonardtillman@chorizon.com", + "phone": "+1 (864) 541-3456", + "address": "985 Provost Street, Charco, New Hampshire, 8632", + "about": "Consectetur ut magna sit id officia nostrud ipsum. Lorem cupidatat laborum nostrud aliquip magna qui est cupidatat exercitation et. Officia qui magna commodo id cillum magna ut ad veniam sunt sint ex. Id minim do in do exercitation aliquip incididunt ex esse. Nisi aliqua quis excepteur qui aute excepteur dolore eu pariatur irure id eu cupidatat eiusmod. Aliqua amet et dolore enim et eiusmod qui irure pariatur qui officia adipisicing nulla duis.\r\n", + "registered": "2018-05-06T08:21:27 -02:00", + "latitude": -8.581801, + "longitude": -61.910062, + "tags": [ + "wontfix", + "new issue", + "bug", + "bug" + ] + }, + { + "id": 19, + "isActive": true, + "balance": "$3,572.55", + "picture": "http://placehold.it/32x32", + "age": 33, + "color": "brown", + "name": "Dale Payne", + "gender": "male", + "email": "dalepayne@chorizon.com", + "phone": "+1 (814) 469-3499", + "address": "536 Dare Court, Ironton, Arkansas, 8605", + "about": "Et velit cupidatat velit incididunt mollit. Occaecat do labore aliqua dolore excepteur occaecat ut veniam ad ullamco tempor. Ut anim laboris deserunt culpa esse. Pariatur Lorem nulla cillum cupidatat nostrud Lorem commodo reprehenderit ut est. In dolor cillum reprehenderit laboris incididunt ad reprehenderit aute ipsum officia id in consequat. Culpa exercitation voluptate fugiat est Lorem ipsum in dolore dolor consequat Lorem et.\r\n", + "registered": "2019-10-11T01:01:33 -02:00", + "latitude": -18.280968, + "longitude": -126.091797, + "tags": [ + "bug", + "wontfix", + "wontfix", + "wontfix" + ] + }, + { + "id": 20, + "isActive": true, + "balance": "$1,986.48", + "picture": "http://placehold.it/32x32", + "age": 38, + "color": "green", + "name": "Florence Long", + "gender": "female", + "email": "florencelong@chorizon.com", + "phone": "+1 (972) 557-3858", + "address": "519 Hendrickson Street, Templeton, Hawaii, 2389", + "about": "Quis officia occaecat veniam veniam. Ex minim enim labore cupidatat qui. Proident esse deserunt laborum laboris sunt nostrud.\r\n", + "registered": "2016-05-02T09:18:59 -02:00", + "latitude": -27.110866, + "longitude": -45.09445, + "tags": [] + }, + { + "id": 21, + "isActive": true, + "balance": "$1,440.09", + "picture": "http://placehold.it/32x32", + "age": 40, + "color": "blue", + "name": "Levy Whitley", + "gender": "male", + "email": "levywhitley@chorizon.com", + "phone": "+1 (911) 458-2411", + "address": "187 Thomas Street, Hachita, North Carolina, 2989", + "about": "Velit laboris non minim elit sint deserunt fugiat. Aute minim ex commodo aute cillum aliquip fugiat pariatur nulla eiusmod pariatur consectetur. Qui ex ea qui laborum veniam adipisicing magna minim ut. In irure anim voluptate mollit et. Adipisicing labore ea mollit magna aliqua culpa velit est. Excepteur nisi veniam enim velit in ad officia irure laboris.\r\n", + "registered": "2014-04-30T07:31:38 -02:00", + "latitude": -6.537315, + "longitude": 171.813536, + "tags": [ + "bug" + ] + }, + { + "id": 22, + "isActive": false, + "balance": "$2,938.57", + "picture": "http://placehold.it/32x32", + "age": 35, + "color": "blue", + "name": "Bernard Mcfarland", + "gender": "male", + "email": "bernardmcfarland@chorizon.com", + "phone": "+1 (979) 442-3386", + "address": "409 Hall Street, Keyport, Federated States Of Micronesia, 7011", + "about": "Reprehenderit irure aute et anim ullamco enim est tempor id ipsum mollit veniam aute ullamco. Consectetur dolor velit tempor est reprehenderit ut id non est ullamco voluptate. Commodo aute ullamco culpa non voluptate incididunt non culpa culpa nisi id proident cupidatat.\r\n", + "registered": "2017-08-10T10:07:59 -02:00", + "latitude": 63.766795, + "longitude": 68.177069, + "tags": [] + }, + { + "id": 23, + "isActive": true, + "balance": "$1,678.49", + "picture": "http://placehold.it/32x32", + "age": 31, + "color": "brown", + "name": "Blanca Mcclain", + "gender": "female", + "email": "blancamcclain@chorizon.com", + "phone": "+1 (976) 439-2772", + "address": "176 Crooke Avenue, Valle, Virginia, 5373", + "about": "Aliquip sunt irure ut consectetur elit. Cillum amet incididunt et anim elit in incididunt adipisicing fugiat veniam esse veniam. Nisi qui sit occaecat tempor nostrud est aute cillum anim excepteur laboris magna in. Fugiat fugiat veniam cillum laborum ut pariatur amet nulla nulla. Nostrud mollit in laborum minim exercitation aute. Lorem aute ipsum laboris est adipisicing qui ullamco tempor adipisicing cupidatat mollit.\r\n", + "registered": "2015-10-12T11:57:28 -02:00", + "latitude": -8.944564, + "longitude": -150.711709, + "tags": [ + "bug", + "wontfix", + "good first issue" + ] + }, + { + "id": 24, + "isActive": true, + "balance": "$2,276.87", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "brown", + "name": "Espinoza Ford", + "gender": "male", + "email": "espinozaford@chorizon.com", + "phone": "+1 (945) 429-3975", + "address": "137 Bowery Street, Itmann, District Of Columbia, 1864", + "about": "Deserunt nisi aliquip esse occaecat laborum qui aliqua excepteur ea cupidatat dolore magna consequat. Culpa aliquip cillum incididunt proident est officia consequat duis. Elit tempor ut cupidatat nisi ea sint non labore aliquip amet. Deserunt labore cupidatat laboris dolor duis occaecat velit aliquip reprehenderit esse. Sit ad qui consectetur id anim nisi amet eiusmod.\r\n", + "registered": "2014-03-26T02:16:08 -01:00", + "latitude": -37.137666, + "longitude": -51.811757, + "tags": [ + "wontfix", + "bug" + ] + }, + { + "id": 25, + "isActive": true, + "balance": "$3,973.43", + "picture": "http://placehold.it/32x32", + "age": 29, + "color": "green", + "name": "Sykes Conley", + "gender": "male", + "email": "sykesconley@chorizon.com", + "phone": "+1 (851) 401-3916", + "address": "345 Grand Street, Woodlands, Missouri, 4461", + "about": "Pariatur ullamco duis reprehenderit ad sit dolore. Dolore ex fugiat labore incididunt nostrud. Minim deserunt officia sunt enim magna elit veniam reprehenderit nisi cupidatat dolor eiusmod. Veniam laboris sint cillum et laboris nostrud culpa laboris anim. Incididunt velit pariatur cupidatat sit dolore in. Voluptate consectetur officia id nostrud velit mollit dolor. Id laboris consectetur culpa sunt pariatur minim sunt laboris sit.\r\n", + "registered": "2015-09-12T06:03:56 -02:00", + "latitude": 67.282955, + "longitude": -64.341323, + "tags": [ + "wontfix" + ] + }, + { + "id": 26, + "isActive": false, + "balance": "$1,431.50", + "picture": "http://placehold.it/32x32", + "age": 35, + "color": "blue", + "name": "Barlow Duran", + "gender": "male", + "email": "barlowduran@chorizon.com", + "phone": "+1 (995) 436-2562", + "address": "481 Everett Avenue, Allison, Nebraska, 3065", + "about": "Proident quis eu officia adipisicing aliquip. Lorem laborum magna dolor et incididunt cillum excepteur et amet. Veniam consectetur officia fugiat magna consequat dolore elit aute exercitation fugiat excepteur ullamco. Sit qui proident reprehenderit ea ad qui culpa exercitation reprehenderit anim cupidatat. Nulla et duis Lorem cillum duis pariatur amet voluptate labore ut aliqua mollit anim ea. Nostrud incididunt et proident adipisicing non consequat tempor ullamco adipisicing incididunt. Incididunt cupidatat tempor fugiat officia qui eiusmod reprehenderit.\r\n", + "registered": "2017-06-29T04:28:43 -02:00", + "latitude": -38.70606, + "longitude": 55.02816, + "tags": [ + "new issue" + ] + }, + { + "id": 27, + "isActive": true, + "balance": "$3,478.27", + "picture": "http://placehold.it/32x32", + "age": 31, + "color": "blue", + "name": "Schwartz Morgan", + "gender": "male", + "email": "schwartzmorgan@chorizon.com", + "phone": "+1 (861) 507-2067", + "address": "451 Lincoln Road, Fairlee, Washington, 2717", + "about": "Labore eiusmod sint dolore sunt eiusmod esse et in id aliquip. Aliqua consequat occaecat laborum labore ipsum enim non nostrud adipisicing adipisicing cillum occaecat. Duis minim est culpa sunt nulla ullamco adipisicing magna irure. Occaecat quis irure eiusmod fugiat quis commodo reprehenderit labore cillum commodo id et.\r\n", + "registered": "2016-05-10T08:34:54 -02:00", + "latitude": -75.886403, + "longitude": 93.044471, + "tags": [ + "bug", + "bug", + "wontfix", + "wontfix" + ] + }, + { + "id": 28, + "isActive": true, + "balance": "$2,825.59", + "picture": "http://placehold.it/32x32", + "age": 32, + "color": "blue", + "name": "Kristy Leon", + "gender": "female", + "email": "kristyleon@chorizon.com", + "phone": "+1 (948) 465-2563", + "address": "594 Macon Street, Floris, South Dakota, 3565", + "about": "Proident veniam voluptate magna id do. Laboris enim dolor culpa quis. Esse voluptate elit commodo duis incididunt velit aliqua. Qui aute commodo incididunt elit eu Lorem dolore. Non esse duis do reprehenderit culpa minim. Ullamco consequat id do exercitation exercitation mollit ipsum velit eiusmod quis.\r\n", + "registered": "2014-12-14T04:10:29 -01:00", + "latitude": -50.01615, + "longitude": -68.908804, + "tags": [ + "wontfix", + "good first issue" + ] + }, + { + "id": 29, + "isActive": false, + "balance": "$3,028.03", + "picture": "http://placehold.it/32x32", + "age": 39, + "color": "blue", + "name": "Ashley Pittman", + "gender": "male", + "email": "ashleypittman@chorizon.com", + "phone": "+1 (928) 507-3523", + "address": "646 Adelphi Street, Clara, Colorado, 6056", + "about": "Incididunt cillum consectetur nulla sit sit labore nulla sit. Ullamco nisi mollit reprehenderit tempor irure in Lorem duis. Sunt eu aute laboris dolore commodo ipsum sint cupidatat veniam amet culpa incididunt aute ad. Quis dolore aliquip id aute mollit eiusmod nisi ipsum ut labore adipisicing do culpa.\r\n", + "registered": "2016-01-07T10:40:48 -01:00", + "latitude": -58.766037, + "longitude": -124.828485, + "tags": [ + "wontfix" + ] + }, + { + "id": 30, + "isActive": true, + "balance": "$2,021.11", + "picture": "http://placehold.it/32x32", + "age": 32, + "color": "blue", + "name": "Stacy Espinoza", + "gender": "female", + "email": "stacyespinoza@chorizon.com", + "phone": "+1 (999) 487-3253", + "address": "931 Alabama Avenue, Bangor, Alaska, 8215", + "about": "Id reprehenderit cupidatat exercitation anim ad nisi irure. Minim est proident mollit laborum. Duis ad duis eiusmod quis.\r\n", + "registered": "2014-07-16T06:15:53 -02:00", + "latitude": 41.560197, + "longitude": 177.697, + "tags": [ + "new issue", + "new issue", + "bug" + ] + }, + { + "id": 31, + "isActive": false, + "balance": "$3,609.82", + "picture": "http://placehold.it/32x32", + "age": 32, + "color": "blue", + "name": "Vilma Garza", + "gender": "female", + "email": "vilmagarza@chorizon.com", + "phone": "+1 (944) 585-2021", + "address": "565 Tech Place, Sedley, Puerto Rico, 858", + "about": "Excepteur et fugiat mollit incididunt cupidatat. Mollit nisi veniam sint eu exercitation amet labore. Voluptate est magna est amet qui minim excepteur cupidatat dolor quis id excepteur aliqua reprehenderit. Proident nostrud ex veniam officia nisi enim occaecat ex magna officia id consectetur ad eu. In et est reprehenderit cupidatat ad minim veniam proident nulla elit nisi veniam proident ex. Eu in irure sit veniam amet incididunt fugiat proident quis ullamco laboris.\r\n", + "registered": "2017-06-30T07:43:52 -02:00", + "latitude": -12.574889, + "longitude": -54.771186, + "tags": [ + "new issue", + "wontfix", + "wontfix" + ] + }, + { + "id": 32, + "isActive": false, + "balance": "$2,882.34", + "picture": "http://placehold.it/32x32", + "age": 38, + "color": "brown", + "name": "June Dunlap", + "gender": "female", + "email": "junedunlap@chorizon.com", + "phone": "+1 (997) 504-2937", + "address": "353 Cozine Avenue, Goodville, Indiana, 1438", + "about": "Non dolore ut Lorem dolore amet veniam fugiat reprehenderit ut amet ea ut. Non aliquip cillum ad occaecat non et sint quis proident velit laborum ullamco et. Quis qui tempor eu voluptate et proident duis est commodo laboris ex enim. Nisi aliquip laboris nostrud veniam aliqua ullamco. Et officia proident dolor aliqua incididunt veniam proident.\r\n", + "registered": "2016-08-23T08:54:11 -02:00", + "latitude": -27.883363, + "longitude": -163.919683, + "tags": [ + "new issue", + "new issue", + "bug", + "wontfix" + ] + }, + { + "id": 33, + "isActive": true, + "balance": "$3,556.54", + "picture": "http://placehold.it/32x32", + "age": 33, + "color": "brown", + "name": "Cecilia Greer", + "gender": "female", + "email": "ceciliagreer@chorizon.com", + "phone": "+1 (977) 573-3498", + "address": "696 Withers Street, Lydia, Oklahoma, 3220", + "about": "Dolor pariatur veniam ad enim eiusmod fugiat ullamco nulla veniam. Dolore dolor sit excepteur veniam adipisicing adipisicing excepteur commodo qui reprehenderit magna exercitation enim reprehenderit. Cupidatat eu ullamco excepteur sint do. Et cupidatat ex adipisicing veniam eu tempor reprehenderit ut eiusmod amet proident veniam nostrud. Tempor ex enim mollit laboris magna tempor. Et aliqua nostrud esse pariatur quis. Ut pariatur ea ipsum pariatur.\r\n", + "registered": "2017-01-13T11:30:12 -01:00", + "latitude": 60.467215, + "longitude": 84.684575, + "tags": [ + "wontfix", + "good first issue", + "good first issue", + "wontfix" + ] + }, + { + "id": 34, + "isActive": true, + "balance": "$1,413.35", + "picture": "http://placehold.it/32x32", + "age": 33, + "color": "brown", + "name": "Mckay Schroeder", + "gender": "male", + "email": "mckayschroeder@chorizon.com", + "phone": "+1 (816) 480-3657", + "address": "958 Miami Court, Rehrersburg, Northern Mariana Islands, 567", + "about": "Amet do velit excepteur tempor sit eu voluptate. Excepteur amet culpa ipsum in pariatur mollit amet nisi veniam. Laboris elit consectetur id anim qui laboris. Reprehenderit mollit laboris occaecat esse sunt Lorem Lorem sunt occaecat.\r\n", + "registered": "2016-02-08T04:50:15 -01:00", + "latitude": -72.413287, + "longitude": -159.254371, + "tags": [ + "good first issue" + ] + }, + { + "id": 35, + "isActive": true, + "balance": "$2,306.53", + "picture": "http://placehold.it/32x32", + "age": 34, + "color": "blue", + "name": "Sawyer Mccormick", + "gender": "male", + "email": "sawyermccormick@chorizon.com", + "phone": "+1 (829) 569-3012", + "address": "749 Apollo Street, Eastvale, Texas, 7373", + "about": "Est irure ex occaecat aute. Lorem ad ullamco esse cillum deserunt qui proident anim officia dolore. Incididunt tempor cupidatat nulla cupidatat ullamco reprehenderit Lorem. Laboris tempor do pariatur sint non officia id qui deserunt amet Lorem pariatur consectetur exercitation. Adipisicing reprehenderit pariatur duis ex cupidatat cillum ad laboris ex. Sunt voluptate pariatur esse amet dolore minim aliquip reprehenderit nisi velit mollit.\r\n", + "registered": "2019-11-30T11:53:23 -01:00", + "latitude": -48.978194, + "longitude": 110.950191, + "tags": [ + "good first issue", + "new issue", + "new issue", + "bug" + ] + }, + { + "id": 36, + "isActive": false, + "balance": "$1,844.54", + "picture": "http://placehold.it/32x32", + "age": 37, + "color": "brown", + "name": "Barbra Valenzuela", + "gender": "female", + "email": "barbravalenzuela@chorizon.com", + "phone": "+1 (992) 512-2649", + "address": "617 Schenck Court, Reinerton, Michigan, 2908", + "about": "Deserunt adipisicing nisi et amet aliqua amet. Veniam occaecat et elit excepteur veniam. Aute irure culpa nostrud occaecat. Excepteur sit aute mollit commodo. Do ex pariatur consequat sint Lorem veniam laborum excepteur. Non voluptate ex laborum enim irure. Adipisicing excepteur anim elit esse.\r\n", + "registered": "2019-03-29T01:59:31 -01:00", + "latitude": 45.193723, + "longitude": -12.486778, + "tags": [ + "new issue", + "new issue", + "wontfix", + "wontfix" + ] + }, + { + "id": 37, + "isActive": false, + "balance": "$3,469.82", + "picture": "http://placehold.it/32x32", + "age": 39, + "color": "brown", + "name": "Opal Weiss", + "gender": "female", + "email": "opalweiss@chorizon.com", + "phone": "+1 (809) 400-3079", + "address": "535 Bogart Street, Frizzleburg, Arizona, 5222", + "about": "Reprehenderit nostrud minim adipisicing voluptate nisi consequat id sint. Proident tempor est esse cupidatat minim irure esse do do sint dolor. In officia duis et voluptate Lorem minim cupidatat ipsum enim qui dolor quis in Lorem. Aliquip commodo ex quis exercitation reprehenderit. Lorem id reprehenderit cillum adipisicing sunt ipsum incididunt incididunt.\r\n", + "registered": "2019-09-04T07:22:28 -02:00", + "latitude": 72.50376, + "longitude": 61.656435, + "tags": [ + "bug", + "bug", + "good first issue", + "good first issue" + ] + }, + { + "id": 38, + "isActive": true, + "balance": "$1,992.38", + "picture": "http://placehold.it/32x32", + "age": 40, + "color": "green", + "name": "Christina Short", + "gender": "female", + "email": "christinashort@chorizon.com", + "phone": "+1 (884) 589-2705", + "address": "594 Willmohr Street, Dexter, Montana, 660", + "about": "Quis commodo eu dolor incididunt. Nisi magna mollit nostrud do consequat irure exercitation mollit aute deserunt. Magna aute quis occaecat incididunt deserunt tempor nostrud sint ullamco ipsum. Anim in occaecat exercitation laborum nostrud eiusmod reprehenderit ea culpa et sit. Culpa voluptate consectetur nostrud do eu fugiat excepteur officia pariatur enim duis amet.\r\n", + "registered": "2014-01-21T09:31:56 -01:00", + "latitude": -42.762739, + "longitude": 77.052349, + "tags": [ + "bug", + "new issue" + ] + }, + { + "id": 39, + "isActive": false, + "balance": "$1,722.85", + "picture": "http://placehold.it/32x32", + "age": 29, + "color": "brown", + "name": "Golden Horton", + "gender": "male", + "email": "goldenhorton@chorizon.com", + "phone": "+1 (903) 426-2489", + "address": "191 Schenck Avenue, Mayfair, North Dakota, 5000", + "about": "Cillum velit aliqua velit in quis do mollit in et veniam. Nostrud proident non irure commodo. Ea culpa duis enim adipisicing do sint et est culpa reprehenderit officia laborum. Non et nostrud tempor nostrud nostrud ea duis esse laboris occaecat laborum. In eu ipsum sit tempor esse eiusmod enim aliquip aute. Officia ea anim ea ea. Consequat aute deserunt tempor nulla nisi tempor velit.\r\n", + "registered": "2015-08-19T02:56:41 -02:00", + "latitude": 69.922534, + "longitude": 9.881433, + "tags": [ + "bug" + ] + }, + { + "id": 40, + "isActive": false, + "balance": "$1,656.54", + "picture": "http://placehold.it/32x32", + "age": 21, + "color": "blue", + "name": "Stafford Emerson", + "gender": "male", + "email": "staffordemerson@chorizon.com", + "phone": "+1 (992) 455-2573", + "address": "523 Thornton Street, Conway, Vermont, 6331", + "about": "Adipisicing cupidatat elit minim elit nostrud elit non eiusmod sunt ut. Enim minim irure officia irure occaecat mollit eu nostrud eiusmod adipisicing sunt. Elit deserunt commodo minim dolor qui. Nostrud officia ex proident mollit et dolor tempor pariatur. Ex consequat tempor eiusmod irure mollit cillum laboris est veniam ea mollit deserunt. Tempor sit voluptate excepteur elit ullamco.\r\n", + "registered": "2019-02-16T04:07:08 -01:00", + "latitude": -29.143111, + "longitude": -57.207703, + "tags": [ + "wontfix", + "good first issue", + "good first issue" + ] + }, + { + "id": 41, + "isActive": false, + "balance": "$1,861.56", + "picture": "http://placehold.it/32x32", + "age": 21, + "color": "brown", + "name": "Salinas Gamble", + "gender": "male", + "email": "salinasgamble@chorizon.com", + "phone": "+1 (901) 525-2373", + "address": "991 Nostrand Avenue, Kansas, Mississippi, 6756", + "about": "Consequat tempor adipisicing cupidatat aliquip. Mollit proident incididunt ad ipsum laborum. Dolor in elit minim aliquip aliquip voluptate reprehenderit mollit eiusmod excepteur aliquip minim nulla cupidatat.\r\n", + "registered": "2017-08-21T05:47:53 -02:00", + "latitude": -22.593819, + "longitude": -63.613004, + "tags": [ + "good first issue", + "bug", + "bug", + "wontfix" + ] + }, + { + "id": 42, + "isActive": true, + "balance": "$3,179.74", + "picture": "http://placehold.it/32x32", + "age": 34, + "color": "brown", + "name": "Graciela Russell", + "gender": "female", + "email": "gracielarussell@chorizon.com", + "phone": "+1 (893) 464-3951", + "address": "361 Greenpoint Avenue, Shrewsbury, New Jersey, 4713", + "about": "Ex amet duis incididunt consequat minim dolore deserunt reprehenderit adipisicing in mollit aliqua adipisicing sunt. In ullamco eu qui est eiusmod qui. Fugiat esse est Lorem dolore nisi mollit exercitation. Aliquip occaecat esse exercitation ex non aute velit excepteur duis aliquip id. Velit id non aliquip fugiat minim qui exercitation culpa tempor consectetur. Minim dolor labore ea aute aute eu.\r\n", + "registered": "2015-05-18T09:52:56 -02:00", + "latitude": -14.634444, + "longitude": 12.931783, + "tags": [ + "wontfix", + "bug", + "wontfix" + ] + }, + { + "id": 43, + "isActive": true, + "balance": "$1,777.38", + "picture": "http://placehold.it/32x32", + "age": 25, + "color": "blue", + "name": "Arnold Bender", + "gender": "male", + "email": "arnoldbender@chorizon.com", + "phone": "+1 (945) 581-3808", + "address": "781 Lorraine Street, Gallina, American Samoa, 1832", + "about": "Et mollit laboris duis ut duis eiusmod aute laborum duis irure labore deserunt. Ut occaecat ullamco quis excepteur. Et commodo non sint laboris tempor laboris aliqua consequat magna ea aute minim tempor pariatur. Dolore occaecat qui irure Lorem nulla consequat non.\r\n", + "registered": "2018-12-23T02:26:30 -01:00", + "latitude": 41.208579, + "longitude": 51.948925, + "tags": [ + "bug", + "good first issue", + "good first issue", + "wontfix" + ] + }, + { + "id": 44, + "isActive": true, + "balance": "$2,893.45", + "picture": "http://placehold.it/32x32", + "age": 22, + "color": "green", + "name": "Joni Spears", + "gender": "female", + "email": "jonispears@chorizon.com", + "phone": "+1 (916) 565-2124", + "address": "307 Harwood Place, Canterwood, Maryland, 2047", + "about": "Dolore consequat deserunt aliquip duis consequat minim occaecat enim est. Nulla aute reprehenderit est enim duis cillum ullamco aliquip eiusmod sunt. Labore eiusmod aliqua Lorem velit aliqua quis ex mollit mollit duis culpa et qui in. Cupidatat est id ullamco irure dolor nulla.\r\n", + "registered": "2015-03-01T12:38:28 -01:00", + "latitude": 8.19071, + "longitude": 146.323808, + "tags": [ + "wontfix", + "new issue", + "good first issue", + "good first issue" + ] + }, + { + "id": 45, + "isActive": true, + "balance": "$2,830.36", + "picture": "http://placehold.it/32x32", + "age": 20, + "color": "brown", + "name": "Irene Bennett", + "gender": "female", + "email": "irenebennett@chorizon.com", + "phone": "+1 (904) 431-2211", + "address": "353 Ridgecrest Terrace, Springdale, Marshall Islands, 2686", + "about": "Consectetur Lorem dolor reprehenderit sunt duis. Pariatur non velit velit veniam elit reprehenderit in. Aute quis Lorem quis pariatur Lorem incididunt nulla magna adipisicing. Et id occaecat labore officia occaecat occaecat adipisicing.\r\n", + "registered": "2018-04-17T05:18:51 -02:00", + "latitude": -36.435177, + "longitude": -127.552573, + "tags": [ + "bug", + "wontfix" + ] + }, + { + "id": 46, + "isActive": true, + "balance": "$1,348.04", + "picture": "http://placehold.it/32x32", + "age": 34, + "color": "green", + "name": "Lawson Curtis", + "gender": "male", + "email": "lawsoncurtis@chorizon.com", + "phone": "+1 (896) 532-2172", + "address": "942 Gerritsen Avenue, Southmont, Kansas, 8915", + "about": "Amet consectetur minim aute nostrud excepteur sint labore in culpa. Mollit qui quis ea amet sint ex incididunt nulla. Elit id esse ea consectetur laborum consequat occaecat aute consectetur ex. Commodo duis aute elit occaecat cupidatat non consequat ad officia qui dolore nostrud reprehenderit. Occaecat velit velit adipisicing exercitation consectetur. Incididunt et amet nostrud tempor do esse ullamco est Lorem irure. Eu aliqua eu exercitation sint.\r\n", + "registered": "2016-08-23T01:41:09 -02:00", + "latitude": -48.783539, + "longitude": 20.492944, + "tags": [] + }, + { + "id": 47, + "isActive": true, + "balance": "$1,132.41", + "picture": "http://placehold.it/32x32", + "age": 38, + "color": "green", + "name": "Goff May", + "gender": "male", + "email": "goffmay@chorizon.com", + "phone": "+1 (859) 453-3415", + "address": "225 Rutledge Street, Boonville, Massachusetts, 4081", + "about": "Sint occaecat velit anim sint reprehenderit est. Adipisicing ea pariatur amet id non ex. Aute id laborum tempor aliquip magna ex eu incididunt aliquip eiusmod elit quis dolor. Anim est minim deserunt amet exercitation nulla elit nulla nulla culpa ullamco. Velit consectetur ipsum amet proident labore excepteur ut id excepteur voluptate commodo. Exercitation et laboris labore esse est laboris consectetur et sint.\r\n", + "registered": "2014-10-25T07:32:30 -02:00", + "latitude": 13.079225, + "longitude": 76.215086, + "tags": [ + "bug" + ] + }, + { + "id": 48, + "isActive": true, + "balance": "$1,201.87", + "picture": "http://placehold.it/32x32", + "age": 38, + "color": "green", + "name": "Goodman Becker", + "gender": "male", + "email": "goodmanbecker@chorizon.com", + "phone": "+1 (825) 470-3437", + "address": "388 Seigel Street, Sisquoc, Kentucky, 8231", + "about": "Velit excepteur aute esse fugiat laboris aliqua magna. Est ex sit do labore ullamco aliquip. Duis ea commodo nostrud in fugiat. Aliqua consequat mollit dolore excepteur nisi ullamco commodo ea nostrud ea minim. Minim occaecat ut laboris ea consectetur veniam ipsum qui sit tempor incididunt anim amet eu. Velit sint incididunt eu adipisicing ipsum qui labore. Anim commodo labore reprehenderit aliquip labore elit minim deserunt amet exercitation officia non ea consectetur.\r\n", + "registered": "2019-09-05T04:49:03 -02:00", + "latitude": -23.792094, + "longitude": -13.621221, + "tags": [ + "bug", + "bug", + "wontfix", + "new issue" + ] + }, + { + "id": 49, + "isActive": true, + "balance": "$1,476.39", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "brown", + "name": "Maureen Dale", + "gender": "female", + "email": "maureendale@chorizon.com", + "phone": "+1 (984) 538-3684", + "address": "817 Newton Street, Bannock, Wyoming, 1468", + "about": "Tempor mollit exercitation excepteur cupidatat reprehenderit ad ex. Nulla laborum proident incididunt quis. Esse laborum deserunt qui anim. Sunt incididunt pariatur cillum anim proident eu ullamco dolor excepteur. Ullamco amet culpa nostrud adipisicing duis aliqua consequat duis non eu id mollit velit. Deserunt ullamco amet in occaecat.\r\n", + "registered": "2018-04-26T06:04:40 -02:00", + "latitude": -64.196802, + "longitude": -117.396238, + "tags": [ + "wontfix" + ] + }, + { + "id": 50, + "isActive": true, + "balance": "$1,947.08", + "picture": "http://placehold.it/32x32", + "age": 21, + "color": "green", + "name": "Guerra Mcintyre", + "gender": "male", + "email": "guerramcintyre@chorizon.com", + "phone": "+1 (951) 536-2043", + "address": "423 Lombardy Street, Stewart, West Virginia, 908", + "about": "Sunt proident proident deserunt exercitation consectetur deserunt labore non commodo amet. Duis aute aliqua amet deserunt consectetur velit. Quis Lorem dolore occaecat deserunt reprehenderit non esse ullamco nostrud enim sunt ea fugiat. Elit amet veniam eu magna tempor. Mollit cupidatat laboris ex deserunt et labore sit tempor nostrud anim. Tempor aliqua occaecat voluptate reprehenderit eiusmod aliqua incididunt officia.\r\n", + "registered": "2015-07-16T05:11:42 -02:00", + "latitude": 79.733743, + "longitude": -20.602356, + "tags": [ + "bug", + "good first issue", + "good first issue" + ] + }, + { + "id": 51, + "isActive": true, + "balance": "$2,960.90", + "picture": "http://placehold.it/32x32", + "age": 23, + "color": "blue", + "name": "Key Cervantes", + "gender": "male", + "email": "keycervantes@chorizon.com", + "phone": "+1 (931) 474-3865", + "address": "410 Barbey Street, Vernon, Oregon, 2328", + "about": "Duis amet minim eu consectetur laborum ad exercitation eiusmod nulla velit cillum consectetur. Nostrud aliqua cillum minim veniam quis do cupidatat mollit laborum. Culpa fugiat consectetur cillum non occaecat tempor non fugiat esse pariatur in ullamco. Occaecat amet officia et culpa officia deserunt in qui magna aute consequat eiusmod.\r\n", + "registered": "2019-12-15T12:13:35 -01:00", + "latitude": 47.627647, + "longitude": 117.049918, + "tags": [ + "new issue" + ] + }, + { + "id": 52, + "isActive": false, + "balance": "$1,884.02", + "picture": "http://placehold.it/32x32", + "age": 35, + "color": "blue", + "name": "Karen Nelson", + "gender": "female", + "email": "karennelson@chorizon.com", + "phone": "+1 (993) 528-3607", + "address": "930 Frank Court, Dunbar, New York, 8810", + "about": "Occaecat officia veniam consectetur aliqua laboris dolor irure nulla. Lorem ipsum sit nisi veniam mollit ea sint nisi irure. Eiusmod officia do laboris nostrud enim ullamco nulla officia in Lorem qui. Sint sunt incididunt quis reprehenderit incididunt. Sit dolore nulla consequat ea magna.\r\n", + "registered": "2014-06-23T09:21:44 -02:00", + "latitude": -59.059033, + "longitude": 76.565373, + "tags": [ + "new issue", + "bug" + ] + }, + { + "id": 53, + "isActive": true, + "balance": "$3,559.55", + "picture": "http://placehold.it/32x32", + "age": 32, + "color": "brown", + "name": "Caitlin Burnett", + "gender": "female", + "email": "caitlinburnett@chorizon.com", + "phone": "+1 (945) 480-2796", + "address": "516 Senator Street, Emory, Iowa, 4145", + "about": "In aliqua ea esse in. Magna aute cupidatat culpa enim proident ad adipisicing laborum consequat exercitation nisi. Qui esse aliqua duis anim nulla esse enim nostrud ipsum tempor. Lorem deserunt ullamco do mollit culpa ipsum duis Lorem velit duis occaecat.\r\n", + "registered": "2019-01-09T02:26:31 -01:00", + "latitude": -82.774237, + "longitude": 42.316194, + "tags": [ + "bug", + "good first issue" + ] + }, + { + "id": 54, + "isActive": true, + "balance": "$2,113.29", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "green", + "name": "Richards Walls", + "gender": "male", + "email": "richardswalls@chorizon.com", + "phone": "+1 (865) 517-2982", + "address": "959 Brightwater Avenue, Stevens, Nevada, 2968", + "about": "Ad aute Lorem non pariatur anim ullamco ad amet eiusmod tempor velit. Mollit et tempor nisi aute adipisicing exercitation mollit do amet amet est fugiat enim. Ex voluptate nulla id tempor officia ullamco cillum dolor irure irure mollit et magna nisi. Pariatur voluptate qui laboris dolor id. Eu ipsum nulla dolore aute voluptate deserunt anim aliqua. Ut enim enim velit officia est nisi. Duis amet ut veniam aliquip minim tempor Lorem amet Lorem dolor duis.\r\n", + "registered": "2014-09-25T06:51:22 -02:00", + "latitude": 80.09202, + "longitude": 87.49759, + "tags": [ + "wontfix", + "wontfix", + "bug" + ] + }, + { + "id": 55, + "isActive": true, + "balance": "$1,977.66", + "picture": "http://placehold.it/32x32", + "age": 36, + "color": "brown", + "name": "Combs Stanley", + "gender": "male", + "email": "combsstanley@chorizon.com", + "phone": "+1 (827) 419-2053", + "address": "153 Beverley Road, Siglerville, South Carolina, 3666", + "about": "Commodo ullamco consequat eu ipsum eiusmod aute voluptate in. Ea laboris id deserunt nostrud pariatur et laboris minim tempor quis qui consequat non esse. Magna elit commodo mollit veniam Lorem enim nisi pariatur. Nisi non nisi adipisicing ea ipsum laborum dolore cillum. Amet do nisi esse laboris ipsum proident non veniam ullamco ea cupidatat sunt. Aliquip aute cillum quis laboris consectetur enim eiusmod nisi non id ullamco cupidatat sunt.\r\n", + "registered": "2019-08-22T07:53:15 -02:00", + "latitude": 78.386181, + "longitude": 143.661058, + "tags": [] + }, + { + "id": 56, + "isActive": false, + "balance": "$3,886.12", + "picture": "http://placehold.it/32x32", + "age": 23, + "color": "brown", + "name": "Tucker Barry", + "gender": "male", + "email": "tuckerbarry@chorizon.com", + "phone": "+1 (808) 544-3433", + "address": "805 Jamaica Avenue, Cornfields, Minnesota, 3689", + "about": "Enim est sunt ullamco nulla aliqua commodo. Enim minim veniam non fugiat id tempor ad velit quis velit ad sunt consectetur laborum. Cillum deserunt tempor est adipisicing Lorem esse qui. Magna quis sunt cillum ea officia adipisicing eiusmod eu et nisi consectetur.\r\n", + "registered": "2016-08-29T07:28:00 -02:00", + "latitude": 71.701551, + "longitude": 9.903068, + "tags": [] + }, + { + "id": 57, + "isActive": false, + "balance": "$1,844.56", + "picture": "http://placehold.it/32x32", + "age": 20, + "color": "green", + "name": "Kaitlin Conner", + "gender": "female", + "email": "kaitlinconner@chorizon.com", + "phone": "+1 (862) 467-2666", + "address": "501 Knight Court, Joppa, Rhode Island, 274", + "about": "Occaecat id reprehenderit pariatur ea. Incididunt laborum reprehenderit ipsum velit labore excepteur nostrud voluptate officia ut culpa. Sint sunt in qui duis cillum aliqua do ullamco. Non do aute excepteur non labore sint consectetur tempor ad ea fugiat commodo labore. Dolor tempor culpa Lorem voluptate esse nostrud anim tempor irure reprehenderit. Deserunt ipsum cillum fugiat ut labore labore anim. In aliqua sunt dolore irure reprehenderit voluptate commodo consequat mollit amet laboris sit anim.\r\n", + "registered": "2019-05-30T06:38:24 -02:00", + "latitude": 15.613464, + "longitude": 171.965629, + "tags": [] + }, + { + "id": 58, + "isActive": true, + "balance": "$2,876.10", + "picture": "http://placehold.it/32x32", + "age": 38, + "color": "green", + "name": "Mamie Fischer", + "gender": "female", + "email": "mamiefischer@chorizon.com", + "phone": "+1 (948) 545-3901", + "address": "599 Hunterfly Place, Haena, Georgia, 6005", + "about": "Cillum eu aliquip ipsum anim in dolore labore ea. Laboris velit esse ea ea aute do adipisicing ullamco elit laborum aute tempor. Esse consectetur quis irure occaecat nisi cillum et consectetur cillum cillum quis quis commodo.\r\n", + "registered": "2019-05-27T05:07:10 -02:00", + "latitude": 70.915079, + "longitude": -48.813584, + "tags": [ + "bug", + "wontfix", + "wontfix", + "good first issue" + ] + }, + { + "id": 59, + "isActive": true, + "balance": "$1,921.58", + "picture": "http://placehold.it/32x32", + "age": 31, + "color": "green", + "name": "Harper Carson", + "gender": "male", + "email": "harpercarson@chorizon.com", + "phone": "+1 (912) 430-3243", + "address": "883 Dennett Place, Knowlton, New Mexico, 9219", + "about": "Exercitation minim esse proident cillum velit et deserunt incididunt adipisicing minim. Cillum Lorem consectetur laborum id consequat exercitation velit. Magna dolor excepteur sunt deserunt dolor ullamco non sint proident ipsum. Reprehenderit voluptate sit veniam consectetur ea sunt duis labore deserunt ipsum aute. Eiusmod aliqua anim voluptate id duis tempor aliqua commodo sunt. Do officia ea consectetur nostrud eiusmod laborum.\r\n", + "registered": "2019-12-07T07:33:15 -01:00", + "latitude": -60.812605, + "longitude": -27.129016, + "tags": [ + "bug", + "new issue" + ] + }, + { + "id": 60, + "isActive": true, + "balance": "$1,770.93", + "picture": "http://placehold.it/32x32", + "age": 23, + "color": "brown", + "name": "Jody Herrera", + "gender": "female", + "email": "jodyherrera@chorizon.com", + "phone": "+1 (890) 583-3222", + "address": "261 Jay Street, Strykersville, Ohio, 9248", + "about": "Sit adipisicing pariatur irure non sint cupidatat ex ipsum pariatur exercitation ea. Enim consequat enim eu eu sint eu elit ex esse aliquip. Pariatur ipsum dolore veniam nisi id tempor elit exercitation dolore ad fugiat labore velit.\r\n", + "registered": "2016-05-21T01:00:02 -02:00", + "latitude": -36.846586, + "longitude": 131.156223, + "tags": [] + }, + { + "id": 61, + "isActive": false, + "balance": "$2,813.41", + "picture": "http://placehold.it/32x32", + "age": 37, + "color": "green", + "name": "Charles Castillo", + "gender": "male", + "email": "charlescastillo@chorizon.com", + "phone": "+1 (934) 467-2108", + "address": "675 Morton Street, Rew, Pennsylvania, 137", + "about": "Velit amet laborum amet sunt sint sit cupidatat deserunt dolor laborum consectetur veniam. Minim cupidatat amet exercitation nostrud ex deserunt ad Lorem amet aute consectetur labore reprehenderit. Minim mollit aliqua et deserunt ex nisi. Id irure dolor labore consequat ipsum consectetur.\r\n", + "registered": "2019-06-10T02:54:22 -02:00", + "latitude": -16.423202, + "longitude": -146.293752, + "tags": [ + "new issue", + "new issue" + ] + }, + { + "id": 62, + "isActive": true, + "balance": "$3,341.35", + "picture": "http://placehold.it/32x32", + "age": 33, + "color": "blue", + "name": "Estelle Ramirez", + "gender": "female", + "email": "estelleramirez@chorizon.com", + "phone": "+1 (816) 459-2073", + "address": "636 Nolans Lane, Camptown, California, 7794", + "about": "Dolor proident incididunt ex labore quis ullamco duis. Sit esse laboris nisi eu voluptate nulla cupidatat nulla fugiat veniam. Culpa cillum est esse dolor consequat. Pariatur ex sit irure qui do fugiat. Fugiat culpa veniam est nisi excepteur quis cupidatat et minim in esse minim dolor et. Anim aliquip labore dolor occaecat nisi sunt dolore pariatur veniam nostrud est ut.\r\n", + "registered": "2015-02-14T01:05:50 -01:00", + "latitude": -46.591249, + "longitude": -83.385587, + "tags": [ + "good first issue", + "bug" + ] + }, + { + "id": 63, + "isActive": true, + "balance": "$2,478.30", + "picture": "http://placehold.it/32x32", + "age": 21, + "color": "blue", + "name": "Knowles Hebert", + "gender": "male", + "email": "knowleshebert@chorizon.com", + "phone": "+1 (819) 409-2308", + "address": "361 Kathleen Court, Gratton, Connecticut, 7254", + "about": "Esse mollit nulla eiusmod esse duis non proident excepteur labore. Nisi ex culpa do mollit dolor ea deserunt elit anim ipsum nostrud. Cupidatat nostrud duis ipsum dolore amet et. Veniam in cillum ea cillum deserunt excepteur officia laboris nulla. Commodo incididunt aliquip qui sunt dolore occaecat labore do laborum irure. Labore culpa duis pariatur reprehenderit ad laboris occaecat anim cillum et fugiat ea.\r\n", + "registered": "2016-03-08T08:34:52 -01:00", + "latitude": 71.042482, + "longitude": 152.460406, + "tags": [ + "good first issue", + "wontfix" + ] + }, + { + "id": 64, + "isActive": false, + "balance": "$2,559.09", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "brown", + "name": "Thelma Mckenzie", + "gender": "female", + "email": "thelmamckenzie@chorizon.com", + "phone": "+1 (941) 596-2777", + "address": "202 Leonard Street, Riverton, Illinois, 8577", + "about": "Non ad ipsum elit commodo fugiat Lorem ipsum reprehenderit. Commodo incididunt officia cillum eiusmod officia proident ea incididunt ullamco magna commodo consectetur dolor. Nostrud esse nisi ea laboris. Veniam et dolore nulla excepteur pariatur laborum non. Eiusmod reprehenderit do tempor esse eu eu aliquip. Magna quis consectetur ipsum adipisicing mollit elit ad elit.\r\n", + "registered": "2020-04-14T12:43:06 -02:00", + "latitude": 16.026129, + "longitude": 105.464476, + "tags": [] + }, + { + "id": 65, + "isActive": true, + "balance": "$1,025.08", + "picture": "http://placehold.it/32x32", + "age": 34, + "color": "blue", + "name": "Carole Rowland", + "gender": "female", + "email": "carolerowland@chorizon.com", + "phone": "+1 (862) 558-3448", + "address": "941 Melba Court, Bluetown, Florida, 9555", + "about": "Ullamco occaecat ipsum aliqua sit proident eu. Occaecat ut consectetur proident culpa aliqua excepteur quis qui anim irure sit proident mollit irure. Proident cupidatat deserunt dolor adipisicing.\r\n", + "registered": "2014-12-01T05:55:35 -01:00", + "latitude": -0.191998, + "longitude": 43.389652, + "tags": [ + "wontfix" + ] + }, + { + "id": 66, + "isActive": true, + "balance": "$1,061.49", + "picture": "http://placehold.it/32x32", + "age": 35, + "color": "brown", + "name": "Higgins Aguilar", + "gender": "male", + "email": "higginsaguilar@chorizon.com", + "phone": "+1 (911) 540-3791", + "address": "132 Sackman Street, Layhill, Guam, 8729", + "about": "Anim ea dolore exercitation minim. Proident cillum non deserunt cupidatat veniam non occaecat aute ullamco irure velit laboris ex aliquip. Voluptate incididunt non ex nulla est ipsum. Amet anim do velit sunt irure sint minim nisi occaecat proident tempor elit exercitation nostrud.\r\n", + "registered": "2015-04-05T02:10:07 -02:00", + "latitude": 74.702813, + "longitude": 151.314972, + "tags": [ + "bug" + ] + }, + { + "id": 67, + "isActive": true, + "balance": "$3,510.14", + "picture": "http://placehold.it/32x32", + "age": 28, + "color": "brown", + "name": "Ilene Gillespie", + "gender": "female", + "email": "ilenegillespie@chorizon.com", + "phone": "+1 (937) 575-2676", + "address": "835 Lake Street, Naomi, Alabama, 4131", + "about": "Quis laborum consequat id cupidatat exercitation aute ad ex nulla dolore velit qui proident minim. Et do consequat nisi eiusmod exercitation exercitation enim voluptate elit ullamco. Cupidatat ut adipisicing consequat aute est voluptate sit ipsum culpa ullamco. Ex pariatur ex qui quis qui.\r\n", + "registered": "2015-06-28T09:41:45 -02:00", + "latitude": 71.573342, + "longitude": -95.295989, + "tags": [ + "wontfix", + "wontfix" + ] + }, + { + "id": 68, + "isActive": false, + "balance": "$1,539.98", + "picture": "http://placehold.it/32x32", + "age": 24, + "color": "green", + "name": "Angelina Dyer", + "gender": "female", + "email": "angelinadyer@chorizon.com", + "phone": "+1 (948) 574-3949", + "address": "575 Division Place, Gorham, Louisiana, 3458", + "about": "Cillum magna eu est veniam incididunt laboris laborum elit mollit incididunt proident non mollit. Dolor mollit culpa ullamco dolore aliqua adipisicing culpa officia. Reprehenderit minim nisi fugiat consectetur dolore.\r\n", + "registered": "2014-07-08T06:34:36 -02:00", + "latitude": -85.649593, + "longitude": 66.126018, + "tags": [ + "good first issue" + ] + }, + { + "id": 69, + "isActive": true, + "balance": "$3,367.69", + "picture": "http://placehold.it/32x32", + "age": 30, + "color": "brown", + "name": "Marks Burt", + "gender": "male", + "email": "marksburt@chorizon.com", + "phone": "+1 (895) 497-3138", + "address": "819 Village Road, Wadsworth, Delaware, 6099", + "about": "Fugiat tempor aute voluptate proident exercitation tempor esse dolor id. Duis aliquip exercitation Lorem elit magna sint sit. Culpa adipisicing occaecat aliqua officia reprehenderit laboris sint aliquip. Magna do sunt consequat excepteur nisi do commodo non. Cillum officia nostrud consequat excepteur elit proident in. Tempor ipsum in ut qui cupidatat exercitation est nulla exercitation voluptate.\r\n", + "registered": "2014-08-31T06:12:18 -02:00", + "latitude": 26.854112, + "longitude": -143.313948, + "tags": [ + "good first issue" + ] + }, + { + "id": 70, + "isActive": false, + "balance": "$3,755.72", + "picture": "http://placehold.it/32x32", + "age": 23, + "color": "blue", + "name": "Glass Perkins", + "gender": "male", + "email": "glassperkins@chorizon.com", + "phone": "+1 (923) 486-3725", + "address": "899 Roosevelt Court, Belleview, Idaho, 1737", + "about": "Esse magna id labore sunt qui eu enim esse cillum consequat enim eu culpa enim. Duis veniam cupidatat deserunt sunt irure ad Lorem proident aliqua mollit. Laborum mollit aute nulla est. Sunt id proident incididunt ipsum et dolor consectetur laborum enim dolor officia dolore laborum. Est commodo duis et ea consequat labore id id eu aliqua. Qui veniam sit eu aliquip ad sit dolor ullamco et laborum voluptate quis fugiat ex. Exercitation dolore cillum amet ad nisi consectetur occaecat sit aliqua laborum qui proident aliqua exercitation.\r\n", + "registered": "2015-05-22T05:44:33 -02:00", + "latitude": 54.27147, + "longitude": -65.065604, + "tags": [ + "wontfix" + ] + }, + { + "id": 71, + "isActive": true, + "balance": "$3,381.63", + "picture": "http://placehold.it/32x32", + "age": 38, + "color": "green", + "name": "Candace Sawyer", + "gender": "female", + "email": "candacesawyer@chorizon.com", + "phone": "+1 (830) 404-2636", + "address": "334 Arkansas Drive, Bordelonville, Tennessee, 8449", + "about": "Et aliqua elit incididunt et aliqua. Deserunt ut elit proident ullamco ut. Ex exercitation amet non eu reprehenderit ea voluptate qui sit reprehenderit ad sint excepteur.\r\n", + "registered": "2014-04-04T08:45:00 -02:00", + "latitude": 6.484262, + "longitude": -37.054928, + "tags": [ + "new issue", + "new issue" + ] + }, + { + "id": 72, + "isActive": true, + "balance": "$1,640.98", + "picture": "http://placehold.it/32x32", + "age": 27, + "color": "green", + "name": "Hendricks Martinez", + "gender": "male", + "email": "hendricksmartinez@chorizon.com", + "phone": "+1 (857) 566-3245", + "address": "636 Agate Court, Newry, Utah, 3304", + "about": "Do sit culpa amet incididunt officia enim occaecat incididunt excepteur enim tempor deserunt qui. Excepteur adipisicing anim consectetur adipisicing proident anim laborum qui. Aliquip nostrud cupidatat sit ullamco.\r\n", + "registered": "2018-06-15T10:36:11 -02:00", + "latitude": 86.746034, + "longitude": 10.347893, + "tags": [ + "new issue" + ] + }, + { + "id": 73, + "isActive": false, + "balance": "$1,239.74", + "picture": "http://placehold.it/32x32", + "age": 38, + "color": "blue", + "name": "Eleanor Shepherd", + "gender": "female", + "email": "eleanorshepherd@chorizon.com", + "phone": "+1 (894) 567-2617", + "address": "670 Lafayette Walk, Darlington, Palau, 8803", + "about": "Adipisicing ad incididunt id veniam magna cupidatat et labore eu deserunt mollit. Lorem voluptate exercitation elit eu aliquip cupidatat occaecat anim excepteur reprehenderit est est. Ipsum excepteur ea mollit qui nisi laboris ex qui. Cillum velit culpa culpa commodo laboris nisi Lorem non elit deserunt incididunt. Officia quis velit nulla sint incididunt duis mollit tempor adipisicing qui officia eu nisi Lorem. Do proident pariatur ex enim nostrud eu aute esse deserunt eu velit quis culpa exercitation. Occaecat ad cupidatat ullamco consequat duis anim deserunt occaecat aliqua sunt consectetur ipsum magna.\r\n", + "registered": "2020-02-29T12:15:28 -01:00", + "latitude": 35.749621, + "longitude": -94.40842, + "tags": [ + "good first issue", + "new issue", + "new issue", + "bug" + ] + }, + { + "id": 74, + "isActive": true, + "balance": "$1,180.90", + "picture": "http://placehold.it/32x32", + "age": 36, + "color": "green", + "name": "Stark Wong", + "gender": "male", + "email": "starkwong@chorizon.com", + "phone": "+1 (805) 575-3055", + "address": "522 Bond Street, Bawcomville, Wisconsin, 324", + "about": "Aute qui sit incididunt eu adipisicing exercitation sunt nostrud. Id laborum incididunt proident ipsum est cillum esse. Officia ullamco eu ut Lorem do minim ea dolor consequat sit eu est voluptate. Id commodo cillum enim culpa aliquip ullamco nisi Lorem cillum ipsum cupidatat anim officia eu. Dolore sint elit labore pariatur. Officia duis nulla voluptate et nulla ut voluptate laboris eu commodo veniam qui veniam.\r\n", + "registered": "2020-01-25T10:47:48 -01:00", + "latitude": -80.452139, + "longitude": 160.72546, + "tags": [ + "wontfix" + ] + }, + { + "id": 75, + "isActive": false, + "balance": "$1,913.42", + "picture": "http://placehold.it/32x32", + "age": 24, + "color": "green", + "name": "Emma Jacobs", + "gender": "female", + "email": "emmajacobs@chorizon.com", + "phone": "+1 (899) 554-3847", + "address": "173 Tapscott Street, Esmont, Maine, 7450", + "about": "Laboris consequat consectetur tempor labore ullamco ullamco voluptate quis quis duis ut ad. In est irure quis amet sunt nulla ad ut sit labore ut eu quis duis. Nostrud cupidatat aliqua sunt occaecat minim id consequat officia deserunt laborum. Ea dolor reprehenderit laborum veniam exercitation est nostrud excepteur laborum minim id qui et.\r\n", + "registered": "2019-03-29T06:24:13 -01:00", + "latitude": -35.53722, + "longitude": 155.703874, + "tags": [] + }, + { + "id": 76, + "isActive": false, + "balance": "$1,274.29", + "picture": "http://placehold.it/32x32", + "age": 25, + "color": "green", + "name": "Clarice Gardner", + "gender": "female", + "email": "claricegardner@chorizon.com", + "phone": "+1 (810) 407-3258", + "address": "894 Brooklyn Road, Utting, New Hampshire, 6404", + "about": "Elit occaecat aute ea adipisicing mollit cupidatat aliquip excepteur veniam minim. Sunt quis dolore in commodo aute esse quis. Lorem in cillum commodo eu anim commodo mollit. Adipisicing enim sunt adipisicing cupidatat adipisicing eiusmod eu do sit nisi.\r\n", + "registered": "2014-10-20T10:13:32 -02:00", + "latitude": 17.11935, + "longitude": 65.38197, + "tags": [ + "new issue", + "wontfix" + ] + } +] diff --git a/meilisearch-http/tests/common.rs b/meilisearch-http/tests/common.rs index 63dfdaef4..731d69987 100644 --- a/meilisearch-http/tests/common.rs +++ b/meilisearch-http/tests/common.rs @@ -39,6 +39,74 @@ impl Server { } } + pub async fn test_server() -> Self { + + let mut server = Self::with_uid("test"); + + let body = json!({ + "uid": "test", + "primaryKey": "id", + }); + + server.create_index(body).await; + + let body = json!({ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness", + ], + "searchableAttributes": [ + "balance", + "picture", + "age", + "color", + "name", + "gender", + "email", + "phone", + "address", + "about", + "registered", + "latitude", + "longitude", + "tags", + ], + "displayedAttributes": [ + "id", + "isActive", + "balance", + "picture", + "age", + "color", + "name", + "gender", + "email", + "phone", + "address", + "about", + "registered", + "latitude", + "longitude", + "tags", + ], + "acceptNewFields": false, + }); + + server.update_all_settings(body).await; + + let dataset = include_bytes!("assets/test_set.json"); + + let body: Value = serde_json::from_slice(dataset).unwrap(); + + server.add_or_replace_multiple_documents(body).await; + server + } + + pub async fn wait_update_id(&mut self, update_id: u64) { loop { let (response, status_code) = self.get_update_status(update_id).await; @@ -90,6 +158,7 @@ impl Server { eprintln!("post_request_async: {}", url); let (response, status_code) = self.post_request(url, body).await; + eprintln!("response: {}", response); assert_eq!(status_code, 202); assert!(response["updateId"].as_u64().is_some()); self.wait_update_id(response["updateId"].as_u64().unwrap()) diff --git a/meilisearch-http/tests/search.rs b/meilisearch-http/tests/search.rs index c16cdc469..635f46560 100644 --- a/meilisearch-http/tests/search.rs +++ b/meilisearch-http/tests/search.rs @@ -2,6 +2,7 @@ use std::convert::Into; use assert_json_diff::assert_json_eq; use serde_json::json; +use serde_json::Value; mod common; @@ -1133,3 +1134,161 @@ async fn search_with_differents_attributes_8() { let (response, _status_code) = server.search(query).await; assert_json_eq!(expected, response["hits"].clone(), ordered: false); } + +#[actix_rt::test] +async fn test_faceted_search_valid() { + let mut server = common::Server::test_server().await; + + // simple tests on attributes with string value + let body = json!({ + "attributesForFaceting": ["color"] + }); + server.update_all_settings(body).await; + let query = "q=a&facetFilters=%5B%22color%3Agreen%22%5D"; + let (response, _status_code) = server.search(query).await; + assert!(!response.get("hits").unwrap().as_array().unwrap().is_empty()); + assert!(response + .get("hits") + .unwrap() + .as_array() + .unwrap() + .iter() + .all(|value| value.get("color").unwrap() == "green")); + let query = "q=a&facetFilters=%5B%22color%3Ablue%22%5D"; + let (response, _status_code) = server.search(query).await; + assert!(!response.get("hits").unwrap().as_array().unwrap().is_empty()); + assert!(response + .get("hits") + .unwrap() + .as_array() + .unwrap() + .iter() + .all(|value| value.get("color").unwrap() == "blue")); + + // test case insensitive : ["color:Blue"] + let query = "q=a&facetFilters=%5B%22color%3ABlue%22%5D"; + let (response, _status_code) = server.search(query).await; + assert!(!response.get("hits").unwrap().as_array().unwrap().is_empty()); + assert!(response + .get("hits") + .unwrap() + .as_array() + .unwrap() + .iter() + .all(|value| value.get("color").unwrap() == "blue")); + + // test on arrays: ["tags:bug"] + let body = json!({ + "attributesForFaceting": ["color", "tags"] + }); + server.update_all_settings(body).await; + + let query = "q=a&facetFilters=%5B%22tags%3Abug%22%5D"; + let (response, _status_code) = server.search(query).await; + assert!(!response.get("hits").unwrap().as_array().unwrap().is_empty()); + assert!(response + .get("hits") + .unwrap() + .as_array() + .unwrap() + .iter() + .all(|value| value.get("tags").unwrap().as_array().unwrap().contains(&Value::String("bug".to_owned())))); + + // test and: ["color:blue", "tags:bug"] + let query = "q=a&facetFilters=%5B%22color%3Ablue%22,%20%22tags%3Abug%22%20%5D"; + let (response, _status_code) = server.search(query).await; + assert!(!response.get("hits").unwrap().as_array().unwrap().is_empty()); + assert!(response + .get("hits") + .unwrap() + .as_array() + .unwrap() + .iter() + .all(|value| value + .get("color") + .unwrap() == "blue" + && value.get("tags").unwrap().as_array().unwrap().contains(&Value::String("bug".to_owned())))); + + // test or: [["color:blue", "color:green"]] + let query = "q=a&facetFilters=%5B%5B%22color%3Ablue%22,%20%22color%3Agreen%22%5D%5D"; + let (response, _status_code) = server.search(query).await; + assert!(!response.get("hits").unwrap().as_array().unwrap().is_empty()); + assert!(response + .get("hits") + .unwrap() + .as_array() + .unwrap() + .iter() + .all(|value| + value + .get("color") + .unwrap() == "blue" + || value + .get("color") + .unwrap() == "green")); + // test and-or: ["tags:bug", ["color:blue", "color:green"]] + let query = "q=a&facetFilters=%5B%22color%3Ablue%22,%20%22tags%3Abug%22%20%5D"; + let (response, _status_code) = server.search(query).await; + assert!(!response.get("hits").unwrap().as_array().unwrap().is_empty()); + assert!(response + .get("hits") + .unwrap() + .as_array() + .unwrap() + .iter() + .all(|value| + value + .get("tags") + .unwrap() + .as_array() + .unwrap() + .contains(&Value::String("bug".to_owned())) + && (value + .get("color") + .unwrap() == "blue" + || value + .get("color") + .unwrap() == "green"))); +} + +#[actix_rt::test] +async fn test_faceted_search_invalid() { + let mut server = common::Server::test_server().await; + + //no faceted attributes set + let query = "q=a&facetFilters=%5B%22color%3Ablue%22,%20%22tags%3Abug%22%20%5D"; + let (_response, status_code) = server.search(query).await; + assert_ne!(status_code, 202); + + let body = json!({ + "attributesForFaceting": ["color", "tags"] + }); + server.update_all_settings(body).await; + // empty arrays are error + // [] + let query = "q=a&facetFilters=%5B%5D"; + let (_response, status_code) = server.search(query).await; + assert_ne!(status_code, 202); + // [[]] + let query = "q=a&facetFilters=%5B%5B%5D"; + let (_response, status_code) = server.search(query).await; + assert_ne!(status_code, 202); + // ["color:green", []] + let query = "q=a&facetFilters=%5B%22color%3Agreen%22,%20%5B%5D"; + let (_response, status_code) = server.search(query).await; + assert_ne!(status_code, 202); + + // too much depth + // [[[]]] + let query = "q=a&facetFilters=%5B%5B%5B%5D%5D%5D"; + let (_response, status_code) = server.search(query).await; + assert_ne!(status_code, 202); + // [["color:green", ["color:blue"]]] + let query = "q=a&facetFilters=%5B%5B%22color%3Agreen%22,%20%5B%22color%3Ablue%22%5D%5D%5D"; + let (_response, status_code) = server.search(query).await; + assert_ne!(status_code, 202); + // "color:green" + let query = "q=a&facetFilters=%22color%3Agreen%22"; + let (_response, status_code) = server.search(query).await; + assert_ne!(status_code, 202); +} diff --git a/meilisearch-http/tests/settings.rs b/meilisearch-http/tests/settings.rs index 63f91da56..ad2d8caab 100644 --- a/meilisearch-http/tests/settings.rs +++ b/meilisearch-http/tests/settings.rs @@ -8,7 +8,6 @@ mod common; async fn write_all_and_delete() { let mut server = common::Server::with_uid("movies"); server.populate_movies().await; - // 2 - Send the settings let body = json!({ @@ -48,9 +47,11 @@ async fn write_all_and_delete() { "wolverine": ["xmen", "logan"], "logan": ["wolverine"], }, + "attributesForFaceting": ["title"], "acceptNewFields": false, }); + server.update_all_settings(body.clone()).await; // 3 - Get all settings and compare to the previous one @@ -119,6 +120,7 @@ async fn write_all_and_delete() { ], "stopWords": [], "synonyms": {}, + "attributesForFaceting": null, "acceptNewFields": true, }); @@ -169,6 +171,7 @@ async fn write_all_and_update() { "wolverine": ["xmen", "logan"], "logan": ["wolverine"], }, + "attributesForFaceting": ["title"], "acceptNewFields": false, }); @@ -210,6 +213,7 @@ async fn write_all_and_update() { "wolverine": ["xmen", "logan"], "logan": ["wolverine", "xmen"], }, + "attributesForFaceting": ["title"], "acceptNewFields": false, }); @@ -247,6 +251,7 @@ async fn write_all_and_update() { "wolverine": ["xmen", "logan"], "logan": ["wolverine", "xmen"], }, + "attributesForFaceting": ["title"], "acceptNewFields": false }); @@ -277,6 +282,7 @@ async fn test_default_settings() { "displayedAttributes": [], "stopWords": [], "synonyms": {}, + "attributesForFaceting": null, "acceptNewFields": true, }); @@ -314,6 +320,7 @@ async fn test_default_settings_2() { ], "stopWords": [], "synonyms": {}, + "attributesForFaceting": null, "acceptNewFields": true, }); @@ -421,6 +428,7 @@ async fn write_setting_and_update_partial() { "wolverine": ["xmen", "logan"], "logan": ["wolverine"], }, + "attributesForFaceting": null, "acceptNewFields": false, }); diff --git a/meilisearch-http/tests/settings_accept_new_fields.rs b/meilisearch-http/tests/settings_accept_new_fields.rs index 6127c6478..8d2ebe1d9 100644 --- a/meilisearch-http/tests/settings_accept_new_fields.rs +++ b/meilisearch-http/tests/settings_accept_new_fields.rs @@ -341,6 +341,7 @@ async fn accept_new_fields_does_not_take_into_account_the_primary_key() { "displayedAttributes": ["title"], "stopWords": [], "synonyms": {}, + "attributesForFaceting": null, "acceptNewFields": false, }); diff --git a/meilisearch-schema/Cargo.toml b/meilisearch-schema/Cargo.toml index 126fe04bb..712fdb008 100644 --- a/meilisearch-schema/Cargo.toml +++ b/meilisearch-schema/Cargo.toml @@ -11,3 +11,4 @@ indexmap = { version = "1.3.2", features = ["serde-1"] } serde = { version = "1.0.105", features = ["derive"] } serde_json = { version = "1.0.50", features = ["preserve_order"] } toml = { version = "0.5.6", features = ["preserve_order"] } +zerocopy = "0.3.0" diff --git a/meilisearch-schema/src/lib.rs b/meilisearch-schema/src/lib.rs index c56ac151e..a35c30c03 100644 --- a/meilisearch-schema/src/lib.rs +++ b/meilisearch-schema/src/lib.rs @@ -6,6 +6,7 @@ pub use error::{Error, SResult}; pub use fields_map::FieldsMap; pub use schema::Schema; use serde::{Deserialize, Serialize}; +use zerocopy::{AsBytes, FromBytes}; #[derive(Serialize, Deserialize, Debug, Copy, Clone, Default, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct IndexedPos(pub u16); @@ -36,7 +37,10 @@ impl Into for IndexedPos { } } -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default, PartialOrd, Ord, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, Default, PartialOrd, Ord, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize)] +#[derive(AsBytes, FromBytes)] +#[repr(C)] pub struct FieldId(pub u16); impl FieldId { @@ -63,8 +67,8 @@ impl From for FieldId { } } -impl Into for FieldId { - fn into(self) -> u16 { - self.0 +impl From for u16 { + fn from(other: FieldId) -> u16 { + other.0 } }