feat(http): update the documents resource

- Return Documents API resources on `/documents` in an array in the the results field.
- Add limit, offset and total in the response body.
- Rename `attributesToRetrieve` into `fields` (only for the `/documents` endpoints, not for the `/search` ones).
- The `displayedAttributes` settings does not impact anymore the displayed fields returned in the `/documents` endpoints. These settings only impacts the `/search` endpoint.

Fix #2372
This commit is contained in:
Irevoire 2022-05-25 11:51:26 +02:00 committed by Tamo
parent ab39df9693
commit ddad6cc069
No known key found for this signature in database
GPG Key ID: 20CD8020AFA88D69
11 changed files with 217 additions and 200 deletions

View File

@ -13,7 +13,8 @@ use meilisearch_lib::MeiliSearch;
use mime::Mime; use mime::Mime;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_cs::vec::CS;
use serde_json::{json, Value};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::analytics::Analytics; use crate::analytics::Analytics;
@ -21,11 +22,9 @@ use crate::error::MeilisearchHttpError;
use crate::extractors::authentication::{policies::*, GuardedData}; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::extractors::payload::Payload; use crate::extractors::payload::Payload;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::{fold_star_or, StarOr};
use crate::task::SummarizedTaskView; use crate::task::SummarizedTaskView;
const DEFAULT_RETRIEVE_DOCUMENTS_OFFSET: usize = 0;
const DEFAULT_RETRIEVE_DOCUMENTS_LIMIT: usize = 20;
static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| { static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| {
vec![ vec![
"application/json".to_string(), "application/json".to_string(),
@ -86,14 +85,24 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GetDocument {
fields: Option<CS<StarOr<String>>>,
}
pub async fn get_document( pub async fn get_document(
meilisearch: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, MeiliSearch>, meilisearch: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, MeiliSearch>,
path: web::Path<DocumentParam>, path: web::Path<DocumentParam>,
params: web::Query<GetDocument>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index = path.index_uid.clone(); let index = path.index_uid.clone();
let id = path.document_id.clone(); let id = path.document_id.clone();
let GetDocument { fields } = params.into_inner();
let attributes_to_retrieve = fields.map(CS::into_inner).and_then(fold_star_or);
let document = meilisearch let document = meilisearch
.document(index, id, None as Option<Vec<String>>) .document(index, id, attributes_to_retrieve)
.await?; .await?;
debug!("returns: {:?}", document); debug!("returns: {:?}", document);
Ok(HttpResponse::Ok().json(document)) Ok(HttpResponse::Ok().json(document))
@ -113,12 +122,16 @@ pub async fn delete_document(
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BrowseQuery { pub struct BrowseQuery {
offset: Option<usize>, #[serde(default)]
limit: Option<usize>, offset: usize,
attributes_to_retrieve: Option<String>, #[serde(default = "PAGINATION_DEFAULT_LIMIT")]
limit: usize,
fields: Option<CS<StarOr<String>>>,
} }
pub async fn get_all_documents( pub async fn get_all_documents(
@ -127,27 +140,21 @@ pub async fn get_all_documents(
params: web::Query<BrowseQuery>, params: web::Query<BrowseQuery>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
debug!("called with params: {:?}", params); debug!("called with params: {:?}", params);
let attributes_to_retrieve = params.attributes_to_retrieve.as_ref().and_then(|attrs| { let BrowseQuery {
let mut names = Vec::new(); offset,
for name in attrs.split(',').map(String::from) { limit,
if name == "*" { fields,
return None; } = params.into_inner();
} let attributes_to_retrieve = fields.map(CS::into_inner).and_then(fold_star_or);
names.push(name);
}
Some(names)
});
let documents = meilisearch let (total, documents) = meilisearch
.documents( .documents(path.into_inner(), offset, limit, attributes_to_retrieve)
path.into_inner(),
params.offset.unwrap_or(DEFAULT_RETRIEVE_DOCUMENTS_OFFSET),
params.limit.unwrap_or(DEFAULT_RETRIEVE_DOCUMENTS_LIMIT),
attributes_to_retrieve,
)
.await?; .await?;
debug!("returns: {:?}", documents); debug!("returns: {:?}", documents);
Ok(HttpResponse::Ok().json(documents)) Ok(HttpResponse::Ok().json(json!(
{ "limit": limit, "offset": offset, "total": total, "results": documents }
)))
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -24,6 +26,38 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(web::scope("/indexes").configure(indexes::configure)); .service(web::scope("/indexes").configure(indexes::configure));
} }
/// A type that tries to match either a star (*) or
/// any other thing that implements `FromStr`.
#[derive(Debug)]
pub enum StarOr<T> {
Star,
Other(T),
}
impl<T: FromStr> FromStr for StarOr<T> {
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim() == "*" {
Ok(StarOr::Star)
} else {
T::from_str(s).map(StarOr::Other)
}
}
}
/// Extracts the raw values from the `StarOr` types and
/// return None if a `StarOr::Star` is encountered.
pub fn fold_star_or<T>(content: impl IntoIterator<Item = StarOr<T>>) -> Option<Vec<T>> {
content
.into_iter()
.map(|value| match value {
StarOr::Star => None,
StarOr::Other(val) => Some(val),
})
.collect()
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[serde(tag = "name")] #[serde(tag = "name")]

View File

@ -6,13 +6,14 @@ use meilisearch_lib::{IndexUid, MeiliSearch};
use serde::Deserialize; use serde::Deserialize;
use serde_cs::vec::CS; use serde_cs::vec::CS;
use serde_json::json; use serde_json::json;
use std::str::FromStr;
use crate::analytics::Analytics; use crate::analytics::Analytics;
use crate::extractors::authentication::{policies::*, GuardedData}; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::task::{TaskListView, TaskStatus, TaskType, TaskView}; use crate::task::{TaskListView, TaskStatus, TaskType, TaskView};
use super::{fold_star_or, StarOr};
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("").route(web::get().to(SeqHandler(get_tasks)))) cfg.service(web::resource("").route(web::get().to(SeqHandler(get_tasks))))
.service(web::resource("/{task_id}").route(web::get().to(SeqHandler(get_task)))); .service(web::resource("/{task_id}").route(web::get().to(SeqHandler(get_task))));
@ -27,40 +28,6 @@ pub struct TaskFilterQuery {
index_uid: Option<CS<StarOr<IndexUid>>>, index_uid: Option<CS<StarOr<IndexUid>>>,
} }
/// A type that tries to match either a star (*) or
/// any other thing that implements `FromStr`.
#[derive(Debug)]
enum StarOr<T> {
Star,
Other(T),
}
impl<T: FromStr> FromStr for StarOr<T> {
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim() == "*" {
Ok(StarOr::Star)
} else {
T::from_str(s).map(StarOr::Other)
}
}
}
/// Extracts the raw values from the `StarOr` types and
/// return None if a `StarOr::Star` is encountered.
fn fold_star_or<T>(content: Vec<StarOr<T>>) -> Option<Vec<T>> {
content
.into_iter()
.fold(Some(Vec::new()), |acc, val| match (acc, val) {
(None, _) | (_, StarOr::Star) => None,
(Some(mut acc), StarOr::Other(uid)) => {
acc.push(uid);
Some(acc)
}
})
}
#[rustfmt::skip] #[rustfmt::skip]
fn task_type_matches_content(type_: &TaskType, content: &TaskContent) -> bool { fn task_type_matches_content(type_: &TaskType, content: &TaskContent) -> bool {
matches!((type_, content), matches!((type_, content),

View File

@ -145,9 +145,12 @@ impl Index<'_> {
pub async fn get_document( pub async fn get_document(
&self, &self,
id: u64, id: u64,
_options: Option<GetDocumentOptions>, options: Option<GetDocumentOptions>,
) -> (Value, StatusCode) { ) -> (Value, StatusCode) {
let url = format!("/indexes/{}/documents/{}", encode(self.uid.as_ref()), id); let mut url = format!("/indexes/{}/documents/{}", encode(self.uid.as_ref()), id);
if let Some(fields) = options.and_then(|o| o.fields) {
url.push_str(&format!("?fields={}", fields.join(",")));
}
self.service.get(url).await self.service.get(url).await
} }
@ -162,10 +165,7 @@ impl Index<'_> {
} }
if let Some(attributes_to_retrieve) = options.attributes_to_retrieve { if let Some(attributes_to_retrieve) = options.attributes_to_retrieve {
url.push_str(&format!( url.push_str(&format!("fields={}&", attributes_to_retrieve.join(",")));
"attributesToRetrieve={}&",
attributes_to_retrieve.join(",")
));
} }
self.service.get(url).await self.service.get(url).await
@ -245,7 +245,9 @@ impl Index<'_> {
make_settings_test_routes!(distinct_attribute); make_settings_test_routes!(distinct_attribute);
} }
pub struct GetDocumentOptions; pub struct GetDocumentOptions {
pub fields: Option<Vec<&'static str>>,
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct GetAllDocumentsOptions { pub struct GetAllDocumentsOptions {

View File

@ -828,7 +828,7 @@ async fn add_larger_dataset() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 77); assert_eq!(response["results"].as_array().unwrap().len(), 77);
} }
#[actix_rt::test] #[actix_rt::test]
@ -849,7 +849,7 @@ async fn update_larger_dataset() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 77); assert_eq!(response["results"].as_array().unwrap().len(), 77);
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -72,7 +72,7 @@ async fn clear_all_documents() {
.get_all_documents(GetAllDocumentsOptions::default()) .get_all_documents(GetAllDocumentsOptions::default())
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert!(response.as_array().unwrap().is_empty()); assert!(response["results"].as_array().unwrap().is_empty());
} }
#[actix_rt::test] #[actix_rt::test]
@ -89,7 +89,7 @@ async fn clear_all_documents_empty_index() {
.get_all_documents(GetAllDocumentsOptions::default()) .get_all_documents(GetAllDocumentsOptions::default())
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert!(response.as_array().unwrap().is_empty()); assert!(response["results"].as_array().unwrap().is_empty());
} }
#[actix_rt::test] #[actix_rt::test]
@ -125,8 +125,8 @@ async fn delete_batch() {
.get_all_documents(GetAllDocumentsOptions::default()) .get_all_documents(GetAllDocumentsOptions::default())
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 1); assert_eq!(response["results"].as_array().unwrap().len(), 1);
assert_eq!(response.as_array().unwrap()[0]["id"], 3); assert_eq!(response["results"][0]["id"], json!(3));
} }
#[actix_rt::test] #[actix_rt::test]
@ -143,5 +143,5 @@ async fn delete_no_document_batch() {
.get_all_documents(GetAllDocumentsOptions::default()) .get_all_documents(GetAllDocumentsOptions::default())
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 3); assert_eq!(response["results"].as_array().unwrap().len(), 3);
} }

