This commit is contained in:
Timothy Jaeryang Baek
2026-06-22 16:10:19 +02:00
parent 88901bfa04
commit 223f484ded
6 changed files with 315 additions and 11 deletions
@@ -24,7 +24,7 @@ from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
)
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.retrieval.vector.utils import merge_hybrid_search_results, process_metadata
from open_webui.utils.misc import sanitize_text_for_db
from pgvector.sqlalchemy import HALFVEC, Vector
from sqlalchemy import (
@@ -153,6 +153,7 @@ class PgvectorClient(VectorDBBase):
index_method, index_options = self._vector_index_configuration()
self._ensure_vector_index(index_method, index_options)
self._ensure_text_search_index()
self.session.execute(
text(
@@ -236,6 +237,19 @@ class PgvectorClient(VectorDBBase):
f' {index_options}' if index_options else '',
)
def _ensure_text_search_index(self) -> None:
if PGVECTOR_PGCRYPTO:
return
self.session.execute(
text("""
CREATE INDEX IF NOT EXISTS idx_document_chunk_text_search
ON document_chunk
USING GIN (to_tsvector('simple', coalesce(text, '')));
""")
)
log.info("Ensured text search index 'idx_document_chunk_text_search'.")
def check_vector_length(self) -> None:
"""
Check if the VECTOR_LENGTH matches the existing vector column dimension in the database.
@@ -521,6 +535,71 @@ class PgvectorClient(VectorDBBase):
log.exception(f'Error during search: {e}')
return None
def hybrid_search(
self,
collection_name: str,
query: str,
vectors: List[List[float]],
filter: Optional[Dict[str, Any]] = None,
limit: int = 10,
hybrid_bm25_weight: float = 0.5,
) -> Optional[SearchResult]:
if PGVECTOR_PGCRYPTO or filter:
return None
try:
limit = max(1, limit)
vectors = [self.adjust_vector_length(vector) for vector in vectors] if vectors else []
num_queries = len(vectors) if vectors else 1
bm25_weight = min(max(hybrid_bm25_weight, 0.0), 1.0)
vector_weight = 1.0 - bm25_weight
vector_result = None
if vector_weight > 0 and vectors:
vector_result = self.search(collection_name=collection_name, vectors=vectors, limit=limit)
fts_results = []
if bm25_weight > 0 and query and query.strip():
fts_rows = self.session.execute(
text("""
WITH fts_query AS (
SELECT plainto_tsquery('simple', :query) AS query
)
SELECT
document_chunk.id AS id,
document_chunk.text AS text,
document_chunk.vmetadata AS vmetadata,
ts_rank_cd(
to_tsvector('simple', coalesce(document_chunk.text, '')),
fts_query.query
) AS rank
FROM document_chunk, fts_query
WHERE document_chunk.collection_name = :collection_name
AND to_tsvector('simple', coalesce(document_chunk.text, '')) @@ fts_query.query
ORDER BY rank DESC
LIMIT :limit
"""),
{
'collection_name': collection_name,
'query': query,
'limit': limit,
},
)
fts_results = [dict(row) for row in fts_rows.mappings().all()]
self.session.rollback()
return merge_hybrid_search_results(
vector_result=vector_result,
fts_results=fts_results,
num_queries=num_queries,
limit=limit,
hybrid_bm25_weight=hybrid_bm25_weight,
)
except Exception as e:
self.session.rollback()
log.exception(f'Error during hybrid search: {e}')
return None
def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]:
try:
if PGVECTOR_PGCRYPTO: