Pular para o conteúdo
Docker

Qdrant: instalando um banco de dados vetorial na VPS

Atualizado em
Ilustração colorida de um unicórnio de crina arco-íris dentro de uma estante de vidro cheia de cristais que se agrupam por semelhança, ligados por fios de luz, com uma coruja apontando uma varinha e uma torre de castelo servindo de servidor

Olá meus Unicórnios! 🦄✨

Toda vez que alguém fala em "IA com memória", "busca semântica" ou "RAG", aparece no meio da conversa um bicho chamado banco de dados vetorial. E aí vem aquela sensação de que falta um pré-requisito que todo mundo tem menos você. 😅

Pois hoje a gente instala um. Do zero, numa VPS, com o terminal aberto. No fim deste artigo você vai ter um Qdrant rodando no seu servidor, protegido por chave, com dados dentro e respondendo consultas por semelhança.

🧠 Antes de tudo: o que esse banco faz de diferente

Um banco comum guarda o que você escreveu e devolve quando bate exatamente. Você pergunta "onde nome = 'maçã'" e ele te dá a maçã. Se você digitar "maca" sem cedilha, ele não acha nada. Ele compara igualdade.

Um banco vetorial guarda listas de números e compara semelhança. Cada item vira uma lista assim:

maça   → [0.9,  0.1,  0.1,  0.1]
cereja → [0.85, 0.15, 0.1,  0.1]
banana → [0.1,  0.9,  0.1,  0.1]

Repare que maçã e cereja têm números parecidos, e a banana tem números diferentes. A pergunta que esse banco responde não é "qual é igual a este", é "quais são os mais próximos deste". É essa mudança que permite procurar por sentido em vez de por letra.

Essas listas de números têm nome: embeddings. Quem as produz é um modelo de IA, que lê um texto (ou uma imagem, ou um áudio) e devolve a lista. O banco vetorial não gera nada disso, ele guarda e procura. Neste tutorial os números vão ser escritos à mão, de propósito: assim dá para ver a busca funcionando sem depender de nenhuma chave de API de modelo.

🛒 Um exemplo que mostra por que isso vale a pena

Antes de instalar qualquer coisa, deixa eu te mostrar aonde a gente chega. Imagine uma loja com estes produtos cadastrados:

PC Gamer Ryzen 7 com 32GB DDR5 e RTX 4060      R$ 6500   estoque 4
PC Gamer Ryzen 5 com 16GB DDR5 e RTX 3050      R$ 1950   estoque 7
Computador basico Intel i3 com 8GB DDR4        R$ 1750   estoque 0
Computador escritorio Intel i5 com 16GB DDR5   R$ 1890   estoque 3
Notebook Dell i5 com 16GB DDR5 tela 15"        R$ 3400   estoque 2
Notebook Acer Celeron com 4GB DDR4             R$ 1600   estoque 5

Um cliente digita na busca do site: "computador com memória ddr5". Com um banco comum, você escreveria algo assim:

SELECT * FROM produtos WHERE nome LIKE '%computador%'

E o resultado seria pobre por dois motivos. Primeiro, os dois notebooks ficariam de fora, porque nenhum tem a palavra "computador" no nome, mesmo sendo exatamente o que a pessoa quer. Segundo, os dois PCs Gamer também sumiriam, pelo mesmo motivo. Sobraria só o que tem a palavra escrita literalmente.

Com o Qdrant, a mesma busca traz isto (são resultados de verdade, do artigo que continua este):

72%  Computador escritorio Intel i5 com 16GB DDR5  |  R$ 1890  |  estoque 3
66%  Computador basico Intel i3 com 8GB DDR4       |  R$ 1750  |  estoque 0
63%  Notebook Acer Celeron com 4GB DDR4            |  R$ 1600  |  estoque 5

Olhe o notebook na terceira posição. Ele apareceu numa busca por "computador" sem ter essa palavra no nome, porque o banco entendeu que notebook é um computador. Nenhum LIKE faz isso, e nenhuma lista de sinônimos escrita à mão dá conta de todos os casos.

E dá para ir além, misturando com filtros comuns na mesma consulta. O cliente quer até R$ 2.000 e só o que tem em estoque:

72%  Computador escritorio Intel i5 com 16GB DDR5  |  R$ 1890  |  estoque 3
63%  Notebook Acer Celeron com 4GB DDR4            |  R$ 1600  |  estoque 5
57%  PC Gamer Ryzen 5 com 16GB DDR5 e RTX 3050     |  R$ 1950  |  estoque 7

O i3 sem estoque saiu, nada acima de R$ 2.000 entrou, e o PC Gamer subiu para o lugar vago. Semelhança e filtro, no mesmo pedido. 🎯

É para chegar aí que a gente instala o Qdrant hoje. Neste artigo o foco é deixar o servidor de pé, protegido e funcionando; a busca por texto que você acabou de ver tem artigo próprio, linkado no fim.

🖥️ O que você precisa ter em mãos

Uma VPS com Ubuntu e acesso root. Eu usei uma bem modesta, de 4 vCPU e 7,8 GB de RAM, mas você vai ver daqui a pouco que o Qdrant pede bem menos que isso.

Para entrar nela, o comando é o mesmo em qualquer sistema. Abra o terminal (no Windows, o PowerShell serve) e digite:

ssh root@SEU_IP_AQUI

Troque SEU_IP_AQUI pelo IP que o seu provedor te deu. Ele vai pedir a senha, e aqui vem o primeiro susto de quem nunca usou SSH: a senha não aparece enquanto você digita. Nem asterisco, nem pontinho, nada. Parece que o teclado morreu. Não morreu: digite e dê Enter. 🙂

Na primeiríssima conexão ele pergunta se você confia na máquina. Responda yes, por extenso.

🐳 Instalando o Docker

O Qdrant vai rodar dentro de um contêiner, então o Docker precisa estar na máquina. Se a sua VPS ainda não tem, o próprio site oficial dá um script que resolve:

curl -fsSL https://get.docker.com | sh

Aquele | no meio é o "cano": ele pega o que o curl baixou e entrega para o sh executar. Leva um ou dois minutos.

Para conferir se deu certo:

docker --version
docker compose version

Foi o que respondeu aqui:

Docker version 29.7.2, build a7dcaa6
Docker Compose version v5.5.0

As duas linhas precisam aparecer. O compose é o que lê o arquivo de configuração que a gente vai escrever agora, e nas versões atuais ele já vem junto com o Docker, sem instalação separada.

📁 Criando a pasta e o arquivo de configuração

Toda a instalação vai morar numa pasta só. Assim, no dia em que você quiser mexer, apagar ou fazer backup, é um lugar só para olhar:

mkdir -p /opt/qdrant
cd /opt/qdrant

O mkdir cria a pasta e o cd entra nela. O -p só evita reclamação caso ela já exista.

Agora o arquivo que descreve o contêiner. Vamos criá-lo com o nano, que é o editor de texto mais simples do Linux:

nano docker-compose.yml

A tela fica preta e vazia, com um rodapé cheio de atalhos. Cole o conteúdo abaixo e preste atenção em como se cola no terminal, porque este é o ponto em que muita gente trava: o Ctrl+V de sempre não funciona aqui. Use Ctrl+Shift+V, ou clique com o botão direito do mouse.

services:
  qdrant:
    image: qdrant/qdrant:v1.12.4
    container_name: qdrant
    restart: always
    ports:
      - 6333:6333
      - 6334:6334
    volumes:
      - qdrant_dados:/qdrant/storage
    environment:
      - QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY}

volumes:
  qdrant_dados:

Para salvar: Ctrl+O (a letra O, de "output"), Enter para confirmar o nome, e Ctrl+X para sair. Aquele ^ que aparece no rodapé do nano significa a tecla Ctrl, então ^O se lê "Control O".

Vale entender três coisas desse arquivo, porque elas voltam mais para a frente:

A versão fixa na imagem (:v1.12.4) é de propósito. Se você escrever :latest, um docker compose pull feito daqui a seis meses pode trazer uma versão diferente da que você testou, e a surpresa vem sozinha. Versão presa, comportamento previsível.

O volume (qdrant_dados) é onde os dados de verdade ficam. Ele vive fora do contêiner, e é por isso que o contêiner pode ser destruído e recriado sem você perder nada. Daqui a pouco a gente prova isso na prática.

