feat(search)!: filter across collections through declared joins - #719
Open
ddeboer wants to merge 1 commit into
Open
feat(search)!: filter across collections through declared joins#719ddeboer wants to merge 1 commit into
ddeboer wants to merge 1 commit into
Conversation
- add `joinable` to a reference field, valid only alongside `labelSource`, and `joinGraph(schema)` holding the edges it declares: the components a rebuild is scoped by, and the type a query path resolves to - give a criterion an `on` path, capped at three hops, so `where` stays the flat conjunction of disjunctions it was and a joined criterion can sit in an `or` beside a local one - compile a path into nested Typesense `$collection(…)` clauses, with the leaf term compiled against the target type's own declaration - emit a joinable reference as a Typesense reference field targeting `.id`, `async_reference`, no cascade delete - make the join component the unit of rebuild: open referenced-first, commit per component referrers-first, and fail loudly when an existing In-place collection lacks a declared reference - replace the rebuild option `name` with `collectionNameFor`, so a writer can name a peer's versioned collection - serve a joinable reference as `‹Target›ReferenceFilter` `@oneOf` over `in` and the target's own `where`, and key skip-own-filter by (path, field) BREAKING CHANGE: the Typesense rebuild and collection-definition options take `collectionNameFor: (searchType) => string` instead of `name: string`. Pass `collectionNameFor: () => 'x'` where a single name was passed before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gives the query model cross-collection filtering by declaring joins on the edges
the schema already describes, so “every object published by institution X”
becomes one query instead of two round trips.
compiles to
filter_by: $datasets($publishers(id:=X))– one engine round-trip,with a correct
total, ranking and facet counts.Fix #712. Decisions and their reasoning: ADR 19.
What changed
@lde/search–joinable: trueon areferencefield, valid onlyalongside
labelSource(which already asserts its values are ids in thattype’s collection).
joinGraph(schema)holds everything that follows: thecomponents a rebuild is scoped by, and what a query path resolves to. Built
eagerly by
searchSchema, so the schema-wide rules – one joinable reference pertarget, no cycles – fail at startup. A criterion gains an
onpath, capped atthree hops in the IR so a later REST surface inherits the cap;
wherestays theflat conjunction of disjunctions ADR 18 made it.
@lde/search-typesense– a joinable reference is emitted as a referencefield targeting
.id, withasync_reference: trueandcascade_delete: false(all three forced, reasons in the ADR). An
onpath compiles to nested$collection(…)clauses, the leaf compiled against the target type’sdeclaration.
InPlaceRebuildfails loudly when an existing collection lacks adeclared reference, naming the drop-and-rebuild – it would otherwise index and
commit happily and then 400 on every join.
@lde/search-pipeline– the join component is the unit of rebuild: runsopen referenced-first (a collection cannot reference one that does not exist
yet) and commit per component, referrers-first (a blue/green commit drops the
collection it supersedes). A type with no joinable edge is a singleton
component, so a schema without joins behaves exactly as before.
@lde/search-api-graphql– a joinable reference takes‹Target›ReferenceFilter @oneOf { in, where }, one per target and shared byevery field pointing at it; a non-joinable one keeps
StringFilter, so thecapability difference is visible in the schema rather than a runtime error.
Skip-own-filter is keyed by
(path, field).Found while testing: Typesense loses references under concurrent import
The integration test caught something the issue assumed was safe. Back-fill is
exact sequentially – a referrer imported before its referent is accepted and
resolves the moment the referent lands (pinned by a new test). But per-type
stages import into a referring and a referenced collection concurrently, and
30.2 can then lose a reference permanently: every document present, the join
finding nothing, and which edge loses varying per run. Reproduced 4 of 5 times
with two 500-document concurrent imports.
So a component built from scratch needs its indexer run twice before its
joins resolve; a second run meets referents that already exist and resolves
every reference at write time. Steady-state runs over a stable corpus are
unaffected.
async_referenceis still strictly right – without it thosedocuments would be rejected outright and, under
throwOnFail: false, dropped insilence.
This is documented as a limitation in the ADR and flagged in the
search-typesensereference; the end-to-end test indexes twice with a commentsaying why. Not ours to fix, and worth re-checking on every engine upgrade –
when it is fixed, the second run and the caveat both go.
Breaking change
The Typesense rebuild and collection-definition options take
collectionNameFor: (searchType) => stringinstead ofname: string. A writernow names more than its own collection: an emitted reference names its peer’s,
and a blue/green build must name the peer’s fresh collection rather than its
live alias. Migration is mechanical –
name: 'x'becomescollectionNameFor: () => 'x'.Out of v1
Reverse joins, facets and sorting through a join, free text through a join, and
shadow collections – each with its reason in the ADR. The
(path, field)facetkey already anticipates joined facets.