macula_rag

Federated retrieval over the Macula mesh: ask every shard of an org, merge the answers.

This exists so an index split across many nodes can be searched as one, without a central server, and without any node the org did not delegate being able to answer for it.

What it does

A shard is one node holding part of an index. It registers a callback that searches its part, and publishes a summary of what it holds. A query asks every shard of the org at once and merges their hits by score.

It needs macula 12.1 or later, for macula:providers/3,4 and the provider option of macula:call/6.

Status

0.1.0, the first release. No node issues federated queries in production yet. What this version does not do:

Using it

{deps, [{macula_rag, "~> 0.1"}]}.

Once a macula pool is connected:

ok = macula_rag:configure(Pool, RealmId,
#{org => <<"acme">>,
shard_id => <<"library-shard-1">>,
realm_name => <<"io.macula">>,
embedding => #{model => <<"nomic-embed-text">>, dim => 768}}),
%% This node is a shard: answer the org's queries.
ok = macula_rag:register_responder(
fun(Query, #{top_k := K}) -> {ok, my_index:search(Query, K)} end),
ok = macula_rag:advertise([<<"library/books">>], Bloom),
%% Ask every shard of the org.
{ok, Hits, #{answered := Answered, failed := Failed}} =
macula_rag:query(#{<<"text">> => <<"federated search">>}, #{top_k => 10}).

A hit needs at least a binary id and a numeric score; the rest of the map is yours and is carried as it is. Hits and queries arrive with binary keys, as every received payload does, and a merged hit also carries node_id and shard_id, the shard it came from.

realm_name must be the name whose SHA-256 is RealmId, or configure/3 refuses: the topics carry the name, and a topic naming another realm reaches nobody.

The grant

register_responder/1 advertises the org's procedure as soon as macula grants it, which needs the org's D25 procedure_delegation naming this node. Until then, status/0 says why not:

#{responder := {not_granted, #{reason := {provider_authorization,
{procedure_delegation, not_found}},
since_ms := 42000}}}

{procedure_delegation, not_found} means an operator has to delegate this node for the org. The grant is asked for again every grant_retry_ms (30 s), so a delegation made later is picked up without a restart, and advertised once. A link that drops and comes back needs nothing: macula replays the advertisement on it.

Configuration

Option Default Meaning
org required whose shards these are; the procedure is <org>/rag.query_shard_v1
shard_id required a label for this node's shard
realm_name required the realm's name; its SHA-256 must be the realm id
embedding required #{model => binary(), dim => pos_integer()} this node's index uses
query_timeout_ms 1500 how long a query waits for the slowest shard
grant_retry_ms 30000 how often a refused grant is asked for again
summary_republish_ms 60000 how often this shard's summary is republished; a summary unheard for three periods is stale

The wire contract

procedure <org>/rag.query_shard_v1
fact <realm>/<org>/rag/shard/shard_summarized_v1: shard_id, model, dim, topics, bloom, at_ms
fact <realm>/<org>/rag/shard/shard_withdrawn_v1: shard_id, at_ms
query query, top_k, model, dim
answer shard_id, model, dim, hits

A summary belongs to the node macula verified as its publisher, whatever the payload claims. test/macula_rag_contract_tests.erl pins every key and reads each payload back through macula's codec.

Build and test

scripts/check.sh # compile, eunit, lint, xref, dialyzer, as CI runs them

OTP 28.4.3, pinned in .tool-versions and CI. The suite includes a federation test that starts three shard VMs with peer: two answer one query and their hits merge by score; the third, built with another embedding, is reported and never asked.

Releasing

A pushed vX.Y.Z tag publishes to hex (.github/workflows/publish-hex.yml). Before publishing, the workflow refuses anything but the clean, pushed tag whose version is in src/macula_rag.app.src and has a CHANGELOG section, dry-runs the publish, and checks that hex.pm accepts the key. After publishing it checks that hex serves the tagged files.

Guides

License

Apache-2.0.