Как создать сопоставления для индекса родитель-потомок и поиск детей, отфильтрованных по родительскому элементу - PullRequest
0 голосов
/ 01 августа 2020

Цель:

Я хочу создать родительско-дочерний индекс с 2 объектами. А profile и comment. A profile (для простоты) имеет настраиваемый идентификатор (UUID, преобразованный в строку), возраст и местоположение (GeoPoint). A comment (для простоты) имеет собственный идентификатор (UUID преобразован в строку). Имея эту информацию, я хочу иметь возможность искать все комментарии с учетом некоторых данных фильтрации по профилю. Например, я хочу найти все комментарии по профилям в возрасте от 26 до 36 лет, расположенные в пределах 100 км от широты: 3.0, длинной 5.0.

Классы:

// Profile.kt
import org.elasticsearch.common.geo.GeoPoint
import org.springframework.data.annotation.Id
import org.springframework.data.elasticsearch.annotations.Document
import org.springframework.data.elasticsearch.annotations.Field
import org.springframework.data.elasticsearch.annotations.FieldType
import org.springframework.data.elasticsearch.annotations.GeoPointField

@Document(indexName = "message_board", createIndex = false, type = "profile")
data class Profile(
    @Id
    val profileId: String,
    @Field(type = FieldType.Short, store = true)
    val age: Short,
    @GeoPointField
    val location: GeoPoint
)
// Comment.kt
import org.springframework.data.annotation.Id
import org.springframework.data.elasticsearch.annotations.Document
import org.springframework.data.elasticsearch.annotations.Field
import org.springframework.data.elasticsearch.annotations.FieldType
import org.springframework.data.elasticsearch.annotations.Parent

@Document(indexName = "message_board", createIndex = false, type = "comment")
data class Comment(
    @Id
    val commentId: String,
    @Field(type = FieldType.Text, store = true)
    @Parent(type = "profile")
    val parentId: String
)
// RestClientConfig.kt
import org.elasticsearch.client.RestHighLevelClient
import org.springframework.context.annotation.Configuration
import org.springframework.data.elasticsearch.client.ClientConfiguration
import org.springframework.data.elasticsearch.client.RestClients
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration

@Configuration
class RestClientConfig(
    private val elasticSearchConfig: ElasticSearchConfig
) : AbstractElasticsearchConfiguration() {
    override fun elasticsearchClient(): RestHighLevelClient {
        val clientConfiguration: ClientConfiguration = ClientConfiguration.builder()
            .connectedTo("${elasticSearchConfig.endpoint}:${elasticSearchConfig.port}")
            .build()
        return RestClients.create(clientConfiguration).rest()
    }
}
// Controller.kt
import org.springframework.web.bind.annotation.RestController
import org.springframework.data.elasticsearch.core.ElasticsearchOperations
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping

@RestController
@RequestMapping("/", produces = [MediaType.APPLICATION_JSON_VALUE])
class Controller constructor(
    private val elasticsearchOperations: ElasticsearchOperations
) {
    init {
        elasticsearchOperations.indexOps(IndexCoordinates.of("message_board")).let { indexOp ->
            if (!indexOp.exists() && indexOp.create()) {
                val profileMapping = indexOp.createMapping(Profile::class.java)
                println("Profile Mapping: $profileMapping")
                indexOp.putMapping(profileMapping)
                val commentMapping = indexOp.createMapping(Comment::class.java)
                println("Comment Mapping: $commentMapping")
                indexOp.putMapping(commentMapping)
                indexOp.refresh()
            }
        }
    }

    @GetMapping("comments")
    fun getComments(): List<Comment> {
        val searchQuery = NativeSearchQueryBuilder()
            .withFilter(
                HasParentQueryBuilder(
                    "profile",
                    QueryBuilders
                        .boolQuery()
                        .must(
                            QueryBuilders
                                .geoDistanceQuery("location")
                                .distance(100, DistanceUnit.KILOMETERS)
                                .point(3.0, 5.0)
                        )
                        .must(
                            QueryBuilders
                                .rangeQuery("age")
                                .gte(26)
                                .lte(36)
                        ),
                    false
                )
            )
            .build()
        return elasticsearchOperations.search(searchQuery, Comment::class.java, IndexCoordinates.of("message_board")).toList().map(SearchHit<Comment>::getContent)
    }
}

Моя настройка:

У меня работает elasticsearch в docker через:

docker run --name es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -d -v es_data:/usr/share/elasticsearch/data docker.elastic.co/elasticsearch/elasticsearch:7.4.2

Spring Boot: «2.3.2.RELEASE»

Spring Data Elasticsearch: «4.0.2.RELEASE»

Проблемы:

Я не могу получить мимо блока инициализации моего контроллера со следующим исключением:

Profile Mapping: MapDocument@?#? {"properties":{"age":{"store":true,"type":"short"},"location":{"type":"geo_point"}}}
Comment Mapping: MapDocument@?#? {"_parent":{"type":"profile"},"properties":{"parentId":{"store":true,"type":"text"}}}

Suppressed: org.elasticsearch.client.ResponseException: method [PUT], host [http://localhost:9200], URI [/message_board/_mapping?master_timeout=30s&timeout=30s], status line [HTTP/1.1 400 Bad Request]

Caused by: org.elasticsearch.ElasticsearchStatusException: Elasticsearch exception [type=mapper_parsing_exception, reason=Root mapping definition has unsupported parameters:  [_parent : {type=profile}]]

Мне нужно решение, которое не требует прямого запроса POST в ES. В идеале это решается с помощью клиентского API Elasticsearch. Похоже, что в моих аннотациях к классам данных чего-то не хватает, но я не смог найти никакой документации по этому поводу.

Ответы [ 2 ]

0 голосов
/ 02 августа 2020

Мне удалось найти краткосрочное решение со следующими изменениями.

// Comment.kt
@Document(indexName = "message_board")
data class Comment(
    @Id
    val commentId: String,
    val relationField: Map<String, String>
)

// Profile.kt
@Document(indexName = "message_board")
data class Profile(
    @Id
    val profileId: String,
    @Field(type = FieldType.Short)
    val age: Short,
    @GeoPointField
    val location: GeoPoint,
    @Field(type = FieldType.Text)
    val relationField: String = "profile"
)
// Controller.kt
class Controller constructor(
    private val elasticsearchOperations: ElasticsearchOperations
) {
    init {
        elasticsearchOperations.indexOps(IndexCoordinates.of("message_board")).let { indexOp ->
            if (!indexOp.exists() && indexOp.create()) {
                val relationMap = Document.from(
                    mapOf(
                        "properties" to mapOf(
                            "relationField" to mapOf(
                                "type" to "join",
                                "relations" to mapOf(
                                    "profile" to "comment"
                                )
                            ),
                            "location" to mapOf(
                                "type" to "geo_point"
                            )
                        )
                    )
                )
                indexOp.putMapping(relationMap)
                indexOp.refresh()
            }
        }
    }
}

Обратите внимание на relationField, добавленное к обоим классам данных, а также к вручную сгенерированному документу сопоставления. Теперь ES имеет правильное сопоставление при инициализации:

{
  "message_board" : {
    "mappings" : {
      "properties" : {
        "location" : {
          "type" : "geo_point"
        },
        "relationField" : {
          "type" : "join",
          "eager_global_ordinals" : true,
          "relations" : {
            "profile" : "comment"
          }
        }
      }
    }
  }
}

Теперь создание profile просто:

val profile = Profile(
    profileId = UUID.randomUUID().toString(),
    age = 27,
    location = GeoPoint(3.0, 5.0)
)
val indexQuery = IndexQueryBuilder()
    .withId(profile.profileId)
    .withObject(profile)
    .build()
elasticsearchOperations.index(indexQuery, IndexCoordinates.of("message_board"))

Однако создание comment немного сложнее, потому что требуется идентификатор маршрутизации:

val comment = Comment(
    commentId = UUID.randomUUID().toString(),
    relationField = mapOf(
        "name" to "comment",
        "parent" to profileId
    )
)
val bulkOptions = BulkOptions.builder()
    .withRoutingId(profileId)
    .build()
val indexQuery = IndexQueryBuilder()
    .withId(comment.commentId)
    .withObject(comment)
    .withParentId(profileId)
    .build()
elasticsearchOperations.bulkIndex(listOf(indexQuery), bulkOptions, IndexCoordinates.of("message_board"))

Вот как я смог получить отношение родитель-потомок с новым типом отношения JOIN.

0 голосов
/ 01 августа 2020

Это не проблема использования REST API. Эти вызовы создаются Elasticsearch RestHighlevelClient.

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

Elasticsearch поддерживает для этого тип данных соединения . В настоящее время мы работаем над PR, который добавит поддержку этого для следующей версии Spring Data Elasticsearch (4.1).

...