14 Commits

Author SHA1 Message Date
Andrey Vasnetsov
8888046cd3 Add usage skill file for edge-shard-query tool (#9836)
Standalone usage reference for `edge-shard-query`: backends (S3 / GCS /
uio-grpc), connection and tuning flags, the scroll / search / search-sparse
sub-commands, filtering, and the live-reload diff mode.

Written to stand on its own, so it can be shared as a link without also
sharing the source.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:54:33 +02:00
Andrey Vasnetsov
a5a0ecc5e9 Add search-sparse sub-command to edge-shard-query tool (#9834)
Sparse nearest-neighbour search over a ReadOnlyEdgeShard, reusing the
same SearchRequest path as dense search with VectorInternal::Sparse.
The query vector is accepted as a JSON object or an index:value pair
list, sorted and validated before use. --vector is required (no random
fallback: the sparse vocabulary is not recoverable from the shard
config), and --hnsw-ef is omitted since it does not apply to the
sparse index.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 15:35:38 +02:00
Andrey Vasnetsov
43e3d6ea8d [UIO] Request-specific load profile for read-only opens (#9797)
* Introduce request-specific LoadProfile with per-component placement

A read-only shard opened for one known request (the serverless cold-start
path) doesn't have to warm components the request will never touch.
LoadProfile captures that from the request: warm components keep the
persisted-config placement, everything else is parked cold. All placement
decisions live in one place, so the memory placement of a whole segment
under a profile is reviewable in one file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais

* Thread LoadProfile through the read-only segment open

ReadOnlySegment::open takes an optional profile; first_preopen and
open_via resolve it into per-component populate overrides so the opens
make the same placement decisions the prefetches did. Pinned components
that materialize on open regardless (quantized RAM storage kinds, the
immutable-RAM sparse index) and appendable components ignore the
override; the HNSW graph and immutable payload indexes demote fully.
Config reloads follow the new config alone and pass no override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais

* Open ReadOnlyEdgeShard under a request-derived load profile

ReadOnlyEdgeShard::open takes an optional LoadProfile, applies it to
every segment open and keeps it so segments discovered by a later
refresh load with the same placement. ScrollRequestInternal and
CoreSearchRequest gain load_profile() constructors, and edge-shard-query
builds the request before the open and passes its profile (opt out with
--no-load-profile).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais

* Demote pinned quantized vectors and sparse index under a cold profile

Within the immutable layout the quantized RAM and mmap loaders share the
on-disk format — only how the data is brought into memory differs — and
the immutable-RAM sparse index has the same lazy mmap open low-memory
mode already downgrades to. So a cold populate override now demotes the
effective placement itself (Memory::with_populate_override, shared with
the HNSW residency mapping) instead of only skipping cache priming: a
pinned quantized storage opens the mmap kind cold, and a pinned sparse
index opens as Mmap, so neither reads its data on a cold start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais

* Add LoadProfile::merge for composite queries

A composite query runs multiple core requests — e.g. a hybrid search
runs one core search per vector, each with its own filter. Its profile
is the union of its parts': merge extends the warm sets and ORs the
payload-storage flag, so a component either part needs warm stays warm.

The union is sound because every placement method is monotone in the
warm sets (growing them only turns "park cold" into "keep configured
placement"), so the merged profile dominates each input; and minimal,
warming nothing no part asked for.

Combine profiles with reduce, not fold: merge's identity element is
the coldest profile (empty warm sets), the opposite of passing no
profile at all — deliberately no empty()/Default constructor exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Adapt vanished-segment test to the profile-aware open signature

The test landed on dev (#9777) after the load-profile signature change
was written, so the rebase left its ReadOnlySegment::open calls without
the new load_profile argument.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Defer vector index open entirely under a cold load profile

A cold placement is not enough for the vector index on remote backends:
GraphLinksView requires the whole links file as one contiguous slice,
and the disk cache can only lend a borrowed slice once every block is
locally present — so even a Cold HNSW open mirrors the entire
links_compressed.bin (8.2 MB / 1.1 s per segment in the serverless
cold-start trace), plus the unconditional graph.bin metadata read.
The only way not to fetch the index is not to open it.

LoadProfile::vector_index_placement is replaced by
vector_index_deferred: a vector the request never scores now gets a
DeferredVectorIndex — a new VectorIndexReadEnum variant holding the
open arguments (an owned clone of the segment's raw backend, path,
config, shared component handles) and a OnceLock. Nothing is opened or
prefetched for it at segment open.

Per-method policy of the deferred variant:
- search, fill_idf_statistics and populate open the index on first use
  (with the cold placement the profile chose), so the profile contract
  holds: a request the profile did not predict still works, just pays
  the open then;
- is_index reports true without opening (deferral only ever wraps a
  real HNSW or sparse index; plain opens no files and is never
  deferred);
- telemetry, indexed_vector_count and sizes answer conservative
  defaults rather than trigger a remote fetch for a statistic.

Tests: deleting the vector_index directory before an open under a
scroll profile leaves open, filtered reads and payload reads working —
proof that nothing of the index is read — while a search surfaces the
missing files; and a segment opened under a scroll profile answers
searches identically to an eagerly opened one via the transparent
first-use open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make --vector optional in edge-shard-query: random query after open

Omitting --vector on the search sub-command now searches with a random
vector. The request is still built before the shard opens — the load
profile only needs the vector name, not its values — with an empty
placeholder; once the shard is open, fill_random_vector reads the
dimension of the queried vector from the derived shard config and
fills in uniform-random f32s (with a clear error if the named vector
is not in the config).

The vector is generated once, so live-reload iterations re-run the
identical random query and the printed diffs stay meaningful.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Scope index deferral to the HNSW graph via a lazy OnceLock load

Replace the DeferredVectorIndex wrapper (and the VectorIndexReadEnum::
Deferred variant) with deferral inside ReadOnlyHNSWIndex itself: the
graph lives in a OnceLock (same first-wins arbitration as
ReadOnlyRoaringFlags::bitmap) alongside the retained raw backend and
residency, and loads on first use with a cold placement. The config
read stays eager — one tiny, absence-tolerated file — so telemetry,
is_on_disk and indexed_vector_count report real values where the
Deferred arms answered with hard defaults.

The sparse index needs no deferral: its mmap open reads lazily, with
only small JSON metadata eager. A profile that never scores the vector
now passes a cold placement override (LoadProfile::
vector_index_placement) into the eager open_sparse, which demotes
ImmutableRam to the lazy Mmap open like low-memory mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Express HNSW graph deferral as a populate override, not a bool param

Replace the `deferred: bool` on the read-only HNSW open/preopen (and
the VectorIndexReadEnum pass-through) with the same
`populate_override: Option<Populate>` every other component takes. A
cold *override* defers the graph load — graph_deferred() mirrors the
cold-override match of open_sparse — while a config-derived cold
placement (or the low-memory clamp) keeps the eager load, since only a
request-specific override carries the "never scored" prediction.

With dense and sparse now consuming the same signal,
LoadProfile::vector_index_deferred is gone: a single
vector_index_placement() serves both index kinds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Serialize the deferred graph load via once_cell's get_or_try_init

Loading outside the lock (std OnceLock's fallible init is still
unstable) let a search burst on a deferred vector fetch the whole
graph once per thread. Swap the cell for once_cell::sync::OnceCell:
the fallible load runs inside the cell's lock, concurrent first users
block on the one load, and a failed load leaves the cell empty so the
next caller retries.

Addresses https://github.com/qdrant/qdrant/pull/9797#discussion_r3573727836

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 23:03:14 +02:00
Andrey Vasnetsov
8f54076cbe Add uio-grpc backend and key-triggered live-reload to edge-shard-query (#9795)
* Add uio-grpc backend and key-triggered live-reload to edge-shard-query

--backend uio-grpc opens the shard directly over a running Qdrant
peer's StorageRead gRPC service (public gRPC endpoint, api-key aware),
addressed by --collection/--shard-id — no object storage involved. The
UioGrpcSource backend existed since #9634 but was never wired into the
tool's CLI. --bucket is now per-backend optional (required for
aws/gcs), and the default cache dir is scoped by collection/shard for
uio-grpc, whose mirror has no distinguishing key prefix.

--live-reload-key runs the same watch loop as --live-reload with each
reload triggered by pressing Enter instead of a timer — easier when
stepping through a debug scenario. Timer mode is unchanged; the two
flags are mutually exclusive. Closed stdin ends the loop gracefully,
so the flag cannot busy-loop on piped input (and is documented as
incompatible with @- stdin request arguments).

Verified end-to-end against a live instance started with
QDRANT__FEATURE_FLAGS__WRITE_SEGMENT_MANIFEST=true: scroll over
uio-grpc returns all points with payloads, timer mode picks up an
upsert + delete as +/- diff lines, and key mode fires one reload per
Enter and exits cleanly on EOF.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Log StorageRead not-found gRPC failures at debug level

A read-only follower routinely probes files the writer creates lazily
(e.g. the mutable id tracker's mappings and versions before the first
flush), so every uio-grpc follower poll spammed INFO logs like:

  gRPC /qdrant.StorageRead/FileLength failed with NotFound
  "File not found: .../mutable_id_tracker.versions"

NotFound on /qdrant.StorageRead/* now logs at debug; NotFound on all
other services (missing collection etc.) stays at info. The service is
matched by parsing the path's service component and comparing it to
the tonic-generated storage_read_server::SERVICE_NAME constant rather
than a hard-coded path string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:04:54 +02:00
Andrey Vasnetsov
a43abbd3b8 Add --live-reload watch mode to edge-shard-query (#9791)
With --live-reload <SECONDS> the tool keeps running after the first
answer: every interval it refreshes the ReadOnlyEdgeShard from object
storage, re-runs the same request, and prints the difference against
the previous results — `+` id appeared, `-` id disappeared,
`~ old -> new` for changed content (payload/vector/score/version).
Each cycle prints a summary line; pure reordering prints nothing.

The request is parsed once into a PreparedRequest (ScrollRequest /
SearchRequest are Clone), so every iteration re-runs the identical
request and the filter/vector parsing and logging no longer repeat.
The first run prints the full result set in the existing format.

A failed refresh is logged and retried next tick — the shard keeps
serving its previous state, so a transiently unreachable bucket does
not kill the watch loop. Status lines go to stderr via log, diff rows
to stdout, keeping stdout machine-consumable.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:47:35 +02:00
Daniel Boros
78688733cb feat: support S3 Express One Zone buckets in the S3 bridge (#9757) 2026-07-09 15:02:28 +02:00
Andrey Vasnetsov
02cacfe192 debug logging of the s3 connection (#9597)
* debug logging of the s3 connection

* refactor(io_bridge): make S3 latency logs opt-in and low-overhead

Move the blob-backend latency traces onto a dedicated `io_bridge::latency`
log target at `trace` level, so they are silent by default and can be
toggled as one group at runtime without a rebuild (e.g.
`RUST_LOG=io_bridge::latency=trace`). Guard the timing `Instant::now()`
behind `log_enabled!` so there is no overhead when the target is disabled.

Also add `list_files` timing, and switch the shard_query CLI logger to
millisecond timestamp resolution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:16:16 +02:00
Andrey Vasnetsov
8311ab62a9 feat(edge): add --search-threads to shard_query tool (#9736)
Add a `--search-threads` option to the edge-shard-query tool so the
number of threads in the shard's search thread pool can be specified.
When set, it builds an EdgeConfig with `max_search_threads` and passes
it to `ReadOnlyEdgeShard::open`, overriding the CPU-derived default used
for both parallel segment reads at open and running searches.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:24:53 +02:00
Andrey Vasnetsov
729dc6af43 Make EdgeConfig tunables optional with a layered fallback chain (#9714)
Tunable EdgeConfig parameters (on_disk_payload, hnsw_config, optimizers)
are now Option, and every tunable resolves through the fallback chain
provided -> persisted -> derived from segments -> default when loading
an existing shard. Leaving a parameter unspecified keeps the shard as it
is; an explicit value overwrites it and existing segments converge to it
through the optimizers.

vectors/sparse_vectors are excluded from overwrite semantics: an empty
map inherits the persisted/segment-derived definitions, a non-empty map
is validated for compatibility against the loaded segments (size,
distance, multivector, datatype, sparse modifier) and fails the load on
mismatch.

The derived layer folds over all segments in UUID order instead of
taking an arbitrary first segment, so a plain appendable segment (which
carries no HNSW parameters) can never mask an indexed segment's actual
build parameters. Previously a lost edge_config.json could resolve
unspecified HNSW params to compiled-in defaults and silently trigger a
full re-index via ConfigMismatchOptimizer.

The read-only follower accepts an optional config on open: provided
tunables are applied once over the segment-derived config (vectors
always come from the segments), and refresh re-derives from segments
alone.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:45:02 +02:00
Andrey Vasnetsov
5dec49802e Split io_bridge core out of io_bridge_object_store; add uio backend (#9634)
* Split io_bridge core out of io_bridge_object_store; add uio backend

Separate the backend-agnostic sync<->async bridge from the object-store
backends so a second backend can plug in, and add a uio-client backend.

- io_bridge (new): the bridge core (AsyncRead, BridgeRuntime, BlobFile,
  BlobFs, pipeline), with no object_store/tonic deps. BridgeRuntime::block_on
  is now pub so sibling crates can drive it.
- io_bridge_object_store (slimmed): keeps BlobBackend + S3/GCS/Azure. The
  blanket `AsyncRead for Arc<S>` would now break the orphan rule, so it moves
  onto a local newtype `ObjectStoreSource<S>`. Re-exports the bridge core.
- io_bridge_uio (new): UioSource implements AsyncRead over uio_client::Client,
  pinned to one (collection, shard_id) replica. Streams gRPC chunks into the
  aligned buffer; read_from = FileLength + tail stream; writes are Unsupported
  (read-only service). The client is built lazily on first use inside the
  bridge runtime, because tonic's connect_lazy captures the reactor at
  construction and would panic in the runtime-free AsyncRead::open.
- uio-client: add sync connect_lazy, read_bytes_stream_raw (BoxStream<Bytes>),
  NOT_FOUND -> UniversalIoError::NotFound mapping, and an api-key auth
  interceptor (UioConfig::api_key) injecting the `api-key` gRPC header.
- common: add UniversalKind::Uio.
- Consumers (segment, edge-shard-query): Arc<AmazonS3> -> ObjectStoreSource<_>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Rename uio crates/types to uio-grpc

Clarify that this "uio" backend is the gRPC transport variant (the "uio"
shorthand for Universal IO is used elsewhere in the tree and is left alone).

- crate uio-client -> uio-grpc-client (dir lib/uio-client -> lib/uio-grpc-client)
- crate io_bridge_uio -> io_bridge_uio_grpc
- UniversalKind::Uio -> UniversalKind::UioGrpc
- UioConfig/UioSource/UioFile/UioFs -> UioGrpc{Config,Source,File,Fs}
- doc/import references updated accordingly

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:51:15 +02:00
Andrey Vasnetsov
33771fb6c1 Generalize edge S3 scroll tool into edge-shard-query (#9598)
Turn the single-purpose `edge-s3-scroll` tool into a sub-command-based
`edge-shard-query` that can run different read requests against a
ReadOnlyEdgeShard opened over object storage.

- Add `scroll` and `search` sub-commands; shared connection/cache args
  live at the top level.
- Accept arbitrary payload filters as JSON via `--filter` (curl
  `--data` style: literal JSON, `@file`, or `@-` for stdin), parsed
  straight into the filter DSL. Keep `--filter-key`/`--filter-value`
  as a shortcut.
- `search` accepts a query vector (JSON array, comma list, or
  `@file`/`@-`), named vector, offset, score threshold, and HNSW
  params.
- Rename the package/binary/dir (`s3_scroll` -> `shard_query`) since
  the tool is no longer scroll-only nor S3-only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:58:20 +02:00
Luis Cossío
a3c313b9a6 Add s3_proxy.sh tool to simulate an S3 bucket locally (#9589) 2026-06-27 22:42:28 +02:00
Andrey Vasnetsov
f70a1462fb feat: read-only edge shard follower (ReadOnlyEdgeShard) (#9529)
* feat: read-only edge shard follower (ReadOnlyEdgeShard)

Add a read-only follower of EdgeShard, mirroring the segment-level
ReadOnlySegment + live_reload design one level up at the shard. A leader
process owns a read-write EdgeShard; one or more followers open the same
on-disk directory as a ReadOnlyEdgeShard, serve reads, and periodically
refresh() to pick up the leader's flushed writes and optimizations.

Shared read logic lives once in a crate-internal EdgeReadView<H>, generic
over a ReadSegmentHandle: the follower is monomorphized over the concrete
ReadOnlySegment<S> (no dynamic dispatch), while the read-write shard's
heterogeneous Segment/ProxySegment holder uses the LockedSegment enum
(the only dyn ReadSegmentEntry left, mirroring LockedSegment::get_read).

EdgeShardRead is the public read API: it exposes the read methods
(search/query/retrieve/count/facet/scroll/info/...) as default methods,
requiring implementors only to provide read_segments() + config_snapshot().
EdgeShard keeps its inherent read methods for backwards compatibility.

Segment discovery is injected via a SegmentEnumerator (temporary seam):
LocalSegmentEnumerator scans the segments/ directory for the local/mmap
case; S3 followers supply their own. This will be replaced by an on-disk
segment manifest in follow-up work.

shard: add LockedSegment::get_read_arc(); split retrieve_blocking into a
handle-collecting wrapper + generic retrieve_over; generalize the private
_read_points over the segment type (public signatures unchanged); remove
the now-unused read_points_locked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Emj8TFxdtrf3K32eWhGgor

* feat: manifest-based segment loading for ReadOnlyEdgeShard

Replace the read-only follower's directory-scan discovery with the segment
manifest from the leader, the proper mechanism the SegmentEnumerator seam
was a placeholder for.

shard: add SegmentsManifest::load so readers can read manifest.json.

edge (leader): EdgeShard now writes segments/manifest.json (gated by the
write_segment_manifest feature flag), initialized from the live segment
set on new/load and refreshed after optimize() swaps segments.

edge (follower): ManifestSegmentEnumerator reads the manifest's `active`
segments instead of scanning, and open_mmap uses it. It requires a manifest
and errors when none is present (no silent scan fallback); discover by
scanning explicitly via open() with a LocalSegmentEnumerator instead.

Since the manifest only reports ready segments (and future versions will
mark segments retiring/under-construction rather than removing them from
active immediately), the read path no longer races with the leader, so the
defensive `is_transient_open_error` skip handling is removed — a failure to
open a reported segment is now a genuine error and propagates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Emj8TFxdtrf3K32eWhGgor

* feat: edge-s3-scroll experiment binary for ReadOnlyEdgeShard over S3

Adds a standalone `edge-s3-scroll` binary that opens a ReadOnlyEdgeShard
directly over an S3 (or S3-compatible) bucket and runs a single scroll
request with a hard-coded filter, for experimentation.

Takes the bucket endpoint, credentials and key prefix as CLI flags/env
vars, builds an S3-backed BlobFs, and discovers segments via a custom
enumerator that reads the segment manifest over object storage. The
edge_config.json is fetched to a local temp dir because
ReadOnlyEdgeShard::open reads config from the local filesystem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* filter config in s3 scroll tool

* fix: read segment manifest from new location after dev rebase

dev #9564 moved the segment manifest next to (rather than inside) the
segments/ directory and named it segments_manifest.json. Adapt the
read-only follower enumerators accordingly so the follower reads the
manifest where the leader now writes it:

- ManifestSegmentEnumerator reads via segment_manifest_path() (shard root)
  while still resolving segment dirs under segments/<uuid>.
- The S3 scroll tool's enumerator mirrors the same layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: read-only edge shard works over object storage without edge_config

Make ReadOnlyEdgeShard self-sufficient over arbitrary backends (S3/GCS),
and harden the read-only segment loading it depends on.

- ReadOnlyEdgeShard derives its config from the segments via
  EdgeConfig::from_segment_config instead of requiring edge_config.json
  (open and refresh both derive); a follower never has that file.
- ReadOnlyEdgeShard::open(fs, path) no longer takes an enumerator: a
  read-only follower always discovers segments through the manifest.
  open_with_enumerator remains as a pub(crate) seam for tests.
- ManifestSegmentEnumerator is generic over UniversalReadFs (reads the
  manifest via read_json_via), so it works over local mmap or a blob/S3
  backend; the tool's bespoke enumerator is removed.
- Read-only mutable ID tracker tolerates absent mappings/versions files
  (they are not written while empty), matching MutableIdTracker::open:
  files are opened lazily and NotFound is treated as empty, avoiding an
  extra exists() round-trip on object storage.

edge-s3-scroll experiment tool:
- Supports AWS S3 / S3-compatible and GCS backends (--backend), with a
  hard-coded-free --filter-key/--filter-value scroll filter.
- Reads segment data through a DiskCache so remote blocks are fetched
  once and served from a local mirror afterwards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: remove unused SegmentsManifest::load

Its only caller (edge's ManifestSegmentEnumerator) now reads the manifest
via read_json_via over UniversalReadFs, so the local-only load helper is
dead. Drop it and its now-unused read_json/Path imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:04:29 +02:00
xzfc
d8383861b2 Test edge examples on CI (#8318)
* lib/edge/publish/cargo: improve target directory discovery

Don't hardcode workspace root, ask cargo instead.

* Fix warning in qdrant-edge amalgamation

`cargo check` in `lib/edge/publish` produces the following warning:

    warning: type `PointerUpdates` is more private than the item `gridstore::tracker::Tracker::<S>::write_pending`

* lib/edge/publish/examples: fix compiler warnings

* refactor: lib/edge/publish/examples: put DATA_DIRECTORY into lib.rs

To match python examples that have the same constant in `common.py`.

* Use `lib/edge/data` dir for both Python and Rust examples

Before this commit, `lib/edge/publish/data` and `lib/edge/python/data`
were used. I'd like Python and Rust to use the same prepared resources
in the future.

* Add lib/edge/Justfile with recipes to build/run examples

I constantly keep forgetting how to compile and run edge stuff.
Also, this would be used in CI in subsequent commits.

* doc: lib/edge/README.md: Combine Rust and Python Edge readmes

Also now we can rename `creates-io-readme.md` to `README.md` like
reasonable people.

* doc: Add qdrant-edge-py README.md

Since the previous commit removed qdrant-edge-py README.md, we need to
add something in its place instead.
The new README is adapted from Rust edge README.
And the previous README wasn't good anyway, see [1], it mentions maturin
which is irrelevant for users.

[1]: https://pypi.org/project/qdrant-edge-py/0.5.0/

* GHA: Add "Setup Qdrant" composite action

We need running Qdrant instance to test edge examples on CI. We can't
use docker for this because we want to run tests on Windows and macOS.
OTOH, this action downloads binaries for all platforms provided in
https://github.com/qdrant/qdrant/releases/tag/v1.17.0

* GHA: Use single worflow for python and rust edge examples

Merge workflows to reuse build cache. Also, modernize it by using `uv`,
and recently introduced `setup-qdrant` and Justfile. Also, make it run
on three platforms (only on merges).

* GHA: edge-{py,rust}-release: also run examples before publishing

Use similar approach as in the previous commit. But note that this time
we build/test also on ARM Linux and Windows.
2026-03-11 17:13:46 +00:00