Documentation
¶
Overview ¶
Package index defines the consumer-side chunker contract: the bridge between the SDK's per-space change feed and the search indexer (internal/indexer). Chunkers turn one object's records into a stream of IndexEntry values ordered by ApplySeq — the SDK's per-space, peer-local, monotonic apply counter (advances on every apply that mutates a record, including non-DAG ones). Record deletions are first-class: a deleted record yields a removal entry (Data == ""); structural removals (object deletion, type detach) are derived by the indexer from the shared objects row and applied as primary-key prefix deletes. See docs/13-index.md for the full contract.
Index ¶
- Constants
- func IsDeleted(rec *anyenc.Value) bool
- func LinkMode(xf map[string]any) string
- func Members(row *anyenc.Value) map[string]bool
- func RecordsSince(ctx context.Context, q space.Query, since uint64, ...) error
- func ValidLinkMode(s string) bool
- func ValidScope(s string) bool
- type CatalogInvalidator
- type Chunker
- type DynamicChunker
- type IndexEntry
- type LinkEntry
- type ModuleChunker
- func (c *ModuleChunker) ChunksSince(ctx context.Context, sp space.Space, objectId string, since uint64, ...) error
- func (c *ModuleChunker) Dataset() string
- func (c *ModuleChunker) EvictDatasets(ctx context.Context, sp space.Space, attached map[string]bool) ([]string, error)
- func (c *ModuleChunker) Module() string
- func (c *ModuleChunker) ReconcileAll(ctx context.Context, sp space.Space, objectId string, since uint64) (map[string][]IndexEntry, error)
- func (c *ModuleChunker) Reconciles() bool
- func (c *ModuleChunker) TypeId() string
- type ModuleReconcile
- type ModuleStream
- type MultiReconciler
- type PropChunker
- type Reconciler
- type Registry
- type SchemaChunker
- func (c *SchemaChunker) ChunksSince(ctx context.Context, sp space.Space, objectId string, since uint64, ...) error
- func (c *SchemaChunker) Dataset() string
- func (c *SchemaChunker) EvictDatasets(ctx context.Context, sp space.Space, attached map[string]bool) ([]string, error)
- func (c *SchemaChunker) EvictText(_ context.Context, sp space.Space, attached map[string]bool) []string
- func (c *SchemaChunker) TypeId() string
- type TextEvictor
- type WholeCollectionLinks
Constants ¶
const ( ScopeBasic = "basic" // editor blocks, object names/descriptions ScopeChat = "chat" // chat messages // ScopeProps is the default scope for user property values (see // PropChunker). FTS-only: the indexer never embeds props-scope docs — // short "Name: value" entries embed badly and would pollute vector // recall. A meta.index override into another scope opts back in. ScopeProps = "props" )
Conventional scope slugs. Scopes are an OPEN set — any slug passing ValidScope works (property meta flags carry arbitrary scopes); these constants are just the established vocabulary.
const ( // LinkKindMention — an identity referenced in text (`m` URI). LinkKindMention = "mention" // LinkKindLink — an ordinary reference in text: an object, one of // its records or property values, a file. LinkKindLink = "link" // LinkKindRelation — a value of a link-bearing property or dataset // field (the `relation` slug, or an explicit `links` marker on a // value field). LinkKindRelation = "relation" // LinkKindCard — an editor block that is exactly one link to an // object or file, rendered as a card (the whole-line rule the web // client promotes on). LinkKindCard = "card" // LinkKindEmbed — an editor block that embeds another object's // content: the synced-block envelope referencing a source block. LinkKindEmbed = "embed" )
Link kinds. An open slug set the extractors mint; these are the established vocabulary.
const ( XFormatLinksKey = "links" LinkModeOne = "link" LinkModeMany = "links" LinkModeMarkdown = "markdown" LinkModeNone = "none" )
Link markers on a descriptor: the explicit `links` key, or the slug that implies one. What the marker means for the value:
link the string value is one reference links the array value lists references markdown the text is scanned for references none the value is never scanned, whatever the slug implies
const ( NamePropRecordId = "name" DescriptionPropRecordId = "description" )
Reserved RecordIds for the always-indexed built-in `any` properties. Both are valid base58, so a collision with a real hash-derived propId is theoretically possible — and harmless: it would merge two text sources under one doc id.
const ApplySeqField = "_applySeq"
ApplySeqField is the reserved record field carrying the per-space applySeq watermark. Mirrors the SDK's crdt.ApplySeqField — duplicated here so the index package doesn't depend on an SDK internal. It is the record-level twin of space.ObjectChange.ApplySeq, so the chunker window and the change-index cursor share one ordering axis.
const DatasetProp = "prop"
DatasetProp is the VIRTUAL dataset name the prop chunker writes under (the middle segment of its doc ids). No SDK handler registers a dataset called "prop", so it can't collide with real dataset chunks.
const DatasetSchemaVirtual = "schema"
DatasetSchemaVirtual is the SchemaChunker's VIRTUAL Dataset() name. Never used in doc ids — every entry carries its real runtime dataset name — but it must stay unique among chunkers, and (like DatasetProp) reserved against user dataset names so runtime doc ids can't collide with a chunker's namespace.
const MetaCollectionLabel = "__collection__"
MetaCollectionLabel is the `any.type` value of a collection object.
const MetaIndexKey = "index"
MetaIndexKey is the property-definition meta key controlling value indexing. Three states: absent/empty → indexed under the default ScopeProps; a scope slug (e.g. "agent") → indexed under that scope; MetaIndexNone → excluded (opt-out for blobs and noisy enums).
const MetaIndexNone = "none"
MetaIndexNone is the MetaIndexKey value that excludes a property from indexing. The literal word — an empty string means "default".
const MetaTypeLabel = "__type__"
MetaTypeLabel is the SDK's reserved meta-type marker: type-definition objects carry `any.type = "__type__"` and their `any.name` is the type name. The SDK keeps the literal internal, but it is wire-visible on every objects-query row, so it is restated here. Type definitions are schema, not knowledge (discovery is `GET /types`) — the prop chunker always excludes them: their one-word names otherwise win BM25 on field-length normalization and surface as top search hits.
Variables ¶
This section is empty.
Functions ¶
func IsDeleted ¶
IsDeleted reports whether rec is a tombstone — the SDK wipes content and sets _deletedAt on delete, preserving _ver / _traces / _applySeq.
func LinkMode ¶
LinkMode resolves a descriptor's link marker: the explicit `links` key wins whatever it holds (`none`, an unknown or a malformed value disable — the gate refuses the latter two on write, so they only arrive from a vendor or a bypass), else the `relation` slug implies `links` and the `markdown` slug implies `markdown`. Empty when the field carries no links.
func Members ¶
Members reads an objects row's membership into a set: its one type (`any.type`), its collections (`any.collections`) and — a definition object implements itself — its own id when the row carries a marker. Nil for a nil row.
func RecordsSince ¶
func RecordsSince(ctx context.Context, q space.Query, since uint64, yield func(rec *anyenc.Value, applySeq uint64) error) error
RecordsSince is the shared windowed streamer every chunker uses. It chains the deletion opt-in (Projection{IncludeDeleted: true}), the cursor window (Filter {"_applySeq": {"$gt": since}}), and ascending ApplySeq order onto the supplied query, then iterates and yields each record paired with its parsed ApplySeq.
Projection{IncludeDeleted: true} is what makes tombstones visible — without it the SDK's find path skips deleted rows, so deletions would never reach the indexer. Tombstones carry _applySeq (the SDK stamps it on delete too), so they land in the same window as content. The $gt window mirrors the SDK's own ChangedSince contract: rows never stamped with _applySeq (pre-applySeq writes) sort below any since >= 0 and are excluded — "index from the next change."
A yield error stops iteration and is returned after the iterator is closed.
func ValidLinkMode ¶
ValidLinkMode reports whether s is one of the marker values.
func ValidScope ¶
ValidScope reports whether s is an acceptable scope slug: 1..64 chars of [a-z0-9_-]. Scopes are an open set — property meta flags carry arbitrary slugs — so the search handler and the prop-chunker catalog share this sanity rule instead of a fixed list.
Types ¶
type CatalogInvalidator ¶
type CatalogInvalidator interface {
Invalidate(spaceId string)
}
CatalogInvalidator is an optional Chunker capability for chunkers holding a per-space catalog snapshot with a TTL (the prop chunker): the worker calls Invalidate when a type object changed in the feed, so a property added a moment ago is in the catalog when the values written right after it are extracted.
type Chunker ¶
type Chunker interface {
// Dataset is the dataset this chunker writes — the middle segment
// of the index doc id (objectId:dataset:recordId). May be virtual
// (see DatasetProp): it only has to be unique among chunkers,
// colon-free, and stable.
Dataset() string
// TypeId is the type gating this chunker: the indexer runs
// ChunksSince only while the object has the type (or is the type
// itself), and prefix-evicts objectId:<dataset>: when it does not
// — covering a retype. Empty = ungated, runs for every object.
TypeId() string
// ChunksSince streams every entry of objectId with ApplySeq > since,
// ascending by ApplySeq. Cleared/deleted records yield removal
// entries (Data == ""). yield is called once per entry; a yield
// error stops the stream and is returned.
ChunksSince(ctx context.Context, sp space.Space, objectId string, since uint64, yield func(IndexEntry) error) error
}
Chunker streams the IndexEntry values for one dataset on one object.
type DynamicChunker ¶
type DynamicChunker interface {
Chunker
// EvictDatasets returns the runtime dataset names to structurally
// evict for an object with the given membership set (its type and
// its collections): catalog datasets whose owning type is not the
// object's (the retype path), plus names retired since process
// start (definition removed).
EvictDatasets(ctx context.Context, sp space.Space, attached map[string]bool) ([]string, error)
}
DynamicChunker is an optional Chunker capability for chunkers whose dataset set is defined at runtime (schema-driven datasets). The worker replaces the static Dataset()/TypeId() gate for such a chunker: it prefix-evicts objectId:<ds>: for every name returned by EvictDatasets, then runs ChunksSince as usual — the chunker self-gates its active set, so an evicted dataset is never also streamed in the same page.
type IndexEntry ¶
type IndexEntry struct {
Scope string
ObjectId string
Dataset string
RecordId string
Data string // text to index; empty = remove this record from the index
Title string // optional BM25F-boosted field (heading / signature / summary);
// its terms should also appear in Data — Title only adds ranking weight,
// and the content hash (embed-skip) is over Data alone.
ApplySeq uint64 // peer-local, per-space monotonic apply counter
// Links are the edges found in the record(s) this entry covers —
// the link sink's input (docs/13-index.md § Links). Each carries
// its own source place: a coalesced editor window reports the
// links of every member block under that block's id. A streaming
// chunker's entry replaces its record's edges (nil = the record
// links nothing); a reconciling chunker's set replaces the
// collection's edges. Not part of the content hash.
Links []LinkEntry
}
IndexEntry is one unit handed to the indexer: the text of a single record, tagged with enough identity to address it (Scope + ObjectId + Dataset + RecordId) and ordered by ApplySeq.
An empty Data is a record-level removal signal: "this record is gone (or has nothing to index), remove it." Removing a never-indexed record is a no-op on the indexer side, so removals are safe to emit unconditionally. Structural removals — a whole object (deletion) or one object's dataset (type detach) — are NOT expressed as entries: the indexer derives them from the shared objects row inside the same ChangedSince window and applies them as id-prefix deletes in the same page transaction.
type LinkEntry ¶
type LinkEntry struct {
ObjectId string
Dataset string
RecordId string
// TypeId names the type declaring a property value's source
// (DatasetProp edges — a value lives at record[typeId][propId]);
// empty for record sources.
TypeId string
// Field names the record field the reference was read from when
// the record has several link-bearing fields (runtime datasets);
// empty when the source has one text (blocks, messages) or is a
// property value.
Field string
Kind string
Target anyuri.URI // canonical (URI.Canonical), never a space
}
LinkEntry is one edge: the record it was found in (the source place — same identity vocabulary as IndexEntry, `DatasetProp` + propId for a property value) and the canonical target.
func TextLinks ¶
TextLinks scans markdown text for references and returns the edges for source (objectId, dataset, recordId) in spaceId: mentions as LinkKindMention, everything else as LinkKindLink. Space references, unknown kinds and a link to the source object itself (no record, no property — the parent is not a backlink) are dropped; duplicates merge on the canonical target.
func ValueLinks ¶
ValueLinks reads references off a value field by its marker mode: one string for LinkModeOne, an array of strings for LinkModeMany, scanned text for LinkModeMarkdown. Value links are LinkKindRelation; text links keep their text kinds. Malformed elements are skipped.
type ModuleChunker ¶
type ModuleChunker struct {
// contains filtered or unexported fields
}
ModuleChunker indexes every collection a dataset module serves in a space: the module's shared canonical collection and each namespaced `<typeId>_<key>` instance a type declares. One registered chunker per module, resolved per space from Space.Datasets (the `Module` of each discovered dataset) — the same in-memory snapshot the SDK refreshes synchronously when a declaration applies, so a read is never stale relative to the applySeq window being processed.
The gate is collection ownership: an object holds a collection when it carries one of the collection's Owners — the one declaring type of a namespaced instance, any of the types sharing a canonical one. The worker structurally evicts objectId:<collection>: for every collection the object cannot hold (DynamicChunker), plus collections that vanished from the catalog since process start (a part removed — the SchemaChunker's retired set, same restart caveat: docs/13-index.md § Removal semantics). Entries carry the real collection as their dataset, so doc ids stay per collection; Dataset() is the module's virtual name.
Exactly one of stream / reconcile is set. A reconciling module implements MultiReconciler: the worker diffs each collection's stored docs against the returned set, so an edit re-embeds only the windows that changed.
func NewModuleChunker ¶
func NewModuleChunker(module string, shape any) *ModuleChunker
NewModuleChunker constructs a chunker for module. Pass either a ModuleStream or a ModuleReconcile as the shape.
func (*ModuleChunker) ChunksSince ¶
func (c *ModuleChunker) ChunksSince(ctx context.Context, sp space.Space, objectId string, since uint64, yield func(IndexEntry) error) error
ChunksSince streams every active collection's entries (stream shape). A reconciling module answers through ReconcileAll instead; this path then renders its full sets, which the worker treats as a plain stream only when it does not implement MultiReconciler.
func (*ModuleChunker) Dataset ¶
func (c *ModuleChunker) Dataset() string
Dataset is the module's virtual name — unique among chunkers, never a doc-id segment (entries carry their collection).
func (*ModuleChunker) EvictDatasets ¶
func (c *ModuleChunker) EvictDatasets(ctx context.Context, sp space.Space, attached map[string]bool) ([]string, error)
EvictDatasets returns the collections to structurally evict for an object with the given membership: every collection of the module none of whose owners is attached, plus collections retired from the catalog.
func (*ModuleChunker) Module ¶
func (c *ModuleChunker) Module() string
Module returns the module slug this chunker indexes.
func (*ModuleChunker) ReconcileAll ¶
func (c *ModuleChunker) ReconcileAll(ctx context.Context, sp space.Space, objectId string, since uint64) (map[string][]IndexEntry, error)
ReconcileAll returns, per active collection, the full current entry set (reconcile shape). Only meaningful for reconciling modules; the worker checks MultiReconciler membership through Reconciles.
func (*ModuleChunker) Reconciles ¶
func (c *ModuleChunker) Reconciles() bool
Reconciles reports whether this chunker diffs full sets per collection (reconcile shape) rather than streaming records.
func (*ModuleChunker) TypeId ¶
func (c *ModuleChunker) TypeId() string
TypeId is empty: gating is per collection through EvictDatasets.
type ModuleReconcile ¶
type ModuleReconcile func(ctx context.Context, sp space.Space, objectId, collection string) ([]IndexEntry, error)
ModuleReconcile renders one collection's full current entry set — the coalesced shape (editor windows), diffed against the store by the indexer.
type ModuleStream ¶
type ModuleStream func(ctx context.Context, sp space.Space, objectId, collection string, since uint64, yield func(IndexEntry) error) error
ModuleStream streams one collection's entries past the cursor — the per-record shape (chat messages).
type MultiReconciler ¶
type MultiReconciler interface {
DynamicChunker
Reconciles() bool
ReconcileAll(ctx context.Context, sp space.Space, objectId string, since uint64) (map[string][]IndexEntry, error)
}
MultiReconciler is a DynamicChunker whose index unit spans multiple records on SEVERAL collections at once (a module's canonical collection plus namespaced instances). ReconcileAll returns the full current entry set per active collection; the indexer diffs each collection's stored docs (prefix objectId:<collection>:) against its set exactly as Reconcile does for one dataset. Reconciles reports whether the chunker takes this path at all — a streaming module chunker returns false and is served through ChunksSince.
type PropChunker ¶
type PropChunker struct {
// contains filtered or unexported fields
}
PropChunker indexes property VALUES from the shared `objects` collection: one entry per (object, indexed property), doc id objectId:prop:propId. User properties index BY DEFAULT under the dedicated scope ScopeProps with self-describing entry text ("<prop name>: <value>"); a property definition's meta["index"] = "<scope>" overrides the scope and meta["index"] = "none" opts out (see the SDK's PropertyDraft.Meta). The built-in any.name and any.description are always indexed under scope "basic", raw (no name prefix). Ungated: it runs for every object except type-definition rows (MetaTypeLabel) and the wired-in excludeTypes.
func NewPropChunker ¶
func NewPropChunker(excludeTypeIds ...string) *PropChunker
NewPropChunker constructs the chunker with an empty catalog cache. excludeTypeIds names types whose objects are skipped entirely (e.g. a diagnostic type — see PropChunker.excludeTypes); MetaTypeLabel is always excluded on top of them.
func (*PropChunker) ChunksSince ¶
func (c *PropChunker) ChunksSince(ctx context.Context, sp space.Space, objectId string, since uint64, yield func(IndexEntry) error) error
ChunksSince streams the object's property entries past the cursor. Per live row it emits, unconditionally:
- any.name / any.description under scope "basic";
- one entry per catalog property — value rendered when its type is attached, Data "" otherwise (idempotent record-level eviction of cleared values and detached-type props).
Tombstoned rows yield nothing — structural eviction is the indexer's job (it never calls chunkers for deleted objects).
func (*PropChunker) Dataset ¶
func (c *PropChunker) Dataset() string
func (*PropChunker) Invalidate ¶
func (c *PropChunker) Invalidate(spaceId string)
Invalidate drops the space's catalog snapshot (test/ops hook — the next ChunksSince re-resolves immediately instead of waiting out the TTL).
func (*PropChunker) LinksReplaceCollection ¶
func (c *PropChunker) LinksReplaceCollection()
LinksReplaceCollection marks the chunker's streams as complete per row (WholeCollectionLinks): a removed definition's edges are evicted on the row's next stream.
func (*PropChunker) TypeId ¶
func (c *PropChunker) TypeId() string
type Reconciler ¶
type Reconciler interface {
Chunker
// Reconcile returns every current index entry for objectId on this
// chunker's dataset. since is the indexer's cursor (the current
// implementation rebuilds the full set and ignores it; a future
// incremental implementation could use it to scope work).
Reconcile(ctx context.Context, sp space.Space, objectId string, since uint64) ([]IndexEntry, error)
}
Reconciler is an optional chunker capability for datasets whose index unit spans MULTIPLE records — e.g. coalesced editor windows, where one index doc concatenates several blocks. An incremental per-record stream (ChunksSince) can't express such a unit: a window's text needs sibling records below the cursor, and a deleted record's position is wiped from its tombstone, so the affected window can't be located incrementally.
Reconcile returns the object's FULL current doc set for the dataset. The indexer diffs it against what is already stored (by content hash): docs that vanished are deleted, new/changed docs are upserted, and unchanged docs are left untouched — so their vectors are preserved and not needlessly re-embedded. The indexer prefers Reconcile over ChunksSince when a chunker implements this interface.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the set of chunkers known to the process. Phase 1 has no consumer — it's constructed at boot and stored on server deps so the phase-2 indexer can look up "which chunkers feed this dataset" without re-wiring. Read-only after construction; safe for concurrent reads.
func NewRegistry ¶
NewRegistry builds a registry over the given chunkers, in order.
func (*Registry) ForDataset ¶
ForDataset returns the chunkers reading the given dataset. Usually one, but the contract allows several (e.g. multiple scopes over one dataset). Empty when no chunker covers the dataset — datasets excluded from indexing return nothing.
type SchemaChunker ¶
type SchemaChunker struct {
// contains filtered or unexported fields
}
SchemaChunker indexes records of RUNTIME-DEFINED datasets by their schema's x-search {title, text, scope} mapping: one registered chunker covers every dataset any type in the space declares with a search annotation. Entries carry the real dataset name (doc ids objectId:<dataset>:<recordId>) under the declared scope — absent defaults to "basic" (runtime dataset records are user content on par with editor blocks), an invalid slug makes the dataset unsearchable (a broken override must not silently land in the default scope — the resolveIndexedProp stance).
Unlike PropChunker there is NO catalog TTL cache: Space.Datasets() is an atomic in-memory snapshot the SDK refreshes synchronously when a definitions change applies, so a fresh read is never stale relative to the applySeq window being processed — and a cached one could skip records in the primary define-then-write flow while the cursor advances past them.
It implements DynamicChunker: per object, datasets whose owning type is not attached are structurally evicted by the worker (the DetachType path), and dataset names that vanish from the catalog (definition removed) are remembered for the process lifetime and evicted the same way as objects get dirty. Restart drops the retired set — stale docs of a removed definition survive until a boot-time sweep exists (docs/13-index.md § Removal semantics).
func NewSchemaChunker ¶
func NewSchemaChunker(staticDatasets ...string) *SchemaChunker
NewSchemaChunker constructs the chunker. staticDatasets are the compiled-in dataset names (indexed or not) to never treat as runtime; the virtual chunker names are always skipped on top of them.
func (*SchemaChunker) ChunksSince ¶
func (c *SchemaChunker) ChunksSince(ctx context.Context, sp space.Space, objectId string, since uint64, yield func(IndexEntry) error) error
ChunksSince streams the object's runtime-dataset entries past the cursor: per active dataset (owning type attached), every record with ApplySeq > since maps x-search.title → Title and title + the joined text fields → Data. Tombstones and records whose mapped fields render empty yield removal entries (Data == ""). Datasets the worker just evicted (type not attached) are never streamed in the same page.
func (*SchemaChunker) Dataset ¶
func (c *SchemaChunker) Dataset() string
func (*SchemaChunker) EvictDatasets ¶
func (c *SchemaChunker) EvictDatasets(ctx context.Context, sp space.Space, attached map[string]bool) ([]string, error)
EvictDatasets implements DynamicChunker: searchable catalog datasets whose owning type is not attached to this object, every unsearchable runtime name (x-search absent or cleared), and every retired name. The resolved catalog is stashed for the paired ChunksSince.
func (*SchemaChunker) EvictText ¶
func (c *SchemaChunker) EvictText(_ context.Context, sp space.Space, attached map[string]bool) []string
EvictText implements TextEvictor: link-only datasets (link fields, no usable search mapping — a cleared `x-search` included) keep their edges but must lose their text docs every page, the stance `unsearchable` takes for datasets with nothing to keep.
func (*SchemaChunker) TypeId ¶
func (c *SchemaChunker) TypeId() string
type TextEvictor ¶
type TextEvictor interface {
EvictText(ctx context.Context, sp space.Space, attached map[string]bool) []string
}
TextEvictor is an optional DynamicChunker capability for datasets whose TEXT docs must go while their edges stay: the worker prefix-evicts `objectId:<ds>:` on the text collection only for every name returned, and the chunker keeps streaming the dataset for its links (docs/13-index.md § Links). A dataset that lost its search mapping but keeps link fields is the case.
type WholeCollectionLinks ¶
type WholeCollectionLinks interface {
LinksReplaceCollection()
}
WholeCollectionLinks is an optional Chunker capability for streaming chunkers whose every stream carries the record's complete edge set for the whole collection — the prop chunker emits one entry per catalog property per row, so a property that left the catalog (definition removed) simply stops being emitted. The worker then replaces the collection's edges on every stream instead of only the streamed records', and the removed property's edges fall out.