Выборка данных из массивов

Reindexer поддерживает следующие виды выборки данных из массивов:

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

Выборка с условиями из полей-массивов

Reindexer поддерживает выборку из полей-массивов со следующими операторами сравнения и атомарными функциями (условиями фильтрации): <, >, =, IN, IS NULL, ALLSET, now(), flat_array_len().

Запросы с условиями фильтрации <, > и = возвращают записи, в которых в поле-массиве найден хотя бы один элемент, соответствующий условию. Примеры запросов:

db.Query("test_namespace").
	Where("array_field", reindexer.GT, 5)
(
    db.new_query("test_namespace")
        .where("array_field", CondType.CondGt, 5)
)
Iterator<Item> query = db.query("test_namespace", Item.class)
    .where("array_field", Condition.GT, 5);
SELECT * FROM test_namespace WHERE array_field > 5
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw \
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "array_field",
      "cond": "GT",
      "value": [
        5
      ]
    }
  ]
}
'

Запросы с условием фильтрации IN (SET) возвращают записи, в которых найдено любое из заданных в условии значений. Примеры запросов:

db.Query("test_namespace").
	WhereInt("array_field", reindexer.SET, 5, 6, 9)
(
    db.new_query("test_namespace")
        .where("array_field", CondType.CondSet, [5, 6, 9])
)
Iterator<Item> query = db.query("test_namespace", Item.class)
    .where("array_field", Condition.SET, 5, 6, 9);
SELECT * FROM test_namespace WHERE array_field IN (5, 6, 9)
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw \
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "array_field",
      "cond": "SET",
      "value": [
        5,
        6,
        9
      ]
    }
  ]
}
'

Условие фильтрации EMPTY(IS NULL) используется для поиска пустых массивов и null-значений (для sparse-индексов вместо поля-массива допустим null, и такой документ тоже будет найден по IS NULL, как и в случае со скалярными индексами).

db.Query("test_namespace").
  WhereInt("array_field", reindexer.EMPTY)
(
    db.new_query("test_namespace")
        .where("array_field", CondType.CondEmpty)
)
Iterator<Item> query = db.query("test_namespace", Item.class)
    .where("array_field", Condition.EMPTY);
SELECT * FROM test_namespace WHERE array_field IS NULL
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "array_field",
      "cond": "EMPTY"
    }
  ]
}
'

Запросы с условием фильтрации ALLSET возвращают записи, в которых в поле-массиве найдены все из заданных в условии значений.

db.Query("test_namespace").
  WhereInt("array_field", reindexer.ALLSET, 1, 2, 3)
(
    db.new_query("test_namespace")
        .where("array_field", CondType.CondAllSet, [1, 2, 3])
)
Iterator<Item> query = db.query("test_namespace", Item.class)
    .where("array_field", Condition.ALLSET, 1, 2, 3);
SELECT * FROM test_namespace WHERE array_field ALLSET (1, 2, 3)
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "array_field",
      "cond": "ALLSET",
      "value": [
        1,
        2,
        3
      ]
    }
  ]
}
'

Также запросы с условием фильтрации ALLSET могут использоваться для поиска вхождений одного поля-массива в другое. Примеры:

db.Query("test_namespace").
  WhereBetweenFields("array_field1", reindexer.ALLSET, "array_field2")
(
    db.new_query("test_namespace")
        .where("array_field1", CondType.CondAllSet, "array_field2")
)
Iterator<Item> query = db.query("test_namespace", Item.class)
    .where("array_field1", Condition.ALLSET, "array_field2");
SELECT * FROM test_namespace WHERE array_field1 ALLSET array_field2
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
    "cond": "allset",
    "left_expression": {
        "type": "field",
        "value": "array_field1"
    },
    "right_expression": {
        "type": "field",
        "value": "array_field2"
    }
  ]
}
'

Выборка данных с использованием агрегатных функций

