Preparação dos índices antes da atualização para 2.5.1#

Este procedimento se aplica à atualização de 2.5.0 para 2.5.1. Para outra compilação com um esquema de banco de dados particionado equivalente, use-o somente depois que o administrador e a equipe de suporte confirmarem que ele se aplica. Se a versão atual for anterior à 2.5.0, por exemplo 2.4.10, não execute os comandos SQL abaixo sem combinar antes o caminho de atualização.

Para esta atualização, prepare os índices antes de executar update.sh ou as migrações manuais da versão 2.5.1. O script de atualização inicia as migrações, mas não faz essa preparação. A migração verifica se os índices estão prontos e não será concluída sem eles.

Esta tarefa deve ser realizada por um administrador de banco de dados. A criação de índices em um banco grande pode levar tempo; faça um backup e programe uma janela de atualização. É necessário usar psql conectado ao mesmo banco de dados usado pelo Sherpa AI Server e uma função com permissão para criar índices nas tabelas. Crie os arquivos SQL no computador onde executará o psql: o pacote de instalação do cliente não os contém.

1. Verifique a conexão com o banco de dados#

Substitua DB_HOST, DB_OWNER e SHERPA_DB pelas configurações reais do banco de dados. Se executar psql dentro do contêiner do banco, use as configurações de conexão disponíveis nele. Confirme o banco de dados e o usuário antes de continuar:

export PGHOST='DB_HOST'
export PGPORT='5432'
export PGUSER='DB_OWNER'
export PGDATABASE='SHERPA_DB'
psql -X -v ON_ERROR_STOP=1 -c 'SELECT current_database(), current_user;'

Em uma instalação padrão a partir do pacote do cliente, você pode usar o psql dentro do contêiner PostgreSQL. No diretório com docker-compose.yml e .env, verifique a conexão sem instalar o psql no host:

docker compose exec -T aiserver-pg sh -c 'psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT current_database(), current_user;"'

Verifique se public.embeddings é uma tabela particionada com uma coluna account_id. Use o comando correspondente ao seu modo de conexão:

psql -X -v ON_ERROR_STOP=1 -Atc "SELECT EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_attribute a ON a.attrelid = c.oid WHERE n.nspname = 'public' AND c.relname = 'embeddings' AND c.relkind = 'p' AND a.attname = 'account_id' AND a.attnum > 0 AND NOT a.attisdropped) AS schema_ready;"

Na instalação padrão do cliente, use o contêiner do banco de dados:

printf '%s\n' "SELECT EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_attribute a ON a.attrelid = c.oid WHERE n.nspname = 'public' AND c.relname = 'embeddings' AND c.relkind = 'p' AND a.attname = 'account_id' AND a.attnum > 0 AND NOT a.attisdropped) AS schema_ready;" | docker compose exec -T aiserver-pg sh -c 'psql -X -v ON_ERROR_STOP=1 -At -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f -'

O resultado esperado é t. Se aparecer f, um erro ou nenhum resultado, não execute os arquivos SQL abaixo nem update.sh. Combine primeiro as próximas etapas com a equipe de suporte.

2. Salve o SQL para a busca vetorial#

Copie todo o conteúdo do bloco a seguir para o arquivo prepare_dimension_hnsw.sql sem alterações:

-- Run with psql -X -v ON_ERROR_STOP=1 -f prepare_dimension_hnsw.sql before
-- Phinx migration 20260930160000 on a database that already has vectors.
-- Each command runs outside a transaction so PostgreSQL can build online.
-- The migration verifies these indexes before its short metadata cutover.

SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON public.%I USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=384, m=8, efconstruction=64, efsearch=800) '
    || 'WHERE cardinality(embedding_vector) = 384',
    child.relname || '_dim_384_hnsw_idx', child.relname
)
FROM pg_inherits inheritance
JOIN pg_class child ON child.oid = inheritance.inhrelid
WHERE inheritance.inhparent = 'public.embeddings'::regclass
ORDER BY child.relname
\gexec

SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON orchestrator.samples USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=384, m=8, efconstruction=64, efsearch=64) '
    || 'WHERE cardinality(embedding_vector) = 384',
    'samples_dim_384_hnsw_idx'
)
\gexec

-- Prepare the standard 384↔1024 model switch before the migration. The
-- 1024 predicates are empty on 384-only accounts but building them online
-- here prevents the first 1024 request from building HNSW on live data.
SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON public.%I USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=1024, m=8, efconstruction=64, efsearch=800) '
    || 'WHERE cardinality(embedding_vector) = 1024',
    child.relname || '_dim_1024_hnsw_idx', child.relname
)
FROM pg_inherits inheritance
JOIN pg_class child ON child.oid = inheritance.inhrelid
WHERE inheritance.inhparent = 'public.embeddings'::regclass
ORDER BY child.relname
\gexec

SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON orchestrator.samples USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=1024, m=8, efconstruction=64, efsearch=64) '
    || 'WHERE cardinality(embedding_vector) = 1024',
    'samples_dim_1024_hnsw_idx'
)
\gexec

-- Older installations might already contain other dimensions where the old
-- index was absent. Preserve them without building a blocking index in Phinx.
SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON public.%I USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=%s, m=8, efconstruction=64, efsearch=800) '
    || 'WHERE cardinality(embedding_vector) = %s',
    format('embeddings_account_%s_dim_%s_hnsw_idx', account_id, dimension),
    format('embeddings_account_%s', account_id),
    dimension, dimension
)
FROM (
    SELECT account_id, cardinality(embedding_vector) AS dimension
    FROM public.embeddings
    WHERE cardinality(embedding_vector) NOT IN (384, 1024)
    GROUP BY account_id, cardinality(embedding_vector)
) dimensions
ORDER BY account_id, dimension
\gexec

SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON orchestrator.samples USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=%s, m=8, efconstruction=64, efsearch=64) '
    || 'WHERE cardinality(embedding_vector) = %s',
    format('samples_dim_%s_hnsw_idx', dimension), dimension, dimension
)
FROM (
    SELECT cardinality(embedding_vector) AS dimension
    FROM orchestrator.samples
    WHERE embedding_vector IS NOT NULL
      AND cardinality(embedding_vector) NOT IN (384, 1024)
    GROUP BY cardinality(embedding_vector)
) dimensions
ORDER BY dimension
\gexec

3. Salve o SQL para verificar fragmentos duplicados#

Copie todo o conteúdo do bloco a seguir para o arquivo prepare_documents_dedup_index.sql sem alterações:

-- Run with psql -X -v ON_ERROR_STOP=1 -f before Phinx migration
-- 20260930161000 on an existing database with documents. psql must not wrap
-- this command in a transaction: the index is built without blocking writes.
CREATE INDEX CONCURRENTLY IF NOT EXISTS documents_file_text_md5_active_idx
ON public.documents (file_id, md5(text_chunk))
WHERE NOT is_deleted;

Confira os arquivos salvos com estes hashes SHA-256 dos blocos acima (incluindo a quebra de linha final):

sha256sum prepare_dimension_hnsw.sql prepare_documents_dedup_index.sql
fe7e626f0bfb5d8d6b8a8147ee128f4ae6b830ca7753c44648ab812229c17521  prepare_dimension_hnsw.sql
dfde574e8a213c32b2e2cb16f713701d29d1991de905f1e16d414e9b0dbe9401  prepare_documents_dedup_index.sql

Se os hashes forem diferentes, corrija os arquivos antes de executá-los.

4. Execute os dois arquivos antes da atualização#

Execute estes comandos um de cada vez no computador que contém os arquivos. Cada comando deve terminar com código 0. Não use psql -1 nem BEGIN: CREATE INDEX CONCURRENTLY deve ser executado fora de uma transação compartilhada.

psql -X -v ON_ERROR_STOP=1 -f prepare_dimension_hnsw.sql
psql -X -v ON_ERROR_STOP=1 -f prepare_documents_dedup_index.sql

Se o psql não estiver disponível no host, use estes comandos no diretório com docker-compose.yml. Os arquivos SQL permanecem no host e são enviados ao contêiner pela entrada padrão:

docker compose exec -T aiserver-pg sh -c 'psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f -' < prepare_dimension_hnsw.sql
docker compose exec -T aiserver-pg sh -c 'psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f -' < prepare_documents_dedup_index.sql

5. Verifique o resultado#

Depois que os dois arquivos forem executados com sucesso, esta consulta não deve retornar nenhuma linha com índices inválidos:

psql -X -v ON_ERROR_STOP=1 -c "SELECT n.nspname AS schema_name, c.relname AS invalid_index FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE NOT i.indisvalid AND (c.relname ~ '_dim_[0-9]+_hnsw_idx$' OR c.relname = 'documents_file_text_md5_active_idx');"

Se usar o contêiner PostgreSQL, faça a mesma verificação assim:

printf "%s\n" "SELECT n.nspname AS schema_name, c.relname AS invalid_index FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE NOT i.indisvalid AND (c.relname ~ '_dim_[0-9]+_hnsw_idx$' OR c.relname = 'documents_file_text_md5_active_idx');" | docker compose exec -T aiserver-pg sh -c 'psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f -'

