Vector Search no Oracle 26ai: quando o banco entende o que você quer dizer

Artigo 30 de set de 2026
🇺🇸 Read this post in English: Vector Search on Oracle 26ai: When the Database Understands What You Mean

O problema que o SQL não resolve

Você já precisou encontrar todos os chamados de suporte relacionados a "problemas de memória no banco", mas sem saber exatamente quais palavras o analista usou na hora de abrir o ticket?

Talvez ele tenha escrito "ORA-04031", talvez "shared pool insuficiente", talvez "banco travou por falta de memória". São três formas diferentes de descrever o mesmo problema.

Com SQL tradicional, você escolhe: ou escreve dez condições com LIKE e ainda assim não pega tudo, ou aceita que alguns registros vão escapar. O banco não entende o que você está procurando — ele só compara texto.

É exatamente esse gap que o Vector Search resolve. Em vez de comparar caracteres, o banco compara significados. E o resultado prático é impressionante: uma query com "banco travou por falta de memória" encontra, com distância 0,6453, o documento cujo título é "ORA-04031: memória insuficiente no Shared Pool" — sem nenhuma palavra em comum entre a pergunta e o título.

Neste artigo vou mostrar como isso funciona no Oracle 26ai, como configurar o ambiente com um modelo de embedding local via ONNX, e como construir buscas semânticas em cima dos dados que você já tem no banco.

📝 Este lab foi executado no Oracle 26ai on-premises, usando o modelo all-MiniLM-L12-v2 carregado localmente via ONNX — sem nenhuma chamada externa, sem dependência de API. O Oracle 23ai na nuvem (ADB) suporta a mesma feature via OCI GenAI, com configuração diferente, mas que não será tratado neste artigo.

O que é um vetor e por que isso importa

Embeddings: transformando texto em coordenadas

Um embedding é uma representação numérica de um texto em um espaço de alta dimensão. Em vez de armazenar as palavras, o modelo de embedding transforma o texto em um array de números — tipicamente centenas de dimensões — onde textos semanticamente próximos ficam geometricamente próximos no espaço.

Pense assim: se você plotar palavras em um gráfico onde a posição reflete o significado, "cachorro" e "cão" ficam muito próximos, enquanto "cachorro" e "tablespace" ficam distantes. O Vector Search opera exatamente nessa lógica, mas em 384 dimensões (no caso do modelo que use) em vez de 2.

O resultado prático: quando você busca por "falha de backup", o banco encontra documentos que falam de "ORA-19809", "FRA cheio" e "recovery point" — mesmo que nenhuma dessas palavras apareça na sua query.

O modelo aprendeu, durante o treinamento, que todos esses conceitos habitam a mesma vizinhança semântica.

Similaridade: distância como relevância

A distância entre dois vetores é o que determina a relevância.

O Oracle 26ai suporta cinco métricas, mas a mais usada para texto é a distância cosseno — que mede o ângulo entre dois vetores, independente do tamanho do texto. Quanto menor a distância, mais similares são os textos:

Métrica Valor ideal Uso recomendado
COSINE 0 (idêntico) Textos de tamanhos variados — mais comum
EUCLIDEAN 0 (idêntico) Vetores normalizados, dados numéricos
DOT_PRODUCT Máximo positivo Vetores normalizados, mais rápido
MANHATTAN 0 (idêntico) Dados esparsos
HAMMING 0 (idêntico) Vetores binários

Nos resultados do nosso lab, a distância 0,6453 entre "banco travou por falta de memória" e o documento sobre ORA-04031 indica similaridade semântica relevante. Documentos com distância acima de 0,80 raramente são relevantes para a query.


O que o Oracle 26ai oferece

O suporte nativo a vetores no Oracle foi introduzido no 23ai e evoluiu no 26ai. Os componentes principais são:

Componente Descrição
VECTOR Tipo de dado nativo para armazenar embeddings diretamente em tabelas
VECTOR_EMBEDDING Função SQL que gera embedding usando um modelo ONNX carregado no banco
VECTOR_DISTANCE Função SQL que calcula a distância entre dois vetores
VECTOR INDEX (HNSW) Índice especializado para busca aproximada de vizinhos mais próximos
DBMS_VECTOR Pacote PL/SQL para carregar e gerenciar modelos ONNX
DBMS_VECTOR_CHAIN Pacote PL/SQL para pipelines de chunking, extração de texto e embeddings via API

O tipo VECTOR

A coluna VECTOR(384, FLOAT32) armazena um array de 384 números de ponto flutuante diretamente na tabela — sem tabelas auxiliares, sem extensões externas. Fixar as dimensões na definição da coluna é uma boa prática: garante que apenas vetores do modelo correto sejam inseridos.

HNSW: o índice que torna a busca viável

Sem índice, uma busca vetorial compara a query com cada linha da tabela — impraticável em ambiente de produção.

O índice HNSW (Hierarchical Navigable Small World) constrói um grafo de navegação em memória que permite encontrar os vizinhos mais próximos em tempo sub-linear. O parâmetro TARGET ACCURACY 95 equilibra velocidade e precisão — 95% dos resultados corretos são retornados, com latência muito menor que a busca exata.

📝 Oracle 26ai também suporta o índice IVF (Inverted File / Neighbor Partitions), que usa menos memória ao custo de um pouco menos de precisão. Para datasets grandes que não cabem completamente em memória, IVF pode ser a escolha certa.

