diff --git a/scripts/README.md b/scripts/README.md
index d9ec3f8..793bcc3 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -26,6 +26,12 @@ pnpm test:db:seed:token # against the token server
pnpm test:db:seed:basic # against the basic-auth server
```
+The seed data includes:
+
+- `articles` and `code_snippets` with small 8-dimensional fixture embeddings.
+- `huggingface_384d` with HuggingFace-style 384-dimensional fixture embeddings
+ for testing inferred collection dimensions and custom embedding server URLs.
+
Stop with `Ctrl+C`.
## Auth credentials
diff --git a/scripts/seed-test-db.mjs b/scripts/seed-test-db.mjs
index ad8990e..5967f11 100644
--- a/scripts/seed-test-db.mjs
+++ b/scripts/seed-test-db.mjs
@@ -27,13 +27,11 @@ if (authMode === 'token') {
const client = new ChromaClient({ host: 'localhost', port, ssl: false, headers })
-const EMBED_DIM = 8
-
-function seededVector(seed) {
+function seededVector(seed, dimension) {
// Deterministic pseudo-random vector so re-seeds produce stable embeddings.
- const out = new Array(EMBED_DIM)
+ const out = new Array(dimension)
let s = seed
- for (let i = 0; i < EMBED_DIM; i++) {
+ for (let i = 0; i < dimension; i++) {
s = (s * 9301 + 49297) % 233280
out[i] = s / 233280 - 0.5
}
@@ -43,6 +41,7 @@ function seededVector(seed) {
const collections = [
{
name: 'articles',
+ dimension: 8,
metadata: { description: 'Sample blog articles for explorer testing' },
docs: [
{ id: 'a1', document: 'Getting started with vector databases.', metadata: { topic: 'intro', words: 5 } },
@@ -54,6 +53,7 @@ const collections = [
},
{
name: 'code_snippets',
+ dimension: 8,
metadata: { description: 'Code snippets across languages' },
docs: [
{ id: 'c1', document: 'def hello():\n print("hi")', metadata: { language: 'python', lines: 2 } },
@@ -63,6 +63,32 @@ const collections = [
{ id: 'c5', document: 'puts "hello"', metadata: { language: 'ruby', lines: 1 } },
],
},
+ {
+ name: 'huggingface_384d',
+ dimension: 384,
+ metadata: {
+ description: 'HuggingFace-style 384-dimensional embeddings for URL and dimension testing',
+ embedding_provider: 'huggingface-server',
+ embedding_model: 'sentence-transformers/all-MiniLM-L6-v2',
+ },
+ docs: [
+ {
+ id: 'hf1',
+ document: 'A local HuggingFace embedding server can expose sentence-transformer vectors.',
+ metadata: { provider: 'huggingface-server', model: 'all-MiniLM-L6-v2', dimension: 384 },
+ },
+ {
+ id: 'hf2',
+ document: 'MiniLM embeddings are compact enough for fast document exploration.',
+ metadata: { provider: 'huggingface-server', model: 'all-MiniLM-L6-v2', dimension: 384 },
+ },
+ {
+ id: 'hf3',
+ document: 'This fixture helps verify inferred dimensions in the collection UI.',
+ metadata: { provider: 'huggingface-server', model: 'all-MiniLM-L6-v2', dimension: 384 },
+ },
+ ],
+ },
]
async function main() {
@@ -76,9 +102,9 @@ async function main() {
const ids = spec.docs.map((d) => d.id)
const documents = spec.docs.map((d) => d.document)
const metadatas = spec.docs.map((d) => d.metadata)
- const embeddings = spec.docs.map((_, idx) => seededVector(seedBase + idx + 1))
+ const embeddings = spec.docs.map((_, idx) => seededVector(seedBase + idx + 1, spec.dimension))
await col.upsert({ ids, documents, metadatas, embeddings })
- console.log(`[seed] upserted ${ids.length} docs into "${spec.name}"`)
+ console.log(`[seed] upserted ${ids.length} docs into "${spec.name}" (${spec.dimension}d)`)
}
console.log('[seed] done')
diff --git a/src/components/collections/CollectionConfigView.tsx b/src/components/collections/CollectionConfigView.tsx
index 03a91ff..6415624 100644
--- a/src/components/collections/CollectionConfigView.tsx
+++ b/src/components/collections/CollectionConfigView.tsx
@@ -3,7 +3,16 @@ import { ChevronDown, ChevronRight, Plus, X } from 'lucide-react'
import { useDraftCollection, DraftHNSWConfig } from '../../context/DraftCollectionContext'
import { useChromaDB } from '../../providers/ChromaDBProvider'
import { useCollection } from '../../context/CollectionContext'
-import { EMBEDDING_FUNCTIONS, EMBEDDING_FUNCTION_GROUPS, getEmbeddingFunctionById } from '../../constants/embedding-functions'
+import {
+ buildEmbeddingFunctionOverride,
+ EMBEDDING_FUNCTIONS,
+ EMBEDDING_FUNCTION_GROUPS,
+ embeddingFunctionUsesUrl,
+ getEmbeddingFunctionById,
+ getEmbeddingFunctionUrlLabel,
+ getEmbeddingFunctionUrlPlaceholder,
+ validateEmbeddingFunctionUrl,
+} from '../../constants/embedding-functions'
import { MetadataValueType, validateMetadataValue } from '../../types/metadata'
import { CopyProgressDialog } from './CopyProgressDialog'
@@ -29,19 +38,19 @@ export function CollectionConfigView() {
})
const selectedEf = draftCollection ? getEmbeddingFunctionById(draftCollection.embeddingFunctionId) : EMBEDDING_FUNCTIONS[0]
+ const embeddingFunctionUrlError = draftCollection && selectedEf
+ ? validateEmbeddingFunctionUrl(selectedEf, draftCollection.embeddingFunctionUrl)
+ : null
+ const dimensionsLabel = selectedEf?.dimensions ? `${selectedEf.dimensions}d` : 'Inferred from first embedding'
// Handle embedding function change - auto-populate dimension
const handleEfChange = useCallback(
(efId: string) => {
- updateDraft({ embeddingFunctionId: efId, dimensionOverride: '' })
- },
- [updateDraft]
- )
-
- // Handle dimension override change
- const handleDimensionChange = useCallback(
- (value: string) => {
- updateDraft({ dimensionOverride: value })
+ const nextEf = getEmbeddingFunctionById(efId)
+ updateDraft({
+ embeddingFunctionId: efId,
+ embeddingFunctionUrl: nextEf?.url || '',
+ })
},
[updateDraft]
)
@@ -70,13 +79,16 @@ export function CollectionConfigView() {
if (!draftCollection?.sourceCollection) return false
const source = draftCollection.sourceCollection
- const sourceType = source.embeddingFunction?.name === 'openai' ? 'openai' : 'default'
+ const sourceType = source.embeddingFunction?.name ?? 'default'
const sourceModel = source.embeddingFunction?.config?.model_name as string | undefined
+ const sourceUrl = source.embeddingFunction?.config?.url as string | undefined
const draftEf = getEmbeddingFunctionById(draftCollection.embeddingFunctionId)
+ const draftUrl = draftCollection.embeddingFunctionUrl.trim() || draftEf?.url
if (draftEf?.type !== sourceType) return true
- if (draftEf?.type === 'openai' && draftEf?.modelName !== sourceModel) return true
+ if (sourceModel && draftEf?.modelName !== sourceModel) return true
+ if (draftUrl !== sourceUrl) return true
return false
}, [draftCollection])
@@ -89,6 +101,11 @@ export function CollectionConfigView() {
if (!draftCollection.name.trim()) {
errors.name = 'Collection name is required'
}
+ const draftEf = getEmbeddingFunctionById(draftCollection.embeddingFunctionId)
+ const urlError = validateEmbeddingFunctionUrl(draftEf, draftCollection.embeddingFunctionUrl)
+ if (urlError) {
+ errors.embeddingFunctionUrl = urlError
+ }
if (Object.keys(errors).length > 0) {
return
@@ -132,10 +149,9 @@ export function CollectionConfigView() {
const result = await window.electronAPI.chromadb.copyCollection(currentProfile.id, {
sourceCollectionName: draftCollection.sourceCollection.name,
targetName: draftCollection.name.trim(),
- embeddingFunction: draftEf ? {
- type: draftEf.type,
- modelName: draftEf.modelName,
- } : undefined,
+ embeddingFunction: draftEf
+ ? buildEmbeddingFunctionOverride(draftEf, { url: draftCollection.embeddingFunctionUrl })
+ : undefined,
hnsw: Object.keys(hnswConfig).length > 0 ? hnswConfig : undefined,
regenerateEmbeddings: needsEmbeddingRegeneration(),
})
@@ -283,22 +299,40 @@ export function CollectionConfigView() {
{selectedEf &&
{selectedEf.modelName}
}
- {/* Dimension Input */}
+ {embeddingFunctionUsesUrl(selectedEf) && (
+
+
+
updateDraft({ embeddingFunctionUrl: e.target.value })}
+ placeholder={getEmbeddingFunctionUrlPlaceholder(selectedEf)}
+ className={`${inputClassName} ${embeddingFunctionUrlError ? 'border-destructive' : ''}`}
+ style={inputStyle}
+ />
+ {embeddingFunctionUrlError ? (
+
{embeddingFunctionUrlError}
+ ) : (
+
+ Used by the client when generating embeddings.
+
+ )}
+
+ )}
+
+ {/* Dimension Info */}
-
-
handleDimensionChange(e.target.value)}
- placeholder={selectedEf?.dimensions?.toString() || 'auto'}
- className={inputClassName}
- style={inputStyle}
- />
+
+ Dimensions
+
+
+ {dimensionsLabel}
+
- Default: {selectedEf?.dimensions ?? 'auto'}
+ Chroma sets the collection dimension from generated embeddings.
@@ -586,7 +620,7 @@ export function CollectionConfigView() {