CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-chroma

LangChain4j integration for Chroma embedding store enabling storage, retrieval, and similarity search of vector embeddings with metadata filtering support for both API V1 and V2.

Pending
Overview
Eval results
Files

search.mddocs/operations/

Search Operations

Finding similar embeddings in the Chroma vector store using cosine similarity.

Basic Search

Simple Search

import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.data.embedding.Embedding;

Embedding queryEmbedding = Embedding.from(new float[]{0.15f, 0.25f, 0.35f});

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(10)
    .build();

EmbeddingSearchResult<TextSegment> result = store.search(request);

With Minimum Score Threshold

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(10)
    .minScore(0.7)  // Only return matches with score >= 0.7
    .build();

EmbeddingSearchResult<TextSegment> result = store.search(request);

Processing Results

Extracting Match Information

import dev.langchain4j.store.embedding.EmbeddingMatch;

EmbeddingSearchResult<TextSegment> result = store.search(request);

for (EmbeddingMatch<TextSegment> match : result.matches()) {
    // Similarity score (0.0 to 1.0, higher is more similar)
    double score = match.score();

    // Embedding ID
    String embeddingId = match.embeddingId();

    // The matching embedding vector
    Embedding matchedEmbedding = match.embedding();

    // Associated text segment (may be null)
    TextSegment textSegment = match.embedded();
}

Accessing Text and Metadata

for (EmbeddingMatch<TextSegment> match : result.matches()) {
    TextSegment segment = match.embedded();

    if (segment != null) {
        // Get text content
        String text = segment.text();

        // Get metadata
        Metadata metadata = segment.metadata();
        if (metadata != null) {
            String author = metadata.getString("author");
            Integer year = metadata.getInteger("year");
        }
    }
}

Stream Processing

import java.util.stream.Collectors;

List<String> relevantTexts = result.matches().stream()
    .filter(match -> match.score() > 0.8)
    .map(match -> match.embedded().text())
    .collect(Collectors.toList());

Search with Filters

Single Condition Filter

import dev.langchain4j.store.embedding.filter.Filter;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.*;

Filter filter = metadataKey("author").isEqualTo("John Doe");

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(10)
    .filter(filter)
    .build();

Numeric Comparison Filters

// Greater than or equal
Filter yearFilter = metadataKey("year").isGreaterThanOrEqualTo(2020);

// Greater than
Filter scoreFilter = metadataKey("score").isGreaterThan(7.5);

// Less than or equal
Filter priceFilter = metadataKey("price").isLessThanOrEqualTo(100.0);

// Less than
Filter ageFilter = metadataKey("age").isLessThan(30);

Important: Comparison operators only work with numeric metadata values in Chroma.

Multiple Values (IN/NOT IN)

// Match any of the values
Filter categoryFilter = metadataKey("category")
    .isIn(Arrays.asList("tech", "science", "math"));

// Exclude values
Filter excludeFilter = metadataKey("status")
    .isNotIn(Arrays.asList("draft", "archived"));

Combining Filters with AND

Filter combined = metadataKey("status").isEqualTo("published")
    .and(metadataKey("year").isGreaterThanOrEqualTo(2020))
    .and(metadataKey("author").isEqualTo("John Doe"));

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(5)
    .filter(combined)
    .build();

Combining Filters with OR

Filter orFilter = metadataKey("category").isEqualTo("tech")
    .or(metadataKey("category").isEqualTo("science"))
    .or(metadataKey("category").isEqualTo("math"));

Complex Nested Filters

// (status = "published") AND ((priority >= 5) OR (urgent = true))
Filter complex = metadataKey("status").isEqualTo("published")
    .and(
        metadataKey("priority").isGreaterThanOrEqualTo(5)
        .or(metadataKey("urgent").isEqualTo(true))
    );

NOT Filters

// Exclude specific value
Filter notArchived = metadataKey("status").isNotEqualTo("archived");

// Alternative using NOT wrapper (automatically converted)
Filter notDraft = Filter.not(metadataKey("status").isEqualTo("draft"));

Note: Chroma doesn't natively support NOT operations. The library converts them to equivalent positive operations where possible.

Score Interpretation

Score Calculation

// Chroma uses cosine distance
// distance range: [0, 2]
// score = 1 - (distance / 2)
// score range: [0, 1]

// Examples:
// distance = 0.0  → score = 1.0 (perfect match)
// distance = 1.0  → score = 0.5 (orthogonal)
// distance = 2.0  → score = 0.0 (opposite)

Score Thresholds

