Linkagem semântica por embeddings em 5 propriedades Sanar

Arquitetura que conecta CID-10, Exames, Ferramentas médicas, Blog e SanarPós via embeddings vetoriais. Cada página descobre suas vizinhas semânticas automaticamente, o usuário fica no ecossistema e o funil converge pros cursos pagos.

Cliente
Plataforma de educação médica B2C, #1 no Brasil
Setor
EdTech médica
Categoria
Papel
Arquiteto da camada de linkagem semântica
Duração
2026
Stack
Embeddings multilingual 1024dpgvectorPHP 8.2WordPress shortcodesPython (ingest)NER para backlinks contextuais
S01

Contexto

O que é: uma camada de linkagem semântica entre 5 propriedades Sanar, CID-10, Exames, Ferramentas médicas, Blog e SanarPós, via embeddings vetoriais. Cada página descobre automaticamente suas vizinhas semânticas (sem depender de tag ou taxonomia manual) e o funil de navegação converge para o catálogo de cursos pagos em pos.sanar.com.br.

Contexto: 5 propriedades com SEO individual excelente vivendo em silos. Um artigo sobre hiperplasia adrenal congênita no blog não linkava pro CID E25, nem pro exame 17-OH progesterona, nem pra calculadora pediátrica, nem pro curso de Endocrinologia. Era a quíntupla óbvia daquele usuário, e ninguém ligava os pontos.

S02

Problema

Linkagem manual é inviável em escala. O Sanar publica novos posts, exames, códigos CID e cursos toda semana sem coordenação entre times. Matching por keyword é superficial: um artigo sobre "insulina basal" deveria linkar pra "hemoglobina glicada", mas o matcher de tag não enxerga isso. Resultado: o usuário entrava via busca orgânica, lia uma página, saía.

Além disso, cada property fazia SEO para si. Ninguém passava link juice de forma sistemática pros cursos, a camada que realmente monetiza.

Baseline CTR inter-property < 1%. Páginas/sessão ~1,5. Bounce rate ~68%. Entrada orgânica em /cursos a partir de conteúdo Sanar: desprezível.
S03

Abordagem

A ideia central: transformar cada URL em um vetor que representa o significado semântico da página (não só as palavras). Vetores próximos = páginas que tratam de conceitos próximos, mesmo sem palavra em comum. Na renderização, o engine busca os top-K vizinhos mais próximos e diversifica por tipo de propriedade, 1 CID + 1 Exame + 1 Ferramenta + 1 Curso, para que o bloco "Relacionados" nunca caia em câmara de eco.

Ecossistema Sanar conectado por embeddings Quatro propriedades de conteúdo (CID-10, Exames, Ferramentas médicas e Blog) alimentam um engine de embeddings que mede similaridade semântica entre todas as páginas e roteia o usuário para o catálogo de cursos em SanarPós, que é a camada de conversão comercial. ECOSSISTEMA SANAR · 5 PROPRIEDADES · 1 ENGINE DE EMBEDDINGS PROPRIEDADE CID-10 ~12k códigos · /cid10/ referência normativa PROPRIEDADE Exames 114 páginas A-Z · /exames/ +295% keywords (case 6) PROPRIEDADE Ferramentas calculadoras médicas Web Components (case 2) PROPRIEDADE Blog Sanar ~2,5k artigos · sanarmed.com pipeline IA + revisão (case 1) ingest · embeddings · indexação ENGINE Embeddings + similaridade semântica modelo multilingual 1024d · pgvector · top-K cosine sim · diversificação por tipo NER injeta backlinks contextuais (termo médico → página canônica) funil de conversão · bloco Relacionados + backlinks contextuais CAMADA COMERCIAL · CONVERSÃO $ SanarPós - Vitrine de cursos 20+ especialidades · Matricule-se · pos.sanar.com.br

Um segundo passo detecta entidades médicas no corpo do artigo via NER e injeta backlinks contextuais automaticamente: toda menção a um nome de exame vira link pra página canônica em /exames/, todo código CID vira link pra /cid10/, etc. Isso é o que fecha o anel de autoridade tópica.

S04

Execução

Ingest: crawler Python coleta title + body + meta de todas as 5 properties, Blog (~2,5k artigos), Exames (114 páginas), CID-10 (~12k códigos), Ferramentas (calculadoras) e SanarPós (20+ cursos). Chunking de 2k tokens por documento, embedding via API multilingual (1024 dimensões).

