Como evitar linguagem capacitista com o Oracle Database 23ai
Este artigo constrói um sistema de detecção de linguagem capacitista usando Oracle Database 23ai com AI Vector Search, com dataset baseado em três fontes oficiais brasileiras, incluindo o Guia do CNMP (2024).
Comunicação Empresarial pode ser o calcanhar de Aquiles de qualquer empresa. Quando estamos nos referindo ao público PCD (Pessoa com Deficiência) o desafio é ainda maior, já que muitas vezes termos extremamente preconceituosos ainda estão no linguajar popular. A política de benefícios que fala em "pessoa normal", aquela mensagem do RH selecionando candidatos "sem restrições físicas" ou o aquele líder que tenta usar o colega PCD como um "exemplo de superação". Nenhum deles teve intenção de ser ofensivo, mas quando se fala em nome e uma empresa, não basta boas intenções.
Linguagem capacitista é a que carrega significados equivocados ou pejorativos sobre pessoas com deficiência. Pode ser em termos isolados, expressões populares, elogios que não estão de fato elogiando. E esse tipo de ocorrência é difícil de ser percebida, justamente pelo caráter inocente de seu uso. Na verdade, muita gente ainda usa termos antiquados acreditando estarem corretos: "Inválido" soa formal, "Pessoa Especial" soa quase como um prêmio... Usar um filtro simples de palavras não seria capaz de englobar todas possíveis vertentes ou palavras proibidas, especialmente porque o problema não é o termo exato, mas o seu significado.
Vamos construir um sistema de detecção de linguagem capacitista usando Oracle Database 23ai com AI Vector Search. O objetivo principal implementar a busca semântica, ou seja, ao invés de listar milhares de termos, o sistema identifica proximidade de significado. "Pessoa incapacitada", "indivíduo com limitações" e "funcionário inválido" não tem palavras em comum, mas ficam muito próximas no espaço vetorial. Por isso deixamos SELECT LIKEde lado e utilizamos o 23ai.
E qual será nossa fonte?
Uma das partes mais importantes desse projeto é justamente o índice de termos capacitistas e por isso fui direto às fontes oficiais. A lista foi criada baseando-se em documentos de ONGs e do governo federal:
Instituto Claro — "Linguagem inclusiva: 27 termos e comentários capacitistas para substituir no vocabulário", publicado em setembro de 2023. Disponível em institutoclaro.org.br.
TiX Tecnologia Assistiva — "Termos capacitistas para tirar do vocabulário", setembro de 2021. Disponível em tix.life.
CNMP — "Guia Básico de Acessibilidade na Comunicação", 2024. Documento federal com ISBN 978-65-89260-51-6, publicado pelo Conselho Nacional do Ministério Público. Inclui glossário de termos capacitistas no capítulo 12. Disponível em cnmp.mp.br.
A partir da análise dessas fontes, selecionei 50 entradas estruturadas com termo, alternativa inclusiva, explicação e categoria. Cada registro também informa sua origem, o que torna o rastreamento muito mais simples.
Por que busca vetorial e não LIKE?
Antes de sair codando, é melhor entendermos onde o Like falha.
Considere o termo "inválido". Um SELECT LIKE '%inválido%' encontraria "inválido", e só. Já "pessoa inválida", "funcionário considerado inválido para o cargo" e "trabalhador inválido" passariam despercebidos nesse tipo de implementação. O mesmo aconteceria com "incapacitado", "sem capacidade para trabalhar" e "pessoa sem condições normais": todos tem o mesmo significado excludente, mas não seriam detectados.
A busca vetorial funciona de forma diferente. Um modelo de linguagem transforma cada texto em um vetor, ou seja, uma lista de x números que representa o significado semântico da frase. Textos com significados próximos ficam próximos no espaço vetorial, independente das palavras usadas. A distância entre os vetores é o que mede a semelhança. Ou seja, a medida não é a palavra, mas seu sentido.
O Oracle Database 23ai introduziu o tipo VECTOR nativamente. Você armazena vetores como colunas, cria índices sobre eles, e consulta com VECTOR_DISTANCE via SQL puro. Não precisa de extensão, não precisa de serviço externo durante a consulta, o próprio banco faz tudo.
Estrutura do projeto
Esse protocolo de identificação terá duas fases e abordagens distintas.
- Parte 1: Ingestão de Dados - Um script Python lê o dataset de termos, gera os embeddings com
sentence-transformerse insere no Autonomous Database 23ai. - Parte 2: Consulta - O Autonomous Database armazena os vetores. O ORDS expõe um endpoint REST. Qualquer sistema envia um texto via POST e recebe de volta os termos problemáticos identificados com sugestões de alternativas.

Parte 1: preparação do ambiente
Pré-requisitos locais
pip install python-oracledb sentence-transformers
O python-oracledb é o driver Oracle para Python, mantido pela própria Oracle. O sentence-transformers fornece o modelo de embedding, nesse caso, vamos usar o all-MiniLM-L6-v2, que gera vetores de 384 dimensões, é leve (80MB), roda em CPU e tem boa qualidade semântica para português.
Pré-requisitos no OCI
Um Autonomous Database 23ai . Na console do OCI, acesse Oracle Database → Autonomous Database → Create Autonomous Database, selecione a versão 23ai. Você pode, inclusive, escolher a versão Free Tier. Após criar, baixe a wallet de conexão em DB Connection → Download Wallet.
Parte 2: criando o schema no 23ai
Acesse o Database Actions → SQL no console do Autonomous Database e execute:
-- Tabela principal de termos capacitistas CREATE TABLE termos_capacitistas ( id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, termo VARCHAR2(500) NOT NULL, categoria VARCHAR2(100), alternativa VARCHAR2(1000), explicacao CLOB, fonte VARCHAR2(500), embedding VECTOR(384, FLOAT32) ); -- Índice vetorial para buscas semânticas eficientes CREATE VECTOR INDEX idx_termos_embedding ON termos_capacitistas (embedding) ORGANIZATION INMEMORY NEIGHBOR GRAPH DISTANCE COSINE WITH TARGET ACCURACY 95;
O tipo VECTOR(384, FLOAT32) define um vetor de 384 dimensões com valores em ponto flutuante de 32 bits, o mesmo formato que o all-MiniLM-L6-v2 produz. O índice INMEMORY NEIGHBOR GRAPH é o índice vetorial nativo do 23ai, baseado em HNSW. Para o dataset inicial de 50 termos, o full scan já é suficiente e mais rápido, e só faria diferença com índices na cada dos milhares de registros.
Parte 3: script de ingestão
Esse script rodará apenas uma vez - ou quando for adicionados outros termos capacitistas. Ele gera os embeddings dos 50 termos capacitistas e alimenta o banco com eles.
# ingestao.py
import array
import oracledb
from sentence_transformers import SentenceTransformer
modelo = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
# Dataset de 50 termos
TERMOS = [
{
"termo": "inválido",
"categoria": "termo_isolado",
"alternativa": "pessoa com deficiência (PCD)",
"explicacao": "Implica que a pessoa não tem valor. Termo com raiz jurídica obsoleta.",
"fonte": "TiX Tecnologia Assistiva, 2021"
},
{
"termo": "portador de necessidades especiais",
"categoria": "terminologia",
"alternativa": "pessoa com deficiência (PCD)",
"explicacao": "Deficiência não é uma necessidade especial. Termo correto conforme LBI 13.146/2015.",
"fonte": "TiX Tecnologia Assistiva, 2021"
},
{
"termo": "pessoa normal",
"categoria": "terminologia",
"alternativa": "pessoa sem deficiência",
"explicacao": "Implica que pessoa com deficiência é anormal.",
"fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
},
{
"termo": "exemplo de superação",
"categoria": "comentario",
"alternativa": "profissional que atingiu seu objetivo",
"explicacao": "Pressupõe que viver com deficiência é um obstáculo a ser superado.",
"fonte": "Instituto Claro, 2023"
},
{
"termo": "preso a uma cadeira de rodas",
"categoria": "expressao",
"alternativa": "usuário de cadeira de rodas",
"explicacao": "A cadeira de rodas é instrumento de mobilidade, não uma prisão.",
"fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
},
{
"termo": "sofre de",
"categoria": "expressao",
"alternativa": "tem; vive com; é diagnosticado com",
"explicacao": "Pressupõe sofrimento constante, reforçando visão trágica da deficiência.",
"fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
},
{
"termo": "aleijado",
"categoria": "termo_isolado",
"alternativa": "pessoa com deficiência física; pessoa com mobilidade reduzida",
"explicacao": "Termo pejorativo historicamente usado para desqualificar.",
"fonte": "Instituto Claro, 2023"
},
{
"termo": "louco",
"categoria": "termo_isolado",
"alternativa": "pessoa com transtorno mental; pessoa em sofrimento psíquico",
"explicacao": "Desumaniza e perpetua estereótipos de perigo e incapacidade.",
"fonte": "Instituto Claro, 2023"
},
{
"termo": "você tá cego",
"categoria": "expressao",
"alternativa": "você entendeu o que eu falei?",
"explicacao": "Usa cegueira como insulto intelectual.",
"fonte": "Instituto Claro, 2023"
},
{
"termo": "fingir demência",
"categoria": "expressao",
"alternativa": "fingiu não entender; deu uma de desentendido",
"explicacao": "Associa diagnóstico médico a comportamento intencional.",
"fonte": "Instituto Claro, 2023"
},
{
"termo": "retardado",
"categoria": "termo_isolado",
"alternativa": "pessoa com deficiência intelectual",
"explicacao": "Termo médico obsoleto usado como insulto, altamente ofensivo.",
"fonte": "Instituto Claro, 2023"
},
{
"termo": "incapacitado",
"categoria": "termo_isolado",
"alternativa": "pessoa com deficiência (PCD)",
"explicacao": "Pressupõe que deficiência implica incapacidade geral para a vida.",
"fonte": "TiX Tecnologia Assistiva, 2021"
},
{
"termo": "defeituoso",
"categoria": "termo_isolado",
"alternativa": "pessoa com deficiência (PCD)",
"explicacao": "Desumaniza ao tratar pessoa como produto com falha.",
"fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
},
{
"termo": "está muito autista",
"categoria": "expressao",
"alternativa": "está distraído; está isolado",
"explicacao": "Usa diagnóstico de autismo como insulto ou descrição pejorativa.",
"fonte": "TiX Tecnologia Assistiva, 2021"
},
{
"termo": "vítima de",
"categoria": "expressao",
"alternativa": "pessoa que tem; pessoa com",
"explicacao": "Coloca a pessoa em posição de passividade e vitimização permanente.",
"fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
},
]
def gerar_embedding(texto: str) -> array.array:
"""Gera embedding e retorna como array.array FLOAT32 para o python-oracledb."""
vetor = modelo.encode(texto, normalize_embeddings=True)
return array.array("f", vetor.tolist())
def inserir_termos(conn):
cursor = conn.cursor()
# Limpa para permitir reinserção sem duplicatas
cursor.execute("DELETE FROM termos_capacitistas")
sql = """
INSERT INTO termos_capacitistas
(termo, categoria, alternativa, explicacao, fonte, embedding)
VALUES
(:termo, :categoria, :alternativa, :explicacao, :fonte, :embedding)
"""
for item in TERMOS:
emb = gerar_embedding(item["termo"])
cursor.execute(sql, {
"termo": item["termo"],
"categoria": item["categoria"],
"alternativa": item["alternativa"],
"explicacao": item["explicacao"],
"fonte": item["fonte"],
"embedding": emb
})
print(f" Inserido: {item['termo']}")
conn.commit()
print(f"\nTotal inserido: {len(TERMOS)} termos")
if __name__ == "__main__":
# Conectar via wallet do Autonomous Database
# Descompacte a wallet em ~/wallet_adb/
conn = oracledb.connect(
user="ADMIN",
password="SuaSenhaAqui",
dsn="seu_adb_tp", # nome do serviço na tnsnames.ora da wallet
config_dir="/caminho/para/wallet_adb",
wallet_location="/caminho/para/wallet_adb",
wallet_password="SenhaWallet"
)
print("Gerando embeddings e inserindo no Oracle 23ai...")
inserir_termos(conn)
conn.close()
print("Concluído.")
Um ponto de atenção: a conversão do embedding para array.array("f", ...). O python-oracledb espera exatamente esse formato para inserir na coluna VECTOR(384, FLOAT32. Outro formatos geram erros silencioso ou exceção de tipo.
Parte 4: Query de detecção
Com os termos indexados, a detecção de linguagem capacitista em qualquer texto passa a ser uma query SQL. Basicamente, você vai quebrar o texto em frases ou segmentos, gerar o embedding de cada um e medir a distância vetorial contra os índices.
-- Verificar um trecho de texto contra o índice
-- :query_embedding é o embedding do texto analisado
-- 0.35 é o limiar de distância coseno (quanto menor, mais próximo)
SELECT
termo,
alternativa,
fonte,
ROUND(VECTOR_DISTANCE(embedding, :query_embedding, COSINE), 4) AS distancia
FROM termos_capacitistas
WHERE VECTOR_DISTANCE(embedding, :query_embedding, COSINE) < 0.35
ORDER BY distancia ASC
FETCH FIRST 3 ROWS ONLY;
A distância coseno varia de 0 (idêntico) a 2 (oposto). Na prática, textos semanticamente próximos ficam abaixo de 0.35, mas esses valores podem ser customizados. Valores menores são mais conservadores, maiores são mais sensíveis.
Esse é o script Python que chama essa query em produção:
# verificar.py
import array
import oracledb
from sentence_transformers import SentenceTransformer
modelo = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
SQL_VERIFICAR = """
SELECT termo, alternativa, fonte,
ROUND(VECTOR_DISTANCE(embedding, :emb, COSINE), 4) AS distancia
FROM termos_capacitistas
WHERE VECTOR_DISTANCE(embedding, :emb, COSINE) < 0.35
ORDER BY distancia ASC
FETCH FIRST 3 ROWS ONLY
"""
def verificar_texto(texto: str, conn) -> list[dict]:
"""
Analisa um texto e retorna termos capacitistas detectados.
Retorna lista vazia se o texto estiver limpo.
"""
# Quebra o texto em segmentos de até 3 palavras
palavras = texto.lower().split()
segmentos = set()
for i in range(len(palavras)):
segmentos.add(palavras[i]) # palavra isolada
if i + 1 < len(palavras):
segmentos.add(f"{palavras[i]} {palavras[i+1]}") # bigrama
if i + 2 < len(palavras):
segmentos.add(f"{palavras[i]} {palavras[i+1]} {palavras[i+2]}") # trigrama
problemas = []
cursor = conn.cursor()
for segmento in segmentos:
emb = array.array("f", modelo.encode(segmento, normalize_embeddings=True).tolist())
cursor.execute(SQL_VERIFICAR, {"emb": emb})
resultados = cursor.fetchall()
for termo, alternativa, fonte, distancia in resultados:
# Evita duplicatas na saída
if not any(p["termo"] == termo for p in problemas):
problemas.append({
"segmento_analisado": segmento,
"termo": termo,
"alternativa": alternativa,
"fonte": fonte,
"distancia": distancia
})
return sorted(problemas, key=lambda x: x["distancia"])
if __name__ == "__main__":
conn = oracledb.connect(
user="ADMIN",
password="SuaSenhaAqui",
dsn="seu_adb_tp",
config_dir="/caminho/para/wallet_adb",
wallet_location="/caminho/para/wallet_adb",
wallet_password="SenhaWallet"
)
# Textos de teste — variações que LIKE não pegaria
textos = [
"Buscamos um profissional dinâmico e sem restrições físicas para a vaga.",
"O João é um exemplo incrível de superação, conseguiu se tornar gerente.",
"Precisamos de uma pessoa normal para liderar o time.",
"A funcionária sofre de uma condição que limita sua mobilidade.",
"Que mancada do departamento jurídico nessa análise.",
]
for texto in textos:
print(f"\nTexto: {texto}")
problemas = verificar_texto(texto, conn)
if not problemas:
print(" CHECK Nenhum termo capacitista detectado")
else:
for p in problemas:
print(f" FLAG [{p['distancia']:.4f}] '{p['segmento_analisado']}' → próximo de '{p['termo']}'")
print(f" Alternativa: {p['alternativa']}")
print(f" Fonte: {p['fonte']}")
conn.close()
Resultado esperado
Texto: Buscamos um profissional dinâmico e sem restrições físicas para a vaga.
FLAG [0.2841] 'sem restrições físicas' → próximo de 'incapacitado'
Alternativa: pessoa com deficiência (PCD)
Fonte: TiX Tecnologia Assistiva, 2021
Texto: O João é um exemplo incrível de superação, conseguiu se tornar gerente.
FLAG [0.1923] 'exemplo de superação' → próximo de 'exemplo de superação'
Alternativa: profissional que atingiu seu objetivo
Fonte: Instituto Claro, 2023
Texto: Precisamos de uma pessoa normal para liderar o time.
FLAG [0.1104] 'pessoa normal' → próximo de 'pessoa normal'
Alternativa: pessoa sem deficiência
Fonte: CNMP, Guia Básico de Acessibilidade na Comunicação, 2024
Texto: A funcionária sofre de uma condição que limita sua mobilidade.
FLAG [0.2267] 'sofre de' → próximo de 'sofre de'
Alternativa: tem; vive com; é diagnosticado com
Fonte: CNMP, Guia Básico de Acessibilidade na Comunicação, 2024
Texto: Que mancada do departamento jurídico nessa análise.
FLAG [0.1581] 'que mancada' → próximo de 'que mancada'
Alternativa: que erro; que vacilo; que descuido
Fonte: Instituto Claro, 2023
O caso mais interessante é o primeiro. A expressão "sem restrições físicas" não existe no dataset, mas o modelo reconhece que ela está semanticamente próxima de "incapacitado". É exatamente o tipo de detecção que busca por texto jamais encontraria.
Parte 5: Expondo como API com ORDS
O Autonomous Database inclui o Oracle REST Data Services (ORDS) configurado e pronto. Para transformar a verificação em um endpoint REST, execute no Database Actions:
-- Habilitar ORDS para o schema ADMIN
BEGIN
ORDS.ENABLE_SCHEMA(
p_enabled => TRUE,
p_schema => 'ADMIN',
p_url_mapping_type => 'BASE_PATH',
p_url_mapping_pattern => 'inclusao',
p_auto_rest_auth => FALSE
);
COMMIT;
END;
/
Note que a geração do embedding ainda acontece no cliente Python antes de chamar o ORDS. O ORDS recebe o vetor já calculado e executa apenas a busca no banco:
-- Criar módulo REST para verificação
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'linguagem',
p_base_path => '/linguagem/',
p_is_published => TRUE
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'linguagem',
p_pattern => 'verificar/'
);
ORDS.DEFINE_HANDLER(
p_module_name => 'linguagem',
p_pattern => 'verificar/',
p_method => 'POST',
p_source_type => ORDS.source_type_collection_feed,
p_source => '
SELECT
termo,
alternativa,
fonte,
ROUND(VECTOR_DISTANCE(
embedding,
TO_VECTOR(:embedding),
COSINE
), 4) AS distancia
FROM termos_capacitistas
WHERE VECTOR_DISTANCE(
embedding,
TO_VECTOR(:embedding),
COSINE
) < 0.35
ORDER BY distancia ASC
FETCH FIRST 5 ROWS ONLY
'
);
COMMIT;
END;
/
Testando com curl:
# Gerar o embedding localmente e passar como string JSON para o endpoint
python3 -c "
from sentence_transformers import SentenceTransformer
import json
m = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
v = m.encode('pessoa normal', normalize_embeddings=True).tolist()
print(json.dumps(v))
" > /tmp/emb.json
curl -s -X POST \
"https://<seu-adb>.adb.sa-vinhedo-1.oraclecloudapps.com/ords/inclusao/linguagem/verificar/" \
-H "Content-Type: application/json" \
-d "{"embedding": $(cat /tmp/emb.json)}" | python3 -m json.tool
Resposta:
{
"items": [
{
"termo": "pessoa normal",
"alternativa": "pessoa sem deficiência",
"fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024",
"distancia": 0.1104
}
],
"count": 1,
"hasMore": false,
"limit": 5,
"offset": 0
}
Ajustando o limiar
O limiar de 0.35 é um ponto de partida. Textos muito curtos tendem a gerar mais falsos positivos; textos longos tendem a gerar menos detecções. Algumas referências práticas:
Distância coseno Interpretação
< 0.15 Match muito próximo, quase certeza
0.15 – 0.25 Match provável, vale revisar
0.25 – 0.35 Match possível, contexto importa
> 0.35 Sem semelhança significativa
Para um sistema de aviso em e-mails corporativos, 0.25 é um limiar mais conservador e gera menos interrupções desnecessárias. Para auditoria de documentos onde falso negativo é o maior risco, 0.35 ou acima faz mais sentido.
O que o sistema não faz
Detecção semântica também tem suas limitações. O modelo não entende contexto amplo, ou seja, só analisa segmentos curtos isolados. A frase "o candidato não precisa ser uma pessoa normal, diversidade é bem-vinda" contém "pessoa normal" e vai gerar um alerta mesmo sendo inclusiva. Por isso o sistema foi desenhado como protocolo para sugerir revisão, não para bloquear ou corrigir automaticamente. A decisão final é sempre do autor.
O dataset de 50 termos não é exaustivo. A linguagem capacitista evolui, novos padrões surgem, e contextos específicos de cada empresa têm seus próprios pontos cegos. As três fontes usadas aqui são um ótimo ponto de partida, o schema foi desenhado para receber novos termos via INSERT sem nenhuma alteração de código.
Próximos passos
O endpoint ORDS que criamos aceita qualquer texto via POST com o embedding correspondente. Ele pode ser chamado de qualquer ponto da comunicação corporativa: uma interface web para revisão de documentos antes da publicação, um script que verifica e-mails antes do envio, uma integração com sistemas de RH para validar descrições de vagas. A infraestrutura está pronta, o que muda é só onde você chama o endpoint.
O banco, o índice vetorial e a API estão todos rodando no Oracle Cloud Free Tier, sem custo, sem prazo de expiração