mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-09-06 12:46:31 +00:00
Force docs to be objects
This commit is contained in:
@ -16,7 +16,7 @@ use meilisearch_types::error::ResponseError;
|
|||||||
use meilisearch_types::heed::RoTxn;
|
use meilisearch_types::heed::RoTxn;
|
||||||
use meilisearch_types::index_uid::IndexUid;
|
use meilisearch_types::index_uid::IndexUid;
|
||||||
use meilisearch_types::keys::actions;
|
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::db::IndexEmbeddingConfig;
|
||||||
use meilisearch_types::milli::vector::json_template::{self, JsonTemplate};
|
use meilisearch_types::milli::vector::json_template::{self, JsonTemplate};
|
||||||
use meilisearch_types::milli::vector::EmbedderOptions;
|
use meilisearch_types::milli::vector::EmbedderOptions;
|
||||||
@ -179,6 +179,7 @@ enum RenderError<'a> {
|
|||||||
ExpectedValue(milli::Span<'a>),
|
ExpectedValue(milli::Span<'a>),
|
||||||
|
|
||||||
DocumentNotFound(String),
|
DocumentNotFound(String),
|
||||||
|
DocumentMustBeMap,
|
||||||
BothInlineDocAndDocId,
|
BothInlineDocAndDocId,
|
||||||
TemplateParsing(json_template::Error),
|
TemplateParsing(json_template::Error),
|
||||||
TemplateRendering(json_template::Error),
|
TemplateRendering(json_template::Error),
|
||||||
@ -309,6 +310,10 @@ impl From<RenderError<'_>> for ResponseError {
|
|||||||
format!("Document with ID `{doc_id}` not found."),
|
format!("Document with ID `{doc_id}` not found."),
|
||||||
Code::RenderDocumentNotFound,
|
Code::RenderDocumentNotFound,
|
||||||
),
|
),
|
||||||
|
DocumentMustBeMap => ResponseError::from_msg(
|
||||||
|
String::from("The `doc` field must be a map."),
|
||||||
|
Code::InvalidRenderInput,
|
||||||
|
),
|
||||||
BothInlineDocAndDocId => ResponseError::from_msg(
|
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."),
|
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,
|
Code::InvalidRenderInput,
|
||||||
@ -477,17 +482,16 @@ fn parse_template_id<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn render(index: Index, query: RenderQuery) -> Result<RenderResult, ResponseError> {
|
async fn render(index: Index, query: RenderQuery) -> Result<RenderResult, ResponseError> {
|
||||||
|
let RenderQuery { template, input } = query;
|
||||||
let rtxn = index.read_txn()?;
|
let rtxn = index.read_txn()?;
|
||||||
|
let (template, fields_available) = match (template.inline, template.id) {
|
||||||
let (template, fields_available) = match (query.template.inline, query.template.id) {
|
|
||||||
(Some(inline), None) => (inline, true),
|
(Some(inline), None) => (inline, true),
|
||||||
(None, Some(id)) => parse_template_id(&index, &rtxn, &id)?,
|
(None, Some(id)) => parse_template_id(&index, &rtxn, &id)?,
|
||||||
(Some(_), Some(_)) => return Err(MultipleTemplates.into()),
|
(Some(_), Some(_)) => return Err(MultipleTemplates.into()),
|
||||||
(None, None) => return Err(MissingTemplate.into()),
|
(None, None) => return Err(MissingTemplate.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let fields_already_present = query
|
let fields_already_present = input
|
||||||
.input
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|i| i.inline.as_ref().is_some_and(|i| i.get("fields").is_some()));
|
.is_some_and(|i| i.inline.as_ref().is_some_and(|i| i.get("fields").is_some()));
|
||||||
let fields_unused = match template.as_str() {
|
let fields_unused = match template.as_str() {
|
||||||
@ -498,11 +502,9 @@ async fn render(index: Index, query: RenderQuery) -> Result<RenderResult, Respon
|
|||||||
}
|
}
|
||||||
None => true, // non-text templates cannot use `fields`
|
None => true, // non-text templates cannot use `fields`
|
||||||
};
|
};
|
||||||
let has_inline_doc = query
|
let has_inline_doc =
|
||||||
.input
|
input.as_ref().is_some_and(|i| i.inline.as_ref().is_some_and(|i| i.get("doc").is_some()));
|
||||||
.as_ref()
|
let has_document_id = input.as_ref().is_some_and(|i| i.document_id.is_some());
|
||||||
.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_doc = has_inline_doc || has_document_id;
|
let has_doc = has_inline_doc || has_document_id;
|
||||||
let insert_fields = fields_available && has_doc && !fields_unused && !fields_already_present;
|
let insert_fields = fields_available && has_doc && !fields_unused && !fields_already_present;
|
||||||
if has_inline_doc && has_document_id {
|
if has_inline_doc && has_document_id {
|
||||||
@ -510,14 +512,21 @@ async fn render(index: Index, query: RenderQuery) -> Result<RenderResult, Respon
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut rendered = Value::Null;
|
let mut rendered = Value::Null;
|
||||||
if let Some(input) = query.input {
|
if let Some(input) = input {
|
||||||
let inline = input.inline.unwrap_or_default();
|
let inline = input.inline.unwrap_or_default();
|
||||||
let mut object = liquid::to_object(&inline).unwrap();
|
let mut object = liquid::to_object(&inline).map_err(InputConversion)?;
|
||||||
|
|
||||||
if let Some(doc) = inline.get("doc") {
|
let doc = match object.get_mut("doc") {
|
||||||
if insert_fields {
|
Some(liquid::model::Value::Object(doc)) => Some(doc),
|
||||||
let fields =
|
Some(liquid::model::Value::Nil) => None,
|
||||||
get_inline_document_fields(&index, &rtxn, doc)?.map_err(InputConversion)?;
|
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());
|
object.insert("fields".into(), fields.to_value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 crate::json;
|
||||||
use meili_snap::{json_string, snapshot};
|
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]
|
#[actix_rt::test]
|
||||||
async fn chat_completions() {
|
async fn chat_completions() {
|
||||||
let index = shared_index_for_fragments().await;
|
let index = shared_index_for_fragments().await;
|
||||||
|
@ -13,8 +13,7 @@ pub(crate) use document::{Document, ParseableDocument};
|
|||||||
use error::{NewPromptError, RenderPromptError};
|
use error::{NewPromptError, RenderPromptError};
|
||||||
pub use fields::{BorrowedFields, OwnedFields};
|
pub use fields::{BorrowedFields, OwnedFields};
|
||||||
use heed::RoTxn;
|
use heed::RoTxn;
|
||||||
use liquid::model::Value as LiquidValue;
|
use liquid::{model::Value as LiquidValue, ValueView};
|
||||||
use liquid::ValueView;
|
|
||||||
|
|
||||||
pub use self::context::Context;
|
pub use self::context::Context;
|
||||||
use crate::fields_ids_map::metadata::FieldIdMapWithMetadata;
|
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<Result<LiquidValue, liquid::Error>, 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(
|
pub fn get_document(
|
||||||
index: &Index,
|
index: &Index,
|
||||||
rtxn: &RoTxn<'_>,
|
rtxn: &RoTxn<'_>,
|
||||||
|
Reference in New Issue
Block a user