feat: add browser-server search example with server search augmentations

This commit is contained in:
David Snelling 2025-06-06 10:48:10 -07:00
parent bf3d9a055b
commit a206b9926d
7 changed files with 2021 additions and 0 deletions

View file

@ -0,0 +1,187 @@
# Brainy Browser-Server Search Example
This example demonstrates how to use Brainy in a browser, call a server-hosted version for search, store the results locally, and then perform further searches against the local instance.
## Overview
The solution consists of:
1. A `BrainyServerSearch` class that handles the connection to the server and local storage
2. An HTML interface for testing the functionality
3. Server-side setup using the Brainy cloud wrapper
This approach allows you to:
- Search a server-hosted Brainy instance from a browser
- Store the search results in a local Brainy instance
- Perform further searches against the local instance without needing to query the server again
- Add data to both local and server instances
## How It Works
1. The browser creates a local Brainy instance
2. It connects to the server-hosted Brainy instance using WebSocket
3. When a search is performed:
- The query is sent to the server
- The server returns the search results
- The results are stored in the local Brainy instance
- The results are displayed to the user
4. Subsequent searches can be performed against the local instance
5. A combined search mode first checks the local instance and then queries the server only if needed
## Setup Instructions
### Server Setup
1. Set up the Brainy cloud wrapper:
```bash
# Clone the repository if you haven't already
git clone https://github.com/soulcraft/brainy.git
cd brainy/cloud-wrapper
# Install dependencies
npm install --legacy-peer-deps
# Configure the server
cp .env.example .env
# Edit .env to configure your environment
# Build and start the server
npm run build
npm run start
```
2. Note the WebSocket URL of your server (e.g., `wss://your-server.com/ws` or `ws://localhost:3000/ws` for local development)
### Client Setup
1. Copy the example files to your project:
```bash
cp -r examples/browser-server-search your-project/
```
2. Include the Brainy library in your project:
```bash
npm install @soulcraft/brainy --legacy-peer-deps
```
3. Open the HTML file in a browser or serve it using a local server:
```bash
# Using a simple HTTP server
cd your-project
npx http-server
```
4. Navigate to http://localhost:8080/browser-server-search/ in your browser
5. Enter the WebSocket URL of your server and start using the example
## Usage
### Using the HTML Interface
1. Enter the WebSocket URL of your Brainy server
2. Click "Connect" to establish a connection
3. Enter a search query and click one of the search buttons:
- "Search Server" - Search the server and store results locally
- "Search Local" - Search only the local instance
- "Search Combined" - Search local first, then server if needed
4. To add data, enter text in the "Add Data" field and click "Add to Both"
### Using the BrainyServerSearch Class in Your Code
```javascript
import { BrainyServerSearch } from './index.js';
// Create a new instance
const brainySearch = new BrainyServerSearch('wss://your-brainy-server.com/ws');
// Initialize and connect
await brainySearch.init();
// Search the server and store results locally
const serverResults = await brainySearch.searchServer('machine learning', 5);
// Search the local instance
const localResults = await brainySearch.searchLocal('machine learning', 5);
// Perform a combined search
const combinedResults = await brainySearch.searchCombined('neural networks', 5);
// Add data to both local and server
const id = await brainySearch.add('Deep learning is a subset of machine learning', {
noun: 'Concept',
category: 'AI',
tags: ['deep learning', 'neural networks']
});
// Close the connection when done
await brainySearch.close();
```
## API Reference
### BrainyServerSearch Class
#### Constructor
```javascript
const brainySearch = new BrainyServerSearch(serverUrl);
```
- `serverUrl` (string): WebSocket URL of the Brainy server
#### Methods
- `init()`: Initialize the local Brainy instance and connect to the server
- `searchServer(query, limit = 10)`: Search the server-hosted Brainy instance, store results locally, and return them
- `searchLocal(query, limit = 10)`: Search the local Brainy instance
- `searchCombined(query, limit = 10)`: Search both server and local instances, combine results, and store server results locally
- `add(data, metadata = {})`: Add data to both local and server instances
- `close()`: Close the connection to the server
## Advanced Configuration
### Custom Embedding Function
You can customize the embedding function used by the local Brainy instance:
```javascript
import { createSimpleEmbeddingFunction } from '@soulcraft/brainy';
// In your code, before calling init():
brainySearch.setEmbeddingFunction(createSimpleEmbeddingFunction());
```
### Persistent Storage
To enable persistent storage for the local Brainy instance:
```javascript
// In your code, before calling init():
brainySearch.setStorageOptions({
requestPersistentStorage: true
});
```
## Troubleshooting
### Connection Issues
- Ensure the server is running and accessible
- Check that the WebSocket URL is correct
- Verify that your browser supports WebSockets
- Check for CORS issues if the server is on a different domain
### Search Issues
- Ensure the server has data to search
- Check that the query is not empty
- Verify that the server is properly configured for search
## License
MIT

