perf: make getRelations() pagination consistent and efficient
**Problem**: Pagination behavior was inconsistent across different query patterns:
- getRelations({ from: id, limit: 10 }) fetched ALL relationships then sliced
- getRelations({ limit: 10 }) paginated at storage layer
- storage.getVerbs() offset parameter wasn't being passed to adapters
**Root Cause**:
1. getRelations() used different code paths for from/to vs no-filter queries
2. storage.getVerbs() called getVerbsWithPagination without offset
3. Then tried to slice results, which failed for paginated queries
**Solution**:
- Unified getRelations() to ALWAYS use storage.getVerbs() with pagination
- Fixed storage.getVerbs() to convert offset to cursor for adapters
- All query patterns now paginate efficiently at storage layer
- Eliminated inefficient "fetch all then slice" pattern
**Performance Impact**:
- Before: getRelations({ from: entityId, limit: 10 }) on entity with 1000 relationships = 1000 fetched
- After: Only 10 fetched ✅
- All tests passing (14/14)
**Breaking**: None - fully backward compatible
This commit is contained in:
parent
8d217f3b84
commit
54d819cfcf
3 changed files with 39 additions and 46 deletions
|
|
@ -11,6 +11,9 @@ All notable changes to this project will be documented in this file. See [standa
|
|||
- Fixed critical bug where `brain.getRelations()` returned `[]` instead of all relationships
|
||||
- Added support for retrieving all relationships with pagination (default limit: 100)
|
||||
- Added string ID shorthand syntax: `brain.getRelations(entityId)` as alias for `brain.getRelations({ from: entityId })`
|
||||
- **Performance**: Made pagination consistent - now ALL query patterns paginate at storage layer
|
||||
- **Efficiency**: `getRelations({ from: id, limit: 10 })` now fetches only 10 instead of fetching ALL then slicing
|
||||
- Fixed storage.getVerbs() offset handling - now properly converts offset to cursor for adapters
|
||||
- Production safety: Warns when fetching >10k relationships without filters
|
||||
- Fixed broken method calls in improvedNeuralAPI.ts (replaced non-existent `getVerbsForNoun` with `getRelations`)
|
||||
- Fixed property access bugs: `verb.target` → `verb.to`, `verb.verb` → `verb.type`
|
||||
|
|
|
|||
|
|
@ -911,58 +911,45 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
const limit = params.limit || 100
|
||||
const offset = params.offset || 0
|
||||
|
||||
let relations: Relation<T>[] = []
|
||||
// Production safety: warn for large unfiltered queries
|
||||
if (!params.from && !params.to && !params.type && limit > 10000) {
|
||||
console.warn(
|
||||
`[Brainy] getRelations(): Fetching ${limit} relationships without filters. ` +
|
||||
`Consider adding 'from', 'to', or 'type' filter for better performance.`
|
||||
)
|
||||
}
|
||||
|
||||
// Build filter for storage query
|
||||
const filter: any = {}
|
||||
|
||||
// Case 1: Filter by source
|
||||
if (params.from) {
|
||||
const verbs = await this.storage.getVerbsBySource(params.from)
|
||||
relations.push(...this.verbsToRelations(verbs as any))
|
||||
}
|
||||
// Case 2: Filter by target
|
||||
else if (params.to) {
|
||||
const verbs = await this.storage.getVerbsByTarget(params.to)
|
||||
relations.push(...this.verbsToRelations(verbs as any))
|
||||
}
|
||||
// Case 3: Get ALL relationships (NEW - fixes v4.1.2 bug)
|
||||
else {
|
||||
// Production safety: warn for large unfiltered queries
|
||||
if (!params.type && limit > 10000) {
|
||||
console.warn(
|
||||
`[Brainy] getRelations(): Fetching ${limit} relationships without filters. ` +
|
||||
`Consider adding 'type' filter or reducing 'limit' for better performance.`
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch from storage using pagination
|
||||
const result = await this.storage.getVerbs({
|
||||
pagination: {
|
||||
limit: limit + offset, // Fetch enough for offset + limit
|
||||
offset: 0,
|
||||
cursor: params.cursor
|
||||
},
|
||||
filter: params.type
|
||||
? { verbType: Array.isArray(params.type) ? params.type : [params.type] as any }
|
||||
: undefined
|
||||
})
|
||||
|
||||
relations = this.verbsToRelations(result.items as any)
|
||||
filter.sourceId = params.from
|
||||
}
|
||||
|
||||
// Filter by type (only if not already filtered at storage level)
|
||||
let filtered = relations
|
||||
if (params.type && (params.from || params.to)) {
|
||||
// Type filter only needed for from/to queries
|
||||
const types = Array.isArray(params.type) ? params.type : [params.type]
|
||||
filtered = relations.filter((r) => types.includes(r.type))
|
||||
if (params.to) {
|
||||
filter.targetId = params.to
|
||||
}
|
||||
|
||||
if (params.type) {
|
||||
filter.verbType = Array.isArray(params.type) ? params.type : [params.type]
|
||||
}
|
||||
|
||||
// Filter by service
|
||||
if (params.service) {
|
||||
filtered = filtered.filter((r) => r.service === params.service)
|
||||
filter.service = params.service
|
||||
}
|
||||
|
||||
// Apply pagination (for from/to queries, or trim excess from storage query)
|
||||
return filtered.slice(offset, offset + limit)
|
||||
// Fetch from storage with pagination at storage layer (efficient!)
|
||||
const result = await this.storage.getVerbs({
|
||||
pagination: {
|
||||
limit,
|
||||
offset,
|
||||
cursor: params.cursor
|
||||
},
|
||||
filter: Object.keys(filter).length > 0 ? filter : undefined
|
||||
})
|
||||
|
||||
// Convert to Relation format
|
||||
return this.verbsToRelations(result.items as any)
|
||||
}
|
||||
|
||||
// ============= SEARCH & DISCOVERY =============
|
||||
|
|
|
|||
|
|
@ -752,14 +752,17 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// Check if the adapter has a paginated method for getting verbs
|
||||
if (typeof (this as any).getVerbsWithPagination === 'function') {
|
||||
// Use the adapter's paginated method
|
||||
// Convert offset to cursor if no cursor provided (adapters use cursor for offset)
|
||||
const effectiveCursor = cursor || (offset > 0 ? offset.toString() : undefined)
|
||||
|
||||
const result = await (this as any).getVerbsWithPagination({
|
||||
limit,
|
||||
cursor,
|
||||
cursor: effectiveCursor,
|
||||
filter: options?.filter
|
||||
})
|
||||
|
||||
// Apply offset if needed (some adapters might not support offset)
|
||||
const items = result.items.slice(offset)
|
||||
// Items are already offset by the adapter via cursor, no need to slice
|
||||
const items = result.items
|
||||
|
||||
// CRITICAL SAFETY CHECK: Prevent infinite loops
|
||||
// If we have no items but hasMore is true, force hasMore to false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue