@@ -156,6 +156,267 @@ const results = db
156156 .all (vector );
157157```
158158
159+ ## Metadata Columns and Filtered Search
160+
161+ Add metadata columns to enable filtered vector search. Filters are evaluated ** during** graph traversal using the Filtered-DiskANN algorithm - not before or after search.
162+
163+ ### Creating an Index with Metadata
164+
165+ ``` typescript
166+ import { DatabaseSync } from " @photostructure/sqlite" ;
167+ import { loadDiskAnnExtension } from " @photostructure/sqlite-diskann" ;
168+
169+ const db = new DatabaseSync (" :memory:" , { allowExtension: true });
170+ loadDiskAnnExtension (db );
171+
172+ // Create index with metadata columns
173+ db .exec (`
174+ CREATE VIRTUAL TABLE photos USING diskann(
175+ dimension=512,
176+ metric=cosine,
177+ category TEXT,
178+ year INTEGER,
179+ score REAL
180+ )
181+ ` );
182+ ```
183+
184+ ** Supported column types** : ` TEXT ` , ` INTEGER ` , ` REAL ` , ` BLOB `
185+
186+ ** Reserved names** : Cannot use ` vector ` , ` distance ` , ` k ` , or ` rowid ` as metadata column names
187+
188+ ### Inserting Vectors with Metadata
189+
190+ ``` typescript
191+ const embedding = new Float32Array (512 ); // Your vector embedding
192+
193+ db .prepare (
194+ " INSERT INTO photos(rowid, vector, category, year, score) VALUES (?, ?, ?, ?, ?)"
195+ ).run (1 , embedding , " landscape" , 2024 , 0.95 );
196+
197+ db .prepare (
198+ " INSERT INTO photos(rowid, vector, category, year, score) VALUES (?, ?, ?, ?, ?)"
199+ ).run (2 , embedding , " portrait" , 2023 , 0.87 );
200+ ```
201+
202+ ### Searching with Metadata Filters
203+
204+ Metadata filters are evaluated ** during beam search** , not as a post-filter. This ensures correct recall even with selective filters.
205+
206+ ``` typescript
207+ const query = new Float32Array (512 );
208+
209+ // Filter by category
210+ const landscapes = db
211+ .prepare (
212+ `
213+ SELECT rowid, distance, category, year
214+ FROM photos
215+ WHERE vector MATCH ? AND k = 10 AND category = 'landscape'
216+ `
217+ )
218+ .all (query );
219+
220+ // Multiple filters
221+ const recent = db
222+ .prepare (
223+ `
224+ SELECT rowid, distance, category, year, score
225+ FROM photos
226+ WHERE vector MATCH ? AND k = 10
227+ AND category = 'landscape'
228+ AND year >= 2023
229+ AND score > 0.8
230+ `
231+ )
232+ .all (query );
233+
234+ // Range filters
235+ const filtered = db
236+ .prepare (
237+ `
238+ SELECT rowid, distance, category
239+ FROM photos
240+ WHERE vector MATCH ? AND k = 10 AND year BETWEEN 2020 AND 2024
241+ `
242+ )
243+ .all (query );
244+ ```
245+
246+ ** Supported filter operators** : ` = ` , ` != ` , ` < ` , ` <= ` , ` > ` , ` >= ` , ` BETWEEN ` , ` IN `
247+
248+ ### TypeScript Helper Functions
249+
250+ ``` typescript
251+ import { createDiskAnnIndex } from " @photostructure/sqlite-diskann" ;
252+
253+ // Create index with metadata columns
254+ createDiskAnnIndex (db , " photos" , {
255+ dimension: 512 ,
256+ metric: " cosine" ,
257+ metadataColumns: [
258+ { name: " category" , type: " TEXT" },
259+ { name: " year" , type: " INTEGER" },
260+ { name: " score" , type: " REAL" },
261+ ],
262+ });
263+
264+ // Insert using raw SQL for metadata
265+ const vec = new Float32Array (512 );
266+ db .prepare (" INSERT INTO photos(rowid, vector, category, year) VALUES (?, ?, ?, ?)" ).run (
267+ 1 ,
268+ vec ,
269+ " landscape" ,
270+ 2024
271+ );
272+
273+ // Search with filters (use raw SQL)
274+ const results = db
275+ .prepare (
276+ `
277+ SELECT rowid, distance, category, year
278+ FROM photos
279+ WHERE vector MATCH ? AND k = 10 AND category = ?
280+ `
281+ )
282+ .all (vec , " landscape" );
283+ ```
284+
285+ ## MATCH Operator Syntax
286+
287+ The ` MATCH ` operator triggers ANN search. It must be combined with the ` k ` parameter.
288+
289+ ### Basic Search
290+
291+ ``` sql
292+ SELECT rowid, distance
293+ FROM embeddings
294+ WHERE vector MATCH < vector_blob> AND k = < neighbor_count>
295+ ```
296+
297+ - ` vector MATCH <blob> ` : Triggers ANN search with the query vector (must be BLOB)
298+ - ` k = <number> ` : Number of nearest neighbors to return
299+ - Results are automatically sorted by distance (ascending)
300+
301+ ### With LIMIT
302+
303+ ``` sql
304+ -- LIMIT caps result rows, not search beam width
305+ SELECT rowid, distance
306+ FROM embeddings
307+ WHERE vector MATCH ? AND k = 100
308+ LIMIT 10 -- Returns closest 10 of the 100 candidates
309+ ```
310+
311+ ** Note** : ` k ` controls the search beam width (quality), ` LIMIT ` controls result count.
312+
313+ ### With Metadata Filters
314+
315+ ``` sql
316+ -- Filters are evaluated DURING graph traversal (Filtered-DiskANN)
317+ SELECT rowid, distance, category, year
318+ FROM photos
319+ WHERE vector MATCH ? AND k = 50 AND category = ' landscape' AND year > 2020
320+ ```
321+
322+ ** How filtering works** :
323+
324+ 1 . Graph traversal visits all nodes (respecting graph edges as bridges)
325+ 2 . Only matching nodes are added to the top-k results
326+ 3 . Non-matching nodes are still traversed (to reach matching nodes elsewhere)
327+ 4 . Returns up to k matching results
328+
329+ ### Invalid Queries
330+
331+ ``` sql
332+ -- ❌ Missing k parameter
333+ SELECT rowid, distance FROM embeddings WHERE vector MATCH ?
334+
335+ -- ❌ k without MATCH
336+ SELECT rowid, distance FROM embeddings WHERE k = 10
337+
338+ -- ❌ Wrong column type (vector must be BLOB, not TEXT)
339+ SELECT rowid, distance FROM embeddings WHERE vector MATCH ' [1.0, 2.0, ...]' AND k = 10
340+ ```
341+
342+ ## Performance Tips
343+
344+ ### Index Metadata Columns
345+
346+ For fast filtered search, create SQLite indexes on metadata columns you filter by:
347+
348+ ``` sql
349+ -- Create index with metadata columns
350+ CREATE VIRTUAL TABLE photos USING diskann(
351+ dimension= 512 , metric= cosine, category TEXT , year INTEGER
352+ );
353+
354+ -- Add index on frequently filtered columns in the shadow table
355+ -- Shadow table name pattern: {tableName}_attrs
356+ CREATE INDEX idx_photos_category ON photos_attrs(category);
357+ CREATE INDEX idx_photos_year ON photos_attrs(year);
358+ CREATE INDEX idx_photos_combined ON photos_attrs(category, year);
359+ ```
360+
361+ ** Why** : Metadata is stored in a shadow table named ` {tableName}_attrs ` (e.g., ` photos_attrs ` for a table named ` photos ` ). SQLite indexes on this shadow table speed up the pre-filtering step before beam search.
362+
363+ ** When to index** :
364+
365+ - ✅ Columns used in WHERE clauses (e.g., ` category = 'landscape' ` )
366+ - ✅ High-cardinality columns (many unique values)
367+ - ✅ Selective filters (< 50% of rows match)
368+ - ❌ Low-cardinality columns (e.g., boolean flags)
369+ - ❌ Columns rarely used in filters
370+
371+ ### Tuning Search Parameters
372+
373+ ``` sql
374+ -- Create index with tuned parameters
375+ CREATE VIRTUAL TABLE embeddings USING diskann(
376+ dimension= 512 ,
377+ metric= cosine,
378+ max_degree= 64 , -- Graph connectivity (default: 64)
379+ build_search_list_size= 100 -- Beam width during insert (default: 100)
380+ );
381+ ```
382+
383+ - ** ` max_degree ` ** : Higher values improve recall but increase memory and index size
384+ - Default: 64
385+ - Range: 16-128
386+ - Recommendation: 64 for most use cases
387+
388+ - ** ` build_search_list_size ` ** : Higher values improve index quality but slow down inserts
389+ - Default: 100
390+ - Range: 50-200
391+ - Recommendation: 100 for balanced performance
392+
393+ ### Vector Format
394+
395+ Use ` Float32Array ` for best performance:
396+
397+ ``` typescript
398+ // ✅ Good - direct binary encoding
399+ const vec = new Float32Array (512 );
400+ db .prepare (" INSERT INTO embeddings(rowid, vector) VALUES (?, ?)" ).run (1 , vec );
401+
402+ // ✅ Also good - automatic conversion
403+ const vecArray = [0.1 , 0.2 , 0.3 , ... ]; // number[]
404+ insertVector (db , " embeddings" , 1 , vecArray ); // Converts to Float32Array internally
405+ ```
406+
407+ ### Batch Operations
408+
409+ Use transactions for bulk inserts:
410+
411+ ``` typescript
412+ db .exec (" BEGIN TRANSACTION" );
413+ const stmt = db .prepare (" INSERT INTO embeddings(rowid, vector) VALUES (?, ?)" );
414+ for (let i = 0 ; i < 10000 ; i ++ ) {
415+ stmt .run (i , vectors [i ]);
416+ }
417+ db .exec (" COMMIT" );
418+ ```
419+
159420### C API (Advanced)
160421
161422For direct C API usage, the lower-level functions are still available:
0 commit comments