chore: import upstream snapshot with attribution
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:17 +08:00
commit 498b235461
5446 changed files with 2748612 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
from enum import Enum
from pymilvus import ExceptionsMessage
class ErrorCode(Enum):
ErrorOk = 0
Error = 1
ErrorMessage = {ErrorCode.ErrorOk: "",
ErrorCode.Error: "is illegal"}
class ErrorMap:
def __init__(self, err_code, err_msg):
self.err_code = err_code
self.err_msg = err_msg
class ConnectionErrorMessage(ExceptionsMessage):
FailConnect = "Fail connecting to server on %s:%s. Timeout"
ConnectExist = "The connection named %s already creating, but passed parameters don't match the configured parameters"
class CollectionErrorMessage(ExceptionsMessage):
CollNotLoaded = "collection %s was not loaded into memory"
class PartitionErrorMessage(ExceptionsMessage):
pass
class IndexErrorMessage(ExceptionsMessage):
WrongFieldName = "cannot create index on non-vector field: %s"
DropLoadedIndex = "index cannot be dropped, collection is loaded, please release it first"
CheckVectorIndex = "data type {0} can't build with this index {1}"
SparseFloatVectorMetricType = "only IP&BM25 is the supported metric type for sparse index"
VectorMetricTypeExist = "metric type not set for vector index"
# please update the msg below as #37543 fixed
CheckBitmapIndex = "bitmap index are only supported on bool, int, string"
CheckBitmapOnPK = "create bitmap index on primary key not supported"
CheckBitmapCardinality = "failed to check bitmap cardinality limit, should be larger than 0 and smaller than 1000"
NotConfigable = "{0} is not a configable index property"
InvalidOffsetCache = "invalid offset cache index params"
OneIndexPerField = "at most one distinct index is allowed per field"
AlterOnLoadedCollection = "can't alter index on loaded collection, please release the collection first"
class QueryErrorMessage(ExceptionsMessage):
ParseExpressionFailed = "failed to create query plan: cannot parse expression: "
File diff suppressed because it is too large Load Diff
+670
View File
@@ -0,0 +1,670 @@
from dataclasses import dataclass
from typing import List, Dict, Optional
""" Define param names"""
class IndexName:
# Vector
AUTOINDEX = "AUTOINDEX"
FLAT = "FLAT"
IVF_FLAT = "IVF_FLAT"
IVF_SQ8 = "IVF_SQ8"
IVF_PQ = "IVF_PQ"
IVF_HNSW = "IVF_HNSW"
HNSW = "HNSW"
DISKANN = "DISKANN"
SCANN = "SCANN"
# binary
BIN_FLAT = "BIN_FLAT"
BIN_IVF_FLAT = "BIN_IVF_FLAT"
# Sparse
SPARSE_WAND = "SPARSE_WAND"
SPARSE_INVERTED_INDEX = "SPARSE_INVERTED_INDEX"
# GPU
GPU_IVF_FLAT = "GPU_IVF_FLAT"
GPU_IVF_PQ = "GPU_IVF_PQ"
GPU_CAGRA = "GPU_CAGRA"
GPU_BRUTE_FORCE = "GPU_BRUTE_FORCE"
# Scalar
INVERTED = "INVERTED"
BITMAP = "BITMAP"
Trie = "Trie"
STL_SORT = "STL_SORT"
class MetricType:
L2 = "L2"
IP = "IP"
COSINE = "COSINE"
JACCARD = "JACCARD"
""" expressions """
@dataclass
class ExprBase:
expr: str
@property
def subset(self):
return f"({self.expr})"
def __repr__(self):
return self.expr
@property
def value(self) -> str:
return self.expr
class Expr:
# BooleanConstant: 'true' | 'True' | 'TRUE' | 'false' | 'False' | 'FALSE'
@staticmethod
def LT(left, right):
return ExprBase(expr=f"{left} < {right}")
@staticmethod
def LE(left, right):
return ExprBase(expr=f"{left} <= {right}")
@staticmethod
def GT(left, right):
return ExprBase(expr=f"{left} > {right}")
@staticmethod
def GE(left, right):
return ExprBase(expr=f"{left} >= {right}")
@staticmethod
def EQ(left, right):
return ExprBase(expr=f"{left} == {right}")
@staticmethod
def NE(left, right):
return ExprBase(expr=f"{left} != {right}")
@staticmethod
def like(left, right):
return ExprBase(expr=f'{left} like "{right}"')
@staticmethod
def LIKE(left, right):
return ExprBase(expr=f'{left} LIKE "{right}"')
@staticmethod
def exists(name):
return ExprBase(expr=f'exists {name}')
@staticmethod
def EXISTS(name):
return ExprBase(expr=f'EXISTS {name}')
@staticmethod
def ADD(left, right):
return ExprBase(expr=f"{left} + {right}")
@staticmethod
def SUB(left, right):
return ExprBase(expr=f"{left} - {right}")
@staticmethod
def MUL(left, right):
return ExprBase(expr=f"{left} * {right}")
@staticmethod
def DIV(left, right):
return ExprBase(expr=f"{left} / {right}")
@staticmethod
def MOD(left, right):
return ExprBase(expr=f"{left} % {right}")
@staticmethod
def POW(left, right):
return ExprBase(expr=f"{left} ** {right}")
@staticmethod
def SHL(left, right):
# Note: not supported
return ExprBase(expr=f"{left}<<{right}")
@staticmethod
def SHR(left, right):
# Note: not supported
return ExprBase(expr=f"{left}>>{right}")
@staticmethod
def BAND(left, right):
# Note: not supported
return ExprBase(expr=f"{left} & {right}")
@staticmethod
def BOR(left, right):
# Note: not supported
return ExprBase(expr=f"{left} | {right}")
@staticmethod
def BXOR(left, right):
# Note: not supported
return ExprBase(expr=f"{left} ^ {right}")
@staticmethod
def AND(left, right):
return ExprBase(expr=f"{left} && {right}")
@staticmethod
def And(left, right):
return ExprBase(expr=f"{left} and {right}")
@staticmethod
def OR(left, right):
return ExprBase(expr=f"{left} || {right}")
@staticmethod
def Or(left, right):
return ExprBase(expr=f"{left} or {right}")
@staticmethod
def BNOT(name):
# Note: not supported
return ExprBase(expr=f"~{name}")
@staticmethod
def NOT(name):
return ExprBase(expr=f"!{name}")
@staticmethod
def Not(name):
return ExprBase(expr=f"not {name}")
@staticmethod
def In(left, right):
return ExprBase(expr=f"{left} in {right}")
@staticmethod
def Nin(left, right):
return ExprBase(expr=f"{left} not in {right}")
@staticmethod
def json_contains(left, right):
return ExprBase(expr=f"json_contains({left}, {right})")
@staticmethod
def JSON_CONTAINS(left, right):
return ExprBase(expr=f"JSON_CONTAINS({left}, {right})")
@staticmethod
def json_contains_all(left, right):
return ExprBase(expr=f"json_contains_all({left}, {right})")
@staticmethod
def JSON_CONTAINS_ALL(left, right):
return ExprBase(expr=f"JSON_CONTAINS_ALL({left}, {right})")
@staticmethod
def json_contains_any(left, right):
return ExprBase(expr=f"json_contains_any({left}, {right})")
@staticmethod
def JSON_CONTAINS_ANY(left, right):
return ExprBase(expr=f"JSON_CONTAINS_ANY({left}, {right})")
@staticmethod
def array_contains(left, right):
return ExprBase(expr=f"array_contains({left}, {right})")
@staticmethod
def ARRAY_CONTAINS(left, right):
return ExprBase(expr=f"ARRAY_CONTAINS({left}, {right})")
@staticmethod
def array_contains_all(left, right):
return ExprBase(expr=f"array_contains_all({left}, {right})")
@staticmethod
def ARRAY_CONTAINS_ALL(left, right):
return ExprBase(expr=f"ARRAY_CONTAINS_ALL({left}, {right})")
@staticmethod
def array_contains_any(left, right):
return ExprBase(expr=f"array_contains_any({left}, {right})")
@staticmethod
def ARRAY_CONTAINS_ANY(left, right):
return ExprBase(expr=f"ARRAY_CONTAINS_ANY({left}, {right})")
@staticmethod
def array_length(name):
return ExprBase(expr=f"array_length({name})")
@staticmethod
def ARRAY_LENGTH(name):
return ExprBase(expr=f"ARRAY_LENGTH({name})")
"""" Define pass in params """
@dataclass
class BasePrams:
@property
def to_dict(self):
return {k: v for k, v in vars(self).items() if v is not None}
@dataclass
class FieldParams(BasePrams):
description: str = None
# varchar
max_length: int = None
# array
max_capacity: int = None
# for vector
dim: int = None
# scalar
is_primary: bool = None
# auto_id: bool = None
is_partition_key: bool = None
is_clustering_key: bool = None
nullable: bool = None
# warmup (tiered storage)
warmup: str = None
# text match (varchar with analyzer)
enable_analyzer: bool = None
enable_match: bool = None
@dataclass
class IndexPrams(BasePrams):
index_type: str = None
params: dict = None
metric_type: str = None
@dataclass
class SearchInsidePrams(BasePrams):
# inside params
radius: Optional[float] = None
range_filter: Optional[float] = None
group_by_field: Optional[str] = None
@dataclass
class SearchPrams(BasePrams):
metric_type: str = MetricType.L2
params: dict = None
""" Define default params """
class DefaultVectorIndexParams:
@staticmethod
def FLAT(field: str, metric_type=MetricType.L2):
return {field: IndexPrams(index_type=IndexName.FLAT, params={}, metric_type=metric_type)}
@staticmethod
def IVF_FLAT(field: str, nlist: int = 1024, metric_type=MetricType.L2):
return {
field: IndexPrams(index_type=IndexName.IVF_FLAT, params={"nlist": nlist}, metric_type=metric_type)
}
@staticmethod
def IVF_PQ(field: str, nlist: int = 1024, m: int = 8, nbits: int = 8, metric_type=MetricType.L2):
return {
field: IndexPrams(index_type=IndexName.IVF_PQ, params={"nlist": nlist, "m": m, "nbits": nbits},
metric_type=metric_type)
}
@staticmethod
def IVF_SQ8(field: str, nlist: int = 1024, metric_type=MetricType.L2):
return {
field: IndexPrams(index_type=IndexName.IVF_SQ8, params={"nlist": nlist}, metric_type=metric_type)
}
@staticmethod
def HNSW(field: str, m: int = 8, efConstruction: int = 200, metric_type=MetricType.L2):
return {
field: IndexPrams(index_type=IndexName.HNSW, params={"M": m, "efConstruction": efConstruction}, metric_type=metric_type)
}
@staticmethod
def SCANN(field: str, nlist: int = 128, metric_type=MetricType.L2):
return {
field: IndexPrams(index_type=IndexName.SCANN, params={"nlist": nlist}, metric_type=metric_type)
}
@staticmethod
def DISKANN(field: str, metric_type=MetricType.L2):
return {field: IndexPrams(index_type=IndexName.DISKANN, params={}, metric_type=metric_type)}
@staticmethod
def BIN_FLAT(field: str, nlist: int = 1024, metric_type=MetricType.JACCARD):
return {
field: IndexPrams(index_type=IndexName.BIN_FLAT, params={"nlist": nlist}, metric_type=metric_type)
}
@staticmethod
def BIN_IVF_FLAT(field: str, nlist: int = 1024, metric_type=MetricType.JACCARD):
return {
field: IndexPrams(index_type=IndexName.BIN_IVF_FLAT, params={"nlist": nlist},
metric_type=metric_type)
}
@staticmethod
def SPARSE_WAND(field: str, drop_ratio_build: float = 0.2, metric_type=MetricType.IP):
return {
field: IndexPrams(index_type=IndexName.SPARSE_WAND, params={"drop_ratio_build": drop_ratio_build},
metric_type=metric_type)
}
@staticmethod
def SPARSE_INVERTED_INDEX(field: str, drop_ratio_build: float = 0.2, metric_type=MetricType.IP):
return {
field: IndexPrams(index_type=IndexName.SPARSE_INVERTED_INDEX, params={"drop_ratio_build": drop_ratio_build},
metric_type=metric_type)
}
class DefaultIndexSearchParams:
@staticmethod
def FLAT(**kwargs):
metric_type = kwargs.get("metric_type", MetricType.L2)
return {
"metric_type": metric_type,
"params": {}
}
@staticmethod
def IVF_FLAT(**kwargs):
"""
nprobe: [1, nlist]
"""
metric_type = kwargs.get("metric_type", MetricType.L2)
nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
return {
"metric_type": metric_type,
"params": {"nprobe": nprobe}
}
@staticmethod
def IVF_PQ(**kwargs):
"""
nprobe: [1, nlist]
"""
metric_type = kwargs.get("metric_type", MetricType.L2)
nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
return {
"metric_type": metric_type,
"params": {"nprobe": nprobe}
}
@staticmethod
def IVF_SQ8(**kwargs):
"""
nprobe: [1, nlist]
"""
metric_type = kwargs.get("metric_type", MetricType.L2)
nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
return {
"metric_type": metric_type,
"params": {"nprobe": nprobe}
}
@staticmethod
def HNSW(**kwargs):
"""
ef: [top_k, int_max]
"""
metric_type = kwargs.get("metric_type", MetricType.L2)
limit = kwargs.get("limit", 64)
ef = max(limit, 128)
return {
"metric_type": metric_type,
"params": {"ef": ef}
}
@staticmethod
def SCANN(**kwargs):
"""
nprobe: [1, nlist]
reorder_k: [top_k, ∞]
"""
metric_type = kwargs.get("metric_type", MetricType.L2)
nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
limit = kwargs.get("limit", 64)
reorder_k = max(limit, 128)
return {
"metric_type": metric_type,
"params": {"nprobe": nprobe, "reorder_k": reorder_k}
}
@staticmethod
def DISKANN(**kwargs):
"""
search_list: [top_k, int_max]
"""
metric_type = kwargs.get("metric_type", MetricType.L2)
limit = kwargs.get("limit", 64)
search_list = max(limit, 128)
return {
"metric_type": metric_type,
"params": {"search_list": search_list}
}
@staticmethod
def BIN_FLAT(**kwargs):
metric_type = kwargs.get("metric_type", MetricType.JACCARD)
return {
"metric_type": metric_type,
"params": {}
}
@staticmethod
def BIN_IVF_FLAT(**kwargs):
"""
nprobe: [1, nlist]
"""
metric_type = kwargs.get("metric_type", MetricType.JACCARD)
nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
return {
"metric_type": metric_type,
"params": {"nprobe": nprobe}
}
@staticmethod
def SPARSE_WAND(**kwargs):
"""
drop_ratio_search: [0.0, 1.0]
"""
metric_type = kwargs.get("metric_type", MetricType.IP)
drop_ratio_search = kwargs.get("drop_ratio_build", 0.2)
return {
"metric_type": metric_type,
"params": {"drop_ratio_search": drop_ratio_search}
}
@staticmethod
def SPARSE_INVERTED_INDEX(**kwargs):
"""
drop_ratio_search: [0.0, 1.0]
"""
metric_type = kwargs.get("metric_type", MetricType.IP)
drop_ratio_search = kwargs.get("drop_ratio_build", 0.2)
return {
"metric_type": metric_type,
"params": {"drop_ratio_search": drop_ratio_search}
}
class DefaultScalarIndexParams:
@staticmethod
def Default(field: str):
return {field: IndexPrams()}
@staticmethod
def list_default(fields: List[str]) -> Dict[str, IndexPrams]:
return {n: IndexPrams() for n in fields}
@staticmethod
def Trie(field: str):
return {field: IndexPrams(index_type=IndexName.Trie)}
@staticmethod
def STL_SORT(field: str):
return {field: IndexPrams(index_type=IndexName.STL_SORT)}
@staticmethod
def INVERTED(field: str):
return {field: IndexPrams(index_type=IndexName.INVERTED)}
@staticmethod
def list_inverted(fields: List[str]) -> Dict[str, IndexPrams]:
return {n: IndexPrams(index_type=IndexName.INVERTED) for n in fields}
@staticmethod
def BITMAP(field: str):
return {field: IndexPrams(index_type=IndexName.BITMAP)}
@staticmethod
def list_bitmap(fields: List[str]) -> Dict[str, IndexPrams]:
return {n: IndexPrams(index_type=IndexName.BITMAP) for n in fields}
class AlterIndexParams:
@staticmethod
def index_offset_cache(enable: bool = True):
return {'indexoffsetcache.enabled': enable}
@staticmethod
def index_mmap(enable: bool = True):
return {'mmap.enabled': enable}
class DefaultVectorSearchParams:
@staticmethod
def FLAT(metric_type=MetricType.L2, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def IVF_FLAT(metric_type=MetricType.L2, nprobe: int = 32, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"nprobe": nprobe}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def IVF_PQ(metric_type=MetricType.L2, nprobe: int = 16, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"nprobe": nprobe}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def IVF_SQ8(metric_type=MetricType.L2, nprobe: int = 32, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"nprobe": nprobe}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def HNSW(metric_type=MetricType.L2, ef: int = 200, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"ef": ef}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def SCANN(metric_type=MetricType.L2, nprobe: int = 32, reorder_k: int = 200, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"nprobe": nprobe, "reorder_k": reorder_k}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def DISKANN(metric_type=MetricType.L2, search_list: int = 30, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"search_list": search_list}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def BIN_FLAT(metric_type=MetricType.JACCARD, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def BIN_IVF_FLAT(metric_type=MetricType.JACCARD, nprobe: int = 32, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"nprobe": nprobe}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def SPARSE_WAND(metric_type=MetricType.IP, drop_ratio_search: float = 0.2, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"drop_ratio_search": drop_ratio_search}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@staticmethod
def SPARSE_INVERTED_INDEX(metric_type=MetricType.IP, drop_ratio_search: float = 0.2, inside_params: SearchInsidePrams = None, **kwargs):
inside_params_dict = {"drop_ratio_search": drop_ratio_search}
if inside_params is not None:
inside_params_dict.update(inside_params.to_dict)
sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
sp.update(kwargs)
return sp
@dataclass
class ExprCheckParams:
field: str
field_expr: str
rex: str
+597
View File
@@ -0,0 +1,597 @@
import numpy as np
from pymilvus import DataType
""" Initialized parameters """
port = 19530
epsilon = 0.000001
namespace = "milvus"
default_flush_interval = 1
big_flush_interval = 1000
default_drop_interval = 3
default_dim = 128
default_nb = 3000
default_nb_medium = 5000
default_max_capacity = 100
default_max_length = 500
default_top_k = 10
default_nq = 2
default_limit = 10
default_batch_size = 1000
default_int32_value = np.int32(1234)
min_limit = 1
max_limit = 16384
max_top_k = 16384
max_nq = 16384
max_partition_num = 1024
max_role_num = 10
default_partition_num = 16 # default num_partitions for partition key feature
default_segment_row_limit = 1000
default_server_segment_row_limit = 1024 * 512
default_alias = "default"
default_user = "root"
default_password = "Milvus"
default_primary_field_name = "pk"
default_bool_field_name = "bool"
default_int8_field_name = "int8"
default_int16_field_name = "int16"
default_int32_field_name = "int32"
default_int64_field_name = "int64"
default_float_field_name = "float"
default_double_field_name = "double"
default_string_field_name = "varchar"
default_json_field_name = "json_field"
default_geometry_field_name = "geometry_field"
default_timestamptz_field_name = "timestamptz_field"
default_array_field_name = "int_array"
default_int8_array_field_name = "int8_array"
default_int16_array_field_name = "int16_array"
default_int32_array_field_name = "int32_array"
default_int64_array_field_name = "int64_array"
default_bool_array_field_name = "bool_array"
default_float_array_field_name = "float_array"
default_double_array_field_name = "double_array"
default_string_array_field_name = "string_array"
default_float_vec_field_name = "float_vector"
default_float16_vec_field_name = "float16_vector"
default_bfloat16_vec_field_name = "bfloat16_vector"
default_int8_vec_field_name = "int8_vector"
another_float_vec_field_name = "float_vector1"
default_binary_vec_field_name = "binary_vector"
default_sparse_vec_field_name = "sparse_vector"
text_sparse_vector = "TEXT_SPARSE_VECTOR"
default_reranker_field_name = "reranker_field"
default_new_field_name = "field_new"
all_vector_types = [
DataType.FLOAT_VECTOR,
DataType.FLOAT16_VECTOR,
DataType.BFLOAT16_VECTOR,
DataType.SPARSE_FLOAT_VECTOR,
DataType.INT8_VECTOR,
DataType.BINARY_VECTOR,
]
default_metric_for_vector_type = {
DataType.FLOAT_VECTOR: "COSINE",
DataType.FLOAT16_VECTOR: "L2",
DataType.BFLOAT16_VECTOR: "IP",
DataType.SPARSE_FLOAT_VECTOR: "IP",
DataType.INT8_VECTOR: "COSINE",
DataType.BINARY_VECTOR: "HAMMING",
}
all_scalar_data_types = [
DataType.INT8,
DataType.INT16,
DataType.INT32,
DataType.INT64,
DataType.BOOL,
DataType.FLOAT,
DataType.DOUBLE,
DataType.VARCHAR,
DataType.ARRAY,
DataType.JSON,
DataType.GEOMETRY,
DataType.TIMESTAMPTZ,
]
default_field_name_map = {
DataType.INT8: default_int8_field_name,
DataType.INT16: default_int16_field_name,
DataType.INT32: default_int32_field_name,
DataType.INT64: default_int64_field_name,
DataType.BOOL: default_bool_field_name,
DataType.FLOAT: default_float_field_name,
DataType.DOUBLE: default_double_field_name,
DataType.VARCHAR: default_string_field_name,
DataType.ARRAY: default_array_field_name,
DataType.JSON: default_json_field_name,
DataType.FLOAT_VECTOR: default_float_vec_field_name,
DataType.FLOAT16_VECTOR: default_float16_vec_field_name,
DataType.BFLOAT16_VECTOR: default_bfloat16_vec_field_name,
DataType.SPARSE_FLOAT_VECTOR: default_sparse_vec_field_name,
DataType.INT8_VECTOR: default_int8_vec_field_name,
DataType.BINARY_VECTOR: default_binary_vec_field_name,
}
append_vector_type = [
DataType.FLOAT16_VECTOR,
DataType.BFLOAT16_VECTOR,
DataType.SPARSE_FLOAT_VECTOR,
DataType.INT8_VECTOR,
]
all_dense_vector_types = [
DataType.FLOAT_VECTOR,
DataType.FLOAT16_VECTOR,
DataType.BFLOAT16_VECTOR,
DataType.INT8_VECTOR,
]
all_float_vector_dtypes = [
DataType.FLOAT_VECTOR,
DataType.FLOAT16_VECTOR,
DataType.BFLOAT16_VECTOR,
DataType.SPARSE_FLOAT_VECTOR,
DataType.INT8_VECTOR,
]
default_partition_name = "_default"
default_resource_group_name = "__default_resource_group"
default_resource_group_capacity = 1000000
default_tag = "1970_01_01"
row_count = "row_count"
default_length = 65535
default_json_list_length = 1
default_desc = ""
default_collection_desc = "default collection"
default_index_name = "default_index_name"
default_binary_desc = "default binary collection"
collection_desc = "collection"
int_field_desc = "int64 type field"
float_field_desc = "float type field"
float_vec_field_desc = "float vector type field"
binary_vec_field_desc = "binary vector type field"
max_dim = 32768
min_dim = 2
max_binary_vector_dim = 262144
max_sparse_vector_dim = 4294967294
min_sparse_vector_dim = 1
gracefulTime = 1
default_nlist = 128
compact_segment_num_threshold = 3
compact_delta_ratio_reciprocal = 5 # compact_delta_binlog_ratio is 0.2
compact_retention_duration = 40 # compaction travel time retention range 20s
max_compaction_interval = 60 # the max time interval (s) from the last compaction
max_field_num = 64 # Maximum number of fields in a collection
max_vector_field_num = 10 # Maximum number of vector fields in a collection
max_name_length = 255 # Maximum length of name for a collection or alias
default_replica_num = 1
default_graceful_time = 5 #
default_shards_num = 1
max_shards_num = 16
default_db = "default"
max_database_num = 64
max_collections_per_db = 65536
max_collection_num = 65536
max_hybrid_search_req_num = 1024
default_primary_key_field_name = "id"
default_vector_field_name = "vector"
IMAGE_REPOSITORY_MILVUS = "harbor.milvus.io/dockerhub/milvusdb/milvus"
NAMESPACE_CHAOS_TESTING = "chaos-testing"
Not_Exist = "Not_Exist"
Connect_Object_Name = True
list_content = "list_content"
dict_content = "dict_content"
value_content = "value_content"
code = "code"
err_code = "err_code"
err_msg = "err_msg"
in_cluster_env = "IN_CLUSTER"
default_count_output = "count(*)"
rows_all_data_type_file_path = "/tmp/rows_all_data_type"
"""" List of parameters used to pass """
invalid_resource_names = [
None, # None
" ", # space
"", # empty
"12name", # start with number
"n12 ame", # contain space
"n-ame", # contain hyphen
"nam(e)", # contain special character
"name中文", # contain Chinese character
"name%$#", # contain special character
"".join("a" for i in range(max_name_length + 1)),
] # exceed max length
valid_resource_names = [
"name", # valid name
"_name", # start with underline
"_12name", # start with underline and contains number
"n12ame_", # end with letter and contains number and underline
"nam_e", # contains underline
"".join("a" for i in range(max_name_length)),
] # max length
invalid_dims = [min_dim - 1, 32.1, -32, "vii", "十六", max_dim + 1]
get_not_string = [[], {}, None, (1,), 1, 1.0, [1, "2", 3]]
get_invalid_vectors = [
"1*2",
[1],
[1, 2],
[" "],
["a"],
[None],
None,
(1, 2),
{"a": 1},
" ",
"",
"String",
" siede ",
"中文",
"a".join("a" for i in range(256)),
]
get_invalid_ints = [
9999999999,
1.0,
None,
[1, 2, 3],
" ",
"",
-1,
"String",
"=c",
"中文",
"a".join("a" for i in range(256)),
]
get_invalid_dict = [
[],
1,
[1, "2", 3],
(1,),
None,
"",
" ",
"12-s",
{1: 1},
{"中文": 1},
{"%$#": ["a"]},
{"a".join("a" for i in range(256)): "a"},
]
get_invalid_metric_type = [
[],
1,
[1, "2", 3],
(1,),
{1: 1},
" ",
"12-s",
"12 s",
"(mn)",
"中文",
"%$#",
"".join("a" for i in range(max_name_length + 1)),
]
get_dict_without_host_port = [{"host": "host"}, {"": ""}]
get_wrong_format_dict = [{"host": "string_host", "port": {}}, {"host": 0, "port": 19520}]
get_all_kind_data_distribution = [
1,
np.float64(1.0),
np.double(1.0),
9707199254740993.0,
9707199254740992,
"1",
"123",
"321",
"213",
True,
False,
None,
[1, 2],
[1.0, 2],
{},
{"a": 1},
{"a": 1.0},
{"a": 9707199254740993.0},
{"a": 9707199254740992},
{"a": "1"},
{"a": "123"},
{"a": "321"},
{"a": "213"},
{"a": True},
{"a": [1, 2, 3]},
{"a": [1.0, 2, "1"]},
{"a": [1.0, 2]},
{"a": None},
{"a": {"b": 1}},
{"a": {"b": 1.0}},
{"a": [{"b": 1}, 2.0, np.double(3.0), "4", True, [1, 3.0], None]},
]
""" Specially defined list """
L0_index_types = ["IVF_SQ8", "HNSW", "DISKANN"]
all_index_types = [
"FLAT",
"IVF_FLAT",
"IVF_SQ8",
"IVF_PQ",
"IVF_RABITQ",
"HNSW",
"SCANN",
"DISKANN",
"BIN_FLAT",
"BIN_IVF_FLAT",
"SPARSE_INVERTED_INDEX",
"SPARSE_WAND",
"GPU_IVF_FLAT",
"GPU_IVF_PQ",
]
all_dense_float_index_types = ["FLAT", "IVF_FLAT", "IVF_SQ8", "IVF_PQ", "IVF_RABITQ", "HNSW", "SCANN", "DISKANN"]
inverted_index_algo = ["TAAT_NAIVE", "DAAT_WAND", "DAAT_MAXSCORE"]
int8_vector_index = ["HNSW"]
default_all_indexes_params = [
{},
{"nlist": 128},
{"nlist": 128},
{"nlist": 128, "m": 16, "nbits": 8},
{"nlist": 128, "refine": "true", "refine_type": "SQ8"},
{"M": 32, "efConstruction": 360},
{"nlist": 128},
{},
{},
{"nlist": 64},
{},
{"drop_ratio_build": 0.2},
{"nlist": 64},
{"nlist": 64, "m": 16, "nbits": 8},
]
default_all_search_params_params = [
{},
{"nprobe": 32},
{"nprobe": 32},
{"nprobe": 32},
{"nprobe": 8, "rbq_bits_query": 8, "refine_k": 10.0},
{"ef": 100},
{"nprobe": 32, "reorder_k": 100},
{"search_list": 30},
{},
{"nprobe": 32},
{"drop_ratio_search": "0.2"},
{"drop_ratio_search": "0.2"},
{},
{},
]
Handler_type = ["GRPC", "HTTP"]
binary_supported_index_types = ["BIN_FLAT", "BIN_IVF_FLAT"]
sparse_supported_index_types = ["SPARSE_INVERTED_INDEX", "SPARSE_WAND"]
gpu_supported_index_types = ["GPU_IVF_FLAT", "GPU_IVF_PQ"]
default_L0_metric = "COSINE"
dense_metrics = ["L2", "IP", "COSINE"]
binary_metrics = ["JACCARD", "HAMMING", "SUBSTRUCTURE", "SUPERSTRUCTURE"]
structure_metrics = ["SUBSTRUCTURE", "SUPERSTRUCTURE"]
sparse_metrics = ["IP", "BM25"]
# all_scalar_data_types = ['int8', 'int16', 'int32', 'int64', 'float', 'double', 'bool', 'varchar']
varchar_supported_index_types = ["STL_SORT", "TRIE", "INVERTED", "AUTOINDEX", ""]
numeric_supported_index_types = ["STL_SORT", "INVERTED", "AUTOINDEX", ""]
default_flat_index = {"index_type": "FLAT", "params": {}, "metric_type": default_L0_metric}
default_bin_flat_index = {"index_type": "BIN_FLAT", "params": {}, "metric_type": "JACCARD"}
default_sparse_inverted_index = {
"index_type": "SPARSE_INVERTED_INDEX",
"metric_type": "IP",
"params": {"drop_ratio_build": 0.2},
}
default_text_sparse_inverted_index = {
"index_type": "SPARSE_INVERTED_INDEX",
"metric_type": "BM25",
"params": {
"drop_ratio_build": 0.2,
"bm25_k1": 1.5,
"bm25_b": 0.75,
},
}
default_search_params = {"params": {"nlist": 128}}
default_search_ip_params = {"metric_type": "IP", "params": {"nlist": 128}}
default_search_binary_params = {"metric_type": "JACCARD", "params": {"nprobe": 32}}
default_index = {"index_type": "IVF_SQ8", "metric_type": default_L0_metric, "params": {"nlist": 128}}
default_binary_index = {"index_type": "BIN_IVF_FLAT", "metric_type": "JACCARD", "params": {"nlist": 64}}
default_diskann_index = {"index_type": "DISKANN", "metric_type": default_L0_metric, "params": {}}
default_diskann_search_params = {"params": {"search_list": 30}}
default_sparse_search_params = {"metric_type": "IP", "params": {"drop_ratio_search": "0.2"}}
default_text_sparse_search_params = {"metric_type": "BM25", "params": {}}
built_in_privilege_groups = [
"CollectionReadWrite",
"CollectionReadOnly",
"CollectionAdmin",
"DatabaseReadWrite",
"DatabaseReadOnly",
"DatabaseAdmin",
"ClusterReadWrite",
"ClusterReadOnly",
"ClusterAdmin",
]
privilege_group_privilege_dict = {
"Query": False,
"Search": False,
"GetLoadState": False,
"GetLoadingProgress": False,
"HasPartition": False,
"ShowPartitions": False,
"ShowCollections": False,
"ListAliases": False,
"ListDatabases": False,
"DescribeDatabase": False,
"DescribeAlias": False,
"GetStatistics": False,
"CreateIndex": False,
"DropIndex": False,
"CreatePartition": False,
"DropPartition": False,
"Load": False,
"Release": False,
"Insert": False,
"Delete": False,
"Upsert": False,
"Import": False,
"Flush": False,
"Compaction": False,
"LoadBalance": False,
"RenameCollection": False,
"CreateAlias": False,
"DropAlias": False,
"CreateCollection": False,
"DropCollection": False,
"CreateOwnership": False,
"DropOwnership": False,
"SelectOwnership": False,
"ManageOwnership": False,
"UpdateUser": False,
"SelectUser": False,
"CreateResourceGroup": False,
"DropResourceGroup": False,
"UpdateResourceGroups": False,
"DescribeResourceGroup": False,
"ListResourceGroups": False,
"TransferNode": False,
"TransferReplica": False,
"CreateDatabase": False,
"DropDatabase": False,
"AlterDatabase": False,
"FlushAll": False,
"ListPrivilegeGroups": False,
"CreatePrivilegeGroup": False,
"DropPrivilegeGroup": False,
"OperatePrivilegeGroup": False,
}
all_expr_fields = [
default_int8_field_name,
default_int16_field_name,
default_int32_field_name,
default_int64_field_name,
default_float_field_name,
default_double_field_name,
default_string_field_name,
default_bool_field_name,
default_int8_array_field_name,
default_int16_array_field_name,
default_int32_array_field_name,
default_int64_array_field_name,
default_bool_array_field_name,
default_float_array_field_name,
default_double_array_field_name,
default_string_array_field_name,
]
not_supported_json_cast_types = [
DataType.INT8.name,
DataType.INT16.name,
DataType.INT32.name,
DataType.INT64.name,
DataType.FLOAT.name,
DataType.ARRAY.name,
DataType.FLOAT_VECTOR.name,
DataType.FLOAT16_VECTOR.name,
DataType.BFLOAT16_VECTOR.name,
DataType.BINARY_VECTOR.name,
DataType.SPARSE_FLOAT_VECTOR.name,
DataType.INT8_VECTOR.name,
]
class CheckTasks:
"""The name of the method used to check the result"""
check_nothing = "check_nothing"
err_res = "error_response"
ccr = "check_connection_result"
check_collection_property = "check_collection_property"
check_partition_property = "check_partition_property"
check_search_results = "check_search_results"
check_search_iterator = "check_search_iterator"
check_query_results = "check_query_results"
check_query_iterator = "check_query_iterator"
check_query_empty = "check_query_empty" # verify that query result is empty
check_query_not_empty = "check_query_not_empty"
check_distance = "check_distance"
check_delete_compact = "check_delete_compact"
check_merge_compact = "check_merge_compact"
check_role_property = "check_role_property"
check_permission_deny = "check_permission_deny"
check_auth_failure = "check_auth_failure"
check_value_equal = "check_value_equal"
check_rg_property = "check_resource_group_property"
check_describe_collection_property = "check_describe_collection_property"
check_describe_database_property = "check_describe_database_property"
check_insert_result = "check_insert_result"
check_collection_fields_properties = "check_collection_fields_properties"
check_describe_index_property = "check_describe_index_property"
class BulkLoadStates:
BulkLoadPersisted = "BulkLoadPersisted"
BulkLoadFailed = "BulkLoadFailed"
BulkLoadDataQueryable = "BulkLoadDataQueryable"
BulkLoadDataIndexed = "BulkLoadDataIndexed"
class CaseLabel:
"""
Testcase Levels
CI Regression:
L0:
part of CI Regression
triggered by github commit
optional used for dev to verify his fix before submitting a PR(like smoke)
~100 testcases and run in 3 mins
L1:
part of CI Regression
triggered by github commit
must pass before merge
run in 15 mins
Benchmark:
L2:
E2E tests and bug-fix verification
Nightly run triggered by cron job
run in 60 mins
L3:
Stability/Performance/reliability, etc. special tests
Triggered by cron job or manually
run duration depends on test configuration
Loadbalance:
loadbalance testcases which need to be run in multi query nodes
ClusterOnly:
For functions only suitable to cluster mode
GPU:
For GPU supported cases
"""
L0 = "L0"
L1 = "L1"
L2 = "L2"
L3 = "L3"
RBAC = "RBAC"
Loadbalance = "Loadbalance" # loadbalance testcases which need to be run in multi query nodes
ClusterOnly = "ClusterOnly" # For functions only suitable to cluster mode
MultiQueryNodes = "MultiQueryNodes" # for 8 query nodes configs tests, such as resource group
GPU = "GPU"
CDC = "CDC"
+21
View File
@@ -0,0 +1,21 @@
# constants for old pymilvus API test
import utils.util_pymilvus as utils
default_fields = utils.gen_default_fields()
default_binary_fields = utils.gen_binary_default_fields()
default_entity = utils.gen_entities(1)
default_raw_binary_vector, default_binary_entity = utils.gen_binary_entities(1)
default_entity_row = utils.gen_entities_rows(1)
default_raw_binary_vector_row, default_binary_entity_row = utils.gen_binary_entities_rows(1)
default_entities = utils.gen_entities(utils.default_nb)
default_raw_binary_vectors, default_binary_entities = utils.gen_binary_entities(utils.default_nb)
default_entities_new = utils.gen_entities_new(utils.default_nb)
default_raw_binary_vectors_new, default_binary_entities_new = utils.gen_binary_entities_new(utils.default_nb)
default_entities_rows = utils.gen_entities_rows(utils.default_nb)
default_raw_binary_vectors_rows, default_binary_entities_rows = utils.gen_binary_entities_rows(utils.default_nb)
@@ -0,0 +1,106 @@
from __future__ import print_function
import os
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from utils.util_log import test_log as log
from common.common_type import in_cluster_env
_GROUP = 'milvus.io'
_VERSION = 'v1alpha1'
_NAMESPACE = "default"
class CustomResourceOperations(object):
def __init__(self, kind, group=_GROUP, version=_VERSION, namespace=_NAMESPACE):
self.group = group
self.version = version
self.namespace = namespace
if kind.lower()[-1] != "s":
self.plural = kind.lower() + "s"
else:
self.plural = kind.lower()
# init k8s client config
in_cluster = os.getenv(in_cluster_env, default='False')
log.debug(f"env variable IN_CLUSTER: {in_cluster}")
if in_cluster.lower() == 'true':
config.load_incluster_config()
else:
config.load_kube_config()
def create(self, body):
"""create or apply a custom resource in k8s"""
pretty = 'true'
api_instance = client.CustomObjectsApi()
try:
api_response = api_instance.create_namespaced_custom_object(self.group, self.version, self.namespace,
plural=self.plural, body=body, pretty=pretty)
log.info(f"create custom resource response: {api_response}")
except ApiException as e:
log.error("Exception when calling CustomObjectsApi->create_namespaced_custom_object: %s\n" % e)
raise Exception(str(e))
return api_response
def delete(self, metadata_name, raise_ex=True):
"""delete or uninstall a custom resource in k8s"""
print(metadata_name)
try:
api_instance = client.CustomObjectsApi()
api_response = api_instance.delete_namespaced_custom_object(self.group, self.version, self.namespace,
self.plural,
metadata_name)
log.info(f"delete custom resource response: {api_response}")
except ApiException as e:
if raise_ex:
log.error("Exception when calling CustomObjectsApi->delete_namespaced_custom_object: %s\n" % e)
raise Exception(str(e))
def patch(self, metadata_name, body):
"""patch a custom resource in k8s"""
api_instance = client.CustomObjectsApi()
try:
api_response = api_instance.patch_namespaced_custom_object(self.group, self.version, self.namespace,
plural=self.plural,
name=metadata_name,
body=body)
log.debug(f"patch custom resource response: {api_response}")
except ApiException as e:
log.error("Exception when calling CustomObjectsApi->patch_namespaced_custom_object: %s\n" % e)
raise Exception(str(e))
return api_response
def list_all(self):
"""list all the customer resources in k8s"""
pretty = 'true'
try:
api_instance = client.CustomObjectsApi()
api_response = api_instance.list_namespaced_custom_object(self.group, self.version, self.namespace,
plural=self.plural, pretty=pretty)
log.debug(f"list custom resource response: {api_response}")
except ApiException as e:
log.error("Exception when calling CustomObjectsApi->list_namespaced_custom_object: %s\n" % e)
raise Exception(str(e))
return api_response
def get(self, metadata_name):
"""get a customer resources by name in k8s"""
try:
api_instance = client.CustomObjectsApi()
api_response = api_instance.get_namespaced_custom_object(self.group, self.version,
self.namespace, self.plural,
name=metadata_name)
# log.debug(f"get custom resource response: {api_response}")
except ApiException as e:
log.error("Exception when calling CustomObjectsApi->get_namespaced_custom_object: %s\n" % e)
raise Exception(str(e))
return api_response
def delete_all(self):
"""delete all the customer resources in k8s"""
cus_objects = self.list_all()
if len(cus_objects["items"]) > 0:
for item in cus_objects["items"]:
metadata_name = item["metadata"]["name"]
self.delete(metadata_name)
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
import ujson
import json
from pymilvus.grpc_gen import milvus_pb2 as milvus_types
from pymilvus import connections
# from utils.util_log import test_log as log
sys_info_req = ujson.dumps({"metric_type": "system_info"})
sys_statistics_req = ujson.dumps({"metric_type": "system_statistics"})
sys_logs_req = ujson.dumps({"metric_type": "system_logs"})
class MilvusSys:
def __init__(self, alias='default'):
self.alias = alias
self.handler = connections._fetch_handler(alias=self.alias)
if self.handler is None:
raise Exception(f"Connection {alias} is disconnected or nonexistent")
# TODO: for now it only supports non_orm style API for getMetricsRequest
req = milvus_types.GetMetricsRequest(request=sys_info_req)
self.sys_info = self.handler._stub.GetMetrics(req, wait_for_ready=True, timeout=None)
# req = milvus_types.GetMetricsRequest(request=sys_statistics_req)
# self.sys_statistics = self.handler._stub.GetMetrics(req, wait_for_ready=True, timeout=None)
# req = milvus_types.GetMetricsRequest(request=sys_logs_req)
# self.sys_logs = self.handler._stub.GetMetrics(req, wait_for_ready=True, timeout=None)
self.sys_info = self.handler._stub.GetMetrics(req, wait_for_ready=True, timeout=60)
# log.debug(f"sys_info: {self.sys_info}")
def refresh(self):
req = milvus_types.GetMetricsRequest(request=sys_info_req)
self.sys_info = self.handler._stub.GetMetrics(req, wait_for_ready=True, timeout=None)
# req = milvus_types.GetMetricsRequest(request=sys_statistics_req)
# self.sys_statistics = self.handler._stub.GetMetrics(req, wait_for_ready=True, timeout=None)
# req = milvus_types.GetMetricsRequest(request=sys_logs_req)
# self.sys_logs = self.handler._stub.GetMetrics(req, wait_for_ready=True, timeout=None)
# log.debug(f"sys info response: {self.sys_info.response}")
@property
def system_version(self):
"""get the first node's build version as milvus build version"""
return self.nodes[0].get('infos').get('system_info').get('system_version')
@property
def build_version(self):
"""get the first node's build version as milvus build version"""
return self.nodes[0].get('infos').get('system_info').get('build_version')
@property
def build_time(self):
"""get the first node's build time as milvus build time"""
return self.nodes[0].get('infos').get('system_info').get('build_time')
@property
def deploy_mode(self):
"""get the first node's deploy_mode as milvus deploy_mode"""
return self.nodes[0].get('infos').get('system_info').get('deploy_mode')
@property
def simd_type(self):
"""
get simd type that milvus is running against
return the first query node's simd type
"""
for node in self.query_nodes:
return node.get('infos').get('system_configurations').get('simd_type')
raise Exception("No query node found")
@property
def query_nodes(self):
"""get all query nodes in Milvus deployment"""
query_nodes = []
for node in self.nodes:
if 'querynode' == node.get('infos').get('type'):
query_nodes.append(node)
return query_nodes
@property
def data_nodes(self):
"""get all data nodes in Milvus deployment"""
data_nodes = []
for node in self.nodes:
if 'datanode' == node.get('infos').get('type'):
data_nodes.append(node)
return data_nodes
@property
def proxy_nodes(self):
"""get all proxy nodes in Milvus deployment"""
proxy_nodes = []
for node in self.nodes:
if 'proxy' == node.get('infos').get('type'):
proxy_nodes.append(node)
return proxy_nodes
@property
def nodes(self):
"""get all the nodes in Milvus deployment"""
self.refresh()
all_nodes = json.loads(self.sys_info.response).get('nodes_info')
online_nodes = [node for node in all_nodes if node["infos"]["has_error"] is False]
return online_nodes
def get_nodes_by_type(self, node_type=None):
"""get milvus nodes by node type"""
target_nodes = []
if node_type is not None:
for node in self.nodes:
if str(node_type).lower() == str(node.get('infos').get('type')).lower():
target_nodes.append(node)
return target_nodes
if __name__ == '__main__':
uri = ""
token = ""
connections.connect(uri=uri, token=token)
ms = MilvusSys()
print(ms.build_version)
+43
View File
@@ -0,0 +1,43 @@
import os
from minio import Minio
from minio.error import S3Error
from utils.util_log import test_log as log
def copy_files_to_bucket(client, r_source, target_files, bucket_name, force=False):
# check the bucket exist
found = client.bucket_exists(bucket_name)
if not found:
log.error(f"Bucket {bucket_name} not found.")
return
# copy target files from root source folder
os.chdir(r_source)
for target_file in target_files:
found = False
try:
result = client.stat_object(bucket_name, target_file)
found = True
except S3Error as exc:
pass
if force or not found:
res = client.fput_object(bucket_name, target_file, f"{r_source}/{target_file}")
log.info(f"copied {res.object_name} to minio")
else:
log.info(f"skip copy {target_file} to minio")
def copy_files_to_minio(host, r_source, files, bucket_name, access_key="minioadmin", secret_key="minioadmin",
secure=False, force=False):
client = Minio(
host,
access_key=access_key,
secret_key=secret_key,
secure=secure,
)
try:
copy_files_to_bucket(client, r_source=r_source, target_files=files, bucket_name=bucket_name, force=force)
except S3Error as exc:
log.error("fail to copy files to minio", exc)
@@ -0,0 +1,408 @@
"""
Mock TEI (Text Embeddings Inference) Server for testing.
This module provides utilities to mock TEI API using pytest-httpserver.
It can be used to test scenarios where the embedding service becomes unavailable
after a collection function has been created.
TEI API Reference:
- POST /embed: Generate embeddings for input texts
- Request: {"inputs": ["text1", "text2"], "truncate": true, "truncation_direction": "Left"}
- Response: [[0.1, 0.2, ...], [0.3, 0.4, ...]]
Usage with pytest-httpserver (recommended):
@pytest.fixture
def mock_tei(httpserver):
return MockTEIHandler(httpserver, dim=768)
def test_example(mock_tei):
mock_tei.setup_embed()
endpoint = mock_tei.endpoint
# use endpoint...
# Simulate error
mock_tei.setup_error(503, "Service unavailable")
Usage with standalone server (for environments without pytest-httpserver):
server = MockTEIServer(dim=768)
server.start()
endpoint = server.endpoint
server.set_error_mode(True)
server.stop()
"""
import json
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Optional, Callable, Any
import socket
def generate_mock_embedding(text: str, dim: int) -> list:
"""Generate a deterministic mock embedding based on text content."""
hash_val = hash(text) & 0xFFFFFFFF
embedding = []
for i in range(dim):
val = ((hash_val * (i + 1)) % 10000) / 10000.0 * 2 - 1
embedding.append(round(val, 6))
return embedding
# =============================================================================
# pytest-httpserver based implementation (recommended)
# =============================================================================
class MockTEIHandler:
"""
TEI mock handler for pytest-httpserver.
This is the recommended way to mock TEI in pytest tests.
Example:
def test_with_tei(httpserver):
tei = MockTEIHandler(httpserver, dim=768)
tei.setup_embed()
# Your test code using tei.endpoint
...
# Simulate service failure
tei.setup_error(503, "Model integration is not active")
"""
def __init__(self, httpserver, dim: int = 768):
"""
Initialize TEI handler.
Args:
httpserver: pytest-httpserver's HTTPServer fixture
dim: Embedding dimension
"""
self.httpserver = httpserver
self.dim = dim
@property
def endpoint(self) -> str:
"""Get the server endpoint URL."""
return self.httpserver.url_for("")
def setup_embed(self):
"""Setup /embed endpoint to return mock embeddings."""
def handle_embed(request):
data = request.json
inputs = data.get("inputs", [])
embeddings = [generate_mock_embedding(text, self.dim) for text in inputs]
return json.dumps(embeddings)
self.httpserver.expect_request(
"/embed",
method="POST"
).respond_with_handler(handle_embed)
return self
def setup_error(self, status_code: int = 500, message: str = "Service unavailable"):
"""
Setup server to return errors for all requests.
Args:
status_code: HTTP status code
message: Error message
"""
self.httpserver.clear()
error_response = json.dumps({"error": message})
self.httpserver.expect_request(
"/embed",
method="POST"
).respond_with_data(
error_response,
status=status_code,
content_type="application/json"
)
return self
def setup_health(self):
"""Setup /health endpoint."""
self.httpserver.expect_request(
"/health",
method="GET"
).respond_with_json({"status": "ok"})
return self
def clear(self):
"""Clear all handlers."""
self.httpserver.clear()
return self
# =============================================================================
# Standalone server implementation (fallback for environments without pytest-httpserver)
# =============================================================================
def create_handler_class(server_state: dict):
"""Create a handler class with instance-specific state."""
class _StandaloneHandler(BaseHTTPRequestHandler):
"""HTTP request handler for standalone mock TEI server."""
def log_message(self, format, *args):
pass
def _send_json(self, data, status: int = 200):
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode('utf-8'))
def do_POST(self):
if server_state.get('error_mode', False):
self._send_json(
{"error": server_state.get('error_message', 'Service unavailable')},
server_state.get('error_status_code', 500)
)
return
if self.path == '/embed':
content_length = int(self.headers.get('Content-Length', 0))
body = json.loads(self.rfile.read(content_length).decode('utf-8'))
inputs = body.get('inputs', [])
dim = server_state.get('dim', 768)
embeddings = [generate_mock_embedding(text, dim) for text in inputs]
self._send_json(embeddings)
else:
self._send_json({"error": "Not found"}, 404)
def do_GET(self):
if server_state.get('error_mode', False):
self._send_json(
{"error": server_state.get('error_message', 'Service unavailable')},
server_state.get('error_status_code', 500)
)
return
if self.path == '/health':
self._send_json({"status": "ok"})
else:
self._send_json({"error": "Not found"}, 404)
return _StandaloneHandler
def get_local_ip() -> str:
"""
Get the local IP address that can be accessed from external hosts.
Returns the first non-loopback IPv4 address.
"""
# Method 1: get IP from socket connection to external host
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
s.close()
if not ip.startswith('127.'):
return ip
except (OSError, socket.error):
# Socket connection failed, try next method
pass
# Method 2: get from hostname
try:
hostname = socket.gethostname()
ip = socket.gethostbyname(hostname)
if not ip.startswith('127.'):
return ip
except (OSError, socket.error, socket.gaierror):
# Hostname resolution failed, fall back to localhost
pass
return '127.0.0.1'
def get_docker_host() -> str:
"""
Get the hostname that Docker containers can use to access the host machine.
- macOS/Windows Docker Desktop: host.docker.internal
- Linux: returns the host's IP address (containers need --add-host or host network)
"""
import platform
system = platform.system().lower()
if system in ('darwin', 'windows'):
# Docker Desktop provides this special DNS name
return 'host.docker.internal'
else:
# Linux: use host IP
return get_local_ip()
class MockTEIServer:
"""
Standalone mock TEI server.
Use this when pytest-httpserver is not available.
For pytest tests, prefer using MockTEIHandler with httpserver fixture.
Example:
with MockTEIServer(dim=768) as server:
endpoint = server.endpoint
# use endpoint...
server.set_error_mode(True, 503, "Service unavailable")
For remote Milvus access, use external_host parameter:
# Auto-detect external IP
server = MockTEIServer(dim=768, host='0.0.0.0', external_host='auto')
# For Docker container access (macOS/Windows)
server = MockTEIServer(dim=768, host='0.0.0.0', external_host='docker')
# Or specify explicit IP
server = MockTEIServer(dim=768, host='0.0.0.0', external_host='192.168.1.100')
"""
def __init__(self, port: int = 0, dim: int = 768, host: str = '127.0.0.1', external_host: str = None):
self.host = host
self.port = port
self.dim = dim
self._external_host = external_host
self._server: Optional[HTTPServer] = None
self._thread: Optional[threading.Thread] = None
self._running = False
# Instance-specific state (not shared between servers)
self._state = {
'dim': dim,
'error_mode': False,
'error_status_code': 500,
'error_message': 'Service unavailable'
}
@property
def endpoint(self) -> str:
if self._server is None:
raise RuntimeError("Server not started")
# Use external_host for endpoint URL if specified
if self._external_host:
if self._external_host == 'auto':
host = get_local_ip()
elif self._external_host == 'docker':
host = get_docker_host()
else:
host = self._external_host
else:
host = self.host
return f"http://{host}:{self._server.server_address[1]}"
def start(self) -> str:
if self._running:
return self.endpoint
# Create handler class with instance-specific state
handler_class = create_handler_class(self._state)
self._server = HTTPServer((self.host, self.port), handler_class)
self.port = self._server.server_address[1]
self._thread = threading.Thread(target=self._server.serve_forever)
self._thread.daemon = True
self._thread.start()
self._running = True
self._wait_for_server()
return self.endpoint
def _wait_for_server(self, timeout: float = 5.0):
start = time.time()
while time.time() - start < timeout:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
if sock.connect_ex((self.host, self.port)) == 0:
sock.close()
return
sock.close()
except (OSError, socket.error):
# Connection not ready yet, retry
pass
time.sleep(0.1)
raise RuntimeError(f"Server failed to start within {timeout}s")
def stop(self):
if self._server:
self._server.shutdown()
self._server.server_close()
self._server = None
if self._thread:
self._thread.join(timeout=10)
self._thread = None
self._running = False
def set_error_mode(self, enabled: bool, status_code: int = 500, message: str = "Service unavailable"):
self._state['error_mode'] = enabled
self._state['error_status_code'] = status_code
self._state['error_message'] = message
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
return False
# =============================================================================
# Pytest fixtures
# =============================================================================
def pytest_httpserver_fixture(dim: int = 768):
"""
Create a pytest fixture for MockTEIHandler.
Usage in conftest.py:
from common.mock_tei_server import pytest_httpserver_fixture
@pytest.fixture
def mock_tei(httpserver):
handler = MockTEIHandler(httpserver, dim=768)
handler.setup_embed()
yield handler
"""
def fixture(httpserver):
handler = MockTEIHandler(httpserver, dim=dim)
handler.setup_embed()
yield handler
return fixture
if __name__ == '__main__':
import urllib.request
import urllib.error
print("Testing standalone MockTEIServer...")
with MockTEIServer(port=8080, dim=768) as server:
print(f"Server: {server.endpoint}")
# Test embed
req = urllib.request.Request(
f"{server.endpoint}/embed",
data=json.dumps({"inputs": ["Hello", "World"]}).encode(),
headers={'Content-Type': 'application/json'}
)
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(f"Embed: {len(result)} vectors, dim={len(result[0])}")
# Test error mode
server.set_error_mode(True, 503, "Model integration is not active")
try:
urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
print(f"Error mode: {e.code} - {json.loads(e.read())}")
print("Done!")
@@ -0,0 +1,374 @@
import random
import re
import numpy as np
import rjieba
from faker import Faker
from tantivy import Document, Index, Query, SchemaBuilder
class PhraseMatchTestGenerator:
def __init__(self, language="en"):
"""
Initialize the test data generator
Args:
language: Language for text generation ('en' for English, 'zh' for Chinese)
"""
self.language = language
self.index = None
self.documents = []
# English vocabulary
self.en_activities = [
"swimming",
"football",
"basketball",
"tennis",
"volleyball",
"baseball",
"golf",
"rugby",
"cricket",
"boxing",
"running",
"cycling",
"skating",
"skiing",
"surfing",
"diving",
"climbing",
"yoga",
"dancing",
"hiking",
]
self.en_verbs = [
"love",
"like",
"enjoy",
"play",
"practice",
"prefer",
"do",
"learn",
"teach",
"watch",
"start",
"begin",
"continue",
"finish",
"master",
"try",
]
self.en_connectors = [
"and",
"or",
"but",
"while",
"after",
"before",
"then",
"also",
"plus",
"with",
]
self.en_modifiers = [
"very much",
"a lot",
"seriously",
"casually",
"professionally",
"regularly",
"often",
"sometimes",
"daily",
"weekly",
]
# Chinese vocabulary
self.zh_activities = [
"游泳",
"足球",
"篮球",
"网球",
"排球",
"棒球",
"高尔夫",
"橄榄球",
"板球",
"拳击",
"跑步",
"骑行",
"滑冰",
"滑雪",
"冲浪",
"潜水",
"攀岩",
"瑜伽",
"跳舞",
"徒步",
]
self.zh_verbs = [
"喜欢",
"热爱",
"享受",
"",
"练习",
"偏好",
"",
"学习",
"",
"观看",
"开始",
"开启",
"继续",
"完成",
"掌握",
"尝试",
]
self.zh_connectors = [
"",
"或者",
"但是",
"同时",
"之后",
"之前",
"然后",
"",
"加上",
"",
]
self.zh_modifiers = [
"非常",
"很多",
"认真地",
"随意地",
"专业地",
"定期地",
"经常",
"有时候",
"每天",
"每周",
]
# Set vocabulary based on language
self.activities = self.zh_activities if language == "zh" else self.en_activities
self.verbs = self.zh_verbs if language == "zh" else self.en_verbs
self.connectors = self.zh_connectors if language == "zh" else self.en_connectors
self.modifiers = self.zh_modifiers if language == "zh" else self.en_modifiers
def tokenize_text(self, text: str) -> list[str]:
"""Tokenize text using jieba tokenizer"""
text = text.strip()
text = re.sub(r"[^\w\s]", " ", text)
text = text.replace("\n", " ")
if self.language == "zh":
text = text.replace(" ", "")
return list(rjieba.cut_for_search(text))
else:
return list(text.split())
def generate_embedding(self, dim: int) -> list[float]:
"""Generate random embedding vector"""
return list(np.random.random(dim))
def generate_text_pattern(self) -> str:
"""Generate test document text with various patterns"""
patterns = [
# Simple pattern with two activities
lambda: f"{random.choice(self.activities)} {random.choice(self.activities)}",
# Pattern with connector between activities
lambda: (
f"{random.choice(self.activities)} {random.choice(self.connectors)} {random.choice(self.activities)}"
),
# Pattern with modifier between activities
lambda: (
f"{random.choice(self.activities)} {random.choice(self.modifiers)} {random.choice(self.activities)}"
),
# Complex pattern with verb and activities
lambda: f"{random.choice(self.verbs)} {random.choice(self.activities)} {random.choice(self.activities)}",
# Pattern with multiple gaps
lambda: (
f"{random.choice(self.activities)} {random.choice(self.modifiers)} {random.choice(self.connectors)} {random.choice(self.activities)}"
),
]
return random.choice(patterns)()
def generate_test_data(self, num_documents: int, dim: int) -> list[dict]:
"""
Generate test documents with text and embeddings
Args:
num_documents: Number of documents to generate
dim: Dimension of embedding vectors
Returns:
List of dictionaries containing document data
"""
# Generate documents
self.documents = []
for i in range(num_documents):
self.documents.append(
{
"id": i,
"text": self.generate_text_pattern()
if self.language == "en"
else self.generate_text_pattern().replace(" ", ""),
"emb": self.generate_embedding(dim),
}
)
# Initialize Tantivy index
schema_builder = SchemaBuilder()
schema_builder.add_text_field("text", stored=True)
schema_builder.add_unsigned_field("doc_id", stored=True)
schema = schema_builder.build()
self.index = Index(schema=schema, path=None)
writer = self.index.writer()
# Index all documents
for doc in self.documents:
document = Document()
new_text = " ".join(self.tokenize_text(doc["text"]))
document.add_text("text", new_text)
document.add_unsigned("doc_id", doc["id"])
writer.add_document(document)
writer.commit()
self.index.reload()
return self.documents
def _generate_random_word(self, exclude_words: list[str]) -> str:
"""
Generate a random word that is not in the exclude_words list using Faker
"""
fake = Faker()
while True:
word = fake.word()
if word not in exclude_words:
return word
def generate_pattern_documents(self, patterns: list[tuple], dim: int, num_docs_per_pattern: int = 1) -> list[dict]:
"""
Generate documents that match specific test patterns with their corresponding slop values
Args:
patterns: List of tuples containing (pattern, slop) pairs
dim: Dimension of embedding vectors
num_docs_per_pattern: Number of documents to generate for each pattern
Returns:
List of dictionaries containing document data with text and embeddings
"""
pattern_documents = []
for pattern, slop in patterns:
# Split pattern into components
pattern_words = pattern.split()
# Generate multiple documents for each pattern
if slop == 0: # Exact phrase
text = " ".join(pattern_words)
pattern_documents.append(
{"id": random.randint(0, 1000000), "text": text, "emb": self.generate_embedding(dim)}
)
else: # Pattern with gaps
# Generate slop number of unique words
insert_words = []
for _ in range(slop):
new_word = self._generate_random_word(pattern_words + insert_words)
insert_words.append(new_word)
# Insert the words randomly between the pattern words
all_words = pattern_words.copy()
for word in insert_words:
# Random position between pattern words
pos = random.randint(1, len(all_words))
all_words.insert(pos, word)
text = " ".join(all_words)
pattern_documents.append(
{"id": random.randint(0, 1000000), "text": text, "emb": self.generate_embedding(dim)}
)
new_pattern_documents = []
start = 1000000
for i in range(num_docs_per_pattern):
for doc in pattern_documents:
new_doc = dict(doc)
new_doc["id"] = start + len(new_pattern_documents)
new_pattern_documents.append(new_doc)
return new_pattern_documents
def generate_test_queries(self, num_queries: int) -> list[dict]:
"""
Generate test queries with varying slop values
Args:
num_queries: Number of queries to generate
Returns:
List of dictionaries containing query information
"""
queries = []
slop_values = [0, 1, 2, 3] # Common slop values
for i in range(num_queries):
# Randomly select two or three words for the query
num_words = random.choice([2, 3])
words = random.sample(self.activities, num_words)
queries.append(
{
"id": i,
"query": " ".join(words) if self.language == "en" else "".join(words),
"slop": random.choice(slop_values),
"type": f"{num_words}_words",
}
)
return queries
def get_query_results(self, query: str, slop: int) -> list[dict]:
"""
Get all documents that match the phrase query
Args:
query: Query phrase
slop: Maximum allowed word gap
Returns:
List[Dict]: List of matching documents with their ids and texts
"""
if self.index is None:
raise RuntimeError("No documents indexed. Call generate_test_data first.")
# Clean and normalize query
query_terms = self.tokenize_text(query)
# Create phrase query
searcher = self.index.searcher()
phrase_query = Query.phrase_query(self.index.schema, "text", query_terms, slop)
# Search for matches
results = searcher.search(phrase_query, limit=len(self.documents))
# Extract all matching documents
matched_docs = []
for _, doc_address in results.hits:
doc = searcher.doc(doc_address)
doc_id = doc.to_dict()["doc_id"]
matched_docs.extend(doc_id)
return matched_docs
@@ -0,0 +1,179 @@
from faker import Faker
import random
class ICUTextGenerator:
"""
ICU(International Components for Unicode)TextGenerator:
Generate test sentences containing multiple languages (Chinese, English, Japanese, Korean), emojis, and special symbols.
"""
def __init__(self):
self.fake_en = Faker("en_US")
self.fake_zh = Faker("zh_CN")
self.fake_ja = Faker("ja_JP")
self.fake_de = Faker("de_DE")
self.korean_samples = [
"안녕하세요 세계", "파이썬 프로그래밍", "데이터 분석", "인공지능",
"밀버스 테스트", "한국어 샘플", "자연어 처리"
]
self.emojis = ["😊", "🐍", "🚀", "🌏", "💡", "🔥", "", "👍"]
self.specials = ["#", "@", "$"]
def word(self):
"""
Generate a list of words containing multiple languages, emojis, and special symbols.
"""
parts = [
self.fake_en.word(),
self.fake_zh.word(),
self.fake_ja.word(),
self.fake_de.word(),
random.choice(self.korean_samples),
random.choice(self.emojis),
random.choice(self.specials),
]
return random.choice(parts)
def sentence(self):
"""
Generate a sentence containing multiple languages, emojis, and special symbols.
"""
parts = [
self.fake_en.sentence(),
self.fake_zh.sentence(),
self.fake_ja.sentence(),
self.fake_de.sentence(),
random.choice(self.korean_samples),
" ".join(random.sample(self.emojis, 2)),
" ".join(random.sample(self.specials, 2)),
]
random.shuffle(parts)
return " ".join(parts)
def paragraph(self, num_sentences=3):
"""
Generate a paragraph containing multiple sentences, each with multiple languages, emojis, and special symbols.
"""
return ' '.join([self.sentence() for _ in range(num_sentences)])
def text(self, num_sentences=5):
"""
Generate multiple sentences containing multiple languages, emojis, and special symbols.
"""
return ' '.join([self.sentence() for _ in range(num_sentences)])
class KoreanTextGenerator:
"""
KoreanTextGenerator: Generate test sentences containing Korean activities, verbs, connectors, and modifiers.
"""
def __init__(self):
# Sports/Activities (Nouns)
self.activities = [
"수영", "축구", "농구", "테니스",
"배구", "야구", "골프", "럭비",
"달리기", "자전거", "스케이트", "스키",
"서핑", "다이빙", "등산", "요가",
"", "하이킹", "독서", "요리"
]
# Verbs (Base Form)
self.verbs = [
"좋아하다", "즐기다", "하다", "배우다",
"가르치다", "보다", "시작하다", "계속하다",
"연습하다", "선호하다", "마스터하다", "도전하다"
]
# Connectors
self.connectors = [
"그리고", "또는", "하지만", "그런데",
"그래서", "또한", "게다가", "그러면서",
"동시에", "함께"
]
# Modifiers (Frequency/Degree)
self.modifiers = [
"매우", "자주", "가끔", "열심히",
"전문적으로", "규칙적으로", "매일", "일주일에 한 번",
"취미로", "진지하게"
]
def conjugate_verb(self, verb):
# Simple Korean verb conjugation (using informal style "-아/어요")
if verb.endswith("하다"):
return verb.replace("하다", "해요")
elif verb.endswith(""):
return verb[:-1] + "아요"
return verb
def word(self):
return random.choice(self.activities + self.verbs + self.modifiers + self.connectors)
def sentence(self):
# Build basic sentence structure
activity = random.choice(self.activities)
verb = random.choice(self.verbs)
modifier = random.choice(self.modifiers)
# Conjugate verb
conjugated_verb = self.conjugate_verb(verb)
# Build sentence (Korean word order: Subject + Object + Modifier + Verb)
sentence = f"저는 {activity}를/을 {modifier} {conjugated_verb}"
# Randomly add connector and another activity
if random.choice([True, False]):
connector = random.choice(self.connectors)
second_activity = random.choice(self.activities)
second_verb = self.conjugate_verb(random.choice(self.verbs))
sentence += f" {connector} {second_activity}{second_verb}"
return sentence + "."
def paragraph(self, num_sentences=3):
return '\n'.join([self.sentence() for _ in range(num_sentences)])
def text(self, num_sentences=5):
return '\n'.join([self.sentence() for _ in range(num_sentences)])
def generate_text_by_analyzer(analyzer_params):
"""
Generate text data based on the given analyzer parameters
Args:
analyzer_params: Dictionary containing the analyzer parameters
Returns:
str: Generated text data
"""
if analyzer_params["tokenizer"] == "standard":
fake = Faker("en_US")
elif analyzer_params["tokenizer"] == "jieba":
fake = Faker("zh_CN")
elif analyzer_params["tokenizer"] == "icu":
fake = ICUTextGenerator()
elif analyzer_params["tokenizer"]["type"] == "lindera":
# Generate random Japanese text
if analyzer_params["tokenizer"]["dict_kind"] == "ipadic":
fake = Faker("ja_JP")
elif analyzer_params["tokenizer"]["dict_kind"] == "ko-dic":
fake = KoreanTextGenerator()
elif analyzer_params["tokenizer"]["dict_kind"] == "cc-cedict":
fake = Faker("zh_CN")
else:
raise ValueError("Invalid dict_kind")
else:
raise ValueError("Invalid analyzer parameters")
text = fake.text()
stop_words = []
if "filter" in analyzer_params:
for filter in analyzer_params["filter"]:
if filter["type"] == "stop":
stop_words.extend(filter["stop_words"])
# add stop words to the text
text += " " + " ".join(stop_words)
return text