mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-04 04:28:44 +00:00
feat: default create_index to create-only
This commit is contained in:
@@ -1526,11 +1526,7 @@ describe("When creating an index", () => {
|
||||
|
||||
it("should allow me to replace (or not) an existing index", async () => {
|
||||
await tbl.createIndex("id");
|
||||
// Default is replace=true
|
||||
await tbl.createIndex("id");
|
||||
await expect(tbl.createIndex("id", { replace: false })).rejects.toThrow(
|
||||
"already exists",
|
||||
);
|
||||
await expect(tbl.createIndex("id")).rejects.toThrow("already exists");
|
||||
await tbl.createIndex("id", { replace: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -841,7 +841,7 @@ export interface IndexOptions {
|
||||
* and the same name, then an error will be returned. This is true even if
|
||||
* that index is out of date.
|
||||
*
|
||||
* The default is true
|
||||
* The default is false
|
||||
*/
|
||||
replace?: boolean;
|
||||
|
||||
|
||||
@@ -402,6 +402,7 @@ class RemoteTable(Table):
|
||||
/,
|
||||
*,
|
||||
config: IndexConfigType,
|
||||
replace: bool = ...,
|
||||
wait_timeout: Optional[timedelta] = ...,
|
||||
name: Optional[str] = ...,
|
||||
train: bool = ...,
|
||||
@@ -416,7 +417,7 @@ class RemoteTable(Table):
|
||||
index_cache_size: Optional[int] = ...,
|
||||
num_partitions: Optional[int] = ...,
|
||||
num_sub_vectors: Optional[int] = ...,
|
||||
replace: Optional[bool] = ...,
|
||||
replace: bool = ...,
|
||||
accelerator: Optional[str] = ...,
|
||||
index_type: Literal[
|
||||
"VECTOR", "IVF_FLAT", "IVF_SQ", "IVF_PQ", "IVF_HNSW_SQ", "IVF_HNSW_PQ"
|
||||
@@ -435,7 +436,7 @@ class RemoteTable(Table):
|
||||
index_cache_size: Optional[int] = None,
|
||||
num_partitions: Optional[int] = None,
|
||||
num_sub_vectors: Optional[int] = None,
|
||||
replace: Optional[bool] = None,
|
||||
replace: bool = False,
|
||||
accelerator: Optional[str] = None,
|
||||
index_type="vector",
|
||||
wait_timeout: Optional[timedelta] = None,
|
||||
@@ -479,7 +480,6 @@ class RemoteTable(Table):
|
||||
vector_column_name,
|
||||
accelerator,
|
||||
index_cache_size,
|
||||
replace,
|
||||
)
|
||||
|
||||
if is_legacy:
|
||||
@@ -503,12 +503,6 @@ class RemoteTable(Table):
|
||||
"If you have 100M+ vectors to index,"
|
||||
"please contact us at contact@lancedb.com"
|
||||
)
|
||||
if replace is not None:
|
||||
logging.warning(
|
||||
"replace is not supported on LanceDB cloud."
|
||||
"Existing indexes will always be replaced."
|
||||
)
|
||||
|
||||
idx_type = index_type.upper()
|
||||
if idx_type == "VECTOR" or idx_type == "IVF_PQ":
|
||||
config = IvfPq(
|
||||
@@ -561,7 +555,7 @@ class RemoteTable(Table):
|
||||
column: str,
|
||||
*,
|
||||
config: IndexConfigType,
|
||||
replace: Optional[bool] = None,
|
||||
replace: bool = False,
|
||||
wait_timeout: Optional[timedelta] = None,
|
||||
name: Optional[str] = None,
|
||||
train: bool = True,
|
||||
@@ -593,7 +587,6 @@ class RemoteTable(Table):
|
||||
vector_column_name: str,
|
||||
accelerator: Optional[str],
|
||||
index_cache_size: Optional[int],
|
||||
replace: Optional[bool],
|
||||
) -> bool:
|
||||
"""Detect if this is a legacy create_index call."""
|
||||
if config is not None:
|
||||
@@ -605,7 +598,6 @@ class RemoteTable(Table):
|
||||
num_sub_vectors,
|
||||
accelerator,
|
||||
index_cache_size,
|
||||
replace,
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
@@ -1132,7 +1132,7 @@ class Table(ABC):
|
||||
num_partitions: Optional[int] = None,
|
||||
num_sub_vectors: Optional[int] = None,
|
||||
vector_column_name: str = VECTOR_COLUMN_NAME,
|
||||
replace: bool = True,
|
||||
replace: bool = False,
|
||||
accelerator: Optional[str] = None,
|
||||
index_cache_size: Optional[int] = None,
|
||||
*,
|
||||
@@ -1166,7 +1166,7 @@ class Table(ABC):
|
||||
The index configuration object. If provided, uses the new unified API.
|
||||
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
|
||||
BTree, Bitmap, LabelList, Fm, FTS.
|
||||
replace : bool, default True
|
||||
replace : bool, default False
|
||||
Whether to replace an existing index on this column.
|
||||
wait_timeout : timedelta, optional
|
||||
Timeout to wait for async indexing to complete.
|
||||
@@ -1198,7 +1198,7 @@ class Table(ABC):
|
||||
column: str,
|
||||
*,
|
||||
config: IndexConfigType,
|
||||
replace: Optional[bool] = None,
|
||||
replace: bool = False,
|
||||
wait_timeout: Optional[timedelta] = None,
|
||||
name: Optional[str] = None,
|
||||
train: bool = True,
|
||||
@@ -1260,7 +1260,7 @@ class Table(ABC):
|
||||
self,
|
||||
column: str,
|
||||
*,
|
||||
replace: bool = True,
|
||||
replace: bool = False,
|
||||
index_type: ScalarIndexType = "BTREE",
|
||||
wait_timeout: Optional[timedelta] = None,
|
||||
name: Optional[str] = None,
|
||||
@@ -1272,7 +1272,7 @@ class Table(ABC):
|
||||
column : str
|
||||
The column to be indexed. Must be a boolean, integer, float,
|
||||
or string column.
|
||||
replace : bool, default True
|
||||
replace : bool, default False
|
||||
Replace the existing index if it exists.
|
||||
index_type: Literal["BTREE", "BITMAP", "LABEL_LIST"], default "BTREE"
|
||||
The type of index to create.
|
||||
@@ -2988,7 +2988,7 @@ class LanceTable(Table):
|
||||
num_partitions: Optional[int] = None,
|
||||
num_sub_vectors: Optional[int] = None,
|
||||
vector_column_name: str = VECTOR_COLUMN_NAME,
|
||||
replace: bool = True,
|
||||
replace: bool = False,
|
||||
accelerator: Optional[str] = None,
|
||||
index_cache_size: Optional[int] = None,
|
||||
num_bits: int = 8,
|
||||
@@ -3030,7 +3030,7 @@ class LanceTable(Table):
|
||||
The index configuration object. If provided, uses the new unified API.
|
||||
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
|
||||
BTree, Bitmap, LabelList, Fm, FTS.
|
||||
replace : bool, default True
|
||||
replace : bool, default False
|
||||
Whether to replace an existing index on this column.
|
||||
wait_timeout : timedelta, optional
|
||||
Timeout to wait for async indexing to complete.
|
||||
@@ -3169,7 +3169,7 @@ class LanceTable(Table):
|
||||
column: str,
|
||||
*,
|
||||
config: IndexConfigType,
|
||||
replace: Optional[bool] = None,
|
||||
replace: bool = False,
|
||||
wait_timeout: Optional[timedelta] = None,
|
||||
name: Optional[str] = None,
|
||||
train: bool = True,
|
||||
@@ -3424,7 +3424,7 @@ class LanceTable(Table):
|
||||
self,
|
||||
column: str,
|
||||
*,
|
||||
replace: bool = True,
|
||||
replace: bool = False,
|
||||
index_type: ScalarIndexType = "BTREE",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
@@ -5313,7 +5313,7 @@ class AsyncTable:
|
||||
self,
|
||||
column: str,
|
||||
*,
|
||||
replace: Optional[bool] = None,
|
||||
replace: bool = False,
|
||||
config: Optional[
|
||||
Union[
|
||||
IvfFlat,
|
||||
@@ -5344,14 +5344,14 @@ class AsyncTable:
|
||||
----------
|
||||
column: str
|
||||
The column to index.
|
||||
replace: bool, default True
|
||||
replace: bool, default False
|
||||
Whether to replace the existing index
|
||||
|
||||
If this is false, and another index already exists on the same columns
|
||||
and the same name, then an error will be returned. This is true even if
|
||||
that index is out of date.
|
||||
|
||||
The default is True
|
||||
The default is False
|
||||
config: default None
|
||||
For advanced configuration you can specify the type of index you would
|
||||
like to create. You can also specify index-specific parameters when
|
||||
@@ -5409,7 +5409,7 @@ class AsyncTable:
|
||||
self,
|
||||
column: str,
|
||||
*,
|
||||
replace: Optional[bool] = None,
|
||||
replace: bool = False,
|
||||
config: Optional[
|
||||
Union[
|
||||
IvfFlat,
|
||||
|
||||
@@ -97,6 +97,9 @@ async def test_create_index_async_returns_done_job(some_table: AsyncTable):
|
||||
async def test_create_scalar_index(some_table: AsyncTable):
|
||||
# Can create
|
||||
await some_table.create_index("id")
|
||||
# Can't recreate by default
|
||||
with pytest.raises(RuntimeError, match="already exists"):
|
||||
await some_table.create_index("id")
|
||||
# Can recreate if replace=True
|
||||
await some_table.create_index("id", replace=True)
|
||||
indices = await some_table.list_indices()
|
||||
@@ -110,7 +113,7 @@ async def test_create_scalar_index(some_table: AsyncTable):
|
||||
with pytest.raises(RuntimeError, match="already exists"):
|
||||
await some_table.create_index("id", replace=False)
|
||||
# can also specify index type
|
||||
await some_table.create_index("id", config=BTree())
|
||||
await some_table.create_index("id", config=BTree(), replace=True)
|
||||
|
||||
await some_table.drop_index("id_idx")
|
||||
indices = await some_table.list_indices()
|
||||
@@ -351,13 +354,18 @@ async def test_full_text_search_index(some_table: AsyncTable):
|
||||
async def test_create_vector_index(some_table: AsyncTable):
|
||||
# Can create
|
||||
await some_table.create_index("vector")
|
||||
# Can't recreate by default
|
||||
with pytest.raises(RuntimeError, match="already exists"):
|
||||
await some_table.create_index("vector")
|
||||
# Can recreate if replace=True
|
||||
await some_table.create_index("vector", replace=True)
|
||||
# Can't recreate if replace=False
|
||||
with pytest.raises(RuntimeError, match="already exists"):
|
||||
await some_table.create_index("vector", replace=False)
|
||||
# Can also specify index type
|
||||
await some_table.create_index("vector", config=IvfPq(num_partitions=100))
|
||||
await some_table.create_index(
|
||||
"vector", config=IvfPq(num_partitions=100), replace=True
|
||||
)
|
||||
indices = await some_table.list_indices()
|
||||
assert len(indices) == 1
|
||||
assert indices[0].index_type == "IvfPq"
|
||||
|
||||
@@ -1600,7 +1600,7 @@ def test_create_index_method(mock_create_index, mem_db: DBConnection):
|
||||
)
|
||||
mock_create_index.assert_called_with(
|
||||
"my_vector",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=expected_config,
|
||||
wait_timeout=None,
|
||||
name=None,
|
||||
@@ -1620,7 +1620,7 @@ def test_create_index_method(mock_create_index, mem_db: DBConnection):
|
||||
)
|
||||
mock_create_index.assert_called_with(
|
||||
"my_vector",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=expected_config,
|
||||
wait_timeout=None,
|
||||
name=None,
|
||||
@@ -1646,7 +1646,7 @@ def test_create_index_name_and_train_parameters(
|
||||
expected_config = IvfPq() # Default config
|
||||
mock_create_index.assert_called_with(
|
||||
"vector",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=expected_config,
|
||||
wait_timeout=None,
|
||||
name="my_custom_index",
|
||||
@@ -1657,7 +1657,7 @@ def test_create_index_name_and_train_parameters(
|
||||
table.create_index(vector_column_name="vector", train=False)
|
||||
mock_create_index.assert_called_with(
|
||||
"vector",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=expected_config,
|
||||
wait_timeout=None,
|
||||
name=None,
|
||||
@@ -1668,7 +1668,7 @@ def test_create_index_name_and_train_parameters(
|
||||
table.create_index(vector_column_name="vector", name="my_index_name", train=True)
|
||||
mock_create_index.assert_called_with(
|
||||
"vector",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=expected_config,
|
||||
wait_timeout=None,
|
||||
name="my_index_name",
|
||||
@@ -1705,7 +1705,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
|
||||
table.create_index("vector", config=IvfPq(distance_type="l2"))
|
||||
mock_create_index.assert_called_with(
|
||||
"vector",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=IvfPq(distance_type="l2"),
|
||||
wait_timeout=None,
|
||||
name=None,
|
||||
@@ -1716,7 +1716,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
|
||||
table.create_index("category", config=BTree())
|
||||
mock_create_index.assert_called_with(
|
||||
"category",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=BTree(),
|
||||
wait_timeout=None,
|
||||
name=None,
|
||||
@@ -1727,7 +1727,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
|
||||
table.create_index("text", config=FTS(with_position=True))
|
||||
mock_create_index.assert_called_with(
|
||||
"text",
|
||||
replace=True,
|
||||
replace=False,
|
||||
config=FTS(with_position=True),
|
||||
wait_timeout=None,
|
||||
name=None,
|
||||
|
||||
@@ -200,14 +200,14 @@ impl IndexBuilder {
|
||||
parent,
|
||||
index,
|
||||
columns,
|
||||
replace: true,
|
||||
replace: false,
|
||||
train: true,
|
||||
wait_timeout: None,
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to replace the existing index, the default is `true`.
|
||||
/// Whether to replace the existing index, the default is `false`.
|
||||
///
|
||||
/// If this is false, and another index already exists on the same columns
|
||||
/// and the same name, then an error will be returned. This is true even if
|
||||
|
||||
+35
-17
@@ -40,6 +40,7 @@ impl TerminalResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "remote")]
|
||||
pub(crate) fn remote(value: Option<Value>, request_id: String) -> Self {
|
||||
Self {
|
||||
value,
|
||||
@@ -47,30 +48,46 @@ impl TerminalResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "remote")]
|
||||
pub(crate) fn value(&self) -> Option<&Value> {
|
||||
self.value.as_ref()
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
None => Error::Runtime {
|
||||
let value = self.value.ok_or_else(|| {
|
||||
#[cfg(feature = "remote")]
|
||||
if let Some(request_id) = &self.request_id {
|
||||
return Error::Http {
|
||||
source: "successful typed job response did not contain a result".into(),
|
||||
request_id: request_id.clone(),
|
||||
status_code: 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(),
|
||||
request_id,
|
||||
status_code: None,
|
||||
},
|
||||
None => Error::Runtime {
|
||||
message: format!("failed to parse typed job result: {error}"),
|
||||
},
|
||||
serde_json::from_value(value).map_err(|error| {
|
||||
#[cfg(feature = "remote")]
|
||||
{
|
||||
match self.request_id {
|
||||
Some(request_id) => Error::Http {
|
||||
source: format!("failed to parse typed job result: {error}").into(),
|
||||
request_id,
|
||||
status_code: None,
|
||||
},
|
||||
None => Error::Runtime {
|
||||
message: format!("failed to parse typed job result: {error}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "remote"))]
|
||||
{
|
||||
let _ = self.request_id;
|
||||
Error::Runtime {
|
||||
message: format!("failed to parse typed job result: {error}"),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,6 +134,7 @@ impl Job<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "remote")]
|
||||
pub(crate) fn new(handle: Box<dyn JobHandle>) -> Self {
|
||||
Self {
|
||||
inner: JobInner::Handle {
|
||||
|
||||
@@ -524,13 +524,10 @@ impl<S: HttpSend> RemoteTable<S> {
|
||||
_ => resolve_arrow_field_path(&schema, &column)?,
|
||||
};
|
||||
let mut body = serde_json::json!({
|
||||
"column": canonical_column
|
||||
"column": canonical_column,
|
||||
"replace": index.replace,
|
||||
});
|
||||
|
||||
if !index.replace {
|
||||
body["replace"] = false.into();
|
||||
}
|
||||
|
||||
// Add name parameter if provided (for backwards compatibility, only include if Some)
|
||||
if let Some(ref name) = index.name {
|
||||
body["name"] = serde_json::Value::String(name.clone());
|
||||
@@ -6327,7 +6324,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_index_forwards_replace_false_on_existing_route() {
|
||||
async fn test_create_index_forwards_default_replace_false_on_existing_route() {
|
||||
let table = Table::new_with_handler("my_table", move |request| {
|
||||
assert_eq!(request.method(), "POST");
|
||||
match request.url().path() {
|
||||
@@ -6354,7 +6351,40 @@ mod tests {
|
||||
|
||||
table
|
||||
.create_index(&["a"], Index::BTree(Default::default()))
|
||||
.replace(false)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_index_forwards_explicit_replace_true_on_existing_route() {
|
||||
let table = Table::new_with_handler("my_table", move |request| {
|
||||
assert_eq!(request.method(), "POST");
|
||||
match request.url().path() {
|
||||
"/v1/table/my_table/describe/" => {
|
||||
let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(describe_response(&schema))
|
||||
.unwrap()
|
||||
}
|
||||
"/v1/table/my_table/create_index/" => {
|
||||
let body = request.body().unwrap().as_bytes().unwrap();
|
||||
let body: serde_json::Value = serde_json::from_slice(body).unwrap();
|
||||
assert_eq!(body["replace"], json!(true));
|
||||
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body("{}".to_string())
|
||||
.unwrap()
|
||||
}
|
||||
path => panic!("Unexpected path: {}", path),
|
||||
}
|
||||
});
|
||||
|
||||
table
|
||||
.create_index(&["a"], Index::BTree(Default::default()))
|
||||
.replace(true)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -725,12 +725,11 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Rebuilding the same index without replace fails once the build
|
||||
// Rebuilding the same index without explicit replace fails once the build
|
||||
// starts, so the failure reaches the job rather than execute_async.
|
||||
let job = Arc::new(
|
||||
table
|
||||
.create_index(&["id"], Index::BTree(BTreeIndexBuilder::default()))
|
||||
.replace(false)
|
||||
.execute_async()
|
||||
.await
|
||||
.unwrap(),
|
||||
@@ -769,7 +768,6 @@ mod tests {
|
||||
|
||||
let job = table
|
||||
.create_index(&["id"], Index::BTree(BTreeIndexBuilder::default()))
|
||||
.replace(false)
|
||||
.execute_async()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1106,6 +1104,7 @@ mod tests {
|
||||
// Can also specify btree
|
||||
table
|
||||
.create_index(&["i"], Index::BTree(BTreeIndexBuilder::default()))
|
||||
.replace(true)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user