zudo-text

検索したい単語を入力

いつでも検索バーを開ける

File Search and Similarity

This page covers two related search subsystems:

  • File Search — macOS-only Spotlight integration via mdfind. Streams file-system hits as events.

  • Similarity (Similar Docs) — BM25 index over the archives directory. Returns ranked similar documents for the current draft.

File Search (Spotlight)

Source: tauri-app/src/commands/file_search.rs

Spotlight search is macOS-only. On non-macOS targets the commands compile as stubs that return errors. Use file_search_is_supported to feature-detect at runtime. See also: Spotlight Picker.

Data Structures

FileSearchOpts

Options passed to file_search_start:

interface FileSearchOpts {
  mode: 'Any' | 'DirectoryOnly' | 'FileOnly';
  scope?: string;       // Absolute path to limit the search scope; null = global
  maxResults?: number;  // Default: 200; hard cap: 1000
}

FileSearchHit

Payload of each file-search:{id}:hit event:

interface FileSearchHit {
  path: string;   // Absolute path to the matched file or directory
  kind: 'file' | 'directory';
}

Commands

file_search_is_supported

Check whether Spotlight search is available on the current platform.

const supported = await invoke<boolean>('file_search_is_supported');

Parameters: none

Returns: true on macOS, false on all other platforms.

Behavior: Compile-time platform check — no filesystem access. Safe to call unconditionally.

file_search_start

Start a Spotlight search and receive results via events.

const searchId = await invoke<string>('file_search_start', {
  query: 'meeting notes',
  opts: { mode: 'FileOnly', scope: '/Users/me/Documents', maxResults: 100 },
});

Parameters:

NameTypeDescription
querystringSpotlight query string
optsFileSearchOptsSearch options

Returns: A UUID searchId string on success. Throws if Spotlight is unsupported or if the subprocess cannot be started.

Behavior:

  1. Spawns mdfind as a subprocess with the given query and scope.

  2. Streams each result line as a file-search:{searchId}:hit event.

  3. Emits file-search:{searchId}:complete when mdfind exits normally.

  4. Emits file-search:{searchId}:error with an error message if mdfind fails.

At most maxResults hits are emitted; additional results are silently dropped.

file_search_cancel

Cancel an in-progress Spotlight search.

await invoke('file_search_cancel', { searchId });

Parameters:

NameTypeDescription
search_idstringThe UUID returned by file_search_start

Returns: null on success. Throws if the search ID is not found.

Behavior: Kills the mdfind subprocess. No further events are emitted for the cancelled search.

Events

file-search:{searchId}:hit

Emitted for each Spotlight match. Subscribe using the searchId returned by file_search_start.

import { listen } from '@tauri-apps/api/event';

const unlisten = await listen<FileSearchHit>(`file-search:${searchId}:hit`, (event) => {
  console.log(event.payload.path, event.payload.kind);
});

file-search:{searchId}:complete

Emitted when the search finishes without error.

const unlisten = await listen(`file-search:${searchId}:complete`, () => {
  console.log('Search done');
});

file-search:{searchId}:error

Emitted when mdfind exits with an error.

const unlisten = await listen<{ message: string }>(`file-search:${searchId}:error`, (event) => {
  console.error('Search error:', event.payload.message);
});

Similarity Search (Similar Docs)

Source: packages/backend-bridge/src/similarity/ (engine) + packages/backend-bridge/src/workspace-core/similarity.ts (workspace corpus)

Similarity search has no Tauri commands and no REST routes. It is a TypeScript BM25 index over the in-memory workspace model, so the same engine serves desktop, web, and iOS. The Rust index and its four similar_docs_* commands were deleted in S21 (#4225): they scanned the workspace directory, which cloud-primary storage no longer fills (epic #4204 D9).

  • Corpus — the notes workspaceNotesList surfaces for the requested directories (numbered notes first, then named .md; internal files excluded). Frontmatter is stripped before tokenization; the caller still receives the complete file payload.

  • Scoring — BM25 with k1 = 1.5, b = 0.75, smoothed Robertson–Sparck Jones IDF, score > 0 only, ranked score descending with the canonical path as a deterministic tie break.

  • TokenizationIntl.Segmenter word segmentation (Japanese + English), with a character-bigram fallback where Intl.Segmenter is unavailable.

  • Invalidation — one cached index per canonical directory SET, dropped when subscribeWorkspaceFiles reports a change under one of its directories, and dropped wholesale when the workspace model is replaced (disarm / switch / seed).

Data Structures

SimilarDocResult

interface SimilarDocResult {
  path: string;           // Canonical Note Tray identity (e.g. "archives/20250101-hello.md")
  score: number;          // BM25 relevance score (higher = more similar)
  title: string;          // Frontmatter `title` → first H1 → filename stem
  snippet: string;        // ~150-character window around the best match
  matchedTerms: string[]; // Query terms that contributed to the match
  content: string;        // Complete note payload, frontmatter included
}

SimilarDocsQueryResponse

interface SimilarDocsQueryResponse {
  results: SimilarDocResult[];
  unfilteredResultCount: number; // positive-score matches before keyword + limit
  filteredResultCount: number;   // after keyword eligibility, before limit
}

Bridge Facade

bridge.fileSearch

// Check platform support
const ok = bridge.fileSearch.isSupported();   // boolean (sync)

// Start a search — returns searchId + event streams
const { searchId, onHit, onComplete, onError } = await bridge.fileSearch.start(query, opts);

// Cancel
await bridge.fileSearch.cancel(searchId);

bridge.similarDocs

isSupported() reports workspace readiness: an unarmed workspace has no corpus, and the Finder gates on it so that state reads as "feature off" rather than "no matches". getContent accepts any canonical Note Tray identity; writeContent stays archive-only.

// Query for similar documents
if (bridge.similarDocs.isSupported()) {
  const response = await bridge.similarDocs.query({
    content: draftContent,
    directories: ["inbox", "archives", "projects/client"],
    keyword: optionalKeyword,
    excludePaths: ["archives/current-note.md"],
    limit: 10,
  });
  // response.results contain canonical paths and complete note bodies.
  // response.unfilteredResultCount / filteredResultCount are pre-limit counts.
}

// Read an archive file
const content = await bridge.similarDocs.getContent(path);  // string | null

// Write an archive file
await bridge.similarDocs.writeContent(path, content);

// Force index rebuild
await bridge.similarDocs.rebuildIndex();