okengine 0.3.6 → 0.4.3
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/AGENTS.md +2 -0
- package/package.json +12 -12
- package/site/content/docs/ai/skills.mdx +5 -3
- package/site/content/docs/elements/ai.mdx +2 -0
- package/site/content/docs/elements/channel.mdx +2 -0
- package/site/content/docs/elements/clock.mdx +1 -4
- package/site/content/docs/elements/flow.mdx +4 -10
- package/site/content/docs/elements/gate.mdx +2 -0
- package/site/content/docs/elements/signal.mdx +1 -5
- package/site/content/docs/elements/store.mdx +27 -6
- package/site/content/docs/elements/vault.mdx +10 -11
- package/site/content/docs/get-started/basic-usage.mdx +76 -41
- package/site/content/docs/get-started/installation.mdx +95 -43
- package/site/content/docs/get-started/introduction.mdx +128 -75
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/why.mdx +141 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
- package/site/content/docs/plugins/security-headers.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/environment-variables.mdx +5 -3
- package/site/content/docs/reference/fx.mdx +16 -3
- package/src/cli/competitor-mention-removal.test.ts +117 -0
- package/src/cli/dev.ts +20 -0
- package/src/cli/meilisearch-local.test.ts +69 -0
- package/src/cli/meilisearch-local.ts +188 -0
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
- package/src/console/server/store.test.ts +1 -1
- package/src/console/server/store.ts +11 -1
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
- package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
- package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
- package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
- package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
- package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
- package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
- package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
- package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
- package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
- package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
- package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
- package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
- package/src/console/ui/dist/index.html +2 -2
- package/src/docker/compose.ts +5 -0
- package/src/docker/docker.test.ts +41 -0
- package/src/docker/recipes/index.ts +10 -2
- package/src/docker/recipes/meilisearch.ts +31 -0
- package/src/drivers/conformance.test.ts +16 -1
- package/src/drivers/conformance.ts +40 -3
- package/src/drivers/index.ts +14 -2
- package/src/drivers/libsql.ts +4 -4
- package/src/drivers/meilisearch.integration.test.ts +77 -0
- package/src/drivers/meilisearch.test.ts +181 -0
- package/src/drivers/meilisearch.ts +208 -0
- package/src/drivers/memory.ts +4 -4
- package/src/drivers/pgvector.ts +6 -6
- package/src/drivers/types.ts +93 -12
- package/src/drivers/vault-driver-removal.test.ts +6 -0
- package/src/drivers/vault-types.ts +4 -4
- package/src/elements/ai/runtime.ts +6 -0
- package/src/elements/ai.test.ts +22 -0
- package/src/elements/store/index-boot.test.ts +49 -7
- package/src/elements/store/runtime.ts +50 -15
- package/src/elements/store.ts +2 -0
- package/src/elements/vault.test.ts +27 -4
- package/src/elements/vault.ts +1 -1
- package/src/index.ts +4 -0
- package/src/kernel/boot-bind/store.test.ts +9 -0
- package/src/kernel/boot-bind/store.ts +30 -2
- package/src/kernel/concurrency.test.ts +58 -0
- package/src/kernel/concurrency.ts +48 -0
- package/src/kernel/fx.test.ts +11 -2
- package/src/kernel/fx.ts +37 -5
- package/src/kernel/index.ts +10 -1
- package/src/kernel/redacted.ts +74 -0
- package/src/kernel/router.ts +3 -3
- package/src/test/provisions.integration.test.ts +1 -1
- package/site/content/docs/get-started/comparison.mdx +0 -65
- package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
- package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
- package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
- package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
- package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
- package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
- package/src/drivers/vault-infisical.ts +0 -57
|
@@ -43,6 +43,29 @@ describe("image recipes", () => {
|
|
|
43
43
|
expect(redis.match("redis:7")).toBe(true);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
test("meilisearch matches the official image and emits a http URL", () => {
|
|
47
|
+
expect(recipeFor("getmeili/meilisearch:v1.37").id).toBe("meilisearch");
|
|
48
|
+
const spec: ServiceSpec = {
|
|
49
|
+
role: "store.index",
|
|
50
|
+
serviceName: "store-index",
|
|
51
|
+
image: "getmeili/meilisearch:v1.37",
|
|
52
|
+
port: 7700,
|
|
53
|
+
hostPort: 7700,
|
|
54
|
+
credentials: { user: "oke", password: "meili-master-key", database: "oke" },
|
|
55
|
+
};
|
|
56
|
+
const applied = recipeFor(spec.image).apply(spec);
|
|
57
|
+
expect(applied.environment?.MEILI_MASTER_KEY).toBe("${OKE_STORE_INDEX_KEY}");
|
|
58
|
+
expect(applied.volumes).toContain("store-index-data:/meili_data");
|
|
59
|
+
const url = recipeFor(spec.image).url(spec, {
|
|
60
|
+
host: "127.0.0.1",
|
|
61
|
+
port: 7700,
|
|
62
|
+
user: "oke",
|
|
63
|
+
password: "meili-master-key",
|
|
64
|
+
database: "oke",
|
|
65
|
+
});
|
|
66
|
+
expect(url).toBe("http://127.0.0.1:7700");
|
|
67
|
+
});
|
|
68
|
+
|
|
46
69
|
test("a new image recipe is ≤15 lines", async () => {
|
|
47
70
|
const src = await Bun.file(`${import.meta.dir}/recipes/postgres.ts`).text();
|
|
48
71
|
const exportLines = src
|
|
@@ -197,6 +220,24 @@ describe("deriveInfrastructure", () => {
|
|
|
197
220
|
expect(formatStackEnv(result.stackEnv)).toContain("OKE_STORE_SQL_PASSWORD=");
|
|
198
221
|
});
|
|
199
222
|
|
|
223
|
+
test("store.index meilisearch emits its own URL + master key env", () => {
|
|
224
|
+
const result = deriveInfrastructure({
|
|
225
|
+
images: { "store.index": "getmeili/meilisearch:v1.37" },
|
|
226
|
+
credentials: {
|
|
227
|
+
"store.index": { user: "oke", password: "meili-master-key", database: "oke" },
|
|
228
|
+
},
|
|
229
|
+
app: "skyport",
|
|
230
|
+
});
|
|
231
|
+
const yml = result.files.find((f) => f.path === "compose.store.index.yml")!.content;
|
|
232
|
+
expect(yml).toContain("getmeili/meilisearch:v1.37");
|
|
233
|
+
expect(yml).toContain("${OKE_STORE_INDEX_KEY}");
|
|
234
|
+
expect(yml).toContain("meili_data");
|
|
235
|
+
expect(yml).not.toContain("meili-master-key");
|
|
236
|
+
expect(result.stackEnv.OKE_STORE_INDEX_URL).toBe("http://127.0.0.1:7700");
|
|
237
|
+
expect(result.stackEnv.OKE_STORE_INDEX_KEY).toBe("meili-master-key");
|
|
238
|
+
expect(result.stackEnv.DATABASE_URL).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
200
241
|
test("emits protocol-specific env keys plus optional control notes", () => {
|
|
201
242
|
const result = deriveInfrastructure({
|
|
202
243
|
images: {
|
|
@@ -4,15 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ImageRecipe } from "../types.ts";
|
|
6
6
|
import { mailpit } from "./mailpit.ts";
|
|
7
|
+
import { meilisearch } from "./meilisearch.ts";
|
|
7
8
|
import { openbao } from "./openbao.ts";
|
|
8
9
|
import { postgres } from "./postgres.ts";
|
|
9
10
|
import { redis } from "./redis.ts";
|
|
10
11
|
import { rustfs } from "./rustfs.ts";
|
|
11
12
|
|
|
12
13
|
/** Default recipe catalogue. */
|
|
13
|
-
export const builtinRecipes: readonly ImageRecipe[] = [
|
|
14
|
+
export const builtinRecipes: readonly ImageRecipe[] = [
|
|
15
|
+
postgres,
|
|
16
|
+
redis,
|
|
17
|
+
mailpit,
|
|
18
|
+
rustfs,
|
|
19
|
+
openbao,
|
|
20
|
+
meilisearch,
|
|
21
|
+
];
|
|
14
22
|
|
|
15
|
-
export { mailpit, openbao, postgres, redis, rustfs };
|
|
23
|
+
export { mailpit, meilisearch, openbao, postgres, redis, rustfs };
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* Resolve the recipe for an image reference.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meilisearch image recipe — full-text search service (`getmeili/meilisearch`).
|
|
3
|
+
*
|
|
4
|
+
* Data lives on a named volume under `/meili_data`; the master key is injected
|
|
5
|
+
* via `${VAR}` (never cleartext in YAML). The official image is Alpine-based
|
|
6
|
+
* and musl-clean — the glibc binary gap only affects raw host installs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ImageRecipe } from "../types.ts";
|
|
10
|
+
|
|
11
|
+
/** Meilisearch FTS service. API on 7700. */
|
|
12
|
+
export const meilisearch: ImageRecipe = {
|
|
13
|
+
id: "meilisearch",
|
|
14
|
+
port: 7700,
|
|
15
|
+
match: (i) => /meilisearch|getmeili/i.test(i),
|
|
16
|
+
apply: (s) => ({
|
|
17
|
+
environment: {
|
|
18
|
+
MEILI_MASTER_KEY: "${OKE_STORE_INDEX_KEY}",
|
|
19
|
+
MEILI_ENV: "${OKE_MEILI_ENV:-production}",
|
|
20
|
+
MEILI_NO_ANALYTICS: "true",
|
|
21
|
+
},
|
|
22
|
+
volumes: [`${s.serviceName}-data:/meili_data`],
|
|
23
|
+
healthcheck: {
|
|
24
|
+
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:7700/health || exit 1"],
|
|
25
|
+
interval: "5s",
|
|
26
|
+
timeout: "3s",
|
|
27
|
+
retries: 12,
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
url: (_s, c) => `http://${c.host}:${c.port}`,
|
|
31
|
+
};
|
|
@@ -9,10 +9,13 @@ import {
|
|
|
9
9
|
runIndexConformance,
|
|
10
10
|
runKvConformance,
|
|
11
11
|
runSqlConformance,
|
|
12
|
+
runTextIndexConformance,
|
|
12
13
|
} from "./conformance.ts";
|
|
13
14
|
import { fsDriver } from "./fs.ts";
|
|
14
15
|
import { libsqlDriver, libsqlIndexDriver } from "./libsql.ts";
|
|
15
16
|
import { memoryFilesDriver, memoryIndexDriver, memoryKvDriver, memorySqlDriver } from "./memory.ts";
|
|
17
|
+
import { meilisearchDriver } from "./meilisearch.ts";
|
|
18
|
+
import { createMeilisearchFakeFetch } from "./meilisearch.test.ts";
|
|
16
19
|
import { pgliteDriver } from "./pglite.ts";
|
|
17
20
|
import { pgvectorDriver } from "./pgvector.ts";
|
|
18
21
|
import { createPostgresFakeClient, postgresDriver } from "./postgres.ts";
|
|
@@ -24,7 +27,8 @@ describe("sql conformance", () => {
|
|
|
24
27
|
test("memory", () => runSqlConformance(memorySqlDriver));
|
|
25
28
|
test("sqlite", () => runSqlConformance(sqliteDriver, { url: ":memory:" }));
|
|
26
29
|
test("libsql", () => runSqlConformance(libsqlDriver, { url: ":memory:" }));
|
|
27
|
-
|
|
30
|
+
// pglite cold-starts WASM + extension load — slow disks / CI need > default 5s
|
|
31
|
+
test("pglite", () => runSqlConformance(pgliteDriver, { url: "memory://" }), 15_000);
|
|
28
32
|
test("postgres (fake Bun.SQL client)", () =>
|
|
29
33
|
runSqlConformance(postgresDriver, {
|
|
30
34
|
client: createPostgresFakeClient(),
|
|
@@ -53,6 +57,15 @@ describe("index conformance", () => {
|
|
|
53
57
|
test("libsql", () => runIndexConformance(libsqlIndexDriver, { dims: 3 }));
|
|
54
58
|
});
|
|
55
59
|
|
|
60
|
+
describe("text index conformance", () => {
|
|
61
|
+
test("meilisearch (injected fetch)", () =>
|
|
62
|
+
runTextIndexConformance(meilisearchDriver, {
|
|
63
|
+
url: "http://127.0.0.1:7700",
|
|
64
|
+
apiKey: "master-key",
|
|
65
|
+
fetch: createMeilisearchFakeFetch(),
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
|
|
56
69
|
describe("protocol naming", () => {
|
|
57
70
|
test("driver ids are protocols, never vendors", () => {
|
|
58
71
|
const ids = [
|
|
@@ -69,6 +82,7 @@ describe("protocol naming", () => {
|
|
|
69
82
|
memoryIndexDriver.id,
|
|
70
83
|
pgvectorDriver.id,
|
|
71
84
|
libsqlIndexDriver.id,
|
|
85
|
+
meilisearchDriver.id,
|
|
72
86
|
];
|
|
73
87
|
expect(ids).toEqual([
|
|
74
88
|
"memory",
|
|
@@ -84,6 +98,7 @@ describe("protocol naming", () => {
|
|
|
84
98
|
"memory",
|
|
85
99
|
"pgvector",
|
|
86
100
|
"libsql",
|
|
101
|
+
"meilisearch",
|
|
87
102
|
]);
|
|
88
103
|
for (const id of ids) {
|
|
89
104
|
expect(id).not.toMatch(/neon|dragonfly|minio|upstash|cloudflare/i);
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
import { expect } from "bun:test";
|
|
9
9
|
import { createSqlStoreHandle } from "../elements/store/sql-session.ts";
|
|
10
10
|
import { defineTable } from "../elements/store/table.ts";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
FilesDriver,
|
|
13
|
+
KvDriver,
|
|
14
|
+
SqlDriver,
|
|
15
|
+
SqlConnectOptions,
|
|
16
|
+
TextIndexDriver,
|
|
17
|
+
VectorIndexDriver,
|
|
18
|
+
} from "./types.ts";
|
|
12
19
|
|
|
13
20
|
/** SQL conformance against one driver. */
|
|
14
21
|
export async function runSqlConformance(
|
|
@@ -118,9 +125,9 @@ export async function runFilesConformance(
|
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
/**
|
|
128
|
+
/** Vector index conformance against one driver. */
|
|
122
129
|
export async function runIndexConformance(
|
|
123
|
-
driver:
|
|
130
|
+
driver: VectorIndexDriver,
|
|
124
131
|
openOpts: {
|
|
125
132
|
name?: string;
|
|
126
133
|
dims?: number;
|
|
@@ -147,3 +154,33 @@ export async function runIndexConformance(
|
|
|
147
154
|
await index.close();
|
|
148
155
|
}
|
|
149
156
|
}
|
|
157
|
+
|
|
158
|
+
/** Full-text index conformance against one driver. */
|
|
159
|
+
export async function runTextIndexConformance(
|
|
160
|
+
driver: TextIndexDriver,
|
|
161
|
+
openOpts: {
|
|
162
|
+
name?: string;
|
|
163
|
+
url?: string;
|
|
164
|
+
apiKey?: string;
|
|
165
|
+
fetch?: typeof fetch;
|
|
166
|
+
} = {},
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
const index = await driver.open({
|
|
169
|
+
name: openOpts.name ?? "conf-text",
|
|
170
|
+
dims: 0,
|
|
171
|
+
url: openOpts.url,
|
|
172
|
+
apiKey: openOpts.apiKey,
|
|
173
|
+
fetch: openOpts.fetch,
|
|
174
|
+
});
|
|
175
|
+
try {
|
|
176
|
+
await index.upsert("d1", { id: "d1", title: "hello world" });
|
|
177
|
+
await index.upsert("d2", { id: "d2", title: "goodbye world" });
|
|
178
|
+
const found = await index.search("hello", { topK: 1 });
|
|
179
|
+
expect(found.hits.length).toBeGreaterThan(0);
|
|
180
|
+
expect(found.hits[0]?.id).toBe("d1");
|
|
181
|
+
expect(await index.delete("d1")).toBe(true);
|
|
182
|
+
expect(index.driverId).toBe(driver.id);
|
|
183
|
+
} finally {
|
|
184
|
+
await index.close();
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/drivers/index.ts
CHANGED
|
@@ -23,12 +23,18 @@ export type {
|
|
|
23
23
|
FilesDriver,
|
|
24
24
|
IndexHit,
|
|
25
25
|
IndexStore,
|
|
26
|
+
VectorIndexStore,
|
|
27
|
+
TextIndexStore,
|
|
28
|
+
TextIndexSearchOptions,
|
|
29
|
+
TextIndexSearchResult,
|
|
26
30
|
IndexOpenOptions,
|
|
27
31
|
IndexDriver,
|
|
32
|
+
VectorIndexDriver,
|
|
33
|
+
TextIndexDriver,
|
|
28
34
|
StoreDriver,
|
|
29
35
|
ClassificationMap,
|
|
30
36
|
} from "./types.ts";
|
|
31
|
-
export { classificationKey } from "./types.ts";
|
|
37
|
+
export { classificationKey, indexDriverNeedsSql } from "./types.ts";
|
|
32
38
|
|
|
33
39
|
export type {
|
|
34
40
|
SignalDriverId,
|
|
@@ -79,6 +85,13 @@ export { pgvectorDriver, openPgvectorIndex } from "./pgvector.ts";
|
|
|
79
85
|
|
|
80
86
|
export { libsqlDriver, connectLibsql, libsqlIndexDriver, openLibsqlIndex } from "./libsql.ts";
|
|
81
87
|
|
|
88
|
+
export {
|
|
89
|
+
meilisearchDriver,
|
|
90
|
+
openMeilisearchIndex,
|
|
91
|
+
MeilisearchUnavailableError,
|
|
92
|
+
type MeilisearchIndexOptions,
|
|
93
|
+
} from "./meilisearch.ts";
|
|
94
|
+
|
|
82
95
|
export { pgliteDriver, connectPglite } from "./pglite.ts";
|
|
83
96
|
|
|
84
97
|
export { memorySignalDriver, openMemorySignal } from "./signal-memory.ts";
|
|
@@ -107,7 +120,6 @@ export type { VaultDriverId, VaultOpenOptions, VaultBag, VaultDriver } from "./v
|
|
|
107
120
|
export { memoryVaultDriver } from "./vault-memory.ts";
|
|
108
121
|
export { envVaultDriver } from "./vault-env.ts";
|
|
109
122
|
export { openbaoVaultDriver, OpenBaoUnavailableError } from "./vault-openbao.ts";
|
|
110
|
-
export { infisicalVaultDriver } from "./vault-infisical.ts";
|
|
111
123
|
export { managedVaultDriver } from "./vault-managed.ts";
|
|
112
124
|
|
|
113
125
|
export type {
|
package/src/drivers/libsql.ts
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
import { customType, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
15
15
|
import type {
|
|
16
|
-
IndexDriver,
|
|
17
16
|
IndexHit,
|
|
18
17
|
IndexOpenOptions,
|
|
19
|
-
IndexStore,
|
|
20
18
|
SqlConnectOptions,
|
|
21
19
|
SqlConnection,
|
|
22
20
|
SqlDriver,
|
|
23
21
|
SqlRow,
|
|
22
|
+
VectorIndexDriver,
|
|
23
|
+
VectorIndexStore,
|
|
24
24
|
} from "./types.ts";
|
|
25
25
|
|
|
26
26
|
/** `@libsql/client` is an optional peer — loaded only when this driver runs. */
|
|
@@ -100,7 +100,7 @@ const f32Blob = customType<{
|
|
|
100
100
|
*
|
|
101
101
|
* @param options - Name / dims / shared SQL connection
|
|
102
102
|
*/
|
|
103
|
-
export async function openLibsqlIndex(options: IndexOpenOptions): Promise<
|
|
103
|
+
export async function openLibsqlIndex(options: IndexOpenOptions): Promise<VectorIndexStore> {
|
|
104
104
|
const ownsSql = !options.sql;
|
|
105
105
|
const sql = options.sql ?? (await connectLibsql({ url: options.url }));
|
|
106
106
|
if (sql.driverId !== "libsql") {
|
|
@@ -172,7 +172,7 @@ export async function openLibsqlIndex(options: IndexOpenOptions): Promise<IndexS
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/** Protocol-named libsql index driver. */
|
|
175
|
-
export const libsqlIndexDriver:
|
|
175
|
+
export const libsqlIndexDriver: VectorIndexDriver = {
|
|
176
176
|
id: "libsql",
|
|
177
177
|
facet: "index",
|
|
178
178
|
open: openLibsqlIndex,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Meilisearch round-trip — Arabic + typo tolerance.
|
|
3
|
+
*
|
|
4
|
+
* The only acceptable evidence that Meilisearch actually handles Arabic (not
|
|
5
|
+
* just "supported" in marketing copy) is a real round-trip against a live
|
|
6
|
+
* server: index Arabic content, then search it — with and without the
|
|
7
|
+
* definite article (ال), with diacritics stripped, and with a Latin typo that
|
|
8
|
+
* typo-tolerance must forgive.
|
|
9
|
+
*
|
|
10
|
+
* Gated on a live server: set `OKE_TEST_MEILI_URL` (and optionally
|
|
11
|
+
* `OKE_TEST_MEILI_KEY`). Without it the whole suite skips, so unit runs never
|
|
12
|
+
* depend on Docker or a spawned binary.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
16
|
+
import { openMeilisearchIndex, type MeilisearchIndexOptions } from "./meilisearch.ts";
|
|
17
|
+
|
|
18
|
+
const URL = process.env.OKE_TEST_MEILI_URL;
|
|
19
|
+
const KEY = process.env.OKE_TEST_MEILI_KEY;
|
|
20
|
+
if (!URL) {
|
|
21
|
+
console.log("skip: live meilisearch e2e (OKE_TEST_MEILI_URL not set)");
|
|
22
|
+
}
|
|
23
|
+
const live = URL ? test : test.skip;
|
|
24
|
+
|
|
25
|
+
const opened: Array<{ close(): Promise<void> }> = [];
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
while (opened.length > 0) {
|
|
28
|
+
await opened.pop()?.close();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function liveIndex(name: string): ReturnType<typeof openMeilisearchIndex> {
|
|
33
|
+
const opts: MeilisearchIndexOptions = { name, dims: 0, url: URL, apiKey: KEY };
|
|
34
|
+
const index = await openMeilisearchIndex(opts);
|
|
35
|
+
opened.push(index);
|
|
36
|
+
return index;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("meilisearch live — arabic + typo tolerance", () => {
|
|
40
|
+
live("indexes and searches Arabic content (round-trip)", async () => {
|
|
41
|
+
const index = await liveIndex(`ar-${crypto.randomUUID().slice(0, 8)}`);
|
|
42
|
+
await index.upsert("d1", { title: "كتاب عن محركات البحث" });
|
|
43
|
+
await index.upsert("d2", { title: "مقال عن قواعد البيانات" });
|
|
44
|
+
|
|
45
|
+
const found = await index.search("البحث", { topK: 5 });
|
|
46
|
+
expect(found.hits.length).toBeGreaterThan(0);
|
|
47
|
+
expect(found.hits[0]?.id).toBe("d1");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
live("matches a query without the definite article ال", async () => {
|
|
51
|
+
const index = await liveIndex(`ar-${crypto.randomUUID().slice(0, 8)}`);
|
|
52
|
+
await index.upsert("d1", { title: "الكتاب" });
|
|
53
|
+
// Charabia segments/normalises ال, so the bare stem should still match.
|
|
54
|
+
const found = await index.search("كتاب", { topK: 5 });
|
|
55
|
+
expect(found.hits.map((h) => h.id)).toContain("d1");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
live("typo tolerance forgives a latin misspelling", async () => {
|
|
59
|
+
const index = await liveIndex(`en-${crypto.randomUUID().slice(0, 8)}`);
|
|
60
|
+
await index.upsert("d1", { title: "search engine" });
|
|
61
|
+
// One-transposition typo ("serach") must still hit the document.
|
|
62
|
+
const found = await index.search("serach", { topK: 5 });
|
|
63
|
+
expect(found.hits.map((h) => h.id)).toContain("d1");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
live("filter + facets aggregate real results", async () => {
|
|
67
|
+
const index = await liveIndex(`fac-${crypto.randomUUID().slice(0, 8)}`);
|
|
68
|
+
await index.upsert("d1", { title: "red apple", color: "red" });
|
|
69
|
+
await index.upsert("d2", { title: "green apple", color: "green" });
|
|
70
|
+
const found = await index.search("apple", {
|
|
71
|
+
topK: 5,
|
|
72
|
+
filter: "color = red",
|
|
73
|
+
facets: ["color"],
|
|
74
|
+
});
|
|
75
|
+
expect(found.hits.map((h) => h.id)).toEqual(["d1"]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `meilisearch` index driver — task-waited writes, relevance search mapping,
|
|
3
|
+
* and fail-loud semantics against an injected fetch (never a live server).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import {
|
|
8
|
+
MeilisearchUnavailableError,
|
|
9
|
+
meilisearchDriver,
|
|
10
|
+
openMeilisearchIndex,
|
|
11
|
+
} from "./meilisearch.ts";
|
|
12
|
+
|
|
13
|
+
/** A scripted Meilisearch over the wire — in-memory task + document store. */
|
|
14
|
+
export function createMeilisearchFakeFetch(opts?: {
|
|
15
|
+
readonly failHealth?: boolean;
|
|
16
|
+
readonly failTask?: boolean;
|
|
17
|
+
}): typeof fetch {
|
|
18
|
+
const documents = new Map<string, Record<string, unknown>>();
|
|
19
|
+
const taskResults = new Map<number, { fail: boolean }>();
|
|
20
|
+
let taskSeq = 0;
|
|
21
|
+
|
|
22
|
+
const json = (body: unknown, status = 200): Response =>
|
|
23
|
+
new Response(JSON.stringify(body), {
|
|
24
|
+
status,
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const task = (type: string, fail: boolean): object => {
|
|
29
|
+
taskSeq += 1;
|
|
30
|
+
taskResults.set(taskSeq, { fail });
|
|
31
|
+
return { taskUid: taskSeq, indexUid: "kb", status: "enqueued", type };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
35
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
36
|
+
const method = init?.method ?? "GET";
|
|
37
|
+
|
|
38
|
+
if (url.endsWith("/health")) {
|
|
39
|
+
if (opts?.failHealth) return json({ message: "down" }, 503);
|
|
40
|
+
return json({ status: "available" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (url.endsWith("/indexes") && method === "POST") {
|
|
44
|
+
return json(task("indexCreation", false), 202);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (url.includes("/tasks/")) {
|
|
48
|
+
const taskUid = Number(url.split("/tasks/")[1]);
|
|
49
|
+
const fail = taskResults.get(taskUid)?.fail ?? false;
|
|
50
|
+
return json({
|
|
51
|
+
taskUid,
|
|
52
|
+
status: fail ? "failed" : "succeeded",
|
|
53
|
+
error: fail ? { message: "boom" } : null,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (url.includes("/documents/") && method === "DELETE") {
|
|
58
|
+
const id = decodeURIComponent(url.split("/documents/")[1] ?? "");
|
|
59
|
+
if (!documents.delete(id)) return json({ message: "not found" }, 404);
|
|
60
|
+
return json(task("documentDeletion", opts?.failTask ?? false), 202);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (url.endsWith("/documents") && method === "POST") {
|
|
64
|
+
const docs = JSON.parse(String(init?.body ?? "[]")) as Record<string, unknown>[];
|
|
65
|
+
if (!(opts?.failTask ?? false)) {
|
|
66
|
+
for (const doc of docs) documents.set(String(doc.id), doc);
|
|
67
|
+
}
|
|
68
|
+
return json(task("documentAdditionOrUpdate", opts?.failTask ?? false), 202);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (url.endsWith("/search") && method === "POST") {
|
|
72
|
+
const {
|
|
73
|
+
q,
|
|
74
|
+
limit = 10,
|
|
75
|
+
facets,
|
|
76
|
+
} = JSON.parse(String(init?.body ?? "{}")) as {
|
|
77
|
+
q?: string;
|
|
78
|
+
limit?: number;
|
|
79
|
+
facets?: string[];
|
|
80
|
+
};
|
|
81
|
+
const query = (q ?? "").toLowerCase();
|
|
82
|
+
const hits = [...documents.values()]
|
|
83
|
+
.filter((doc) => query === "" || JSON.stringify(doc).toLowerCase().includes(query))
|
|
84
|
+
.slice(0, limit)
|
|
85
|
+
.map((doc) => ({ ...doc, _rankingScore: 0.95 }));
|
|
86
|
+
const facetDistribution: Record<string, Record<string, number>> = {};
|
|
87
|
+
for (const facet of facets ?? []) {
|
|
88
|
+
facetDistribution[facet] = { tagged: documents.size };
|
|
89
|
+
}
|
|
90
|
+
return json({
|
|
91
|
+
hits,
|
|
92
|
+
...(facets && facets.length > 0 ? { facetDistribution } : {}),
|
|
93
|
+
estimatedTotalHits: hits.length,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return json({ message: `no route for ${method} ${url}` }, 404);
|
|
98
|
+
}) as typeof fetch;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("meilisearch driver", () => {
|
|
102
|
+
test("upsert → search maps _rankingScore into relevance score", async () => {
|
|
103
|
+
const index = await openMeilisearchIndex({
|
|
104
|
+
name: "kb",
|
|
105
|
+
dims: 0,
|
|
106
|
+
url: "http://127.0.0.1:7700",
|
|
107
|
+
apiKey: "master-key",
|
|
108
|
+
fetch: createMeilisearchFakeFetch(),
|
|
109
|
+
});
|
|
110
|
+
await index.upsert("d1", { title: "hello world", tag: "a" });
|
|
111
|
+
await index.upsert("d2", { title: "goodbye world", tag: "b" });
|
|
112
|
+
|
|
113
|
+
const found = await index.search("hello", { topK: 1 });
|
|
114
|
+
expect(found.hits).toHaveLength(1);
|
|
115
|
+
expect(found.hits[0]?.id).toBe("d1");
|
|
116
|
+
expect(found.hits[0]?.score).toBeGreaterThan(0);
|
|
117
|
+
expect(found.hits[0]?.meta).toMatchObject({ title: "hello world", tag: "a" });
|
|
118
|
+
await index.close();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("facets return a facetDistribution", async () => {
|
|
122
|
+
const index = await openMeilisearchIndex({
|
|
123
|
+
name: "kb",
|
|
124
|
+
dims: 0,
|
|
125
|
+
url: "http://127.0.0.1:7700",
|
|
126
|
+
fetch: createMeilisearchFakeFetch(),
|
|
127
|
+
});
|
|
128
|
+
await index.upsert("d1", { title: "x", tag: "a" });
|
|
129
|
+
const found = await index.search("x", { facets: ["tag"] });
|
|
130
|
+
expect(found.facetDistribution).toEqual({ tag: { tagged: 1 } });
|
|
131
|
+
await index.close();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("delete removes the document; missing id is false", async () => {
|
|
135
|
+
const index = await openMeilisearchIndex({
|
|
136
|
+
name: "kb",
|
|
137
|
+
dims: 0,
|
|
138
|
+
url: "http://127.0.0.1:7700",
|
|
139
|
+
fetch: createMeilisearchFakeFetch(),
|
|
140
|
+
});
|
|
141
|
+
await index.upsert("d1", { title: "x" });
|
|
142
|
+
expect(await index.delete("d1")).toBe(true);
|
|
143
|
+
expect(await index.delete("d1")).toBe(false);
|
|
144
|
+
await index.close();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("missing URL fails loud (no memory fallback)", async () => {
|
|
148
|
+
await expect(openMeilisearchIndex({ name: "kb", dims: 0 })).rejects.toBeInstanceOf(
|
|
149
|
+
MeilisearchUnavailableError,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("unhealthy server fails loud at open", async () => {
|
|
154
|
+
await expect(
|
|
155
|
+
openMeilisearchIndex({
|
|
156
|
+
name: "kb",
|
|
157
|
+
dims: 0,
|
|
158
|
+
url: "http://127.0.0.1:7700",
|
|
159
|
+
fetch: createMeilisearchFakeFetch({ failHealth: true }),
|
|
160
|
+
}),
|
|
161
|
+
).rejects.toBeInstanceOf(MeilisearchUnavailableError);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a failed indexing task surfaces as a loud error, not a silent write", async () => {
|
|
165
|
+
const index = await openMeilisearchIndex({
|
|
166
|
+
name: "kb",
|
|
167
|
+
dims: 0,
|
|
168
|
+
url: "http://127.0.0.1:7700",
|
|
169
|
+
fetch: createMeilisearchFakeFetch({ failTask: true }),
|
|
170
|
+
});
|
|
171
|
+
await expect(index.upsert("d1", { title: "x" })).rejects.toBeInstanceOf(
|
|
172
|
+
MeilisearchUnavailableError,
|
|
173
|
+
);
|
|
174
|
+
await index.close();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("driver id + facet", () => {
|
|
178
|
+
expect(meilisearchDriver.id).toBe("meilisearch");
|
|
179
|
+
expect(meilisearchDriver.facet).toBe("index");
|
|
180
|
+
});
|
|
181
|
+
});
|