feat(search): narrow a reference’s facet to the keys its target admits - #765
Open
ddeboer wants to merge 2 commits into
Open
feat(search): narrow a reference’s facet to the keys its target admits#765ddeboer wants to merge 2 commits into
ddeboer wants to merge 2 commits into
Conversation
A Root Type may declare `facetKeys: { only }`, a predicate over the document
key that says which of its documents get a facet bucket. It is declared once,
on the target, and inherited by every facetable reference that names the type
(a `lookup`’s `target`, an `idOnly`’s `labelSource`) – the same boundary along
which a reference is re-keyed and a join is drawn. Only the facet narrows: the
field keeps every value, so an excluded referent still displays and still
filters exactly.
`physicalFields(field, schema?)` gains a `facet` member – the field itself, or
the `${name}_facet` companion of a reference inheriting a policy – so the
projection, the collection definition and the query compiler keep reading one
source for which engine field a facet reads. The projection writes the
companion from the values the field stores, after its own `transform`, so the
policy applies to keys and never to node IRIs. The `schema` parameter is
optional, as it already is on `projectDocument` and `buildCollectionDefinition`:
without one a field facets on its own name, the same degraded reading the
projection makes when it cannot re-key a reference.
The engine conformance suite gains one case: a policy facet buckets admitted
keys only and carries labels, and a filter on an excluded value stays exact.
…cet policy
For a facetable reference whose target declares `facetKeys`, the collection
definition declares the field itself as a plain stored value and its
`${name}_facet` companion as the facet. Only the `schema` option resolves the
policy; without it the field itself stays the facet, the reading the projection
makes without a schema. An in-place writer refuses a declared dataset field
that inherits a policy: it enumerates the indexed datasets by faceting that
field, and a narrowed facet would hide the excluded datasets from the sweep.
The query compiler facets `physicalFields(field, schema).facet` and picks the
membership operator off the field’s engine facet status rather than its
declaration: the base field is still `facetable` in the schema but no longer a
facet in the engine, so a filter on an excluded value – the case the policy
promises stays whole – would otherwise compile to the tokenised operator and
partial-match on a shared path prefix.
Typesense reports a facet under the field it faceted, so the engine maps each
`facet_counts[].field_name` back to the declared name once, at the response
boundary, before the label lookup and the response parsing – either of which
meeting the engine name would silently lose the facet’s labels or file its
counts under the wrong key.
An in-place rebuild over a collection that predates the policy fails at run
open – after the lock, before any write – naming the missing companion and the
drop that fixes it, as it does for a missing reference field: the documents
would carry the companion, the collection would not declare it, and every facet
on a reference to the policy’s type would fail until someone dropped the
collection by hand.
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.
PR 2 of 2 for the design in #763 (PR 1,
key, is #764): the index-side facet policy.A Root Type may declare
facetKeys: { only: (id) => boolean }– which of its documents get a facet bucket, as a predicate over the document key. It is declared once, on the target, and inherited by every facetable reference that names the type (lookup.target,idOnly.labelSource) – the same boundary along which a reference is re-keyed and a join is drawn. Only the facet narrows: the field keeps every value, so an excluded referent still displays and still filters exactly. Facets are discovery, filters are exact.What changes
@lde/search(feat)RootType.facetKeys, validated (onlya function; not allowed on a Reference Type).physicalFields(field, schema?)gainsfacet: the field itself, or the${name}_facetcompanion of a reference inheriting a policy,undefinedfor a non-facetable field – so the projection, the collection definition and the compiler keep reading one source for which engine field a facet reads.applyFacet, from the values the field stores – after the field’s owntransform– so the policy applies to keys, never node IRIs (pinned by tests, as the handoff asked).@lde/search-typesense(feat)facet: false, companionfacet: true, optional. Only theschemaoption resolves the policy; without it the field itself stays the facet (the projection’s reading without a schema). Not guarded: whether a policy applies cannot be told without the schema, and a schema-less build for a type with one meets Typesense’s own “could not find a facet field” on the first facet query, not a silently empty facet.InPlaceRebuildrefuses a declared dataset field that inherits a policy: it enumerates the indexed datasets by faceting that field, so a narrowed facet would hide the excluded datasets from the membership sweep.facet_byreadsphysicalFields(field, schema).facet; the membership operator follows the field’s engine facet status, notfield.facetable, sowhere: { locationCreated: { in: [kessel] } }compiles to:=and cannot partial-matchkessel/centrum.facet_counts[].field_nameis mapped back to the declared name once, before bothlabelLookupGroupsandparseSearchResponse.InPlaceRebuild: the run-open check now also fails, with the same drop instruction, when an existing collection lacks a companion its facets read.projectRoots→ import; an aligned place, two unaligned “Kessel”s and a place whose IRI extends Kessel’s by a path segment; every facet referencingPlaceshows one labelled bucket; the filter matches exactly; an in-place writer over a pre-policy collection fails at run open naming both companions.Docs:
search.md(Facet policy),search-typesense.md(collection fields, in-place check), ADR 22 (boundary sentence, facet section, two consequences).Decisions worth a look
schemais optional onphysicalFields, not required – the handoff left this open. Schema-optional is already the shape of this degradation in the codebase (projectDocument(node, type, schema?),buildCollectionDefinition(type, { schema? })): without one, no re-keying, no nesting, and now no companion. A required parameter would have been the one place breaking that pattern, for a function with only in-repo callers. So no!on either commit.derived one (which reads no referent and is re-keyed by nothing), do not. A single-valued field’s companion is taken from the value it stores, not from every value the graph offered, so a facet count is always reproducible by a filter on the field.about_facetbeside a policy-inheritingabout– the same collision already possible with_searchand_sort_<locale>companions. A reserved-suffix check would be a separate, schema-wide rule.n3becomes a devDependency ofsearch-typesense, for the end-to-end fixture: hand-written flat documents would encode the companion name by hand and miss exactly the projection ⟷ collection drift the test exists to catch. The lockfile change is that addition only; the post-release drift is left out.Sequencing for LOL
LOL must adopt PR 1 before this: with the policy in place and the stored values still node IRIs,
isCoveredrejects every Drapo node and thePlacefacets come back empty, silently. That is the documented footgun onfacetKeys. Under in-place rebuild, the first run after declaring the policy stops at run open asking for thePlace-referencing collections to be dropped, once.Fix #763