Se algum comando falhar ou a consulta mostrar um índice inválido, interrompa o processo e resolva o problema com o administrador do banco antes de atualizar. Após as verificações, execute update.sh ou o procedimento de migração manual. Não é necessário reindexar os documentos apenas para esta preparação: os índices são criados nas tabelas existentes e abrangem os registros que já estiverem presentes.

6. Dimensões adicionais após a atualização #

Use esta seção após as migrações da versão 2.5.1 se o novo modelo produzir vetores com dimensão diferente de 384 e 1024. Prepare um índice para cada conta afetada antes de salvar o novo modelo nas configurações de indexação. Descubra o account_id numérico no banco de dados do Sherpa AI Server e a dimensão real do vetor (dimension, de 32 a 2000). Execute a preparação separadamente para cada conta. As dimensões padrão 384 e 1024 foram preparadas nas etapas anteriores, antes da atualização.

Copie todo o conteúdo do bloco a seguir para o arquivo prepare_additional_dimension_hnsw.sql sem alterações:

-- Prepare one account and the shared samples table for an additional embedding
-- dimension after migration 20260930160000. Run with psql outside a transaction:
-- psql -X -v ON_ERROR_STOP=1 -v account_id=123 -v dimension=768 \
--   -f backend/migrations/prepare_additional_dimension_hnsw.sql
-- CREATE INDEX CONCURRENTLY keeps ordinary inserts and updates available.

\set AUTOCOMMIT on

SELECT CASE WHEN EXISTS (
    SELECT 1 FROM orchestrator.accounts WHERE id = :'account_id'::integer
) AND :'dimension'::integer BETWEEN 32 AND 2000
THEN 'true' ELSE 'false' END AS valid_parameters
\gset
\if :valid_parameters
\else
    \echo 'Expected an existing account_id and a dimension between 32 and 2000'
    SELECT 1 / 0;
\endif

-- Before the tuning migration, keep the index compatible with its migration
-- precheck. Afterwards create future dimensions with the tuned search depth.
SELECT CASE WHEN EXISTS (
    SELECT 1 FROM orchestrator.phinxlog WHERE version = 20260930163000
) THEN 32000 ELSE 800 END AS hnsw_efsearch
\gset

-- The session lock serializes simultaneous preparation of the shared samples
-- index. It is released automatically if psql exits on an error.
SELECT pg_advisory_lock(2291, -1);
SELECT public.ensure_embeddings_account_partition(:'account_id'::integer);

SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON public.%I USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=%s, m=8, efconstruction=64, efsearch=%s) '
    || 'WHERE cardinality(embedding_vector) = %s',
    format('embeddings_account_%s_dim_%s_hnsw_idx', :'account_id'::integer, :'dimension'::integer),
    format('embeddings_account_%s', :'account_id'::integer),
    :'dimension'::integer,
    :'hnsw_efsearch'::integer,
    :'dimension'::integer
)
\gexec

SELECT format(
    'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON orchestrator.samples USING hnsw '
    || '(embedding_vector public.ann_cos_ops) '
    || 'WITH (dims=%s, m=8, efconstruction=64, efsearch=64) '
    || 'WHERE cardinality(embedding_vector) = %s',
    format('samples_dim_%s_hnsw_idx', :'dimension'::integer),
    :'dimension'::integer,
    :'dimension'::integer
)
\gexec

SELECT public.ensure_embeddings_dimension_index(:'account_id'::integer, :'dimension'::integer);
SELECT orchestrator.ensure_samples_dimension_index(:'dimension'::integer);
SELECT pg_advisory_unlock(2291, -1);

Hash SHA-256 do arquivo, incluindo a quebra de linha final:

sha256sum prepare_additional_dimension_hnsw.sql
94e18e038e02371a0859d60ca8f8e6345d0d5091e04f1659a8beecba034c4702  prepare_additional_dimension_hnsw.sql

Substitua ACCOUNT_ID e DIMENSION pelos números reais. Execute este comando fora de uma transação compartilhada com as mesmas configurações de conexão do psql:

psql -X -v ON_ERROR_STOP=1 -v account_id=ACCOUNT_ID -v dimension=DIMENSION -f prepare_additional_dimension_hnsw.sql

Para a instalação padrão do cliente, execute o seguinte no diretório com docker-compose.yml para usar o contêiner do banco de dados:

docker compose exec -T aiserver-pg sh -c 'psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v account_id=ACCOUNT_ID -v dimension=DIMENSION -f -' < prepare_additional_dimension_hnsw.sql

O comando deve terminar com código 0: o próprio script verifica se a conta existe, se a dimensão é permitida e se os dois índices estão prontos. Em caso de falha, não salve o novo modelo de indexação até resolver a causa. Após a preparação, volte ao procedimento para alterar o modelo de indexação.