refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
This commit is contained in:
1 parent
067114c280
commit
1d6d4e6e66
12 files changed
+190
-34
No files matched your search
@@ -3,7 +3,7 @@ NOTE: This vector database integration is community-supported and maintained on
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import ssl
|
import ssl
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from elasticsearch import BadRequestError, Elasticsearch
|
from elasticsearch import BadRequestError, Elasticsearch
|
||||||
from elasticsearch.helpers import bulk, scan
|
from elasticsearch.helpers import bulk, scan
|
||||||
@@ -23,7 +23,13 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import process_metadata
|
from open_webui.retrieval.vector.utils import iter_filter_conditions, process_metadata
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_filter(key: str, op: str, value: Any) -> dict:
|
||||||
|
if op == '$in':
|
||||||
|
return {'terms': {f'metadata.{key}': value}}
|
||||||
|
return {'term': {f'metadata.{key}': value}}
|
||||||
|
|
||||||
|
|
||||||
class ElasticsearchClient(VectorDBBase):
|
class ElasticsearchClient(VectorDBBase):
|
||||||
@@ -161,12 +167,16 @@ class ElasticsearchClient(VectorDBBase):
|
|||||||
filter: Optional[dict] = None,
|
filter: Optional[dict] = None,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> Optional[SearchResult]:
|
) -> Optional[SearchResult]:
|
||||||
|
filters = [{'term': {'collection': collection_name}}]
|
||||||
|
if filter:
|
||||||
|
filters.extend(_metadata_filter(key, op, value) for key, op, value in iter_filter_conditions(filter))
|
||||||
|
|
||||||
query = {
|
query = {
|
||||||
'size': limit,
|
'size': limit,
|
||||||
'_source': ['text', 'metadata'],
|
'_source': ['text', 'metadata'],
|
||||||
'query': {
|
'query': {
|
||||||
'script_score': {
|
'script_score': {
|
||||||
'query': {'bool': {'filter': [{'term': {'collection': collection_name}}]}},
|
'query': {'bool': {'filter': filters}},
|
||||||
'script': {
|
'script': {
|
||||||
'source': "cosineSimilarity(params.vector, 'vector') + 1.0",
|
'source': "cosineSimilarity(params.vector, 'vector') + 1.0",
|
||||||
'params': {'vector': vectors[0]}, # Assuming single query vector
|
'params': {'vector': vectors[0]}, # Assuming single query vector
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ NOTE: This vector database integration is community-supported and maintained on
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
import re
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
MILVUS_DB,
|
MILVUS_DB,
|
||||||
@@ -23,7 +24,7 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import process_metadata
|
from open_webui.retrieval.vector.utils import iter_filter_conditions, process_metadata
|
||||||
from open_webui.utils.json_codec import JSONCodec
|
from open_webui.utils.json_codec import JSONCodec
|
||||||
from pymilvus import DataType
|
from pymilvus import DataType
|
||||||
from pymilvus import MilvusClient as Client
|
from pymilvus import MilvusClient as Client
|
||||||
@@ -35,6 +36,36 @@ log = logging.getLogger(__name__)
|
|||||||
# field). Clamp long chunks before insert so one oversized chunk can't fail the
|
# field). Clamp long chunks before insert so one oversized chunk can't fail the
|
||||||
# whole batch and leave the file with zero embeddings.
|
# whole batch and leave the file with zero embeddings.
|
||||||
MILVUS_TEXT_MAX_LENGTH = 65535
|
MILVUS_TEXT_MAX_LENGTH = 65535
|
||||||
|
_SAFE_METADATA_KEY_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,63}$')
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_milvus_string(value: str) -> str:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError(f'Expected str, got {type(value).__name__}')
|
||||||
|
return value.replace('\\', '\\\\').replace("'", "\\'")
|
||||||
|
|
||||||
|
|
||||||
|
def _milvus_literal(value: Any) -> str:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return f"'{_escape_milvus_string(value)}'"
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return str(value).lower()
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return str(value)
|
||||||
|
raise TypeError(f'Unsupported Milvus filter value type: {type(value).__name__}')
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_exprs(filter: Optional[dict]) -> list[str]:
|
||||||
|
exprs = []
|
||||||
|
for key, op, value in iter_filter_conditions(filter):
|
||||||
|
if not isinstance(key, str) or not _SAFE_METADATA_KEY_RE.fullmatch(key):
|
||||||
|
raise ValueError(f'Invalid Milvus metadata filter key: {key!r}')
|
||||||
|
if op == '$in':
|
||||||
|
items = [f"metadata['{key}'] == {_milvus_literal(item)}" for item in value]
|
||||||
|
exprs.append(f'({" or ".join(items)})' if items else 'false')
|
||||||
|
else:
|
||||||
|
exprs.append(f"metadata['{key}'] == {_milvus_literal(value)}")
|
||||||
|
return exprs
|
||||||
|
|
||||||
|
|
||||||
class MilvusClient(VectorDBBase):
|
class MilvusClient(VectorDBBase):
|
||||||
@@ -193,6 +224,9 @@ class MilvusClient(VectorDBBase):
|
|||||||
) -> Optional[SearchResult]:
|
) -> Optional[SearchResult]:
|
||||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||||
collection_name = collection_name.replace('-', '_')
|
collection_name = collection_name.replace('-', '_')
|
||||||
|
kwargs = {}
|
||||||
|
if filter:
|
||||||
|
kwargs['filter'] = ' and '.join(_metadata_exprs(filter))
|
||||||
# For some index types like IVF_FLAT, search params like nprobe can be set.
|
# For some index types like IVF_FLAT, search params like nprobe can be set.
|
||||||
# Example: search_params = {"nprobe": 10} if using IVF_FLAT
|
# Example: search_params = {"nprobe": 10} if using IVF_FLAT
|
||||||
# For simplicity, not adding configurable search_params here, but could be extended.
|
# For simplicity, not adding configurable search_params here, but could be extended.
|
||||||
@@ -201,6 +235,7 @@ class MilvusClient(VectorDBBase):
|
|||||||
data=vectors,
|
data=vectors,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
output_fields=['data', 'metadata'],
|
output_fields=['data', 'metadata'],
|
||||||
|
**kwargs,
|
||||||
# search_params=search_params # Potentially add later if needed
|
# search_params=search_params # Potentially add later if needed
|
||||||
)
|
)
|
||||||
return self._result_to_search_result(result)
|
return self._result_to_search_result(result)
|
||||||
@@ -364,7 +399,9 @@ class MilvusClient(VectorDBBase):
|
|||||||
ids=ids,
|
ids=ids,
|
||||||
)
|
)
|
||||||
elif filter:
|
elif filter:
|
||||||
filter_string = ' && '.join([f'metadata["{key}"] == {JSONCodec.dumps(value)}' for key, value in filter.items()])
|
filter_string = ' && '.join(
|
||||||
|
[f'metadata["{key}"] == {JSONCodec.dumps(value)}' for key, value in filter.items()]
|
||||||
|
)
|
||||||
log.info(
|
log.info(
|
||||||
'Deleting items by filter from %s_%s. Filter: %s',
|
'Deleting items by filter from %s_%s. Filter: %s',
|
||||||
self.collection_prefix,
|
self.collection_prefix,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from open_webui.config import (
|
|||||||
MILVUS_TOKEN,
|
MILVUS_TOKEN,
|
||||||
MILVUS_URI,
|
MILVUS_URI,
|
||||||
)
|
)
|
||||||
|
from open_webui.retrieval.vector.dbs.milvus import _metadata_exprs
|
||||||
from open_webui.retrieval.vector.main import (
|
from open_webui.retrieval.vector.main import (
|
||||||
GetResult,
|
GetResult,
|
||||||
SearchResult,
|
SearchResult,
|
||||||
@@ -221,13 +222,14 @@ class MilvusClient(VectorDBBase):
|
|||||||
|
|
||||||
self.client.load_collection(mt_collection)
|
self.client.load_collection(mt_collection)
|
||||||
|
|
||||||
|
expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'", *_metadata_exprs(filter)]
|
||||||
results = self.client.search(
|
results = self.client.search(
|
||||||
collection_name=mt_collection,
|
collection_name=mt_collection,
|
||||||
data=vectors,
|
data=vectors,
|
||||||
anns_field='vector',
|
anns_field='vector',
|
||||||
search_params={'metric_type': MILVUS_METRIC_TYPE, 'params': {}},
|
search_params={'metric_type': MILVUS_METRIC_TYPE, 'params': {}},
|
||||||
limit=limit,
|
limit=limit,
|
||||||
filter=f"{RESOURCE_ID_FIELD} == '{resource_id}'",
|
filter=' and '.join(expr),
|
||||||
output_fields=['id', 'text', 'metadata'],
|
output_fields=['id', 'text', 'metadata'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import process_metadata
|
from open_webui.retrieval.vector.utils import iter_filter_conditions, process_metadata
|
||||||
|
|
||||||
VECTOR_LENGTH = OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH
|
VECTOR_LENGTH = OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
@@ -86,6 +86,12 @@ class DocumentChunk(Base):
|
|||||||
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
|
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_clause(key: str, op: str, value: Any):
|
||||||
|
if op == '$in':
|
||||||
|
return DocumentChunk.vmetadata[key].astext.in_([str(v) for v in value])
|
||||||
|
return DocumentChunk.vmetadata[key].astext == str(value)
|
||||||
|
|
||||||
|
|
||||||
class OpenGaussClient(VectorDBBase):
|
class OpenGaussClient(VectorDBBase):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
if not OPENGAUSS_DB_URL:
|
if not OPENGAUSS_DB_URL:
|
||||||
@@ -242,10 +248,15 @@ class OpenGaussClient(VectorDBBase):
|
|||||||
DocumentChunk.vmetadata,
|
DocumentChunk.vmetadata,
|
||||||
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label('distance'),
|
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label('distance'),
|
||||||
]
|
]
|
||||||
|
where_clauses = [DocumentChunk.collection_name == collection_name]
|
||||||
|
if filter:
|
||||||
|
where_clauses.extend(
|
||||||
|
_metadata_clause(key, op, value) for key, op, value in iter_filter_conditions(filter)
|
||||||
|
)
|
||||||
|
|
||||||
subq = (
|
subq = (
|
||||||
select(*result_fields)
|
select(*result_fields)
|
||||||
.where(DocumentChunk.collection_name == collection_name)
|
.where(*where_clauses)
|
||||||
.order_by(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))
|
.order_by(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))
|
||||||
)
|
)
|
||||||
if limit is not None:
|
if limit is not None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
NOTE: This vector database integration is community-supported and maintained on a best-effort basis.
|
NOTE: This vector database integration is community-supported and maintained on a best-effort basis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
OPENSEARCH_CERT_VERIFY,
|
OPENSEARCH_CERT_VERIFY,
|
||||||
@@ -17,11 +17,17 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import process_metadata
|
from open_webui.retrieval.vector.utils import iter_filter_conditions, process_metadata
|
||||||
from opensearchpy import OpenSearch
|
from opensearchpy import OpenSearch
|
||||||
from opensearchpy.helpers import bulk
|
from opensearchpy.helpers import bulk
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_filter(key: str, op: str, value: Any) -> dict:
|
||||||
|
if op == '$in':
|
||||||
|
return {'terms': {f'metadata.{key}.keyword': value}}
|
||||||
|
return {'term': {f'metadata.{key}.keyword': value}}
|
||||||
|
|
||||||
|
|
||||||
class OpenSearchClient(VectorDBBase):
|
class OpenSearchClient(VectorDBBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.index_prefix = 'open_webui'
|
self.index_prefix = 'open_webui'
|
||||||
@@ -121,6 +127,8 @@ class OpenSearchClient(VectorDBBase):
|
|||||||
filter: Optional[dict] = None,
|
filter: Optional[dict] = None,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> Optional[SearchResult]:
|
) -> Optional[SearchResult]:
|
||||||
|
filter_clauses = [_metadata_filter(key, op, value) for key, op, value in iter_filter_conditions(filter)]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.has_collection(collection_name):
|
if not self.has_collection(collection_name):
|
||||||
return None
|
return None
|
||||||
@@ -130,7 +138,7 @@ class OpenSearchClient(VectorDBBase):
|
|||||||
'_source': ['text', 'metadata'],
|
'_source': ['text', 'metadata'],
|
||||||
'query': {
|
'query': {
|
||||||
'script_score': {
|
'script_score': {
|
||||||
'query': {'match_all': {}},
|
'query': {'bool': {'filter': filter_clauses}} if filter_clauses else {'match_all': {}},
|
||||||
'script': {
|
'script': {
|
||||||
'source': '(cosineSimilarity(params.query_value, doc[params.field]) + 1.0) / 2.0',
|
'source': '(cosineSimilarity(params.query_value, doc[params.field]) + 1.0) / 2.0',
|
||||||
'params': {
|
'params': {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import array
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -56,9 +57,29 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
|
from open_webui.retrieval.vector.utils import iter_filter_conditions
|
||||||
from open_webui.utils.json_codec import JSONCodec
|
from open_webui.utils.json_codec import JSONCodec
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
_SAFE_METADATA_KEY_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,63}$')
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_where(filter: Optional[dict]) -> tuple[str, dict[str, Any]]:
|
||||||
|
clause = ''
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
for i, (key, op, value) in enumerate(iter_filter_conditions(filter)):
|
||||||
|
if not isinstance(key, str) or not _SAFE_METADATA_KEY_RE.fullmatch(key):
|
||||||
|
raise ValueError(f'Invalid Oracle metadata filter key: {key!r}')
|
||||||
|
json_value = f"JSON_VALUE(dc.vmetadata, '$.{key}' RETURNING VARCHAR2(4096))"
|
||||||
|
if op == '$in':
|
||||||
|
names = [f'value_{i}_{j}' for j, _ in enumerate(value)]
|
||||||
|
clause += f' AND {json_value} IN ({", ".join(f":{name}" for name in names)})' if names else ' AND 1 = 0'
|
||||||
|
params.update({name: str(item) for name, item in zip(names, value)})
|
||||||
|
else:
|
||||||
|
name = f'value_{i}'
|
||||||
|
clause += f' AND {json_value} = :{name}'
|
||||||
|
params[name] = str(value)
|
||||||
|
return clause, params
|
||||||
|
|
||||||
|
|
||||||
class Oracle23aiClient(VectorDBBase):
|
class Oracle23aiClient(VectorDBBase):
|
||||||
@@ -549,6 +570,7 @@ class Oracle23aiClient(VectorDBBase):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
num_queries = len(vectors)
|
num_queries = len(vectors)
|
||||||
|
filter_clause, filter_params = _metadata_where(filter)
|
||||||
|
|
||||||
ids = [[] for _ in range(num_queries)]
|
ids = [[] for _ in range(num_queries)]
|
||||||
distances = [[] for _ in range(num_queries)]
|
distances = [[] for _ in range(num_queries)]
|
||||||
@@ -561,12 +583,12 @@ class Oracle23aiClient(VectorDBBase):
|
|||||||
vector_blob = self._vector_to_blob(vector)
|
vector_blob = self._vector_to_blob(vector)
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT dc.id, dc.text,
|
SELECT dc.id, dc.text,
|
||||||
JSON_SERIALIZE(dc.vmetadata RETURNING VARCHAR2(4096)) as vmetadata,
|
JSON_SERIALIZE(dc.vmetadata RETURNING VARCHAR2(4096)) as vmetadata,
|
||||||
VECTOR_DISTANCE(dc.vector, :query_vector, COSINE) as distance
|
VECTOR_DISTANCE(dc.vector, :query_vector, COSINE) as distance
|
||||||
FROM document_chunk dc
|
FROM document_chunk dc
|
||||||
WHERE dc.collection_name = :collection_name
|
WHERE dc.collection_name = :collection_name{filter_clause}
|
||||||
ORDER BY VECTOR_DISTANCE(dc.vector, :query_vector, COSINE)
|
ORDER BY VECTOR_DISTANCE(dc.vector, :query_vector, COSINE)
|
||||||
FETCH APPROX FIRST :limit ROWS ONLY
|
FETCH APPROX FIRST :limit ROWS ONLY
|
||||||
""",
|
""",
|
||||||
@@ -574,6 +596,7 @@ class Oracle23aiClient(VectorDBBase):
|
|||||||
'query_vector': vector_blob,
|
'query_vector': vector_blob,
|
||||||
'collection_name': collection_name,
|
'collection_name': collection_name,
|
||||||
'limit': limit,
|
'limit': limit,
|
||||||
|
**filter_params,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import process_metadata
|
from open_webui.retrieval.vector.utils import normalize_filter, process_metadata
|
||||||
|
|
||||||
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
|
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
|
||||||
BATCH_SIZE = 100 # Recommended batch size for Pinecone operations
|
BATCH_SIZE = 100 # Recommended batch size for Pinecone operations
|
||||||
@@ -372,13 +372,15 @@ class PineconeClient(VectorDBBase):
|
|||||||
try:
|
try:
|
||||||
# Search using the first vector (assuming this is the intended behavior)
|
# Search using the first vector (assuming this is the intended behavior)
|
||||||
query_vector = vectors[0]
|
query_vector = vectors[0]
|
||||||
|
pinecone_filter = normalize_filter(filter)
|
||||||
|
pinecone_filter['collection_name'] = collection_name_with_prefix
|
||||||
|
|
||||||
# Perform the search
|
# Perform the search
|
||||||
query_response = self.index.query(
|
query_response = self.index.query(
|
||||||
vector=query_vector,
|
vector=query_vector,
|
||||||
top_k=limit,
|
top_k=limit,
|
||||||
include_metadata=True,
|
include_metadata=True,
|
||||||
filter={'collection_name': collection_name_with_prefix},
|
filter=pinecone_filter,
|
||||||
)
|
)
|
||||||
|
|
||||||
matches = getattr(query_response, 'matches', []) or []
|
matches = getattr(query_response, 'matches', []) or []
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ NOTE: This vector database integration is community-supported and maintained on
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
@@ -22,6 +22,7 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
|
from open_webui.retrieval.vector.utils import iter_filter_conditions
|
||||||
from qdrant_client import QdrantClient as Qclient
|
from qdrant_client import QdrantClient as Qclient
|
||||||
from qdrant_client.http.models import PointStruct
|
from qdrant_client.http.models import PointStruct
|
||||||
from qdrant_client.models import models
|
from qdrant_client.models import models
|
||||||
@@ -31,6 +32,11 @@ NO_LIMIT = 999999999
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_filter(key: str, op: str, value: Any) -> models.FieldCondition:
|
||||||
|
match = models.MatchAny(any=value) if op == '$in' else models.MatchValue(value=value)
|
||||||
|
return models.FieldCondition(key=f'metadata.{key}', match=match)
|
||||||
|
|
||||||
|
|
||||||
class QdrantClient(VectorDBBase):
|
class QdrantClient(VectorDBBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.collection_prefix = QDRANT_COLLECTION_PREFIX
|
self.collection_prefix = QDRANT_COLLECTION_PREFIX
|
||||||
@@ -152,10 +158,13 @@ class QdrantClient(VectorDBBase):
|
|||||||
if limit is None:
|
if limit is None:
|
||||||
limit = NO_LIMIT # otherwise qdrant would set limit to 10!
|
limit = NO_LIMIT # otherwise qdrant would set limit to 10!
|
||||||
|
|
||||||
|
conditions = [_metadata_filter(key, op, value) for key, op, value in iter_filter_conditions(filter)]
|
||||||
|
query_filter = models.Filter(must=conditions) if conditions else None
|
||||||
query_response = self.client.query_points(
|
query_response = self.client.query_points(
|
||||||
collection_name=f'{self.collection_prefix}_{collection_name}',
|
collection_name=f'{self.collection_prefix}_{collection_name}',
|
||||||
query=vectors[0],
|
query=vectors[0],
|
||||||
limit=limit,
|
limit=limit,
|
||||||
|
query_filter=query_filter,
|
||||||
)
|
)
|
||||||
get_result = self._result_to_get_result(query_response.points)
|
get_result = self._result_to_get_result(query_response.points)
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
|
from open_webui.retrieval.vector.utils import iter_filter_conditions
|
||||||
from qdrant_client import QdrantClient as Qclient
|
from qdrant_client import QdrantClient as Qclient
|
||||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||||
from qdrant_client.http.models import PointStruct
|
from qdrant_client.http.models import PointStruct
|
||||||
@@ -39,8 +40,9 @@ def _tenant_filter(tenant_id: str) -> models.FieldCondition:
|
|||||||
return models.FieldCondition(key=TENANT_ID_FIELD, match=models.MatchValue(value=tenant_id))
|
return models.FieldCondition(key=TENANT_ID_FIELD, match=models.MatchValue(value=tenant_id))
|
||||||
|
|
||||||
|
|
||||||
def _metadata_filter(key: str, value: Any) -> models.FieldCondition:
|
def _metadata_filter(key: str, op: str, value: Any) -> models.FieldCondition:
|
||||||
return models.FieldCondition(key=f'metadata.{key}', match=models.MatchValue(value=value))
|
match = models.MatchAny(any=value) if op == '$in' else models.MatchValue(value=value)
|
||||||
|
return models.FieldCondition(key=f'metadata.{key}', match=match)
|
||||||
|
|
||||||
|
|
||||||
class QdrantClient(VectorDBBase):
|
class QdrantClient(VectorDBBase):
|
||||||
@@ -234,7 +236,7 @@ class QdrantClient(VectorDBBase):
|
|||||||
# whose payload omits an id (e.g. memories), leaving orphaned vectors.
|
# whose payload omits an id (e.g. memories), leaving orphaned vectors.
|
||||||
must_conditions.append(models.HasIdCondition(has_id=ids))
|
must_conditions.append(models.HasIdCondition(has_id=ids))
|
||||||
elif filter:
|
elif filter:
|
||||||
must_conditions += [_metadata_filter(k, v) for k, v in filter.items()]
|
must_conditions += [_metadata_filter(k, '$eq', v) for k, v in filter.items()]
|
||||||
|
|
||||||
return self.client.delete(
|
return self.client.delete(
|
||||||
collection_name=mt_collection,
|
collection_name=mt_collection,
|
||||||
@@ -258,12 +260,14 @@ class QdrantClient(VectorDBBase):
|
|||||||
log.debug("Collection %s doesn't exist, search returns None", mt_collection)
|
log.debug("Collection %s doesn't exist, search returns None", mt_collection)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tenant_filter = _tenant_filter(tenant_id)
|
conditions = [_tenant_filter(tenant_id)]
|
||||||
|
if filter:
|
||||||
|
conditions.extend(_metadata_filter(key, op, value) for key, op, value in iter_filter_conditions(filter))
|
||||||
query_response = self.client.query_points(
|
query_response = self.client.query_points(
|
||||||
collection_name=mt_collection,
|
collection_name=mt_collection,
|
||||||
query=vectors[0],
|
query=vectors[0],
|
||||||
limit=limit,
|
limit=limit,
|
||||||
query_filter=models.Filter(must=[tenant_filter]),
|
query_filter=models.Filter(must=conditions),
|
||||||
)
|
)
|
||||||
get_result = self._result_to_get_result(query_response.points)
|
get_result = self._result_to_get_result(query_response.points)
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
@@ -286,7 +290,7 @@ class QdrantClient(VectorDBBase):
|
|||||||
if limit is None:
|
if limit is None:
|
||||||
limit = NO_LIMIT
|
limit = NO_LIMIT
|
||||||
tenant_filter = _tenant_filter(tenant_id)
|
tenant_filter = _tenant_filter(tenant_id)
|
||||||
field_conditions = [_metadata_filter(k, v) for k, v in filter.items()]
|
field_conditions = [_metadata_filter(k, '$eq', v) for k, v in filter.items()]
|
||||||
combined_filter = models.Filter(must=[tenant_filter, *field_conditions])
|
combined_filter = models.Filter(must=[tenant_filter, *field_conditions])
|
||||||
points = self.client.scroll(
|
points = self.client.scroll(
|
||||||
collection_name=mt_collection,
|
collection_name=mt_collection,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import process_metadata
|
from open_webui.retrieval.vector.utils import metadata_matches_filter, normalize_filter, process_metadata
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -309,6 +309,7 @@ class S3VectorClient(VectorDBBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
log.info("Searching collection '%s' with %s query vectors, limit=%s", collection_name, len(vectors), limit)
|
log.info("Searching collection '%s' with %s query vectors, limit=%s", collection_name, len(vectors), limit)
|
||||||
|
vector_filter = normalize_filter(filter)
|
||||||
|
|
||||||
# Initialize result lists
|
# Initialize result lists
|
||||||
all_ids = []
|
all_ids = []
|
||||||
@@ -323,15 +324,18 @@ class S3VectorClient(VectorDBBase):
|
|||||||
# Prepare the query vector in S3 Vector format
|
# Prepare the query vector in S3 Vector format
|
||||||
query_vector_dict = {'float32': [float(x) for x in query_vector]}
|
query_vector_dict = {'float32': [float(x) for x in query_vector]}
|
||||||
|
|
||||||
# Call S3 Vector query API
|
request_params = {
|
||||||
response = self.client.query_vectors(
|
'vectorBucketName': self.bucket_name,
|
||||||
vectorBucketName=self.bucket_name,
|
'indexName': collection_name,
|
||||||
indexName=collection_name,
|
'topK': limit,
|
||||||
topK=limit,
|
'queryVector': query_vector_dict,
|
||||||
queryVector=query_vector_dict,
|
'returnMetadata': True,
|
||||||
returnMetadata=True,
|
'returnDistance': True,
|
||||||
returnDistance=True,
|
}
|
||||||
)
|
if vector_filter:
|
||||||
|
request_params['filter'] = vector_filter
|
||||||
|
|
||||||
|
response = self.client.query_vectors(**request_params)
|
||||||
|
|
||||||
# Process results for this query
|
# Process results for this query
|
||||||
query_ids = []
|
query_ids = []
|
||||||
@@ -346,6 +350,9 @@ class S3VectorClient(VectorDBBase):
|
|||||||
vector_metadata = vector.get('metadata', {})
|
vector_metadata = vector.get('metadata', {})
|
||||||
vector_distance = vector.get('distance', 0.0)
|
vector_distance = vector.get('distance', 0.0)
|
||||||
|
|
||||||
|
if vector_filter and not metadata_matches_filter(vector_metadata, vector_filter):
|
||||||
|
continue
|
||||||
|
|
||||||
# Extract document text from metadata
|
# Extract document text from metadata
|
||||||
document_text = ''
|
document_text = ''
|
||||||
if isinstance(vector_metadata, dict):
|
if isinstance(vector_metadata, dict):
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from open_webui.retrieval.vector.main import (
|
|||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import process_metadata
|
from open_webui.retrieval.vector.utils import iter_filter_conditions, process_metadata
|
||||||
|
|
||||||
|
|
||||||
def _convert_uuids_to_strings(obj: Any) -> Any:
|
def _convert_uuids_to_strings(obj: Any) -> Any:
|
||||||
@@ -54,6 +54,20 @@ def _convert_uuids_to_strings(obj: Any) -> Any:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_filter(filter: Optional[dict]) -> Any:
|
||||||
|
clauses = []
|
||||||
|
for key, op, value in iter_filter_conditions(filter):
|
||||||
|
if op == '$in':
|
||||||
|
clauses.append(
|
||||||
|
weaviate.classes.query.Filter.any_of(
|
||||||
|
[weaviate.classes.query.Filter.by_property(name=key).equal(item) for item in value]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
clauses.append(weaviate.classes.query.Filter.by_property(name=key).equal(value))
|
||||||
|
return weaviate.classes.query.Filter.all_of(clauses) if len(clauses) > 1 else (clauses[0] if clauses else None)
|
||||||
|
|
||||||
|
|
||||||
class WeaviateClient(VectorDBBase):
|
class WeaviateClient(VectorDBBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = WEAVIATE_HTTP_HOST
|
self.url = WEAVIATE_HTTP_HOST
|
||||||
@@ -168,6 +182,7 @@ class WeaviateClient(VectorDBBase):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
collection = self.client.collections.get(sane_collection_name)
|
collection = self.client.collections.get(sane_collection_name)
|
||||||
|
weaviate_filter = _metadata_filter(filter)
|
||||||
|
|
||||||
result_ids, result_documents, result_metadatas, result_distances = (
|
result_ids, result_documents, result_metadatas, result_distances = (
|
||||||
[],
|
[],
|
||||||
@@ -181,6 +196,7 @@ class WeaviateClient(VectorDBBase):
|
|||||||
response = collection.query.near_vector(
|
response = collection.query.near_vector(
|
||||||
near_vector=vector_embedding,
|
near_vector=vector_embedding,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
|
filters=weaviate_filter,
|
||||||
return_metadata=weaviate.classes.query.MetadataQuery(distance=True),
|
return_metadata=weaviate.classes.query.MetadataQuery(distance=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,33 @@ def process_metadata(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def iter_filter_conditions(filter: dict[str, Any] | None):
|
||||||
|
for key, value in (filter or {}).items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if set(value) != {'$in'}:
|
||||||
|
raise ValueError(f"Unsupported metadata filter for '{key}': {value}")
|
||||||
|
yield key, '$in', list(value['$in'])
|
||||||
|
else:
|
||||||
|
yield key, '$eq', value
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_filter(filter: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
return {key: {'$in': value} if op == '$in' else value for key, op, value in iter_filter_conditions(filter)}
|
||||||
|
|
||||||
|
|
||||||
|
def metadata_matches_filter(metadata: dict[str, Any], filter: dict[str, Any] | None) -> bool:
|
||||||
|
if not isinstance(metadata, dict):
|
||||||
|
return False
|
||||||
|
for key, op, value in iter_filter_conditions(filter):
|
||||||
|
actual = metadata.get(key)
|
||||||
|
if op == '$in':
|
||||||
|
if actual not in value:
|
||||||
|
return False
|
||||||
|
elif actual != value:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def merge_hybrid_search_results(
|
def merge_hybrid_search_results(
|
||||||
vector_result: SearchResult | None,
|
vector_result: SearchResult | None,
|
||||||
fts_results: list[dict[str, Any]],
|
fts_results: list[dict[str, Any]],
|
||||||
|
|||||||
Reference in new issue
Block a user