CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-qdrant

LangChain4j Qdrant integration providing a vector store embedding implementation for Qdrant database with metadata filtering support

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

filters.mddocs/

Metadata Filtering

Complete guide to metadata filtering in QdrantEmbeddingStore.

Overview

The library supports complex metadata filtering through the LangChain4j filter API. Filters are automatically converted to Qdrant's native filter format and can be used in:

  • Search operations: Filter results by metadata criteria
  • Remove operations: Delete embeddings matching criteria

Quick Filter Syntax

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

// Simple equality
Filter filter = metadataKey("category").isEqualTo("documentation");

// Comparison
Filter filter = metadataKey("year").isGreaterThan(2020);

// String search
Filter filter = metadataKey("content").containsString("important");

// Collection membership
Filter filter = metadataKey("status").isIn(Arrays.asList("active", "pending"));

// Logical combination
Filter filter = new And(
    metadataKey("category").isEqualTo("tech"),
    metadataKey("priority").isGreaterThan(5)
);

Comparison Filters

Equality Filters

import dev.langchain4j.store.embedding.filter.comparison.IsEqualTo;
import dev.langchain4j.store.embedding.filter.comparison.IsNotEqualTo;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.*;

// String equality
Filter filter1 = metadataKey("category").isEqualTo("documentation");
Filter filter2 = new IsEqualTo("category", "documentation");

// Integer equality
Filter filter3 = metadataKey("count").isEqualTo(42);
Filter filter4 = new IsEqualTo("count", 42);

// Long equality
Filter filter5 = metadataKey("timestamp").isEqualTo(1234567890L);
Filter filter6 = new IsEqualTo("timestamp", 1234567890L);

// UUID equality
import java.util.UUID;
UUID documentId = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
Filter filter7 = metadataKey("documentId").isEqualTo(documentId);
Filter filter8 = new IsEqualTo("documentId", documentId);

// Not equal
Filter filter9 = metadataKey("status").isNotEqualTo("archived");
Filter filter10 = new IsNotEqualTo("status", "archived");

Supported Types: String, UUID, Integer, Long

Numeric Comparison Filters

import dev.langchain4j.store.embedding.filter.comparison.IsGreaterThan;
import dev.langchain4j.store.embedding.filter.comparison.IsGreaterThanOrEqualTo;
import dev.langchain4j.store.embedding.filter.comparison.IsLessThan;
import dev.langchain4j.store.embedding.filter.comparison.IsLessThanOrEqualTo;

// Greater than
Filter filter1 = metadataKey("score").isGreaterThan(7.5);
Filter filter2 = new IsGreaterThan("score", 7.5);

// Greater than or equal
Filter filter3 = metadataKey("year").isGreaterThanOrEqualTo(2020);
Filter filter4 = new IsGreaterThanOrEqualTo("year", 2020);

// Less than
Filter filter5 = metadataKey("price").isLessThan(100.0);
Filter filter6 = new IsLessThan("price", 100.0);

// Less than or equal
Filter filter7 = metadataKey("age").isLessThanOrEqualTo(65);
Filter filter8 = new IsLessThanOrEqualTo("age", 65);

Supported Types: Integer, Long, Double, Float

Collection Filters

import dev.langchain4j.store.embedding.filter.comparison.IsIn;
import dev.langchain4j.store.embedding.filter.comparison.IsNotIn;
import java.util.Arrays;
import java.util.Collection;

// String collection
Collection<String> statuses = Arrays.asList("active", "pending", "review");
Filter filter1 = metadataKey("status").isIn(statuses);
Filter filter2 = new IsIn("status", statuses);

// Integer collection
Collection<Integer> priorities = Arrays.asList(1, 2, 3);
Filter filter3 = metadataKey("priority").isIn(priorities);
Filter filter4 = new IsIn("priority", priorities);

// Long collection
Collection<Long> timestamps = Arrays.asList(1000L, 2000L, 3000L);
Filter filter5 = metadataKey("timestamp").isIn(timestamps);

// UUID collection
import java.util.UUID;
Collection<UUID> ids = Arrays.asList(
    UUID.fromString("550e8400-e29b-41d4-a716-446655440000"),
    UUID.fromString("550e8400-e29b-41d4-a716-446655440001")
);
Filter filter6 = metadataKey("documentId").isIn(ids);

// Not in collection
Filter filter7 = metadataKey("category").isNotIn(Arrays.asList("spam", "archived"));
Filter filter8 = new IsNotIn("category", Arrays.asList("spam", "archived"));

Supported Types: String, UUID, Integer, Long

String Filters

import dev.langchain4j.store.embedding.filter.comparison.ContainsString;

// Substring search (case-sensitive)
Filter filter1 = metadataKey("title").containsString("important");
Filter filter2 = new ContainsString("title", "important");

// Partial match
Filter filter3 = metadataKey("content").containsString("user guide");

Note: String matching is case-sensitive.

Logical Filters

