Sua biblioteca de PDFs, do jeito que trabalho de verdade funciona.
Advogado com jurisprudência e contratos, engenheiro com manuais técnicos e datasheets, chef com receitas escaneadas, pesquisador com artigos científicos — todo mundo que lida com PDF em volume conhece o problema: dezenas de pastas, nomes de arquivo sem sentido, um documento perdido que você sabe que tem mas não lembra onde guardou. newPdfDing é uma biblioteca self-hosted, single-user, que organiza, indexa e — principalmente — encontra seus PDFs por você, mesmo quando você não lembra a palavra exata que procura.
Sem assinatura, sem SaaS, sem seus documentos saindo do seu servidor. Um binário único em Go, SQLite embutido, frontend SvelteKit no mesmo processo — sobe em segundos, roda em qualquer VPS barata ou num Raspberry Pi na estante.
A caixa de busca da biblioteca não é só léxica: quando GEMINI_API_KEY está configurada, ela também busca por significado, não só por palavra exata. É a diferença entre vasculhar título por título, esperando lembrar o nome exato do arquivo, e simplesmente lembrar do que ele tratava.
Cada profissão tem seu próprio vocabulário instável — sinônimos, jargão técnico, termos que mudam de autor para autor ou de época para época. A busca léxica exige a palavra exata; a busca semântica entende o conceito por trás dela:
- No mundo acadêmico, buscar por um termo técnico antigo ainda encontra o artigo publicado sob nomenclatura atualizada, porque a busca captura o significado do conteúdo, não a grafia exata.
- No escritório de advocacia, buscar "cláusula de rescisão" encontra um contrato cujo texto só fala em "extinção antecipada do vínculo contratual" — redação diferente, mesma cláusula.
- Na engenharia, buscar "superaquecimento do motor" traz o manual técnico que descreve "falha térmica por sobrecarga", mesmo sem usar as palavras exatas.
- Na cozinha, buscar "sobremesa sem glúten" recupera receitas escaneadas cujo título é só o nome do prato, porque a busca entende o conteúdo, não a etiqueta.
Na prática: sua biblioteca de PDFs — contratos, manuais, receitas, artigos, relatórios, o que for — vira pesquisável por conceito, não só por título e palavra-chave, sem depender de você lembrar exatamente como cada documento foi escrito.
Funciona assim:
- Cada PDF tem um botão "Embedar" — sob demanda, um documento por vez, nunca automático (sem varredura, sem embedding no upload, sem lote).
- Ao clicar, o servidor responde
202de imediato e apenas enfileira o pedido — o processamento em si roda num worker único de background que drena a fila em série, para nunca haver duas chamadas Gemini simultâneas. O job passa pelos estadosna fila→extraindo texto→embedando→concluído: extrai nome + descrição + início do texto do PDF e chama a API Gemini (batchEmbedContents) uma única vez, guardando o vetor resultante (3072 dimensões) em SQLite. - A interface acompanha o job ao vivo em
GET /api/embed/jobs: o rótulo do botão muda de fase sozinho, em qualquer página que exiba aquele documento — inclusive se o job foi disparado antes de a página carregar, em outra aba ou em outro dispositivo — e o ícone vira "Embedado" assim que termina, sem precisar recarregar. - A partir daí, toda busca na biblioteca funde dois candidatos — o rank léxico do FTS5 e o rank semântico por similaridade de cosseno — via Reciprocal Rank Fusion (RRF) numa caixa única. Não há seletor de "modo léxico vs. semântico": é sempre os dois, fundidos.
- Documentos ainda não embedados continuam aparecendo normalmente nos resultados léxicos — a busca nunca fica pior por não ter embedding, só melhor com ele. Três chips na biblioteca ("Sem embedding", "Atualizado", "Desatualizado") filtram pelo estado do embedding — útil para achar o que ainda falta embedar sem rolar a lista caçando ícone; clicar no chip ativo desliga o filtro.
- Editar nome, descrição ou o texto do PDF marca o embedding como
stale(desatualizado) até você reembedar — a interface avisa com o rótulo "Reembedar".
Documentos que chegam sem texto extraído — pela watch-dir, ou herdados de uma importação antiga já concluída em produção — têm o texto extraído automaticamente pelo visualizador na primeira vez que você os abre (mesmo pdf.js do upload, rodando no navegador) e enviado de volta ao servidor; só depois disso podem ser embedados, sem precisar re-enviar o arquivo.
Sem GEMINI_API_KEY configurada, nada disso quebra: o botão "Embedar" aparece desabilitado com um aviso, e a busca cai de volta para puramente léxica (FTS5 + fallback LIKE). Ver a mecânica completa — fórmula do texto embedado, fusão RRF, custo, tetos de escala — em refatoracao/04-busca-hibrida.md.
Com GEMINI_API_KEY configurada, a página de detalhes de cada PDF ganha três recursos que reaproveitam o texto já extraído do documento (o mesmo usado pela busca):
- Descrever com IA: gera um parágrafo de até 60 palavras a partir do conteúdo e preenche/salva o campo Descrição — útil para os PDFs escaneados/importados que chegam sem descrição nenhuma.
- Sugerir tags: lê o mesmo texto e sugere, como chips clicáveis, apenas tags que já existem no seu acervo — nunca inventa uma tag nova; um clique aplica a sugestão.
- Chat do documento: um card entre Descrição/Tags e Notas onde você faz perguntas sobre o conteúdo do PDF — "qual o prazo de rescisão?", "resuma o capítulo 3", "que temperatura o manual recomenda?". O modelo recebe o texto extraído inteiro (até 400 mil caracteres, com aviso quando corta) e responde só com base no documento, dizendo quando o assunto não está nele. A conversa não fica guardada: "Copiar para Notas" leva perguntas e respostas para o editor de Notas, onde você edita e salva quando quiser (a página avisa antes de sair com Notas não salvas). Não aparece em links de compartilhamento — ninguém gasta sua chave de API por você.
O modelo usado nesses recursos é escolhido em Configurações → IA ("Modelo para descrição e sugestão de tags"), numa lista alimentada em tempo real pela models.list da própria chave configurada. O modelo de embedding da busca semântica, por sua vez, é fixo no binário (models/gemini-embedding-2, ver refatoracao/04-busca-hibrida.md) — trocá-lo exigiria recompilar, porque mudaria o content_hash e invalidaria todo vetor já gravado. Sem GEMINI_API_KEY ou sem modelo escolhido, os botões e o chat continuam visíveis mas respondem com uma mensagem clara pedindo a configuração, em vez de falhar em silêncio. PDFs escaneados sem texto extraível não têm chat.
- Biblioteca: 4 layouts (grade/lista/compacto/mínimo), 7 ordenações, rolagem infinita, upload individual e em lote com processamento no navegador (preview e texto via pdf.js) — nada de fila de processamento no servidor, o PDF já entra pronto para busca.
- Busca híbrida: caixa única na biblioteca, léxica sempre ativa, semântica quando configurada (ver acima) — pesquise por conceito, não só por título exato.
- Metadados com IA: descrição, sugestão de tags e chat sobre o conteúdo gerados sob demanda a partir do texto do PDF via Gemini (ver acima) — sem custo de API além do que você aciona explicitamente.
- Filtro por estado de embedding: três chips na biblioteca ("Sem embedding", "Atualizado", "Desatualizado") isolam os PDFs em cada fase, combináveis com a busca — ache o que falta embedar sem procurar ícone por ícone.
- Tags: autocompletar com sugestão das tags existentes e criação de tag nova inline, direto na página de detalhes do PDF; lista de tags clicável na biblioteca para filtrar por tag; administração (renomear/excluir/fundir) em
/tags— organize contratos, manuais e receitas do seu jeito, sem taxonomia imposta. - Anotações: comentários e destaques por página, com exportação em YAML/JSON — grife o que importa e leve as anotações para fora, sem lock-in.
- Compartilhamento público: link sem senha/expiração, com contador de visualizações, revogável a qualquer momento — mande um documento pra alguém sem depender de e-mail com anexo de 40MB.
- Watch-dir opcional: deixa cair PDFs numa pasta observada e eles entram sozinhos no acervo, com tags padrão configuráveis — sincronize com a pasta que você já usa hoje.
- Administração:
/adminmostra contagens do acervo (total de PDFs, tags, espaço em disco e quantos documentos em cada estado de embedding), reindexa o FTS5 sob demanda e faz backup/restauração completa do banco SQLite.
docker run -d \
--name newpdfding \
--read-only \
--cap-drop=ALL \
-p 8000:8000 \
-e ADMIN_PASSWORD=change-me \
-e DB_PATH=/data/newpdfding.db \
-e FILES=/files \
-v newpdfding-data:/data \
-v newpdfding-files:/files \
ghcr.io/edalcin/newpdfding:latest--read-only --cap-drop=ALL é a execução recomendada (ver refatoracao/08-seguranca.md): a imagem é distroless e não escreve em lugar nenhum além dos volumes /data e /files montados acima, então travar o resto do filesystem como somente-leitura e derrubar todas as capabilities Linux não quebra nada.
Acesse http://localhost:8000 e entre com a senha definida em ADMIN_PASSWORD.
Some uma variável ao docker run acima (ou ao seu .env/template do Unraid):
-e GEMINI_API_KEY=sua-chave-aquiChave gratuita (com cota diária) em aistudio.google.com/apikey. Sem ela, tudo funciona normalmente — só a busca fica puramente léxica.
cp .env.example .env # preencher ADMIN_PASSWORD, GEMINI_API_KEY (opcional) e ajustar o resto
docker compose up --buildGuia dedicado com os campos exatos de Docker → Add Container: UNRAID.md.
Lista fechada — nenhuma variável fora dela é lida pelo binário. Ver .env.example para os placeholders e refatoracao/07-docker-ci-deploy.md para a tabela completa (obrigatória/default/significado).
| Variável | Obrigatória | Default |
|---|---|---|
ADMIN_PASSWORD |
Sim | — |
DB_PATH |
Sim | — |
FILES |
Sim | — |
LISTEN_ADDR |
Não | :8000 |
BASE_URL |
Não | derivado da requisição |
SESSION_IDLE_MINUTES |
Não | 43200 |
MAX_UPLOAD_MB |
Não | 200 |
TRUST_PROXY_HEADERS |
Não | false |
LOG_LEVEL |
Não | info |
GEMINI_API_KEY |
Não | (vazio — desliga a busca semântica) |
CONSUME_ENABLE |
Não | false |
CONSUME_DIR |
Não | <FILES>/consume |
CONSUME_INTERVAL_MINUTES |
Não | 5 |
CONSUME_TAGS |
Não | "" |
CONSUME_SKIP_EXISTING |
Não | true |
O modelo de texto usado por "Descrever com IA", "Sugerir tags" e pelo Chat do documento pode ser escolhido em Configurações → IA, numa lista populada pela sua própria chave. O modelo de embedding não é configurável: é fixo em models/gemini-embedding-2 (ver refatoracao/04-busca-hibrida.md).
Requer Go 1.27+ e Node 22+.
# Backend
go build ./cmd/newpdfding
go test ./...
# Frontend (gera frontend/build, embutido via go:embed em internal/server/web/dist)
cd frontend
npm ci
npm run buildServidor local:
export ADMIN_PASSWORD=dev DB_PATH=./dev.db FILES=./dev-files
go run ./cmd/newpdfdingBinário único: Go (chi, SQLite puro-Go, FTS5) servindo uma API REST e a SPA SvelteKit embutida via go:embed. Busca híbrida (FTS5 + embeddings Gemini sob demanda, fusão RRF), com um worker único de background que serializa o processamento de embedding — nunca automático, sempre disparado por um clique explícito. Detalhes completos do plano de refatoração e da arquitetura alvo: refatoracao/.
Este projeto é derivado de PdfDing por mrmn2, licenciado sob AGPL-3.0 — ver LICENSE.
