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

Constraints that shape chunking and the write step, cited and dated
Per-vector metadata limitNo 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 itselfYes. The payload is arbitrary JSON stored with the point, so the chunk text is a normal payload field.
ID formatAn unsigned 64-bit integer or a UUID string. No other string is accepted.
Max vector dimensions65,535 dimensions for a dense vector.
Batch upsert sizeNo 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 typeHNSW, 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.

Python
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.

Submit (async)
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"}
Poll
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
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
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
JavaScript
// 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-..." });
Python
# 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

Wire the extraction step in.

The RAG recipe shows the whole path, from a file to the Qdrant upsert.

Read the RAG recipe →

Get an API key →