Skip to content

Commit 6f81d6c

Browse files
Prototype of Elasticsearch-based full text search solution (#1528)
* Renamed existing natural language service stuff ElasticSearch first steps: - followed footsepts of natural language search components - makefile: ElasticSearch start, stop, and clean actions - achieved connectivity to started ElasticSearch cluster - achieved connectivity to self-launched child container service - test GQL endpoint, routing cluster health info for now - indexer: scaffold, no real indexing yet - search service API: scaffold, no real implementation yet Full text search: - support framework for entity schemas registration (no versioning/migrations yet) - ES: first client calls for index check, registration, documents counting - feeding schemas for accounts and datasets from corresponding domains (very simplified fields now) - entire process happens via plugin style, partiular domains register provider components that are accessed by template shared process ElasticSearch code structure extended: - separated low level operations in `ElasticSearchClient`: deals with engine connection building, sending queries in the right format, interpreting responses, and dealing with API errors - `ElasticSearchIndexMappings` handles creation of index mappings for the given schema + hashing it's content, this will be a future place to apply complex column properties depending on configs - `ElasticSearchVersionedEntityIndex` manages indexes for entities and aliases, auto-registers indcies, validates schema metadata, automatically detects drifts without version modification, automatically applies breaking or reindsable upgrades - main repository code stays at very high level Basic shape of full ElasticSearch index re-indexing + sketched simplest indexing procedure for Datasets Indexing owner-id in datasets index (for filters) ElasticSearch indexing added for Accounts Indexing creation time for Accounts/Datasets Indexing dataset documents similarly to natural language seach: added schema fields, description, keywords, and attachments More realstic field roles: hierarchical identifiers (account name, dataset name, alias, schema field), prose (description, attachments), keywords (owner_id, keyword, dataset_kind) - with corresponding analyzers and properties for ElasticSearch Added "Title" field role, which is in between Prose and Identifier, using for account's display names for now. Identifier fields get inner-ngrams (3..6) and wider edge-ngrams (2..10). Account life cycle events update ElasticSearch index: - massaged events format a bit to satisfy new needs - new outbox event handler for account search index updates - reorganized account schema code to encapsulate 1 document operations, while indexer and update handler use it's helpers - issuing bulk insert, update, delete operations in ES for account events Dummy implementation in e2e tests (until a better solution is found, as containerized ES starts for over 20s per each command, and that's affected by acocunt/dataset lifecycle events) Implemented updates to ElasticSearch for dataset-related events: lifecycle, reference update, parent account rename/delete. Fixed account deletion handler in datasets domain, no ReBAC/dangling checks should be executed during system event handling. Datasets schema: better incremental re-indexings for partial updates Hotfix: improved detection of invalid intervals in case of breaking changes in the dataset, when expected tail is ahead of head First sketching of a search function: - simplest querying: query_string vs match_all, depending if non-empty query was received - support specifying list of indexes vs defaulting to all schemas - ES: sending search request, decoding response - trivial GQL endpoint support Naive pagination support (size/from). Requesting source fields in multiple modes: None, All, Particular, Complex (include+exclude patterns) Next ElasticSearch steps: - Search schemas constants moved and published by domains, so that GQL can reference those fields. - Support flexible sorting of search results: N criterias, by field or relevance score, configurable direction. - Each schema now provides a field that can be used for universal alphabetial sorting ("title" alias) Support basic search filters (keyword = value, keyword in {values}) and compound from those (and, or, not). Added convenience macros to specify compound filters and for sort specifications. Search highlights for textual fields: displays best fragments explaining why certain document's field matched the query On-demand "explain" option: outputs low-level ElasticSearch scoring computation formula * ES: support unprocessed objects field (stored, but not indexed or searched) * Supporting boolean fields + added a generic banning feature for ES indices (filter is auto-attached to "read alias"). Fix: search should always be directed to "read alias", never to "writable index". * Merge corrections * Corrections in ES client: use single bulk update operation with encoded comands, so that heterogenous ops are possible in one bulk. Simplified search context: we don't need account for now. * Lock correction * Backported ElasticSearch-focused changes from Molecule branch * Deps correction * Simplifying renames * Unified account/dataset schemas to the style in Molecule branch * Prototyped framework for integration tests with ElasticSearch involved: - EsTestContext: main facility, lazily initializes reusable ES client - a test proc-macro hiding the plumbing of the context - each test receives a dill::Catalog prefilled with ES client, ES repository impl, with unique randomly generated index prefix - a succesful test automatically cleans it's own indices, while a failing test keeps the indices available for inspection - on first ES client initialization, the potentially abandoned test indices from previous sessions are discarded automatically - written first couple tests for Accounts indexing * Correct spelling of "Elasticsearch" brand name * ElasticSearch test group on CI/CD * More account indexing tests * Initial test suite for datasets indexing. Hardening es_client against async waiting issues: created index must be reachable, assigned alias must be reachable. Makefile: automated cleaning of abandoned test artifacts from previous experiments. Not doing this in fixtures, as `cargo nextest run` creates races around it executing every test in separate process. * Common searching harness + aligning dataset use case harness to be more pluggable * Shared fixture for account use case tests + indexing tests. Accounts indexing: testing predefined indexer * Tests: predefined datasets indexing * MT version of predefiend datasets indexing test * MT version of incremental indexing * Tests: renaming or deleting account affects index of it's datasets * Udeps fixed * Improvements and tests for detailed dataset content indexing (schema, setInfo, attachments). Not indexing default vocabulary fields in schema, as those do not contribute to search relevance. Indexing tests run with real outbox to maximize realism: forcing sync when necessary between test steps * Added basic test suide for datasets searching: checking analyzers, filters, stemmers, ... * Merged useful testability changes from Molecule prototype * Elasticsearch: abiility to setup, connect to, and test with a server using HTTPS/TLS * udeps fix * Admin: - endpoint to force reset search indices - temporarily hidden querying full text GQL endpoint under admin's guard * minor rename * Makefiule actions to start Qdrant container * QDrant parity with ES approaches: - dummy implementation - lazy init via background catalog - passing explicit context for queries * changelog * DEVELOPER.md notes on how to use Elasticsearch * v0.256.0
1 parent 3e1cc11 commit 6f81d6c

