Как сделать запрос по нескольким условиям в faunadb? - PullRequest
2 голосов
/ 29 апреля 2020

Я пытаюсь улучшить мое понимание FaunaDB.

У меня есть коллекция, которая содержит записи вроде:

{
  "ref": Ref(Collection("regions"), "261442015390073344"),
  "ts": 1587576285055000,
  "data": {
    "name": "italy",
    "attributes": {
      "amenities": {
        "camping": 1,
        "swimming": 7,
        "hiking": 3,
        "culture": 7,
        "nightlife": 10,
        "budget": 6
      }
    }
  }
}

Я бы хотел гибко запрашивать различные атрибуты, такие как:

  • data.attributes.aferences.camping> 5
  • data.attributes.aabilities.camping> 5 AND data.attributes.aabilities.hiking> 6
  • data .attributes.aferences.camping <6 AND data.attributes.aferences.culture> 6 AND hiking> 5 AND ...

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

Мой запасной вариант - создать индекс для каждого атрибута и использовать пересечение для получения записей, которые есть во всех подзапросах, которые я хочу проверить, но это чувствует себя как-то не так:

Запрос: бюджет> = 6 И кемпинг> = 8 будет:

Index:
{
  name: "all_regions_by_all_attributes",
  unique: false,
  serialized: true,
  source: "regions",
  terms: [],
  values: [
    {
      field: ["data", "attributes", "amenities", "culture"]
    },
    {
      field: ["data", "attributes", "amenities", "hiking"]
    },
    {
      field: ["data", "attributes", "amenities", "swimming"]
    },
    {
      field: ["data", "attributes", "amenities", "budget"]
    },
    {
      field: ["data", "attributes", "amenities", "nightlife"]
    },
    {
      field: ["data", "attributes", "amenities", "camping"]
    },
    {
      field: ["ref"]
    }
  ]
}

Запрос:

Map(
  Paginate(
    Intersection(
      Range(Match(Index("all_regions_by_all_attributes")), [0, 0, 0, 6, 0, 8], [10, 10, 10, 10, 10, 10]),
    )

  ),
  Lambda(
    ["culture", "hiking", "swimming", "budget", "nightlife", "camping", "ref"],
    Get(Var("ref"))
  )
)

Этот подход имеет следующий недостатки:

  • не ш ork, как и ожидалось, если, например, первый (культура) атрибут находится в этом диапазоне, а второй (пеший) нет, то я все равно получу возвращаемые значения
  • Это вызывает много чтений из-за ссылки что мне нужно следить за каждым результатом.

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

заранее спасибо

Ответы [ 2 ]

5 голосов
/ 04 мая 2020

Есть несколько заблуждений, которые, я думаю, вводят вас в заблуждение. Наиболее важный из них: Match(Index($x)) генерирует ссылку на набор, который представляет собой упорядоченный набор кортежей. Кортежи соответствуют массиву полей, которые присутствуют в разделе значений индекса. По умолчанию это будет просто один кортеж, содержащий ссылку на документ в коллекции, выбранной индексом. Range работает с ссылкой на набор и не знает ничего о терминах, используемых для выбора возвращаемого набора ref. Итак, как нам составить запрос?

Начиная с первых принципов. Давайте представим, что у нас только что был этот материал в памяти. Если бы у нас был набор (атрибут, баллы), упорядоченный по атрибуту, то в наборе баллов использовались только те, где attribute == $attribute приблизит нас, а затем фильтрация по score > $score даст нам то, что мы хотели. Это точно соответствует диапазонному запросу по оценкам с атрибутами в качестве терминов, при условии, что мы смоделировали пары значений атрибутов как документы. Мы также можем встраивать указатели обратно в местоположение, чтобы мы могли получить их также в том же запросе. Достаточно болтовни, давайте сделаем это:

Первая остановка: наши коллекции.

jnr> CreateCollection({name: "place_attribute"})
{
  ref: Collection("place_attribute"),
  ts: 1588528443250000,
  history_days: 30,
  name: 'place_attribute'
}
jnr> CreateCollection({name: "place"})
{
  ref: Collection("place"),
  ts: 1588528453350000,
  history_days: 30,
  name: 'place'
}

Далее некоторые данные. Мы выберем пару мест и дадим им некоторые атрибуты.

jnr> Create(Collection("place"), {data: {"name": "mullion"}})
jnr> Create(Collection("place"), {data: {"name": "church cove"}})
jnr> Create(Collection("place_attribute"), {data: {"attribute": "swimming", "score": 3, "place": Ref(Collection("place"), 264525084639625739)}})
jnr> Create(Collection("place_attribute"), {data: {"attribute": "hiking", "score": 1, "place": Ref(Collection("place"), 264525084639625739)}}) 
jnr> Create(Collection("place_attribute"), {data: {"attribute": "hiking", "score": 7, "place": Ref(Collection("place"), 264525091487875586)}})

Теперь для более интересной части. Индекс.

jnr> CreateIndex({name: "attr_score", source: Collection("place_attribute"), terms:[{"field":["data", "attribute"]}], values:[{"field": ["data", "score"]}, {"field": ["data", "place"]}]})
{
  ref: Index("attr_score"),
  ts: 1588529816460000,
  active: true,
  serialized: true,
  name: 'attr_score',
  source: Collection("place_attribute"),
  terms: [ { field: [ 'data', 'attribute' ] } ],
  values: [ { field: [ 'data', 'score' ] }, { field: [ 'data', 'place' ] } ],
  partitions: 1
}

Ок. Простой запрос. У кого есть Пешие прогулки?

jnr> Paginate(Match(Index("attr_score"), "hiking"))
{
  data: [
    [ 1, Ref(Collection("place"), "264525084639625730") ],
    [ 7, Ref(Collection("place"), "264525091487875600") ]
  ]
}

Без особой фантазии можно было бы проколоть вызов Get, чтобы вытащить место.

А как насчет только пешего туризма со счетом более 5? У нас есть упорядоченный набор кортежей, поэтому достаточно указать первый компонент (счет), чтобы получить то, что мы хотим.

jnr> Paginate(Range(Match(Index("attr_score"), "hiking"), [5], null))
{ data: [ [ 7, Ref(Collection("place"), "264525091487875600") ] ] }

Как насчет составного условия? Походы до 5 лет и плавание (любая оценка). Это где вещи принимают поворот. Мы хотим смоделировать соединение, которое в фауне означает пересекающиеся множества. Проблема, которая у нас есть, заключается в том, что до сих пор мы использовали индекс, который возвращает счет, а также место ссылки. Для пересечения на работу нам нужны только ссылки. Время ловкости рук:

jnr> Get(Index("doc_by_doc"))
{
  ref: Index("doc_by_doc"),
  ts: 1588530936380000,
  active: true,
  serialized: true,
  name: 'doc_by_doc',
  source: Collection("place"),
  terms: [ { field: [ 'ref' ] } ],
  partitions: 1
}

Какой смысл такого индекса вы спрашиваете? Ну, мы можем использовать его для удаления любых данных, которые нам нравятся, из любого индекса и оставляем только ссылки через соединение. Это дает нам ссылки на места с рейтингом похода менее 5 (пустой массив сортируется раньше всего, поэтому работает как заполнитель для нижней границы).

jnr> Paginate(Join(Range(Match(Index("attr_score"), "hiking"), [], [5]), Lambda(["s", "p"], Match(Index("doc_by_doc"), Var("p")))))
{ data: [ Ref(Collection("place"), "264525084639625739") ] }

Итак, наконец, часть сопротивления: все места с swimming and (hiking < 5):

jnr> Let({
...   hiking: Join(Range(Match(Index("attr_score"), "hiking"), [], [5]), Lambda(["s", "p"], Match(Index("doc_by_doc"), Var("p")))),
...   swimming: Join(Match(Index("attr_score"), "swimming"), Lambda(["s", "p"], Match(Index("doc_by_doc"), Var("p"))))
... },
... Map(Paginate(Intersection(Var("hiking"), Var("swimming"))), Lambda("ref", Get(Var("ref"))))
... )
{
  data: [
    {
      ref: Ref(Collection("place"), "264525084639625739"),
      ts: 1588529629270000,
      data: { name: 'mullion' }
    }
  ]
}

Тада. Это может быть очень полезно с парой UDF, упражнение оставлено читателю. Условиями, включающими or, можно управлять с помощью union почти таким же образом.

4 голосов
/ 06 мая 2020

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