E as duas portas: a 6333 é a API REST, aquela que você chama com curl e onde fica o painel web. A 6334 é a mesma coisa em gRPC, que é o protocolo mais rápido usado pelas bibliotecas oficiais. Para este tutorial, quem interessa é a 6333.

🔑 A chave de API, que não é opcional

Repare que no compose eu não escrevi a chave: escrevi ${QDRANT_API_KEY}. Isso é uma referência a uma variável que vai morar num arquivo separado, o .env. Assim o docker-compose.yml pode ser copiado, mostrado e versionado sem levar o segredo junto.

Gere uma chave aleatória de verdade, em vez de inventar uma:

openssl rand -hex 32

Ele cospe uma sequência de 64 caracteres. Copie a sua e coloque no arquivo:

nano .env

Com uma linha só dentro, usando a sua chave:

QDRANT_API_KEY=SUA_CHAVE_AQUI

Salve do mesmo jeito: Ctrl+O, Enter, Ctrl+X.

Agora um detalhe que confunde: o arquivo some quando você lista a pasta. Digite ls e o .env não aparece. Ele não sumiu, é que no Linux todo nome que começa com ponto é considerado oculto. Para ver, peça o -la:

ls -la
-rw-r--r-- 1 root root   80 Aug 17 20:32 .env
-rw-r--r-- 1 root root  291 Aug 17 20:32 docker-compose.yml

Antes de subir qualquer coisa, peça ao Compose para conferir se ele entendeu o arquivo:

docker compose config

Esse comando mostra a configuração já resolvida, com a variável trocada pelo valor real. Se a indentação do YAML estiver errada, é aqui que você descobre, em vez de descobrir com o contêiner reiniciando sozinho. E ele serve de prova de que o .env está sendo lido: a chave aparece por extenso na saída, no lugar do ${QDRANT_API_KEY}.

🚀 Subindo o Qdrant

Chegou a hora:

docker compose up -d

O -d significa "modo destacado": o contêiner sobe e devolve o terminal para você, em vez de prender a tela com os logs. Na primeira vez ele baixa a imagem, o que leva um minuto:

Image qdrant/qdrant:v1.12.4 Pulled
Network qdrant_default Created
Volume qdrant_qdrant_dados Created
Container qdrant Created
Container qdrant Started

"Started" quer dizer que o contêiner subiu, não necessariamente que a aplicação está pronta. Confira o log:

docker logs qdrant

E aparece o Qdrant se apresentando, em ASCII art, com a versão e as duas portas:

           _                 _
  __ _  __| |_ __ __ _ _ __ | |_
 / _` |/ _` | '__/ _` | '_ \| __|
