@dbx-tools/search 0.6.9

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.
@@ -0,0 +1,304 @@
1
+ /**
2
+ * A small, Meilisearch-shaped client over Databricks AI Search (Vector
3
+ * Search). The whole point of this module is ergonomics: the Databricks SDK's
4
+ * `vectorSearchIndexes.queryIndex({ index_name, columns, query_text,
5
+ * query_type, num_results, filters_json })` is powerful but verbose, and the
6
+ * response is columnar. This client hides all of that behind two objects:
7
+ *
8
+ * ```ts
9
+ * const client = createSearchClient();
10
+ * const index = client.index("main.support.docs");
11
+ * const { hits } = await index.search("reset my password", { limit: 5 });
12
+ * await index.addDocuments([{ id: "42", title: "Reset", body: "..." }]);
13
+ * ```
14
+ *
15
+ * `client.search(query, opts)` searches the default index; `client.index(name)`
16
+ * returns a handle bound to one index; `client.universalSearch(query)` fans a
17
+ * query across several indexes and merges the results (Meilisearch's federated
18
+ * / multi-search, the "universal search" the caller asked for). Everything is
19
+ * async and cancellable, resolves the OBO workspace client from the active
20
+ * AppKit execution context (falling back to a service-principal client outside
21
+ * a request), and returns the browser-safe shapes from
22
+ * `@dbx-tools/shared-search`.
23
+ *
24
+ * Autocomplete is just a search with a small `limit` and the raw query text -
25
+ * hybrid mode already prefix-matches, so no separate endpoint is needed; the
26
+ * {@link SearchIndex.autocomplete} helper is a thin, self-documenting alias.
27
+ *
28
+ * @module
29
+ */
30
+ import { appkit } from "@dbx-tools/appkit";
31
+ import type { SearchDocument, SearchMode, SearchResult, UpsertResult } from "@dbx-tools/shared-search";
32
+ import { type ResolvedSearchConfig } from "./config.ts";
33
+ import type { LakebaseSearchBackend } from "./lakebase.ts";
34
+ type WorkspaceClientLike = appkit.WorkspaceClientLike;
35
+ /** Options accepted by a single-index search. */
36
+ export interface SearchOptions {
37
+ /** Maximum hits to return. Defaults to the configured page size. */
38
+ limit?: number;
39
+ /** Match mode. Defaults to the configured mode (hybrid). */
40
+ mode?: SearchMode;
41
+ /** Columns to return per hit. Defaults to the index's configured columns. */
42
+ columns?: readonly string[];
43
+ /** Attribute filters as `{ column: value }` or `{ column: { ">=": n } }`. */
44
+ filter?: Record<string, unknown>;
45
+ /** Drop hits below this score. */
46
+ scoreThreshold?: number;
47
+ /** External cancellation. */
48
+ signal?: AbortSignal;
49
+ }
50
+ /** Options accepted by a universal (federated) search. */
51
+ export interface UniversalSearchOptions {
52
+ /** Indexes to search. Defaults to every known index. */
53
+ indexes?: readonly string[];
54
+ /** Maximum hits per index before merging. */
55
+ limit?: number;
56
+ /** Match mode. */
57
+ mode?: SearchMode;
58
+ /** External cancellation. */
59
+ signal?: AbortSignal;
60
+ }
61
+ /**
62
+ * A handle bound to one index. Modeled on Meilisearch's `client.index(uid)`:
63
+ * `search` / `autocomplete` read, `addDocuments` / `deleteDocuments` write (to
64
+ * a direct-access index), and `info` fetches the live definition.
65
+ */
66
+ export declare class SearchIndex {
67
+ readonly name: string;
68
+ private readonly client;
69
+ constructor(name: string, client: SearchClient);
70
+ /** Search this index. See {@link SearchClient.search}. */
71
+ search(query: string, options?: SearchOptions): Promise<SearchResult>;
72
+ /**
73
+ * Autocomplete against this index: a search with a small default `limit` and
74
+ * the raw prefix as the query. Hybrid mode already handles prefixes, so this
75
+ * is a self-documenting alias rather than a separate code path.
76
+ */
77
+ autocomplete(prefix: string, options?: SearchOptions): Promise<SearchResult>;
78
+ /** Add or update documents in this (direct-access) index. */
79
+ addDocuments(documents: SearchDocument[], signal?: AbortSignal): Promise<UpsertResult>;
80
+ /** Delete documents from this (direct-access) index by primary key. */
81
+ deleteDocuments(ids: Array<string | number>, signal?: AbortSignal): Promise<UpsertResult>;
82
+ /** Fetch this index's live definition (primary key, columns, readiness). */
83
+ info(signal?: AbortSignal): Promise<IndexInfo>;
84
+ /** Trigger a sync of this (Delta Sync) index from its source table. */
85
+ sync(signal?: AbortSignal): Promise<void>;
86
+ /** Delete this index. */
87
+ delete(signal?: AbortSignal): Promise<void>;
88
+ /** Create this index if it does not exist, otherwise return the existing one. */
89
+ ensure(options?: CreateIndexOptions): Promise<IndexInfo>;
90
+ }
91
+ /** A resolved live index definition. */
92
+ export interface IndexInfo {
93
+ name: string;
94
+ endpoint?: string;
95
+ primaryKey?: string;
96
+ columns: string[];
97
+ ready: boolean;
98
+ rowCount?: number;
99
+ /** True for a DIRECT_ACCESS index (you supply vectors; queries embed client-side). */
100
+ directAccess?: boolean;
101
+ }
102
+ /**
103
+ * Options for {@link SearchClient.createIndex} / {@link SearchClient.ensureIndex}.
104
+ * The goal is the same as the rest of the client: name a source table and a
105
+ * text column, and everything else - the endpoint, the embedding model, the
106
+ * index type, the sync mode - has a sensible default that infers from the
107
+ * workspace, overridable when a deployment needs to go deeper.
108
+ */
109
+ export interface CreateIndexOptions {
110
+ /**
111
+ * Source Delta table (catalog.schema.table) for a Delta Sync index. Provide
112
+ * this for the common case: Databricks computes and syncs embeddings from it.
113
+ * Omit it (and pass {@link embeddingDimension}) to create a direct-access
114
+ * index you write vectors to yourself.
115
+ */
116
+ sourceTable?: string;
117
+ /** Primary-key column. Defaults to `id`. */
118
+ primaryKey?: string;
119
+ /**
120
+ * The text column embeddings are computed from (Delta Sync). Defaults to the
121
+ * first of `text` / `content` / `body` present, else the caller must set it.
122
+ */
123
+ embeddingSourceColumn?: string;
124
+ /**
125
+ * Embedding model endpoint. A loose name is fuzzy-matched; when omitted the
126
+ * best embedding endpoint in the workspace is chosen ({@link resolveEmbeddingModel}).
127
+ */
128
+ embeddingModel?: string;
129
+ /** Vector Search endpoint to host the index on. Defaults to the plugin's `endpoint`. */
130
+ endpoint?: string;
131
+ /** For a direct-access index: the embedding vector dimension (self-managed vectors, no source table). */
132
+ embeddingDimension?: number;
133
+ /** For a direct-access index: the column the vector is stored in. Defaults to `embedding`. */
134
+ embeddingVectorColumn?: string;
135
+ /**
136
+ * The column-name -> type map used to build a direct-access index's
137
+ * `schema_json` (types: `string`, `int`, `long`, `float`, `double`,
138
+ * `boolean`, `date`, `timestamp`). Must include the primary key and any
139
+ * columns you upsert. Defaults to `{ id: "string", text: "string" }`. The
140
+ * embedding vector column is added automatically.
141
+ */
142
+ schema?: Record<string, string>;
143
+ /** Sync mode for a Delta Sync index. `TRIGGERED` (default) syncs on demand; `CONTINUOUS` keeps fresh. */
144
+ pipelineType?: "TRIGGERED" | "CONTINUOUS";
145
+ /** Extra columns to sync alongside the embedding source (Delta Sync). */
146
+ columnsToSync?: string[];
147
+ /** External cancellation. */
148
+ signal?: AbortSignal;
149
+ }
150
+ /** Options for {@link SearchClient.ensureEndpoint}. */
151
+ export interface EnsureEndpointOptions {
152
+ /** Wait for the endpoint to come online before returning. Defaults to false. */
153
+ wait?: boolean;
154
+ /** External cancellation. */
155
+ signal?: AbortSignal;
156
+ }
157
+ /**
158
+ * Options for {@link SearchClient.provision} - a one-call "make this index real
159
+ * and searchable" used at boot or in a seed script. It ensures the endpoint,
160
+ * ensures the index, and (optionally) seeds documents when the index is empty.
161
+ */
162
+ export interface ProvisionOptions extends CreateIndexOptions {
163
+ /** Documents to seed when the index has no rows yet. Skipped if it already has data. */
164
+ seed?: SearchDocument[];
165
+ /**
166
+ * Wait for the endpoint AND index to come online before returning (needed
167
+ * before seeding). Defaults to true. Endpoint creation can take many minutes.
168
+ */
169
+ wait?: boolean;
170
+ /** How long to wait for readiness before giving up. Defaults to 20 minutes. */
171
+ timeoutMs?: number;
172
+ }
173
+ /**
174
+ * The AI Search client. Construct it with {@link createSearchClient} (which
175
+ * reads a resolved config) or directly for one-off use. All reads resolve the
176
+ * OBO workspace client from the active execution context.
177
+ */
178
+ export declare class SearchClient {
179
+ private readonly config;
180
+ private readonly workspaceClientFactory;
181
+ /**
182
+ * Optional Lakebase full-text FALLBACK backend. Present only when no Vector
183
+ * Search endpoint is configured but a Lakebase pool is available; when set,
184
+ * search / provision / write operations delegate to it and return the exact
185
+ * same shapes, so nothing downstream can tell which backend answered.
186
+ */
187
+ private readonly lakebase?;
188
+ constructor(config?: ResolvedSearchConfig, workspaceClientFactory?: () => WorkspaceClientLike,
189
+ /**
190
+ * Optional Lakebase full-text FALLBACK backend. Present only when no Vector
191
+ * Search endpoint is configured but a Lakebase pool is available; when set,
192
+ * search / provision / write operations delegate to it and return the exact
193
+ * same shapes, so nothing downstream can tell which backend answered.
194
+ */
195
+ lakebase?: LakebaseSearchBackend | undefined);
196
+ /** True when this client is answering out of the Lakebase fallback backend. */
197
+ get usesLakebase(): boolean;
198
+ /** A handle bound to one index (by full UC name or configured alias). */
199
+ index(reference: string): SearchIndex;
200
+ /**
201
+ * Search one index. `index` may be a full UC name, a configured alias, or
202
+ * omitted to use the default index. Returns hits sorted most-relevant-first.
203
+ */
204
+ search(query: string, options?: SearchOptions & {
205
+ index?: string;
206
+ }): Promise<SearchResult>;
207
+ /** Cache of index-name -> is-DIRECT_ACCESS, so a search embeds its query only when needed. */
208
+ private readonly directAccessCache;
209
+ /** Whether an index is DIRECT_ACCESS (memoized); a lookup failure assumes Delta Sync. */
210
+ private isDirectAccess;
211
+ /**
212
+ * Fan a query across several indexes and merge the hits, sorted by score -
213
+ * the "universal search" a single box over many collections needs. Each
214
+ * index is searched concurrently; an index that errors is logged and skipped
215
+ * so one bad index does not sink the whole search.
216
+ */
217
+ universalSearch(query: string, options?: UniversalSearchOptions): Promise<SearchResult>;
218
+ /**
219
+ * Resolve an index reference (full UC name or configured alias) to the name
220
+ * the API expects, failing fast when it resolves to nothing. Without this a
221
+ * blank reference - what an omitted `index` becomes when no default is
222
+ * configured - reaches the SDK as an empty `index_name`, which builds a
223
+ * URL with the name segment missing and comes back as a confusing
224
+ * `ENDPOINT_NOT_FOUND` instead of naming the real problem.
225
+ */
226
+ private requireIndexName;
227
+ /** Fetch an index's live definition. */
228
+ getIndex(reference: string, signal?: AbortSignal): Promise<IndexInfo>;
229
+ /** Add or update documents in a direct-access index. */
230
+ addDocuments(reference: string, documents: SearchDocument[], signal?: AbortSignal): Promise<UpsertResult>;
231
+ /** Delete documents from a direct-access index by primary key. */
232
+ deleteDocuments(reference: string, ids: Array<string | number>, signal?: AbortSignal): Promise<UpsertResult>;
233
+ /**
234
+ * Resolve an embedding endpoint id for creating a Delta Sync index. Reuses
235
+ * the model resolver: a configured / passed name is fuzzy-matched against the
236
+ * live catalogue, otherwise the highest-ranked embedding endpoint is chosen.
237
+ */
238
+ resolveEmbeddingModel(requested?: string, signal?: AbortSignal): Promise<string | null>;
239
+ /**
240
+ * Embed text via a Databricks embedding serving endpoint, returning one
241
+ * vector per input. Used to seed a direct-access index and to turn a search
242
+ * query into a query vector - Databricks only manages embeddings for Delta
243
+ * Sync indexes, so a direct-access index (no Delta table, no warehouse) needs
244
+ * the client to embed on write and on query. The endpoint is resolved the
245
+ * same way as for index creation when not named.
246
+ */
247
+ embed(texts: string[], model?: string, signal?: AbortSignal): Promise<number[][]>;
248
+ /** The vector dimension a model produces (embeds a probe string once). */
249
+ private embeddingDimension;
250
+ /**
251
+ * Create an AI Search index with as little ceremony as possible. Two shapes:
252
+ *
253
+ * - **Delta Sync** (the default): pass `sourceTable`; Databricks computes
254
+ * embeddings from the text column and keeps the index synced. The
255
+ * embedding model is resolved automatically when not named.
256
+ * - **Direct Access**: omit `sourceTable` and pass `embeddingDimension`;
257
+ * you write vectors yourself via {@link addDocuments}.
258
+ *
259
+ * Everything else infers: the endpoint from the plugin config, the primary
260
+ * key (`id`), the text column (`text` / `content` / `body`), and the vector
261
+ * column (`embedding`). Returns the created index's {@link IndexInfo}.
262
+ */
263
+ createIndex(name: string, options?: CreateIndexOptions): Promise<IndexInfo>;
264
+ /**
265
+ * Create the index if it does not already exist, otherwise return the
266
+ * existing one. Idempotent - safe to call on every boot to guarantee an
267
+ * index is present.
268
+ */
269
+ ensureIndex(name: string, options?: CreateIndexOptions): Promise<IndexInfo>;
270
+ /**
271
+ * Ensure an index exists, is online, and (optionally) holds seed data - the
272
+ * "wire up a real index on boot" path. Idempotent and cheap to call every
273
+ * boot: it creates the endpoint and index only if missing, waits for them to
274
+ * come online, and seeds documents ONLY when the index is still empty.
275
+ *
276
+ * For the demo/dummy-data case this needs no Delta table and no warehouse:
277
+ * the default is a MANAGED direct-access index (Databricks embeds the `text`
278
+ * column on write and query), so seeding is just an `addDocuments` of plain
279
+ * rows and search-by-text works immediately.
280
+ */
281
+ provision(name: string, options?: ProvisionOptions): Promise<IndexInfo>;
282
+ /** Poll an index until it reports ready, or throw after {@link timeoutMs}. */
283
+ private waitForIndexReady;
284
+ /** Trigger a sync of a Delta Sync index from its source table. */
285
+ syncIndex(reference: string, signal?: AbortSignal): Promise<void>;
286
+ /** Delete an index. */
287
+ deleteIndex(reference: string, signal?: AbortSignal): Promise<void>;
288
+ /** List the indexes hosted on a Vector Search endpoint (name + type only). */
289
+ listIndexes(endpoint?: string, signal?: AbortSignal): Promise<string[]>;
290
+ /**
291
+ * Ensure a Vector Search endpoint exists, creating a `STANDARD` one when it
292
+ * does not. Optionally wait for it to come online. Idempotent.
293
+ */
294
+ ensureEndpoint(name?: string, options?: EnsureEndpointOptions): Promise<void>;
295
+ /**
296
+ * Resolve the workspace client and run one call under a bounded timeout. The
297
+ * caller's signal (if any) and a timeout are merged into one SDK `Context`
298
+ * via {@link appkit.databricks.toContext}, so either unwinds the request.
299
+ */
300
+ private withClient;
301
+ }
302
+ /** Construct a {@link SearchClient} from a resolved config. */
303
+ export declare function createSearchClient(config?: ResolvedSearchConfig, workspaceClientFactory?: () => WorkspaceClientLike, lakebase?: LakebaseSearchBackend): SearchClient;
304
+ export {};