perf: use the orjson codec for Valkey vector metadata (#27805)

The Valkey backend serializes chunk metadata on every insert and parses it back on every result row in `get` and `query`. Both directions now go through `JSONCodec`, which selects orjson when `ENABLE_ORJSON` is set.

The stored `metadata_json` field is never matched against as text. `_build_filter_expression` only emits TAG predicates, and the TAG fields are `id`, `hash`, `file_id`, `source` and `knowledge_base_id`; `metadata_json` appears only as a return field that is immediately re-parsed. So rows written with escaped non-ASCII and rows written raw are indistinguishable to every reader, and no migration is needed.

`process_metadata` already stringifies datetimes and strips null bytes and lone surrogates before the write, so the two backends cannot disagree about what is serializable here.

Both read `except` clauses widen from `(json.JSONDecodeError, TypeError)` to `(ValueError, TypeError)`. The codec falls back to engineio's codec, which installs `parse_int=_safe_int` and raises a bare `ValueError` for integer literals longer than 100 characters; the narrower clause would have let that escape and abort a search instead of yielding empty metadata. `json.JSONDecodeError` is a `ValueError` subclass, so this is a strict superset. That removes the module's last use of stdlib `json`, so the import goes with it.

With `ENABLE_ORJSON` unset, which is the default, `JSONCodec` is stdlib `json` and this call site behaves exactly as before.
This commit is contained in:
Classic298
2026-07-31 17:24:57 -04:00
committed by GitHub
parent 466e05801b
commit f4c6a76651
@@ -2,7 +2,6 @@
# Requires Valkey core >= 9.0.1 with the valkey-search module >= 1.2.0 loaded.
import atexit
import json
import logging
import re
import struct
@@ -24,6 +23,7 @@ from open_webui.retrieval.vector.main import (
VectorItem,
)
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.utils.json_codec import JSONCodec
log = logging.getLogger(__name__)
@@ -482,7 +482,7 @@ class ValkeyClient(VectorDBBase):
'id': item['id'],
'vector': _vector_to_bytes(item['vector']),
'text': item['text'],
'metadata_json': json.dumps(metadata),
'metadata_json': JSONCodec.dumps(metadata),
# `or ''` prevents indexing literal 'None' as a TAG value, which would
# poison $ne / equality queries.
'hash': str(metadata.get('hash') or ''),
@@ -588,8 +588,8 @@ class ValkeyClient(VectorDBBase):
ids.append(_decode(fields.get(b'id', b'')))
documents.append(_decode(fields.get(b'text', b'')))
try:
metadatas.append(json.loads(_decode(fields.get(b'metadata_json', b'{}'))))
except (json.JSONDecodeError, TypeError):
metadatas.append(JSONCodec.loads(_decode(fields.get(b'metadata_json', b'{}'))))
except (ValueError, TypeError):
metadatas.append({})
if limit is not None and limit > 0 and len(ids) >= limit:
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
@@ -734,8 +734,8 @@ class ValkeyClient(VectorDBBase):
ids.append(_decode(fields.get(b'id', b'')))
documents.append(_decode(fields.get(b'text', b'')))
try:
metadatas.append(json.loads(_decode(fields.get(b'metadata_json', b'{}'))))
except (json.JSONDecodeError, TypeError):
metadatas.append(JSONCodec.loads(_decode(fields.get(b'metadata_json', b'{}'))))
except (ValueError, TypeError):
metadatas.append({})
if include_score: