@dbx-tools/search 0.6.107 → 0.6.111
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 +96 -110
- package/index.ts +6 -2
- package/lib/index.d.ts +6 -2
- package/lib/index.js +4 -1
- package/lib/src/client.d.ts +23 -43
- package/lib/src/client.js +51 -114
- package/lib/src/lakebase-plugin.d.ts +73 -0
- package/lib/src/lakebase-plugin.js +191 -0
- package/lib/src/lakebase.d.ts +6 -4
- package/lib/src/lakebase.js +45 -13
- package/lib/src/native.d.ts +18 -0
- package/lib/src/native.js +79 -0
- package/lib/src/plugin.d.ts +31 -59
- package/lib/src/plugin.js +63 -130
- package/lib/src/query.d.ts +1 -57
- package/lib/src/query.js +2 -93
- package/lib/src/runtime.d.ts +4 -11
- package/lib/src/runtime.js +3 -7
- package/lib/src/schema.d.ts +1 -1
- package/lib/src/tool.d.ts +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/client.ts +69 -137
- package/src/lakebase-plugin.ts +258 -0
- package/src/lakebase.ts +56 -15
- package/src/native.ts +117 -0
- package/src/plugin.ts +63 -148
- package/src/query.ts +1 -109
- package/src/runtime.ts +6 -16
package/src/plugin.ts
CHANGED
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AppKit plugin (registered name: `search`)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* AppKit extension plugin (registered name: `search`) around a sibling
|
|
3
|
+
* `aiSearch` provider. Native AppKit owns Vector Search query execution; this
|
|
4
|
+
* plugin adds the surfaces that remain useful across native and Lakebase
|
|
5
|
+
* providers:
|
|
6
6
|
*
|
|
7
7
|
* - a `search` / `universal_search` / (opt-in) `add_documents` tool set for
|
|
8
8
|
* both Mastra and AppKit agents (autocomplete is a small-`limit` search);
|
|
9
|
-
* -
|
|
9
|
+
* - compatibility and extension routes under `/api/search`
|
|
10
10
|
* (`POST /` search, `POST /universal` federated, `GET /indexes` catalogue,
|
|
11
11
|
* and `POST /documents` when writes are enabled);
|
|
12
12
|
* - a `clientConfig()` payload so a UI knows the indexes, default, and page
|
|
13
13
|
* size at boot with no round-trip;
|
|
14
14
|
* - `exports()` so app code can `appkit.search.search(...)` directly.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* first search.
|
|
16
|
+
* Vector Search reads delegate to the registered native AppKit `aiSearch`
|
|
17
|
+
* plugin, preserving its OBO, caching, reranking, and response handling. The
|
|
18
|
+
* alternative `lakebaseAiSearch` provider supplies the same query contract
|
|
19
|
+
* over PostgreSQL full-text search.
|
|
21
20
|
*
|
|
22
21
|
* @module
|
|
23
22
|
*/
|
|
24
23
|
|
|
24
|
+
import { Plugin, toPlugin, type IAppRouter, type PluginManifest } from "@databricks/appkit";
|
|
25
25
|
import {
|
|
26
|
-
|
|
27
|
-
Plugin,
|
|
28
|
-
ResourceType,
|
|
29
|
-
toPlugin,
|
|
30
|
-
type IAppRouter,
|
|
31
|
-
type PluginManifest,
|
|
32
|
-
type ResourceRequirement,
|
|
33
|
-
} from "@databricks/appkit";
|
|
34
|
-
import {
|
|
26
|
+
aiSearch,
|
|
35
27
|
defineTool,
|
|
36
28
|
executeFromRegistry,
|
|
37
29
|
toolsFromRegistry,
|
|
@@ -42,16 +34,9 @@ import {
|
|
|
42
34
|
import { plugin as appkitPlugin } from "@dbx-tools/appkit";
|
|
43
35
|
import { error as sharedError, log, string } from "@dbx-tools/shared-core";
|
|
44
36
|
import { search as sharedSearch, type SearchClientConfig } from "@dbx-tools/shared-search";
|
|
45
|
-
import {
|
|
46
|
-
SEARCH_CONFIG_SCHEMA,
|
|
47
|
-
DATABRICKS_INDEX_ENV,
|
|
48
|
-
ENDPOINT_ENV,
|
|
49
|
-
INDEX_ENV,
|
|
50
|
-
resolveSearchConfig,
|
|
51
|
-
type SearchPluginConfig,
|
|
52
|
-
} from "./config.ts";
|
|
37
|
+
import { SEARCH_CONFIG_SCHEMA, resolveSearchConfig, type SearchPluginConfig } from "./config.ts";
|
|
53
38
|
import { toCreateIndexOptions } from "./index-tools.ts";
|
|
54
|
-
import {
|
|
39
|
+
import { nativeAiSearchBackend } from "./native.ts";
|
|
55
40
|
import { toDocumentArray } from "./query.ts";
|
|
56
41
|
import { getSearchRuntime, resetSearchRuntime } from "./runtime.ts";
|
|
57
42
|
import {
|
|
@@ -64,9 +49,6 @@ import {
|
|
|
64
49
|
|
|
65
50
|
const logger = log.logger("search");
|
|
66
51
|
|
|
67
|
-
/** Mount-relative route (under `/api/search`) for a single-index search. */
|
|
68
|
-
const SEARCH_ROUTE = "/";
|
|
69
|
-
|
|
70
52
|
/** Mount-relative route for a universal (federated) search across indexes. */
|
|
71
53
|
const UNIVERSAL_ROUTE = "/universal";
|
|
72
54
|
|
|
@@ -83,53 +65,31 @@ const INDEX_ROUTE = "/index";
|
|
|
83
65
|
const INDEX_SYNC_ROUTE = "/index/sync";
|
|
84
66
|
|
|
85
67
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*/
|
|
90
|
-
const INDEX_RESOURCE = {
|
|
91
|
-
type: ResourceType.VECTOR_SEARCH_INDEX,
|
|
92
|
-
alias: "AI Search Index",
|
|
93
|
-
resourceKey: "search-index",
|
|
94
|
-
description:
|
|
95
|
-
"Databricks AI Search (Vector Search) index the app searches by default " +
|
|
96
|
-
"(catalog.schema.index). Optional: a request may name any index the caller can read.",
|
|
97
|
-
permission: "SELECT",
|
|
98
|
-
fields: {
|
|
99
|
-
name: {
|
|
100
|
-
env: INDEX_ENV,
|
|
101
|
-
description: `Default AI Search index. ${DATABRICKS_INDEX_ENV} is also honored.`,
|
|
102
|
-
discovery: {
|
|
103
|
-
type: "cli",
|
|
104
|
-
cliCommand:
|
|
105
|
-
"databricks vector-search-indexes list-indexes --endpoint-name <ENDPOINT> --output json",
|
|
106
|
-
selectField: ".name",
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
} satisfies Omit<ResourceRequirement, "required">;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* AppKit plugin exposing AI Search as a search box, an agent tool set, and a
|
|
114
|
-
* direct API.
|
|
68
|
+
* AppKit extension plugin for agent tools, federated search, and optional
|
|
69
|
+
* Vector Search lifecycle operations. Register `aiSearch()` or
|
|
70
|
+
* `lakebaseAiSearch()` before this plugin.
|
|
115
71
|
*
|
|
116
72
|
* @example
|
|
117
73
|
* ```ts
|
|
118
74
|
* import { createApp, server } from "@databricks/appkit";
|
|
75
|
+
* import { aiSearch } from "@databricks/appkit/beta";
|
|
119
76
|
* import { plugin as searchPlugin } from "@dbx-tools/search";
|
|
120
77
|
*
|
|
121
78
|
* await createApp({
|
|
122
79
|
* plugins: [
|
|
123
80
|
* server(),
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
81
|
+
* aiSearch({
|
|
82
|
+
* indexes: {
|
|
83
|
+
* docs: {
|
|
84
|
+
* indexName: "main.support.docs",
|
|
85
|
+
* columns: ["id", "title", "body"],
|
|
86
|
+
* },
|
|
87
|
+
* },
|
|
88
|
+
* }),
|
|
89
|
+
* searchPlugin.search({
|
|
90
|
+
* index: "main.support.docs",
|
|
91
|
+
* indexes: [{ name: "main.support.docs", alias: "docs" }],
|
|
92
|
+
* }),
|
|
133
93
|
* ],
|
|
134
94
|
* });
|
|
135
95
|
* ```
|
|
@@ -144,7 +104,7 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
144
104
|
stability: "beta",
|
|
145
105
|
resources: {
|
|
146
106
|
required: [],
|
|
147
|
-
optional: [
|
|
107
|
+
optional: [],
|
|
148
108
|
},
|
|
149
109
|
config: { schema: SEARCH_CONFIG_SCHEMA },
|
|
150
110
|
} satisfies PluginManifest<"search">;
|
|
@@ -154,18 +114,6 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
154
114
|
return `/api/${SearchPlugin.manifest.name}`;
|
|
155
115
|
}
|
|
156
116
|
|
|
157
|
-
/**
|
|
158
|
-
* Promote the index to a required resource once a deployment pins a default,
|
|
159
|
-
* through plugin config or either environment name.
|
|
160
|
-
*/
|
|
161
|
-
static getResourceRequirements(config: SearchPluginConfig): ResourceRequirement[] {
|
|
162
|
-
const pinned =
|
|
163
|
-
string.trimToNull(config.index) ??
|
|
164
|
-
string.trimToNull(process.env[INDEX_ENV]) ??
|
|
165
|
-
string.trimToNull(process.env[DATABRICKS_INDEX_ENV]);
|
|
166
|
-
return pinned === null ? [] : [{ ...INDEX_RESOURCE, required: true }];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
117
|
/**
|
|
170
118
|
* The tools this plugin offers to an AppKit agent. `search` /
|
|
171
119
|
* `universal_search` are reads; `add_documents` / `create_index` /
|
|
@@ -174,7 +122,7 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
174
122
|
* granted explicitly.
|
|
175
123
|
*/
|
|
176
124
|
private get tools(): ToolRegistry {
|
|
177
|
-
const { config } = getSearchRuntime({ config: this.config });
|
|
125
|
+
const { config, readBackend } = getSearchRuntime({ config: this.config });
|
|
178
126
|
const registry: ToolRegistry = {
|
|
179
127
|
search: defineTool({
|
|
180
128
|
description: SEARCH_TOOL_DESCRIPTION,
|
|
@@ -201,40 +149,38 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
201
149
|
autoInheritable: false,
|
|
202
150
|
execute: async (args, signal) => this.runAddDocuments(args, signal),
|
|
203
151
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
152
|
+
if (readBackend?.supportsLifecycle) {
|
|
153
|
+
registry.create_index = defineTool({
|
|
154
|
+
description: CREATE_INDEX_TOOL_DESCRIPTION,
|
|
155
|
+
schema: sharedSearch.createIndexRequestSchema,
|
|
156
|
+
annotations: { effect: "write", requiresUserContext: true },
|
|
157
|
+
autoInheritable: false,
|
|
158
|
+
execute: async (args, signal) => this.runCreateIndex(args, signal),
|
|
159
|
+
});
|
|
160
|
+
registry.sync_index = defineTool({
|
|
161
|
+
description: SYNC_INDEX_TOOL_DESCRIPTION,
|
|
162
|
+
schema: sharedSearch.syncIndexRequestSchema,
|
|
163
|
+
annotations: { effect: "write", requiresUserContext: true },
|
|
164
|
+
autoInheritable: false,
|
|
165
|
+
execute: async (args, signal) => this.runSyncIndex(args, signal),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
218
168
|
}
|
|
219
169
|
return registry;
|
|
220
170
|
}
|
|
221
171
|
|
|
222
172
|
/** Prime the shared runtime from config and log the effective policy at boot. */
|
|
223
173
|
override async setup(): Promise<void> {
|
|
224
|
-
|
|
225
|
-
// endpoint is configured but the AppKit `lakebase` plugin is registered,
|
|
226
|
-
// fall back to a Postgres full-text index. Both answer with the identical
|
|
227
|
-
// shape, so this choice is invisible to tools, routes, and the UI.
|
|
228
|
-
const lakebaseBackend = this.resolveLakebaseBackend();
|
|
174
|
+
const readBackend = this.resolveProviderBackend();
|
|
229
175
|
// The runtime may already have been built (config-only) when `tools()` ran
|
|
230
176
|
// during registration; rebuild it so it carries the chosen backend.
|
|
231
177
|
resetSearchRuntime();
|
|
232
178
|
const { config } = getSearchRuntime({
|
|
233
179
|
config: this.config,
|
|
234
|
-
|
|
180
|
+
readBackend,
|
|
235
181
|
});
|
|
236
182
|
logger.info("ready", {
|
|
237
|
-
backend:
|
|
183
|
+
backend: readBackend.supportsLifecycle ? "appkit-ai-search" : "lakebase-ai-search",
|
|
238
184
|
defaultIndex: config.defaultIndex ?? "(none - pass per request)",
|
|
239
185
|
indexes: config.indexes.map((i) => i.alias),
|
|
240
186
|
pageSize: config.pageSize,
|
|
@@ -247,33 +193,15 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
247
193
|
});
|
|
248
194
|
// Provision a real index in the BACKGROUND so a slow first-time endpoint or
|
|
249
195
|
// index build never blocks the server from coming up.
|
|
250
|
-
if (config.ensureOnSetup
|
|
196
|
+
if (config.ensureOnSetup && readBackend.supportsLifecycle) {
|
|
197
|
+
void this.runEnsureOnSetup(config);
|
|
198
|
+
}
|
|
251
199
|
}
|
|
252
200
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
* `DATABRICKS_VECTOR_SEARCH_ENDPOINT`) AND the sibling AppKit `lakebase`
|
|
258
|
-
* plugin is registered. The pg pool is built from that plugin's
|
|
259
|
-
* service-principal config exactly like `@dbx-tools/appkit-mastra`'s memory
|
|
260
|
-
* pool - no auth is re-implemented here.
|
|
261
|
-
*/
|
|
262
|
-
private resolveLakebaseBackend(): LakebaseSearchBackend | undefined {
|
|
263
|
-
const config = resolveSearchConfig(this.config);
|
|
264
|
-
const hasEndpoint =
|
|
265
|
-
config.endpoint !== undefined ||
|
|
266
|
-
string.trimToNull(process.env[ENDPOINT_ENV]) !== null ||
|
|
267
|
-
string.trimToNull(process.env.DATABRICKS_VECTOR_SEARCH_ENDPOINT) !== null;
|
|
268
|
-
if (hasEndpoint) return undefined;
|
|
269
|
-
const lake = appkitPlugin.instance(this.context, lakebase);
|
|
270
|
-
if (!lake) return undefined;
|
|
271
|
-
logger.info("backend-lakebase", {
|
|
272
|
-
reason: "no Vector Search endpoint configured; using the Lakebase full-text fallback",
|
|
273
|
-
});
|
|
274
|
-
// `getPgConfig()` must be read OUTSIDE any asUser scope (as it is here at
|
|
275
|
-
// setup) so it carries the SP connection target + token-refresh callback.
|
|
276
|
-
return new LakebaseSearchBackend(() => lake.exports().getPgConfig());
|
|
201
|
+
/** Resolve the registered AppKit-compatible AI Search provider. */
|
|
202
|
+
private resolveProviderBackend(): ReturnType<typeof nativeAiSearchBackend> {
|
|
203
|
+
const provider = appkitPlugin.require(this.context, aiSearch, this).exports();
|
|
204
|
+
return nativeAiSearchBackend(provider, resolveSearchConfig(this.config));
|
|
277
205
|
}
|
|
278
206
|
|
|
279
207
|
/**
|
|
@@ -356,23 +284,10 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
356
284
|
}
|
|
357
285
|
|
|
358
286
|
/**
|
|
359
|
-
* Mount
|
|
360
|
-
*
|
|
361
|
-
* Unity Catalog ACLs apply. `GET /indexes` needs no user scope - it just
|
|
362
|
-
* echoes the configured catalogue.
|
|
287
|
+
* Mount extension routes under `/api/search`. Single-index queries stay on
|
|
288
|
+
* the provider's native `/api/ai-search/:alias/query` surface.
|
|
363
289
|
*/
|
|
364
290
|
override injectRoutes(router: IAppRouter): void {
|
|
365
|
-
this.route(router, {
|
|
366
|
-
name: "search",
|
|
367
|
-
method: "post",
|
|
368
|
-
path: SEARCH_ROUTE,
|
|
369
|
-
handler: async (req, res) => {
|
|
370
|
-
await this.respond(res, "search", () => {
|
|
371
|
-
const request = sharedSearch.searchRequestSchema.parse(req.body ?? {});
|
|
372
|
-
return this.asUser(req).runSearch(request);
|
|
373
|
-
});
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
291
|
this.route(router, {
|
|
377
292
|
name: "universalSearch",
|
|
378
293
|
method: "post",
|
|
@@ -412,8 +327,8 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
412
327
|
method: "post",
|
|
413
328
|
path: INDEX_ROUTE,
|
|
414
329
|
handler: async (req, res) => {
|
|
415
|
-
const { config } = getSearchRuntime();
|
|
416
|
-
if (!config.allowWrite) {
|
|
330
|
+
const { config, readBackend } = getSearchRuntime();
|
|
331
|
+
if (!config.allowWrite || !readBackend?.supportsLifecycle) {
|
|
417
332
|
res.status(403).json({ error: "the index write surface is disabled" });
|
|
418
333
|
return;
|
|
419
334
|
}
|
|
@@ -427,8 +342,8 @@ export class SearchPlugin extends Plugin<SearchPluginConfig> implements ToolProv
|
|
|
427
342
|
method: "post",
|
|
428
343
|
path: INDEX_SYNC_ROUTE,
|
|
429
344
|
handler: async (req, res) => {
|
|
430
|
-
const { config } = getSearchRuntime();
|
|
431
|
-
if (!config.allowWrite) {
|
|
345
|
+
const { config, readBackend } = getSearchRuntime();
|
|
346
|
+
if (!config.allowWrite || !readBackend?.supportsLifecycle) {
|
|
432
347
|
res.status(403).json({ error: "the index write surface is disabled" });
|
|
433
348
|
return;
|
|
434
349
|
}
|
package/src/query.ts
CHANGED
|
@@ -1,115 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Translation between the browser-safe search contract and the Databricks
|
|
3
|
-
* Vector Search query API, kept in one place so the client, the tools, and the
|
|
4
|
-
* routes never hand-roll a `filters_json` string or unpack a `data_array` by
|
|
5
|
-
* hand.
|
|
6
|
-
*
|
|
7
|
-
* - {@link toQueryType} maps the friendly {@link SearchMode} onto the API's
|
|
8
|
-
* `query_type` (`HYBRID` / `ANN`). Keyword-only search rides on `ANN` with
|
|
9
|
-
* text but no vector, which Databricks answers with its BM25 path.
|
|
10
|
-
* - {@link compileFilter} turns the `{ column: value }` filter object (with
|
|
11
|
-
* optional operator maps like `{ ">=": 10 }`) into the `filters_json`
|
|
12
|
-
* string the API expects, so a caller never learns Databricks' filter
|
|
13
|
-
* spelling.
|
|
14
|
-
* - {@link toHits} unpacks the columnar `{ manifest, result }` response into
|
|
15
|
-
* `{ id, score, fields }` hits, pulling the score out of the reserved
|
|
16
|
-
* `__db_score` / `score` column and the id out of the primary-key column.
|
|
17
|
-
*
|
|
18
|
-
* @module
|
|
19
|
-
*/
|
|
1
|
+
/** Request-shape helpers for search extension write routes and tools. */
|
|
20
2
|
|
|
21
3
|
import { ValidationError } from "@databricks/appkit";
|
|
22
4
|
import { json, object } from "@dbx-tools/shared-core";
|
|
23
|
-
import type { SearchHit, SearchMode } from "@dbx-tools/shared-search";
|
|
24
|
-
|
|
25
|
-
/** The column name Databricks Vector Search returns the relevance score under. */
|
|
26
|
-
const SCORE_COLUMN = "__db_score";
|
|
27
|
-
|
|
28
|
-
/** Map a {@link SearchMode} onto the serving API `query_type`. */
|
|
29
|
-
export function toQueryType(mode: SearchMode): string {
|
|
30
|
-
return mode === "hybrid" ? "HYBRID" : "ANN";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Compile a `{ column: value }` filter object into the `filters_json` string
|
|
35
|
-
* the query API expects. A scalar becomes an equality; an array becomes an
|
|
36
|
-
* IN-style match; an operator map (`{ ">=": 10, "<": 20 }`) expands to the
|
|
37
|
-
* `column operator` keys Databricks uses. Returns `undefined` for an empty
|
|
38
|
-
* filter so the field is omitted rather than sent as `{}`.
|
|
39
|
-
*/
|
|
40
|
-
export function compileFilter(filter: Record<string, unknown> | undefined): string | undefined {
|
|
41
|
-
if (!filter || Object.keys(filter).length === 0) return undefined;
|
|
42
|
-
const compiled: Record<string, unknown> = {};
|
|
43
|
-
for (const [column, value] of Object.entries(filter)) {
|
|
44
|
-
if (object.isRecord(value)) {
|
|
45
|
-
for (const [op, operand] of Object.entries(value)) {
|
|
46
|
-
compiled[`${column} ${op}`] = operand;
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
compiled[column] = value;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return JSON.stringify(compiled);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** A minimal structural view of the Vector Search query response. */
|
|
56
|
-
export interface QueryResponseLike {
|
|
57
|
-
manifest?: { columns?: Array<{ name?: string }> };
|
|
58
|
-
result?: { data_array?: Array<Array<unknown>> };
|
|
59
|
-
next_page_token?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Unpack a columnar query response into {@link SearchHit}s. The manifest names
|
|
64
|
-
* the columns in order; each row is a positional array. The score comes from
|
|
65
|
-
* the reserved score column and the id from `primaryKey` (falling back to the
|
|
66
|
-
* first column when the key is unknown). The score column is stripped from
|
|
67
|
-
* `fields` so a hit's fields are just the document.
|
|
68
|
-
*/
|
|
69
|
-
export function toHits(
|
|
70
|
-
response: QueryResponseLike,
|
|
71
|
-
primaryKey: string | undefined,
|
|
72
|
-
indexName?: string,
|
|
73
|
-
): SearchHit[] {
|
|
74
|
-
const columns = (response.manifest?.columns ?? []).map((c) => c.name ?? "");
|
|
75
|
-
const rows = response.result?.data_array ?? [];
|
|
76
|
-
const scoreIdx = columns.indexOf(SCORE_COLUMN);
|
|
77
|
-
const keyIdx = primaryKey ? columns.indexOf(primaryKey) : -1;
|
|
78
|
-
return rows.map((row, rowIndex) => {
|
|
79
|
-
const fields: Record<string, unknown> = {};
|
|
80
|
-
columns.forEach((name, i) => {
|
|
81
|
-
if (i === scoreIdx || !name) return;
|
|
82
|
-
fields[name] = row[i];
|
|
83
|
-
});
|
|
84
|
-
const scoreRaw = scoreIdx >= 0 ? Number(row[scoreIdx]) : NaN;
|
|
85
|
-
const idRaw =
|
|
86
|
-
keyIdx >= 0 ? row[keyIdx] : primaryKey ? fields[primaryKey] : (row[0] ?? rowIndex);
|
|
87
|
-
return {
|
|
88
|
-
id: String(idRaw ?? rowIndex),
|
|
89
|
-
score: Number.isFinite(scoreRaw) ? scoreRaw : 0,
|
|
90
|
-
fields,
|
|
91
|
-
...(indexName ? { index: indexName } : {}),
|
|
92
|
-
};
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The columns to request from an index for a search. When neither the request
|
|
98
|
-
* nor the index config names columns, the score column alone is requested and
|
|
99
|
-
* the primary key is appended so a hit always has an id. Callers that want the
|
|
100
|
-
* whole document should pass the index's own column list.
|
|
101
|
-
*/
|
|
102
|
-
export function toRequestColumns(
|
|
103
|
-
requested: readonly string[] | undefined,
|
|
104
|
-
fallback: readonly string[] | undefined,
|
|
105
|
-
primaryKey: string | undefined,
|
|
106
|
-
): string[] {
|
|
107
|
-
const base = requested && requested.length > 0 ? requested : (fallback ?? []);
|
|
108
|
-
const columns = new Set<string>(base);
|
|
109
|
-
if (primaryKey) columns.add(primaryKey);
|
|
110
|
-
if (columns.size === 0 && primaryKey) columns.add(primaryKey);
|
|
111
|
-
return [...columns];
|
|
112
|
-
}
|
|
113
5
|
|
|
114
6
|
/**
|
|
115
7
|
* Parse a JSON document payload the model / a route supplied for a write.
|
package/src/runtime.ts
CHANGED
|
@@ -8,31 +8,24 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { createSearchClient, SearchClient } from "./client.ts";
|
|
11
|
+
import { createSearchClient, SearchClient, type SearchReadBackend } from "./client.ts";
|
|
12
12
|
import {
|
|
13
13
|
resolveSearchConfig,
|
|
14
14
|
type SearchPluginConfig,
|
|
15
15
|
type ResolvedSearchConfig,
|
|
16
16
|
} from "./config.ts";
|
|
17
|
-
import { LakebaseSearchBackend } from "./lakebase.ts";
|
|
18
17
|
|
|
19
|
-
/**
|
|
20
|
-
* How the runtime is built. `lakebase` (when present) is the Postgres full-text
|
|
21
|
-
* FALLBACK backend the plugin wires up when no Vector Search endpoint is
|
|
22
|
-
* configured but the AppKit `lakebase` plugin is registered. Every read/write
|
|
23
|
-
* returns the same shape either way.
|
|
24
|
-
*/
|
|
18
|
+
/** Configuration and provider used to build the shared extension runtime. */
|
|
25
19
|
export interface SearchRuntimeOptions {
|
|
26
20
|
config?: SearchPluginConfig;
|
|
27
|
-
|
|
21
|
+
readBackend?: SearchReadBackend;
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
/** The shared resolved config plus the client reads run through. */
|
|
31
25
|
export interface SearchRuntime {
|
|
32
26
|
config: ResolvedSearchConfig;
|
|
33
27
|
client: SearchClient;
|
|
34
|
-
|
|
35
|
-
lakebase?: LakebaseSearchBackend;
|
|
28
|
+
readBackend?: SearchReadBackend;
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
let runtime: SearchRuntime | undefined;
|
|
@@ -46,11 +39,10 @@ let runtime: SearchRuntime | undefined;
|
|
|
46
39
|
export function getSearchRuntime(options?: SearchRuntimeOptions): SearchRuntime {
|
|
47
40
|
if (!runtime) {
|
|
48
41
|
const config = resolveSearchConfig(options?.config);
|
|
49
|
-
const lakebase = options?.lakebase;
|
|
50
42
|
runtime = {
|
|
51
43
|
config,
|
|
52
|
-
client: createSearchClient(config, undefined,
|
|
53
|
-
...(
|
|
44
|
+
client: createSearchClient(config, undefined, options?.readBackend),
|
|
45
|
+
...(options?.readBackend ? { readBackend: options.readBackend } : {}),
|
|
54
46
|
};
|
|
55
47
|
}
|
|
56
48
|
return runtime;
|
|
@@ -58,7 +50,5 @@ export function getSearchRuntime(options?: SearchRuntimeOptions): SearchRuntime
|
|
|
58
50
|
|
|
59
51
|
/** Drop the memoized runtime so the next {@link getSearchRuntime} rebuilds it. */
|
|
60
52
|
export function resetSearchRuntime(): void {
|
|
61
|
-
const backend = runtime?.lakebase;
|
|
62
53
|
runtime = undefined;
|
|
63
|
-
if (backend) void backend.close();
|
|
64
54
|
}
|