Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions packages/search/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,23 +927,52 @@ function tuplesOf(
const identity = field.ref.identity;
const weldable = nestedType.fields
.filter((nested) => nested.filterable === true || nested.name === identity)
.map((nested) => irAlias(nestedType, nested))
// A leaf the frame carries at most one value for is already a tuple
// position; splitting it would copy the node to no purpose.
.filter((alias) => valuesOf(node, alias).length > 1);
.map((nested) => ({ nested, alias: irAlias(nestedType, nested) }))
// A leaf the frame carries at most one READABLE value for is already a
// tuple position; splitting it would copy the node to no purpose.
.filter(({ nested, alias }) => splittable(node, alias, nested).length > 1);
if (weldable.length === 0) {
return [node];
}
let tuples: FramedNode[] = [node];
for (const alias of weldable) {
const values = valuesOf(node, alias);
for (const { nested, alias } of weldable) {
const values = splittable(node, alias, nested);
tuples = tuples.flatMap((tuple) =>
values.map((value) => ({ ...tuple, [alias]: value })),
);
}
return tuples;
}

/**
* The framed values of one leaf that fan-out may split on: those the field can
* actually READ.
*
* A leaf reads its own kind of value and passes over the rest: a `keyword`
* takes literals and ignores an IRI stated beside them, which real data does –
* a role named both as a string and as a Wikidata entity on one node.
* Splitting on a value the leaf then reads nothing from mints an entry the leaf
* is absent from – the same endpoint, apparently in no role at all, shown to a
* reader and matched by a filter. So the split follows the reader.
*
* `keyword` is the only kind that needs narrowing. A `text` field has no filter
* operator (`filterOperatorFor`), so nothing welds it and it is never a tuple
* position. A `reference` is, and both of its shapes are meaningful: an IRI,
* and – for a {@link ReferenceStrategy.local local} lookup – a node the graph
* named inline, which has no IRI and is a referent all the same.
*/
function splittable(
node: FramedNode,
alias: string,
field: SearchField,
): readonly unknown[] {
const values = valuesOf(node, alias);
if (field.kind === 'keyword') {
return values.filter((value) => literalString(value) !== undefined);
}
return values;
}

// --- Framed-IR readers: read a field’s value off the framed node by its
// {@link irAlias IR Alias} key. Internal to projection – a `derive` reads the
// projected document, never the node, so `path` stays the whole statement of
Expand Down
28 changes: 28 additions & 0 deletions packages/search/test/qualified-relation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,34 @@ describe('fanning an edge out into one entry per tuple', () => {
expect(entries).toEqual([{ role: 'etser' }]);
});

it('splits only on values the leaf can read', () => {
// Real data states a role both as a string and as a Wikidata entity on one
// node. A `keyword` leaf reads literals and passes over the IRI, so
// splitting on it would mint an entry the role is absent from – the same
// endpoint, apparently in no role at all, shown to a reader and matched by
// a filter.
const tagged = {
'@id': 'https://ex/work/16',
[workKey('creator')]: [
{
[edgeKey('role')]: [
{ '@value': 'fotograaf' },
{ '@id': 'http://www.wikidata.org/entity/Q33231' },
],
[edgeKey('creator')]: [
{ '@id': 'https://a/1', [personKey('sameAs')]: [{ '@id': RKD }] },
],
},
],
};
const entries = entriesOf(
projectDocument(tagged, work, searchSchema(work, person, weldableEdge)),
);

expect(entries).toHaveLength(1);
expect(entries[0].role).toBe('fotograaf');
});

it('treats the identity field as a tuple position', () => {
// Nothing welds the identity field by name, but the flat companion
// harvested from it is the leaf a weld uses to name the endpoint. An entry
Expand Down
2 changes: 1 addition & 1 deletion packages/search/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default mergeConfig(
thresholds: {
functions: 100,
lines: 100,
branches: 99.71,
branches: 99.72,
statements: 100,
},
},
Expand Down