Template retrieval logic

This commit is contained in:
Mubelotix
2025-07-16 16:52:28 +02:00
parent f50a5b17b6
commit e124c161ec
4 changed files with 348 additions and 20 deletions

View File

@ -873,7 +873,7 @@ impl IndexScheduler {
.into_inner() .into_inner()
.into_iter() .into_iter()
.map(|fragment| { .map(|fragment| {
let value = embedder_options.fragment(&fragment.name).unwrap(); let value = embedder_options.indexing_fragment(&fragment.name).unwrap();
let template = JsonTemplate::new(value.clone()).unwrap(); let template = JsonTemplate::new(value.clone()).unwrap();
RuntimeFragment { name: fragment.name, id: fragment.id, template } RuntimeFragment { name: fragment.name, id: fragment.id, template }
}) })

View File

@ -6,11 +6,12 @@ use index_scheduler::IndexScheduler;
use itertools::structs; use itertools::structs;
use meilisearch_types::deserr::query_params::Param; use meilisearch_types::deserr::query_params::Param;
use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError};
use meilisearch_types::error::{deserr_codes::*, Code};
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::error::{deserr_codes::*, Code};
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::serde_cs::vec::CS; use meilisearch_types::serde_cs::vec::CS;
use meilisearch_types::{heed, Index};
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use tracing::debug; use tracing::debug;
@ -38,10 +39,7 @@ use crate::search::{
pub struct RenderApi; pub struct RenderApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(web::resource("").route(web::post().to(SeqHandler(render_post))));
web::resource("")
.route(web::post().to(SeqHandler(render_post)))
);
} }
/// Render templates with POST /// Render templates with POST
@ -84,13 +82,14 @@ pub async fn render_post(
analytics: web::Data<Analytics>, analytics: web::Data<Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?; let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let index = index_scheduler.index(&index_uid)?;
let query = params.into_inner(); let query = params.into_inner();
debug!(parameters = ?query, "Render template"); debug!(parameters = ?query, "Render template");
//let mut aggregate = SimilarAggregator::<SimilarPOST>::from_query(&query); //let mut aggregate = SimilarAggregator::<SimilarPOST>::from_query(&query);
let result = render(query).await?; let result = render(index, query).await?;
// if let Ok(similar) = &similar { // if let Ok(similar) = &similar {
// aggregate.succeed(similar); // aggregate.succeed(similar);
@ -101,9 +100,69 @@ pub async fn render_post(
Ok(HttpResponse::Ok().json(result)) Ok(HttpResponse::Ok().json(result))
} }
enum FragmentKind {
Indexing,
Search,
}
impl FragmentKind {
fn adjective(&self) -> &'static str {
match self {
FragmentKind::Indexing => "indexing",
FragmentKind::Search => "search",
}
}
fn adjective_capitalized(&self) -> &'static str {
match self {
FragmentKind::Indexing => "Indexing",
FragmentKind::Search => "Search",
}
}
}
enum RenderError { enum RenderError {
TemplateBothInlineAndId, MultipleTemplates,
MissingTemplate,
EmptyTemplateId,
UnknownTemplateRoot(String),
MissingEmbedderName {
available: Vec<String>,
},
EmbedderDoesNotExist {
embedder_name: String,
available: Vec<String>,
},
EmbedderUsesFragments {
embedder_name: String,
},
MissingTemplateAfterEmbedder {
embedder_name: String,
available_indexing_fragments: Vec<String>,
available_search_fragments: Vec<String>,
},
UnknownTemplatePrefix(String),
ReponseError(ResponseError),
MissingFragment {
embedder_name: String,
kind: FragmentKind,
available: Vec<String>,
},
FragmentDoesNotExist {
embedder_name: String,
fragment_name: String,
kind: FragmentKind,
available: Vec<String>,
},
LeftOverToken(String),
MissingChatCompletionTemplate,
UnknownChatCompletionTemplate(String),
}
impl From<heed::Error> for RenderError {
fn from(error: heed::Error) -> Self {
RenderError::ReponseError(error.into())
}
} }
use RenderError::*; use RenderError::*;
@ -111,22 +170,233 @@ use RenderError::*;
impl From<RenderError> for ResponseError { impl From<RenderError> for ResponseError {
fn from(error: RenderError) -> Self { fn from(error: RenderError) -> Self {
match error { match error {
TemplateBothInlineAndId => ResponseError::from_msg( MultipleTemplates => ResponseError::from_msg(
"Cannot provide both an inline template and a template ID.".to_string(), String::from("Cannot provide both an inline template and a template ID."),
Code::InvalidRenderTemplate, Code::InvalidRenderTemplate,
),
MissingTemplate => ResponseError::from_msg(
String::from("No template provided. Please provide either an inline template or a template ID."),
Code::InvalidRenderTemplate,
),
EmptyTemplateId => ResponseError::from_msg(
String::from("The template ID is empty.\n Hint: Valid prefixes are `embedders` or `chatCompletions`."),
Code::InvalidRenderTemplateId,
),
UnknownTemplateRoot(root) => ResponseError::from_msg(
format!("Template ID must start with `embedders` or `chatCompletions`, but found `{root}`."),
Code::InvalidRenderTemplateId,
),
MissingEmbedderName { mut available } => {
available.sort_unstable();
ResponseError::from_msg(
format!("Template ID configured with `embedders` but no embedder name provided.\n Hint: Available embedders are {}.",
available.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")),
Code::InvalidRenderTemplateId,
) )
},
EmbedderDoesNotExist { embedder_name, mut available } => {
available.sort_unstable();
ResponseError::from_msg(
format!("Embedder `{embedder_name}` does not exist.\n Hint: Available embedders are {}.",
available.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")),
Code::InvalidRenderTemplateId,
)
},
EmbedderUsesFragments { embedder_name } => ResponseError::from_msg(
format!("Requested document template for embedder `{embedder_name}` but it uses fragments.\n Hint: Use `indexingFragments` or `searchFragments` instead."),
Code::InvalidRenderTemplateId,
),
MissingTemplateAfterEmbedder { embedder_name, mut available_indexing_fragments, mut available_search_fragments } => {
if available_indexing_fragments.is_empty() && available_search_fragments.is_empty() {
ResponseError::from_msg(
format!("Missing template id after embedder `{embedder_name}`.\n Hint: Available fragments: `documentTemplate`."),
Code::InvalidRenderTemplateId,
)
} else {
available_indexing_fragments.sort_unstable();
available_search_fragments.sort_unstable();
ResponseError::from_msg(
format!("Template ID configured with `embedders.{embedder_name}` but no template kind provided.\n Hint: Available fragments are {}.",
available_indexing_fragments.iter().map(|s| format!("`indexingFragments.{s}`")).chain(
available_search_fragments.iter().map(|s| format!("`searchFragments.{s}`"))).collect::<Vec<_>>().join(", ")),
Code::InvalidRenderTemplateId,
)
}
},
UnknownTemplatePrefix(prefix) => ResponseError::from_msg(
format!("Template ID must start with `embedders` or `chatCompletions`, but found `{prefix}`."),
Code::InvalidRenderTemplateId,
),
ReponseError(response_error) => response_error,
MissingFragment { embedder_name, kind, mut available } => {
available.sort_unstable();
ResponseError::from_msg(
format!("{} fragment name was not provided.\n Hint: Available {} fragments for embedder `{embedder_name}` are {}.",
kind.adjective_capitalized(),
kind.adjective(),
available.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")),
Code::InvalidRenderTemplateId,
)
},
FragmentDoesNotExist { embedder_name, fragment_name, kind, mut available } => {
available.sort_unstable();
ResponseError::from_msg(
format!("{} fragment `{fragment_name}` does not exist for embedder `{embedder_name}`.\n Hint: Available {} fragments are {}.",
kind.adjective_capitalized(),
kind.adjective(),
available.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")),
Code::InvalidRenderTemplateId,
)
},
LeftOverToken(token) => ResponseError::from_msg(
format!("Leftover token `{token}` after parsing template ID"),
Code::InvalidRenderTemplateId,
),
MissingChatCompletionTemplate => ResponseError::from_msg(
String::from("Missing chat completion template ID. The only available template is `documentTemplate`."),
Code::InvalidRenderTemplateId,
),
UnknownChatCompletionTemplate(id) => ResponseError::from_msg(
format!("Unknown chat completion template ID `{id}`. The only available template is `documentTemplate`."),
Code::InvalidRenderTemplateId,
),
} }
} }
} }
async fn render(query: RenderQuery) -> Result<RenderResult, RenderError> { async fn render(index: Index, query: RenderQuery) -> Result<RenderResult, RenderError> {
if query.template.inline.is_some() && query.template.id.is_some() { let rtxn = index.read_txn()?;
return Err(TemplateBothInlineAndId);
let template = match (query.template.inline, query.template.id) {
(Some(inline), None) => inline,
(None, Some(id)) => {
let mut parts = id.split('.');
let root = parts.next().ok_or(EmptyTemplateId)?;
let template = match root {
"embedders" => {
let index_embedding_configs = index.embedding_configs();
let embedding_configs = index_embedding_configs.embedding_configs(&rtxn)?;
let embedder_name = parts.next().ok_or_else(|| MissingEmbedderName {
available: embedding_configs.iter().map(|c| c.name.clone()).collect(),
})?;
let embedding_config = embedding_configs
.iter()
.find(|config| config.name == embedder_name)
.ok_or_else(|| EmbedderDoesNotExist {
embedder_name: embedder_name.to_string(),
available: embedding_configs.iter().map(|c| c.name.clone()).collect(),
})?;
let template_kind =
parts.next().ok_or_else(|| MissingTemplateAfterEmbedder {
embedder_name: embedder_name.to_string(),
available_indexing_fragments: embedding_config
.config
.embedder_options
.indexing_fragments(),
available_search_fragments: embedding_config
.config
.embedder_options
.search_fragments(),
})?;
match template_kind {
"documentTemplate" | "documenttemplate" => {
if !embedding_config.fragments.as_slice().is_empty() {
return Err(EmbedderUsesFragments {
embedder_name: embedder_name.to_string(),
});
} }
Ok(RenderResult { serde_json::Value::String(
rendered: String::from("TODO: Implement render logic here") embedding_config.config.prompt.template.clone(),
}) )
}
"indexingFragments" | "indexingfragments" => {
let fragment_name = parts.next().ok_or_else(|| MissingFragment {
embedder_name: embedder_name.to_string(),
kind: FragmentKind::Indexing,
available: embedding_config
.config
.embedder_options
.indexing_fragments(),
})?;
let fragment = embedding_config
.config
.embedder_options
.indexing_fragment(fragment_name)
.ok_or_else(|| FragmentDoesNotExist {
embedder_name: embedder_name.to_string(),
fragment_name: fragment_name.to_string(),
kind: FragmentKind::Indexing,
available: embedding_config
.config
.embedder_options
.indexing_fragments(),
})?;
fragment.clone()
}
"searchFragments" | "searchfragments" => {
let fragment_name = parts.next().ok_or_else(|| MissingFragment {
embedder_name: embedder_name.to_string(),
kind: FragmentKind::Search,
available: embedding_config
.config
.embedder_options
.search_fragments(),
})?;
let fragment = embedding_config
.config
.embedder_options
.search_fragment(fragment_name)
.ok_or_else(|| FragmentDoesNotExist {
embedder_name: embedder_name.to_string(),
fragment_name: fragment_name.to_string(),
kind: FragmentKind::Search,
available: embedding_config
.config
.embedder_options
.search_fragments(),
})?;
fragment.clone()
}
_ => return Err(UnknownTemplateRoot(root.to_owned())),
}
}
"chatCompletions" | "chatcompletions" => {
let template_name = parts.next().ok_or(MissingChatCompletionTemplate)?;
if template_name != "documentTemplate" {
return Err(UnknownChatCompletionTemplate(template_name.to_string()));
}
let chat_config = index.chat_config(&rtxn)?;
serde_json::Value::String(chat_config.prompt.template.clone())
}
unknown => {
return Err(UnknownTemplatePrefix(unknown.to_string()));
}
};
if let Some(next) = parts.next() {
return Err(LeftOverToken(next.to_string()));
}
template
}
(Some(_), Some(_)) => return Err(MultipleTemplates),
(None, None) => return Err(MissingTemplate),
};
Ok(RenderResult { template, rendered: String::from("TODO: Implement render logic here") })
} }
#[derive(Debug, Clone, PartialEq, Deserr, ToSchema)] #[derive(Debug, Clone, PartialEq, Deserr, ToSchema)]
@ -158,5 +428,6 @@ pub struct RenderQueryInput {
#[derive(Debug, Clone, Serialize, PartialEq, ToSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ToSchema)]
pub struct RenderResult { pub struct RenderResult {
template: serde_json::Value,
rendered: String, rendered: String,
} }

View File

@ -2041,7 +2041,7 @@ fn embedders(embedding_configs: Vec<IndexEmbeddingConfig>) -> Result<RuntimeEmbe
.into_iter() .into_iter()
.map(|fragment| { .map(|fragment| {
let template = JsonTemplate::new( let template = JsonTemplate::new(
embedder_options.fragment(&fragment.name).unwrap().clone(), embedder_options.indexing_fragment(&fragment.name).unwrap().clone(),
) )
.unwrap(); .unwrap();

View File

@ -823,7 +823,26 @@ pub enum EmbedderOptions {
} }
impl EmbedderOptions { impl EmbedderOptions {
pub fn fragment(&self, name: &str) -> Option<&serde_json::Value> { pub fn indexing_fragments(&self) -> Vec<String> {
match &self {
EmbedderOptions::HuggingFace(_)
| EmbedderOptions::OpenAi(_)
| EmbedderOptions::Ollama(_)
| EmbedderOptions::UserProvided(_) => vec![],
EmbedderOptions::Rest(embedder_options) => {
embedder_options.indexing_fragments.keys().cloned().collect()
}
EmbedderOptions::Composite(embedder_options) => {
if let SubEmbedderOptions::Rest(embedder_options) = &embedder_options.index {
embedder_options.indexing_fragments.keys().cloned().collect()
} else {
vec![]
}
}
}
}
pub fn indexing_fragment(&self, name: &str) -> Option<&serde_json::Value> {
match &self { match &self {
EmbedderOptions::HuggingFace(_) EmbedderOptions::HuggingFace(_)
| EmbedderOptions::OpenAi(_) | EmbedderOptions::OpenAi(_)
@ -841,6 +860,44 @@ impl EmbedderOptions {
} }
} }
} }
pub fn search_fragments(&self) -> Vec<String> {
match &self {
EmbedderOptions::HuggingFace(_)
| EmbedderOptions::OpenAi(_)
| EmbedderOptions::Ollama(_)
| EmbedderOptions::UserProvided(_) => vec![],
EmbedderOptions::Rest(embedder_options) => {
embedder_options.search_fragments.keys().cloned().collect()
}
EmbedderOptions::Composite(embedder_options) => {
if let SubEmbedderOptions::Rest(embedder_options) = &embedder_options.search {
embedder_options.search_fragments.keys().cloned().collect()
} else {
vec![]
}
}
}
}
pub fn search_fragment(&self, name: &str) -> Option<&serde_json::Value> {
match &self {
EmbedderOptions::HuggingFace(_)
| EmbedderOptions::OpenAi(_)
| EmbedderOptions::Ollama(_)
| EmbedderOptions::UserProvided(_) => None,
EmbedderOptions::Rest(embedder_options) => {
embedder_options.search_fragments.get(name)
}
EmbedderOptions::Composite(embedder_options) => {
if let SubEmbedderOptions::Rest(embedder_options) = &embedder_options.search {
embedder_options.search_fragments.get(name)
} else {
None
}
}
}
}
} }
impl Default for EmbedderOptions { impl Default for EmbedderOptions {