AND Filter

import dev.langchain4j.store.embedding.filter.logical.And;

// Combine two conditions
Filter filter1 = new And(
    metadataKey("category").isEqualTo("documentation"),
    metadataKey("year").isGreaterThanOrEqualTo(2020)
);

// Chain multiple AND conditions
Filter filter2 = new And(
    metadataKey("status").isEqualTo("published"),
    new And(
        metadataKey("priority").isGreaterThan(5),
        metadataKey("author").isEqualTo("John Doe")
    )
);

OR Filter

import dev.langchain4j.store.embedding.filter.logical.Or;

// Either condition
Filter filter1 = new Or(
    metadataKey("priority").isEqualTo(1),
    metadataKey("urgent").isEqualTo(true)
);

// Chain multiple OR conditions
Filter filter2 = new Or(
    metadataKey("category").isEqualTo("bug"),
    new Or(
        metadataKey("category").isEqualTo("critical"),
        metadataKey("category").isEqualTo("security")
    )
);

NOT Filter

import dev.langchain4j.store.embedding.filter.logical.Not;

// Negate condition
Filter filter1 = new Not(
    metadataKey("status").isEqualTo("archived")
);

// Negate complex condition
Filter filter2 = new Not(
    new And(
        metadataKey("category").isEqualTo("spam"),
        metadataKey("score").isLessThan(3)
    )
);

Complex Logical Combinations

// (category = "tech" AND year >= 2020) OR (priority > 8)
Filter filter1 = new Or(
    new And(
        metadataKey("category").isEqualTo("tech"),
        metadataKey("year").isGreaterThanOrEqualTo(2020)
    ),
    metadataKey("priority").isGreaterThan(8)
);

// category = "documentation" AND NOT(status = "draft" OR status = "archived")
Filter filter2 = new And(
    metadataKey("category").isEqualTo("documentation"),
    new Not(
        new Or(
            metadataKey("status").isEqualTo("draft"),
            metadataKey("status").isEqualTo("archived")
        )
    )
);

// (author = "Alice" OR author = "Bob") AND year > 2020 AND status != "deleted"
Filter filter3 = new And(
    new Or(
        metadataKey("author").isEqualTo("Alice"),
        metadataKey("author").isEqualTo("Bob")
    ),
    new And(
        metadataKey("year").isGreaterThan(2020),
        metadataKey("status").isNotEqualTo("deleted")
    )
);

Using Filters in Search

Basic Filtered Search

import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.data.segment.TextSegment;

Filter filter = metadataKey("category").isEqualTo("documentation");

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

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

Date Range Search

// Find documents from 2020-2023
Filter dateFilter = new And(
    metadataKey("year").isGreaterThanOrEqualTo(2020),
    metadataKey("year").isLessThanOrEqualTo(2023)
);

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

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

Multi-Category Search

// Find documents in specific categories
Filter categoryFilter = metadataKey("category").isIn(
    Arrays.asList("tutorial", "guide", "documentation")
);

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

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

Exclude Low Quality Results

// Find high-quality, recent documents
Filter qualityFilter = new And(
    metadataKey("quality_score").isGreaterThan(7.0),
    new And(
        metadataKey("year").isGreaterThanOrEqualTo(2022),
        metadataKey("status").isNotEqualTo("deprecated")
    )
);

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

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

Using Filters in Remove Operations

Remove by Category

Filter filter = metadataKey("category").isEqualTo("temporary");
store.removeAll(filter);

Remove Old Documents

// Remove documents older than 2020
Filter filter = metadataKey("year").isLessThan(2020);
store.removeAll(filter);

Remove by Multiple Criteria

// Remove draft documents from specific author
Filter filter = new And(
    metadataKey("status").isEqualTo("draft"),
    metadataKey("author").isEqualTo("test_user")
);
store.removeAll(filter);

Remove Archived or Deleted

// Remove documents marked as archived or deleted
Filter filter = metadataKey("status").isIn(
    Arrays.asList("archived", "deleted", "obsolete")
);
store.removeAll(filter);

Common Filter Patterns

Recent and Relevant

// Documents from last 2 years with high priority
Filter filter = new And(
    metadataKey("year").isGreaterThanOrEqualTo(2022),
    metadataKey("priority").isGreaterThanOrEqualTo(7)
);

Active Content Only

// Exclude draft, archived, and deleted content
Filter filter = new Not(
    metadataKey("status").isIn(
        Arrays.asList("draft", "archived", "deleted")
    )
);

Category with Quality Threshold

// Specific category with minimum quality
Filter filter = new And(
    metadataKey("category").isEqualTo("tutorial"),
    metadataKey("rating").isGreaterThanOrEqualTo(4.0)
);

Multi-Author Content

// Documents from multiple authors
Filter filter = metadataKey("author").isIn(
    Arrays.asList("Alice", "Bob", "Carol")
);

Keyword Search within Category

