Существует несколько способов реализовать полнотекстовый поиск:
- Tantivy
Это высокопроизводительная библиотека для полнотекстового поиска, написанная на Rust. Она предоставляет гибкие возможности для создания индексов и поиска текста, поддерживает различные типы полей (текст, числа, даты) и предлагает API для запросов, схожих с Lucene. - Elasticsearch Rust Client
Elasticsearch — популярный движок для полнотекстового поиска. Для Rust доступен клиент, который позволяет взаимодействовать с его API, что удобно для интеграции в проекты, уже использующие Elasticsearch. - Meilisearch Rust Client
Meilisearch — это легковесный и быстрый движок для полнотекстового поиска с простым API. Для Rust существует клиент, который упрощает подключение этого сервиса к вашим приложениям. - Sonic
Sonic — это простой и легковесный сервер для полнотекстового поиска, написанный на Rust. Его можно использовать как самостоятельный сервер или интегрировать в приложение через простой протокол. - Rust-BERT
Для более сложных задач, таких как семантический поиск, можно использовать модели машинного обучения, например BERT. Rust-BERT — это библиотека, которая позволяет применять такие модели в Rust для обработки текста и поиска.
Собственная реализация
Если у вас есть специфические требования или желание глубже разобраться в теме, можно создать собственный движок полнотекстового поиска на Rust. Это может включать разработку инвертированного индекса, обработку запросов и ранжирование результатов.
1. Сохранение эмбеддингов документов в памяти или на диске
Цель: Обеспечить быстрый доступ к эмбеддингам документов, минимизируя повторные вычисления.
Решение:
- В памяти: Используем структуру HashMap для хранения эмбеддингов, где ключ — идентификатор документа (например, строка), а значение — вектор эмбеддинга (массив Vec<f32>).
- На диске: Сериализуем эмбеддинги в файл, например, в формате JSON, для долговременного хранения и экономии оперативной памяти.
Реализация:
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use serde::{Deserialize, Serialize};
// Структура для кэша эмбеддингов
#[derive(Serialize, Deserialize)]
struct EmbeddingsCache {
embeddings: HashMap<String, Vec<f32>>,
}
impl EmbeddingsCache {
// Создание нового кэша
fn new() -> Self {
EmbeddingsCache {
embeddings: HashMap::new(),
}
}
// Добавление эмбеддинга
fn add(&mut self, doc_id: String, embedding: Vec<f32>) {
self.embeddings.insert(doc_id, embedding);
}
// Получение эмбеддинга
fn get(&self, doc_id: &str) -> Option<&Vec<f32>> {
self.embeddings.get(doc_id)
}
// Сохранение в файл
fn save_to_file(&self, path: &str) -> std::io::Result<()> {
let serialized = serde_json::to_string(&self)?;
let mut file = File::create(path)?;
file.write_all(serialized.as_bytes())?;
Ok(())
}
// Загрузка из файла
fn load_from_file(path: &str) -> std::io::Result<Self> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let cache: EmbeddingsCache = serde_json::from_str(&contents)?;
Ok(cache)
}
}
Как использовать:
- Для небольших объемов данных держите EmbeddingsCache в памяти.
- Для больших коллекций сохраняйте кэш на диск с помощью save_to_file и загружайте при необходимости через load_from_file.
2. Использование Tokio для параллельного вычисления эмбеддингов
Цель: Ускорить вычисление эмбеддингов за счет параллельной обработки.
Решение:
- Используем асинхронный фреймворк Tokio для создания задач с помощью tokio::spawn.
- Собираем результаты параллельных вычислений с помощью futures::future::join_all.
Реализация:
use tokio::task;
use futures::future::join_all;
use rust_bert::pipelines::sentence_embeddings::SentenceEmbeddingsBuilder;
async fn compute_embeddings_parallel(
model: &SentenceEmbeddingsBuilder,
texts: Vec<String>,
) -> Vec<Vec<f32>> {
let mut handles = vec![];
for text in texts {
let model_clone = model.clone(); // Клонируем модель для каждой задачи
let handle = task::spawn(async move {
model_clone.encode(&[text]).unwrap()[0].clone()
});
handles.push(handle);
}
let results = join_all(handles).await;
results.into_iter().map(|res| res.unwrap()).collect()
}
Пример вызова:
#[tokio::main]
async fn main() {
let model = SentenceEmbeddingsBuilder::remote().create_model().unwrap();
let texts = vec!["Текст 1".to_string(), "Текст 2".to_string()];
let embeddings = compute_embeddings_parallel(&model, texts).await;
println!("Эмбеддинги: {:?}", embeddings);
}
3. Добавление очереди задач (например, Kafka) для больших данных
Цель: Обеспечить масштабируемость системы при работе с большими объемами данных.
Решение:
- Используем Apache Kafka как распределенную очередь задач.
- Отправляем тексты документов в Kafka (через producer), а затем асинхронно вычисляем эмбеддинги (через consumer).
Реализация:
- Установите библиотеку rdkafka через Cargo.toml:
[dependencies]
rdkafka = "0.28"
Producer (отправка задач):
use rdkafka::producer::{FutureProducer, FutureRecord};
use rdkafka::config::ClientConfig;
use std::time::Duration;
async fn send_to_kafka(text: String) {
let producer: FutureProducer = ClientConfig::new()
.set("bootstrap.servers", "localhost:9092")
.create()
.expect("Ошибка создания producer");
let record = FutureRecord::to("embeddings-topic").payload(&text).key(&"key");
producer.send(record, Duration::from_secs(0)).await.unwrap();
}
Consumer (обработка задач):
use rdkafka::consumer::{StreamConsumer, Consumer};
use rdkafka::message::Message;
use rust_bert::pipelines::sentence_embeddings::SentenceEmbeddingsBuilder;
async fn consume_and_compute(model: SentenceEmbeddingsBuilder) {
let consumer: StreamConsumer = ClientConfig::new()
.set("bootstrap.servers", "localhost:9092")
.set("group.id", "embeddings-group")
.create()
.expect("Ошибка создания consumer");
consumer.subscribe(&["embeddings-topic"]).unwrap();
loop {
match consumer.recv().await {
Ok(message) => {
let text = message.payload_view::<str>().unwrap().unwrap();
let embedding = model.encode(&[text]).unwrap()[0].clone();
// Сохраните embedding в кэш или используйте далее
println!("Эмбеддинг для {}: {:?}", text, embedding);
}
Err(e) => eprintln!("Ошибка Kafka: {}", e),
}
}
}
Как использовать:
- Запустите Kafka на localhost:9092.
- Используйте send_to_kafka для отправки текстов в очередь.
- Запустите consume_and_compute в отдельном процессе для обработки задач.
Примечание: Это решение позволяет распределять нагрузку между несколькими узлами, что идеально для больших данных.
4. Настройка модели на ваших данных с помощью Tch-rs
Цель: Улучшить качество эмбеддингов, адаптировав модель к вашим данным.
Решение:
- Используем Tch-rs, библиотеку для машинного обучения на Rust, чтобы дообучить модель (например, BERT) на вашем наборе данных.
Реализация:
- Установите tch через Cargo.toml:
[dependencies]
tch = "0.7"
Пример дообучения:
use tch::{nn, Device, Tensor, Kind};
use tch::nn::OptimizerConfig;
fn fine_tune_model() {
let vs = nn::VarStore::new(Device::Cpu);
// Здесь предполагается загрузка предобученной модели, например, BERT
// В реальной реализации используйте tch для загрузки модели
let mut optimizer = nn::Adam::default().build(&vs, 1e-4).unwrap();
// Пример данных (замените на ваши)
for epoch in 0..10 {
let input = Tensor::of_slice(&[1.0, 2.0, 3.0]).to_kind(Kind::Float); // Входные данные
let target = Tensor::of_slice(&[0.5, 1.5, 2.5]).to_kind(Kind::Float); // Целевые значения
let output = input * 2.0; // Пример forward pass (замените на вашу модель)
let loss = output.mse_loss(&target, tch::Reduction::Mean);
optimizer.backward_step(&loss);
println!("Эпоха {}: потеря {}", epoch, f64::from(loss));
}
}
ак использовать:
- Подготовьте размеченный набор данных (например, пары запрос-документ с метками релевантности).
- Замените пример input и output на реальные данные и вызов модели.
- Выполните обучение на ваших данных.
Примечание: Для реального дообучения потребуется загрузить предобученную модель (например, через tch::vision или вручную) и адаптировать код под вашу задачу.