FQL FaunaDB довольно мощный, что означает, что есть несколько способов сделать это, но с такой мощью приходит небольшая кривая обучения, поэтому я рад помочь :). Причина, по которой потребовалось некоторое время, чтобы ответить на этот вопрос, заключается в том, что такой сложный ответ действительно заслуживает полного поста в блоге. Ну, я никогда не писал пост в блоге в Stack Overflow, есть первый для всего!

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

Подготовка. Давайте добавим некоторые данные, как это делал Бен

. Я сохраню их в одной коллекции, чтобы упростить их, и здесь я использую разновидность JavaScript языка запросов фауны. Существует веская причина для разделения данных во второй коллекции, которая связана с вашей второй картой / вопросом о получении (см. Конец этого ответа)

Создать коллекцию

 CreateCollection({ name: 'place' })

Бросить в некоторых данных

    Do(
      Select(
        ['ref'],
        Create(Collection('place'), {
          data: {
            name: 'mullion',
            focus: 'team-building',
            camping: 1,
            swimming: 7,
            hiking: 3,
            culture: 7,
            nightlife: 10,
            budget: 6
          }
        })
      ),
      Select(
        ['ref'],
        Create(Collection('place'), {
          data: {
            name: 'church covet',
            focus: 'private',
            camping: 1,
            swimming: 7,
            hiking: 9,
            culture: 7,
            nightlife: 10,
            budget: 6
          }
        })
      ),
      Select(
        ['ref'],
        Create(Collection('place'), {
          data: {
            name: 'the great outdoors',
            focus: 'private',
            camping: 5,
            swimming: 3,
            hiking: 2,
            culture: 1,
            nightlife: 9,
            budget: 3
          }
        })
      )
    )

ВАРИАНТ 1: Составные индексы с несколькими значениями

Мы можем поместить столько значений, сколько значений в индексе, и использовать Match и Диапазон для запроса тех. Однако! Диапазон, вероятно, дает вам нечто иное, чем вы ожидаете, если вы используете несколько значений. Диапазон дает вам именно то, что делает индекс, и индекс сортирует значения лексически. Если мы посмотрим на пример Range в документах, мы увидим пример, который мы можем расширить для нескольких значений.

Представьте, что у нас будет индекс с двумя значениями, и мы напишем:

    Range(Match(Index('people_by_age_first')), [80, 'Leslie'], [92, 'Marvin'])

Тогда результатом будет то, что вы видите слева, а не то, что вы видите справа. Это очень масштабируемое поведение, которое демонстрирует необработанную мощность без издержек на базовый индекс, но не совсем то, что вы ищете!

Range indexes behaviour

Итак, давайте перейдем к другому решению!

ВАРИАНТ 2: Сначала диапазон, затем фильтр

Другой Довольно гибким решением является использование Range, а затем Filter. Это, однако, не очень хорошая идея, если вы много фильтруете с помощью фильтра, поскольку ваши страницы станут более пустыми. Представьте, что у вас есть 10 элементов на странице после «Диапазона» и вы используете фильтр, и в итоге вы получите страницы из 2, 5, 4 элементов в зависимости от того, что отфильтровано. Однако это отличная идея, если одно из этих свойств имеет такую ​​высокую мощность, что оно отфильтрует большинство объектов. Например, представьте, что все помечено временем, вы хотите сначала получить диапазон дат, а затем продолжить фильтровать что-то, что исключит лишь небольшой процент из набора результатов. Я считаю, что в вашем случае все эти значения довольно равны, так что это третье решение (см. Ниже) будет лучшим для вас.

В этом случае мы можем просто добавить все значения, чтобы они all были возвращены, что позволяет избежать Get. Например, скажем, что «кемпинг» является нашим самым важным фильтром.

    CreateIndex({
      name: 'all_camping_first',
      source: Collection('place'),
      values: [
        { field: ['data', 'camping'] },
        // and the rest will not be used for filter
        // but we want to return them to avoid Map/Get
        { field: ['data', 'swimming'] },
        { field: ['data', 'hiking'] },
        { field: ['data', 'culture'] },
        { field: ['data', 'nightlife'] },
        { field: ['data', 'budget'] },
        { field: ['data', 'name'] },
        { field: ['data', 'focus'] },
      ]
    })

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

    Paginate(Range(Match('all_camping_first'), [1], [3]))