O modelo de embedding: all-MiniLM-L12-v2 via ONNX

O ONNX (Open Neural Network Exchange) é um formato aberto para modelos de machine learning. O Oracle 26ai suporta o carregamento de modelos ONNX diretamente no banco via DBMS_VECTOR.LOAD_ONNX_MODEL — o modelo fica armazenado no dicionário do Oracle como um objeto de mineração e é invocado pela função SQL VECTOR_EMBEDDING sem nenhuma chamada de rede externa.

O modelo escolhido para este lab é o all-MiniLM-L12-v2, disponibilizado pela Oracle em formato ONNX aumentado — inclui tokenização e pós-processamento integrados. Ele gera vetores de 384 dimensões e suporta múltiplos idiomas, incluindo português.

Foi treinado para capturar similaridade semântica em tarefas de recuperação de informação — exatamente o que precisamos.

Característica all-MiniLM-L12-v2 (ONNX) cohere.embed-multilingual-v3.0 (OCI GenAI)
Dimensões 384 1024
Execução Local — dentro do banco API externa — OCI GenAI
Custo por chamada Zero Por transação (caracteres)
Dependência de rede Nenhuma Sim — endpoint OCI GenAI
Qualidade multilingual Boa (fine-tuned) Alta (treinado em 100+ idiomas)
Disponível no ADB Sim (*) Sim
Disponível no 26ai on-prem Sim Sim (requer credencial API Key)

(*) via DBMS_VECTOR.LOAD_ONNX_MODEL_CLOUD (Object Storage)

A Oracle disponibiliza o modelo neste link, conforme este artigo.


O Lab: configuração do ambiente

Pré-requisito: Vector Memory Area

Antes de criar o primeiro índice vetorial, o Oracle precisa que a vector_memory_size esteja configurada. O parâmetro pode ser alterado dinamicamente no CDB, mas precisa ser gravado no SPFILE para se tornar persistente a reinicializações do banco de dados:

-- Como SYS no CDB root
ALTER SYSTEM SET vector_memory_size = 1040M SCOPE=BOTH;

-- Configurar no PDB
ALTER SESSION SET CONTAINER = RRPDB;
ALTER SYSTEM SET vector_memory_size = 512M SCOPE=BOTH;

-- Confirmar
SELECT name, value
  FROM v$spparameter
 WHERE name = 'vector_memory_size';

NAME                      VALUE
------------------------- ---------------
vector_memory_size        536870912
⚠️ O erro ORA-51962 (vector memory area out of space) ocorre quando vector_memory_size = 0. Ele aparece no PDB, mas a causa está no CDB — o que pode confundir quem está configurando pela primeira vez.

Carregando o modelo ONNX

Inicialmente é preciso que o modelo ONNX esteja em algum diretório no servidor de banco de dados, e então cria-se um diretório no banco de dados apontando para o diretório no sistema operacional para que o modelo possa ser carregado no dicionário de dados.

SQL> show pdbs

    CON_ID CON_NAME       OPEN MODE  RESTRICTED
---------- -------------- ---------- ----------
         2 PDB$SEED       READ ONLY  NO
         3 RRPDB          READ WRITE NO

SQL> alter session set container=RRPDB;

Session altered.

SQL> show pdbs

    CON_ID CON_NAME       OPEN MODE  RESTRICTED
---------- -------------- ---------- ----------
         3 RRPDB          READ WRITE NO
SQL> show user
USER is "SYS"

SQL> CREATE OR REPLACE DIRECTORY onnx_models_dir
  2    AS '/home/oracle/app/oracle/onnx_models';

Directory created.

SQL> select *
  2    from dba_directories
  3    where directory_name like 'ONNX%';

OWNER  DIRECTORY_NAME     DIRECTORY_PATH                      ORIGIN_CON_ID
------ ------------------ ----------------------------------- -------------
SYS      ONNX_MODELS_DIR /home/oracle/app/oracle/onnx_models              3

1 row selected.

SQL> GRANT READ ON DIRECTORY onnx_models_dir TO rrezende;

Grant succeeded.

SQL> GRANT CREATE MINING MODEL TO rrezende;

Grant succeeded.

SQL> GRANT EXECUTE ON dbms_vector TO rrezende;

Grant succeeded.

SQL> GRANT EXECUTE ON dbms_vector_chain TO rrezende;

Grant succeeded.

Com o diretório Oracle criado e os grants concedidos, o carregamento do modelo é direto:

-- Como RREZENDE
SQL> conn rrezende@rrpdb
Enter password:
Connected.

SQL> BEGIN
  2    DBMS_VECTOR.LOAD_ONNX_MODEL(
  3      directory  => 'ONNX_MODELS_DIR',
  4      file_name  => 'all_MiniLM_L12_v2.onnx',
  5      model_name => 'ALL_MINILM_L12_V2'
  6    );
  7  END;
  8  /

PL/SQL procedure successfully completed.

-- Verificar carregamento
SQL> COL model_name      FOR A25
SQL> COL algorithm       FOR A10
SQL> COL mining_function FOR A12
SQL> SELECT model_name, algorithm, mining_function
  2    FROM user_mining_models
  3    WHERE model_name = 'ALL_MINILM_L12_V2';