Store: pgvector via Supabase. Index HNSW para busca sub-100ms em ~20k vetores. Metadata indexada separadamente para filtro por property_type e published_at.

Runtime no WordPress: shortcode [sp_related diversify=true limit=4] consulta a similarity API e renderiza o bloco. Middleware em the_content escaneia o HTML e converte termos médicos em links contextuais usando um léxico canônico mantido pelo time editorial.

phpinc/embeddings/class-related.php
final class Sp_Related_Shortcode {
    public function render( array $atts ): string {
        $atts = shortcode_atts( [
            'limit'     => 4,
            'diversify' => 'true',
        ], $atts );

        $post_id = get_the_ID();
        $key     = 'sp_related_' . $post_id . '_' . md5( serialize( $atts ) );
        $cached  = get_transient( $key );
        if ( $cached !== false ) {
            return $cached;
        }

        $response = wp_remote_post( self::API . '/similar', [
            'timeout' => 8,
            'body'    => wp_json_encode( [
                'url'       => get_permalink( $post_id ),
                'k'         => (int) $atts['limit'],
                'diversify' => $atts['diversify'] === 'true',
            ] ),
        ] );

        if ( is_wp_error( $response ) ) {
            return '';
        }

        $items = json_decode( wp_remote_retrieve_body( $response ), true ) ?: [];
        $html  = $this->render_items( $items );
        set_transient( $key, $html, HOUR_IN_SECONDS );
        return $html;
    }
}
Shortcode com cache de 1h: a similarity API é externa mas o custo fica amortizado por hits.
pythoningest/embed_and_index.py
from pgvector.psycopg import register_vector

def embed_and_index(doc: dict) -> None:
    '''Embed a single document and upsert into pgvector.'''
    text = f"{doc['title']}\n\n{doc['body'][:8000]}"
    vector = embedding_client.embed(text)  # 1024-dim

    with conn.cursor() as cur:
        cur.execute(
            '''
            INSERT INTO documents (url, title, property_type, published_at, embedding)
            VALUES (%s, %s, %s, %s, %s)
            ON CONFLICT (url) DO UPDATE SET
                title = EXCLUDED.title,
                embedding = EXCLUDED.embedding,
                updated_at = NOW()
            ''',
            (doc['url'], doc['title'], doc['property_type'],
             doc['published_at'], vector),
        )

def similar(url: str, k: int = 4, diversify: bool = True) -> list[dict]:
    with conn.cursor() as cur:
        cur.execute('SELECT embedding FROM documents WHERE url = %s', (url,))
        source = cur.fetchone()
        if not source:
            return []

        query = '''
            SELECT url, title, property_type,
                   1 - (embedding <=> %s) AS similarity
            FROM documents
            WHERE url != %s
            ORDER BY embedding <=> %s ASC
            LIMIT %s
        '''
        cur.execute(query, (source[0], url, source[0], k * 4))
        rows = cur.fetchall()

    return _diversify_by_property(rows, k) if diversify else rows[:k]
Pipeline de ingest em Python. A fase de diversificação garante mix de CID + Exame + Ferramenta + Curso.
S05

Resultados

A história que os números contam: o usuário que entrava no blog pelo Google para ler sobre hiperplasia adrenal congênita agora termina a sessão com quatro páginas vistas, um código CID pra referência, um exame laboratorial pra diagnóstico, uma calculadora pediátrica pra dose de corticoide e, no fim, uma página de curso de Endocrinologia Pediátrica no SanarPós. Ele não foi empurrado pro curso; ele chegou lá seguindo o próprio interesse, porque cada página tinha a vizinha semântica certa.

-24 pp
Bounce rate
De 68% para 44% em páginas com bloco Relacionados
+73%
Páginas por sessão
1,5 → 2,6 em média
12%
CTR inter-property
De <1% para ~12% após injeção de backlinks
+18%
Entries em SanarPós
A partir de conteúdo do ecossistema
S06

Output reutilizável

O engine é agnóstico de CMS, o contrato é {url, title, body, property_type, published_at}. Reaproveitável em qualquer ecossistema com múltiplas propriedades (e-commerce multi-marca, editora com várias revistas, SaaS com docs + blog + pricing).

Artefatos reutilizáveis: pipeline de ingest (Python), shortcode WordPress com cache, léxico de entidades médicas para NER, playbook de diversificação por tipo de propriedade, queries pgvector otimizadas com HNSW.