Skip to content

Commit 4aea36f

Browse files
geetu040joein
andcommitted
feat: Expose Setting for GRPC Channel-Level Compression at Client Side (#480)
* expose grpc channel-level compression settings in base functions * expose grpc channel-level compression settings in remote classes * expose grpc channel-level compression settings in client * raise TypeError for compression * added test cases for grcp channel-level compression * move grpc_compression parameter from client's signature to **kwargs * use grpc.Compression instead of creating new enum qdrant.grpc.Compression in qdrant/grpc/__init__.py * refactor grpc_compression type hint * fix: Compression instead of grpc.Compression in type hint * tests: move and update tests * chore: remove magic method * fix: fix async client generator, update precommit dependencies * fix: update isort options * fix: update dev dependencies --------- Co-authored-by: George Panchuk <[email protected]>
1 parent 90586d2 commit 4aea36f

File tree

8 files changed

+503
-454
lines changed

8 files changed

+503
-454
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ repos:
1414
- id: check-added-large-files
1515

1616
- repo: https://github.com/psf/black
17-
rev: 23.1.0
17+
rev: 23.12.1
1818
hooks:
1919
- id: black
2020
name: "Black: The uncompromising Python code formatter"
2121
args: ["--line-length", "99"]
2222

2323
- repo: https://github.com/PyCQA/isort
24-
rev: 5.12.0
24+
rev: 5.13.2
2525
hooks:
2626
- id: isort
2727
name: "Sort Imports"
28-
args: ["--profile", "black"]
28+
args: ["--profile", "black", "--py", "310"]

poetry.lock

Lines changed: 440 additions & 440 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ coverage = "^6.3.3"
4141
pytest-asyncio = "^0.21.0"
4242
pytest-timeout = "^2.1.0"
4343
autoflake = "^2.2.1"
44-
isort = "^5.12.0"
45-
black = "^23.9.1"
44+
isort = "^5.13.0"
45+
black = "^23.12.1"
4646

4747
[tool.poetry.group.docs.dependencies]
4848
sphinx = "^4.5.0"
@@ -71,3 +71,6 @@ markers = [
7171
"fastembed: marks tests that require the fastembed package (deselect with '-m \"not fastembed\"')",
7272
"no_fastembed: marks tests that do not require the fastembed package (deselect with '-m \"not no_fastembed\"')"
7373
]
74+
75+
[tool.isort]
76+
known_third_party = "grpc"

qdrant_client/async_qdrant_remote.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import httpx
3131
import numpy as np
32+
from grpc import Compression
3233
from urllib3.util import Url, parse_url
3334

3435
from qdrant_client import grpc as grpc
@@ -111,6 +112,16 @@ def __init__(
111112
warnings.warn("Api key is used with unsecure connection.")
112113
self._rest_headers["api-key"] = api_key
113114
self._grpc_headers.append(("api-key", api_key))
115+
grpc_compression: Optional[Compression] = kwargs.pop("grpc_compression", None)
116+
if grpc_compression is not None and (not isinstance(grpc_compression, Compression)):
117+
raise TypeError(
118+
f"Expected 'grpc_compression' to be of type grpc.Compression or None, but got {type(grpc_compression)}"
119+
)
120+
if grpc_compression == Compression.Deflate:
121+
raise ValueError(
122+
"grpc.Compression.Deflate is not supported. Try grpc.Compression.Gzip or grpc.Compression.NoCompression"
123+
)
124+
self._grpc_compression = grpc_compression
114125
address = f"{self._host}:{self._port}" if self._port is not None else self._host
115126
self.rest_uri = f"{self._scheme}://{address}{self._prefix}"
116127
self._rest_args = {"headers": self._rest_headers, "http2": http2, **kwargs}
@@ -170,6 +181,7 @@ def _init_grpc_channel(self) -> None:
170181
ssl=self._https,
171182
metadata=self._grpc_headers,
172183
options=self._grpc_options,
184+
compression=self._grpc_compression,
173185
)
174186

175187
def _init_grpc_points_client(self) -> None:

qdrant_client/connection.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def get_channel(
199199
ssl: bool,
200200
metadata: Optional[List[Tuple[str, str]]] = None,
201201
options: Optional[Dict[str, Any]] = None,
202+
compression: Optional[grpc.Compression] = None,
202203
) -> grpc.Channel:
203204
# gRPC client options
204205
_options = parse_channel_options(options)
@@ -223,14 +224,14 @@ def metadata_callback(context: Any, callback: Any) -> None:
223224
creds = grpc.ssl_channel_credentials()
224225

225226
# finally pass in the combined credentials when creating a channel
226-
return grpc.secure_channel(f"{host}:{port}", creds, _options)
227+
return grpc.secure_channel(f"{host}:{port}", creds, _options, compression)
227228
else:
228229
if metadata:
229230
metadata_interceptor = header_adder_interceptor(metadata)
230-
channel = grpc.insecure_channel(f"{host}:{port}", _options)
231+
channel = grpc.insecure_channel(f"{host}:{port}", _options, compression)
231232
return grpc.intercept_channel(channel, metadata_interceptor)
232233
else:
233-
return grpc.insecure_channel(f"{host}:{port}", _options)
234+
return grpc.insecure_channel(f"{host}:{port}", _options, compression)
234235

235236

236237
def get_async_channel(
@@ -239,6 +240,7 @@ def get_async_channel(
239240
ssl: bool,
240241
metadata: Optional[List[Tuple[str, str]]] = None,
241242
options: Optional[Dict[str, Any]] = None,
243+
compression: Optional[grpc.Compression] = None,
242244
) -> grpc.aio.Channel:
243245
# gRPC client options
244246
_options = parse_channel_options(options)
@@ -263,12 +265,12 @@ def metadata_callback(context: Any, callback: Any) -> None:
263265
creds = grpc.ssl_channel_credentials()
264266

265267
# finally pass in the combined credentials when creating a channel
266-
return grpc.aio.secure_channel(f"{host}:{port}", creds, _options)
268+
return grpc.aio.secure_channel(f"{host}:{port}", creds, _options, compression)
267269
else:
268270
if metadata:
269271
metadata_interceptor = header_adder_async_interceptor(metadata)
270272
return grpc.aio.insecure_channel(
271-
f"{host}:{port}", _options, interceptors=[metadata_interceptor]
273+
f"{host}:{port}", _options, compression, interceptors=[metadata_interceptor]
272274
)
273275
else:
274-
return grpc.aio.insecure_channel(f"{host}:{port}", _options)
276+
return grpc.aio.insecure_channel(f"{host}:{port}", _options, compression)

qdrant_client/qdrant_remote.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import httpx
2121
import numpy as np
22+
from grpc import Compression
2223
from urllib3.util import Url, parse_url
2324

2425
from qdrant_client import grpc as grpc
@@ -126,6 +127,19 @@ def __init__(
126127
self._rest_headers["api-key"] = api_key
127128
self._grpc_headers.append(("api-key", api_key))
128129

130+
# GRPC Channel-Level Compression
131+
grpc_compression: Optional[Compression] = kwargs.pop("grpc_compression", None)
132+
if grpc_compression is not None and not isinstance(grpc_compression, Compression):
133+
raise TypeError(
134+
f"Expected 'grpc_compression' to be of type "
135+
f"grpc.Compression or None, but got {type(grpc_compression)}"
136+
)
137+
if grpc_compression == Compression.Deflate:
138+
raise ValueError(
139+
"grpc.Compression.Deflate is not supported. Try grpc.Compression.Gzip or grpc.Compression.NoCompression"
140+
)
141+
self._grpc_compression = grpc_compression
142+
129143
address = f"{self._host}:{self._port}" if self._port is not None else self._host
130144
self.rest_uri = f"{self._scheme}://{address}{self._prefix}"
131145

@@ -206,6 +220,7 @@ def _init_grpc_channel(self) -> None:
206220
ssl=self._https,
207221
metadata=self._grpc_headers,
208222
options=self._grpc_options,
223+
compression=self._grpc_compression,
209224
)
210225

211226
def _init_async_grpc_channel(self) -> None:
@@ -219,6 +234,7 @@ def _init_async_grpc_channel(self) -> None:
219234
ssl=self._https,
220235
metadata=self._grpc_headers,
221236
options=self._grpc_options,
237+
compression=self._grpc_compression,
222238
)
223239

224240
def _init_grpc_points_client(self) -> None:

tests/test_qdrant_client.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import numpy as np
99
import pytest
10-
from grpc import RpcError
10+
from grpc import Compression, RpcError
1111

1212
from qdrant_client import QdrantClient, models
1313
from qdrant_client._pydantic_compat import to_dict
@@ -48,6 +48,7 @@
4848
)
4949
from qdrant_client.qdrant_remote import QdrantRemote
5050
from qdrant_client.uploader.grpc_uploader import payload_to_grpc
51+
from tests.congruence_tests.test_common import generate_fixtures, init_client
5152
from tests.fixtures.payload import (
5253
one_random_payload_please,
5354
random_payload,
@@ -1730,6 +1731,21 @@ def test_grpc_options():
17301731
)
17311732

