филтриране и сортиране въз основа на агрегат с Cloudant/CouchDB верижно намаляване на картата

Бих искал да филтрирам списък и да го сортирам въз основа на агрегат; нещо, което е доста лесно за изразяване в SQL, но съм озадачен относно най-добрия начин да го направя с итеративно Map Reduce. Специално използвам добавката "dbcopy" на Cloudant към CouchDB, но мисля, че подходът може да е подобен с други архитектури за карта/намаляване.

Псевдокодът SQL може да изглежда така:

SELECT   grouping_field, aggregate(*)
FROM     data
WHERE    #{filter}
GROUP BY grouping_field
ORDER BY aggregate(*), grouping_field
LIMIT    page_size

Филтърът може да търси съвпадение или може да търси в диапазон; напр. field in ('foo', 'bar') или field between 37 and 42.

Като конкретен пример, помислете за набор от имейли; полето за групиране може да бъде "List-id", "Sender" или "Subject"; агрегатната функция може да бъде count(*), или max(date) или min(date); и филтърната клауза може да вземе предвид флагове, диапазон от дати или идентификатор на пощенска кутия. Документите може да изглеждат така:

{
  "id": "foobar", "mailbox": "INBOX", "date": "2013-03-29",
  "sender": "[email protected]", "subject": "Foo Bar"
}

Получаването на брой имейли с един и същ подател е тривиално:

"map": "function (doc) { emit(doc.sender, null) }",
"reduce": "_count"

И Cloudant има добър пример за сортиране по брой при второто преминаване на намаляване на картата. Но когато искам да филтрирам (например по пощенска кутия), нещата бързо се объркват.

Ако добавя филтъра към ключовете за преглед (напр. крайният резултат изглежда като {"key": ["INBOX", 1234, "[email protected]"], "value": null}, тогава е тривиално да сортирате по брой в рамките на една стойност на филтъра. Но сортирането на тези данни по брой с множество филтри ще изисква преминаване през целия набор от данни (на ключ), което е твърде бавно при големи набори от данни.

Или мога да създам индекс за всеки потенциален избор на филтър; напр. крайният резултат изглежда като {"key": [["mbox1", "mbox2"], 1234, "[email protected]"], "value": null}, (когато са избрани и „mbox1“ и „mbox2“) или {"key": [["mbox1"], 1234, "[email protected]"], "value": {...}}, (когато е избрано само „mbox1“). Това е лесно за запитване и бързо. Но изглежда, че размерът на диска на индекса ще нараства експоненциално (с броя на отделните филтрирани полета). И изглежда, че е напълно несъстоятелно за филтриране на отворени данни, като диапазони от дати.

И накрая, мога динамично да генерирам изгледи, които обработват желаните филтри в движение, само при необходимост, и да ги разруша, след като вече не се използват (за да спестя дисково пространство). Недостатъците тук са огромен скок в сложността на кода и големи предварителни разходи всеки път, когато се избира нов филтър.

Има ли по-добър начин?


person nicholas a. evans    schedule 03.04.2013    source източник


Отговори (1)


Мислех за това от почти ден и смятам, че няма по-добър начин да се направи това от това, което предложихте. Предизвикателствата, пред които сте изправени, са следните:

1) Работата по агрегиране (броене, сума и т.н.) може да се извърши само в CouchDB/Cloudant API чрез механизма за материализиран изглед (mapreduce).

2) Докато API на group_level предоставя известна гъвкавост за указване на променлива детайлност по време на заявка, той не е достатъчно гъвкав за произволни булеви заявки.

3) Възможни са произволни булеви заявки в Cloudant API чрез базирания на lucene _search API. Приложният програмен интерфейс (API) _search обаче не поддържа заявка за post агрегиране. Ограничената поддръжка за това, което искате да направите, е възможна само в lucene, използвайки фасетиране, което все още не се поддържа в Cloudant. Дори тогава вярвам, че може да поддържа само count, а не sum или по-сложни агрегации.

Мисля, че най-добрият вариант, с който се сблъсквате, е да използвате _search API и да използвате sort, group_by или group_sort и след това да извършите агрегиране на клиента. Няколко примерни URL адреса за тестване биха изглеждали така:

GET /db/_design/ddoc/_search/indexname?q=name:mike И възраст:[1.2 ДО 4.5]&sort=["age","name"]

ВЗЕМЕТЕ /db/_design/ddoc/_search/indexname?q=име:майк И group_by="пощенска кутия" И group_sort=["age","name"]

person Mike Miller    schedule 04.04.2013
comment
Страхувах се от това. След като експериментирах с Cloudant _search API вчера, стигнах до заключението, че цялото агрегиране се е случило преди заявка, така че нямаше начин да сортирам комбинираната сума. - person nicholas a. evans; 05.04.2013
comment
Също така, не съм сигурен дали запитването към _search API и извършването на агрегиране на клиента е по-добро от това, което правим сега, което е създаване на изглед на карта/намаляване на първо преминаване и агрегиране на клиента. За малки или средни набори от данни това работи добре, но за големи набори от данни (напр. десетки хиляди уникални субекти или податели) може да бъде доста бавно. - person nicholas a. evans; 05.04.2013
comment
Не съм много запознат с фасетирането в lucene. Но дори и да поддържа само count, вероятно бихме могли да накараме това да работи за най-важния случай (сортиране по брой). - person nicholas a. evans; 05.04.2013
comment
re: Не съм сигурен дали запитването към _search API и извършването на агрегиране на клиента е по-добро от това, което правим сега, а именно създаване на изглед на карта/намаляване на първо преминаване и агрегиране на клиента; Като се замисля, вероятно би било много по-добре за работа с филтри за диапазон (напр. дати). - person nicholas a. evans; 05.04.2013