Reindexer поддерживает выборку из полей-массивов c использованием следующих агрегатных функций: SUM, AVG, MIN, MAX, COUNT, FACET.

Подробнее об агрегатных функциях для SQL и Go в Reindexer — в разделе «Агрегатные функции».

Запросы с агрегатной функцией SUM могут использоваться для:

  • вычисления суммы элементов всех полей-массивов с указанным в запросе именем из всех записей неймспейса. Пример:
query := db.Query("test_namespace")
query.AggregateSum("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
  panic(err)
}
defer iterator.Close()

aggSumRes := iterator.AggResults()[0]

if aggSumRes.Value != nil {
  fmt.Printf("Сумма всех элементов полей-массивов 'array_field' всех записей неймспейса = %v\n", *aggSumRes.Value)
} else {
  fmt.Println("Нет данных для агрегации")
}
query = (
    db.new_query("test_namespace")
        .aggregate_sum("array_field")
        .execute()
)

result = query.get_agg_results()[0]

if result.get("value") is not None:
    print(f"Сумма всех элементов полей-массивов 'array_field' всех записей неймспейса = {result.get("value")}")
else:
    print("Нет данных для агрегации")
Query<Item> query = db.query("test_namespace", Item.class);
query.aggregateSum("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);

System.out.println("Сумма всех элементов полей-массивов 'array_field' всех записей неймспейса = " + result.getValue());
SELECT SUM (array_field) FROM test_namespace
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "array_field"
      ],
      "type": "SUM"
    }
  ]
}
'
  • вычисления суммы элементов определенного поля-массива записи, указанной в условии. Пример:
query := db.Query("test_namespace").
  Where("id", reindexer.EQ, 0)
query.AggregateSum("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
  panic(err)
}
defer iterator.Close()

aggSumRes := iterator.AggResults()[0]

if aggSumRes.Value != nil {
  fmt.Printf("Сумма элементов поля-массива 'array_field' для записи неймспейса с заданным `id` = %v\n", *aggSumRes.Value)
} else {
  fmt.Println("Нет данных для агрегации")
}
query = (
    db.new_query("test_namespace")
        .where('id', CondType.CondEq, 0)
        .aggregate_sum("array_field")
        .execute()
)

result = query.get_agg_results()[0]

if result.get("value") is not None:
    print(f"Сумма элементов поля-массива 'array_field' для записи неймспейса с заданным id = {result.get("value")}")
else:
    print("Нет данных для агрегации")
Query<Item> query = db.query("test_namespace", Item.class)
    .where("id", Condition.EQ, 0);
query.aggregateSum("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);

System.out.println("Сумма элементов поля-массива 'array_field' для записи неймспейса с заданным id = " + result.getValue());
SELECT SUM(array_field) FROM test_namespace WHERE id = 0
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "array_field"
      ],
      "type": "SUM"
    }
  ],
  "filters": [
    {
      "field": "id",
      "cond": "EQ",
      "value": [
        0
      ]
    }
  ]
}
'

Запросы с агрегатной функцией AVG могут использоваться для:

  • вычисления среднего значения элемента всех полей-массивов с указанным в запросе именем из всех записей неймспейса. Пример:
query := db.Query("test_namespace")
query.AggregateAvg("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
  panic(err)
}
defer iterator.Close()

aggAvgRes := iterator.AggResults()[0]

if aggAvgRes.Value != nil {
  fmt.Printf("Среднее значение элемента поля-массива 'array_field' всех записей неймспейса = %v", *aggAvgRes.Value)
} else {
  fmt.Println("Нет данных для агрегации")
}
query = (
    db.new_query("test_namespace")
        .aggregate_avg("array_field")
        .execute()
)

result = query.get_agg_results()[0]

if result.get("value") is not None:
    print(f"Среднее значение элемента поля-массива 'array_field' всех записей неймспейса = {result.get("value")}")
