voyageai-cli 1.20.6 → 1.22.0
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.
- package/CHANGELOG.md +142 -26
- package/README.md +130 -2
- package/package.json +3 -2
- package/src/cli.js +10 -0
- package/src/commands/bug.js +249 -0
- package/src/commands/eval.js +420 -10
- package/src/commands/generate.js +220 -0
- package/src/commands/playground.js +93 -0
- package/src/commands/purge.js +271 -0
- package/src/commands/refresh.js +322 -0
- package/src/commands/scaffold.js +217 -0
- package/src/lib/codegen.js +339 -0
- package/src/lib/explanations.js +155 -0
- package/src/lib/scaffold-structure.js +114 -0
- package/src/lib/templates/nextjs/README.md.tpl +106 -0
- package/src/lib/templates/nextjs/env.example.tpl +8 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
- package/src/lib/templates/nextjs/package.json.tpl +33 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
- package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
- package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
- package/src/lib/templates/nextjs/theme.js.tpl +84 -0
- package/src/lib/templates/python/README.md.tpl +145 -0
- package/src/lib/templates/python/app.py.tpl +221 -0
- package/src/lib/templates/python/chunker.py.tpl +127 -0
- package/src/lib/templates/python/env.example.tpl +12 -0
- package/src/lib/templates/python/mongo_client.py.tpl +125 -0
- package/src/lib/templates/python/requirements.txt.tpl +10 -0
- package/src/lib/templates/python/voyage_client.py.tpl +124 -0
- package/src/lib/templates/vanilla/README.md.tpl +156 -0
- package/src/lib/templates/vanilla/client.js.tpl +103 -0
- package/src/lib/templates/vanilla/connection.js.tpl +126 -0
- package/src/lib/templates/vanilla/env.example.tpl +11 -0
- package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
- package/src/lib/templates/vanilla/package.json.tpl +31 -0
- package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
- package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
- package/src/lib/templates/vanilla/server.js.tpl +81 -0
- package/src/lib/zip.js +130 -0
- package/src/playground/index.html +708 -3
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Page (MUI)
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Box,
|
|
11
|
+
Container,
|
|
12
|
+
TextField,
|
|
13
|
+
Button,
|
|
14
|
+
Card,
|
|
15
|
+
CardContent,
|
|
16
|
+
Typography,
|
|
17
|
+
List,
|
|
18
|
+
ListItem,
|
|
19
|
+
Chip,
|
|
20
|
+
CircularProgress,
|
|
21
|
+
Alert,
|
|
22
|
+
InputAdornment,
|
|
23
|
+
} from '@mui/material';
|
|
24
|
+
import SearchIcon from '@mui/icons-material/Search';
|
|
25
|
+
|
|
26
|
+
export default function SearchPage() {
|
|
27
|
+
const [query, setQuery] = useState('');
|
|
28
|
+
const [results, setResults] = useState([]);
|
|
29
|
+
const [loading, setLoading] = useState(false);
|
|
30
|
+
const [error, setError] = useState(null);
|
|
31
|
+
const [meta, setMeta] = useState(null);
|
|
32
|
+
|
|
33
|
+
const handleSearch = async (e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
if (!query.trim()) return;
|
|
36
|
+
|
|
37
|
+
setLoading(true);
|
|
38
|
+
setError(null);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch('/api/search', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ query, limit: 5 }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
throw new Error(data.error || 'Search failed');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
setResults(data.results);
|
|
54
|
+
setMeta(data.meta);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
setError(err.message);
|
|
57
|
+
setResults([]);
|
|
58
|
+
} finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Container maxWidth="md" sx={{ py: 4 }}>
|
|
65
|
+
<Typography variant="h4" component="h1" gutterBottom>
|
|
66
|
+
Semantic Search
|
|
67
|
+
</Typography>
|
|
68
|
+
|
|
69
|
+
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
|
70
|
+
Search your documents using {{model}} embeddings
|
|
71
|
+
{{#if rerank}} with {{rerankModel}} reranking{{/if}}.
|
|
72
|
+
</Typography>
|
|
73
|
+
|
|
74
|
+
<Box component="form" onSubmit={handleSearch} sx={{ mb: 4 }}>
|
|
75
|
+
<TextField
|
|
76
|
+
fullWidth
|
|
77
|
+
value={query}
|
|
78
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
79
|
+
placeholder="Enter your search query..."
|
|
80
|
+
variant="outlined"
|
|
81
|
+
InputProps={{
|
|
82
|
+
startAdornment: (
|
|
83
|
+
<InputAdornment position="start">
|
|
84
|
+
<SearchIcon />
|
|
85
|
+
</InputAdornment>
|
|
86
|
+
),
|
|
87
|
+
endAdornment: (
|
|
88
|
+
<InputAdornment position="end">
|
|
89
|
+
<Button
|
|
90
|
+
type="submit"
|
|
91
|
+
variant="contained"
|
|
92
|
+
disabled={loading || !query.trim()}
|
|
93
|
+
>
|
|
94
|
+
{loading ? <CircularProgress size={24} /> : 'Search'}
|
|
95
|
+
</Button>
|
|
96
|
+
</InputAdornment>
|
|
97
|
+
),
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</Box>
|
|
101
|
+
|
|
102
|
+
{error && (
|
|
103
|
+
<Alert severity="error" sx={{ mb: 3 }}>
|
|
104
|
+
{error}
|
|
105
|
+
</Alert>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{meta && (
|
|
109
|
+
<Box sx={{ mb: 2, display: 'flex', gap: 1 }}>
|
|
110
|
+
<Chip label={`${results.length} results`} size="small" />
|
|
111
|
+
<Chip label={`${meta.took}ms`} size="small" variant="outlined" />
|
|
112
|
+
<Chip label={meta.model} size="small" variant="outlined" />
|
|
113
|
+
</Box>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<List>
|
|
117
|
+
{results.map((result, index) => (
|
|
118
|
+
<ListItem key={index} sx={{ px: 0 }}>
|
|
119
|
+
<Card sx={{ width: '100%' }}>
|
|
120
|
+
<CardContent>
|
|
121
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
|
122
|
+
<Typography variant="subtitle2" color="text.secondary">
|
|
123
|
+
{result.metadata?.source || 'Document'}
|
|
124
|
+
</Typography>
|
|
125
|
+
<Chip
|
|
126
|
+
label={`Score: ${result.score.toFixed(3)}`}
|
|
127
|
+
size="small"
|
|
128
|
+
color={result.score > 0.8 ? 'success' : 'default'}
|
|
129
|
+
/>
|
|
130
|
+
</Box>
|
|
131
|
+
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
132
|
+
{result.text}
|
|
133
|
+
</Typography>
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
</ListItem>
|
|
137
|
+
))}
|
|
138
|
+
</List>
|
|
139
|
+
|
|
140
|
+
{results.length === 0 && !loading && query && !error && (
|
|
141
|
+
<Typography color="text.secondary" align="center">
|
|
142
|
+
No results found for "{query}"
|
|
143
|
+
</Typography>
|
|
144
|
+
)}
|
|
145
|
+
</Container>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest API Route Handler
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*
|
|
5
|
+
* POST /api/ingest
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextResponse } from 'next/server';
|
|
9
|
+
import { embed } from '@/lib/voyage';
|
|
10
|
+
import { insertDocuments } from '@/lib/mongodb';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Chunk text using recursive splitting.
|
|
14
|
+
*/
|
|
15
|
+
function chunkText(text, size = {{chunkSize}}, overlap = {{chunkOverlap}}) {
|
|
16
|
+
const separators = ['\n\n', '\n', '. ', ' '];
|
|
17
|
+
|
|
18
|
+
function splitRecursive(text, sepIndex = 0) {
|
|
19
|
+
if (text.length <= size) {
|
|
20
|
+
const trimmed = text.trim();
|
|
21
|
+
return trimmed ? [trimmed] : [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (sepIndex >= separators.length) {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let start = 0;
|
|
27
|
+
while (start < text.length) {
|
|
28
|
+
const chunk = text.slice(start, start + size).trim();
|
|
29
|
+
if (chunk) chunks.push(chunk);
|
|
30
|
+
start += size - overlap;
|
|
31
|
+
}
|
|
32
|
+
return chunks;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const separator = separators[sepIndex];
|
|
36
|
+
const parts = text.split(separator);
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let current = '';
|
|
39
|
+
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
const potential = current ? current + separator + part : part;
|
|
42
|
+
if (potential.length <= size) {
|
|
43
|
+
current = potential;
|
|
44
|
+
} else {
|
|
45
|
+
if (current) chunks.push(...splitRecursive(current, sepIndex + 1));
|
|
46
|
+
current = part;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (current) chunks.push(...splitRecursive(current, sepIndex + 1));
|
|
51
|
+
return chunks;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return splitRecursive(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function POST(request) {
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const body = await request.json();
|
|
62
|
+
const { text, metadata = {} } = body;
|
|
63
|
+
|
|
64
|
+
if (!text || typeof text !== 'string') {
|
|
65
|
+
return NextResponse.json(
|
|
66
|
+
{ error: 'Text is required' },
|
|
67
|
+
{ status: 400 }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Chunk the text
|
|
72
|
+
const chunks = chunkText(text);
|
|
73
|
+
|
|
74
|
+
if (chunks.length === 0) {
|
|
75
|
+
return NextResponse.json({
|
|
76
|
+
success: true,
|
|
77
|
+
chunks: 0,
|
|
78
|
+
tokens: 0,
|
|
79
|
+
took: Date.now() - start,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Embed all chunks
|
|
84
|
+
const { embeddings, usage } = await embed(chunks, { inputType: 'document' });
|
|
85
|
+
|
|
86
|
+
// Prepare documents
|
|
87
|
+
const documents = chunks.map((chunk, i) => ({
|
|
88
|
+
text: chunk,
|
|
89
|
+
embedding: embeddings[i],
|
|
90
|
+
metadata: {
|
|
91
|
+
...metadata,
|
|
92
|
+
source: metadata.source || 'api',
|
|
93
|
+
chunkIndex: i,
|
|
94
|
+
totalChunks: chunks.length,
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
// Insert into MongoDB
|
|
99
|
+
await insertDocuments(documents);
|
|
100
|
+
|
|
101
|
+
return NextResponse.json({
|
|
102
|
+
success: true,
|
|
103
|
+
chunks: chunks.length,
|
|
104
|
+
tokens: usage.total_tokens,
|
|
105
|
+
took: Date.now() - start,
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Ingest error:', error);
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{ error: error.message },
|
|
111
|
+
{ status: 500 }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search API Route Handler
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*
|
|
5
|
+
* POST /api/search
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextResponse } from 'next/server';
|
|
9
|
+
import { embedQuery{{#if rerank}}, rerank{{/if}} } from '@/lib/voyage';
|
|
10
|
+
import { vectorSearch } from '@/lib/mongodb';
|
|
11
|
+
|
|
12
|
+
export async function POST(request) {
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const body = await request.json();
|
|
17
|
+
const { query, limit = 5, includeContext = false, filter } = body;
|
|
18
|
+
|
|
19
|
+
if (!query || typeof query !== 'string') {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: 'Query is required' },
|
|
22
|
+
{ status: 400 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
{{#if rerank}}
|
|
27
|
+
const candidates = body.candidates || 20;
|
|
28
|
+
{{/if}}
|
|
29
|
+
|
|
30
|
+
// Step 1: Embed the query
|
|
31
|
+
const queryEmbedding = await embedQuery(query);
|
|
32
|
+
|
|
33
|
+
// Step 2: Vector search
|
|
34
|
+
{{#if rerank}}
|
|
35
|
+
const searchResults = await vectorSearch(queryEmbedding, {
|
|
36
|
+
limit: candidates,
|
|
37
|
+
filter,
|
|
38
|
+
});
|
|
39
|
+
{{else}}
|
|
40
|
+
const searchResults = await vectorSearch(queryEmbedding, {
|
|
41
|
+
limit,
|
|
42
|
+
filter,
|
|
43
|
+
});
|
|
44
|
+
{{/if}}
|
|
45
|
+
|
|
46
|
+
if (searchResults.length === 0) {
|
|
47
|
+
return NextResponse.json({
|
|
48
|
+
results: [],
|
|
49
|
+
meta: {
|
|
50
|
+
model: '{{model}}',
|
|
51
|
+
took: Date.now() - start,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
{{#if rerank}}
|
|
57
|
+
// Step 3: Rerank for better relevance
|
|
58
|
+
const documents = searchResults.map(r => r.document.text);
|
|
59
|
+
const { results: reranked } = await rerank(query, documents, { topK: limit });
|
|
60
|
+
|
|
61
|
+
const results = reranked.map(r => ({
|
|
62
|
+
text: searchResults[r.index].document.text,
|
|
63
|
+
score: r.relevanceScore,
|
|
64
|
+
metadata: searchResults[r.index].document.metadata,
|
|
65
|
+
}));
|
|
66
|
+
{{else}}
|
|
67
|
+
const results = searchResults.map(r => ({
|
|
68
|
+
text: r.document.text,
|
|
69
|
+
score: r.score,
|
|
70
|
+
metadata: r.document.metadata,
|
|
71
|
+
}));
|
|
72
|
+
{{/if}}
|
|
73
|
+
|
|
74
|
+
const response = {
|
|
75
|
+
results,
|
|
76
|
+
meta: {
|
|
77
|
+
model: '{{model}}',
|
|
78
|
+
{{#if rerank}}
|
|
79
|
+
rerankModel: '{{rerankModel}}',
|
|
80
|
+
{{/if}}
|
|
81
|
+
took: Date.now() - start,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (includeContext) {
|
|
86
|
+
response.context = results.map(r => r.text).join('\n\n---\n\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return NextResponse.json(response);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Search error:', error);
|
|
92
|
+
return NextResponse.json(
|
|
93
|
+
{ error: error.message },
|
|
94
|
+
{ status: 500 }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUI Theme Configuration
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { createTheme } from '@mui/material/styles';
|
|
9
|
+
|
|
10
|
+
// MongoDB-inspired color palette
|
|
11
|
+
const palette = {
|
|
12
|
+
primary: {
|
|
13
|
+
main: '#00ED64', // MongoDB Green
|
|
14
|
+
light: '#4DFF94',
|
|
15
|
+
dark: '#00B84A',
|
|
16
|
+
contrastText: '#000',
|
|
17
|
+
},
|
|
18
|
+
secondary: {
|
|
19
|
+
main: '#001E2B', // MongoDB Dark
|
|
20
|
+
light: '#1C3A4B',
|
|
21
|
+
dark: '#000F17',
|
|
22
|
+
contrastText: '#fff',
|
|
23
|
+
},
|
|
24
|
+
background: {
|
|
25
|
+
default: '#FAFAFA',
|
|
26
|
+
paper: '#FFFFFF',
|
|
27
|
+
},
|
|
28
|
+
text: {
|
|
29
|
+
primary: '#001E2B',
|
|
30
|
+
secondary: '#5C6C75',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const theme = createTheme({
|
|
35
|
+
palette,
|
|
36
|
+
typography: {
|
|
37
|
+
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
|
38
|
+
h1: {
|
|
39
|
+
fontWeight: 700,
|
|
40
|
+
},
|
|
41
|
+
h2: {
|
|
42
|
+
fontWeight: 600,
|
|
43
|
+
},
|
|
44
|
+
h3: {
|
|
45
|
+
fontWeight: 600,
|
|
46
|
+
},
|
|
47
|
+
h4: {
|
|
48
|
+
fontWeight: 600,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
shape: {
|
|
52
|
+
borderRadius: 8,
|
|
53
|
+
},
|
|
54
|
+
components: {
|
|
55
|
+
MuiButton: {
|
|
56
|
+
styleOverrides: {
|
|
57
|
+
root: {
|
|
58
|
+
textTransform: 'none',
|
|
59
|
+
fontWeight: 600,
|
|
60
|
+
},
|
|
61
|
+
contained: {
|
|
62
|
+
boxShadow: 'none',
|
|
63
|
+
'&:hover': {
|
|
64
|
+
boxShadow: 'none',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
MuiCard: {
|
|
70
|
+
styleOverrides: {
|
|
71
|
+
root: {
|
|
72
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.06)',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
MuiChip: {
|
|
77
|
+
styleOverrides: {
|
|
78
|
+
root: {
|
|
79
|
+
fontWeight: 500,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A semantic search API powered by Voyage AI embeddings and MongoDB Atlas Vector Search.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
| Setting | Value |
|
|
8
|
+
|---------|-------|
|
|
9
|
+
| Embedding Model | `{{model}}` |
|
|
10
|
+
| Dimensions | {{dimensions}} |
|
|
11
|
+
| Database | `{{db}}` |
|
|
12
|
+
| Collection | `{{collection}}` |
|
|
13
|
+
| Vector Index | `{{index}}` |
|
|
14
|
+
{{#if rerank}}
|
|
15
|
+
| Rerank Model | `{{rerankModel}}` |
|
|
16
|
+
{{/if}}
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
### 1. Create virtual environment
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m venv venv
|
|
24
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Install dependencies
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Configure environment
|
|
34
|
+
|
|
35
|
+
Copy `.env.example` to `.env` and fill in your credentials:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cp .env.example .env
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Required variables:
|
|
42
|
+
- `VOYAGE_API_KEY` - Your Voyage AI API key from [dash.voyageai.com](https://dash.voyageai.com)
|
|
43
|
+
- `MONGODB_URI` - Your MongoDB Atlas connection string
|
|
44
|
+
|
|
45
|
+
### 4. Create vector index
|
|
46
|
+
|
|
47
|
+
In MongoDB Atlas, create a vector search index on your collection:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"fields": [
|
|
52
|
+
{
|
|
53
|
+
"type": "vector",
|
|
54
|
+
"path": "{{field}}",
|
|
55
|
+
"numDimensions": {{dimensions}},
|
|
56
|
+
"similarity": "cosine"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Name the index `{{index}}`.
|
|
63
|
+
|
|
64
|
+
### 5. Start the server
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python app.py
|
|
68
|
+
|
|
69
|
+
# For production
|
|
70
|
+
gunicorn -w 4 -b 0.0.0.0:3000 app:app
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Endpoints
|
|
74
|
+
|
|
75
|
+
### POST /api/search
|
|
76
|
+
|
|
77
|
+
Search for relevant documents.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
curl -X POST http://localhost:3000/api/search \
|
|
81
|
+
-H "Content-Type: application/json" \
|
|
82
|
+
-d '{"query": "How does vector search work?", "limit": 5}'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Response:
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"results": [
|
|
89
|
+
{
|
|
90
|
+
"text": "Vector search uses...",
|
|
91
|
+
"score": 0.95,
|
|
92
|
+
"metadata": {"source": "docs/guide.md"}
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"meta": {
|
|
96
|
+
"model": "{{model}}",
|
|
97
|
+
"took": 123
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### POST /api/ingest
|
|
103
|
+
|
|
104
|
+
Ingest text documents.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
curl -X POST http://localhost:3000/api/ingest \
|
|
108
|
+
-H "Content-Type: application/json" \
|
|
109
|
+
-d '{"text": "Your document content...", "metadata": {"source": "api"}}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### GET /api/health
|
|
113
|
+
|
|
114
|
+
Check API health and database connection.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
curl http://localhost:3000/api/health
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Project Structure
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
{{projectName}}/
|
|
124
|
+
├── app.py # Flask application
|
|
125
|
+
├── voyage_client.py # Voyage AI API client
|
|
126
|
+
├── mongo_client.py # MongoDB connection helper
|
|
127
|
+
├── chunker.py # Text chunking utilities
|
|
128
|
+
├── requirements.txt
|
|
129
|
+
├── .env.example
|
|
130
|
+
└── README.md
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Chunking Configuration
|
|
134
|
+
|
|
135
|
+
Documents are chunked with the following settings:
|
|
136
|
+
|
|
137
|
+
| Setting | Value |
|
|
138
|
+
|---------|-------|
|
|
139
|
+
| Strategy | `{{chunkStrategy}}` |
|
|
140
|
+
| Chunk Size | {{chunkSize}} characters |
|
|
141
|
+
| Overlap | {{chunkOverlap}} characters |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}}
|