Помогите составить агрегирующий запрос с фильтром


(Alkalit) #1

Здравствуйте. Я уже задавал этот вопрос неделю назад на английском языке, но ответа не получил, а он очень нужен. Мне надо составить запрос на агрегирование с фильтрованием.

Дано - есть 3 таблицы базы данных - Объявление, Фильтр и Категория Фильтра (Ad, Filter, FilterCategory соответственно). Версия эластика - 2.3
Ad имеет связь m2m с Filter и с Filter прооброшен fk на FilterCategory. Т.е. у объявления и категории может быть множество фильтров. Объявления проиндексированы в эластик. Вот пример документа с продакшена http://pastebin.com/hY6aneyz

где фильтры объявления обозначены полями - filters_ids, filters_names и filters_slugs
а категории - filter_categories_ids, filter_categories_names и filter_categories_slugs

Надо - отфильтровать и агрегировать объявления по запросу с примерно таким псевдокодом:

WHERE (filter = 'othodyi' OR filter = 'pervichka') AND (category = 'po-forme' AND category = 'po-vidu')

Т.е. у объявления обязательно должны присутствовать все категории и любые из фильтров

P.S. на самом деле все еще немного сложнее - на проекте с эластиком работаем не напрямую, а через "обертку" - https://github.com/django-haystack/django-haystack
Но в вопросе это не принципиально.


(Igor Motov) #2

Если filters_slags и filters_categories_slugs проиндексированы как not_analyzed, то фильтр может выглядеть примерно так

{
  "query": {
    "constant_score" : {
      "filter" : {
        "bool" : {
          "should": [
             {"term": {"filters_slugs" : "othodyi"}},
             {"term": {"filters_slags" : "pervichka"}}
          ],
         "must": [
             {"term": {"filter_categories_slugs" : "po-forme"}},
             {"term": {"filter_categories_slugs" : "po-vidu"}}
         ]
        }
      }
    }
  }
}

(Alkalit) #3

Благодарю за быстрый ответ. Запрос работает, но дает не тот результат который я ожидал, поэтому мне придется задать вопрос по-другому.

Мне надо реализовать выборку объявлений по их классификации. Например, если у нас есть такие объявления:

Объявление 1
    по-цвету
        красный
        черный 
    по-форме
        гранула
        порошок
        крошка

Объявление 2
    по-цвету
        красный 

Объявление 3
    по-форме
        гранула
        крошка

Объявление 4
    по-цвету
        белый 
    по-типу
        пп

Тогда изначальная выборка по ним будет выглядеть так:

по-форме
    гранула 2
    крошка 2
    порошок 1

по-цвету
    красный 2
    черный 1
    белый 1

по-типу
    пп 1

Если юзер выберет "крошка", то "по-форме" не изменится, но "по-цвету" будут только "красный" и "черный" - 1, т.к. только одно объявление соответствует каждому из них, и категория "по-типу" не войдет в выборку. Соответственно мне нужен запрос который это выполнит.


(Igor Motov) #4

Я не понимаю, как из вашего документа в примере вытащить эти данные:

Объявление 1
    по-цвету
        красный
        черный 
    по-форме
        гранула
        порошок
        крошка

(Alkalit) #5

Это поля filters_names и filter_categories_names. Или вы имеете в виду что в таком виде документ не пригоден для реализации такого поиска?


(Igor Motov) #6

У вас документ в индексе представлен в виде

Объявление 1
    категории
        по-цвету
        по-форме
    фильтры 
        гранула
        красный
        крошка
        порошок
        черный 

Я не понимаю, как из этого сделать вот это

Объявление 1
    по-цвету
        красный
        черный 
    по-форме
        гранула
        порошок
        крошка

Другими словами, где храниться информация, что порошок это форма и красный - это цвет?


(Alkalit) #7

