Module: Pikuri::Os::LocalSearch

Defined in:
lib/pikuri/os/local_search.rb

Overview

Backend wrapper over GNOME's localsearch CLI (the desktop file index, 2024 rename of tracker3 / tracker-miners). Not a Tool — the shared, stateless seam behind fileindex_search / fileindex_read, so localsearch-specific knowledge (argv, parsing, json-ld extraction, binary probe) lives in one place.

localsearch search returns at most one match snippet per file from a possibly-stale index. LocalSearch.search returns exactly those one-per-file hits and the tools tell the LLM to read the file for the rest — enumerating every per-file hit would need a stemmer (the index is stemmed, "mouse" matches "mice"), deliberately out of scope.

PDFs and office docs come for free: the index extracted their text at index time, so LocalSearch.read_text returns it via info -c with no extractor dependency here.

Defined Under Namespace

Classes: CommandError

Constant Summary collapse

BINARY =

Returns the localsearch CLI.

Returns:

  • (String)

    the localsearch CLI.

'localsearch'
LIMITATIONS =

Plain-English note of this backend's structural blind spots, appended to the fileindex tools' LLM description so the model knows when to fall back to a filesystem text/regex search. Lives on the backend (a different index ships its own). All three are confirmed against GNOME localsearch (measured; see pikuri-os/DESIGN.md).

Returns:

  • (String)
<<~LIMITS.chomp
  Not covered by this index (fall back to a text/regex search over the files for these):
  - Source code and other developer files are not content-indexed — only their names are. Search their text directly.
  - Nothing inside a git checkout is indexed (any folder containing a .git). Search those files directly.
  - No folder scoping: it ranks across your whole home at once, so a common word confined to one project may stay buried below the results shown. Prefer distinctive words, or search that folder's text directly.
LIMITS
ANSI_SGR =

Returns one ANSI SGR escape (color/bold), stripped from snippets before they reach the LLM.

Returns:

  • (Regexp)

    one ANSI SGR escape (color/bold), stripped from snippets before they reach the LLM.

/\e\[[0-9;]*m/

Class Method Summary collapse

Class Method Details

.available?Boolean

Non-raising presence probe — lets a caller wire the file-index tools only when localsearch is installed and degrade gracefully otherwise, rather than failing the whole agent.

Returns:

  • (Boolean)


148
149
150
151
152
# File 'lib/pikuri/os/local_search.rb', line 148

def self.available?
  Pikuri::Subprocess.spawn(BINARY, '--version', chdir: '/').wait.status.success?
rescue Errno::ENOENT
  false
end

.check_binaries!Object

Verify localsearch is reachable; raise loudly otherwise.

Raises:

  • (RuntimeError)

    if the binary is missing or unusable



134
135
136
137
138
139
140
141
# File 'lib/pikuri/os/local_search.rb', line 134

def self.check_binaries!
  result = Pikuri::Subprocess.spawn(BINARY, '--version', chdir: '/').wait
  return if result.status.success?

  raise install_hint
rescue Errno::ENOENT
  raise install_hint
end

.extract_plain_text(raw) ⇒ String?

Find nie:plainTextContent anywhere in the json-ld document (it nests under @graph, a named graph). Lenient: if the raw bytes don't parse as JSON (a stray diagnostic prefix), retry from the first {.

Parameters:

  • raw (String)

Returns:

  • (String, nil)


126
127
128
129
# File 'lib/pikuri/os/local_search.rb', line 126

def self.extract_plain_text(raw)
  data = parse_json_lenient(raw)
  data && find_plain_text(data)
end

.labelString

Returns short human name for this backend, used by the fileindex tools when prefixing an "Error: ..." observation.

Returns:

  • (String)

    short human name for this backend, used by the fileindex tools when prefixing an "Error: ..." observation.



29
# File 'lib/pikuri/os/local_search.rb', line 29

def self.label = 'localsearch'

.limitationsString

Uniform accessor mirroring Recoll.limitations. A constant here (unlike recoll's runtime method) because this backend's blind spots are the same on every host.

Returns:

  • (String)


36
# File 'lib/pikuri/os/local_search.rb', line 36

def self.limitations = LIMITATIONS

.parse_search_output(raw) ⇒ Array<Hash>

Parse localsearch search -d output into hits. Each result is a file://<uri> (urn:…) line followed by an indented snippet line; the path is percent-decoded and the snippet ANSI-stripped.

Parameters:

  • raw (String)

Returns:

  • (Array<Hash>)

    [{ path:, snippet: }, …] de-duped by path



88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/pikuri/os/local_search.rb', line 88

def self.parse_search_output(raw)
  hits = []
  current = nil
  raw.each_line do |line|
    encoded = line[%r{\Afile://(\S+)}, 1]
    if encoded
      current = { path: decode_file_uri(encoded), snippet: nil }
      hits << current
    elsif current && current[:snippet].nil? && !line.strip.empty?
      current[:snippet] = line.gsub(ANSI_SGR, '').strip
    end
  end
  hits.uniq { |hit| hit[:path] }
end

.read_text(path) ⇒ String?

Full stored plain text of an indexed file, via +localsearch info -c -o json-ld +. Works for PDFs/office docs (extracted at index time). nil when the path isn't in the index or has no stored text — the caller turns that into an LLM-facing message.

Parameters:

  • path (String)

Returns:

  • (String, nil)


110
111
112
113
114
115
116
117
# File 'lib/pikuri/os/local_search.rb', line 110

def self.read_text(path)
  result = Pikuri::Subprocess.spawn(BINARY, 'info', '-c', '-o', 'json-ld', path,
                                    chdir: '/').wait
  return nil unless result.status.success?

  text = extract_plain_text(result.output)
  text && !text.strip.empty? ? text : nil
end

.search(query:, limit:) ⇒ Array<Hash>

Run localsearch search -d --limit <limit> <query> and parse the results. -d adds the (ignored) URN and, on the line after each path, a short snippet with the matched word highlighted; we strip the ANSI and keep the text.

Parameters:

  • query (String)
  • limit (Integer)

Returns:

  • (Array<Hash>)

    [{ path: String, snippet: String|nil }, …], de-duplicated by path

Raises:



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/pikuri/os/local_search.rb', line 70

def self.search(query:, limit:)
  result = Pikuri::Subprocess.spawn(BINARY, 'search', '-d', '--limit', limit.to_s, query,
                                    chdir: '/').wait
  unless result.status.success?
    stderr = result.output.strip
    stderr = "exited #{result.status.exitstatus}" if stderr.empty?
    raise CommandError, stderr
  end

  parse_search_output(result.output)
end