Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.

Commit 6f6d6fc

Browse files
committed
feat(index): complete Phase 4 TypeScript polish and documentation
Phase 4 delivers working TypeScript APIs and comprehensive documentation for metadata columns and filtered search. TypeScript API fixes: - searchNearest() now uses MATCH operator (was calling non-existent diskann_search function) - insertVector() cleaned up (removed broken metadata parameter) - All helper functions accept Float32Array or number[] for vectors - Proper validation for metadata column definitions Documentation: - Added metadata columns section with CREATE/INSERT/SEARCH examples - Documented MATCH operator syntax and query patterns - Performance tuning guide (indexing _attrs, search parameters, batch operations) - Explained Filtered-DiskANN algorithm (filters during traversal, not post-filter) Breaking changes: - NearestNeighborResult.id renamed to rowid (aligns with SQLite) - searchNearest() removes unused searchListSize parameter - insertVector() removes broken metadata parameter (use raw SQL instead) Test results: - 88 TypeScript tests pass (unskipped all .skip tests) - 175 C tests pass (126 C API + 49 vtab) - ASan clean, Valgrind clean Phase 4 complete. All features working end-to-end.
1 parent 6a4a61b commit 6f6d6fc

6 files changed

Lines changed: 724 additions & 89 deletions

File tree

README.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

161422
For direct C API usage, the lower-level functions are still available:

0 commit comments

Comments
 (0)