Add rag module: Spring AI 2.0 RAG with pgvector, chunking, reranking and a faithfulness check

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01B38FGKKam5SCGgwgduVAh3
This commit is contained in:
Claude
2026-09-21 19:09:05 +00:00
commit 1d4625a1c2
62 changed files with 3715 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Writes docs/output/13-dependencies.txt: which artifact brings what, straight from Maven.
set -euo pipefail
cd "$(dirname "$0")/.."
mkdir -p docs/output target
mvn -B dependency:tree -DoutputFile=target/tree.txt >/dev/null
OUT=docs/output/13-dependencies.txt
{
echo "# Dependency tree, filtered"
echo
echo "## Spring Boot and Spring AI versions"
echo "spring-boot-starter-parent $(grep -A2 'spring-boot-starter-parent' pom.xml | grep -m1 -o '<version>[^<]*' | sed 's/.*>//')"
echo "spring-ai-bom $(grep -m1 -o '<spring-ai.version>[^<]*' pom.xml | sed 's/.*>//')"
echo
echo "## where spring-jdbc comes from (it is not under any Spring AI artifact)"
grep -n -E 'spring-jdbc|spring-boot-starter-jdbc|spring-boot-jdbc|HikariCP' target/tree.txt
echo
echo "## what the pgvector starter brings"
grep -A8 'spring-ai-starter-vector-store-pgvector' target/tree.txt | sed -n 1,8p
echo
echo "## every Spring AI artifact on the classpath"
grep -o 'org.springframework.ai:[a-z0-9-]*:jar:[0-9.]*' target/tree.txt | sort -u
} > "$OUT"
echo "wrote $OUT"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Writes docs/output/12-api-facts.txt: the public API of every Spring AI class this repository
# relies on, read from the jars the build resolves (not from documentation).
set -euo pipefail
cd "$(dirname "$0")/.."
mkdir -p docs/output target
mvn -B -q dependency:build-classpath -Dmdep.outputFile=target/classpath.txt >/dev/null
CP="$(cat target/classpath.txt)"
OUT=docs/output/12-api-facts.txt
jar_of() { tr ':' '\n' < target/classpath.txt | grep "/$1-[0-9]" | head -1; }
jp() { echo "## $1"; javap -cp "$CP" -public "$1" | grep -v '^Compiled' | sed 's/java\.util\.//g; s/java\.lang\.//g'; echo; }
{
echo "# Spring AI API facts, read with javap from the jars this build resolves"
echo
echo "spring-ai.version: $(grep -m1 -o '<spring-ai.version>[^<]*' pom.xml | sed 's/.*>//')"
echo
echo "## classes in org/springframework/ai/rag (top level, from spring-ai-rag)"
unzip -Z1 "$(jar_of spring-ai-rag)" | grep '\.class$' | grep -v '\$' | sed 's|org/springframework/ai/rag/||; s|\.class||' | sort
echo
echo "## classes anywhere in spring-ai-rag whose name contains \"rerank\": $(unzip -Z1 "$(jar_of spring-ai-rag)" | grep -ic rerank || true)"
echo
echo "## classes in any jar on this project's classpath whose name contains \"SemanticSearchCache\" or \"SemanticCache\": $(for j in $(tr ':' '\n' < target/classpath.txt); do unzip -Z1 "$j" 2>/dev/null; done | grep -Eic 'SemanticSearchCache|SemanticCache' || true)"
echo
jp org.springframework.ai.transformer.splitter.TokenTextSplitter
jp 'org.springframework.ai.transformer.splitter.TokenTextSplitter$Builder'
jp org.springframework.ai.reader.pdf.PagePdfDocumentReader
jp 'org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig$Builder'
jp org.springframework.ai.rag.postretrieval.document.DocumentPostProcessor
jp org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever
jp 'org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever$Builder'
jp 'org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter$Builder'
jp 'org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor$Builder'
jp 'org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor$Builder'
jp org.springframework.ai.chat.evaluation.FactCheckingEvaluator
jp org.springframework.ai.vectorstore.VectorStore
jp 'org.springframework.ai.vectorstore.SearchRequest$Builder'
} > "$OUT"
echo "wrote $OUT"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Writes docs/output/14-legacy-1x.txt: what happens to the code in the 1.x version of the article.
# Needs network access to Maven Central. It runs deliberately failing builds, so it never exits non-zero
# because of them. Needs unzip and python3 for the configuration-metadata part.
set -uo pipefail
cd "$(dirname "$0")/.."
mkdir -p docs/output target/legacy
OUT=docs/output/14-legacy-1x.txt
CENTRAL=https://repo1.maven.org/maven2/org/springframework/ai
latest() { curl -s "$CENTRAL/$1/maven-metadata.xml" | grep -o '<latest>[^<]*' | sed 's/.*>//'; }
first_errors() { grep -E 'ERROR.*(is missing|Could not|error:)|error:' | sed 's/^\[ERROR\] *//' | sort -u | head -"${1:-6}"; }
{
echo "# The 1.x article's code, against 1.1.0 and 2.0.1"
echo
echo "## The starter artifact ids in the 1.x article: latest version ever published"
echo "spring-ai-openai-spring-boot-starter latest: $(latest spring-ai-openai-spring-boot-starter)"
echo "spring-ai-pgvector-store-spring-boot-starter latest: $(latest spring-ai-pgvector-store-spring-boot-starter)"
echo "the ids that replaced them:"
echo "spring-ai-starter-model-openai latest: $(latest spring-ai-starter-model-openai)"
echo "spring-ai-starter-vector-store-pgvector latest: $(latest spring-ai-starter-vector-store-pgvector)"
for v in 1.1.0 2.0.1; do
echo
echo "## mvn validate on the article's dependency block with spring-ai-bom $v"
mvn -B -f legacy-1x/pom.xml validate -Dspring-ai.version=$v 2>&1 | first_errors 3
done
# Compile the two Spring AI calls against the 1.1.0 jars, then against 2.0.1.
for m in spring-ai-commons spring-ai-pdf-document-reader; do
[ -f target/legacy/$m-1.1.0.jar ] || curl -s -o target/legacy/$m-1.1.0.jar "$CENTRAL/$m/1.1.0/$m-1.1.0.jar"
done
mvn -B -q dependency:build-classpath -Dmdep.outputFile=target/classpath.txt >/dev/null 2>&1
OTHERS="$(tr ':' '\n' < target/classpath.txt | grep -v '/spring-ai-' | paste -sd:)"
CP21="$(cat target/classpath.txt)"
CP11="target/legacy/spring-ai-commons-1.1.0.jar:target/legacy/spring-ai-pdf-document-reader-1.1.0.jar:$OTHERS"
for v in 1.1.0 2.0.1; do
echo
echo "## javac legacy-1x/src/LegacyIngestion.java against Spring AI $v"
if [ "$v" = 1.1.0 ]; then CP="$CP11"; else CP="$CP21"; fi
rm -rf target/legacy/out && mkdir -p target/legacy/out
if RESULT="$(javac -proc:none -Xmaxerrs 10 -d target/legacy/out -cp "$CP" legacy-1x/src/LegacyIngestion.java 2>&1)"; then
echo "(compiles)"
else
echo "$RESULT" | grep -A2 'error:' | grep -v '^--$'
fi
done
# The three 1.x configuration keys, read from the configuration metadata inside the auto-configuration jar.
echo
echo "## the 1.x configuration keys in the metadata of spring-ai-autoconfigure-model-openai"
for v in 1.1.0 2.0.1; do
J=target/legacy/spring-ai-autoconfigure-model-openai-$v.jar
[ -f "$J" ] || curl -s -o "$J" "$CENTRAL/spring-ai-autoconfigure-model-openai/$v/spring-ai-autoconfigure-model-openai-$v.jar"
unzip -p "$J" META-INF/spring-configuration-metadata.json | python3 -c '
import json, sys
v = sys.argv[1]
props = {p["name"]: p for p in json.load(sys.stdin)["properties"]}
for k in ("spring.ai.openai.chat.options.model", "spring.ai.openai.chat.options.temperature", "spring.ai.openai.embedding.options.model"):
p = props.get(k)
state = "unknown" if p is None else ("deprecated, use " + p["deprecation"]["replacement"] if "deprecation" in p else "current")
print("%-6s %-46s %s" % (v, k, state))
' "$v"
done
} > "$OUT"
echo "wrote $OUT"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Starts a throwaway PostgreSQL with the pgvector extension, with no Docker, and prepares the
# ragdb database. Use it when docker compose is not available. It is what produced the
# transcripts in docs/output/ in this repository's authoring environment.
#
# Requires PostgreSQL 14+ and the pgvector package (Debian/Ubuntu: apt install postgresql-16 postgresql-16-pgvector).
# Environment: PGDATA (default /tmp/pgrag/data), PGPORT (default 5439).
# Afterwards: export RAG_PG_URL=jdbc:postgresql://127.0.0.1:${PGPORT:-5439}/ragdb
set -euo pipefail
PGDATA="${PGDATA:-/tmp/pgrag/data}"
PGPORT="${PGPORT:-5439}"
SOCKDIR="$(dirname "$PGDATA")"
BIN="$(ls -d /usr/lib/postgresql/*/bin 2>/dev/null | sort -V | tail -1)"
[ -x "$BIN/initdb" ] || { echo "PostgreSQL server binaries not found under /usr/lib/postgresql" >&2; exit 1; }
HERE="$(cd "$(dirname "$0")/.." && pwd)"
# PostgreSQL refuses to run as root, so hand the work to the postgres user in that case.
as_pg() { if [ "$(id -u)" = 0 ]; then su postgres -c "$*"; else bash -c "$*"; fi; }
mkdir -p "$SOCKDIR"
[ "$(id -u)" = 0 ] && chown postgres "$SOCKDIR"
if [ ! -d "$PGDATA/base" ]; then
as_pg "'$BIN/initdb' -D '$PGDATA' -A trust >'$SOCKDIR/initdb.log' 2>&1"
fi
if ! as_pg "'$BIN/pg_ctl' -D '$PGDATA' status" >/dev/null 2>&1; then
as_pg "'$BIN/pg_ctl' -D '$PGDATA' -o \"-p $PGPORT -c listen_addresses=127.0.0.1 -c unix_socket_directories=$SOCKDIR\" -l '$SOCKDIR/pg.log' -w start"
fi
PSQL="$BIN/psql -h $SOCKDIR -p $PGPORT -U postgres -v ON_ERROR_STOP=1 -q"
as_pg "$PSQL -d postgres -tc \"select 1 from pg_roles where rolname='raguser'\"" | grep -q 1 \
|| as_pg "$PSQL -d postgres -c \"create role raguser login superuser password 'ragpass'\""
as_pg "$PSQL -d postgres -tc \"select 1 from pg_database where datname='ragdb'\"" | grep -q 1 \
|| as_pg "$PSQL -d postgres -c 'create database ragdb owner raguser'"
as_pg "$PSQL -d ragdb -c 'create extension if not exists vector'"
as_pg "$PSQL -d ragdb -f '$HERE/init.sql'"
echo "PostgreSQL is up on 127.0.0.1:$PGPORT (database ragdb, user raguser, password ragpass)"
echo "export RAG_PG_URL=jdbc:postgresql://127.0.0.1:$PGPORT/ragdb"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Regenerates everything under docs/output/.
#
# scripts/run-all.sh uses RAG_PG_URL if set, otherwise starts PostgreSQL with scripts/pg-up.sh
# docker compose up -d && RAG_PG_URL=jdbc:postgresql://localhost:5432/ragdb scripts/run-all.sh
#
# No API key is needed: the tests use scripted stand-ins for the chat and embedding models.
set -euo pipefail
cd "$(dirname "$0")/.."
if [ -z "${RAG_PG_URL:-}" ]; then
scripts/pg-up.sh
export RAG_PG_URL="jdbc:postgresql://127.0.0.1:${PGPORT:-5439}/ragdb"
fi
mvn -B -q test # transcripts 01-11 and 15 (each test writes one and asserts the same numbers)
scripts/capture-javap.sh # 12
scripts/capture-dependencies.sh # 13
scripts/capture-legacy-compile.sh # 14
ls docs/output