| (_| | (_| | | | (_| | | | | |_
 \__, |\__,_|_|  \__,_|_| |_|\__|
    |_|

Version: 1.12.4, build: 5b578c4f

INFO qdrant::actix: Qdrant HTTP listening on 6333
INFO qdrant::tonic: Qdrant gRPC listening on 6334
INFO qdrant: Distributed mode disabled
INFO qdrant::actix: TLS disabled for REST API

Duas linhas dessas merecem atenção. O Distributed mode disabled é o esperado: você tem um nó só, e é isso que a gente quer agora. Já o TLS disabled é um aviso de verdade, e eu volto nele no fim do artigo.

🔒 Provando que a chave está sendo exigida

Esse é o teste que eu não pulo em instalação nenhuma. Configurar a chave e acreditar que ela está valendo é diferente de conferir. Ainda de dentro da VPS:

curl http://localhost:6333/collections

Sem cabeçalho nenhum, a resposta é seca:

Invalid API key or JWT

E o código HTTP é 401. Agora a mesma chamada com a chave, no cabeçalho api-key:

curl http://localhost:6333/collections \
  -H "api-key: SUA_CHAVE_AQUI"
{
    "result": {
        "collections": []
    },
    "status": "ok",
    "time": 0.000052648
}

Passou, e a lista está vazia porque ainda não criamos nada. São trinta segundos de teste que respondem a pergunta que importa: a chave está sendo exigida de verdade, e não só decorando o arquivo de configuração.

Isso importa mais no Qdrant do que parece. Sem aquela variável no compose, ele sobe sem autenticação nenhuma, e não reclama disso em lugar nenhum: quem souber o IP e a porta lê, escreve e apaga suas coleções. A proteção não vem ligada por padrão; é você que liga.

🔥 Liberando a porta no firewall

Até agora todos os testes foram de dentro da máquina (localhost). Para acessar do seu computador, o firewall precisa deixar passar:

ufw allow 6333/tcp
ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
6333/tcp                   ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
6333/tcp (v6)              ALLOW       Anywhere (v6)

Se o firewall ainda estiver desligado na sua VPS e você for ligá-lo agora, libere o 22 antes de dar o ufw enable. A porta 22 é a do próprio SSH: ligando o firewall sem liberá-la, a sua conexão cai e você se tranca do lado de fora da máquina. É um erro que só se comete uma vez na vida. 😬

ufw allow 22/tcp
ufw allow 6333/tcp
ufw enable

Repare que a 6334 ficou de fora. É proposital: a gRPC só precisa estar aberta se algum sistema de fora for falar por ela. Porta que ninguém usa é porta que não se abre.

Agora, do seu computador, o mesmo teste de antes:

curl http://SEU_IP_AQUI:6333/collections \
  -H "api-key: SUA_CHAVE_AQUI"

Respondeu {"result":{"collections":[]},"status":"ok"}? Então o serviço está no ar e alcançável. 🎉

🖱️ O painel web, e o que ele pede primeiro

O Qdrant já vem com uma interface visual, sem instalar nada. Abra no navegador:

http://SEU_IP_AQUI:6333/dashboard

E a primeira coisa que aparece é ela, a chave sendo cobrada:

Painel do Qdrant exibindo a caixa Set API Key, que pede a chave de API antes de mostrar qualquer conteúdo

Essa tela é a mesma prova do 401, só que visual. Cole a chave que você gerou com o openssl e clique em Apply. Se você tivesse esquecido a variável no compose, o painel abriria direto, sem pedir nada, e qualquer pessoa com o link faria o mesmo.

Feito o login, a aba Collections mostra a lista, ainda vazia. Vamos enchê-la.

🍎 Criando a primeira coleção

Coleção, no Qdrant, é o equivalente à tabela: é onde os vetores ficam. Ao criar, você decide duas coisas que não dá para mudar depois: o tamanho do vetor e a forma de medir distância.

curl -X PUT http://SEU_IP_AQUI:6333/collections/frutas \
  -H "api-key: SUA_CHAVE_AQUI" \
  -H 'Content-Type: application/json' \
  -d '{"vectors":{"size":4,"distance":"Cosine"}}'

O size: 4 diz que cada vetor tem quatro números. No mundo real esse número vem do modelo que gera os embeddings (1.536 é comum), e tem que bater exatamente. Aqui usei 4 para caber na tela e você conseguir acompanhar as contas com o olho.

O distance: Cosine é a régua da comparação. A distância por cosseno olha para a direção dos números, não para o tamanho deles, e é a escolha padrão para busca por texto. As outras opções são Euclid e Dot.

Resposta: {"result":true,"status":"ok"}. Coleção criada.

➕ Inserindo os dados

Agora as quatro frutas. Cada ponto tem um id, o vector (os números) e o payload, que são os dados normais que você quer guardar junto:

curl -X PUT "http://SEU_IP_AQUI:6333/collections/frutas/points?wait=true" \
  -H "api-key: SUA_CHAVE_AQUI" \
  -H 'Content-Type: application/json' \
  -d '{"points":[
    {"id":1,"vector":[0.9,0.1,0.1,0.1],"payload":{"nome":"maca","cor":"vermelha"}},
    {"id":2,"vector":[0.85,0.15,0.1,0.1],"payload":{"nome":"cereja","cor":"vermelha"}},
    {"id":3,"vector":[0.1,0.9,0.1,0.1],"payload":{"nome":"banana","cor":"amarela"}},
    {"id":4,"vector":[0.1,0.1,0.9,0.1],"payload":{"nome":"uva","cor":"roxa"}}]}'

Aquele ?wait=true no fim da URL é pequeno e importante. Sem ele, o Qdrant aceita a escrita e responde na hora, gravando logo em seguida. Você faria a busca no instante seguinte e poderia não achar nada, passando meia hora procurando erro na consulta, quando o problema era só pressa. Com o wait=true, ele só responde depois de gravar:

{
    "result": {
        "operation_id": 0,
        "status": "completed"
    },
    "status": "ok",
    "time": 0.011761709
}

Voltando ao painel, a coleção agora aparece na lista:

Aba Collections do painel do Qdrant mostrando a coleção frutas com status green, 4 pontos, 4 segmentos e configuração de vetor tamanho 4 com distância Cosine

Status green, quatro pontos, e a configuração que a gente escolheu ali do lado: default 4 Cosine. No canto inferior esquerdo, a versão batendo com a que fixamos no compose.

🔍 A busca por semelhança

Agora o motivo de tudo isso. Vamos perguntar: quais frutas são mais parecidas com a maçã? Para isso, mandamos o vetor da maçã e pedimos os três primeiros:

curl -X POST http://SEU_IP_AQUI:6333/collections/frutas/points/search \
  -H "api-key: SUA_CHAVE_AQUI" \
  -H 'Content-Type: application/json' \
  -d '{"vector":[0.9,0.1,0.1,0.1],"limit":3,"with_payload":true}'

Dá para fazer isso pelo painel também, na aba do console, que é o jeito mais confortável de brincar com as consultas:

Console do painel do Qdrant com a consulta de busca à esquerda e o resultado à direita, mostrando maca com score 1, cereja com 0.99797505 e uva com 0.23809524

E olhe os números da coluna da direita, porque eles contam a história inteira:

maca    score 1.0
cereja  score 0.99797505
uva     score 0.23809524

A maçã tirou 1.0 porque comparei o vetor dela com ela mesma: semelhança perfeita. A cereja tirou 0,998, quase empatada. E a uva ficou lá embaixo, com 0,238.

Isso é a busca por semelhança acontecendo. Eu não perguntei por cor, não filtrei por nada, não escrevi "vermelha" em lugar nenhum da consulta: perguntei quem se parece com a maçã, e ele me trouxe a cereja. Troque as frutas por textos de artigos, e é exatamente assim que uma busca semântica funciona. ✨

Repare também no que não apareceu: pedi limit: 3 e vieram três, então a banana ficou de fora por ser a menos parecida. O limit aqui não é paginação, é o "me traga os N mais próximos".

✍️ "E se eu quiser buscar digitando uma palavra?"

Foi a primeira pergunta que me fizeram quando mostrei isto funcionando, e ela merece uma resposta clara: não dá para mandar texto direto para o Qdrant. Tentando, ele reclama:

{
    "status": {
        "error": "Format error in JSON body: data did not match any variant of untagged enum NamedVectorStruct"
    }
}

É que ele não lê palavra nenhuma. O Qdrant guarda e compara números, só isso. Quem transforma "laranja vermelha" em números é o tal modelo de embeddings que citei lá no começo, e ele fica fora do banco.

O caminho completo é este:

Fluxograma da busca por texto: na gravação, cada produto passa pelo modelo de embeddings e é salvo no Qdrant; na busca, a frase digitada passa pelo mesmo modelo e o Qdrant devolve os mais parecidos UMA VEZ, AO CADASTRAR Cada produto "maca vermelha" Modelo de embeddings Qdrant guarda o vetor + o payload TODA VEZ, AO BUSCAR Voce digita "laranja vermelha" O MESMO modelo vira 384 numeros Qdrant compara e devolve os parecidos tem de ser o mesmo Trocar o modelo depois de gravar e como medir o acervo em centimetros e consultar em polegadas: responde, mas o resultado nao presta.

E tem uma regra que não perdoa: o modelo da busca precisa ser o mesmo que gerou os vetores guardados. É como uma régua. Se você mediu tudo em centímetros, não adianta consultar em polegadas: os números até chegam, mas a comparação vira sorteio.

Para buscar digitando, o caminho é criar outra coleção, com o tamanho que o seu modelo devolve (384, 768 e 1.536 são os comuns), e gerar os vetores pelo modelo tanto na hora de gravar quanto na hora de buscar. Aí sim você digita "laranja vermelha" e ele acha "maçã vermelha e doce" sem que a palavra bata.

Busca semântica no Qdrant: digitando o que você querO passo a passo em Node.js, com o modelo rodando de graça no seu servidor e o filtro de preço e estoque na mesma consulta.blog.palomamacetko.com.br

🎯 Misturando semelhança com filtro comum

Esse é o pulo do gato que costuma decidir a escolha do banco. Dá para pedir "o mais parecido com a maçã, mas só entre as amarelas", misturando a busca vetorial com um filtro nos dados normais:

curl -X POST http://SEU_IP_AQUI:6333/collections/frutas/points/search \
  -H "api-key: SUA_CHAVE_AQUI" \
  -H 'Content-Type: application/json' \
  -d '{"vector":[0.9,0.1,0.1,0.1],"limit":3,"with_payload":true,
       "filter":{"must":[{"key":"cor","match":{"value":"amarela"}}]}}'