MODEL_NAME         ALGORITHM  MINING_FUNCT
------------------ ---------- ------------
ALL_MINILM_L12_V2  ONNX       EMBEDDING

1 row selected.

Depois de carregado, o modelo é uma função SQL nativa. Testar é simples:

SQL> SELECT VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING 'Teste do modelo ONNX local' AS DATA)
  2    AS embedding_onnx
  3  FROM dual;

EMBEDDING_ONNX
--------------------------------------------------------------------------
[-6.71416521E-002,2.61541009E-002,-4.83012293E-003,3.49481567E-003,

SQL> SELECT VECTOR_DIMENSION_COUNT(VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING 'Teste de dimensões' AS DATA)) AS dimensoes
  2    FROM dual;

 DIMENSOES
----------
       384

SQL> SELECT VECTOR_DIMENSION_COUNT(VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING 'Test in English' AS DATA)) AS dimensoes_en
  2    FROM dual;

DIMENSOES_EN
------------
   384

384 dimensões confirmadas — em português e em inglês. O modelo foi projetado para ser multilingual.

Estrutura das tabelas

O lab usa quatro tabelas no schema RREZENDE:

Tabela Conteúdo Embeddings
doc_suporte 12 notas de atendimento (8 PT + 4 EN) 1 por documento
doc_tecnico 11 documentos técnicos Oracle (5 PT + 6 EN) 1 por documento
doc_pdf PDFs originais como BLOB Nenhum (só o arquivo)
doc_pdf_chunks Chunks dos PDFs 1 por chunk

A criação das tabelas e inserção de dados estão disponíveis nos scripts do lab:
doc_suporte
doc_tecnico
doc_pdf

A coluna de embedding é definida com dimensão fixa: VECTOR(384, FLOAT32). Isso impede a inserção acidental de vetores de outro modelo — uma boa prática de governança quando o ambiente pode evoluir.

Gerando os embeddings

Com o modelo carregado, gerar os embeddings é um loop PL/SQL simples. Para a tabela doc_suporte:

SQL> BEGIN
  2    FOR rec IN (
  3      SELECT doc_id, titulo || '. ' || conteudo AS texto
  4        FROM rrezende.doc_suporte
  5       WHERE embedding IS NULL
  6    ) LOOP
  7      UPDATE rrezende.doc_suporte
  8         SET embedding = VECTOR_EMBEDDING(
  9                           ALL_MINILM_L12_V2 USING rec.texto AS DATA
 10                         )
 11       WHERE doc_id = rec.doc_id;
 12    END LOOP;
 13    COMMIT;
 14    DBMS_OUTPUT.PUT_LINE('doc_suporte: embeddings gerados com sucesso.');
 15  END;
 16  /
doc_suporte: embeddings gerados com sucesso.

PL/SQL procedure successfully completed.

SQL> SELECT doc_id, SUBSTR(titulo, 1, 50) AS titulo, VECTOR_DIMENSION_COUNT(embedding) AS dims
  2    FROM rrezende.doc_suporte
  3    ORDER BY doc_id;

DOC_ID TITULO                                              DIMS
------ -------------------------------------------------- -----
    13 Lentidão em queries após reorganização de índices    384
    14 Falha de conexão intermitente — TNS-12541            384
    15 Crescimento inesperado de tablespace — alerta 85%    384
    16 ORA-04031: memória insuficiente no Shared Pool       384
    17 Backup RMAN falhou — ORA-19809 limit exceeded        384
    18 Relatório de manutenção preventiva — agosto 2026     384
    19 Deadlock recorrente no módulo de faturamento         384
    20 Migração de tablespace para compressão avançada      384
    21 High CPU usage — unoptimized full table scan detec   384
    22 Redo log switch storm — I/O saturation               384
    23 Data Guard lag exceeding threshold — 45 minutes      384
    24 Password expiration causing application outage       384

12 rows selected.
📝 Por que concatenar título + conteúdo? O embedding captura o significado semântico do texto inteiro. Incluir o título melhora a qualidade da representação — especialmente em documentos curtos onde o título carrega informação central.

O mesmo loop é repetido para a tabela doc_tecnico, com resultado idêntico: 10 documentos, todos com 384 dimensões.

SQL> BEGIN
  2    FOR rec IN (
  3      SELECT doc_id, titulo || '. ' || conteudo AS texto
  4        FROM rrezende.doc_tecnico
  5       WHERE embedding IS NULL
  6    ) LOOP
  7      UPDATE rrezende.doc_tecnico
  8         SET embedding = VECTOR_EMBEDDING(
  9                           ALL_MINILM_L12_V2 USING rec.texto AS DATA
 10                         )
 11       WHERE doc_id = rec.doc_id;
 12    END LOOP;
 13    COMMIT;
 14    DBMS_OUTPUT.PUT_LINE('doc_tecnico: embeddings gerados com sucesso.');
 15  END;
 16  /
doc_tecnico: embeddings gerados com sucesso.

PL/SQL procedure successfully completed.

SQL> SELECT doc_id, SUBSTR(titulo, 1, 50) AS titulo, VECTOR_DIMENSION_COUNT(embedding) AS dims
  2    FROM rrezende.doc_tecnico
  3    ORDER BY doc_id;

