Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
This commit is contained in:
Timothy Jaeryang BaekandClassic298 committed 2026-08-25 16:27:17 -04:00
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]],