📱 Подписаться
IT и цифровая трансформация

Делаем RAG-систему с нуля

📰 Habr 👁️ 0 просмотров

JBFW6 часов назадОбъяснить с

Делаем RAG-систему с нуля

Время на прочтение17 минОхват и читатели5.5KНастройка Linux*Perl*DIY или Сделай самУ меня с давних времен сохранилась куча всяких заметок, в основном в виде простых текстовых файлов.Самых разных: от скриптов и конфигов до всяких рецептов, всё что когда-то пригодилось или могло пригодиться.

Хранение в виде файлов - это на самом деле очень удобно: их практически всегда можно прочитать, их легко создавать и редактировать, они не требуют установки каких-то специальных особых редакторов и информационных систем, при этом их легко копировать, добавлять в архивы и т.д. И даже полнотекстовый поиск по ним организуется элементарно.

Единственный минус, когда их много - для того чтобы что-то найти нужно пройтись по каталогам где они лежат и увидеть глазами. Особенно - если их никто не пытался строго структурировать, и они лежат где-то так:"Документы/старое/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 - при желании можно разнести несколько запросов по нескольким машинам, отправляя разные на разные. Другой вопрос, что мне дома это не нужно, да и вообще больше эксперимент.

Пока просматривается несколько сценариев применения, где такой поиск может быть удобен...Теги:• rag
• поиск
• ollama
• perl
• экспериментыХабы:• Настройка Linux
• Perl
• DIY или Сделай сам

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал