Sections in this lesson

RAG with PostgreSQL & pgvector

Persist document vectors, filter by trusted tenant and return answers with verifiable source IDs.

Reviewed: · Content owner: Interview Preparator

What you will learn

  • Initialize a local pgvector store with explicit dimensions.
  • Apply tenant filters before generation.
  • Test evidence deletion and unanswerable queries against PostgreSQL.

Before you start

The flow

  1. 1.Seed documents
  2. 2.Embed
  3. 3.Persist vectors
  4. 4.Filter & retrieve
  5. 5.Validate citations

Persist the evidence lifecycle

The configuration builds PgVectorStore from JdbcTemplate and EmbeddingModel and explicitly controls schema initialization. The teaching corpus uses stable UUIDs and tenant metadata. It is intentionally small; document parsing, chunking, updates and production migrations need their own ingestion design.

RagService retrieves the top three candidates with a calibrated-for-fixtures threshold of 0.75 and a tenant filter derived from authenticated identity. The number is not a universal semantic relevance threshold.

Keep vector spaces compatible

Demo embeddings use eight synthetic dimensions to make database behavior repeatable. Ollama embeddings use their actual model dimension and must live in a separate index/database. A dimension change alone is not a migration strategy.

Local initialize-schema is opt-in. Use migration-controlled schema and a restricted application role in production; extension creation and index construction do not belong in an unrestricted runtime account.

Verify failure paths with the real store

The integration suite starts pgvector and checks tenant-A versus tenant-B results for the same query, a missing topic, inaccessible evidence and document deletion. When no authorized evidence remains, the service abstains before calling the model.

The final answer still needs semantic evaluation: an allowed citation does not prove a supported claim. Keep retrieval recall and answer grounding as separate measurements.

Try it

LabConfiguration.java
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.interviewpreparator.lab;
import java.util.List;
import java.util.Map;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class LabConfiguration {
public static final String RECORD_A = "00000000-0000-0000-0000-000000000001";
public static final String THREAD_A = "00000000-0000-0000-0000-000000000002";
public static final String PRIVATE_B = "00000000-0000-0000-0000-000000000003";
@Bean
PgVectorStore vectorStore(JdbcTemplate jdbc, EmbeddingModel embedding,
@Value("${lab.dimensions}") int dimensions,
@Value("${lab.initialize-schema}") boolean initialize) {
return PgVectorStore.builder(jdbc, embedding).dimensions(dimensions)
.initializeSchema(initialize).build();
}
@Bean
@Profile({"demo", "ollama"})
ApplicationRunner seed(PgVectorStore store) {
// Small teaching corpus only; no user uploads or production ingestion endpoint.
return args -> store.add(List.of(
new Document(RECORD_A, "A Java record is a data carrier; mutable components need defensive copying.", Map.of("tenant", "tenant-a", "approved", true)),
new Document(THREAD_A, "Virtual threads improve concurrency for blocking I/O; resource pools still need limits.", Map.of("tenant", "tenant-a", "approved", true)),
new Document(PRIVATE_B, "Private tenant B record policy: internal-only review procedure.", Map.of("tenant", "tenant-b", "approved", true))));
}
}

examples/spring-ai-lab/src/main/java/com/interviewpreparator/lab/LabConfiguration.java · mvn -f examples/spring-ai-lab/pom.xml -Pintegration verify. Java 21+, Spring Boot 4.0.5, Spring AI 2.0.2 where applicable. The demo uses synthetic models; live inference quality is a separate check.

Interview questions

Why filter before generation?

Once private evidence reaches the model, output filtering is too late to undo disclosure.

Can demo embeddings measure semantic quality?

No. They isolate persistence and filter behavior; live retrieval quality needs labeled real queries.

What happens when a document is deleted?

Remove its indexed evidence and verify that retrieval can no longer cite it; also plan source and backup retention separately.

Practice

Add a stale-document version case and prove an update replaces the old evidence rather than duplicating it.

Check your solution

  • Both tenants receive only their sources.
  • Missing evidence abstains.
  • Deletion removes the source from actual database retrieval.

Official references

Continue learning

Category sequence

Spring AI

Lesson 5 / 7

Was this lesson useful?

Change notes: Phase 2: bilingual lesson with versioned executable reference.