DOC_ID TITULO                                                    DIMS
------ ------------------------------------------------------- ------
    11 Bind Variables: por que seu banco odeia SQL litera         384
    12 Estatísticas do otimizador: quando coletar e quand         384
    13 AWR e Statspack: diagnóstico de performance no Ora         384
    14 Data Guard: conceitos e modos de proteção                  384
    15 RMAN: estratégia de backup para ambientes críticos         384
    16 Index internals: B-tree, Bitmap, and when to use e         384
    17 Partitioning strategies for large tables                   384
    18 Oracle Unified Auditing: tracking who did what and         384
    19 Vector data type and AI Vector Search in Oracle 23         384
    20 JSON Relational Duality Views — Oracle 23ai                384

10 rows selected.

Criando o índice vetorial

Com os embeddings gerados, o próximo passo é criar o índice que torna a busca eficiente. Sem índice, uma query de similaridade faria um full table scan vetorial — comparando a query com cada linha da tabela, uma a uma. Funciona em tabelas pequenas, mas é inviável em ambiente de produção.

O Oracle 26ai oferece dois tipos de índice vetorial:

  • HNSW (Hierarchical Navigable Small World) — constrói um grafo de navegação em memória. É o mais rápido para buscas, ideal para datasets que cabem na memória alocada para a Vector Memory Area.
  • IVF (Inverted File / Neighbor Partitions) — divide o espaço vetorial em clusters e busca apenas nos clusters mais próximos da query. Usa menos memória que o HNSW, indicado para datasets muito grandes.

Para o lab, usei HNSW com DISTANCE COSINE e TARGET ACCURACY 95:

SQL> CREATE VECTOR INDEX vidx_suporte_onnx
  2    ON rrezende.doc_suporte (embedding)
  3    ORGANIZATION INMEMORY NEIGHBOR GRAPH
  4    DISTANCE COSINE
  5    WITH TARGET ACCURACY 95;

Index created.

SQL> CREATE VECTOR INDEX vidx_tecnico_onnx
  2    ON rrezende.doc_tecnico (embedding)
  3    ORGANIZATION INMEMORY NEIGHBOR GRAPH
  4    DISTANCE COSINE
  5    WITH TARGET ACCURACY 95;

Index created.

SQL> COL index_name  FOR A30
SQL> COL table_name  FOR A20
SQL> COL status      FOR A10
SQL> SELECT index_name, table_name, status
  2    FROM user_indexes
  3    WHERE index_type LIKE '%VECTOR%'
  4    ORDER BY table_name;

INDEX_NAME	          TABLE_NAME       STATUS
--------------------- --------------- -------
VIDX_SUPORTE_ONNX     DOC_SUPORTE       VALID
VIDX_TECNICO_ONNX     DOC_TECNICO       VALID

2 rows selected.
⚠️ O índice HNSW utiliza a Vector Memory Area — o parâmetro vector_memory_size configurado anteriormente. Se o valor for 0 ou insuficiente, o Oracle retorna ORA-51962 ao tentar criar o índice.

PDFs: o pipeline de chunking

Documentos longos como PDFs precisam de tratamento diferente. Um único embedding para 300 páginas perderia toda a granularidade semântica. A solução é o chunking: dividir o PDF em pedaços menores, gerar um embedding por chunk, e armazenar cada chunk como uma linha.

O Oracle 26ai oferece esse pipeline nativamente via DBMS_VECTOR_CHAIN:

PDF (BLOB)
    │
    ▼  UTL_TO_TEXT      — extrai texto do PDF
    │
    ▼  UTL_TO_CHUNKS    — divide em chunks de 150 palavras com overlap de 20
    │
    ▼  VECTOR_EMBEDDING — gera embedding por chunk via ONNX local
    │
    ▼  INSERT           — grava chunks + embeddings na tabela

O bloco PL/SQL abaixo resolve esse pipeline de uma só vez:

SQL> DECLARE
  2    l_chunks_gerados NUMBER := 0;
  3  BEGIN
  4    FOR pdf IN (
  5  SELECT p.doc_id, p.nome_arquivo, p.conteudo_pdf
  6    FROM rrezende.doc_pdf p
  7   WHERE NOT EXISTS (
  8     SELECT 1 FROM rrezende.doc_pdf_chunks c
  9      WHERE c.doc_id = p.doc_id
 10   )
 11    ) LOOP
 12  DBMS_OUTPUT.PUT_LINE('Processando: ' || pdf.nome_arquivo);
 13  INSERT INTO rrezende.doc_pdf_chunks
 14    (doc_id, chunk_seq, chunk_offset, chunk_length, chunk_text, embedding)
 15  SELECT
 16    pdf.doc_id,
 17    JSON_VALUE(c.column_value, '$.chunk_id'     RETURNING NUMBER),
 18    JSON_VALUE(c.column_value, '$.chunk_offset' RETURNING NUMBER),
 19    JSON_VALUE(c.column_value, '$.chunk_length' RETURNING NUMBER),
 20    JSON_VALUE(c.column_value, '$.chunk_data'),
 21    VECTOR_EMBEDDING(
 22      ALL_MINILM_L12_V2
 23      USING JSON_VALUE(c.column_value, '$.chunk_data') AS DATA
 24    )
 25  FROM DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(
 26   DBMS_VECTOR_CHAIN.UTL_TO_TEXT(pdf.conteudo_pdf),
 27   JSON('{"by":"words","max":"150","overlap":"20",
 28          "split":"recursively","normalize":"all"}')
 29       ) c;
 30  -- SQL%ROWCOUNT deve ser lido ANTES do COMMIT
 31  -- O COMMIT zera o valor de SQL%ROWCOUNT
 32  l_chunks_gerados := l_chunks_gerados + SQL%ROWCOUNT;
 33      DBMS_OUTPUT.PUT_LINE('  → ' || SQL%ROWCOUNT || ' chunks gerados.');
 34  COMMIT;
 35    END LOOP;
 36    DBMS_OUTPUT.PUT_LINE('Total de chunks: ' || l_chunks_gerados);
 37* END;
 38  /



