mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-31 18:48:25 +00:00
Fix index UUID review gaps
This commit is contained in:
@@ -446,6 +446,15 @@ paths:
|
||||
properties:
|
||||
column:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description: Optional name for the created index.
|
||||
replace:
|
||||
type: boolean
|
||||
default: true
|
||||
description: |
|
||||
Whether to replace an existing index with the same resolved
|
||||
name. Defaults to true.
|
||||
metric_type:
|
||||
type: string
|
||||
nullable: false
|
||||
@@ -472,6 +481,65 @@ paths:
|
||||
$ref: "#/components/responses/unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/not_found"
|
||||
/v1/table/{name}/create_index_if_not_exists/:
|
||||
post:
|
||||
description: |
|
||||
Create vector index on a Table without replacing an existing index.
|
||||
Servers that do not support create-only index creation may return 404
|
||||
for this route.
|
||||
tags:
|
||||
- Tables
|
||||
summary: Create vector index on a Table if it does not already exist
|
||||
operationId: createIndexIfNotExists
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/table_name"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
column:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description: Optional name for the created index.
|
||||
replace:
|
||||
type: boolean
|
||||
default: false
|
||||
description: |
|
||||
Must be false for create-only behavior. If an index with
|
||||
the same resolved name already exists, the server returns a
|
||||
conflict or invalid request error without replacing it.
|
||||
metric_type:
|
||||
type: string
|
||||
nullable: false
|
||||
description: |
|
||||
The metric type to use for the index. l2, Cosine, Dot are supported.
|
||||
index_type:
|
||||
type: string
|
||||
custom_stop_words:
|
||||
type: [array, "null"]
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
The custom stop-word list for an FTS index. A non-null
|
||||
array replaces the language's built-in stop-word list and is only
|
||||
applied when remove_stop_words is enabled. Null uses the built-in
|
||||
language list, while an empty array explicitly replaces it with no
|
||||
stop words.
|
||||
responses:
|
||||
"200":
|
||||
description: Index successfully created, or the create-only index job was accepted.
|
||||
"400":
|
||||
$ref: "#/components/responses/invalid_request"
|
||||
"401":
|
||||
$ref: "#/components/responses/unauthorized"
|
||||
"409":
|
||||
description: An index with the same resolved name already exists.
|
||||
"404":
|
||||
$ref: "#/components/responses/not_found"
|
||||
/v1/table/{name}/create_scalar_index/:
|
||||
post:
|
||||
description: Create a scalar index on a table
|
||||
|
||||
@@ -220,6 +220,37 @@ impl IndexBuilder {
|
||||
}
|
||||
|
||||
/// Use a caller-selected UUID for the created index.
|
||||
///
|
||||
/// This is supported for native LanceDB tables. Remote tables currently
|
||||
/// reject caller-selected UUIDs because the remote create-index protocol
|
||||
/// does not carry index UUIDs.
|
||||
///
|
||||
/// The UUID must not already belong to a committed index on the table. If
|
||||
/// the UUID is already in use, index creation fails before building the new
|
||||
/// index. This check is independent of [`Self::replace`]: `replace(true)`
|
||||
/// may replace an index name, but it may not reuse another committed
|
||||
/// index's UUID.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lancedb::{connect, index::{Index, scalar::BTreeIndexBuilder}};
|
||||
/// use uuid::Uuid;
|
||||
///
|
||||
/// # async fn index_uuid_example() -> lancedb::Result<()> {
|
||||
/// let db = connect("data/sample-lancedb").execute().await?;
|
||||
/// let table = db.open_table("my_table").execute().await?;
|
||||
/// let index_uuid = Uuid::new_v4();
|
||||
///
|
||||
/// table
|
||||
/// .create_index(&["user_id"], Index::BTree(BTreeIndexBuilder::default()))
|
||||
/// .name("user_id_btree_index".to_string())
|
||||
/// .index_uuid(index_uuid)
|
||||
/// .execute()
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn index_uuid(mut self, uuid: uuid::Uuid) -> Self {
|
||||
self.index_uuid = Some(uuid);
|
||||
self
|
||||
|
||||
+21
-9
@@ -51,23 +51,35 @@ impl TerminalResult {
|
||||
self.value.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "remote")]
|
||||
fn remote_decode_error(request_id: String, message: String) -> Error {
|
||||
Error::Http {
|
||||
source: message.into(),
|
||||
request_id,
|
||||
status_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "remote"))]
|
||||
fn remote_decode_error(_request_id: String, message: String) -> Error {
|
||||
Error::Runtime { message }
|
||||
}
|
||||
|
||||
fn decode<T: DeserializeOwned>(self) -> Result<T> {
|
||||
let value = self.value.ok_or_else(|| match &self.request_id {
|
||||
Some(request_id) => Error::Http {
|
||||
source: "successful typed job response did not contain a result".into(),
|
||||
request_id: request_id.clone(),
|
||||
status_code: None,
|
||||
},
|
||||
Some(request_id) => Self::remote_decode_error(
|
||||
request_id.clone(),
|
||||
"successful typed job response did not contain a result".to_string(),
|
||||
),
|
||||
None => Error::Runtime {
|
||||
message: "successful typed job did not contain a result".to_string(),
|
||||
},
|
||||
})?;
|
||||
serde_json::from_value(value).map_err(|error| match self.request_id {
|
||||
Some(request_id) => Error::Http {
|
||||
source: format!("failed to parse typed job result: {error}").into(),
|
||||
Some(request_id) => Self::remote_decode_error(
|
||||
request_id,
|
||||
status_code: None,
|
||||
},
|
||||
format!("failed to parse typed job result: {error}"),
|
||||
),
|
||||
None => Error::Runtime {
|
||||
message: format!("failed to parse typed job result: {error}"),
|
||||
},
|
||||
|
||||
@@ -491,6 +491,12 @@ impl<S: HttpSend> std::fmt::Debug for RemoteTable<S> {
|
||||
impl<S: HttpSend> RemoteTable<S> {
|
||||
async fn submit_create_index(&self, mut index: IndexBuilder) -> Result<Option<String>> {
|
||||
self.check_mutable().await?;
|
||||
if index.index_uuid.is_some() {
|
||||
return Err(Error::NotSupported {
|
||||
message: "caller-selected index UUIDs are only supported for native LanceDB tables"
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
let route = if index.replace {
|
||||
"create_index"
|
||||
} else {
|
||||
@@ -6120,6 +6126,30 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_index_rejects_remote_index_uuid() {
|
||||
let request_sent = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let sent = request_sent.clone();
|
||||
let table = Table::new_with_handler("my_table", move |_request| {
|
||||
sent.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
http::Response::builder()
|
||||
.status(500)
|
||||
.body("unexpected request".to_string())
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let err = table
|
||||
.create_index(&["a"], Index::BTree(Default::default()))
|
||||
.index_uuid(uuid::Uuid::new_v4())
|
||||
.execute()
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, Error::NotSupported { .. }));
|
||||
assert!(err.to_string().contains("native LanceDB tables"));
|
||||
assert!(!request_sent.load(std::sync::atomic::Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_index_replace_false_does_not_use_legacy_route_after_backend_downgrade() {
|
||||
let legacy_create_request_sent = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
|
||||
@@ -151,6 +151,19 @@ impl NativeTable {
|
||||
let (column, lance_idx_params, index_type) = prepared;
|
||||
let mut dataset = (*self.dataset.get().await?).clone();
|
||||
let columns = [column.as_str()];
|
||||
|
||||
if let Some(index_uuid) = opts.index_uuid {
|
||||
let indices = dataset.load_indices().await?;
|
||||
if let Some(existing_index) = indices.iter().find(|index| index.uuid == index_uuid) {
|
||||
return Err(Error::InvalidInput {
|
||||
message: format!(
|
||||
"Index UUID '{}' is already used by index '{}'",
|
||||
index_uuid, existing_index.name
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut builder = dataset
|
||||
.create_index_builder(&columns, index_type, lance_idx_params.as_ref())
|
||||
.train(opts.train)
|
||||
@@ -1070,6 +1083,67 @@ mod tests {
|
||||
assert_eq!(stats.num_unindexed_rows, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_index_uses_selected_uuid() {
|
||||
let conn = connect("memory://").execute().await.unwrap();
|
||||
let batch = record_batch!(("i", Int32, [1])).unwrap();
|
||||
let table = conn
|
||||
.create_table("my_table", batch)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
let index_uuid = uuid::Uuid::new_v4();
|
||||
|
||||
table
|
||||
.create_index(&["i"], Index::BTree(BTreeIndexBuilder::default()))
|
||||
.name("i_idx".to_string())
|
||||
.index_uuid(index_uuid)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let index_configs = table.list_indices().await.unwrap();
|
||||
assert_eq!(index_configs.len(), 1);
|
||||
let index = index_configs.into_iter().next().unwrap();
|
||||
assert_eq!(index.name, "i_idx");
|
||||
assert_eq!(index.index_uuid, Some(index_uuid.to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_index_rejects_selected_uuid_collision() {
|
||||
let conn = connect("memory://").execute().await.unwrap();
|
||||
let batch = record_batch!(("a", Int32, [1]), ("b", Int32, [2])).unwrap();
|
||||
let table = conn
|
||||
.create_table("my_table", batch)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
let index_uuid = uuid::Uuid::new_v4();
|
||||
|
||||
table
|
||||
.create_index(&["a"], Index::BTree(BTreeIndexBuilder::default()))
|
||||
.name("a_idx".to_string())
|
||||
.index_uuid(index_uuid)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = table
|
||||
.create_index(&["b"], Index::BTree(BTreeIndexBuilder::default()))
|
||||
.name("b_idx".to_string())
|
||||
.index_uuid(index_uuid)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("already used by index 'a_idx'"));
|
||||
|
||||
let index_configs = table.list_indices().await.unwrap();
|
||||
assert_eq!(index_configs.len(), 1);
|
||||
let index = index_configs.into_iter().next().unwrap();
|
||||
assert_eq!(index.name, "a_idx");
|
||||
assert_eq!(index.index_uuid, Some(index_uuid.to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_fm_index() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user