feat: queryAggregate() + HAVING, plus aggregate backfill, traversal depth/via, extraction typing (BR-ADV-FEATURES-BUN)

New report APIs:
- brain.queryAggregate(name, params) returns the clean AggregateResult[] shape
  ({ groupKey, metrics, count }) directly, instead of the search-Result wrapper that
  find({ aggregate }) returns.
- HAVING: find({ aggregate, having }) and queryAggregate filter groups by computed
  metric values (e.g. revenue > 1000), complementing where (which filters group keys).
  Evaluated per group: O(groups), independent of entity count.

Fixes (all reproducible on Node; surfaced under Cortex+Bun by Memory):
- Aggregate backfill-on-define: defining an aggregate over a store that already holds
  matching entities returned []. It now backfills from existing entities on first query
  (storage-agnostic via getNouns), so it works under durable backends that reopen
  pre-populated. groupBy:['noun'] resolves to the entity type; find({ aggregate }) rows
  expose groupKey/metrics/count at the top level.
- Multi-hop find({ connected: { depth, via } }) honors depth and via at every hop
  (was 1-hop only, with verb filtering applied to hop 1 only); BFS bounded by limit.
- extractEntities no longer bleeds a neighbour's type indicator across candidates; each
  candidate is typed by its own span. Also fixes the "Dr." title pattern.

Adds real-embedding regression tests; full unit suite green.
This commit is contained in:
David Snelling 2026-05-26 13:55:43 -07:00
parent 513186d951
commit 1a98e4276a
9 changed files with 481 additions and 96 deletions

View file

@ -30,12 +30,11 @@ const brain = new Brainy()
await brain.init()
// Extract all entities
const entities = await brain.extractEntities('John Smith founded Acme Corp in New York')
// Returns:
const entities = await brain.extractEntities('Sarah Chen founded Acme Corp')
// Returns (fast pattern + embedding ensemble; confidences are approximate):
// [
// { text: 'John Smith', type: NounType.Person, confidence: 0.95 },
// { text: 'Acme Corp', type: NounType.Organization, confidence: 0.92 },
// { text: 'New York', type: NounType.Location, confidence: 0.88 }
// { text: 'Sarah Chen', type: NounType.Person, confidence: 0.68 },
// { text: 'Acme Corp', type: NounType.Organization, confidence: 0.85 }
// ]
// Extract with filters
@ -46,6 +45,17 @@ const people = await brain.extractEntities('...', {
})
```
> **What this is (and isn't).** `extractEntities` is a fast, dependency-free **heuristic
> ensemble** (regex/pattern + type-embedding similarity + context), not a trained NER model.
> Each candidate is typed by its own span — a type indicator in a neighbour's text (e.g. "Corp"
> in "Sarah Chen founded Acme Corp") will **not** bleed onto another candidate.
>
> **Known limitation:** a bare proper-noun place name with no structural cue (e.g. "New York",
> no comma, state code, or "in"/"at" preposition handling) can be typed as `Person` by the
> generic full-name pattern. For high-precision typing, pass `types` to constrain results, supply
> richer context, or post-validate. For state-of-the-art NER, drive extraction from an LLM at the
> application layer and store the results in Brainy.
### Method 2: Direct Import (Advanced)
```typescript