else:
    print("Нет данных для агрегации")
Query<Item> query = db.query("test_namespace", Item.class);
query.aggregateAvg("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);

System.out.println("Среднее значение элемента поля-массива 'array_field' всех записей неймспейса = " + result.getValue());
SELECT AVG(array_field) FROM test_namespace
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "array_field"
      ],
      "type": "AVG"
    }
  ]
}
'
  • вычисления среднего значения элемента определенного поля-массива записи, указанной в условии. Пример:
query := db.Query("test_namespace")
	.Where("id", reindexer.EQ, 0)
query.AggregateAvg("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
  panic(err)
}
defer iterator.Close()

aggAvgRes := iterator.AggResults()[0]

if aggAvgRes.Value != nil {
  fmt.Printf("Среднее значение элемента поля-массива 'array_field' для записи неймспейса с заданным 'id' = %v\n", *aggAvgRes.Value)
} else {
  fmt.Println("Нет данных для агрегации")
}
query = (
    db.new_query("test_namespace")
        .where('id', CondType.CondEq, 0)
        .aggregate_avg("array_field")
        .execute()
)

result = query.get_agg_results()[0]

if result.get("value") is not None:
    print(f"Среднее значение элемента поля-массива 'array_field' для записи неймспейса с заданным 'id' = {result.get("value")}")
else:
    print("Нет данных для агрегации")
Query<Item> query = db.query("test_namespace", Item.class)
    .where("id", Condition.EQ, 0);
query.aggregateAvg("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);

System.out.println("Среднее значение элемента поля-массива 'array_field' для записи неймспейса с заданным `id` = " + result.getValue());
SELECT AVG(array_field) FROM test_namespace WHERE id = 0
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "array_field"
      ],
      "type": "AVG"
    }
  ],
  "filters": [
    {
      "field": "id",
      "cond": "EQ",
      "value": [
        0
      ]
    }
  ]
}
'

Запросы с агрегатными функциями MIN/MAX могут использоваться для:

  • поиска минимального/максимального значения элемента во всех полях-массива с указанным в запросе именем из всех записей неймспейса. Пример:
query := db.Query("test_namespace")
query.AggregateMin("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
  panic(err)
}
defer iterator.Close()

aggMinRes := iterator.AggResults()[0]

if aggMinRes.Value != nil {
  fmt.Printf("Минимальное значение элемента для полей-массивов 'array_field' всех записей неймспейса = %v\n", *aggSumRes.Value)
} else {
  fmt.Println("Нет данных для агрегации")
}
query = (
    db.new_query("test_namespace")
        .aggregate_min("array_field")
        .execute()
)

result = query.get_agg_results()[0]

if result.get("value") is not None:
    print(f"Минимальное значение элемента для полей-массивов 'array_field' всех записей неймспейса = {result.get("value")}")
else:
    print("Нет данных для агрегации")
Query<Item> query = db.query("test_namespace", Item.class);
query.aggregateMin("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);

System.out.println("Минимальное значение элемента для полей-массивов 'array_field' всех записей неймспейса =" + result.getValue());
SELECT MIN(array_field) FROM test_namespace
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "array_field"
      ],
      "type": "MIN"
    }
  ]
}
'
  • поиска минимального/максимального значения элемента определенного поля-массива записи, указанной в условии. Пример:
query := db.Query("test_namespace").
	Where("id", reindexer.EQ, 0)
query.AggregateMin("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
	panic(err)
}
defer iterator.Close()

aggMinRes := iterator.AggResults()[0]

if aggMinRes.Value != nil {
	fmt.Printf("Минимальное значение элемента поля-массива 'array_field' для записи неймспейса с заданным 'id' = %v\n", *aggMinRes.Value)
} else {
	fmt.Println("Нет данных для агрегации")
}
query = (
    db.new_query("test_namespace")
        .where('id', CondType.CondEq, 0)
        .aggregate_min("array_field")
        .execute()
)

result = query.get_agg_results()[0]

if result.get("value") is not None:
    print(f"Минимальное значение элемента поля-массива 'array_field' для записи неймспейса с заданным 'id' = {result.get("value")}")
else:
    print("Нет данных для агрегации")
Query<Item> query = db.query("test_namespace", Item.class)
    .where("id", Condition.EQ, 0);
query.aggregateMin("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);

System.out.println("Минимальное значение элемента поля-массива 'array_field' для записи неймспейса с заданным `id` =" + result.getValue());
SELECT MIN(array_field) FROM test_namespace WHERE id = 0
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "array_field"
      ],
      "type": "MIN"
    }
  ],
  "filters": [
    {
      "field": "id",
      "cond": "EQ",
      "value": [
        0
      ]
    }
  ]
}
'

Агрегатная функция FACET используется для получения уникальных значений полей-массивов в записях неймспейса и подсчета их количества. Фасет формируется по значениям элементов в массивах, а не по самим уникальным массивам.

Например, если в неймспейсе есть 3 записи с полем-массивом array_field:

Запись 1:

{
  "id": 0,
  "array_field": [1, 2]
}

Запись 2:

{
  "id": 1,
  "array_field": [1, 3]
}

Запись 3:

{
  "id": 2,
  "array_field": [1, 4]
}

то выдача по запросу SELECT FACET(array_field) FROM test_namespace будет выглядеть следующим образом:

count array_field
3 1
1 2
1 3
1 4

В этом примере количество уникальных значений элементов полей-массивов будет равно 4 (уникальные значения: 1, 2, 3, 4). Количество самих значений в полях-массивах суммарно во всех записях указано в поле count.

Примеры запросов:

query := db.Query("test_namespace")
query.AggregateFacet("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
  panic(err)
}
defer iterator.Close()

aggFacet := iterator.AggResults()[0]

fmt.Printf("'Значение поля массива' -> Кол. уникальных\n")
for _, facet := range aggFacet.Facets {
  fmt.Printf("'%v' -> %v\n", facet.Values[0], facet.Count)
}
query = db.new_query("test_namespace")
(
    query
        .aggregate_facet("array_field")
)
result = (
    query
    .execute()
    .get_agg_results()[0]
)

print("'Значение поля массива' -> Кол. уникальных")
for facet in result["facets"]:
    print(facet["values"][0], " -> ", facet["count"])
Query<Item> query = db.query("test_namespace", Item.class);
query.aggregateFacet("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);
List<AggregationResult.Facet> facets = result.getFacets();

System.out.println("'Значение name' -> Кол. уникальных");
for (int i = 0; i < facets.size() - 1; i++) {
    System.out.println(facets.get(i).getValues().get(0) + " -> "
      + facets.get(i).getCount());
}
SELECT FACET(array_field) FROM test_namespace
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "array_field"
      ],
      "type": "FACET"
    }
  ]
}
'

Агрегатная функция FACET может использоваться совместно с операторами сравнения (подробнее о них — в разделе «Выборка данных с условиями»). Примеры:

query := db.Query("test_namespace").
  WhereInt("id", reindexer.GT, 5)
query.AggregateFacet("array_field")

iterator := query.Exec()

if err := iterator.Error(); err != nil {
  panic(err)
}
defer iterator.Close()

aggFacet := iterator.AggResults()[0]

fmt.Printf("'Значение поля массива' -> Кол. уникальных\n")
for _, facet := range aggFacet.Facets {
  fmt.Printf("'%v' -> %v\n", facet.Values[0], facet.Count)
}
query = db.new_query("test_namespace")
(
    query
        .where("id", CondType.CondGt, 5)
        .aggregate_facet("array_field")
)
result = (
    query
    .execute()
    .get_agg_results()[0]
)

print("'Значение поля массива' -> Кол. уникальных")
for facet in result["facets"]:
    print(facet["values"][0], " -> ", facet["count"])
