mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-27 09:46:07 -04:00
fix: build the pgvector ivfflat index once there are rows to cluster on (#30143)
ivfflat places its centroids by clustering the rows it can see when the index is built, so an index built on an empty table gets centroids that mean nothing, and everything inserted afterwards is filed against them. On a fresh install the index is created immediately after the table, before a single chunk exists, and it is never rebuilt, so that install keeps a permanently untrained index and quietly retrieves the wrong chunks. An install that upgraded into the version introducing the index is unaffected, its table already had rows. The index is now created once the table holds 50 rows per list, the sample size ivfflat itself aims for. Below that, and until the next start, searches fall back to an exact scan, which is correct and costs about a millisecond at that size. hnsw is untouched, it builds its graph as rows are inserted and has nothing to train on. One trade-off: the build moves from the first start to that later one, so an instance that has grown large in between pays a one-time index build during startup. Measured on PostgreSQL 17 and pgvector 0.8 with the default lists=100 and probes=1, 384 dimensions, recall@10 against an exact scan, varying only how many rows existed when the index was built: | rows at build | 200 | 1000 | 2500 | 5000 | 20000 | |---|---|---|---|---|---| | recall@10 | 0.180 | 0.563 | 0.967 | 1.000 | 1.000 | Through PgvectorClient.search on a 20000-row table, an index built as it is today scores 0.480 against 1.000 built after the rows arrive, at the same 5 ms. An existing install can repair its index with REINDEX INDEX idx_document_chunk_vector, measured to take it from 0.480 back to 1.000. Fixes #30134
This commit is contained in:
@@ -223,6 +223,9 @@ class PgvectorClient(VectorDBBase):
|
||||
)
|
||||
|
||||
if not existing_index_def:
|
||||
if index_method == 'ivfflat' and not self._has_enough_ivfflat_training_rows():
|
||||
return
|
||||
|
||||
index_sql = (
|
||||
f'CREATE INDEX IF NOT EXISTS {index_name} '
|
||||
f'ON document_chunk USING {index_method} (vector {VECTOR_OPCLASS})'
|
||||
@@ -237,6 +240,24 @@ class PgvectorClient(VectorDBBase):
|
||||
f' {index_options}' if index_options else '',
|
||||
)
|
||||
|
||||
def _has_enough_ivfflat_training_rows(self) -> bool:
|
||||
# ivfflat samples 50 rows per list to place its centroids, so recall stays poor until the table holds that many
|
||||
min_training_rows = 50 * PGVECTOR_IVFFLAT_LISTS
|
||||
row_count = self.session.execute(
|
||||
text('SELECT count(*) FROM (SELECT 1 FROM document_chunk LIMIT :min_training_rows) AS sample'),
|
||||
{'min_training_rows': min_training_rows},
|
||||
).scalar()
|
||||
|
||||
if row_count < min_training_rows:
|
||||
log.info(
|
||||
"Deferring vector index 'idx_document_chunk_vector' until document_chunk holds %s rows to cluster on, "
|
||||
'it has %s. Searches run as an exact scan until then.',
|
||||
min_training_rows,
|
||||
row_count,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _ensure_text_search_index(self) -> None:
|
||||
if PGVECTOR_PGCRYPTO:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user