@nacre.work/core 0.0.0
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 +3 -0
- package/dist/authz/cache.d.ts +54 -0
- package/dist/authz/cache.d.ts.map +1 -0
- package/dist/authz/cache.js +63 -0
- package/dist/authz/cache.js.map +1 -0
- package/dist/authz/filter.d.ts +60 -0
- package/dist/authz/filter.d.ts.map +1 -0
- package/dist/authz/filter.js +54 -0
- package/dist/authz/filter.js.map +1 -0
- package/dist/authz/index.d.ts +15 -0
- package/dist/authz/index.d.ts.map +1 -0
- package/dist/authz/index.js +9 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/authz/permissions.d.ts +20 -0
- package/dist/authz/permissions.d.ts.map +1 -0
- package/dist/authz/permissions.js +43 -0
- package/dist/authz/permissions.js.map +1 -0
- package/dist/authz/principals.d.ts +34 -0
- package/dist/authz/principals.d.ts.map +1 -0
- package/dist/authz/principals.js +42 -0
- package/dist/authz/principals.js.map +1 -0
- package/dist/authz/reference.d.ts +25 -0
- package/dist/authz/reference.d.ts.map +1 -0
- package/dist/authz/reference.js +34 -0
- package/dist/authz/reference.js.map +1 -0
- package/dist/authz/resolve.d.ts +45 -0
- package/dist/authz/resolve.d.ts.map +1 -0
- package/dist/authz/resolve.js +98 -0
- package/dist/authz/resolve.js.map +1 -0
- package/dist/authz/scope-tree.d.ts +52 -0
- package/dist/authz/scope-tree.d.ts.map +1 -0
- package/dist/authz/scope-tree.js +56 -0
- package/dist/authz/scope-tree.js.map +1 -0
- package/dist/authz/store.d.ts +50 -0
- package/dist/authz/store.d.ts.map +1 -0
- package/dist/authz/store.js +115 -0
- package/dist/authz/store.js.map +1 -0
- package/dist/authz/tags.d.ts +40 -0
- package/dist/authz/tags.d.ts.map +1 -0
- package/dist/authz/tags.js +48 -0
- package/dist/authz/tags.js.map +1 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +157 -0
- package/dist/config.js.map +1 -0
- package/dist/db/client.d.ts +38 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +34 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/migrate.d.ts +32 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +103 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +77 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +179 -0
- package/dist/metrics.js.map +1 -0
- package/dist/migrate-main.d.ts +2 -0
- package/dist/migrate-main.d.ts.map +1 -0
- package/dist/migrate-main.js +34 -0
- package/dist/migrate-main.js.map +1 -0
- package/dist/migrations/0001_init.sql +226 -0
- package/dist/migrations/0002_rls_coverage.sql +128 -0
- package/dist/migrations/0003_groups_version.sql +48 -0
- package/dist/migrations/0004_acl_version.sql +26 -0
- package/dist/migrations/0005_grants_bump_version.sql +38 -0
- package/dist/migrations/0006_layer_slug_unique.sql +47 -0
- package/dist/migrations/0007_index_lease.sql +42 -0
- package/dist/observability.d.ts +11 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +55 -0
- package/dist/observability.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/vector/query.d.ts +140 -0
- package/dist/vector/query.d.ts.map +1 -0
- package/dist/vector/query.js +94 -0
- package/dist/vector/query.js.map +1 -0
- package/dist/vector/search.d.ts +62 -0
- package/dist/vector/search.d.ts.map +1 -0
- package/dist/vector/search.js +98 -0
- package/dist/vector/search.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reciprocal Rank Fusion over a dense branch and a sparse one, then the caller
|
|
3
|
+
* reranks. Defaults follow docs/architecture.md: 50 candidates per branch.
|
|
4
|
+
*/
|
|
5
|
+
export function buildHybridQuery(options) {
|
|
6
|
+
const { branches, filter, limit, prefetchLimit = 50, withPayload = true } = options;
|
|
7
|
+
if (branches.length === 0) {
|
|
8
|
+
// An empty prefetch list is a query with no constraints on which points it
|
|
9
|
+
// considers, which is the same failure as a missing filter arriving by a
|
|
10
|
+
// different route. Refuse rather than emit it.
|
|
11
|
+
throw new Error('a hybrid query needs at least one branch');
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
prefetch: branches.map((branch) => ({
|
|
15
|
+
query: branch.kind === 'dense'
|
|
16
|
+
? [...branch.vector]
|
|
17
|
+
: { indices: [...branch.vector.indices], values: [...branch.vector.values] },
|
|
18
|
+
using: branch.using,
|
|
19
|
+
// The single source. Every branch gets it; there is no path that skips one.
|
|
20
|
+
filter,
|
|
21
|
+
limit: prefetchLimit,
|
|
22
|
+
})),
|
|
23
|
+
query: { fusion: 'rrf' },
|
|
24
|
+
limit,
|
|
25
|
+
with_payload: withPayload,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The payload indexes the permission filter needs.
|
|
30
|
+
*
|
|
31
|
+
* Without them the filter is evaluated by scanning, which does not fail a test
|
|
32
|
+
* — it just gets slow enough to matter at a customer's volume, which is the
|
|
33
|
+
* worst way to find out. `acl_tags` stays in memory because it participates in
|
|
34
|
+
* every single query.
|
|
35
|
+
*/
|
|
36
|
+
export const PAYLOAD_INDEXES = [
|
|
37
|
+
{ field_name: 'layer_id', field_schema: 'uuid' },
|
|
38
|
+
{ field_name: 'doc_id', field_schema: 'uuid' },
|
|
39
|
+
{ field_name: 'org_id', field_schema: 'uuid' },
|
|
40
|
+
{ field_name: 'deleted', field_schema: 'bool' },
|
|
41
|
+
{
|
|
42
|
+
field_name: 'acl_tags',
|
|
43
|
+
field_schema: { type: 'keyword', is_tenant: false, on_disk: false },
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Collection configuration for one organization.
|
|
48
|
+
*
|
|
49
|
+
* int8 quantization is not a tuning knob: roughly 4x less memory for a recall
|
|
50
|
+
* loss inside one percent, which for corporate search is noise. Originals stay
|
|
51
|
+
* on disk for rescoring.
|
|
52
|
+
*/
|
|
53
|
+
export function collectionConfig(vectorName, size) {
|
|
54
|
+
return {
|
|
55
|
+
vectors: {
|
|
56
|
+
[vectorName]: {
|
|
57
|
+
size,
|
|
58
|
+
distance: 'Cosine',
|
|
59
|
+
hnsw_config: { m: 32, ef_construct: 256 },
|
|
60
|
+
quantization_config: {
|
|
61
|
+
scalar: { type: 'int8', quantile: 0.99, always_ram: true },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
sparse_vectors: { bm25: {} },
|
|
66
|
+
optimizers_config: { default_segment_number: 4 },
|
|
67
|
+
on_disk_payload: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The named vector for a model and dimension: `v_{model}_{dim}`.
|
|
72
|
+
*
|
|
73
|
+
* One function because there are three callers that must agree — the API and
|
|
74
|
+
* MCP search paths derive it from configuration, and a layer stores it in a
|
|
75
|
+
* column the worker reads when writing points. They disagreed: layers created
|
|
76
|
+
* through `POST /v1/layers` got the literal `default`, so the worker wrote to a
|
|
77
|
+
* vector the collection did not have and every upsert failed with `Bad
|
|
78
|
+
* Request`, while search looked for the configured name and would have found
|
|
79
|
+
* nothing even if the write had worked.
|
|
80
|
+
*
|
|
81
|
+
* A mismatch here is not a type error and not a test failure; it is an empty
|
|
82
|
+
* index. Deriving it in more than one place is what made that possible.
|
|
83
|
+
*/
|
|
84
|
+
export function vectorName(model, dimensions) {
|
|
85
|
+
return `v_${model.replace(/[^a-z0-9]/gi, '_')}_${dimensions}`;
|
|
86
|
+
}
|
|
87
|
+
/** `org_{slug}` — offboarding a tenant is then one delete, with no rows to forget. */
|
|
88
|
+
export function collectionName(orgSlug) {
|
|
89
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(orgSlug)) {
|
|
90
|
+
throw new Error(`refusing to build a collection name from: ${orgSlug}`);
|
|
91
|
+
}
|
|
92
|
+
return `org_${orgSlug.replace(/-/g, '_')}`;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../vector/query.ts"],"names":[],"mappings":"AA2DA;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA2B;IAC1D,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,EAAE,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAEnF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,2EAA2E;QAC3E,yEAAyE;QACzE,+CAA+C;QAC/C,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IAC7D,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAClC,KAAK,EACH,MAAM,CAAC,IAAI,KAAK,OAAO;gBACrB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;gBACpB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YAChF,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,4EAA4E;YAC5E,MAAM;YACN,KAAK,EAAE,aAAa;SACrB,CAAC,CAAC;QACH,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QACxB,KAAK;QACL,YAAY,EAAE,WAAW;KAC1B,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE;IAChD,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE;IAC9C,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE;IAC9C,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE;IAC/C;QACE,UAAU,EAAE,UAAU;QACtB,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE;KACpE;CACO,CAAA;AAEV;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,IAAY;IAC/D,OAAO;QACL,OAAO,EAAE;YACP,CAAC,UAAU,CAAC,EAAE;gBACZ,IAAI;gBACJ,QAAQ,EAAE,QAAiB;gBAC3B,WAAW,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE;gBACzC,mBAAmB,EAAE;oBACnB,MAAM,EAAE,EAAE,IAAI,EAAE,MAAe,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE;iBACpE;aACF;SACF;QACD,cAAc,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QAC5B,iBAAiB,EAAE,EAAE,sBAAsB,EAAE,CAAC,EAAE;QAChD,eAAe,EAAE,IAAI;KACtB,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,UAAkB;IAC1D,OAAO,KAAK,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,IAAI,UAAU,EAAE,CAAA;AAC/D,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,6CAA6C,OAAO,EAAE,CAAC,CAAA;IACzE,CAAC;IACD,OAAO,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAA;AAC5C,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type QueryablePlan } from '../authz/filter.js';
|
|
2
|
+
import { type Branch } from './query.js';
|
|
3
|
+
export interface VectorStoreOptions {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
readonly apiKey?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SearchRequest {
|
|
8
|
+
readonly orgId: string;
|
|
9
|
+
readonly orgSlug: string;
|
|
10
|
+
/** What the caller may reach. `none` is not accepted — see buildFilter. */
|
|
11
|
+
readonly plan: QueryablePlan;
|
|
12
|
+
readonly branches: readonly Branch[];
|
|
13
|
+
readonly topK: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Hit {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly score: number;
|
|
18
|
+
readonly payload: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The vector store.
|
|
22
|
+
*
|
|
23
|
+
* One method matters: `search` takes an `AccessPlan` and never a filter. The
|
|
24
|
+
* filter is derived here, from the plan, and there is no way to pass one in —
|
|
25
|
+
* so no caller can assemble a query that reaches the index without going
|
|
26
|
+
* through the permission model first.
|
|
27
|
+
*
|
|
28
|
+
* `top_k` is passed straight through, uncorrected. Asking for more and trimming
|
|
29
|
+
* is a post-filter that also costs more, and it is the specific mistake
|
|
30
|
+
* invariant I2 is written against.
|
|
31
|
+
*/
|
|
32
|
+
export declare class VectorStore {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(options: VectorStoreOptions);
|
|
35
|
+
ensureCollection(orgSlug: string, vectorName: string, size: number): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Mark every point of a document deleted.
|
|
38
|
+
*
|
|
39
|
+
* A payload write, not a removal: physical deletion is the collector's job
|
|
40
|
+
* and runs on its own schedule. This is what actually takes the document out
|
|
41
|
+
* of results, because `deleted = false` is what the pre-filter tests — the
|
|
42
|
+
* Postgres tombstone alone changes nothing a query looks at.
|
|
43
|
+
*
|
|
44
|
+
* It lives on the search client rather than in the worker because the
|
|
45
|
+
* request that deletes a document is served by the API, and a document that
|
|
46
|
+
* is tombstoned in Postgres but not here stays searchable until a background
|
|
47
|
+
* job gets to it. That window is exactly what invariant I5 forbids.
|
|
48
|
+
*/
|
|
49
|
+
tombstone(orgSlug: string, documentId: string): Promise<void>;
|
|
50
|
+
search(request: SearchRequest): Promise<readonly Hit[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Re-check the tenant on the way out.
|
|
53
|
+
*
|
|
54
|
+
* Invariant I1 asks for this explicitly: the filter is checked once more when
|
|
55
|
+
* a response is serialized, and a mismatch is an error rather than a quiet
|
|
56
|
+
* drop. Filtering silently here would hide the bug that produced the row —
|
|
57
|
+
* and a bug that produces one cross-tenant row will produce more.
|
|
58
|
+
*/
|
|
59
|
+
static assertTenant(orgId: string, hits: readonly Hit[]): readonly Hit[];
|
|
60
|
+
close(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../vector/search.ts"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EAAuE,KAAK,MAAM,EAAE,MAAM,YAAY,CAAA;AAE7G,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC1C;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,WAAW;;gBAGV,OAAO,EAAE,kBAAkB;IAWjC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxF;;;;;;;;;;;;OAYG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7D,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,SAAS,GAAG,EAAE,CAAC;IAiB7D;;;;;;;OAOG;IACH,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,GAAG,SAAS,GAAG,EAAE;IAYlE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { QdrantClient } from '@qdrant/js-client-rest';
|
|
2
|
+
import { buildFilter } from '../authz/filter.js';
|
|
3
|
+
import { buildHybridQuery, collectionConfig, collectionName, PAYLOAD_INDEXES } from './query.js';
|
|
4
|
+
/**
|
|
5
|
+
* The vector store.
|
|
6
|
+
*
|
|
7
|
+
* One method matters: `search` takes an `AccessPlan` and never a filter. The
|
|
8
|
+
* filter is derived here, from the plan, and there is no way to pass one in —
|
|
9
|
+
* so no caller can assemble a query that reaches the index without going
|
|
10
|
+
* through the permission model first.
|
|
11
|
+
*
|
|
12
|
+
* `top_k` is passed straight through, uncorrected. Asking for more and trimming
|
|
13
|
+
* is a post-filter that also costs more, and it is the specific mistake
|
|
14
|
+
* invariant I2 is written against.
|
|
15
|
+
*/
|
|
16
|
+
export class VectorStore {
|
|
17
|
+
#client;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
// exactOptionalPropertyTypes distinguishes "absent" from "present and
|
|
20
|
+
// undefined", and the client's parameters mean it: passing apiKey:
|
|
21
|
+
// undefined is not the same as omitting it.
|
|
22
|
+
this.#client = new QdrantClient(options.apiKey === undefined
|
|
23
|
+
? { url: options.url }
|
|
24
|
+
: { url: options.url, apiKey: options.apiKey });
|
|
25
|
+
}
|
|
26
|
+
async ensureCollection(orgSlug, vectorName, size) {
|
|
27
|
+
const name = collectionName(orgSlug);
|
|
28
|
+
const existing = await this.#client.getCollections();
|
|
29
|
+
if (existing.collections.some((c) => c.name === name))
|
|
30
|
+
return;
|
|
31
|
+
await this.#client.createCollection(name, collectionConfig(vectorName, size));
|
|
32
|
+
// Created here rather than left to an operator: a filter that falls back to
|
|
33
|
+
// a scan is a latency problem that only shows up at a customer's volume.
|
|
34
|
+
for (const index of PAYLOAD_INDEXES) {
|
|
35
|
+
await this.#client.createPayloadIndex(name, {
|
|
36
|
+
field_name: index.field_name,
|
|
37
|
+
field_schema: index.field_schema,
|
|
38
|
+
wait: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Mark every point of a document deleted.
|
|
44
|
+
*
|
|
45
|
+
* A payload write, not a removal: physical deletion is the collector's job
|
|
46
|
+
* and runs on its own schedule. This is what actually takes the document out
|
|
47
|
+
* of results, because `deleted = false` is what the pre-filter tests — the
|
|
48
|
+
* Postgres tombstone alone changes nothing a query looks at.
|
|
49
|
+
*
|
|
50
|
+
* It lives on the search client rather than in the worker because the
|
|
51
|
+
* request that deletes a document is served by the API, and a document that
|
|
52
|
+
* is tombstoned in Postgres but not here stays searchable until a background
|
|
53
|
+
* job gets to it. That window is exactly what invariant I5 forbids.
|
|
54
|
+
*/
|
|
55
|
+
async tombstone(orgSlug, documentId) {
|
|
56
|
+
await this.#client.setPayload(collectionName(orgSlug), {
|
|
57
|
+
wait: true,
|
|
58
|
+
payload: { deleted: true },
|
|
59
|
+
filter: { must: [{ key: 'doc_id', match: { value: documentId } }] },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async search(request) {
|
|
63
|
+
const filter = buildFilter(request.orgId, request.plan);
|
|
64
|
+
const query = buildHybridQuery({
|
|
65
|
+
branches: request.branches,
|
|
66
|
+
filter,
|
|
67
|
+
limit: request.topK,
|
|
68
|
+
});
|
|
69
|
+
const result = await this.#client.query(collectionName(request.orgSlug), query);
|
|
70
|
+
return result.points.map((p) => ({
|
|
71
|
+
id: String(p.id),
|
|
72
|
+
score: p.score,
|
|
73
|
+
payload: (p.payload ?? {}),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Re-check the tenant on the way out.
|
|
78
|
+
*
|
|
79
|
+
* Invariant I1 asks for this explicitly: the filter is checked once more when
|
|
80
|
+
* a response is serialized, and a mismatch is an error rather than a quiet
|
|
81
|
+
* drop. Filtering silently here would hide the bug that produced the row —
|
|
82
|
+
* and a bug that produces one cross-tenant row will produce more.
|
|
83
|
+
*/
|
|
84
|
+
static assertTenant(orgId, hits) {
|
|
85
|
+
for (const hit of hits) {
|
|
86
|
+
if (hit.payload.org_id !== orgId) {
|
|
87
|
+
throw new Error(`tenant mismatch on the way out: point ${hit.id} carries org_id ` +
|
|
88
|
+
`${String(hit.payload.org_id)} for a request scoped to ${orgId}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return hits;
|
|
92
|
+
}
|
|
93
|
+
async close() {
|
|
94
|
+
// The REST client holds no pool; this exists so callers have one shape to
|
|
95
|
+
// depend on when it does.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../vector/search.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nacre.work/core",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Data model, permission resolver, shared types",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/nacre-work/nacre.git",
|
|
8
|
+
"directory": "packages/core"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://nacre.work",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -b && node ./scripts/copy-migrations.mjs",
|
|
15
|
+
"dev": "tsx watch index.ts"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@qdrant/js-client-rest": "^1.18.0",
|
|
33
|
+
"pg": "^8.22.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^26.1.2",
|
|
37
|
+
"jose": "^6.2.7"
|
|
38
|
+
}
|
|
39
|
+
}
|