Query<Item> query = db.query("test_namespace", Itm.class)
        .where("id", Condition.GT, 5);
query.aggregateFacet("array_field");

ResultIterator<Item> iterator = query.execute();
AggregationResult result = iterator.aggResults().get(0);
List<AggregationResult.Facet> facets = result.getFacets();

System.out.println("'Значение name' -> Кол. уникальных");
for (int i = 0; i < facets.size() - 1; i++) {
    System.out.println(facets.get(i).getValues().get(0) + " -> "
      + facets.get(i).getCount());
}
SELECT FACET(array_field) FROM test_namespace WHERE id > 5
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "aggregations": [
    {
      "fields": [],
      "type": "FACET"
    }
  ],
  "filters": [
    {
      "field": "array_field",
      "cond": "GT",
      "value": [
        5
      ]
    }
  ]
}
'

Выборка элементов с одинаковыми индексами из полей-массивов

В Reindexer есть возможность добавить дополнительное условие к фильтрам по полям-массивам, требующее, чтобы индексы элементов массивов, удовлетворяющих фильтрам, совпадали для указанных в условии полей. Для этого используется функция EQUAL_POSITION().

Например, есть массив структур:

type Elem struct {
  Price    int `reindex:"array_field1"`
  Discount int `reindex:"array_field2"`
}

type Payment struct {
  Elems []Elem
}
json_schema = {
  "type": "object",
  "properties": {
    "Elems": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "Price": {
            "type": "integer"
          },
          "Discount": {
            "type": "integer"
          }
        }
      }
    }
  }
}

index_definition = [
  {
    "name": "id",
    "json_paths": ["id"],
    "field_type": "int",
    "is_pk": True
  },
  {
    "name": "array_field1",
    "json_paths": ["Elems.Price"],
    "field_type": "int",
    "is_array": True
  },
  {
    "name": "array_field2",
    "json_paths": ["Elems.Discount"],
    "field_type": "int",
    "is_array": True
  }
]

и запись:

{
  "Elems": [
    {
      "Price": 99,
      "Discount": 1
    },
    {
      "Price": 87,
      "Discount": 2
    }
  ]
}

В случае простой выборки запрос будет выглядеть следующим образом:

db.Query("test_namespace").
	Where("array_field1", reindexer.EQ, 1).
	Where("array_field2", reindexer.EQ, 2)
(
    db.new_query("test_namespace")
        .where("array_field1", CondType.CondEq, 1)
        .where("array_field2", CondType.CondEq, 2)
)
SELECT * FROM test_namespace WHERE array_field1 = 1 AND array_field2 = 2
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "array_field1",
      "cond": "EQ",
      "value": [
        1
      ]
    },
    {
      "field": "array_field2",
      "cond": "EQ",
      "value": [
        2
      ]
    },
  ]
}
'

Этот запрос вернет все записи, в которых в поле array_field1 найдется значение 1, а в поле array_field2 найдется значение 2. Например, если в таблице будет 2 записи:

{
  "id": 0,
  "array_field1": [1, 2, 7],
  "array_field2": [1, 2, 5]
}

и

{
  "id": 1,
  "array_field1": [1, 2, 7],
  "array_field2": [2, 1, 5]
}

запрос вернет их обе.

Запрос с EQUAL_POSITION будет выглядеть следующим образом:

db.Query("test_namespace").
  Where("array_field1", reindexer.EQ, 1).
  Where("array_field2", reindexer.EQ, 2).
  EqualPosition("array_field1", "array_field2")
