mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-31 07:00:56 -05:00
367 lines
11 KiB
Python
Executable File
367 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import random
|
|
import uuid
|
|
import datetime
|
|
from typing import List, Optional
|
|
|
|
import requests
|
|
|
|
QDRANT_HOST = os.environ.get("QDRANT_HOST", "localhost:6333")
|
|
|
|
POINTS_COUNT = 1000
|
|
DENSE_DIM = 256
|
|
MULTI_DENSE_DIM = 128
|
|
|
|
|
|
def drop_collection(name: str):
|
|
# cleanup collection if it exists
|
|
requests.delete(f"http://{QDRANT_HOST}/collections/{name}")
|
|
|
|
|
|
def create_collection(name: str, memmap_threshold_kb: int, on_disk: bool, datatype: str, quantization_config: Optional[dict] = None):
|
|
# create collection with a lower `indexing_threshold_kb` to generate the HNSW index
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}",
|
|
headers={"Content-Type": "application/json"},
|
|
json={
|
|
"vectors": {
|
|
"image": {
|
|
"size": DENSE_DIM,
|
|
"distance": "Dot",
|
|
"on_disk": on_disk,
|
|
"datatype": datatype,
|
|
},
|
|
"multi-image": {
|
|
"size": MULTI_DENSE_DIM,
|
|
"distance": "Dot",
|
|
"on_disk": on_disk,
|
|
"datatype": datatype,
|
|
"multivector_config": {
|
|
"comparator": "max_sim"
|
|
}
|
|
}
|
|
},
|
|
"sparse_vectors": {
|
|
"text": {
|
|
"index": {
|
|
"on_disk": on_disk,
|
|
"datatype": datatype,
|
|
}
|
|
}
|
|
},
|
|
"optimizers_config": {
|
|
"default_segment_number": 2,
|
|
"indexing_threshold_kb": 10,
|
|
"memmap_threshold_kb": memmap_threshold_kb,
|
|
|
|
},
|
|
"quantization_config": quantization_config,
|
|
"on_disk_payload": on_disk,
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
|
|
def create_payload_indexes(name: str, on_disk_payload_index: bool):
|
|
# keyword
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "keyword_field",
|
|
"field_schema": {
|
|
"type": "keyword",
|
|
"on_disk": on_disk_payload_index
|
|
}
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
# float
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "float_field",
|
|
"field_schema": {
|
|
"type": "float",
|
|
"on_disk": on_disk_payload_index
|
|
}
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
# integer
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "integer_field",
|
|
"field_schema": {
|
|
"type": "integer",
|
|
"on_disk": on_disk_payload_index,
|
|
"lookup": True,
|
|
"range": True
|
|
}
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
# boolean
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "boolean_field",
|
|
"field_schema": {
|
|
"type": "bool",
|
|
"on_disk": on_disk_payload_index
|
|
}
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
# geo
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "geo_field",
|
|
"field_schema": {
|
|
"type": "geo",
|
|
"on_disk": on_disk_payload_index
|
|
}
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
# text
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "text_field",
|
|
"field_schema": {
|
|
"type": "text",
|
|
"tokenizer": "word",
|
|
"min_token_len": 2,
|
|
"max_token_len": 20,
|
|
"lowercase": True,
|
|
"on_disk": on_disk_payload_index,
|
|
},
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
# uuid
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "uuid_field",
|
|
"field_schema": {
|
|
"type": "uuid",
|
|
"on_disk": on_disk_payload_index
|
|
}
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
# datetime
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/index",
|
|
json={
|
|
"field_name": "datetime_field",
|
|
"field_schema": {
|
|
"type": "datetime",
|
|
"on_disk": on_disk_payload_index
|
|
}
|
|
},
|
|
)
|
|
assert response.ok
|
|
|
|
|
|
def rand_dense_vec(dims: int = DENSE_DIM):
|
|
return [(random.random() * 20) - 10 for _ in range(dims)]
|
|
|
|
|
|
# Create multiple dense vectors
|
|
def random_multi_dense_vec(dims: int = MULTI_DENSE_DIM):
|
|
return [rand_dense_vec(dims) for _ in range(3)]
|
|
|
|
|
|
# Generate random sparse vector with given size and density
|
|
# The density is the probability of non-zero value over the whole vector
|
|
def rand_sparse_vec(size: int = 1000, density: float = 0.1):
|
|
num_non_zero = int(size * density)
|
|
indices: List[int] = random.sample(range(size), num_non_zero)
|
|
values: List[float] = [round(random.random(), 6) for _ in range(num_non_zero)]
|
|
sparse = {
|
|
"indices": indices,
|
|
"values": values,
|
|
}
|
|
return sparse
|
|
|
|
|
|
def rand_string():
|
|
return random.choice(["hello", "world", "foo", "bar"])
|
|
|
|
|
|
def rand_int():
|
|
return random.randint(0, 100)
|
|
|
|
|
|
def rand_bool():
|
|
return random.random() < 0.5
|
|
|
|
|
|
def rand_text():
|
|
return " ".join([rand_string() for _ in range(10)])
|
|
|
|
|
|
def rand_geo():
|
|
return {
|
|
"lat": random.random(),
|
|
"lon": random.random(),
|
|
}
|
|
|
|
def rand_uuid():
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
def rand_datetime():
|
|
return str(datetime.datetime.now())
|
|
|
|
|
|
def single_or_multi_value(generator):
|
|
if random.random() < 0.5:
|
|
return generator()
|
|
else:
|
|
return [generator() for _ in range(random.randint(1, 3))]
|
|
|
|
|
|
def rand_point(num: int, use_uuid: bool):
|
|
point_id = None
|
|
if use_uuid:
|
|
point_id = str(uuid.uuid1())
|
|
else:
|
|
point_id = num
|
|
|
|
# draw [0, 1)
|
|
vec_draw = random.random()
|
|
vec = {}
|
|
if vec_draw < 0.2:
|
|
# just a dense vector
|
|
vec = {"image": rand_dense_vec()}
|
|
elif vec_draw < 0.4:
|
|
# just a multi dense vector
|
|
vec = {"multi-image": random_multi_dense_vec()}
|
|
elif vec_draw < 0.6:
|
|
# just a sparse vector
|
|
vec = {"text": rand_sparse_vec()}
|
|
else:
|
|
# else mixed vector
|
|
vec = {
|
|
"image": rand_dense_vec(),
|
|
"text": rand_sparse_vec(),
|
|
"multi-image": random_multi_dense_vec(),
|
|
}
|
|
|
|
payload = {}
|
|
if random.random() < 0.5:
|
|
payload["keyword_field"] = single_or_multi_value(rand_string)
|
|
|
|
if random.random() < 0.5:
|
|
payload["count_field"] = single_or_multi_value(rand_int)
|
|
|
|
if random.random() < 0.5:
|
|
payload["float_field"] = single_or_multi_value(random.random)
|
|
|
|
if random.random() < 0.5:
|
|
payload["integer_field"] = single_or_multi_value(rand_int)
|
|
|
|
if random.random() < 0.5:
|
|
payload["boolean_field"] = single_or_multi_value(rand_bool)
|
|
|
|
if random.random() < 0.5:
|
|
payload["geo_field"] = single_or_multi_value(rand_geo)
|
|
|
|
if random.random() < 0.5:
|
|
payload["text_field"] = single_or_multi_value(rand_text)
|
|
|
|
if random.random() < 0.5:
|
|
payload["uuid_field"] = single_or_multi_value(rand_uuid)
|
|
|
|
if random.random() < 0.5:
|
|
payload["datetime_field"] = single_or_multi_value(rand_datetime)
|
|
|
|
point = {
|
|
"id": point_id,
|
|
"vector": vec,
|
|
"payload": payload,
|
|
}
|
|
return point
|
|
|
|
|
|
def upload_points(name: str):
|
|
random.seed(42)
|
|
|
|
points = []
|
|
for i in range(POINTS_COUNT):
|
|
# Use uuid as id for half of the points
|
|
use_uuid = i > POINTS_COUNT / 2
|
|
point = rand_point(i, use_uuid)
|
|
points.append(point)
|
|
|
|
response = requests.put(
|
|
f"http://{QDRANT_HOST}/collections/{name}/points?wait=true",
|
|
headers={"Content-Type": "application/json"},
|
|
json={
|
|
"points": points,
|
|
},
|
|
)
|
|
|
|
assert response.ok
|
|
|
|
|
|
def basic_retrieve(name: str):
|
|
response = requests.get(
|
|
f"http://{QDRANT_HOST}/collections/{name}/points/2",
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
assert response.ok
|
|
|
|
response = requests.post(
|
|
f"http://{QDRANT_HOST}/collections/{name}/points",
|
|
headers={"Content-Type": "application/json"},
|
|
json={"ids": [1, 2]},
|
|
)
|
|
assert response.ok
|
|
|
|
|
|
# Populate collection with different configurations
|
|
#
|
|
# There are two ways to configure the usage of memmap storage:
|
|
# - `memmap_threshold_kb` - the threshold for the indexer to use memmap storage
|
|
# - `on_disk` - to store vectors immediately on disk
|
|
def populate_collection(name: str, on_disk: bool, quantization_config: Optional[dict] = None, memmap_threshold: bool = False, on_disk_payload_index: bool = False, datatype: str = "float32"):
|
|
drop_collection(name)
|
|
memmap_threshold_kb = 0
|
|
if memmap_threshold:
|
|
memmap_threshold_kb = 10 # low value to force transition to memmap storage
|
|
create_collection(name, memmap_threshold_kb, on_disk, datatype, quantization_config)
|
|
create_payload_indexes(name, on_disk_payload_index)
|
|
upload_points(name)
|
|
basic_retrieve(name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Create collection
|
|
populate_collection("test_collection_vector_memory", on_disk=False)
|
|
populate_collection("test_collection_vector_on_disk", on_disk=True)
|
|
populate_collection("test_collection_vector_on_disk_threshold", on_disk=False, memmap_threshold=True)
|
|
populate_collection("test_collection_scalar_int8", on_disk=False, quantization_config={"scalar": {"type": "int8"}})
|
|
populate_collection("test_collection_product_x64", on_disk=False, quantization_config={"product": {"compression": "x64"}})
|
|
populate_collection("test_collection_product_x32", on_disk=False, quantization_config={"product": {"compression": "x32"}})
|
|
populate_collection("test_collection_product_x16", on_disk=False, quantization_config={"product": {"compression": "x16"}})
|
|
populate_collection("test_collection_product_x8", on_disk=False, quantization_config={"product": {"compression": "x8"}})
|
|
populate_collection("test_collection_binary", on_disk=False, quantization_config={"binary": {"always_ram": True}})
|
|
populate_collection("test_collection_mmap_field_index", on_disk=True, on_disk_payload_index=True)
|
|
populate_collection("test_collection_vector_datatype_u8", on_disk=True, datatype="uint8")
|
|
populate_collection("test_collection_vector_datatype_f16", on_disk=True, datatype="float16")
|