diff --git a/crates/meilisearch/src/routes/indexes/render.rs b/crates/meilisearch/src/routes/indexes/render.rs index 42d6a23b1..b22c931c7 100644 --- a/crates/meilisearch/src/routes/indexes/render.rs +++ b/crates/meilisearch/src/routes/indexes/render.rs @@ -16,7 +16,7 @@ use meilisearch_types::error::ResponseError; use meilisearch_types::heed::RoTxn; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::keys::actions; -use meilisearch_types::milli::prompt::{get_document, get_inline_document_fields}; +use meilisearch_types::milli::prompt::{get_document, OwnedFields}; use meilisearch_types::milli::vector::db::IndexEmbeddingConfig; use meilisearch_types::milli::vector::json_template::{self, JsonTemplate}; use meilisearch_types::milli::vector::EmbedderOptions; @@ -179,6 +179,7 @@ enum RenderError<'a> { ExpectedValue(milli::Span<'a>), DocumentNotFound(String), + DocumentMustBeMap, BothInlineDocAndDocId, TemplateParsing(json_template::Error), TemplateRendering(json_template::Error), @@ -309,6 +310,10 @@ impl From> for ResponseError { format!("Document with ID `{doc_id}` not found."), Code::RenderDocumentNotFound, ), + DocumentMustBeMap => ResponseError::from_msg( + String::from("The `doc` field must be a map."), + Code::InvalidRenderInput, + ), BothInlineDocAndDocId => ResponseError::from_msg( String::from("A document id was provided but adding it to the input would overwrite the `doc` field that you already defined inline."), Code::InvalidRenderInput, @@ -477,17 +482,16 @@ fn parse_template_id<'a>( } async fn render(index: Index, query: RenderQuery) -> Result { + let RenderQuery { template, input } = query; let rtxn = index.read_txn()?; - - let (template, fields_available) = match (query.template.inline, query.template.id) { + let (template, fields_available) = match (template.inline, template.id) { (Some(inline), None) => (inline, true), (None, Some(id)) => parse_template_id(&index, &rtxn, &id)?, (Some(_), Some(_)) => return Err(MultipleTemplates.into()), (None, None) => return Err(MissingTemplate.into()), }; - let fields_already_present = query - .input + let fields_already_present = input .as_ref() .is_some_and(|i| i.inline.as_ref().is_some_and(|i| i.get("fields").is_some())); let fields_unused = match template.as_str() { @@ -498,11 +502,9 @@ async fn render(index: Index, query: RenderQuery) -> Result true, // non-text templates cannot use `fields` }; - let has_inline_doc = query - .input - .as_ref() - .is_some_and(|i| i.inline.as_ref().is_some_and(|i| i.get("doc").is_some())); - let has_document_id = query.input.as_ref().is_some_and(|i| i.document_id.is_some()); + let has_inline_doc = + input.as_ref().is_some_and(|i| i.inline.as_ref().is_some_and(|i| i.get("doc").is_some())); + let has_document_id = input.as_ref().is_some_and(|i| i.document_id.is_some()); let has_doc = has_inline_doc || has_document_id; let insert_fields = fields_available && has_doc && !fields_unused && !fields_already_present; if has_inline_doc && has_document_id { @@ -510,14 +512,21 @@ async fn render(index: Index, query: RenderQuery) -> Result Some(doc), + Some(liquid::model::Value::Nil) => None, + None => None, + _ => return Err(DocumentMustBeMap.into()), + }; + if insert_fields { + if let Some(doc) = doc { + let doc = doc.clone(); + let fid_map_with_meta = index.fields_ids_map_with_metadata(&rtxn)?; + let fields = OwnedFields::new(&doc, &fid_map_with_meta); object.insert("fields".into(), fields.to_value()); } } diff --git a/crates/meilisearch/tests/documents/render_documents.rs b/crates/meilisearch/tests/documents/render_documents.rs index 909932ec9..672f6ab42 100644 --- a/crates/meilisearch/tests/documents/render_documents.rs +++ b/crates/meilisearch/tests/documents/render_documents.rs @@ -1,4 +1,4 @@ -use crate::common::{shared_index_for_fragments, Server}; +use crate::common::{shared_empty_index, shared_index_for_fragments, Server}; use crate::json; use meili_snap::{json_string, snapshot}; @@ -413,6 +413,41 @@ async fn render_inline_document_iko() { "#); } +#[actix_rt::test] +async fn render_doc_not_object() { + let index = shared_empty_index().await; + + let (value, code) = index + .render(json! {{ + "template": { "inline": "{{ doc }}" }, + "input": { "inline": { "doc": "that's not an object, that's a string" } }, + }}) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(value, @r#" + { + "message": "The `doc` field must be a map.", + "code": "invalid_render_input", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_render_input" + } + "#); + + let (value, code) = index + .render(json! {{ + "template": { "inline": "default" }, + "input": { "inline": { "doc": null } }, + }}) + .await; + snapshot!(code, @"200 OK"); + snapshot!(value, @r#" + { + "template": "default", + "rendered": "default" + } + "#); +} + #[actix_rt::test] async fn chat_completions() { let index = shared_index_for_fragments().await; diff --git a/crates/milli/src/prompt/mod.rs b/crates/milli/src/prompt/mod.rs index b5e616832..8fcdce5ad 100644 --- a/crates/milli/src/prompt/mod.rs +++ b/crates/milli/src/prompt/mod.rs @@ -13,8 +13,7 @@ pub(crate) use document::{Document, ParseableDocument}; use error::{NewPromptError, RenderPromptError}; pub use fields::{BorrowedFields, OwnedFields}; use heed::RoTxn; -use liquid::model::Value as LiquidValue; -use liquid::ValueView; +use liquid::{model::Value as LiquidValue, ValueView}; pub use self::context::Context; use crate::fields_ids_map::metadata::FieldIdMapWithMetadata; @@ -168,23 +167,6 @@ fn truncate(s: &mut String, max_bytes: usize) { } } -pub fn get_inline_document_fields( - index: &Index, - rtxn: &RoTxn<'_>, - inline_doc: &serde_json::Value, -) -> Result, crate::Error> { - let fid_map_with_meta = index.fields_ids_map_with_metadata(rtxn)?; - let inline_doc = match liquid::to_object(&inline_doc) { - Ok(inline_doc) => inline_doc, - Err(e) => { - return Ok(Err(e)); - } - }; - let fields = OwnedFields::new(&inline_doc, &fid_map_with_meta); - - Ok(Ok(fields.to_value())) -} - pub fn get_document( index: &Index, rtxn: &RoTxn<'_>,