Prompt
How do I set up a document ingestion pipeline for fresh content indexing in an AI answer engine?
Latest observation
A good fresh-content ingestion pipeline for an AI answer engine usually has 6 stages:
- Source discovery
- Change detection
- Fetch + normalize
- Chunk + enrich
- Index + version
- Serve + refresh
Here’s a practical setup.
1) Define your content sources
Start by listing every place new knowledge can appear:
- Websites / docs sites
- PDFs / Word docs
- CMSs like Contentful, WordPress, Sanity
- Databases
- Internal wikis: Confluence, Notion, SharePoint
- Product docs repositories: GitHub/GitLab
- Support systems: Zendesk, Intercom
- Feeds / webhooks / RSS
For each source, note:
- Update mechanism: webhook, API polling, file watcher, manual drop
- Latency needs: seconds, minutes, hours
- Access method: API, scrape, export
- Permissions: public, tenant-scoped, user-scoped
- Deletions: do you get delete events?
2) Build change detection first
You don’t want to re-ingest everything repeatedly.
Common approaches:
- Webhooks for near-real-time updates
- Polling with cursors/ETags/updated_at for APIs
- File hash checks for file systems / object storage
- Sitemap + Last-Modified for websites
- Git commit triggers for docs in repos
Recommended metadata to track per document:
source_idexternal_idcontent_hashversionupdated_atdeleted_atingested_ataccess_scope
If the hash or timestamp changes, enqueue a reprocess.
3) Fetch and normalize content
Raw content usually needs cleanup before indexing.
Normalize into a canonical document format
Example structure:
{
"doc_id": "stable-internal-id",
"source": "confluence",
"external_id": "page-123",
"title": "How Billing Works",
"url": "https://...",
"body_text": "...",
"html": "...",
"language": "en",
"author": "Jane",
"updated_at": "2026-07-20T10:00:00Z",
"tags": ["billing", "faq"],
"acl": ["team-a", "team-b"]
}
Extraction steps
- HTML → text
- PDF → text with layout preservation if possible
- OCR for scanned images
- Remove boilerplate: nav, footers, cookie banners
- Preserve structure:
- headings
- lists
- tables
- code blocks
For answer engines, structure matters a lot because it improves retrieval and snippet quality.
4) Chunk intelligently
Don’t index giant documents as a single blob.
Good chunking strategy:
- Split by sections/headings first
- Then by token length if needed
- Keep overlapping context between chunks
- Attach section path metadata
Example chunk metadata:
doc_idchunk_idheading_path:["Billing", "Refunds", "Eligibility"]chunk_textchunk_indextoken_count
Typical chunk size:
- ~200–500 tokens for general QA
- smaller for highly precise documents
- larger if you rely on reranking
Avoid splitting tables and code in the middle.
5) Enrich for retrieval
Before indexing, add metadata that helps filtering and ranking:
titlesection headingssummaryentitiesproduct/versionlanguagedocument typesource trust levelpermission labels
You can also generate:
- short semantic summaries
- FAQs from the content
- embeddings for each chunk
- sparse keywords for hybrid search
For best performance, use hybrid retrieval:
- dense embeddings + keyword/BM25
- then rerank top results
6) Index into the right stores
Most answer engines use at least two indexes:
A. Search index
For keyword and metadata filtering:
- Elasticsearch
- OpenSearch
- Typesense
- Meilisearch
B. Vector index
For semantic retrieval:
- Pinecone
- Weaviate
- Milvus
- pgvector
- OpenSearch vector fields
Store:
- chunk text
- embedding
- metadata
- permissions
- source pointers
A typical query flow:
- User asks question
- Retrieve from vector + keyword index
- Filter by ACL / tenant / recency
- Rerank top 20–100 chunks
- Pass top context into the LLM
- Return answer with citations
7) Make it incremental and idempotent
Your pipeline should support retries without duplicate data.
Best practices:
- Use a stable document ID
- Use content hash for change detection
- Upsert chunks instead of append-only duplicates
- Delete old chunks when a document changes
- Handle partial failures with retry queues
A common pattern:
- ingest event → queue job
- fetch job → normalize
- chunk job → embed
- index job → upsert
- delete job → tombstone removed docs
8) Handle freshness and priority
Not all sources need the same latency.
Create tiers:
- Tier 1: webhooks, support docs, urgent product updates
- Tier 2: hourly polling
- Tier 3: daily bulk sync
Add a recency boost in retrieval if freshness matters:
- prioritize recently updated documents
- but don’t ignore authoritative older docs
You can also re-embed only when content meaningfully changes.
9) Preserve permissions
This is critical for enterprise answer engines.
At ingestion time, attach ACLs:
- user IDs
- group IDs
- tenant IDs
- roles
At query time:
- filter before retrieval if possible
- or filter immediately after retrieval before context assembly
Never let the model see chunks the user shouldn’t access.
10) Add observability and quality checks
You need to know when ingestion breaks.
Track:
- number of docs ingested per source
- ingestion lag
- failed parse rate
- chunk counts
- embedding failures
- duplicate rate
- index freshness
- answer citation coverage
Add validation:
- empty text detection
- language detection
- malformed HTML/PDF extraction
- sudden document size changes
- checksum diffs
- regression tests on known questions
Suggested architecture
A simple production-ready stack:
- Scheduler / event consumer: Airflow, Dagster, Temporal, or queue consumers
- Queue: SQS, Kafka, RabbitMQ, Pub/Sub
- Extractor service: HTML/PDF/Office parsers
- Normalizer: cleans and canonicalizes content
- Chunker: heading-aware splitter
- Embedder: model inference service
- Indexers: search + vector DB
- Metadata store: Postgres
- Blob store: S3/GCS/Azure Blob for raw artifacts
- Monitoring: Prometheus/Grafana/Datadog
Minimal pipeline pseudocode
def ingest_event(event):
doc_ref = resolve_source(event)
raw = fetch_document(doc_ref)
canonical = normalize(raw)
if canonical.hash == get_last_hash(canonical.id):
return # no change
chunks = chunk_document(canonical)
embeddings = embed_chunks(chunks)
upsert_search_index(canonical, chunks)
upsert_vector_index(canonical, chunks, embeddings)
save_metadata(canonical)
if canonical.was_deleted:
delete_document(canonical.id)
Practical design tips
- Prefer webhooks + polling fallback
- Keep raw source snapshots for debugging
- Index citations and source URLs for answer grounding
- Use hybrid retrieval
- Re-embed only changed chunks if possible
- Support deletions and permission changes as first-class events
- Keep a dead-letter queue for failures
- Version documents so answers can be traced to a specific revision
Recommended “good default” pipeline
If you want a solid baseline:
- Detect changes using webhooks or
updated_at - Store raw docs in object storage
- Normalize to canonical JSON
- Chunk by headings
- Generate embeddings per chunk
- Index into OpenSearch + vector DB
- Store ACLs and metadata in Postgres
- Rerank top hits at query time
- Run incremental updates every few minutes
- Maintain deletion sync and audit logs
If you want, I can also provide:
- a reference architecture diagram,
- a Python implementation skeleton, or
- a recommended schema for documents/chunks/indexes.
Observation history
Unlock observation history
Create a free Obsurfable account to unlock historical responses, model comparisons, and deeper brand intelligence.
How did Obsurfable measure this prompt?
Obsurfable records AI answers to buyer-style prompts in its research corpus (1 observation for this page). Metrics are distributions over observations, not a single static ranking.
Which AI systems does Obsurfable collect answers from?
OpenAI, ChatGPT, Google, Gemini, Google AI Mode, Anthropic, Claude, Perplexity, Grok, DeepSeek, Mistral, Copilot, and Meta AI.