Make a write visible to search
You index a document, search for it straight away, and it is not there. Nothing is broken. Two separate delays sit between a write and a search that can see it. This guide shows how write visibility works in Exofind and the options you have to make written documents visible to search queries.
Understanding the two delays
Section titled “Understanding the two delays”When you write a document, the change passes through two stages before all nodes can return it in search results.
Write -> [Commit delay on writer] -> Remote storage -> [Refresh delay on readers] -> Searchable everywhere- The commit on the writer node: Only one node writes to an index at a time. Changes become searchable on the writer when that node commits them to Lucene and pushes the commit to remote storage. A commit triggers automatically based on two configuration variables:
EXOFIND_INDEXES_COMMIT_MAX_CHANGES(default:10000): Triggers a commit when this many uncommitted changes accumulate.EXOFIND_INDEXES_COMMIT_MAX_INTERVAL(default:5s): Triggers a commit when the oldest uncommitted change waits this long.
- The refresh on reader nodes: Every other node serves searches from its own local copy of the index. A reader node polls the index registry, which names the version of every index the deployment holds, and pulls an index whose version has moved past its copy.
EXOFIND_INDEXES_REFRESH_INTERVAL(default:30s) sets the longest a node waits before it considers pulling, and the shortest it leaves between two pulls of the same generation. The variableEXOFIND_INDEXES_VERIFY_INTERVAL(default:10m) sets the maximum time a node can go without verifying an index manifest.
With default settings, a write takes about 5 seconds to become searchable on the writer node. It becomes searchable on every node about 15 seconds after the write on an index that is otherwise quiet, and within about 35 seconds on an index that is written continuously.
In LOCAL storage mode, there is only one node and one copy of the index, so only the commit delay exists. In OBJECT storage mode with multiple nodes, both delays exist.
Why searches miss behind a load balancer
Section titled “Why searches miss behind a load balancer”When your application sits behind a load balancer:
- Writes are forwarded automatically to the writer node, regardless of which node receives the request.
- Searches are served locally by whichever node receives them and are never forwarded.
Because the load balancer typically sends the write and the search to different nodes, the search lands on a reader node whose local copy has not yet pulled the latest commit.
For more details on configuration settings, see the Configuration reference.
Prerequisites
Section titled “Prerequisites”- An active Exofind deployment with at least one index.
- An API key with the permissions required for the strategy you choose:
documents.writeto index documents.searchto run search queries.indexes.readto locate writer nodes and check status.indexes.committo trigger immediate commits.indexes.pullto force a node to synchronize with storage.
Choose a visibility strategy
Section titled “Choose a visibility strategy”Select one of the following strategies based on your application architecture and consistency requirements. Option 1 works through a load balancer without additional configuration.
Option 1: Pass the freshness token from the write to the search
Section titled “Option 1: Pass the freshness token from the write to the search”Every write operation returns a freshness token that identifies the state containing the change. When you include the token in a search request, the receiving node answers only after reaching that state. The writer node commits immediately when requested, and reader nodes pull from storage to reach the required commit.
-
Index the document and save the
freshnesstoken from the response (requiresdocuments.write):POST /v1alpha1/indexes/products/documents HTTP/1.1Host: exofind.example.comAuthorization: Bearer <token>Content-Type: application/json{ "documents": [ { "id": "42", "name": "laptop" } ] }{ "indexed": 1, "failed": [], "freshness": "AQoIcHJvZHVjdHMSATIYBw" } -
Send the search request with the token in
freshness.atLeast(requiressearch):POST /v1alpha1/indexes/products/search HTTP/1.1Host: exofind.example.comAuthorization: Bearer <token>Content-Type: application/json{"query": [{ "type": "text", "text": "laptop" }],"freshness": { "atLeast": "AQoIcHJvZHVjdHMSATIYBw" }}The node answers after reaching the specified state. If the search reaches the writer node, the writer commits immediately. If the search reaches a reader node, the reader waits for the writer to commit and pulls the new state from storage.
-
Retry the request if the server returns
503 Service Unavailablewith the error codesearch:freshness:unavailable:This error indicates that the node reached
EXOFIND_SEARCH_FRESHNESS_WAIT(default:10s) before the state became available. Retry the request after the delay in theRetry-Afterheader.
Write operations return one token for the entire batch. The commit action, search settings changes, and generation promotions also return freshness tokens. For all endpoints that return tokens and the errors a token can produce, see the Search API reference.
Trade-off: Searches on reader nodes wait for the writer commit interval (up to EXOFIND_INDEXES_COMMIT_MAX_INTERVAL, default: 5s) plus the pull duration. A freshness token does not guarantee durability. If the writer node fails before committing, uncommitted writes are lost. See Write guarantees.
Option 2: Search the writer node directly
Section titled “Option 2: Search the writer node directly”The writer node makes changes searchable immediately after a commit finishes. If your application can route requests directly to a specific node, you can send searches straight to the writer.
-
Find the writer node by requesting the index status (requires
indexes.read):GET /v1alpha1/admin/indexes/products HTTP/1.1Host: exofind.example.comAuthorization: Bearer <token>The response contains the
indexerobject with the writer node’s address:{"state": "usable","readOnly": false,"indexer": {"node": "node-1","address": "http://node-1.internal:8080"}}Alternatively, list the candidate nodes and which node writes each index using
GET /v1alpha1/admin/indexers. An index that your key covers nothing of is left out of the listing. -
Send your search request directly to the writer’s address (requires
search):POST /v1alpha1/indexes/products/search HTTP/1.1Host: node-1.internal:8080Authorization: Bearer <token>Content-Type: application/json{"query": [{ "type": "text", "text": "laptop" }]}
Trade-off: This provides the lowest latency for read-after-write consistency, but it concentrates search traffic on the writer node and requires direct network access to individual nodes.
Option 3: Explicitly commit and wait out the refresh interval
Section titled “Option 3: Explicitly commit and wait out the refresh interval”If your application cannot pass a token along, trigger an explicit commit and wait for reader nodes to pull the update.
-
Trigger a commit on the index (requires
indexes.commit):POST /v1alpha1/admin/indexes/products/actions/commit HTTP/1.1Host: exofind.example.comAuthorization: Bearer <token>You can send this request to any node behind the load balancer; it is forwarded to the writer.
-
Wait for the duration of
EXOFIND_INDEXES_REFRESH_INTERVAL(default: 30 seconds) before executing search queries against the load balancer.
Trade-off: This works behind standard load balancers without special routing, but your client must tolerate a delay of up to 30 seconds.
Option 4: Force a pull on the answering node
Section titled “Option 4: Force a pull on the answering node”You can instruct a specific node to pull the latest state from remote storage immediately.
-
Ensure the writer has committed the changes, either automatically or through an explicit commit call.
-
Send a pull action directly to the node you plan to search (requires
indexes.pull):POST /v1alpha1/admin/indexes/products/actions/pull HTTP/1.1Host: node-2.internal:8080Authorization: Bearer <token>
Trade-off: A pull runs only on the node that receives the request and is never forwarded. This only works if your client addresses that specific node directly for both the pull and the subsequent search.
Option 5: Design ingestion around eventual consistency
Section titled “Option 5: Design ingestion around eventual consistency”For bulk data loading or asynchronous ingestion pipelines, structure your workflow so that immediate search visibility is not required.
- Stream write requests using
POST /v1alpha1/indexes/{name}/documents(requiresdocuments.write) without committing between batches. - Send a single commit request at the end of the ingestion run.
- Wait for one
EXOFIND_INDEXES_REFRESH_INTERVALbefore directing user traffic to the index.
Trade-off: This yields the highest write throughput and lowest resource overhead, but requires asynchronous application design.
Confirming the result
Section titled “Confirming the result”To verify that an index is ready and check how many documents a node can search:
-
Check the index status on the node (requires
indexes.read):GET /v1alpha1/admin/indexes/products HTTP/1.1Host: exofind.example.comAuthorization: Bearer <token>Check these fields in the response:
state: Must beusable(ormodifiedon a writer with pending local changes). A state ofneeds_pullorpullingindicates the node is synchronizing with storage.readOnly: Showsfalseon the writer node andtrueon reader nodes.indexer: Identifies the node currently responsible for writing the index.
-
Count the searchable documents on the node by running a search with
limitset to0(requiressearch):POST /v1alpha1/indexes/products/search HTTP/1.1Host: exofind.example.comAuthorization: Bearer <token>Content-Type: application/json{"limit": 0,"total": "exact","freshness": { "atLeast": "AQoIcHJvZHVjdHMSATIYBw" }}The
total.countfield in the response shows how many documents the answering node can search. Uncommitted documents are not included in this count. Ask for"total": "exact"as shown: counting defaults to"estimate", which stops once the count exceeds the returned window and reportstotal.exactasfalse, so an estimate cannot tell you whether one particular write has arrived. When you pass the token of the write infreshness.atLeast, the node counts documents only after reaching that state.
What Exofind does not provide
Section titled “What Exofind does not provide”Exofind write visibility has the following operational boundaries:
- No durability receipt: A freshness token indicates when a write is visible, not that it is durable. If the writer node fails before committing, uncommitted writes are lost. See Write guarantees.
- No search forwarding: Reader nodes do not forward searches to the writer node. A reader node waits for the writer commit interval and pulls the change from storage.
- No commit guarantees from index status: An index state of
usableon a reader node indicates that the local index is operational, but does not guarantee that the node holds the latest commit. Thefreshnessproperty in a search response identifies the state the node holds. - No admin access for search keys: An API key with only the
searchpermission cannot call commit, pull, or status endpoints. Passing a freshness token requires no permissions beyondsearch.
Related
Section titled “Related”- Indexing documents - Send documents, load datasets, and commit changes.
- Testing an application against a node - Commit changes before asserting on search results in tests.
- Searching an index - Construct and execute search queries.
- Paginating search results - Pass freshness tokens across paginated requests.
- Running multiple nodes - Configure node roles and writer assignment.
- Configuration reference - Commit, refresh, and wait configuration settings.
- Admin API reference - Index status, lifecycle states, and admin actions.
- Search API reference - Freshness token properties, request formats, and error codes.
- API conventions - Request forwarding and local execution behavior.
- Write guarantees - Acknowledgment semantics, durability, and failover behavior.
- Architecture - Overview of storage as the single source of truth.
- Synchronization - Concurrency control and bounded staleness across nodes.
Exofind is built by Level Four AB and is available under the Apache License 2.0.