@open-mercato/search 0.6.4-develop.4239.1.4a264a5828 → 0.6.4-develop.4264.1.53368d85fe
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/dist/fulltext/drivers/meilisearch/index.js +14 -2
- package/dist/fulltext/drivers/meilisearch/index.js.map +2 -2
- package/dist/lib/merger.js +10 -3
- package/dist/lib/merger.js.map +2 -2
- package/dist/modules/search/api/search/global/route.js +3 -0
- package/dist/modules/search/api/search/global/route.js.map +2 -2
- package/dist/modules/search/api/search/route.js +3 -0
- package/dist/modules/search/api/search/route.js.map +2 -2
- package/dist/service.js +25 -1
- package/dist/service.js.map +2 -2
- package/dist/strategies/fulltext.strategy.js +6 -0
- package/dist/strategies/fulltext.strategy.js.map +2 -2
- package/dist/strategies/token.strategy.js +16 -4
- package/dist/strategies/token.strategy.js.map +2 -2
- package/dist/strategies/vector.strategy.js +16 -2
- package/dist/strategies/vector.strategy.js.map +2 -2
- package/dist/vector/drivers/pgvector/index.js +8 -2
- package/dist/vector/drivers/pgvector/index.js.map +2 -2
- package/dist/vector/types.js.map +2 -2
- package/package.json +4 -4
- package/src/__tests__/service.test.ts +44 -0
- package/src/fulltext/drivers/meilisearch/index.ts +16 -2
- package/src/fulltext/types.ts +2 -0
- package/src/lib/merger.ts +9 -2
- package/src/modules/search/api/__tests__/org-scoping.routes.test.ts +29 -1
- package/src/modules/search/api/search/global/route.ts +3 -0
- package/src/modules/search/api/search/route.ts +3 -0
- package/src/service.ts +32 -1
- package/src/strategies/fulltext.strategy.ts +9 -0
- package/src/strategies/token.strategy.ts +25 -4
- package/src/strategies/vector.strategy.ts +21 -3
- package/src/vector/drivers/pgvector/index.ts +14 -2
- package/src/vector/types.ts +1 -0
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { sql } from "kysely";
|
|
2
|
+
function normalizeOrganizationIds(options) {
|
|
3
|
+
const single = typeof options.organizationId === "string" ? options.organizationId.trim() : "";
|
|
4
|
+
if (single) return [single];
|
|
5
|
+
if (!Array.isArray(options.organizationIds)) return null;
|
|
6
|
+
return Array.from(new Set(
|
|
7
|
+
options.organizationIds.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0)
|
|
8
|
+
));
|
|
9
|
+
}
|
|
2
10
|
class TokenSearchStrategy {
|
|
3
11
|
constructor(db, config) {
|
|
4
12
|
this.db = db;
|
|
@@ -14,6 +22,8 @@ class TokenSearchStrategy {
|
|
|
14
22
|
async ensureReady() {
|
|
15
23
|
}
|
|
16
24
|
async search(query, options) {
|
|
25
|
+
const organizationIds = normalizeOrganizationIds(options);
|
|
26
|
+
if (organizationIds && organizationIds.length === 0) return [];
|
|
17
27
|
const { tokenizeText } = await import("@open-mercato/shared/lib/search/tokenize");
|
|
18
28
|
const { resolveSearchConfig } = await import("@open-mercato/shared/lib/search/config");
|
|
19
29
|
const config = resolveSearchConfig();
|
|
@@ -25,10 +35,11 @@ class TokenSearchStrategy {
|
|
|
25
35
|
let queryBuilder = this.db.selectFrom("search_tokens").select([
|
|
26
36
|
"entity_type",
|
|
27
37
|
"entity_id",
|
|
38
|
+
"organization_id",
|
|
28
39
|
sql`count(*)`.as("match_count")
|
|
29
|
-
]).where("token_hash", "in", hashes).where("tenant_id", "=", options.tenantId).groupBy(["entity_type", "entity_id"]).having(sql`count(distinct token_hash) >= ${minMatches}`).orderBy(sql`count(distinct token_hash) desc`).limit(limit);
|
|
30
|
-
if (
|
|
31
|
-
queryBuilder = queryBuilder.where("organization_id", "
|
|
40
|
+
]).where("token_hash", "in", hashes).where("tenant_id", "=", options.tenantId).groupBy(["entity_type", "entity_id", "organization_id"]).having(sql`count(distinct token_hash) >= ${minMatches}`).orderBy(sql`count(distinct token_hash) desc`).limit(limit);
|
|
41
|
+
if (organizationIds) {
|
|
42
|
+
queryBuilder = queryBuilder.where("organization_id", "in", organizationIds);
|
|
32
43
|
}
|
|
33
44
|
if (options.entityTypes?.length) {
|
|
34
45
|
queryBuilder = queryBuilder.where("entity_type", "in", options.entityTypes);
|
|
@@ -41,7 +52,8 @@ class TokenSearchStrategy {
|
|
|
41
52
|
entityId: row.entity_type,
|
|
42
53
|
recordId: row.entity_id,
|
|
43
54
|
score,
|
|
44
|
-
source: this.id
|
|
55
|
+
source: this.id,
|
|
56
|
+
organizationId: row.organization_id ?? null
|
|
45
57
|
};
|
|
46
58
|
});
|
|
47
59
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/strategies/token.strategy.ts"],
|
|
4
|
-
"sourcesContent": ["import { type Kysely, sql, type SqlBool } from 'kysely'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\n\n/**\n * Configuration for TokenSearchStrategy.\n */\nexport type TokenStrategyConfig = {\n /** Minimum number of query tokens that must match (0-1 ratio, default 0.5) */\n minMatchRatio?: number\n /** Default limit for search results */\n defaultLimit?: number\n}\n\n/**\n * TokenSearchStrategy provides hash-based search using the existing search_tokens table.\n * This strategy is always available and serves as a fallback when other strategies fail.\n *\n * It tokenizes queries into hashes and matches against pre-indexed token hashes,\n * enabling search on encrypted fields without exposing plaintext to external services.\n */\nexport class TokenSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'tokens'\n readonly name = 'Token Search'\n readonly priority = 10 // Lowest priority, always available as fallback\n\n private readonly minMatchRatio: number\n private readonly defaultLimit: number\n\n constructor(\n private readonly db: Kysely<any>,\n config?: TokenStrategyConfig,\n ) {\n this.minMatchRatio = config?.minMatchRatio ?? 0.5\n this.defaultLimit = config?.defaultLimit ?? 50\n }\n\n async isAvailable(): Promise<boolean> {\n return true // Always available\n }\n\n async ensureReady(): Promise<void> {\n // No initialization needed\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n // Dynamically import tokenization to avoid circular dependencies\n const { tokenizeText } = await import('@open-mercato/shared/lib/search/tokenize')\n const { resolveSearchConfig } = await import('@open-mercato/shared/lib/search/config')\n\n const config = resolveSearchConfig()\n if (!config.enabled) return []\n\n const { hashes } = tokenizeText(query, config)\n if (hashes.length === 0) return []\n\n const minMatches = Math.max(1, Math.ceil(hashes.length * this.minMatchRatio))\n const limit = options.limit ?? this.defaultLimit\n\n let queryBuilder = this.db\n .selectFrom('search_tokens' as any)\n .select([\n 'entity_type' as any,\n 'entity_id' as any,\n sql<string>`count(*)`.as('match_count'),\n ])\n .where('token_hash' as any, 'in', hashes)\n .where('tenant_id' as any, '=', options.tenantId)\n .groupBy(['entity_type' as any, 'entity_id' as any])\n .having(sql<SqlBool>`count(distinct token_hash) >= ${minMatches}`)\n .orderBy(sql`count(distinct token_hash) desc`)\n .limit(limit)\n\n if (
|
|
5
|
-
"mappings": "AAAA,SAAsB,WAAyB;
|
|
4
|
+
"sourcesContent": ["import { type Kysely, sql, type SqlBool } from 'kysely'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\n\n/**\n * Configuration for TokenSearchStrategy.\n */\nexport type TokenStrategyConfig = {\n /** Minimum number of query tokens that must match (0-1 ratio, default 0.5) */\n minMatchRatio?: number\n /** Default limit for search results */\n defaultLimit?: number\n}\n\nfunction normalizeOrganizationIds(options: SearchOptions): string[] | null {\n const single = typeof options.organizationId === 'string' ? options.organizationId.trim() : ''\n if (single) return [single]\n if (!Array.isArray(options.organizationIds)) return null\n return Array.from(new Set(\n options.organizationIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ))\n}\n\n/**\n * TokenSearchStrategy provides hash-based search using the existing search_tokens table.\n * This strategy is always available and serves as a fallback when other strategies fail.\n *\n * It tokenizes queries into hashes and matches against pre-indexed token hashes,\n * enabling search on encrypted fields without exposing plaintext to external services.\n */\nexport class TokenSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'tokens'\n readonly name = 'Token Search'\n readonly priority = 10 // Lowest priority, always available as fallback\n\n private readonly minMatchRatio: number\n private readonly defaultLimit: number\n\n constructor(\n private readonly db: Kysely<any>,\n config?: TokenStrategyConfig,\n ) {\n this.minMatchRatio = config?.minMatchRatio ?? 0.5\n this.defaultLimit = config?.defaultLimit ?? 50\n }\n\n async isAvailable(): Promise<boolean> {\n return true // Always available\n }\n\n async ensureReady(): Promise<void> {\n // No initialization needed\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const organizationIds = normalizeOrganizationIds(options)\n if (organizationIds && organizationIds.length === 0) return []\n\n // Dynamically import tokenization to avoid circular dependencies\n const { tokenizeText } = await import('@open-mercato/shared/lib/search/tokenize')\n const { resolveSearchConfig } = await import('@open-mercato/shared/lib/search/config')\n\n const config = resolveSearchConfig()\n if (!config.enabled) return []\n\n const { hashes } = tokenizeText(query, config)\n if (hashes.length === 0) return []\n\n const minMatches = Math.max(1, Math.ceil(hashes.length * this.minMatchRatio))\n const limit = options.limit ?? this.defaultLimit\n\n let queryBuilder = this.db\n .selectFrom('search_tokens' as any)\n .select([\n 'entity_type' as any,\n 'entity_id' as any,\n 'organization_id' as any,\n sql<string>`count(*)`.as('match_count'),\n ])\n .where('token_hash' as any, 'in', hashes)\n .where('tenant_id' as any, '=', options.tenantId)\n .groupBy(['entity_type' as any, 'entity_id' as any, 'organization_id' as any])\n .having(sql<SqlBool>`count(distinct token_hash) >= ${minMatches}`)\n .orderBy(sql`count(distinct token_hash) desc`)\n .limit(limit)\n\n if (organizationIds) {\n queryBuilder = queryBuilder.where('organization_id' as any, 'in', organizationIds)\n }\n\n if (options.entityTypes?.length) {\n queryBuilder = queryBuilder.where('entity_type' as any, 'in', options.entityTypes)\n }\n\n const rows = await queryBuilder.execute() as Array<{\n entity_type: string\n entity_id: string\n organization_id: string | null\n match_count: string | number\n }>\n\n return rows.map((row) => {\n const matchCount = typeof row.match_count === 'string'\n ? parseInt(row.match_count, 10)\n : row.match_count\n // Calculate score based on match ratio\n const score = matchCount / hashes.length\n\n return {\n entityId: row.entity_type as EntityId,\n recordId: row.entity_id,\n score,\n source: this.id,\n organizationId: row.organization_id ?? null,\n }\n })\n }\n\n async index(record: IndexableRecord): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { replaceSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await replaceSearchTokensForRecord(this.db, {\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields,\n })\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { deleteSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await deleteSearchTokensForRecord(this.db, {\n entityType: entityId,\n recordId,\n tenantId,\n })\n }\n\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (records.length === 0) return\n\n const { replaceSearchTokensForBatch } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n const payloads = records.map((record) => ({\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields as Record<string, unknown>,\n }))\n\n await replaceSearchTokensForBatch(this.db, payloads)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n await this.db\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', entityId)\n .where('tenant_id' as any, '=', tenantId)\n .execute()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
2
|
import { searchDebugWarn } from "../lib/debug.js";
|
|
3
|
+
function normalizeOrganizationIds(options) {
|
|
4
|
+
const single = typeof options.organizationId === "string" ? options.organizationId.trim() : "";
|
|
5
|
+
if (single) return [single];
|
|
6
|
+
if (!Array.isArray(options.organizationIds)) return null;
|
|
7
|
+
return Array.from(new Set(
|
|
8
|
+
options.organizationIds.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0)
|
|
9
|
+
));
|
|
10
|
+
}
|
|
3
11
|
class VectorSearchStrategy {
|
|
4
12
|
constructor(embeddingService, vectorDriver, config) {
|
|
5
13
|
this.embeddingService = embeddingService;
|
|
@@ -24,14 +32,19 @@ class VectorSearchStrategy {
|
|
|
24
32
|
return this.readyPromise;
|
|
25
33
|
}
|
|
26
34
|
async search(query, options) {
|
|
35
|
+
const organizationIds = normalizeOrganizationIds(options);
|
|
36
|
+
if (organizationIds && organizationIds.length === 0) return [];
|
|
27
37
|
await this.ensureReady();
|
|
28
38
|
const embedding = await this.embeddingService.createEmbedding(query);
|
|
29
39
|
const filter = {
|
|
30
40
|
tenantId: options.tenantId,
|
|
31
41
|
entityIds: options.entityTypes
|
|
32
42
|
};
|
|
33
|
-
if (
|
|
34
|
-
filter.
|
|
43
|
+
if (organizationIds) {
|
|
44
|
+
filter.organizationIds = organizationIds;
|
|
45
|
+
if (organizationIds.length === 1) {
|
|
46
|
+
filter.organizationId = organizationIds[0];
|
|
47
|
+
}
|
|
35
48
|
}
|
|
36
49
|
const results = await this.vectorDriver.query({
|
|
37
50
|
vector: embedding,
|
|
@@ -43,6 +56,7 @@ class VectorSearchStrategy {
|
|
|
43
56
|
recordId: hit.recordId,
|
|
44
57
|
score: hit.score,
|
|
45
58
|
source: this.id,
|
|
59
|
+
organizationId: hit.organizationId ?? null,
|
|
46
60
|
presenter: hit.presenter ?? void 0,
|
|
47
61
|
url: hit.primaryLinkHref ?? hit.url ?? void 0,
|
|
48
62
|
links: hit.links?.map((link) => ({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/strategies/vector.strategy.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from 'crypto'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { VectorDriver, VectorDriverDocument } from '../vector/types'\nimport { searchDebugWarn } from '../lib/debug'\n\n/**\n * Embedding service interface - minimal subset needed by VectorSearchStrategy.\n */\nexport interface EmbeddingService {\n createEmbedding(text: string): Promise<number[]>\n available: boolean\n}\n\n/**\n * Configuration for VectorSearchStrategy.\n */\nexport type VectorStrategyConfig = {\n /** Default limit for search results */\n defaultLimit?: number\n}\n\n/**\n * VectorSearchStrategy provides semantic search using embeddings.\n * It wraps the existing vector module infrastructure.\n */\nexport class VectorSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'vector'\n readonly name = 'Vector Search'\n readonly priority = 20 // Medium priority\n\n private readonly defaultLimit: number\n private ready = false\n private readyPromise: Promise<void> | null = null\n\n constructor(\n private readonly embeddingService: EmbeddingService,\n private readonly vectorDriver: VectorDriver,\n config?: VectorStrategyConfig,\n ) {\n this.defaultLimit = config?.defaultLimit ?? 20\n }\n\n async isAvailable(): Promise<boolean> {\n return this.embeddingService.available\n }\n\n async ensureReady(): Promise<void> {\n if (this.ready) return\n if (!this.readyPromise) {\n this.readyPromise = this.vectorDriver.ensureReady().then(() => {\n this.ready = true\n })\n }\n return this.readyPromise\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n await this.ensureReady()\n const embedding = await this.embeddingService.createEmbedding(query)\n\n // Build filter - only include organizationId if it's a real value\n // The pgvector driver treats null as \"only records with null org_id\",\n // but we want null/undefined to mean \"no organization filter\"\n const filter: {\n tenantId: string\n organizationId?: string | null\n entityIds?: EntityId[]\n } = {\n tenantId: options.tenantId,\n entityIds: options.entityTypes as EntityId[],\n }\n\n
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAU3B,SAAS,uBAAuB;
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'crypto'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { VectorDriver, VectorDriverDocument } from '../vector/types'\nimport { searchDebugWarn } from '../lib/debug'\n\n/**\n * Embedding service interface - minimal subset needed by VectorSearchStrategy.\n */\nexport interface EmbeddingService {\n createEmbedding(text: string): Promise<number[]>\n available: boolean\n}\n\n/**\n * Configuration for VectorSearchStrategy.\n */\nexport type VectorStrategyConfig = {\n /** Default limit for search results */\n defaultLimit?: number\n}\n\nfunction normalizeOrganizationIds(options: SearchOptions): string[] | null {\n const single = typeof options.organizationId === 'string' ? options.organizationId.trim() : ''\n if (single) return [single]\n if (!Array.isArray(options.organizationIds)) return null\n return Array.from(new Set(\n options.organizationIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ))\n}\n\n/**\n * VectorSearchStrategy provides semantic search using embeddings.\n * It wraps the existing vector module infrastructure.\n */\nexport class VectorSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'vector'\n readonly name = 'Vector Search'\n readonly priority = 20 // Medium priority\n\n private readonly defaultLimit: number\n private ready = false\n private readyPromise: Promise<void> | null = null\n\n constructor(\n private readonly embeddingService: EmbeddingService,\n private readonly vectorDriver: VectorDriver,\n config?: VectorStrategyConfig,\n ) {\n this.defaultLimit = config?.defaultLimit ?? 20\n }\n\n async isAvailable(): Promise<boolean> {\n return this.embeddingService.available\n }\n\n async ensureReady(): Promise<void> {\n if (this.ready) return\n if (!this.readyPromise) {\n this.readyPromise = this.vectorDriver.ensureReady().then(() => {\n this.ready = true\n })\n }\n return this.readyPromise\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const organizationIds = normalizeOrganizationIds(options)\n if (organizationIds && organizationIds.length === 0) return []\n\n await this.ensureReady()\n const embedding = await this.embeddingService.createEmbedding(query)\n\n // Build filter - only include organizationId if it's a real value\n // The pgvector driver treats null as \"only records with null org_id\",\n // but we want null/undefined to mean \"no organization filter\"\n const filter: {\n tenantId: string\n organizationId?: string | null\n organizationIds?: string[] | null\n entityIds?: EntityId[]\n } = {\n tenantId: options.tenantId,\n entityIds: options.entityTypes as EntityId[],\n }\n\n if (organizationIds) {\n filter.organizationIds = organizationIds\n if (organizationIds.length === 1) {\n filter.organizationId = organizationIds[0]\n }\n }\n\n const results = await this.vectorDriver.query({\n vector: embedding,\n limit: options.limit ?? this.defaultLimit,\n filter,\n })\n\n return results.map((hit) => ({\n entityId: hit.entityId,\n recordId: hit.recordId,\n score: hit.score,\n source: this.id,\n organizationId: hit.organizationId ?? null,\n presenter: hit.presenter ?? undefined,\n url: hit.primaryLinkHref ?? hit.url ?? undefined,\n links: hit.links?.map((link) => ({\n href: link.href,\n label: link.label ?? '',\n kind: link.kind,\n })),\n metadata: hit.payload ?? undefined,\n }))\n }\n\n async index(record: IndexableRecord): Promise<void> {\n await this.ensureReady()\n // Use text from buildSource if available, otherwise fall back to generic extraction\n const textContent = record.text\n ? (Array.isArray(record.text) ? record.text.join('\\n') : record.text)\n : this.buildTextContent(record)\n if (!textContent) return\n\n const embedding = await this.embeddingService.createEmbedding(textContent)\n\n const doc: VectorDriverDocument = {\n entityId: record.entityId as EntityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n checksum: this.computeChecksum(record),\n embedding,\n url: record.url,\n presenter: record.presenter,\n links: record.links,\n driverId: this.vectorDriver.id,\n resultTitle: record.presenter?.title ?? record.recordId,\n resultSubtitle: record.presenter?.subtitle,\n resultIcon: record.presenter?.icon,\n resultBadge: record.presenter?.badge,\n }\n\n await this.vectorDriver.upsert(doc)\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n await this.ensureReady()\n await this.vectorDriver.delete(entityId, recordId, tenantId)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n await this.ensureReady()\n if (this.vectorDriver.purge) {\n await this.vectorDriver.purge(entityId, tenantId)\n }\n }\n\n /**\n * Build text content from record fields for embedding.\n */\n private buildTextContent(record: IndexableRecord): string {\n const parts: string[] = []\n\n // Add presenter info\n if (record.presenter?.title) {\n parts.push(record.presenter.title)\n }\n if (record.presenter?.subtitle) {\n parts.push(record.presenter.subtitle)\n }\n\n // Add string fields from record\n for (const [, value] of Object.entries(record.fields)) {\n if (typeof value === 'string' && value.trim()) {\n parts.push(value)\n }\n }\n\n return parts.join(' ').trim()\n }\n\n /**\n * Compute a checksum for change detection using SHA-256.\n * Uses checksumSource from buildSource if available, otherwise uses fields/presenter/url.\n */\n private computeChecksum(record: IndexableRecord): string {\n const source = record.checksumSource !== undefined\n ? record.checksumSource\n : {\n fields: record.fields,\n presenter: record.presenter,\n url: record.url,\n }\n const content = JSON.stringify(source)\n return createHash('sha256').update(content).digest('hex').slice(0, 16)\n }\n\n /**\n * List entries in the vector index (for admin/debugging).\n */\n async listEntries(options: {\n tenantId: string\n organizationId?: string | null\n entityId?: string\n limit?: number\n offset?: number\n }): Promise<Array<{\n entityId: string\n recordId: string\n tenantId: string\n organizationId: string | null\n presenter?: unknown\n url?: string\n }>> {\n await this.ensureReady()\n // Delegate to vector driver's list method if available\n const listMethod = (this.vectorDriver as unknown as {\n list?: (options: {\n tenantId: string\n organizationId?: string | null\n entityId?: string\n limit?: number\n offset?: number\n }) => Promise<unknown[]>\n }).list\n\n if (typeof listMethod === 'function') {\n const entries = await listMethod.call(this.vectorDriver, options)\n return entries as Array<{\n entityId: string\n recordId: string\n tenantId: string\n organizationId: string | null\n presenter?: unknown\n url?: string\n }>\n }\n\n // Fallback: return empty array if driver doesn't support listing\n searchDebugWarn('VectorSearchStrategy', 'Vector driver does not support listing entries')\n return []\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -252,11 +252,17 @@ function createPgVectorDriver(opts = {}) {
|
|
|
252
252
|
const vectorLiteral = toVectorLiteral(input.vector);
|
|
253
253
|
const filter = input.filter ?? { tenantId: "" };
|
|
254
254
|
const normalizedOrganizationId = typeof filter.organizationId === "string" && filter.organizationId.trim().length > 0 ? filter.organizationId.trim() : null;
|
|
255
|
+
const normalizedOrganizationIds = normalizedOrganizationId ? [normalizedOrganizationId] : Array.isArray(filter.organizationIds) ? Array.from(new Set(
|
|
256
|
+
filter.organizationIds.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0)
|
|
257
|
+
)) : null;
|
|
258
|
+
if (normalizedOrganizationIds && normalizedOrganizationIds.length === 0) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
255
261
|
const params = [
|
|
256
262
|
vectorLiteral,
|
|
257
263
|
DRIVER_ID,
|
|
258
264
|
filter.tenantId,
|
|
259
|
-
|
|
265
|
+
normalizedOrganizationIds,
|
|
260
266
|
Array.isArray(filter.entityIds) && filter.entityIds.length ? filter.entityIds : null,
|
|
261
267
|
input.limit ?? 20
|
|
262
268
|
];
|
|
@@ -282,7 +288,7 @@ function createPgVectorDriver(opts = {}) {
|
|
|
282
288
|
FROM ${tableIdent}
|
|
283
289
|
WHERE driver_id = $2
|
|
284
290
|
AND tenant_id = $3::uuid
|
|
285
|
-
AND ($4::uuid IS NULL OR organization_id = $4::uuid)
|
|
291
|
+
AND ($4::uuid[] IS NULL OR organization_id = ANY($4::uuid[]))
|
|
286
292
|
AND (
|
|
287
293
|
$5::text[] IS NULL OR entity_id = ANY($5::text[])
|
|
288
294
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/vector/drivers/pgvector/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { Pool } from 'pg'\nimport { searchDebugWarn } from '../../../lib/debug'\n\ntype PgPoolQueryResult<T> = { rows: T[]; rowCount?: number }\ntype PgPoolClient = {\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n release(): void\n}\ntype PgPool = {\n connect(): Promise<PgPoolClient>\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n end(): Promise<void>\n}\nimport type {\n VectorDriver,\n VectorDriverDocument,\n VectorDriverQuery,\n VectorDriverQueryResult,\n VectorDriverListParams,\n VectorDriverCountParams,\n VectorIndexEntry,\n VectorDriverRemoveOrphansParams,\n VectorResultPresenter,\n VectorLinkDescriptor,\n} from '../../types'\n\ntype PgVectorDriverOptions = {\n pool?: PgPool\n connectionString?: string\n tableName?: string\n migrationsTable?: string\n dimension?: number\n distanceMetric?: 'cosine' | 'euclidean' | 'inner'\n}\n\nconst DEFAULT_TABLE = 'vector_search'\nconst DEFAULT_MIGRATIONS_TABLE = 'vector_search_migrations'\nconst DEFAULT_DIMENSION = 1536\nconst DRIVER_ID = 'pgvector' as const\n\nfunction assertIdentifier(name: string, defaultName: string): string {\n const candidate = name ?? defaultName\n if (!candidate || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(candidate)) return defaultName\n return candidate\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name}\"`\n}\n\nfunction toVectorLiteral(values: number[]): string {\n const formatted = values.map((n) => {\n if (!Number.isFinite(n)) return '0'\n const rounded = Math.fround(n)\n return Number.isInteger(rounded) ? `${rounded}.0` : `${rounded}`\n })\n return `[${formatted.join(',')}]`\n}\n\nfunction parseJsonColumn<T>(value: unknown): T | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n try {\n return JSON.parse(value) as T\n } catch {\n // When `jsonb` stores a JSON string, node-postgres parses it into a plain JS string.\n // In that case, there is nothing to JSON.parse \u2014 return the raw string value.\n return value as unknown as T\n }\n }\n if (typeof value === 'object') {\n return value as T\n }\n return null\n}\n\nasync function withClient<T>(pool: PgPool, fn: (client: PgPoolClient) => Promise<T>): Promise<T> {\n const client = await pool.connect()\n try {\n return await fn(client)\n } finally {\n client.release()\n }\n}\n\nexport function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDriver {\n const tableName = assertIdentifier(opts.tableName ?? DEFAULT_TABLE, DEFAULT_TABLE)\n const migrationsTable = assertIdentifier(opts.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE, DEFAULT_MIGRATIONS_TABLE)\n let dimension = opts.dimension ?? DEFAULT_DIMENSION\n const distanceMetric = opts.distanceMetric ?? 'cosine'\n const tableIdent = quoteIdent(tableName)\n const migrationsIdent = quoteIdent(migrationsTable)\n\n const pool: PgPool =\n opts.pool ??\n (() => {\n const conn = opts.connectionString ?? process.env.DATABASE_URL\n if (!conn) {\n throw new Error('[vector.pgvector] DATABASE_URL is not configured')\n }\n return new Pool({ connectionString: conn }) as unknown as PgPool\n })()\n\n let ready: Promise<void> | null = null\n\n const ensureReady = async () => {\n if (!ready) {\n ready = withClient(pool, async (client) => {\n const ensureExtension = async (extension: 'pgcrypto' | 'vector') => {\n try {\n await client.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`)\n } catch (error) {\n const pgError = error as { code?: string; message?: string }\n if (pgError?.code === '42501') {\n const details = pgError.message ? ` (${pgError.message})` : ''\n searchDebugWarn('vector.pgvector', `skipping ${extension} extension creation; requires superuser${details}`)\n return\n }\n throw error\n }\n }\n\n await ensureExtension('pgcrypto')\n await ensureExtension('vector')\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${migrationsIdent} (\n id text primary key,\n applied_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${tableIdent} (\n id uuid primary key default gen_random_uuid(),\n driver_id text not null,\n entity_id text not null,\n record_id text not null,\n tenant_id uuid not null,\n organization_id uuid null,\n checksum text not null,\n embedding vector(${dimension}) not null,\n url text null,\n presenter jsonb null,\n links jsonb null,\n payload jsonb null,\n result_title text null,\n result_subtitle text null,\n result_icon text null,\n result_badge text null,\n result_snapshot text null,\n primary_link_href text null,\n primary_link_label text null,\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS ${tableName}_uniq ON ${tableIdent} (driver_id, entity_id, record_id, tenant_id)`,\n )\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_lookup ON ${tableIdent} (tenant_id, organization_id, entity_id)`,\n )\n // ivfflat index only supports up to 2000 dimensions\n // For higher dimensions, skip the index (uses sequential scan, slower but works)\n // Also check actual table dimension in case driver was initialized with different value\n let actualDimension = dimension\n try {\n const dimResult = await client.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (dimResult.rows.length > 0 && dimResult.rows[0].atttypmod > 0) {\n actualDimension = dimResult.rows[0].atttypmod\n }\n } catch {\n // Ignore errors reading dimension, use configured value\n }\n\n if (actualDimension <= 2000) {\n try {\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx ON ${tableIdent}\n USING ivfflat (embedding vector_${distanceMetric}_ops) WITH (lists = 100)`,\n )\n } catch (indexErr: unknown) {\n // Handle case where dimension exceeds ivfflat limit\n const errorMessage = indexErr instanceof Error ? indexErr.message : String(indexErr)\n if (errorMessage.includes('2000 dimensions')) {\n searchDebugWarn('pgvector', 'Skipping ivfflat index - dimension exceeds 2000 limit. Searches will use sequential scan.')\n } else {\n throw indexErr\n }\n }\n } else {\n searchDebugWarn('pgvector', `Skipping ivfflat index - dimension ${actualDimension} exceeds 2000 limit. Searches will use sequential scan.`)\n }\n\n const columnAlters = [\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_title text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_subtitle text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_icon text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_badge text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_snapshot text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_href text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_label text`,\n ]\n for (const statement of columnAlters) {\n await client.query(statement)\n }\n\n await client.query(\n `INSERT INTO ${migrationsIdent} (id, applied_at) VALUES ($1, now()) ON CONFLICT (id) DO NOTHING`,\n ['0001_init'],\n )\n }).catch((err) => {\n ready = null\n throw err\n })\n }\n return ready\n }\n\n const upsert = async (doc: VectorDriverDocument) => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(doc.embedding)\n await pool.query(\n `\n INSERT INTO ${tableIdent} (\n driver_id, entity_id, record_id, tenant_id, organization_id, checksum,\n embedding, url, presenter, links, payload,\n result_title, result_subtitle, result_icon, result_badge, result_snapshot,\n primary_link_href, primary_link_label,\n created_at, updated_at\n )\n VALUES (\n $1, $2, $3, $4::uuid, $5::uuid, $6, $7::vector, $8, $9::jsonb, $10::jsonb, $11::jsonb,\n $12, $13, $14, $15, $16, $17, $18,\n now(), now()\n )\n ON CONFLICT (driver_id, entity_id, record_id, tenant_id)\n DO UPDATE SET\n organization_id = EXCLUDED.organization_id,\n checksum = EXCLUDED.checksum,\n embedding = EXCLUDED.embedding,\n url = EXCLUDED.url,\n presenter = EXCLUDED.presenter,\n links = EXCLUDED.links,\n payload = EXCLUDED.payload,\n result_title = EXCLUDED.result_title,\n result_subtitle = EXCLUDED.result_subtitle,\n result_icon = EXCLUDED.result_icon,\n result_badge = EXCLUDED.result_badge,\n result_snapshot = EXCLUDED.result_snapshot,\n primary_link_href = EXCLUDED.primary_link_href,\n primary_link_label = EXCLUDED.primary_link_label,\n updated_at = now()\n `,\n [\n doc.driverId ?? DRIVER_ID,\n doc.entityId,\n doc.recordId,\n doc.tenantId,\n doc.organizationId ?? null,\n doc.checksum,\n vectorLiteral,\n doc.url ?? null,\n doc.presenter ? JSON.stringify(doc.presenter) : null,\n doc.links ? JSON.stringify(doc.links) : null,\n doc.payload ? JSON.stringify(doc.payload) : null,\n doc.resultTitle,\n doc.resultSubtitle ?? null,\n doc.resultIcon ?? null,\n doc.resultBadge ?? null,\n doc.resultSnapshot ?? null,\n doc.primaryLinkHref ?? null,\n doc.primaryLinkLabel ?? null,\n ],\n )\n }\n\n const remove = async (entityId: string, recordId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n }\n\n const getChecksum = async (entityId: string, recordId: string, tenantId: string): Promise<string | null> => {\n await ensureReady()\n const res = await pool.query<{ checksum: string }>(\n `SELECT checksum FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n return res.rowCount ? res.rows[0].checksum : null\n }\n\n const purge = async (entityId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND tenant_id = $3::uuid`,\n [DRIVER_ID, entityId, tenantId],\n )\n }\n\n const query = async (input: VectorDriverQuery): Promise<VectorDriverQueryResult[]> => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(input.vector)\n const filter = input.filter ?? { tenantId: '' }\n const normalizedOrganizationId =\n typeof filter.organizationId === 'string' && filter.organizationId.trim().length > 0\n ? filter.organizationId.trim()\n : null\n const params: any[] = [\n vectorLiteral,\n DRIVER_ID,\n filter.tenantId,\n normalizedOrganizationId,\n Array.isArray(filter.entityIds) && filter.entityIds.length ? filter.entityIds : null,\n input.limit ?? 20,\n ]\n const res = await pool.query<{\n entity_id: string\n record_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n distance: number\n }>(\n `\n SELECT\n entity_id,\n record_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n embedding <=> $1::vector AS distance\n FROM ${tableIdent}\n WHERE driver_id = $2\n AND tenant_id = $3::uuid\n AND ($4::uuid IS NULL OR organization_id = $4::uuid)\n AND (\n $5::text[] IS NULL OR entity_id = ANY($5::text[])\n )\n ORDER BY embedding <=> $1::vector\n LIMIT $6\n `,\n params,\n )\n return res.rows.map<VectorDriverQueryResult>((row) => {\n const distance = typeof row.distance === 'number' ? row.distance : Number(row.distance || 1)\n const score = 1 - distance\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter: parseJsonColumn<VectorResultPresenter>(row.presenter),\n links: parseJsonColumn<VectorLinkDescriptor[]>(row.links),\n payload: parseJsonColumn<Record<string, unknown>>(row.payload),\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n score,\n }\n })\n }\n\n const list = async (params: VectorDriverListParams): Promise<VectorIndexEntry[]> => {\n await ensureReady()\n const limit = Math.max(1, Math.min(params.limit ?? 50, 200))\n const offset = Math.max(0, params.offset ?? 0)\n const orderColumn = params.orderBy === 'created' ? 'created_at' : 'updated_at'\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const limitParam = nextParam\n const offsetParam = nextParam + 1\n values.push(limit, offset)\n\n const sql = `\n SELECT\n entity_id,\n record_id,\n tenant_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n created_at,\n updated_at\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n ORDER BY ${orderColumn} DESC\n LIMIT $${limitParam} OFFSET $${offsetParam}\n `\n\n const res = await pool.query<{\n entity_id: string\n record_id: string\n tenant_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n created_at: Date | string\n updated_at: Date | string\n }>(sql, values)\n return res.rows.map<VectorIndexEntry>((row) => {\n const presenter = parseJsonColumn<VectorResultPresenter>(row.presenter)\n const links = parseJsonColumn<VectorLinkDescriptor[]>(row.links)\n const payload = parseJsonColumn<Record<string, unknown>>(row.payload)\n const createdAt =\n row.created_at instanceof Date\n ? row.created_at.toISOString()\n : new Date(row.created_at ?? Date.now()).toISOString()\n const updatedAt =\n row.updated_at instanceof Date\n ? row.updated_at.toISOString()\n : new Date(row.updated_at ?? Date.now()).toISOString()\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n driverId: DRIVER_ID,\n tenantId: row.tenant_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter,\n links,\n payload,\n metadata: payload,\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n createdAt,\n updatedAt,\n score: null,\n }\n })\n }\n\n const count = async (params: VectorDriverCountParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const sql = `\n SELECT count(*)::bigint AS total\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query<{ total: string }>(sql, values)\n const raw = res.rows?.[0]?.total\n if (!raw) return 0\n const parsed = Number(raw)\n return Number.isFinite(parsed) ? parsed : 0\n }\n\n const removeOrphans = async (params: VectorDriverRemoveOrphansParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'entity_id = $2',\n 'updated_at < $3::timestamptz',\n ]\n const values: any[] = [DRIVER_ID, params.entityId, (params.olderThan instanceof Date ? params.olderThan : new Date(params.olderThan)).toISOString()]\n let nextParam = 4\n\n if (params.tenantId !== undefined) {\n conditions.push(`tenant_id is not distinct from $${nextParam}::uuid`)\n values.push(params.tenantId)\n nextParam += 1\n }\n\n if (params.organizationId !== undefined) {\n conditions.push(`organization_id is not distinct from $${nextParam}::uuid`)\n values.push(params.organizationId)\n nextParam += 1\n }\n\n const sql = `\n DELETE FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query(sql, values)\n return res.rowCount ?? 0\n }\n\n const getTableDimension = async (): Promise<number | null> => {\n try {\n const res = await pool.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (res.rows.length > 0 && res.rows[0].atttypmod > 0) {\n return res.rows[0].atttypmod\n }\n return null\n } catch {\n return null\n }\n }\n\n const recreateWithDimension = async (newDimension: number): Promise<void> => {\n await withClient(pool, async (client) => {\n await client.query(`DROP TABLE IF EXISTS ${tableIdent} CASCADE`)\n await client.query(`DROP TABLE IF EXISTS ${migrationsIdent} CASCADE`)\n })\n ready = null\n dimension = newDimension\n await ensureReady()\n }\n\n return {\n id: 'pgvector',\n ensureReady,\n upsert,\n delete: remove,\n getChecksum,\n purge,\n query,\n list,\n count,\n removeOrphans,\n getTableDimension,\n recreateWithDimension,\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { Pool } from 'pg'\nimport { searchDebugWarn } from '../../../lib/debug'\n\ntype PgPoolQueryResult<T> = { rows: T[]; rowCount?: number }\ntype PgPoolClient = {\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n release(): void\n}\ntype PgPool = {\n connect(): Promise<PgPoolClient>\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n end(): Promise<void>\n}\nimport type {\n VectorDriver,\n VectorDriverDocument,\n VectorDriverQuery,\n VectorDriverQueryResult,\n VectorDriverListParams,\n VectorDriverCountParams,\n VectorIndexEntry,\n VectorDriverRemoveOrphansParams,\n VectorResultPresenter,\n VectorLinkDescriptor,\n} from '../../types'\n\ntype PgVectorDriverOptions = {\n pool?: PgPool\n connectionString?: string\n tableName?: string\n migrationsTable?: string\n dimension?: number\n distanceMetric?: 'cosine' | 'euclidean' | 'inner'\n}\n\nconst DEFAULT_TABLE = 'vector_search'\nconst DEFAULT_MIGRATIONS_TABLE = 'vector_search_migrations'\nconst DEFAULT_DIMENSION = 1536\nconst DRIVER_ID = 'pgvector' as const\n\nfunction assertIdentifier(name: string, defaultName: string): string {\n const candidate = name ?? defaultName\n if (!candidate || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(candidate)) return defaultName\n return candidate\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name}\"`\n}\n\nfunction toVectorLiteral(values: number[]): string {\n const formatted = values.map((n) => {\n if (!Number.isFinite(n)) return '0'\n const rounded = Math.fround(n)\n return Number.isInteger(rounded) ? `${rounded}.0` : `${rounded}`\n })\n return `[${formatted.join(',')}]`\n}\n\nfunction parseJsonColumn<T>(value: unknown): T | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n try {\n return JSON.parse(value) as T\n } catch {\n // When `jsonb` stores a JSON string, node-postgres parses it into a plain JS string.\n // In that case, there is nothing to JSON.parse \u2014 return the raw string value.\n return value as unknown as T\n }\n }\n if (typeof value === 'object') {\n return value as T\n }\n return null\n}\n\nasync function withClient<T>(pool: PgPool, fn: (client: PgPoolClient) => Promise<T>): Promise<T> {\n const client = await pool.connect()\n try {\n return await fn(client)\n } finally {\n client.release()\n }\n}\n\nexport function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDriver {\n const tableName = assertIdentifier(opts.tableName ?? DEFAULT_TABLE, DEFAULT_TABLE)\n const migrationsTable = assertIdentifier(opts.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE, DEFAULT_MIGRATIONS_TABLE)\n let dimension = opts.dimension ?? DEFAULT_DIMENSION\n const distanceMetric = opts.distanceMetric ?? 'cosine'\n const tableIdent = quoteIdent(tableName)\n const migrationsIdent = quoteIdent(migrationsTable)\n\n const pool: PgPool =\n opts.pool ??\n (() => {\n const conn = opts.connectionString ?? process.env.DATABASE_URL\n if (!conn) {\n throw new Error('[vector.pgvector] DATABASE_URL is not configured')\n }\n return new Pool({ connectionString: conn }) as unknown as PgPool\n })()\n\n let ready: Promise<void> | null = null\n\n const ensureReady = async () => {\n if (!ready) {\n ready = withClient(pool, async (client) => {\n const ensureExtension = async (extension: 'pgcrypto' | 'vector') => {\n try {\n await client.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`)\n } catch (error) {\n const pgError = error as { code?: string; message?: string }\n if (pgError?.code === '42501') {\n const details = pgError.message ? ` (${pgError.message})` : ''\n searchDebugWarn('vector.pgvector', `skipping ${extension} extension creation; requires superuser${details}`)\n return\n }\n throw error\n }\n }\n\n await ensureExtension('pgcrypto')\n await ensureExtension('vector')\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${migrationsIdent} (\n id text primary key,\n applied_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${tableIdent} (\n id uuid primary key default gen_random_uuid(),\n driver_id text not null,\n entity_id text not null,\n record_id text not null,\n tenant_id uuid not null,\n organization_id uuid null,\n checksum text not null,\n embedding vector(${dimension}) not null,\n url text null,\n presenter jsonb null,\n links jsonb null,\n payload jsonb null,\n result_title text null,\n result_subtitle text null,\n result_icon text null,\n result_badge text null,\n result_snapshot text null,\n primary_link_href text null,\n primary_link_label text null,\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS ${tableName}_uniq ON ${tableIdent} (driver_id, entity_id, record_id, tenant_id)`,\n )\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_lookup ON ${tableIdent} (tenant_id, organization_id, entity_id)`,\n )\n // ivfflat index only supports up to 2000 dimensions\n // For higher dimensions, skip the index (uses sequential scan, slower but works)\n // Also check actual table dimension in case driver was initialized with different value\n let actualDimension = dimension\n try {\n const dimResult = await client.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (dimResult.rows.length > 0 && dimResult.rows[0].atttypmod > 0) {\n actualDimension = dimResult.rows[0].atttypmod\n }\n } catch {\n // Ignore errors reading dimension, use configured value\n }\n\n if (actualDimension <= 2000) {\n try {\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx ON ${tableIdent}\n USING ivfflat (embedding vector_${distanceMetric}_ops) WITH (lists = 100)`,\n )\n } catch (indexErr: unknown) {\n // Handle case where dimension exceeds ivfflat limit\n const errorMessage = indexErr instanceof Error ? indexErr.message : String(indexErr)\n if (errorMessage.includes('2000 dimensions')) {\n searchDebugWarn('pgvector', 'Skipping ivfflat index - dimension exceeds 2000 limit. Searches will use sequential scan.')\n } else {\n throw indexErr\n }\n }\n } else {\n searchDebugWarn('pgvector', `Skipping ivfflat index - dimension ${actualDimension} exceeds 2000 limit. Searches will use sequential scan.`)\n }\n\n const columnAlters = [\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_title text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_subtitle text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_icon text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_badge text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_snapshot text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_href text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_label text`,\n ]\n for (const statement of columnAlters) {\n await client.query(statement)\n }\n\n await client.query(\n `INSERT INTO ${migrationsIdent} (id, applied_at) VALUES ($1, now()) ON CONFLICT (id) DO NOTHING`,\n ['0001_init'],\n )\n }).catch((err) => {\n ready = null\n throw err\n })\n }\n return ready\n }\n\n const upsert = async (doc: VectorDriverDocument) => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(doc.embedding)\n await pool.query(\n `\n INSERT INTO ${tableIdent} (\n driver_id, entity_id, record_id, tenant_id, organization_id, checksum,\n embedding, url, presenter, links, payload,\n result_title, result_subtitle, result_icon, result_badge, result_snapshot,\n primary_link_href, primary_link_label,\n created_at, updated_at\n )\n VALUES (\n $1, $2, $3, $4::uuid, $5::uuid, $6, $7::vector, $8, $9::jsonb, $10::jsonb, $11::jsonb,\n $12, $13, $14, $15, $16, $17, $18,\n now(), now()\n )\n ON CONFLICT (driver_id, entity_id, record_id, tenant_id)\n DO UPDATE SET\n organization_id = EXCLUDED.organization_id,\n checksum = EXCLUDED.checksum,\n embedding = EXCLUDED.embedding,\n url = EXCLUDED.url,\n presenter = EXCLUDED.presenter,\n links = EXCLUDED.links,\n payload = EXCLUDED.payload,\n result_title = EXCLUDED.result_title,\n result_subtitle = EXCLUDED.result_subtitle,\n result_icon = EXCLUDED.result_icon,\n result_badge = EXCLUDED.result_badge,\n result_snapshot = EXCLUDED.result_snapshot,\n primary_link_href = EXCLUDED.primary_link_href,\n primary_link_label = EXCLUDED.primary_link_label,\n updated_at = now()\n `,\n [\n doc.driverId ?? DRIVER_ID,\n doc.entityId,\n doc.recordId,\n doc.tenantId,\n doc.organizationId ?? null,\n doc.checksum,\n vectorLiteral,\n doc.url ?? null,\n doc.presenter ? JSON.stringify(doc.presenter) : null,\n doc.links ? JSON.stringify(doc.links) : null,\n doc.payload ? JSON.stringify(doc.payload) : null,\n doc.resultTitle,\n doc.resultSubtitle ?? null,\n doc.resultIcon ?? null,\n doc.resultBadge ?? null,\n doc.resultSnapshot ?? null,\n doc.primaryLinkHref ?? null,\n doc.primaryLinkLabel ?? null,\n ],\n )\n }\n\n const remove = async (entityId: string, recordId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n }\n\n const getChecksum = async (entityId: string, recordId: string, tenantId: string): Promise<string | null> => {\n await ensureReady()\n const res = await pool.query<{ checksum: string }>(\n `SELECT checksum FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n return res.rowCount ? res.rows[0].checksum : null\n }\n\n const purge = async (entityId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND tenant_id = $3::uuid`,\n [DRIVER_ID, entityId, tenantId],\n )\n }\n\n const query = async (input: VectorDriverQuery): Promise<VectorDriverQueryResult[]> => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(input.vector)\n const filter = input.filter ?? { tenantId: '' }\n const normalizedOrganizationId =\n typeof filter.organizationId === 'string' && filter.organizationId.trim().length > 0\n ? filter.organizationId.trim()\n : null\n const normalizedOrganizationIds = normalizedOrganizationId\n ? [normalizedOrganizationId]\n : Array.isArray(filter.organizationIds)\n ? Array.from(new Set(\n filter.organizationIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ))\n : null\n if (normalizedOrganizationIds && normalizedOrganizationIds.length === 0) {\n return []\n }\n const params: any[] = [\n vectorLiteral,\n DRIVER_ID,\n filter.tenantId,\n normalizedOrganizationIds,\n Array.isArray(filter.entityIds) && filter.entityIds.length ? filter.entityIds : null,\n input.limit ?? 20,\n ]\n const res = await pool.query<{\n entity_id: string\n record_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n distance: number\n }>(\n `\n SELECT\n entity_id,\n record_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n embedding <=> $1::vector AS distance\n FROM ${tableIdent}\n WHERE driver_id = $2\n AND tenant_id = $3::uuid\n AND ($4::uuid[] IS NULL OR organization_id = ANY($4::uuid[]))\n AND (\n $5::text[] IS NULL OR entity_id = ANY($5::text[])\n )\n ORDER BY embedding <=> $1::vector\n LIMIT $6\n `,\n params,\n )\n return res.rows.map<VectorDriverQueryResult>((row) => {\n const distance = typeof row.distance === 'number' ? row.distance : Number(row.distance || 1)\n const score = 1 - distance\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter: parseJsonColumn<VectorResultPresenter>(row.presenter),\n links: parseJsonColumn<VectorLinkDescriptor[]>(row.links),\n payload: parseJsonColumn<Record<string, unknown>>(row.payload),\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n score,\n }\n })\n }\n\n const list = async (params: VectorDriverListParams): Promise<VectorIndexEntry[]> => {\n await ensureReady()\n const limit = Math.max(1, Math.min(params.limit ?? 50, 200))\n const offset = Math.max(0, params.offset ?? 0)\n const orderColumn = params.orderBy === 'created' ? 'created_at' : 'updated_at'\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const limitParam = nextParam\n const offsetParam = nextParam + 1\n values.push(limit, offset)\n\n const sql = `\n SELECT\n entity_id,\n record_id,\n tenant_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n created_at,\n updated_at\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n ORDER BY ${orderColumn} DESC\n LIMIT $${limitParam} OFFSET $${offsetParam}\n `\n\n const res = await pool.query<{\n entity_id: string\n record_id: string\n tenant_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n created_at: Date | string\n updated_at: Date | string\n }>(sql, values)\n return res.rows.map<VectorIndexEntry>((row) => {\n const presenter = parseJsonColumn<VectorResultPresenter>(row.presenter)\n const links = parseJsonColumn<VectorLinkDescriptor[]>(row.links)\n const payload = parseJsonColumn<Record<string, unknown>>(row.payload)\n const createdAt =\n row.created_at instanceof Date\n ? row.created_at.toISOString()\n : new Date(row.created_at ?? Date.now()).toISOString()\n const updatedAt =\n row.updated_at instanceof Date\n ? row.updated_at.toISOString()\n : new Date(row.updated_at ?? Date.now()).toISOString()\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n driverId: DRIVER_ID,\n tenantId: row.tenant_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter,\n links,\n payload,\n metadata: payload,\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n createdAt,\n updatedAt,\n score: null,\n }\n })\n }\n\n const count = async (params: VectorDriverCountParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const sql = `\n SELECT count(*)::bigint AS total\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query<{ total: string }>(sql, values)\n const raw = res.rows?.[0]?.total\n if (!raw) return 0\n const parsed = Number(raw)\n return Number.isFinite(parsed) ? parsed : 0\n }\n\n const removeOrphans = async (params: VectorDriverRemoveOrphansParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'entity_id = $2',\n 'updated_at < $3::timestamptz',\n ]\n const values: any[] = [DRIVER_ID, params.entityId, (params.olderThan instanceof Date ? params.olderThan : new Date(params.olderThan)).toISOString()]\n let nextParam = 4\n\n if (params.tenantId !== undefined) {\n conditions.push(`tenant_id is not distinct from $${nextParam}::uuid`)\n values.push(params.tenantId)\n nextParam += 1\n }\n\n if (params.organizationId !== undefined) {\n conditions.push(`organization_id is not distinct from $${nextParam}::uuid`)\n values.push(params.organizationId)\n nextParam += 1\n }\n\n const sql = `\n DELETE FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query(sql, values)\n return res.rowCount ?? 0\n }\n\n const getTableDimension = async (): Promise<number | null> => {\n try {\n const res = await pool.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (res.rows.length > 0 && res.rows[0].atttypmod > 0) {\n return res.rows[0].atttypmod\n }\n return null\n } catch {\n return null\n }\n }\n\n const recreateWithDimension = async (newDimension: number): Promise<void> => {\n await withClient(pool, async (client) => {\n await client.query(`DROP TABLE IF EXISTS ${tableIdent} CASCADE`)\n await client.query(`DROP TABLE IF EXISTS ${migrationsIdent} CASCADE`)\n })\n ready = null\n dimension = newDimension\n await ensureReady()\n }\n\n return {\n id: 'pgvector',\n ensureReady,\n upsert,\n delete: remove,\n getChecksum,\n purge,\n query,\n list,\n count,\n removeOrphans,\n getTableDimension,\n recreateWithDimension,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAkChC,MAAM,gBAAgB;AACtB,MAAM,2BAA2B;AACjC,MAAM,oBAAoB;AAC1B,MAAM,YAAY;AAElB,SAAS,iBAAiB,MAAc,aAA6B;AACnE,QAAM,YAAY,QAAQ;AAC1B,MAAI,CAAC,aAAa,CAAC,2BAA2B,KAAK,SAAS,EAAG,QAAO;AACtE,SAAO;AACT;AAEA,SAAS,WAAW,MAAsB;AACxC,SAAO,IAAI,IAAI;AACjB;AAEA,SAAS,gBAAgB,QAA0B;AACjD,QAAM,YAAY,OAAO,IAAI,CAAC,MAAM;AAClC,QAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAChC,UAAM,UAAU,KAAK,OAAO,CAAC;AAC7B,WAAO,OAAO,UAAU,OAAO,IAAI,GAAG,OAAO,OAAO,GAAG,OAAO;AAAA,EAChE,CAAC;AACD,SAAO,IAAI,UAAU,KAAK,GAAG,CAAC;AAChC;AAEA,SAAS,gBAAmB,OAA0B;AACpD,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI;AACF,aAAO,KAAK,MAAM,KAAK;AAAA,IACzB,QAAQ;AAGN,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,eAAe,WAAc,MAAc,IAAsD;AAC/F,QAAM,SAAS,MAAM,KAAK,QAAQ;AAClC,MAAI;AACF,WAAO,MAAM,GAAG,MAAM;AAAA,EACxB,UAAE;AACA,WAAO,QAAQ;AAAA,EACjB;AACF;AAEO,SAAS,qBAAqB,OAA8B,CAAC,GAAiB;AACnF,QAAM,YAAY,iBAAiB,KAAK,aAAa,eAAe,aAAa;AACjF,QAAM,kBAAkB,iBAAiB,KAAK,mBAAmB,0BAA0B,wBAAwB;AACnH,MAAI,YAAY,KAAK,aAAa;AAClC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,aAAa,WAAW,SAAS;AACvC,QAAM,kBAAkB,WAAW,eAAe;AAElD,QAAM,OACJ,KAAK,SACJ,MAAM;AACL,UAAM,OAAO,KAAK,oBAAoB,QAAQ,IAAI;AAClD,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,kDAAkD;AAAA,IACpE;AACA,WAAO,IAAI,KAAK,EAAE,kBAAkB,KAAK,CAAC;AAAA,EAC5C,GAAG;AAEL,MAAI,QAA8B;AAElC,QAAM,cAAc,YAAY;AAC9B,QAAI,CAAC,OAAO;AACV,cAAQ,WAAW,MAAM,OAAO,WAAW;AACzC,cAAM,kBAAkB,OAAO,cAAqC;AAClE,cAAI;AACF,kBAAM,OAAO,MAAM,kCAAkC,SAAS,EAAE;AAAA,UAClE,SAAS,OAAO;AACd,kBAAM,UAAU;AAChB,gBAAI,SAAS,SAAS,SAAS;AAC7B,oBAAM,UAAU,QAAQ,UAAU,KAAK,QAAQ,OAAO,MAAM;AAC5D,8BAAgB,mBAAmB,YAAY,SAAS,0CAA0C,OAAO,EAAE;AAC3G;AAAA,YACF;AACA,kBAAM;AAAA,UACR;AAAA,QACF;AAEA,cAAM,gBAAgB,UAAU;AAChC,cAAM,gBAAgB,QAAQ;AAE9B,cAAM,OAAO;AAAA,UACX,8BAA8B,eAAe;AAAA;AAAA;AAAA;AAAA,QAI/C;AAEA,cAAM,OAAO;AAAA,UACX,8BAA8B,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAQnB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAehC;AAEA,cAAM,OAAO;AAAA,UACX,qCAAqC,SAAS,YAAY,UAAU;AAAA,QACtE;AACA,cAAM,OAAO;AAAA,UACX,8BAA8B,SAAS,cAAc,UAAU;AAAA,QACjE;AAIA,YAAI,kBAAkB;AACtB,YAAI;AACF,gBAAM,YAAY,MAAM,OAAO;AAAA,YAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAMA,CAAC,SAAS;AAAA,UACZ;AACA,cAAI,UAAU,KAAK,SAAS,KAAK,UAAU,KAAK,CAAC,EAAE,YAAY,GAAG;AAChE,8BAAkB,UAAU,KAAK,CAAC,EAAE;AAAA,UACtC;AAAA,QACF,QAAQ;AAAA,QAER;AAEA,YAAI,mBAAmB,KAAM;AAC3B,cAAI;AACF,kBAAM,OAAO;AAAA,cACX,8BAA8B,SAAS,qBAAqB,UAAU;AAAA,kDAClC,cAAc;AAAA,YACpD;AAAA,UACF,SAAS,UAAmB;AAE1B,kBAAM,eAAe,oBAAoB,QAAQ,SAAS,UAAU,OAAO,QAAQ;AACnF,gBAAI,aAAa,SAAS,iBAAiB,GAAG;AAC5C,8BAAgB,YAAY,2FAA2F;AAAA,YACzH,OAAO;AACL,oBAAM;AAAA,YACR;AAAA,UACF;AAAA,QACF,OAAO;AACL,0BAAgB,YAAY,sCAAsC,eAAe,yDAAyD;AAAA,QAC5I;AAEA,cAAM,eAAe;AAAA,UACnB,eAAe,UAAU;AAAA,UACzB,eAAe,UAAU;AAAA,UACzB,eAAe,UAAU;AAAA,UACzB,eAAe,UAAU;AAAA,UACzB,eAAe,UAAU;AAAA,UACzB,eAAe,UAAU;AAAA,UACzB,eAAe,UAAU;AAAA,QAC3B;AACA,mBAAW,aAAa,cAAc;AACpC,gBAAM,OAAO,MAAM,SAAS;AAAA,QAC9B;AAEA,cAAM,OAAO;AAAA,UACX,eAAe,eAAe;AAAA,UAC9B,CAAC,WAAW;AAAA,QACd;AAAA,MACF,CAAC,EAAE,MAAM,CAAC,QAAQ;AAChB,gBAAQ;AACR,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,OAAO,QAA8B;AAClD,UAAM,YAAY;AAClB,UAAM,gBAAgB,gBAAgB,IAAI,SAAS;AACnD,UAAM,KAAK;AAAA,MACT;AAAA,sBACgB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MA8B1B;AAAA,QACE,IAAI,YAAY;AAAA,QAChB,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI,kBAAkB;AAAA,QACtB,IAAI;AAAA,QACJ;AAAA,QACA,IAAI,OAAO;AAAA,QACX,IAAI,YAAY,KAAK,UAAU,IAAI,SAAS,IAAI;AAAA,QAChD,IAAI,QAAQ,KAAK,UAAU,IAAI,KAAK,IAAI;AAAA,QACxC,IAAI,UAAU,KAAK,UAAU,IAAI,OAAO,IAAI;AAAA,QAC5C,IAAI;AAAA,QACJ,IAAI,kBAAkB;AAAA,QACtB,IAAI,cAAc;AAAA,QAClB,IAAI,eAAe;AAAA,QACnB,IAAI,kBAAkB;AAAA,QACtB,IAAI,mBAAmB;AAAA,QACvB,IAAI,oBAAoB;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,OAAO,UAAkB,UAAkB,aAAqB;AAC7E,UAAM,YAAY;AAClB,UAAM,KAAK;AAAA,MACT,eAAe,UAAU;AAAA,MACzB,CAAC,WAAW,UAAU,UAAU,QAAQ;AAAA,IAC1C;AAAA,EACF;AAEA,QAAM,cAAc,OAAO,UAAkB,UAAkB,aAA6C;AAC1G,UAAM,YAAY;AAClB,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,wBAAwB,UAAU;AAAA,MAClC,CAAC,WAAW,UAAU,UAAU,QAAQ;AAAA,IAC1C;AACA,WAAO,IAAI,WAAW,IAAI,KAAK,CAAC,EAAE,WAAW;AAAA,EAC/C;AAEA,QAAM,QAAQ,OAAO,UAAkB,aAAqB;AAC1D,UAAM,YAAY;AAClB,UAAM,KAAK;AAAA,MACT,eAAe,UAAU;AAAA,MACzB,CAAC,WAAW,UAAU,QAAQ;AAAA,IAChC;AAAA,EACF;AAEA,QAAM,QAAQ,OAAO,UAAiE;AACpF,UAAM,YAAY;AAClB,UAAM,gBAAgB,gBAAgB,MAAM,MAAM;AAClD,UAAM,SAAS,MAAM,UAAU,EAAE,UAAU,GAAG;AAC9C,UAAM,2BACJ,OAAO,OAAO,mBAAmB,YAAY,OAAO,eAAe,KAAK,EAAE,SAAS,IAC/E,OAAO,eAAe,KAAK,IAC3B;AACN,UAAM,4BAA4B,2BAC9B,CAAC,wBAAwB,IACzB,MAAM,QAAQ,OAAO,eAAe,IAClC,MAAM,KAAK,IAAI;AAAA,MACb,OAAO,gBACJ,IAAI,CAAC,UAAW,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,EAAG,EAC9D,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC;AAAA,IACvC,CAAC,IACD;AACN,QAAI,6BAA6B,0BAA0B,WAAW,GAAG;AACvE,aAAO,CAAC;AAAA,IACV;AACA,UAAM,SAAgB;AAAA,MACpB;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA,MAAM,QAAQ,OAAO,SAAS,KAAK,OAAO,UAAU,SAAS,OAAO,YAAY;AAAA,MAChF,MAAM,SAAS;AAAA,IACjB;AACA,UAAM,MAAM,MAAM,KAAK;AAAA,MAkBrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAkBS,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAUnB;AAAA,IACF;AACA,WAAO,IAAI,KAAK,IAA6B,CAAC,QAAQ;AACpD,YAAM,WAAW,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW,OAAO,IAAI,YAAY,CAAC;AAC3F,YAAM,QAAQ,IAAI;AAClB,aAAO;AAAA,QACL,UAAU,IAAI;AAAA,QACd,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI,mBAAmB;AAAA,QACvC,UAAU,IAAI;AAAA,QACd,KAAK,IAAI,OAAO;AAAA,QAChB,WAAW,gBAAuC,IAAI,SAAS;AAAA,QAC/D,OAAO,gBAAwC,IAAI,KAAK;AAAA,QACxD,SAAS,gBAAyC,IAAI,OAAO;AAAA,QAC7D,aAAa,IAAI,gBAAgB;AAAA,QACjC,gBAAgB,IAAI,mBAAmB;AAAA,QACvC,YAAY,IAAI,eAAe;AAAA,QAC/B,aAAa,IAAI,gBAAgB;AAAA,QACjC,gBAAgB,IAAI,mBAAmB;AAAA,QACvC,iBAAiB,IAAI,qBAAqB;AAAA,QAC1C,kBAAkB,IAAI,sBAAsB;AAAA,QAC5C;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,OAAO,OAAO,WAAgE;AAClF,UAAM,YAAY;AAClB,UAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,SAAS,IAAI,GAAG,CAAC;AAC3D,UAAM,SAAS,KAAK,IAAI,GAAG,OAAO,UAAU,CAAC;AAC7C,UAAM,cAAc,OAAO,YAAY,YAAY,eAAe;AAClE,UAAM,aAAuB;AAAA,MAC3B;AAAA,MACA;AAAA,IACF;AACA,UAAM,SAAgB,CAAC,WAAW,OAAO,QAAQ;AACjD,QAAI,YAAY;AAEhB,UAAM,2BACJ,OAAO,OAAO,mBAAmB,YAAY,OAAO,eAAe,KAAK,EAAE,SAAS,IAC/E,OAAO,eAAe,KAAK,IAC3B;AACN,QAAI,6BAA6B,MAAM;AACrC,iBAAW,KAAK,sBAAsB,SAAS,QAAQ;AACvD,aAAO,KAAK,wBAAwB;AACpC,mBAAa;AAAA,IACf;AAEA,QAAI,OAAO,UAAU;AACnB,iBAAW,KAAK,gBAAgB,SAAS,QAAQ;AACjD,aAAO,KAAK,OAAO,QAAQ;AAC3B,mBAAa;AAAA,IACf;AAEA,UAAM,aAAa;AACnB,UAAM,cAAc,YAAY;AAChC,WAAO,KAAK,OAAO,MAAM;AAEzB,UAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAoBD,UAAU;AAAA,gBACT,WAAW,KAAK,kBAAkB,CAAC;AAAA,mBAChC,WAAW;AAAA,iBACb,UAAU,YAAY,WAAW;AAAA;AAG9C,UAAM,MAAM,MAAM,KAAK,MAmBpB,KAAK,MAAM;AACd,WAAO,IAAI,KAAK,IAAsB,CAAC,QAAQ;AAC7C,YAAM,YAAY,gBAAuC,IAAI,SAAS;AACtE,YAAM,QAAQ,gBAAwC,IAAI,KAAK;AAC/D,YAAM,UAAU,gBAAyC,IAAI,OAAO;AACpE,YAAM,YACJ,IAAI,sBAAsB,OACtB,IAAI,WAAW,YAAY,IAC3B,IAAI,KAAK,IAAI,cAAc,KAAK,IAAI,CAAC,EAAE,YAAY;AACzD,YAAM,YACJ,IAAI,sBAAsB,OACtB,IAAI,WAAW,YAAY,IAC3B,IAAI,KAAK,IAAI,cAAc,KAAK,IAAI,CAAC,EAAE,YAAY;AACzD,aAAO;AAAA,QACL,UAAU,IAAI;AAAA,QACd,UAAU,IAAI;AAAA,QACd,UAAU;AAAA,QACV,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI,mBAAmB;AAAA,QACvC,UAAU,IAAI;AAAA,QACd,KAAK,IAAI,OAAO;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV,aAAa,IAAI,gBAAgB;AAAA,QACjC,gBAAgB,IAAI,mBAAmB;AAAA,QACvC,YAAY,IAAI,eAAe;AAAA,QAC/B,aAAa,IAAI,gBAAgB;AAAA,QACjC,gBAAgB,IAAI,mBAAmB;AAAA,QACvC,iBAAiB,IAAI,qBAAqB;AAAA,QAC1C,kBAAkB,IAAI,sBAAsB;AAAA,QAC5C;AAAA,QACA;AAAA,QACA,OAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,QAAQ,OAAO,WAAqD;AACxE,UAAM,YAAY;AAClB,UAAM,aAAuB;AAAA,MAC3B;AAAA,MACA;AAAA,IACF;AACA,UAAM,SAAgB,CAAC,WAAW,OAAO,QAAQ;AACjD,QAAI,YAAY;AAEhB,UAAM,2BACJ,OAAO,OAAO,mBAAmB,YAAY,OAAO,eAAe,KAAK,EAAE,SAAS,IAC/E,OAAO,eAAe,KAAK,IAC3B;AACN,QAAI,6BAA6B,MAAM;AACrC,iBAAW,KAAK,sBAAsB,SAAS,QAAQ;AACvD,aAAO,KAAK,wBAAwB;AACpC,mBAAa;AAAA,IACf;AACA,QAAI,OAAO,UAAU;AACnB,iBAAW,KAAK,gBAAgB,SAAS,QAAQ;AACjD,aAAO,KAAK,OAAO,QAAQ;AAC3B,mBAAa;AAAA,IACf;AAEA,UAAM,MAAM;AAAA;AAAA,eAED,UAAU;AAAA,gBACT,WAAW,KAAK,kBAAkB,CAAC;AAAA;AAE/C,UAAM,MAAM,MAAM,KAAK,MAAyB,KAAK,MAAM;AAC3D,UAAM,MAAM,IAAI,OAAO,CAAC,GAAG;AAC3B,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,SAAS,OAAO,GAAG;AACzB,WAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAAA,EAC5C;AAEA,QAAM,gBAAgB,OAAO,WAA6D;AACxF,UAAM,YAAY;AAClB,UAAM,aAAuB;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,SAAgB,CAAC,WAAW,OAAO,WAAW,OAAO,qBAAqB,OAAO,OAAO,YAAY,IAAI,KAAK,OAAO,SAAS,GAAG,YAAY,CAAC;AACnJ,QAAI,YAAY;AAEhB,QAAI,OAAO,aAAa,QAAW;AACjC,iBAAW,KAAK,mCAAmC,SAAS,QAAQ;AACpE,aAAO,KAAK,OAAO,QAAQ;AAC3B,mBAAa;AAAA,IACf;AAEA,QAAI,OAAO,mBAAmB,QAAW;AACvC,iBAAW,KAAK,yCAAyC,SAAS,QAAQ;AAC1E,aAAO,KAAK,OAAO,cAAc;AACjC,mBAAa;AAAA,IACf;AAEA,UAAM,MAAM;AAAA,sBACM,UAAU;AAAA,gBAChB,WAAW,KAAK,kBAAkB,CAAC;AAAA;AAE/C,UAAM,MAAM,MAAM,KAAK,MAAM,KAAK,MAAM;AACxC,WAAO,IAAI,YAAY;AAAA,EACzB;AAEA,QAAM,oBAAoB,YAAoC;AAC5D,QAAI;AACF,YAAM,MAAM,MAAM,KAAK;AAAA,QACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMA,CAAC,SAAS;AAAA,MACZ;AACA,UAAI,IAAI,KAAK,SAAS,KAAK,IAAI,KAAK,CAAC,EAAE,YAAY,GAAG;AACpD,eAAO,IAAI,KAAK,CAAC,EAAE;AAAA,MACrB;AACA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,wBAAwB,OAAO,iBAAwC;AAC3E,UAAM,WAAW,MAAM,OAAO,WAAW;AACvC,YAAM,OAAO,MAAM,wBAAwB,UAAU,UAAU;AAC/D,YAAM,OAAO,MAAM,wBAAwB,eAAe,UAAU;AAAA,IACtE,CAAC;AACD,YAAQ;AACR,gBAAY;AACZ,UAAM,YAAY;AAAA,EACpB;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/vector/types.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/vector/types.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n} from '@open-mercato/shared/modules/vector'\n\nexport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n}\n\nexport type VectorDriverDocument = {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n checksum: string\n embedding: number[]\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n driverId: VectorDriverId\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverQuery = {\n vector: number[]\n limit?: number\n filter?: {\n entityIds?: EntityId[]\n organizationId?: string | null\n tenantId: string\n }\n}\n\nexport type VectorDriverQueryResult = {\n entityId: EntityId\n recordId: string\n organizationId?: string | null\n score: number\n checksum: string\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverListParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n limit?: number\n offset?: number\n orderBy?: 'created' | 'updated'\n}\n\nexport type VectorDriverCountParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n}\n\nexport type VectorDriverRemoveOrphansParams = {\n entityId: EntityId\n tenantId?: string | null\n organizationId?: string | null\n olderThan: Date\n}\n\nexport interface VectorDriver {\n readonly id: VectorDriverId\n ensureReady(): Promise<void>\n upsert(doc: VectorDriverDocument): Promise<void>\n delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void>\n query(input: VectorDriverQuery): Promise<VectorDriverQueryResult[]>\n getChecksum(entityId: EntityId, recordId: string, tenantId: string): Promise<string | null>\n purge?(entityId: EntityId, tenantId: string): Promise<void>\n list?(params: VectorDriverListParams): Promise<VectorIndexEntry[]>\n count?(params: VectorDriverCountParams): Promise<number>\n removeOrphans?(params: VectorDriverRemoveOrphansParams): Promise<number | void>\n getTableDimension?(): Promise<number | null>\n recreateWithDimension?(newDimension: number): Promise<void>\n}\n\n// ============================================================================\n// Embedding Provider Configuration Types\n// ============================================================================\n\nexport type EmbeddingProviderId =\n | 'openai'\n | 'google'\n | 'mistral'\n | 'cohere'\n | 'bedrock'\n | 'ollama'\n\nexport type EmbeddingProviderConfig = {\n providerId: EmbeddingProviderId\n model: string\n dimension: number\n outputDimensionality?: number\n baseUrl?: string\n updatedAt: string\n}\n\nexport type EmbeddingModelInfo = {\n id: string\n name: string\n dimension: number\n configurableDimension?: boolean\n minDimension?: number\n maxDimension?: number\n}\n\nexport type EmbeddingProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: EmbeddingModelInfo[]\n}\n\nexport const EMBEDDING_PROVIDERS: Record<EmbeddingProviderId, EmbeddingProviderInfo> = {\n openai: {\n name: 'OpenAI',\n envKeyRequired: 'OPENAI_API_KEY',\n defaultModel: 'text-embedding-3-small',\n models: [\n { id: 'text-embedding-3-small', name: 'text-embedding-3-small', dimension: 1536 },\n { id: 'text-embedding-3-large', name: 'text-embedding-3-large', dimension: 3072, configurableDimension: true, minDimension: 256, maxDimension: 3072 },\n { id: 'text-embedding-ada-002', name: 'text-embedding-ada-002', dimension: 1536 },\n ],\n },\n google: {\n name: 'Google Generative AI',\n envKeyRequired: 'GOOGLE_GENERATIVE_AI_API_KEY',\n defaultModel: 'text-embedding-004',\n models: [\n { id: 'text-embedding-004', name: 'text-embedding-004', dimension: 768, configurableDimension: true, minDimension: 1, maxDimension: 768 },\n { id: 'embedding-001', name: 'embedding-001', dimension: 768 },\n ],\n },\n mistral: {\n name: 'Mistral',\n envKeyRequired: 'MISTRAL_API_KEY',\n defaultModel: 'mistral-embed',\n models: [\n { id: 'mistral-embed', name: 'mistral-embed', dimension: 1024 },\n ],\n },\n cohere: {\n name: 'Cohere',\n envKeyRequired: 'COHERE_API_KEY',\n defaultModel: 'embed-english-v3.0',\n models: [\n { id: 'embed-english-v3.0', name: 'embed-english-v3.0', dimension: 1024 },\n { id: 'embed-multilingual-v3.0', name: 'embed-multilingual-v3.0', dimension: 1024 },\n { id: 'embed-english-light-v3.0', name: 'embed-english-light-v3.0', dimension: 384 },\n { id: 'embed-multilingual-light-v3.0', name: 'embed-multilingual-light-v3.0', dimension: 384 },\n ],\n },\n bedrock: {\n name: 'Amazon Bedrock',\n envKeyRequired: 'AWS_ACCESS_KEY_ID',\n defaultModel: 'amazon.titan-embed-text-v2:0',\n models: [\n { id: 'amazon.titan-embed-text-v2:0', name: 'Titan Embed Text v2', dimension: 1024, configurableDimension: true, minDimension: 256, maxDimension: 1024 },\n { id: 'amazon.titan-embed-text-v1', name: 'Titan Embed Text v1', dimension: 1536 },\n { id: 'cohere.embed-english-v3', name: 'Cohere Embed English v3', dimension: 1024 },\n { id: 'cohere.embed-multilingual-v3', name: 'Cohere Embed Multilingual v3', dimension: 1024 },\n ],\n },\n ollama: {\n name: 'Ollama (Local)',\n envKeyRequired: 'OLLAMA_BASE_URL',\n defaultModel: 'nomic-embed-text',\n models: [\n { id: 'nomic-embed-text', name: 'nomic-embed-text', dimension: 768 },\n { id: 'mxbai-embed-large', name: 'mxbai-embed-large', dimension: 1024 },\n { id: 'all-minilm', name: 'all-minilm', dimension: 384 },\n { id: 'snowflake-arctic-embed', name: 'snowflake-arctic-embed', dimension: 1024 },\n ],\n },\n}\n\nexport const EMBEDDING_CONFIG_KEY = 'embedding_provider'\n\nexport const DEFAULT_EMBEDDING_CONFIG: EmbeddingProviderConfig = {\n providerId: 'openai',\n model: 'text-embedding-3-small',\n dimension: 1536,\n updatedAt: new Date().toISOString(),\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n} from '@open-mercato/shared/modules/vector'\n\nexport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n}\n\nexport type VectorDriverDocument = {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n checksum: string\n embedding: number[]\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n driverId: VectorDriverId\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverQuery = {\n vector: number[]\n limit?: number\n filter?: {\n entityIds?: EntityId[]\n organizationId?: string | null\n organizationIds?: string[] | null\n tenantId: string\n }\n}\n\nexport type VectorDriverQueryResult = {\n entityId: EntityId\n recordId: string\n organizationId?: string | null\n score: number\n checksum: string\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverListParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n limit?: number\n offset?: number\n orderBy?: 'created' | 'updated'\n}\n\nexport type VectorDriverCountParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n}\n\nexport type VectorDriverRemoveOrphansParams = {\n entityId: EntityId\n tenantId?: string | null\n organizationId?: string | null\n olderThan: Date\n}\n\nexport interface VectorDriver {\n readonly id: VectorDriverId\n ensureReady(): Promise<void>\n upsert(doc: VectorDriverDocument): Promise<void>\n delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void>\n query(input: VectorDriverQuery): Promise<VectorDriverQueryResult[]>\n getChecksum(entityId: EntityId, recordId: string, tenantId: string): Promise<string | null>\n purge?(entityId: EntityId, tenantId: string): Promise<void>\n list?(params: VectorDriverListParams): Promise<VectorIndexEntry[]>\n count?(params: VectorDriverCountParams): Promise<number>\n removeOrphans?(params: VectorDriverRemoveOrphansParams): Promise<number | void>\n getTableDimension?(): Promise<number | null>\n recreateWithDimension?(newDimension: number): Promise<void>\n}\n\n// ============================================================================\n// Embedding Provider Configuration Types\n// ============================================================================\n\nexport type EmbeddingProviderId =\n | 'openai'\n | 'google'\n | 'mistral'\n | 'cohere'\n | 'bedrock'\n | 'ollama'\n\nexport type EmbeddingProviderConfig = {\n providerId: EmbeddingProviderId\n model: string\n dimension: number\n outputDimensionality?: number\n baseUrl?: string\n updatedAt: string\n}\n\nexport type EmbeddingModelInfo = {\n id: string\n name: string\n dimension: number\n configurableDimension?: boolean\n minDimension?: number\n maxDimension?: number\n}\n\nexport type EmbeddingProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: EmbeddingModelInfo[]\n}\n\nexport const EMBEDDING_PROVIDERS: Record<EmbeddingProviderId, EmbeddingProviderInfo> = {\n openai: {\n name: 'OpenAI',\n envKeyRequired: 'OPENAI_API_KEY',\n defaultModel: 'text-embedding-3-small',\n models: [\n { id: 'text-embedding-3-small', name: 'text-embedding-3-small', dimension: 1536 },\n { id: 'text-embedding-3-large', name: 'text-embedding-3-large', dimension: 3072, configurableDimension: true, minDimension: 256, maxDimension: 3072 },\n { id: 'text-embedding-ada-002', name: 'text-embedding-ada-002', dimension: 1536 },\n ],\n },\n google: {\n name: 'Google Generative AI',\n envKeyRequired: 'GOOGLE_GENERATIVE_AI_API_KEY',\n defaultModel: 'text-embedding-004',\n models: [\n { id: 'text-embedding-004', name: 'text-embedding-004', dimension: 768, configurableDimension: true, minDimension: 1, maxDimension: 768 },\n { id: 'embedding-001', name: 'embedding-001', dimension: 768 },\n ],\n },\n mistral: {\n name: 'Mistral',\n envKeyRequired: 'MISTRAL_API_KEY',\n defaultModel: 'mistral-embed',\n models: [\n { id: 'mistral-embed', name: 'mistral-embed', dimension: 1024 },\n ],\n },\n cohere: {\n name: 'Cohere',\n envKeyRequired: 'COHERE_API_KEY',\n defaultModel: 'embed-english-v3.0',\n models: [\n { id: 'embed-english-v3.0', name: 'embed-english-v3.0', dimension: 1024 },\n { id: 'embed-multilingual-v3.0', name: 'embed-multilingual-v3.0', dimension: 1024 },\n { id: 'embed-english-light-v3.0', name: 'embed-english-light-v3.0', dimension: 384 },\n { id: 'embed-multilingual-light-v3.0', name: 'embed-multilingual-light-v3.0', dimension: 384 },\n ],\n },\n bedrock: {\n name: 'Amazon Bedrock',\n envKeyRequired: 'AWS_ACCESS_KEY_ID',\n defaultModel: 'amazon.titan-embed-text-v2:0',\n models: [\n { id: 'amazon.titan-embed-text-v2:0', name: 'Titan Embed Text v2', dimension: 1024, configurableDimension: true, minDimension: 256, maxDimension: 1024 },\n { id: 'amazon.titan-embed-text-v1', name: 'Titan Embed Text v1', dimension: 1536 },\n { id: 'cohere.embed-english-v3', name: 'Cohere Embed English v3', dimension: 1024 },\n { id: 'cohere.embed-multilingual-v3', name: 'Cohere Embed Multilingual v3', dimension: 1024 },\n ],\n },\n ollama: {\n name: 'Ollama (Local)',\n envKeyRequired: 'OLLAMA_BASE_URL',\n defaultModel: 'nomic-embed-text',\n models: [\n { id: 'nomic-embed-text', name: 'nomic-embed-text', dimension: 768 },\n { id: 'mxbai-embed-large', name: 'mxbai-embed-large', dimension: 1024 },\n { id: 'all-minilm', name: 'all-minilm', dimension: 384 },\n { id: 'snowflake-arctic-embed', name: 'snowflake-arctic-embed', dimension: 1024 },\n ],\n },\n}\n\nexport const EMBEDDING_CONFIG_KEY = 'embedding_provider'\n\nexport const DEFAULT_EMBEDDING_CONFIG: EmbeddingProviderConfig = {\n providerId: 'openai',\n model: 'text-embedding-3-small',\n dimension: 1536,\n updatedAt: new Date().toISOString(),\n}\n"],
|
|
5
|
+
"mappings": "AAsJO,MAAM,sBAA0E;AAAA,EACrF,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,MAChF,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,MAAM,uBAAuB,MAAM,cAAc,KAAK,cAAc,KAAK;AAAA,MACpJ,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,IAClF;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,sBAAsB,MAAM,sBAAsB,WAAW,KAAK,uBAAuB,MAAM,cAAc,GAAG,cAAc,IAAI;AAAA,MACxI,EAAE,IAAI,iBAAiB,MAAM,iBAAiB,WAAW,IAAI;AAAA,IAC/D;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,iBAAiB,MAAM,iBAAiB,WAAW,KAAK;AAAA,IAChE;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,sBAAsB,MAAM,sBAAsB,WAAW,KAAK;AAAA,MACxE,EAAE,IAAI,2BAA2B,MAAM,2BAA2B,WAAW,KAAK;AAAA,MAClF,EAAE,IAAI,4BAA4B,MAAM,4BAA4B,WAAW,IAAI;AAAA,MACnF,EAAE,IAAI,iCAAiC,MAAM,iCAAiC,WAAW,IAAI;AAAA,IAC/F;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,gCAAgC,MAAM,uBAAuB,WAAW,MAAM,uBAAuB,MAAM,cAAc,KAAK,cAAc,KAAK;AAAA,MACvJ,EAAE,IAAI,8BAA8B,MAAM,uBAAuB,WAAW,KAAK;AAAA,MACjF,EAAE,IAAI,2BAA2B,MAAM,2BAA2B,WAAW,KAAK;AAAA,MAClF,EAAE,IAAI,gCAAgC,MAAM,gCAAgC,WAAW,KAAK;AAAA,IAC9F;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,oBAAoB,MAAM,oBAAoB,WAAW,IAAI;AAAA,MACnE,EAAE,IAAI,qBAAqB,MAAM,qBAAqB,WAAW,KAAK;AAAA,MACtE,EAAE,IAAI,cAAc,MAAM,cAAc,WAAW,IAAI;AAAA,MACvD,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,IAClF;AAAA,EACF;AACF;AAEO,MAAM,uBAAuB;AAE7B,MAAM,2BAAoD;AAAA,EAC/D,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AACpC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/search",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.4264.1.53368d85fe",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -126,9 +126,9 @@
|
|
|
126
126
|
"zod": "^4.4.3"
|
|
127
127
|
},
|
|
128
128
|
"peerDependencies": {
|
|
129
|
-
"@open-mercato/core": "0.6.4-develop.
|
|
130
|
-
"@open-mercato/queue": "0.6.4-develop.
|
|
131
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
129
|
+
"@open-mercato/core": "0.6.4-develop.4264.1.53368d85fe",
|
|
130
|
+
"@open-mercato/queue": "0.6.4-develop.4264.1.53368d85fe",
|
|
131
|
+
"@open-mercato/shared": "0.6.4-develop.4264.1.53368d85fe"
|
|
132
132
|
},
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"@types/jest": "^30.0.0",
|
|
@@ -232,6 +232,50 @@ describe('SearchService', () => {
|
|
|
232
232
|
expect(results[0].source).toBe('fallback')
|
|
233
233
|
})
|
|
234
234
|
|
|
235
|
+
it('should drop cross-organization results for scoped multi-org searches', async () => {
|
|
236
|
+
const strategy = createMockStrategy({
|
|
237
|
+
id: 'test',
|
|
238
|
+
search: jest.fn().mockResolvedValue([
|
|
239
|
+
createMockResult({ recordId: 'rec-a', organizationId: 'org-A' }),
|
|
240
|
+
createMockResult({ recordId: 'rec-b', organizationId: 'org-B' }),
|
|
241
|
+
createMockResult({ recordId: 'rec-c', organizationId: 'org-C' }),
|
|
242
|
+
createMockResult({ recordId: 'rec-unknown', organizationId: undefined }),
|
|
243
|
+
]),
|
|
244
|
+
})
|
|
245
|
+
const service = new SearchService({
|
|
246
|
+
strategies: [strategy],
|
|
247
|
+
defaultStrategies: ['test'],
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const results = await service.search('test', {
|
|
251
|
+
tenantId: 'tenant-123',
|
|
252
|
+
organizationIds: ['org-A', 'org-B'],
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(results.map((result) => result.recordId)).toEqual(['rec-a', 'rec-b'])
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should fail closed for an empty organization allowlist', async () => {
|
|
259
|
+
const strategy = createMockStrategy({
|
|
260
|
+
id: 'test',
|
|
261
|
+
search: jest.fn().mockResolvedValue([
|
|
262
|
+
createMockResult({ recordId: 'rec-a', organizationId: 'org-A' }),
|
|
263
|
+
]),
|
|
264
|
+
})
|
|
265
|
+
const service = new SearchService({
|
|
266
|
+
strategies: [strategy],
|
|
267
|
+
defaultStrategies: ['test'],
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const results = await service.search('test', {
|
|
271
|
+
tenantId: 'tenant-123',
|
|
272
|
+
organizationIds: [],
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
expect(results).toEqual([])
|
|
276
|
+
expect(strategy.search).not.toHaveBeenCalled()
|
|
277
|
+
})
|
|
278
|
+
|
|
235
279
|
it('should enrich results when navigation metadata is missing even if presenter title exists', async () => {
|
|
236
280
|
const strategy = createMockStrategy({
|
|
237
281
|
id: 'test',
|
|
@@ -66,8 +66,21 @@ export function createMeilisearchDriver(
|
|
|
66
66
|
function buildFilters(options: FullTextSearchQuery): string[] {
|
|
67
67
|
const filters: string[] = []
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const organizationId = typeof options.organizationId === 'string' ? options.organizationId.trim() : ''
|
|
70
|
+
if (organizationId) {
|
|
71
|
+
filters.push(`_organizationId = "${escapeFilterValue(organizationId)}"`)
|
|
72
|
+
} else if (Array.isArray(options.organizationIds)) {
|
|
73
|
+
const organizationIds = Array.from(new Set(
|
|
74
|
+
options.organizationIds
|
|
75
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
76
|
+
.filter((value) => value.length > 0),
|
|
77
|
+
))
|
|
78
|
+
if (organizationIds.length === 0) {
|
|
79
|
+
filters.push('_organizationId = "__open_mercato_no_matching_organization__"')
|
|
80
|
+
} else {
|
|
81
|
+
const orgFilter = organizationIds.map((id) => `"${escapeFilterValue(id)}"`).join(', ')
|
|
82
|
+
filters.push(`_organizationId IN [${orgFilter}]`)
|
|
83
|
+
}
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
if (options.entityTypes?.length) {
|
|
@@ -216,6 +229,7 @@ export function createMeilisearchDriver(
|
|
|
216
229
|
recordId: hit._id as string,
|
|
217
230
|
entityId: hit._entityId as EntityId,
|
|
218
231
|
score: (hit._rankingScore as number) ?? 0.5,
|
|
232
|
+
organizationId: hit._organizationId as string | null | undefined,
|
|
219
233
|
presenter: hit._presenter as FullTextSearchHit['presenter'],
|
|
220
234
|
url: hit._url as string | undefined,
|
|
221
235
|
links: hit._links as FullTextSearchHit['links'],
|
package/src/fulltext/types.ts
CHANGED
|
@@ -39,6 +39,7 @@ export type FullTextSearchDocument = {
|
|
|
39
39
|
export type FullTextSearchQuery = {
|
|
40
40
|
tenantId: string
|
|
41
41
|
organizationId?: string | null
|
|
42
|
+
organizationIds?: string[] | null
|
|
42
43
|
entityTypes?: EntityId[]
|
|
43
44
|
limit?: number
|
|
44
45
|
offset?: number
|
|
@@ -52,6 +53,7 @@ export type FullTextSearchHit = {
|
|
|
52
53
|
recordId: string
|
|
53
54
|
entityId: EntityId
|
|
54
55
|
score: number
|
|
56
|
+
organizationId?: string | null
|
|
55
57
|
presenter?: SearchResultPresenter
|
|
56
58
|
url?: string
|
|
57
59
|
links?: SearchResultLink[]
|
package/src/lib/merger.ts
CHANGED
|
@@ -65,15 +65,22 @@ export function mergeAndRankResults(
|
|
|
65
65
|
presenter: result.presenter,
|
|
66
66
|
url: existing.result.url ?? result.url,
|
|
67
67
|
links: existing.result.links ?? result.links,
|
|
68
|
+
organizationId: existing.result.organizationId ?? result.organizationId,
|
|
68
69
|
}
|
|
69
70
|
existing.bestContribution = Math.max(existing.bestContribution, rrfScore)
|
|
70
71
|
} else if (hasExistingPresenter && hasNewPresenter && rrfScore > existing.bestContribution) {
|
|
71
72
|
// Both have presenter, keep the one with better RRF contribution (not raw score)
|
|
72
|
-
existing.result = {
|
|
73
|
+
existing.result = {
|
|
74
|
+
...result,
|
|
75
|
+
organizationId: result.organizationId ?? existing.result.organizationId,
|
|
76
|
+
}
|
|
73
77
|
existing.bestContribution = rrfScore
|
|
74
78
|
} else if (!hasExistingPresenter && !hasNewPresenter && rrfScore > existing.bestContribution) {
|
|
75
79
|
// Neither has presenter, keep result with better RRF contribution
|
|
76
|
-
existing.result = {
|
|
80
|
+
existing.result = {
|
|
81
|
+
...result,
|
|
82
|
+
organizationId: result.organizationId ?? existing.result.organizationId,
|
|
83
|
+
}
|
|
77
84
|
existing.bestContribution = rrfScore
|
|
78
85
|
}
|
|
79
86
|
// If existing has presenter and new doesn't, keep existing (do nothing)
|