Processando: database-concepts.pdf
¿ 2883 chunks gerados.
Processando: SELECT_AI_no_Oracle_23ai_e_26ai-Controlando_o_consumo_de_tokens_por_schema.pdf
¿ 52 chunks gerados.
Total de chunks: 2935

PL/SQL procedure successfully completed.

O resultado para os dois PDFs carregados:

SQL> COL nome_arquivo  FOR A80
SQL> COL total_chunks  FOR 9999
SQL> COL min_chars     FOR 9999
SQL> COL max_chars     FOR 9999
SQL> COL sem_embedding FOR 9999
SQL> SELECT p.doc_id, p.nome_arquivo, COUNT(c.chunk_id) AS total_chunks, MIN(c.chunk_length) AS min_chars, MAX(c.chunk_length) AS max_chars,
  2         SUM(CASE WHEN c.embedding IS NULL THEN 1 ELSE 0 END) AS sem_embedding
  3    FROM rrezende.doc_pdf p
  4    LEFT JOIN rrezende.doc_pdf_chunks c
  5      ON c.doc_id = p.doc_id
  6    GROUP BY p.doc_id, p.nome_arquivo
  7    ORDER BY p.doc_id;

 DOC_ID NOME_ARQUIVO                                                                     TOTAL_CHUNKS MIN_CHARS MAX_CHARS SEM_EMBEDDING
------- -------------------------------------------------------------------------------- ------------ --------- --------- -------------
      1 database-concepts.pdf                                                                    2883       158      1181             0
      2 SELECT_AI_no_Oracle_23ai_e_26ai-Controlando_o_consumo_de_tokens_por_schema.pdf             52       393       799             0
   
2 rows selected.

2883 chunks do Oracle Database Concepts (12,73 MB, EN) e 52 chunks do artigo anterior desta série sobre SELECT AI (1,08 MB, PT) — este artigo que você provavelmente já leu. 😄

Criando o índice vetorial para os chunks

Com os chunks gerados e os embeddings populados, criamos o índice HNSW para a tabela doc_pdf_chunks:

SQL> CREATE VECTOR INDEX vidx_pdf_chunks_onnx
  2    ON rrezende.doc_pdf_chunks (embedding)
  3    ORGANIZATION INMEMORY NEIGHBOR GRAPH
  4    DISTANCE COSINE
  5    WITH TARGET ACCURACY 95;

Index created.

Uma observação importante: com 2935 chunks no total, o índice HNSW precisa de mais memória que os índices das tabelas de documentos curtos. Se a vector_memory_size estiver no limite, crie o índice dos chunks por último — ele é o maior dos três.

Após a criação, todos os índices vetoriais do lab estão no ar:

SQL> COL index_name for a20
SQL> COL table_name for a20
SQL> SELECT index_name, table_name, status
  2    FROM user_indexes
  3    WHERE index_type LIKE '%VECTOR%'
  4    ORDER BY table_name;

INDEX_NAME            TABLE_NAME       STATUS
--------------------- --------------- -------
VIDX_PDF_CHUNKS_ONNX  DOC_PDF_CHUNKS    VALID
VIDX_SUPORTE_ONNX     DOC_SUPORTE.      VALID
VIDX_TECNICO_ONNX     DOC_TECNICO.      VALID

3 rows selected.

Com os três índices válidos, o banco está pronto para buscas semânticas em todas as fontes simultaneamente.


Os resultados: o banco entendendo o que você quer dizer

Busca semântica básica

A busca semântica usa VECTOR_DISTANCE com FETCH FIRST N ROWS ONLY — nada de JOIN complicado ou subquery. O índice HNSW garante que o Oracle não percorra todas as linhas.

Para facilitar os testes, armazenei o "assunto" a ser buscado em uma bind variable:

SQL> COL doc_id     FOR 999
SQL> COL titulo     FOR A55
SQL> COL categoria  FOR A15
SQL> COL dist       FOR 0.9999
SQL> VARIABLE query VARCHAR2(200)
SQL> EXEC :query := 'banco travou por falta de memória'

PL/SQL procedure successfully completed.

SQL> SELECT doc_id, SUBSTR(titulo, 1, 55) AS titulo, categoria,
  2         ROUND(VECTOR_DISTANCE(embedding, VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), COSINE), 4) AS dist
  3    FROM rrezende.doc_suporte
  4    ORDER BY distancia ASC
  5    FETCH FIRST 3 ROWS ONLY;

DOC_ID TITULO                                         CATEGORIA      DISTANCIA
------ ---------------------------------------------- -------------- ---------
    16 ORA-04031: memória insuficiente no Shared Pool Memória           0.6453
    14 Falha de conexão intermitente — TNS-12541      Conectividade     0.7498
    17 Backup RMAN falhou — ORA-19809 limit exceeded  Backup            0.7544

    3 rows selected.

Nenhuma das palavras da query aparece no título "ORA-04031: memória insuficiente no Shared Pool" — a query usa "banco", "travou", "falta", "memória". Mas o modelo conhece a vizinhança semântica desses conceitos e posiciona os dois textos a 0,6453 de distância no espaço vetorial.

Outro exemplo que demonstra a capacidade multilingual:

-- Query em inglês, documentos em português
SQL> EXEC :query := 'database performance degradation after maintenance'

PL/SQL procedure successfully completed.

SQL> SELECT doc_id, SUBSTR(titulo, 1, 55) AS titulo, categoria,
  2         ROUND(VECTOR_DISTANCE(embedding, VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), COSINE), 4) AS dist
  3    FROM rrezende.doc_suporte
  4    ORDER BY distancia ASC
  5    FETCH FIRST 3 ROWS ONLY;

DOC_ID TITULO                                                CATEGORIA       DISTANCIA
------ ----------------------------------------------------- --------------- ---------
    13 Lentidão em queries após reorganização de índices     Performance        0.5568
    23 Data Guard lag exceeding threshold — 45 minutes       Alta Disponibil    0.6349
                     idade
    21 High CPU usage — unoptimized full table scan detected Performance        0.6445

3 rows selected.

A query em inglês encontrou documentos em português como primeiro resultado. O modelo foi treinado para entender que "performance degradation after maintenance" e "lentidão após reorganização" são o mesmo conceito em idiomas diferentes.

Busca com filtro relacional

Vector Search e SQL convencional se combinam naturalmente — basta adicionar uma cláusula WHERE:

-- Busca semântica restrita a documentos em português
SQL> EXEC :query := 'problema de performance no banco'

PL/SQL procedure successfully completed.

SQL> SELECT doc_id, SUBSTR(titulo, 1, 55) AS titulo, idioma, categoria,
  2         ROUND(VECTOR_DISTANCE(embedding, VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), COSINE), 4) AS distancia
  3    FROM rrezende.doc_suporte
  4    WHERE idioma = 'PT'
  5    ORDER BY distancia ASC
  6    FETCH FIRST 3 ROWS ONLY;

DOC_ID TITULO                                             IDIOMA  CATEGORIA      DISTANCIA
------ -------------------------------------------------- ------- -------------- ---------
    13 Lentidão em queries após reorganização de índices       PT Performance       0.5998
    16 ORA-04031: memória insuficiente no Shared Pool          PT Memória           0.6987
    20 Migração de tablespace para compressão avançada         PT Armazenamento     0.7059

3 rows selected.

Busca híbrida: semântica + keyword

Termos técnicos específicos como códigos de erro (ORA-04031) podem não ter representação semântica forte no modelo — afinal, é uma string arbitrária. A busca híbrida combina o score semântico com a presença do termo no texto:

SQL> EXEC :query := 'ORA-04031'

PL/SQL procedure successfully completed.

SQL> WITH sem AS (SELECT doc_id, VECTOR_DISTANCE(embedding, VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), COSINE) AS dist_semantica
  2       FROM rrezende.doc_suporte
  3       ORDER BY dist_semantica ASC
  4       FETCH FIRST 10 ROWS ONLY),
  5   kw AS (SELECT doc_id, 1 AS match_keyword
  6      FROM rrezende.doc_suporte
  7      WHERE UPPER(conteudo) LIKE UPPER('%' || :query || '%')
  8         OR UPPER(titulo)   LIKE UPPER('%' || :query || '%'))
  9  SELECT s.doc_id, SUBSTR(d.titulo, 1, 55) AS titulo, d.categoria, ROUND(s.dist_semantica, 4) AS dist_semantica, NVL(k.match_keyword, 0) AS keyword_match,
 10     ROUND(s.dist_semantica - (NVL(k.match_keyword, 0) * 0.05), 4) AS score_hibrido
 11    FROM sem s
 12    JOIN rrezende.doc_suporte d
 13  ON d.doc_id = s.doc_id
 14    LEFT JOIN kw k
 15  ON k.doc_id = s.doc_id
 16*   ORDER BY score_hibrido ASC
SQL> /

DOC_ID TITULO                                             CATEGORIA       DIST_SEMANTICA KEYWORD_MATCH SCORE_HIBRIDO
------ -------------------------------------------------- --------------- -------------- ------------- -------------
    16 ORA-04031: memória insuficiente no Shared Pool     Memória                  .5008             1         .4508
    14 Falha de conexão intermitente — TNS-12541          Conectividade            .5687             0         .5687
    17 Backup RMAN falhou — ORA-19809 limit exceeded      Backup                    .651             0          .651
    15 Crescimento inesperado de tablespace — alerta 85%  Armazenamento            .7328             0         .7328
    24 Password expiration causing application outage     Conectividade            .7904             0         .7904
    22 Redo log switch storm — I/O saturation             Performance               .803             0          .803
    18 Relatório de manutenção preventiva — agosto 2026   Manutenção               .8219             0         .8219
    19 Deadlock recorrente no módulo de faturamento       Concorrência             .8264             0         .8264
    23 Data Guard lag exceeding threshold — 45 minutes    Alta Disponibil          .8383             0         .8383
                     idade
    13 Lentidão em queries após reorganização de índices  Performance              .8492             0         .8492