E vem só a banana, com o score baixo que ela merece:

{
    "result": [
        {
            "id": 3,
            "score": 0.23809524,
            "payload": {
                "nome": "banana",
                "cor": "amarela"
            }
        }
    ],
    "status": "ok"
}

Na prática é isso que você usa o tempo todo: "artigos parecidos com este, mas só os publicados", "produtos parecidos com este, mas só os que têm estoque". A semelhança escolhe, o filtro restringe.

😵 Três erros que eu vi enquanto fazia isto

Todos os três dão mensagens claras, o que é uma boa notícia. Vale conhecer as três agora, para reconhecer na hora em vez de procurar no escuro.

1. O vetor com o tamanho errado. Mandei três números numa coleção de quatro:

{
    "status": {
        "error": "Wrong input: Vector dimension error: expected dim: 4, got 3"
    },
    "time": 0.001680171
}

Esse é o erro mais comum de quem está começando com embeddings de verdade, e quase sempre significa a mesma coisa: você trocou o modelo que gera os vetores, e o novo devolve um tamanho diferente do que a coleção espera. Como o tamanho é decidido na criação, o jeito é criar outra coleção e migrar.

2. A coleção que não existe. Um erro de digitação no nome:

{
    "status": {
        "error": "Not found: Collection `naoexiste` doesn't exist!"
    },
    "time": 0.000016
}