(
    db.new_query("test_namespace")
        .where("array_field1", CondType.CondEq, 1)
        .where("array_field2", CondType.CondEq, 2)
        .equal_position("array_field1", "array_field2")
)
SELECT * FROM test_namespace WHERE array_field1 = 1 AND array_field2 = 2 EQUAL_POSITION(array_field1, array_field2)
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "array_field1",
      "cond": "EQ",
      "value": [
        1
      ]
    },
    {
      "field": "array_field2",
      "cond": "EQ",
      "value": [
        2
      ]
    },
    {
      "equal_positions": [
        {
          "positions": [
            "array_field1",
            "array_field2"
          ]
        }
      ]
    }
  ]
}
'

Этот запрос вернет все записи, в которых в поле array_field1 найдется значение 1, а в поле array_field2 найдется значение 2 и индексы найденных значений в массивах совпадут. Например, если в таблице будет 2 записи:

{
  "id": 0,
  "array_field1": [1, 2, 7],
  "array_field2": [1, 2, 5]
}

и

{
  "id": 1,
  "array_field1": [1, 2, 7],
  "array_field2": [2, 1, 5]
}

запрос вернет только вторую запись:

{
  "id": 1,
  "array_field1": [1, 2, 7],
  "array_field2": [2, 1, 5]
}

т. к. в первой для заданных в запросе значений не совпадают индексы в массивах: значение array_field1 = 1 найдено на нулевой позиции в массиве [1, 2 ,7], значение array_field2 = 2 найдено на первой позиции в массиве [1, 2, 5].

В EQUAL_POSITION можно передавать не менее 2 полей и не более, чем полей в условии.

Использование EQUAL_POSITION в сложных выражениях

Функция EQUAL_POSITION() может использоваться в сложных выражениях. При этом она должна находиться внутри скобок, т.к. применяется к полям внутри скобок. При использовании функции EQUAL_POSITION() в сложных выражениях вне скобок возможно неопределенное поведение.

Примеры:

EQUAL_POSITION() применяется к полям внутри скобок:

SELECT
    *
FROM
    test_namespace
WHERE
    (
        array_field1 >= 5
        AND array_field2 = 100
        EQUAL_POSITION(array_field1, array_field2)
    )
    OR
    (
        array_field3 = 3
        AND array_field4 < 4
        AND array_field5 = 7
        EQUAL_POSITION(array_field3, array_field4, array_field5)
    )

EQUAL_POSITION() применяется к полям внутри скобок:

SELECT
    *
FROM
    test_namespace
WHERE
    (
        array_field1 >= 5
        AND array_field2 = 100
        AND array_field3 = 3
        AND array_field4 < 4
        EQUAL_POSITION(array_field1, array_field3)
        EQUAL_POSITION(array_field2, array_field4)
    )
    OR
    (
        array_field5 = 3
        AND array_field6 < 4
        AND array_field7 = 7
        EQUAL_POSITION(array_field5, array_field7)
    )
SELECT
    *
FROM
    test_namespace
WHERE
    array_field1 >= 5
    AND
    (
        array_field2 = 100
        AND array_field3 = 3
        AND array_field4 < 4
        EQUAL_POSITION(array_field2, array_field3)
    )
    AND array_field5 = 3
    AND array_field6 < 4
    EQUAL_POSITION(array_field1, array_field5, array_field6)

Ограничения EQUAL_POSITION

В Reindexer функция EQUAL_POSITION() не работает в запросах со следующими условиями:

  • IS NULL,
  • IS EMPTY,
  • IN() с пустым списком значений.

В условиях и EQUAL_POSITION() можно передавать как имена индексов, так и JSON-пути. Для поля в условии и EQUAL_POSITION возможны следующие комбинации:

  • индекс (name), построенный поверх одного JSON-пути (json_paths1):

    Условие EQUAL_POSITION Допустимо
    name name +
    name json_paths1 +
    json_paths1 json_paths1 +
    json_paths1 name +
  • индекс (name), построенный поверх нескольких JSON-путей (json_paths1, json_paths2):

    Условие EQUAL_POSITION Допустимо
    name name +
    name json_paths1 +
    name json_paths2 +
    json_paths1 json_paths1 +
    json_paths1 json_paths2 -
    json_paths2 json_paths1 -
    json_paths2 json_paths2 +
    json_paths1 name -
    json_paths1 name -

