Статьи

Redis и Lua: мощная лошадь NoSQL

Недавно я начал внедрять ряд решений на базе Redis для клиентов Datablend. Redis часто называют базой данных NoSQL от Swiss Army Knife и по праву заслуживает этого звания. По своей сути это хранилище ключей в памяти . Значения, которые назначаются ключам, могут быть «структурированы» с помощью строк, хэшей, списков, наборов и отсортированных наборов. Мощь этих простых структур данных в сочетании с интуитивно понятным API-интерфейсом делает Redis действительно мощным средством решения различных проблем, связанных с «большими данными». Чтобы проиллюстрировать это, я повторно реализовал свой поиск молекулярного сходства на основе MongoDB через Redis и его интегрированную поддержку Lua. Как всегда, полный исходный код можно найти наDatablend публичный репозиторий GitHub .

1. Redis «Отпечаток пальца» Модель данных

Молекулярное сходство относится к сходству с химическими соединений по отношению к их структурным и / или функциональным качествам. Вычисляя молекулярные сходства, Cheminformatics может помочь в разработке новых лекарств, просматривая большие базы данных для потенциально интересных химических соединений. Химическое сходство может быть определено посредством использования так называемых отпечатков пальцев (то есть линейных химических структур субструктуры определенной длины). Сходство между соединениями определяется путем расчета коэффициента Танимото . Это вычисление включает в себя вычисление пересечений между наборами отпечатков пальцев, эта операция изначально поддерживается Redis.

Наша модель данных Redis для хранения отпечатков пальцев требует трех различных структур данных:

  1. Для каждого соединения , идентифицируемого уникальным ключом , мы храним его набор отпечатков пальцев (где каждый отпечаток снова идентифицируется уникальным ключом).
  2. Для каждого отпечатка пальца , идентифицируемого уникальным ключом , мы храним набор соединений, содержащих этот отпечаток. Эти наборы отпечатков пальцев могут восприниматься как инвертированные индексы составных наборов, упомянутых выше.
  3. Для каждого отпечатка пальца мы храним его количество вхождений через специальную клавишу веса .

Отпечатки пальцев рассчитываются с использованием библиотеки jCompoundMapper . Библиотека Jedis (Java Redis Client) используется для сохранения структур данных, упомянутых выше. Трех простых атомарных операций (строки 30, 33 и 35) достаточно для создания как инвертированных индексов (составные-> отпечатки пальцев и отпечатки-> составные), так и приращения сопровождающих счетчиков .

RandomAccessMDLReader reader = new RandomAccessMDLReader(new File(...));
EncodingFingerprint fingerprinter = new Encoding2DMolprint();

// We will use a pipeline in order to speedup the persisting process
Pipeline p = jedis.pipelined();

// Iterate the compounds one by one
for (int i = 0; i < reader.getSize(); i++) {

  // Retrieve the molecule and the fingerprints for this molecule
  Molecule molecule = reader.getMol(i);
  FeatureMap fingerprints = new FeatureMap(fingerprinter.getFingerprint(molecule));

  // Retrieve some of the compound properties we want to use later on
  String compound_cid = (String)molecule.getProperty("PUBCHEM_COMPOUND_CID");

  // Iterate the fingerprints
  for (IFeature fingerprint : fingerprints.getKeySet()) {

    // Check whether we already encountered the feature and create accordingly (
    String thefeaturestring = fingerprint.featureToString();
    if (!fingerprintlist.contains(thefeaturestring)) {
      fingerprintlist.add(thefeaturestring);
    }

    // Get the index of the fingerprint
    int fingerprintindex = fingerprintlist.indexOf(thefeaturestring);

    // Increment the weight of this fingerprint (number of occurences)
    p.incr(fingerprintindex + ":w");
    // Create the inverted indexes
    // Add the fingerprint to the set of fingerprints of this compound
    p.sadd((compound_cid + ":f"), fingerprintindex + "");
    // Add the compound to the set of compounds of this fingerprint
    p.sadd(fingerprintindex + ":c", compound_cid + "");

  }
  // Sync the changes
  p.sync();
}

2. Поиск похожих химических соединений

