All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Status: Complete
README.md— Comprehensive documentation covering install, quick start, query DSL, document CRUD, index management, bulk indexing, scroll/PIT iteration, configuration, error handling, and memory ownership. README serves as the primary docs surface following Zig ecosystem convention.examples/directory — Standalone example project with its ownbuild.zigandbuild.zig.zonproving the library is consumable as a dependency:basic_search.zig— Connect, index SNOMED concepts, search with comptime query DSLbulk_index.zig— Batch-index 500 docs withBulkIndexerauto-flushscroll_large.zig— Page through 200 docs withScrollIteratorandPitIterator
justfile— Task runner with all dev/test commands (just build,just test,just smoke,just integration,just all,just es-start,just bench, etc.)test-allbuild step — Runs unit + smoke + integration tests in one commandexamples-buildCI job — Verifies examples compile on every push- Module tutorial in
root.zig— Expanded//!doc comment with overview, compatibility, quick start examples, query DSL, bulk indexing, scrolling, error handling, and memory ownership sections
build.zig— Cleaned up from 274 lines to 83 lines:- Removed all
zig inittemplate comments - Removed CLI executable (
src/main.zig,runstep,exe_tests) - Consolidated integration tests into a loop
- Added
test-allstep
- Removed all
build.zig.zon— Finalized for v0.1.0:- Version bumped from
0.0.0to0.1.0 - Added
LICENSEandREADME.mdto.paths - Removed all boilerplate template comments
- Version bumped from
release.yml— Converted from cross-compiled binary release to source-only GitHub Release (library consumers usezig fetch, not binaries). Added pre-release test gate running unit + smoke + integration tests.ci.yml— Addedexamples-buildjob to verify examples compile
src/main.zig— CLI executable removed; this is a library, not a CLI tool
- Doc comment audit — all public symbols have
///doc comments - Module tutorial in
root.zigexpanded with full usage guide -
README.mdcreated with comprehensive documentation -
examples/directory with 3 standalone examples + build system -
build.zig.zonfinalized (v0.1.0, paths, clean metadata) -
build.zigcleaned up (removed CLI, addedtest-all, loop for integration tests) -
justfilecreated with all dev/test commands - CI hardened (examples build job, release workflow updated)
- Changelog updated with M8 section
elaztic v0.1.0 prepared for public release. README serves as comprehensive
documentation with progressive quickstart examples, full API reference, and the
comptime field validation story front and center. Standalone examples prove the
library is consumable. zig fetch --save works out of the box. CI validates
formatting, unit tests, smoke tests, integration tests, and example compilation.
162+ unit tests with zero memory leaks.
Status: Complete Test backend: Elasticsearch (OpenSearch) on port 9200
-
src/pool.zig— Major hardening improvements to the connection pool:- Jittered exponential backoff: Replaced deterministic
backoff *= 2with full-jitter usingstd.crypto.random.intRangeAtMost. Backoff capped bymax_retry_backoff_ms(default 30s) to prevent unbounded growth. Prevents thundering herd on 429/503. - Node health recovery: Added
dead_since: ?i64toNodefor tracking when a node was marked unhealthy. When all nodes are unhealthy,nextNode()attempts to resurrect the node dead longest (if pastresurrect_after_ms, default 60s). On successful request,dead_sinceis cleared and the node is marked healthy. - Auth support: Pre-computes
Authorizationheader at init time. Supports HTTP Basic auth (Authorization: Basic <base64>) and API key auth (Authorization: ApiKey <key>). Basic auth takes precedence when both are set. Header is added to every request viaextra_headers. - HTTPS/TLS support: Reads
schemefrom config (default"http"). Uses it when creating the initial node.std.http.Clienthandles TLS natively forhttps://URIs. - Structured logging: Added
LogEventtagged union with 6 variants (request_start,request_success,request_retry,request_error,node_unhealthy,node_recovered). Optionallog_fncallback in config — zero overhead when null. Request timing tracked viaduration_msin success events. - 429 vs 5xx differentiation: Returns
error.TooManyRequestsfor 429,error.ServerErrorfor 5xx. - 10 unit tests (6 new + 4 updated): node resurrection, jitter bounds, auth headers, scheme config, log emission.
- Jittered exponential backoff: Replaced deterministic
-
src/client.zig— New config fields and convenience methods:ClientConfigextended with:api_key,scheme,max_retry_backoff_ms,resurrect_after_ms,log_fn.ESClient.initFromUrl(allocator, url)— parseshttp://host:portorhttps://host:portinto ClientConfig. Dupes host/scheme strings so they outlive the input URL.- Fixed pre-existing use-after-free in
ping():std.json.Value.jsonParsehardcodes.alloc_always, so all strings lived in the parsed arena and became dangling pointers afterparsed.deinit(). Now copiescluster_nameandstatusstrings into the client allocator. ClusterHealthnow owns its string copies (_name_copy,_status_copy) instead of referencing the raw response body.
-
src/root.zig— Re-exportsLogEventandLogLevel. -
tests/integration/hardening_integration.zig— M7 integration tests (5 tests):integration_basic_auth— client withbasic_auth, ping + create/delete index + count.integration_node_failover— adds fake dead node (192.0.2.1:19200), verifies operations succeed via healthy node.integration_logging_events— verifiesrequest_startandrequest_successevents are emitted during ping and createIndex.integration_retry_on_failure— exercises retry machinery with bulk index, search, count.integration_scheme_from_config— explicitscheme="http"+initFromUrlvalidation.
-
build.zig— Addedhardening_integration.zigtotest-integrationstep.
- Jittered exponential backoff with
std.crypto.random, capped bymax_retry_backoff_ms - Node health recovery:
dead_sincetimestamp,resurrect_after_ms, oldest-dead-first strategy - HTTP Basic auth:
basic_authconfig →Authorization: Basic <base64>header - API key auth:
api_keyconfig →Authorization: ApiKey <key>header - Basic auth takes precedence over API key when both set
- HTTPS/TLS:
schemeconfig field,std.http.Clienthandles TLS natively -
ESClient.initFromUrl— parses URL, dupes host/scheme for correct lifetime -
LogEventtagged union with 6 event variants -
LogLevelenum (debug,info,warn,err) -
log_fncallback in config — zero overhead when null - Request timing (
duration_ms) in success events - 429 →
TooManyRequests, 5xx →ServerErrordifferentiation - Fixed
ping()use-after-free: copies strings out of parsed arena -
ClusterHealthowns its strings via_name_copy/_status_copy - Unit tests: 10 tests (node resurrection, jitter bounds, auth, scheme, logging)
- Integration tests: 5 tests (auth, failover, logging, retry, scheme)
-
build.zig—hardening_integration.zigintest-integrationstep - Memory safety: zero leaks in all tests (GPA-verified)
Production-ready transport layer with jittered backoff preventing thundering herd on 429/503,
automatic node health recovery with oldest-dead-first resurrection, HTTP Basic and API key
authentication, HTTPS support via std.http.Client's native TLS, and structured logging hooks
for observability. Pre-existing use-after-free in ping() discovered and fixed during memory
audit. 163 unit tests + 38 integration tests pass with zero memory leaks.
Status: Complete Test backend: Elasticsearch (OpenSearch) on port 9200
-
src/api/scroll.zig— Scroll API types and iterator for paging through large result sets.ScrollSearchRequest— initial scroll search (POST /<index>/_search?scroll=<duration>). Builds the same JSON body asSearchRequest(query, size, from,_source, aggs).ScrollNextRequest— fetch next page (POST /_search/scroll). Body:{"scroll":"<duration>","scroll_id":"<id>"}.ClearScrollRequest— clear scroll context (DELETE /_search/scroll). Body:{"scroll_id":"<id>"}.ScrollSearchResponse(T)— generic response with_scroll_id,hits: HitsEnvelope(T),took.ScrollIterator(T)— pages through results using a two-page memory strategy:init()sends the initial scroll search and parses the first page.next()returns the current page's hits (valid until next call), then prefetches the next page.deinit()sendsDELETE /_search/scroll(errors silently ignored) and frees all memory.- Uses
previous_page/current_pageto keep returned hits alive until the nextnext()call. scroll_idis an owned copy — survives parsed response arena frees.
- 14 unit tests: request method/path/body serialization, response deserialization.
-
src/api/pit.zig— Point-in-Time (PIT) API types and iterator.PitOpenRequest— open a PIT (POST /<index>/_search/point_in_time?keep_alive=<duration>), no body.PitOpenResponse— parses{"pit_id": "..."}from the open response.PitCloseRequest— close a PIT (DELETE /_search/point_in_time). Body:{"pit_id":"<id>"}.PitSearchRequest— search with PIT context (POST /_search, no index in path). Body includespit,query,size,sort,search_after. Defaults to[{"_doc":"asc"}]sort when none specified.SortField— sort specification withfieldandorderstrings.PitHit(T)— likeHit(T)but with an additionalsort: ?[]const std.json.Valuefield forsearch_afterpagination.PitHitsEnvelope(T)— hits envelope withtotal,hits,max_score.PitSearchResponse(T)— response withpit_id,hits,took.PitIterator(T)— pages through results using PIT +search_after:init()opens a PIT, sends the initial search, stores the first page.next()returns the current page's hits and prefetches the next page; returnsnullwhen exhausted.deinit()frees all memory and sends a best-effortDELETEto close the server-side PIT.- PIT ID is updated if the server returns a refreshed one.
- Only one page of hits is live at a time (previous page freed on next
next()call).
- 14 unit tests: request method/path/body serialization, response deserialization.
-
src/pool.zig— AddedsendBodyForMethodWithoutBodyhelper for DELETE-with-body. Zig 0.15'sstd.http.Clienthard-asserts insendBodyUnflushedthat the HTTP method supports a body (requestHasBody()returnsfalsefor DELETE). Elasticsearch legitimately requires DELETE with a JSON body for clear-scroll, close-PIT, and delete-by-query. The helper replicates the essential parts of the privatesendHeadfunction, writing the request line, headers (Host, Connection, Accept-Encoding, Content-Length, extra headers), body, and flushing directly to the connection's buffered writer. After this call the connection is in the correct state forreceiveHead. -
src/client.zig— Added 4 convenience methods onESClient:scrollSearch(comptime T, index, query, opts, scroll_duration)→ScrollIterator(T)— creates and initializes a scroll iterator.openPit(index, keep_alive)→[]u8— opens a PIT, returns thepit_idas an owned string.closePit(pit_id)→void— closes a PIT via DELETE.pitSearch(comptime T, index, query, page_size, keep_alive)→PitIterator(T)— creates and initializes a PIT iterator.
-
src/request.zig— Replaced 4 placeholder structs with real types:ScrollRequest→scroll_api.ScrollSearchRequestClearScrollRequest→scroll_api.ClearScrollRequestPitOpenRequest→pit_api.PitOpenRequestPitCloseRequest→pit_api.PitCloseRequest
-
src/root.zig— Re-exports for all new public types:ScrollSearchRequest,ScrollNextRequest,ClearScrollRequest,ScrollSearchResponse,ScrollIterator,PitOpenRequest,PitOpenResponse,PitCloseRequest,PitSearchRequest,SortField,PitSearchResponse,PitHit,PitIterator. -
tests/integration/scroll_pit_integration.zig— M6 integration tests (11 tests):integration_scroll_basic— 25 docs, page size 10, expects 3 pages totaling 25 hits.integration_scroll_with_query— 20 docs with alternating active, term query filters to 10.integration_scroll_empty_result— empty index,next()returns null immediately.integration_scroll_single_page— 5 docs, page size 10, all fit in one page.integration_scroll_large_dataset— 500 docs, page size 50, expects 10 pages.integration_scroll_auto_clear— partial consumption thendeinit()clears scroll context.integration_pit_open_close— explicit open/close PIT lifecycle.integration_pit_basic— 25 docs, page size 10, collects all via PIT iterator.integration_pit_with_query— 20 docs with alternating active, term query filters to 10.integration_pit_empty_result— empty index,next()returns null immediately.integration_pit_auto_close— partial consumption thendeinit()closes PIT.- All tests use
ESClientdirectly viaelazticmodule.
-
build.zig— Addedscroll_pit_integration.zigtotest-integrationstep. -
CLAUDE.md— Documented thestd.http.ClientDELETE-with-body limitation in §3a and enriched M6 section with detailed phase-by-phase checklist.
-
ScrollSearchRequestwithhttpMethod(),httpPath(),httpBody()— initial scroll search -
ScrollNextRequest— fetch next page with scroll_id -
ClearScrollRequest— clear scroll context (DELETE /_search/scrollwith body) -
ScrollSearchResponse(T)— response with_scroll_idfield -
ScrollIterator(T)— two-page memory strategy, auto-clear ondeinit() -
ScrollIteratorscroll_id is an owned copy — survives parsed response arena frees -
PitOpenRequest— open PIT (OpenSearch-compatible/_search/point_in_timepath) -
PitOpenResponse— parsepit_idfrom response -
PitCloseRequest— close PIT (DELETE /_search/point_in_timewith body) -
PitSearchRequest— search with PIT context,search_after,sort -
PitSearchResponse(T)— response with refreshedpit_id -
PitHit(T)— hit withsortvalues forsearch_afterpagination -
PitIterator(T)— PIT + search_after pagination, auto-close ondeinit() -
SortFieldstruct — sort specification with field and order - Default sort
[{"_doc":"asc"}]for efficient full-index scans -
sendBodyForMethodWithoutBodyin pool.zig — DELETE-with-body workaround for Zig 0.15 -
ESClient.scrollSearch()— convenience method -
ESClient.openPit()/closePit()— explicit PIT lifecycle -
ESClient.pitSearch()— convenience method -
request.zig— replaced 4 M6 placeholder structs with real types -
root.zig— re-exports for all scroll/PIT public types - Unit tests: 28 tests (14 scroll + 14 PIT) for request/response serialization
- Integration tests: 11 tests (6 scroll + 5 PIT) against OpenSearch
-
build.zig—scroll_pit_integration.zigintest-integrationstep - Memory safety: zero leaks in all tests (GPA-verified)
ScrollIterator and PitIterator page through arbitrarily large result sets without
buffering more than one page in memory. Auto-clear/close on deinit() prevents leaked
server-side resources. Both iterators use the same document type T from SearchResponse.
Integration tests verify pagination (25 docs across 3 pages, 500 docs across 10 pages),
query filtering, empty results, single-page results, and resource cleanup against OpenSearch.
The DELETE-with-body workaround in pool.zig enables clear-scroll and close-PIT operations
despite Zig 0.15's std.http.Client limitation. 153 unit tests + 33 integration tests pass.
Status: Complete Test backend: Elasticsearch (OpenSearch) on port 9200
-
src/api/bulk_indexer.zig— Bulk indexer for batching documents and flushing to_bulk.BulkConfigstruct —max_docs(default 1000),max_bytes(default 5MB),flush_interval_ms(reserved).BulkIndexerstruct — accumulates NDJSON in a singleArrayList(u8)buffer, no per-doc allocation.BulkIndexer.init(allocator, *ConnectionPool, compression, BulkConfig)— creates indexer.BulkIndexer.deinit()— frees buffer (does NOT auto-flush; caller must flush first).BulkIndexer.add(comptime T, index, ?id, doc)— serialize doc to JSON, append NDJSON lines. Returns?BulkResultif auto-flush was triggered.BulkIndexer.addRaw(index, ?id, json_bytes)— append pre-serialized JSON (no double-serialize).BulkIndexer.addDelete(index, id)— append delete action (no source line).BulkIndexer.flush()— send buffered NDJSON toPOST /_bulk, parseBulkResponse, reset buffer.BulkIndexer.pendingCount()/pendingBytes()— inspect buffered state.- Auto-flush: triggers when
max_docsormax_bytesthreshold is exceeded. - Action line format:
{"index":{"_index":"<idx>","_id":"<id>"}}\n(or{"delete":...}). - NDJSON ends with trailing newline (ES requirement).
- 10 unit tests: init/deinit, NDJSON format, pending counts, add without ID, mixed actions, empty flush, trailing newline, typed doc serialization, BulkResult.hasFailures.
-
BulkResult— Result of a bulk flush operation.total,succeeded,failed,took_ms— summary counters.items: []BulkItemResult— per-action results for inspection.hasFailures()→bool.deinit()— frees underlyingBulkResponsearena.
-
src/client.zig— AddedESClient.bulkIndexer(BulkConfig)convenience method. Returns aBulkIndexerbound to the client's connection pool. -
src/pool.zig— Fixed missingContent-Type: application/jsonheader. Now automatically sets the header when a request body is present. This fixesESClientconvenience methods (createIndex, putMapping, etc.) which were failing against OpenSearch without the header. -
src/root.zig— Re-exportsBulkIndexer,BulkConfig,BulkResult. -
tests/integration/bulk_integration.zig— M5 integration tests (6 tests):integration_bulk_index_basic— bulk index 10 docs, flush, verify all succeeded, count confirms 10.integration_bulk_auto_flush—max_docs=5, add 7 docs, auto-flush at 5, manual flush for 2, count 7.integration_bulk_mixed_actions— bulk insert 3 + bulk delete 1 + add 1, count stays 3.integration_bulk_large_batch— index 1000 docs in one flush, verify all succeeded.integration_bulk_byte_threshold—max_bytes=500, auto-flush triggers on buffer size.integration_bulk_empty_flush— flush with nothing pending, zero result, no error.- All tests use
ESClientdirectly viaelazticmodule.
-
bench/bulk_bench.zig— Throughput benchmark harness.- Indexes 50,000 docs with batch size 1000 against localhost OpenSearch.
- Reports wall-clock time, ES server time, throughput (docs/sec), avg latency per flush.
- Verifies final document count via
client.count(). - Debug mode: ~17K docs/sec. ReleaseSafe: ~40K docs/sec.
- Run with
zig build benchorzig build bench -Doptimize=ReleaseSafe.
-
build.zig— Addedbulk_integration.zigtotest-integrationstep. Addedbenchbuild step for the throughput benchmark executable.
-
BulkConfigstruct withmax_docs,max_bytes,flush_interval_msthresholds -
BulkIndexerwith singleArrayList(u8)buffer — no per-doc allocation -
BulkIndexer.add()— serialize typed doc, append NDJSON action+source lines -
BulkIndexer.addRaw()— append pre-serialized JSON -
BulkIndexer.addDelete()— delete action (no source line) - Auto-flush on
max_docsormax_bytesthreshold -
BulkIndexer.flush()— POST/_bulk, parseBulkResponse, returnBulkResult -
BulkIndexer.pendingCount()/pendingBytes()— inspect state - NDJSON format: action line + source line + trailing newline
-
BulkResultwithtotal,succeeded,failed,took_ms,items,hasFailures(),deinit() -
ESClient.bulkIndexer(config)— convenience method -
Content-Type: application/jsonheader fix inpool.zig - Per-action failure parsing via existing
parseBulkResponse - Unit tests: NDJSON format, pending counts, empty flush, mixed actions
- Integration tests: 6 tests covering all flush modes against OpenSearch
- Benchmark: 50K docs, ~40K docs/sec in ReleaseSafe
-
build.zig—benchstep andbulk_integration.zigintest-integration
BulkIndexer handles batching, NDJSON serialization, auto-flush on doc count and byte
size thresholds, and per-action failure reporting. Throughput benchmark achieves ~40K
docs/sec in ReleaseSafe against localhost OpenSearch. Integration tests verify all
flush modes — basic, auto-flush, mixed actions, large batch, byte threshold, empty flush.
Status: Complete Test backend: Elasticsearch (OpenSearch) on port 9200
-
src/api/index_mgmt.zig— Index management request types.CreateIndexRequest— index name, optionalIndexSettings(shards, replicas), optional mappings JSON.DeleteIndexRequest— index name.RefreshRequest— index name (or_all).PutMappingRequest— index name + mapping body (JSON[]u8).PutAliasRequest— index name + alias name.- Each type has uniform
httpMethod(),httpPath(allocator),httpBody(allocator)interface. - 21 unit tests: correct HTTP method, path, and body for each request type, including settings-only, mappings-only, both, and neither for CreateIndexRequest.
-
src/api/document.zig— Document CRUD request/response types.IndexDocRequest— index name, optional doc ID, serialized document body. PUT with explicit ID, POST without (auto-generated).GetDocRequest— index name + doc ID.DeleteDocRequest— index name + doc ID.GetDocResponse(T)— generic over_sourcedocument type, with_index,_id,_version,found.IndexDocResponse—_index,_id,_version,result("created"/"updated").DeleteDocResponse—_index,_id,_version,result("deleted"/"not_found").IndexDocOptions— optional doc ID for indexing.- 15 unit tests: method/path generation, body duplication, response deserialization from JSON fixtures (including SNOMED u64 concept IDs).
-
src/api/search.zig— Search and count request/response types.SearchRequest— index name/pattern, optionalQuery,SearchOptions(size, from, source filter, aggs).SearchRequest.httpBody(allocator)→ full{"query": {...}, "size": N, ...}body.CountRequest— index name/pattern, optional query.CountResponse—count: u64, optionalShardsInfo.- 12 unit tests: path, method, body with all combinations of options, count body with/without query, CountResponse deserialization.
-
src/client.zig— ESClient convenience methods (10 new public methods).createIndex(index, opts)→ void (or error).deleteIndex(index)→ void (or error).refresh(index)→ void.putMapping(index, mapping_body)→ void.putAlias(index, alias)→ void.indexDoc(comptime T, index, doc, opts)→IndexDocResponse.getDoc(comptime T, index, id)→Parsed(GetDocResponse(T)).deleteDoc(index, id)→DeleteDocResponse.searchDocs(comptime T, index, query, opts)→Parsed(SearchResponse(T)).count(index, query)→u64.- 3 private generic dispatch helpers:
executeSimple,executeTyped,executeTypedParsed. handleErrorResponse— parsesErrorEnvelope→ESError, proper body ownership handling.
-
src/request.zig— Replaced placeholder structs with real API types.ElasticRequestvariants now carry actual request data fromapi/modules.- Added
index_docandput_aliasvariants. - Kept placeholders for M5/M6 types (BulkRequest, ScrollRequest, PitOpenRequest, etc.).
-
src/root.zig— Re-exports all new API types:CreateIndexRequest,DeleteIndexRequest,RefreshRequest,PutMappingRequest,PutAliasRequest,IndexSettings,IndexDocRequest,GetDocRequest,DeleteDocRequest,IndexDocResponse,DeleteDocResponse,GetDocResponse,IndexDocOptions,SearchRequest,SearchOptions,CountRequest,CountResponse. -
tests/integration/api_integration.zig— M4 integration tests (10 tests):integration_create_delete_index— create with settings, verify exists, delete, verify gone.integration_index_get_doc— index a Concept doc, GET by ID, verify all fields.integration_delete_doc— index a doc, delete it, verify 404 on re-GET.integration_search_with_query— index 3 docs, term query, verify 2 hits.integration_count— count all (3) and filtered (2) via_countendpoint.integration_refresh— verify doc not searchable before refresh, searchable after.integration_put_mapping— add new keyword field, verify accepted.integration_put_alias— create alias, search via alias, verify hits.integration_index_without_id— POST without ID, verify auto-generated_id.integration_error_index_not_found— search nonexistent index, verify 404.- Each test creates UUID-named index, performs operation, asserts, cleans up.
-
build.zig— Addedapi_integration.zigto thetest-integrationstep.
-
CreateIndexRequestwith optional settings and mappings (src/api/index_mgmt.zig) -
DeleteIndexRequest— index name (src/api/index_mgmt.zig) -
RefreshRequest— index name or_all(src/api/index_mgmt.zig) -
PutMappingRequest— index name + mapping body (src/api/index_mgmt.zig) -
PutAliasRequest— index name + alias name (src/api/index_mgmt.zig) -
IndexDocRequest— PUT with ID / POST without, document body (src/api/document.zig) -
GetDocRequest/GetDocResponse(T)— typed document retrieval (src/api/document.zig) -
DeleteDocRequest/DeleteDocResponse(src/api/document.zig) -
IndexDocResponse— created/updated result (src/api/document.zig) -
SearchRequestwith query, size, from, source filter, aggs (src/api/search.zig) -
CountRequest/CountResponse(src/api/search.zig) -
ESClient.createIndex,deleteIndex,refresh,putMapping,putAlias -
ESClient.indexDoc,getDoc,deleteDoc,searchDocs,count -
executeSimple,executeTyped,executeTypedParsedgeneric dispatch helpers -
handleErrorResponse— ErrorEnvelope → ESError mapping -
ElasticRequesttagged union uses real request types fromapi/modules - Unit tests: HTTP method, path, body for all request types; response deserialization
- Integration tests: 10 tests covering all CRUD operations against OpenSearch
-
build.zig—api_integration.zigadded totest-integrationstep
Complete CRUD surface. All operations available through ESClient convenience methods
or via typed request structs. Error responses parsed into ESError via ErrorEnvelope.
10 integration tests verify every operation against OpenSearch — index lifecycle, document
CRUD, search, count, refresh, mappings, aliases, auto-ID, and error handling.
Status: Complete Test backend: Elasticsearch (OpenSearch) on port 9200
-
src/query/field.zig— Comptime-validated field path accessor.FieldPathstruct holding the dotted field name string for ES JSON serialization.field(comptime T, comptime name)— validates field exists via@hasField, returnsFieldPath.- Nested path support:
field(Outer, "inner.value")splits on.and walks struct types at comptime. - Optional unwrapping: paths walk through
?Tfields automatically. @compileErrorwith human-readable messages on invalid fields, empty paths, leading/trailing dots.- Unit tests: simple fields, nested paths, nested through optionals, name preservation.
-
src/query/builder.zig— Comptime query DSL builder for Elasticsearch.Querytagged union with variants for all query types, composable viastd.json.Valuetrees.Query.term(field_name, value)→{"term": {"field": value}}— supports bool, u64, i64, f64, string.Query.terms(field_name, values)→{"terms": {"field": [...]}}— handles large[]u64slices.Query.match(field_name, text)→{"match": {"field": text}}.Query.matchAll()→{"match_all": {}}.Query.boolQuery(opts)with.must,.filter,.should,.must_not(each?[]const Query).Query.range(field_name)returnsRangeBuilderwith chainable.gt(),.gte(),.lt(),.lte(),.build().Query.exists(field_name)→{"exists": {"field": "name"}}.Query.prefix(field_name, value)→{"prefix": {"field": value}}.Query.ids(values)→{"ids": {"values": [...]}}.Query.nested(path, query)→{"nested": {"path": "...", "query": {...}}}.Query.wildcard(field_name, pattern)→{"wildcard": {"field": pattern}}.toJsonValue(allocator)→std.json.Valuetree for composability.toJson(allocator)→ caller-owned[]u8(arena internally for value tree).- Comptime value coercion:
toTermValue,toTermsValues,toRangeValuehandle comptime_int, signed/unsigned ints, floats, and strings via@typeInfo. - 20 unit tests covering every query type, edge cases (negative i64, all range bounds, deeply nested bool, all bool clause types).
-
src/query/aggregation.zig— Aggregation DSL for Elasticsearch.Aggregationstruct withname,agg_type(tagged union), and optionalsub_aggs.Aggregation.termsAgg(name, field_name, size)→{"name": {"terms": {"field": "...", "size": N}}}.Aggregation.valueCount(name, field_name)→{"name": {"value_count": {"field": "..."}}}.Aggregation.topHits(name, size)→{"name": {"top_hits": {"size": N}}}.withSubAggs(sub_aggs)for nesting aggregations (e.g. terms → top_hits).aggsToJsonValue(aggs, allocator)→ full"aggs"object value.aggsToJson(aggs, allocator)→ caller-owned[]u8.- 6 unit tests: each agg type, sub-aggregation nesting, multiple aggs, full round-trip.
-
src/query/source_filter.zig— Source filtering for ES search requests.SourceFiltertagged union with three modes:.disabled→"_source": false.includes→"_source": ["field1", "field2"].full→"_source": {"includes": [...], "excludes": [...]}
toJsonValue(allocator)andtoJson(allocator)serialization methods.- 6 unit tests: disabled, includes, empty includes, full form, empty full form, disabled-no-allocate.
-
src/root.zig— Re-exports query DSL underpub const query:Query,BoolOpts,RangeBuilder,TermValue,TermsValues,RangeValue,FieldPath,field,Aggregation,SourceFilter. -
tests/integration/query_integration.zig— M3 integration tests (6 tests):integration_term_query— index 3 docs, term query onactive=true, assert 2 hits.integration_terms_query— terms query with[]u64concept IDs, assert 2 hits.integration_bool_query— must + filter combination, verify correct count.integration_range_query— range onmodule_idwith.gte(), verify boundaries.integration_match_all— matchAll query, assert all 3 docs returned.integration_exists_query— filter docs with/without optional field, assert count.- Each test creates UUID-named index, indexes docs, refreshes, queries, asserts, deletes index.
- Tests skip automatically if
ES_URLis not set.
-
build.zig— Addedtest-integrationbuild step wiring up integration test files.
-
FieldPathstruct with comptime field name validation (src/query/field.zig) -
field(T, name)with@hasFieldvalidation and@compileErrormessages - Nested path support: splits on
.and walks struct types, unwraps optionals -
Query.term— bool, u64, i64, f64, string values -
Query.terms— large[]u64slice support -
Query.match— full-text match -
Query.matchAll— match all documents -
Query.boolQuery— must, filter, should, must_not -
Query.range— chainable gt/gte/lt/lte viaRangeBuilder -
Query.exists— field existence check -
Query.prefix— prefix matching -
Query.ids— document ID filtering -
Query.nested— nested object queries -
Query.wildcard— wildcard pattern matching - All queries serialize to
std.json.Valuefor composability -
Aggregation.termsAgg,valueCount,topHitswith sub-aggregation nesting -
SourceFilter— disabled, includes, full include/exclude modes - Unit tests per query type: construct → serialize → diff against expected JSON
- Integration tests against ES: 6 tests, each with index lifecycle
-
build.zig—test-integrationstep added
Full query DSL with compile-time field validation. All query types (term, terms, match, matchAll, bool, range, exists, prefix, ids, nested, wildcard) serialize to correct ES JSON. Aggregations (terms, value_count, top_hits) with sub-agg nesting. Source filtering in all three ES modes. Integration tests pass against OpenSearch.
Status: Complete Test backend: Elasticsearch (OpenSearch) on port 9200
-
src/json/serialize.zig— Comptime JSON serializer for Elasticsearch, built onstd.json.toJson(allocator, value)— serialize any struct to caller-owned[]u8.toJsonWithOptions(allocator, value, options)— same with explicitSerializeOptions.toJsonWriter(writer, value)— stream directly into a writer (zero heap allocation).SerializeOptionswithprettyandomit_null_optional_fieldsknobs.- Null optional fields omitted by default (ES treats missing and
nulldifferently). u64values (SNOMED concept IDs) serialize as JSON numbers.- Enums serialize as lowercase name strings.
- Unit tests: all field types, null omission, nested structs,
[]u64slices, enum fields, round-trip verification, writer parity, pretty-print, and explicit-null mode.
-
src/json/deserialize.zig— Comptime JSON deserializer for Elasticsearch responses.fromJson(T, allocator, json_bytes)— returnsstd.json.Parsed(T)with arena ownership.fromJsonLeaky(T, allocator, json_bytes)— allocates into caller-provided allocator (arena-friendly).- Pre-configured with
ignore_unknown_fields = trueandallocate = .alloc_always. SearchResponse(T)— generic ES_searchresponse envelope over document typeT.HitsEnvelope(T)— thehitsobject withtotal,max_score, andhitsarray.Hit(T)— a single hit with_index,_id,_score,_source.TotalHits—totalobject withvalueandrelation.- Unit tests: simple struct, unknown fields ignored, optional fields (present/absent/null),
nested structs,
[]u64arrays, realistic ES search response, malformed JSON, arena allocation, and enum parsing.
-
src/api/bulk.zig— Bulk API response types and parsing.BulkResponse— parsed response withtook,errors,items,failureCount(),successCount(), and arena-baseddeinit().BulkItemResult— per-action result withaction,index,id,version,result,status,error_type,error_reason, andisSuccess().parseBulkResponse(allocator, body)— parse raw JSON into typedBulkResponse.- Uses internal arena allocator for string ownership (not raw body slices).
- Unit tests: all-success, mixed success/failure, counter helpers,
isSuccess()boundaries, error detail extraction, malformed JSON (8 sub-cases), and empty items.
-
src/error.zig— AddedErrorEnvelopeandparseErrorEnvelope.ErrorEnvelopestruct withstatus,error_type,reason,index, and arena-baseddeinit().parseErrorEnvelope(allocator, body)— parse ES error JSON into typed envelope.toESError()— map ES exception type strings toESErrorvalues:index_not_found_exception→IndexNotFound,document_missing_exception→DocumentNotFound,version_conflict_engine_exception→VersionConflict,mapper_parsing_exception/illegal_argument_exception→MappingConflict, else →UnexpectedResponse.- Uses arena allocator for string ownership (fixes use-after-free with
std.json.alloc_always). - Unit tests: index_not_found, version_conflict, all known mappings, and malformed JSON (5 sub-cases).
-
src/root.zig— Re-exportsserialize,deserialize,BulkResponse,BulkItemResult, andparseBulkResponse. -
tests/smoke/smoke_roundtrip.zig— M2 smoke tests:smoke_serialize_deserialize_roundtrip— serialize aConceptstruct, deserialize back, verify all fields including optionals (no network).smoke_search_response_parse— parse a realistic ES_searchresponse JSON intoSearchResponse(Concept), verify envelope and hit fields (no network).smoke_es_index_roundtrip— full end-to-end: serialize a doc withelaztic.serialize, PUT it to Elasticsearch viastd.http.Client, search for it, deserialize theSearchResponsewithelaztic.deserialize, verify field-level equality, and clean up.
-
build.zig— Addedsmoke_roundtrip.zigto thetest-smokebuild step.
- Comptime struct serializer → ES JSON (
src/json/serialize.zig) - Comptime JSON deserializer → typed structs (
src/json/deserialize.zig) -
SearchResponse(T)— generic over_sourcedocument type -
BulkResponse— parse per-action results -
ErrorEnvelope— parse ES error JSON - Unit tests with captured response fixtures (no network)
- Smoke test: round-trip a struct through Elasticsearch index → search
Typed JSON round-trip works: Zig structs serialize to ES-compatible JSON and ES responses (search, bulk, error) deserialize into typed Zig structs. Full end-to-end verified against Elasticsearch (OpenSearch).
Status: Complete Test backend: Elasticsearch (OpenSearch) on port 9200
-
src/client.zig—ESClientstruct withinit,deinit,ping,rawRequest, andaddNode.ClientConfigwith host, port, retry, backoff, compression, and basic auth options.ClusterHealthresponse struct with JSON parsing from/_cluster/health.rawRequestescape hatch for arbitrary HTTP methods and paths.
-
src/pool.zig—ConnectionPoolwith round-robin node selection and health tracking.- Uses
std.http.Clientunder the hood for proper HTTP/1.1 keep-alive and gzip. Nodestruct with scheme, host, port, healthy flag, and last-seen timestamp.sendRequestwith retry logic and exponential backoff (doubles each attempt).- Retries on 429 (Too Many Requests) and 5xx (server errors).
- Nodes marked unhealthy on connection failures; re-marked healthy on success.
- Unit tests for round-robin selection and unhealthy node skipping.
- Uses
-
src/error.zig—ESErrorerror set covering all Elasticsearch error conditions.shouldRetryhelper for classifying retryable errors.
-
src/request.zig—ElasticRequesttagged union with placeholder variants for all future operations (search, bulk, get, delete, scroll, PIT, etc.). -
src/root.zig— Public API surface re-exportingESClient,ClientConfig,ClusterHealth,ElasticRequest,ESError,ConnectionPool,HttpResponse, andNode. -
tests/smoke/smoke_ping.zig— Smoke tests against Elasticsearch (OpenSearch):smoke_ping_healthz— verifies/_cluster/healthreturns a valid response.smoke_raw_request_root— verifies a raw GET returns 200.smoke_connection_pool_reuse— issues 5 sequential requests to verify keep-alive reuse.
-
build.zig— Addedtest-smokebuild step that compiles and runs smoke tests with theelazticmodule imported.
-
ConnectionPool— persistent HTTP connections, keep-alive, round-robin node selection - HTTP/1.1 request serializer and response parser (via
std.http.Client) - gzip body compression (
std.http.Clientnegotiates Accept-Encoding automatically) - Retry logic with exponential backoff (configurable count and initial delay)
- Smoke test:
client.rawRequest("GET", "/", null)against Elasticsearch (OpenSearch) on port 9200
ESClient connects to Elasticsearch (OpenSearch), issues HTTP requests, receives and parses JSON
responses, retries on transient failures, and reuses connections via keep-alive.
- Nix flake (
flake.nix) with stable, nightly, and CI dev shells. - Elasticsearch helper scripts:
es-start,es-stop,es-status,es-logs. build.zigandbuild.zig.zonfor Zig 0.15.2.CLAUDE.mdproject documentation with architecture, milestones, and conventions.