惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Cloudflare Blog
N
News and Events Feed by Topic
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
AWS News Blog
AWS News Blog
S
SegmentFault 最新的问题
T
Tailwind CSS Blog
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Spread Privacy
Spread Privacy
J
Java Code Geeks
博客园 - 聂微东
T
Tor Project blog
宝玉的分享
宝玉的分享
博客园 - 叶小钗
Webroot Blog
Webroot Blog
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Heimdal Security Blog
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
I
InfoQ
Security Latest
Security Latest
Martin Fowler
Martin Fowler
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy International News Feed
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
H
Help Net Security
L
LINUX DO - 最新话题
L
LINUX DO - 热门话题

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
RAG and Vector Search with pgvector and Amazon Bedrock (Part 4)
Josh Blair · 2026-05-21 · via DEV Community

RAG and Vector Search with pgvector and Amazon Bedrock (Part 4)

How to build retrieval-augmented generation that actually cites its sources — without a vector database subscription.


Most RAG tutorials reach for Pinecone, Chroma, or Weaviate as the vector store. Those are all fine services, but they add another cost line, another auth boundary, and a dependency you don't control. If you're already running Postgres — and for multi-tenant SaaS, you should be — the pgvector extension gives you vector similarity search inside your existing database, protected by the same Row-Level Security policies you already have.

This post covers the full query path in Sift: how a user's question becomes a vector, how pgvector finds the closest document chunks, and how Claude turns those chunks into a cited answer.


What RAG Actually Does

The core idea is simple. At query time:

  1. Embed the user's question with the same model used to embed the documents
  2. Find the document chunks whose embeddings are closest to the question embedding
  3. Send those chunks to an LLM, tell it to answer the question using only that context
  4. Return the answer with numbered citations linking back to the source text

That's it. The sophistication is in the details of each step.


Embeddings with Bedrock Titan Embed v2

Both the pipeline (at ingest time) and the chat handler (at query time) use the same embedding model: amazon.titan-embed-text-v2:0. Using the same model for both sides of the search is a hard requirement — embeddings from different models live in incompatible vector spaces.

The Python implementation in the pipeline's shared module:

EMBED_MODEL_ID = "amazon.titan-embed-text-v2:0"

def embed(text: str) -> list[float]:
    payload = json.dumps({"inputText": text, "dimensions": 1024, "normalize": True})
    response = _get_client().invoke_model(
        modelId=EMBED_MODEL_ID,
        contentType="application/json",
        accept="application/json",
        body=payload,
    )
    return json.loads(response["body"].read())["embedding"]

Enter fullscreen mode Exit fullscreen mode

Two parameters worth noting.

dimensions: 1024 — Titan Embed v2 supports multiple output sizes (256, 512, or 1024 dimensions). Fewer dimensions mean smaller storage and faster search at the cost of some precision. 1024 is the maximum and gives the best retrieval quality; for a demo at this scale, there's no reason to trade it away.

normalize: True — this asks Bedrock to return a unit-length vector. Normalized embeddings mean cosine similarity is equivalent to dot product. pgvector can compute dot products slightly faster than cosine distance, and it simplifies reasoning about scores. More importantly, it means you don't have to normalize manually — if you skip it and your embeddings have different magnitudes, your similarity scores will be skewed by vector length rather than semantic meaning.

Authentication is IAM. The Lambda execution role has bedrock:InvokeModel permission via its attached policy — no API keys, no secrets to rotate.


Schema: Storing Vectors in Postgres