174 files changed

Lines changed: 11226 additions & 1118 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.config/nextest.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ setup = { max-threads = 1 } # Serialize the setup steps
88
containerized = { max-threads = 8 } # Don't use too much memory
99
heavy-engine = { max-threads = 2 } # Heavy engine tests are very memory-hungry
1010
database = { max-threads = 8 } # Don't use too much memory
11+
elasticsearch = { max-threads = 4 } # Elasticsearch tests can be parallelized moderately
1112

1213
# NOTE: Periodic missing rows when the system is under load
1314
# https://github.com/kamu-data/kamu-engine-risingwave/issues/7
@@ -31,6 +32,10 @@ test-group = "heavy-engine"
3132
filter = "test(::database::)"
3233
test-group = "database"
3334

35+
[[profile.default.overrides]]
36+
filter = "test(::elasticsearch::)"
37+
test-group = "elasticsearch"
38+
3439
[[profile.default.overrides]]
3540
filter = "test(::flaky::)"
3641
retries = { count = 3, backoff = "exponential", delay = "3s" }

.github/workflows/build.yaml

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,23 +95,27 @@ jobs:
9595
runs-on: ubuntu-latest
9696
cargo-flags: --profile ci --features ingest-ftp,web-ui
9797
nextest-flags: --cargo-profile ci --features ingest-ftp,web-ui
98-
nextest-exclusions-main-set-tests: -E '!(test(::database::) | test(::spark::) | test(::flink::))'
98+
nextest-exclusions-main-set-tests: -E '!(test(::database::) | test(::elasticsearch::) | test(::spark::) | test(::flink::))'
9999
nextest-exclusions-database-set-part: ''
100+
nextest-exclusions-elasticsearch-set-part: ''
100101
container-runtime: podman
101102
ipfs: true
102103
database: true
104+
elasticsearch: true
103105
maximize-space: true
104106
# TODO: M1 cannot run docker due to lack of nested virtualization
105107
# See: https://github.com/marketplace/actions/setup-docker-on-macos#arm64-processors-m1-m2-m3-series-used-on-macos-14-images-are-unsupported
106108
- name: MacOS / arm64
107109
runs-on: macos-15
108110
cargo-flags: --profile ci
109111
nextest-flags: --cargo-profile ci
110-
nextest-exclusions-main-set-tests: -E '!(test(::containerized::) | test(::database::))'
112+
nextest-exclusions-main-set-tests: -E '!(test(::containerized::) | test(::database::) | test(::elasticsearch::))'
111113
nextest-exclusions-database-set-part: '& !(test(::containerized::))'
114+
nextest-exclusions-elasticsearch-set-part: '& !(test(::containerized::))'
112115
container-runtime: ''
113116
ipfs: false
114117
database: false
118+
elasticsearch: false
115119
maximize-space: false
116120
# TODO: DataFusion tests are temporarily disabled
117121
# See: https://github.com/kamu-data/kamu-cli/issues/226
@@ -120,11 +124,13 @@ jobs:
120124
cargo-flags: --profile ci
121125
nextest-flags: --cargo-profile ci
122126
# platform(target) is a workaround for Windows only issue in cargo-nextest upon proc macro crates
123-
nextest-exclusions-main-set-tests: -E 'platform(target) & !(test(::containerized::) | test(::datafusion::) | test(::database::))'
127+
nextest-exclusions-main-set-tests: -E 'platform(target) & !(test(::containerized::) | test(::datafusion::) | test(::database::) | test(::elasticsearch::))'
124128
nextest-exclusions-database-set-part: '& !(test(::datafusion::))'
129+
nextest-exclusions-elasticsearch-set-part: '& !(test(::datafusion::))'
125130
container-runtime: ''
126131
ipfs: false
127132
database: false
133+
elasticsearch: false
128134
maximize-space: false
129135
name: Test / ${{ matrix.name }}
130136
runs-on: ${{ matrix.runs-on }}
@@ -144,6 +150,21 @@ jobs:
144150
MARIADB_DATABASE: kamu-test
145151
ports:
146152
- 3306:3306
153+
elasticsearch:
154+
image: ${{ matrix.elasticsearch && 'elasticsearch:9.2.1' || '' }}
155+
env:
156+
discovery.type: single-node
157+
ES_JAVA_OPTS: -Xms1g -Xmx1g
158+
xpack.security.enabled: "false"
159+
xpack.security.http.ssl.enabled: "false"
160+
xpack.security.transport.ssl.enabled: "false"
161+
xpack.ml.enabled: "false"
162+
ingest.geoip.downloader.enabled: "false"
163+
node.store.allow_mmap: "false"
164+
cluster.routing.allocation.disk.threshold_enabled: "false"
165+
ports:
166+
- 9200:9200
167+
- 9300:9300
147168
steps:
148169
- name: Setup IPFS
149170
uses: ibnesayeed/setup-ipfs@master
@@ -194,18 +215,54 @@ jobs:
194215
- name: Run main set of tests
195216
run: cargo nextest run ${{ matrix.nextest-flags }} ${{ matrix.nextest-exclusions-main-set-tests }} -v
196217

