mirror of
https://github.com/meilisearch/MeiliSearch
synced 2025-07-04 20:37:15 +02:00
Rename MeiliDB into MeiliSearch
This commit is contained in:
parent
58eaf78dc4
commit
7cc096e0a2
94 changed files with 126 additions and 126 deletions
243
meilisearch-http/src/routes/document.rs
Normal file
243
meilisearch-http/src/routes/document.rs
Normal file
|
@ -0,0 +1,243 @@
|
|||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use http::StatusCode;
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tide::querystring::ContextExt as QSContextExt;
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::Data;
|
||||
|
||||
pub async fn get_document(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let identifier = ctx.identifier()?;
|
||||
let document_id = meilisearch_core::serde::compute_document_id(identifier.clone());
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let response = index
|
||||
.document::<IndexMap<String, Value>>(&reader, None, document_id)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::document_not_found(&identifier))?;
|
||||
|
||||
if response.is_empty() {
|
||||
return Err(ResponseError::document_not_found(identifier));
|
||||
}
|
||||
|
||||
Ok(tide::response::json(response))
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndexUpdateResponse {
|
||||
pub update_id: u64,
|
||||
}
|
||||
|
||||
pub async fn delete_document(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let identifier = ctx.identifier()?;
|
||||
let document_id = meilisearch_core::serde::compute_document_id(identifier.clone());
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut documents_deletion = index.documents_deletion();
|
||||
documents_deletion.delete_document_by_id(document_id);
|
||||
let update_id = documents_deletion
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct BrowseQuery {
|
||||
offset: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
attributes_to_retrieve: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_all_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let query: BrowseQuery = ctx.url_query().unwrap_or(BrowseQuery::default());
|
||||
|
||||
let offset = query.offset.unwrap_or(0);
|
||||
let limit = query.limit.unwrap_or(20);
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let documents_ids: Result<BTreeSet<_>, _> =
|
||||
match index.documents_fields_counts.documents_ids(&reader) {
|
||||
Ok(documents_ids) => documents_ids.skip(offset).take(limit).collect(),
|
||||
Err(e) => return Err(ResponseError::internal(e)),
|
||||
};
|
||||
|
||||
let documents_ids = match documents_ids {
|
||||
Ok(documents_ids) => documents_ids,
|
||||
Err(e) => return Err(ResponseError::internal(e)),
|
||||
};
|
||||
|
||||
let mut response_body = Vec::<IndexMap<String, Value>>::new();
|
||||
|
||||
if let Some(attributes) = query.attributes_to_retrieve {
|
||||
let attributes = attributes.split(',').collect::<HashSet<&str>>();
|
||||
for document_id in documents_ids {
|
||||
if let Ok(Some(document)) = index.document(&reader, Some(&attributes), document_id) {
|
||||
response_body.push(document);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for document_id in documents_ids {
|
||||
if let Ok(Some(document)) = index.document(&reader, None, document_id) {
|
||||
response_body.push(document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tide::response::json(response_body))
|
||||
}
|
||||
|
||||
fn infered_schema(document: &IndexMap<String, Value>) -> Option<meilisearch_schema::Schema> {
|
||||
use meilisearch_schema::{SchemaBuilder, DISPLAYED, INDEXED};
|
||||
|
||||
let mut identifier = None;
|
||||
for key in document.keys() {
|
||||
if identifier.is_none() && key.to_lowercase().contains("id") {
|
||||
identifier = Some(key);
|
||||
}
|
||||
}
|
||||
|
||||
match identifier {
|
||||
Some(identifier) => {
|
||||
let mut builder = SchemaBuilder::with_identifier(identifier);
|
||||
for key in document.keys() {
|
||||
builder.new_attribute(key, DISPLAYED | INDEXED);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_multiple_documents(mut ctx: Context<Data>, is_partial: bool) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
|
||||
let data: Vec<IndexMap<String, Value>> =
|
||||
ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let current_schema = index
|
||||
.main
|
||||
.schema(&writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
if current_schema.is_none() {
|
||||
match data.first().and_then(infered_schema) {
|
||||
Some(schema) => {
|
||||
index
|
||||
.schema_update(&mut writer, schema)
|
||||
.map_err(ResponseError::internal)?;
|
||||
}
|
||||
None => return Err(ResponseError::bad_request("Could not infer a schema")),
|
||||
}
|
||||
}
|
||||
|
||||
let mut document_addition = if is_partial {
|
||||
index.documents_partial_addition()
|
||||
} else {
|
||||
index.documents_addition()
|
||||
};
|
||||
|
||||
for document in data {
|
||||
document_addition.update_document(document);
|
||||
}
|
||||
|
||||
let update_id = document_addition
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn add_or_replace_multiple_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
update_multiple_documents(ctx, false).await
|
||||
}
|
||||
|
||||
pub async fn add_or_update_multiple_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
update_multiple_documents(ctx, true).await
|
||||
}
|
||||
|
||||
pub async fn delete_multiple_documents(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
|
||||
let data: Vec<Value> = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut documents_deletion = index.documents_deletion();
|
||||
|
||||
for identifier in data {
|
||||
if let Some(identifier) = meilisearch_core::serde::value_to_string(&identifier) {
|
||||
documents_deletion
|
||||
.delete_document_by_id(meilisearch_core::serde::compute_document_id(identifier));
|
||||
}
|
||||
}
|
||||
|
||||
let update_id = documents_deletion
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn clear_all_documents(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(DocumentsWrite)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
let update_id = index
|
||||
.clear_all(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
79
meilisearch-http/src/routes/health.rs
Normal file
79
meilisearch-http/src/routes/health.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::Data;
|
||||
|
||||
use heed::types::{Str, Unit};
|
||||
use serde::Deserialize;
|
||||
use tide::Context;
|
||||
|
||||
const UNHEALTHY_KEY: &str = "_is_unhealthy";
|
||||
|
||||
pub async fn get_health(ctx: Context<Data>) -> SResult<()> {
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let common_store = ctx.state().db.common_store();
|
||||
|
||||
if let Ok(Some(_)) = common_store.get::<Str, Unit>(&reader, UNHEALTHY_KEY) {
|
||||
return Err(ResponseError::Maintenance);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_healthy(ctx: Context<Data>) -> SResult<()> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let common_store = ctx.state().db.common_store();
|
||||
match common_store.delete::<Str>(&mut writer, UNHEALTHY_KEY) {
|
||||
Ok(_) => (),
|
||||
Err(e) => return Err(ResponseError::internal(e)),
|
||||
}
|
||||
|
||||
if let Err(e) = writer.commit() {
|
||||
return Err(ResponseError::internal(e));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_unhealthy(ctx: Context<Data>) -> SResult<()> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let common_store = ctx.state().db.common_store();
|
||||
|
||||
if let Err(e) = common_store.put::<Str, Unit>(&mut writer, UNHEALTHY_KEY, &()) {
|
||||
return Err(ResponseError::internal(e));
|
||||
}
|
||||
|
||||
if let Err(e) = writer.commit() {
|
||||
return Err(ResponseError::internal(e));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct HealtBody {
|
||||
health: bool,
|
||||
}
|
||||
|
||||
pub async fn change_healthyness(mut ctx: Context<Data>) -> SResult<()> {
|
||||
let body: HealtBody = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
if body.health {
|
||||
set_healthy(ctx).await
|
||||
} else {
|
||||
set_unhealthy(ctx).await
|
||||
}
|
||||
}
|
441
meilisearch-http/src/routes/index.rs
Normal file
441
meilisearch-http/src/routes/index.rs
Normal file
|
@ -0,0 +1,441 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use http::StatusCode;
|
||||
use log::error;
|
||||
use meilisearch_core::ProcessedUpdateResult;
|
||||
use meilisearch_schema::{Schema, SchemaBuilder};
|
||||
use rand::seq::SliceRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tide::querystring::ContextExt as QSContextExt;
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::schema::SchemaBody;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::routes::document::IndexUpdateResponse;
|
||||
use crate::Data;
|
||||
|
||||
fn generate_uid() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let sample = b"abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
sample
|
||||
.choose_multiple(&mut rng, 8)
|
||||
.map(|c| *c as char)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn list_indexes(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let indexes_uids = ctx.state().db.indexes_uids();
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut response_body = Vec::new();
|
||||
|
||||
for index_uid in indexes_uids {
|
||||
let index = ctx.state().db.open_index(&index_uid);
|
||||
|
||||
match index {
|
||||
Some(index) => {
|
||||
let name = index
|
||||
.main
|
||||
.name(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'name' not found"))?;
|
||||
let created_at = index
|
||||
.main
|
||||
.created_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'created_at' date not found"))?;
|
||||
let updated_at = index
|
||||
.main
|
||||
.updated_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'updated_at' date not found"))?;
|
||||
|
||||
let index_reponse = IndexResponse {
|
||||
name,
|
||||
uid: index_uid,
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
response_body.push(index_reponse);
|
||||
}
|
||||
None => error!(
|
||||
"Index {} is referenced in the indexes list but cannot be found",
|
||||
index_uid
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tide::response::json(response_body))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IndexResponse {
|
||||
name: String,
|
||||
uid: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn get_index(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let uid = ctx.url_param("index")?;
|
||||
let name = index
|
||||
.main
|
||||
.name(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'name' not found"))?;
|
||||
let created_at = index
|
||||
.main
|
||||
.created_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'created_at' date not found"))?;
|
||||
let updated_at = index
|
||||
.main
|
||||
.updated_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'updated_at' date not found"))?;
|
||||
|
||||
let response_body = IndexResponse {
|
||||
name,
|
||||
uid,
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response_body))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct IndexCreateRequest {
|
||||
name: String,
|
||||
uid: Option<String>,
|
||||
schema: Option<SchemaBody>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IndexCreateResponse {
|
||||
name: String,
|
||||
uid: String,
|
||||
schema: Option<SchemaBody>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
update_id: Option<u64>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn create_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let body = ctx
|
||||
.body_json::<IndexCreateRequest>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
|
||||
let uid = match body.uid {
|
||||
Some(uid) => uid,
|
||||
None => loop {
|
||||
let uid = generate_uid();
|
||||
if db.open_index(&uid).is_none() {
|
||||
break uid;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let created_index = match db.create_index(&uid) {
|
||||
Ok(index) => index,
|
||||
Err(e) => return Err(ResponseError::create_index(e)),
|
||||
};
|
||||
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
created_index
|
||||
.main
|
||||
.put_name(&mut writer, &body.name)
|
||||
.map_err(ResponseError::internal)?;
|
||||
created_index
|
||||
.main
|
||||
.put_created_at(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
created_index
|
||||
.main
|
||||
.put_updated_at(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let schema: Option<Schema> = body.schema.clone().map(Into::into);
|
||||
let mut response_update_id = None;
|
||||
if let Some(schema) = schema {
|
||||
let update_id = created_index
|
||||
.schema_update(&mut writer, schema)
|
||||
.map_err(ResponseError::internal)?;
|
||||
response_update_id = Some(update_id)
|
||||
}
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexCreateResponse {
|
||||
name: body.name,
|
||||
uid,
|
||||
schema: body.schema,
|
||||
update_id: response_update_id,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::CREATED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct UpdateIndexRequest {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateIndexResponse {
|
||||
name: String,
|
||||
uid: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn update_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let body = ctx
|
||||
.body_json::<UpdateIndexRequest>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
index
|
||||
.main
|
||||
.put_name(&mut writer, &body.name)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
index
|
||||
.main
|
||||
.put_updated_at(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let created_at = index
|
||||
.main
|
||||
.created_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'created_at' date not found"))?;
|
||||
let updated_at = index
|
||||
.main
|
||||
.updated_at(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'updated_at' date not found"))?;
|
||||
|
||||
let response_body = UpdateIndexResponse {
|
||||
name: body.name,
|
||||
uid: index_uid,
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct SchemaParams {
|
||||
raw: bool,
|
||||
}
|
||||
|
||||
pub async fn get_index_schema(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
// Tide doesn't support "no query param"
|
||||
let params: SchemaParams = ctx.url_query().unwrap_or_default();
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let schema = index
|
||||
.main
|
||||
.schema(&reader)
|
||||
.map_err(ResponseError::open_index)?;
|
||||
|
||||
match schema {
|
||||
Some(schema) => {
|
||||
if params.raw {
|
||||
Ok(tide::response::json(schema))
|
||||
} else {
|
||||
Ok(tide::response::json(SchemaBody::from(schema)))
|
||||
}
|
||||
}
|
||||
None => Err(ResponseError::not_found("missing index schema")),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_schema(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
|
||||
let params: SchemaParams = ctx.url_query().unwrap_or_default();
|
||||
|
||||
let schema = if params.raw {
|
||||
ctx.body_json::<SchemaBuilder>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?
|
||||
.build()
|
||||
} else {
|
||||
ctx.body_json::<SchemaBody>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?
|
||||
.into()
|
||||
};
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let index = db
|
||||
.open_index(&index_uid)
|
||||
.ok_or(ResponseError::index_not_found(index_uid))?;
|
||||
|
||||
let update_id = index
|
||||
.schema_update(&mut writer, schema.clone())
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn get_update_status(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let update_id = ctx
|
||||
.param::<u64>("update_id")
|
||||
.map_err(|e| ResponseError::bad_parameter("update_id", e))?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let status = index
|
||||
.update_status(&reader, update_id)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = match status {
|
||||
Some(status) => tide::response::json(status)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response(),
|
||||
None => tide::response::json(json!({ "message": "unknown update id" }))
|
||||
.with_status(StatusCode::NOT_FOUND)
|
||||
.into_response(),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get_all_updates_status(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(IndexesRead)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let all_status = index
|
||||
.all_updates_status(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = tide::response::json(all_status)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response();
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn delete_index(ctx: Context<Data>) -> SResult<StatusCode> {
|
||||
ctx.is_allowed(IndexesWrite)?;
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
|
||||
let found = ctx
|
||||
.state()
|
||||
.db
|
||||
.delete_index(&index_uid)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
if found {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Ok(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index_update_callback(index_uid: &str, data: &Data, status: ProcessedUpdateResult) {
|
||||
if status.error.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(index) = data.db.open_index(&index_uid) {
|
||||
let env = &data.db.env;
|
||||
let mut writer = match env.write_txn() {
|
||||
Ok(writer) => writer,
|
||||
Err(e) => {
|
||||
error!("Impossible to get write_txn; {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = data.compute_stats(&mut writer, &index_uid) {
|
||||
error!("Impossible to compute stats; {}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = data.set_last_update(&mut writer) {
|
||||
error!("Impossible to update last_update; {}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = index.main.put_updated_at(&mut writer) {
|
||||
error!("Impossible to update updated_at; {}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = writer.commit() {
|
||||
error!("Impossible to get write_txn; {}", e);
|
||||
}
|
||||
}
|
||||
}
|
199
meilisearch-http/src/routes/key.rs
Normal file
199
meilisearch-http/src/routes/key.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
use chrono::serde::ts_seconds;
|
||||
use chrono::{DateTime, Utc};
|
||||
use heed::types::{SerdeBincode, Str};
|
||||
use http::StatusCode;
|
||||
use rand::seq::SliceRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::models::token::*;
|
||||
use crate::Data;
|
||||
|
||||
fn generate_api_key() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let sample = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
sample
|
||||
.choose_multiple(&mut rng, 40)
|
||||
.map(|c| *c as char)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn list(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let common_store = db.common_store();
|
||||
|
||||
let mut response: Vec<Token> = Vec::new();
|
||||
|
||||
let iter = common_store
|
||||
.prefix_iter::<Str, SerdeBincode<Token>>(&reader, TOKEN_PREFIX_KEY)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
for result in iter {
|
||||
let (_, token) = result.map_err(ResponseError::internal)?;
|
||||
response.push(token);
|
||||
}
|
||||
|
||||
Ok(tide::response::json(response))
|
||||
}
|
||||
|
||||
pub async fn get(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
let request_key = ctx.url_param("key")?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let token_key = format!("{}{}", TOKEN_PREFIX_KEY, request_key);
|
||||
|
||||
let token_config = db
|
||||
.common_store()
|
||||
.get::<Str, SerdeBincode<Token>>(&reader, &token_key)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::not_found(format!(
|
||||
"token key: {}",
|
||||
token_key
|
||||
)))?;
|
||||
|
||||
Ok(tide::response::json(token_config))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct CreatedRequest {
|
||||
description: String,
|
||||
acl: Vec<ACL>,
|
||||
indexes: Vec<Wildcard>,
|
||||
#[serde(with = "ts_seconds")]
|
||||
expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn create(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
|
||||
let data: CreatedRequest = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let key = generate_api_key();
|
||||
let token_key = format!("{}{}", TOKEN_PREFIX_KEY, key);
|
||||
|
||||
let token_definition = Token {
|
||||
key,
|
||||
description: data.description,
|
||||
acl: data.acl,
|
||||
indexes: data.indexes,
|
||||
expires_at: data.expires_at,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
revoked: false,
|
||||
};
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
db.common_store()
|
||||
.put::<Str, SerdeBincode<Token>>(&mut writer, &token_key, &token_definition)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
Ok(tide::response::json(token_definition)
|
||||
.with_status(StatusCode::CREATED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct UpdatedRequest {
|
||||
description: Option<String>,
|
||||
acl: Option<Vec<ACL>>,
|
||||
indexes: Option<Vec<Wildcard>>,
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
revoked: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn update(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
let request_key = ctx.url_param("key")?;
|
||||
|
||||
let data: UpdatedRequest = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let common_store = db.common_store();
|
||||
|
||||
let token_key = format!("{}{}", TOKEN_PREFIX_KEY, request_key);
|
||||
|
||||
let mut token_config = common_store
|
||||
.get::<Str, SerdeBincode<Token>>(&writer, &token_key)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::not_found(format!(
|
||||
"token key: {}",
|
||||
token_key
|
||||
)))?;
|
||||
|
||||
// apply the modifications
|
||||
if let Some(description) = data.description {
|
||||
token_config.description = description;
|
||||
}
|
||||
|
||||
if let Some(acl) = data.acl {
|
||||
token_config.acl = acl;
|
||||
}
|
||||
|
||||
if let Some(indexes) = data.indexes {
|
||||
token_config.indexes = indexes;
|
||||
}
|
||||
|
||||
if let Some(expires_at) = data.expires_at {
|
||||
token_config.expires_at = expires_at;
|
||||
}
|
||||
|
||||
if let Some(revoked) = data.revoked {
|
||||
token_config.revoked = revoked;
|
||||
}
|
||||
|
||||
token_config.updated_at = Utc::now();
|
||||
|
||||
common_store
|
||||
.put::<Str, SerdeBincode<Token>>(&mut writer, &token_key, &token_config)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
Ok(tide::response::json(token_config)
|
||||
.with_status(StatusCode::OK)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete(ctx: Context<Data>) -> SResult<StatusCode> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
let request_key = ctx.url_param("key")?;
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let common_store = db.common_store();
|
||||
|
||||
let token_key = format!("{}{}", TOKEN_PREFIX_KEY, request_key);
|
||||
|
||||
common_store
|
||||
.delete::<Str>(&mut writer, &token_key)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
122
meilisearch-http/src/routes/mod.rs
Normal file
122
meilisearch-http/src/routes/mod.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use crate::data::Data;
|
||||
|
||||
pub mod document;
|
||||
pub mod health;
|
||||
pub mod index;
|
||||
pub mod key;
|
||||
pub mod search;
|
||||
pub mod setting;
|
||||
pub mod stats;
|
||||
pub mod stop_words;
|
||||
pub mod synonym;
|
||||
|
||||
pub fn load_routes(app: &mut tide::App<Data>) {
|
||||
app.at("").nest(|router| {
|
||||
router.at("/indexes").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(index::list_indexes)
|
||||
.post(index::create_index);
|
||||
|
||||
router.at("/search").post(search::search_multi_index);
|
||||
|
||||
router.at("/:index").nest(|router| {
|
||||
router.at("/search").get(search::search_with_url_query);
|
||||
|
||||
router.at("/updates").nest(|router| {
|
||||
router.at("/").get(index::get_all_updates_status);
|
||||
|
||||
router.at("/:update_id").get(index::get_update_status);
|
||||
});
|
||||
|
||||
router
|
||||
.at("/")
|
||||
.get(index::get_index)
|
||||
.put(index::update_index)
|
||||
.delete(index::delete_index);
|
||||
|
||||
router
|
||||
.at("/schema")
|
||||
.get(index::get_index_schema)
|
||||
.put(index::update_schema);
|
||||
|
||||
router.at("/documents").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(document::get_all_documents)
|
||||
.post(document::add_or_replace_multiple_documents)
|
||||
.put(document::add_or_update_multiple_documents)
|
||||
.delete(document::clear_all_documents);
|
||||
|
||||
router.at("/:identifier").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(document::get_document)
|
||||
.delete(document::delete_document);
|
||||
});
|
||||
|
||||
router
|
||||
.at("/delete")
|
||||
.post(document::delete_multiple_documents);
|
||||
});
|
||||
|
||||
router.at("/synonyms").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(synonym::list)
|
||||
.post(synonym::create)
|
||||
.delete(synonym::clear);
|
||||
|
||||
router
|
||||
.at("/:synonym")
|
||||
.get(synonym::get)
|
||||
.put(synonym::update)
|
||||
.delete(synonym::delete);
|
||||
|
||||
router.at("/batch").post(synonym::batch_write);
|
||||
});
|
||||
|
||||
router.at("/stop-words").nest(|router| {
|
||||
router
|
||||
.at("/")
|
||||
.get(stop_words::list)
|
||||
.patch(stop_words::add)
|
||||
.delete(stop_words::delete);
|
||||
});
|
||||
|
||||
router
|
||||
.at("/settings")
|
||||
.get(setting::get)
|
||||
.post(setting::update);
|
||||
});
|
||||
});
|
||||
|
||||
router.at("/keys").nest(|router| {
|
||||
router.at("/").get(key::list).post(key::create);
|
||||
|
||||
router
|
||||
.at("/:key")
|
||||
.get(key::get)
|
||||
.put(key::update)
|
||||
.delete(key::delete);
|
||||
});
|
||||
});
|
||||
|
||||
// Private
|
||||
app.at("").nest(|router| {
|
||||
router
|
||||
.at("/health")
|
||||
.get(health::get_health)
|
||||
.post(health::set_healthy)
|
||||
.put(health::change_healthyness)
|
||||
.delete(health::set_unhealthy);
|
||||
|
||||
router.at("/stats").get(stats::get_stats);
|
||||
router.at("/stats/:index").get(stats::index_stat);
|
||||
router.at("/version").get(stats::get_version);
|
||||
router.at("/sys-info").get(stats::get_sys_info);
|
||||
router
|
||||
.at("/sys-info/pretty")
|
||||
.get(stats::get_sys_info_pretty);
|
||||
});
|
||||
}
|
245
meilisearch-http/src/routes/search.rs
Normal file
245
meilisearch-http/src/routes/search.rs
Normal file
|
@ -0,0 +1,245 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use meilisearch_core::Index;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tide::querystring::ContextExt as QSContextExt;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::meilisearch::{Error, IndexSearchExt, SearchHit};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::Data;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct SearchQuery {
|
||||
q: String,
|
||||
offset: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
attributes_to_retrieve: Option<String>,
|
||||
attributes_to_search_in: Option<String>,
|
||||
attributes_to_crop: Option<String>,
|
||||
crop_length: Option<usize>,
|
||||
attributes_to_highlight: Option<String>,
|
||||
filters: Option<String>,
|
||||
timeout_ms: Option<u64>,
|
||||
matches: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn search_with_url_query(ctx: Context<Data>) -> SResult<Response> {
|
||||
// ctx.is_allowed(DocumentsRead)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let schema = index
|
||||
.main
|
||||
.schema(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::open_index("No Schema found"))?;
|
||||
|
||||
let query: SearchQuery = ctx
|
||||
.url_query()
|
||||
.map_err(|_| ResponseError::bad_request("invalid query parameter"))?;
|
||||
|
||||
let mut search_builder = index.new_search(query.q.clone());
|
||||
|
||||
if let Some(offset) = query.offset {
|
||||
search_builder.offset(offset);
|
||||
}
|
||||
if let Some(limit) = query.limit {
|
||||
search_builder.limit(limit);
|
||||
}
|
||||
|
||||
if let Some(attributes_to_retrieve) = query.attributes_to_retrieve {
|
||||
for attr in attributes_to_retrieve.split(',') {
|
||||
search_builder.add_retrievable_field(attr.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(attributes_to_search_in) = query.attributes_to_search_in {
|
||||
for attr in attributes_to_search_in.split(',') {
|
||||
search_builder.add_attribute_to_search_in(attr.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(attributes_to_crop) = query.attributes_to_crop {
|
||||
let crop_length = query.crop_length.unwrap_or(200);
|
||||
if attributes_to_crop == "*" {
|
||||
let attributes_to_crop = schema
|
||||
.iter()
|
||||
.map(|(attr, ..)| (attr.to_string(), crop_length))
|
||||
.collect();
|
||||
search_builder.attributes_to_crop(attributes_to_crop);
|
||||
} else {
|
||||
let attributes_to_crop = attributes_to_crop
|
||||
.split(',')
|
||||
.map(|r| (r.to_string(), crop_length))
|
||||
.collect();
|
||||
search_builder.attributes_to_crop(attributes_to_crop);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(attributes_to_highlight) = query.attributes_to_highlight {
|
||||
let attributes_to_highlight = if attributes_to_highlight == "*" {
|
||||
schema.iter().map(|(attr, ..)| attr.to_string()).collect()
|
||||
} else {
|
||||
attributes_to_highlight
|
||||
.split(',')
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
};
|
||||
|
||||
search_builder.attributes_to_highlight(attributes_to_highlight);
|
||||
}
|
||||
|
||||
if let Some(filters) = query.filters {
|
||||
search_builder.filters(filters);
|
||||
}
|
||||
|
||||
if let Some(timeout_ms) = query.timeout_ms {
|
||||
search_builder.timeout(Duration::from_millis(timeout_ms));
|
||||
}
|
||||
|
||||
if let Some(matches) = query.matches {
|
||||
if matches {
|
||||
search_builder.get_matches();
|
||||
}
|
||||
}
|
||||
|
||||
let response = match search_builder.search(&reader) {
|
||||
Ok(response) => response,
|
||||
Err(Error::Internal(message)) => return Err(ResponseError::Internal(message)),
|
||||
Err(others) => return Err(ResponseError::bad_request(others)),
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response))
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct SearchMultiBody {
|
||||
indexes: HashSet<String>,
|
||||
query: String,
|
||||
offset: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
attributes_to_retrieve: Option<HashSet<String>>,
|
||||
attributes_to_search_in: Option<HashSet<String>>,
|
||||
attributes_to_crop: Option<HashMap<String, usize>>,
|
||||
attributes_to_highlight: Option<HashSet<String>>,
|
||||
filters: Option<String>,
|
||||
timeout_ms: Option<u64>,
|
||||
matches: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SearchMultiBodyResponse {
|
||||
hits: HashMap<String, Vec<SearchHit>>,
|
||||
offset: usize,
|
||||
hits_per_page: usize,
|
||||
processing_time_ms: usize,
|
||||
query: String,
|
||||
}
|
||||
|
||||
pub async fn search_multi_index(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
// ctx.is_allowed(DocumentsRead)?;
|
||||
let body = ctx
|
||||
.body_json::<SearchMultiBody>()
|
||||
.await
|
||||
.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let mut index_list = body.clone().indexes;
|
||||
|
||||
for index in index_list.clone() {
|
||||
if index == "*" {
|
||||
index_list = ctx.state().db.indexes_uids().into_iter().collect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
let mut count = 20;
|
||||
|
||||
if let Some(body_offset) = body.offset {
|
||||
if let Some(limit) = body.limit {
|
||||
offset = body_offset;
|
||||
count = limit;
|
||||
}
|
||||
}
|
||||
|
||||
let offset = offset;
|
||||
let count = count;
|
||||
let db = &ctx.state().db;
|
||||
let par_body = body.clone();
|
||||
let responses_per_index: Vec<SResult<_>> = index_list
|
||||
.into_par_iter()
|
||||
.map(move |index_uid| {
|
||||
let index: Index = db
|
||||
.open_index(&index_uid)
|
||||
.ok_or(ResponseError::index_not_found(&index_uid))?;
|
||||
|
||||
let mut search_builder = index.new_search(par_body.query.clone());
|
||||
|
||||
search_builder.offset(offset);
|
||||
search_builder.limit(count);
|
||||
|
||||
if let Some(attributes_to_retrieve) = par_body.attributes_to_retrieve.clone() {
|
||||
search_builder.attributes_to_retrieve(attributes_to_retrieve);
|
||||
}
|
||||
if let Some(attributes_to_search_in) = par_body.attributes_to_search_in.clone() {
|
||||
search_builder.attributes_to_search_in(attributes_to_search_in);
|
||||
}
|
||||
if let Some(attributes_to_crop) = par_body.attributes_to_crop.clone() {
|
||||
search_builder.attributes_to_crop(attributes_to_crop);
|
||||
}
|
||||
if let Some(attributes_to_highlight) = par_body.attributes_to_highlight.clone() {
|
||||
search_builder.attributes_to_highlight(attributes_to_highlight);
|
||||
}
|
||||
if let Some(filters) = par_body.filters.clone() {
|
||||
search_builder.filters(filters);
|
||||
}
|
||||
if let Some(timeout_ms) = par_body.timeout_ms {
|
||||
search_builder.timeout(Duration::from_millis(timeout_ms));
|
||||
}
|
||||
if let Some(matches) = par_body.matches {
|
||||
if matches {
|
||||
search_builder.get_matches();
|
||||
}
|
||||
}
|
||||
|
||||
let env = &db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let response = search_builder
|
||||
.search(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
Ok((index_uid, response))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut hits_map = HashMap::new();
|
||||
|
||||
let mut max_query_time = 0;
|
||||
|
||||
for response in responses_per_index {
|
||||
if let Ok((index_uid, response)) = response {
|
||||
if response.processing_time_ms > max_query_time {
|
||||
max_query_time = response.processing_time_ms;
|
||||
}
|
||||
hits_map.insert(index_uid, response.hits);
|
||||
}
|
||||
}
|
||||
|
||||
let response = SearchMultiBodyResponse {
|
||||
hits: hits_map,
|
||||
offset,
|
||||
hits_per_page: count,
|
||||
processing_time_ms: max_query_time,
|
||||
query: body.query,
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response))
|
||||
}
|
87
meilisearch-http/src/routes/setting.rs
Normal file
87
meilisearch-http/src/routes/setting.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::routes::document::IndexUpdateResponse;
|
||||
use crate::Data;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SettingBody {
|
||||
pub ranking_order: Option<RankingOrder>,
|
||||
pub distinct_field: Option<DistinctField>,
|
||||
pub ranking_rules: Option<RankingRules>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RankingOrdering {
|
||||
Asc,
|
||||
Dsc,
|
||||
}
|
||||
|
||||
pub type RankingOrder = Vec<String>;
|
||||
pub type DistinctField = String;
|
||||
pub type RankingRules = HashMap<String, RankingOrdering>;
|
||||
|
||||
pub async fn get(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsRead)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let settings = match index.main.customs(&reader).unwrap() {
|
||||
Some(bytes) => bincode::deserialize(bytes).unwrap(),
|
||||
None => SettingBody::default(),
|
||||
};
|
||||
|
||||
Ok(tide::response::json(settings))
|
||||
}
|
||||
|
||||
pub async fn update(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsWrite)?;
|
||||
|
||||
let settings: SettingBody = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut current_settings = match index.main.customs(&writer).unwrap() {
|
||||
Some(bytes) => bincode::deserialize(bytes).unwrap(),
|
||||
None => SettingBody::default(),
|
||||
};
|
||||
|
||||
if let Some(ranking_order) = settings.ranking_order {
|
||||
current_settings.ranking_order = Some(ranking_order);
|
||||
}
|
||||
|
||||
if let Some(distinct_field) = settings.distinct_field {
|
||||
current_settings.distinct_field = Some(distinct_field);
|
||||
}
|
||||
|
||||
if let Some(ranking_rules) = settings.ranking_rules {
|
||||
current_settings.ranking_rules = Some(ranking_rules);
|
||||
}
|
||||
|
||||
let bytes = bincode::serialize(¤t_settings).unwrap();
|
||||
|
||||
let update_id = index
|
||||
.customs_update(&mut writer, bytes)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
337
meilisearch-http/src/routes/stats.rs
Normal file
337
meilisearch-http/src/routes/stats.rs
Normal file
|
@ -0,0 +1,337 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::error;
|
||||
use pretty_bytes::converter::convert;
|
||||
use serde::Serialize;
|
||||
use sysinfo::{NetworkExt, Pid, ProcessExt, ProcessorExt, System, SystemExt};
|
||||
use tide::{Context, Response};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::Data;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IndexStatsResponse {
|
||||
number_of_documents: u64,
|
||||
is_indexing: bool,
|
||||
fields_frequency: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
pub async fn index_stat(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
let index_uid = ctx.url_param("index")?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let number_of_documents = index
|
||||
.main
|
||||
.number_of_documents(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let fields_frequency = index
|
||||
.main
|
||||
.fields_frequency(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_indexing = ctx
|
||||
.state()
|
||||
.is_indexing(&reader, &index_uid)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'is_indexing' date not found"))?;
|
||||
|
||||
let response = IndexStatsResponse {
|
||||
number_of_documents,
|
||||
is_indexing,
|
||||
fields_frequency,
|
||||
};
|
||||
Ok(tide::response::json(response))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StatsResult {
|
||||
database_size: u64,
|
||||
last_update: Option<DateTime<Utc>>,
|
||||
indexes: HashMap<String, IndexStatsResponse>,
|
||||
}
|
||||
|
||||
pub async fn get_stats(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
|
||||
let mut index_list = HashMap::new();
|
||||
|
||||
let db = &ctx.state().db;
|
||||
let env = &db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let indexes_set = ctx.state().db.indexes_uids();
|
||||
for index_uid in indexes_set {
|
||||
let index = ctx.state().db.open_index(&index_uid);
|
||||
|
||||
match index {
|
||||
Some(index) => {
|
||||
let number_of_documents = index
|
||||
.main
|
||||
.number_of_documents(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let fields_frequency = index
|
||||
.main
|
||||
.fields_frequency(&reader)
|
||||
.map_err(ResponseError::internal)?
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_indexing = ctx
|
||||
.state()
|
||||
.is_indexing(&reader, &index_uid)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::internal("'is_indexing' date not found"))?;
|
||||
|
||||
let response = IndexStatsResponse {
|
||||
number_of_documents,
|
||||
is_indexing,
|
||||
fields_frequency,
|
||||
};
|
||||
index_list.insert(index_uid, response);
|
||||
}
|
||||
None => error!(
|
||||
"Index {:?} is referenced in the indexes list but cannot be found",
|
||||
index_uid
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let database_size = WalkDir::new(ctx.state().db_path.clone())
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.metadata().ok())
|
||||
.filter(|metadata| metadata.is_file())
|
||||
.fold(0, |acc, m| acc + m.len());
|
||||
|
||||
let last_update = ctx
|
||||
.state()
|
||||
.last_update(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let response = StatsResult {
|
||||
database_size,
|
||||
last_update,
|
||||
indexes: index_list,
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VersionResponse {
|
||||
commit_sha: String,
|
||||
build_date: String,
|
||||
pkg_version: String,
|
||||
}
|
||||
|
||||
pub async fn get_version(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
let response = VersionResponse {
|
||||
commit_sha: env!("VERGEN_SHA").to_string(),
|
||||
build_date: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||
pkg_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
|
||||
Ok(tide::response::json(response))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SysGlobal {
|
||||
total_memory: u64,
|
||||
used_memory: u64,
|
||||
total_swap: u64,
|
||||
used_swap: u64,
|
||||
input_data: u64,
|
||||
output_data: u64,
|
||||
}
|
||||
|
||||
impl SysGlobal {
|
||||
fn new() -> SysGlobal {
|
||||
SysGlobal {
|
||||
total_memory: 0,
|
||||
used_memory: 0,
|
||||
total_swap: 0,
|
||||
used_swap: 0,
|
||||
input_data: 0,
|
||||
output_data: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SysProcess {
|
||||
memory: u64,
|
||||
cpu: f32,
|
||||
}
|
||||
|
||||
impl SysProcess {
|
||||
fn new() -> SysProcess {
|
||||
SysProcess {
|
||||
memory: 0,
|
||||
cpu: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SysInfo {
|
||||
memory_usage: f64,
|
||||
processor_usage: Vec<f32>,
|
||||
global: SysGlobal,
|
||||
process: SysProcess,
|
||||
}
|
||||
|
||||
impl SysInfo {
|
||||
fn new() -> SysInfo {
|
||||
SysInfo {
|
||||
memory_usage: 0.0,
|
||||
processor_usage: Vec::new(),
|
||||
global: SysGlobal::new(),
|
||||
process: SysProcess::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn report(pid: Pid) -> SysInfo {
|
||||
let mut sys = System::new();
|
||||
let mut info = SysInfo::new();
|
||||
|
||||
info.memory_usage = sys.get_used_memory() as f64 / sys.get_total_memory() as f64 * 100.0;
|
||||
|
||||
for processor in sys.get_processor_list() {
|
||||
info.processor_usage.push(processor.get_cpu_usage() * 100.0);
|
||||
}
|
||||
|
||||
info.global.total_memory = sys.get_total_memory();
|
||||
info.global.used_memory = sys.get_used_memory();
|
||||
info.global.total_swap = sys.get_total_swap();
|
||||
info.global.used_swap = sys.get_used_swap();
|
||||
info.global.input_data = sys.get_network().get_income();
|
||||
info.global.output_data = sys.get_network().get_outcome();
|
||||
|
||||
if let Some(process) = sys.get_process(pid) {
|
||||
info.process.memory = process.memory();
|
||||
info.process.cpu = process.cpu_usage() * 100.0;
|
||||
}
|
||||
|
||||
sys.refresh_all();
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub async fn get_sys_info(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
Ok(tide::response::json(report(ctx.state().server_pid)))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SysGlobalPretty {
|
||||
total_memory: String,
|
||||
used_memory: String,
|
||||
total_swap: String,
|
||||
used_swap: String,
|
||||
input_data: String,
|
||||
output_data: String,
|
||||
}
|
||||
|
||||
impl SysGlobalPretty {
|
||||
fn new() -> SysGlobalPretty {
|
||||
SysGlobalPretty {
|
||||
total_memory: "None".to_owned(),
|
||||
used_memory: "None".to_owned(),
|
||||
total_swap: "None".to_owned(),
|
||||
used_swap: "None".to_owned(),
|
||||
input_data: "None".to_owned(),
|
||||
output_data: "None".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SysProcessPretty {
|
||||
memory: String,
|
||||
cpu: String,
|
||||
}
|
||||
|
||||
impl SysProcessPretty {
|
||||
fn new() -> SysProcessPretty {
|
||||
SysProcessPretty {
|
||||
memory: "None".to_owned(),
|
||||
cpu: "None".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SysInfoPretty {
|
||||
memory_usage: String,
|
||||
processor_usage: Vec<String>,
|
||||
global: SysGlobalPretty,
|
||||
process: SysProcessPretty,
|
||||
}
|
||||
|
||||
impl SysInfoPretty {
|
||||
fn new() -> SysInfoPretty {
|
||||
SysInfoPretty {
|
||||
memory_usage: "None".to_owned(),
|
||||
processor_usage: Vec::new(),
|
||||
global: SysGlobalPretty::new(),
|
||||
process: SysProcessPretty::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn report_pretty(pid: Pid) -> SysInfoPretty {
|
||||
let mut sys = System::new();
|
||||
let mut info = SysInfoPretty::new();
|
||||
|
||||
info.memory_usage = format!(
|
||||
"{:.1} %",
|
||||
sys.get_used_memory() as f64 / sys.get_total_memory() as f64 * 100.0
|
||||
);
|
||||
|
||||
for processor in sys.get_processor_list() {
|
||||
info.processor_usage
|
||||
.push(format!("{:.1} %", processor.get_cpu_usage() * 100.0));
|
||||
}
|
||||
|
||||
info.global.total_memory = convert(sys.get_total_memory() as f64 * 1024.0);
|
||||
info.global.used_memory = convert(sys.get_used_memory() as f64 * 1024.0);
|
||||
info.global.total_swap = convert(sys.get_total_swap() as f64 * 1024.0);
|
||||
info.global.used_swap = convert(sys.get_used_swap() as f64 * 1024.0);
|
||||
info.global.input_data = convert(sys.get_network().get_income() as f64);
|
||||
info.global.output_data = convert(sys.get_network().get_outcome() as f64);
|
||||
|
||||
if let Some(process) = sys.get_process(pid) {
|
||||
info.process.memory = convert(process.memory() as f64 * 1024.0);
|
||||
info.process.cpu = format!("{:.1} %", process.cpu_usage() * 100.0);
|
||||
}
|
||||
|
||||
sys.refresh_all();
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub async fn get_sys_info_pretty(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(Admin)?;
|
||||
Ok(tide::response::json(report_pretty(ctx.state().server_pid)))
|
||||
}
|
82
meilisearch-http/src/routes/stop_words.rs
Normal file
82
meilisearch-http/src/routes/stop_words.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use http::StatusCode;
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::routes::document::IndexUpdateResponse;
|
||||
use crate::Data;
|
||||
|
||||
pub async fn list(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsRead)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let stop_words_fst = index
|
||||
.main
|
||||
.stop_words_fst(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let stop_words = stop_words_fst
|
||||
.unwrap_or_default()
|
||||
.stream()
|
||||
.into_strs()
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
Ok(tide::response::json(stop_words))
|
||||
}
|
||||
|
||||
pub async fn add(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsRead)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let data: Vec<String> = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut stop_words_addition = index.stop_words_addition();
|
||||
for stop_word in data {
|
||||
stop_words_addition.add_stop_word(stop_word);
|
||||
}
|
||||
|
||||
let update_id = stop_words_addition
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsRead)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let data: Vec<String> = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut stop_words_deletion = index.stop_words_deletion();
|
||||
for stop_word in data {
|
||||
stop_words_deletion.delete_stop_word(stop_word);
|
||||
}
|
||||
|
||||
let update_id = stop_words_deletion
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
235
meilisearch-http/src/routes/synonym.rs
Normal file
235
meilisearch-http/src/routes/synonym.rs
Normal file
|
@ -0,0 +1,235 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tide::response::IntoResponse;
|
||||
use tide::{Context, Response};
|
||||
|
||||
use crate::error::{ResponseError, SResult};
|
||||
use crate::helpers::tide::ContextExt;
|
||||
use crate::models::token::ACL::*;
|
||||
use crate::routes::document::IndexUpdateResponse;
|
||||
use crate::Data;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Synonym {
|
||||
OneWay(SynonymOneWay),
|
||||
MultiWay { synonyms: Vec<String> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SynonymOneWay {
|
||||
pub input: String,
|
||||
pub synonyms: Vec<String>,
|
||||
}
|
||||
|
||||
pub type Synonyms = Vec<Synonym>;
|
||||
|
||||
pub async fn list(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsRead)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let synonyms_fst = index
|
||||
.main
|
||||
.synonyms_fst(&reader)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let synonyms_fst = synonyms_fst.unwrap_or_default();
|
||||
let synonyms_list = synonyms_fst.stream().into_strs().unwrap();
|
||||
|
||||
let mut response = HashMap::new();
|
||||
|
||||
let index_synonyms = &index.synonyms;
|
||||
|
||||
for synonym in synonyms_list {
|
||||
let alternative_list = index_synonyms
|
||||
.synonyms(&reader, synonym.as_bytes())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.stream()
|
||||
.into_strs()
|
||||
.unwrap();
|
||||
response.insert(synonym, alternative_list);
|
||||
}
|
||||
|
||||
Ok(tide::response::json(response))
|
||||
}
|
||||
|
||||
pub async fn get(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsRead)?;
|
||||
let synonym = ctx.url_param("synonym")?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let synonym_list = index
|
||||
.synonyms
|
||||
.synonyms(&reader, synonym.as_bytes())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.stream()
|
||||
.into_strs()
|
||||
.unwrap();
|
||||
|
||||
Ok(tide::response::json(synonym_list))
|
||||
}
|
||||
|
||||
pub async fn create(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsWrite)?;
|
||||
|
||||
let data: Synonym = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut synonyms_addition = index.synonyms_addition();
|
||||
|
||||
match data.clone() {
|
||||
Synonym::OneWay(content) => {
|
||||
synonyms_addition.add_synonym(content.input, content.synonyms.into_iter())
|
||||
}
|
||||
Synonym::MultiWay { mut synonyms } => {
|
||||
if synonyms.len() > 1 {
|
||||
for _ in 0..synonyms.len() {
|
||||
let (first, elems) = synonyms.split_first().unwrap();
|
||||
synonyms_addition.add_synonym(first, elems.iter());
|
||||
synonyms.rotate_left(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let update_id = synonyms_addition
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn update(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsWrite)?;
|
||||
let synonym = ctx.url_param("synonym")?;
|
||||
let index = ctx.index()?;
|
||||
let data: Vec<String> = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut synonyms_addition = index.synonyms_addition();
|
||||
synonyms_addition.add_synonym(synonym.clone(), data.clone().into_iter());
|
||||
let update_id = synonyms_addition
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsWrite)?;
|
||||
let synonym = ctx.url_param("synonym")?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut synonyms_deletion = index.synonyms_deletion();
|
||||
synonyms_deletion.delete_all_alternatives_of(synonym);
|
||||
let update_id = synonyms_deletion
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn batch_write(mut ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsWrite)?;
|
||||
|
||||
let data: Synonyms = ctx.body_json().await.map_err(ResponseError::bad_request)?;
|
||||
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let mut synonyms_addition = index.synonyms_addition();
|
||||
for raw in data {
|
||||
match raw {
|
||||
Synonym::OneWay(content) => {
|
||||
synonyms_addition.add_synonym(content.input, content.synonyms.into_iter())
|
||||
}
|
||||
Synonym::MultiWay { mut synonyms } => {
|
||||
if synonyms.len() > 1 {
|
||||
for _ in 0..synonyms.len() {
|
||||
let (first, elems) = synonyms.split_first().unwrap();
|
||||
synonyms_addition.add_synonym(first, elems.iter());
|
||||
synonyms.rotate_left(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let update_id = synonyms_addition
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn clear(ctx: Context<Data>) -> SResult<Response> {
|
||||
ctx.is_allowed(SettingsWrite)?;
|
||||
let index = ctx.index()?;
|
||||
|
||||
let env = &ctx.state().db.env;
|
||||
let mut writer = env.write_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let synonyms_fst = index
|
||||
.main
|
||||
.synonyms_fst(&writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
let synonyms_fst = synonyms_fst.unwrap_or_default();
|
||||
let synonyms_list = synonyms_fst.stream().into_strs().unwrap();
|
||||
|
||||
let mut synonyms_deletion = index.synonyms_deletion();
|
||||
for synonym in synonyms_list {
|
||||
synonyms_deletion.delete_all_alternatives_of(synonym);
|
||||
}
|
||||
let update_id = synonyms_deletion
|
||||
.finalize(&mut writer)
|
||||
.map_err(ResponseError::internal)?;
|
||||
|
||||
writer.commit().map_err(ResponseError::internal)?;
|
||||
|
||||
let response_body = IndexUpdateResponse { update_id };
|
||||
Ok(tide::response::json(response_body)
|
||||
.with_status(StatusCode::ACCEPTED)
|
||||
.into_response())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue