Windows Recall шпионит? Пора сделать свой поиск
Помните тот скандал, когда выяснилось, что Windows Recall тихонько скриншотит ваш экран и отправляет куда-то в облако? Даже если нет - суть ясна: доверять свои файлы кому попало - плохая идея. Особенно если эти файлы - ваши заметки, код, документы и прочий цифровой хлам, который иногда нужно найти не по имени, а по смыслу.
Вот и я подумал: а что если собрать свой приватный поиск, который работает только на моем компьютере, не требует интернета и понимает, что я имею в виду? Не "найди файл с названием отчет", а "найди тот документ, где мы обсуждали интеграцию с API в прошлом месяце". Звучит как магия, но на самом деле это просто эмбеддинги и косинусная близость. И Rust.
На момент 17 февраля 2026 года, самые эффективные модели для эмбеддингов текста - это дообученные версии E5-v3 и BGE-M3. Но для локального запуска на слабом железе лучше взять квантованную all-MiniLM-L12-v2 - она до сих пор отлично работает и весит всего 90 МБ.
1 Готовим инструменты: Rust, Tauri и модель
Первое - ставим Rust. Если у вас его нет, запускаете в терминале curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh и ждете. Никаких волшебных кнопок, только командная строка.
Дальше - Tauri. Это фреймворк для создания десктопных приложений на Rust с интерфейсом на веб-технологиях. Устанавливаем его глобально: cargo install tauri-cli. Пока грузится, можно почитать про Tandem - там похожий подход, но для другого.
Теперь самая важная часть - модель для эмбеддингов. Мы будем использовать квантованную версию all-MiniLM-L12-v2 через библиотеку Candle. Почему? Потому что она работает быстрее чем черепаха и не жрет всю оперативку. Добавляем в Cargo.toml:
[dependencies]
tauri = { version = "2.0", features = ["shell-open"] }
candle-core = "0.4"
candle-transformers = "0.4"
tokio = { version = "1.0", features = ["full"] }
walkdir = "2.5"
serde_json = "1.0"
anyhow = "1.0"
Модель качаем заранее и кладем в папку assets. Или используем candle-transformers для загрузки на лету - но тогда первый запуск будет долгим. Я предпочитаю качать сразу.
2 Пишем код: от файла до вектора
Создаем новое Tauri-приложение: cargo tauri init. Выбираем простой шаблон, без изысков.
Теперь нужно написать функцию, которая берет текстовый файл и превращает его в вектор. Вот как это выглядит (упрощенно):
use candle_core::{Device, Tensor};
use candle_transformers::models::bert::{BertModel, Config, DTYPE};
use tokenizers::Tokenizer;
async fn text_to_embedding(text: &str) -> anyhow::Result> {
let device = Device::Cpu;
let config = Config::mini_lm_l12_v2();
let model = BertModel::load(&config, "assets/model.safetensors", &device)?;
let tokenizer = Tokenizer::from_file("assets/tokenizer.json").unwrap();
let encoding = tokenizer.encode(text, true).unwrap();
let token_ids = Tensor::new(encoding.get_ids(), &device)?;
let attention_mask = Tensor::new(encoding.get_attention_mask(), &device)?;
let output = model.forward(&token_ids, &attention_mask)?;
let embedding = output.last_hidden_state.mean(1)?;
Ok(embedding.to_vec1::()?)
}
Этот код загружает модель, токенизирует текст, пропускает через BERT и усредняет выходы последнего слоя. Получается вектор из 384 чисел - это и есть эмбеддинг. Если хотите понять, как это работает под капотом, посмотрите статью про запуск BERT в браузере на Rust - там те же принципы.
3 Индексация: обходим файлы и строим базу векторов
Теперь нужно просканировать папку с документами. Используем walkdir для рекурсивного обхода. Для каждого файла (пока только .txt, .md, .rs - но можно добавить парсеры для PDF и DOCX) читаем содержимое, генерируем эмбеддинг и сохраняем в SQLite.
use walkdir::WalkDir;
use rusqlite::{Connection, params};
async fn index_folder(path: &str, conn: &Connection) -> anyhow::Result {
let mut count = 0;
for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let ext = entry.path().extension().and_then(|s| s.to_str());
if let Some("txt" | "md" | "rs") = ext {
let content = std::fs::read_to_string(entry.path())?;
let embedding = text_to_embedding(&content).await?;
conn.execute(
"INSERT INTO files (path, content, embedding) VALUES (?, ?, ?)",
params![
entry.path().to_string_lossy(),
content,
serde_json::to_string(&embedding)?
],
)?;
count += 1;
}
}
}
Ok(count)
}
В базе создаем таблицу с полями: id, path, content, embedding. Эмбеддинг храним как JSON-массив. Да, это не оптимально, но для тысячи файлов сойдет. Если нужно больше - смотрите в сторону специализированных векторных баз, но для локального поиска по личным файлам SQLite - идеальный вариант. Кстати, про векторный поиск для базы знаний есть отдельная статья.
4 Поиск: от запроса до результата
Пользователь вводит запрос. Мы превращаем его в эмбеддинг точно так же, как и файлы. Потом вычисляем косинусную близость между запросом и всеми эмбеддингами в базе. Самые близкие - самые релевантные.
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::().sqrt();
dot / (norm_a * norm_b)
}
async fn search(query: &str, conn: &Connection, limit: i64) -> anyhow::Result> {
let query_embedding = text_to_embedding(query).await?;
let mut stmt = conn.prepare("SELECT path, embedding FROM files")?;
let rows = stmt.query_map([], |row| {
let path: String = row.get(0)?;
let embedding_json: String = row.get(1)?;
Ok((path, embedding_json))
})?;
let mut results = Vec::new();
for row in rows {
let (path, embedding_json) = row?;
let file_embedding: Vec = serde_json::from_str(&embedding_json)?;
let similarity = cosine_similarity(&query_embedding, &file_embedding);
results.push((path, similarity));
}
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
results.truncate(limit as usize);
Ok(results)
}
Да, это наивный поиск по всем векторам. Для 10 тысяч файлов он будет работать медленно. Но для личного использования с парой тысяч документов - вполне сносно. Если нужно быстрее, добавьте индекс HNSW через библиотеку hnsw-rs. Или посмотрите на Ragex, где семантический поиск по коду сделан через графы.
Чем этот подход лучше облачных аналогов?
| Критерий | Наш Rust-поиск | Windows Recall | Облачные RAG-сервисы |
|---|---|---|---|
| Приватность | 100% локально, данные никуда не уходят | Отправляет в облако Microsoft | Зависит от провайдера, обычно данные на их серверах |
| Скорость | Мгновенно после индексации, нет сетевых задержек | Зависит от интернета и серверов Microsoft | Задержки от 100 мс до нескольких секунд |
| Стоимость | Бесплатно (кроме электричества) | Включено в Windows, но вы платите данными | От $10/месяц за API-вызовы |
| Кастомизация | Можете поменять любую часть кода | Закрытая система, настройки минимальны | Ограничено API и настройками провайдера |
Главное преимущество - контроль. Вы решаете, какие файлы индексировать, где хранить базу, как часто обновлять индекс. Никаких внезапных обновлений, которые ломают функциональность. Никаких соглашений о конфиденциальности, которые вы не читали.
Где это можно использовать?
- Поиск в заметках. У меня их несколько тысяч в Markdown. Обычный текстовый поиск не справляется, когда нужно найти "ту идею про кэширование". Семантический - находит сразу. Похожий подход используется в Yttri.
- Поиск в архиве писем. Экспортируйте почту в текстовые файлы и индексируйте. Намного эффективнее, чем встроенный поиск в почтовике.
- Поиск по кодовой базе. Особенно если нужно найти не конкретную функцию, а "где у нас обрабатываются платежи". Для этого есть и более специализированные инструменты вроде srag.
- Персональная база знаний. Собираете все документы, статьи, сохраненные посты в одном месте и ищете по смыслу. Почти как RAG-чатбот для корпоративных знаний, но только для себя.
Если вы только начинаете изучать Rust и машинное обучение, рекомендую книгу "Machine Learning with Rust" (партнерская ссылка). Там разобраны не только эмбеддинги, но и классификация, кластеризация и другие алгоритмы. И курс "Practical ML in Rust" - с живыми примерами и проектами.
А что насчет производительности?
На моем ноутбуке с Intel i5 и 16 ГБ ОЗУ индексация 1000 текстовых файлов (в среднем по 10 КБ каждый) занимает около 5 минут. Поиск - менее 100 мс. Потребление памяти - около 300 МБ в пике. Модель all-MiniLM-L12-v2 квантована до 8-бит, так что работает даже на Raspberry Pi.
Если нужно обрабатывать PDF или DOCX, добавьте библиотеки pdf-extract и calamine. Для веб-страниц - html2text. Главное - не пытайтесь запихнуть все в один эмбеддинг. Разбивайте документы на логические части: для книги - по главам, для кода - по модулям, для длинных статей - по секциям.
И последний совет: не делайте интерфейс слишком сложным. Tauri позволяет использовать любые фронтенд-фреймворки, но для такого проекта достаточно простого HTML с полем ввода и списком результатов. Весь смысл в том, чтобы поиск работал быстро и не мешал. Ведь вы делаете его для себя, а не для выставки в App Store.
Теперь у вас есть свой приватный Windows Recall. Только без шпионажа, без облака и с открытым исходным кодом. И если захотите расширить функциональность - например, добавить чат-интерфейс как в промптах для RAG - код уже у вас в руках. Меняйте, улучшайте, делитесь. Или не делитесь - это же ваш приватный поиск.