View File

@ -1,5 +1,5 @@
use crate::common::GetAllDocumentsOptions;
use crate::common::Server; use crate::common::Server;
use crate::common::{GetAllDocumentsOptions, GetDocumentOptions};
use serde_json::json; use serde_json::json;
@ -39,7 +39,7 @@ async fn get_document() {
let documents = serde_json::json!([ let documents = serde_json::json!([
{ {
"id": 0, "id": 0,
"content": "foobar", "nested": { "content": "foobar" },
} }
]); ]);
let (_, code) = index.add_documents(documents, None).await; let (_, code) = index.add_documents(documents, None).await;
@ -51,9 +51,43 @@ async fn get_document() {
response, response,
serde_json::json!({ serde_json::json!({
"id": 0, "id": 0,
"content": "foobar", "nested": { "content": "foobar" },
}) })
); );
let (response, code) = index
.get_document(
0,
Some(GetDocumentOptions {
fields: Some(vec!["id"]),
}),
)
.await;
assert_eq!(code, 200);
assert_eq!(
response,
serde_json::json!({
"id": 0,
})
);
/* This currently doesn't work but should be fixed by #2433
let (response, code) = index
.get_document(
0,
Some(GetDocumentOptions {
fields: Some(vec!["nested.content"]),
}),
)
.await;
assert_eq!(code, 200);
assert_eq!(
response,
serde_json::json!({
"nested": { "content": "foobar" },
})
);
*/
} }
#[actix_rt::test] #[actix_rt::test]
@ -88,7 +122,7 @@ async fn get_no_document() {
.get_all_documents(GetAllDocumentsOptions::default()) .get_all_documents(GetAllDocumentsOptions::default())
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert!(response.as_array().unwrap().is_empty()); assert!(response["results"].as_array().unwrap().is_empty());
} }
#[actix_rt::test] #[actix_rt::test]
@ -101,7 +135,7 @@ async fn get_all_documents_no_options() {
.get_all_documents(GetAllDocumentsOptions::default()) .get_all_documents(GetAllDocumentsOptions::default())
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
let arr = response.as_array().unwrap(); let arr = response["results"].as_array().unwrap();
assert_eq!(arr.len(), 20); assert_eq!(arr.len(), 20);
let first = serde_json::json!({ let first = serde_json::json!({
"id":0, "id":0,
@ -137,8 +171,11 @@ async fn test_get_all_documents_limit() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 5); assert_eq!(response["results"].as_array().unwrap().len(), 5);
assert_eq!(response.as_array().unwrap()[0]["id"], 0); assert_eq!(response["results"][0]["id"], json!(0));
assert_eq!(response["offset"], json!(0));
assert_eq!(response["limit"], json!(5));
assert_eq!(response["total"], json!(77));
} }
#[actix_rt::test] #[actix_rt::test]
@ -154,8 +191,11 @@ async fn test_get_all_documents_offset() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!(response.as_array().unwrap()[0]["id"], 5); assert_eq!(response["results"][0]["id"], json!(5));
assert_eq!(response["offset"], json!(5));
assert_eq!(response["limit"], json!(20));
assert_eq!(response["total"], json!(77));
} }
#[actix_rt::test] #[actix_rt::test]
@ -171,20 +211,14 @@ async fn test_get_all_documents_attributes_to_retrieve() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!( for results in response["results"].as_array().unwrap() {
response.as_array().unwrap()[0] assert_eq!(results.as_object().unwrap().keys().count(), 1);
.as_object() assert!(results["name"] != json!(null));
.unwrap() }
.keys() assert_eq!(response["offset"], json!(0));
.count(), assert_eq!(response["limit"], json!(20));
1 assert_eq!(response["total"], json!(77));
);
assert!(response.as_array().unwrap()[0]
.as_object()
.unwrap()
.get("name")
.is_some());
let (response, code) = index let (response, code) = index
.get_all_documents(GetAllDocumentsOptions { .get_all_documents(GetAllDocumentsOptions {
@ -193,15 +227,13 @@ async fn test_get_all_documents_attributes_to_retrieve() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!( for results in response["results"].as_array().unwrap() {
response.as_array().unwrap()[0] assert_eq!(results.as_object().unwrap().keys().count(), 0);
.as_object() }
.unwrap() assert_eq!(response["offset"], json!(0));
.keys() assert_eq!(response["limit"], json!(20));
.count(), assert_eq!(response["total"], json!(77));
0
);
let (response, code) = index let (response, code) = index
.get_all_documents(GetAllDocumentsOptions { .get_all_documents(GetAllDocumentsOptions {
@ -210,15 +242,13 @@ async fn test_get_all_documents_attributes_to_retrieve() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!( for results in response["results"].as_array().unwrap() {
response.as_array().unwrap()[0] assert_eq!(results.as_object().unwrap().keys().count(), 0);
.as_object() }
.unwrap() assert_eq!(response["offset"], json!(0));
.keys() assert_eq!(response["limit"], json!(20));
.count(), assert_eq!(response["total"], json!(77));
0
);
let (response, code) = index let (response, code) = index
.get_all_documents(GetAllDocumentsOptions { .get_all_documents(GetAllDocumentsOptions {
@ -227,15 +257,12 @@ async fn test_get_all_documents_attributes_to_retrieve() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!( for results in response["results"].as_array().unwrap() {
response.as_array().unwrap()[0] assert_eq!(results.as_object().unwrap().keys().count(), 2);
.as_object() assert!(results["name"] != json!(null));
.unwrap() assert!(results["tags"] != json!(null));
.keys() }
.count(),
2
);
let (response, code) = index let (response, code) = index
.get_all_documents(GetAllDocumentsOptions { .get_all_documents(GetAllDocumentsOptions {
@ -244,15 +271,10 @@ async fn test_get_all_documents_attributes_to_retrieve() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!( for results in response["results"].as_array().unwrap() {
response.as_array().unwrap()[0] assert_eq!(results.as_object().unwrap().keys().count(), 16);
.as_object() }
.unwrap()
.keys()
.count(),
16
);
let (response, code) = index let (response, code) = index
.get_all_documents(GetAllDocumentsOptions { .get_all_documents(GetAllDocumentsOptions {
@ -261,19 +283,14 @@ async fn test_get_all_documents_attributes_to_retrieve() {
}) })
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!( for results in response["results"].as_array().unwrap() {
response.as_array().unwrap()[0] assert_eq!(results.as_object().unwrap().keys().count(), 16);
.as_object() }
.unwrap()
.keys()
.count(),
16
);
} }
#[actix_rt::test] #[actix_rt::test]
async fn get_documents_displayed_attributes() { async fn get_documents_displayed_attributes_is_ignored() {
let server = Server::new().await; let server = Server::new().await;
let index = server.index("test"); let index = server.index("test");
index index
@ -285,23 +302,19 @@ async fn get_documents_displayed_attributes() {
.get_all_documents(GetAllDocumentsOptions::default()) .get_all_documents(GetAllDocumentsOptions::default())
.await; .await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_array().unwrap().len(), 20); assert_eq!(response["results"].as_array().unwrap().len(), 20);
assert_eq!( assert_eq!(
response.as_array().unwrap()[0] response["results"][0].as_object().unwrap().keys().count(),
.as_object() 16
.unwrap()
.keys()
.count(),
1
); );
assert!(response.as_array().unwrap()[0] assert!(response["results"][0]["gender"] != json!(null));
.as_object()
.unwrap() assert_eq!(response["offset"], json!(0));
.get("gender") assert_eq!(response["limit"], json!(20));
.is_some()); assert_eq!(response["total"], json!(77));
let (response, code) = index.get_document(0, None).await; let (response, code) = index.get_document(0, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(response.as_object().unwrap().keys().count(), 1); assert_eq!(response.as_object().unwrap().keys().count(), 16);
assert!(response.as_object().unwrap().get("gender").is_some()); assert!(response.as_object().unwrap().get("gender").is_some());
} }

View File

@ -142,21 +142,21 @@ async fn import_dump_v2_movie_with_settings() {
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "title": "Lock, Stock and Two Smoking Barrels", "genres": ["Comedy", "Crime"], "overview": "A card shark and his unwillingly-enlisted friends need to make a lot of cash quick after losing a sketchy poker match. To do this they decide to pull a heist on a small-time gang who happen to be operating out of the flat next door.", "poster": "https://image.tmdb.org/t/p/w500/8kSerJrhrJWKLk1LViesGcnrUPE.jpg", "release_date": 889056000 }) json!({ "id": 100, "title": "Lock, Stock and Two Smoking Barrels", "genres": ["Comedy", "Crime"], "overview": "A card shark and his unwillingly-enlisted friends need to make a lot of cash quick after losing a sketchy poker match. To do this they decide to pull a heist on a small-time gang who happen to be operating out of the flat next door.", "poster": "https://image.tmdb.org/t/p/w500/8kSerJrhrJWKLk1LViesGcnrUPE.jpg", "release_date": 889056000 })
); );
let (document, code) = index.get_document(500, None).await; let (document, code) = index.get_document(500, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"title": "Reservoir Dogs", "genres": ["Crime", "Thriller"], "overview": "A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.", "poster": "https://image.tmdb.org/t/p/w500/AjTtJNumZyUDz33VtMlF1K8JPsE.jpg", "release_date": 715392000}) json!({ "id": 500, "title": "Reservoir Dogs", "genres": ["Crime", "Thriller"], "overview": "A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.", "poster": "https://image.tmdb.org/t/p/w500/AjTtJNumZyUDz33VtMlF1K8JPsE.jpg", "release_date": 715392000})
); );
let (document, code) = index.get_document(10006, None).await; let (document, code) = index.get_document(10006, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"title": "Wild Seven", "genres": ["Action", "Crime", "Drama"], "overview": "In this darkly karmic vision of Arizona, a man who breathes nothing but ill will begins a noxious domino effect as quickly as an uncontrollable virus kills. As he exits Arizona State Penn after twenty-one long years, Wilson has only one thing on the brain, leveling the score with career criminal, Mackey Willis.", "poster": "https://image.tmdb.org/t/p/w500/y114dTPoqn8k2Txps4P2tI95YCS.jpg", "release_date": 1136073600}) json!({ "id": 10006, "title": "Wild Seven", "genres": ["Action", "Crime", "Drama"], "overview": "In this darkly karmic vision of Arizona, a man who breathes nothing but ill will begins a noxious domino effect as quickly as an uncontrollable virus kills. As he exits Arizona State Penn after twenty-one long years, Wilson has only one thing on the brain, leveling the score with career criminal, Mackey Willis.", "poster": "https://image.tmdb.org/t/p/w500/y114dTPoqn8k2Txps4P2tI95YCS.jpg", "release_date": 1136073600})
); );
} }
@ -211,21 +211,21 @@ async fn import_dump_v2_rubygems_with_settings() {
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"name": "meilisearch", "summary": "An easy-to-use ruby client for Meilisearch API", "description": "An easy-to-use ruby client for Meilisearch API. See https://github.com/meilisearch/MeiliSearch", "version": "0.15.2", "total_downloads": "7465"}) json!({ "name": "meilisearch", "summary": "An easy-to-use ruby client for Meilisearch API", "description": "An easy-to-use ruby client for Meilisearch API. See https://github.com/meilisearch/MeiliSearch", "id": "188040", "version": "0.15.2", "total_downloads": "7465"})
); );
let (document, code) = index.get_document(191940, None).await; let (document, code) = index.get_document(191940, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"name": "doggo", "summary": "RSpec 3 formatter - documentation, with progress indication", "description": "Similar to \"rspec -f d\", but also indicates progress by showing the current test number and total test count on each line.", "version": "1.1.0", "total_downloads": "9394"}) json!({ "name": "doggo", "summary": "RSpec 3 formatter - documentation, with progress indication", "description": "Similar to \"rspec -f d\", but also indicates progress by showing the current test number and total test count on each line.", "id": "191940", "version": "1.1.0", "total_downloads": "9394"})
); );
let (document, code) = index.get_document(159227, None).await; let (document, code) = index.get_document(159227, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"name": "vortex-of-agony", "summary": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "description": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "version": "0.1.0", "total_downloads": "1007"}) json!({ "name": "vortex-of-agony", "summary": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "description": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "id": "159227", "version": "0.1.0", "total_downloads": "1007"})
); );
} }
@ -341,21 +341,21 @@ async fn import_dump_v3_movie_with_settings() {
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "title": "Lock, Stock and Two Smoking Barrels", "genres": ["Comedy", "Crime"], "overview": "A card shark and his unwillingly-enlisted friends need to make a lot of cash quick after losing a sketchy poker match. To do this they decide to pull a heist on a small-time gang who happen to be operating out of the flat next door.", "poster": "https://image.tmdb.org/t/p/w500/8kSerJrhrJWKLk1LViesGcnrUPE.jpg", "release_date": 889056000 }) json!({ "id": 100, "title": "Lock, Stock and Two Smoking Barrels", "genres": ["Comedy", "Crime"], "overview": "A card shark and his unwillingly-enlisted friends need to make a lot of cash quick after losing a sketchy poker match. To do this they decide to pull a heist on a small-time gang who happen to be operating out of the flat next door.", "poster": "https://image.tmdb.org/t/p/w500/8kSerJrhrJWKLk1LViesGcnrUPE.jpg", "release_date": 889056000 })
); );
let (document, code) = index.get_document(500, None).await; let (document, code) = index.get_document(500, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"title": "Reservoir Dogs", "genres": ["Crime", "Thriller"], "overview": "A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.", "poster": "https://image.tmdb.org/t/p/w500/AjTtJNumZyUDz33VtMlF1K8JPsE.jpg", "release_date": 715392000}) json!({ "id": 500, "title": "Reservoir Dogs", "genres": ["Crime", "Thriller"], "overview": "A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.", "poster": "https://image.tmdb.org/t/p/w500/AjTtJNumZyUDz33VtMlF1K8JPsE.jpg", "release_date": 715392000})
); );
let (document, code) = index.get_document(10006, None).await; let (document, code) = index.get_document(10006, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"title": "Wild Seven", "genres": ["Action", "Crime", "Drama"], "overview": "In this darkly karmic vision of Arizona, a man who breathes nothing but ill will begins a noxious domino effect as quickly as an uncontrollable virus kills. As he exits Arizona State Penn after twenty-one long years, Wilson has only one thing on the brain, leveling the score with career criminal, Mackey Willis.", "poster": "https://image.tmdb.org/t/p/w500/y114dTPoqn8k2Txps4P2tI95YCS.jpg", "release_date": 1136073600}) json!({ "id": 10006, "title": "Wild Seven", "genres": ["Action", "Crime", "Drama"], "overview": "In this darkly karmic vision of Arizona, a man who breathes nothing but ill will begins a noxious domino effect as quickly as an uncontrollable virus kills. As he exits Arizona State Penn after twenty-one long years, Wilson has only one thing on the brain, leveling the score with career criminal, Mackey Willis.", "poster": "https://image.tmdb.org/t/p/w500/y114dTPoqn8k2Txps4P2tI95YCS.jpg", "release_date": 1136073600})
); );
} }
@ -410,21 +410,21 @@ async fn import_dump_v3_rubygems_with_settings() {
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"name": "meilisearch", "summary": "An easy-to-use ruby client for Meilisearch API", "description": "An easy-to-use ruby client for Meilisearch API. See https://github.com/meilisearch/MeiliSearch", "version": "0.15.2", "total_downloads": "7465"}) json!({ "name": "meilisearch", "summary": "An easy-to-use ruby client for Meilisearch API", "description": "An easy-to-use ruby client for Meilisearch API. See https://github.com/meilisearch/MeiliSearch", "id": "188040", "version": "0.15.2", "total_downloads": "7465"})
); );
let (document, code) = index.get_document(191940, None).await; let (document, code) = index.get_document(191940, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"name": "doggo", "summary": "RSpec 3 formatter - documentation, with progress indication", "description": "Similar to \"rspec -f d\", but also indicates progress by showing the current test number and total test count on each line.", "version": "1.1.0", "total_downloads": "9394"}) json!({ "name": "doggo", "summary": "RSpec 3 formatter - documentation, with progress indication", "description": "Similar to \"rspec -f d\", but also indicates progress by showing the current test number and total test count on each line.", "id": "191940", "version": "1.1.0", "total_downloads": "9394"})
); );
let (document, code) = index.get_document(159227, None).await; let (document, code) = index.get_document(159227, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({"name": "vortex-of-agony", "summary": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "description": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "version": "0.1.0", "total_downloads": "1007"}) json!({ "name": "vortex-of-agony", "summary": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "description": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "id": "159227", "version": "0.1.0", "total_downloads": "1007"})
); );
} }
@ -540,21 +540,21 @@ async fn import_dump_v4_movie_with_settings() {
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "title": "Lock, Stock and Two Smoking Barrels", "genres": ["Comedy", "Crime"], "overview": "A card shark and his unwillingly-enlisted friends need to make a lot of cash quick after losing a sketchy poker match. To do this they decide to pull a heist on a small-time gang who happen to be operating out of the flat next door.", "poster": "https://image.tmdb.org/t/p/w500/8kSerJrhrJWKLk1LViesGcnrUPE.jpg", "release_date": 889056000 }) json!({ "id": 100, "title": "Lock, Stock and Two Smoking Barrels", "genres": ["Comedy", "Crime"], "overview": "A card shark and his unwillingly-enlisted friends need to make a lot of cash quick after losing a sketchy poker match. To do this they decide to pull a heist on a small-time gang who happen to be operating out of the flat next door.", "poster": "https://image.tmdb.org/t/p/w500/8kSerJrhrJWKLk1LViesGcnrUPE.jpg", "release_date": 889056000 })
); );
let (document, code) = index.get_document(500, None).await; let (document, code) = index.get_document(500, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "title": "Reservoir Dogs", "genres": ["Crime", "Thriller"], "overview": "A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.", "poster": "https://image.tmdb.org/t/p/w500/AjTtJNumZyUDz33VtMlF1K8JPsE.jpg", "release_date": 715392000}) json!({ "id": 500, "title": "Reservoir Dogs", "genres": ["Crime", "Thriller"], "overview": "A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.", "poster": "https://image.tmdb.org/t/p/w500/AjTtJNumZyUDz33VtMlF1K8JPsE.jpg", "release_date": 715392000})
); );
let (document, code) = index.get_document(10006, None).await; let (document, code) = index.get_document(10006, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "title": "Wild Seven", "genres": ["Action", "Crime", "Drama"], "overview": "In this darkly karmic vision of Arizona, a man who breathes nothing but ill will begins a noxious domino effect as quickly as an uncontrollable virus kills. As he exits Arizona State Penn after twenty-one long years, Wilson has only one thing on the brain, leveling the score with career criminal, Mackey Willis.", "poster": "https://image.tmdb.org/t/p/w500/y114dTPoqn8k2Txps4P2tI95YCS.jpg", "release_date": 1136073600}) json!({ "id": 10006, "title": "Wild Seven", "genres": ["Action", "Crime", "Drama"], "overview": "In this darkly karmic vision of Arizona, a man who breathes nothing but ill will begins a noxious domino effect as quickly as an uncontrollable virus kills. As he exits Arizona State Penn after twenty-one long years, Wilson has only one thing on the brain, leveling the score with career criminal, Mackey Willis.", "poster": "https://image.tmdb.org/t/p/w500/y114dTPoqn8k2Txps4P2tI95YCS.jpg", "release_date": 1136073600})
); );
} }
@ -609,20 +609,20 @@ async fn import_dump_v4_rubygems_with_settings() {
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "name": "meilisearch", "summary": "An easy-to-use ruby client for Meilisearch API", "description": "An easy-to-use ruby client for Meilisearch API. See https://github.com/meilisearch/MeiliSearch", "version": "0.15.2", "total_downloads": "7465"}) json!({ "name": "meilisearch", "summary": "An easy-to-use ruby client for Meilisearch API", "description": "An easy-to-use ruby client for Meilisearch API. See https://github.com/meilisearch/MeiliSearch", "id": "188040", "version": "0.15.2", "total_downloads": "7465"})
); );
let (document, code) = index.get_document(191940, None).await; let (document, code) = index.get_document(191940, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "name": "doggo", "summary": "RSpec 3 formatter - documentation, with progress indication", "description": "Similar to \"rspec -f d\", but also indicates progress by showing the current test number and total test count on each line.", "version": "1.1.0", "total_downloads": "9394"}) json!({ "name": "doggo", "summary": "RSpec 3 formatter - documentation, with progress indication", "description": "Similar to \"rspec -f d\", but also indicates progress by showing the current test number and total test count on each line.", "id": "191940", "version": "1.1.0", "total_downloads": "9394"})
); );
let (document, code) = index.get_document(159227, None).await; let (document, code) = index.get_document(159227, None).await;
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!( assert_eq!(
document, document,
json!({ "name": "vortex-of-agony", "summary": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "description": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "version": "0.1.0", "total_downloads": "1007"}) json!({ "name": "vortex-of-agony", "summary": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "description": "You dont need to use nodejs or go, just install this plugin. It will crash your application at random", "id": "159227", "version": "0.1.0", "total_downloads": "1007"})
); );
} }

