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
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:
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
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user