Выборка элементов с одинаковыми индексами с учетом группировки

Для вложенных массивов поддерживается логика группировки. Для этого после имени поля надо поставить метку [#]. Она должна присутствовать в каждом поле, переданном в EQUAL_POSITION, причём только одна. В этом случае при обработке EQUAL_POSITION() будет сформирована таблица. Каждая строка содержит все значения по одному индексу помеченного массива, а номер строки равен этому индексу.

Примеры выборки с группировкой

Пусть есть структура:

type Element struct {
  Price   int64       `reindex:"price"`
  Discounts []int64   `reindex:"discount"`
}

type Filters struct {
  ID      int         `reindex:"id,,pk"`
  Filters []Element   `reindex:"filters"`
}
json_schema = {
  "title": "Filters",
  "type": "object",
  "properties": {
    "ID": {
      "type": "integer"
    },
    "Filters": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "Price": {
            "type": "integer"
          },
          "Discounts": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          }
        }
      }
    }
  }
}

index_definition = [
  {
    "name": "id",
    "json_paths": ["id"],
    "field_type": "int",
    "is_pk": True
  },
  {
    "name": "filters.price",
    "json_paths": [
      "filters.price"
    ],
    "field_type": "int64",
    "is_array": True
},
  {
    "name": "filters.discounts",
    "json_paths": [
      "filters.discounts"
    ],
    "field_type": "int64",
    "is_array": True
  }
]

и запись:

{
  "ID": 1,
  "Filters": [
    {
      "Price": 99,
      "Discounts": [
        1,
        2
      ]
    },
    {
      "Price": 87,
      "Discounts": [
        1
      ]
    }
  ]
}

Пример 1:

Если требуется найти все документы, у которых в массиве Filters есть хотя бы один объект, удовлетворяющий условиям, запрос будет выглядеть следующим образом:

db.Query("test_namespace").
	Where("Filters.Price", reindexer.EQ, 87).
	Where("Filters.Discounts", reindexer.EQ, 1).
	EqualPosition("Filters[#].Price", "Filters[#].Discounts")
(
    db.new_query("test_namespace")
        .where("filters.price", CondType.CondEq, 87)
        .where("filters.discounts", CondType.CondEq, 1)
        .equal_position("filters[#].price", "filters[#].discounts")
)
SELECT * FROM test_namespace WHERE Filters.Price = 87 AND Filters.Discounts = 1 EQUAL_POSITION(Filters[#].Price, Filters[#].Discounts)
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "Filters.Price",
      "cond": "EQ",
      "value": [
        87
      ]
    },
    {
      "field": "Filters.Discounts",
      "cond": "EQ",
      "value": [
        1
      ]
    },
    {
      "equal_positions": [
        {
          "positions": [
            "Filters[#].Price",
            "Filters[#].Discounts"
          ]
        }
      ]
    }
  ]
}
'