Common threshold guidelines:

// Very strict - only near-perfect matches
.minScore(0.95)

// Strict - very similar content
.minScore(0.85)

// Moderate - reasonably similar
.minScore(0.70)

// Loose - any similarity
.minScore(0.50)

// Very loose - include marginally related
.minScore(0.30)

Common Search Patterns

Semantic Search

import dev.langchain4j.model.embedding.EmbeddingModel;

String userQuery = "What is machine learning?";

// Convert query to embedding
EmbeddingModel model = createEmbeddingModel();
Embedding queryEmbedding = model.embed(userQuery).content();

// Search for similar content
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(5)
    .minScore(0.7)
    .build();

List<String> relevantDocs = store.search(request).matches().stream()
    .map(match -> match.embedded().text())
    .collect(Collectors.toList());

RAG Context Retrieval

// Retrieve context for RAG (Retrieval-Augmented Generation)
String question = "How does authentication work?";
Embedding queryEmbedding = model.embed(question).content();

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(3)
    .minScore(0.75)
    .build();

String context = store.search(request).matches().stream()
    .map(match -> match.embedded().text())
    .collect(Collectors.joining("\n\n"));

// Use context with LLM
String prompt = "Context:\n" + context + "\n\nQuestion: " + question;

Filtered Semantic Search

String query = "recent AI advances";
Embedding queryEmbedding = model.embed(query).content();

// Only search in recent, published documents
Filter filter = metadataKey("status").isEqualTo("published")
    .and(metadataKey("year").isGreaterThanOrEqualTo(2023))
    .and(metadataKey("category").isEqualTo("AI"));

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(10)
    .minScore(0.8)
    .filter(filter)
    .build();

Finding Near Duplicates

// Find near-duplicate content
Embedding documentEmbedding = getDocumentEmbedding();

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(documentEmbedding)
    .maxResults(10)
    .minScore(0.95)  // Very high threshold for near-duplicates
    .build();

// Filter out the document itself if needed
List<EmbeddingMatch<TextSegment>> duplicates =
    store.search(request).matches().stream()
        .filter(match -> !match.embeddingId().equals(originalDocId))
        .collect(Collectors.toList());

Category-Based Search

// Search within specific categories
List<String> allowedCategories = Arrays.asList("tech", "science", "engineering");

Filter categoryFilter = metadataKey("category").isIn(allowedCategories);

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(20)
    .filter(categoryFilter)
    .build();

Time-Bounded Search

// Search only recent documents (last 90 days)
long ninetyDaysAgo = System.currentTimeMillis() - (90L * 24 * 60 * 60 * 1000);

Filter timeFilter = metadataKey("timestamp").isGreaterThanOrEqualTo(ninetyDaysAgo);

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(15)
    .filter(timeFilter)
    .build();

Performance Considerations

Result Limits

// Smaller maxResults = faster search
.maxResults(5)   // Fast, good for most use cases
.maxResults(50)  // Moderate, for broader exploration
.maxResults(100) // Slower, for comprehensive results

Using Minimum Score

// minScore acts as early termination hint
// Can improve performance by reducing result processing
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(100)
    .minScore(0.8)  // May return fewer than 100 results
    .build();

Filter Selectivity

More selective filters improve performance:

// Less selective - scans more data
Filter broad = metadataKey("year").isGreaterThan(2000);

// More selective - scans less data
Filter specific = metadataKey("status").isEqualTo("published")
    .and(metadataKey("year").isEqualTo(2024))
    .and(metadataKey("category").isEqualTo("AI"));

Error Handling

try {
    EmbeddingSearchResult<TextSegment> result = store.search(request);

} catch (IllegalArgumentException e) {
    // Invalid request parameters
    System.err.println("Invalid search request: " + e.getMessage());

} catch (java.net.http.HttpTimeoutException e) {
    // Search timed out
    System.err.println("Search timed out: " + e.getMessage());

} catch (Exception e) {
    // Other errors (network, Chroma errors)
    System.err.println("Search failed: " + e.getMessage());
}

Limitations

Filter Limitations:

  1. Comparison operators (>, >=, <, <=) only work with numeric values
  2. String comparisons using operators are not supported
  3. NOT operations are converted to equivalent positive operations

Embedding Limitations:

  1. Query embedding must have same dimensions as stored embeddings
  2. Distance metric is always cosine (cannot be changed)

API Reference

See: EmbeddingSearchRequest API for complete type signatures.

Related:

Install with Tessl CLI

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

docs

index.md

tile.json