Map-Reduce Query для подсчета тегов

У меня есть база данных документов, помеченных ключевыми словами. Я пытаюсь найти (а затем подсчитать) уникальные теги, которые используются рядом друг с другом. Итак, для любого данного тега я хочу знать, какие теги использовались вместе с этим тегом.

Например, если бы у меня был один документ с тегами [fruit, apple, plant], то при запросе [apple] я должен получить [fruit, plant]. Если в другом документе есть теги [apple, banana], то мой запрос для [apple] вместо этого даст мне [fruit, plant, banana].

Это моя функция карты, которая выдает все теги и их соседей:

function(doc) {
  if(doc.tags) {
    doc.tags.forEach(function(tag1) {
      doc.tags.forEach(function(tag2) {
        emit(tag1, tag2);
      });
    });
  }
}

Итак, в моем примере выше он будет излучать

apple -- fruit
apple -- plant
apple -- banana
fruit -- apple
fruit -- plant
...

Мой вопрос: какой должна быть моя функция сокращения? Функция сокращения должна по существу отфильтровывать дубликаты и группировать их все вместе.

Я пробовал несколько разных попыток, но мой сервер базы данных (CouchDB) продолжает выдавать ошибку: reduce_overflow_error. Сокращение объема производства должно сокращаться быстрее.


EDIT: я нашел что-то, что работает, но я не уверен, почему. Я вижу, что есть необязательный параметр "rereduce" для вызова функции сокращения. Если я проигнорирую эти особые случаи, то он перестанет выдавать reduce_overflow_errors. Кто-нибудь может объяснить, почему? И еще, должен ли я просто игнорировать их, или это потом укусит меня за задницу?

function(keys, values, rereduce) {
  if(rereduce) return null; // Throws error without this.

  var a = [];
  values.forEach(function(tag) {
    if(a.indexOf(tag) < 0) a.push(tag);
  });
  return a;
}

person Dave    schedule 02.05.2012    source источник
comment
Как сказал Джейсон, вы не должны использовать функцию сокращения для построения массива значений. Результат сокращения должен иметь небольшой и предсказуемый размер. С if(rereduce) return null вы выбрасываете часть данных в унитаз.   -  person Marcello Nuccio    schedule 03.05.2012


Ответы (2)


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

CouchDB любит длинные списки, а не толстые списки. Вместо строк представления, хранящих массив со всеми предыдущими тегами, когда-либо увиденными, это решение сохраняет теги «близнецов» в ключе строк представления, а затем сгруппируйте их вместе, чтобы гарантировать один уникальный родственный тег для каждой строки. Каждая строка состоит всего из двух тегов, но могут быть тысячи или миллионы строк: длинный список, который предпочитает CouchDB.

Основная идея состоит в том, чтобы создать 2-массив пар тегов. Предположим, у нас есть один документ с тегом fruit, apple, plant.

// Pseudo-code visualization of view rows (before reduce)
// Key         , Value
[apple, fruit ], 1
[apple, plant ], 1 // Basically this is every combination of 2 tags in the set.
[fruit, apple ], 1
[fruit, plant ], 1
[plant, apple ], 1
[plant, fruit ], 1

Затем я помечаю что-то apple, banana.

// Pseudo-code visualization of view rows (before reduce)
// Key         , Value
[apple, banana], 1 // This is from my new doc
[apple, fruit ], 1
[apple, plant ], 1 // This is also from my new doc
[banana, apple], 1
[fruit, apple ], 1
[fruit, plant ], 1
[plant, apple ], 1
[plant, fruit ], 1

Почему значение всегда 1? Потому что я могу сделать очень простую встроенную функцию сокращения: _sum чтобы сообщить мне количество всех пар тегов. Затем запрос с ?group_level=2 и CouchDB даст вам уникальные пары с подсчетом их общего количества.

Функция карты для создания такого представления может выглядеть так:

function(doc) {
  // Emit "sibling" tags, keyed on tag pairs.
  var tags = doc.tags || []
  tags.forEach(function(tag1) {
    tags.forEach(function(tag2) {
      if(tag1 != tag2)
        emit([tag1, tag2], 1)
    })
  })
}
person JasonSmith    schedule 03.05.2012
comment
Блестяще, спасибо, Джейсон. Это намного элегантнее и дает мне счет. Спасибо за совет, в будущем постараюсь следовать принципу длинного списка :) - person Dave; 03.05.2012
comment
Я вижу, что я также могу запросить свое представление для одного ключа, используя этот трюк ?group=true&startkey=["apple"]&endkey=["apple",{}]. - person Dave; 03.05.2012
comment
Да, или вы также можете сделать ?group_level=1&key=["apple"], который объединит все теги Apple. CouchDB расслаблен. - person JasonSmith; 04.05.2012

Я нашел правильное решение, которым я намного доволен. Хитрость заключалась в том, что CouchDB должен быть установлен на reduce_limit = false, чтобы он перестал проверять свою эвристику по вашему запросу.

Вы можете установить это через Futon на http://localhost:5984/_utils/config.html в разделе настройки query_server_config, дважды щелкнув значение.

Как только это будет сделано, вот моя новая функция карты, которая лучше работает с «повторным уменьшением» части функции сокращения:

function(doc) {
  if(doc.tags) {
    doc.tags.forEach(function(tag1) {
      doc.tags.forEach(function(tag2) {
        emit(tag1, [tag2]); // Array with single value
      });
    });
  }
}

А вот функция сокращения:

function(keys, values) {
  var a = [];
  values.forEach(function(tags) {
    tags.forEach(function(tag) {
      if(a.indexOf(tag) < 0) a.push(tag);
    });
  });
  return a;
}

Надеюсь, это поможет кому-то!

person Dave    schedule 02.05.2012
comment
Благодаря этому комментарию, который мне помог: Couchdb%23comment6920983_5460779"> stackoverflow.com/questions/5456682/ - person Dave; 02.05.2012
comment
Это решение в конечном итоге не будет масштабироваться. Функции сокращения должны уменьшать объем данных, а не просто переформатировать их (поддерживая постоянно растущий массив). Тем не менее, CouchDB расслаблен, поэтому я рекомендую вам придерживаться этого, если вы знаете, что это не будет проблемой. Я опубликую ответ с альтернативой, которую вы можете оставить себе на потом, если вам когда-нибудь понадобится вернуться к этому. - person JasonSmith; 03.05.2012