View file

@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Brainy Browser-Server Search Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1, h2 {
color: #333;
}
pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 5px;
}
input[type="text"] {
padding: 10px;
width: 70%;
margin-right: 10px;
border-radius: 5px;
border: 1px solid #ddd;
}
.result-container {
margin-top: 20px;
border: 1px solid #ddd;
padding: 10px;
border-radius: 5px;
max-height: 400px;
overflow-y: auto;
}
.log {
margin-top: 10px;
color: #666;
}
.error {
color: red;
}
</style>
</head>
<body>
<h1>Brainy Browser-Server Search Example</h1>
<p>
This example demonstrates how to use Brainy in a browser, call a server-hosted version for search,
store the results locally, and then perform further searches against the local instance.
</p>
<div>
<h2>Server URL</h2>
<input type="text" id="serverUrl" value="wss://your-brainy-server.com/ws" placeholder="WebSocket URL of your Brainy server">
<button id="connectBtn">Connect</button>
</div>
<div>
<h2>Search</h2>
<input type="text" id="searchQuery" placeholder="Enter search query">
<button id="searchServerBtn">Search Server</button>
<button id="searchLocalBtn">Search Local</button>
<button id="searchCombinedBtn">Search Combined</button>
</div>
<div>
<h2>Add Data</h2>
<input type="text" id="addData" placeholder="Enter text to add">
<button id="addBtn">Add to Both</button>
</div>
<div class="result-container">
<h2>Results</h2>
<pre id="results">Connect to a server to begin...</pre>
</div>
<div class="log" id="log"></div>
<!-- Import the Brainy library -->
<script type="module">
// Import the BrainyServerSearch class
import { BrainyServerSearch } from './index.js';
// Global variables
let brainySearch = null;
// DOM elements
const serverUrlInput = document.getElementById('serverUrl');
const connectBtn = document.getElementById('connectBtn');
const searchQueryInput = document.getElementById('searchQuery');
const searchServerBtn = document.getElementById('searchServerBtn');
const searchLocalBtn = document.getElementById('searchLocalBtn');
const searchCombinedBtn = document.getElementById('searchCombinedBtn');
const addDataInput = document.getElementById('addData');
const addBtn = document.getElementById('addBtn');
const resultsElement = document.getElementById('results');
const logElement = document.getElementById('log');
// Helper functions
function log(message, isError = false) {
const div = document.createElement('div');
div.textContent = message;
if (isError) {
div.classList.add('error');
}
logElement.appendChild(div);
logElement.scrollTop = logElement.scrollHeight;
}
function displayResults(results) {
resultsElement.textContent = JSON.stringify(results, null, 2);
}
// Event handlers
connectBtn.addEventListener('click', async () => {
const serverUrl = serverUrlInput.value.trim();
if (!serverUrl) {
log('Please enter a server URL', true);
return;
}
try {
log(`Connecting to ${serverUrl}...`);
brainySearch = new BrainyServerSearch(serverUrl);
await brainySearch.init();
log('Connected successfully!');
displayResults({ status: 'Connected', serverUrl });
} catch (error) {
log(`Connection error: ${error.message}`, true);
displayResults({ error: error.message });
}
});
searchServerBtn.addEventListener('click', async () => {
if (!brainySearch) {
log('Please connect to a server first', true);
return;
}
const query = searchQueryInput.value.trim();
if (!query) {
log('Please enter a search query', true);
return;
}
try {
log(`Searching server for "${query}"...`);
const results = await brainySearch.searchServer(query, 5);
log(`Found ${results.length} results from server`);
displayResults(results);
} catch (error) {
log(`Search error: ${error.message}`, true);
displayResults({ error: error.message });
}
});
searchLocalBtn.addEventListener('click', async () => {
if (!brainySearch) {
log('Please connect to a server first', true);
return;
}
const query = searchQueryInput.value.trim();
if (!query) {
log('Please enter a search query', true);
return;
}
try {
log(`Searching local database for "${query}"...`);
const results = await brainySearch.searchLocal(query, 5);
log(`Found ${results.length} results locally`);
displayResults(results);
} catch (error) {
log(`Search error: ${error.message}`, true);
displayResults({ error: error.message });
}
});
searchCombinedBtn.addEventListener('click', async () => {
if (!brainySearch) {
log('Please connect to a server first', true);
return;
}
const query = searchQueryInput.value.trim();
if (!query) {
log('Please enter a search query', true);
return;
}
try {
log(`Performing combined search for "${query}"...`);
const results = await brainySearch.searchCombined(query, 5);
log(`Found ${results.length} combined results`);
displayResults(results);
} catch (error) {
log(`Search error: ${error.message}`, true);
displayResults({ error: error.message });
}
});
addBtn.addEventListener('click', async () => {
if (!brainySearch) {
log('Please connect to a server first', true);
return;
}
const data = addDataInput.value.trim();
if (!data) {
log('Please enter data to add', true);
return;
}
try {
log(`Adding data: "${data}"...`);
const id = await brainySearch.add(data, {
noun: 'Concept',
category: 'UserInput',
timestamp: new Date().toISOString()
});
log(`Added data with ID: ${id}`);
displayResults({ success: true, id, data });
} catch (error) {
log(`Add error: ${error.message}`, true);
displayResults({ error: error.message });
}
});
// Initial log
log('Ready to connect to a Brainy server');
</script>
</body>
</html>

View file

@ -0,0 +1,278 @@
// Browser-Server Search Example
// This example demonstrates how to use Brainy in a browser, call a server-hosted version for search,
// store the results locally, and then perform further searches against the local instance.
import {
BrainyData,
augmentationPipeline,
createConduitAugmentation,
NounType
} from '@soulcraft/brainy';
/**
* BrainyServerSearch class
* Provides functionality to search a server-hosted Brainy instance and store results locally
*/
class BrainyServerSearch {
constructor(serverUrl) {
this.serverUrl = serverUrl;
this.localDb = null;
this.wsConduit = null;
this.connection = null;
this.isInitialized = false;
}
/**
* Initialize the local Brainy instance and connect to the server
*/
async init() {
if (this.isInitialized) {
return;
}
try {
// Initialize local Brainy instance
this.localDb = new BrainyData();
await this.localDb.init();
// Create a WebSocket conduit augmentation
this.wsConduit = await createConduitAugmentation('websocket', 'server-search-conduit');
// Register the augmentation with the pipeline
augmentationPipeline.register(this.wsConduit);
// Connect to the server
const connectionResult = await augmentationPipeline.executeConduitPipeline(
'establishConnection',
[this.serverUrl, { protocols: 'brainy-sync' }]
);
if (connectionResult[0] && (await connectionResult[0]).success) {
this.connection = (await connectionResult[0]).data;
console.log('Connected to server:', this.serverUrl);
this.isInitialized = true;
} else {
throw new Error('Failed to connect to server');
}
} catch (error) {
console.error('Failed to initialize BrainyServerSearch:', error);
throw error;
}
}
/**
* Search the server-hosted Brainy instance, store results locally, and return them
* @param {string} query - The search query
* @param {number} limit - Maximum number of results to return
* @returns {Promise<Array>} - Search results
*/
async searchServer(query, limit = 10) {
await this.ensureInitialized();
try {
// Create a search request
const readResult = await augmentationPipeline.executeConduitPipeline(
'readData',
[{
connectionId: this.connection.connectionId,
query: {
type: 'search',
query: query,
limit: limit
}
}]
);
if (readResult[0] && (await readResult[0]).success) {
const searchResults = (await readResult[0]).data;
// Store the results in the local Brainy instance
for (const result of searchResults) {
// Check if the noun already exists in the local database
const existingNoun = await this.localDb.get(result.id);
if (!existingNoun) {
// Add the noun to the local database
await this.localDb.add(result.vector, result.metadata);
}
}
return searchResults;
} else {
const error = readResult[0] ? (await readResult[0]).error : 'Unknown error';
throw new Error(`Failed to search server: ${error}`);
}
} catch (error) {
console.error('Error searching server:', error);
throw error;
}
}
/**
* Search the local Brainy instance
* @param {string} query - The search query
* @param {number} limit - Maximum number of results to return
* @returns {Promise<Array>} - Search results
*/
async searchLocal(query, limit = 10) {
await this.ensureInitialized();
try {
return await this.localDb.searchText(query, limit);
} catch (error) {
console.error('Error searching local database:', error);
throw error;
}
}
/**
* Search both server and local instances, combine results, and store server results locally
* @param {string} query - The search query
* @param {number} limit - Maximum number of results to return
* @returns {Promise<Array>} - Combined search results
*/
async searchCombined(query, limit = 10) {
await this.ensureInitialized();
try {
// Search local first
const localResults = await this.searchLocal(query, limit);
// If we have enough local results, return them
if (localResults.length >= limit) {
return localResults;
}
// Otherwise, search server for additional results
const serverResults = await this.searchServer(query, limit - localResults.length);
// Combine results, removing duplicates
const combinedResults = [...localResults];
const localIds = new Set(localResults.map(r => r.id));
for (const result of serverResults) {
if (!localIds.has(result.id)) {
combinedResults.push(result);
}
}
return combinedResults;
} catch (error) {
console.error('Error performing combined search:', error);
throw error;
}
}
/**
* Add data to both local and server instances
* @param {string|Array} data - Text or vector to add
* @param {Object} metadata - Metadata for the data
* @returns {Promise<string>} - ID of the added data
*/
async add(data, metadata = {}) {
await this.ensureInitialized();
try {
// Add to local first
const id = await this.localDb.add(data, metadata);
// Get the vector and metadata
const noun = await this.localDb.get(id);
// Add to server
await augmentationPipeline.executeConduitPipeline(
'writeData',
[{
connectionId: this.connection.connectionId,
data: {
type: 'addNoun',
vector: noun.vector,
metadata: noun.metadata
}
}]
);
return id;
} catch (error) {
console.error('Error adding data:', error);
throw error;
}
}
/**
* Ensure the instance is initialized
*/
async ensureInitialized() {
if (!this.isInitialized) {
await this.init();
}
}
/**
* Close the connection to the server
*/
async close() {
if (this.connection) {
try {
await this.wsConduit.closeWebSocket(this.connection.connectionId);
this.connection = null;
} catch (error) {
console.error('Error closing connection:', error);
}
}
this.isInitialized = false;
}
}
// Example usage
async function runExample() {
// Create a BrainyServerSearch instance
const brainySearch = new BrainyServerSearch('wss://your-brainy-server.com/ws');
try {
// Initialize
await brainySearch.init();
// Search the server and store results locally
console.log('Searching server for "machine learning"...');
const serverResults = await brainySearch.searchServer('machine learning', 5);
console.log('Server results:', serverResults);
// Now search locally - this should return the results we just stored
console.log('Searching local database for "machine learning"...');
const localResults = await brainySearch.searchLocal('machine learning', 5);
console.log('Local results:', localResults);
// Search for something related but different
console.log('Searching local database for "artificial intelligence"...');
const aiResults = await brainySearch.searchLocal('artificial intelligence', 5);
console.log('AI results:', aiResults);
// Perform a combined search
console.log('Performing combined search for "neural networks"...');
const combinedResults = await brainySearch.searchCombined('neural networks', 5);
console.log('Combined results:', combinedResults);
// Add new data to both local and server
console.log('Adding new data...');
const id = await brainySearch.add('Deep learning is a subset of machine learning', {
noun: NounType.Concept,
category: 'AI',
tags: ['deep learning', 'neural networks']
});
console.log('Added data with ID:', id);
// Close the connection
await brainySearch.close();
} catch (error) {
console.error('Example failed:', error);
}
}
// In a browser environment, you would call this when the page loads
// runExample();
// Export for use in other modules
export { BrainyServerSearch };