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

Reindexer может объединять документы из нескольких неймспейсов в один результат при выполнении запросов на выборку данных.

JOIN

Оператор JOIN используется для выборки данных из двух неймспейсов и включения этих данных в один результирующий набор. Примеры:

type Actor struct {
	ID        int    `reindex:"id"`
	Name      string `reindex:"name"`
	IsVisible bool   `reindex:"is_visible"`
}

type ItemWithJoin struct {
	ID          int      `reindex:"id"`
	Name        string   `reindex:"name"`
	ActorsIDs   []int    `reindex:"actors_ids"`
	ActorsNames []int    `reindex:"actors_names"`
	Actors      []*Actor `reindex:"actors,,joined"`
}
....

query := db.Query("items_with_join").Join(
	db.Query("actors").
		WhereBool("is_visible", reindexer.EQ, true),
	"actors"
).On("actors_ids", reindexer.SET, "id")

query.Exec ()

В данном примере Go-коннектор при помощи рефлексии создаёт объектное представление приджойненных данных в виде указателя на массив Actors.

Чтобы избежать использования рефлексии, можно создать структуру, которая реализует интерфейс Joinable. Это увеличивает общую производительность на 10-20% и уменьшает количество аллокаций:

// Реализация Joinable интерфейса.
// Join добавляет к объекту `ItemWithJoin` элементы из присоединенного неймспейса.
// При вызове Joinable interface можно передать дополнительную контекстную переменную для реализации дополнительной логики в Join.
func (item *ItemWithJoin) Join(field string, subitems []interface{}, context interface{}) {

	switch field {
	case "actors":
		for _, joinItem := range subitems {
			item.Actors = append(item.Actors, joinItem.(*Actor))
		}
	}
}

Запрос с Join может содержать одно или несколько условий в On, соединенных с помощью операторов And (по умолчанию), Or или Not:

query := db.Query("items_with_join").
	Join(
		db.Query("actors").
			WhereBool("is_visible", reindexer.EQ, true),
		"actors").
	On("actors_ids", reindexer.SET, "id").
	Or().
	On("actors_names", reindexer.SET, "name")

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

LeftJoin возвращает все элементы из неймспейса на левой стороне ключевого слова LeftJoin, вместе со значениями из неймспейса на правой стороне, или ничего, если соответствующий элемент не существует. Join - это псевдоним для LeftJoin.

InnerJoin может использоваться как условие в Where:

query1 := db.Query("items_with_join").
	WhereInt("id", reindexer.RANGE, []int{0, 100}).
	Or().
	InnerJoin(db.Query("actors").WhereString("name", reindexer.EQ, "ActorName"), "actors").
	On("actors_ids", reindexer.SET, "id").
	Or().
	InnerJoin(db.Query("actors").WhereInt("id", reindexer.RANGE, []int{100, 200}), "actors").
	On("actors_ids", reindexer.SET, "id")

query2 := db.Query("items_with_join").
	WhereInt("id", reindexer.RANGE, []int{0, 100}).
	Or().
	OpenBracket().
		InnerJoin(db.Query("actors").WhereString("name", reindexer.EQ, "ActorName"), "actors").
		On("actors_ids", reindexer.SET, "id").
		InnerJoin(db.Query("actors").WhereInt("id", reindexer.RANGE, []int{100, 200}), "actors").
		On("actors_ids", reindexer.SET, "id").
	CloseBracket()

Обратите внимание, что обычно оператор Or реализует логику сокращенных вычислений для условий: если предыдущее условие верно, то следующее не вычисляется. Но в случае InnerJoin это работает иначе: в query1 (из примера выше) оба условия InnerJoin вычисляются, несмотря на результат WhereInt.

INNER JOIN дает пересечение данных неймспейсов, LEFT JOIN — добавляет данные в левую часть неймспейса. Запросы JOIN могут быть объединены с другими SQL-запросами.