3. A chave errada. Volta o Invalid API key or JWT de antes. Detalhe cruel: é a mesma mensagem de quando você esquece o cabeçalho inteiro. Então, se aparecer isso, confira as duas coisas: se o cabeçalho está indo e se o valor dele está certo.

🤔 O zero que assusta e não é problema

Peça as informações da coleção e olhe com atenção:

curl http://SEU_IP_AQUI:6333/collections/frutas \
  -H "api-key: SUA_CHAVE_AQUI"
{
    "result": {
        "status": "green",
        "indexed_vectors_count": 0,
        "points_count": 4,
        "segments_count": 4
    }
}

São quatro pontos, mas indexed_vectors_count é zero. Parece que a indexação falhou, ou que os dados não entraram direito. Não é isso.

O Qdrant só constrói o índice HNSW (a estrutura que deixa a busca rápida em volume grande) a partir de 20 mil vetores por segmento, que é o valor padrão do indexing_threshold. Abaixo disso, ele simplesmente compara um por um, e nesse tamanho isso é mais rápido do que consultar um índice, além de dar resultado exato.

Ou seja: com poucos dados, esse número vai ficar zero e a busca vai funcionar perfeitamente, como você acabou de ver. Vale saber disso agora, porque é o tipo de campo que assusta em produção e faz a pessoa sair mexendo em configuração que não precisava ser mexida. 😌

📊 Quanto ele consome, de verdade

Com tudo de pé, perguntei ao Docker:

docker stats --no-stream
NAME     MEM USAGE / LIMIT     CPU %
qdrant   28.31MiB / 7.755GiB   0.14%

28 MB de RAM. É menos que muita aba de navegador. A imagem no disco tem 279 MB, o que também é modesto perto de outros bancos.

Claro que esse número cresce: o Qdrant mantém o índice em memória, então quem manda no consumo é a quantidade de vetores e o tamanho de cada um. Mas para começar, testar e rodar um projeto pequeno, a VPS mais barata dá conta com folga.

💾 Provando que os dados sobrevivem

Lembra do volume lá no compose? Vamos provar que ele funciona, derrubando o contêiner de propósito:

cd /opt/qdrant
docker compose down
Container qdrant Removed
Network qdrant_default Removing
Network qdrant_default Removed

O contêiner foi removido, não só parado. Mas o volume continua lá:

docker volume ls
local     qdrant_qdrant_dados

Subindo de novo e perguntando quantos pontos existem:

