LangChain4j PGVector integration for PostgreSQL-based vector embedding storage and retrieval
Search for similar embeddings using vector similarity (VECTOR mode) or hybrid search combining vector similarity with full-text keyword search (HYBRID mode).
Search for the most similar embeddings based on a query embedding, with support for filtering, score thresholds, and result limits.
/**
* Searches for the most similar embeddings
* All search criteria are defined inside the EmbeddingSearchRequest
* For HYBRID mode, both queryEmbedding and query text must be provided in the request
* @param request A request to search in an EmbeddingStore containing all search criteria
* @return An EmbeddingSearchResult containing all found embeddings with scores
*/
EmbeddingSearchResult<TextSegment> search(EmbeddingSearchRequest request);Standard vector similarity search using cosine distance.
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.data.embedding.Embedding;
// Generate query embedding
String query = "What is LangChain4j?";
Embedding queryEmbedding = embeddingModel.embed(query).content();
// Create search request
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(5)
.build();
// Execute search
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);
// Process results
result.matches().forEach(match -> {
System.out.println("Score: " + match.score());
System.out.println("ID: " + match.embeddingId());
if (match.embedded() != null) {
System.out.println("Text: " + match.embedded().text());
}
});Filter results by minimum similarity score:
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(10)
.minScore(0.7) // Only return results with score >= 0.7
.build();
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);Search with metadata filtering:
import dev.langchain4j.store.embedding.filter.Filter;
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder;
// Build metadata filter
Filter filter = MetadataFilterBuilder.metadataKey("source").isEqualTo("documentation");
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(5)
.minScore(0.6)
.filter(filter)
.build();
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);Use advanced metadata filters:
import dev.langchain4j.store.embedding.filter.Filter;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.*;
// Multiple conditions with AND
Filter complexFilter = metadataKey("source").isEqualTo("documentation")
.and(metadataKey("page").isGreaterThan(10))
.and(metadataKey("section").isNotEqualTo("deprecated"));
// OR conditions
Filter orFilter = metadataKey("priority").isEqualTo("high")
.or(metadataKey("priority").isEqualTo("critical"));
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(5)
.filter(complexFilter)
.build();
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);Combines vector similarity search with PostgreSQL full-text keyword search using Reciprocal Rank Fusion (RRF).
For hybrid search, both query embedding and query text are required:
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
String queryText = "How to configure PostgreSQL vector search?";
Embedding queryEmbedding = embeddingModel.embed(queryText).content();
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding) // For vector similarity search
.query(queryText) // For keyword search (REQUIRED in HYBRID mode)
.maxResults(5)
.build();
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);Hybrid search uses Reciprocal Rank Fusion (RRF) to combine results:
tsvectorRRF_Score = 1/(k + rank_vector) + 1/(k + rank_keyword)Where:
k is a constant (configurable via rrfK() in builder, default: 60)rank_vector is the ranking position from vector search (1 = best match)rank_keyword is the ranking position from keyword search (1 = best match)RRF scores are rank-based and typically range from ~0.02 to ~1.0:
// Example: A document that ranks 1st in both vector and keyword search
// with k=60:
// Score = 1/(60+1) + 1/(60+1) = 1/61 + 1/61 = 0.0328
// A document that ranks 1st in vector but 10th in keyword:
// Score = 1/(60+1) + 1/(60+10) = 1/61 + 1/70 = 0.0307Important: RRF scores are different from cosine similarity scores:
Hybrid search is beneficial when:
Configure hybrid search mode when creating the store:
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore.SearchMode;
PgVectorEmbeddingStore store = PgVectorEmbeddingStore.builder()
.host("localhost")
.port(5432)
.database("postgres")
.user("my_user")
.password("my_password")
.table("embeddings")
.dimension(384)
.searchMode(SearchMode.HYBRID) // Enable hybrid search
.textSearchConfig("english") // Language-specific text search
.rrfK(60) // RRF parameter (optional, default: 60)
.build();Adjust the rrfK parameter to control ranking sensitivity:
// Lower k (20-40): More weight to top-ranked results
PgVectorEmbeddingStore store = PgVectorEmbeddingStore.builder()
// ... other config ...
.searchMode(SearchMode.HYBRID)
.rrfK(40) // Emphasizes top matches more
.build();
// Higher k (80-100): More balanced ranking
PgVectorEmbeddingStore store = PgVectorEmbeddingStore.builder()
// ... other config ...
.searchMode(SearchMode.HYBRID)
.rrfK(80) // More balanced between top and lower-ranked results
.build();import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.data.segment.TextSegment;
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);
// Get all matches
List<EmbeddingMatch<TextSegment>> matches = result.matches();
// Process each match
for (EmbeddingMatch<TextSegment> match : matches) {
double score = match.score();
String embeddingId = match.embeddingId();
Embedding embedding = match.embedding();
TextSegment textSegment = match.embedded();
if (textSegment != null) {
String text = textSegment.text();
Metadata metadata = textSegment.metadata();
System.out.println("Score: " + score);
System.out.println("Text: " + text);
System.out.println("Source: " + metadata.getString("source"));
}
}Use search results to build context for language model prompts:
String question = "What is the refund policy?";
Embedding questionEmbedding = embeddingModel.embed(question).content();
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(questionEmbedding)
.maxResults(3)
.minScore(0.7)
.build();
EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(request);
// Build context from retrieved segments
String context = searchResult.matches().stream()
.map(match -> match.embedded().text())
.collect(Collectors.joining("\n\n"));
// Create prompt with context
String prompt = String.format("""
Answer the question based on the following context.
Context:
%s
Question: %s
Answer:
""", context, question);
// Send to LLM
String answer = chatModel.generate(prompt);Complete API for building search requests:
/**
* Builder for creating EmbeddingSearchRequest
*/
class EmbeddingSearchRequest {
/**
* Creates a new builder instance
* @return Builder for EmbeddingSearchRequest
*/
static Builder builder();
/**
* Builder for EmbeddingSearchRequest
*/
static class Builder {
/**
* Sets the query embedding for vector similarity search
* @param queryEmbedding The embedding to search for
* @return Builder instance for chaining
*/
Builder queryEmbedding(Embedding queryEmbedding);
/**
* Sets the query text for keyword search (required for HYBRID mode)
* @param query The text query for full-text search
* @return Builder instance for chaining
*/
Builder query(String query);
/**
* Sets the maximum number of results to return
* @param maxResults Maximum number of results
* @return Builder instance for chaining
*/
Builder maxResults(int maxResults);
/**
* Sets the minimum similarity score threshold
* Results with scores below this threshold are filtered out
* @param minScore Minimum score (0.0 to 1.0 for VECTOR mode)
* @return Builder instance for chaining
*/
Builder minScore(double minScore);
/**
* Sets the metadata filter for filtering results
* @param filter Metadata filter criteria
* @return Builder instance for chaining
*/
Builder filter(Filter filter);
/**
* Builds the search request
* @return Configured EmbeddingSearchRequest
*/
EmbeddingSearchRequest build();
}
}Container for search results returned by the search operation.
/**
* Result of an embedding search
* @param <Embedded> Type of embedded content (typically TextSegment)
*/
class EmbeddingSearchResult<Embedded> {
/**
* Returns the list of matching embeddings
* @return List of EmbeddingMatch objects, ordered by score (highest first)
*/
List<EmbeddingMatch<Embedded>> matches();
}Represents a single matching embedding with its score and associated content.
/**
* A match returned from an embedding search
* @param <Embedded> Type of embedded content (typically TextSegment)
*/
class EmbeddingMatch<Embedded> {
/**
* Returns the similarity score
* For VECTOR mode: cosine similarity (0.0-1.0)
* For HYBRID mode: RRF score (~0.02-1.0)
* @return Similarity score
*/
double score();
/**
* Returns the ID of the matched embedding
* @return Embedding ID (UUID string)
*/
String embeddingId();
/**
* Returns the embedding vector
* @return Embedding object containing the vector
*/
Embedding embedding();
/**
* Returns the embedded content (text segment)
* @return TextSegment with text and metadata, or null if not stored
*/
Embedded embedded();
}For large datasets (>100k embeddings), use IVFFlat indexing:
PgVectorEmbeddingStore store = PgVectorEmbeddingStore.builder()
// ... other config ...
.useIndex(true)
.indexListSize(100) // Adjust based on dataset size
.build();Balance between relevance and performance:
// For most RAG applications, 3-5 results are sufficient
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(3) // Good balance for RAG
.minScore(0.7) // Filter low-quality matches
.build();For better query performance with metadata filtering:
Handle potential errors during search:
try {
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);
} catch (RuntimeException e) {
if (e.getMessage().contains("query must be provided")) {
// Hybrid mode requires both query embedding and query text
logger.error("Missing query text for HYBRID search mode", e);
} else {
throw e;
}
}queryEmbedding and query text must be provided in the requesttextSearchConfig parameter affects language-specific stemming and parsing in HYBRID modeInstall with Tessl CLI
npx tessl i tessl/maven-dev-langchain4j--langchain4j-pgvector@1.11.0