Спасибо за ваш вопрос. Бен уже написал полный пример, который показывает, что вы можете сделать, и я буду опираться на его рекомендации и постараюсь уточнить дальше.
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'])
Тогда результатом будет то, что вы видите слева, а не то, что вы видите справа. Это очень масштабируемое поведение, которое демонстрирует необработанную мощность без издержек на базовый индекс, но не совсем то, что вы ищете!
Итак, давайте перейдем к другому решению!
ВАРИАНТ 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).