docker compose up -d

E a coleção frutas responde points_count: 4, com as mesmas frutas de antes. Os dados nunca estiveram dentro do contêiner: estavam no volume, ao lado dele.

🧰 Os comandos do dia a dia

Todos rodam de dentro de /opt/qdrant:

docker compose ps          # o que esta de pe
docker compose logs -f     # logs ao vivo (Ctrl+C para sair)
docker compose restart     # reiniciar
docker compose up -d       # subir, ou aplicar mudanca no compose
docker compose down        # parar (volumes preservados)

O up -d é o mesmo comando de subir e de aplicar mudança: se você editar o docker-compose.yml, é ele que percebe a diferença e recria o contêiner.

🔓 O HTTP puro, e onde eu não pararia por aqui

Tem uma linha lá do log que eu deixei para o fim, de propósito:

INFO qdrant::actix: TLS disabled for REST API

Sem HTTPS, a sua chave de API viaja em texto puro em toda requisição. Para estudar, testar e montar um protótipo, tudo bem. Para guardar dado que importa, não: o caminho é pôr um proxy reverso na frente (Nginx, Caddy ou Traefik) com certificado, e deixar o Qdrant escutando só localmente.

E, já que a gente falou de firewall, vale a versão mais cuidadosa: em vez de ufw allow 6333/tcp para o mundo inteiro, dá para liberar só o IP que precisa acessar:

ufw allow from SEU_IP_DE_CASA to any port 6333 proto tcp

Assim o banco fica visível para você e invisível para o resto da internet, o que é bem mais confortável enquanto o HTTPS não entra.

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Perguntas frequentes

O que é um banco de dados vetorial?
É um banco que guarda listas de números (os vetores) em vez de texto, e busca por semelhança em vez de igualdade. Enquanto um banco comum responde "qual linha tem exatamente este valor", o vetorial responde "quais são os cinco itens mais parecidos com este". É o que está por trás de busca semântica, recomendação e da memória de sistemas com IA.
Quanto de servidor o Qdrant precisa?
Muito menos do que se imagina para começar. Com o contêiner de pé e uma coleção pequena, o consumo medido com docker stats foi de 28 MB de RAM e CPU praticamente parada. A imagem ocupa 279 MB no disco. Uma VPS básica dá conta com folga, e o que cresce depois é a memória, conforme a quantidade de vetores.
O Qdrant já vem protegido por senha?
Não. Sem a variável QDRANT__SERVICE__API_KEY, ele sobe sem autenticação nenhuma: quem souber o IP e a porta lê, escreve e apaga suas coleções. A proteção não é padrão, é opção. Depois de configurar a chave, qualquer requisição sem o cabeçalho api-key passa a receber 401.
Por que meus vetores aparecem com indexed_vectors_count igual a zero?
Porque o índice HNSW só é construído a partir de 20 mil vetores por segmento, que é o valor padrão de indexing_threshold. Abaixo disso o Qdrant compara um por um, o que é mais rápido no volume pequeno. A busca funciona normalmente: o zero ali não significa que os dados sumiram nem que algo deu errado.
O docker compose down apaga as coleções do Qdrant?
Não. O docker compose down remove o contêiner mas preserva o volume, então as coleções continuam lá quando você subir de novo. O que apaga tudo é o docker compose down -v: o -v remove os volumes junto. É a mesma diferença de qualquer stack com Docker, e vale conferir antes de digitar.
Dá para buscar digitando uma palavra, tipo "laranja vermelha"?
Não direto no Qdrant: ele compara números, não lê texto, e responde com erro de formato se você mandar uma frase. Quem traduz a frase em números é um modelo de embeddings, que roda fora do banco, e ele precisa ser o mesmo que gerou os vetores guardados. A coleção deste tutorial não aceita busca por texto porque os quatro números de cada fruta foram escritos à mão, e nenhum modelo produz aqueles valores.
Qual a diferença entre as portas 6333 e 6334 do Qdrant?
A 6333 é a API REST, a que você usa com curl e onde vive o painel web. A 6334 é a mesma API em gRPC, mais rápida e usada pelas bibliotecas oficiais quando você configura o cliente para isso. Para começar, basta liberar a 6333 no firewall: a gRPC só precisa ser exposta se algum sistema de fora for falar por ela.

Leia também