У меня с давних времен сохранилась куча всяких заметок, в основном в виде простых текстовых файлов.
Самых разных: от скриптов и конфигов до всяких рецептов, всё что когда-то пригодилось или могло пригодиться.
Хранение в виде файлов - это на самом деле очень удобно: их практически всегда можно прочитать, их легко создавать и редактировать, они не требуют установки каких-то специальных особых редакторов и информационных систем, при этом их легко копировать, добавлять в архивы и т.д.
И даже полнотекстовый поиск по ним организуется элементарно.
Единственный минус, когда их много - для того чтобы что-то найти нужно пройтись по каталогам где они лежат и увидеть глазами.
Особенно - если их никто не пытался строго структурировать, и они лежат где-то так:
"Документы/старое/1/1/разобрать/старый диск/Документы/Работа/...".
Ну, это конечно запущенный случай, и надо бы разобрать, когда-нибудь, может быть - ну, собственно говоря, там так и написано...
Что если попробовать прикрутить к этому поиск?
Вот только нужно соблюсти несколько условий:
1 - ничего не испортить. Информация хранится в файлах - и эти файлы-первоисточники не должны потеряться.
2 - только локальная обработка. Никаких облаков, подписок и прочего.
3 - не должны быть завышены системные требования, задача должна решаться как можно проще, без приобретения дорогостоящего оборудования, буквально на том что есть.
А есть машинки с 4 гигами памяти, с этим не проблема.
И вот, исходя из таких предпосылок, начинаем...
Дисклеймер: с нуля означает с нуля, то есть от "я читал про это" до более-менее работающей системы, и без претензий на продакшен, чисто для себя.
Инженерам по строительству RAG-систем тут вряд ли будет что-то интересно.
В соответствии с нормами текущего времени - базовые вопросы были адресованы LLM, а вот с ответами уже пришлось разбираться самому.
В теории это должно работать примерно так: в основе лежит некая ИИ-модель, которая умеет оценивать тексты.
Оценивает они их по принципу "существует всего 768 тем, и вот этот текст соответствует этим темам на [0.32, 0.522, 0.12, 0.023, ...]".
Полученный массив - 768-мерный вектор, который описывает данный кусочек текста.
Поисковой запрос в виде текста также оценивается этой же моделью, также формируется вектор, а потом этот вектор прогоняется по базе сохраненных векторов в поисках наиболее близких по теме, для чего нужна так называемая векторная БД.
Ищется не прямое совпадение, а "дистанция" между векторами: чем она меньше - тем больше тексты похожи.
В результате найденные куски текстов по идее должны более-менее соответствовать запросу, ну а по сохраненным метаданным можно найти тот документ, к которому данный текст относился.
Ну, в идеале сюда еще добавляется полнотекстовой поиск, для поиска отдельных ключевых слов и уточнения результатов поиска - но это уже примочки.
Также LLM предложил готовый пример скрипта, реализующего данную логику. Скрипт был, конечно же, на Python.
Python - язык №1 в топах, и почему я его не люблю
Есть такие языки программирования, которые просто не нравятся. У меня это - Pascal и Python. Один к счастью сейчас почти не встречается, а вот второй занимает первые строчки всяческих рейтингов.
Не нравится же он по нескольким причинам, и сейчас первая, чисто вкусовщина:
он создает своеобразный вайб, как будто в институте на лекции очень пожилой профессор, прекрасный математик, но реально писавший программы последний раз 50 лет назад на PL/1 еще для ЕС, пытается обьяснить студентам алгоритм с использованием псевдокода.
А студенты потом тащат этот псевдокод в продакшен - работает же?
Работает, но есть нюансы...
import faiss import numpy as np from sentence_transformers import SentenceTransformer embedder = SentenceTransformer( "intfloat/multilingual-e5-base" ) chunks = [ { "text": "Текст 1", "source": "/data/manual1.pdf", }, { "text": "Текст 2", "source": "/data/manual2.pdf", } ] texts = [chunk["text"] for chunk in chunks] vectors = embedder.encode( texts, normalize_embeddings=True, show_progress_bar=True ) vectors = np.asarray(vectors, dtype="float32") dimension = vectors.shape[1] index = faiss.IndexFlatIP(dimension) index.add(vectors) faiss.write_index(index, "documents.faiss") import json with open("chunks.json", "w", encoding="utf-8") as f: json.dump(chunks, f, ensure_ascii=False, indent=2) def search(query, index, chunks, embedder, top_k=5): query_vector = embedder.encode( [query], normalize_embeddings=True ).astype("float32") scores, ids = index.search(query_vector, top_k) results = [] for score, idx in zip(scores[0], ids[0]): if idx >= 0: item = chunks[idx].copy() item["score"] = float(score) results.append(item) return results results = search( "текст запроса", index, chunks, embedder, top_k=5 ) for result in results: print(result["source"], result["score"]) print(result["text"])
(скрипт не мой, но нужен тут для понимания откуда ног растут)
В данном случае вектора создаются с помощью модели "intfloat/multilingual-e5-base", записываются в векторную базу FAISS, а метаданные, что откуда взято - в JSON-файл.
ИМХО, неплохой учебный пример, демонстрирующий логику работы.
Исходные файлы разбиваются на чанки, чанки индексируются, по векторам находятся совпадения, результаты можно отсортировать по рейтингу, и в итоге уже выдать документы-первоисточники, откуда что взято.
После нескольких доработок скрипт превратился в первую версию: он реально читал файлы из заданного каталога и индексировал их, а второй скрипт (по сути часть исходного) - уже умел что-то искать в этой базе.