View File

@ -1,4 +1,4 @@
use std::collections::{BTreeSet, HashSet}; use std::collections::BTreeSet;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
@ -218,17 +218,17 @@ impl Index {
}) })
} }
/// Return the total number of documents contained in the index + the selected documents.
pub fn retrieve_documents<S: AsRef<str>>( pub fn retrieve_documents<S: AsRef<str>>(
&self, &self,
offset: usize, offset: usize,
limit: usize, limit: usize,
attributes_to_retrieve: Option<Vec<S>>, attributes_to_retrieve: Option<Vec<S>>,
) -> Result<Vec<Map<String, Value>>> { ) -> Result<(u64, Vec<Document>)> {
let txn = self.read_txn()?; let txn = self.read_txn()?;
let fields_ids_map = self.fields_ids_map(&txn)?; let fields_ids_map = self.fields_ids_map(&txn)?;
let fields_to_display = let fields_to_display = self.fields_to_display(&attributes_to_retrieve, &fields_ids_map)?;
self.fields_to_display(&txn, &attributes_to_retrieve, &fields_ids_map)?;
let iter = self.documents.range(&txn, &(..))?.skip(offset).take(limit); let iter = self.documents.range(&txn, &(..))?.skip(offset).take(limit);
@ -240,20 +240,20 @@ impl Index {
documents.push(object); documents.push(object);
} }
Ok(documents) let number_of_documents = self.number_of_documents(&txn)?;
Ok((number_of_documents, documents))
} }
pub fn retrieve_document<S: AsRef<str>>( pub fn retrieve_document<S: AsRef<str>>(
&self, &self,
doc_id: String, doc_id: String,
attributes_to_retrieve: Option<Vec<S>>, attributes_to_retrieve: Option<Vec<S>>,
) -> Result<Map<String, Value>> { ) -> Result<Document> {
let txn = self.read_txn()?; let txn = self.read_txn()?;
let fields_ids_map = self.fields_ids_map(&txn)?; let fields_ids_map = self.fields_ids_map(&txn)?;
let fields_to_display = self.fields_to_display(&attributes_to_retrieve, &fields_ids_map)?;
let fields_to_display =
self.fields_to_display(&txn, &attributes_to_retrieve, &fields_ids_map)?;
let internal_id = self let internal_id = self
.external_documents_ids(&txn)? .external_documents_ids(&txn)?
@ -278,25 +278,18 @@ impl Index {
fn fields_to_display<S: AsRef<str>>( fn fields_to_display<S: AsRef<str>>(
&self, &self,
txn: &milli::heed::RoTxn,
attributes_to_retrieve: &Option<Vec<S>>, attributes_to_retrieve: &Option<Vec<S>>,
fields_ids_map: &milli::FieldsIdsMap, fields_ids_map: &milli::FieldsIdsMap,
) -> Result<Vec<FieldId>> { ) -> Result<Vec<FieldId>> {
let mut displayed_fields_ids = match self.displayed_fields_ids(txn)? {
Some(ids) => ids.into_iter().collect::<Vec<_>>(),
None => fields_ids_map.iter().map(|(id, _)| id).collect(),
};
let attributes_to_retrieve_ids = match attributes_to_retrieve { let attributes_to_retrieve_ids = match attributes_to_retrieve {
Some(attrs) => attrs Some(attrs) => attrs
.iter() .iter()
.filter_map(|f| fields_ids_map.id(f.as_ref())) .filter_map(|f| fields_ids_map.id(f.as_ref()))
.collect::<HashSet<_>>(), .collect(),
None => fields_ids_map.iter().map(|(id, _)| id).collect(), None => fields_ids_map.iter().map(|(id, _)| id).collect(),
}; };
displayed_fields_ids.retain(|fid| attributes_to_retrieve_ids.contains(fid)); Ok(attributes_to_retrieve_ids)
Ok(displayed_fields_ids)
} }
pub fn snapshot(&self, path: impl AsRef<Path>) -> Result<()> { pub fn snapshot(&self, path: impl AsRef<Path>) -> Result<()> {

View File

@ -32,11 +32,11 @@ pub mod test {
use milli::update::IndexerConfig; use milli::update::IndexerConfig;
use milli::update::{DocumentAdditionResult, DocumentDeletionResult, IndexDocumentsMethod}; use milli::update::{DocumentAdditionResult, DocumentDeletionResult, IndexDocumentsMethod};
use nelson::Mocker; use nelson::Mocker;
use serde_json::{Map, Value};
use uuid::Uuid; use uuid::Uuid;
use super::error::Result; use super::error::Result;
use super::index::Index; use super::index::Index;
use super::Document;
use super::{Checked, IndexMeta, IndexStats, SearchQuery, SearchResult, Settings}; use super::{Checked, IndexMeta, IndexStats, SearchQuery, SearchResult, Settings};
use crate::update_file_store::UpdateFileStore; use crate::update_file_store::UpdateFileStore;
@ -102,7 +102,7 @@ pub mod test {
offset: usize, offset: usize,
limit: usize, limit: usize,
attributes_to_retrieve: Option<Vec<S>>, attributes_to_retrieve: Option<Vec<S>>,
) -> Result<Vec<Map<String, Value>>> { ) -> Result<(u64, Vec<Document>)> {
match self { match self {
MockIndex::Real(index) => { MockIndex::Real(index) => {
index.retrieve_documents(offset, limit, attributes_to_retrieve) index.retrieve_documents(offset, limit, attributes_to_retrieve)
@ -115,7 +115,7 @@ pub mod test {
&self, &self,
doc_id: String, doc_id: String,
attributes_to_retrieve: Option<Vec<S>>, attributes_to_retrieve: Option<Vec<S>>,
) -> Result<Map<String, Value>> { ) -> Result<Document> {
match self { match self {
MockIndex::Real(index) => index.retrieve_document(doc_id, attributes_to_retrieve), MockIndex::Real(index) => index.retrieve_document(doc_id, attributes_to_retrieve),
MockIndex::Mock(_) => todo!(), MockIndex::Mock(_) => todo!(),

View File

@ -524,18 +524,19 @@ where
Ok(settings) Ok(settings)
} }
/// Return the total number of documents contained in the index + the selected documents.
pub async fn documents( pub async fn documents(
&self, &self,
uid: String, uid: String,
offset: usize, offset: usize,
limit: usize, limit: usize,
attributes_to_retrieve: Option<Vec<String>>, attributes_to_retrieve: Option<Vec<String>>,
) -> Result<Vec<Document>> { ) -> Result<(u64, Vec<Document>)> {
let index = self.index_resolver.get_index(uid).await?; let index = self.index_resolver.get_index(uid).await?;
let documents = let result =
spawn_blocking(move || index.retrieve_documents(offset, limit, attributes_to_retrieve)) spawn_blocking(move || index.retrieve_documents(offset, limit, attributes_to_retrieve))
.await??; .await??;
Ok(documents) Ok(result)
} }
pub async fn document( pub async fn document(