// Documents containing keyword in specific category
Filter filter = new And(
    metadataKey("category").isEqualTo("technical"),
    metadataKey("content").containsString("algorithm")
);

Exclude Test Data

// Production data only
Filter filter = new And(
    new Not(metadataKey("environment").isEqualTo("test")),
    new Not(metadataKey("tags").containsString("test"))
);

Supported Metadata Types

import dev.langchain4j.data.document.Metadata;
import java.util.UUID;

Metadata metadata = new Metadata();

// String - Full support for all filter types
metadata.put("category", "documentation");
metadata.put("author", "John Doe");

// Integer - Supports equality, comparison, in/not-in
metadata.put("count", 42);
metadata.put("priority", 5);

// Long - Supports equality, comparison, in/not-in
metadata.put("timestamp", 1234567890L);
metadata.put("fileSize", 1024L);

// Double - Supports comparison filters
metadata.put("score", 9.5);
metadata.put("rating", 4.8);

// Float - Supports comparison filters
metadata.put("confidence", 0.95f);
metadata.put("weight", 1.5f);

// UUID - Supports equality and in/not-in
metadata.put("documentId", UUID.randomUUID());

Filter Conversion Details

The library automatically converts LangChain4j filters to Qdrant filter format:

  • IsEqualTo → Qdrant Match condition
  • IsNotEqualTo → Qdrant Must Not with Match condition
  • IsGreaterThan → Qdrant Range with gt
  • IsGreaterThanOrEqualTo → Qdrant Range with gte
  • IsLessThan → Qdrant Range with lt
  • IsLessThanOrEqualTo → Qdrant Range with lte
  • IsIn → Qdrant Match Any condition
  • IsNotIn → Qdrant Must Not with Match Any condition
  • ContainsString → Qdrant Match Text condition
  • And → Qdrant Must conditions
  • Or → Qdrant Should conditions
  • Not → Qdrant Must Not conditions

Limitations and Unsupported Features

Unsupported Filter Types

The following will throw UnsupportedOperationException:

  • Custom filter implementations not in the LangChain4j filter API
  • Boolean value comparisons (use integer 0/1 instead)
  • Null value checks (add metadata only when value exists)
  • Regular expressions (use ContainsString for substring matching)

Unsupported Value Types

The following metadata types are not supported:

  • Boolean (use Integer with 0/1)
  • Arrays or Lists
  • Nested objects
  • Binary data

Workarounds

// Instead of Boolean
metadata.put("isActive", true);  // NOT SUPPORTED

// Use Integer
metadata.put("isActive", 1);  // 1 for true, 0 for false
Filter filter = metadataKey("isActive").isEqualTo(1);

// Instead of null checks
// Add metadata only when value exists
if (value != null) {
    metadata.put("optionalField", value);
}

Performance Considerations

Filter Selectivity

More selective filters (matching fewer documents) perform better:

// Good: Selective filter
Filter selective = metadataKey("documentId").isEqualTo(specificUUID);

// Less optimal: Broad filter
Filter broad = metadataKey("category").isNotEqualTo("archived");

Index Usage

Qdrant automatically indexes metadata fields. Equality and range filters are optimized:

// Optimized filters
metadataKey("category").isEqualTo("tech");
metadataKey("year").isGreaterThan(2020);
metadataKey("status").isIn(Arrays.asList("active", "pending"));

// Less optimized
metadataKey("content").containsString("keyword");  // Text search

Complex Filter Performance

Simpler filters generally perform better:

// Simple AND (good performance)
new And(
    metadataKey("category").isEqualTo("tech"),
    metadataKey("year").isGreaterThan(2020)
);

// Complex nested logic (slower)
new Or(
    new And(filter1, filter2),
    new And(
        new Not(filter3),
        new Or(filter4, filter5)
    )
);

Debugging Filters

Test Filter Independently

// Test filter before using in production
Filter testFilter = metadataKey("category").isEqualTo("test");

// Add test document
Metadata metadata = new Metadata();
metadata.put("category", "test");
TextSegment segment = TextSegment.from("Test content", metadata);
String id = store.add(embedding, segment);

// Verify filter works
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(10)
    .filter(testFilter)
    .build();

EmbeddingSearchResult<TextSegment> results = store.search(request);
System.out.println("Found " + results.matches().size() + " matches");

// Cleanup
store.remove(id);

Verify Metadata Storage

// After adding, verify metadata is stored correctly
String id = store.add(embedding, segment);

// Search and check metadata
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
    .queryEmbedding(queryEmbedding)
    .maxResults(1)
    .build();

EmbeddingSearchResult<TextSegment> results = store.search(request);
for (EmbeddingMatch<TextSegment> match : results.matches()) {
    if (match.embeddingId().equals(id)) {
        System.out.println("Metadata: " + match.embedded().metadata());
    }
}

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-qdrant@1.11.0

docs

api.md

error-handling.md

examples.md

filters.md

index.md

setup.md

tile.json