test: add vanilla FAISS e2e coverage (#50126)

## Summary

Add E2E coverage for vanilla FAISS index support across Milvus clients:

- Add Python client index tests for vanilla FAISS factory strings,
metrics, vector types, search params, scalar filters,
release/load/search, range search, and expected unsupported behavior
- Add Go client coverage through the generic index API
- Add REST client create/list/describe metadata coverage

Dependency note: this PR is temporarily based on Milvus #49912 and
currently depends on the Knowhere `faiss-passthrough` branch. The
dependency will be replaced after the final Knowhere reference is
available.

issue: #50124

## Tests

- python -m py_compile
tests/python_client/testcases/indexes/idx_faiss.py
tests/python_client/testcases/indexes/test_faiss.py
tests/restful_client_v2/testcases/test_index_operation.py
- pytest -c /dev/null
tests/python_client/testcases/indexes/test_faiss.py -q -o
cache_dir=/tmp/pytest-cache-vanilla-faiss --tb=short
- pytest tests/restful_client_v2/testcases/test_index_operation.py -q -k
faiss --tb=short -o cache_dir=/tmp/pytest-cache-vanilla-faiss-rest
- cd tests/go_client && go test ./testcases -run
TestCreateIndexVanillaFaissGeneric -count=1 -v

---------

Signed-off-by: xianliang.li <xianliang.li@zilliz.com>
This commit is contained in:
foxspy
2026-06-05 17:56:17 +08:00
committed by GitHub
parent 06bd26e697
commit 3e720ce05b
4 changed files with 777 additions and 139 deletions
+41
View File
@@ -972,6 +972,47 @@ func TestCreateIndexGeneric(t *testing.T) {
}
}
func TestCreateIndexVanillaFaissGeneric(t *testing.T) {
t.Parallel()
ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
mc := hp.CreateDefaultMilvusClient(ctx, t)
cp := hp.NewCreateCollectionParams(hp.Int64Vec)
prepare, schema := hp.CollPrepare.CreateCollection(ctx, t, mc, cp, hp.TNewFieldsOption(), hp.TNewSchemaOption())
// insert
ip := hp.NewInsertParams(schema)
prepare.InsertData(ctx, t, mc, ip, hp.TNewDataOption())
prepare.FlushData(ctx, t, mc, schema.CollectionName)
idx := index.NewGenericIndex(common.DefaultFloatVecFieldName, map[string]string{
index.IndexTypeKey: "FAISS",
index.MetricTypeKey: "L2",
"faiss_index_name": "IVF64,Flat",
})
idxTask, err := mc.CreateIndex(ctx, client.NewCreateIndexOption(schema.CollectionName, common.DefaultFloatVecFieldName, idx))
common.CheckErr(t, err, true)
err = idxTask.Await(ctx)
common.CheckErr(t, err, true)
descIdx, err := mc.DescribeIndex(ctx, client.NewDescribeIndexOption(schema.CollectionName, common.DefaultFloatVecFieldName))
common.CheckErr(t, err, true)
common.CheckIndex(t, descIdx, index.NewGenericIndex(common.DefaultFloatVecFieldName, idx.Params()), common.TNewCheckIndexOpt(common.DefaultNb))
require.Equal(t, "FAISS", descIdx.Index.Params()[index.IndexTypeKey])
require.Equal(t, "IVF64,Flat", descIdx.Index.Params()["faiss_index_name"])
prepare.Load(ctx, t, mc, hp.NewLoadParams(schema.CollectionName))
queryVec := hp.GenSearchVectors(common.DefaultNq, common.DefaultDim, entity.FieldTypeFloatVector)
searchRes, err := mc.Search(ctx, client.NewSearchOption(schema.CollectionName, common.DefaultLimit, queryVec).
WithANNSField(common.DefaultFloatVecFieldName).
WithSearchParam("nprobe", "8").
WithConsistencyLevel(entity.ClStrong))
common.CheckErr(t, err, true)
common.CheckSearchResult(t, searchRes, common.DefaultNq, common.DefaultLimit)
}
// test create index with not exist index name and not exist field name
func TestIndexNotExistName(t *testing.T) {
t.Parallel()
@@ -0,0 +1,103 @@
from pymilvus import DataType
success = "success"
class FAISS:
supported_vector_types = [
DataType.FLOAT_VECTOR,
DataType.BINARY_VECTOR,
]
supported_metrics = ["L2", "IP", "COSINE"]
build_params = [
{
"description": "Flat float index",
"params": {"faiss_index_name": "Flat"},
"expected": success,
},
{
"description": "IVF Flat float index",
"params": {"faiss_index_name": "IVF64,Flat"},
"expected": success,
},
{
"description": "HNSW Flat float index",
"params": {"faiss_index_name": "HNSW16,Flat"},
"expected": success,
},
{
"description": "OPQ IVF PQ float index",
"params": {"faiss_index_name": "OPQ16,IVF64,PQ16x4"},
"expected": success,
},
{
"description": "IVF PQ RFlat float index",
"params": {"faiss_index_name": "IVF64,PQ8x4,RFlat"},
"expected": success,
},
{
"description": "PQ float index",
"params": {"faiss_index_name": "PQ8x4"},
"searchable": False,
"expected": success,
},
{
"description": "Binary flat index",
"params": {"faiss_index_name": "BFlat"},
"vector_data_type": DataType.BINARY_VECTOR,
"metric_type": "HAMMING",
"expected": success,
},
]
search_params = [
{
"description": "IVF Flat nprobe",
"build_params": {"faiss_index_name": "IVF64,Flat"},
"search_params": {"nprobe": 8},
"expected": success,
},
{
"description": "IVF Flat stringified nprobe",
"build_params": {"faiss_index_name": "IVF64,Flat"},
"search_params": {"nprobe": "8"},
"expected": success,
},
{
"description": "IVF Flat invalid nprobe string",
"build_params": {"faiss_index_name": "IVF64,Flat"},
"search_params": {"nprobe": "invalid"},
"expected": {"err_code": 999, "err_msg": "expects a number"},
},
{
"description": "HNSW Flat efSearch",
"build_params": {"faiss_index_name": "HNSW16,Flat"},
"search_params": {"efSearch": 64},
"expected": success,
},
{
"description": "HNSW Flat invalid efSearch string",
"build_params": {"faiss_index_name": "HNSW16,Flat"},
"search_params": {"efSearch": "invalid"},
"expected": {"err_code": 999, "err_msg": "expects a number"},
},
{
"description": "IVF PQ RFlat rerank",
"build_params": {"faiss_index_name": "IVF64,PQ8x4,RFlat"},
"search_params": {"nprobe": 8, "k_factor": 4},
"expected": success,
},
{
"description": "IVF PQ RFlat invalid k_factor string",
"build_params": {"faiss_index_name": "IVF64,PQ8x4,RFlat"},
"search_params": {"nprobe": 8, "k_factor": "invalid"},
"expected": {"err_code": 999, "err_msg": "expects a number"},
},
]
metric_factories = [
{"faiss_index_name": "IVF64,Flat"},
{"faiss_index_name": "HNSW16,Flat"},
]
@@ -0,0 +1,402 @@
import pytest
from base.client_v2_base import TestMilvusClientV2Base
from common import common_func as cf
from common import common_type as ct
from common.common_type import CaseLabel, CheckTasks
from idx_faiss import FAISS
from pymilvus import DataType
index_type = "FAISS"
success = "success"
pk_field_name = "id"
vector_field_name = "vector"
dim = ct.default_dim
default_nb = ct.default_nb
default_search_params = {"nprobe": 8}
def _default_search_params_for_faiss_factory(faiss_index_name):
if faiss_index_name.startswith("IVF"):
return {"nprobe": 8}
if faiss_index_name.startswith("HNSW"):
return {"efSearch": 64}
return {}
class TestFaissBase(TestMilvusClientV2Base):
def _create_collection(self, client, collection_name, vector_data_type=DataType.FLOAT_VECTOR):
schema, _ = self.create_schema(client)
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
schema.add_field(vector_field_name, datatype=vector_data_type)
else:
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim)
self.create_collection(client, collection_name, schema=schema)
return schema
def _insert_rows(self, client, collection_name, vector_data_type=DataType.FLOAT_VECTOR):
vectors = cf.gen_vectors(default_nb, dim=dim, vector_data_type=vector_data_type)
rows = [{pk_field_name: i, vector_field_name: vectors[i]} for i in range(default_nb)]
self.insert(client, collection_name, rows)
self.flush(client, collection_name)
def _create_faiss_index(
self, client, collection_name, metric_type="L2", params=None, check_task=None, check_items=None
):
index_params = self.prepare_index_params(client)[0]
index_params.add_index(
field_name=vector_field_name, metric_type=metric_type, index_type=index_type, params=params
)
return self.create_index(client, collection_name, index_params, check_task=check_task, check_items=check_items)
def _search_and_check(self, client, collection_name, vector_data_type=DataType.FLOAT_VECTOR, search_params=None):
nq = ct.default_nq
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
self.search(
client,
collection_name,
search_vectors,
search_params=search_params,
limit=ct.default_limit,
check_task=CheckTasks.check_search_results,
check_items={
"enable_milvus_client_api": True,
"nq": nq,
"limit": ct.default_limit,
"pk_name": pk_field_name,
},
)
def _assert_index_params(self, client, collection_name, params, metric_type):
idx_info = client.describe_index(collection_name, vector_field_name)
assert idx_info["index_type"] == index_type
assert idx_info["metric_type"] == metric_type
for key, value in params.items():
assert key in idx_info.keys()
assert str(value) in [str(v) for v in idx_info.values()]
class TestFaissBuildParams(TestFaissBase):
@pytest.mark.tags(CaseLabel.L1)
@pytest.mark.parametrize("params", FAISS.build_params)
def test_faiss_build_params(self, params):
"""
Test vanilla Faiss factory build parameters.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
vector_data_type = params.get("vector_data_type", DataType.FLOAT_VECTOR)
metric_type = params.get("metric_type", "L2")
build_params = params.get("params", None)
self._create_collection(client, collection_name, vector_data_type)
self._insert_rows(client, collection_name, vector_data_type)
if params.get("expected", None) != success:
self._create_faiss_index(
client,
collection_name,
metric_type=metric_type,
params=build_params,
check_task=CheckTasks.err_res,
check_items=params.get("expected"),
)
else:
self._create_faiss_index(client, collection_name, metric_type=metric_type, params=build_params)
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
if vector_data_type == DataType.FLOAT_VECTOR and params.get("searchable", True):
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
self._search_and_check(client, collection_name, vector_data_type, search_params=search_params)
elif vector_data_type != DataType.FLOAT_VECTOR:
search_params = {}
self._search_and_check(client, collection_name, vector_data_type, search_params=search_params)
self._assert_index_params(client, collection_name, build_params, metric_type)
@pytest.mark.tags(CaseLabel.L2)
@pytest.mark.parametrize("metric", FAISS.supported_metrics)
@pytest.mark.parametrize("build_params", [{"faiss_index_name": "Flat"}] + FAISS.metric_factories)
def test_faiss_on_all_float_metrics(self, metric, build_params):
"""
Test vanilla Faiss float index factories on all supported float metrics.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
self._create_faiss_index(client, collection_name, metric_type=metric, params=build_params)
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
self._search_and_check(client, collection_name, DataType.FLOAT_VECTOR, search_params=search_params)
self._assert_index_params(client, collection_name, build_params, metric)
@pytest.mark.tags(CaseLabel.L2)
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
def test_faiss_on_all_vector_types(self, vector_data_type):
"""
Test vanilla Faiss vector type support.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
self._create_collection(client, collection_name, vector_data_type)
self._insert_rows(client, collection_name, vector_data_type)
if vector_data_type == DataType.BINARY_VECTOR:
metric_type = "HAMMING"
build_params = {"faiss_index_name": "BFlat"}
else:
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
build_params = {"faiss_index_name": "Flat"}
if vector_data_type not in FAISS.supported_vector_types:
self._create_faiss_index(
client,
collection_name,
metric_type=metric_type,
params=build_params,
check_task=CheckTasks.err_res,
check_items={"err_code": 999, "err_msg": "invalid parameter"},
)
else:
self._create_faiss_index(client, collection_name, metric_type=metric_type, params=build_params)
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
self._search_and_check(client, collection_name, vector_data_type, search_params={})
self._assert_index_params(client, collection_name, build_params, metric_type)
@pytest.mark.tags(CaseLabel.L2)
@pytest.mark.parametrize(
"params", [p for p in FAISS.build_params if p.get("expected") == success and p.get("searchable", True)]
)
def test_faiss_build_release_load_search(self, params):
"""
Test vanilla Faiss index survives the full Milvus build -> release -> load -> search flow.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
vector_data_type = params.get("vector_data_type", DataType.FLOAT_VECTOR)
metric_type = params.get("metric_type", "L2")
build_params = params["params"]
self._create_collection(client, collection_name, vector_data_type)
self._insert_rows(client, collection_name, vector_data_type)
self._create_faiss_index(client, collection_name, metric_type=metric_type, params=build_params)
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.release_collection(client, collection_name)
self.load_collection(client, collection_name)
search_params = {}
if vector_data_type == DataType.FLOAT_VECTOR:
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
self._search_and_check(client, collection_name, vector_data_type, search_params=search_params)
self._assert_index_params(client, collection_name, build_params, metric_type)
class TestFaissSearchParams(TestFaissBase):
@pytest.mark.tags(CaseLabel.L1)
@pytest.mark.parametrize("params", FAISS.search_params)
def test_faiss_search_params(self, params):
"""
Test vanilla Faiss search parameter forwarding.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
build_params = params["build_params"]
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
self._create_faiss_index(client, collection_name, metric_type="L2", params=build_params)
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
if params.get("expected", None) != success:
nq = ct.default_nq
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
self.search(
client,
collection_name,
search_vectors,
search_params=params["search_params"],
limit=ct.default_limit,
check_task=CheckTasks.err_res,
check_items=params.get("expected"),
)
else:
self._search_and_check(
client, collection_name, DataType.FLOAT_VECTOR, search_params=params["search_params"]
)
@pytest.mark.tags(CaseLabel.L2)
def test_faiss_incompatible_search_params(self):
"""
Test vanilla Faiss rejects search parameters incompatible with the factory index.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
build_params = {"faiss_index_name": "Flat"}
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
self._create_faiss_index(client, collection_name, metric_type="L2", params=build_params)
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
nq = ct.default_nq
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
self.search(
client,
collection_name,
search_vectors,
search_params={"efSearch": 64},
limit=ct.default_limit,
check_task=CheckTasks.err_res,
check_items={"err_code": 999, "err_msg": "not supported"},
)
@pytest.mark.tags(CaseLabel.L2)
@pytest.mark.parametrize(
"build_params",
[
{"faiss_index_name": "Flat"},
{"faiss_index_name": "IVF64,Flat"},
{"faiss_index_name": "HNSW16,Flat"},
],
)
def test_faiss_search_with_scalar_filter(self, build_params):
"""
Test vanilla Faiss search honors Milvus scalar filter bitset.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
self._create_faiss_index(client, collection_name, metric_type="L2", params=build_params)
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
results = client.search(
collection_name,
search_vectors,
filter=f"{pk_field_name} >= 100",
search_params=search_params,
limit=ct.default_limit,
)
assert len(results) == 1
assert len(results[0]) == ct.default_limit
assert all(hit["id"] >= 100 for hit in results[0])
@pytest.mark.tags(CaseLabel.L2)
def test_faiss_flat_range_search(self):
"""
Test vanilla Faiss float Flat range search. The current adapter implements
RangeSearch for float FAISS indexes.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
self._create_faiss_index(client, collection_name, metric_type="L2", params={"faiss_index_name": "Flat"})
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
range_params = {"radius": 100000.0, "range_filter": 0.0}
self.search(
client,
collection_name,
search_vectors,
search_params=range_params,
limit=ct.default_limit,
check_task=CheckTasks.check_search_results,
check_items={
"enable_milvus_client_api": True,
"nq": 1,
"limit": ct.default_limit,
"pk_name": pk_field_name,
},
)
@pytest.mark.tags(CaseLabel.L2)
def test_faiss_binary_range_search_not_supported(self):
"""
Test vanilla Faiss binary range search is explicitly not implemented.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
self._create_collection(client, collection_name, DataType.BINARY_VECTOR)
self._insert_rows(client, collection_name, DataType.BINARY_VECTOR)
self._create_faiss_index(client, collection_name, metric_type="HAMMING", params={"faiss_index_name": "BFlat"})
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.BINARY_VECTOR)
self.search(
client,
collection_name,
search_vectors,
search_params={"radius": 1000, "range_filter": 0},
limit=ct.default_limit,
check_task=CheckTasks.err_res,
check_items={"err_code": 999, "err_msg": "RangeSearch unsupported for binary faiss indexes"},
)
@pytest.mark.tags(CaseLabel.L2)
def test_faiss_pq_search_selector_not_supported(self):
"""
Test vanilla Faiss IndexPQ rejects Milvus search because Milvus passes
an ID selector/bitset and native FAISS IndexPQ does not support it.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
self._create_faiss_index(client, collection_name, metric_type="L2", params={"faiss_index_name": "PQ8x4"})
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
self.search(
client,
collection_name,
search_vectors,
filter=f"{pk_field_name} >= 100",
search_params={},
limit=ct.default_limit,
check_task=CheckTasks.err_res,
check_items={"err_code": 999, "err_msg": "selector not supported"},
)
@pytest.mark.tags(CaseLabel.L2)
def test_faiss_search_iterator_not_supported(self):
"""
Test vanilla Faiss search iterator is rejected because the adapter does
not expose raw-vector retrieval.
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
self._create_faiss_index(client, collection_name, metric_type="L2", params={"faiss_index_name": "Flat"})
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
self.load_collection(client, collection_name)
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
self.search_iterator(
client,
collection_name,
data=search_vectors,
batch_size=100,
search_params={},
check_task=CheckTasks.err_res,
check_items={"err_code": 65535, "err_msg": "Failed to create iterators from index"},
)
@@ -1,19 +1,21 @@
import random
from sklearn import preprocessing
import numpy as np
import time
import concurrent.futures
from typing import Dict, List
from utils.utils import gen_collection_name, patch_faker_text, en_vocabularies_distribution, \
zh_vocabularies_distribution
from utils.util_log import test_log as logger
import random
import time
import numpy as np
import pytest
from base.testbase import TestBase
from utils.utils import gen_vector
from pymilvus import (
Collection
)
from faker import Faker
from pymilvus import Collection
from sklearn import preprocessing
from utils.util_log import test_log as logger
from utils.utils import (
en_vocabularies_distribution,
gen_collection_name,
gen_vector,
patch_faker_text,
zh_vocabularies_distribution,
)
Faker.seed(19530)
fake_en = Faker("en_US")
@@ -27,13 +29,12 @@ index_param_map = {
"IVF_SQ8": {"nlist": 128},
"HNSW": {"M": 16, "efConstruction": 200},
"BM25_SPARSE_INVERTED_INDEX": {"bm25_k1": 0.5, "bm25_b": 0.5},
"AUTOINDEX": {}
"AUTOINDEX": {},
}
@pytest.mark.L0
class TestCreateIndex(TestBase):
@pytest.mark.parametrize("metric_type", ["L2", "COSINE", "IP"])
@pytest.mark.parametrize("index_type", ["AUTOINDEX", "IVF_SQ8", "HNSW"])
@pytest.mark.parametrize("dim", [128])
@@ -52,9 +53,9 @@ class TestCreateIndex(TestBase):
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "word_count", "dataType": "Int64", "elementTypeParams": {}},
{"fieldName": "book_describe", "dataType": "VarChar", "elementTypeParams": {"max_length": "256"}},
{"fieldName": "book_intro", "dataType": "FloatVector", "elementTypeParams": {"dim": f"{dim}"}}
{"fieldName": "book_intro", "dataType": "FloatVector", "elementTypeParams": {"dim": f"{dim}"}},
]
}
},
}
logger.info(f"create collection {name} with payload: {payload}")
rsp = client.collection_create(payload)
@@ -66,19 +67,21 @@ class TestCreateIndex(TestBase):
payload = {
"collectionName": name,
"indexParams": [
{"fieldName": "book_intro", "indexName": "book_intro_vector",
"metricType": f"{metric_type}",
"indexType": f"{index_type}",
"params": index_param_map[index_type]
}
]
{
"fieldName": "book_intro",
"indexName": "book_intro_vector",
"metricType": f"{metric_type}",
"indexType": f"{index_type}",
"params": index_param_map[index_type],
}
],
}
# Create multiple index creation tasks
num_threads = 10 # Number of concurrent tasks
payloads = [payload.copy() for _ in range(num_threads)]
def create_index(idx_payload: Dict) -> Dict:
def create_index(idx_payload: dict) -> dict:
return self.index_client.index_create(idx_payload)
# Execute index creation concurrently
@@ -87,9 +90,9 @@ class TestCreateIndex(TestBase):
for future in concurrent.futures.as_completed(future_to_payload):
try:
rsp = future.result()
assert rsp['code'] == 0
assert rsp["code"] == 0
except Exception as e:
logger.info(f'Index creation failed with error: {str(e)}')
logger.info(f"Index creation failed with error: {str(e)}")
raise
time.sleep(10) # Wait for all indexes to be ready
@@ -97,15 +100,15 @@ class TestCreateIndex(TestBase):
rsp = self.index_client.index_list(collection_name=name)
# describe index
rsp = self.index_client.index_describe(collection_name=name, index_name="book_intro_vector")
assert rsp['code'] == 0
assert len(rsp['data']) == len(payload['indexParams'])
expected_index = sorted(payload['indexParams'], key=lambda x: x['fieldName'])
actual_index = sorted(rsp['data'], key=lambda x: x['fieldName'])
assert rsp["code"] == 0
assert len(rsp["data"]) == len(payload["indexParams"])
expected_index = sorted(payload["indexParams"], key=lambda x: x["fieldName"])
actual_index = sorted(rsp["data"], key=lambda x: x["fieldName"])
for i in range(len(expected_index)):
assert expected_index[i]['fieldName'] == actual_index[i]['fieldName']
assert expected_index[i]['indexName'] == actual_index[i]['indexName']
assert expected_index[i]['metricType'] == actual_index[i]['metricType']
assert expected_index[i]["indexType"] == actual_index[i]['indexType']
assert expected_index[i]["fieldName"] == actual_index[i]["fieldName"]
assert expected_index[i]["indexName"] == actual_index[i]["indexName"]
assert expected_index[i]["metricType"] == actual_index[i]["metricType"]
assert expected_index[i]["indexType"] == actual_index[i]["indexType"]
# check index by pymilvus
index_info = [index.to_dict() for index in c.indexes]
logger.info(f"index_info: {index_info}")
@@ -120,15 +123,78 @@ class TestCreateIndex(TestBase):
assert index_param.get("params", {}) == index_param_map[index_type]
# drop index
for i in range(len(actual_index)):
payload = {
"collectionName": name,
"indexName": actual_index[i]['indexName']
}
payload = {"collectionName": name, "indexName": actual_index[i]["indexName"]}
rsp = self.index_client.index_drop(payload)
assert rsp['code'] == 0
assert rsp["code"] == 0
# list index, expect empty
rsp = self.index_client.index_list(collection_name=name)
assert rsp['data'] == []
assert rsp["data"] == []
@pytest.mark.parametrize("dim", [128])
def test_create_vanilla_faiss_index(self, dim):
"""
target: test create vanilla Faiss index
method: create a float vector collection and create FAISS index with faiss_index_name
expected: create index success and metadata persisted
"""
name = gen_collection_name()
client = self.collection_client
payload = {
"collectionName": name,
"schema": {
"fields": [
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "word_count", "dataType": "Int64", "elementTypeParams": {}},
{"fieldName": "book_describe", "dataType": "VarChar", "elementTypeParams": {"max_length": "256"}},
{"fieldName": "book_intro", "dataType": "FloatVector", "elementTypeParams": {"dim": f"{dim}"}},
]
},
}
logger.info(f"create collection {name} with payload: {payload}")
rsp = client.collection_create(payload)
assert rsp["code"] == 0
c = Collection(name)
c.flush()
index_name = "book_intro_faiss"
index_params = {"faiss_index_name": "IVF64,Flat"}
payload = {
"collectionName": name,
"indexParams": [
{
"fieldName": "book_intro",
"indexName": index_name,
"metricType": "L2",
"indexType": "FAISS",
"params": index_params,
}
],
}
rsp = self.index_client.index_create(payload)
assert rsp["code"] == 0
time.sleep(10)
rsp = self.index_client.index_list(collection_name=name)
assert rsp["code"] == 0
assert index_name in rsp["data"]
rsp = self.index_client.index_describe(collection_name=name, index_name=index_name)
assert rsp["code"] == 0
assert len(rsp["data"]) == 1
actual_index = rsp["data"][0]
assert actual_index["fieldName"] == "book_intro"
assert actual_index["indexName"] == index_name
assert actual_index["metricType"] == "L2"
assert actual_index["indexType"] == "FAISS"
index_info = [index.to_dict() for index in c.indexes]
logger.info(f"index_info: {index_info}")
assert len(index_info) == 1
index_param = index_info[0]["index_param"]
assert index_param["metric_type"] == "L2"
assert index_param["index_type"] == "FAISS"
persisted_params = index_param.get("params", {})
assert persisted_params["faiss_index_name"] == index_params["faiss_index_name"]
@pytest.mark.parametrize("index_type", ["INVERTED"])
@pytest.mark.parametrize("dim", [128])
@@ -147,9 +213,9 @@ class TestCreateIndex(TestBase):
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "word_count", "dataType": "Int64", "elementTypeParams": {}},
{"fieldName": "book_describe", "dataType": "VarChar", "elementTypeParams": {"max_length": "256"}},
{"fieldName": "book_intro", "dataType": "FloatVector", "elementTypeParams": {"dim": f"{dim}"}}
{"fieldName": "book_intro", "dataType": "FloatVector", "elementTypeParams": {"dim": f"{dim}"}},
]
}
},
}
logger.info(f"create collection {name} with payload: {payload}")
rsp = client.collection_create(payload)
@@ -162,13 +228,11 @@ class TestCreateIndex(TestBase):
"word_count": j,
"book_describe": f"book_{j}",
"book_intro": preprocessing.normalize([np.array([random.random() for _ in range(dim)])])[
0].tolist(),
0
].tolist(),
}
data.append(tmp)
payload = {
"collectionName": name,
"data": data
}
payload = {"collectionName": name, "data": data}
rsp = self.vector_client.vector_insert(payload)
c = Collection(name)
c.flush()
@@ -178,24 +242,30 @@ class TestCreateIndex(TestBase):
# create index
payload = {
"collectionName": name,
"indexParams": [{"fieldName": "word_count", "indexName": "word_count_vector", "indexType": "INVERTED",
"params": {"index_type": "INVERTED"}}]
"indexParams": [
{
"fieldName": "word_count",
"indexName": "word_count_vector",
"indexType": "INVERTED",
"params": {"index_type": "INVERTED"},
}
],
}
rsp = self.index_client.index_create(payload)
assert rsp['code'] == 0
assert rsp["code"] == 0
time.sleep(10)
# list index, expect not empty
rsp = self.index_client.index_list(collection_name=name)
# describe index
rsp = self.index_client.index_describe(collection_name=name, index_name="word_count_vector")
assert rsp['code'] == 0
assert len(rsp['data']) == len(payload['indexParams'])
expected_index = sorted(payload['indexParams'], key=lambda x: x['fieldName'])
actual_index = sorted(rsp['data'], key=lambda x: x['fieldName'])
assert rsp["code"] == 0
assert len(rsp["data"]) == len(payload["indexParams"])
expected_index = sorted(payload["indexParams"], key=lambda x: x["fieldName"])
actual_index = sorted(rsp["data"], key=lambda x: x["fieldName"])
for i in range(len(expected_index)):
assert expected_index[i]['fieldName'] == actual_index[i]['fieldName']
assert expected_index[i]['indexName'] == actual_index[i]['indexName']
assert expected_index[i]['indexType'] == actual_index[i]['indexType']
assert expected_index[i]["fieldName"] == actual_index[i]["fieldName"]
assert expected_index[i]["indexName"] == actual_index[i]["indexName"]
assert expected_index[i]["indexType"] == actual_index[i]["indexType"]
@pytest.mark.parametrize("index_type", ["BIN_FLAT", "BIN_IVF_FLAT"])
@pytest.mark.parametrize("metric_type", ["JACCARD", "HAMMING"])
@@ -215,9 +285,9 @@ class TestCreateIndex(TestBase):
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "word_count", "dataType": "Int64", "elementTypeParams": {}},
{"fieldName": "book_describe", "dataType": "VarChar", "elementTypeParams": {"max_length": "256"}},
{"fieldName": "binary_vector", "dataType": "BinaryVector", "elementTypeParams": {"dim": f"{dim}"}}
{"fieldName": "binary_vector", "dataType": "BinaryVector", "elementTypeParams": {"dim": f"{dim}"}},
]
}
},
}
logger.info(f"create collection {name} with payload: {payload}")
rsp = client.collection_create(payload)
@@ -229,13 +299,10 @@ class TestCreateIndex(TestBase):
"book_id": j,
"word_count": j,
"book_describe": f"book_{j}",
"binary_vector": gen_vector(datatype="BinaryVector", dim=dim)
"binary_vector": gen_vector(datatype="BinaryVector", dim=dim),
}
data.append(tmp)
payload = {
"collectionName": name,
"data": data
}
payload = {"collectionName": name, "data": data}
rsp = self.vector_client.vector_insert(payload)
c = Collection(name)
c.flush()
@@ -246,26 +313,33 @@ class TestCreateIndex(TestBase):
index_name = "binary_vector_index"
payload = {
"collectionName": name,
"indexParams": [{"fieldName": "binary_vector", "indexName": index_name, "metricType": metric_type, "indexType": index_type,
"params": {"index_type": index_type}}]
"indexParams": [
{
"fieldName": "binary_vector",
"indexName": index_name,
"metricType": metric_type,
"indexType": index_type,
"params": {"index_type": index_type},
}
],
}
if index_type == "BIN_IVF_FLAT":
payload["indexParams"][0]["params"]["nlist"] = "16384"
rsp = self.index_client.index_create(payload)
assert rsp['code'] == 0
assert rsp["code"] == 0
time.sleep(10)
# list index, expect not empty
rsp = self.index_client.index_list(collection_name=name)
# describe index
rsp = self.index_client.index_describe(collection_name=name, index_name=index_name)
assert rsp['code'] == 0
assert len(rsp['data']) == len(payload['indexParams'])
expected_index = sorted(payload['indexParams'], key=lambda x: x['fieldName'])
actual_index = sorted(rsp['data'], key=lambda x: x['fieldName'])
assert rsp["code"] == 0
assert len(rsp["data"]) == len(payload["indexParams"])
expected_index = sorted(payload["indexParams"], key=lambda x: x["fieldName"])
actual_index = sorted(rsp["data"], key=lambda x: x["fieldName"])
for i in range(len(expected_index)):
assert expected_index[i]['fieldName'] == actual_index[i]['fieldName']
assert expected_index[i]['indexName'] == actual_index[i]['indexName']
assert expected_index[i]['indexType'] == actual_index[i]['indexType']
assert expected_index[i]["fieldName"] == actual_index[i]["fieldName"]
assert expected_index[i]["indexName"] == actual_index[i]["indexName"]
assert expected_index[i]["indexType"] == actual_index[i]["indexType"]
@pytest.mark.parametrize("insert_round", [1])
@pytest.mark.parametrize("auto_id", [True])
@@ -273,12 +347,23 @@ class TestCreateIndex(TestBase):
@pytest.mark.parametrize("enable_dynamic_schema", [True])
@pytest.mark.parametrize("nb", [3000])
@pytest.mark.parametrize("dim", [128])
@pytest.mark.parametrize("tokenizer", ['standard', 'jieba'])
@pytest.mark.parametrize("index_type", ['SPARSE_INVERTED_INDEX', 'SPARSE_WAND'])
@pytest.mark.parametrize("tokenizer", ["standard", "jieba"])
@pytest.mark.parametrize("index_type", ["SPARSE_INVERTED_INDEX", "SPARSE_WAND"])
@pytest.mark.parametrize("bm25_k1", [1.2, 1.5])
@pytest.mark.parametrize("bm25_b", [0.7, 0.5])
def test_create_index_for_full_text_search(self, nb, dim, insert_round, auto_id, is_partition_key,
enable_dynamic_schema, tokenizer, index_type, bm25_k1, bm25_b):
def test_create_index_for_full_text_search(
self,
nb,
dim,
insert_round,
auto_id,
is_partition_key,
enable_dynamic_schema,
tokenizer,
index_type,
bm25_k1,
bm25_b,
):
"""
Insert a vector with a simple payload
"""
@@ -291,16 +376,26 @@ class TestCreateIndex(TestBase):
"enableDynamicField": enable_dynamic_schema,
"fields": [
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "user_id", "dataType": "Int64", "isPartitionKey": is_partition_key,
"elementTypeParams": {}},
{
"fieldName": "user_id",
"dataType": "Int64",
"isPartitionKey": is_partition_key,
"elementTypeParams": {},
},
{"fieldName": "word_count", "dataType": "Int64", "elementTypeParams": {}},
{"fieldName": "book_describe", "dataType": "VarChar", "elementTypeParams": {"max_length": "256"}},
{"fieldName": "document_content", "dataType": "VarChar",
"elementTypeParams": {"max_length": "1000", "enable_analyzer": True,
"analyzer_params": {
"tokenizer": tokenizer,
},
"enable_match": True}},
{
"fieldName": "document_content",
"dataType": "VarChar",
"elementTypeParams": {
"max_length": "1000",
"enable_analyzer": True,
"analyzer_params": {
"tokenizer": tokenizer,
},
"enable_match": True,
},
},
{"fieldName": "sparse_vector", "dataType": "SparseFloatVector"},
],
"functions": [
@@ -309,19 +404,19 @@ class TestCreateIndex(TestBase):
"type": "BM25",
"inputFieldNames": ["document_content"],
"outputFieldNames": ["sparse_vector"],
"params": {}
"params": {},
}
]
],
},
}
rsp = self.collection_client.collection_create(payload)
assert rsp['code'] == 0
assert rsp["code"] == 0
rsp = self.collection_client.collection_describe(name)
logger.info(f"rsp: {rsp}")
assert rsp['code'] == 0
if tokenizer == 'standard':
assert rsp["code"] == 0
if tokenizer == "standard":
fake = fake_en
elif tokenizer == 'jieba':
elif tokenizer == "jieba":
fake = fake_zh
else:
raise Exception("Invalid tokenizer")
@@ -354,30 +449,32 @@ class TestCreateIndex(TestBase):
"data": data,
}
rsp = self.vector_client.vector_insert(payload)
assert rsp['code'] == 0
assert rsp['data']['insertCount'] == nb
assert rsp['code'] == 0
assert rsp["code"] == 0
assert rsp["data"]["insertCount"] == nb
assert rsp["code"] == 0
# create index
payload = {
"collectionName": name,
"indexParams": [
{"fieldName": "sparse_vector", "indexName": "sparse_vector",
"metricType": "BM25",
"indexType": index_type,
"params": {"bm25_k1": bm25_k1, "bm25_b": bm25_b}
}
]
{
"fieldName": "sparse_vector",
"indexName": "sparse_vector",
"metricType": "BM25",
"indexType": index_type,
"params": {"bm25_k1": bm25_k1, "bm25_b": bm25_b},
}
],
}
rsp = self.index_client.index_create(payload)
c = Collection(name)
index_info = [index.to_dict() for index in c.indexes]
logger.info(f"index_info: {index_info}")
for info in index_info:
assert info['index_param']['metric_type'] == 'BM25'
assert info['index_param']["params"]['bm25_k1'] == bm25_k1
assert info['index_param']["params"]['bm25_b'] == bm25_b
assert info['index_param']['index_type'] == index_type
assert info["index_param"]["metric_type"] == "BM25"
assert info["index_param"]["params"]["bm25_k1"] == bm25_k1
assert info["index_param"]["params"]["bm25_b"] == bm25_b
assert info["index_param"]["index_type"] == index_type
@pytest.mark.L0
@@ -398,9 +495,9 @@ class TestIndexProperties(TestBase):
"schema": {
"fields": [
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "my_vector", "dataType": "FloatVector", "elementTypeParams": {"dim": 128}}
{"fieldName": "my_vector", "dataType": "FloatVector", "elementTypeParams": {"dim": 128}},
]
}
},
}
collection_client.collection_create(payload)
@@ -414,15 +511,14 @@ class TestIndexProperties(TestBase):
"indexName": "my_vector",
"indexType": "IVF_SQ8",
"metricType": "L2",
"params": {"nlist": 128}
"params": {"nlist": 128},
}
],
}
index_client.index_create(index_payload)
# list index
rsp = index_client.index_list(name)
assert rsp['code'] == 0
assert rsp["code"] == 0
# Alter index properties
properties = {"mmap.enabled": True}
@@ -431,7 +527,7 @@ class TestIndexProperties(TestBase):
# describe index
rsp = index_client.index_describe(name, "my_vector")
assert rsp['code'] == 0
assert rsp["code"] == 0
# Drop index properties
delete_keys = ["mmap.enabled"]
@@ -440,12 +536,9 @@ class TestIndexProperties(TestBase):
# describe index
rsp = index_client.index_describe(name, "my_vector")
assert rsp['code'] == 0
assert rsp["code"] == 0
@pytest.mark.parametrize("invalid_property", [
{"invalid_key": True},
{"mmap.enabled": "invalid_value"}
])
@pytest.mark.parametrize("invalid_property", [{"invalid_key": True}, {"mmap.enabled": "invalid_value"}])
def test_alter_index_properties_with_invalid_properties(self, invalid_property):
"""
target: test alter index properties with invalid properties
@@ -460,9 +553,9 @@ class TestIndexProperties(TestBase):
"schema": {
"fields": [
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "my_vector", "dataType": "FloatVector", "elementTypeParams": {"dim": 128}}
{"fieldName": "my_vector", "dataType": "FloatVector", "elementTypeParams": {"dim": 128}},
]
}
},
}
collection_client.collection_create(payload)
@@ -476,16 +569,15 @@ class TestIndexProperties(TestBase):
"indexName": "my_vector",
"indexType": "IVF_SQ8",
"metricType": "L2",
"params": {"nlist": 128}
"params": {"nlist": 128},
}
],
}
index_client.index_create(index_payload)
# Alter index properties with invalid property
rsp = index_client.alter_index_properties(name, "my_vector", invalid_property)
assert rsp['code'] == 1100
assert rsp["code"] == 1100
def test_drop_index_properties_with_nonexistent_key(self):
"""
@@ -501,9 +593,9 @@ class TestIndexProperties(TestBase):
"schema": {
"fields": [
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "my_vector", "dataType": "FloatVector", "elementTypeParams": {"dim": 128}}
{"fieldName": "my_vector", "dataType": "FloatVector", "elementTypeParams": {"dim": 128}},
]
}
},
}
collection_client.collection_create(payload)
@@ -517,9 +609,8 @@ class TestIndexProperties(TestBase):
"indexName": "my_vector",
"indexType": "IVF_SQ8",
"metricType": "L2",
"params": {"nlist": 128}
"params": {"nlist": 128},
}
],
}
index_client.index_create(index_payload)
@@ -527,18 +618,16 @@ class TestIndexProperties(TestBase):
# Drop index properties with nonexistent key
delete_keys = ["nonexistent.key"]
rsp = index_client.drop_index_properties(name, "my_vector", delete_keys)
assert rsp['code'] == 1100
assert rsp["code"] == 1100
@pytest.mark.L1
class TestCreateIndexNegative(TestBase):
@pytest.mark.parametrize("index_type", ["BIN_FLAT", "BIN_IVF_FLAT"])
@pytest.mark.parametrize("metric_type", ["L2", "IP", "COSINE"])
@pytest.mark.parametrize("dim", [128])
def test_index_for_binary_vector_field_with_mismatch_metric_type(self, dim, metric_type, index_type):
"""
"""
""" """
name = gen_collection_name()
client = self.collection_client
payload = {
@@ -548,9 +637,9 @@ class TestCreateIndexNegative(TestBase):
{"fieldName": "book_id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
{"fieldName": "word_count", "dataType": "Int64", "elementTypeParams": {}},
{"fieldName": "book_describe", "dataType": "VarChar", "elementTypeParams": {"max_length": "256"}},
{"fieldName": "binary_vector", "dataType": "BinaryVector", "elementTypeParams": {"dim": f"{dim}"}}
{"fieldName": "binary_vector", "dataType": "BinaryVector", "elementTypeParams": {"dim": f"{dim}"}},
]
}
},
}
logger.info(f"create collection {name} with payload: {payload}")
rsp = client.collection_create(payload)
@@ -562,13 +651,10 @@ class TestCreateIndexNegative(TestBase):
"book_id": j,
"word_count": j,
"book_describe": f"book_{j}",
"binary_vector": gen_vector(datatype="BinaryVector", dim=dim)
"binary_vector": gen_vector(datatype="BinaryVector", dim=dim),
}
data.append(tmp)
payload = {
"collectionName": name,
"data": data
}
payload = {"collectionName": name, "data": data}
rsp = self.vector_client.vector_insert(payload)
c = Collection(name)
c.flush()
@@ -579,11 +665,17 @@ class TestCreateIndexNegative(TestBase):
index_name = "binary_vector_index"
payload = {
"collectionName": name,
"indexParams": [{"fieldName": "binary_vector", "indexName": index_name, "metricType": metric_type,
"params": {"index_type": index_type}}]
"indexParams": [
{
"fieldName": "binary_vector",
"indexName": index_name,
"metricType": metric_type,
"params": {"index_type": index_type},
}
],
}
if index_type == "BIN_IVF_FLAT":
payload["indexParams"][0]["params"]["nlist"] = "16384"
rsp = self.index_client.index_create(payload)
assert rsp['code'] == 1100
assert "not supported" in rsp['message']
assert rsp["code"] == 1100
assert "not supported" in rsp["message"]