@lde/search-typesense 0.20.0 → 0.21.1

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.
@@ -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,EAEL,KAAK,MAAM,EAEX,KAAK,WAAW,EAChB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAarB;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;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;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,wBAA6B,GACrC,YAAY,CAAC,MAAM,CAAC,CAkCtB;AAqMD;;;;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,EAEX,KAAK,WAAW,EAChB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAarB;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;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;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,wBAA6B,GACrC,YAAY,CAAC,MAAM,CAAC,CAkCtB;AA4PD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
@@ -109,35 +109,85 @@ function compileFilterBy(where, searchType, onIgnoredFilter) {
109
109
  .filter((clause) => clause !== undefined)
110
110
  .join(' && ');
111
111
  }
112
+ /**
113
+ * One `where` clause: each of its criteria compiled and OR-joined with `||`,
114
+ * parenthesised so the disjunction binds tighter than the `&&` between clauses.
115
+ * A one-criterion clause – the ordinary single-field filter – compiles to a bare
116
+ * term: the parentheses would be harmless but would make every ordinary
117
+ * `filter_by` noisier to read and to assert on.
118
+ *
119
+ * A criterion that yields no term is dropped or voids the clause depending on
120
+ * **why**, because the two readings differ under `||`:
121
+ *
122
+ * - a **vacuous** criterion states no constraint (an empty `in` on a value
123
+ * field – a facet UI with nothing selected – or a `range` with no usable
124
+ * bound). It is therefore *true*, and `true || X` is true, so the whole clause
125
+ * constrains nothing and is skipped. Dropping only the criterion would
126
+ * NARROW the result to its siblings, the opposite of what an unset filter
127
+ * means;
128
+ * - an **unsatisfiable** or malformed criterion (an empty `id` membership, an
129
+ * unknown field, an operator that mismatches the field’s kind) is *false*, and
130
+ * `false || X` is X, so it drops out and its siblings still stand.
131
+ *
132
+ * A clause left with no terms either way is skipped and reported to
133
+ * `onIgnoredFilter`.
134
+ */
112
135
  function compileFilter(filter, searchType) {
136
+ const terms = [];
137
+ for (const criterion of filter.or) {
138
+ const outcome = compileCriterion(criterion, searchType);
139
+ if (outcome === VACUOUS) {
140
+ return undefined;
141
+ }
142
+ if (outcome !== UNUSABLE) {
143
+ terms.push(outcome);
144
+ }
145
+ }
146
+ if (terms.length === 0) {
147
+ return undefined;
148
+ }
149
+ return terms.length === 1 ? terms[0] : `(${terms.join(' || ')})`;
150
+ }
151
+ /** A criterion that states **no constraint** – true for every document. */
152
+ const VACUOUS = Symbol('vacuous');
153
+ /** A criterion that matches nothing, or cannot be compiled at all – false. */
154
+ const UNUSABLE = Symbol('unusable');
155
+ /**
156
+ * What one criterion contributes: a Typesense term, or which of the two ways it
157
+ * yields none. The distinction is load-bearing under `||` ({@link compileFilter})
158
+ * and cannot be recovered from a bare `undefined`, so it is returned rather than
159
+ * re-derived by a second pass over the same rules.
160
+ */
161
+ function compileCriterion(criterion, searchType) {
113
162
  // `id` is the Typesense document key, not a declared field. Exact `:=`
114
163
  // membership, like a non-facet field ({@link compileMembership}), so an IRI
115
164
  // cannot partial-match on a shared path segment. (`fetchLabels` resolves
116
165
  // labels with the looser `id:[…]`; these are deliberately not the same
117
- // clause.)
118
- if (filter.field === ID_FIELD) {
119
- return 'in' in filter && filter.in.length > 0
120
- ? `${ID_FIELD}:=[${filter.in.map(escapeFilterValue).join(',')}]`
121
- : undefined;
166
+ // clause.) An empty identity membership enumerates NO document, so it is
167
+ // unusable rather than vacuous – the one place the two readings diverge.
168
+ if (criterion.field === ID_FIELD) {
169
+ return 'in' in criterion && criterion.in.length > 0
170
+ ? `${ID_FIELD}:=[${criterion.in.map(escapeFilterValue).join(',')}]`
171
+ : UNUSABLE;
122
172
  }
123
- const field = fieldNamed(searchType, filter.field);
173
+ const field = fieldNamed(searchType, criterion.field);
124
174
  if (field === undefined) {
125
- return undefined;
175
+ return UNUSABLE;
126
176
  }
127
- // A clause whose operator does not match the field's kind (e.g. `range` on a
128
- // keyword) would reach the engine as garbage syntax – skip it instead.
129
- if (filterOperatorFor(field.kind) !== filterOperator(filter)) {
130
- return undefined;
177
+ // A criterion whose operator does not match the field's kind (e.g. `range` on
178
+ // a keyword) would reach the engine as garbage syntax – skip it instead.
179
+ if (filterOperatorFor(field.kind) !== filterOperator(criterion)) {
180
+ return UNUSABLE;
131
181
  }
132
- if ('in' in filter) {
133
- return filter.in.length > 0
134
- ? compileMembership(field, filter.in)
135
- : undefined;
182
+ if ('in' in criterion) {
183
+ return criterion.in.length > 0
184
+ ? compileMembership(field, criterion.in)
185
+ : VACUOUS;
136
186
  }
137
- if ('range' in filter) {
138
- return compileRange(field, filter.range);
187
+ if ('range' in criterion) {
188
+ return compileRange(field, criterion.range) ?? VACUOUS;
139
189
  }
140
- return `${field.name}:=${filter.is}`;
190
+ return `${field.name}:=${criterion.is}`;
141
191
  }
142
192
  /**
143
193
  * A membership clause. A non-facet (tokenized) field uses the exact `:=`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search-typesense",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
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.17.0",
28
+ "@lde/search": "^0.18.1",
29
29
  "@lde/text-normalization": "^0.1.2",
30
30
  "tslib": "^2.3.0",
31
31
  "typesense": "^3.0.6"