The document_chunks table has a vector(1024) column — the native pgvector type:

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_chunks (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  document_id   UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
  tenant_id     UUID NOT NULL,
  chunk_index   INT NOT NULL,
  content       TEXT NOT NULL,
  embedding     vector(1024),
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Enter fullscreen mode Exit fullscreen mode

The (1024) in the column type is a hard constraint — Postgres will reject inserts with a vector of any other dimension. That's a useful guardrail: if the embedding model changes and the dimension changes with it, the insert fails loudly rather than silently storing mismatched vectors.

The IVFFlat Index

An exact nearest-neighbor search scans every vector in the table and computes distance to the query vector. For a small dataset that's fine. At tens of millions of chunks it becomes expensive.

IVFFlat (Inverted File Flat) is an approximate nearest-neighbor index. It clusters the vectors into groups (called "lists") at index build time. At query time, it only searches the most promising lists rather than the entire table:

CREATE INDEX ON document_chunks
  USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

Enter fullscreen mode Exit fullscreen mode

vector_cosine_ops tells the index to use cosine distance as its metric, which matches the <=> operator in the query. The lists = 100 parameter controls how many clusters to build — the pgvector docs recommend roughly sqrt(rows) as a starting point.

The IVFFlat gotcha: the index needs data to exist when it's built. An IVFFlat index built on an empty table is useless. In Sift, the initial migration creates the index after the schema is established, and the seed data runs in the same migration. For a production system where the table grows continuously, HNSW is a better choice — it maintains good search quality as data is inserted without needing a rebuild.

Inserting Vectors from Python

The psycopg2 driver doesn't natively understand the pgvector type. Rather than adding the pgvector Python package (which requires a compiled extension and adds deploy complexity), the pipeline constructs a Postgres vector literal as a plain string and casts it:

vector_literal = "[" + ",".join(str(v) for v in embedding) + "]"
cur.execute(
    """
    INSERT INTO document_chunks
        (document_id, tenant_id, chunk_index, content, embedding)
    VALUES (%s, %s, %s, %s, %s::vector)
    ON CONFLICT DO NOTHING
    """,
    (document_id, tenant_id, chunk_index, content, vector_literal),
)

Enter fullscreen mode Exit fullscreen mode

The ::vector cast in the SQL converts the string to the native vector type at insert time. This works on any Postgres driver, any Lambda architecture (x86 or ARM), without native extensions. The ON CONFLICT DO NOTHING handles at-least-once delivery from the Step Functions Map state — if an EmbedChunk Lambda retries, it won't create duplicate chunks.


Similarity Search

At query time, the C# ChatService embeds the user's question and runs the search. The same vector literal approach works from the .NET side:

private async Task<List<ChunkResult>> SearchChunksAsync(Guid tenantId, float[] embedding)
{
    var vectorLiteral = $"[{string.Join(",", embedding)}]";

    await using var conn = await db.CreateAsync();
    await TenantContext.SetAsync(conn, tenantId);

    await using var cmd = conn.CreateCommand();
    cmd.CommandText = """
        SELECT dc.id, dc.document_id, dc.chunk_index, dc.content,
               d.filename,
               dc.embedding <=> $1::vector AS distance
        FROM document_chunks dc
        JOIN documents d ON d.id = dc.document_id
        ORDER BY distance
        LIMIT $2
        """;
    cmd.Parameters.AddWithValue(NpgsqlTypes.NpgsqlDbType.Text, vectorLiteral);
    cmd.Parameters.AddWithValue(TopK);
    // ...
}

Enter fullscreen mode Exit fullscreen mode

The <=> operator is pgvector's cosine distance operator. It returns values between 0 and 2 — 0 means identical vectors, 2 means pointing in opposite directions. Ordering by ascending distance gives the most semantically similar chunks first.

Notice that TenantContext.SetAsync runs before the query. This sets the Postgres session variable that the RLS policy reads. The similarity search is automatically tenant-scoped — there's no WHERE tenant_id = $3 in this query, but Postgres applies the policy invisibly. A user from Acme Corp can only find chunks from their own documents, even though the <=> distance calculation runs across an index that spans all tenants' data.

Why 8 chunks? TopK = 8 is a constant in ChatService.cs. Eight chunks at ~512 tokens each is roughly 4,000 tokens of context — enough to answer most questions without overwhelming the model or the latency budget. The tradeoff is real: more chunks means higher recall (better chance the right information is included) at the cost of slower generation and more noise in the prompt. Eight is a practical default, not a theoretically derived optimum.


The RAG Prompt

With the top 8 chunks retrieved, the service builds the prompt:

var context = string.Join("\n\n", chunks.Select((c, i) =>
    $"[{i + 1}] From \"{c.Filename}\" (chunk {c.ChunkIndex}):\n{c.Content}"));

var systemPrompt = """
    You are a helpful document assistant. Answer the user's question using only
    the provided document excerpts. Cite your sources using [1], [2], etc.
    If the answer cannot be found in the excerpts, say so clearly.
    """;

var userMessage = $"Document excerpts:\n{context}\n\nQuestion: {question}";

Enter fullscreen mode Exit fullscreen mode

Each chunk gets a numbered label [1], [2], etc., with the filename and chunk index. The system prompt instructs Claude to use those same numbers as inline citations. The model sees something like:

[1] From "Q3_Report.pdf" (chunk 4):
Revenue for Q3 was $4.2M, up 18% year-over-year driven by enterprise contracts...

[2] From "Q3_Report.pdf" (chunk 5):
The increase was concentrated in the healthcare vertical, which grew 31%...

Question: What drove the Q3 revenue increase?

Enter fullscreen mode Exit fullscreen mode

And responds with an answer that cites [1] and [2] inline, so the reader knows exactly which passage each claim came from.

The model for this step is Claude Haiku 4.5 — fast and cheap for a task that's mostly about summarizing and organizing provided context rather than knowledge retrieval or reasoning. The max_tokens: 1024 cap keeps response times predictable.

Citations as First-Class Data

The response doesn't just return the answer string. The ChatResponse model carries a parallel citations array:

public class ChatResponse
{
    public string             Answer    { get; set; } = "";
    public List<ChatCitation> Citations { get; set; } = [];
}

public class ChatCitation
{
    public Guid   DocumentId { get; set; }
    public string Filename   { get; set; } = "";
    public string Excerpt    { get; set; } = "";
    public int    ChunkIndex { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

Each citation includes the first 200 characters of the chunk's content. The React frontend renders them as expandable cards below the answer — the user can click [1] to see the exact excerpt that grounded that part of the response, with the source document and chunk position shown.

This matters for trust. A RAG system that returns confident-sounding answers with no way to verify them is worse than one that shows its work.


Limitations and What Production Would Change

The implementation above works well at demo scale. A few things I'd change for a real production deployment:

Chunking strategy. The sliding-window chunker in Part 3 splits on character count, not semantic boundaries. A 512-token window can cut off mid-sentence, mid-table, or mid-list. Better approaches: a recursive sentence splitter that tries to preserve paragraph boundaries, or a semantic chunker that uses an embedding model to detect topic shifts. The trade-off is complexity and ingest latency.

Index type. IVFFlat is good for static or slowly-growing datasets, but it degrades as data is inserted after the index is built — you need periodic reindexing. HNSW (Hierarchical Navigable Small World) maintains search quality dynamically as data grows, at the cost of higher memory usage. For a production system with continuous ingestion, HNSW is the right default.

Reranking. Vector similarity is a good first filter but not a perfect one. A cross-encoder reranker — a small model that takes (question, chunk) pairs and scores their relevance directly — can significantly improve the precision of the final context window. The typical pattern is: retrieve top 20–50 chunks with vector search, rerank with a cross-encoder, pass the top 8 to the LLM.

Streaming. The current API waits for Claude to finish generating the full answer before returning it. For longer answers that can take 3–5 seconds, that's a noticeable pause. Lambda Function URLs support response streaming, which would let the frontend display tokens as they arrive. API Gateway HTTP APIs don't support streaming, so switching to Function URLs for the chat endpoint would be the path there.


What's Next

Part 5 covers the React frontend: how the upload flow works, the polling pattern that drives the document status cards, and how Amplify's auth integration wires up the Cognito token flow.

The code for this post:

  • backend/shared/bedrock.py — embedding call, normalize flag
  • migrations/001_initial_schema.sqlvector(1024) column, IVFFlat index
  • backend/pipeline/embed/embed_handler.py — vector literal insert, ON CONFLICT DO NOTHING
  • backend/src/Sift.Api/Services/ChatService.cs — full query path: embed → search → generate
  • backend/src/Sift.Api/Models/Chat.cs — response shape with citations

Part of the Sift series: building a production-ready multi-tenant RAG platform on AWS.