From f25e1e80f6302af7cbefb81d9307db1c00007116 Mon Sep 17 00:00:00 2001 From: Chris Weaver <25087905+Weves@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:54:11 -0800 Subject: [PATCH] Add option to not re-index (#4157) * Add option to not re-index * Add quantizaton / dimensionality override support * Fix build / ut --- ...03_add_background_reindex_enabled_field.py | 55 +++ backend/model_server/encoders.py | 14 +- .../background/celery/tasks/indexing/tasks.py | 26 +- .../background/celery/tasks/indexing/utils.py | 7 +- backend/onyx/context/search/models.py | 4 + backend/onyx/db/enums.py | 11 + backend/onyx/db/models.py | 31 +- backend/onyx/db/search_settings.py | 6 + backend/onyx/db/swap_index.py | 95 +++-- backend/onyx/document_index/interfaces.py | 10 +- .../vespa/app_config/schemas/danswer_chunk.sd | 4 +- .../vespa/app_config/validation-overrides.xml | 3 + .../document_index/vespa/chunk_retrieval.py | 5 + backend/onyx/document_index/vespa/index.py | 52 ++- .../onyx/document_index/vespa_constants.py | 1 + backend/onyx/indexing/embedder.py | 5 + backend/onyx/indexing/models.py | 14 + .../search_nlp_models.py | 4 + backend/onyx/server/manage/search_settings.py | 23 +- backend/onyx/setup.py | 26 +- .../query_time_check/seed_dummy_docs.py | 2 +- backend/shared_configs/model_server_models.py | 6 + .../tests/integration/common_utils/reset.py | 6 +- .../tests/unit/model_server/test_embedding.py | 6 +- .../configuration/search/UpgradingPage.tsx | 74 ++-- .../pages/ConnectorInput/NumberInput.tsx | 19 +- .../pages/formelements/NumberInput.tsx | 42 --- .../EmbeddingModelSelectionForm.tsx | 40 +-- .../admin/embeddings/RerankingFormPage.tsx | 83 ++++- web/src/app/admin/embeddings/interfaces.ts | 8 + .../modals/InstantSwitchConfirmModal.tsx | 37 ++ .../embeddings/modals/ModelSelectionModal.tsx | 7 +- .../embeddings/modals/SelectModelModal.tsx | 15 +- .../pages/AdvancedEmbeddingFormPage.tsx | 279 +++++++++----- .../embeddings/pages/EmbeddingFormPage.tsx | 340 +++++++++++++++--- .../embeddings/pages/OpenEmbeddingPage.tsx | 2 +- web/src/app/admin/embeddings/pages/utils.ts | 4 +- web/src/components/admin/connectors/Field.tsx | 10 +- .../embedding/ReindexingProgressTable.tsx | 1 + web/src/components/embedding/interfaces.tsx | 1 + 40 files changed, 1020 insertions(+), 358 deletions(-) create mode 100644 backend/alembic/versions/b7c2b63c4a03_add_background_reindex_enabled_field.py delete mode 100644 web/src/app/admin/connectors/[connector]/pages/formelements/NumberInput.tsx create mode 100644 web/src/app/admin/embeddings/modals/InstantSwitchConfirmModal.tsx diff --git a/backend/alembic/versions/b7c2b63c4a03_add_background_reindex_enabled_field.py b/backend/alembic/versions/b7c2b63c4a03_add_background_reindex_enabled_field.py new file mode 100644 index 0000000000..cf31f9f270 --- /dev/null +++ b/backend/alembic/versions/b7c2b63c4a03_add_background_reindex_enabled_field.py @@ -0,0 +1,55 @@ +"""add background_reindex_enabled field + +Revision ID: b7c2b63c4a03 +Revises: f11b408e39d3 +Create Date: 2024-03-26 12:34:56.789012 + +""" +from alembic import op +import sqlalchemy as sa + +from onyx.db.enums import EmbeddingPrecision + + +# revision identifiers, used by Alembic. +revision = "b7c2b63c4a03" +down_revision = "f11b408e39d3" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add background_reindex_enabled column with default value of True + op.add_column( + "search_settings", + sa.Column( + "background_reindex_enabled", + sa.Boolean(), + nullable=False, + server_default="true", + ), + ) + + # Add embedding_precision column with default value of FLOAT + op.add_column( + "search_settings", + sa.Column( + "embedding_precision", + sa.Enum(EmbeddingPrecision, native_enum=False), + nullable=False, + server_default=EmbeddingPrecision.FLOAT.name, + ), + ) + + # Add reduced_dimension column with default value of None + op.add_column( + "search_settings", + sa.Column("reduced_dimension", sa.Integer(), nullable=True), + ) + + +def downgrade() -> None: + # Remove the background_reindex_enabled column + op.drop_column("search_settings", "background_reindex_enabled") + op.drop_column("search_settings", "embedding_precision") + op.drop_column("search_settings", "reduced_dimension") diff --git a/backend/model_server/encoders.py b/backend/model_server/encoders.py index 8521cd0010..9215042f1f 100644 --- a/backend/model_server/encoders.py +++ b/backend/model_server/encoders.py @@ -78,7 +78,7 @@ class CloudEmbedding: self._closed = False async def _embed_openai( - self, texts: list[str], model: str | None + self, texts: list[str], model: str | None, reduced_dimension: int | None ) -> list[Embedding]: if not model: model = DEFAULT_OPENAI_MODEL @@ -91,7 +91,11 @@ class CloudEmbedding: final_embeddings: list[Embedding] = [] try: for text_batch in batch_list(texts, _OPENAI_MAX_INPUT_LEN): - response = await client.embeddings.create(input=text_batch, model=model) + response = await client.embeddings.create( + input=text_batch, + model=model, + dimensions=reduced_dimension or openai.NOT_GIVEN, + ) final_embeddings.extend( [embedding.embedding for embedding in response.data] ) @@ -223,9 +227,10 @@ class CloudEmbedding: text_type: EmbedTextType, model_name: str | None = None, deployment_name: str | None = None, + reduced_dimension: int | None = None, ) -> list[Embedding]: if self.provider == EmbeddingProvider.OPENAI: - return await self._embed_openai(texts, model_name) + return await self._embed_openai(texts, model_name, reduced_dimension) elif self.provider == EmbeddingProvider.AZURE: return await self._embed_azure(texts, f"azure/{deployment_name}") elif self.provider == EmbeddingProvider.LITELLM: @@ -326,6 +331,7 @@ async def embed_text( prefix: str | None, api_url: str | None, api_version: str | None, + reduced_dimension: int | None, gpu_type: str = "UNKNOWN", ) -> list[Embedding]: if not all(texts): @@ -369,6 +375,7 @@ async def embed_text( model_name=model_name, deployment_name=deployment_name, text_type=text_type, + reduced_dimension=reduced_dimension, ) if any(embedding is None for embedding in embeddings): @@ -508,6 +515,7 @@ async def process_embed_request( text_type=embed_request.text_type, api_url=embed_request.api_url, api_version=embed_request.api_version, + reduced_dimension=embed_request.reduced_dimension, prefix=prefix, gpu_type=gpu_type, ) diff --git a/backend/onyx/background/celery/tasks/indexing/tasks.py b/backend/onyx/background/celery/tasks/indexing/tasks.py index e8063e5750..76c327bb54 100644 --- a/backend/onyx/background/celery/tasks/indexing/tasks.py +++ b/backend/onyx/background/celery/tasks/indexing/tasks.py @@ -23,9 +23,9 @@ from sqlalchemy.orm import Session from onyx.background.celery.apps.app_base import task_logger from onyx.background.celery.celery_utils import httpx_init_vespa_pool -from onyx.background.celery.tasks.indexing.utils import _should_index from onyx.background.celery.tasks.indexing.utils import get_unfenced_index_attempt_ids from onyx.background.celery.tasks.indexing.utils import IndexingCallback +from onyx.background.celery.tasks.indexing.utils import should_index from onyx.background.celery.tasks.indexing.utils import try_creating_indexing_task from onyx.background.celery.tasks.indexing.utils import validate_indexing_fences from onyx.background.indexing.checkpointing_utils import cleanup_checkpoint @@ -61,7 +61,7 @@ from onyx.db.index_attempt import mark_attempt_canceled from onyx.db.index_attempt import mark_attempt_failed from onyx.db.search_settings import get_active_search_settings_list from onyx.db.search_settings import get_current_search_settings -from onyx.db.swap_index import check_index_swap +from onyx.db.swap_index import check_and_perform_index_swap from onyx.natural_language_processing.search_nlp_models import EmbeddingModel from onyx.natural_language_processing.search_nlp_models import warm_up_bi_encoder from onyx.redis.redis_connector import RedisConnector @@ -406,7 +406,7 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None: # check for search settings swap with get_session_with_current_tenant() as db_session: - old_search_settings = check_index_swap(db_session=db_session) + old_search_settings = check_and_perform_index_swap(db_session=db_session) current_search_settings = get_current_search_settings(db_session) # So that the first time users aren't surprised by really slow speed of first # batch of documents indexed @@ -439,6 +439,15 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None: with get_session_with_current_tenant() as db_session: search_settings_list = get_active_search_settings_list(db_session) for search_settings_instance in search_settings_list: + # skip non-live search settings that don't have background reindex enabled + # those should just auto-change to live shortly after creation without + # requiring any indexing till that point + if ( + not search_settings_instance.status.is_current() + and not search_settings_instance.background_reindex_enabled + ): + continue + redis_connector_index = redis_connector.new_index( search_settings_instance.id ) @@ -456,23 +465,18 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None: cc_pair.id, search_settings_instance.id, db_session ) - search_settings_primary = False - if search_settings_instance.id == search_settings_list[0].id: - search_settings_primary = True - - if not _should_index( + if not should_index( cc_pair=cc_pair, last_index=last_attempt, search_settings_instance=search_settings_instance, - search_settings_primary=search_settings_primary, secondary_index_building=len(search_settings_list) > 1, db_session=db_session, ): continue reindex = False - if search_settings_instance.id == search_settings_list[0].id: - # the indexing trigger is only checked and cleared with the primary search settings + if search_settings_instance.status.is_current(): + # the indexing trigger is only checked and cleared with the current search settings if cc_pair.indexing_trigger is not None: if cc_pair.indexing_trigger == IndexingMode.REINDEX: reindex = True diff --git a/backend/onyx/background/celery/tasks/indexing/utils.py b/backend/onyx/background/celery/tasks/indexing/utils.py index cfb528799e..dbe425f938 100644 --- a/backend/onyx/background/celery/tasks/indexing/utils.py +++ b/backend/onyx/background/celery/tasks/indexing/utils.py @@ -346,11 +346,10 @@ def validate_indexing_fences( return -def _should_index( +def should_index( cc_pair: ConnectorCredentialPair, last_index: IndexAttempt | None, search_settings_instance: SearchSettings, - search_settings_primary: bool, secondary_index_building: bool, db_session: Session, ) -> bool: @@ -415,9 +414,9 @@ def _should_index( ): return False - if search_settings_primary: + if search_settings_instance.status.is_current(): if cc_pair.indexing_trigger is not None: - # if a manual indexing trigger is on the cc pair, honor it for primary search settings + # if a manual indexing trigger is on the cc pair, honor it for live search settings return True # if no attempt has ever occurred, we should index regardless of refresh_freq diff --git a/backend/onyx/context/search/models.py b/backend/onyx/context/search/models.py index 7eeb356869..3d19db1865 100644 --- a/backend/onyx/context/search/models.py +++ b/backend/onyx/context/search/models.py @@ -76,6 +76,10 @@ class SavedSearchSettings(InferenceSettings, IndexingSetting): provider_type=search_settings.provider_type, index_name=search_settings.index_name, multipass_indexing=search_settings.multipass_indexing, + embedding_precision=search_settings.embedding_precision, + reduced_dimension=search_settings.reduced_dimension, + # Whether switching to this model requires re-indexing + background_reindex_enabled=search_settings.background_reindex_enabled, # Reranking Details rerank_model_name=search_settings.rerank_model_name, rerank_provider_type=search_settings.rerank_provider_type, diff --git a/backend/onyx/db/enums.py b/backend/onyx/db/enums.py index 329ee35fcd..c5a3ced2fd 100644 --- a/backend/onyx/db/enums.py +++ b/backend/onyx/db/enums.py @@ -63,6 +63,9 @@ class IndexModelStatus(str, PyEnum): PRESENT = "PRESENT" FUTURE = "FUTURE" + def is_current(self) -> bool: + return self == IndexModelStatus.PRESENT + class ChatSessionSharedStatus(str, PyEnum): PUBLIC = "public" @@ -83,3 +86,11 @@ class AccessType(str, PyEnum): PUBLIC = "public" PRIVATE = "private" SYNC = "sync" + + +class EmbeddingPrecision(str, PyEnum): + # matches vespa tensor type + # only support float / bfloat16 for now, since there's not a + # good reason to specify anything else + BFLOAT16 = "bfloat16" + FLOAT = "float" diff --git a/backend/onyx/db/models.py b/backend/onyx/db/models.py index 6c6eadcb8e..2da8be9eca 100644 --- a/backend/onyx/db/models.py +++ b/backend/onyx/db/models.py @@ -46,7 +46,13 @@ from onyx.configs.constants import DEFAULT_BOOST, MilestoneRecordType from onyx.configs.constants import DocumentSource from onyx.configs.constants import FileOrigin from onyx.configs.constants import MessageType -from onyx.db.enums import AccessType, IndexingMode, SyncType, SyncStatus +from onyx.db.enums import ( + AccessType, + EmbeddingPrecision, + IndexingMode, + SyncType, + SyncStatus, +) from onyx.configs.constants import NotificationType from onyx.configs.constants import SearchFeedbackType from onyx.configs.constants import TokenRateLimitScope @@ -716,6 +722,23 @@ class SearchSettings(Base): ForeignKey("embedding_provider.provider_type"), nullable=True ) + # Whether switching to this model should re-index all connectors in the background + # if no re-index is needed, will be ignored. Only used during the switch-over process. + background_reindex_enabled: Mapped[bool] = mapped_column(Boolean, default=True) + + # allows for quantization -> less memory usage for a small performance hit + embedding_precision: Mapped[EmbeddingPrecision] = mapped_column( + Enum(EmbeddingPrecision, native_enum=False) + ) + + # can be used to reduce dimensionality of vectors and save memory with + # a small performance hit. More details in the `Reducing embedding dimensions` + # section here: + # https://platform.openai.com/docs/guides/embeddings#embedding-models + # If not specified, will just use the model_dim without any reduction. + # NOTE: this is only currently available for OpenAI models + reduced_dimension: Mapped[int | None] = mapped_column(Integer, nullable=True) + # Mini and Large Chunks (large chunk also checks for model max context) multipass_indexing: Mapped[bool] = mapped_column(Boolean, default=True) @@ -797,6 +820,12 @@ class SearchSettings(Base): self.multipass_indexing, self.model_name, self.provider_type ) + @property + def final_embedding_dim(self) -> int: + if self.reduced_dimension: + return self.reduced_dimension + return self.model_dim + @staticmethod def can_use_large_chunks( multipass: bool, model_name: str, provider_type: EmbeddingProvider | None diff --git a/backend/onyx/db/search_settings.py b/backend/onyx/db/search_settings.py index 444df08c03..bddaf21158 100644 --- a/backend/onyx/db/search_settings.py +++ b/backend/onyx/db/search_settings.py @@ -14,6 +14,7 @@ from onyx.configs.model_configs import OLD_DEFAULT_MODEL_DOC_EMBEDDING_DIM from onyx.configs.model_configs import OLD_DEFAULT_MODEL_NORMALIZE_EMBEDDINGS from onyx.context.search.models import SavedSearchSettings from onyx.db.engine import get_session_with_current_tenant +from onyx.db.enums import EmbeddingPrecision from onyx.db.llm import fetch_embedding_provider from onyx.db.models import CloudEmbeddingProvider from onyx.db.models import IndexAttempt @@ -59,12 +60,15 @@ def create_search_settings( index_name=search_settings.index_name, provider_type=search_settings.provider_type, multipass_indexing=search_settings.multipass_indexing, + embedding_precision=search_settings.embedding_precision, + reduced_dimension=search_settings.reduced_dimension, multilingual_expansion=search_settings.multilingual_expansion, disable_rerank_for_streaming=search_settings.disable_rerank_for_streaming, rerank_model_name=search_settings.rerank_model_name, rerank_provider_type=search_settings.rerank_provider_type, rerank_api_key=search_settings.rerank_api_key, num_rerank=search_settings.num_rerank, + background_reindex_enabled=search_settings.background_reindex_enabled, ) db_session.add(embedding_model) @@ -305,6 +309,7 @@ def get_old_default_embedding_model() -> IndexingSetting: model_dim=( DOC_EMBEDDING_DIM if is_overridden else OLD_DEFAULT_MODEL_DOC_EMBEDDING_DIM ), + embedding_precision=(EmbeddingPrecision.FLOAT), normalize=( NORMALIZE_EMBEDDINGS if is_overridden @@ -322,6 +327,7 @@ def get_new_default_embedding_model() -> IndexingSetting: return IndexingSetting( model_name=DOCUMENT_ENCODER_MODEL, model_dim=DOC_EMBEDDING_DIM, + embedding_precision=(EmbeddingPrecision.FLOAT), normalize=NORMALIZE_EMBEDDINGS, query_prefix=ASYM_QUERY_PREFIX, passage_prefix=ASYM_PASSAGE_PREFIX, diff --git a/backend/onyx/db/swap_index.py b/backend/onyx/db/swap_index.py index abe7bdaf59..6e5472ea9d 100644 --- a/backend/onyx/db/swap_index.py +++ b/backend/onyx/db/swap_index.py @@ -8,10 +8,12 @@ from onyx.db.index_attempt import cancel_indexing_attempts_past_model from onyx.db.index_attempt import ( count_unique_cc_pairs_with_successful_index_attempts, ) +from onyx.db.models import ConnectorCredentialPair from onyx.db.models import SearchSettings from onyx.db.search_settings import get_current_search_settings from onyx.db.search_settings import get_secondary_search_settings from onyx.db.search_settings import update_search_settings_status +from onyx.document_index.factory import get_default_document_index from onyx.key_value_store.factory import get_kv_store from onyx.utils.logger import setup_logger @@ -19,7 +21,49 @@ from onyx.utils.logger import setup_logger logger = setup_logger() -def check_index_swap(db_session: Session) -> SearchSettings | None: +def _perform_index_swap( + db_session: Session, + current_search_settings: SearchSettings, + secondary_search_settings: SearchSettings, + all_cc_pairs: list[ConnectorCredentialPair], +) -> None: + """Swap the indices and expire the old one.""" + current_search_settings = get_current_search_settings(db_session) + update_search_settings_status( + search_settings=current_search_settings, + new_status=IndexModelStatus.PAST, + db_session=db_session, + ) + + update_search_settings_status( + search_settings=secondary_search_settings, + new_status=IndexModelStatus.PRESENT, + db_session=db_session, + ) + + if len(all_cc_pairs) > 0: + kv_store = get_kv_store() + kv_store.store(KV_REINDEX_KEY, False) + + # Expire jobs for the now past index/embedding model + cancel_indexing_attempts_past_model(db_session) + + # Recount aggregates + for cc_pair in all_cc_pairs: + resync_cc_pair(cc_pair, db_session=db_session) + + # remove the old index from the vector db + document_index = get_default_document_index(secondary_search_settings, None) + document_index.ensure_indices_exist( + primary_embedding_dim=secondary_search_settings.final_embedding_dim, + primary_embedding_precision=secondary_search_settings.embedding_precision, + # just finished swap, no more secondary index + secondary_index_embedding_dim=None, + secondary_index_embedding_precision=None, + ) + + +def check_and_perform_index_swap(db_session: Session) -> SearchSettings | None: """Get count of cc-pairs and count of successful index_attempts for the new model grouped by connector + credential, if it's the same, then assume new index is done building. If so, swap the indices and expire the old one. @@ -27,52 +71,45 @@ def check_index_swap(db_session: Session) -> SearchSettings | None: Returns None if search settings did not change, or the old search settings if they did change. """ - - old_search_settings = None - # Default CC-pair created for Ingestion API unused here all_cc_pairs = get_connector_credential_pairs(db_session) cc_pair_count = max(len(all_cc_pairs) - 1, 0) - search_settings = get_secondary_search_settings(db_session) + secondary_search_settings = get_secondary_search_settings(db_session) - if not search_settings: + if not secondary_search_settings: return None + # If the secondary search settings are not configured to reindex in the background, + # we can just swap over instantly + if not secondary_search_settings.background_reindex_enabled: + current_search_settings = get_current_search_settings(db_session) + _perform_index_swap( + db_session=db_session, + current_search_settings=current_search_settings, + secondary_search_settings=secondary_search_settings, + all_cc_pairs=all_cc_pairs, + ) + return current_search_settings + unique_cc_indexings = count_unique_cc_pairs_with_successful_index_attempts( - search_settings_id=search_settings.id, db_session=db_session + search_settings_id=secondary_search_settings.id, db_session=db_session ) # Index Attempts are cleaned up as well when the cc-pair is deleted so the logic in this # function is correct. The unique_cc_indexings are specifically for the existing cc-pairs + old_search_settings = None if unique_cc_indexings > cc_pair_count: logger.error("More unique indexings than cc pairs, should not occur") if cc_pair_count == 0 or cc_pair_count == unique_cc_indexings: # Swap indices current_search_settings = get_current_search_settings(db_session) - update_search_settings_status( - search_settings=current_search_settings, - new_status=IndexModelStatus.PAST, + _perform_index_swap( db_session=db_session, + current_search_settings=current_search_settings, + secondary_search_settings=secondary_search_settings, + all_cc_pairs=all_cc_pairs, ) - - update_search_settings_status( - search_settings=search_settings, - new_status=IndexModelStatus.PRESENT, - db_session=db_session, - ) - - if cc_pair_count > 0: - kv_store = get_kv_store() - kv_store.store(KV_REINDEX_KEY, False) - - # Expire jobs for the now past index/embedding model - cancel_indexing_attempts_past_model(db_session) - - # Recount aggregates - for cc_pair in all_cc_pairs: - resync_cc_pair(cc_pair, db_session=db_session) - - old_search_settings = current_search_settings + old_search_settings = current_search_settings return old_search_settings diff --git a/backend/onyx/document_index/interfaces.py b/backend/onyx/document_index/interfaces.py index 663e5feeeb..463abbc951 100644 --- a/backend/onyx/document_index/interfaces.py +++ b/backend/onyx/document_index/interfaces.py @@ -6,6 +6,7 @@ from typing import Any from onyx.access.models import DocumentAccess from onyx.context.search.models import IndexFilters from onyx.context.search.models import InferenceChunkUncleaned +from onyx.db.enums import EmbeddingPrecision from onyx.indexing.models import DocMetadataAwareIndexChunk from shared_configs.model_server_models import Embedding @@ -145,17 +146,21 @@ class Verifiable(abc.ABC): @abc.abstractmethod def ensure_indices_exist( self, - index_embedding_dim: int, + primary_embedding_dim: int, + primary_embedding_precision: EmbeddingPrecision, secondary_index_embedding_dim: int | None, + secondary_index_embedding_precision: EmbeddingPrecision | None, ) -> None: """ Verify that the document index exists and is consistent with the expectations in the code. Parameters: - - index_embedding_dim: Vector dimensionality for the vector similarity part of the search + - primary_embedding_dim: Vector dimensionality for the vector similarity part of the search + - primary_embedding_precision: Precision of the vector similarity part of the search - secondary_index_embedding_dim: Vector dimensionality of the secondary index being built behind the scenes. The secondary index should only be built when switching embedding models therefore this dim should be different from the primary index. + - secondary_index_embedding_precision: Precision of the vector similarity part of the secondary index """ raise NotImplementedError @@ -164,6 +169,7 @@ class Verifiable(abc.ABC): def register_multitenant_indices( indices: list[str], embedding_dims: list[int], + embedding_precisions: list[EmbeddingPrecision], ) -> None: """ Register multitenant indices with the document index. diff --git a/backend/onyx/document_index/vespa/app_config/schemas/danswer_chunk.sd b/backend/onyx/document_index/vespa/app_config/schemas/danswer_chunk.sd index 2fd861b779..f846c32fca 100644 --- a/backend/onyx/document_index/vespa/app_config/schemas/danswer_chunk.sd +++ b/backend/onyx/document_index/vespa/app_config/schemas/danswer_chunk.sd @@ -37,7 +37,7 @@ schema DANSWER_CHUNK_NAME { summary: dynamic } # Title embedding (x1) - field title_embedding type tensor(x[VARIABLE_DIM]) { + field title_embedding type tensor(x[VARIABLE_DIM]) { indexing: attribute | index attribute { distance-metric: angular @@ -45,7 +45,7 @@ schema DANSWER_CHUNK_NAME { } # Content embeddings (chunk + optional mini chunks embeddings) # "t" and "x" are arbitrary names, not special keywords - field embeddings type tensor(t{},x[VARIABLE_DIM]) { + field embeddings type tensor(t{},x[VARIABLE_DIM]) { indexing: attribute | index attribute { distance-metric: angular diff --git a/backend/onyx/document_index/vespa/app_config/validation-overrides.xml b/backend/onyx/document_index/vespa/app_config/validation-overrides.xml index c5d1598bfc..7b0709620a 100644 --- a/backend/onyx/document_index/vespa/app_config/validation-overrides.xml +++ b/backend/onyx/document_index/vespa/app_config/validation-overrides.xml @@ -5,4 +5,7 @@ indexing-change + field-type-change diff --git a/backend/onyx/document_index/vespa/chunk_retrieval.py b/backend/onyx/document_index/vespa/chunk_retrieval.py index 37225b4520..5f3dff5c8e 100644 --- a/backend/onyx/document_index/vespa/chunk_retrieval.py +++ b/backend/onyx/document_index/vespa/chunk_retrieval.py @@ -310,6 +310,11 @@ def query_vespa( f"Request Headers: {e.request.headers}\n" f"Request Payload: {params}\n" f"Exception: {str(e)}" + + ( + f"\nResponse: {e.response.text}" + if isinstance(e, httpx.HTTPStatusError) + else "" + ) ) raise httpx.HTTPError(error_base) from e diff --git a/backend/onyx/document_index/vespa/index.py b/backend/onyx/document_index/vespa/index.py index c2e631f6c8..17aadb36c4 100644 --- a/backend/onyx/document_index/vespa/index.py +++ b/backend/onyx/document_index/vespa/index.py @@ -26,6 +26,7 @@ from onyx.configs.chat_configs import VESPA_SEARCHER_THREADS from onyx.configs.constants import KV_REINDEX_KEY from onyx.context.search.models import IndexFilters from onyx.context.search.models import InferenceChunkUncleaned +from onyx.db.enums import EmbeddingPrecision from onyx.document_index.document_index_utils import get_document_chunk_ids from onyx.document_index.interfaces import DocumentIndex from onyx.document_index.interfaces import DocumentInsertionRecord @@ -63,6 +64,7 @@ from onyx.document_index.vespa_constants import DATE_REPLACEMENT from onyx.document_index.vespa_constants import DOCUMENT_ID_ENDPOINT from onyx.document_index.vespa_constants import DOCUMENT_REPLACEMENT_PAT from onyx.document_index.vespa_constants import DOCUMENT_SETS +from onyx.document_index.vespa_constants import EMBEDDING_PRECISION_REPLACEMENT_PAT from onyx.document_index.vespa_constants import HIDDEN from onyx.document_index.vespa_constants import NUM_THREADS from onyx.document_index.vespa_constants import SEARCH_THREAD_NUMBER_PAT @@ -112,6 +114,21 @@ def _create_document_xml_lines(doc_names: list[str | None] | list[str]) -> str: return "\n".join(doc_lines) +def _replace_template_values_in_schema( + schema_template: str, + index_name: str, + embedding_dim: int, + embedding_precision: EmbeddingPrecision, +) -> str: + return ( + schema_template.replace( + EMBEDDING_PRECISION_REPLACEMENT_PAT, embedding_precision.value + ) + .replace(DANSWER_CHUNK_REPLACEMENT_PAT, index_name) + .replace(VESPA_DIM_REPLACEMENT_PAT, str(embedding_dim)) + ) + + def add_ngrams_to_schema(schema_content: str) -> str: # Add the match blocks containing gram and gram-size to title and content fields schema_content = re.sub( @@ -163,8 +180,10 @@ class VespaIndex(DocumentIndex): def ensure_indices_exist( self, - index_embedding_dim: int, + primary_embedding_dim: int, + primary_embedding_precision: EmbeddingPrecision, secondary_index_embedding_dim: int | None, + secondary_index_embedding_precision: EmbeddingPrecision | None, ) -> None: if MULTI_TENANT: logger.info( @@ -221,18 +240,29 @@ class VespaIndex(DocumentIndex): schema_template = schema_f.read() schema_template = schema_template.replace(TENANT_ID_PAT, "") - schema = schema_template.replace( - DANSWER_CHUNK_REPLACEMENT_PAT, self.index_name - ).replace(VESPA_DIM_REPLACEMENT_PAT, str(index_embedding_dim)) + schema = _replace_template_values_in_schema( + schema_template, + self.index_name, + primary_embedding_dim, + primary_embedding_precision, + ) schema = add_ngrams_to_schema(schema) if needs_reindexing else schema schema = schema.replace(TENANT_ID_PAT, "") zip_dict[f"schemas/{schema_names[0]}.sd"] = schema.encode("utf-8") if self.secondary_index_name: - upcoming_schema = schema_template.replace( - DANSWER_CHUNK_REPLACEMENT_PAT, self.secondary_index_name - ).replace(VESPA_DIM_REPLACEMENT_PAT, str(secondary_index_embedding_dim)) + if secondary_index_embedding_dim is None: + raise ValueError("Secondary index embedding dimension is required") + if secondary_index_embedding_precision is None: + raise ValueError("Secondary index embedding precision is required") + + upcoming_schema = _replace_template_values_in_schema( + schema_template, + self.secondary_index_name, + secondary_index_embedding_dim, + secondary_index_embedding_precision, + ) zip_dict[f"schemas/{schema_names[1]}.sd"] = upcoming_schema.encode("utf-8") zip_file = in_memory_zip_from_file_bytes(zip_dict) @@ -251,6 +281,7 @@ class VespaIndex(DocumentIndex): def register_multitenant_indices( indices: list[str], embedding_dims: list[int], + embedding_precisions: list[EmbeddingPrecision], ) -> None: if not MULTI_TENANT: raise ValueError("Multi-tenant is not enabled") @@ -309,13 +340,14 @@ class VespaIndex(DocumentIndex): for i, index_name in enumerate(indices): embedding_dim = embedding_dims[i] + embedding_precision = embedding_precisions[i] logger.info( f"Creating index: {index_name} with embedding dimension: {embedding_dim}" ) - schema = schema_template.replace( - DANSWER_CHUNK_REPLACEMENT_PAT, index_name - ).replace(VESPA_DIM_REPLACEMENT_PAT, str(embedding_dim)) + schema = _replace_template_values_in_schema( + schema_template, index_name, embedding_dim, embedding_precision + ) schema = schema.replace( TENANT_ID_PAT, TENANT_ID_REPLACEMENT if MULTI_TENANT else "" ) diff --git a/backend/onyx/document_index/vespa_constants.py b/backend/onyx/document_index/vespa_constants.py index a259aede5c..82bb591983 100644 --- a/backend/onyx/document_index/vespa_constants.py +++ b/backend/onyx/document_index/vespa_constants.py @@ -6,6 +6,7 @@ from onyx.configs.app_configs import VESPA_TENANT_PORT from onyx.configs.constants import SOURCE_TYPE VESPA_DIM_REPLACEMENT_PAT = "VARIABLE_DIM" +EMBEDDING_PRECISION_REPLACEMENT_PAT = "EMBEDDING_PRECISION" DANSWER_CHUNK_REPLACEMENT_PAT = "DANSWER_CHUNK_NAME" DOCUMENT_REPLACEMENT_PAT = "DOCUMENT_REPLACEMENT" SEARCH_THREAD_NUMBER_PAT = "SEARCH_THREAD_NUMBER" diff --git a/backend/onyx/indexing/embedder.py b/backend/onyx/indexing/embedder.py index a692827c5f..67bf56fc89 100644 --- a/backend/onyx/indexing/embedder.py +++ b/backend/onyx/indexing/embedder.py @@ -38,6 +38,7 @@ class IndexingEmbedder(ABC): api_url: str | None, api_version: str | None, deployment_name: str | None, + reduced_dimension: int | None, callback: IndexingHeartbeatInterface | None, ): self.model_name = model_name @@ -60,6 +61,7 @@ class IndexingEmbedder(ABC): api_url=api_url, api_version=api_version, deployment_name=deployment_name, + reduced_dimension=reduced_dimension, # The below are globally set, this flow always uses the indexing one server_host=INDEXING_MODEL_SERVER_HOST, server_port=INDEXING_MODEL_SERVER_PORT, @@ -87,6 +89,7 @@ class DefaultIndexingEmbedder(IndexingEmbedder): api_url: str | None = None, api_version: str | None = None, deployment_name: str | None = None, + reduced_dimension: int | None = None, callback: IndexingHeartbeatInterface | None = None, ): super().__init__( @@ -99,6 +102,7 @@ class DefaultIndexingEmbedder(IndexingEmbedder): api_url, api_version, deployment_name, + reduced_dimension, callback, ) @@ -219,6 +223,7 @@ class DefaultIndexingEmbedder(IndexingEmbedder): api_url=search_settings.api_url, api_version=search_settings.api_version, deployment_name=search_settings.deployment_name, + reduced_dimension=search_settings.reduced_dimension, callback=callback, ) diff --git a/backend/onyx/indexing/models.py b/backend/onyx/indexing/models.py index 0c4451cc7a..cffbdaa9bb 100644 --- a/backend/onyx/indexing/models.py +++ b/backend/onyx/indexing/models.py @@ -5,6 +5,7 @@ from pydantic import Field from onyx.access.models import DocumentAccess from onyx.connectors.models import Document +from onyx.db.enums import EmbeddingPrecision from onyx.utils.logger import setup_logger from shared_configs.enums import EmbeddingProvider from shared_configs.model_server_models import Embedding @@ -143,10 +144,20 @@ class IndexingSetting(EmbeddingModelDetail): model_dim: int index_name: str | None multipass_indexing: bool + embedding_precision: EmbeddingPrecision + reduced_dimension: int | None = None + + background_reindex_enabled: bool = True # This disables the "model_" protected namespace for pydantic model_config = {"protected_namespaces": ()} + @property + def final_embedding_dim(self) -> int: + if self.reduced_dimension: + return self.reduced_dimension + return self.model_dim + @classmethod def from_db_model(cls, search_settings: "SearchSettings") -> "IndexingSetting": return cls( @@ -158,6 +169,9 @@ class IndexingSetting(EmbeddingModelDetail): provider_type=search_settings.provider_type, index_name=search_settings.index_name, multipass_indexing=search_settings.multipass_indexing, + embedding_precision=search_settings.embedding_precision, + reduced_dimension=search_settings.reduced_dimension, + background_reindex_enabled=search_settings.background_reindex_enabled, ) diff --git a/backend/onyx/natural_language_processing/search_nlp_models.py b/backend/onyx/natural_language_processing/search_nlp_models.py index 5f1f2d59a2..3a7fcdf6fa 100644 --- a/backend/onyx/natural_language_processing/search_nlp_models.py +++ b/backend/onyx/natural_language_processing/search_nlp_models.py @@ -89,6 +89,7 @@ class EmbeddingModel: callback: IndexingHeartbeatInterface | None = None, api_version: str | None = None, deployment_name: str | None = None, + reduced_dimension: int | None = None, ) -> None: self.api_key = api_key self.provider_type = provider_type @@ -100,6 +101,7 @@ class EmbeddingModel: self.api_url = api_url self.api_version = api_version self.deployment_name = deployment_name + self.reduced_dimension = reduced_dimension self.tokenizer = get_tokenizer( model_name=model_name, provider_type=provider_type ) @@ -188,6 +190,7 @@ class EmbeddingModel: manual_query_prefix=self.query_prefix, manual_passage_prefix=self.passage_prefix, api_url=self.api_url, + reduced_dimension=self.reduced_dimension, ) start_time = time.time() @@ -300,6 +303,7 @@ class EmbeddingModel: retrim_content=retrim_content, api_version=search_settings.api_version, deployment_name=search_settings.deployment_name, + reduced_dimension=search_settings.reduced_dimension, ) diff --git a/backend/onyx/server/manage/search_settings.py b/backend/onyx/server/manage/search_settings.py index 22012fb69a..f059d8553d 100644 --- a/backend/onyx/server/manage/search_settings.py +++ b/backend/onyx/server/manage/search_settings.py @@ -72,11 +72,13 @@ def set_new_search_settings( and not search_settings.index_name.endswith(ALT_INDEX_SUFFIX) ): index_name += ALT_INDEX_SUFFIX - search_values = search_settings_new.dict() + search_values = search_settings_new.model_dump() search_values["index_name"] = index_name new_search_settings_request = SavedSearchSettings(**search_values) else: - new_search_settings_request = SavedSearchSettings(**search_settings_new.dict()) + new_search_settings_request = SavedSearchSettings( + **search_settings_new.model_dump() + ) secondary_search_settings = get_secondary_search_settings(db_session) @@ -103,8 +105,10 @@ def set_new_search_settings( document_index = get_default_document_index(search_settings, new_search_settings) document_index.ensure_indices_exist( - index_embedding_dim=search_settings.model_dim, - secondary_index_embedding_dim=new_search_settings.model_dim, + primary_embedding_dim=search_settings.final_embedding_dim, + primary_embedding_precision=search_settings.embedding_precision, + secondary_index_embedding_dim=new_search_settings.final_embedding_dim, + secondary_index_embedding_precision=new_search_settings.embedding_precision, ) # Pause index attempts for the currently in use index to preserve resources @@ -137,6 +141,17 @@ def cancel_new_embedding( db_session=db_session, ) + # remove the old index from the vector db + primary_search_settings = get_current_search_settings(db_session) + document_index = get_default_document_index(primary_search_settings, None) + document_index.ensure_indices_exist( + primary_embedding_dim=primary_search_settings.final_embedding_dim, + primary_embedding_precision=primary_search_settings.embedding_precision, + # just finished swap, no more secondary index + secondary_index_embedding_dim=None, + secondary_index_embedding_precision=None, + ) + @router.delete("/delete-search-settings") def delete_search_settings_endpoint( diff --git a/backend/onyx/setup.py b/backend/onyx/setup.py index fc4b9f9834..1dff601ef7 100644 --- a/backend/onyx/setup.py +++ b/backend/onyx/setup.py @@ -21,6 +21,7 @@ from onyx.db.connector_credential_pair import get_connector_credential_pairs from onyx.db.connector_credential_pair import resync_cc_pair from onyx.db.credentials import create_initial_public_credential from onyx.db.document import check_docs_exist +from onyx.db.enums import EmbeddingPrecision from onyx.db.index_attempt import cancel_indexing_attempts_past_model from onyx.db.index_attempt import expire_index_attempts from onyx.db.llm import fetch_default_provider @@ -32,7 +33,7 @@ from onyx.db.search_settings import get_current_search_settings from onyx.db.search_settings import get_secondary_search_settings from onyx.db.search_settings import update_current_search_settings from onyx.db.search_settings import update_secondary_search_settings -from onyx.db.swap_index import check_index_swap +from onyx.db.swap_index import check_and_perform_index_swap from onyx.document_index.factory import get_default_document_index from onyx.document_index.interfaces import DocumentIndex from onyx.document_index.vespa.index import VespaIndex @@ -73,7 +74,7 @@ def setup_onyx( The Tenant Service calls the tenants/create endpoint which runs this. """ - check_index_swap(db_session=db_session) + check_and_perform_index_swap(db_session=db_session) active_search_settings = get_active_search_settings(db_session) search_settings = active_search_settings.primary @@ -243,10 +244,18 @@ def setup_vespa( try: logger.notice(f"Setting up Vespa (attempt {x+1}/{num_attempts})...") document_index.ensure_indices_exist( - index_embedding_dim=index_setting.model_dim, - secondary_index_embedding_dim=secondary_index_setting.model_dim - if secondary_index_setting - else None, + primary_embedding_dim=index_setting.final_embedding_dim, + primary_embedding_precision=index_setting.embedding_precision, + secondary_index_embedding_dim=( + secondary_index_setting.final_embedding_dim + if secondary_index_setting + else None + ), + secondary_index_embedding_precision=( + secondary_index_setting.embedding_precision + if secondary_index_setting + else None + ), ) logger.notice("Vespa setup complete.") @@ -360,6 +369,11 @@ def setup_vespa_multitenant(supported_indices: list[SupportedEmbeddingModel]) -> ], embedding_dims=[index.dim for index in supported_indices] + [index.dim for index in supported_indices], + # on the cloud, just use float for all indices, the option to change this + # is not exposed to the user + embedding_precisions=[ + EmbeddingPrecision.FLOAT for _ in range(len(supported_indices) * 2) + ], ) logger.notice("Vespa setup complete.") diff --git a/backend/scripts/query_time_check/seed_dummy_docs.py b/backend/scripts/query_time_check/seed_dummy_docs.py index 36690e88b8..4353fc4f4a 100644 --- a/backend/scripts/query_time_check/seed_dummy_docs.py +++ b/backend/scripts/query_time_check/seed_dummy_docs.py @@ -136,7 +136,7 @@ def seed_dummy_docs( search_settings = get_current_search_settings(db_session) multipass_config = get_multipass_config(search_settings) index_name = search_settings.index_name - embedding_dim = search_settings.model_dim + embedding_dim = search_settings.final_embedding_dim vespa_index = VespaIndex( index_name=index_name, diff --git a/backend/shared_configs/model_server_models.py b/backend/shared_configs/model_server_models.py index 9f7e853d26..644f315fa6 100644 --- a/backend/shared_configs/model_server_models.py +++ b/backend/shared_configs/model_server_models.py @@ -30,6 +30,12 @@ class EmbedRequest(BaseModel): manual_passage_prefix: str | None = None api_url: str | None = None api_version: str | None = None + + # allows for the truncation of the vector to a lower dimension + # to reduce memory usage. Currently only supported for OpenAI models. + # will be ignored for other providers. + reduced_dimension: int | None = None + # This disables the "model_" protected namespace for pydantic model_config = {"protected_namespaces": ()} diff --git a/backend/tests/integration/common_utils/reset.py b/backend/tests/integration/common_utils/reset.py index aa611021bf..153fedc9e6 100644 --- a/backend/tests/integration/common_utils/reset.py +++ b/backend/tests/integration/common_utils/reset.py @@ -17,7 +17,7 @@ from onyx.db.engine import get_session_context_manager from onyx.db.engine import get_session_with_tenant from onyx.db.engine import SYNC_DB_API from onyx.db.search_settings import get_current_search_settings -from onyx.db.swap_index import check_index_swap +from onyx.db.swap_index import check_and_perform_index_swap from onyx.document_index.document_index_utils import get_multipass_config from onyx.document_index.vespa.index import DOCUMENT_ID_ENDPOINT from onyx.document_index.vespa.index import VespaIndex @@ -194,7 +194,7 @@ def reset_vespa() -> None: with get_session_context_manager() as db_session: # swap to the correct default model - check_index_swap(db_session) + check_and_perform_index_swap(db_session) search_settings = get_current_search_settings(db_session) multipass_config = get_multipass_config(search_settings) @@ -289,7 +289,7 @@ def reset_vespa_multitenant() -> None: for tenant_id in get_all_tenant_ids(): with get_session_with_tenant(tenant_id=tenant_id) as db_session: # swap to the correct default model for each tenant - check_index_swap(db_session) + check_and_perform_index_swap(db_session) search_settings = get_current_search_settings(db_session) multipass_config = get_multipass_config(search_settings) diff --git a/backend/tests/unit/model_server/test_embedding.py b/backend/tests/unit/model_server/test_embedding.py index 6781ab27aa..17068f3a6e 100644 --- a/backend/tests/unit/model_server/test_embedding.py +++ b/backend/tests/unit/model_server/test_embedding.py @@ -64,7 +64,7 @@ async def test_openai_embedding( embedding = CloudEmbedding("fake-key", EmbeddingProvider.OPENAI) result = await embedding._embed_openai( - ["test1", "test2"], "text-embedding-ada-002" + ["test1", "test2"], "text-embedding-ada-002", None ) assert result == sample_embeddings @@ -89,6 +89,7 @@ async def test_embed_text_cloud_provider() -> None: prefix=None, api_url=None, api_version=None, + reduced_dimension=None, ) assert result == [[0.1, 0.2], [0.3, 0.4]] @@ -114,6 +115,7 @@ async def test_embed_text_local_model() -> None: prefix=None, api_url=None, api_version=None, + reduced_dimension=None, ) assert result == [[0.1, 0.2], [0.3, 0.4]] @@ -157,6 +159,7 @@ async def test_rate_limit_handling() -> None: prefix=None, api_url=None, api_version=None, + reduced_dimension=None, ) @@ -179,6 +182,7 @@ async def test_concurrent_embeddings() -> None: manual_passage_prefix=None, api_url=None, api_version=None, + reduced_dimension=None, ) with patch("model_server.encoders.get_embedding_model") as mock_get_model: diff --git a/web/src/app/admin/configuration/search/UpgradingPage.tsx b/web/src/app/admin/configuration/search/UpgradingPage.tsx index 87290bb462..ccc45d5dfe 100644 --- a/web/src/app/admin/configuration/search/UpgradingPage.tsx +++ b/web/src/app/admin/configuration/search/UpgradingPage.tsx @@ -108,15 +108,13 @@ export default function UpgradingPage({ >
- Are you sure you want to cancel? -
-
- Cancelling will revert to the previous model and all progress will - be lost. + Are you sure you want to cancel? Cancelling will revert to the + previous model and all progress will be lost.
-
- +
@@ -141,30 +139,46 @@ export default function UpgradingPage({ {connectors && connectors.length > 0 ? ( - <> - {failedIndexingStatus && failedIndexingStatus.length > 0 && ( - - )} + futureEmbeddingModel.background_reindex_enabled ? ( + <> + {failedIndexingStatus && failedIndexingStatus.length > 0 && ( + + )} - - The table below shows the re-indexing progress of all existing - connectors. Once all connectors have been re-indexed - successfully, the new model will be used for all search - queries. Until then, we will use the old model so that no - downtime is necessary during this transition. - + + The table below shows the re-indexing progress of all + existing connectors. Once all connectors have been + re-indexed successfully, the new model will be used for all + search queries. Until then, we will use the old model so + that no downtime is necessary during this transition. + - {sortedReindexingProgress ? ( - - ) : ( - - )} - + {sortedReindexingProgress ? ( + + ) : ( + + )} + + ) : ( +
+

+ Switching Embedding Models +

+

+ You're currently switching embedding models, and + you've selected the instant switch option. The + transition will complete shortly. +

+

+ The new model will be active soon. +

+
+ ) ) : (

diff --git a/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/NumberInput.tsx b/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/NumberInput.tsx index 89e01be60c..4952714d3d 100644 --- a/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/NumberInput.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/NumberInput.tsx @@ -1,5 +1,5 @@ -import { SubLabel } from "@/components/admin/connectors/Field"; -import { Field } from "formik"; +import { Label, SubLabel } from "@/components/admin/connectors/Field"; +import { ErrorMessage, Field } from "formik"; export default function NumberInput({ label, @@ -16,10 +16,12 @@ export default function NumberInput({ }) { return (
- + {description && {description}} +
); } diff --git a/web/src/app/admin/connectors/[connector]/pages/formelements/NumberInput.tsx b/web/src/app/admin/connectors/[connector]/pages/formelements/NumberInput.tsx deleted file mode 100644 index 9e1cf8dcf1..0000000000 --- a/web/src/app/admin/connectors/[connector]/pages/formelements/NumberInput.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { SubLabel } from "@/components/admin/connectors/Field"; -import { Field } from "formik"; - -export default function NumberInput({ - label, - value, - optional, - description, - name, - showNeverIfZero, -}: { - value?: number; - label: string; - name: string; - optional?: boolean; - description?: string; - showNeverIfZero?: boolean; -}) { - return ( -
- - {description && {description}} - - -
- ); -} diff --git a/web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx b/web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx index 41e3f9bef9..df5128dce8 100644 --- a/web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx +++ b/web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx @@ -103,42 +103,6 @@ export function EmbeddingModelSelection({ { refreshInterval: 5000 } // 5 seconds ); - const { data: connectors } = useSWR[]>( - "/api/manage/connector", - errorHandlingFetcher, - { refreshInterval: 5000 } // 5 seconds - ); - - const onConfirmSelection = async (model: EmbeddingModelDescriptor) => { - const response = await fetch( - "/api/search-settings/set-new-search-settings", - { - method: "POST", - body: JSON.stringify({ ...model, index_name: null }), - headers: { - "Content-Type": "application/json", - }, - } - ); - if (response.ok) { - setShowTentativeModel(null); - mutate("/api/search-settings/get-secondary-search-settings"); - if (!connectors || !connectors.length) { - setShowAddConnectorPopup(true); - } - } else { - alert(`Failed to update embedding model - ${await response.text()}`); - } - }; - - const onSelectOpenSource = async (model: HostedEmbeddingModel) => { - if (selectedProvider?.model_name === INVALID_OLD_MODEL) { - await onConfirmSelection(model); - } else { - setShowTentativeOpenProvider(model); - } - }; - return (
{alreadySelectedModel && ( @@ -270,7 +234,9 @@ export function EmbeddingModelSelection({ {modelTab == "open" && ( { + setShowTentativeOpenProvider(model); + }} /> )} diff --git a/web/src/app/admin/embeddings/RerankingFormPage.tsx b/web/src/app/admin/embeddings/RerankingFormPage.tsx index 236db2a985..cf0d53933e 100644 --- a/web/src/app/admin/embeddings/RerankingFormPage.tsx +++ b/web/src/app/admin/embeddings/RerankingFormPage.tsx @@ -30,6 +30,10 @@ interface RerankingDetailsFormProps { originalRerankingDetails: RerankingDetails; modelTab: "open" | "cloud" | null; setModelTab: Dispatch>; + onValidationChange?: ( + isValid: boolean, + errors: Record + ) => void; } const RerankingDetailsForm = forwardRef< @@ -43,6 +47,7 @@ const RerankingDetailsForm = forwardRef< currentRerankingDetails, modelTab, setModelTab, + onValidationChange, }, ref ) => { @@ -55,26 +60,78 @@ const RerankingDetailsForm = forwardRef< const combinedSettings = useContext(SettingsContext); const gpuEnabled = combinedSettings?.settings.gpu_enabled; + // Define the validation schema + const validationSchema = Yup.object().shape({ + rerank_model_name: Yup.string().nullable(), + rerank_provider_type: Yup.mixed() + .nullable() + .oneOf(Object.values(RerankerProvider)) + .optional(), + rerank_api_key: Yup.string() + .nullable() + .test( + "required-if-cohere", + "API Key is required for Cohere reranking", + function (value) { + const { rerank_provider_type } = this.parent; + return ( + rerank_provider_type !== RerankerProvider.COHERE || + (value !== null && value !== "") + ); + } + ), + rerank_api_url: Yup.string() + .url("Must be a valid URL") + .matches(/^https?:\/\//, "URL must start with http:// or https://") + .nullable() + .test( + "required-if-litellm", + "API URL is required for LiteLLM reranking", + function (value) { + const { rerank_provider_type } = this.parent; + return ( + rerank_provider_type !== RerankerProvider.LITELLM || + (value !== null && value !== "") + ); + } + ), + }); + return ( () - .nullable() - .oneOf(Object.values(RerankerProvider)) - .optional(), - api_key: Yup.string().nullable(), - num_rerank: Yup.number().min(1, "Must be at least 1"), - rerank_api_url: Yup.string() - .url("Must be a valid URL") - .matches(/^https?:\/\//, "URL must start with http:// or https://") - .nullable(), - })} + validationSchema={validationSchema} onSubmit={async (_, { setSubmitting }) => { setSubmitting(false); }} + validate={(values) => { + // Update parent component with values + setRerankingDetails(values); + + // Run validation and report errors + if (onValidationChange) { + // We'll return an empty object here since Yup will handle the actual validation + // But we need to check if there are any validation errors + const errors: Record = {}; + try { + // Manually validate against the schema + validationSchema.validateSync(values, { abortEarly: false }); + onValidationChange(true, {}); + } catch (validationError) { + if (validationError instanceof Yup.ValidationError) { + validationError.inner.forEach((err) => { + if (err.path) { + errors[err.path] = err.message; + } + }); + onValidationChange(false, errors); + } + } + } + + return {}; // Return empty object as Formik will handle the errors + }} enableReinitialize={true} > {({ values, setFieldValue, resetForm }) => { diff --git a/web/src/app/admin/embeddings/interfaces.ts b/web/src/app/admin/embeddings/interfaces.ts index 2a53acdb17..cc62945485 100644 --- a/web/src/app/admin/embeddings/interfaces.ts +++ b/web/src/app/admin/embeddings/interfaces.ts @@ -20,6 +20,11 @@ export enum RerankerProvider { LITELLM = "litellm", } +export enum EmbeddingPrecision { + FLOAT = "float", + BFLOAT16 = "bfloat16", +} + export interface AdvancedSearchConfiguration { index_name: string | null; multipass_indexing: boolean; @@ -27,12 +32,15 @@ export interface AdvancedSearchConfiguration { disable_rerank_for_streaming: boolean; api_url: string | null; num_rerank: number; + embedding_precision: EmbeddingPrecision; + reduced_dimension: number | null; } export interface SavedSearchSettings extends RerankingDetails, AdvancedSearchConfiguration { provider_type: EmbeddingProvider | null; + background_reindex_enabled: boolean; } export interface RerankingModel { diff --git a/web/src/app/admin/embeddings/modals/InstantSwitchConfirmModal.tsx b/web/src/app/admin/embeddings/modals/InstantSwitchConfirmModal.tsx new file mode 100644 index 0000000000..ce4bd3400f --- /dev/null +++ b/web/src/app/admin/embeddings/modals/InstantSwitchConfirmModal.tsx @@ -0,0 +1,37 @@ +import { Modal } from "@/components/Modal"; +import { Button } from "@/components/ui/button"; + +interface InstantSwitchConfirmModalProps { + onClose: () => void; + onConfirm: () => void; +} + +export const InstantSwitchConfirmModal = ({ + onClose, + onConfirm, +}: InstantSwitchConfirmModalProps) => { + return ( + + <> +
+ Instant switching will immediately change the embedding model without + re-indexing. Searches will be over a partial set of documents + (starting with 0 documents) until re-indexing is complete. +
+
+ This is not reversible. +
+
+ + +
+ +
+ ); +}; diff --git a/web/src/app/admin/embeddings/modals/ModelSelectionModal.tsx b/web/src/app/admin/embeddings/modals/ModelSelectionModal.tsx index a8c59afdde..12b087d529 100644 --- a/web/src/app/admin/embeddings/modals/ModelSelectionModal.tsx +++ b/web/src/app/admin/embeddings/modals/ModelSelectionModal.tsx @@ -51,9 +51,10 @@ export function ModelSelectionConfirmationModal({ )} -
- +
diff --git a/web/src/app/admin/embeddings/modals/SelectModelModal.tsx b/web/src/app/admin/embeddings/modals/SelectModelModal.tsx index 7b9347a377..ccc4f3f04e 100644 --- a/web/src/app/admin/embeddings/modals/SelectModelModal.tsx +++ b/web/src/app/admin/embeddings/modals/SelectModelModal.tsx @@ -21,15 +21,14 @@ export function SelectModelModal({ >
- You're selecting a new embedding model, {model.model_name}. If - you update to this model, you will need to undergo a complete - re-indexing. -
- Are you sure? + You're selecting a new embedding model, {model.model_name} + . If you update to this model, you will need to undergo a complete + re-indexing. Are you sure?
-
- +
diff --git a/web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx b/web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx index bea80322e1..b5003f835a 100644 --- a/web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx +++ b/web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx @@ -3,13 +3,15 @@ import { Formik, Form, FormikProps, FieldArray, Field } from "formik"; import * as Yup from "yup"; import { TrashIcon } from "@/components/icons/icons"; import { FaPlus } from "react-icons/fa"; -import { AdvancedSearchConfiguration } from "../interfaces"; +import { AdvancedSearchConfiguration, EmbeddingPrecision } from "../interfaces"; import { BooleanFormField, Label, SubLabel, + SelectorFormField, } from "@/components/admin/connectors/Field"; import NumberInput from "../../connectors/[connector]/pages/ConnectorInput/NumberInput"; +import { StringOrNumberOption } from "@/components/Dropdown"; interface AdvancedEmbeddingFormPageProps { updateAdvancedEmbeddingDetails: ( @@ -17,102 +19,207 @@ interface AdvancedEmbeddingFormPageProps { value: any ) => void; advancedEmbeddingDetails: AdvancedSearchConfiguration; + embeddingProviderType: string | null; + onValidationChange?: ( + isValid: boolean, + errors: Record + ) => void; } +// Options for embedding precision based on EmbeddingPrecision enum +const embeddingPrecisionOptions: StringOrNumberOption[] = [ + { name: EmbeddingPrecision.BFLOAT16, value: EmbeddingPrecision.BFLOAT16 }, + { name: EmbeddingPrecision.FLOAT, value: EmbeddingPrecision.FLOAT }, +]; + const AdvancedEmbeddingFormPage = forwardRef< FormikProps, AdvancedEmbeddingFormPageProps ->(({ updateAdvancedEmbeddingDetails, advancedEmbeddingDetails }, ref) => { - return ( -
- { - setSubmitting(false); - }} - validate={(values) => { - // Call updateAdvancedEmbeddingDetails for each changed field - Object.entries(values).forEach(([key, value]) => { - updateAdvancedEmbeddingDetails( - key as keyof AdvancedSearchConfiguration, - value - ); - }); - }} - enableReinitialize={true} - > - {({ values }) => ( -
- - {({ push, remove }) => ( -
- +>( + ( + { + updateAdvancedEmbeddingDetails, + advancedEmbeddingDetails, + embeddingProviderType, + onValidationChange, + }, + ref + ) => { + return ( +
+ value === null || value === undefined || value >= 256 + ) + .test( + "openai", + "Reduced Dimensions is only supported for OpenAI embedding models", + (value) => { + return embeddingProviderType === "openai" || value === null; + } + ), + })} + onSubmit={async (_, { setSubmitting }) => { + setSubmitting(false); + }} + validate={(values) => { + // Call updateAdvancedEmbeddingDetails for each changed field + Object.entries(values).forEach(([key, value]) => { + updateAdvancedEmbeddingDetails( + key as keyof AdvancedSearchConfiguration, + value + ); + }); - Add additional languages to the search. - {values.multilingual_expansion.map( - (_: any, index: number) => ( -
- = {}; + try { + // Manually validate against the schema + Yup.object() + .shape({ + multilingual_expansion: Yup.array().of(Yup.string()), + multipass_indexing: Yup.boolean(), + disable_rerank_for_streaming: Yup.boolean(), + num_rerank: Yup.number() + .required("Number of results to rerank is required") + .min(1, "Must be at least 1"), + embedding_precision: Yup.string().nullable(), + reduced_dimension: Yup.number() + .nullable() + .test( + "positive", + "Must be larger than or equal to 256", + (value) => + value === null || value === undefined || value >= 256 + ) + .test( + "openai", + "Reduced Dimensions is only supported for OpenAI embedding models", + (value) => { + return ( + embeddingProviderType === "openai" || value === null + ); + } + ), + }) + .validateSync(values, { abortEarly: false }); + onValidationChange(true, {}); + } catch (validationError) { + if (validationError instanceof Yup.ValidationError) { + validationError.inner.forEach((err) => { + if (err.path) { + errors[err.path] = err.message; + } + }); + onValidationChange(false, errors); + } + } + } + + return {}; // Return empty object as Formik will handle the errors + }} + enableReinitialize={true} + > + {({ values }) => ( + + + {({ push, remove }) => ( +
+ + + Add additional languages to the search. + {values.multilingual_expansion.map( + (_: any, index: number) => ( +
+ - -
- ) - )} - +
+ ) + )} + -
- )} - + > + + Add Language + +
+ )} + - - - - - )} - -
- ); -}); + + + + + + + + + )} +
+
+ ); + } +); export default AdvancedEmbeddingFormPage; AdvancedEmbeddingFormPage.displayName = "AdvancedEmbeddingFormPage"; diff --git a/web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx b/web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx index 78f603cf54..91260d491a 100644 --- a/web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx +++ b/web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx @@ -3,10 +3,16 @@ import { usePopup } from "@/components/admin/connectors/Popup"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import { EmbeddingModelSelection } from "../EmbeddingModelSelectionForm"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import Text from "@/components/ui/text"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, ArrowRight, WarningCircle } from "@phosphor-icons/react"; +import { + ArrowLeft, + ArrowRight, + WarningCircle, + CaretDown, + Warning, +} from "@phosphor-icons/react"; import { CloudEmbeddingModel, EmbeddingProvider, @@ -19,16 +25,35 @@ import { ThreeDotsLoader } from "@/components/Loading"; import AdvancedEmbeddingFormPage from "./AdvancedEmbeddingFormPage"; import { AdvancedSearchConfiguration, + EmbeddingPrecision, RerankingDetails, SavedSearchSettings, } from "../interfaces"; import RerankingDetailsForm from "../RerankingFormPage"; import { useEmbeddingFormContext } from "@/components/context/EmbeddingContext"; import { Modal } from "@/components/Modal"; +import { InstantSwitchConfirmModal } from "../modals/InstantSwitchConfirmModal"; import { useRouter } from "next/navigation"; import CardSection from "@/components/admin/CardSection"; import { combineSearchSettings } from "./utils"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +enum ReindexType { + REINDEX = "reindex", + INSTANT = "instant", +} export default function EmbeddingForm() { const { formStep, nextFormStep, prevFormStep } = useEmbeddingFormContext(); @@ -43,6 +68,8 @@ export default function EmbeddingForm() { disable_rerank_for_streaming: false, api_url: null, num_rerank: 0, + embedding_precision: EmbeddingPrecision.FLOAT, + reduced_dimension: null, }); const [rerankingDetails, setRerankingDetails] = useState({ @@ -52,6 +79,19 @@ export default function EmbeddingForm() { rerank_api_url: null, }); + const [reindexType, setReindexType] = useState( + ReindexType.REINDEX + ); + + const [formErrors, setFormErrors] = useState>({}); + const [isFormValid, setIsFormValid] = useState(true); + const [rerankFormErrors, setRerankFormErrors] = useState< + Record + >({}); + const [isRerankFormValid, setIsRerankFormValid] = useState(true); + const advancedFormRef = useRef(null); + const rerankFormRef = useRef(null); + const updateAdvancedEmbeddingDetails = ( key: keyof AdvancedSearchConfiguration, value: any @@ -82,6 +122,8 @@ export default function EmbeddingForm() { }; const [displayPoorModelName, setDisplayPoorModelName] = useState(true); const [showPoorModel, setShowPoorModel] = useState(false); + const [showInstantSwitchConfirm, setShowInstantSwitchConfirm] = + useState(false); const [modelTab, setModelTab] = useState<"open" | "cloud" | null>(null); const { @@ -115,6 +157,8 @@ export default function EmbeddingForm() { searchSettings.disable_rerank_for_streaming, num_rerank: searchSettings.num_rerank, api_url: null, + embedding_precision: searchSettings.embedding_precision, + reduced_dimension: searchSettings.reduced_dimension, }); setRerankingDetails({ @@ -146,17 +190,14 @@ export default function EmbeddingForm() { } }, [currentEmbeddingModel]); - const handleReindex = async () => { - const update = await updateSearch(); - if (update) { - await onConfirm(); - } - }; - const needsReIndex = currentEmbeddingModel != selectedProvider || searchSettings?.multipass_indexing != - advancedEmbeddingDetails.multipass_indexing; + advancedEmbeddingDetails.multipass_indexing || + searchSettings?.embedding_precision != + advancedEmbeddingDetails.embedding_precision || + searchSettings?.reduced_dimension != + advancedEmbeddingDetails.reduced_dimension; const updateSearch = useCallback(async () => { if (!selectedProvider) { @@ -166,18 +207,44 @@ export default function EmbeddingForm() { selectedProvider, advancedEmbeddingDetails, rerankingDetails, - selectedProvider.provider_type?.toLowerCase() as EmbeddingProvider | null + selectedProvider.provider_type?.toLowerCase() as EmbeddingProvider | null, + reindexType === ReindexType.REINDEX ); const response = await updateSearchSettings(searchSettings); if (response.ok) { return true; } else { - setPopup({ message: "Failed to update search settings", type: "error" }); + setPopup({ + message: "Failed to update search settings", + type: "error", + }); return false; } }, [selectedProvider, advancedEmbeddingDetails, rerankingDetails, setPopup]); + const handleValidationChange = useCallback( + (isValid: boolean, errors: Record) => { + setIsFormValid(isValid); + setFormErrors(errors); + }, + [] + ); + + const handleRerankValidationChange = useCallback( + (isValid: boolean, errors: Record) => { + setIsRerankFormValid(isValid); + setRerankFormErrors(errors); + }, + [] + ); + + // Combine validation states for both forms + const isOverallFormValid = isFormValid && isRerankFormValid; + const combinedFormErrors = useMemo(() => { + return { ...formErrors, ...rerankFormErrors }; + }, [formErrors, rerankFormErrors]); + const ReIndexingButton = useMemo(() => { const ReIndexingButtonComponent = ({ needsReIndex, @@ -186,47 +253,204 @@ export default function EmbeddingForm() { }) => { return needsReIndex ? (
- -
- -
-

Needs re-indexing due to:

-
    - {currentEmbeddingModel != selectedProvider && ( -
  • Changed embedding provider
  • - )} - {searchSettings?.multipass_indexing != - advancedEmbeddingDetails.multipass_indexing && ( -
  • Multipass indexing modification
  • - )} -
-
+
+ + + + + + + { + setReindexType(ReindexType.REINDEX); + }} + > + + + + (Recommended) Re-index + + +

+ Re-runs all connectors in the background before + switching over. Takes longer but ensures no + degredation of search during the switch. +

+
+
+
+
+ { + setReindexType(ReindexType.INSTANT); + }} + > + + + + Instant Switch + + +

+ Immediately switches to new settings without + re-indexing. Searches will be degraded until the + re-indexing is complete. +

+
+
+
+
+
+
+ {isOverallFormValid && ( +
+ +
+

Needs re-indexing due to:

+
    + {currentEmbeddingModel != selectedProvider && ( +
  • Changed embedding provider
  • + )} + {searchSettings?.multipass_indexing != + advancedEmbeddingDetails.multipass_indexing && ( +
  • Multipass indexing modification
  • + )} + {searchSettings?.embedding_precision != + advancedEmbeddingDetails.embedding_precision && ( +
  • Embedding precision modification
  • + )} + {searchSettings?.reduced_dimension != + advancedEmbeddingDetails.reduced_dimension && ( +
  • Reduced dimension modification
  • + )} +
+
+
+ )} + {!isOverallFormValid && + Object.keys(combinedFormErrors).length > 0 && ( +
+ +
+

Validation Errors:

+
    + {Object.entries(combinedFormErrors).map( + ([field, error]) => ( +
  • + {field}: {error} +
  • + ) + )} +
+
+
+ )}
) : ( - +
+ + {!isOverallFormValid && + Object.keys(combinedFormErrors).length > 0 && ( +
+ +
+

+ Validation Errors: +

+
    + {Object.entries(combinedFormErrors).map( + ([field, error]) => ( +
  • {error}
  • + ) + )} +
+
+
+ )} +
); }; ReIndexingButtonComponent.displayName = "ReIndexingButton"; return ReIndexingButtonComponent; - }, [needsReIndex, updateSearch]); + }, [needsReIndex, reindexType, isOverallFormValid, combinedFormErrors]); if (!selectedProvider) { return ; @@ -246,7 +470,7 @@ export default function EmbeddingForm() { router.push("/admin/configuration/search?message=search-settings"); }; - const onConfirm = async () => { + const handleReIndex = async () => { if (!selectedProvider) { return; } @@ -260,7 +484,8 @@ export default function EmbeddingForm() { rerankingDetails, selectedProvider.provider_type ?.toLowerCase() - .split(" ")[0] as EmbeddingProvider | null + .split(" ")[0] as EmbeddingProvider | null, + reindexType === ReindexType.REINDEX ); } else { // This is a locally hosted model @@ -268,7 +493,8 @@ export default function EmbeddingForm() { selectedProvider, advancedEmbeddingDetails, rerankingDetails, - null + null, + reindexType === ReindexType.REINDEX ); } @@ -381,6 +607,17 @@ export default function EmbeddingForm() { )} + {showInstantSwitchConfirm && ( + setShowInstantSwitchConfirm(false)} + onConfirm={() => { + setShowInstantSwitchConfirm(false); + handleReIndex(); + navigateToEmbeddingPage("search settings"); + }} + /> + )} + {formStep == 1 && ( <>

@@ -395,6 +632,7 @@ export default function EmbeddingForm() { @@ -444,8 +683,11 @@ export default function EmbeddingForm() { diff --git a/web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx b/web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx index e84781f377..a9c2f91d99 100644 --- a/web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx +++ b/web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx @@ -16,7 +16,7 @@ export default function OpenEmbeddingPage({ onSelectOpenSource, selectedProvider, }: { - onSelectOpenSource: (model: HostedEmbeddingModel) => Promise; + onSelectOpenSource: (model: HostedEmbeddingModel) => void; selectedProvider: HostedEmbeddingModel | CloudEmbeddingModel; }) { const [configureModel, setConfigureModel] = useState(false); diff --git a/web/src/app/admin/embeddings/pages/utils.ts b/web/src/app/admin/embeddings/pages/utils.ts index 039b142428..4889091a23 100644 --- a/web/src/app/admin/embeddings/pages/utils.ts +++ b/web/src/app/admin/embeddings/pages/utils.ts @@ -63,12 +63,14 @@ export const combineSearchSettings = ( selectedProvider: CloudEmbeddingProvider | HostedEmbeddingModel, advancedEmbeddingDetails: AdvancedSearchConfiguration, rerankingDetails: RerankingDetails, - provider_type: EmbeddingProvider | null + provider_type: EmbeddingProvider | null, + background_reindex_enabled: boolean ): SavedSearchSettings => { return { ...selectedProvider, ...advancedEmbeddingDetails, ...rerankingDetails, provider_type: provider_type, + background_reindex_enabled, }; }; diff --git a/web/src/components/admin/connectors/Field.tsx b/web/src/components/admin/connectors/Field.tsx index 15480ab61b..b302073d94 100644 --- a/web/src/components/admin/connectors/Field.tsx +++ b/web/src/components/admin/connectors/Field.tsx @@ -51,13 +51,13 @@ export function Label({ className?: string; }) { return ( -
{children} -
+ ); } @@ -686,7 +686,7 @@ export function SelectorFormField({ defaultValue, tooltip, includeReset = false, - fontSize = "sm", + fontSize = "md", small = false, }: SelectorFormFieldProps) { const [field] = useField(name); diff --git a/web/src/components/embedding/ReindexingProgressTable.tsx b/web/src/components/embedding/ReindexingProgressTable.tsx index ad26c595c5..186f3444a9 100644 --- a/web/src/components/embedding/ReindexingProgressTable.tsx +++ b/web/src/components/embedding/ReindexingProgressTable.tsx @@ -29,6 +29,7 @@ export function ReindexingProgressTable({ Connector Name Status Docs Re-Indexed + diff --git a/web/src/components/embedding/interfaces.tsx b/web/src/components/embedding/interfaces.tsx index e03d163f9c..1ac3f5da6a 100644 --- a/web/src/components/embedding/interfaces.tsx +++ b/web/src/components/embedding/interfaces.tsx @@ -55,6 +55,7 @@ export interface EmbeddingModelDescriptor { api_version?: string | null; deployment_name?: string | null; index_name: string | null; + background_reindex_enabled?: boolean; } export interface CloudEmbeddingModel extends EmbeddingModelDescriptor {