Промежуточная таблица для Filters[#].Price будет такой:

N
0 99
1 87

Промежуточная таблица для Filters[#].Discounts будет такой:

N
0 1 2
1 1

Условия выполняются для строки 2 обеих таблиц, и запрос возвращает всю запись.

Пример 2:

Рассмотрим случай, когда Filters.Discounts должно быть равно 2:

db.Query("test_namespace").
	Where("Filters.Price", reindexer.EQ, 87).
	Where("Filters.Discounts", reindexer.EQ, 2).
	EqualPosition("Filters[#].Price", "Filters[#].Discounts")
(
    db.new_query("test_namespace")
        .where("filters.price", CondType.CondEq, 87)
        .where("filters.discounts", CondType.CondEq, 2)
        .equal_position("filters[#].price", "filters[#].discounts")
)
SELECT
    *
FROM
    test_namespace
WHERE
    Filters.Price = 87
    AND Filters.Discounts = 2
    EQUAL_POSITION(Filters[#].Price, Filters[#].Discounts)
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "Filters.Price",
      "cond": "EQ",
      "value": [
        87
      ]
    },
    {
      "field": "Filters.Discounts",
      "cond": "EQ",
      "value": [
        2
      ]
    },
    {
      "equal_positions": [
        {
          "positions": [
            "Filters[#].Price",
            "Filters[#].Discounts"
          ]
        }
      ]
    }
  ]
}
'

Промежуточная таблица для Filters[#].Price:

N
0 99
1 87

Промежуточная таблица для Filters[#].Discounts:

N
0 1 2
1 1

Условия выполняются на разных строках таблиц, запрос не возвращает записей.

Пример 3:

Пусть теперь Filters.Price должно быть равно 99:

db.Query("test_namespace").
	Where("Filters.Price", reindexer.EQ, 99).
	Where("Filters.Discounts", reindexer.EQ, 2).
	EqualPosition("Filters[#].Price", "Filters[#].Discounts")
(
    db.new_query("test_namespace")
        .where("filters.price", CondType.CondEq, 99)
        .where("filters.discounts", CondType.CondEq, 2)
        .equal_position("filters[#].price", "filters[#].discounts")
)
SELECT * FROM test_namespace WHERE Filters.Price = 99 AND Filters.Discounts = 2 EQUAL_POSITION(Filters[#].Price, Filters[#].Discounts)
curl \
  --location \
  --request POST \
  http://127.0.0.1:9088/api/v1/db/testdb/query \
  --header 'Content-Type: application/json' \
  --data-raw
'
{
  "namespace": "test_namespace",
  "type": "select",
  "filters": [
    {
      "field": "Filters.Price",
      "cond": "EQ",
      "value": [
        99
      ]
    },
    {
      "field": "Filters.Discounts",
      "cond": "EQ",
      "value": [
        2
      ]
    },
    {
      "equal_positions": [
        {
          "positions": [
            "Filters[#].Price",
            "Filters[#].Discounts"
          ]
        }
      ]
    }
  ]
}
'

Промежуточная таблица для Filters[#].Price:

N
0 99
1 87

Промежуточная таблица для Filters[#].Discounts:

N
0 1 2
1 1

Условия выполняются для строки 1 обеих таблиц и запрос возвращает запись.

Примеры сложных случаев группировки

Рассмотрим запись:

{
  "array": [
    {
      "nested_object": {
        "nested_array": [
          {
            "field": 1
          },
          {
            "field": [5, 3]
          }
        ]
      }
    },
    {},
    {
      "nested_object": {
        "nested_array": [
          {
            "field": null
          },
          {
            "field": [4, 7]
          }
        ]
      }
    }
  ]
}

Группировка array.nested_object.nested_array.field[#] последовательно помещает все элементы массивов в полях field на строку, соответствующую их номеру, включая скаляры 1 и null:

N
0 1 5 null 4
1 3 7

Группировка array.nested_object.nested_array[#].field заполняет строки таблицы всеми значениями внутри элементов массива nested_array:

N
0 1 null
1 5 3 4 7

Группировка array.nested_object[#].nested_array.field помещает все значения на нулевую строку, т. к. оба объекта nested_object по одному индексу:

N
0 1 5 3 null 4 7

Группировка array[#].nested_object.nested_array.field действует следующим образом:

N
0 1 5 3
1
2 null 4 7

Важно

Обратите внимание, что метка [#] может быть помещена только после названия поля, т. е. для примера ниже группировка по индексам вложенных массивов ([3, 4], [4, 7]) не поддерживается:

{
  "array": [
    {
      "field": [
         5,
         [3, 4]
      ]
    },
    {
      "field": [
         1,
         [4, 7]
      ]
    }
  ]
}