Class: OKF::Bundle::Search

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/bundle/search.rb

Overview

Deterministic text retrieval over an in-memory bundle — the browser page's search brought server-side and extended to bodies. Terms are ANDed: every term must hit at least one searched field, though not necessarily the same one. A term is a case-insensitive substring, or a Ruby regular expression with regexp: true. Matches rank by where they hit (a title hit outranks a body hit) and carry one bounded context snippet, so answering "which concept covers X?" costs a row, not a body read.

Deliberately not fuzzy: the consuming agent is the fuzzy layer — synonyms and vocabulary drift are judgment over the index map, not string distance.

Pure — no disk, no stdio. The CLI's okf search and any embedding app share it: OKF::Bundle::Search.call(bundle, [ "dedup", "key" ]).

Constant Summary collapse

WEIGHTS =

The searchable fields with their rank weight, strongest signal first. A concept's score sums the weights of the fields that matched; hitting a field twice does not stack. Tags match against the space-joined list, mirroring the server page's haystack.

{
  "title" => 5,
  "id" => 4,
  "tags" => 3,
  "type" => 2,
  "description" => 2,
  "body" => 1
}.freeze
FIELDS =
WEIGHTS.keys.freeze
SNIPPET_FIELDS =

Fields whose match is only meaningful with surrounding context. The other fields already appear whole on the result row.

%w[description body].freeze
SNIPPET_RADIUS =

Characters of context kept on each side of the first matched term.

44

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bundle, terms, fields: nil, regexp: false) ⇒ Search

Raises RegexpError on an invalid pattern with regexp: true — the caller owns turning that into a usage error.



47
48
49
50
51
52
# File 'lib/okf/bundle/search.rb', line 47

def initialize(bundle, terms, fields: nil, regexp: false)
  @bundle = bundle
  raw = Array(terms).reject { |term| OKF.blank?(term) }
  @matchers = raw.map { |term| regexp ? Regexp.new(term.to_s, Regexp::IGNORECASE) : term.to_s.downcase }
  @fields = fields.nil? || fields.empty? ? FIELDS : fields
end

Class Method Details

.call(bundle, terms, fields: nil, regexp: false) ⇒ Object



41
42
43
# File 'lib/okf/bundle/search.rb', line 41

def self.call(bundle, terms, fields: nil, regexp: false)
  new(bundle, terms, fields: fields, regexp: regexp).results
end

Instance Method Details

#resultsObject

Ranked match rows, catalog-style identity plus where the terms hit: [{ id:, title:, type:, area:, tags:, matched: [field, …], score:, snippet: }, …] ordered by score descending, then id. No terms means no matches.



57
58
59
60
61
62
63
64
# File 'lib/okf/bundle/search.rb', line 57

def results
  return [] if @matchers.empty?

  @bundle.concepts
         .map { |concept| match(concept) }
         .compact
         .sort_by { |row| [ -row[:score], row[:id] ] }
end