@lde/search-typesense 0.25.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,13 +39,16 @@ export interface BuildSearchParamsOptions {
39
39
  */
40
40
  readonly maxFacetValues?: number;
41
41
  /**
42
- * Called for each `where` clause that compiles to nothing and is therefore
43
- * skipped: an unknown field, an operator that does not match the field’s
44
- * kind ({@link filterOperatorFor}), an empty `in` list, or a `range` with no
45
- * usable bound. Skipping keeps a malformed clause from reaching the engine
46
- * as garbage; supply this to log it instead of losing it silently. Through
47
- * the engine, a structurally invalid query throws up front
48
- * (`assertValidQuery`), so there only the vacuous clauses reach this.
42
+ * Called for each `where` clause that does not compile as the caller wrote
43
+ * it – either because it states no constraint (an empty `in` list, a `range`
44
+ * with no usable bound, no criteria at all) and is skipped, or because every
45
+ * criterion is malformed (an unknown field, an operator that does not match
46
+ * the field’s kind – {@link filterOperatorFor}) or unsatisfiable, leaving a
47
+ * clause no document can match. Supply this to log the clause instead of
48
+ * losing it silently; the compiled query itself is faithful either way.
49
+ * Through the engine, a structurally invalid query throws up front
50
+ * (`assertValidQuery`), so there only the clauses that state no constraint
51
+ * reach this.
49
52
  */
50
53
  readonly onIgnoredFilter?: (filter: Filter) => void;
51
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"query-compiler.d.ts","sourceRoot":"","sources":["../src/query-compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,QAAQ,EAEb,KAAK,WAAW,EAChB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAarB;mDACmD;AACnD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,UAAU,GAAG,SAAS,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,wBAA6B,GACrC,YAAY,CAAC,MAAM,CAAC,CA8BtB;AAsSD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
1
+ {"version":3,"file":"query-compiler.d.ts","sourceRoot":"","sources":["../src/query-compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,QAAQ,EAEb,KAAK,WAAW,EAChB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAarB;mDACmD;AACnD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,UAAU,GAAG,SAAS,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,wBAA6B,GACrC,YAAY,CAAC,MAAM,CAAC,CA8BtB;AA+UD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
@@ -100,13 +100,15 @@ function queryFields(searchType, locale) {
100
100
  }
101
101
  return { names, weights };
102
102
  }
103
- /** AND-join the compiled `where` clauses; a clause that compiles to nothing is
104
- * skipped and reported to `onIgnoredFilter`. */
103
+ /** AND-join the compiled `where` clauses. A clause that states no constraint is
104
+ * skipped; one that can match nothing compiles to {@link MATCHES_NOTHING}
105
+ * rather than being skipped ({@link compileFilter}). Either way it did not
106
+ * compile as written, so it is reported to `onIgnoredFilter`. */
105
107
  function compileFilterBy(where, searchType, options) {
106
108
  return where
107
109
  .map((filter) => {
108
110
  const clause = compileFilter(filter, searchType, options);
109
- if (clause === undefined) {
111
+ if (clause === undefined || clause === MATCHES_NOTHING) {
110
112
  options.onIgnoredFilter?.(filter);
111
113
  }
112
114
  return clause;
@@ -134,8 +136,16 @@ function compileFilterBy(where, searchType, options) {
134
136
  * unknown field, an operator that mismatches the field’s kind) is *false*, and
135
137
  * `false || X` is X, so it drops out and its siblings still stand.
136
138
  *
137
- * A clause left with no terms either way is skipped and reported to
138
- * `onIgnoredFilter`.
139
+ * Which reading applies decides what a clause left with **no terms** compiles
140
+ * to, and the two are opposites: a vacuous clause states no constraint, so it
141
+ * is skipped; an all-false clause states the query can have no answer, so it
142
+ * compiles to {@link MATCHES_NOTHING}. Skipping that one instead would drop a
143
+ * conjunct and widen the query – `false` compiled as `true`. Both are reported
144
+ * to `onIgnoredFilter`, because neither compiles to what the caller wrote.
145
+ *
146
+ * The engine never meets either: `assertValidQuery` rejects every malformed
147
+ * criterion, and `isUnsatisfiable` short-circuits the empty `id` membership
148
+ * before a query is dispatched.
139
149
  */
140
150
  function compileFilter(filter, searchType, options) {
141
151
  const terms = [];
@@ -149,10 +159,30 @@ function compileFilter(filter, searchType, options) {
149
159
  }
150
160
  }
151
161
  if (terms.length === 0) {
152
- return undefined;
162
+ // Every criterion was unusable, so the clause is FALSE – and `false && X`
163
+ // is false, however many clauses stand beside it. Leaving it out would drop
164
+ // a conjunct, and a conjunct missing from `filter_by` constrains nothing:
165
+ // the query would come back WIDER than the caller wrote, which is how a
166
+ // misspelled field name used to return the whole collection.
167
+ //
168
+ // A clause carrying no criteria at all says nothing rather than saying
169
+ // false, so it stays the vacuous no-op it reads as.
170
+ return filter.or.length === 0 ? undefined : MATCHES_NOTHING;
153
171
  }
154
172
  return terms.length === 1 ? terms[0] : `(${terms.join(' || ')})`;
155
173
  }
174
+ /**
175
+ * The term for a clause that can match no document: an **empty identity
176
+ * membership** – “the document is one of no documents”. Not a sentinel value
177
+ * but the literal reading, and the same one {@link isUnsatisfiable} gives an
178
+ * empty `id` membership in the IR; Typesense answers it with zero hits.
179
+ *
180
+ * A filter language has no keyword for `false`, so this stands in for one. It
181
+ * must be a term the engine *applies* rather than ignores – an empty string
182
+ * (``id:=` ` ``) is rejected outright as a filter value, and an omitted clause
183
+ * is read as true.
184
+ */
185
+ const MATCHES_NOTHING = `${ID_FIELD}:=[]`;
156
186
  /** A criterion that states **no constraint** – true for every document. */
157
187
  const VACUOUS = Symbol('vacuous');
158
188
  /** A criterion that matches nothing, or cannot be compiled at all – false. */
@@ -237,11 +267,21 @@ function compileMembership(field, values) {
237
267
  ? `${field.name}:=${list}`
238
268
  : `${field.name}:${list}`;
239
269
  }
240
- /** An inclusive Typesense range clause, or `undefined` when neither bound is set. */
270
+ /** An inclusive Typesense range clause, or `undefined` when neither bound is
271
+ * usable. Which of the two readings that is – the caller set no bounds, or the
272
+ * codec rejected the ones they set – is deliberately NOT decided here: a
273
+ * criterion dropped from a clause narrows the query while a clause dropped
274
+ * from the conjunction widens it, so the same reading cannot be right in both
275
+ * positions. `validateQuery` rejects an unreadable bound outright instead,
276
+ * which is the only answer that neither widens nor narrows. */
241
277
  function compileRange(field, range) {
242
278
  const name = field.name;
243
- const min = storedBound(field, range.min);
244
- const max = storedBound(field, range.max);
279
+ // A bound a caller sent as `null` – a GraphQL variable left unfilled, which
280
+ // the surface passes through – is a bound NOT SET, not a bound of `null`.
281
+ // Read literally it reaches the engine as `datePosted:[null..…]`, which
282
+ // Typesense rejects outright.
283
+ const min = storedBound(field, range.min ?? undefined);
284
+ const max = storedBound(field, range.max ?? undefined);
245
285
  if (min !== undefined && max !== undefined) {
246
286
  return `${name}:[${min}..${max}]`;
247
287
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search-typesense",
3
- "version": "0.25.1",
3
+ "version": "0.27.0",
4
4
  "description": "Typesense implementation of the @lde/search SearchEngine port: collection-schema builder, query compiler, label-resolving result reconstruction, and blue/green index lifecycle. Engine-specific (Typesense) but domain-agnostic.",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/ldelements/lde.git",
@@ -25,7 +25,7 @@
25
25
  "!**/*.tsbuildinfo"
26
26
  ],
27
27
  "dependencies": {
28
- "@lde/search": "^0.22.1",
28
+ "@lde/search": "^0.23.0",
29
29
  "@lde/text-normalization": "^0.1.2",
30
30
  "tslib": "^2.3.0",
31
31
  "typesense": "^3.0.6"