Skip to content

Commit 7f61e6d

Browse files
tae2089claude
andcommitted
feat: pgvector integration — auto-embed documents on build --graph
- Add InitPGVector() and SyncPGVectorDocuments() to agestore - ccg build --graph now syncs AGE graph + pgvector documents - Documents include node name + annotations as content, metadata as JSONB - mcp-pgvector-server handles embedding generation automatically - Update .mcp.json with pgvector MCP server config Flow: ccg build --graph → GORM + AGE + pgvector table → mcp-pgvector-server generates embeddings → Claude uses semantic search via pgvector MCP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a919aaf commit 7f61e6d

3 files changed

Lines changed: 83 additions & 1 deletion

File tree

.mcp.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
"ccg": {
44
"command": "./ccg",
55
"args": ["serve", "--db", "sqlite", "--dsn", "ccg.db"]
6+
},
7+
"pgvector": {
8+
"command": "npx",
9+
"args": ["-y", "mcp-pgvector-server"],
10+
"env": {
11+
"DATABASE_URL": "postgresql://ccg:ccg@localhost:5455/ccg"
12+
}
613
}
714
}
815
}

internal/cli/build.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/imtaebin/code-context-graph/internal/model"
1313
"github.com/imtaebin/code-context-graph/internal/parse"
14+
"github.com/imtaebin/code-context-graph/internal/store/agestore"
1415
)
1516

1617
var skipDirs = map[string]bool{
@@ -173,7 +174,7 @@ func newBuildCmd(deps *Deps) *cobra.Command {
173174
}
174175
}
175176

176-
// Sync to Apache AGE graph if --graph flag is set
177+
// Sync to Apache AGE graph + pgvector if --graph flag is set
177178
if syncGraph && deps.AgeStore != nil {
178179
deps.Logger.Info("syncing to AGE graph")
179180
if err := deps.AgeStore.ClearGraph(ctx); err != nil {
@@ -190,6 +191,30 @@ func newBuildCmd(deps *Deps) *cobra.Command {
190191
deps.Logger.Info("AGE graph synced", "nodes", len(indexNodes), "edges", len(edges))
191192
}
192193
}
194+
195+
// Sync pgvector documents for semantic search
196+
if err := deps.AgeStore.InitPGVector(ctx); err != nil {
197+
deps.Logger.Warn("pgvector init failed", "error", err)
198+
} else {
199+
var pvDocs []agestore.PGVectorDocument
200+
for _, n := range indexNodes {
201+
pvDocs = append(pvDocs, agestore.PGVectorDocument{
202+
NodeID: n.ID,
203+
Content: buildContent(n),
204+
Metadata: map[string]string{
205+
"qualified_name": n.QualifiedName,
206+
"kind": string(n.Kind),
207+
"file_path": n.FilePath,
208+
"language": n.Language,
209+
},
210+
})
211+
}
212+
if err := deps.AgeStore.SyncPGVectorDocuments(ctx, pvDocs); err != nil {
213+
deps.Logger.Warn("pgvector sync failed", "error", err)
214+
} else {
215+
deps.Logger.Info("pgvector documents synced", "documents", len(pvDocs))
216+
}
217+
}
193218
}
194219

195220
fmt.Fprintf(stdout(cmd), "Build complete: %d files, %d nodes, %d edges\n", totalFiles, totalNodes, totalEdges)

internal/store/agestore/agestore.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,56 @@ func (s *Store) ExecuteCypherJSON(ctx context.Context, cypher string, columnCoun
167167
return string(b), nil
168168
}
169169

170+
// InitPGVector creates the pgvector extension and documents table.
171+
func (s *Store) InitPGVector(ctx context.Context) error {
172+
stmts := []string{
173+
"CREATE EXTENSION IF NOT EXISTS vector",
174+
`CREATE TABLE IF NOT EXISTS ccg_documents (
175+
id SERIAL PRIMARY KEY,
176+
node_id INTEGER NOT NULL,
177+
content TEXT NOT NULL,
178+
metadata JSONB,
179+
embedding vector(1536),
180+
created_at TIMESTAMP DEFAULT NOW()
181+
)`,
182+
"CREATE INDEX IF NOT EXISTS idx_ccg_documents_node_id ON ccg_documents(node_id)",
183+
}
184+
for _, stmt := range stmts {
185+
if _, err := s.db.ExecContext(ctx, stmt); err != nil {
186+
return fmt.Errorf("pgvector init %q: %w", stmt[:40], err)
187+
}
188+
}
189+
return nil
190+
}
191+
192+
// PGVectorDocument represents a document for pgvector embedding.
193+
type PGVectorDocument struct {
194+
NodeID uint
195+
Content string
196+
Metadata map[string]string
197+
}
198+
199+
// SyncPGVectorDocuments clears and inserts documents for pgvector embedding.
200+
func (s *Store) SyncPGVectorDocuments(ctx context.Context, docs []PGVectorDocument) error {
201+
// Clear existing documents
202+
if _, err := s.db.ExecContext(ctx, "DELETE FROM ccg_documents"); err != nil {
203+
return fmt.Errorf("clear pgvector docs: %w", err)
204+
}
205+
206+
// Batch insert
207+
for _, d := range docs {
208+
metaJSON, _ := json.Marshal(d.Metadata)
209+
_, err := s.db.ExecContext(ctx,
210+
"INSERT INTO ccg_documents (node_id, content, metadata) VALUES ($1, $2, $3)",
211+
d.NodeID, d.Content, string(metaJSON),
212+
)
213+
if err != nil {
214+
return fmt.Errorf("insert pgvector doc node_id=%d: %w", d.NodeID, err)
215+
}
216+
}
217+
return nil
218+
}
219+
170220
// Close closes the connection.
171221
func (s *Store) Close() error {
172222
return s.db.Close()

0 commit comments

Comments
 (0)