17321733

1734+
def test_grpc_compression():
1735+
client = QdrantClient(prefer_grpc=True, grpc_compression=Compression.Gzip)
1736+
client.get_collections()
1737+
1738+
client = QdrantClient(prefer_grpc=True, grpc_compression=Compression.NoCompression)
1739+
client.get_collections()
1740+
1741+
with pytest.raises(ValueError):
1742+
# creates a grpc client with not supported Compression type
1743+
QdrantClient(prefer_grpc=True, grpc_compression=Compression.Deflate)
1744+
1745+
with pytest.raises(TypeError):
1746+
QdrantClient(prefer_grpc=True, grpc_compression="gzip")
1747+
1748+
17331749
if __name__ == "__main__":
17341750
test_qdrant_client_integration()
17351751
test_points_crud()

tools/generate_async_client.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mv async_qdrant_local.py $ABSOLUTE_PROJECT_ROOT/qdrant_client/async_qdrant_local
2323
cd $ABSOLUTE_PROJECT_ROOT/qdrant_client
2424

2525
ls -1 async*.py | autoflake --recursive --imports qdrant_client --remove-unused-variables --in-place async*.py
26-
ls -1 async*.py | xargs -I {} isort --profile black --py 39 {}
27-
ls -1 async*.py | xargs -I {} black -l 99 --target-version py39 {}
26+
ls -1 async*.py | xargs -I {} isort --profile black --py 310 {}
27+
ls -1 async*.py | xargs -I {} black -l 99 --target-version py310 {}
2828

2929
mv async_qdrant_local.py local/async_qdrant_local.py

0 commit comments

Comments
 (0)