CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-pgvector

LangChain4j PGVector integration for PostgreSQL-based vector embedding storage and retrieval

Pending
Overview
Eval results
Files

search-operations.mddocs/

Search Operations

Search for similar embeddings using vector similarity (VECTOR mode) or hybrid search combining vector similarity with full-text keyword search (HYBRID mode).

Capabilities

Search for Similar Embeddings

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);

Vector Search (Default)

Standard vector similarity search using cosine distance.

Basic Vector Search

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());
    }
});

Vector Search with Score Threshold

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);

Vector Search with Metadata Filter

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);

Complex Metadata Filtering

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);

Hybrid Search

Combines vector similarity search with PostgreSQL full-text keyword search using Reciprocal Rank Fusion (RRF).

Basic Hybrid Search

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);

How Hybrid Search Works

Hybrid search uses Reciprocal Rank Fusion (RRF) to combine results:

  1. Vector Search: Finds semantically similar text using cosine similarity
  2. Keyword Search: Finds text with matching keywords using PostgreSQL's tsvector
  3. RRF Fusion: Combines rankings using the formula:
RRF_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)

Understanding RRF Scores

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.0307

Important: RRF scores are different from cosine similarity scores:

  • Vector-only search: Scores range from 0.0 to 1.0 (cosine similarity)
  • Hybrid search: Scores typically range from ~0.02 to ~1.0 (RRF rank-based)

When to Use Hybrid Search

Hybrid search is beneficial when:

  • You need both semantic similarity and exact keyword matches
  • Queries contain domain-specific terms, product names, or technical jargon
  • You want to improve retrieval accuracy in RAG applications
  • Users may use exact phrases or proper nouns that need keyword matching

Configuring Hybrid Search

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();

Tuning RRF Parameter

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();

Search Result Processing

Accessing Search Results

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"));
    }
}

Building Context for RAG

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);

EmbeddingSearchRequest Builder

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();
    }
}

Search Result Types

EmbeddingSearchResult

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();
}

EmbeddingMatch

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();
}

Performance Considerations

Index Usage

For large datasets (>100k embeddings), use IVFFlat indexing:

PgVectorEmbeddingStore store = PgVectorEmbeddingStore.builder()
    // ... other config ...
    .useIndex(true)
    .indexListSize(100)  // Adjust based on dataset size
    .build();

Optimal maxResults

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();

Metadata Filter Performance

For better query performance with metadata filtering:

  • Use COLUMN_PER_KEY storage mode for static, known metadata keys
  • Use COMBINED_JSONB (not COMBINED_JSON) for dynamic metadata with queries
  • Create indexes on frequently filtered metadata fields

Error Handling

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;
    }
}

Important Notes

  • HYBRID Mode Requirement: Both queryEmbedding and query text must be provided in the request
  • Score Ranges: VECTOR mode scores are cosine similarity (0.0-1.0), HYBRID mode scores are RRF-based (~0.02-1.0)
  • minScore: Set appropriately based on search mode (e.g., 0.7 for VECTOR, 0.02-0.05 for HYBRID)
  • Empty Results: If no results meet the criteria, an empty list is returned (not null)
  • Cosine Similarity: Only cosine distance is used for vector similarity calculation
  • Text Search Configuration: The textSearchConfig parameter affects language-specific stemming and parsing in HYBRID mode

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-pgvector

docs

embedding-operations.md

index.md

metadata-storage.md

search-operations.md

store-creation.md

tile.json