218+
- name: Wait for Elasticsearch (stable)
219+
if: matrix.elasticsearch
220+
run: |
221+
set -euo pipefail
222+
223+
# Require 5 consecutive successful responses.
224+
ok=0
225+
for i in {1..180}; do
226+
if curl -fsS --max-time 2 "http://127.0.0.1:9200/_cluster/health?wait_for_status=yellow&timeout=1s" >/dev/null; then
227+
ok=$((ok+1))
228+
echo "ES health ok ($ok/5) at attempt $i"
229+
if [ "$ok" -ge 5 ]; then
230+
echo "ES is stable and healthy"
231+
exit 0
232+
fi
233+
else
234+
ok=0
235+
fi
236+
sleep 1
237+
done
238+
239+
echo "ES did not become stable"
240+
docker logs --tail 300 "${{ job.services.elasticsearch.id }}" || true
241+
exit 1
242+
243+
- name: Elasticsearch tests
244+
if: matrix.elasticsearch
245+
env:
246+
ELASTICSEARCH_URL: http://127.0.0.1:9200
247+
run: |
248+
# Show actual disk headroom ES sees
249+
curl -fsS 'http://127.0.0.1:9200/_cat/nodes?v&h=name,ip,disk.used_percent,disk.avail,disk.total'
250+
251+
# Run ES related tests
252+
cargo nextest run ${{ matrix.nextest-flags }} -E 'test(::elasticsearch::)' ${{ matrix.nextest-exclusions-elasticsearch-set-part }}
253+
197254
- name: Postgres database tests
198255
if: matrix.database
199256
env:
200257
SQLX_OFFLINE: false
201-
DATABASE_URL: postgres://root:root@localhost:5432/kamu-test
258+
DATABASE_URL: postgres://root:root@127.0.0.1:5432/kamu-test
202259
run: cargo nextest run ${{ matrix.nextest-flags }} -E 'test(::postgres::) ${{ matrix.nextest-exclusions-database-set-part }}'
203260

204261
- name: MariaDB database tests
205262
if: matrix.database
206263
env:
207264
SQLX_OFFLINE: false
208-
DATABASE_URL: mariadb://root:root@localhost:3306/kamu-test
265+
DATABASE_URL: mariadb://root:root@127.0.0.1:3306/kamu-test
209266
run: cargo nextest run ${{ matrix.nextest-flags }} -E 'test(::mysql::) ${{ matrix.nextest-exclusions-database-set-part }}'
210267

211268
# Not running on windows due to line ending differences

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ target/
77

88
.env
99
/kamu.sqlite.db*
10+
.local/

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,44 @@ Recommendation: for ease of reading, use the following order:
1111
- Fixed
1212
-->
1313

14+
## [0.256.0] - 2026-01-08
15+
### Added
16+
- Prototyped full-text search capabilities via `Elasticsearch`:
17+
- can be connected to external instance or launch an embedded container
18+
- prototyped schema descriptors, supporting multiple field types with different search capabilities, such as :
19+
- prose
20+
- identifiers
21+
- keywords
22+
- dates
23+
- numbers,
24+
- booleans
25+
- ...
26+
- designed search schemas for accounts and datasets
27+
- automatic index creations and alias rotations for search schemas, with detection of mappings drift
28+
- accounts and datasets are indexed automatically on first launch or after explicit reset,
29+
as well as react to lifecycle changes incrementally (via handling outbox events)
30+
- prototyped query language, allowing:
31+
- textual prompts (simplistic, using `simple_query_string` algorithm for now)
32+
- filter expressions: ==, !=, <, >, <=, >=, in {}, prefix
33+
- filter combinations: AND, OR, NOT
34+
- sorting criterias: by field or by relevance, any combination
35+
- pagination controls
36+
- supporting fields sourcing (returning original document)
37+
- supporting highlights (explains which field matched)
38+
- supporting debug explanations of scoring
39+
- predefined capatilities regardless of schema:
40+
- title-based sorting (each schema specifies a field that should act like a title)
41+
- banning field (when set, the document is auto-excluded from read aliases matching search schemas)
42+
- testing mechanisms with Elasticsearch:
43+
- integration tests: creating temporary indices with a random prefix, cleaned at shutdown
44+
- e2e tests
45+
- CI support (separate `elasticsearch` test group, starting container on Github)
46+
- written solid coverage for accounts & datasets indexing and searching logic
47+
- temporary GQL endpoint for testing (gated with admin guard)
48+
- GQL endpoint to force reindexing at run-time
49+
### Fixed
50+
- Qdrant initialization works similarly to Elasticsearch solution, and avoids excessive reindexing
51+
1452
## [0.255.1] - 2026-01-05
1553
### Changed
1654
- Upgraded `prost`, `tonic`, `flat`, `opentelemetry` to latest versions

0 commit comments

Comments
 (0)