Make qdrant the default vector db (#1285)

* Make qdrant the default vector db

---------

Co-authored-by: Pablo Orgaz <pabloogc@gmail.com>
Co-authored-by: lopagela <lpglm@orange.fr>
This commit is contained in:
Iván Martínez 2023-11-20 16:19:22 +01:00 committed by GitHub
parent f1cbff0fb7
commit 510caa576b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 122 additions and 91 deletions

View File

@ -1,15 +1,16 @@
## Vectorstores
PrivateGPT supports [Chroma](https://www.trychroma.com/), [Qdrant](https://qdrant.tech/) as vectorstore providers. Chroma being the default.
PrivateGPT supports [Qdrant](https://qdrant.tech/) and [Chroma](https://www.trychroma.com/) as vectorstore providers. Qdrant being the default.
In order to select one or the other, set the `vectorstore.database` property in the `settings.yaml` file to `qdrant` or `chroma`.
```yaml
vectorstore:
database: qdrant
```
### Qdrant configuration
To enable Qdrant, set the `vectorstore.database` property in the `settings.yaml` file to `qdrant` and install the `qdrant` extra.
```bash
poetry install --extras qdrant
```
By default Qdrant tries to connect to an instance at `http://localhost:3000`.
To enable Qdrant, set the `vectorstore.database` property in the `settings.yaml` file to `qdrant`.
Qdrant settings can be configured by setting values to the `qdrant` property in the `settings.yaml` file.
@ -27,4 +28,23 @@ The available configuration options are:
| timeout | Timeout for REST and gRPC API requests. Default: 5.0 seconds for REST and unlimited for gRPC |
| host | Host name of Qdrant service. If url and host are not set, defaults to 'localhost'.|
| path | Persistence path for QdrantLocal. Eg. `local_data/private_gpt/qdrant`|
| force_disable_check_same_thread | Force disable check_same_thread for QdrantLocal sqlite connection, defaults to True.|
| force_disable_check_same_thread | Force disable check_same_thread for QdrantLocal sqlite connection, defaults to True.|
By default Qdrant tries to connect to an instance of Qdrant server at `http://localhost:3000`.
To obtain a local setup (disk-based database) without running a Qdrant server, configure the `qdrant.path` value in settings.yaml:
```yaml
qdrant:
path: local_data/private_gpt/qdrant
```
### Chroma configuration
To enable Chroma, set the `vectorstore.database` property in the `settings.yaml` file to `chroma` and install the `chroma` extra.
```bash
poetry install --extras chroma
```
By default `chroma` will use a disk-based database stored in local_data_path / "chroma_db" (being local_data_path defined in settings.yaml)

74
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
[[package]]
name = "accelerate"
@ -239,7 +239,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
name = "backoff"
version = "2.2.1"
description = "Function decoration for backoff and retry"
optional = false
optional = true
python-versions = ">=3.7,<4.0"
files = [
{file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
@ -250,7 +250,7 @@ files = [
name = "bcrypt"
version = "4.0.1"
description = "Modern password hashing for your software and your servers"
optional = false
optional = true
python-versions = ">=3.6"
files = [
{file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"},
@ -373,7 +373,7 @@ crt = ["awscrt (==0.19.12)"]
name = "cachetools"
version = "5.3.2"
description = "Extensible memoizing collections and decorators"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"},
@ -505,7 +505,7 @@ files = [
name = "chroma-hnswlib"
version = "0.7.3"
description = "Chromas fork of hnswlib"
optional = false
optional = true
python-versions = "*"
files = [
{file = "chroma-hnswlib-0.7.3.tar.gz", hash = "sha256:b6137bedde49fffda6af93b0297fe00429fc61e5a072b1ed9377f909ed95a932"},
@ -542,7 +542,7 @@ numpy = "*"
name = "chromadb"
version = "0.4.17"
description = "Chroma."
optional = false
optional = true
python-versions = ">=3.8"
files = [
{file = "chromadb-0.4.17-py3-none-any.whl", hash = "sha256:8cb88162bc6124441ba5a4b93819463a10e9aaafbe05a3286e876cbdc7a7e11d"},
@ -1197,7 +1197,7 @@ tqdm = ["tqdm"]
name = "google-auth"
version = "2.23.4"
description = "Google Authentication Library"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "google-auth-2.23.4.tar.gz", hash = "sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3"},
@ -1220,7 +1220,7 @@ requests = ["requests (>=2.20.0,<3.0.0.dev0)"]
name = "googleapis-common-protos"
version = "1.61.0"
description = "Common protobufs used in Google APIs"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "googleapis-common-protos-1.61.0.tar.gz", hash = "sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b"},
@ -1435,7 +1435,7 @@ protobuf = ["grpcio-tools (>=1.59.3)"]
name = "grpcio-tools"
version = "1.59.3"
description = "Protobuf code generator for gRPC"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "grpcio-tools-1.59.3.tar.gz", hash = "sha256:cd160ac4281cd1ae77a2c880377a7728349340b4c91e24285037b57c18e9f651"},
@ -1514,7 +1514,7 @@ files = [
name = "h2"
version = "4.1.0"
description = "HTTP/2 State-Machine based protocol implementation"
optional = true
optional = false
python-versions = ">=3.6.1"
files = [
{file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"},
@ -1529,7 +1529,7 @@ hyperframe = ">=6.0,<7"
name = "hpack"
version = "4.0.0"
description = "Pure-Python HPACK header compression"
optional = true
optional = false
python-versions = ">=3.6.1"
files = [
{file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
@ -1681,7 +1681,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve
name = "hyperframe"
version = "6.0.1"
description = "HTTP/2 framing layer for Python"
optional = true
optional = false
python-versions = ">=3.6.1"
files = [
{file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"},
@ -1717,7 +1717,7 @@ files = [
name = "importlib-metadata"
version = "6.8.0"
description = "Read metadata from Python packages"
optional = false
optional = true
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
@ -1974,7 +1974,7 @@ files = [
name = "kubernetes"
version = "28.1.0"
description = "Kubernetes python client"
optional = false
optional = true
python-versions = ">=3.6"
files = [
{file = "kubernetes-28.1.0-py2.py3-none-any.whl", hash = "sha256:10f56f8160dcb73647f15fafda268e7f60cf7dbc9f8e46d52fcd46d3beb0c18d"},
@ -2233,7 +2233,7 @@ files = [
name = "monotonic"
version = "1.6"
description = "An implementation of time.monotonic() for Python 2 & < 3.3"
optional = false
optional = true
python-versions = "*"
files = [
{file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"},
@ -2679,7 +2679,7 @@ files = [
name = "oauthlib"
version = "3.2.2"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
optional = false
optional = true
python-versions = ">=3.6"
files = [
{file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
@ -2799,7 +2799,7 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
name = "opentelemetry-api"
version = "1.21.0"
description = "OpenTelemetry Python API"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "opentelemetry_api-1.21.0-py3-none-any.whl", hash = "sha256:4bb86b28627b7e41098f0e93280fe4892a1abed1b79a19aec6f928f39b17dffb"},
@ -2814,7 +2814,7 @@ importlib-metadata = ">=6.0,<7.0"
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.21.0"
description = "OpenTelemetry Protobuf encoding"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "opentelemetry_exporter_otlp_proto_common-1.21.0-py3-none-any.whl", hash = "sha256:97b1022b38270ec65d11fbfa348e0cd49d12006485c2321ea3b1b7037d42b6ec"},
@ -2829,7 +2829,7 @@ opentelemetry-proto = "1.21.0"
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.21.0"
description = "OpenTelemetry Collector Protobuf over gRPC Exporter"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "opentelemetry_exporter_otlp_proto_grpc-1.21.0-py3-none-any.whl", hash = "sha256:ab37c63d6cb58d6506f76d71d07018eb1f561d83e642a8f5aa53dddf306087a4"},
@ -2853,7 +2853,7 @@ test = ["pytest-grpc"]
name = "opentelemetry-proto"
version = "1.21.0"
description = "OpenTelemetry Python Proto"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "opentelemetry_proto-1.21.0-py3-none-any.whl", hash = "sha256:32fc4248e83eebd80994e13963e683f25f3b443226336bb12b5b6d53638f50ba"},
@ -2867,7 +2867,7 @@ protobuf = ">=3.19,<5.0"
name = "opentelemetry-sdk"
version = "1.21.0"
description = "OpenTelemetry Python SDK"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "opentelemetry_sdk-1.21.0-py3-none-any.whl", hash = "sha256:9fe633243a8c655fedace3a0b89ccdfc654c0290ea2d8e839bd5db3131186f73"},
@ -2883,7 +2883,7 @@ typing-extensions = ">=3.7.4"
name = "opentelemetry-semantic-conventions"
version = "0.42b0"
description = "OpenTelemetry Semantic Conventions"
optional = false
optional = true
python-versions = ">=3.7"
files = [
{file = "opentelemetry_semantic_conventions-0.42b0-py3-none-any.whl", hash = "sha256:5cd719cbfec448af658860796c5d0fcea2fdf0945a2bed2363f42cb1ee39f526"},
@ -3003,7 +3003,7 @@ files = [
name = "overrides"
version = "7.4.0"
description = "A decorator to automatically detect mismatch when overriding a method."
optional = false
optional = true
python-versions = ">=3.6"
files = [
{file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"},
@ -3197,7 +3197,7 @@ testing = ["pytest", "pytest-benchmark"]
name = "portalocker"
version = "2.8.2"
description = "Wraps the portalocker recipe for easy usage"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"},
@ -3216,7 +3216,7 @@ tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "p
name = "posthog"
version = "3.0.2"
description = "Integrate PostHog into any python application."
optional = false
optional = true
python-versions = "*"
files = [
{file = "posthog-3.0.2-py2.py3-none-any.whl", hash = "sha256:a8c0af6f2401fbe50f90e68c4143d0824b54e872de036b1c2f23b5abb39d88ce"},
@ -3305,7 +3305,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
name = "pulsar-client"
version = "3.3.0"
description = "Apache Pulsar Python client library"
optional = false
optional = true
python-versions = "*"
files = [
{file = "pulsar_client-3.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:c31afd3e67a044ff93177df89e08febf214cc965e95ede097d9fe8755af00e01"},
@ -3411,7 +3411,7 @@ files = [
name = "pyasn1"
version = "0.5.0"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
optional = true
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"},
@ -3422,7 +3422,7 @@ files = [
name = "pyasn1-modules"
version = "0.3.0"
description = "A collection of ASN.1-based protocols modules"
optional = false
optional = true
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"},
@ -3662,7 +3662,7 @@ image = ["Pillow (>=8.0.0)"]
name = "pypika"
version = "0.48.9"
description = "A SQL query builder API for Python"
optional = false
optional = true
python-versions = "*"
files = [
{file = "PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378"},
@ -3792,7 +3792,7 @@ files = [
name = "pywin32"
version = "306"
description = "Python for Window Extensions"
optional = true
optional = false
python-versions = "*"
files = [
{file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
@ -3874,7 +3874,7 @@ files = [
name = "qdrant-client"
version = "1.6.9"
description = "Client library for the Qdrant vector search engine"
optional = true
optional = false
python-versions = ">=3.8,<3.13"
files = [
{file = "qdrant_client-1.6.9-py3-none-any.whl", hash = "sha256:6546f96ceec389375e323586f0948a04183bd494c5c48d0f3daa267b358ad008"},
@ -4030,7 +4030,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "requests-oauthlib"
version = "1.3.1"
description = "OAuthlib authentication support for Requests."
optional = false
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
@ -4192,7 +4192,7 @@ files = [
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
optional = true
python-versions = ">=3.6,<4"
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
@ -5514,7 +5514,7 @@ anyio = ">=3.0.0"
name = "websocket-client"
version = "1.6.4"
description = "WebSocket client for Python with low level API options"
optional = false
optional = true
python-versions = ">=3.8"
files = [
{file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"},
@ -5892,7 +5892,7 @@ multidict = ">=4.0"
name = "zipp"
version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
optional = true
python-versions = ">=3.8"
files = [
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
@ -5904,9 +5904,9 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[extras]
qdrant = ["qdrant-client"]
chroma = ["chromadb"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.11,<3.12"
content-hash = "62688941bd24b24069b81c596ffb713fbe0427b8eec3f849cf809fdfde2db339"
content-hash = "7b9e7bc6e4a9ecbab92e58423c1d6746aa731b681236b1966f986feac3101959"

View File

@ -1,8 +1,6 @@
import logging
import typing
import chromadb
from chromadb.config import Settings as ChromaSettings
from injector import inject, singleton
from llama_index import VectorStoreIndex
from llama_index.indices.vector_store import VectorIndexRetriever
@ -43,6 +41,18 @@ class VectorStoreComponent:
def __init__(self, settings: Settings) -> None:
match settings.vectorstore.database:
case "chroma":
try:
import chromadb # type: ignore
from chromadb.config import ( # type: ignore
Settings as ChromaSettings,
)
except ImportError as e:
raise ImportError(
"'chromadb' is not installed."
"To use PrivateGPT with Chroma, install the 'chroma' extra."
"`poetry install --extras chroma`"
) from e
chroma_settings = ChromaSettings(anonymized_telemetry=False)
chroma_client = chromadb.PersistentClient(
path=str((local_data_path / "chroma_db").absolute()),
@ -60,15 +70,9 @@ class VectorStoreComponent:
)
case "qdrant":
try:
from llama_index.vector_stores.qdrant import QdrantVectorStore
from qdrant_client import QdrantClient # type: ignore
except ImportError as e:
raise ImportError(
"'qdrant_client' is not installed."
"To use PrivateGPT with Qdrant, install the 'qdrant' extra."
"`poetry install --extras qdrant`"
) from e
from llama_index.vector_stores.qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
if settings.qdrant is None:
logger.info(
"Qdrant config not found. Using default settings."
@ -99,11 +103,16 @@ class VectorStoreComponent:
context_filter: ContextFilter | None = None,
similarity_top_k: int = 2,
) -> VectorIndexRetriever:
# TODO this 'where' is specific to chromadb. Implement other vector stores
# This way we support qdrant (using doc_ids) and chroma (using where clause)
return VectorIndexRetriever(
index=index,
similarity_top_k=similarity_top_k,
doc_ids=context_filter.docs_ids if context_filter else None,
vector_store_kwargs={
"where": _chromadb_doc_id_metadata_filter(context_filter)
},
)
def close(self) -> None:
if hasattr(self.vector_store.client, "close"):
self.vector_store.client.close()

View File

@ -207,7 +207,12 @@ class IngestService:
)
# Load the index with store_nodes_override=True to be able to delete them
index = load_index_from_storage(self.storage_context, store_nodes_override=True)
index = load_index_from_storage(
storage_context=self.storage_context,
service_context=self.ingest_service_context,
store_nodes_override=True, # Force store nodes in index and document stores
show_progress=True,
)
# Delete the document from the index
index.delete_ref_doc(doc_id, delete_from_docstore=True)

View File

@ -13,9 +13,9 @@ pyyaml = "^6.0.1"
python-multipart = "^0.0.6"
pypdf = "^3.16.2"
llama-index = { extras = ["local_models"], version = "0.9.3" }
chromadb = "^0.4.13"
watchdog = "^3.0.0"
qdrant-client = {version = "^1.6.9", optional = true}
qdrant-client = "^1.6.9"
chromadb = {version = "^0.4.13", optional = true}
[tool.poetry.group.dev.dependencies]
black = "^22"
@ -44,7 +44,7 @@ torch = ">=2.0.0, !=2.0.1, !=2.1.0"
transformers = "^4.34.0"
[tool.poetry.extras]
qdrant = ["qdrant-client"]
chroma = ["chromadb"]
[build-system]
requires = ["poetry-core>=1.0.0"]
@ -135,6 +135,7 @@ python_version = "3.11"
strict = true
check_untyped_defs = false
explicit_package_bases = true
warn_unused_ignores = false
exclude = ["tests"]
[tool.pytest.ini_options]

View File

@ -5,10 +5,12 @@ server:
# Dummy secrets used for tests
secret: "foo bar; dummy secret"
data:
local_data_folder: local_data/tests
qdrant:
path: local_data/tests
llm:
mode: mock

View File

@ -24,7 +24,10 @@ llm:
mode: local
vectorstore:
database: chroma
database: qdrant
qdrant:
path: local_data/private_gpt/qdrant
local:
llm_hf_repo_id: TheBloke/Mistral-7B-Instruct-v0.1-GGUF

18
tests/fixtures/auto_close_qdrant.py vendored Normal file
View File

@ -0,0 +1,18 @@
import pytest
from private_gpt.components.vector_store.vector_store_component import (
VectorStoreComponent,
)
from tests.fixtures.mock_injector import MockInjector
@pytest.fixture(autouse=True)
def _auto_close_vector_store_client(injector: MockInjector) -> None:
"""Auto close VectorStore client after each test.
VectorStore client (qdrant/chromadb) opens a connection the
Database that causes issues when running tests too fast,
so close explicitly after each test.
"""
yield
injector.get(VectorStoreComponent).close()

View File

@ -1,27 +0,0 @@
from unittest.mock import PropertyMock, patch
from llama_index import Document
from private_gpt.server.ingest.ingest_service import IngestService
from tests.fixtures.mock_injector import MockInjector
def test_save_many_nodes(injector: MockInjector) -> None:
"""This is a specific test for a local Chromadb Vector Database setup.
Extend it when we add support for other vector databases in VectorStoreComponent.
"""
with patch(
"chromadb.api.segment.SegmentAPI.max_batch_size", new_callable=PropertyMock
) as max_batch_size:
# Make max batch size of Chromadb very small
max_batch_size.return_value = 10
ingest_service = injector.get(IngestService)
documents = []
for _i in range(100):
documents.append(Document(text="This is a sentence."))
ingested_docs = ingest_service._save_docs(documents)
assert len(ingested_docs) == len(documents)