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:
Executable
+24
@@ -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"
|
||||
Executable
+39
@@ -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"
|
||||
Executable
+68
@@ -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"
|
||||
Executable
+39
@@ -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"
|
||||
Executable
+20
@@ -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
|
||||
Reference in New Issue
Block a user