@loams/client 0.1.0-preview.2 → 0.1.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.
- package/README.md +85 -1
- package/dist/app.d.ts +149 -0
- package/dist/app.js +317 -0
- package/dist/builders/filter.d.ts +96 -0
- package/dist/builders/filter.js +190 -0
- package/dist/builders/schema.d.ts +50 -0
- package/dist/builders/schema.js +92 -0
- package/dist/builders/search.d.ts +159 -0
- package/dist/builders/search.js +307 -0
- package/dist/gen/facade.d.ts +293 -4
- package/dist/gen/facade.js +223 -1
- package/dist/handle.d.ts +67 -0
- package/dist/handle.js +107 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +14 -5
- package/dist/loams.d.ts +100 -5
- package/dist/loams.js +135 -11
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -2
- package/dist/runtime/batch.d.ts +29 -0
- package/dist/runtime/batch.js +95 -0
- package/dist/runtime/call.d.ts +63 -9
- package/dist/runtime/call.js +243 -41
- package/dist/runtime/consistency.d.ts +65 -11
- package/dist/runtime/consistency.js +212 -46
- package/dist/runtime/errors.d.ts +29 -2
- package/dist/runtime/errors.js +26 -0
- package/dist/runtime/pagination.js +8 -2
- package/dist/runtime/subscribe.d.ts +37 -0
- package/dist/runtime/subscribe.js +128 -0
- package/dist/runtime/transports.d.ts +21 -2
- package/dist/runtime/transports.js +30 -2
- package/dist/vendor/proto/collection-v1.d.ts +3 -0
- package/dist/vendor/proto/collection-v1.js +7 -0
- package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.d.ts +1610 -0
- package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.js +399 -0
- package/dist/vendor/proto/gen/loams/collection/v1/document_pb.d.ts +1054 -0
- package/dist/vendor/proto/gen/loams/collection/v1/document_pb.js +327 -0
- package/dist/vendor/proto/gen/loams/collection/v1/query_pb.d.ts +2101 -0
- package/dist/vendor/proto/gen/loams/collection/v1/query_pb.js +608 -0
- package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.d.ts +2299 -0
- package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.js +596 -0
- package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.d.ts +399 -0
- package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.js +118 -0
- package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.d.ts +182 -0
- package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.js +72 -0
- package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.d.ts +688 -0
- package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.js +180 -0
- package/package.json +2 -2
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// The search builder (SDKP §2.4): a `loams.collection.v1.SearchRequest`, one
|
|
2
|
+
// verb at a time.
|
|
3
|
+
//
|
|
4
|
+
// const request = Search.on('notes')
|
|
5
|
+
// .text('refund', { fields: ['body'] })
|
|
6
|
+
// .vector('embedding', [0.1, 0.2, 0.3, 0.4], { k: 10 })
|
|
7
|
+
// .fuse(rrf(60))
|
|
8
|
+
// .filter(Filter.eq('tags', 'policy'))
|
|
9
|
+
// .highlight('body')
|
|
10
|
+
// .select({ include: ['title'] })
|
|
11
|
+
// .limit(5);
|
|
12
|
+
// const answer = await loams.search.query(request.build());
|
|
13
|
+
//
|
|
14
|
+
// Each verb is pure: it returns a new builder and leaves the old one as it
|
|
15
|
+
// was, so a base query can be shared and specialised. `toJson()` is the
|
|
16
|
+
// request's proto3 JSON, which is how the builder corpus spells each case;
|
|
17
|
+
// `build()` decodes it into the generated message, refusing the requests the
|
|
18
|
+
// builder can tell are wrong (a weighted fusion with the wrong number of
|
|
19
|
+
// weights) before any network call.
|
|
20
|
+
var _a;
|
|
21
|
+
import { fromJson, toJson } from '@bufbuild/protobuf';
|
|
22
|
+
import { ValueSchema } from '@bufbuild/protobuf/wkt';
|
|
23
|
+
import { Code } from '@connectrpc/connect';
|
|
24
|
+
import { SearchRequestSchema } from '../vendor/proto/gen/loams/collection/v1/query_pb.js';
|
|
25
|
+
import { ConsistencyToken } from '../runtime/consistency.js';
|
|
26
|
+
import { InvalidArgumentError } from '../runtime/errors.js';
|
|
27
|
+
import { Filter } from './filter.js';
|
|
28
|
+
/** Reciprocal-rank fusion; `k` absent is the engine's 60. */
|
|
29
|
+
export function rrf(k) {
|
|
30
|
+
return { rrf: k === undefined ? {} : { k } };
|
|
31
|
+
}
|
|
32
|
+
/** Distance-based score fusion, with the engine's parameters. */
|
|
33
|
+
export function dbsf() {
|
|
34
|
+
return { dbsf: {} };
|
|
35
|
+
}
|
|
36
|
+
/** A weighted sum, one weight per retriever in the order they were added. */
|
|
37
|
+
export function weighted(...weights) {
|
|
38
|
+
return { weightedSum: { weights } };
|
|
39
|
+
}
|
|
40
|
+
/** The fusions, as one namespace. */
|
|
41
|
+
export const Fusion = { rrf, dbsf, weighted };
|
|
42
|
+
const order = (value) => (value === 'asc' ? 'SORT_ORDER_ASC' : 'SORT_ORDER_DESC');
|
|
43
|
+
/** The sort keys. The primary key ascending always breaks ties on the server,
|
|
44
|
+
* but `queryAll` needs it spelled out to page. */
|
|
45
|
+
export const Sort = {
|
|
46
|
+
/** By score; descending unless told otherwise. */
|
|
47
|
+
score(direction) {
|
|
48
|
+
return { score: direction === undefined ? {} : { order: order(direction) } };
|
|
49
|
+
},
|
|
50
|
+
/** By primary key; ascending unless told otherwise. */
|
|
51
|
+
pk(direction) {
|
|
52
|
+
return { pk: direction === undefined ? {} : { order: order(direction) } };
|
|
53
|
+
},
|
|
54
|
+
/** By a fast field. `missing` puts documents without it first or last. */
|
|
55
|
+
field(name, direction, missing) {
|
|
56
|
+
const key = { field: name };
|
|
57
|
+
if (direction !== undefined) {
|
|
58
|
+
key.order = order(direction);
|
|
59
|
+
}
|
|
60
|
+
if (missing !== undefined) {
|
|
61
|
+
key.missing = missing === 'first' ? 'MISSING_ORDER_FIRST' : 'MISSING_ORDER_LAST';
|
|
62
|
+
}
|
|
63
|
+
return { field: key };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
/** The proto3 JSON of a `loams.collection.v1.Consistency`. */
|
|
67
|
+
export function consistencyJson(level) {
|
|
68
|
+
if (level === 'strong') {
|
|
69
|
+
return { freshness: 'FRESHNESS_STRONG' };
|
|
70
|
+
}
|
|
71
|
+
if (level === 'eventual') {
|
|
72
|
+
return { freshness: 'FRESHNESS_EVENTUAL' };
|
|
73
|
+
}
|
|
74
|
+
const token = typeof level === 'string' ? ConsistencyToken.parse(level) : level;
|
|
75
|
+
return { atLeast: token.toString() };
|
|
76
|
+
}
|
|
77
|
+
function sortValueJson(value) {
|
|
78
|
+
if (value !== null &&
|
|
79
|
+
typeof value === 'object' &&
|
|
80
|
+
!Array.isArray(value) &&
|
|
81
|
+
value.$typeName === 'google.protobuf.Value') {
|
|
82
|
+
return toJson(ValueSchema, value);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function textQuery(text, fields) {
|
|
87
|
+
if (typeof text !== 'string') {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
if (fields === undefined || fields.length === 0) {
|
|
91
|
+
return Filter.queryString(text);
|
|
92
|
+
}
|
|
93
|
+
if (fields.length === 1) {
|
|
94
|
+
return Filter.match(fields[0], text);
|
|
95
|
+
}
|
|
96
|
+
return Filter.multiMatch(fields, text);
|
|
97
|
+
}
|
|
98
|
+
/** The search builder. */
|
|
99
|
+
export class Search {
|
|
100
|
+
#json;
|
|
101
|
+
constructor(json) {
|
|
102
|
+
this.#json = json;
|
|
103
|
+
}
|
|
104
|
+
/** A search of `collection` (a name or an alias). */
|
|
105
|
+
static on(collection, options = {}) {
|
|
106
|
+
return new _a({
|
|
107
|
+
...(options.namespace === undefined ? {} : { namespace: options.namespace }),
|
|
108
|
+
collection,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** A builder over an existing request's proto3 JSON. */
|
|
112
|
+
static fromJson(json) {
|
|
113
|
+
return new _a(structuredClone(json));
|
|
114
|
+
}
|
|
115
|
+
#with(patch) {
|
|
116
|
+
return new _a({ ...structuredClone(this.#json), ...patch });
|
|
117
|
+
}
|
|
118
|
+
#retrievers() {
|
|
119
|
+
return this.#json.retrievers ?? [];
|
|
120
|
+
}
|
|
121
|
+
/** The namespace, when it is not the client's default. */
|
|
122
|
+
namespace(namespace) {
|
|
123
|
+
return this.#with({ namespace });
|
|
124
|
+
}
|
|
125
|
+
/** A text retriever. A string is a `match`/`multi_match`/`query_string`
|
|
126
|
+
* by `fields`; a `Filter` (`match`, `multiMatch`, `phrase`…) is used as is. */
|
|
127
|
+
text(text, options = {}) {
|
|
128
|
+
const retriever = { query: textQuery(text, options.fields) };
|
|
129
|
+
if (options.k !== undefined) {
|
|
130
|
+
retriever.k = options.k;
|
|
131
|
+
}
|
|
132
|
+
return this.#with({ retrievers: [...this.#retrievers(), { text: retriever }] });
|
|
133
|
+
}
|
|
134
|
+
/** A dense-vector kNN retriever on `field`. */
|
|
135
|
+
vector(field, values, options = {}) {
|
|
136
|
+
const retriever = { field, query: [...values] };
|
|
137
|
+
if (options.k !== undefined) {
|
|
138
|
+
retriever.k = options.k;
|
|
139
|
+
}
|
|
140
|
+
const params = {};
|
|
141
|
+
for (const key of ['exact', 'nprobes', 'refineFactor', 'ef', 'oversampling', 'distance']) {
|
|
142
|
+
if (options[key] !== undefined) {
|
|
143
|
+
params[key] = options[key];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (Object.keys(params).length > 0) {
|
|
147
|
+
retriever.params = params;
|
|
148
|
+
}
|
|
149
|
+
if (options.filter !== undefined) {
|
|
150
|
+
retriever.filter = options.filter;
|
|
151
|
+
}
|
|
152
|
+
return this.#with({ retrievers: [...this.#retrievers(), { vector: retriever }] });
|
|
153
|
+
}
|
|
154
|
+
/** A sparse-vector retriever: `{indices, values}`, or a term-to-weight map
|
|
155
|
+
* whose keys are the indices. */
|
|
156
|
+
sparse(field, terms, options = {}) {
|
|
157
|
+
const query = 'indices' in terms && Array.isArray(terms.indices)
|
|
158
|
+
? { indices: [...terms.indices], values: [...terms.values] }
|
|
159
|
+
: (() => {
|
|
160
|
+
const entries = Object.entries(terms);
|
|
161
|
+
return { indices: entries.map(([index]) => Number(index)), values: entries.map(([, v]) => v) };
|
|
162
|
+
})();
|
|
163
|
+
const retriever = { field, query };
|
|
164
|
+
if (options.k !== undefined) {
|
|
165
|
+
retriever.k = options.k;
|
|
166
|
+
}
|
|
167
|
+
if (options.filter !== undefined) {
|
|
168
|
+
retriever.filter = options.filter;
|
|
169
|
+
}
|
|
170
|
+
return this.#with({ retrievers: [...this.#retrievers(), { sparse: retriever }] });
|
|
171
|
+
}
|
|
172
|
+
/** How the retrievers' ranked lists combine: `rrf()`, `dbsf()` or
|
|
173
|
+
* `weighted(…)`. */
|
|
174
|
+
fuse(fusion) {
|
|
175
|
+
return this.#with({ fusion });
|
|
176
|
+
}
|
|
177
|
+
/** The filter every candidate must match. */
|
|
178
|
+
filter(filter) {
|
|
179
|
+
return this.#with({ filter });
|
|
180
|
+
}
|
|
181
|
+
/** The sort, replacing any earlier one. */
|
|
182
|
+
sort(...keys) {
|
|
183
|
+
return this.#with({ sort: keys });
|
|
184
|
+
}
|
|
185
|
+
/** The window size; the engine's default is 10. */
|
|
186
|
+
limit(limit) {
|
|
187
|
+
return this.#with({ limit });
|
|
188
|
+
}
|
|
189
|
+
/** How many hits to skip before the window. */
|
|
190
|
+
offset(offset) {
|
|
191
|
+
return this.#with({ offset });
|
|
192
|
+
}
|
|
193
|
+
/** The page after the hit whose `sortValues` these are. */
|
|
194
|
+
after(sortValues) {
|
|
195
|
+
return this.#with({ searchAfter: sortValues.map(sortValueJson) });
|
|
196
|
+
}
|
|
197
|
+
/** Drops hits scoring below `score`. */
|
|
198
|
+
minScore(score) {
|
|
199
|
+
return this.#with({ scoreThreshold: score });
|
|
200
|
+
}
|
|
201
|
+
/** What to return of each hit. */
|
|
202
|
+
select(options) {
|
|
203
|
+
const select = {};
|
|
204
|
+
if (options.noSource === true) {
|
|
205
|
+
select.source = 'none';
|
|
206
|
+
}
|
|
207
|
+
else if (options.include !== undefined || options.exclude !== undefined) {
|
|
208
|
+
const source = {};
|
|
209
|
+
if (options.include !== undefined) {
|
|
210
|
+
source.include = [...options.include];
|
|
211
|
+
}
|
|
212
|
+
if (options.exclude !== undefined) {
|
|
213
|
+
source.exclude = [...options.exclude];
|
|
214
|
+
}
|
|
215
|
+
select.source = source;
|
|
216
|
+
}
|
|
217
|
+
if (options.vectors !== undefined) {
|
|
218
|
+
select.vectors = [...options.vectors];
|
|
219
|
+
}
|
|
220
|
+
if (options.fields !== undefined) {
|
|
221
|
+
select.fields = [...options.fields];
|
|
222
|
+
}
|
|
223
|
+
return this.#with({ select });
|
|
224
|
+
}
|
|
225
|
+
/** Highlights `field` in each hit; call once per field. */
|
|
226
|
+
highlight(field, options = {}) {
|
|
227
|
+
const entry = { field };
|
|
228
|
+
if (options.pre !== undefined) {
|
|
229
|
+
entry.preTag = options.pre;
|
|
230
|
+
}
|
|
231
|
+
if (options.post !== undefined) {
|
|
232
|
+
entry.postTag = options.post;
|
|
233
|
+
}
|
|
234
|
+
if (options.fragmentSize !== undefined) {
|
|
235
|
+
entry.fragmentSize = options.fragmentSize;
|
|
236
|
+
}
|
|
237
|
+
if (options.fragments !== undefined) {
|
|
238
|
+
entry.numberOfFragments = options.fragments;
|
|
239
|
+
}
|
|
240
|
+
const existing = this.#json.highlight?.fields ?? [];
|
|
241
|
+
return this.#with({ highlight: { fields: [...existing, entry] } });
|
|
242
|
+
}
|
|
243
|
+
/** Groups the hits by `field`: `size` hits per group, `limit` groups. */
|
|
244
|
+
groupBy(field, options = {}) {
|
|
245
|
+
const group = { field };
|
|
246
|
+
if (options.size !== undefined) {
|
|
247
|
+
group.groupSize = options.size;
|
|
248
|
+
}
|
|
249
|
+
if (options.limit !== undefined) {
|
|
250
|
+
group.limit = options.limit;
|
|
251
|
+
}
|
|
252
|
+
return this.#with({ groupBy: group });
|
|
253
|
+
}
|
|
254
|
+
/** Walks `graph` from the top hits and returns their neighbours. */
|
|
255
|
+
expand(graph, options = {}) {
|
|
256
|
+
const spec = { graph };
|
|
257
|
+
const copy = ['fromField', 'seedLabel', 'seeds', 'hops', 'limitPerSeed', 'limit'];
|
|
258
|
+
for (const key of copy) {
|
|
259
|
+
if (options[key] !== undefined) {
|
|
260
|
+
spec[key] = options[key];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (options.direction !== undefined) {
|
|
264
|
+
spec.direction = `EXPAND_DIRECTION_${options.direction.toUpperCase()}`;
|
|
265
|
+
}
|
|
266
|
+
if (options.edgeTypes !== undefined) {
|
|
267
|
+
spec.edgeTypes = [...options.edgeTypes];
|
|
268
|
+
}
|
|
269
|
+
if (options.output !== undefined) {
|
|
270
|
+
spec.output = `EXPAND_OUTPUT_${options.output.toUpperCase()}`;
|
|
271
|
+
}
|
|
272
|
+
return this.#with({ expandSpec: spec });
|
|
273
|
+
}
|
|
274
|
+
/** How fresh the read must be. */
|
|
275
|
+
consistency(level) {
|
|
276
|
+
return this.#with({ consistency: consistencyJson(level) });
|
|
277
|
+
}
|
|
278
|
+
/** The collection searched. */
|
|
279
|
+
get collection() {
|
|
280
|
+
return this.#json.collection;
|
|
281
|
+
}
|
|
282
|
+
/** Whether the sort ends in a primary-key tiebreaker, which `queryAll`
|
|
283
|
+
* needs to page without skipping or repeating a hit. */
|
|
284
|
+
get hasTiebreak() {
|
|
285
|
+
const sort = this.#json.sort;
|
|
286
|
+
const last = sort?.[sort.length - 1];
|
|
287
|
+
return last !== undefined && 'pk' in last;
|
|
288
|
+
}
|
|
289
|
+
/** The request's proto3 JSON. */
|
|
290
|
+
toJson() {
|
|
291
|
+
return structuredClone(this.#json);
|
|
292
|
+
}
|
|
293
|
+
/** The request as the generated message, checked. */
|
|
294
|
+
build() {
|
|
295
|
+
const weights = this.#json.fusion?.weightedSum
|
|
296
|
+
?.weights;
|
|
297
|
+
const retrievers = this.#retrievers().length;
|
|
298
|
+
if (weights !== undefined && weights.length !== retrievers) {
|
|
299
|
+
throw new InvalidArgumentError(`a weighted fusion needs one weight per retriever: ${weights.length} weights for ${retrievers} retrievers`, {
|
|
300
|
+
code: Code.InvalidArgument,
|
|
301
|
+
metadata: { field: 'fusion.weighted_sum.weights' },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return fromJson(SearchRequestSchema, this.#json);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
_a = Search;
|