mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-26 13:36:13 +00:00
fix(rust): explain client-side model files when remote tokenize fails (#4519)
Addresses option 2 of #4440. tokenize() builds the FTS analyzer on the client. For jieba/* and lindera/* that needs model files under LANCE_LANGUAGE_MODEL_HOME, and when they’re missing you only got lance’s raw Invalid directory path: ... error, even though server-side search works. Now, if a jieba/* or lindera/* tokenizer can’t be built, the error keeps the original cause and adds that tokenize() runs on the client, which directory the files need to be in, and that for remote tables they must match the server’s. I also updated the tokenize docs (Rust, Python, TS, generated JS docs) to say a mismatch can’t be detected, so tokens may differ from the server’s index. Behavior when the files exist is unchanged. Detecting a mismatch needs a server-side tokenize endpoint (option 1), so I left it out. The hint isn’t limited to remote tables, since Table::as_native is slated for removal and the wording fits local tables too. Tests: Rust test with mocked jieba/default index_details and an empty model dir, one checking other tokenizer errors don’t get the hint, and Python sync/async tests.
This commit is contained in:
@@ -8871,6 +8871,131 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(language_model_env)]
|
||||
async fn test_tokenize_model_backed_tokenizer_without_model_files() {
|
||||
let model_home = tempfile::tempdir().unwrap();
|
||||
// SAFETY: This is only called in tests, serialized on the env key.
|
||||
unsafe {
|
||||
std::env::set_var("LANCE_LANGUAGE_MODEL_HOME", model_home.path());
|
||||
}
|
||||
let schema = Schema::new(vec![Field::new("text", DataType::Utf8, false)]);
|
||||
let index_details = serde_json::json!({
|
||||
"base_tokenizer": "jieba/default",
|
||||
"language": "English",
|
||||
"with_position": false,
|
||||
"max_token_length": 40,
|
||||
"lower_case": true,
|
||||
"stem": false,
|
||||
"remove_stop_words": false,
|
||||
"ascii_folding": false,
|
||||
})
|
||||
.to_string();
|
||||
let table = Table::new_with_handler("my_table", move |request| {
|
||||
assert_eq!(request.method(), "POST");
|
||||
match request.url().path() {
|
||||
"/v1/table/my_table/describe/" => http::Response::builder()
|
||||
.status(200)
|
||||
.body(describe_response(&schema))
|
||||
.unwrap(),
|
||||
"/v1/table/my_table/index/list/" => {
|
||||
let body = serde_json::json!({
|
||||
"indexes": [
|
||||
{
|
||||
"index_name": "text_idx",
|
||||
"columns": ["text"],
|
||||
"index_type": "FTS",
|
||||
"index_details": index_details,
|
||||
},
|
||||
]
|
||||
});
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(serde_json::to_string(&body).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
path => panic!("Unexpected path: {}", path),
|
||||
}
|
||||
});
|
||||
|
||||
let result = table.tokenize("我们是中国人", "text_idx").await;
|
||||
// SAFETY: This is only called in tests, serialized on the env key.
|
||||
unsafe {
|
||||
std::env::remove_var("LANCE_LANGUAGE_MODEL_HOME");
|
||||
}
|
||||
|
||||
let err = result.unwrap_err();
|
||||
let Error::InvalidInput { message } = err else {
|
||||
panic!("expected InvalidInput, got {err:?}");
|
||||
};
|
||||
// The original cause and the index context are preserved...
|
||||
assert!(message.contains("Invalid directory path"), "{message}");
|
||||
assert!(
|
||||
message.contains("full text search index 'text_idx'"),
|
||||
"{message}"
|
||||
);
|
||||
// ...and the message explains that this is a client-side limitation.
|
||||
assert!(
|
||||
message.contains("tokenize() runs on the client"),
|
||||
"{message}"
|
||||
);
|
||||
assert!(message.contains("'jieba/default'"), "{message}");
|
||||
assert!(message.contains("LANCE_LANGUAGE_MODEL_HOME"), "{message}");
|
||||
assert!(
|
||||
message.contains("match the files the server uses"),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tokenize_unknown_tokenizer_error_has_no_model_files_hint() {
|
||||
let schema = Schema::new(vec![Field::new("text", DataType::Utf8, false)]);
|
||||
let index_details = serde_json::json!({
|
||||
"base_tokenizer": "no_such_tokenizer",
|
||||
"language": "English",
|
||||
"with_position": false,
|
||||
"max_token_length": 40,
|
||||
"lower_case": true,
|
||||
"stem": false,
|
||||
"remove_stop_words": false,
|
||||
"ascii_folding": false,
|
||||
})
|
||||
.to_string();
|
||||
let table = Table::new_with_handler("my_table", move |request| {
|
||||
assert_eq!(request.method(), "POST");
|
||||
match request.url().path() {
|
||||
"/v1/table/my_table/describe/" => http::Response::builder()
|
||||
.status(200)
|
||||
.body(describe_response(&schema))
|
||||
.unwrap(),
|
||||
"/v1/table/my_table/index/list/" => {
|
||||
let body = serde_json::json!({
|
||||
"indexes": [
|
||||
{
|
||||
"index_name": "text_idx",
|
||||
"columns": ["text"],
|
||||
"index_type": "FTS",
|
||||
"index_details": index_details,
|
||||
},
|
||||
]
|
||||
});
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(serde_json::to_string(&body).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
path => panic!("Unexpected path: {}", path),
|
||||
}
|
||||
});
|
||||
|
||||
let err = table.tokenize("hello", "text_idx").await.unwrap_err();
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
!message.contains("tokenize() runs on the client"),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tokenize_requires_existing_index_name() {
|
||||
let schema = Schema::new(vec![Field::new("text", DataType::Utf8, false)]);
|
||||
|
||||
@@ -2264,7 +2264,11 @@ impl Table {
|
||||
///
|
||||
/// Model-backed tokenizers such as `jieba/*` and `lindera/*` are rebuilt in
|
||||
/// the client process from index metadata. For remote tables, this means the
|
||||
/// same tokenizer model files must also exist locally.
|
||||
/// same tokenizer model files must also exist locally, and must match the
|
||||
/// files the server uses: the client cannot detect a mismatch, so it may
|
||||
/// return tokens the server's index never produces. If the model files are
|
||||
/// missing, this returns an error naming the tokenizer and
|
||||
/// `LANCE_LANGUAGE_MODEL_HOME`.
|
||||
pub async fn tokenize(&self, query: &str, index_name: &str) -> Result<Vec<FtsToken>> {
|
||||
let indices = self.inner.list_indices().await?;
|
||||
let matches = indices
|
||||
@@ -2298,7 +2302,8 @@ impl Table {
|
||||
/// The column must have exactly one FTS index. Model-backed tokenizers such
|
||||
/// as `jieba/*` and `lindera/*` are rebuilt in the client process from
|
||||
/// index metadata. For remote tables, this means the same tokenizer model
|
||||
/// files must also exist locally.
|
||||
/// files must also exist locally, and must match the files the server uses
|
||||
/// (see [`Self::tokenize`]).
|
||||
pub async fn tokenize_with_column(&self, query: &str, column: &str) -> Result<Vec<FtsToken>> {
|
||||
let schema = self.inner.schema().await?;
|
||||
let (column, _) = resolve_arrow_field_path(schema.as_ref(), column)?;
|
||||
@@ -2354,13 +2359,30 @@ impl Table {
|
||||
),
|
||||
}
|
||||
})?;
|
||||
let model_backed_tokenizer = serde_json::from_str::<serde_json::Value>(details)
|
||||
.ok()
|
||||
.and_then(|details| {
|
||||
let tokenizer = details.get("base_tokenizer")?.as_str()?.to_string();
|
||||
(tokenizer.starts_with("jieba/") || tokenizer.starts_with("lindera/"))
|
||||
.then_some(tokenizer)
|
||||
});
|
||||
tokenize(query, ¶ms).map_err(|err| match err {
|
||||
Error::InvalidInput { message } => Error::InvalidInput {
|
||||
message: format!(
|
||||
Error::InvalidInput { message } => {
|
||||
let mut message = format!(
|
||||
"{} for full text search index '{}' for {}",
|
||||
message, index.name, selector_description
|
||||
),
|
||||
},
|
||||
);
|
||||
if let Some(tokenizer) = model_backed_tokenizer {
|
||||
message.push_str(&format!(
|
||||
". tokenize() runs on the client: the '{}' tokenizer is rebuilt \
|
||||
in this process from the index metadata, so its model files must \
|
||||
be available under LANCE_LANGUAGE_MODEL_HOME and, for remote \
|
||||
tables, match the files the server uses",
|
||||
tokenizer
|
||||
));
|
||||
}
|
||||
Error::InvalidInput { message }
|
||||
}
|
||||
err => err,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user