Понял вас. Добавил поле classification. Для документа из оппоста оно будет выглядеть так:

    "classification": [
      {
        "filters_slugs": [
          "othodyi",
          "vtorichka"
        ],
        "category_id": 1
      },
      {
        "filters_slugs": [
          "pnd",
          "pvd",
          "pvh",
          "abs",
          "pp",
          "pa",
          "ps",
          "polietilen",
          "lpvd"
        ],
        "category_id": 3
      },
      {
        "filters_slugs": [
          "ekstruziya",
          "lite",
          "formovanie"
        ],
        "category_id": 5
      },
      {
        "filters_slugs": [
          "krasnyij",
          "belyij",
          "chernyij",
          "sinij",
          "zelenyij",
          "zheltyij"
        ],
        "category_id": 7
      }
    ],

В маппинге:

    "classification": {
        "type": "nested",
        "properties": {
          "category_id": {
            "type": "long"
          },
          "filters_slugs": {
            "type": "string",
            "index": "not_analyzed"
          }
        }
      },

(Igor Motov) #8

В этом случае, можно сделать что-нибудь вроде:

{
  "size": 0,
  "aggs": {
    "categories": {
      "nested": {
        "path": "classification"
      },
      "aggs": {
        "category_id": {
          "terms": {
            "field": "classification.category_id"
          },
          "aggs": {
            "filters_id": {
              "terms": {
                "field": "classification.filters_slugs"
              }
            }
          }
        }
      }
    }
  }
} 

Но если категории не часто обновляются, и их количество не очень большое, то более оптимальное решение было бы создать отдельные поля для каждой категории. Тогда можно было бы обойтись без nested.


(Alkalit) #9

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

А как в таком случае будет выглядеть запрос? Насколько я понимаю, псевдокод будте примерно таким:

WHERE ((classification.category_id=category1 AND classification.filters_slugs=filter1) OR (classification.category_id=category1 AND classification.filters_slugs=filter2)) AND (classification.category_id=category2 AND classification.filters_slugs=filter3)

Пытаюсь написать такой запрос: http://pastebin.com/QaKCSuLj

но он не отрабатывает из-за nested части.


(Igor Motov) #10

nested не на том уровне. Ваш запрос пытается найти одну классификацию, которая одновременно и в 6-й и в 7-й категории.

