This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Indexers

Configuration reference for built-in and plugin indexers shipped with Lucille.

For conceptual documentation — what an Indexer is, why batching matters, deletion as a design pattern, and error handling at the batch level — see Architecture: Indexer.

Generic indexer Configuration

Indexer configuration has two parts: the generic indexer block (common to all backends), and a backend-specific config block (e.g., solr, opensearch, elastic, csv).

indexer {
  type: "Solr"
  batchSize: 100
  batchTimeout: 100
  blacklist: ["internal_field"]
}

solr {
  url: "http://localhost:8983/solr/my-collection"
}

indexer.type is shorthand for a built-in indexer: "Solr", "OpenSearch", "Elasticsearch", or "CSV". For plugin indexers, use indexer.class with the fully qualified class name instead.

Generic Parameters

ParameterTypeDefaultDescription
typeStringShorthand for built-in indexers: Solr, OpenSearch, Elasticsearch, CSV.
classStringFully qualified class name for plugin or custom indexers.
batchSizeInteger100Number of documents to accumulate before sending a batch.
batchTimeoutInteger (ms)100Milliseconds since last add or flush before the batch is sent regardless of size.
idOverrideFieldStringDocument field whose value is used as the ID sent to the destination (instead of id).
indexOverrideFieldStringDocument field whose value determines the target index/collection. Triggers per-index batching. Not supported by OpenSearch or Elasticsearch indexers.
whitelistList<String>Only these fields are sent to the destination. Fields on the blacklist are still excluded.
blacklistList<String>These fields are never sent to the destination.
sendEnabledBooleantrueSet to false to disable actual indexing (useful for testing or pipeline validation).
deletionMarkerFieldStringField name that marks a document as a deletion request.
deletionMarkerFieldValueStringValue in deletionMarkerField that triggers a deletion. Both must be set together.
deleteByFieldFieldStringField name containing the index field to use in a delete-by-query operation.
deleteByFieldValueStringField name containing the value to match in a delete-by-query operation. Both must be set together.
maxRetriesInteger— (disabled)Maximum retry attempts for a failed batch. Must be > 0 when set. Omit to disable retries entirely.
retryWaitDurationMsInteger (ms)1000Initial wait duration before the first retry. Subsequent retries use exponential backoff. Requires maxRetries.
retryMaxWaitDurationMsLong (ms)30000Maximum wait duration between retries (caps the exponential backoff). Requires maxRetries.
retryRandomizationFactorDouble0.5Jitter factor applied to wait duration. 0.5 means actual wait is 50%–150% of computed backoff. Set to 0.0 to disable jitter. Requires maxRetries.
retryableStatusCodesList<Integer>[429, 503, -1]HTTP status codes that trigger a retry. -1 means “no status code available” (e.g., network timeout). An empty list is invalid. Requires maxRetries.

Field Filtering

Whitelist and blacklist are applied at indexing time, not during pipeline processing. Stages see all fields on a document; only the Indexer strips fields before sending to the backend. Reserved internal fields (___dropped, ___skipped, ___children) are always stripped.

When indexOverrideField is set, the Indexer uses a MultiBatch — maintaining a separate batch per distinct field value and flushing each independently when it reaches batchSize or batchTimeout.

Deletion Mechanics

Two distinct deletion mechanisms are available:

Delete by ID: Set deletionMarkerField and deletionMarkerFieldValue. When a document has the marker field set to the marker value, the Indexer issues a delete-by-ID against the search backend for that document’s ID.

Delete by query: Also set deleteByFieldField and deleteByFieldValue. When a document has all four deletion fields set, the Indexer issues a delete-by-query: it deletes all documents in the index where deleteByFieldField’s referenced field equals the value in deleteByFieldValue’s referenced field.

indexer {
  type: "Solr"
  deletionMarkerField: "file_expired"
  deletionMarkerFieldValue: "true"
  deleteByFieldField: "delete_by_field"
  deleteByFieldValue: "delete_by_value"
}

Batching Behavior

Documents accumulate in a batch and are flushed when either condition is met:

  • The batch reaches batchSize documents (default: 100).
  • batchTimeout milliseconds have elapsed since the last document was added or the last flush (default: 100ms).

The timeout flush ensures documents are not left waiting indefinitely in low-volume scenarios.

Batch-level vs. per-document failures: A bulk-API failure that rejects the entire request fails all documents in the batch. Individual document rejections in the response (e.g., mapping errors) fail only those specific documents — the rest succeed. Both cases are tracked separately in the run summary.


Indexer Catalogue

Core Indexers

Plugin Indexers

1 - Solr Indexer

Configuration reference for the Solr Indexer — single-node and SolrCloud.

com.kmwllc.lucille.indexer.SolrIndexer

Config block: solr { ... }

ParameterTypeRequiredDescription
urlList<String>No*Solr base URLs (e.g., http://localhost:8983/solr).
useCloudClientBooleanNoUse SolrCloud client. Default: false.
defaultCollectionStringNoTarget Solr collection.
zkHostsList<String>No*ZooKeeper addresses for SolrCloud (e.g., ["zk1:2181", "zk2:2181"]).
zkChrootStringNoZooKeeper chroot path for SolrCloud (e.g., "/solr").
userNameStringNoHTTP Basic Auth username.
passwordStringNoHTTP Basic Auth password.
acceptInvalidCertBooleanNoAccept invalid TLS certificates. Default: false.

*Provide either url or (for SolrCloud) zkHosts.

# Single node
solr { url: "http://localhost:8983/solr/my-collection" }

# SolrCloud with URL
solr {
  useCloudClient: true
  url: ["http://localhost:8983/solr"]
  defaultCollection: "my-collection"
}

# SolrCloud with ZooKeeper
solr {
  useCloudClient: true
  zkHosts: ["zk1:2181", "zk2:2181"]
  zkChroot: "/solr"
  defaultCollection: "my-collection"
}

Child Document Support

SolrIndexer is the only Lucille indexer that fully supports attached child documents. When a document has children (via doc.addChild()), they are automatically converted to Solr’s _childDocuments_ structure. The ___children field itself is never sent to Solr — only the nested Solr child documents are added.

Children respect the configured blacklist and whitelist: filtered fields are removed from child documents before they are added to the Solr parent.


Collection Routing

When indexer.indexOverrideField is set, documents are routed to different Solr collections within the same batch. The value of the override field on each document determines which collection receives it. The override field itself is stripped from the document before sending.

The target collection must already exist in Solr — the indexer does not create collections.


Interleaved Add/Delete Ordering

SolrIndexer preserves the order of adds and deletes within a batch. If a batch contains an add followed by a delete for the same document ID (or vice versa), the operations are sent to Solr in the correct sequence. This is important for incremental ingestion where a document may be re-added and then deleted (or deleted and re-added) within the same run.

Internally, the indexer detects when an add and a delete for the same ID appear in the same batch and flushes the earlier operation before queuing the later one.


Delete-by-Query via Terms Queries

When deleteByFieldField and deleteByFieldValue are configured, SolrIndexer constructs a Solr terms query for efficient bulk deletion rather than issuing individual delete-by-query calls. Multiple deletions targeting the same field are combined into a single terms query:

(+{!terms f='category' v='obsolete,deprecated'})

This reduces the number of round-trips to Solr when many documents are marked for deletion in the same batch.


Limitations

  • Object fields not supported — If a document field contains a nested Map or Object, SolrIndexer throws an IndexerException. Flatten nested structures in your pipeline before indexing to Solr.

Troubleshooting

Connection validation fails:

  • Single-node mode (useCloudClient: false): Validation uses solrClient.ping(). If this fails, check that the URL includes the collection path (e.g., http://localhost:8983/solr/my-collection) and that the collection exists.
  • SolrCloud mode (useCloudClient: true): Validation checks cluster status via the Collections API. If this fails, the cluster is unreachable — check ZooKeeper connectivity and that at least one Solr node is running.

“Object field is not supported by the SolrIndexer”: A document field contains a nested JSON object. Add a stage to your pipeline that flattens or removes the field before it reaches the indexer.

2 - OpenSearch Indexer

Configuration reference for the OpenSearch Indexer.

com.kmwllc.lucille.indexer.OpenSearchIndexer

Config block: opensearch { ... }

ParameterTypeRequiredDescription
urlStringYesOpenSearch endpoint URL including credentials if needed (e.g., https://admin:password@localhost:9200).
indexStringYesTarget index name.
updateBooleanNoUse the partial update API instead of index (upsert). Default: false.
acceptInvalidCertBooleanNoAccept invalid TLS certificates. Default: false.

Also supports indexer.routingField and indexer.versionType (via the generic indexer block).

opensearch {
  url: "https://admin:admin@localhost:9200"
  url: ${?OPENSEARCH_URL}
  index: "my-index"
  acceptInvalidCert: true
}

Routing

When indexer.routingField is set, the value of that field on each document is used as the _routing parameter in the bulk request. This controls which shard receives the document. Use this when your OpenSearch index has custom shard routing configured.


Version Type

When indexer.versionType is set to external or external_gte, the Kafka message offset is used as the document’s version number. This enables optimistic concurrency control in streaming mode — OpenSearch will reject a write if the incoming version is not greater than (or equal to, for external_gte) the existing version.

This only works with documents that carry Kafka metadata (i.e., in distributed mode where documents are KafkaDocument instances).


Partial Update Mode

When update: true, documents are sent as partial updates (doc-as-upsert) rather than full index operations:

  • Full index (default): Replaces the entire document in the index. Fields not present in the new document are removed.
  • Partial update: Merges fields into the existing document. Fields not present in the update are left unchanged.
opensearch {
  url: ${OPENSEARCH_URL}
  index: "my-index"
  update: true
}

Index Override

OpenSearch supports indexer.indexOverrideField for routing documents to different indices within the same batch. The value of the override field on each document determines the target index. The override field itself is stripped from the document before sending.


Retry Behavior

OpenSearchIndexer wraps both transport-level failures (connection refused, timeout) and HTTP-level failures (e.g., 429, 503) as IndexerRetryableException. This enables the base class retry machinery when indexer.maxRetries is configured.

Per-document failures in the bulk response are also wrapped with the item’s HTTP status code, allowing the retry logic to distinguish between retryable failures (e.g., 429 Too Many Requests) and permanent failures (e.g., 400 Bad Request).


Child Documents

Attached children are flattened into the parent document as nested objects. They are not indexed as separate OpenSearch documents. This is different from Solr’s _childDocuments_ approach — in OpenSearch, children become part of the parent’s JSON structure.


Troubleshooting

“index not found”: The target index must exist before indexing. Create it manually or via an index template.

TLS certificate errors: Set acceptInvalidCert: true for development environments with self-signed certificates. Do not use this in production.

Authentication: Include credentials in the URL: https://user:password@host:9200. Use environment variable substitution to avoid hardcoding: url: ${OPENSEARCH_URL}.

3 - Elasticsearch Indexer

Configuration reference for the Elasticsearch Indexer, including join field support.

com.kmwllc.lucille.indexer.ElasticsearchIndexer

Config block: elastic { ... }

Supports all OpenSearch parameters, plus parent-child join support:

ParameterTypeRequiredDescription
urlStringYesElasticsearch endpoint URL.
indexStringYesTarget index name.
updateBooleanNoUse partial update API. Default: false.
acceptInvalidCertBooleanNoAccept invalid TLS certs. Default: false.
parentNameStringNoParent relation name for join field mappings.

Join field support (for parent-child mappings):

elastic {
  url: "http://localhost:9200"
  index: "my-index"
  join: {
    joinFieldName: "my_join_field"
    isChild: true
    childName: "my_child"
    parentDocumentIdSource: "parent_id_field"
  }
}
indexer {
  type: "Elasticsearch"
  routingField: "routing_field"
}

Join Field Support (Detailed)

Elasticsearch’s join field allows parent-child relationships within a single index. The ElasticsearchIndexer supports this via the elastic.join config block:

ParameterDescription
joinFieldNameThe name of the join field in your Elasticsearch mapping.
isChildWhether documents indexed by this indexer are children in the join relationship.
childNameThe relation name for the child (must match your mapping).
parentDocumentIdSourceThe document field that holds the parent’s ID. Used to set the _routing parameter (required for joins).

When isChild: true, the indexer adds a join field to each document with the child relation name and sets routing to the parent’s ID. Elasticsearch requires parent and child documents to be on the same shard, so indexer.routingField should also be set to the same field as parentDocumentIdSource.


Routing and Versioning

Same as OpenSearch: supports indexer.routingField for custom shard routing and indexer.versionType for optimistic concurrency control using Kafka offsets in distributed mode.


Partial Update Mode

When update: true, documents are sent as partial updates (doc-as-upsert) rather than full index operations. Same behavior as the OpenSearch Indexer.


Differences from OpenSearch Indexer

  • No retry support — ElasticsearchIndexer does not wrap failures as IndexerRetryableException. The base class retry machinery will not trigger for Elasticsearch failures. If retries are needed, configure them at the Elasticsearch client or load balancer level.
  • No indexOverrideField support — All documents are sent to the single configured index. You cannot route documents to different indices within the same batch.
  • Child documents — The code iterates attached children but does not currently add them to the indexed document (this is a known TODO). Use emitted children (separate documents) instead of attached children if you need child documents indexed in Elasticsearch.

Troubleshooting

Join field errors: Ensure your Elasticsearch index mapping includes the join field with the correct parent and child relation names. The joinFieldName in config must match the mapping exactly.

Routing errors with joins: Parent and child documents must be on the same shard. Set indexer.routingField to the field containing the parent ID.

“index not found”: The target index must exist before indexing. Create it manually or via an index template.

4 - CSV Indexer

Configuration reference for the CSV Indexer — write pipeline output to a CSV file.

com.kmwllc.lucille.indexer.CSVIndexer

Config block: csv { ... }

ParameterTypeRequiredDescription
pathStringYesOutput CSV file path.
columnsList<String>YesOrdered list of document fields to write as columns.
includeHeaderBooleanNoWrite a header row. Default: true.
appendBooleanNoAppend to an existing file. Default: false.
indexer { type: "CSV" }
csv {
  path: "./output.csv"
  columns: ["id", "title", "body", "published_at"]
}

Limitations: CSVIndexer does not support indexer.indexOverrideField.


Column Ordering and Field Selection

The columns list determines both which fields are written and their column order. Fields not listed in columns are silently omitted from the output. The document’s id field is not automatically included — add it to columns explicitly if you want it in the CSV.


Multi-Valued Fields

When a document field is multi-valued, CSVIndexer writes the list’s toString() representation (e.g., [val1, val2]). This is a lossy representation — the square brackets and commas become part of the string. If you need a different delimiter or format for multi-valued fields, flatten them in a pipeline stage before indexing.


Append Mode

When append: true, the file is opened in append mode. Set includeHeader: false when appending to avoid duplicate header rows:

csv {
  path: "./output.csv"
  columns: ["id", "title", "body"]
  append: true
  includeHeader: false
}

Deletion Not Supported

CSVIndexer logs a warning if deletionMarkerField or deleteByFieldField are configured but does not perform any deletion. Documents marked for deletion are written as regular rows.


Directory Creation

CSVIndexer creates parent directories automatically if they don’t exist. You can specify a path like ./output/results/data.csv without creating the directories first.


Use Cases

CSVIndexer is primarily useful for:

  • Testing — Verify pipeline output without a search backend.
  • Debugging — Inspect what fields and values reach the indexer after pipeline processing.
  • Exporting — Produce a file for import into another system.

It is not intended for production search indexing.

5 - NopIndexer (No-op)

A no-op indexer that discards all documents — useful for testing pipelines.

com.kmwllc.lucille.indexer.NopIndexer

Discards all documents without sending them anywhere. Useful for testing pipelines when indexing output is not needed. Equivalent to setting indexer.sendEnabled: false on any other indexer.

indexer { type: "Nop" }

When to Use

  • Testing pipeline logic — Run your pipeline end-to-end without needing a search backend running.
  • Validating config — Confirm that connectors, stages, and the indexer block are configured correctly before pointing at a real backend.
  • Benchmarking pipeline throughput — Measure how fast your pipeline processes documents in isolation, without indexer latency as a factor.

Equivalence with sendEnabled: false

NopIndexer and indexer.sendEnabled: false on another indexer are functionally identical — both discard documents after the pipeline processes them. The difference is that NopIndexer doesn’t require configuring a backend-specific block at all:

# These are equivalent:

# Option 1: NopIndexer
indexer { type: "Nop" }

# Option 2: sendEnabled on a real indexer
indexer { type: "Solr", sendEnabled: false }
solr { url: "http://localhost:8983/solr/my-collection" }

Use in RunType.TEST

Runner.runInTestMode() does not use NopIndexer. Instead, it creates the configured indexer with bypass=true, which causes sendToIndex() to skip the actual backend call while still running the full indexer loop (batching, events, metrics). This means your test config still needs a valid indexer block — but the backend does not need to be running. If you want to avoid configuring a backend block entirely in test configs, use NopIndexer explicitly:

indexer { type: "Nop" }

6 - Pinecone Indexer

Configuration reference for the Pinecone Indexer — index vector embeddings into Pinecone.

com.kmwllc.lucille.pinecone.indexer.PineconeIndexer

Indexes vector embeddings into Pinecone. Config block: pinecone { ... }

Maven dependency:

<dependency>
  <groupId>com.kmwllc</groupId>
  <artifactId>lucille-pinecone</artifactId>
  <version>${lucille.version}</version>
</dependency>
ParameterTypeRequiredDescription
apiKeyStringYesPinecone API key. Use ${PINECONE_API_KEY} for environment variable substitution.
indexStringYesName of the Pinecone index to write to.
vectorFieldStringYesDocument field containing the vector embedding.
namespaceStringNoPinecone namespace. Default: "default".
indexer {
  class: "com.kmwllc.lucille.pinecone.indexer.PineconeIndexer"
  deletionMarkerField: "is_deleted"
  deletionMarkerFieldValue: "true"
}

pinecone {
  apiKey: ${PINECONE_API_KEY}
  index: "my-index"
  vectorField: "content_vector"
  namespace: "default"
}

Namespace Routing

When namespaces is configured, it is a map of namespace names to embedding field names. Each document is upserted into every configured namespace using the corresponding embedding field. This enables multi-vector indexing — for example, title embeddings in one namespace and body embeddings in another:

pinecone {
  apiKey: ${PINECONE_API_KEY}
  index: "my-index"
  namespaces: {
    "title-ns": "title_vector"
    "body-ns": "body_vector"
  }
}

When namespaces is not set, all documents go to the default namespace using defaultEmbeddingField.


defaultEmbeddingField vs. namespaces

Two modes are available:

  • Single namespace — Set defaultEmbeddingField to the name of the vector field. All documents are upserted into the default namespace.
  • Multi namespace — Set namespaces as a map of namespace → embedding field. Documents are upserted into each namespace with the corresponding vector.

At least one of these must be set when uploading documents. If both are omitted, the indexer throws at upload time.


Metadata Fields

Only fields listed in metadataFields are sent as Pinecone metadata alongside the vector. All other document fields are ignored. Metadata values are converted to strings.

pinecone {
  apiKey: ${PINECONE_API_KEY}
  index: "my-index"
  defaultEmbeddingField: "content_vector"
  metadataFields: ["title", "category", "source_url"]
}

Upsert vs. Update Mode

ModeBehavior
"upsert" (default)Creates or replaces vectors. If the ID exists, the vector and metadata are overwritten.
"update"Updates existing vectors in place. Does not create new records. Silently succeeds (HTTP 200) if the ID doesn’t exist.
pinecone {
  mode: "update"
  // ...
}

Batch Size Limit

Pinecone’s API limits batches to 1000 vectors or 2MB, whichever is reached first. The indexer enforces the 1000-vector limit at startup — if indexer.batchSize exceeds 1000, the indexer throws an exception and refuses to start.

Higher-dimensional vectors may hit the 2MB limit at counts lower than 1000. If this happens, the Pinecone API returns an error at runtime. Reduce indexer.batchSize accordingly.


Deletion

Deletion is by ID only (uses deletionMarkerField / deletionMarkerFieldValue). Delete-by-query is not supported.

When namespaces is configured, deletion is issued to all configured namespaces.


Troubleshooting

“Maximum batch size for Pinecone is 1000”: Reduce indexer.batchSize to 1000 or lower.

API key invalid: Verify PINECONE_API_KEY is set and the key has write access to the target index.

Index not found: The Pinecone index must be created via the Pinecone console or API before running Lucille.

Vector dimension mismatch: The vector field on each document must have the same number of dimensions as the Pinecone index was created with. A mismatch causes a runtime error from the Pinecone API.

7 - Weaviate Indexer

Configuration reference for the Weaviate Indexer — index documents and vectors into Weaviate.

com.kmwllc.lucille.weaviate.indexer.WeaviateIndexer

Indexes documents into Weaviate. Config block: weaviate { ... }

Maven dependency:

<dependency>
  <groupId>com.kmwllc</groupId>
  <artifactId>lucille-weaviate</artifactId>
  <version>${lucille.version}</version>
</dependency>
ParameterTypeRequiredDescription
apiKeyStringYesWeaviate API key for authentication. Use ${?WEAVIATE_API_KEY} for environment variable substitution.
hostStringYesWeaviate instance hostname (e.g., my-cluster.weaviate.network). Do not include the protocol — HTTPS is used automatically.
classNameStringNoThe Weaviate class (object type) to create or update. Default: "Document".
idDestinationNameStringNoField name under which the document’s original Lucille ID is stored in Weaviate (since Weaviate’s id field must be a UUID). Default: "id_original".
vectorFieldStringNoDocument field containing the vector embedding to index. If omitted, no vector is sent (useful if Weaviate is configured to generate its own embeddings).
indexer {
  class: "com.kmwllc.lucille.weaviate.indexer.WeaviateIndexer"
}

weaviate {
  apiKey: ${WEAVIATE_API_KEY}
  host: "my-cluster.weaviate.network"
  className: "Article"
  idDestinationName: "lucille_id"
  vectorField: "content_vector"
}

Note on IDs: Weaviate requires UUIDs for its internal id field. The WeaviateIndexer generates a UUID from the document’s Lucille ID and stores the original ID under idDestinationName so it can be retrieved later.


UUID Generation

Weaviate requires UUIDs for its internal id field. The WeaviateIndexer generates a deterministic UUID from the document’s Lucille ID using UUID.nameUUIDFromBytes(id.getBytes()). This means:

  • The same Lucille ID always maps to the same Weaviate UUID, enabling idempotent upserts.
  • The original Lucille ID is stored under idDestinationName (default: "id_original") so it can be retrieved later.

Deletion Not Supported

WeaviateIndexer logs a warning if deletionMarkerField or deleteByFieldField are configured but does not perform deletions. Documents marked for deletion are indexed as regular objects. This is a known limitation.


Vector Field Handling

If vectorField is set and the document has that field, the vector is sent alongside the object properties. The vector field is removed from the properties map to avoid storing it twice (once as a vector, once as a property).

If vectorField is omitted, no vector is sent. This is useful when Weaviate is configured to generate its own embeddings via vectorizer modules (e.g., text2vec-openai).


Consistency Level

All writes use ConsistencyLevel.ALL (strongest consistency). This is not currently configurable. In a multi-node Weaviate cluster, this means all replicas must acknowledge the write before it is considered successful.


Connection Timeouts

The Weaviate client is configured with 6-second timeouts for connection, read, and write operations. These are not currently configurable via the Lucille config. If you experience timeout errors with large batches or slow networks, reduce indexer.batchSize.


Class/Schema Requirements

The className parameter determines which Weaviate class (schema type) objects are created under. The class must already exist in the Weaviate schema — the indexer does not create it. If the class doesn’t exist, the batch write will fail.


Troubleshooting

AuthException (“Couldn’t connect to Weaviate instance”): The API key is invalid or the host is unreachable. Verify WEAVIATE_API_KEY and that the host value is correct (hostname only, no https:// prefix).

Class not found in schema: Create the Weaviate class via the Weaviate console or REST API before running Lucille.

Vector dimension mismatch: If using a Weaviate vectorizer module, ensure the vector field dimensions match what the module expects. If sending vectors directly, ensure they match the class’s configured vector dimensions.

Timeout errors: Reduce indexer.batchSize to send smaller batches. The 6-second timeout is fixed.