Merge #5234
Some checks failed
Test suite / Tests on ${{ matrix.os }} (macos-13) (push) Waiting to run
Run the indexing fuzzer / Setup the action (push) Failing after 15s
Test suite / Tests on ${{ matrix.os }} (windows-2022) (push) Failing after 12m54s
Test suite / Tests on macos-13 (push) Waiting to run
Indexing bench (push) / Run and upload benchmarks (push) Has been cancelled
Benchmarks of indexing (push) / Run and upload benchmarks (push) Has been cancelled
Benchmarks of search for geo (push) / Run and upload benchmarks (push) Has been cancelled
Benchmarks of search for songs (push) / Run and upload benchmarks (push) Has been cancelled
Benchmarks of search for Wikipedia articles (push) / Run and upload benchmarks (push) Has been cancelled
Publish binaries to GitHub release / Check the version validity (push) Successful in 11s
Publish binaries to GitHub release / Publish binary for macos-13 (push) Waiting to run
Publish binaries to GitHub release / Publish binary for macOS silicon (meilisearch-macos-apple-silicon, aarch64-apple-darwin) (push) Waiting to run
Publish binaries to GitHub release / Publish binary for Linux (push) Failing after 2s
Publish binaries to GitHub release / Publish binary for aarch64 (meilisearch-linux-aarch64, aarch64-unknown-linux-gnu) (push) Failing after 12s
Publish binaries to GitHub release / Publish binary for windows-2022 (push) Failing after 29s
Test suite / Tests almost all features (push) Failing after 0s
Test suite / Test disabled tokenization (push) Failing after 1s
Test suite / Run tests in debug (push) Failing after 1s
Test suite / Tests on ubuntu-20.04 (push) Failing after 12s
Test suite / Tests on windows-2022 (push) Failing after 24s
Test suite / Run Clippy (push) Failing after 12s
Test suite / Run Rustfmt (push) Successful in 1m36s

5234: Parse ollama URL to adapt configuration depending on the endpoint r=Kerollmops a=dureuill

# Pull Request

## Related issue
Fixes #5002 

## What does this PR do?
- Parses `url` parameter of `ollama` to recognize supported endpoint and adapt the REST configuration to the recognized endpoint
- Throws a new error if no endpoint is recognized
- Add a test for the various recognized endpoints


Thanks to `@Guikingone` for the original report and PR

Co-authored-by: Louis Dureuil <louis@meilisearch.com>
This commit is contained in:
meili-bors[bot] 2025-01-20 09:51:42 +00:00 committed by GitHub
commit 0776217801
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 194 additions and 16 deletions

View File