{
  "query": {
    "constant_score":{
      "filter":{
        "bool": {
          "must": [
            {"term": {"ad_type": "sell"} },
            {"term": {"category_slug": "syire"} },
            {
              "bool": {
                "must": [
                  {
                    "nested": {
                      "path": "classification",
                      "query": {
                        "bool":{
                          "must":[
                             {"term":{"classification.category_id":6}},
                             {"term":{"classification.filters_slugs":"granula"}}
                          ]
                        }
                      }
                    }
                  },
                  {
                   "bool":{
                     "should":[
                        {
                          "nested": {
                            "path": "classification",
                            "query": {
                             "bool":{
                                "must":[
                                   {"term":{"classification.category_id":7 }},
                                   {"term":{"classification.filters_slugs":"chernyij" }}
                                ]
                              }
                            }
                          }
                        },
                        {
                          "nested": {
                            "path": "classification",
                            "query": {
                             "bool":{
                                "must":[
                                   {"term":{"classification.category_id":7}},
                                   {"term":{"classification.filters_slugs": "belyij"}}
                                ]
                             }
                            }
                          }
                        }
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
  }
}

(Alkalit) #11

Боюсь что в Лапландском королевстве все опять не так как надо. Запрос работает и в при выборе фильтров он показывает действительные числа. Проблема с теми фильтрами которые не были выделены и соответственно не вошли в запрос - они "проседают".
Покажу на примере:
стартовая выборка

и здесь мы выбрали несколько фильтров.

У нас действительно 758 объявлений с тегами "белый" и "гранула" и 1819 объявлений с тегами "гранула" и "белый" или "черный". Но например объявлений с "красный" и "гранула" в действительности 62.

Мне надо реализовать этот функционал аналогично заппос , например как на этой странице.

P.S. Извините что сразу с этого не начал.


(Igor Motov) #12

Посмотрите документацию для terms тут.


(Alkalit) #13

Прочел. Потом заметил что ссылка для 1.7 и прочел ту же статью для 2.3. Я так понимаю вы намекаете на Multi-field terms aggregationedit ? Но я не понимаю какое отношение это имеет к моей проблеме.


(Igor Motov) #14

Я, наверное, опять не понял Ваш вопрос. Вы спросили почему

Я прислал ссылку, которая объясняет почему document counts are approximate и как с этим бороться.


(Alkalit) #15

Хорошо, тогда я попробую начать с самого начала и подробнее. У нас есть 3 таблицы: "Объявление" с m2m на "Фильтр" с fk на "Категория фильтра".
На работе дали задание сделать фасетную навигацию по объявлениям с учетом того как они отклассифицированы (это фильтры и их категории), по образу популярных коммерческих площадок. Например:

http://www.ozon.ru/catalog/1137927/?newperiod=4
http://www.tehnosila.ru/catalog/kompjutery_i_orgtehnika/monitory_lcd

И вот тут-то и начинаются проблемы. Как человек привыкший к sql, я даже не могу точно описать принцип по которому там происходит выборка.
Т.е. в общих словах это действительно "ИЛИ" внутри фасетов и "И" между ними. Но мне не очевидно почему, например у той же техносилы, при выборе пары брендов в мониторах "проседать" начинают другие фасеты, кроме текущего. Я только знаю что это реализовано с использованием эластика или солра.

Запрос который вы мне помогли составить похож на то что мне нужно, но он правильно работает только для выбранных фильтров. Т.е. возвращаясь к предыдущему своему посту, "красный", как я понял, отображает количество объявлений из тех где есть "гранула" и ("белый" или "черный"). А должен отображать количество объявлений где есть "красный" и "гранула", даже при том что "красный" не выделен.

P.S. Про document counts are approximate я знаю и везде где это критично стоит size: 0.


(Igor Motov) #16

Я все равно не понимаю, что вы хотите. Может быть это? Если нет, вы не могли бы привести какой-нибудь простой пример - вот 10 записей, вот поля в них, вот запрос, вот что я хочу видеть?


(Yan_Work) #17

Использовать составной запрос

  • запрос_для вытаскивания товаров, фильтры - А, Б, В
  • глобальная агрегация, сбрасывающая скоуп, в нее попадает все
  • внутри глобальной агрегации три фильтрующих под-агрегации
  • одна из них имеет фильтром Б, В (все кроме А), при этом она делает term по A
  • вторая их них имеет фильтром А, В, при это она делает агрегацию по Б
  • третья имеет фильтром Б, В, при этом она делает агрегацию по В

итого запрос выглядит примерно так
Q = A, B, C

  • GA
    • FA
      • filter B C
      • term A
    • FB
      • filter A C
      • term B
    • FC
      • filter A B
      • term C

Итого, допустим у нас 5 документов
док1 = красная большая машина
док2 = красный большой дом
док3 = красный большой квадрат
док4 = красный маленький дом
док5 = желтый большой дом

запрос = красный + большой + дом
глобальная агрегация
агрегация по цвету = ищется в рамках размер + тип
агрегация по размеру = ищется в рамках цвет + тип
агрегация по типу = ищется в рамках цвет + размер

итого, в результате будет
тотал = 1
хит = док2 = красный большой дом
агрегация по цвету = красный (1) + желтый (1) (потому что ищет большие дома)
агрегация по размеру = большой (1) + маленький (1) (потому что ищет красные дома)
агрегация по типу = дом (1) + квадрат (1) (потому что ищет красный + большой)

Если непонятно, задавайте уточняющий вопрос


(Yan_Work) #18

Это все звучит довольно громоздко, но в некоторых случаях кластер справляется с такими запросами приемлемо (тестируйте)


(system) #19