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.
Model choice
Section titled “Model choice”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.
| Property | Value |
|---|---|
| Model ID | amazon.titan-embed-text-v2:0 |
| Output dimensions | 1024 (float32) |
| Max input tokens | 8192 |
| 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 |
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.
Document preparation
Section titled “Document preparation”Before an article reaches the embedding model, it passes through three preparation steps in rag/fetch.py:
-
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.
-
Unicode normalization — Non-ASCII characters are decomposed (NFKD) and stripped to pure ASCII. This prevents encoding issues and reduces token waste on special characters.
-
Truncation — Text is capped at 8000 characters on a word boundary. A marker is appended to indicate truncation occurred.
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"Single-chunk rationale
Section titled “Single-chunk rationale”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-000as 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.
The embedding function
Section titled “The embedding function”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:
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 theembeddingfield. - 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:
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"]Ingest vs query usage
Section titled “Ingest vs query usage”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:
| Path | Input | Purpose |
|---|---|---|
| Ingest | Cleaned article body (up to 8000 chars) | Store vector in S3 Vectors |
| Query | User’s natural language question | Search for similar stored vectors |