feat: add desktop document search and verified native CPU embeddings
Core CI / core (ubuntu-latest) (push) Failing after 1m22s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 3m46s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Failing after 1m22s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 3m46s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
This commit is contained in:
1 parent
f77112db97
commit
a9b6592330
46 files changed
+10794
-77
No files matched your search
@@ -7,6 +7,8 @@ GPU setup. Index only folders you explicitly choose.
|
||||
cargo run -p searchi-cli -- index --db ./searchi.db --root ./documents
|
||||
cargo run -p searchi-cli -- search --db ./searchi.db "termination clause" --limit 10
|
||||
cargo run -p searchi-cli -- search --db ./searchi.db "invoice" --json
|
||||
cargo run -p searchi-cli -- search --db ./searchi.db "invoice" --extension pdf --min-size 1024 --json
|
||||
cargo run -p searchi-cli -- index --db ./searchi.db --root ./documents --exclude private
|
||||
cargo run -p searchi-cli -- stats --db ./searchi.db --json
|
||||
```
|
||||
|
||||
@@ -23,6 +25,12 @@ snippet, ranking score, and match kind. An empty search emits `[]`. `stats --jso
|
||||
emits an object with `files` and `content_files`. JSON is the scripting interface;
|
||||
the human-readable output may change as preview support develops.
|
||||
|
||||
The foundation CLI searches filenames and extracted text. Semantic inference,
|
||||
document formats beyond the core extractor, and desktop integration remain
|
||||
tracked in the project roadmap; this interface does not fabricate embeddings.
|
||||
The CLI searches filenames, bounded text, PDF text and DOCX paragraphs. Results
|
||||
include actual matching source locations when available and extraction errors
|
||||
for documents that could not be read. Semantic inference and media formats
|
||||
remain tracked in the roadmap; this interface does not fabricate embeddings.
|
||||
|
||||
Filters apply before result limits: `--folder`, `--extension`, `--min-size`,
|
||||
`--max-size`, `--modified-after-ms`, and `--modified-before-ms`. Dates are Unix
|
||||
milliseconds and inclusive. Repeated `index --exclude NAME` flags add directory
|
||||
names to the defaults (`.git`, `node_modules`, `target`).
|
||||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use searchi_core::SearchIndex;
|
||||
use searchi_core::{IndexOptions, SearchIndex, SearchOptions};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "searchi", version, about = "Search your local files offline")]
|
||||
@@ -23,6 +23,9 @@ enum Command {
|
||||
/// Folder to index recursively.
|
||||
#[arg(long)]
|
||||
root: PathBuf,
|
||||
/// Additional directory names to exclude; repeat this flag as needed.
|
||||
#[arg(long)]
|
||||
exclude: Vec<String>,
|
||||
},
|
||||
/// Search filenames and indexed document content.
|
||||
Search {
|
||||
@@ -36,6 +39,21 @@ enum Command {
|
||||
/// Emit a JSON array for scripting.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
/// Restrict results to this folder and descendants.
|
||||
#[arg(long)]
|
||||
folder: Option<PathBuf>,
|
||||
/// File extension, with or without the leading dot.
|
||||
#[arg(long)]
|
||||
extension: Option<String>,
|
||||
#[arg(long)]
|
||||
min_size: Option<u64>,
|
||||
#[arg(long)]
|
||||
max_size: Option<u64>,
|
||||
/// Inclusive modification time in Unix milliseconds.
|
||||
#[arg(long)]
|
||||
modified_after_ms: Option<i64>,
|
||||
#[arg(long)]
|
||||
modified_before_ms: Option<i64>,
|
||||
},
|
||||
/// Show the indexed file and content counts.
|
||||
Stats {
|
||||
@@ -48,15 +66,17 @@ enum Command {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
match Cli::parse().command {
|
||||
Command::Index { db, root } => {
|
||||
Command::Index { db, root, exclude } => {
|
||||
let mut options = IndexOptions::default();
|
||||
options.excluded_directory_names.extend(exclude);
|
||||
let mut index = SearchIndex::open(&db)
|
||||
.with_context(|| format!("Cannot open index {}", db.display()))?;
|
||||
let report = index
|
||||
.index_folder(&root)
|
||||
.index_folder_with_options(&root, &options)
|
||||
.with_context(|| format!("Cannot index folder {}", root.display()))?;
|
||||
println!(
|
||||
"Discovered: {} Indexed: {} Removed: {} Skipped: {}",
|
||||
report.discovered, report.indexed, report.removed, report.skipped
|
||||
"Discovered: {} Indexed: {} Unchanged: {} Removed: {} Skipped: {} Extraction failures: {}",
|
||||
report.discovered, report.indexed, report.unchanged, report.removed, report.skipped, report.failures
|
||||
);
|
||||
}
|
||||
Command::Search {
|
||||
@@ -64,6 +84,12 @@ fn main() -> Result<()> {
|
||||
query,
|
||||
limit,
|
||||
json,
|
||||
folder,
|
||||
extension,
|
||||
min_size,
|
||||
max_size,
|
||||
modified_after_ms,
|
||||
modified_before_ms,
|
||||
} => {
|
||||
if query.trim().is_empty() {
|
||||
bail!("Search text cannot be empty");
|
||||
@@ -72,7 +98,18 @@ fn main() -> Result<()> {
|
||||
let index = SearchIndex::open(&db)
|
||||
.with_context(|| format!("Cannot open index {}", db.display()))?;
|
||||
let results = index
|
||||
.search(&query, usize::from(limit))
|
||||
.search_with_options(
|
||||
&query,
|
||||
usize::from(limit),
|
||||
&SearchOptions {
|
||||
folder,
|
||||
extension,
|
||||
min_size,
|
||||
max_size,
|
||||
modified_after_unix_ms: modified_after_ms,
|
||||
modified_before_unix_ms: modified_before_ms,
|
||||
},
|
||||
)
|
||||
.context("Search failed")?;
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&results)?);
|
||||
@@ -81,6 +118,12 @@ fn main() -> Result<()> {
|
||||
} else {
|
||||
for result in results {
|
||||
println!("{} [{}]", result.path, result.match_kind);
|
||||
if let Some(source) = result.source {
|
||||
println!(" {} {}", source.kind, source.number);
|
||||
}
|
||||
if let Some(error) = result.extraction_error {
|
||||
println!(" Text extraction unavailable: {error}");
|
||||
}
|
||||
if !result.snippet.is_empty() {
|
||||
println!(" {}", result.snippet.replace(['\r', '\n'], " "));
|
||||
}
|
||||
|
||||
@@ -88,3 +88,42 @@ fn errors_are_nonzero_and_missing_database_is_not_created() {
|
||||
assert!(!bad_root.status.success());
|
||||
assert!(String::from_utf8_lossy(&bad_root.stderr).contains("missing-folder"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclusions_and_filters_change_real_results() {
|
||||
let fixture = Fixture::new();
|
||||
fs::create_dir_all(fixture.0.join("documents/private")).unwrap();
|
||||
fs::write(fixture.0.join("documents/report.txt"), "quarterly invoice").unwrap();
|
||||
fs::write(fixture.0.join("documents/report.md"), "quarterly invoice").unwrap();
|
||||
fs::write(fixture.0.join("documents/private/secret.txt"), "invoice").unwrap();
|
||||
let indexed = fixture.run(&[
|
||||
"index",
|
||||
"--db",
|
||||
"index.db",
|
||||
"--root",
|
||||
"documents",
|
||||
"--exclude",
|
||||
"private",
|
||||
]);
|
||||
assert!(indexed.status.success());
|
||||
let searched = fixture.run(&[
|
||||
"search",
|
||||
"--db",
|
||||
"index.db",
|
||||
"invoice",
|
||||
"--extension",
|
||||
"md",
|
||||
"--json",
|
||||
]);
|
||||
assert!(
|
||||
searched.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&searched.stderr)
|
||||
);
|
||||
let results: serde_json::Value = serde_json::from_slice(&searched.stdout).unwrap();
|
||||
assert_eq!(results.as_array().unwrap().len(), 1);
|
||||
assert_eq!(results[0]["filename"], "report.md");
|
||||
let stats = fixture.run(&["stats", "--db", "index.db", "--json"]);
|
||||
let stats: serde_json::Value = serde_json::from_slice(&stats.stdout).unwrap();
|
||||
assert_eq!(stats["files"], 2);
|
||||
}
|
||||
Reference in new issue
Block a user