@strav/rag 0.4.31 → 1.0.0-alpha.20

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.
@@ -1,179 +1,270 @@
1
- import type { BaseModel } from '@strav/database'
2
- import type { NormalizeConstructor } from '@strav/kernel'
3
- import { Emitter } from '@strav/kernel'
4
- import { brain } from '@strav/brain'
5
- import RagManager from './rag_manager.ts'
6
- import { createChunker } from './chunking/chunker.ts'
7
- import type { VectorDocument, RetrieveOptions, RetrieveResult } from './types.ts'
8
-
9
- export function retrievable<T extends NormalizeConstructor<typeof BaseModel>>(Base: T) {
10
- return class Retrievable extends Base {
11
- private static _retrievalBooted = false
12
-
13
- static retrievableAs(): string {
14
- return (this as unknown as typeof BaseModel).tableName
1
+ /**
2
+ * `retrievable(Repository)` class mixin that bolts vector-index
3
+ * methods onto a Repository so apps can re-index a row and search
4
+ * its collection without juggling `RagManager` calls by hand.
5
+ *
6
+ * ```ts
7
+ * @inject()
8
+ * export class ArticleRepository extends retrievable(Repository<Article>) {
9
+ * static override readonly schema = articleSchema
10
+ * static override readonly model = Article
11
+ *
12
+ * constructor(db: PostgresDatabase, events: EventBus, rag: RagManager) {
13
+ * super(db, events)
14
+ * this.rag = rag
15
+ * }
16
+ *
17
+ * // Override the extension points as needed:
18
+ * protected override toContent(a: Article): string {
19
+ * return `${a.title}\n\n${a.body}`
20
+ * }
21
+ *
22
+ * protected override toMetadata(a: Article): Record<string, unknown> {
23
+ * return { authorId: a.author_id, tags: a.tags }
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * Usage:
29
+ *
30
+ * ```ts
31
+ * const article = await articles.create(...)
32
+ * await articles.vectorize(article) // index it
33
+ *
34
+ * const { matches } = await articles.retrieve('query') // search
35
+ *
36
+ * await articles.delete(article)
37
+ * await articles.vectorRemove(article) // drop from index
38
+ * ```
39
+ *
40
+ * Why not auto-vectorize on `create` / `update`?
41
+ *
42
+ * V1 ships the explicit pattern. An auto-hook tied to repository
43
+ * events would couple persistence to the embedding provider's
44
+ * availability — a transient rate-limit on the embedder would
45
+ * fail the create call. Apps that want auto-vectorize wire it
46
+ * themselves via `events.on('article.created', m =>
47
+ * articles.vectorize(m))` so they control the failure mode
48
+ * (fire-and-forget vs awaited vs queued via `@strav/queue`).
49
+ *
50
+ * Extension points (all optional overrides):
51
+ *
52
+ * - `collectionName()` — defaults to the table name from the
53
+ * schema. Override when the collection should differ from the
54
+ * table, or to compose a per-tenant suffix dynamically.
55
+ *
56
+ * - `toContent(model)` — defaults to concatenating every string
57
+ * field on the model with `\n`. The default works for simple
58
+ * row shapes; apps with structured content override.
59
+ *
60
+ * - `toMetadata(model)` — defaults to `{}`. Apps return fields
61
+ * they want to filter on (e.g. `author_id`, `lang`, `kind`).
62
+ *
63
+ * - `shouldRetrieve(model)` — gates indexing. Return `false` for
64
+ * draft / soft-deleted / private rows. The default is `true`.
65
+ */
66
+
67
+ import type { Repository } from '@strav/database'
68
+ import type { RagManager } from './rag_manager.ts'
69
+ import type {
70
+ RetrieveOptions,
71
+ RetrieveResult,
72
+ VectorMatch,
73
+ } from './types.ts'
74
+
75
+ /** Minimal constructor type we can mix into. Wider than `typeof Repository` so subclasses with extra ctor args still type-check. */
76
+ // biome-ignore lint/suspicious/noExplicitAny: mixin constructor signatures intentionally accept any[]; the user-side subclass narrows.
77
+ type RepositoryConstructor<TModel extends object> = new (...args: any[]) => Repository<TModel>
78
+
79
+ /**
80
+ * Returns a subclass that extends `Base` with `vectorize` /
81
+ * `vectorRemove` / `retrieve` plus override-points
82
+ * (`collectionName`, `toContent`, `toMetadata`,
83
+ * `shouldRetrieve`). The user-side class declares an explicit
84
+ * constructor that calls `super(...)` and assigns `this.rag`.
85
+ */
86
+ export function retrievable<TModel extends object, TBase extends RepositoryConstructor<TModel>>(
87
+ Base: TBase,
88
+ ) {
89
+ abstract class RetrievableRepository extends Base {
90
+ /**
91
+ * The framework's `RagManager`. Assigned by the user-side
92
+ * subclass constructor. Public on purpose — apps that want to
93
+ * drop down to raw `rag.store()` / `rag.ingest(...)` access
94
+ * have a hook.
95
+ */
96
+ rag!: RagManager
97
+
98
+ /**
99
+ * Collection name for vector storage. Defaults to the table
100
+ * name from `static schema`. Override to point at a different
101
+ * collection (or to compose per-tenant / per-env suffixes).
102
+ */
103
+ protected collectionName(): string {
104
+ const ctor = this.constructor as unknown as { schema: { name: string } }
105
+ return ctor.schema.name
15
106
  }
16
107
 
17
- toRetrievableContent(): string {
108
+ /**
109
+ * Build the indexable text from a model row. Default
110
+ * concatenates every non-underscore string field with `\n`.
111
+ * Apps with structured content override this — typically
112
+ * something like `` `${a.title}\n\n${a.body}` ``.
113
+ */
114
+ protected toContent(model: TModel): string {
18
115
  const parts: string[] = []
19
- for (const key of Object.keys(this)) {
116
+ for (const [key, value] of Object.entries(model as Record<string, unknown>)) {
20
117
  if (key.startsWith('_')) continue
21
- const val = (this as any)[key]
22
- if (typeof val === 'string' && val.length > 0) parts.push(val)
118
+ if (typeof value === 'string' && value.length > 0) parts.push(value)
23
119
  }
24
120
  return parts.join('\n')
25
121
  }
26
122
 
27
- toRetrievableMetadata(): Record<string, unknown> {
123
+ /**
124
+ * Build the metadata bag attached to every chunk. Apps return
125
+ * fields they want to filter retrievals on. The framework
126
+ * automatically adds `chunkIndex`, `startOffset`, `endOffset`
127
+ * — overrides shouldn't try to re-add those.
128
+ */
129
+ protected toMetadata(_model: TModel): Record<string, unknown> {
28
130
  return {}
29
131
  }
30
132
 
31
- shouldBeRetrievable(): boolean {
133
+ /**
134
+ * Whether the model should currently be indexed. Override to
135
+ * skip drafts, soft-deleted rows, private records, etc. The
136
+ * default `true` indexes every model — fine for the common
137
+ * case.
138
+ */
139
+ protected shouldRetrieve(_model: TModel): boolean {
32
140
  return true
33
141
  }
34
142
 
35
- // ── Instance methods ──────────────────────────────────────────────
36
-
37
- async vectorize(): Promise<void> {
38
- if (!this.shouldBeRetrievable()) return
39
-
40
- const ctor = this.constructor as typeof Retrievable
41
- const collection = RagManager.collectionName(ctor.retrievableAs())
42
- const config = RagManager.config
43
- const pkProp = (ctor as unknown as typeof BaseModel).primaryKeyProperty
44
- const id = (this as any)[pkProp]
45
-
46
- const content = this.toRetrievableContent()
47
- if (!content) return
48
-
49
- // Remove existing chunks for this model instance
50
- await RagManager.store().deleteBySource(collection, id)
51
-
52
- const chunker = createChunker(config.chunking)
53
- const chunks = chunker.chunk(content)
54
- if (chunks.length === 0) return
55
-
56
- const texts = chunks.map(c => c.content)
57
- const embeddings = await brain.embed(texts, {
58
- provider: config.embedding.provider,
59
- model: config.embedding.model,
60
- })
61
-
62
- const metadata = this.toRetrievableMetadata()
63
- const documents: VectorDocument[] = chunks.map((chunk, i) => ({
64
- id: `${id}_${i}`,
143
+ /**
144
+ * (Re-)index a single model. Drops any existing chunks for
145
+ * the model's id, then ingests fresh chunks of the current
146
+ * content. When `shouldRetrieve(model)` returns `false`, the
147
+ * chunks are dropped without re-ingest — apps don't need a
148
+ * separate "this just became private" path.
149
+ *
150
+ * Returns the vector ids written. Empty array when content
151
+ * was empty or `shouldRetrieve` returned `false`.
152
+ */
153
+ async vectorize(model: TModel): Promise<string[]> {
154
+ const collection = this.collectionName()
155
+ const id = modelId(model)
156
+
157
+ // Drop existing chunks for this source first so updates
158
+ // replace cleanly. (RagManager.ingest writes fresh ids per
159
+ // call; without this step every re-vectorize would
160
+ // duplicate.)
161
+ await this.rag
162
+ .store()
163
+ .deleteBySource(this.rag.collectionName(collection), id)
164
+
165
+ if (!this.shouldRetrieve(model)) return []
166
+
167
+ const content = this.toContent(model)
168
+ if (!content) return []
169
+
170
+ return this.rag.ingest(collection, content, {
65
171
  sourceId: id,
66
- content: chunk.content,
67
- embedding: embeddings[i]!,
68
- metadata: {
69
- ...metadata,
70
- modelId: id,
71
- chunkIndex: chunk.index,
72
- },
73
- }))
74
-
75
- await RagManager.store().upsert(collection, documents)
172
+ metadata: this.toMetadata(model),
173
+ })
76
174
  }
77
175
 
78
- async vectorRemove(): Promise<void> {
79
- const ctor = this.constructor as typeof Retrievable
80
- const collection = RagManager.collectionName(ctor.retrievableAs())
81
- const pkProp = (ctor as unknown as typeof BaseModel).primaryKeyProperty
82
- const id = (this as any)[pkProp]
83
- await RagManager.store().deleteBySource(collection, id)
176
+ /**
177
+ * Drop every chunk for one model. Apps call this after
178
+ * `delete(model)` in their domain code. The mixin doesn't
179
+ * auto-hook the delete lifecycle for the same reason it
180
+ * doesn't auto-hook create/update keeps embedding-provider
181
+ * availability out of the persistence path.
182
+ */
183
+ async vectorRemove(model: TModel): Promise<void> {
184
+ const collection = this.collectionName()
185
+ const id = modelId(model)
186
+ await this.rag
187
+ .store()
188
+ .deleteBySource(this.rag.collectionName(collection), id)
84
189
  }
85
190
 
86
- // ── Static methods ────────────────────────────────────────────────
87
-
88
- static async retrieve(query: string, options?: RetrieveOptions): Promise<RetrieveResult> {
89
- const { rag } = await import('./helpers.ts')
90
- return rag.retrieve(query, {
191
+ /**
192
+ * Semantic search over this repository's collection. Default
193
+ * `collection` is the mixin's `collectionName()` apps that
194
+ * want to retrieve from another collection pass it explicitly.
195
+ */
196
+ async retrieve(
197
+ query: string,
198
+ options: Omit<RetrieveOptions, 'collection'> & { collection?: string } = {},
199
+ ): Promise<RetrieveResult> {
200
+ return this.rag.retrieve(query, {
91
201
  ...options,
92
- collection: options?.collection ?? this.retrievableAs(),
202
+ collection: options.collection ?? this.collectionName(),
93
203
  })
94
204
  }
95
205
 
96
- static async importAll(chunkSize: number = 100): Promise<number> {
97
- const ModelCtor = this as unknown as typeof BaseModel & typeof Retrievable
98
- const collection = RagManager.collectionName(this.retrievableAs())
99
- const config = RagManager.config
100
- const db = ModelCtor.db
101
- const table = ModelCtor.tableName
102
- const pkCol = ModelCtor.primaryKeyColumn
103
-
104
- await RagManager.store().createCollection(collection, config.embedding.dimension)
105
-
106
- let imported = 0
206
+ /**
207
+ * Re-index every row in the repository. Walks rows in batches
208
+ * of `batchSize` and vectorizes each. Useful for backfilling
209
+ * a new collection or recovering after a schema change.
210
+ *
211
+ * The CLI's `rag:reindex <repository>` doesn't ship in V1 —
212
+ * apps that want one wire it as their own console command
213
+ * pointing at this method.
214
+ *
215
+ * Returns the total count of rows processed (NOT the chunk
216
+ * count chunks per row vary with content size).
217
+ */
218
+ async reindexAll(batchSize: number = 100): Promise<number> {
219
+ let processed = 0
107
220
  let offset = 0
108
-
109
221
  while (true) {
110
- const rows = (await db.sql.unsafe(
111
- `SELECT * FROM "${table}" ORDER BY "${pkCol}" LIMIT $1 OFFSET $2`,
112
- [chunkSize, offset]
113
- )) as Record<string, unknown>[]
114
-
222
+ const rows = await this.query().orderBy('id', 'asc').limit(batchSize).offset(offset).get()
115
223
  if (rows.length === 0) break
116
-
117
- for (const row of rows) {
118
- const instance = ModelCtor.hydrate(row) as InstanceType<typeof Retrievable>
119
- if (instance.shouldBeRetrievable()) {
120
- try {
121
- await instance.vectorize()
122
- imported++
123
- } catch {
124
- // Vectorization is secondary — continue on failure
125
- }
126
- }
127
- }
128
-
129
- offset += chunkSize
130
- if (rows.length < chunkSize) break
224
+ for (const row of rows) await this.vectorize(row)
225
+ processed += rows.length
226
+ offset += rows.length
227
+ if (rows.length < batchSize) break
131
228
  }
132
-
133
- return imported
134
- }
135
-
136
- static async flushVectors(): Promise<void> {
137
- const collection = RagManager.collectionName(this.retrievableAs())
138
- await RagManager.store().flush(collection)
139
- }
140
-
141
- static async createVectorCollection(): Promise<void> {
142
- const collection = RagManager.collectionName(this.retrievableAs())
143
- await RagManager.store().createCollection(collection, RagManager.config.embedding.dimension)
229
+ return processed
144
230
  }
145
231
 
146
- static bootRetrieval(eventPrefix: string): void {
147
- if (this._retrievalBooted) return
148
- this._retrievalBooted = true
149
-
150
- const vectorizeFn = async (model: unknown) => {
151
- if (model && typeof (model as any).vectorize === 'function') {
152
- try {
153
- await (model as any).vectorize()
154
- } catch {
155
- // Vectorization is secondary failures should not break the event pipeline
156
- }
157
- }
158
- }
159
-
160
- const removeFn = async (model: unknown) => {
161
- if (model && typeof (model as any).vectorRemove === 'function') {
162
- try {
163
- await (model as any).vectorRemove()
164
- } catch {
165
- // Vector removal is secondary
166
- }
167
- }
232
+ /**
233
+ * Match-to-models helper. Takes the `matches` array from
234
+ * `retrieve(...)` and hydrates the source rows by id, in
235
+ * match order. Matches whose `sourceId` doesn't resolve to a
236
+ * row (deleted between index time + retrieval) are dropped.
237
+ */
238
+ async resolveMatches(matches: readonly VectorMatch[]): Promise<TModel[]> {
239
+ const ids = [...new Set(matches.map((m) => m.sourceId).filter((s): s is string => !!s))]
240
+ if (ids.length === 0) return []
241
+ const found = await this.findMany(ids as unknown as readonly string[])
242
+ const byId = new Map<string, TModel>(
243
+ found.map((m) => [modelId(m), m]),
244
+ )
245
+ const out: TModel[] = []
246
+ for (const match of matches) {
247
+ if (!match.sourceId) continue
248
+ const row = byId.get(match.sourceId)
249
+ if (row) out.push(row)
168
250
  }
169
-
170
- Emitter.on(`${eventPrefix}.created`, vectorizeFn)
171
- Emitter.on(`${eventPrefix}.updated`, vectorizeFn)
172
- Emitter.on(`${eventPrefix}.synced`, vectorizeFn)
173
- Emitter.on(`${eventPrefix}.deleted`, removeFn)
251
+ return out
174
252
  }
175
253
  }
254
+ return RetrievableRepository
176
255
  }
177
256
 
178
- export type RetrievableInstance = InstanceType<ReturnType<typeof retrievable>>
179
- export type RetrievableModel = ReturnType<typeof retrievable>
257
+ /**
258
+ * Coerce a model's `id` to a string. Repositories use ULID or UUID
259
+ * ids by default, both of which round-trip through `String(...)`
260
+ * cleanly; integer PKs (bigSerial) coerce the same way.
261
+ */
262
+ function modelId(model: object): string {
263
+ const id = (model as { id?: unknown }).id
264
+ if (id === undefined || id === null) {
265
+ throw new Error(
266
+ `retrievable: model has no \`id\` to use as a vector sourceId. The mixin only works on models with a single-column id.`,
267
+ )
268
+ }
269
+ return String(id)
270
+ }
package/src/types.ts CHANGED
@@ -1,47 +1,84 @@
1
- // ── Vector Documents ─────────────────────────────────────────────────────
2
-
1
+ /**
2
+ * `@strav/rag` types — the data shapes apps see when reading and
3
+ * writing vectors and when running retrieval.
4
+ *
5
+ * Three concept clusters:
6
+ *
7
+ * - **Vector docs + queries** — the storage layer. A
8
+ * `VectorDocument` is one indexed unit (a chunk of source
9
+ * content, its embedding, and free-form metadata).
10
+ * `query()` returns `VectorMatch[]` ranked by similarity.
11
+ *
12
+ * - **Retrieval pipeline** — `RetrieveOptions` /
13
+ * `RetrieveResult`. Apps call `rag.retrieve(query, ...)`,
14
+ * the manager embeds the query through `@strav/brain`,
15
+ * queries the active store, and returns matches with
16
+ * normalized similarity scores.
17
+ *
18
+ * - **Chunking** — `Chunk`, `Chunker`. The chunker takes raw
19
+ * content and produces overlapping segments suitable for
20
+ * embedding. Two strategies ship: `fixed` (mechanical N-char
21
+ * windows with overlap) and `recursive` (paragraph-aware,
22
+ * better for prose).
23
+ */
24
+
25
+ // ─── Vector documents + queries ──────────────────────────────────────────
26
+
27
+ /**
28
+ * One indexed unit. `id` is provider-assigned (ULID by default);
29
+ * `sourceId` is the optional app-defined pointer back to the row
30
+ * the chunk came from (e.g., `article_id`) — `deleteBySource`
31
+ * removes every chunk for one source in a single call.
32
+ */
3
33
  export interface VectorDocument {
4
- id?: string | number
5
- sourceId?: string | number
34
+ id?: string
35
+ sourceId?: string | null
6
36
  content: string
7
37
  embedding: number[]
8
38
  metadata?: Record<string, unknown>
9
39
  }
10
40
 
11
- // ── Query Options & Results ──────────────────────────────────────────────
12
-
13
41
  export interface QueryOptions {
42
+ /** Top-K matches to return. Default `5`. */
14
43
  topK?: number
44
+ /** Minimum similarity threshold (0–1). Matches below this are filtered out. */
15
45
  threshold?: number
46
+ /** Metadata filter — flat key/value AND. Driver-specific operators are NOT supported in V1. */
16
47
  filter?: Record<string, unknown>
17
48
  }
18
49
 
19
50
  export interface QueryResult {
20
51
  matches: VectorMatch[]
21
- processingTimeMs?: number
52
+ /** Time the underlying store took to compute the query, in ms. */
53
+ processingTimeMs: number
22
54
  }
23
55
 
24
56
  export interface VectorMatch {
25
- id: string | number
57
+ id: string
26
58
  content: string
59
+ /** Similarity score in [0, 1]. 1.0 = identical embeddings, 0 = orthogonal. */
27
60
  score: number
28
61
  metadata: Record<string, unknown>
62
+ sourceId?: string | null
29
63
  }
30
64
 
31
- // ── Retrieval (high-level pipeline) ──────────────────────────────────────
65
+ // ─── Retrieval pipeline ─────────────────────────────────────────────────
32
66
 
33
67
  export interface RetrieveOptions {
68
+ /** Override the collection. Defaults to the manager's default. */
34
69
  collection?: string
70
+ /** Top-K matches. Default `5`. */
35
71
  topK?: number
72
+ /** Minimum similarity threshold. */
36
73
  threshold?: number
74
+ /** Metadata filter — flat key/value AND. */
37
75
  filter?: Record<string, unknown>
38
- rerank?: RerankOptions
39
- }
40
-
41
- export interface RerankOptions {
42
- authorityWeight?: number
43
- recencyWeight?: number
44
- similarityWeight?: number
76
+ /** Override the store. Defaults to the manager's default store. */
77
+ store?: string
78
+ /** Override the embedding model used to encode the query. */
79
+ embedModel?: string
80
+ /** Override the brain provider used for embedding. */
81
+ embedProvider?: string
45
82
  }
46
83
 
47
84
  export interface RetrieveResult {
@@ -51,19 +88,24 @@ export interface RetrieveResult {
51
88
  }
52
89
 
53
90
  export interface RetrievedDocument {
54
- id: string | number
91
+ id: string
55
92
  content: string
93
+ /** Same as `VectorMatch.score` — kept as a separate field so future re-ranking can diverge `score` from raw `similarity`. */
56
94
  score: number
57
95
  similarity: number
58
96
  metadata: Record<string, unknown>
97
+ sourceId?: string | null
59
98
  }
60
99
 
61
- // ── Chunking ─────────────────────────────────────────────────────────────
100
+ // ─── Chunking ────────────────────────────────────────────────────────────
62
101
 
63
102
  export interface Chunk {
64
103
  content: string
104
+ /** 0-based ordinal within the source. */
65
105
  index: number
106
+ /** Character offset of the chunk's first character in the source. */
66
107
  startOffset: number
108
+ /** Character offset one past the chunk's last character. */
67
109
  endOffset: number
68
110
  }
69
111
 
@@ -71,30 +113,46 @@ export interface Chunker {
71
113
  chunk(content: string): Chunk[]
72
114
  }
73
115
 
74
- // ── Configuration ────────────────────────────────────────────────────────
116
+ // ─── Configuration ──────────────────────────────────────────────────────
75
117
 
118
+ /**
119
+ * `config.rag` shape. Apps that don't configure rag get a sensible
120
+ * default (memory driver, OpenAI text-embedding-3-small, recursive
121
+ * chunking) — see `RagProvider.boot()` for the defaults.
122
+ */
76
123
  export interface RagConfig {
124
+ /** Default store name — must be a key in `stores`. */
77
125
  default: string
78
- prefix: string
126
+ /** Optional collection-name prefix. Used to namespace per-app or per-tenant. */
127
+ prefix?: string
79
128
  embedding: EmbeddingConfig
80
129
  chunking: ChunkingConfig
81
130
  stores: Record<string, StoreConfig>
82
131
  }
83
132
 
84
133
  export interface EmbeddingConfig {
134
+ /** `@strav/brain` provider key (e.g., `'openai'`, `'gemini'`, `'ollama'`). */
85
135
  provider: string
136
+ /** Model identifier — passed to `brain.embed(..., { model })`. */
86
137
  model: string
138
+ /** Vector dimension. Must match the chosen model. */
87
139
  dimension: number
88
140
  }
89
141
 
90
142
  export interface ChunkingConfig {
91
- strategy: string
143
+ /** `'fixed'` or `'recursive'`. Custom strategies aren't pluggable in V1. */
144
+ strategy: 'fixed' | 'recursive'
92
145
  chunkSize: number
93
146
  overlap: number
94
- separators?: string[]
147
+ /** Custom separators for the recursive strategy. Defaults to `['\n\n', '\n', '. ', ' ']`. */
148
+ separators?: readonly string[]
95
149
  }
96
150
 
97
151
  export interface StoreConfig {
152
+ /** `'memory'` or `'pgvector'`; custom drivers register via `rag.extend(name, factory)`. */
98
153
  driver: string
154
+ /** Pgvector: explicit table name override. Default `'rag_vector'`. */
155
+ table?: string
156
+ /** Free-form fields driver-specific (e.g., HNSW tuning for pgvector). */
99
157
  [key: string]: unknown
100
158
  }
@@ -1,15 +1,55 @@
1
- import type { VectorDocument, QueryOptions, QueryResult } from './types.ts'
1
+ /**
2
+ * `VectorStore` — the storage abstraction every driver
3
+ * (`MemoryDriver`, `PgvectorDriver`, custom drivers registered
4
+ * via `rag.extend(...)`) implements.
5
+ *
6
+ * Lifecycle:
7
+ *
8
+ * - `createCollection(name, dimension)` — idempotent. For
9
+ * pgvector this is mostly a no-op (the table holds every
10
+ * collection); the dimension is enforced at INSERT.
11
+ * - `deleteCollection(name)` — drops every vector under
12
+ * `collection = name`.
13
+ *
14
+ * Reads + writes:
15
+ *
16
+ * - `upsert(collection, docs)` — inserts (and overwrites by id
17
+ * when supplied).
18
+ * - `delete(collection, ids)` — removes specific vectors.
19
+ * - `deleteBySource(collection, sourceId)` — removes every
20
+ * vector with the matching `source_id`. Apps call this when
21
+ * re-indexing a source row.
22
+ * - `flush(collection)` — drops every vector in the
23
+ * collection. Faster than `deleteCollection` for the common
24
+ * "wipe + re-ingest" pattern because the collection's
25
+ * identity stays intact.
26
+ * - `query(collection, vector, opts)` — top-K similarity
27
+ * search.
28
+ *
29
+ * Multitenancy lives BELOW this interface — the pgvector driver
30
+ * relies on `app.tenant_id` session settings (set by
31
+ * `tenants.withTenant`) to enforce isolation via RLS. The
32
+ * `MemoryDriver` is single-tenant by construction and ignores
33
+ * tenancy.
34
+ */
35
+
36
+ import type { QueryOptions, QueryResult, VectorDocument } from './types.ts'
2
37
 
3
38
  export interface VectorStore {
39
+ /** Driver identifier — `'memory'`, `'pgvector'`, or the name passed to `rag.extend`. */
4
40
  readonly name: string
5
41
 
6
42
  createCollection(collection: string, dimension: number): Promise<void>
7
43
  deleteCollection(collection: string): Promise<void>
8
44
 
9
- upsert(collection: string, documents: VectorDocument[]): Promise<void>
10
- delete(collection: string, ids: (string | number)[]): Promise<void>
11
- deleteBySource(collection: string, sourceId: string | number): Promise<void>
45
+ upsert(collection: string, documents: readonly VectorDocument[]): Promise<void>
46
+ delete(collection: string, ids: readonly string[]): Promise<void>
47
+ deleteBySource(collection: string, sourceId: string): Promise<void>
12
48
  flush(collection: string): Promise<void>
13
49
 
14
- query(collection: string, vector: number[], options?: QueryOptions): Promise<QueryResult>
50
+ query(
51
+ collection: string,
52
+ vector: readonly number[],
53
+ options?: QueryOptions,
54
+ ): Promise<QueryResult>
15
55
  }