10 rows selected.

O documento sobre ORA-04031 já era o mais próximo semanticamente (0,5008), mas o keyword_match = 1 — por conter o termo exato no título — reduz ainda mais o score híbrido para 0,4508, reforçando a relevância. Uma combinação que captura o melhor dos dois mundos.

Busca em PDFs: chunks semânticos

A busca em documentos longos mostra o poder do chunking. Com "controle de tokens e custo de LLM", o banco encontra trechos específicos do artigo anterior desta série — mesmo sendo um PDF de 52 páginas:

SQL> EXEC :query := 'controle de tokens e custo de LLM'

PL/SQL procedure successfully completed.

SQL> SELECT c.chunk_id, c.doc_id, p.nome_arquivo, c.chunk_seq,
  2     ROUND(VECTOR_DISTANCE(c.embedding, VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), COSINE), 4) AS distancia,
  3     CAST(SUBSTR(c.chunk_text, 1, 60) AS VARCHAR2(60))  AS trecho
  4    FROM rrezende.doc_pdf_chunks c
  5    JOIN rrezende.doc_pdf p
  6  ON p.doc_id = c.doc_id
  7    ORDER BY distancia ASC
  8*   FETCH FIRST 5 ROWS ONLY
SQL> /

CHUNK_ID DOC_ID NOME_ARQUIVO                                                                    CHUNK_SEQ DISTANCIA TRECHO
-------- ------ ------------------------------------------------------------------------------- --------- --------- ------------------------------------------------------------
    5869      2 SELECT_AI_no_Oracle_23ai_e_26ai-Controlando_o_consumo_de_tokens_por_schema.pdf         51    0.5064 AI_OPS + PKG_AI_GATEWAY resolve os três problemas de uma vez
    5868      2 SELECT_AI_no_Oracle_23ai_e_26ai-Controlando_o_consumo_de_tokens_por_schema.pdf         50    0.5515 naturalmente alinhada com o que aparece na fatura. O
    5866      2 SELECT_AI_no_Oracle_23ai_e_26ai-Controlando_o_consumo_de_tokens_por_schema.pdf         48    0.5698 rastreamento de tokens é ainda mais relevante nesse modelo d
    5865      2 SELECT_AI_no_Oracle_23ai_e_26ai-Controlando_o_consumo_de_tokens_por_schema.pdf         47    0.5917 COUNT(*) AS chamadas, SUM(tokens_total * 4) AS total_chars,
    5867      2 SELECT_AI_no_Oracle_23ai_e_26ai-Controlando_o_consumo_de_tokens_por_schema.pdf         49    0.6006 GROUP BY caller_schema, error_code, error_message

5 rows selected.

Os chunks 51, 50 e 48 — todos do artigo PT — foram os mais relevantes, mostrando que o modelo encontrou os trechos certos dentro de um documento real em português.

Busca unificada: múltiplas fontes em uma query

O destaque final do lab: buscar simultaneamente em notas de suporte, documentação técnica e chunks de PDF, com uma única query e o embedding gerado uma só vez:

-- Gerar o embedding da query uma única vez e reutilizar nas três buscas
SQL> EXEC :query := 'falha de backup e recovery no Oracle'

PL/SQL procedure successfully completed.

SQL> WITH q_embed AS (SELECT VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA) AS vec
  2     FROM dual)
  3  SELECT 'Suporte' AS origem, doc_id, TO_NUMBER(NULL) AS chunk_seq,
  4     CAST(SUBSTR(titulo, 1, 60) AS VARCHAR2(60)) AS titulo_trecho,
  5     ROUND(VECTOR_DISTANCE(embedding, q.vec, COSINE), 4) AS distancia
  6    FROM rrezende.doc_suporte, q_embed q
  7    WHERE embedding IS NOT NULL
  8  UNION ALL
  9  SELECT 'Técnico' AS origem, doc_id, TO_NUMBER(NULL) AS chunk_seq,
 10     CAST(SUBSTR(titulo, 1, 60) AS VARCHAR2(60)) AS titulo_trecho,
 11     ROUND(VECTOR_DISTANCE(embedding, q.vec, COSINE), 4) AS distancia
 12    FROM rrezende.doc_tecnico, q_embed q
 13    WHERE embedding IS NOT NULL
 14  UNION ALL
 15  SELECT 'PDF' AS origem, c.doc_id, c.chunk_seq,
 16     CAST(SUBSTR(c.chunk_text, 1, 60) AS VARCHAR2(60)) AS titulo_trecho,
 17     ROUND(VECTOR_DISTANCE(c.embedding, q.vec, COSINE), 4) AS distancia
 18    FROM rrezende.doc_pdf_chunks c, q_embed q
 19    WHERE c.embedding IS NOT NULL
 20    ORDER BY distancia ASC
 21    FETCH FIRST 10 ROWS ONLY;


ORIGEM     DOC_ID  CHUNK_SEQ TITULO_TRECHO                                                          DISTANCIA
---------- ------ ---------- ---------------------------------------------------------------------- ---------
PDF             1       2244 files, the                                                                0.3792
                             server parameter file
                             , and control file. A sam
