Skip to content

Commit be963e7

Browse files
committed
docs: add chunk-aware search (@hasChunks) to graphile-search skill
- pgvector-adapter.md: new 'Chunk-Aware Search' section with @hasChunks config, includeChunks field, generated SQL, and GraphQL examples - codegen-sdk-queries.md: ORM examples for chunk-aware search, opt-out, and distance threshold with chunks - SKILL.md: mention chunk-aware RAG in architecture overview, SDK query patterns, and common pitfalls
1 parent 1521929 commit be963e7

3 files changed

Lines changed: 185 additions & 2 deletions

File tree

skills/graphile-search/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ UnifiedSearchPlugin
3232
├── TsvectorAdapter (keyword search with stemming)
3333
├── Bm25Adapter (relevance-ranked document search)
3434
├── TrgmAdapter (fuzzy matching, typo tolerance)
35-
└── PgvectorAdapter (semantic/embedding similarity)
35+
└── PgvectorAdapter (semantic/embedding similarity + chunk-aware RAG)
3636
```
3737

38+
The pgvector adapter also supports **chunk-aware search** via the `@hasChunks` smart tag — transparently querying across parent and chunk embeddings and returning the minimum distance. See `references/pgvector-adapter.md` for details.
39+
3840
Each adapter implements the `SearchAdapter` interface:
3941
- **`detectColumns()`** — finds searchable columns on a table (e.g. tsvector columns, columns with BM25 indexes)
4042
- **`registerTypes()`** — registers any custom GraphQL types (e.g. filter input types)
@@ -219,6 +221,7 @@ After running `cnc codegen`, the generated SDK client exposes search filters, sc
219221
- **BM25 queries**`bm25Content`, `bm25ContentScore` (negative, sort ASC)
220222
- **Trigram queries**`similarTo`/`wordSimilarTo` via `StringTrgmFilter`, adapter-level `trgmTitle`, ILIKE
221223
- **pgvector queries**`vectorEmbedding`, `embeddingDistance`, distance metrics (COSINE/L2/IP)
224+
- **Chunk-aware search**`includeChunks` toggle for RAG tables with `@hasChunks`, transparent parent + chunk distance
222225
- **Multi-strategy patterns** — fuzzy fallback, autocomplete pipeline, semantic + keyword hybrid
223226

224227
## Score Semantics
@@ -241,6 +244,7 @@ Bounded ranges use linear normalization. Unbounded ranges use sigmoid normalizat
241244
| No search fields on table | No search infrastructure detected | Add tsvector column, BM25 index, or vector column |
242245
| trgm operators missing | Table has no intentional search | Add tsvector/BM25, or use `@trgmSearch` smart tag |
243246
| `searchScore` is null | No search filters active in query | Add a search filter (fullTextSearch, bm25Body, etc.) |
247+
| `includeChunks` field missing | No `@hasChunks` tables in schema | Add `@hasChunks` smart tag to parent table codec |
244248
| `Unknown type "FullText"` | TsvectorCodecPlugin not loaded | Use `UnifiedSearchPreset()` which includes all codecs |
245249
| `Unknown type "Vector"` | VectorCodecPlugin not loaded | Use `UnifiedSearchPreset()` which includes all codecs |
246250
| Duplicate type errors | Multiple search presets | Use only `UnifiedSearchPreset()`, not individual presets |

skills/graphile-search/references/codegen-sdk-queries.md

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,93 @@ const result = await db.document.findMany({
401401
| `content_vec` | `vectorContentVec` | `contentVecDistance` | `CONTENT_VEC_DISTANCE_ASC/DESC` |
402402

403403
**Pattern:**
404-
- Filter: `vector` + CamelCase(column) — accepts `{ vector: [Float!]!, metric?: String, distance?: Float }`
404+
- Filter: `vector` + CamelCase(column) — accepts `{ vector: [Float!]!, metric?: String, distance?: Float, includeChunks?: Boolean }`
405405
- Distance: camelCase(column) + `Distance` (Float, lower = closer, null when no filter active)
406406
- OrderBy: SCREAMING_SNAKE(column) + `_DISTANCE_ASC/DESC`
407407

408+
### Chunk-Aware Vector Search
409+
410+
Tables with the `@hasChunks` smart tag automatically get chunk-aware search. The distance returned is `LEAST(parent_distance, MIN(chunk_distance))` — the best match across the document embedding and all chunk embeddings.
411+
412+
Chunk search is **on by default** for `@hasChunks` tables. No extra code is needed:
413+
414+
```typescript
415+
// Chunk-aware search — ON by default for @hasChunks tables
416+
// Distance = LEAST(parent embedding distance, closest chunk distance)
417+
const result = await db.document.findMany({
418+
where: {
419+
vectorEmbedding: {
420+
vector: queryVector,
421+
metric: 'COSINE',
422+
// includeChunks defaults to true when @hasChunks is present
423+
},
424+
},
425+
orderBy: 'EMBEDDING_DISTANCE_ASC',
426+
first: 10,
427+
select: {
428+
id: true,
429+
title: true,
430+
embeddingDistance: true, // best distance across parent + all chunks
431+
},
432+
}).execute();
433+
434+
if (result.ok) {
435+
result.data.documents.nodes.forEach(d => {
436+
console.log(`${d.title} (distance: ${d.embeddingDistance})`);
437+
});
438+
}
439+
```
440+
441+
### Opt Out of Chunk Search
442+
443+
Set `includeChunks: false` to only search the parent embedding:
444+
445+
```typescript
446+
const result = await db.document.findMany({
447+
where: {
448+
vectorEmbedding: {
449+
vector: queryVector,
450+
metric: 'COSINE',
451+
includeChunks: false, // only use parent embedding
452+
},
453+
},
454+
orderBy: 'EMBEDDING_DISTANCE_ASC',
455+
first: 10,
456+
select: {
457+
id: true,
458+
title: true,
459+
embeddingDistance: true, // parent distance only
460+
},
461+
}).execute();
462+
```
463+
464+
### Chunk-Aware Search with Distance Threshold
465+
466+
The distance threshold applies to the combined (chunk-aware) distance:
467+
468+
```typescript
469+
const result = await db.document.findMany({
470+
where: {
471+
vectorEmbedding: {
472+
vector: queryVector,
473+
metric: 'COSINE',
474+
distance: 0.3, // threshold applies to LEAST(parent, chunk)
475+
},
476+
isPublished: { equalTo: true },
477+
},
478+
orderBy: 'EMBEDDING_DISTANCE_ASC',
479+
first: 20,
480+
select: {
481+
id: true,
482+
title: true,
483+
embeddingDistance: true,
484+
searchScore: true, // composite 0..1 relevance
485+
},
486+
}).execute();
487+
```
488+
489+
> **Note:** `includeChunks` only appears on `VectorNearbyInput` when at least one table in the schema has the `@hasChunks` smart tag. For tables without chunks, the field is absent and vector search behaves as standard parent-only search.
490+
408491
---
409492

410493
## Multi-Strategy Patterns

skills/graphile-search/references/pgvector-adapter.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,102 @@ enum DocumentsOrderBy {
8282

8383
pgvector is the only primary adapter that sets `isIntentionalSearch: false`. This is because vector embeddings operate on a different domain than text search — a table with only pgvector columns shouldn't get trgm similarity fields on its text columns.
8484

85+
## Chunk-Aware Search (`@hasChunks`)
86+
87+
Tables with long-form content (documents, articles, etc.) often split text into **chunks**, each with its own embedding. The pgvector adapter transparently queries across parent and chunk embeddings when the `@hasChunks` smart tag is present on the table's codec.
88+
89+
### How It Works
90+
91+
1. The parent table has a `vector(N)` column (the document-level embedding)
92+
2. A separate chunks table stores per-chunk embeddings with a foreign key back to the parent
93+
3. The `@hasChunks` smart tag tells the adapter where to find the chunks table
94+
4. At query time, the adapter computes `LEAST(parent_distance, MIN(chunk_distance))` — the best match across all embeddings
95+
96+
### Smart Tag Configuration
97+
98+
Set `@hasChunks` on the parent table codec as a JSON object:
99+
100+
```json
101+
{
102+
"chunksTable": "documents_chunks",
103+
"parentFk": "parent_id",
104+
"parentPk": "id",
105+
"embeddingField": "embedding"
106+
}
107+
```
108+
109+
| Field | Default | Description |
110+
|-------|---------|-------------|
111+
| `chunksTable` | *(required)* | Name of the chunks table |
112+
| `chunksSchema` | parent table's schema | Schema of the chunks table |
113+
| `parentFk` | `parent_id` | Column in chunks table that references the parent |
114+
| `parentPk` | `id` | Primary key column on the parent table |
115+
| `embeddingField` | `embedding` | Vector column in the chunks table |
116+
117+
In Constructive, the `DataEmbedding` node type with chunks enabled automatically creates the chunks table and wires up the relationship. The smart tag is applied via a Graphile plugin or smart tags file.
118+
119+
### `includeChunks` Filter Field
120+
121+
When `@hasChunks` is detected, `VectorNearbyInput` gains an `includeChunks` boolean field:
122+
123+
```graphql
124+
input VectorNearbyInput {
125+
vector: [Float!]!
126+
metric: VectorMetric
127+
distance: Float
128+
includeChunks: Boolean # only present when @hasChunks is on the table
129+
}
130+
```
131+
132+
- **`true` (default for `@hasChunks` tables):** Distance = `LEAST(parent_distance, MIN(chunk_distance))`
133+
- **`false`:** Distance = parent embedding distance only
134+
135+
### Generated SQL
136+
137+
When `includeChunks` is active, the adapter generates:
138+
139+
```sql
140+
LEAST(
141+
COALESCE(parent.embedding <=> $query, 'Infinity'::float),
142+
COALESCE(
143+
(SELECT MIN(c.embedding <=> $query)
144+
FROM documents_chunks AS c
145+
WHERE c.parent_id = parent.id),
146+
'Infinity'::float
147+
)
148+
)
149+
```
150+
151+
`COALESCE` handles cases where the parent or chunks may not have embeddings.
152+
153+
### GraphQL Query Examples
154+
155+
```graphql
156+
# Chunk-aware (default) — returns best distance across parent + all chunks
157+
query {
158+
allDocuments(where: {
159+
vectorEmbedding: { vector: [0.1, 0.2, ...], metric: COSINE }
160+
}) {
161+
nodes {
162+
title
163+
embeddingVectorDistance # LEAST(parent, closest chunk)
164+
}
165+
}
166+
}
167+
168+
# Parent-only — opt out of chunk search
169+
query {
170+
allDocuments(where: {
171+
vectorEmbedding: { vector: [0.1, 0.2, ...], metric: COSINE, includeChunks: false }
172+
}) {
173+
nodes {
174+
title
175+
embeddingVectorDistance # parent distance only
176+
}
177+
}
178+
}
179+
```
180+
85181
## VectorCodecPlugin
86182

87183
The `VectorCodecPlugin` (included in `UnifiedSearchPreset`) teaches PostGraphile about the `vector` PostgreSQL type:

0 commit comments

Comments
 (0)