В прошлом примере я рассказал о том, как можно использовать YDB в качестве векторной базы знаний. Сегодня расскажу про то, как использовать Yandex Embedder.

Но сначала несколько слов о том, почему Embedder - это очень важно.

Итак, у нас есть задача: есть некоторая внутренняя система (у моем случае - YouTrack), есть ИИ (не важно какой). Я хочу получать ответы от ИИ с использованием данных из внутренней системы. Классическое решение - это выгрузка данных из внутренней системы в RAG и использование этих данных для контекста в запросе к ИИ. В качестве RAG используется векторная база данных (например YDB). Важным моментом является то, что в контекст запроса к LLM передается не вся база - а только некоторое количество наиболее близких документов ("близость" как раз определяется сравнением векторов). То есть, работает так:

  • мы пишем запрос

  • по этому запросу находим релевантные документы в RAG

  • отправляем запрос в LLM передавая найденные документы

Так вот. То, что вам ответит ИИ зависит от того, что вы найдете в RAG. А это в свою очередь зависит от того, насколько "хорошо" будут сгенерированы векторы. За генерацию векторов как раз и отвечает Embedder.

В моем прошлом примере класс сохранения данных в YDB использовал простой embedder из библиотеки '@xenova/transformers' и результат поиска, если честно, был так себе. Поэтому следующей задачей стало использование уже более "правильного" Embedder-а. В моем случае, так как я в данном прототипе ориентирован на максимально использование сервисов Yandex Cloud то и выбор пал на Yandex Embedder. Доступные модели можно посмотреть в AI Studio в каталоге моделей.

Нам нужна та, которая называется Yandex Text embedding (query) 1

Ниже приведен код класса для работы с Yandex Embedder:

// yandex-embedder.js
export class YandexEmbedder {
  constructor(options = {}) {
    this.apiKey = options.apiKey;
    this.baseURL = options.baseURL;
    this.modelUri = options.modelUri;
    this.requestsPerSecond = options.requestsPerSecond || 8;

    this.delayBetweenRequests = 1000 / this.requestsPerSecond;
    this.lastRequestTime = 0;

    console.log('🔧 Yandex Embedder configured:');
    console.log('   - Model Uri:', this.modelUri);
    console.log('   - Base URL:', this.baseURL);
    console.log('   - delayBetweenRequests:', this.delayBetweenRequests + 'ms');
  }

  async generateEmbedding(text) {
      const now = Date.now();
      const timeSinceLastRequest = now - this.lastRequestTime;

      // console.log(Date.now() + " timeSinceLastRequest: " + timeSinceLastRequest);
      if (timeSinceLastRequest < this.delayBetweenRequests) {
        const waitTime = this.delayBetweenRequests - timeSinceLastRequest;
        await new Promise(resolve => setTimeout(resolve, waitTime));
      }
      // console.log(Date.now() + " After timer:");
      this.lastRequestTime = Date.now();

      return await this.makeRequest(text);
  }

  async makeRequest(text) {
    const controller = new AbortController();

    try {
      // console.log(`🔍 Generating embedding for text: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);

      const response = await fetch(this.baseURL, {
        method: 'POST',
        headers: {
          'Authorization': `Api-Key ${this.apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          modelUri: this.modelUri,
          text: text
        })
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Yandex API error: ${response.status} - ${errorText}`);
      }

      const data = await response.json();

      if (!data.embedding) {
        throw new Error('No embedding in Yandex API response');
      }

      // console.log(`✅ Embedding generated, dimension: ${data.embedding.length}`);
      return data.embedding;

    } catch (error) {
      console.error('❌ Error generating Yandex embedding:', error.message);
      throw error;
    }
  }

  // Batch генерация эмбеддингов
  async generateEmbeddings(texts) {
      console.log(`🔍 Generating embeddings for ${texts.length} texts...`);

      const embeddings = [];

      for (let i = 0; i < texts.length; i++) {
        try {
          const embedding = await this.generateEmbedding(texts[i]);
          embeddings.push(embedding);

          if ((i + 1) % 10 === 0 || i === texts.length - 1) {
            console.log(`📊 Progress: ${i + 1}/${texts.length}`);
          }
        } catch (error) {
          console.error(`❌ Failed to generate embedding for text ${i + 1}:`, error.message);
          embeddings.push(null);
        }
      }

      console.log(`✅ Generated ${embeddings.filter(e => e !== null).length}/${texts.length} embeddings`);
      return embeddings;
  }
}

При инициализации классу необходимо передать несколько параметров:

  • apiKey - ключ созданный для сервисного аккаунта от имени которого вы будете обращаться к сервису. Генерируется, например, в консоли Yandex Cloud

  • baseURL - URL сервиса - чаще всего это https://llm.api.cloud.yandex.net/foundationModels/v1/textEmbedding

  • modelUri - указатель на используемую модель. Чаще всего будет emb://${YourYandexCloudFolderID}/text-search-query/latest - но тоже могут быть нюансы (теоретически вы можете развернуть свою версию модели)

  • requestsPerSecond - количество запросов в сек, которые можно отправить к сервису.

Вот об этом чуть подробней. Сервис Яндекса обрабатывает не более 10-ти запросов в секунду. Так что запросы надо выстраивать в очередь. Реализация очереди может быть самой разной. Так как в моем случае проект в стадии "прототипа" - то я просто поставил таймер между запросами, но в production решении конечно надо придумывать что-то более надежное.

Могу сказать что с использованием Yandex Embedder-а качество ответов, которые стала выдавать мне программа стало прям на порядок лучше.

Надеюсь, этот пример будет кому-то полезен и сэкономит время. Только учтите - это именно пример на уровне прототипа.