SELECT name FROM media_items WHERE year > 1990 INNER JOIN favorites ON content_id IN id
-- Для удобства неймспейсы в условии могут быть заданы явно:
SELECT name FROM media_items WHERE year > 1990 INNER JOIN favorites ON favorites.content_id IN media_items.id
-- Пример использования join в формате подзапроса
SELECT name FROM media_items INNER JOIN (SELECT id, name FROM services WHERE mrf = 'mos' ORDER BY year LIMIT 5) ON media_items.packages IN services.id
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "media_items",
  "type": "select",
  "filters": [
    {
      "field": "year",
      "cond": "GE",
      "value": [
        1990
      ],
      "join_query": {
        "namespace": "favorites",
        "type": "INNER",
        "on": [
          {
            "left_field": "id",
            "right_field": "content_id",
            "cond": "EQ"
          }
        ]
      }
    }
  ],
  "select_filter": [
    "name"
  ]
}'

Также в условии можно использовать вложенные операторы SELECT для предварительной выборки необходимых данных. С помощью оператора LIMIT со значением 0 данные предварительной выборки можно использовать в качестве фильтра без добавления в итоговую выдачу:

query := db.Query("items_with_join").
    WhereInt("id", reindexer.RANGE, []int{0, 100}).
    Or().
    InnerJoin(db.Query("actors").WhereInt("id", reindexer.RANGE, []int{100, 200}), "actors").
    On("actors_ids", reindexer.SET, "id").
    Limit(0)

Здесь Limit(0) как часть InnerJoin работает как фильтр для проверки условий.

SELECT name FROM media_items WHERE year > 1990 INNER JOIN (SELECT id, name FROM services WHERE mrf = 'mos' LIMIT 0) ON media_items.packages IN services.id
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "media_items",
  "type": "select",
  "filters": [
    {
      "field": "year",
      "cond": "GE",
      "value": [
        1990
      ],
      "join_query": {
        "namespace": "services",
        "type": "INNER",
        "filters": [
          {
            "field": "mrf",
            "cond": "EQ",
            "value": [
              "mos"
            ]
          }
        ],
        "limit": 0,
        "on": [
          {
            "left_field": "packages",
            "right_field": "id",
            "cond": "EQ"
          }
        ]
      }
    }
  ],
  "select_filter": [
    "name"
  ]
}'

ANTI-JOIN

Reindexer не поддерживает ANTI JOIN напрямую, однако полностью эквивлентную выборку можно получить, используя комбинацию NOT и INNER JOIN.

!> На текущий момент скобки после NOT являются обязательными, это поведение будет изменено позднее.

query := db.Query("items_with_join").
	Not().
	OpenBracket(). // Brackets are essential here for NOT to work
		InnerJoin(
		db.Query("actors").
			WhereBool("is_visible", reindexer.EQ, true).
			Limit(0),
		"actors").
		On("id", reindexer.EQ, "id")
	CloseBracket()
SELECT * FROM items_with_join WHERE	NOT (INNER JOIN (SELECT * FROM actors WHERE is_visible = true) ON items_with_join.id = actors.id)
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "items_with_join",
  "type": "select",
  "filters": [
    {
      "op": "NOT",
      "filters": [
        {
          "join_query": {
            "type": "inner",
            "namespace": "actors",
            "filters": [
              {
                "field": "is_visible",
                "cond": "EQ",
                "value": [
                  true
                ]
              }
            ],
            "limit": 0,
            "on": [
              {
                "left_field": "id",
                "right_field": "id",
                "cond": "EQ"
              }
            ]
          }
        }
      ]
    }
  ]
}'

MERGE

Для выборки данных из нескольких неймспейсов может использоваться оператор MERGE.

Пример:

query := db.Query("test_namespace").
		Where("year", reindexer.QE, 2022)
query2 := db.Query("test_namespace2").
        Where("price", reindexer.LE, 1000)
query.Merge (query2)
SELECT * FROM test_namespace WHERE year = 2022 MERGE (SELECT * FROM test_namespace2 WHERE price <=1000)
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": "year",
      "cond": "EQ",
      "value": [
        2022
      ]
    }
  ],
  "merge_queries": [
    {
      "namespace": "test_namespace2",
      "type": "select",
      "filters": [
        {
          "field": "price",
          "cond": "LE",
          "value": [
            1000
          ]
        }
      ]
    }
  ]
}'

С информацией про объединение результатов при выборке из нескольких неймспейсов при полнотекстовом поиске можно ознакомиться в разделе «Объединение результатов полнотекстового поиска».