Для получения соединений, которые удовлетворяют определенному коэффициенту Танимото, мы повторно используем те же принципы, которые изложены в моей первоначальной статье MongoDB . Количество обращений к хранилищу данных Redis сводится к минимуму за счет реализации алгоритма с помощью встроенной поддержки сценариев Lua . Мы начнем с получения количества отпечатков пальцев конкретного входного соединения. Основываясь на этой мощности, мы вычисляем интересующие нас отпечатки пальцев (т.е. минимальный набор отпечатков пальцев, который приводит нас к соединениям, способным удовлетворить коэффициент Танимото). Для этого нам нужно идентифицировать подмножество составных отпечатков пальцев, которые встречаются реже всего во всем наборе данных. Redis позволяет нам выполнить этот запрос с помощью одного вида-command; он принимает составной ключ в качестве ввода и сортирует содержащиеся в нем отпечатки пальцев, используя значение внешних клавиш веса отпечатков пальцев. Из этого отсортированного набора отпечатков пальцев мы отбираем верхние интересующие нас отпечатки пальцев . Какая мощная и элегантная команда!

Мы используем инвертированный индекс (фингерпринт-> соединения), чтобы идентифицировать те соединения, которые способны удовлетворить определенный входной коэффициент Танимото. Применение команды Redis union к вычисленному набору ключей отпечатков пальцев возвращает набор потенциальных соединений. После определения этого набора мы вычисляем сходство, используя команду Redis intersect . Только соединения, которые удовлетворяют ограничению Tanimoto, возвращаются.

-- Retrieve the input paramters
local inputCompound = ARGV[1];
local similarity = ARGV[2];

-- Get the number of fingerprints of the input compound
local countToFind = redis.call('scard', inputCompound .. ':f');

-- Calculate the max, min and number of fingerprints to consider
local maxFingerprints = math.floor(countToFind / similarity);
local minFingerprints = math.floor(countToFind * similarity);
local numberOfFingerprintsToconsider = math.floor(countToFind - minFingerprints);

-- Retrieve the fingerprints of interest by subselecting out of the sorted list of fingerprints based upon the number of occurences
local fingerprintsOfInterest = redis.call('sort', inputCompound .. ':f', 'by', '*:w', 'limit', 0, numberOfFingerprintsToconsider + 1);

-- Create the set of all fingerprints we are interested in (creating the redis keys along the way)
local fingerprintKeys = {};
for index = 1, #fingerprintsOfInterest do
  table.insert(fingerprintKeys, fingerprintsOfInterest[index] .. ':c');
end

-- Retreive the set of possible matching compounds by taking the union of the fingerprint -> compound indexes
local possibleCompounds = redis.call('sunion',unpack(fingerprintKeys));

-- Calculate the tanimoto coefficient for the list of possible matching compounds with the inputcompound
local results = {};
for index = 1, #possibleCompounds do
  -- Check whether the count of fingerprints is within the predefined range
  local count = redis.call('scard', possibleCompounds[index] .. ':f');
  if count >= minFingerprints and count <= maxFingerprints then 
    -- Calculate the matching fingerprints by calculating the intersection
    local intersectCount = redis.call('sinter', inputCompound .. ':f', possibleCompounds[index] .. ':f');
    local tanimoto = #intersectCount / (count + countToFind - #intersectCount);
    -- If sufficient similar, add it the set of results
    if tanimoto >= tonumber(similarity) then 
      table.insert(results,possibleCompounds[index]); table.insert(results, '' .. tanimoto);
    end
  end
end

-- Return the results
return results;

3. Вывод

При хранении 25 000 соединений Redis требуется менее 20 мс для извлечения соединений, которые на 70% похожи на конкретное входное соединение. Snappier по сравнению с моей первоначальной реализацией MongoDB. Кроме того, Redis требуется менее 1 ГБ ОЗУ для поддержания живого индекса 460 000 соединений PubChem , у которых есть хотя бы один связанный анализ. Это позволяет ученому размещать локальный экземпляр хранилища составных данных, эффективно устраняя необходимость в отдельной (и дорогой) настройке составной базы данных.