Первая проблема была в том, что всё это было ОЧЕНЬ долго: и в основном время тратилось на загрузку модели.
Особенно при поиске: скрипт запускается, грузит модель, и только потом уже можно отправлять в нее запрос.
Разумеется, о практическом использовании такого говорить нельзя, поэтому в очередной версии "поисковик" запускал http-сервер, который принимал поисковую строку, отправлял ее в модель, и возвращал результаты.
Почему Python так любят многие?
Он простой с точки зрения установки: достаточно команды pip install something - оно будет где-то найдено, скачано и быстро установлено.
Сложности могут возникнуть только на самом первом этапе - подготовки рабочего места студента
python3 -m venv .venv source .venv/bin/activate
И всё скачанное будет устанавливаться в домашний каталог. В каких-то случаях это даже удобно, но в каких-то не очень.
А вот потом - всё просто и как бы само по себе. И экосистема обширная - модулей много понаписали.
В частности, добавить в скрипт http-сервер - раз плюнуть. Удобно.
На этом этапе стало понятно, что клиентская сторона, собственно, отправка запросов, совсем не обязана быть на Python, запросы можно отправлять из чего угодно, хоть из шелла.
Стало гораздо лучше: скрипт-индексатор, скрипт-поисковик и внешний клиент для отправки запросов.
Первые два тормозили при запуске, а вот внешний клиент работал уже независимо от них.

В ходе экспериментов выяснилось, что держать индексы и метаданные необязательно в этих файлах, можно записывать их, ну например, в MongoDB по сети - но без специальных расширений для работы с векторами никакого преимущества оно не дает, просто и так тоже можно.
Можно заменить FAISS на ChromaDB - кроме некоторого замедления эффекта нет.
Также возникло желание заменить эту модель на что-то более другое - тогда выяснилось, что модель может работать вообще отдельно, на llama.cpp или Ollama.
Порядок работы немного поменялся: Ollama запускалась на другом сервере в сети, скрипты обращались к ней за векторами по API, и вот только поиск шел уже в той же FAISS.
from ollama import Client ol_host = "http://ollama:11434" oclient = Client(host=ol_host) ... #new_vector = embedder.encode( # #[f"{content}"], # normalize_embeddings=True, # convert_to_numpy=True #).astype("float32") response = oclient.embed( model=model, input=content, dimensions=dimension ) embeddings = response['embeddings'] new_vector = np.array(embeddings, dtype=np.float32) ...
Теперь обработкой занималась модель в Ollama, выбор вариантов увеличился.
Но по-прежнему не нравилось, что ради этого приходится отдельно устанавливать скрипты на Python.