, который должен возвращать два элемента (третий имеет кемпинг === 5) Теперь представьте, что мы хотим отфильтровать их, и мы устанавливаем маленькие страницы, чтобы избежать ненужной работы.

    Filter(
      Paginate(Range(Match('all_camping_first'), [1], [3]), { size: 2 }),
      Lambda(
        ['camping', 'swimming', 'hiking', 'culture', 'nightlife', 'budget', 'name', 'focus'],
        And(GTE(Var('hiking'), 0), GTE(7, Var('hiking')))
      )
    )

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

    Create(Collection('place'), {
      data: {
        name: 'the safari',
        focus: 'team-building',
        camping: 1,
        swimming: 9,
        hiking: 2,
        culture: 4,
        nightlife: 3,
        budget: 10
      }
    })

Выполнение того же запроса:

    Filter(
      Paginate(Range(Match('all_camping_first'), [1], [3]), { size: 2 }),
      Lambda(
        ['camping', 'swimming', 'hiking', 'culture', 'nightlife', 'budget', 'name', 'focus'],
        And(GTE(Var('hiking'), 0), GTE(7, Var('hiking')))
      )
    )

Теперь по-прежнему возвращает только одно значение , но предоставляет вам ' после курсора, который указывает на следующую страницу . Вы можете подумать: «А? Мой размер страницы был 2?». Ну, это потому, что фильтр работает после Пагинации, и ваша страница изначально имела две сущности, из которых одна была отфильтрована. Таким образом, у вас остается страница со значением 1 и указатель на следующую страницу.


{
  "after": [
    ... 
  ],
  "data": [
    [
      1,
      7,
      3,
      7,
      10,
      6,
      "mullion",
      "team-building"
    ]
  ]

ВАРИАНТ 3: Индексы для одного значения + Пересечения!

Это лучшее решение для вашего варианта использования, но оно требует немного большего понимания и промежуточного индекса.

Когда мы смотрим на примеры do c для пересечения , мы видим этот пример:

    Paginate(
       Intersection(
          Match(q.Index('spells_by_element'), 'fire'),
          Match(q.Index('spells_by_element'), 'water'),
       )
    ) 

Это работает, потому что это в два раза больше одного индекса и это означает что ** результаты являются аналогичными значениями ** (ссылки в этом случае). Допустим, мы добавили два индекса.

    CreateIndex({
      name: 'by_camping',
      source: Collection('place'),
      values: [
        { field: ['data', 'camping']}, {field:  ['ref']}
      ]
    })

    CreateIndex({
      name: 'by_hiking',
      source: Collection('place'),
      values: [
        { field: ['data', 'swimming']}, {field:  ['ref']} 
      ]
    })

Мы можем пересечь их сейчас , но это не даст нам правильный результат . Например ... давайте назовем это:

    Paginate(
      Intersection(
        Range(Match(Index("by_camping")), [3], []),
        Range(Match(Index("by_swimming")), [3], [])
      )
    )

Результат пуст. Хотя у нас был один с плаванием 3 и кемпинг 5. Это именно проблема. Если бы плавание и кемпинг были одинаковыми, мы бы получили результат. Поэтому важно отметить, что пересечение пересекает значения , что включает в себя как значение для кемпинга / плавания, так и ссылку. Это означает, что мы должны отбросить значение, так как нам нужна только ссылка. Способ сделать это за до разбивки на страницы - с помощью объединения. По сути, мы собираемся объединиться с другим индексом, который собирается просто ... вернуть ссылку (без указания значений по умолчанию только ссылка)

CreateIndex({
  name: 'ref_by_ref',
  source: Collection('place'),
  terms: [{field:  ['ref']}]
})

Это объединение выглядит следующим образом

    Paginate(Join(
      Range(Match(Index('by_camping')), [4], [9]),
      Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
    )))

Здесь мы просто взяли результат Match (Index ('by_camping')) и просто отбросили значение, присоединившись к индексу, который возвращает только ссылка Теперь давайте скомбинируем это и просто сделаем запрос диапазона И типа;)

    Paginate(Intersection(
      Join(
        Range(Match(Index('by_camping')), [1], [3]),
        Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
      )),
      Join(
        Range(Match(Index('by_hiking')), [0], [7]),
        Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
      ))
    ))

В результате получим два значения, и оба на одной странице!

Обратите внимание, что вы можете легко расширить или составить FQL, просто используя родной язык (в данном случае JS), чтобы сделать этот вид намного приятнее (заметьте, я не тестировал этот фрагмент кода)

    const DropRef = function(RangeMatch) {
      return Join(
        RangeMatch,
        Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
      ))
    }

    DropRef(
      Range(Match(Index('by_camping')), [1], [3]),
      Range(Match(Index('by_hiking')), [0], [7])
    )

