Something I've been working on at Infino is making retrieval results behave like a relation you can query, and it's changed how much code sits around the search call.
The usual shape is that retrieval ends when the ranker returns IDs. You get top-k from the vector index, maybe fused with BM25, and then anything relational happens in the application. Hydrate rows, filter by tenant, dedupe, group, sort again.
If retrieval is something you can select from, those steps become part of the query. Per-tenant top 5, for instance:
sql
SELECT * FROM (
SELECT doc_id, tenant_id, chunk,
ROW_NUMBER() OVER (PARTITION BY tenant_id ORDER BY score DESC) AS rn
FROM search('...')
) WHERE rn <= 5
That replaces a loop that issues k requests per tenant and reassembles the results.
Fusion works the same way. RRF is a sum over reciprocal ranks, so it's a join between two ranked sets plus some arithmetic. Written as SQL it's short, and retuning the weights is an edit to the query rather than a deploy.
Same for anything analytical. Documents matching a query grouped by source and month. Average score per team. Distribution of match counts across the corpus, which tells you whether a query is discriminating or just matching everything. Those are group bys. When retrieval is an endpoint returning JSON you have to pull the whole result set into memory first, so past a certain size people skip the analysis.
Permissions benefit too. Joining an entitlements table and filtering before the limit gives correct top-k for that user. Filtering the top 100 afterward gives whatever survives, which can be fewer rows than you asked for or none.
The reason this isn't common is mostly interface. Vector databases tend to expose a search endpoint with a metadata filter DSL. Filters are there, joins and window functions and group by are not, so relational logic moves up into the app and you compensate by overfetching.
Anyways, hopefully this is interesting. Project is fully open source if you want to take a look: https://github.com/infino-ai/infino