Files
Timothy Jaeryang Baek 1d6d4e6e66 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-25 16:27:17 -04:00

149 lines
5.1 KiB
Python

import datetime as dt
from typing import Any
from open_webui.env import RAG_METADATA_MAX_VALUE_CHARS
from open_webui.retrieval.vector.main import SearchResult
from open_webui.utils.misc import sanitize_text_for_db
KEYS_TO_EXCLUDE = [
'content',
'pages',
'tables',
'paragraphs',
'sections',
'figures',
'documents',
'keyValuePairs',
'styles',
'languages',
]
def filter_metadata(metadata: dict[str, any]) -> dict[str, any]:
# Removes large/redundant fields from metadata dict.
result = {}
for key, value in metadata.items():
if key in KEYS_TO_EXCLUDE:
continue
if RAG_METADATA_MAX_VALUE_CHARS is not None and isinstance(value, (list, dict)):
try:
if len(str(value)) > RAG_METADATA_MAX_VALUE_CHARS:
continue
except (MemoryError, RecursionError, ValueError):
continue
result[key] = value
return result
def process_metadata(
metadata: dict[str, any],
) -> dict[str, any]:
# Removes large fields, converts non-serializable types (datetime, list, dict) to strings,
# and sanitizes strings for database storage (strips null bytes and invalid surrogates).
result = {}
for key, value in metadata.items():
# Skip large fields
if key in KEYS_TO_EXCLUDE:
continue
if value is None:
continue
if RAG_METADATA_MAX_VALUE_CHARS is not None and isinstance(value, (list, dict)):
try:
if len(str(value)) > RAG_METADATA_MAX_VALUE_CHARS:
continue
except (MemoryError, RecursionError, ValueError):
continue
# Convert non-serializable fields to strings
if isinstance(value, (dt.datetime, list, dict)):
result[key] = sanitize_text_for_db(str(value))
else:
result[key] = sanitize_text_for_db(value)
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(
vector_result: SearchResult | None,
fts_results: list[dict[str, Any]],
num_queries: int,
limit: int,
hybrid_bm25_weight: float,
) -> SearchResult:
rank_constant = 60.0
bm25_weight = min(max(hybrid_bm25_weight, 0.0), 1.0)
vector_weight = 1.0 - bm25_weight
ids = [[] for _ in range(num_queries)]
distances = [[] for _ in range(num_queries)]
documents = [[] for _ in range(num_queries)]
metadatas = [[] for _ in range(num_queries)]
for qid in range(num_queries):
candidates: dict[str, dict[str, Any]] = {}
if vector_result and vector_result.ids and qid < len(vector_result.ids):
for rank, item_id in enumerate(vector_result.ids[qid] or [], start=1):
score = vector_weight / (rank_constant + rank) if vector_weight > 0 else 0
if score <= 0:
continue
candidate = candidates.setdefault(
item_id,
{
'score': 0.0,
'document': vector_result.documents[qid][rank - 1],
'metadata': vector_result.metadatas[qid][rank - 1],
},
)
candidate['score'] += score
for rank, row in enumerate(fts_results, start=1):
score = bm25_weight / (rank_constant + rank) if bm25_weight > 0 else 0
if score <= 0:
continue
item_id = row['id']
candidate = candidates.setdefault(
item_id,
{
'score': 0.0,
'document': row['text'],
'metadata': row['vmetadata'],
},
)
candidate['score'] += score
ranked = sorted(candidates.items(), key=lambda item: item[1]['score'], reverse=True)[:limit]
ids[qid] = [item_id for item_id, _ in ranked]
distances[qid] = [candidate['score'] for _, candidate in ranked]
documents[qid] = [candidate['document'] for _, candidate in ranked]
metadatas[qid] = [candidate['metadata'] for _, candidate in ranked]
return SearchResult(ids=ids, distances=distances, documents=documents, metadatas=metadatas)