@ -255,3 +255,155 @@ async fn reset_embedder_documents() {
}
"###);
}
#[actix_rt::test]
async fn ollama_url_checks() {
let server = super::get_server_vector().await;
let index = server.index("doggo");
let (response, code) = index
.update_settings(json!({
"embedders": { "ollama": {"source": "ollama", "model": "toto", "dimensions": 1, "url": "http://localhost:11434/api/embeddings"}},
}))
.await;
snapshot!(code, @"202 Accepted");
let response = server.wait_task(response.uid()).await;
snapshot!(response, @r###"
{
"uid": "[uid]",
"batchUid": "[batch_uid]",
"indexUid": "doggo",
"status": "succeeded",
"type": "settingsUpdate",
"canceledBy": null,
"details": {
"embedders": {
"ollama": {
"source": "ollama",
"model": "toto",
"dimensions": 1,
"url": "[url]"
}
}
},
"error": null,
"duration": "[duration]",
"enqueuedAt": "[date]",
"startedAt": "[date]",
"finishedAt": "[date]"
}
"###);
let (response, code) = index
.update_settings(json!({
"embedders": { "ollama": {"source": "ollama", "model": "toto", "dimensions": 1, "url": "http://localhost:11434/api/embed"}},
}))
.await;
snapshot!(code, @"202 Accepted");
let response = server.wait_task(response.uid()).await;
snapshot!(response, @r###"
{
"uid": "[uid]",
"batchUid": "[batch_uid]",
"indexUid": "doggo",
"status": "succeeded",
"type": "settingsUpdate",
"canceledBy": null,
"details": {
"embedders": {
"ollama": {
"source": "ollama",
"model": "toto",
"dimensions": 1,
"url": "[url]"
}
}
},
"error": null,
"duration": "[duration]",
"enqueuedAt": "[date]",
"startedAt": "[date]",
"finishedAt": "[date]"
}
"###);
let (response, code) = index
.update_settings(json!({
"embedders": { "ollama": {"source": "ollama", "model": "toto", "dimensions": 1, "url": "http://localhost:11434/api/embedd"}},
}))
.await;
snapshot!(code, @"202 Accepted");
let response = server.wait_task(response.uid()).await;
snapshot!(response, @r###"
{
"uid": "[uid]",
"batchUid": "[batch_uid]",
"indexUid": "doggo",
"status": "failed",
"type": "settingsUpdate",
"canceledBy": null,
"details": {
"embedders": {
"ollama": {
"source": "ollama",
"model": "toto",
"dimensions": 1,
"url": "[url]"
}
}
},
"error": {
"message": "Index `doggo`: Error while generating embeddings: user error: unsupported Ollama URL.\n - For `ollama` sources, the URL must end with `/api/embed` or `/api/embeddings`\n - Got `http://localhost:11434/api/embedd`",
"code": "vector_embedding_error",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#vector_embedding_error"
},
"duration": "[duration]",
"enqueuedAt": "[date]",
"startedAt": "[date]",
"finishedAt": "[date]"
}
"###);
let (response, code) = index
.update_settings(json!({
"embedders": { "ollama": {"source": "ollama", "model": "toto", "dimensions": 1, "url": "http://localhost:11434/v1/embeddings"}},
}))
.await;
snapshot!(code, @"202 Accepted");
let response = server.wait_task(response.uid()).await;
snapshot!(response, @r###"
{
"uid": "[uid]",
"batchUid": "[batch_uid]",
"indexUid": "doggo",
"status": "failed",
"type": "settingsUpdate",
"canceledBy": null,
"details": {
"embedders": {
"ollama": {
"source": "ollama",
"model": "toto",
"dimensions": 1,
"url": "[url]"
}
}
},
"error": {
"message": "Index `doggo`: Error while generating embeddings: user error: unsupported Ollama URL.\n - For `ollama` sources, the URL must end with `/api/embed` or `/api/embeddings`\n - Got `http://localhost:11434/v1/embeddings`",
"code": "vector_embedding_error",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#vector_embedding_error"
},
"duration": "[duration]",
"enqueuedAt": "[date]",
"startedAt": "[date]",
"finishedAt": "[date]"
}
"###);
}

View File

@ -67,7 +67,7 @@ pub enum EmbedErrorKind {
#[error("could not authenticate against {embedding} server{server_reply}{hint}", embedding=match *.1 {
ConfigurationSource::User => "embedding",
ConfigurationSource::OpenAi => "OpenAI",
ConfigurationSource::Ollama => "ollama"
ConfigurationSource::Ollama => "Ollama"
},
server_reply=option_info(.0.as_deref(), "server replied with "),
hint=match *.1 {
@ -306,6 +306,10 @@ impl NewEmbedderError {
fault: FaultSource::User,
}
}
pub(crate) fn ollama_unsupported_url(url: String) -> NewEmbedderError {
Self { kind: NewEmbedderErrorKind::OllamaUnsupportedUrl(url), fault: FaultSource::User }
}
}
#[derive(Debug, thiserror::Error)]
@ -369,6 +373,8 @@ pub enum NewEmbedderErrorKind {
LoadModel(candle_core::Error),
#[error("{0}")]
CouldNotParseTemplate(String),
#[error("unsupported Ollama URL.\n - For `ollama` sources, the URL must end with `/api/embed` or `/api/embeddings`\n - Got `{0}`")]
OllamaUnsupportedUrl(String),
}
pub struct PossibleEmbeddingMistakes {

View File

@ -38,26 +38,46 @@ impl EmbedderOptions {
dimensions,
}
}
fn into_rest_embedder_config(self) -> Result<RestEmbedderOptions, NewEmbedderError> {
let url = self.url.unwrap_or_else(get_ollama_path);
let model = self.embedding_model.as_str();
// **warning**: do not swap these two `if`s, as the second one is always true when the first one is.
let (request, response) = if url.ends_with("/api/embeddings") {
(
serde_json::json!({"model": model, "input": [super::rest::REQUEST_PLACEHOLDER, super::rest::REPEAT_PLACEHOLDER]}),
serde_json::json!({"embeddings": [super::rest::RESPONSE_PLACEHOLDER, super::rest::REPEAT_PLACEHOLDER]}),
)
} else if url.ends_with("/api/embed") {
(
serde_json::json!({
"model": model,
"prompt": super::rest::REQUEST_PLACEHOLDER,
}),
serde_json::json!({
"embedding": super::rest::RESPONSE_PLACEHOLDER,
}),
)
} else {
return Err(NewEmbedderError::ollama_unsupported_url(url));
};
Ok(RestEmbedderOptions {
api_key: self.api_key,
dimensions: self.dimensions,
distribution: self.distribution,
url,
request,
response,
headers: Default::default(),
})
}
}
impl Embedder {
pub fn new(options: EmbedderOptions) -> Result<Self, NewEmbedderError> {
let model = options.embedding_model.as_str();
let rest_embedder = match RestEmbedder::new(
RestEmbedderOptions {
api_key: options.api_key,
dimensions: options.dimensions,
distribution: options.distribution,
url: options.url.unwrap_or_else(get_ollama_path),
request: serde_json::json!({
"model": model,
"prompt": super::rest::REQUEST_PLACEHOLDER,
}),
response: serde_json::json!({
"embedding": super::rest::RESPONSE_PLACEHOLDER,
}),
headers: Default::default(),
},
options.into_rest_embedder_config()?,
super::rest::ConfigurationSource::Ollama,
) {
Ok(embedder) => embedder,