https://txtfetch.com/ingest/qdrant/
Extract text into Qdrant
An open-source and managed vector database with a strict, easy-to-miss ID format. Here's what changes between the text txtfetch returns and the first row you write to Qdrant.
The problem
Qdrant's payload holds arbitrary JSON, so most chunk records fit without changes. The trap is the point ID: Qdrant only accepts an unsigned 64-bit integer or a UUID string. A chunk ID scheme like "doc-3-chunk-9" is rejected outright and needs converting first.
what qdrant needs
| Per-vector metadata limit | No fixed cap on a point's payload. Bound by the request-size ceiling instead: 32 MB per REST call by default, or a much smaller 4 MB default on the gRPC client unless you raise it. |
|---|---|
| Stores the chunk text itself | Yes. The payload is arbitrary JSON stored with the point, so the chunk text is a normal payload field. |
| ID format | An unsigned 64-bit integer or a UUID string. No other string is accepted. |
| Max vector dimensions | 65,535 dimensions for a dense vector. |
| Batch upsert size | No hard point-count cap. Qdrant's own guidance suggests 64 to 256 points per batch, with a few parallel upload threads for a big job. |
| Index type | HNSW, with optional scalar or binary quantization to cut memory use. |
No accuracy, speed, or recall figure is measured against Qdrant on this page. See /benchmarks for txtfetch's own measured extraction numbers.
the pipeline
Extract, chunk, embed, and upsert. No framework, just the txtfetch Python SDK and Qdrant's own client.
import os
import uuid
from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, PointStruct, VectorParams
from txtfetch import Txtfetch
txtfetch = Txtfetch(api_key=os.environ["TXTFETCH_KEY"])
oai = OpenAI()
client = QdrantClient(url=os.environ["QDRANT_URL"], api_key=os.environ["QDRANT_API_KEY"])
# create_collection raises if the collection is already there, so a second
# run of this script needs the guard.
if not client.collection_exists("docs"):
client.create_collection(
collection_name="docs",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
def chunk_text(text: str, size: int = 1000, overlap: int = 100) -> list[str]:
chunks = []
start = 0
while start < len(text):
end = start + size
chunks.append(text[start:end])
start = end - overlap
return chunks
# 1. Extract
result = txtfetch.extract(file="whitepaper.pdf")
# 2. Chunk
chunks = chunk_text(result.extracted_text)
# 3. Embed
response = oai.embeddings.create(model="text-embedding-3-small", input=chunks)
# 4. Upsert — point ids must be an unsigned 64-bit integer or a UUID, so a
# generated UUID replaces any string-based chunk id scheme.
points = [
PointStruct(
id=str(uuid.uuid4()),
vector=item.embedding,
payload={"source": "whitepaper.pdf", "text": chunk},
)
for chunk, item in zip(chunks, response.data)
]
BATCH_SIZE = 128
for i in range(0, len(points), BATCH_SIZE):
client.upsert(collection_name="docs", wait=True, points=points[i : i + BATCH_SIZE])Every call above was resolved against qdrant-client 1.16.1, in September 2026. Client APIs move, so check the vendor's own docs for the version you pin.
Chunking for Qdrant
Qdrant's payload carries no size ceiling of its own, only the surrounding request-size limit. Chunk length is your call, not Qdrant's. The real trap is the point ID. A chunk ID scheme like "doc-3-chunk-9" needs converting to a UUID first, since Qdrant only accepts an integer or a UUID. Check your chunk boundaries in the chunk previewer before you generate ids for the whole batch.
large batches
A large file, or a slow OCR pass, routes to an async job automatically. That returns a 202 plus a job_id to poll, and ?async=true forces that path for any request. Pass webhook_url instead, and txtfetch calls you back when the text is ready, rather than you polling for it. See async jobs & webhooks for the full lifecycle.
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf&async=true" \
-H "Authorization: Bearer $TXTFETCH_KEY"
# {"status": "processing", "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"}curl "https://api.txtfetch.com/v1/extract/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $TXTFETCH_KEY"
# {"status": "processing", "job_id": "..."} while running, then the same
# {"status": "success", "extracted_text": "...", "metadata": {...}} shape
# POST /v1/extract would have returned synchronously.curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
--data-urlencode "webhook_url=https://example.com/webhooks/txtfetch"A retried request, from a client timeout or a dropped response, shouldn't extract and upsert the same document twice. Send the same Idempotency-Key on a retry and txtfetch returns the first result instead of running the job again. See idempotency for the full guarantee.
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-H "Idempotency-Key: 3f29b6e4-9c1a-4b8e-9c2a-1e6f0a2d5b3c" \
-F file=@contract.pdf// The SDK auto-generates and reuses an Idempotency-Key across its own
// retry chain. Pass your own to control it explicitly:
await txtfetch.extract({ file: "./contract.pdf", idempotencyKey: "3f29b6e4-..." });# The SDK auto-generates and reuses an Idempotency-Key across its own
# retry chain. Pass your own to control it explicitly:
client.extract(file="contract.pdf", idempotency_key="3f29b6e4-...")txtfetch ships no connector, plugin, or client for Qdrant. The pipeline above is the whole integration. Extract with txtfetch, then write to Qdrant with Qdrant's own client, the same as you would for any other source of text.
frequently asked questions
- Does txtfetch write directly to Qdrant?
- No. txtfetch returns extracted text over one HTTP call. Chunking, embedding, and the upsert itself stay a caller-side step, exactly like the pipeline above shows.
- Why does the point ID format trip people up?
- Qdrant only accepts an unsigned 64-bit integer or a UUID string as a point id. A chunk ID scheme built from a document name and chunk index, like "doc-3-chunk-9", is rejected outright until you convert it to a UUID.
- Does txtfetch benchmark accuracy or speed against Qdrant?
- No. Qdrant is a vector database, not a text-extraction service, so there's no accuracy or speed comparison to make. See /benchmarks for txtfetch's own measured extraction numbers.
Related
- RAG & LLM ingestion, the wider use case this pipeline feeds.
- RAG recipe: chunk, embed, index, the same pipeline through LangChain or LlamaIndex instead.
- Chunk previewer, to test your own extracted text before you write the first row.
- Async jobs & webhooks, the full lifecycle for a large batch.
Wire the extraction step in.
The RAG recipe shows the whole path, from a file to the Qdrant upsert.
Read the RAG recipe →