И последнее расширение, оно возвращает только индексы, так что вам нужно будет получить карту. Конечно, есть способ обойти это, если вы действительно хотите ... просто используя другой индекс:)

    const index = CreateIndex({
      name: 'all_values_by_ref',
      source: Collection('place'),
      values: [
        { field: ['data', 'camping'] },
        { field: ['data', 'swimming'] },
        { field: ['data', 'hiking'] },
        { field: ['data', 'culture'] },
        { field: ['data', 'nightlife'] },
        { field: ['data', 'budget'] },
        { field: ['data', 'name'] },
        { field: ['data', 'focus'] }
      ],
      terms: [
        { field: ['ref'] }
      ]
    }) 

Теперь у вас есть запрос диапазона, вы получите все без карты / get:

    Intersection(
      Join(
        Range(Match(Index('by_camping')), [1], [3]),
        Lambda(['value', 'ref'], Match(Index('all_values_by_ref'), Var('ref'))
      )),
      Join(
        Range(Match(Index('by_hiking')), [0], [7]),
        Lambda(['value', 'ref'], Match(Index('all_values_by_ref'), Var('ref'))
      ))
    )

При таком подходе к объединению вы можете даже создавать индексы диапазонов для разных коллекций, если вы присоединяете их к одной и той же ссылке перед пересечением! Довольно круто, да?

Могу ли я сохранить больше значений в индексе?

Да, вы можете, индексы в FaunaDB являются представлениями, поэтому давайте назовем их indiviews. Это компромисс, по сути вы обмениваете вычисления для хранения. Делая просмотр со многими значениями, вы получаете очень быстрый доступ к определенному подмножеству ваших данных. Но есть и другой компромисс: гибкость. Вы можете не просто go добавлять элементы, так как для этого потребуется переписать весь ваш индекс. В этом случае вам придется создать новый индекс и дождаться его построения, если у вас много данных (и да, это довольно часто), и убедитесь, что запросы, которые вы делаете (посмотрите на параметры лямбды в фильтре карты), совпадают ваш новый индекс. Вы всегда можете удалить другой индекс позже. Простое использование Map / Get будет более гибким, все в базах данных является компромиссом, а FaunaDB предоставляет вам оба варианта :). Я бы предложил использовать такой подход с того момента, как ваша модель данных будет исправлена, и вы увидите в своем приложении определенную c часть, которую хотите оптимизировать.

Как избежать MapGet

Второй вопрос по Map / Get требует некоторого объяснения. Отделение значений, по которым вы будете искать, от мест (как это делал Бен) - отличная идея, если вы хотите использовать Join, чтобы получить фактические мест более эффективно. Это не потребует Map Get и, следовательно, будет стоить вам гораздо меньше чтений, но обратите внимание, что Join является скорее обходом (он заменит текущие ссылки на целевые ссылки, к которым он присоединяется), так что если вам нужны и значения, и фактическое место Данные в одном объекте в конце вашего запроса, чем вам потребуется Map / Get. Посмотрите на это с этой точки зрения, индексы смехотворно дешевы с точки зрения чтения, и с ними можно довольно далеко ходить go, но для некоторых операций просто нет способа обойти Map / Get, Get по-прежнему только 1 чтение. Учитывая, что вы получаете 100 000 бесплатно в день, это все равно не дорого :). Вы также можете сохранять свои страницы относительно небольшими (параметр размера в paginate), чтобы избежать ненужных загрузок, если вашим пользователям или приложению не требуется больше страниц. Для людей, читающих это, которые еще этого не знают:

  • 1 индексная страница === 1 чтение
  • 1 get === 1 чтение

Заключительные замечания

Мы можем и будем делать это легче в будущем. Однако обратите внимание, что вы работаете с масштабируемой распределенной базой данных, и часто эти вещи просто невозможны в других решениях или очень неэффективны. FaunaDB предоставляет вам очень мощные структуры и простой доступ к работе индексов, а также предоставляет множество возможностей. Он не пытается быть хитрым для вас за кулисами, так как это может привести к очень неэффективным запросам на случай, если мы ошибемся (это будет облом в масштабируемой системе оплаты по мере использования go).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...