Class: Insika::Knowledge::Index::Scan
- Inherits:
-
Object
- Object
- Insika::Knowledge::Index::Scan
- Defined in:
- lib/insika/knowledge.rb
Overview
Pure Ruby term-overlap search over one agent's concepts — no SQL, no
embeddings. Mirrors ToolCatalog#search's tokenizer/scoring shape
(case-insensitive substring match, name weighted over description),
extended per RFC §3.7 with a body tier and a confidence × recency
multiplier. Correct on every backend; at the scale that matters (a
few hundred concepts per agent) this is sub-millisecond.
Constant Summary collapse
- NAME_WEIGHT =
3- DESCRIPTION_WEIGHT =
2- BODY_WEIGHT =
1- RECENCY_HALF_LIFE_DAYS =
30.0
Instance Method Summary collapse
-
#initialize(store:) ⇒ Scan
constructor
A new instance of Scan.
-
#search(agent_id, query:, tenant: nil, top_k: 5) ⇒ Object
-> [description:, type:, confidence:, provenance:, sources:, occurrences:, body:, ...] sorted by score desc, ties by store enumeration order.
Constructor Details
#initialize(store:) ⇒ Scan
Returns a new instance of Scan.
530 531 532 533 534 535 536 537 538 539 540 541 542 543 |
# File 'lib/insika/knowledge.rb', line 530 def initialize(store:) @store = store # Read cache: parsing a concept's YAML frontmatter dominates search # cost (measured: ~90% of it, not the store I/O) — re-parsing it on # every search for a concept nothing wrote to since the last read # is pure waste. Keyed by (agent, tenant, name); a cached entry is # valid only while `updated_at` (the record's own timestamp, read # WITHOUT parsing — KnowledgeStore#meta) still matches, so a write # invalidates itself for free. One instance is meant to survive # across turns (the context provider holds it), fibers included: # a plain Hash is safe here the same way a closure-local counter is # elsewhere in the engine — MRI fibers do not preempt mid-statement. @cache = {} end |
Instance Method Details
#search(agent_id, query:, tenant: nil, top_k: 5) ⇒ Object
-> [description:, type:, confidence:, provenance:, sources:, occurrences:, body:, ...] sorted by score desc, ties by store enumeration order. Excludes zero-overlap concepts entirely.
548 549 550 551 552 553 554 555 556 557 558 559 560 561 |
# File 'lib/insika/knowledge.rb', line 548 def search(agent_id, query:, tenant: nil, top_k: 5) terms = tokenize(query) return [] if terms.empty? candidates = @store.names(agent_id, tenant: tenant).filter_map do |name| cached_concept(agent_id, name, tenant) end scored = candidates.each_with_index.filter_map do |concept, idx| score = score_of(concept, terms) [concept, score, idx] if score.positive? end scored.sort_by { |_concept, score, idx| [-score, idx] } .first(top_k).map(&:first) end |