Técnico        15            RMAN: estratégia de backup para ambientes críticos                        0.3966
PDF             1       2264 Oracle AI Database Backup and Recovery User's Guide fo                    0.3979
PDF             1       2584 unplanned outages of networks, nodes, storage, and databases              0.4036
PDF             1       2270 block corruption                                                          0.4060
                             D:20260420030906-08'00'
                             Data Corruption
PDF             1       2272 concepts                                                                  0.4194
                             Zero Data Loss Recovery Appliance
                             The cloud-scale
PDF             1       2263 Typically, you have multiple ways to solve the preceding pro              0.4206
PDF             1       1663 Oracle AI Database Backup and Recovery User's Guide to learn              0.4257
PDF             1       2278 Copyright © 1993, 2026, Oracle and/or its affiliates.                     0.4417
                             April
PDF             1       2437 D:20260420030906-08'00'                                                   0.4438
                             undo tablespace
                             D:20260420030906-0

10 rows selected.

O documento técnico sobre RMAN aparece em 2º lugar (distância 0,3966), logo após chunks do Database Concepts que tratam exatamente do mesmo tema. O banco varreu três fontes diferentes — texto livre, documentação estruturada e PDF — e retornou os resultados mais relevantes, ordenados por similaridade semântica.

Um detalhe de implementação importante: a subquery q_embed gera o embedding da query uma única vez e o reutiliza nas três buscas. Sem ela, o banco chamaria VECTOR_EMBEDDING três vezes para o mesmo string — triplicando o trabalho do modelo.


COSINE vs EUCLIDEAN: qual métrica usar?

Neste lab também comparei as duas métricas mais comuns. Para "falha de backup no banco de dados":

SQL> EXEC :query := 'falha de backup no banco de dados'

PL/SQL procedure successfully completed.

SQL> SELECT doc_id, SUBSTR(titulo, 1, 40)   AS titulo,
  2         ROUND(VECTOR_DISTANCE(embedding, VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), COSINE), 4) AS dist_cosine,
  3         ROUND(VECTOR_DISTANCE(embedding, VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), EUCLIDEAN), 4) AS dist_euclidean
  4    FROM rrezende.doc_suporte
  5    ORDER BY dist_cosine ASC
  6    FETCH FIRST 5 ROWS ONLY;

DOC_ID TITULO                                    DIST_COSINE DIST_EUCLIDEAN
------ ----------------------------------------- ----------- --------------
    17 Backup RMAN falhou — ORA-19809 limit exc          .57         1.0677
    18 Relatório de manutenção preventiva — ago        .6652         1.1535
    19 Deadlock recorrente no módulo de faturam        .7451         1.2207
    14 Falha de conexão intermitente — TNS-1254        .7544         1.2284
    13 Lentidão em queries após reorganização d        .7562         1.2298

5 rows selected.

O ranking é consistente entre as duas métricas para este dataset — o que é esperado quando os textos têm tamanhos similares.

A diferença aparece em coleções com textos de tamanhos muito variados: COSINE tende a ser mais robusto porque é invariante à magnitude do vetor — dois textos de tamanhos muito diferentes podem ser semanticamente próximos mesmo com normas diferentes.


O que levar pra casa

Vector Search no Oracle 26ai não é uma feature de nicho para projetos de IA. É uma extensão natural da capacidade relacional do banco — você continua usando SQL, continua com suas tabelas normais, continua com ACID e índices. Apenas adiciona uma nova dimensão de busca: o significado.

Os pontos principais:

  • Modelo ONNX local: sem dependência externa, sem custo por chamada, sem latência de rede — o modelo roda dentro do banco.
  • SQL nativo: VECTOR_EMBEDDING e VECTOR_DISTANCE são funções SQL. Combinam naturalmente com WHERE, JOIN e qualquer predicado relacional.
  • Multilingual de graça: o all-MiniLM-L12-v2 entende português e inglês — uma query em PT encontra documentos em EN e vice-versa.
  • Chunking nativo de PDF: o pipeline UTL_TO_TEXT → UTL_TO_CHUNKS → VECTOR_EMBEDDING roda inteiramente no banco, sem ferramentas externas.
  • Busca híbrida: semântica + keyword no mesmo SQL — o melhor de cada abordagem para termos técnicos específicos.

O próximo passo: SELECT AI com RAG

Vector Search resolve o problema da recuperação — encontrar os documentos certos. O próximo artigo da série vai além: usar esses documentos como contexto para um LLM gerar respostas em linguagem natural.

Isso é o RAG (Retrieval-Augmented Generation) — e o Oracle 26ai tem suporte nativo via SELECT AI com a opção vector_index_name no profile.


Participe da conversa

Você já está usando Vector Search no Oracle em produção? Qual modelo de embedding está usando — ONNX local ou API externa? Qual foi o maior obstáculo na configuração?

Deixe um comentário abaixo e me encontre no LinkedIn — esse é exatamente o tipo de assunto que rende uma boa conversa entre DBAs.

Até o próximo artigo.

— Ricardo Rezende (@ricarezende)


Artigos anteriores desta série:

› SELECT AI no Oracle 23ai e 26ai: conversando com o banco em português

› SELECT AI no Oracle 23ai e 26ai: controlando o consumo de tokens por schema

Marcadores