8.0 RC cleanup toward "one place per thing, zero-config, no deprecation":
- Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead
legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})`
/ `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate
entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor,
NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept.
- Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams).
Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` →
now `find({ query, limit })`.
- Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases
(`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the
exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver
feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native
storage provider resolves the identical root (no split-brain).
- Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now
applied as a post-filter on `result.score` (the documented way to bound semantic results).
- Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()`
and failed dimension validation; they are metadata-only updates now.
- Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination
shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an
in-place path change that preserves the blob and the entity id, for files and directories.
- Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability.
Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk
flushes and emits `progress.queryable`, so imported data is queryable during the import.
- Sweep all docs, comments, and JSDoc for the removed/changed APIs.
Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
545 lines
No EOL
14 KiB
Markdown
545 lines
No EOL
14 KiB
Markdown
# Framework Integration Guide
|
|
|
|
Brainy is **framework-friendly** - designed to drop into the server side of any modern JavaScript framework. This guide shows you how to integrate Brainy into framework-based apps.
|
|
|
|
> **Runtime**: Brainy 8.0 runs on Node.js 22+ and Bun (server-side only). It is not a browser library. In framework apps, run Brainy in API routes, server components, server actions, loaders, or a dedicated backend service - never in client-side bundles.
|
|
|
|
## 🎯 Why Server-Side?
|
|
|
|
Brainy embeds an HNSW vector index, a graph engine, and a filesystem-backed persistence layer. These belong on the server:
|
|
|
|
- **Zero configuration**: Just `import { Brainy } from '@soulcraft/brainy'`
|
|
- **Auto storage detection**: `new Brainy()` auto-selects filesystem persistence on Node
|
|
- **Cleaner code**: No browser polyfills, no conditional client/server imports
|
|
- **Better DX**: One instance shared across your server routes
|
|
|
|
## 🚀 Quick Start
|
|
|
|
### Install Brainy
|
|
|
|
```bash
|
|
npm install @soulcraft/brainy
|
|
```
|
|
|
|
### Basic Integration
|
|
|
|
```javascript
|
|
import { Brainy } from '@soulcraft/brainy'
|
|
|
|
// Run on the server (API route, server component, backend service)
|
|
// new Brainy() auto-detects filesystem persistence on Node
|
|
const brain = new Brainy()
|
|
await brain.init()
|
|
|
|
// Add data
|
|
await brain.add({
|
|
data: "Framework integration is awesome!",
|
|
type: "concept",
|
|
metadata: { framework: "any" }
|
|
})
|
|
|
|
// Search
|
|
const results = await brain.find("framework integration")
|
|
```
|
|
|
|
## ⚛️ React Integration
|
|
|
|
Brainy runs on the server, so a React client component talks to it through an API endpoint (see the Next.js API route below). The hook fetches results; it never instantiates Brainy in the browser.
|
|
|
|
### Basic Hook Pattern
|
|
|
|
```jsx
|
|
import { useState, useCallback } from 'react'
|
|
|
|
function useBrainySearch(endpoint = '/api/search') {
|
|
const [results, setResults] = useState([])
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const search = useCallback(async (query) => {
|
|
if (!query) return
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query })
|
|
})
|
|
const { results } = await res.json()
|
|
setResults(results)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [endpoint])
|
|
|
|
return { results, loading, search }
|
|
}
|
|
|
|
// Usage in component
|
|
function SearchComponent() {
|
|
const { results, loading, search } = useBrainySearch()
|
|
|
|
return (
|
|
<div>
|
|
<input
|
|
type="text"
|
|
placeholder="Search..."
|
|
onChange={(e) => search(e.target.value)}
|
|
/>
|
|
{loading && <div>Searching...</div>}
|
|
<div>
|
|
{results.map(result => (
|
|
<div key={result.id}>
|
|
<h3>{result.data}</h3>
|
|
<p>Score: {(result.score * 100).toFixed(1)}%</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Shared Server Instance
|
|
|
|
On the server, create one Brainy instance and reuse it across requests. This module is imported only by server code (API routes, server components), never by client components:
|
|
|
|
```javascript
|
|
// lib/brain.server.js
|
|
import { Brainy } from '@soulcraft/brainy'
|
|
|
|
let brainPromise
|
|
|
|
export function getBrain() {
|
|
if (!brainPromise) {
|
|
brainPromise = (async () => {
|
|
// new Brainy() auto-detects filesystem persistence on Node
|
|
const brain = new Brainy()
|
|
await brain.init()
|
|
return brain
|
|
})()
|
|
}
|
|
return brainPromise
|
|
}
|
|
```
|
|
|
|
## 🟢 Vue.js Integration
|
|
|
|
Vue components call a server endpoint (see the Nuxt server route in the [Vue.js Integration Guide](vue-integration.md)); Brainy itself runs on the server.
|
|
|
|
### Composition API (client component)
|
|
|
|
```vue
|
|
<template>
|
|
<div>
|
|
<input v-model="query" @input="search" placeholder="Search..." />
|
|
<div v-for="result in results" :key="result.id">
|
|
<h3>{{ result.data }}</h3>
|
|
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
|
|
const query = ref('')
|
|
const results = ref([])
|
|
|
|
const search = async () => {
|
|
if (!query.value) return
|
|
const res = await fetch('/api/search', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query: query.value })
|
|
})
|
|
results.value = (await res.json()).results
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Shared Server Instance
|
|
|
|
On the server, create one Brainy instance and reuse it across requests:
|
|
|
|
```javascript
|
|
// server/brain.js (server-only module)
|
|
import { Brainy } from '@soulcraft/brainy'
|
|
|
|
let brainPromise
|
|
|
|
export function getBrain() {
|
|
if (!brainPromise) {
|
|
brainPromise = (async () => {
|
|
// new Brainy() auto-detects filesystem persistence on Node
|
|
const brain = new Brainy()
|
|
await brain.init()
|
|
return brain
|
|
})()
|
|
}
|
|
return brainPromise
|
|
}
|
|
```
|
|
|
|
## 🅰️ Angular Integration
|
|
|
|
The Angular service calls your backend over HTTP; Brainy lives in that backend, not in the browser.
|
|
|
|
### Service Pattern (calls the backend)
|
|
|
|
```typescript
|
|
// brainy.service.ts
|
|
import { Injectable } from '@angular/core'
|
|
import { HttpClient } from '@angular/common/http'
|
|
import { Observable } from 'rxjs'
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class BrainyService {
|
|
constructor(private http: HttpClient) {}
|
|
|
|
search(query: string): Observable<{ results: any[] }> {
|
|
return this.http.post<{ results: any[] }>('/api/search', { query })
|
|
}
|
|
|
|
add(data: any, type: string, metadata?: any): Observable<{ id: string }> {
|
|
return this.http.post<{ id: string }>('/api/add', { data, type, metadata })
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// search.component.ts
|
|
import { Component } from '@angular/core'
|
|
import { BrainyService } from './brainy.service'
|
|
|
|
@Component({
|
|
selector: 'app-search',
|
|
template: `
|
|
<div>
|
|
<input
|
|
[(ngModel)]="query"
|
|
(input)="search()"
|
|
placeholder="Search..."
|
|
/>
|
|
<div *ngFor="let result of results">
|
|
<h3>{{ result.data }}</h3>
|
|
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
|
|
</div>
|
|
</div>
|
|
`
|
|
})
|
|
export class SearchComponent {
|
|
query = ''
|
|
results: any[] = []
|
|
|
|
constructor(private brainyService: BrainyService) {}
|
|
|
|
search() {
|
|
if (!this.query) return
|
|
this.brainyService.search(this.query).subscribe(({ results }) => {
|
|
this.results = results
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
The matching backend endpoint uses Brainy directly (Node/Bun):
|
|
|
|
```typescript
|
|
// server: api/search
|
|
import { Brainy } from '@soulcraft/brainy'
|
|
|
|
const brain = new Brainy() // auto-detects filesystem persistence on Node
|
|
await brain.init()
|
|
|
|
export async function handleSearch(query: string) {
|
|
return await brain.find(query)
|
|
}
|
|
```
|
|
|
|
## 🚀 Next.js Integration
|
|
|
|
In Next.js, Brainy lives in server code only: API routes, server components, or server actions. Create one shared instance in a server-only module.
|
|
|
|
### Shared Server Instance
|
|
|
|
```javascript
|
|
// lib/brain.server.js (imported only by server code)
|
|
import { Brainy } from '@soulcraft/brainy'
|
|
|
|
let brainPromise
|
|
|
|
export function getBrain() {
|
|
if (!brainPromise) {
|
|
brainPromise = (async () => {
|
|
const brain = new Brainy({
|
|
storage: { type: 'filesystem', path: './data' }
|
|
})
|
|
await brain.init()
|
|
return brain
|
|
})()
|
|
}
|
|
return brainPromise
|
|
}
|
|
```
|
|
|
|
### API Routes
|
|
|
|
```javascript
|
|
// app/api/search/route.js
|
|
import { getBrain } from '@/lib/brain.server'
|
|
|
|
export async function POST(request) {
|
|
const { query } = await request.json()
|
|
const brain = await getBrain()
|
|
const results = await brain.find(query)
|
|
|
|
return Response.json({ results })
|
|
}
|
|
```
|
|
|
|
### Server Action
|
|
|
|
```javascript
|
|
// app/actions.js
|
|
'use server'
|
|
import { getBrain } from '@/lib/brain.server'
|
|
|
|
export async function search(query) {
|
|
const brain = await getBrain()
|
|
return await brain.find(query)
|
|
}
|
|
```
|
|
|
|
## 🔷 SvelteKit Integration
|
|
|
|
Brainy runs in a server-only module (`*.server.js`); the component fetches results from an endpoint.
|
|
|
|
```javascript
|
|
// src/lib/server/brain.js (server-only — note the .server suffix)
|
|
import { Brainy } from '@soulcraft/brainy'
|
|
|
|
let brainPromise
|
|
|
|
export function getBrain() {
|
|
if (!brainPromise) {
|
|
brainPromise = (async () => {
|
|
const brain = new Brainy() // auto-detects filesystem persistence
|
|
await brain.init()
|
|
return brain
|
|
})()
|
|
}
|
|
return brainPromise
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
// src/routes/api/search/+server.js
|
|
import { json } from '@sveltejs/kit'
|
|
import { getBrain } from '$lib/server/brain'
|
|
|
|
export async function POST({ request }) {
|
|
const { query } = await request.json()
|
|
const brain = await getBrain()
|
|
return json({ results: await brain.find(query) })
|
|
}
|
|
```
|
|
|
|
```svelte
|
|
<!-- SearchComponent.svelte -->
|
|
<script>
|
|
let query = ''
|
|
let results = []
|
|
|
|
async function search() {
|
|
if (!query) return
|
|
const res = await fetch('/api/search', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query })
|
|
})
|
|
results = (await res.json()).results
|
|
}
|
|
</script>
|
|
|
|
<div>
|
|
<input bind:value={query} on:input={search} placeholder="Search..." />
|
|
|
|
{#each results as result}
|
|
<div>
|
|
<h3>{result.data}</h3>
|
|
<p>Score: {(result.score * 100).toFixed(1)}%</p>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
```
|
|
|
|
## 🌟 Solid.js Integration
|
|
|
|
The component calls a server endpoint (use SolidStart server routes, or any backend, to host Brainy):
|
|
|
|
```jsx
|
|
import { createSignal } from 'solid-js'
|
|
|
|
function SearchComponent() {
|
|
const [query, setQuery] = createSignal('')
|
|
const [results, setResults] = createSignal([])
|
|
|
|
const search = async () => {
|
|
if (!query()) return
|
|
const res = await fetch('/api/search', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query: query() })
|
|
})
|
|
setResults((await res.json()).results)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<input
|
|
value={query()}
|
|
onInput={(e) => {
|
|
setQuery(e.target.value)
|
|
search()
|
|
}}
|
|
placeholder="Search..."
|
|
/>
|
|
|
|
<For each={results()}>
|
|
{(result) => (
|
|
<div>
|
|
<h3>{result.data}</h3>
|
|
<p>Score: {(result.score * 100).toFixed(1)}%</p>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
## 📦 Bundler Configuration
|
|
|
|
Brainy is a server-side dependency, so keep it out of client bundles. Import it only from server-only modules (`*.server.js`, API routes, server components, server actions). If your bundler ever tries to pull Brainy into a client bundle, that's a sign it's being imported from a client component — move the import to a server module.
|
|
|
|
For server builds, mark Brainy as external so the bundler doesn't inline it:
|
|
|
|
```javascript
|
|
// vite.config.js (SSR build)
|
|
import { defineConfig } from 'vite'
|
|
|
|
export default defineConfig({
|
|
ssr: {
|
|
external: ['@soulcraft/brainy']
|
|
}
|
|
})
|
|
```
|
|
|
|
```javascript
|
|
// rollup.config.js (server bundle)
|
|
export default {
|
|
external: ['@soulcraft/brainy', 'node:fs', 'node:path', 'node:crypto']
|
|
}
|
|
```
|
|
|
|
## 🌐 SSR/SSG Considerations
|
|
|
|
### Server-Side Rendering
|
|
|
|
Instantiate Brainy on the server and feed its results into the rendered page. Never construct it in client code.
|
|
|
|
```javascript
|
|
// Server-side data loading (framework loader / getServerSideProps / load fn)
|
|
import { getBrain } from './brain.server'
|
|
|
|
export async function load({ url }) {
|
|
const brain = await getBrain()
|
|
const query = url.searchParams.get('q') ?? ''
|
|
const results = query ? await brain.find(query) : []
|
|
return { results }
|
|
}
|
|
```
|
|
|
|
### Static Site Generation
|
|
|
|
```javascript
|
|
// For build-time usage (runs in Node during the build)
|
|
import { Brainy } from '@soulcraft/brainy'
|
|
|
|
export async function generateStaticProps() {
|
|
const brain = new Brainy({
|
|
storage: { type: 'filesystem', path: './content' }
|
|
})
|
|
await brain.init()
|
|
|
|
// Build search index (paginate with { limit, offset } for larger stores)
|
|
const allContent = await brain.find({ limit: 1000 })
|
|
|
|
return {
|
|
props: { searchIndex: allContent }
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🔧 Framework-Specific Tips
|
|
|
|
### React
|
|
- Keep components client-side and call a Brainy-backed API route
|
|
- Use `useCallback` for fetch handlers to prevent re-renders
|
|
- Debounce keystroke-driven searches before hitting the endpoint
|
|
|
|
### Vue
|
|
- Components call an endpoint; the shared instance lives in a server module
|
|
- Consider Pinia for caching results client-side
|
|
- Debounce reactive search queries
|
|
|
|
### Angular
|
|
- Use `HttpClient` and RxJS to call the backend
|
|
- Hold the shared Brainy instance in your Node backend, not the app
|
|
- Consider lazy loading search features in feature modules
|
|
|
|
### Next.js
|
|
- Put Brainy in server-only modules (`*.server.js`), API routes, or server actions
|
|
- Reuse one shared instance across requests
|
|
- Implement proper error boundaries for failed fetches
|
|
|
|
## 🚨 Common Issues & Solutions
|
|
|
|
### Issue: "fs module not found" / "crypto is not defined" in the browser
|
|
**Cause**: Brainy was imported into a client bundle. Brainy 8.0 is a server-side library (Node 22+/Bun) and uses Node built-ins like `fs` and `crypto`.
|
|
**Solution**: Import Brainy only from server code — server-only modules (`*.server.js`), API routes, server components, or server actions. From client components, call those endpoints instead.
|
|
|
|
### Issue: Large client bundle size
|
|
**Cause**: A client module is pulling in Brainy.
|
|
**Solution**: Move the `import { Brainy } from '@soulcraft/brainy'` into a server-only module so it never reaches the browser bundle.
|
|
|
|
### Issue: SSR hydration mismatch
|
|
**Solution**: Run the search on the server (loader / server action / API route) and pass the results down as props, so server and client render the same markup.
|
|
|
|
## 🎯 Best Practices
|
|
|
|
1. **Initialize Once**: Create one shared Brainy instance per server process, not per request
|
|
2. **Server-Only**: Import Brainy only from server modules — never from client components
|
|
3. **Endpoint Boundary**: Expose search/add through API routes or server actions
|
|
4. **Handle Loading**: Show loading states in the client while the fetch is in flight
|
|
5. **Error Handling**: Catch and surface failed endpoint calls gracefully
|
|
6. **Storage**: Use `filesystem` for persistence (the default on Node) or `memory` for ephemeral/tests
|
|
|
|
## 📚 Next Steps
|
|
|
|
- [Next.js Integration Guide](nextjs-integration.md) - Detailed Next.js examples
|
|
- [Vue.js Integration Guide](vue-integration.md) - Complete Vue.js patterns
|
|
- [API Reference](../api/README.md) - Complete API documentation
|
|
- [Production Deployment](../deployment/CLOUD_DEPLOYMENT_GUIDE.md) - Deploy to production
|
|
|
|
## 🤝 Community Examples
|
|
|
|
Check out community examples in the [examples repository](https://github.com/soulcraftlabs/brainy-examples):
|
|
|
|
- React + TypeScript starter
|
|
- Vue 3 + Composition API
|
|
- Next.js full-stack app
|
|
- Svelte SPA with search
|
|
- Angular enterprise app |