Skip to content

Embedding Strategy

This page explains how the pipeline converts text into vectors using Amazon Titan Embeddings V2
Amazon Titan Embeddings V2 — the Bedrock foundation model used to generate 1024-dimensional text embeddings.
— covering model selection, document preparation, truncation handling, and why each article becomes a single vector rather than multiple chunks.

The project uses Amazon Titan Text Embeddings V2
Amazon Titan Embeddings V2 (amazon.titan-embed-text-v2:0) — converts text into 1024-dimensional vectors for similarity search.
for all embedding operations.

PropertyValue
Model IDamazon.titan-embed-text-v2:0
Output dimensions1024 (float32)
Max input tokens8192
Distance metric Cosine similarity
Cosine similarity — a distance metric that measures the angle between two vectors. Values range from 0 (opposite) to 1 (identical direction). Preferred for text embeddings.
Access

Amazon Bedrock
Amazon Bedrock — a fully managed service for accessing foundation models (embedding and LLM) via a unified API.
runtime API

The same model and dimension are used for both ingest (article bodies) and query (user questions). This consistency is essential — vectors from different models live in incompatible spaces and produce meaningless similarity scores when compared.

Before an article reaches the embedding model, it passes through three preparation steps in rag/fetch.py:

  1. HTML stripping — RSS content often includes HTML markup. The pipeline extracts plain text using a parser, then removes any remaining tags with a regex fallback.

  2. Unicode normalization — Non-ASCII characters are decomposed (NFKD) and stripped to pure ASCII. This prevents encoding issues and reduces token waste on special characters.

  3. Truncation — Text is capped at 8000 characters on a word boundary. A marker is appended to indicate truncation occurred.

rag/fetch.py
MAX_BODY_CHARS = 8000
def _truncate_body(text: str) -> str:
if len(text) <= MAX_BODY_CHARS:
return text
trimmed = text[:MAX_BODY_CHARS].rsplit(" ", 1)[0]
return trimmed + "\n\n[Truncated for demo embedding size.]\n"

Many RAG systems split long documents into multiple chunks (paragraphs or fixed-size windows) and store each chunk as a separate vector. This project takes a simpler approach: one article = one vector.

Why this works here:

  • Short source documents — AWS news articles and blog posts are typically 200–2000 words after HTML stripping. Most fit comfortably within the 8000-character budget without truncation.
  • Atomic retrieval — Each result maps to exactly one article. No need to reassemble chunks or deduplicate overlapping windows.
  • Simpler metadata — The vector key encodes chunk-000 as a fixed suffix, leaving room for future multi-chunk support without a schema change.
  • Demo clarity — Fewer moving parts make the architecture easier to understand and debug.

Both the ingest and query Lambdas call Bedrock
Amazon Bedrock — a fully managed service for accessing foundation models (embedding and LLM) via a unified API.
to generate embeddings. The ingest version includes retry logic for throttling:

rag/ingest.py
def _bedrock_embed(client, model_id, text):
last_error = None
for attempt in range(6):
try:
response = client.invoke_model(
modelId=model_id,
contentType="application/json",
accept="application/json",
body=json.dumps({"inputText": text}),
)
payload = json.loads(response["body"].read())
embedding = payload.get("embedding")
if not isinstance(embedding, list):
raise RuntimeError(
"Bedrock response missing 'embedding' array"
)
return embedding
except ClientError as exc:
code = exc.response["Error"].get("Code", "")
if code not in {
"ThrottlingException",
"ServiceUnavailableException",
}:
raise
last_error = exc
time.sleep(min(2**attempt, 30))
raise RuntimeError(f"Throttled after retries: {last_error}")

Key details:

  • The request body contains only inputText — Titan V2 returns a 1024-dimensional float32 array in the embedding field.
  • Exponential backoff (capped at 30 seconds) handles Bedrock throttling gracefully.
  • Up to 6 attempts gives roughly 60 seconds of retry budget before failing.

The query Lambda uses the same API call but without retries — a single user question is unlikely to be throttled:

rag/query.py
def _bedrock_embed(client, model_id, text):
response = client.invoke_model(
modelId=model_id,
contentType="application/json",
accept="application/json",
body=json.dumps({"inputText": text}),
)
payload = json.loads(response["body"].read())
return payload["embedding"]

Both paths use the same model ID (amazon.titan-embed-text-v2:0) configured via the EMBEDDING_MODEL_ID environment variable. This ensures vectors land in the same semantic space:

PathInputPurpose
IngestCleaned article body (up to 8000 chars)

Store vector in S3 Vectors
Amazon S3 Vectors — a purpose-built vector storage capability within S3 that enables similarity search over embeddings without a separate vector database.
index

QueryUser’s natural language questionSearch for similar stored vectors