Python - технические моменты
Во-первых, ужасно неудобный (для меня) синтаксис.
При всей легкости установки чужих готовых модулей - писать на этом самому совершенно не хотелось.
Почти как в старом анекдоте: "Ты C знаешь? Вот вообще на него не похоже!"
Во-вторых - он тормоз.
Простейшие вещи, которые к примеру, на Perl, отрабатывают мгновенно - тут раздражающе тормозили.
И в третьих - ну вот, допустим, допустили вы очепятку. Переменная не так называется, как должна.
Что делает perl-скрипт при запуске? Он сразу ругается: в строке такой-то переменная "anme" не была обьявлена / не используется, проверьте!
Что делает python-скрипт при запуске? Он запускается, работает, и только при обращении к проблемному коду почему-то происходит ошибка, хорошо если в лог напишет.
А с учетом того что он тормоз, и до нужного места может дойти не сразу - приятного мало.
В общем, возникло сильное желание переписать все на Rust Perl.
К сожалению, экосистема Perl сейчас действительно беднее чем Python: аналогов FAISS нет, модели локально запускать не умеет. А некоторые и вовсе считают, что он был популярным языком для веб в начале 90-х, да весь вышел.
Тем не менее, и для него есть модули работы с векторными БД, а модель мы уже вынесли за скобки, в Ollama.
Можно было конечно сразу начать с Qdrant, а можно попроще, с sqlite3 с векторным модулем.
Называется это SQLite::VecDB, и как большинство Perl-модулей устанавливается через CPAN:
cpan SQLite::VecDB
Но есть нюанс...
Почему Perl быстрый и почему он сегодня проигрывает
Система установки Perl-модулей CPAN сделана с расчетом на то, что модули могут быть написаны как на чистом Perl (PurePerl, PP), так и с использованием вставок на C/C++.
Самые распространенные модули входят в дистрибутив ОС и могут быть скачаны в виде готовых архивов, нестандартные - скачиваются исходниками.
Если с PP всё понятно - модуль скачался и записан в определенный каталог модулей - то те, которые требуют компиляции - должны быть скомпилированы на целевой системе, с подключением нужных библиотек.
Всё это происходит автоматически, нужно только установить необходимое, типа исходников этих библиотек. Как правило, они есть в дистрибутиве, нужно только их найти.
Все модули плотно покрыты автотестами - поэтому если уж у вас установился какой-нибудь XXXXX - это значит, он точно будет работать и будет совместим со всем тем, что у вас понаустановлено.
Это хорошо - но это долго. Но зато хорошо.
В частности, упомянутый модуль требует исходников векторного расширения sqlite3 - и они тоже есть в дистрибутиве, но нужно их найти, установить через пакетный менеджер, вместе с gcc/g++ и прочим.
Получается очень быстрая реализация, по факту на С с оберткой на Perl, но всё это требует несколько больше знаний и движений, чем pip install.
И второй нюанс, на примере этого же пакета: согласно документации (man SQLite::VecDB) есть вариант запуска с использованием специального модуля для работы с Ollama.
Теоретически это должно бы упростить работу - но по факту этот самый "упрощающий модуль" требует предустановки еще 100500 других, которые вероятно уже были установлены у разработчика, и он не задумываясь использовал готовые блоки. Code reuse, вот это всё.
Поэтому, несмотря на то что всё происходит автоматически - оно происходит очень долго (автотесты же, на каждый пакет и чих).
Иногда быстрее написать самому вручную - но это всё еще сложнее pip install.
Итог немного предсказуем: быстрый, но более сложный Perl проигрывает...
Возвращаясь к нашим баранам: SQLite::VecDB позволяет хранить базу векторов и метаданных в файле sqlite3, а также выполнять по ней поиск.
Вектора рассчитывает Ollama, и таким образом удалось наконец всё "переписать на Perl".
Упомянутый модуль для работы с Ollama по сути должен был всего лишь запросить вектор и подставить его куда нужно.
Как и говорил - оказалось быстрее написать свой вариант запроса:
#!/usr/bin/perl use SQLite::VecDB; use HTTP::Tiny; use JSON; my $dim = 768; my $model = 'snowflake-arctic-embed2'; my $url = 'http://ollama:11434/api/embed'; my $vdb = SQLite::VecDB->new( db_file => '/data/vectors.db', dimensions => $dim, ); my $coll = $vdb->collection('documents'); # сохранение в базу sub add_vector { my ($id, $vector, $data, $content) = @_; $coll->add( id => $id, vector => $vector, metadata => $data, content => $content, ); } my $http = HTTP::Tiny->new(timeout => 30); # запрос вектора у Ollama sub get_vector { my ($text) = @_; my $payload = { model => $model, input => $text, dimensions => $dim, }; my $json = to_json($payload); my $response = $http->post( $url, { headers => { 'Content-Type' => 'application/json' }, content => $json, } ); if ($response->{success}) { my $data = from_json($response->{content}); my $vector = $data->{embeddings}[0]; return $vector; } else{ print STDERR "Error: $response->{status} $response->{reason}\n$response->{content}\n"; return undef; } } # поиск по вектору в базе sub search_vector { my ($vector) = @_; my @results = $coll->search( vector => $vector, limit => 10, ); my $ret = []; for my $r (@results) { my $t = { id => $r->{id}, distance => $r->{distance}, content => $r->{content}, metadata => $r->{metadata}, }; push(@$ret, $t); } return $ret; } # пример запроса вектора # my $vector = get_vector('Text1'); # сохранение вектора, метаданных и текста для полнотектового поиска, если надо # add_vector('ID1', $vector, {file => 'file1', page => 12}, 'Text1'); # пример поиска # my $query = get_vector('Search string'); # search_vector($query);
И опять прикрутим HTTP-сервер, чтобы можно было к поисковому скрипту, привязанному к файлу sqllite3 и модулям, обращаться из любого места в сети.
В Perl, конечно же, есть готовые модули, в том числе достаточно серьезные фреймворки - но тут нужен простой, примитивный веб-сервис, для которого тащить что-то большое - всё равно что стрелять по воробьям из Царь-пушки.
Зато такое можно написать самому как нравится, это довольно просто:
#!/usr/bin/perl package MicroHTTPD; use strict; use warnings; use IO::Socket::INET; use URI::Escape qw(uri_unescape); use JSON; # хардкод, но можно переназначить our $port = 8080; our $iface = '0.0.0.0'; our $routes = { 'get' => {}, # сюда добавляем функции, вызываемые по GET 'post' => {}, # сюда - вызываемые через POST 'any' => {}, # сюда - те, которые можно и так и так. }; # запуск сервера sub serve { my $server = IO::Socket::INET->new( Local => $iface, LocalPort => $port, Listen => 10, Reuse => 1, ) or die "Startup error: $!\n"; print "Server started: http://$iface:$port/\n"; while (my $client = $server->accept()) { $client->autoflush(1); my $headers = ''; while (my $line = <$client>) { $headers .= $line; last if $line eq "\r\n"; } my ($request_line, @header_lines) = split /\r\n/, $headers; my ($method, $path, $http_version) = $request_line =~ /^(\S+)\s+(\S+)\s+(HTTP\/\d\.\d)$/; unless ($method && $path) { send_response($client, 400, "Bad Request", "Incorrect HTTP-request"); close $client; next; } my %headers; for my $line (@header_lines) { next unless $line =~ /^([^:]+):\s*(.*)$/; $headers{lc $1} = $2; } my $body = ''; if ($method eq 'POST') { my $content_length = $headers{'content-length'} || 0; if ($content_length > 1024 * 1024) { send_response($client, 413, "Payload Too Large", "Request body too large"); close $client; next; } read($client, $body, $content_length); } if ($method eq 'GET') { my ($route, $query_string) = split /\?/, $path, 2; my %params = parse_params($query_string || ''); my $processed = 0; for my $r (keys( %{ $routes->{get} } )){ if ($route eq $r) { my ($ret_body, $ret_headers) = $routes->{get}->{ $r }->( \%params, \%headers ); send_response($client, 200, "OK", $ret_body, $ret_headers); $processed = 1; last; } } for my $r (keys( %{ $routes->{any} } )){ if ($route eq $r) { my ($ret_body, $ret_headers) = $routes->{any}->{ $r }->( \%params, \%headers ); send_response($client, 200, "OK", $ret_body, $ret_headers); $processed = 1; last; } } if(!$processed){ send_response($client, 404, "Not Found", "Method not found"); } } elsif ($method eq 'POST') { my ($route) = split /\?/, $path, 2; my %params = (); if($headers{'content-type'} eq 'application/x-www-form-urlencoded'){ %params = parse_params($body || ''); } if($headers{'content-type'} eq 'application/json' ){ my $t = from_json($body||'{}'); %params = %$t; } my $processed = 0; for my $r (keys( %{ $routes->{post} } )){ if ($route eq $r) { my ($ret_body, $ret_headers) = $routes->{post}->{ $r }->( \%params, \%headers ); send_response($client, 200, "OK", $ret_body, $ret_headers); $processed = 1; last; } } for my $r (keys( %{ $routes->{any} } )){ if ($route eq $r) { my ($ret_body, $ret_headers) = $routes->{any}->{ $r }->( \%params, \%headers ); send_response($client, 200, "OK", $ret_body, $ret_headers); $processed = 1; last; } } if(!$processed){ send_response($client, 404, "Not Found", "Method not found"); } } else { send_response( $client, 405, "Method Not Allowed", "Method Not Allowed", { 'Content-Type' => "text/plain; charset=utf-8", 'Allow' => "GET, POST", } ); } close $client; } } sub parse_params { my ($data) = @_; my %params; for my $pair (split /&/, $data) { my ($key, $value) = split /=/, $pair, 2; $key = uri_unescape($key // ''); $value = uri_unescape($value // ''); $key =~ tr/+/ /; $value =~ tr/+/ /; $params{$key} = $value; } return %params; } sub send_response { my ($client, $status, $status_text, $body, $headers) = @_; $headers->{'Content-Type'} ||= 'text/plain; charset=utf-8'; my $response = "HTTP/1.1 $status $status_text\r\n" . "Content-Length: " . length($body) . "\r\n"; for my $x (keys(%$headers)){ $response .= "$x:$headers->{$x}\r\n"; } $response .= "Connection: close\r\n" . "\r\n" . $body; print $client $response; } 1;
Это модуль микро-HTTP сервера, к которому можно подключить методы на GET и POST запросы. Что и как они будут выполнять - на усмотрение того кто их напишет. Поддерживается параллельное исполнение - в общем, для вебсервиса сойдет.
Подключаем его к поисковому скрипту:
#!/usr/bin/perl use lib "."; use MicroHTTPD; # $MicroHTTPD::port = 8080; .... .... # обработчик http для добавления в базу sub add_text { my ($params, $headers) = @_; my $text = $params->{text}; my $id = $params->{id}; my $data = $params->{data}; my $vector = get_vector($text); if(defined $vector){ add_vector($id, $vector, $data, $text); return ("OK add vector", {} ); }else{ return ("ERR no vector", {} ); } } # обработчик http для поиска по базе sub search_text { my ($params, $headers) = @_; my $text = $params->{text}; my $vector = get_vector($text); if(defined $vector){ my $ret = search_vector($vector); my $json = to_json($ret); return ($json, { 'Content-Type' => 'application/json' } ); }else{ return ("[]", { 'Content-Type' => 'application/json' } ); } } $MicroHTTPD::routes->{post}->{'/load'} = \&add_text; $MicroHTTPD::routes->{post}->{'/query'} = \&search_text; MicroHTTPD::serve();
Всё это запускается в докер-контейнере, и схема работы получается примерно такая:

И вот теперь это уже можно подключать куда угодно, хоть написать утилиту командной строки, хоть встроить в вебсистему.
Насколько быстро оно работает - за это отвечает Ollama, которая в свою очередь ограничена железом и выбранной моделью.
Но это только программная часть - оказалось, что тут важнее подготовка данных.
Данные
В качестве подопытных были взяты файлы с рецептами, сохраненные примерно сто лет назад, по сути просто копипаста постов с форума.
В качестве "поисковых запросов" - несколько коротких фраз, примерно таких, какие я использую в поисковиках.
Общий принцип: сначала загружаем информацию некоторым образом, затем пытаемся что-то найти.
Потом меняем способ загрузки, модели, параметры - и пробуем снова, тот же набор фраз - чтобы потом оценить наглядно разницу.
Для еще большей наглядности добавлено что-то типа ASCII-графиков.
Теория говорит нам, что входящие файлы надо разбить на чанки, и загружать их.
Первый вариант - разбить текст на абзацы.
Скрипт читает, разбивает, загружает - ОК.
Второй скрипт запускает поисковые фразы и сохраняет результаты.

Вот например, "хлеб" найден в Чебуреках (0.45), в Картошке в духовке (0.53), в Бисквите (0.63). Чем больше цифра - тем хуже.
А "Ремонт велосипедов" - в "Картошке в духовке" несколько раз (0.29-0.39) - хотя оно вообще не по теме.
Зато "Шашлык изз баранины" в Бисквите и Чебуреках, и немного найден в Баранине (0.50), хотя там и про баранину и про шашлык.
В общем, велосипедов в Картошке с духовкой оказалось больше, чем шашлыка в шашлыке, а "Творожная запеканка" в "Творожной запеканке" вообще не обнаружена.
Так-то логично: индексирована живая речь, с эмоциями, восклицаниями и балабольством, что пониманию контекста не способствует.
Ок, пробуем разбить на предложения.
Результат - лучше смотреть на сравнении (слева - по абзацам, справа - по предложениям):
Чуть лучше теперь найден шашлык, всё остальное примерно так же или хуже. Тоже логично, шума стало меньше, но контекста больше не стало.

Но был еще список файлов с описаниями!
И вот тут-то оно заработало: ремонт велосипедов остался примерно так же, зато нашлась Баранина и особенно - Творожная запеканка.
Ну с ней понятно, там было полное совпадение описания с поисковой фразой.
В общем, результаты последующих опытов показали, что лучше всего работают описания и комментарии, размер которых примерно соответствует размеру поисковой фразы: меньше смыслового шума.
На практике же это означает, что нельзя вот так просто взять и всё проиндексировать, нужно сделать краткие аннотации, вот тогда оно работает.
Ну или в тексте должны быть постоянные упоминания тематических слов.
Качество угадывания темы и скорость сильно зависят от модели: одни показывают максимум 4-5 секунд на анализ, другие думают больше 30 секунд.
Так же, радикальное уменьшение размерности вектора, с 768 до 256, на тех же данных и тех же запросах дает совсем какую-то незначительную разницу, во втором-третьем знаке после запятой.
Что ж, это тоже результат: если поиск лучше всего делать по аннотациям - то аннотации можно писать ко всему: к тексту, к документам разных там Вордов, к картинкам, к программным пакетам.
Вообще-то некоторые странные люди для этого используют осмысленные имена файлов и каталогов...
Можно прикрутить LLM для того, чтобы он оценивал файлы и давал им имена/описания, но тут упираемся в первоначальное требование: работать локально на том железе которое есть.
Протестированные модели хоть и справлялись в принципе с подобной задачей, но результаты сильно напоминали мемные "носки белосвежного вида" или "светчики лампады нити праздника": если знать контекст - можно понять почему так написано, но задача-то наоборот: понять контекст, читая это.
Более-менее с задачей справилась qwen2:
Скрипт анализа файла
#!/bin/sh if [ "x$1" = "x" ] ; then exit fi fname=$1 text=$(head -15 $fname) # model: "granite4:350m", # model: "llama3.2:latest", jq -n --arg text "$text" '{ model: "qwen2:1.5b", stream: false, prompt: ( "Прочитай этот файл, опиши о чем в нем написано, на русском языке, не будь многословным, составь ответ от 2 до 7 слов, не больше:\n\n" + $text ) }' | curl 'http://ollama:11434/api/generate' \ -H "Content-Type: application/json" \ -d @- | \ jq -r '.response'
Вызываем его примерно таким образом:
#!/bin/bash ROOT_DIR=/source_directory find "$ROOT_DIR" \ -type f -print0 | while IFS= read -r -d '' file; do mime_type=$(file --brief --mime-type -- "$file") case "$mime_type" in text/* ) echo "$file" info=$( ./describe "$file") ( echo "FILE $file : $mime_type" echo "$info" echo "========================================" ) >> FILE_ID.DIZ ;; *) printf 'Пропуск: %s [%s]\n' "$file" "$mime_type" >&2 ;; esac done
Историческая справка: во времена FIDO и BBS пользователи обменивались файлами, заливая на BBS архивы. Чтобы как-то описать, что это там такое в архиве - создавался специальный файл FILE_ID.DIZ, в котором и было написано, что там внутри.
Поскольку тут по сути примерно то же самое - описание файла, данное моделью - его надо было куда-то записать, так почему бы не сюда? Его можно потом смотреть, корректировать и т.д.
И храниться оно будет вместе с файлами, а не где-то в базе.
Остается только прочитать его в систему, для того чтобы искать потом:
#!/usr/bin/perl # use Data::Dumper; use JSON; use HTTP::Tiny; my $url = 'http://dataserver:3333/load'; my $http = HTTP::Tiny->new(timeout => 30); my $file='FILE_ID.DIZ'; open(my $in, $file); exit if(!defined $in); my $fname = undef; my $info = ''; my $id = 0; while(my $str = <$in>){ $str =~ s/[\r\n]+/ /gm; if(!defined $fname && $str =~ /^FILE (.+) :/){ $fname = $1; } elsif(defined $fname && $str ne ' '){ if($str =~ /^=+ $/){ if($info ne ''){ $id ++; my $d = { id => $id, data => { file => $fname}, text => $info, collection => 'texts', }; my $json = to_json($d); my $response = $http->post( $url, { headers => { 'Content-Type' => 'application/json' }, content => $json, } ); if ($response->{success}) { print "$response->{content}\n"; } else{ print STDERR "Error: $response->{status} $response->{reason}\n$response->{content}\n"; } } $fname = undef; $info = ''; }else{ $info .= $str; } } } close($in);
Теперь достаточно натравить эти скрипты на каталог с какими-то записями, файлам дадут аннотации и загрузят в поисковую систему.
Простейший вариант запроса - ну например, так:
curl -s -X POST -d text="микроконтроллер stm32" -d collection='texts' 'http://dataserver:3333/query'
В результате будет выведен список найденных записей в виде JSON-массива:
[{"metadata":{"file":"/srv/raid/stm32/README"},"distance":0.409470826387405,"content":"Данный файл содержит команда openocd для загрузки программы на микроконтроллер STM32. Он указывает на использование различных типов транспортных средств, включая DAPdirect SWD и HLA-USB SWD. Загруженный файл firmware.bin находится в диапазоне 0x08000000 и проверяется после его загрузки. ","id":"13"}, {"id":"17","content":"Безуспешная установка драйвера CMSIS-DAP для F0xx с ПИСО, указание скорости адаптера на 1000, создание ПИСО $_CHIPNAME.cpu в сеть Cortex-M. Инициализация памяти устройства с областью 0x20000000 до 0x1000 для восстановления области обновления, установка Flash banks на Flash0 STM32F1x. ","metadata":{"file":"/srv/raid/OCD/OpenOCD"},"distance":0.490307033061981}, ... ]
Это можно интегрировать в веб-систему, а можно сделать, например, и такое:
Вот пользуюсь я, скажем, консолью и MidnightCommander в нем, а у MC есть такая фишка - ExternalPanelize, Ctrl-x !: можно вызвать внешнюю команду, которая вернет некоторый список файлов, который в свою очередь будет отображен в окне MC как каталог.
Если при загрузке файлов в поисковую систему сохраняется также путь к файлу, и файлы эти доступны с машины, например по NFS - то можно написать такой скрипт, который в ответ на запрос выведет список файлов.
Тогда они все появятся в одной панели, их можно будет легко просматривать или копировать.
Кроме того, есть еще одна фишка - быстрый просмотр, Ctrl-x q - тогда во второй панели показывается часть содержимого текстового файла.
Можно быстро посмотреть, то или не то - прежде чем уже полноценно открывать файл.
К сожалению, вот тут MC не всесилен: в ExternalPanelize можно ввести свою команду, но нельзя сохранить ее так, чтобы она запрашивала строку.
Придется вводить вместе со строкой.
Пишем скрипт, назовем его просто Q:
#!/bin/sh curl -s -X POST -d text="$@" -d collection='texts' 'http://dataserver:3333/query' | jq -r '.[].metadata.file'
Теперь при поиске в ExternalPanelize пишем команду, например, "Q микропроцессор stm32" - и получаем список файлов...
Не идеально, но и так тоже можно.
Ну и наконец, про скорость работы всего этого.
Самое медленное - работа моделей, тут всё упирается в железо и память.
Но вот у меня, в качестве эсперимента, всё работает вообще на дешевом одноплатнике - по скорости сопоставимо с "найти в Гугле".
Конечно, несколько параллельных запросов заставят систему выполнять их по очереди, но поскольку всё работает через HTTP - при желании можно разнести несколько запросов по нескольким машинам, отправляя разные на разные.
Другой вопрос, что мне дома это не нужно, да и вообще больше эксперимент.
Пока просматривается несколько сценариев применения, где такой поиск может быть удобен...
MEGA_Nexus
Раньше шутили, что ко всему пытаются прикрутить кубернетес и микросервисы. Сейчас в моде LLM.
Для этого LLM не нужна, а нужен простой поисковый движок.
Для поиска чего-либо в локальных файлах давным-давно были придуманы локальные индексаторы, например, Архивариус 3000.
«Архивариус 3000» — это мощная программа для быстрого поиска документов и электронной почты на компьютере, в локальной сети и на съёмных дисках.
Основные возможности
• Мгновенный поиск: Ищет слова и фразы по содержимому документов, а не только по названиям файлов, используя предварительное индексирование.
• Поддержка форматов: Работает с офисными документами (Word, Excel, PDF, RTF), текстами, архивами (ZIP, RAR и др.) и популярными почтовыми клиентами.
• Сетевой режим: Позволяет организовывать удаленный доступ к поисковой базе через веб-интерфейс.
• Языки: Обладает развитой системой морфологии для нескольких языков, включая русский.
Если не понравится Архивариус 3000, который давно не развивается, то можно посмотреть его современные аналоги - https://www.reddit.com/r/software/comments/1ogidtp/listing_of_windows_file_search_tools/?tl=ru
Kano
ЛЛМ тут прикручивают для того что бы оно своими рассуждениями (исходя из заданного контекста) составляло и корректировало запрос (исходя из первого ответа) наиболее подходящим для пользователя образом.
vdudouyt
Я вот все думаю, а не пора ли хабру сменить название юр. лица с “Habr Blockchain Publishing Ltd” на “Habr GPT Publishing Ltd” :)